forked from platypush/platypush
Merge pull request '[#311] Logic to automatically generate the documentation for the dependencies of the integrations' (#330) from 311/auto-generate-deps-docs into master
Reviewed-on: platypush/platypush#330
This commit is contained in:
commit
e6f05dfe07
171 changed files with 6698 additions and 5390 deletions
|
@ -49,7 +49,7 @@ steps:
|
||||||
|
|
||||||
commands:
|
commands:
|
||||||
- echo "Installing required build dependencies"
|
- echo "Installing required build dependencies"
|
||||||
- apk add --update --no-cache make py3-sphinx py3-pip py3-paho-mqtt
|
- apk add --update --no-cache make py3-sphinx py3-pip py3-paho-mqtt py3-yaml
|
||||||
- pip install -U hid sphinx-rtd-theme sphinx-book-theme
|
- pip install -U hid sphinx-rtd-theme sphinx-book-theme
|
||||||
- pip install .
|
- pip install .
|
||||||
- mkdir -p /docs/current
|
- mkdir -p /docs/current
|
||||||
|
|
190
docs/source/_ext/add_dependencies.py
Normal file
190
docs/source/_ext/add_dependencies.py
Normal file
|
@ -0,0 +1,190 @@
|
||||||
|
import inspect
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import textwrap as tw
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
from sphinx.application import Sphinx
|
||||||
|
|
||||||
|
base_path = os.path.abspath(
|
||||||
|
os.path.join(os.path.dirname(os.path.relpath(__file__)), '..', '..', '..')
|
||||||
|
)
|
||||||
|
|
||||||
|
sys.path.insert(0, base_path)
|
||||||
|
|
||||||
|
from platypush.utils import get_plugin_name_by_class # noqa
|
||||||
|
from platypush.utils.mock import mock # noqa
|
||||||
|
from platypush.utils.reflection import IntegrationMetadata, import_file # noqa
|
||||||
|
|
||||||
|
|
||||||
|
class IntegrationEnricher:
|
||||||
|
@staticmethod
|
||||||
|
def add_events(source: list[str], manifest: IntegrationMetadata, idx: int) -> int:
|
||||||
|
if not manifest.events:
|
||||||
|
return idx
|
||||||
|
|
||||||
|
source.insert(
|
||||||
|
idx,
|
||||||
|
'Triggered events\n----------------\n\n'
|
||||||
|
+ '\n'.join(
|
||||||
|
f'\t- :class:`{event.__module__}.{event.__qualname__}`'
|
||||||
|
for event in manifest.events
|
||||||
|
)
|
||||||
|
+ '\n\n',
|
||||||
|
)
|
||||||
|
|
||||||
|
return idx + 1
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def add_actions(source: list[str], manifest: IntegrationMetadata, idx: int) -> int:
|
||||||
|
if not (manifest.actions and manifest.cls):
|
||||||
|
return idx
|
||||||
|
|
||||||
|
source.insert(
|
||||||
|
idx,
|
||||||
|
'Actions\n-------\n\n'
|
||||||
|
+ '\n'.join(
|
||||||
|
f'\t- `{get_plugin_name_by_class(manifest.cls)}.{action} '
|
||||||
|
+ f'<#{manifest.cls.__module__}.{manifest.cls.__qualname__}.{action}>`_'
|
||||||
|
for action in sorted(manifest.actions.keys())
|
||||||
|
)
|
||||||
|
+ '\n\n',
|
||||||
|
)
|
||||||
|
|
||||||
|
return idx + 1
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _shellify(title: str, cmd: str) -> str:
|
||||||
|
return f'**{title}**\n\n' + '.. code-block:: bash\n\n\t' + cmd + '\n\n'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add_install_deps(
|
||||||
|
cls, source: list[str], manifest: IntegrationMetadata, idx: int
|
||||||
|
) -> int:
|
||||||
|
deps = manifest.deps
|
||||||
|
parsed_deps = {
|
||||||
|
'before': deps.before,
|
||||||
|
'pip': deps.pip,
|
||||||
|
'after': deps.after,
|
||||||
|
}
|
||||||
|
|
||||||
|
if not (any(parsed_deps.values()) or deps.by_pkg_manager):
|
||||||
|
return idx
|
||||||
|
|
||||||
|
source.insert(idx, 'Dependencies\n------------\n\n')
|
||||||
|
idx += 1
|
||||||
|
|
||||||
|
if parsed_deps['before']:
|
||||||
|
source.insert(idx, cls._shellify('Pre-install', '\n'.join(deps.before)))
|
||||||
|
idx += 1
|
||||||
|
|
||||||
|
if parsed_deps['pip']:
|
||||||
|
source.insert(idx, cls._shellify('pip', 'pip ' + ' '.join(deps.pip)))
|
||||||
|
idx += 1
|
||||||
|
|
||||||
|
for pkg_manager, sys_deps in deps.by_pkg_manager.items():
|
||||||
|
if not sys_deps:
|
||||||
|
continue
|
||||||
|
|
||||||
|
source.insert(
|
||||||
|
idx,
|
||||||
|
cls._shellify(
|
||||||
|
pkg_manager.value.default_os.value.description,
|
||||||
|
pkg_manager.value.install_doc + ' ' + ' '.join(sys_deps),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
idx += 1
|
||||||
|
|
||||||
|
if parsed_deps['after']:
|
||||||
|
source.insert(idx, cls._shellify('Post-install', '\n'.join(deps.after)))
|
||||||
|
idx += 1
|
||||||
|
|
||||||
|
return idx
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add_description(
|
||||||
|
cls, source: list[str], manifest: IntegrationMetadata, idx: int
|
||||||
|
) -> int:
|
||||||
|
docs = (
|
||||||
|
doc
|
||||||
|
for doc in (
|
||||||
|
inspect.getdoc(manifest.cls) or '',
|
||||||
|
manifest.constructor.doc if manifest.constructor else '',
|
||||||
|
)
|
||||||
|
if doc
|
||||||
|
)
|
||||||
|
|
||||||
|
if not docs:
|
||||||
|
return idx
|
||||||
|
|
||||||
|
docstring = '\n\n'.join(docs)
|
||||||
|
source.insert(idx, f"Description\n-----------\n\n{docstring}\n\n")
|
||||||
|
return idx + 1
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add_conf_snippet(
|
||||||
|
cls, source: list[str], manifest: IntegrationMetadata, idx: int
|
||||||
|
) -> int:
|
||||||
|
source.insert(
|
||||||
|
idx,
|
||||||
|
tw.dedent(
|
||||||
|
f"""
|
||||||
|
Configuration
|
||||||
|
-------------
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
{tw.indent(manifest.config_snippet, ' ')}
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return idx + 1
|
||||||
|
|
||||||
|
def __call__(self, _: Sphinx, doc: str, source: list[str]):
|
||||||
|
if not (source and re.match(r'^platypush/(backend|plugins)/.*', doc)):
|
||||||
|
return
|
||||||
|
|
||||||
|
src = [src.split('\n') for src in source][0]
|
||||||
|
if len(src) < 3:
|
||||||
|
return
|
||||||
|
|
||||||
|
manifest_file = os.path.join(
|
||||||
|
base_path,
|
||||||
|
*doc.split(os.sep)[:-1],
|
||||||
|
*doc.split(os.sep)[-1].split('.'),
|
||||||
|
'manifest.yaml',
|
||||||
|
)
|
||||||
|
|
||||||
|
if not os.path.isfile(manifest_file):
|
||||||
|
return
|
||||||
|
|
||||||
|
with mock_imports():
|
||||||
|
manifest = IntegrationMetadata.from_manifest(manifest_file)
|
||||||
|
idx = self.add_description(src, manifest, idx=3)
|
||||||
|
idx = self.add_conf_snippet(src, manifest, idx=idx)
|
||||||
|
idx = self.add_install_deps(src, manifest, idx=idx)
|
||||||
|
idx = self.add_events(src, manifest, idx=idx)
|
||||||
|
idx = self.add_actions(src, manifest, idx=idx)
|
||||||
|
|
||||||
|
src.insert(idx, '\n\nModule reference\n----------------\n\n')
|
||||||
|
source[0] = '\n'.join(src)
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def mock_imports():
|
||||||
|
conf_mod = import_file(os.path.join(base_path, 'docs', 'source', 'conf.py'))
|
||||||
|
mock_mods = getattr(conf_mod, 'autodoc_mock_imports', [])
|
||||||
|
with mock(*mock_mods):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
def setup(app: Sphinx):
|
||||||
|
app.connect('source-read', IntegrationEnricher())
|
||||||
|
return {
|
||||||
|
'version': '0.1',
|
||||||
|
'parallel_read_safe': True,
|
||||||
|
'parallel_write_safe': True,
|
||||||
|
}
|
|
@ -20,7 +20,6 @@ Backends
|
||||||
platypush/backend/google.pubsub.rst
|
platypush/backend/google.pubsub.rst
|
||||||
platypush/backend/gps.rst
|
platypush/backend/gps.rst
|
||||||
platypush/backend/http.rst
|
platypush/backend/http.rst
|
||||||
platypush/backend/inotify.rst
|
|
||||||
platypush/backend/joystick.rst
|
platypush/backend/joystick.rst
|
||||||
platypush/backend/joystick.jstest.rst
|
platypush/backend/joystick.jstest.rst
|
||||||
platypush/backend/joystick.linux.rst
|
platypush/backend/joystick.linux.rst
|
||||||
|
@ -28,7 +27,6 @@ Backends
|
||||||
platypush/backend/log.http.rst
|
platypush/backend/log.http.rst
|
||||||
platypush/backend/mail.rst
|
platypush/backend/mail.rst
|
||||||
platypush/backend/midi.rst
|
platypush/backend/midi.rst
|
||||||
platypush/backend/mqtt.rst
|
|
||||||
platypush/backend/music.mopidy.rst
|
platypush/backend/music.mopidy.rst
|
||||||
platypush/backend/music.mpd.rst
|
platypush/backend/music.mpd.rst
|
||||||
platypush/backend/music.snapcast.rst
|
platypush/backend/music.snapcast.rst
|
||||||
|
@ -52,4 +50,3 @@ Backends
|
||||||
platypush/backend/weather.darksky.rst
|
platypush/backend/weather.darksky.rst
|
||||||
platypush/backend/weather.openweathermap.rst
|
platypush/backend/weather.openweathermap.rst
|
||||||
platypush/backend/wiimote.rst
|
platypush/backend/wiimote.rst
|
||||||
platypush/backend/zwave.mqtt.rst
|
|
||||||
|
|
|
@ -15,17 +15,14 @@ import sys
|
||||||
# add these directories to sys.path here. If the directory is relative to the
|
# add these directories to sys.path here. If the directory is relative to the
|
||||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||||
#
|
#
|
||||||
# import os
|
|
||||||
# import sys
|
|
||||||
# sys.path.insert(0, os.path.abspath('.'))
|
|
||||||
sys.path.insert(0, os.path.abspath("./_ext"))
|
sys.path.insert(0, os.path.abspath("./_ext"))
|
||||||
|
|
||||||
|
|
||||||
# -- Project information -----------------------------------------------------
|
# -- Project information -----------------------------------------------------
|
||||||
|
|
||||||
project = 'Platypush'
|
project = 'Platypush'
|
||||||
copyright = '2017-2021, Fabio Manganiello'
|
copyright = '2017-2023, Fabio Manganiello'
|
||||||
author = 'Fabio Manganiello'
|
author = 'Fabio Manganiello <fabio@manganiello.tech>'
|
||||||
|
|
||||||
# The short X.Y version
|
# The short X.Y version
|
||||||
version = ''
|
version = ''
|
||||||
|
@ -52,6 +49,7 @@ extensions = [
|
||||||
'sphinx.ext.githubpages',
|
'sphinx.ext.githubpages',
|
||||||
'sphinx_rtd_theme',
|
'sphinx_rtd_theme',
|
||||||
'sphinx_marshmallow',
|
'sphinx_marshmallow',
|
||||||
|
'add_dependencies',
|
||||||
]
|
]
|
||||||
|
|
||||||
# Add any paths that contain templates here, relative to this directory.
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
|
@ -190,11 +188,6 @@ texinfo_documents = [
|
||||||
# Example configuration for intersphinx: refer to the Python standard library.
|
# Example configuration for intersphinx: refer to the Python standard library.
|
||||||
intersphinx_mapping = {'python': ('https://docs.python.org/3', None)}
|
intersphinx_mapping = {'python': ('https://docs.python.org/3', None)}
|
||||||
|
|
||||||
# -- Options for todo extension ----------------------------------------------
|
|
||||||
|
|
||||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
|
||||||
todo_include_todos = True
|
|
||||||
|
|
||||||
autodoc_default_options = {
|
autodoc_default_options = {
|
||||||
'members': True,
|
'members': True,
|
||||||
'show-inheritance': True,
|
'show-inheritance': True,
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
``inotify``
|
|
||||||
=============================
|
|
||||||
|
|
||||||
.. automodule:: platypush.backend.inotify
|
|
||||||
:members:
|
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
``mqtt``
|
|
||||||
==========================
|
|
||||||
|
|
||||||
.. automodule:: platypush.backend.mqtt
|
|
||||||
:members:
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
``zwave.mqtt``
|
|
||||||
================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.backend.zwave.mqtt
|
|
||||||
:members:
|
|
|
@ -1,6 +0,0 @@
|
||||||
``http.request.rss``
|
|
||||||
======================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.plugins.http.request.rss
|
|
||||||
:members:
|
|
||||||
|
|
|
@ -50,7 +50,6 @@ Plugins
|
||||||
platypush/plugins/graphite.rst
|
platypush/plugins/graphite.rst
|
||||||
platypush/plugins/hid.rst
|
platypush/plugins/hid.rst
|
||||||
platypush/plugins/http.request.rst
|
platypush/plugins/http.request.rst
|
||||||
platypush/plugins/http.request.rss.rst
|
|
||||||
platypush/plugins/http.webpage.rst
|
platypush/plugins/http.webpage.rst
|
||||||
platypush/plugins/ifttt.rst
|
platypush/plugins/ifttt.rst
|
||||||
platypush/plugins/inputs.rst
|
platypush/plugins/inputs.rst
|
||||||
|
|
|
@ -2,23 +2,17 @@ from typing import Optional
|
||||||
|
|
||||||
from platypush.backend import Backend
|
from platypush.backend import Backend
|
||||||
from platypush.context import get_plugin
|
from platypush.context import get_plugin
|
||||||
from platypush.message.event.adafruit import ConnectedEvent, DisconnectedEvent, \
|
from platypush.message.event.adafruit import (
|
||||||
FeedUpdateEvent
|
ConnectedEvent,
|
||||||
|
DisconnectedEvent,
|
||||||
|
FeedUpdateEvent,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AdafruitIoBackend(Backend):
|
class AdafruitIoBackend(Backend):
|
||||||
"""
|
"""
|
||||||
Backend that listens to messages received over the Adafruit IO message queue
|
Backend that listens to messages received over the Adafruit IO message queue
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
* :class:`platypush.message.event.adafruit.ConnectedEvent` when the
|
|
||||||
backend connects to the Adafruit queue
|
|
||||||
* :class:`platypush.message.event.adafruit.DisconnectedEvent` when the
|
|
||||||
backend disconnects from the Adafruit queue
|
|
||||||
* :class:`platypush.message.event.adafruit.FeedUpdateEvent` when an
|
|
||||||
update event is received on a monitored feed
|
|
||||||
|
|
||||||
Requires:
|
Requires:
|
||||||
|
|
||||||
* The :class:`platypush.plugins.adafruit.io.AdafruitIoPlugin` plugin to
|
* The :class:`platypush.plugins.adafruit.io.AdafruitIoPlugin` plugin to
|
||||||
|
@ -33,6 +27,7 @@ class AdafruitIoBackend(Backend):
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
from Adafruit_IO import MQTTClient
|
from Adafruit_IO import MQTTClient
|
||||||
|
|
||||||
self.feeds = feeds
|
self.feeds = feeds
|
||||||
self._client: Optional[MQTTClient] = None
|
self._client: Optional[MQTTClient] = None
|
||||||
|
|
||||||
|
@ -41,6 +36,7 @@ class AdafruitIoBackend(Backend):
|
||||||
return
|
return
|
||||||
|
|
||||||
from Adafruit_IO import MQTTClient
|
from Adafruit_IO import MQTTClient
|
||||||
|
|
||||||
plugin = get_plugin('adafruit.io')
|
plugin = get_plugin('adafruit.io')
|
||||||
if not plugin:
|
if not plugin:
|
||||||
raise RuntimeError('Adafruit IO plugin not configured')
|
raise RuntimeError('Adafruit IO plugin not configured')
|
||||||
|
@ -80,8 +76,11 @@ class AdafruitIoBackend(Backend):
|
||||||
def run(self):
|
def run(self):
|
||||||
super().run()
|
super().run()
|
||||||
|
|
||||||
self.logger.info(('Initialized Adafruit IO backend, listening on ' +
|
self.logger.info(
|
||||||
'feeds {}').format(self.feeds))
|
('Initialized Adafruit IO backend, listening on ' + 'feeds {}').format(
|
||||||
|
self.feeds
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
while not self.should_stop():
|
while not self.should_stop():
|
||||||
try:
|
try:
|
||||||
|
@ -94,4 +93,5 @@ class AdafruitIoBackend(Backend):
|
||||||
self.logger.exception(e)
|
self.logger.exception(e)
|
||||||
self._client = None
|
self._client = None
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
|
@ -11,7 +11,11 @@ from dateutil.tz import gettz
|
||||||
|
|
||||||
from platypush.backend import Backend
|
from platypush.backend import Backend
|
||||||
from platypush.context import get_bus, get_plugin
|
from platypush.context import get_bus, get_plugin
|
||||||
from platypush.message.event.alarm import AlarmStartedEvent, AlarmDismissedEvent, AlarmSnoozedEvent
|
from platypush.message.event.alarm import (
|
||||||
|
AlarmStartedEvent,
|
||||||
|
AlarmDismissedEvent,
|
||||||
|
AlarmSnoozedEvent,
|
||||||
|
)
|
||||||
from platypush.plugins.media import MediaPlugin, PlayerState
|
from platypush.plugins.media import MediaPlugin, PlayerState
|
||||||
from platypush.procedure import Procedure
|
from platypush.procedure import Procedure
|
||||||
|
|
||||||
|
@ -28,10 +32,17 @@ class Alarm:
|
||||||
_alarms_count = 0
|
_alarms_count = 0
|
||||||
_id_lock = threading.RLock()
|
_id_lock = threading.RLock()
|
||||||
|
|
||||||
def __init__(self, when: str, actions: Optional[list] = None, name: Optional[str] = None,
|
def __init__(
|
||||||
audio_file: Optional[str] = None, audio_plugin: Optional[str] = None,
|
self,
|
||||||
audio_volume: Optional[Union[int, float]] = None,
|
when: str,
|
||||||
snooze_interval: float = 300.0, enabled: bool = True):
|
actions: Optional[list] = None,
|
||||||
|
name: Optional[str] = None,
|
||||||
|
audio_file: Optional[str] = None,
|
||||||
|
audio_plugin: Optional[str] = None,
|
||||||
|
audio_volume: Optional[Union[int, float]] = None,
|
||||||
|
snooze_interval: float = 300.0,
|
||||||
|
enabled: bool = True,
|
||||||
|
):
|
||||||
with self._id_lock:
|
with self._id_lock:
|
||||||
self._alarms_count += 1
|
self._alarms_count += 1
|
||||||
self.id = self._alarms_count
|
self.id = self._alarms_count
|
||||||
|
@ -42,20 +53,26 @@ class Alarm:
|
||||||
|
|
||||||
if audio_file:
|
if audio_file:
|
||||||
self.audio_file = os.path.abspath(os.path.expanduser(audio_file))
|
self.audio_file = os.path.abspath(os.path.expanduser(audio_file))
|
||||||
assert os.path.isfile(self.audio_file), 'No such audio file: {}'.format(self.audio_file)
|
assert os.path.isfile(self.audio_file), 'No such audio file: {}'.format(
|
||||||
|
self.audio_file
|
||||||
|
)
|
||||||
|
|
||||||
self.audio_plugin = audio_plugin
|
self.audio_plugin = audio_plugin
|
||||||
self.audio_volume = audio_volume
|
self.audio_volume = audio_volume
|
||||||
self.snooze_interval = snooze_interval
|
self.snooze_interval = snooze_interval
|
||||||
self.state: Optional[AlarmState] = None
|
self.state: Optional[AlarmState] = None
|
||||||
self.timer: Optional[threading.Timer] = None
|
self.timer: Optional[threading.Timer] = None
|
||||||
self.actions = Procedure.build(name=name, _async=False, requests=actions or [], id=self.id)
|
self.actions = Procedure.build(
|
||||||
|
name=name, _async=False, requests=actions or [], id=self.id
|
||||||
|
)
|
||||||
|
|
||||||
self._enabled = enabled
|
self._enabled = enabled
|
||||||
self._runtime_snooze_interval = snooze_interval
|
self._runtime_snooze_interval = snooze_interval
|
||||||
|
|
||||||
def get_next(self) -> float:
|
def get_next(self) -> float:
|
||||||
now = datetime.datetime.now().replace(tzinfo=gettz()) # lgtm [py/call-to-non-callable]
|
now = datetime.datetime.now().replace(
|
||||||
|
tzinfo=gettz()
|
||||||
|
) # lgtm [py/call-to-non-callable]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cron = croniter.croniter(self.when, now)
|
cron = croniter.croniter(self.when, now)
|
||||||
|
@ -63,10 +80,14 @@ class Alarm:
|
||||||
except (AttributeError, croniter.CroniterBadCronError):
|
except (AttributeError, croniter.CroniterBadCronError):
|
||||||
try:
|
try:
|
||||||
timestamp = datetime.datetime.fromisoformat(self.when).replace(
|
timestamp = datetime.datetime.fromisoformat(self.when).replace(
|
||||||
tzinfo=gettz()) # lgtm [py/call-to-non-callable]
|
tzinfo=gettz()
|
||||||
|
) # lgtm [py/call-to-non-callable]
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
timestamp = (datetime.datetime.now().replace(tzinfo=gettz()) + # lgtm [py/call-to-non-callable]
|
timestamp = datetime.datetime.now().replace(
|
||||||
datetime.timedelta(seconds=int(self.when)))
|
tzinfo=gettz()
|
||||||
|
) + datetime.timedelta( # lgtm [py/call-to-non-callable]
|
||||||
|
seconds=int(self.when)
|
||||||
|
)
|
||||||
|
|
||||||
return timestamp.timestamp() if timestamp >= now else None
|
return timestamp.timestamp() if timestamp >= now else None
|
||||||
|
|
||||||
|
@ -88,7 +109,9 @@ class Alarm:
|
||||||
self._runtime_snooze_interval = interval or self.snooze_interval
|
self._runtime_snooze_interval = interval or self.snooze_interval
|
||||||
self.state = AlarmState.SNOOZED
|
self.state = AlarmState.SNOOZED
|
||||||
self.stop_audio()
|
self.stop_audio()
|
||||||
get_bus().post(AlarmSnoozedEvent(name=self.name, interval=self._runtime_snooze_interval))
|
get_bus().post(
|
||||||
|
AlarmSnoozedEvent(name=self.name, interval=self._runtime_snooze_interval)
|
||||||
|
)
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
if self.timer:
|
if self.timer:
|
||||||
|
@ -159,7 +182,9 @@ class Alarm:
|
||||||
break
|
break
|
||||||
|
|
||||||
if not sleep_time:
|
if not sleep_time:
|
||||||
sleep_time = self.get_next() - time.time() if self.get_next() else 10
|
sleep_time = (
|
||||||
|
self.get_next() - time.time() if self.get_next() else 10
|
||||||
|
)
|
||||||
|
|
||||||
time.sleep(sleep_time)
|
time.sleep(sleep_time)
|
||||||
|
|
||||||
|
@ -179,18 +204,15 @@ class Alarm:
|
||||||
class AlarmBackend(Backend):
|
class AlarmBackend(Backend):
|
||||||
"""
|
"""
|
||||||
Backend to handle user-configured alarms.
|
Backend to handle user-configured alarms.
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
* :class:`platypush.message.event.alarm.AlarmStartedEvent` when an alarm starts.
|
|
||||||
* :class:`platypush.message.event.alarm.AlarmSnoozedEvent` when an alarm is snoozed.
|
|
||||||
* :class:`platypush.message.event.alarm.AlarmTimeoutEvent` when an alarm times out.
|
|
||||||
* :class:`platypush.message.event.alarm.AlarmDismissedEvent` when an alarm is dismissed.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, alarms: Optional[Union[list, Dict[str, Any]]] = None, audio_plugin: str = 'media.mplayer',
|
def __init__(
|
||||||
*args, **kwargs):
|
self,
|
||||||
|
alarms: Optional[Union[list, Dict[str, Any]]] = None,
|
||||||
|
audio_plugin: str = 'media.mplayer',
|
||||||
|
*args,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
:param alarms: List or name->value dict with the configured alarms. Example:
|
:param alarms: List or name->value dict with the configured alarms. Example:
|
||||||
|
|
||||||
|
@ -231,13 +253,29 @@ class AlarmBackend(Backend):
|
||||||
alarms = [{'name': name, **alarm} for name, alarm in alarms.items()]
|
alarms = [{'name': name, **alarm} for name, alarm in alarms.items()]
|
||||||
|
|
||||||
self.audio_plugin = audio_plugin
|
self.audio_plugin = audio_plugin
|
||||||
alarms = [Alarm(**{'audio_plugin': self.audio_plugin, **alarm}) for alarm in alarms]
|
alarms = [
|
||||||
|
Alarm(**{'audio_plugin': self.audio_plugin, **alarm}) for alarm in alarms
|
||||||
|
]
|
||||||
self.alarms: Dict[str, Alarm] = {alarm.name: alarm for alarm in alarms}
|
self.alarms: Dict[str, Alarm] = {alarm.name: alarm for alarm in alarms}
|
||||||
|
|
||||||
def add_alarm(self, when: str, actions: list, name: Optional[str] = None, audio_file: Optional[str] = None,
|
def add_alarm(
|
||||||
audio_volume: Optional[Union[int, float]] = None, enabled: bool = True) -> Alarm:
|
self,
|
||||||
alarm = Alarm(when=when, actions=actions, name=name, enabled=enabled, audio_file=audio_file,
|
when: str,
|
||||||
audio_plugin=self.audio_plugin, audio_volume=audio_volume)
|
actions: list,
|
||||||
|
name: Optional[str] = None,
|
||||||
|
audio_file: Optional[str] = None,
|
||||||
|
audio_volume: Optional[Union[int, float]] = None,
|
||||||
|
enabled: bool = True,
|
||||||
|
) -> Alarm:
|
||||||
|
alarm = Alarm(
|
||||||
|
when=when,
|
||||||
|
actions=actions,
|
||||||
|
name=name,
|
||||||
|
enabled=enabled,
|
||||||
|
audio_file=audio_file,
|
||||||
|
audio_plugin=self.audio_plugin,
|
||||||
|
audio_volume=audio_volume,
|
||||||
|
)
|
||||||
|
|
||||||
if alarm.name in self.alarms:
|
if alarm.name in self.alarms:
|
||||||
self.logger.info('Overwriting existing alarm {}'.format(alarm.name))
|
self.logger.info('Overwriting existing alarm {}'.format(alarm.name))
|
||||||
|
@ -274,10 +312,15 @@ class AlarmBackend(Backend):
|
||||||
alarm.snooze(interval=interval)
|
alarm.snooze(interval=interval)
|
||||||
|
|
||||||
def get_alarms(self) -> List[Alarm]:
|
def get_alarms(self) -> List[Alarm]:
|
||||||
return sorted([alarm for alarm in self.alarms.values()], key=lambda alarm: alarm.get_next())
|
return sorted(
|
||||||
|
self.alarms.values(),
|
||||||
|
key=lambda alarm: alarm.get_next(),
|
||||||
|
)
|
||||||
|
|
||||||
def get_running_alarm(self) -> Optional[Alarm]:
|
def get_running_alarm(self) -> Optional[Alarm]:
|
||||||
running_alarms = [alarm for alarm in self.alarms.values() if alarm.state == AlarmState.RUNNING]
|
running_alarms = [
|
||||||
|
alarm for alarm in self.alarms.values() if alarm.state == AlarmState.RUNNING
|
||||||
|
]
|
||||||
return running_alarms[0] if running_alarms else None
|
return running_alarms[0] if running_alarms else None
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
|
@ -285,9 +328,11 @@ class AlarmBackend(Backend):
|
||||||
alarm.stop()
|
alarm.stop()
|
||||||
alarm.start()
|
alarm.start()
|
||||||
|
|
||||||
self.logger.info('Initialized alarm backend with {} alarms'.format(len(self.alarms)))
|
self.logger.info(
|
||||||
|
'Initialized alarm backend with {} alarms'.format(len(self.alarms))
|
||||||
|
)
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
def __exit__(self, *_, **__):
|
||||||
for alarm in self.alarms.values():
|
for alarm in self.alarms.values():
|
||||||
alarm.stop()
|
alarm.stop()
|
||||||
|
|
||||||
|
@ -295,7 +340,9 @@ class AlarmBackend(Backend):
|
||||||
|
|
||||||
def loop(self):
|
def loop(self):
|
||||||
for name, alarm in self.alarms.copy().items():
|
for name, alarm in self.alarms.copy().items():
|
||||||
if not alarm.timer or (not alarm.timer.is_alive() and alarm.state == AlarmState.SHUTDOWN):
|
if not alarm.timer or (
|
||||||
|
not alarm.timer.is_alive() and alarm.state == AlarmState.SHUTDOWN
|
||||||
|
):
|
||||||
del self.alarms[name]
|
del self.alarms[name]
|
||||||
|
|
||||||
time.sleep(10)
|
time.sleep(10)
|
||||||
|
|
|
@ -31,40 +31,6 @@ class AssistantGoogleBackend(AssistantBackend):
|
||||||
https://developers.google.com/assistant/sdk/reference/library/python/. This backend still works on most of the
|
https://developers.google.com/assistant/sdk/reference/library/python/. This backend still works on most of the
|
||||||
devices where I use it, but its correct functioning is not guaranteed as the assistant library is no longer
|
devices where I use it, but its correct functioning is not guaranteed as the assistant library is no longer
|
||||||
maintained.
|
maintained.
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
* :class:`platypush.message.event.assistant.ConversationStartEvent` \
|
|
||||||
when a new conversation starts
|
|
||||||
* :class:`platypush.message.event.assistant.SpeechRecognizedEvent` \
|
|
||||||
when a new voice command is recognized
|
|
||||||
* :class:`platypush.message.event.assistant.NoResponse` \
|
|
||||||
when a conversation returned no response
|
|
||||||
* :class:`platypush.message.event.assistant.ResponseEvent` \
|
|
||||||
when the assistant is speaking a response
|
|
||||||
* :class:`platypush.message.event.assistant.ConversationTimeoutEvent` \
|
|
||||||
when a conversation times out
|
|
||||||
* :class:`platypush.message.event.assistant.ConversationEndEvent` \
|
|
||||||
when a new conversation ends
|
|
||||||
* :class:`platypush.message.event.assistant.AlarmStartedEvent` \
|
|
||||||
when an alarm starts
|
|
||||||
* :class:`platypush.message.event.assistant.AlarmEndEvent` \
|
|
||||||
when an alarm ends
|
|
||||||
* :class:`platypush.message.event.assistant.TimerStartedEvent` \
|
|
||||||
when a timer starts
|
|
||||||
* :class:`platypush.message.event.assistant.TimerEndEvent` \
|
|
||||||
when a timer ends
|
|
||||||
* :class:`platypush.message.event.assistant.MicMutedEvent` \
|
|
||||||
when the microphone is muted.
|
|
||||||
* :class:`platypush.message.event.assistant.MicUnmutedEvent` \
|
|
||||||
when the microphone is un-muted.
|
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **google-assistant-library** (``pip install google-assistant-library``)
|
|
||||||
* **google-assistant-sdk[samples]** (``pip install google-assistant-sdk[samples]``)
|
|
||||||
* **google-auth** (``pip install google-auth``)
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_default_credentials_file = os.path.join(
|
_default_credentials_file = os.path.join(
|
||||||
|
@ -164,12 +130,12 @@ class AssistantGoogleBackend(AssistantBackend):
|
||||||
self.bus.post(event)
|
self.bus.post(event)
|
||||||
|
|
||||||
def start_conversation(self):
|
def start_conversation(self):
|
||||||
"""Starts an assistant conversation"""
|
"""Starts a conversation."""
|
||||||
if self.assistant:
|
if self.assistant:
|
||||||
self.assistant.start_conversation()
|
self.assistant.start_conversation()
|
||||||
|
|
||||||
def stop_conversation(self):
|
def stop_conversation(self):
|
||||||
"""Stops an assistant conversation"""
|
"""Stops an active conversation."""
|
||||||
if self.assistant:
|
if self.assistant:
|
||||||
self.assistant.stop_conversation()
|
self.assistant.stop_conversation()
|
||||||
|
|
||||||
|
|
|
@ -15,16 +15,7 @@ class AssistantSnowboyBackend(AssistantBackend):
|
||||||
HotwordDetectedEvent to trigger the conversation on whichever assistant
|
HotwordDetectedEvent to trigger the conversation on whichever assistant
|
||||||
plugin you're using (Google, Alexa...)
|
plugin you're using (Google, Alexa...)
|
||||||
|
|
||||||
Triggers:
|
Manual installation for snowboy and its Python bindings if the installation via package fails::
|
||||||
|
|
||||||
* :class:`platypush.message.event.assistant.HotwordDetectedEvent` \
|
|
||||||
whenever the hotword has been detected
|
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **snowboy** (``pip install snowboy``)
|
|
||||||
|
|
||||||
Manual installation for snowboy and its Python bindings if the command above fails::
|
|
||||||
|
|
||||||
$ [sudo] apt-get install libatlas-base-dev swig
|
$ [sudo] apt-get install libatlas-base-dev swig
|
||||||
$ [sudo] pip install pyaudio
|
$ [sudo] pip install pyaudio
|
||||||
|
|
|
@ -12,15 +12,12 @@ class ButtonFlicBackend(Backend):
|
||||||
Backend that listen for events from the Flic (https://flic.io/) bluetooth
|
Backend that listen for events from the Flic (https://flic.io/) bluetooth
|
||||||
smart buttons.
|
smart buttons.
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
* :class:`platypush.message.event.button.flic.FlicButtonEvent` when a button is pressed.
|
|
||||||
The event will also contain the press sequence
|
|
||||||
(e.g. ``["ShortPressEvent", "LongPressEvent", "ShortPressEvent"]``)
|
|
||||||
|
|
||||||
Requires:
|
Requires:
|
||||||
|
|
||||||
* **fliclib** (https://github.com/50ButtonsEach/fliclib-linux-hci). For the backend to work properly you need to have the ``flicd`` daemon from the fliclib running, and you have to first pair the buttons with your device using any of the scanners provided by the library.
|
* **fliclib** (https://github.com/50ButtonsEach/fliclib-linux-hci). For
|
||||||
|
the backend to work properly you need to have the ``flicd`` daemon
|
||||||
|
from the fliclib running, and you have to first pair the buttons with
|
||||||
|
your device using any of the scanners provided by the library.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -29,16 +26,23 @@ class ButtonFlicBackend(Backend):
|
||||||
ShortPressEvent = "ShortPressEvent"
|
ShortPressEvent = "ShortPressEvent"
|
||||||
LongPressEvent = "LongPressEvent"
|
LongPressEvent = "LongPressEvent"
|
||||||
|
|
||||||
def __init__(self, server='localhost', long_press_timeout=_long_press_timeout,
|
def __init__(
|
||||||
btn_timeout=_btn_timeout, **kwargs):
|
self,
|
||||||
|
server='localhost',
|
||||||
|
long_press_timeout=_long_press_timeout,
|
||||||
|
btn_timeout=_btn_timeout,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
:param server: flicd server host (default: localhost)
|
:param server: flicd server host (default: localhost)
|
||||||
:type server: str
|
:type server: str
|
||||||
|
|
||||||
:param long_press_timeout: How long you should press a button for a press action to be considered "long press" (default: 0.3 secohds)
|
:param long_press_timeout: How long you should press a button for a
|
||||||
|
press action to be considered "long press" (default: 0.3 secohds)
|
||||||
:type long_press_timeout: float
|
:type long_press_timeout: float
|
||||||
|
|
||||||
:param btn_timeout: How long since the last button release before considering the user interaction completed (default: 0.5 seconds)
|
:param btn_timeout: How long since the last button release before
|
||||||
|
considering the user interaction completed (default: 0.5 seconds)
|
||||||
:type btn_timeout: float
|
:type btn_timeout: float
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -55,15 +59,16 @@ class ButtonFlicBackend(Backend):
|
||||||
self._btn_addr = None
|
self._btn_addr = None
|
||||||
self._down_pressed_time = None
|
self._down_pressed_time = None
|
||||||
self._cur_sequence = []
|
self._cur_sequence = []
|
||||||
|
self.logger.info('Initialized Flic buttons backend on %s', self.server)
|
||||||
self.logger.info('Initialized Flic buttons backend on {}'.format(self.server))
|
|
||||||
|
|
||||||
def _got_button(self):
|
def _got_button(self):
|
||||||
def _f(bd_addr):
|
def _f(bd_addr):
|
||||||
cc = ButtonConnectionChannel(bd_addr)
|
cc = ButtonConnectionChannel(bd_addr)
|
||||||
cc.on_button_up_or_down = \
|
cc.on_button_up_or_down = (
|
||||||
lambda channel, click_type, was_queued, time_diff: \
|
lambda channel, click_type, was_queued, time_diff: self._on_event()(
|
||||||
self._on_event()(bd_addr, channel, click_type, was_queued, time_diff)
|
bd_addr, channel, click_type, was_queued, time_diff
|
||||||
|
)
|
||||||
|
)
|
||||||
self.client.add_connection_channel(cc)
|
self.client.add_connection_channel(cc)
|
||||||
|
|
||||||
return _f
|
return _f
|
||||||
|
@ -72,23 +77,27 @@ class ButtonFlicBackend(Backend):
|
||||||
def _f(items):
|
def _f(items):
|
||||||
for bd_addr in items["bd_addr_of_verified_buttons"]:
|
for bd_addr in items["bd_addr_of_verified_buttons"]:
|
||||||
self._got_button()(bd_addr)
|
self._got_button()(bd_addr)
|
||||||
|
|
||||||
return _f
|
return _f
|
||||||
|
|
||||||
def _on_btn_timeout(self):
|
def _on_btn_timeout(self):
|
||||||
def _f():
|
def _f():
|
||||||
self.logger.info('Flic event triggered from {}: {}'.format(
|
self.logger.info(
|
||||||
self._btn_addr, self._cur_sequence))
|
'Flic event triggered from %s: %s', self._btn_addr, self._cur_sequence
|
||||||
|
)
|
||||||
|
|
||||||
self.bus.post(FlicButtonEvent(
|
self.bus.post(
|
||||||
btn_addr=self._btn_addr, sequence=self._cur_sequence))
|
FlicButtonEvent(btn_addr=self._btn_addr, sequence=self._cur_sequence)
|
||||||
|
)
|
||||||
|
|
||||||
self._cur_sequence = []
|
self._cur_sequence = []
|
||||||
|
|
||||||
return _f
|
return _f
|
||||||
|
|
||||||
def _on_event(self):
|
def _on_event(self):
|
||||||
# noinspection PyUnusedLocal
|
# _ = channel
|
||||||
def _f(bd_addr, channel, click_type, was_queued, time_diff):
|
# __ = time_diff
|
||||||
|
def _f(bd_addr, _, click_type, was_queued, __):
|
||||||
if was_queued:
|
if was_queued:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -120,4 +129,3 @@ class ButtonFlicBackend(Backend):
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
||||||
|
|
|
@ -15,10 +15,6 @@ class CameraPiBackend(Backend):
|
||||||
the :class:`platypush.plugins.camera.pi` plugin. Note that the Redis backend
|
the :class:`platypush.plugins.camera.pi` plugin. Note that the Redis backend
|
||||||
must be configured and running to enable camera control.
|
must be configured and running to enable camera control.
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **picamera** (``pip install picamera``)
|
|
||||||
|
|
||||||
This backend is **DEPRECATED**. Use the plugin :class:`platypush.plugins.camera.pi.CameraPiPlugin` instead to run
|
This backend is **DEPRECATED**. Use the plugin :class:`platypush.plugins.camera.pi.CameraPiPlugin` instead to run
|
||||||
Pi camera actions. If you want to start streaming the camera on application start then simply create an event hook
|
Pi camera actions. If you want to start streaming the camera on application start then simply create an event hook
|
||||||
on :class:`platypush.message.event.application.ApplicationStartedEvent` that runs ``camera.pi.start_streaming``.
|
on :class:`platypush.message.event.application.ApplicationStartedEvent` that runs ``camera.pi.start_streaming``.
|
||||||
|
@ -33,15 +29,32 @@ class CameraPiBackend(Backend):
|
||||||
return self.value == other
|
return self.value == other
|
||||||
|
|
||||||
# noinspection PyUnresolvedReferences,PyPackageRequirements
|
# noinspection PyUnresolvedReferences,PyPackageRequirements
|
||||||
def __init__(self, listen_port, bind_address='0.0.0.0', x_resolution=640, y_resolution=480,
|
def __init__(
|
||||||
redis_queue='platypush/camera/pi',
|
self,
|
||||||
start_recording_on_startup=True,
|
listen_port,
|
||||||
framerate=24, hflip=False, vflip=False,
|
bind_address='0.0.0.0',
|
||||||
sharpness=0, contrast=0, brightness=50,
|
x_resolution=640,
|
||||||
video_stabilization=False, iso=0, exposure_compensation=0,
|
y_resolution=480,
|
||||||
exposure_mode='auto', meter_mode='average', awb_mode='auto',
|
redis_queue='platypush/camera/pi',
|
||||||
image_effect='none', color_effects=None, rotation=0,
|
start_recording_on_startup=True,
|
||||||
crop=(0.0, 0.0, 1.0, 1.0), **kwargs):
|
framerate=24,
|
||||||
|
hflip=False,
|
||||||
|
vflip=False,
|
||||||
|
sharpness=0,
|
||||||
|
contrast=0,
|
||||||
|
brightness=50,
|
||||||
|
video_stabilization=False,
|
||||||
|
iso=0,
|
||||||
|
exposure_compensation=0,
|
||||||
|
exposure_mode='auto',
|
||||||
|
meter_mode='average',
|
||||||
|
awb_mode='auto',
|
||||||
|
image_effect='none',
|
||||||
|
color_effects=None,
|
||||||
|
rotation=0,
|
||||||
|
crop=(0.0, 0.0, 1.0, 1.0),
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
See https://www.raspberrypi.org/documentation/usage/camera/python/README.md
|
See https://www.raspberrypi.org/documentation/usage/camera/python/README.md
|
||||||
for a detailed reference about the Pi camera options.
|
for a detailed reference about the Pi camera options.
|
||||||
|
@ -58,7 +71,9 @@ class CameraPiBackend(Backend):
|
||||||
self.bind_address = bind_address
|
self.bind_address = bind_address
|
||||||
self.listen_port = listen_port
|
self.listen_port = listen_port
|
||||||
self.server_socket = socket.socket()
|
self.server_socket = socket.socket()
|
||||||
self.server_socket.bind((self.bind_address, self.listen_port)) # lgtm [py/bind-socket-all-network-interfaces]
|
self.server_socket.bind(
|
||||||
|
(self.bind_address, self.listen_port)
|
||||||
|
) # lgtm [py/bind-socket-all-network-interfaces]
|
||||||
self.server_socket.listen(0)
|
self.server_socket.listen(0)
|
||||||
|
|
||||||
import picamera
|
import picamera
|
||||||
|
@ -87,10 +102,7 @@ class CameraPiBackend(Backend):
|
||||||
self._recording_thread = None
|
self._recording_thread = None
|
||||||
|
|
||||||
def send_camera_action(self, action, **kwargs):
|
def send_camera_action(self, action, **kwargs):
|
||||||
action = {
|
action = {'action': action.value, **kwargs}
|
||||||
'action': action.value,
|
|
||||||
**kwargs
|
|
||||||
}
|
|
||||||
|
|
||||||
self.redis.send_message(msg=json.dumps(action), queue_name=self.redis_queue)
|
self.redis.send_message(msg=json.dumps(action), queue_name=self.redis_queue)
|
||||||
|
|
||||||
|
@ -127,7 +139,9 @@ class CameraPiBackend(Backend):
|
||||||
else:
|
else:
|
||||||
while not self.should_stop():
|
while not self.should_stop():
|
||||||
connection = self.server_socket.accept()[0].makefile('wb')
|
connection = self.server_socket.accept()[0].makefile('wb')
|
||||||
self.logger.info('Accepted client connection on port {}'.format(self.listen_port))
|
self.logger.info(
|
||||||
|
'Accepted client connection on port {}'.format(self.listen_port)
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.camera.start_recording(connection, format=format)
|
self.camera.start_recording(connection, format=format)
|
||||||
|
@ -138,12 +152,16 @@ class CameraPiBackend(Backend):
|
||||||
try:
|
try:
|
||||||
self.stop_recording()
|
self.stop_recording()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning('Could not stop recording: {}'.format(str(e)))
|
self.logger.warning(
|
||||||
|
'Could not stop recording: {}'.format(str(e))
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
connection.close()
|
connection.close()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning('Could not close connection: {}'.format(str(e)))
|
self.logger.warning(
|
||||||
|
'Could not close connection: {}'.format(str(e))
|
||||||
|
)
|
||||||
|
|
||||||
self.send_camera_action(self.CameraAction.START_RECORDING)
|
self.send_camera_action(self.CameraAction.START_RECORDING)
|
||||||
|
|
||||||
|
@ -152,12 +170,13 @@ class CameraPiBackend(Backend):
|
||||||
return
|
return
|
||||||
|
|
||||||
self.logger.info('Starting camera recording')
|
self.logger.info('Starting camera recording')
|
||||||
self._recording_thread = Thread(target=recording_thread,
|
self._recording_thread = Thread(
|
||||||
name='PiCameraRecorder')
|
target=recording_thread, name='PiCameraRecorder'
|
||||||
|
)
|
||||||
self._recording_thread.start()
|
self._recording_thread.start()
|
||||||
|
|
||||||
def stop_recording(self):
|
def stop_recording(self):
|
||||||
""" Stops recording """
|
"""Stops recording"""
|
||||||
|
|
||||||
self.logger.info('Stopping camera recording')
|
self.logger.info('Stopping camera recording')
|
||||||
|
|
||||||
|
|
|
@ -22,17 +22,6 @@ class ChatTelegramBackend(Backend):
|
||||||
"""
|
"""
|
||||||
Telegram bot that listens for messages and updates.
|
Telegram bot that listens for messages and updates.
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
* :class:`platypush.message.event.chat.telegram.TextMessageEvent` when a text message is received.
|
|
||||||
* :class:`platypush.message.event.chat.telegram.PhotoMessageEvent` when a photo is received.
|
|
||||||
* :class:`platypush.message.event.chat.telegram.VideoMessageEvent` when a video is received.
|
|
||||||
* :class:`platypush.message.event.chat.telegram.LocationMessageEvent` when a location is received.
|
|
||||||
* :class:`platypush.message.event.chat.telegram.ContactMessageEvent` when a contact is received.
|
|
||||||
* :class:`platypush.message.event.chat.telegram.DocumentMessageEvent` when a document is received.
|
|
||||||
* :class:`platypush.message.event.chat.telegram.CommandMessageEvent` when a command message is received.
|
|
||||||
* :class:`platypush.message.event.chat.telegram.GroupChatCreatedEvent` when the bot is invited to a new group.
|
|
||||||
|
|
||||||
Requires:
|
Requires:
|
||||||
|
|
||||||
* The :class:`platypush.plugins.chat.telegram.ChatTelegramPlugin` plugin configured
|
* The :class:`platypush.plugins.chat.telegram.ChatTelegramPlugin` plugin configured
|
||||||
|
|
|
@ -10,17 +10,6 @@ from .entities.resources import MonitoredResource, MonitoredPattern, MonitoredRe
|
||||||
class FileMonitorBackend(Backend):
|
class FileMonitorBackend(Backend):
|
||||||
"""
|
"""
|
||||||
This backend monitors changes to local files and directories using the Watchdog API.
|
This backend monitors changes to local files and directories using the Watchdog API.
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
* :class:`platypush.message.event.file.FileSystemCreateEvent` if a resource is created.
|
|
||||||
* :class:`platypush.message.event.file.FileSystemDeleteEvent` if a resource is removed.
|
|
||||||
* :class:`platypush.message.event.file.FileSystemModifyEvent` if a resource is modified.
|
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **watchdog** (``pip install watchdog``)
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class EventHandlerFactory:
|
class EventHandlerFactory:
|
||||||
|
@ -29,20 +18,28 @@ class FileMonitorBackend(Backend):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_resource(resource: Union[str, Dict[str, Any], MonitoredResource]) -> EventHandler:
|
def from_resource(
|
||||||
|
resource: Union[str, Dict[str, Any], MonitoredResource]
|
||||||
|
) -> EventHandler:
|
||||||
if isinstance(resource, str):
|
if isinstance(resource, str):
|
||||||
resource = MonitoredResource(resource)
|
resource = MonitoredResource(resource)
|
||||||
elif isinstance(resource, dict):
|
elif isinstance(resource, dict):
|
||||||
if 'regexes' in resource or 'ignore_regexes' in resource:
|
if 'regexes' in resource or 'ignore_regexes' in resource:
|
||||||
resource = MonitoredRegex(**resource)
|
resource = MonitoredRegex(**resource)
|
||||||
elif 'patterns' in resource or 'ignore_patterns' in resource or 'ignore_directories' in resource:
|
elif (
|
||||||
|
'patterns' in resource
|
||||||
|
or 'ignore_patterns' in resource
|
||||||
|
or 'ignore_directories' in resource
|
||||||
|
):
|
||||||
resource = MonitoredPattern(**resource)
|
resource = MonitoredPattern(**resource)
|
||||||
else:
|
else:
|
||||||
resource = MonitoredResource(**resource)
|
resource = MonitoredResource(**resource)
|
||||||
|
|
||||||
return EventHandler.from_resource(resource)
|
return EventHandler.from_resource(resource)
|
||||||
|
|
||||||
def __init__(self, paths: Iterable[Union[str, Dict[str, Any], MonitoredResource]], **kwargs):
|
def __init__(
|
||||||
|
self, paths: Iterable[Union[str, Dict[str, Any], MonitoredResource]], **kwargs
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
:param paths: List of paths to monitor. Paths can either be expressed in any of the following ways:
|
:param paths: List of paths to monitor. Paths can either be expressed in any of the following ways:
|
||||||
|
|
||||||
|
@ -113,7 +110,9 @@ class FileMonitorBackend(Backend):
|
||||||
|
|
||||||
for path in paths:
|
for path in paths:
|
||||||
handler = self.EventHandlerFactory.from_resource(path)
|
handler = self.EventHandlerFactory.from_resource(path)
|
||||||
self._observer.schedule(handler, handler.resource.path, recursive=handler.resource.recursive)
|
self._observer.schedule(
|
||||||
|
handler, handler.resource.path, recursive=handler.resource.recursive
|
||||||
|
)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
super().run()
|
super().run()
|
||||||
|
|
|
@ -14,10 +14,6 @@ class FoursquareBackend(Backend):
|
||||||
|
|
||||||
* The :class:`platypush.plugins.foursquare.FoursquarePlugin` plugin configured and enabled.
|
* The :class:`platypush.plugins.foursquare.FoursquarePlugin` plugin configured and enabled.
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
- :class:`platypush.message.event.foursquare.FoursquareCheckinEvent` when a new check-in occurs.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_last_created_at_varname = '_foursquare_checkin_last_created_at'
|
_last_created_at_varname = '_foursquare_checkin_last_created_at'
|
||||||
|
@ -30,8 +26,12 @@ class FoursquareBackend(Backend):
|
||||||
self._last_created_at = None
|
self._last_created_at = None
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
self._last_created_at = int(get_plugin('variable').get(self._last_created_at_varname).
|
self._last_created_at = int(
|
||||||
output.get(self._last_created_at_varname) or 0)
|
get_plugin('variable')
|
||||||
|
.get(self._last_created_at_varname)
|
||||||
|
.output.get(self._last_created_at_varname)
|
||||||
|
or 0
|
||||||
|
)
|
||||||
self.logger.info('Started Foursquare backend')
|
self.logger.info('Started Foursquare backend')
|
||||||
|
|
||||||
def loop(self):
|
def loop(self):
|
||||||
|
@ -46,7 +46,9 @@ class FoursquareBackend(Backend):
|
||||||
|
|
||||||
self.bus.post(FoursquareCheckinEvent(checkin=last_checkin))
|
self.bus.post(FoursquareCheckinEvent(checkin=last_checkin))
|
||||||
self._last_created_at = last_checkin_created_at
|
self._last_created_at = last_checkin_created_at
|
||||||
get_plugin('variable').set(**{self._last_created_at_varname: self._last_created_at})
|
get_plugin('variable').set(
|
||||||
|
**{self._last_created_at_varname: self._last_created_at}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
|
@ -60,27 +60,6 @@ class GithubBackend(Backend):
|
||||||
- ``notifications``
|
- ``notifications``
|
||||||
- ``read:org`` if you want to access repositories on organization level.
|
- ``read:org`` if you want to access repositories on organization level.
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
- :class:`platypush.message.event.github.GithubPushEvent` when a new push is created.
|
|
||||||
- :class:`platypush.message.event.github.GithubCommitCommentEvent` when a new commit comment is created.
|
|
||||||
- :class:`platypush.message.event.github.GithubCreateEvent` when a tag or branch is created.
|
|
||||||
- :class:`platypush.message.event.github.GithubDeleteEvent` when a tag or branch is deleted.
|
|
||||||
- :class:`platypush.message.event.github.GithubForkEvent` when a user forks a repository.
|
|
||||||
- :class:`platypush.message.event.github.GithubWikiEvent` when new activity happens on a repository wiki.
|
|
||||||
- :class:`platypush.message.event.github.GithubIssueCommentEvent` when new activity happens on an issue comment.
|
|
||||||
- :class:`platypush.message.event.github.GithubIssueEvent` when new repository issue activity happens.
|
|
||||||
- :class:`platypush.message.event.github.GithubMemberEvent` when new repository collaborators activity happens.
|
|
||||||
- :class:`platypush.message.event.github.GithubPublicEvent` when a repository goes public.
|
|
||||||
- :class:`platypush.message.event.github.GithubPullRequestEvent` when new pull request related activity happens.
|
|
||||||
- :class:`platypush.message.event.github.GithubPullRequestReviewCommentEvent` when activity happens on a pull
|
|
||||||
request commit.
|
|
||||||
- :class:`platypush.message.event.github.GithubReleaseEvent` when a new release happens.
|
|
||||||
- :class:`platypush.message.event.github.GithubSponsorshipEvent` when new sponsorship related activity happens.
|
|
||||||
- :class:`platypush.message.event.github.GithubWatchEvent` when someone stars/starts watching a repository.
|
|
||||||
- :class:`platypush.message.event.github.GithubEvent` for any event that doesn't fall in the above categories
|
|
||||||
(``event_type`` will be set accordingly).
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_base_url = 'https://api.github.com'
|
_base_url = 'https://api.github.com'
|
||||||
|
|
|
@ -13,24 +13,24 @@ class GoogleFitBackend(Backend):
|
||||||
measurements, new fitness activities etc.) on the specified data streams and
|
measurements, new fitness activities etc.) on the specified data streams and
|
||||||
fire an event upon new data.
|
fire an event upon new data.
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
* :class:`platypush.message.event.google.fit.GoogleFitEvent` when a new
|
|
||||||
data point is received on one of the registered streams.
|
|
||||||
|
|
||||||
Requires:
|
Requires:
|
||||||
|
|
||||||
* The **google.fit** plugin
|
* The **google.fit** plugin
|
||||||
(:class:`platypush.plugins.google.fit.GoogleFitPlugin`) enabled.
|
(:class:`platypush.plugins.google.fit.GoogleFitPlugin`) enabled.
|
||||||
* The **db** plugin (:class:`platypush.plugins.db`) configured
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_default_poll_seconds = 60
|
_default_poll_seconds = 60
|
||||||
_default_user_id = 'me'
|
_default_user_id = 'me'
|
||||||
_last_timestamp_varname = '_GOOGLE_FIT_LAST_TIMESTAMP_'
|
_last_timestamp_varname = '_GOOGLE_FIT_LAST_TIMESTAMP_'
|
||||||
|
|
||||||
def __init__(self, data_sources, user_id=_default_user_id,
|
def __init__(
|
||||||
poll_seconds=_default_poll_seconds, *args, **kwargs):
|
self,
|
||||||
|
data_sources,
|
||||||
|
user_id=_default_user_id,
|
||||||
|
poll_seconds=_default_poll_seconds,
|
||||||
|
*args,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
:param data_sources: Google Fit data source IDs to monitor. You can
|
:param data_sources: Google Fit data source IDs to monitor. You can
|
||||||
get a list of the available data sources through the
|
get a list of the available data sources through the
|
||||||
|
@ -53,23 +53,31 @@ class GoogleFitBackend(Backend):
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
super().run()
|
super().run()
|
||||||
self.logger.info('Started Google Fit backend on data sources {}'.format(
|
self.logger.info(
|
||||||
self.data_sources))
|
'Started Google Fit backend on data sources {}'.format(self.data_sources)
|
||||||
|
)
|
||||||
|
|
||||||
while not self.should_stop():
|
while not self.should_stop():
|
||||||
try:
|
try:
|
||||||
for data_source in self.data_sources:
|
for data_source in self.data_sources:
|
||||||
varname = self._last_timestamp_varname + data_source
|
varname = self._last_timestamp_varname + data_source
|
||||||
last_timestamp = float(get_plugin('variable').
|
last_timestamp = float(
|
||||||
get(varname).output.get(varname) or 0)
|
get_plugin('variable').get(varname).output.get(varname) or 0
|
||||||
|
)
|
||||||
|
|
||||||
new_last_timestamp = last_timestamp
|
new_last_timestamp = last_timestamp
|
||||||
self.logger.info('Processing new entries from data source {}, last timestamp: {}'.
|
self.logger.info(
|
||||||
format(data_source,
|
'Processing new entries from data source {}, last timestamp: {}'.format(
|
||||||
str(datetime.datetime.fromtimestamp(last_timestamp))))
|
data_source,
|
||||||
|
str(datetime.datetime.fromtimestamp(last_timestamp)),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
data_points = get_plugin('google.fit').get_data(
|
data_points = (
|
||||||
user_id=self.user_id, data_source_id=data_source).output
|
get_plugin('google.fit')
|
||||||
|
.get_data(user_id=self.user_id, data_source_id=data_source)
|
||||||
|
.output
|
||||||
|
)
|
||||||
new_data_points = 0
|
new_data_points = 0
|
||||||
|
|
||||||
for dp in data_points:
|
for dp in data_points:
|
||||||
|
@ -78,25 +86,34 @@ class GoogleFitBackend(Backend):
|
||||||
del dp['dataSourceId']
|
del dp['dataSourceId']
|
||||||
|
|
||||||
if dp_time > last_timestamp:
|
if dp_time > last_timestamp:
|
||||||
self.bus.post(GoogleFitEvent(
|
self.bus.post(
|
||||||
user_id=self.user_id, data_source_id=data_source,
|
GoogleFitEvent(
|
||||||
data_type=dp.pop('dataTypeName'),
|
user_id=self.user_id,
|
||||||
start_time=dp_time,
|
data_source_id=data_source,
|
||||||
end_time=dp.pop('endTime'),
|
data_type=dp.pop('dataTypeName'),
|
||||||
modified_time=dp.pop('modifiedTime'),
|
start_time=dp_time,
|
||||||
values=dp.pop('values'),
|
end_time=dp.pop('endTime'),
|
||||||
**{camel_case_to_snake_case(k): v
|
modified_time=dp.pop('modifiedTime'),
|
||||||
for k, v in dp.items()}
|
values=dp.pop('values'),
|
||||||
))
|
**{
|
||||||
|
camel_case_to_snake_case(k): v
|
||||||
|
for k, v in dp.items()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
new_data_points += 1
|
new_data_points += 1
|
||||||
|
|
||||||
new_last_timestamp = max(dp_time, new_last_timestamp)
|
new_last_timestamp = max(dp_time, new_last_timestamp)
|
||||||
|
|
||||||
last_timestamp = new_last_timestamp
|
last_timestamp = new_last_timestamp
|
||||||
self.logger.info('Got {} new entries from data source {}, last timestamp: {}'.
|
self.logger.info(
|
||||||
format(new_data_points, data_source,
|
'Got {} new entries from data source {}, last timestamp: {}'.format(
|
||||||
str(datetime.datetime.fromtimestamp(last_timestamp))))
|
new_data_points,
|
||||||
|
data_source,
|
||||||
|
str(datetime.datetime.fromtimestamp(last_timestamp)),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
get_plugin('variable').set(**{varname: last_timestamp})
|
get_plugin('variable').set(**{varname: last_timestamp})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
@ -12,16 +12,6 @@ class GooglePubsubBackend(Backend):
|
||||||
Subscribe to a list of topics on a Google Pub/Sub instance. See
|
Subscribe to a list of topics on a Google Pub/Sub instance. See
|
||||||
:class:`platypush.plugins.google.pubsub.GooglePubsubPlugin` for a reference on how to generate your
|
:class:`platypush.plugins.google.pubsub.GooglePubsubPlugin` for a reference on how to generate your
|
||||||
project and credentials file.
|
project and credentials file.
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
* :class:`platypush.message.event.google.pubsub.GooglePubsubMessageEvent` when a new message is received on
|
|
||||||
a subscribed topic.
|
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **google-cloud-pubsub** (``pip install google-cloud-pubsub``)
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|
|
@ -9,17 +9,6 @@ class GpsBackend(Backend):
|
||||||
"""
|
"""
|
||||||
This backend can interact with a GPS device and listen for events.
|
This backend can interact with a GPS device and listen for events.
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
* :class:`platypush.message.event.gps.GPSVersionEvent` when a GPS device advertises its version data
|
|
||||||
* :class:`platypush.message.event.gps.GPSDeviceEvent` when a GPS device is connected or updated
|
|
||||||
* :class:`platypush.message.event.gps.GPSUpdateEvent` when a GPS device has new data
|
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **gps** (``pip install gps``)
|
|
||||||
* **gpsd** daemon running (``apt-get install gpsd`` or ``pacman -S gpsd`` depending on your distro)
|
|
||||||
|
|
||||||
Once installed gpsd you need to run it and associate it to your device. Example if your GPS device communicates
|
Once installed gpsd you need to run it and associate it to your device. Example if your GPS device communicates
|
||||||
over USB and is available on /dev/ttyUSB0::
|
over USB and is available on /dev/ttyUSB0::
|
||||||
|
|
||||||
|
@ -52,41 +41,68 @@ class GpsBackend(Backend):
|
||||||
|
|
||||||
with self._session_lock:
|
with self._session_lock:
|
||||||
if not self._session:
|
if not self._session:
|
||||||
self._session = gps.gps(host=self.gpsd_server, port=self.gpsd_port, reconnect=True)
|
self._session = gps.gps(
|
||||||
|
host=self.gpsd_server, port=self.gpsd_port, reconnect=True
|
||||||
|
)
|
||||||
self._session.stream(gps.WATCH_ENABLE | gps.WATCH_NEWSTYLE)
|
self._session.stream(gps.WATCH_ENABLE | gps.WATCH_NEWSTYLE)
|
||||||
|
|
||||||
return self._session
|
return self._session
|
||||||
|
|
||||||
def _gps_report_to_event(self, report):
|
def _gps_report_to_event(self, report):
|
||||||
if report.get('class').lower() == 'version':
|
if report.get('class').lower() == 'version':
|
||||||
return GPSVersionEvent(release=report.get('release'),
|
return GPSVersionEvent(
|
||||||
rev=report.get('rev'),
|
release=report.get('release'),
|
||||||
proto_major=report.get('proto_major'),
|
rev=report.get('rev'),
|
||||||
proto_minor=report.get('proto_minor'))
|
proto_major=report.get('proto_major'),
|
||||||
|
proto_minor=report.get('proto_minor'),
|
||||||
|
)
|
||||||
if report.get('class').lower() == 'devices':
|
if report.get('class').lower() == 'devices':
|
||||||
for device in report.get('devices', []):
|
for device in report.get('devices', []):
|
||||||
if device.get('path') not in self._devices or device != self._devices.get('path'):
|
if device.get(
|
||||||
|
'path'
|
||||||
|
) not in self._devices or device != self._devices.get('path'):
|
||||||
# noinspection DuplicatedCode
|
# noinspection DuplicatedCode
|
||||||
self._devices[device.get('path')] = device
|
self._devices[device.get('path')] = device
|
||||||
return GPSDeviceEvent(path=device.get('path'), activated=device.get('activated'),
|
return GPSDeviceEvent(
|
||||||
native=device.get('native'), bps=device.get('bps'),
|
path=device.get('path'),
|
||||||
parity=device.get('parity'), stopbits=device.get('stopbits'),
|
activated=device.get('activated'),
|
||||||
cycle=device.get('cycle'), driver=device.get('driver'))
|
native=device.get('native'),
|
||||||
|
bps=device.get('bps'),
|
||||||
|
parity=device.get('parity'),
|
||||||
|
stopbits=device.get('stopbits'),
|
||||||
|
cycle=device.get('cycle'),
|
||||||
|
driver=device.get('driver'),
|
||||||
|
)
|
||||||
if report.get('class').lower() == 'device':
|
if report.get('class').lower() == 'device':
|
||||||
# noinspection DuplicatedCode
|
# noinspection DuplicatedCode
|
||||||
self._devices[report.get('path')] = report
|
self._devices[report.get('path')] = report
|
||||||
return GPSDeviceEvent(path=report.get('path'), activated=report.get('activated'),
|
return GPSDeviceEvent(
|
||||||
native=report.get('native'), bps=report.get('bps'),
|
path=report.get('path'),
|
||||||
parity=report.get('parity'), stopbits=report.get('stopbits'),
|
activated=report.get('activated'),
|
||||||
cycle=report.get('cycle'), driver=report.get('driver'))
|
native=report.get('native'),
|
||||||
|
bps=report.get('bps'),
|
||||||
|
parity=report.get('parity'),
|
||||||
|
stopbits=report.get('stopbits'),
|
||||||
|
cycle=report.get('cycle'),
|
||||||
|
driver=report.get('driver'),
|
||||||
|
)
|
||||||
if report.get('class').lower() == 'tpv':
|
if report.get('class').lower() == 'tpv':
|
||||||
return GPSUpdateEvent(device=report.get('device'), latitude=report.get('lat'), longitude=report.get('lon'),
|
return GPSUpdateEvent(
|
||||||
altitude=report.get('alt'), mode=report.get('mode'), epv=report.get('epv'),
|
device=report.get('device'),
|
||||||
eph=report.get('eph'), sep=report.get('sep'))
|
latitude=report.get('lat'),
|
||||||
|
longitude=report.get('lon'),
|
||||||
|
altitude=report.get('alt'),
|
||||||
|
mode=report.get('mode'),
|
||||||
|
epv=report.get('epv'),
|
||||||
|
eph=report.get('eph'),
|
||||||
|
sep=report.get('sep'),
|
||||||
|
)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
super().run()
|
super().run()
|
||||||
self.logger.info('Initialized GPS backend on {}:{}'.format(self.gpsd_server, self.gpsd_port))
|
self.logger.info(
|
||||||
|
'Initialized GPS backend on {}:{}'.format(self.gpsd_server, self.gpsd_port)
|
||||||
|
)
|
||||||
last_event = None
|
last_event = None
|
||||||
|
|
||||||
while not self.should_stop():
|
while not self.should_stop():
|
||||||
|
@ -94,15 +110,31 @@ class GpsBackend(Backend):
|
||||||
session = self._get_session()
|
session = self._get_session()
|
||||||
report = session.next()
|
report = session.next()
|
||||||
event = self._gps_report_to_event(report)
|
event = self._gps_report_to_event(report)
|
||||||
if event and (last_event is None or
|
if event and (
|
||||||
abs((last_event.args.get('latitude') or 0) - (event.args.get('latitude') or 0)) >= self._lat_lng_tolerance or
|
last_event is None
|
||||||
abs((last_event.args.get('longitude') or 0) - (event.args.get('longitude') or 0)) >= self._lat_lng_tolerance or
|
or abs(
|
||||||
abs((last_event.args.get('altitude') or 0) - (event.args.get('altitude') or 0)) >= self._alt_tolerance):
|
(last_event.args.get('latitude') or 0)
|
||||||
|
- (event.args.get('latitude') or 0)
|
||||||
|
)
|
||||||
|
>= self._lat_lng_tolerance
|
||||||
|
or abs(
|
||||||
|
(last_event.args.get('longitude') or 0)
|
||||||
|
- (event.args.get('longitude') or 0)
|
||||||
|
)
|
||||||
|
>= self._lat_lng_tolerance
|
||||||
|
or abs(
|
||||||
|
(last_event.args.get('altitude') or 0)
|
||||||
|
- (event.args.get('altitude') or 0)
|
||||||
|
)
|
||||||
|
>= self._alt_tolerance
|
||||||
|
):
|
||||||
self.bus.post(event)
|
self.bus.post(event)
|
||||||
last_event = event
|
last_event = event
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if isinstance(e, StopIteration):
|
if isinstance(e, StopIteration):
|
||||||
self.logger.warning('GPS service connection lost, check that gpsd is running')
|
self.logger.warning(
|
||||||
|
'GPS service connection lost, check that gpsd is running'
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self.logger.exception(e)
|
self.logger.exception(e)
|
||||||
|
|
||||||
|
|
|
@ -40,15 +40,6 @@ class RssUpdates(HttpRequest):
|
||||||
poll_seconds: 86400 # Poll once a day
|
poll_seconds: 86400 # Poll once a day
|
||||||
digest_format: html # Generate an HTML feed with the new items
|
digest_format: html # Generate an HTML feed with the new items
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
- :class:`platypush.message.event.http.rss.NewFeedEvent` when new items are parsed from a feed or a new digest
|
|
||||||
is available.
|
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **feedparser** (``pip install feedparser``)
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
user_agent = (
|
user_agent = (
|
||||||
|
|
|
@ -1,101 +0,0 @@
|
||||||
import os
|
|
||||||
|
|
||||||
from platypush.backend import Backend
|
|
||||||
from platypush.message.event.inotify import InotifyCreateEvent, InotifyDeleteEvent, \
|
|
||||||
InotifyOpenEvent, InotifyModifyEvent, InotifyCloseEvent, InotifyAccessEvent, InotifyMovedEvent
|
|
||||||
|
|
||||||
|
|
||||||
class InotifyBackend(Backend):
|
|
||||||
"""
|
|
||||||
**NOTE**: This backend is *deprecated* in favour of :class:`platypush.backend.file.monitor.FileMonitorBackend`.
|
|
||||||
|
|
||||||
(Linux only) This backend will listen for events on the filesystem (whether
|
|
||||||
a file/directory on a watch list is opened, modified, created, deleted,
|
|
||||||
closed or had its permissions changed) and will trigger a relevant event.
|
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
* :class:`platypush.message.event.inotify.InotifyCreateEvent` if a resource is created
|
|
||||||
* :class:`platypush.message.event.inotify.InotifyAccessEvent` if a resource is accessed
|
|
||||||
* :class:`platypush.message.event.inotify.InotifyOpenEvent` if a resource is opened
|
|
||||||
* :class:`platypush.message.event.inotify.InotifyModifyEvent` if a resource is modified
|
|
||||||
* :class:`platypush.message.event.inotify.InotifyPermissionsChangeEvent` if the permissions of a resource are changed
|
|
||||||
* :class:`platypush.message.event.inotify.InotifyCloseEvent` if a resource is closed
|
|
||||||
* :class:`platypush.message.event.inotify.InotifyDeleteEvent` if a resource is removed
|
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **inotify** (``pip install inotify``)
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
inotify_watch = None
|
|
||||||
|
|
||||||
def __init__(self, watch_paths=None, **kwargs):
|
|
||||||
"""
|
|
||||||
:param watch_paths: Filesystem resources to watch for events
|
|
||||||
:type watch_paths: str
|
|
||||||
"""
|
|
||||||
|
|
||||||
super().__init__(**kwargs)
|
|
||||||
self.watch_paths = set(map(
|
|
||||||
lambda path: os.path.abspath(os.path.expanduser(path)),
|
|
||||||
watch_paths if watch_paths else []))
|
|
||||||
|
|
||||||
def _cleanup(self):
|
|
||||||
if not self.inotify_watch:
|
|
||||||
return
|
|
||||||
|
|
||||||
for path in self.watch_paths:
|
|
||||||
self.inotify_watch.remove_watch(path)
|
|
||||||
|
|
||||||
self.inotify_watch = None
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
import inotify.adapters
|
|
||||||
super().run()
|
|
||||||
|
|
||||||
self.inotify_watch = inotify.adapters.Inotify()
|
|
||||||
for path in self.watch_paths:
|
|
||||||
self.inotify_watch.add_watch(path)
|
|
||||||
|
|
||||||
moved_file = None
|
|
||||||
self.logger.info('Initialized inotify file monitoring backend, monitored resources: {}'
|
|
||||||
.format(self.watch_paths))
|
|
||||||
|
|
||||||
try:
|
|
||||||
for inotify_event in self.inotify_watch.event_gen():
|
|
||||||
if inotify_event is not None:
|
|
||||||
(header, inotify_types, watch_path, filename) = inotify_event
|
|
||||||
event = None
|
|
||||||
resource_type = inotify_types[1] if len(inotify_types) > 1 else None
|
|
||||||
|
|
||||||
if moved_file:
|
|
||||||
new = filename if 'IN_MOVED_TO' in inotify_types else None
|
|
||||||
event = InotifyMovedEvent(path=watch_path, old=moved_file, new=new)
|
|
||||||
moved_file = None
|
|
||||||
|
|
||||||
if 'IN_OPEN' in inotify_types:
|
|
||||||
event = InotifyOpenEvent(path=watch_path, resource=filename, resource_type=resource_type)
|
|
||||||
elif 'IN_ACCESS' in inotify_types:
|
|
||||||
event = InotifyAccessEvent(path=watch_path, resource=filename, resource_type=resource_type)
|
|
||||||
elif 'IN_CREATE' in inotify_types:
|
|
||||||
event = InotifyCreateEvent(path=watch_path, resource=filename, resource_type=resource_type)
|
|
||||||
elif 'IN_MOVED_FROM' in inotify_types:
|
|
||||||
moved_file = filename
|
|
||||||
elif 'IN_MOVED_TO' in inotify_types and not moved_file:
|
|
||||||
event = InotifyMovedEvent(path=watch_path, old=None, new=filename)
|
|
||||||
elif 'IN_DELETE' in inotify_types:
|
|
||||||
event = InotifyDeleteEvent(path=watch_path, resource=filename, resource_type=resource_type)
|
|
||||||
elif 'IN_MODIFY' in inotify_types:
|
|
||||||
event = InotifyModifyEvent(path=watch_path, resource=filename, resource_type=resource_type)
|
|
||||||
elif 'IN_CLOSE_WRITE' in inotify_types or 'IN_CLOSE_NOWRITE' in inotify_types:
|
|
||||||
event = InotifyCloseEvent(path=watch_path, resource=filename, resource_type=resource_type)
|
|
||||||
|
|
||||||
if event:
|
|
||||||
self.bus.post(event)
|
|
||||||
finally:
|
|
||||||
self._cleanup()
|
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
|
|
@ -1,21 +0,0 @@
|
||||||
manifest:
|
|
||||||
events:
|
|
||||||
platypush.message.event.inotify.InotifyAccessEvent: if a resource is accessed
|
|
||||||
platypush.message.event.inotify.InotifyCloseEvent: if a resource is closed
|
|
||||||
platypush.message.event.inotify.InotifyCreateEvent: if a resource is created
|
|
||||||
platypush.message.event.inotify.InotifyDeleteEvent: if a resource is removed
|
|
||||||
platypush.message.event.inotify.InotifyModifyEvent: if a resource is modified
|
|
||||||
platypush.message.event.inotify.InotifyOpenEvent: if a resource is opened
|
|
||||||
platypush.message.event.inotify.InotifyPermissionsChangeEvent: if the permissions
|
|
||||||
of a resource are changed
|
|
||||||
install:
|
|
||||||
apk:
|
|
||||||
- py3-inotify
|
|
||||||
apt:
|
|
||||||
- python3-inotify
|
|
||||||
dnf:
|
|
||||||
- python-inotify
|
|
||||||
pip:
|
|
||||||
- inotify
|
|
||||||
package: platypush.backend.inotify
|
|
||||||
type: backend
|
|
|
@ -8,14 +8,6 @@ class JoystickBackend(Backend):
|
||||||
"""
|
"""
|
||||||
This backend will listen for events from a joystick device and post a
|
This backend will listen for events from a joystick device and post a
|
||||||
JoystickEvent whenever a new event is captured.
|
JoystickEvent whenever a new event is captured.
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
* :class:`platypush.message.event.joystick.JoystickEvent` when a new joystick event is received
|
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **inputs** (``pip install inputs``)
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, device, *args, **kwargs):
|
def __init__(self, device, *args, **kwargs):
|
||||||
|
@ -32,7 +24,9 @@ class JoystickBackend(Backend):
|
||||||
import inputs
|
import inputs
|
||||||
|
|
||||||
super().run()
|
super().run()
|
||||||
self.logger.info('Initialized joystick backend on device {}'.format(self.device))
|
self.logger.info(
|
||||||
|
'Initialized joystick backend on device {}'.format(self.device)
|
||||||
|
)
|
||||||
|
|
||||||
while not self.should_stop():
|
while not self.should_stop():
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -6,8 +6,14 @@ import time
|
||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
|
|
||||||
from platypush.backend import Backend
|
from platypush.backend import Backend
|
||||||
from platypush.message.event.joystick import JoystickConnectedEvent, JoystickDisconnectedEvent, JoystickStateEvent, \
|
from platypush.message.event.joystick import (
|
||||||
JoystickButtonPressedEvent, JoystickButtonReleasedEvent, JoystickAxisEvent
|
JoystickConnectedEvent,
|
||||||
|
JoystickDisconnectedEvent,
|
||||||
|
JoystickStateEvent,
|
||||||
|
JoystickButtonPressedEvent,
|
||||||
|
JoystickButtonReleasedEvent,
|
||||||
|
JoystickAxisEvent,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class JoystickState:
|
class JoystickState:
|
||||||
|
@ -38,9 +44,7 @@ class JoystickState:
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {k: v for k, v in diff.items() if v}
|
||||||
k: v for k, v in diff.items() if v
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class JoystickJstestBackend(Backend):
|
class JoystickJstestBackend(Backend):
|
||||||
|
@ -49,35 +53,17 @@ class JoystickJstestBackend(Backend):
|
||||||
:class:`platypush.backend.joystick.JoystickBackend` backend (this may especially happen with some Bluetooth
|
:class:`platypush.backend.joystick.JoystickBackend` backend (this may especially happen with some Bluetooth
|
||||||
joysticks that don't support the ``ioctl`` requests used by ``inputs``).
|
joysticks that don't support the ``ioctl`` requests used by ``inputs``).
|
||||||
|
|
||||||
This backend only works on Linux and it requires the ``joystick`` package to be installed.
|
This backend only works on Linux, and it requires the ``joystick`` package to be installed.
|
||||||
|
|
||||||
**NOTE**: This backend can be quite slow, since it has to run another program (``jstest``) and parse its output.
|
**NOTE**: This backend can be quite slow, since it has to run another program (``jstest``) and parse its output.
|
||||||
Consider it as a last resort if your joystick works with neither :class:`platypush.backend.joystick.JoystickBackend`
|
Consider it as a last resort if your joystick works with neither :class:`platypush.backend.joystick.JoystickBackend`
|
||||||
nor :class:`platypush.backend.joystick.JoystickLinuxBackend`.
|
nor :class:`platypush.backend.joystick.JoystickLinuxBackend`.
|
||||||
|
|
||||||
Instructions on Debian-based distros::
|
|
||||||
|
|
||||||
# apt-get install joystick
|
|
||||||
|
|
||||||
Instructions on Arch-based distros::
|
|
||||||
|
|
||||||
# pacman -S joyutils
|
|
||||||
|
|
||||||
To test if your joystick is compatible, connect it to your device, check for its path (usually under
|
To test if your joystick is compatible, connect it to your device, check for its path (usually under
|
||||||
``/dev/input/js*``) and run::
|
``/dev/input/js*``) and run::
|
||||||
|
|
||||||
$ jstest /dev/input/js[n]
|
$ jstest /dev/input/js[n]
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
* :class:`platypush.message.event.joystick.JoystickConnectedEvent` when the joystick is connected.
|
|
||||||
* :class:`platypush.message.event.joystick.JoystickDisconnectedEvent` when the joystick is disconnected.
|
|
||||||
* :class:`platypush.message.event.joystick.JoystickStateEvent` when the state of the joystick (i.e. some of its
|
|
||||||
axes or buttons values) changes.
|
|
||||||
* :class:`platypush.message.event.joystick.JoystickButtonPressedEvent` when a joystick button is pressed.
|
|
||||||
* :class:`platypush.message.event.joystick.JoystickButtonReleasedEvent` when a joystick button is released.
|
|
||||||
* :class:`platypush.message.event.joystick.JoystickAxisEvent` when an axis value of the joystick changes.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
js_axes_regex = re.compile(r'Axes:\s+(((\d+):\s*([\-\d]+)\s*)+)')
|
js_axes_regex = re.compile(r'Axes:\s+(((\d+):\s*([\-\d]+)\s*)+)')
|
||||||
|
@ -85,10 +71,12 @@ class JoystickJstestBackend(Backend):
|
||||||
js_axis_regex = re.compile(r'^\s*(\d+):\s*([\-\d]+)\s*(.*)')
|
js_axis_regex = re.compile(r'^\s*(\d+):\s*([\-\d]+)\s*(.*)')
|
||||||
js_button_regex = re.compile(r'^\s*(\d+):\s*(on|off)\s*(.*)')
|
js_button_regex = re.compile(r'^\s*(\d+):\s*(on|off)\s*(.*)')
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(
|
||||||
device: str = '/dev/input/js0',
|
self,
|
||||||
jstest_path: str = '/usr/bin/jstest',
|
device: str = '/dev/input/js0',
|
||||||
**kwargs):
|
jstest_path: str = '/usr/bin/jstest',
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
:param device: Path to the joystick device (default: ``/dev/input/js0``).
|
:param device: Path to the joystick device (default: ``/dev/input/js0``).
|
||||||
:param jstest_path: Path to the ``jstest`` executable that comes with the ``joystick`` system package
|
:param jstest_path: Path to the ``jstest`` executable that comes with the ``joystick`` system package
|
||||||
|
@ -140,7 +128,11 @@ class JoystickJstestBackend(Backend):
|
||||||
if line.endswith('Axes: '):
|
if line.endswith('Axes: '):
|
||||||
break
|
break
|
||||||
|
|
||||||
while os.path.exists(self.device) and not self.should_stop() and len(axes) < len(self._state.axes):
|
while (
|
||||||
|
os.path.exists(self.device)
|
||||||
|
and not self.should_stop()
|
||||||
|
and len(axes) < len(self._state.axes)
|
||||||
|
):
|
||||||
ch = ' '
|
ch = ' '
|
||||||
while ch == ' ':
|
while ch == ' ':
|
||||||
ch = self._process.stdout.read(1).decode()
|
ch = self._process.stdout.read(1).decode()
|
||||||
|
@ -174,7 +166,11 @@ class JoystickJstestBackend(Backend):
|
||||||
if line.endswith('Buttons: '):
|
if line.endswith('Buttons: '):
|
||||||
break
|
break
|
||||||
|
|
||||||
while os.path.exists(self.device) and not self.should_stop() and len(buttons) < len(self._state.buttons):
|
while (
|
||||||
|
os.path.exists(self.device)
|
||||||
|
and not self.should_stop()
|
||||||
|
and len(buttons) < len(self._state.buttons)
|
||||||
|
):
|
||||||
ch = ' '
|
ch = ' '
|
||||||
while ch == ' ':
|
while ch == ' ':
|
||||||
ch = self._process.stdout.read(1).decode()
|
ch = self._process.stdout.read(1).decode()
|
||||||
|
@ -195,10 +191,12 @@ class JoystickJstestBackend(Backend):
|
||||||
return JoystickState(axes=axes, buttons=buttons)
|
return JoystickState(axes=axes, buttons=buttons)
|
||||||
|
|
||||||
def _initialize(self):
|
def _initialize(self):
|
||||||
while self._process.poll() is None and \
|
while (
|
||||||
os.path.exists(self.device) and \
|
self._process.poll() is None
|
||||||
not self.should_stop() and \
|
and os.path.exists(self.device)
|
||||||
not self._state:
|
and not self.should_stop()
|
||||||
|
and not self._state
|
||||||
|
):
|
||||||
line = b''
|
line = b''
|
||||||
ch = None
|
ch = None
|
||||||
|
|
||||||
|
@ -243,7 +241,9 @@ class JoystickJstestBackend(Backend):
|
||||||
self.bus.post(JoystickStateEvent(device=self.device, **state.__dict__))
|
self.bus.post(JoystickStateEvent(device=self.device, **state.__dict__))
|
||||||
|
|
||||||
for button, pressed in diff.get('buttons', {}).items():
|
for button, pressed in diff.get('buttons', {}).items():
|
||||||
evt_class = JoystickButtonPressedEvent if pressed else JoystickButtonReleasedEvent
|
evt_class = (
|
||||||
|
JoystickButtonPressedEvent if pressed else JoystickButtonReleasedEvent
|
||||||
|
)
|
||||||
self.bus.post(evt_class(device=self.device, button=button))
|
self.bus.post(evt_class(device=self.device, button=button))
|
||||||
|
|
||||||
for axis, value in diff.get('axes', {}).items():
|
for axis, value in diff.get('axes', {}).items():
|
||||||
|
@ -259,8 +259,8 @@ class JoystickJstestBackend(Backend):
|
||||||
self._wait_ready()
|
self._wait_ready()
|
||||||
|
|
||||||
with subprocess.Popen(
|
with subprocess.Popen(
|
||||||
[self.jstest_path, '--normal', self.device],
|
[self.jstest_path, '--normal', self.device], stdout=subprocess.PIPE
|
||||||
stdout=subprocess.PIPE) as self._process:
|
) as self._process:
|
||||||
self.logger.info('Device opened')
|
self.logger.info('Device opened')
|
||||||
self._initialize()
|
self._initialize()
|
||||||
|
|
||||||
|
@ -268,7 +268,9 @@ class JoystickJstestBackend(Backend):
|
||||||
break
|
break
|
||||||
|
|
||||||
for state in self._read_states():
|
for state in self._read_states():
|
||||||
if self._process.poll() is not None or not os.path.exists(self.device):
|
if self._process.poll() is not None or not os.path.exists(
|
||||||
|
self.device
|
||||||
|
):
|
||||||
self.logger.warning(f'Connection to {self.device} lost')
|
self.logger.warning(f'Connection to {self.device} lost')
|
||||||
self.bus.post(JoystickDisconnectedEvent(self.device))
|
self.bus.post(JoystickDisconnectedEvent(self.device))
|
||||||
break
|
break
|
||||||
|
|
|
@ -1,17 +1,11 @@
|
||||||
manifest:
|
manifest:
|
||||||
events:
|
events:
|
||||||
platypush.message.event.joystick.JoystickAxisEvent: when an axis value of the
|
- platypush.message.event.joystick.JoystickAxisEvent
|
||||||
joystick changes.
|
- platypush.message.event.joystick.JoystickButtonPressedEvent
|
||||||
platypush.message.event.joystick.JoystickButtonPressedEvent: when a joystick button
|
- platypush.message.event.joystick.JoystickButtonReleasedEvent
|
||||||
is pressed.
|
- platypush.message.event.joystick.JoystickConnectedEvent
|
||||||
platypush.message.event.joystick.JoystickButtonReleasedEvent: when a joystick
|
- platypush.message.event.joystick.JoystickDisconnectedEvent
|
||||||
button is released.
|
- platypush.message.event.joystick.JoystickStateEvent
|
||||||
platypush.message.event.joystick.JoystickConnectedEvent: when the joystick is
|
|
||||||
connected.
|
|
||||||
platypush.message.event.joystick.JoystickDisconnectedEvent: when the joystick
|
|
||||||
is disconnected.
|
|
||||||
platypush.message.event.joystick.JoystickStateEvent: when the state of the joystick
|
|
||||||
(i.e. some of itsaxes or buttons values) changes.
|
|
||||||
install:
|
install:
|
||||||
apk:
|
apk:
|
||||||
- linuxconsoletools
|
- linuxconsoletools
|
||||||
|
|
|
@ -5,8 +5,13 @@ from fcntl import ioctl
|
||||||
from typing import IO
|
from typing import IO
|
||||||
|
|
||||||
from platypush.backend import Backend
|
from platypush.backend import Backend
|
||||||
from platypush.message.event.joystick import JoystickConnectedEvent, JoystickDisconnectedEvent, \
|
from platypush.message.event.joystick import (
|
||||||
JoystickButtonPressedEvent, JoystickButtonReleasedEvent, JoystickAxisEvent
|
JoystickConnectedEvent,
|
||||||
|
JoystickDisconnectedEvent,
|
||||||
|
JoystickButtonPressedEvent,
|
||||||
|
JoystickButtonReleasedEvent,
|
||||||
|
JoystickAxisEvent,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class JoystickLinuxBackend(Backend):
|
class JoystickLinuxBackend(Backend):
|
||||||
|
@ -16,15 +21,6 @@ class JoystickLinuxBackend(Backend):
|
||||||
It is loosely based on https://gist.github.com/rdb/8864666, which itself uses the
|
It is loosely based on https://gist.github.com/rdb/8864666, which itself uses the
|
||||||
`Linux kernel joystick API <https://www.kernel.org/doc/Documentation/input/joystick-api.txt>`_ to interact with
|
`Linux kernel joystick API <https://www.kernel.org/doc/Documentation/input/joystick-api.txt>`_ to interact with
|
||||||
the devices.
|
the devices.
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
* :class:`platypush.message.event.joystick.JoystickConnectedEvent` when the joystick is connected.
|
|
||||||
* :class:`platypush.message.event.joystick.JoystickDisconnectedEvent` when the joystick is disconnected.
|
|
||||||
* :class:`platypush.message.event.joystick.JoystickButtonPressedEvent` when a joystick button is pressed.
|
|
||||||
* :class:`platypush.message.event.joystick.JoystickButtonReleasedEvent` when a joystick button is released.
|
|
||||||
* :class:`platypush.message.event.joystick.JoystickAxisEvent` when an axis value of the joystick changes.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# These constants were borrowed from linux/input.h
|
# These constants were borrowed from linux/input.h
|
||||||
|
@ -39,7 +35,7 @@ class JoystickLinuxBackend(Backend):
|
||||||
0x07: 'rudder',
|
0x07: 'rudder',
|
||||||
0x08: 'wheel',
|
0x08: 'wheel',
|
||||||
0x09: 'gas',
|
0x09: 'gas',
|
||||||
0x0a: 'brake',
|
0x0A: 'brake',
|
||||||
0x10: 'hat0x',
|
0x10: 'hat0x',
|
||||||
0x11: 'hat0y',
|
0x11: 'hat0y',
|
||||||
0x12: 'hat1x',
|
0x12: 'hat1x',
|
||||||
|
@ -50,9 +46,9 @@ class JoystickLinuxBackend(Backend):
|
||||||
0x17: 'hat3y',
|
0x17: 'hat3y',
|
||||||
0x18: 'pressure',
|
0x18: 'pressure',
|
||||||
0x19: 'distance',
|
0x19: 'distance',
|
||||||
0x1a: 'tilt_x',
|
0x1A: 'tilt_x',
|
||||||
0x1b: 'tilt_y',
|
0x1B: 'tilt_y',
|
||||||
0x1c: 'tool_width',
|
0x1C: 'tool_width',
|
||||||
0x20: 'volume',
|
0x20: 'volume',
|
||||||
0x28: 'misc',
|
0x28: 'misc',
|
||||||
}
|
}
|
||||||
|
@ -68,9 +64,9 @@ class JoystickLinuxBackend(Backend):
|
||||||
0x127: 'base2',
|
0x127: 'base2',
|
||||||
0x128: 'base3',
|
0x128: 'base3',
|
||||||
0x129: 'base4',
|
0x129: 'base4',
|
||||||
0x12a: 'base5',
|
0x12A: 'base5',
|
||||||
0x12b: 'base6',
|
0x12B: 'base6',
|
||||||
0x12f: 'dead',
|
0x12F: 'dead',
|
||||||
0x130: 'a',
|
0x130: 'a',
|
||||||
0x131: 'b',
|
0x131: 'b',
|
||||||
0x132: 'c',
|
0x132: 'c',
|
||||||
|
@ -81,20 +77,20 @@ class JoystickLinuxBackend(Backend):
|
||||||
0x137: 'tr',
|
0x137: 'tr',
|
||||||
0x138: 'tl2',
|
0x138: 'tl2',
|
||||||
0x139: 'tr2',
|
0x139: 'tr2',
|
||||||
0x13a: 'select',
|
0x13A: 'select',
|
||||||
0x13b: 'start',
|
0x13B: 'start',
|
||||||
0x13c: 'mode',
|
0x13C: 'mode',
|
||||||
0x13d: 'thumbl',
|
0x13D: 'thumbl',
|
||||||
0x13e: 'thumbr',
|
0x13E: 'thumbr',
|
||||||
0x220: 'dpad_up',
|
0x220: 'dpad_up',
|
||||||
0x221: 'dpad_down',
|
0x221: 'dpad_down',
|
||||||
0x222: 'dpad_left',
|
0x222: 'dpad_left',
|
||||||
0x223: 'dpad_right',
|
0x223: 'dpad_right',
|
||||||
# XBox 360 controller uses these codes.
|
# XBox 360 controller uses these codes.
|
||||||
0x2c0: 'dpad_left',
|
0x2C0: 'dpad_left',
|
||||||
0x2c1: 'dpad_right',
|
0x2C1: 'dpad_right',
|
||||||
0x2c2: 'dpad_up',
|
0x2C2: 'dpad_up',
|
||||||
0x2c3: 'dpad_down',
|
0x2C3: 'dpad_down',
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, device: str = '/dev/input/js0', *args, **kwargs):
|
def __init__(self, device: str = '/dev/input/js0', *args, **kwargs):
|
||||||
|
@ -111,21 +107,21 @@ class JoystickLinuxBackend(Backend):
|
||||||
def _init_joystick(self, dev: IO):
|
def _init_joystick(self, dev: IO):
|
||||||
# Get the device name.
|
# Get the device name.
|
||||||
buf = array.array('B', [0] * 64)
|
buf = array.array('B', [0] * 64)
|
||||||
ioctl(dev, 0x80006a13 + (0x10000 * len(buf)), buf) # JSIOCGNAME(len)
|
ioctl(dev, 0x80006A13 + (0x10000 * len(buf)), buf) # JSIOCGNAME(len)
|
||||||
js_name = buf.tobytes().rstrip(b'\x00').decode('utf-8')
|
js_name = buf.tobytes().rstrip(b'\x00').decode('utf-8')
|
||||||
|
|
||||||
# Get number of axes and buttons.
|
# Get number of axes and buttons.
|
||||||
buf = array.array('B', [0])
|
buf = array.array('B', [0])
|
||||||
ioctl(dev, 0x80016a11, buf) # JSIOCGAXES
|
ioctl(dev, 0x80016A11, buf) # JSIOCGAXES
|
||||||
num_axes = buf[0]
|
num_axes = buf[0]
|
||||||
|
|
||||||
buf = array.array('B', [0])
|
buf = array.array('B', [0])
|
||||||
ioctl(dev, 0x80016a12, buf) # JSIOCGBUTTONS
|
ioctl(dev, 0x80016A12, buf) # JSIOCGBUTTONS
|
||||||
num_buttons = buf[0]
|
num_buttons = buf[0]
|
||||||
|
|
||||||
# Get the axis map.
|
# Get the axis map.
|
||||||
buf = array.array('B', [0] * 0x40)
|
buf = array.array('B', [0] * 0x40)
|
||||||
ioctl(dev, 0x80406a32, buf) # JSIOCGAXMAP
|
ioctl(dev, 0x80406A32, buf) # JSIOCGAXMAP
|
||||||
|
|
||||||
for axis in buf[:num_axes]:
|
for axis in buf[:num_axes]:
|
||||||
axis_name = self.axis_names.get(axis, 'unknown(0x%02x)' % axis)
|
axis_name = self.axis_names.get(axis, 'unknown(0x%02x)' % axis)
|
||||||
|
@ -134,15 +130,21 @@ class JoystickLinuxBackend(Backend):
|
||||||
|
|
||||||
# Get the button map.
|
# Get the button map.
|
||||||
buf = array.array('H', [0] * 200)
|
buf = array.array('H', [0] * 200)
|
||||||
ioctl(dev, 0x80406a34, buf) # JSIOCGBTNMAP
|
ioctl(dev, 0x80406A34, buf) # JSIOCGBTNMAP
|
||||||
|
|
||||||
for btn in buf[:num_buttons]:
|
for btn in buf[:num_buttons]:
|
||||||
btn_name = self.button_names.get(btn, 'unknown(0x%03x)' % btn)
|
btn_name = self.button_names.get(btn, 'unknown(0x%03x)' % btn)
|
||||||
self._button_map.append(btn_name)
|
self._button_map.append(btn_name)
|
||||||
self._button_states[btn_name] = 0
|
self._button_states[btn_name] = 0
|
||||||
|
|
||||||
self.bus.post(JoystickConnectedEvent(device=self.device, name=js_name, axes=self._axis_map,
|
self.bus.post(
|
||||||
buttons=self._button_map))
|
JoystickConnectedEvent(
|
||||||
|
device=self.device,
|
||||||
|
name=js_name,
|
||||||
|
axes=self._axis_map,
|
||||||
|
buttons=self._button_map,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
super().run()
|
super().run()
|
||||||
|
@ -151,39 +153,54 @@ class JoystickLinuxBackend(Backend):
|
||||||
while not self.should_stop():
|
while not self.should_stop():
|
||||||
# Open the joystick device.
|
# Open the joystick device.
|
||||||
try:
|
try:
|
||||||
jsdev = open(self.device, 'rb')
|
jsdev = open(self.device, 'rb') # noqa
|
||||||
self._init_joystick(jsdev)
|
self._init_joystick(jsdev)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.debug(f'Joystick device on {self.device} not available: {e}')
|
self.logger.debug(
|
||||||
|
'Joystick device on %s not available: %s', self.device, e
|
||||||
|
)
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Joystick event loop
|
try:
|
||||||
while not self.should_stop():
|
# Joystick event loop
|
||||||
try:
|
while not self.should_stop():
|
||||||
evbuf = jsdev.read(8)
|
try:
|
||||||
if evbuf:
|
evbuf = jsdev.read(8)
|
||||||
_, value, evt_type, number = struct.unpack('IhBB', evbuf)
|
if evbuf:
|
||||||
|
_, value, evt_type, number = struct.unpack('IhBB', evbuf)
|
||||||
|
|
||||||
if evt_type & 0x80: # Initial state notification
|
if evt_type & 0x80: # Initial state notification
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if evt_type & 0x01:
|
if evt_type & 0x01:
|
||||||
button = self._button_map[number]
|
button = self._button_map[number]
|
||||||
if button:
|
if button:
|
||||||
self._button_states[button] = value
|
self._button_states[button] = value
|
||||||
evt_class = JoystickButtonPressedEvent if value else JoystickButtonReleasedEvent
|
evt_class = (
|
||||||
# noinspection PyTypeChecker
|
JoystickButtonPressedEvent
|
||||||
self.bus.post(evt_class(device=self.device, button=button))
|
if value
|
||||||
|
else JoystickButtonReleasedEvent
|
||||||
|
)
|
||||||
|
# noinspection PyTypeChecker
|
||||||
|
self.bus.post(
|
||||||
|
evt_class(device=self.device, button=button)
|
||||||
|
)
|
||||||
|
|
||||||
if evt_type & 0x02:
|
if evt_type & 0x02:
|
||||||
axis = self._axis_map[number]
|
axis = self._axis_map[number]
|
||||||
if axis:
|
if axis:
|
||||||
fvalue = value / 32767.0
|
fvalue = value / 32767.0
|
||||||
self._axis_states[axis] = fvalue
|
self._axis_states[axis] = fvalue
|
||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
self.bus.post(JoystickAxisEvent(device=self.device, axis=axis, value=fvalue))
|
self.bus.post(
|
||||||
except OSError as e:
|
JoystickAxisEvent(
|
||||||
self.logger.warning(f'Connection to {self.device} lost: {e}')
|
device=self.device, axis=axis, value=fvalue
|
||||||
self.bus.post(JoystickDisconnectedEvent(device=self.device))
|
)
|
||||||
break
|
)
|
||||||
|
except OSError as e:
|
||||||
|
self.logger.warning(f'Connection to {self.device} lost: {e}')
|
||||||
|
self.bus.post(JoystickDisconnectedEvent(device=self.device))
|
||||||
|
break
|
||||||
|
finally:
|
||||||
|
jsdev.close()
|
||||||
|
|
|
@ -11,10 +11,6 @@ class KafkaBackend(Backend):
|
||||||
"""
|
"""
|
||||||
Backend to interact with an Apache Kafka (https://kafka.apache.org/)
|
Backend to interact with an Apache Kafka (https://kafka.apache.org/)
|
||||||
streaming platform, send and receive messages.
|
streaming platform, send and receive messages.
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **kafka** (``pip install kafka-python``)
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_conn_retry_secs = 5
|
_conn_retry_secs = 5
|
||||||
|
@ -24,7 +20,9 @@ class KafkaBackend(Backend):
|
||||||
:param server: Kafka server name or address + port (default: ``localhost:9092``)
|
:param server: Kafka server name or address + port (default: ``localhost:9092``)
|
||||||
:type server: str
|
:type server: str
|
||||||
|
|
||||||
:param topic: (Prefix) topic to listen to (default: platypush). The Platypush device_id (by default the hostname) will be appended to the topic (the real topic name will e.g. be "platypush.my_rpi")
|
:param topic: (Prefix) topic to listen to (default: platypush). The
|
||||||
|
Platypush device_id (by default the hostname) will be appended to
|
||||||
|
the topic (the real topic name will e.g. be "platypush.my_rpi")
|
||||||
:type topic: str
|
:type topic: str
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -40,7 +38,8 @@ class KafkaBackend(Backend):
|
||||||
logging.getLogger('kafka').setLevel(logging.ERROR)
|
logging.getLogger('kafka').setLevel(logging.ERROR)
|
||||||
|
|
||||||
def _on_record(self, record):
|
def _on_record(self, record):
|
||||||
if record.topic != self.topic: return
|
if record.topic != self.topic:
|
||||||
|
return
|
||||||
msg = record.value.decode('utf-8')
|
msg = record.value.decode('utf-8')
|
||||||
is_platypush_message = False
|
is_platypush_message = False
|
||||||
|
|
||||||
|
@ -60,12 +59,12 @@ class KafkaBackend(Backend):
|
||||||
def _topic_by_device_id(self, device_id):
|
def _topic_by_device_id(self, device_id):
|
||||||
return '{}.{}'.format(self.topic_prefix, device_id)
|
return '{}.{}'.format(self.topic_prefix, device_id)
|
||||||
|
|
||||||
def send_message(self, msg, **kwargs):
|
def send_message(self, msg, **_):
|
||||||
target = msg.target
|
target = msg.target
|
||||||
kafka_plugin = get_plugin('kafka')
|
kafka_plugin = get_plugin('kafka')
|
||||||
kafka_plugin.send_message(msg=msg,
|
kafka_plugin.send_message(
|
||||||
topic=self._topic_by_device_id(target),
|
msg=msg, topic=self._topic_by_device_id(target), server=self.server
|
||||||
server=self.server)
|
)
|
||||||
|
|
||||||
def on_stop(self):
|
def on_stop(self):
|
||||||
super().on_stop()
|
super().on_stop()
|
||||||
|
@ -82,21 +81,29 @@ class KafkaBackend(Backend):
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
from kafka import KafkaConsumer
|
from kafka import KafkaConsumer
|
||||||
|
|
||||||
super().run()
|
super().run()
|
||||||
|
|
||||||
self.consumer = KafkaConsumer(self.topic, bootstrap_servers=self.server)
|
self.consumer = KafkaConsumer(self.topic, bootstrap_servers=self.server)
|
||||||
self.logger.info('Initialized kafka backend - server: {}, topic: {}'
|
self.logger.info(
|
||||||
.format(self.server, self.topic))
|
'Initialized kafka backend - server: {}, topic: {}'.format(
|
||||||
|
self.server, self.topic
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for msg in self.consumer:
|
for msg in self.consumer:
|
||||||
self._on_record(msg)
|
self._on_record(msg)
|
||||||
if self.should_stop(): break
|
if self.should_stop():
|
||||||
|
break
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning('Kafka connection error, reconnecting in {} seconds'.
|
self.logger.warning(
|
||||||
format(self._conn_retry_secs))
|
'Kafka connection error, reconnecting in {} seconds'.format(
|
||||||
|
self._conn_retry_secs
|
||||||
|
)
|
||||||
|
)
|
||||||
self.logger.exception(e)
|
self.logger.exception(e)
|
||||||
time.sleep(self._conn_retry_secs)
|
time.sleep(self._conn_retry_secs)
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
|
||||||
|
|
||||||
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
|
@ -7,7 +7,11 @@ from logging import getLogger
|
||||||
from threading import RLock
|
from threading import RLock
|
||||||
from typing import List, Optional, Iterable
|
from typing import List, Optional, Iterable
|
||||||
|
|
||||||
from platypush.backend.file.monitor import FileMonitorBackend, EventHandler, MonitoredResource
|
from platypush.backend.file.monitor import (
|
||||||
|
FileMonitorBackend,
|
||||||
|
EventHandler,
|
||||||
|
MonitoredResource,
|
||||||
|
)
|
||||||
from platypush.context import get_bus
|
from platypush.context import get_bus
|
||||||
from platypush.message.event.log.http import HttpLogEvent
|
from platypush.message.event.log.http import HttpLogEvent
|
||||||
|
|
||||||
|
@ -15,8 +19,10 @@ logger = getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class LogEventHandler(EventHandler):
|
class LogEventHandler(EventHandler):
|
||||||
http_line_regex = re.compile(r'^([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+\[([^]]+)]\s+"([^"]+)"\s+([\d]+)\s+'
|
http_line_regex = re.compile(
|
||||||
r'([\d]+)\s*("([^"\s]+)")?\s*("([^"]+)")?$')
|
r'^([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+\[([^]]+)]\s+"([^"]+)"\s+([\d]+)\s+'
|
||||||
|
r'([\d]+)\s*("([^"\s]+)")?\s*("([^"]+)")?$'
|
||||||
|
)
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class FileResource:
|
class FileResource:
|
||||||
|
@ -25,16 +31,17 @@ class LogEventHandler(EventHandler):
|
||||||
lock: RLock = RLock()
|
lock: RLock = RLock()
|
||||||
last_timestamp: Optional[datetime.datetime] = None
|
last_timestamp: Optional[datetime.datetime] = None
|
||||||
|
|
||||||
def __init__(self, *args, monitored_files: Optional[Iterable[str]] = None, **kwargs):
|
def __init__(
|
||||||
|
self, *args, monitored_files: Optional[Iterable[str]] = None, **kwargs
|
||||||
|
):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self._monitored_files = {}
|
self._monitored_files = {}
|
||||||
self.monitor_files(monitored_files or [])
|
self.monitor_files(monitored_files or [])
|
||||||
|
|
||||||
def monitor_files(self, files: Iterable[str]):
|
def monitor_files(self, files: Iterable[str]):
|
||||||
self._monitored_files.update({
|
self._monitored_files.update(
|
||||||
f: self.FileResource(path=f, pos=self._get_size(f))
|
{f: self.FileResource(path=f, pos=self._get_size(f)) for f in files}
|
||||||
for f in files
|
)
|
||||||
})
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_size(file: str) -> int:
|
def _get_size(file: str) -> int:
|
||||||
|
@ -68,12 +75,17 @@ class LogEventHandler(EventHandler):
|
||||||
try:
|
try:
|
||||||
file_size = os.path.getsize(event.src_path)
|
file_size = os.path.getsize(event.src_path)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
logger.warning('Could not get the size of {}: {}'.format(event.src_path, str(e)))
|
logger.warning(
|
||||||
|
'Could not get the size of {}: {}'.format(event.src_path, str(e))
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if file_info.pos > file_size:
|
if file_info.pos > file_size:
|
||||||
logger.warning('The size of {} been unexpectedly decreased from {} to {} bytes'.format(
|
logger.warning(
|
||||||
event.src_path, file_info.pos, file_size))
|
'The size of {} been unexpectedly decreased from {} to {} bytes'.format(
|
||||||
|
event.src_path, file_info.pos, file_size
|
||||||
|
)
|
||||||
|
)
|
||||||
file_info.pos = 0
|
file_info.pos = 0
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -81,13 +93,18 @@ class LogEventHandler(EventHandler):
|
||||||
f.seek(file_info.pos)
|
f.seek(file_info.pos)
|
||||||
for line in f.readlines():
|
for line in f.readlines():
|
||||||
evt = self._build_event(file=event.src_path, line=line)
|
evt = self._build_event(file=event.src_path, line=line)
|
||||||
if evt and (not file_info.last_timestamp or evt.args['time'] >= file_info.last_timestamp):
|
if evt and (
|
||||||
|
not file_info.last_timestamp
|
||||||
|
or evt.args['time'] >= file_info.last_timestamp
|
||||||
|
):
|
||||||
get_bus().post(evt)
|
get_bus().post(evt)
|
||||||
file_info.last_timestamp = evt.args['time']
|
file_info.last_timestamp = evt.args['time']
|
||||||
|
|
||||||
file_info.pos = f.tell()
|
file_info.pos = f.tell()
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
logger.warning('Error while reading from {}: {}'.format(self.resource.path, str(e)))
|
logger.warning(
|
||||||
|
'Error while reading from {}: {}'.format(self.resource.path, str(e))
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _build_event(cls, file: str, line: str) -> Optional[HttpLogEvent]:
|
def _build_event(cls, file: str, line: str) -> Optional[HttpLogEvent]:
|
||||||
|
@ -139,15 +156,6 @@ class LogHttpBackend(FileMonitorBackend):
|
||||||
"""
|
"""
|
||||||
This backend can be used to monitor one or more HTTP log files (tested on Apache and Nginx) and trigger events
|
This backend can be used to monitor one or more HTTP log files (tested on Apache and Nginx) and trigger events
|
||||||
whenever a new log line is added.
|
whenever a new log line is added.
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
* :class:`platypush.message.event.log.http.HttpLogEvent` when a new log line is created.
|
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **watchdog** (``pip install watchdog``)
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class EventHandlerFactory:
|
class EventHandlerFactory:
|
||||||
|
|
|
@ -60,14 +60,6 @@ class MailBackend(Backend):
|
||||||
|
|
||||||
It requires at least one plugin that extends :class:`platypush.plugins.mail.MailInPlugin` (e.g. ``mail.imap``) to
|
It requires at least one plugin that extends :class:`platypush.plugins.mail.MailInPlugin` (e.g. ``mail.imap``) to
|
||||||
be installed.
|
be installed.
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
- :class:`platypush.message.event.mail.MailReceivedEvent` when a new message is received.
|
|
||||||
- :class:`platypush.message.event.mail.MailSeenEvent` when a message is marked as seen.
|
|
||||||
- :class:`platypush.message.event.mail.MailFlaggedEvent` when a message is marked as flagged/starred.
|
|
||||||
- :class:`platypush.message.event.mail.MailUnflaggedEvent` when a message is marked as unflagged/unstarred.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|
|
@ -10,18 +10,16 @@ class MidiBackend(Backend):
|
||||||
"""
|
"""
|
||||||
This backend will listen for events from a MIDI device and post a
|
This backend will listen for events from a MIDI device and post a
|
||||||
MidiMessageEvent whenever a new MIDI event happens.
|
MidiMessageEvent whenever a new MIDI event happens.
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
* :class:`platypush.message.event.midi.MidiMessageEvent` when a new MIDI event is received
|
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **rtmidi** (``pip install rtmidi``)
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, device_name=None, port_number=None,
|
def __init__(
|
||||||
midi_throttle_time=None, *args, **kwargs):
|
self,
|
||||||
|
device_name=None,
|
||||||
|
port_number=None,
|
||||||
|
midi_throttle_time=None,
|
||||||
|
*args,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
:param device_name: Name of the MIDI device. *N.B.* either
|
:param device_name: Name of the MIDI device. *N.B.* either
|
||||||
`device_name` or `port_number` must be set.
|
`device_name` or `port_number` must be set.
|
||||||
|
@ -40,12 +38,16 @@ class MidiBackend(Backend):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import rtmidi
|
import rtmidi
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
if (device_name and port_number is not None) or \
|
if (device_name and port_number is not None) or (
|
||||||
(not device_name and port_number is None):
|
not device_name and port_number is None
|
||||||
raise RuntimeError('Either device_name or port_number (not both) ' +
|
):
|
||||||
'must be set in the MIDI backend configuration')
|
raise RuntimeError(
|
||||||
|
'Either device_name or port_number (not both) '
|
||||||
|
+ 'must be set in the MIDI backend configuration'
|
||||||
|
)
|
||||||
|
|
||||||
self.midi_throttle_time = midi_throttle_time
|
self.midi_throttle_time = midi_throttle_time
|
||||||
self.midi = rtmidi.MidiIn()
|
self.midi = rtmidi.MidiIn()
|
||||||
|
@ -75,9 +77,12 @@ class MidiBackend(Backend):
|
||||||
def _on_midi_message(self):
|
def _on_midi_message(self):
|
||||||
def flush_midi_message(message):
|
def flush_midi_message(message):
|
||||||
def _f():
|
def _f():
|
||||||
self.logger.info('Flushing throttled MIDI message {} to the bus'.format(message))
|
self.logger.info(
|
||||||
|
'Flushing throttled MIDI message {} to the bus'.format(message)
|
||||||
|
)
|
||||||
delay = time.time() - self.last_trigger_event_time
|
delay = time.time() - self.last_trigger_event_time
|
||||||
self.bus.post(MidiMessageEvent(message=message, delay=delay))
|
self.bus.post(MidiMessageEvent(message=message, delay=delay))
|
||||||
|
|
||||||
return _f
|
return _f
|
||||||
|
|
||||||
# noinspection PyUnusedLocal
|
# noinspection PyUnusedLocal
|
||||||
|
@ -95,8 +100,9 @@ class MidiBackend(Backend):
|
||||||
self.midi_flush_timeout.cancel()
|
self.midi_flush_timeout.cancel()
|
||||||
|
|
||||||
self.midi_flush_timeout = Timer(
|
self.midi_flush_timeout = Timer(
|
||||||
self.midi_throttle_time-event_delta,
|
self.midi_throttle_time - event_delta,
|
||||||
flush_midi_message(message))
|
flush_midi_message(message),
|
||||||
|
)
|
||||||
|
|
||||||
self.midi_flush_timeout.start()
|
self.midi_flush_timeout.start()
|
||||||
return
|
return
|
||||||
|
@ -110,8 +116,11 @@ class MidiBackend(Backend):
|
||||||
super().run()
|
super().run()
|
||||||
|
|
||||||
self.midi.open_port(self.port_number)
|
self.midi.open_port(self.port_number)
|
||||||
self.logger.info('Initialized MIDI backend, listening for events on device {}'.
|
self.logger.info(
|
||||||
format(self.device_name))
|
'Initialized MIDI backend, listening for events on device {}'.format(
|
||||||
|
self.device_name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
while not self.should_stop():
|
while not self.should_stop():
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -1,475 +0,0 @@
|
||||||
import hashlib
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import threading
|
|
||||||
from typing import Any, Dict, Optional, List, Callable
|
|
||||||
|
|
||||||
import paho.mqtt.client as mqtt
|
|
||||||
|
|
||||||
from platypush.backend import Backend
|
|
||||||
from platypush.config import Config
|
|
||||||
from platypush.context import get_plugin
|
|
||||||
from platypush.message import Message
|
|
||||||
from platypush.message.event.mqtt import MQTTMessageEvent
|
|
||||||
from platypush.message.request import Request
|
|
||||||
from platypush.plugins.mqtt import MqttPlugin as MQTTPlugin
|
|
||||||
|
|
||||||
|
|
||||||
class MqttClient(mqtt.Client, threading.Thread):
|
|
||||||
"""
|
|
||||||
Wrapper class for an MQTT client executed in a separate thread.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
*args,
|
|
||||||
host: str,
|
|
||||||
port: int,
|
|
||||||
topics: Optional[List[str]] = None,
|
|
||||||
on_message: Optional[Callable] = None,
|
|
||||||
username: Optional[str] = None,
|
|
||||||
password: Optional[str] = None,
|
|
||||||
client_id: Optional[str] = None,
|
|
||||||
tls_cafile: Optional[str] = None,
|
|
||||||
tls_certfile: Optional[str] = None,
|
|
||||||
tls_keyfile: Optional[str] = None,
|
|
||||||
tls_version=None,
|
|
||||||
tls_ciphers=None,
|
|
||||||
tls_insecure: bool = False,
|
|
||||||
keepalive: Optional[int] = 60,
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
mqtt.Client.__init__(self, *args, client_id=client_id, **kwargs)
|
|
||||||
threading.Thread.__init__(self)
|
|
||||||
|
|
||||||
self.name = f'MQTTClient:{client_id}'
|
|
||||||
self.host = host
|
|
||||||
self.port = port
|
|
||||||
self.topics = set(topics or [])
|
|
||||||
self.keepalive = keepalive
|
|
||||||
self.on_connect = self.connect_hndl()
|
|
||||||
|
|
||||||
if on_message:
|
|
||||||
self.on_message = on_message
|
|
||||||
|
|
||||||
if username and password:
|
|
||||||
self.username_pw_set(username, password)
|
|
||||||
|
|
||||||
if tls_cafile:
|
|
||||||
self.tls_set(
|
|
||||||
ca_certs=tls_cafile,
|
|
||||||
certfile=tls_certfile,
|
|
||||||
keyfile=tls_keyfile,
|
|
||||||
tls_version=tls_version,
|
|
||||||
ciphers=tls_ciphers,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.tls_insecure_set(tls_insecure)
|
|
||||||
|
|
||||||
self._running = False
|
|
||||||
self._stop_scheduled = False
|
|
||||||
|
|
||||||
def subscribe(self, *topics, **kwargs):
|
|
||||||
"""
|
|
||||||
Client subscription handler.
|
|
||||||
"""
|
|
||||||
if not topics:
|
|
||||||
topics = self.topics
|
|
||||||
|
|
||||||
self.topics.update(topics)
|
|
||||||
for topic in topics:
|
|
||||||
super().subscribe(topic, **kwargs)
|
|
||||||
|
|
||||||
def unsubscribe(self, *topics, **kwargs):
|
|
||||||
"""
|
|
||||||
Client unsubscribe handler.
|
|
||||||
"""
|
|
||||||
if not topics:
|
|
||||||
topics = self.topics
|
|
||||||
|
|
||||||
for topic in topics:
|
|
||||||
super().unsubscribe(topic, **kwargs)
|
|
||||||
self.topics.remove(topic)
|
|
||||||
|
|
||||||
def connect_hndl(self):
|
|
||||||
def handler(*_, **__):
|
|
||||||
self.subscribe()
|
|
||||||
|
|
||||||
return handler
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
super().run()
|
|
||||||
self.connect(host=self.host, port=self.port, keepalive=self.keepalive)
|
|
||||||
self._running = True
|
|
||||||
self.loop_forever()
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
if not self.is_alive():
|
|
||||||
return
|
|
||||||
|
|
||||||
self._stop_scheduled = True
|
|
||||||
self.disconnect()
|
|
||||||
self._running = False
|
|
||||||
|
|
||||||
|
|
||||||
class MqttBackend(Backend):
|
|
||||||
"""
|
|
||||||
Backend that reads messages from a configured MQTT topic (default:
|
|
||||||
``platypush_bus_mq/<device_id>``) and posts them to the application bus.
|
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
* :class:`platypush.message.event.mqtt.MQTTMessageEvent` when a new
|
|
||||||
message is received on one of the custom listeners
|
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **paho-mqtt** (``pip install paho-mqtt``)
|
|
||||||
"""
|
|
||||||
|
|
||||||
_default_mqtt_port = 1883
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
*args,
|
|
||||||
host: Optional[str] = None,
|
|
||||||
port: int = _default_mqtt_port,
|
|
||||||
topic: str = 'platypush_bus_mq',
|
|
||||||
subscribe_default_topic: bool = True,
|
|
||||||
tls_cafile: Optional[str] = None,
|
|
||||||
tls_certfile: Optional[str] = None,
|
|
||||||
tls_keyfile: Optional[str] = None,
|
|
||||||
tls_version: Optional[str] = None,
|
|
||||||
tls_ciphers: Optional[str] = None,
|
|
||||||
tls_insecure: bool = False,
|
|
||||||
username: Optional[str] = None,
|
|
||||||
password: Optional[str] = None,
|
|
||||||
client_id: Optional[str] = None,
|
|
||||||
listeners=None,
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
:param host: MQTT broker host. If no host configuration is specified then
|
|
||||||
the backend will use the host configuration specified on the ``mqtt``
|
|
||||||
plugin if it's available.
|
|
||||||
:param port: MQTT broker port (default: 1883)
|
|
||||||
:param topic: Topic to read messages from (default: ``platypush_bus_mq/<device_id>``)
|
|
||||||
:param subscribe_default_topic: Whether the backend should subscribe the default topic (default:
|
|
||||||
``platypush_bus_mq/<device_id>``) and execute the messages received there as action requests
|
|
||||||
(default: True).
|
|
||||||
:param tls_cafile: If TLS/SSL is enabled on the MQTT server and the certificate requires a certificate authority
|
|
||||||
to authenticate it, `ssl_cafile` will point to the provided ca.crt file (default: None)
|
|
||||||
:param tls_certfile: If TLS/SSL is enabled on the MQTT server and a client certificate it required, specify it
|
|
||||||
here (default: None)
|
|
||||||
:param tls_keyfile: If TLS/SSL is enabled on the MQTT server and a client certificate key it required,
|
|
||||||
specify it here (default: None) :type tls_keyfile: str
|
|
||||||
:param tls_version: If TLS/SSL is enabled on the MQTT server and it requires a certain TLS version, specify it
|
|
||||||
here (default: None). Supported versions: ``tls`` (automatic), ``tlsv1``, ``tlsv1.1``, ``tlsv1.2``.
|
|
||||||
:param tls_ciphers: If TLS/SSL is enabled on the MQTT server and an explicit list of supported ciphers is
|
|
||||||
required, specify it here (default: None)
|
|
||||||
:param tls_insecure: Set to True to ignore TLS insecure warnings (default: False).
|
|
||||||
:param username: Specify it if the MQTT server requires authentication (default: None)
|
|
||||||
:param password: Specify it if the MQTT server requires authentication (default: None)
|
|
||||||
:param client_id: ID used to identify the client on the MQTT server (default: None).
|
|
||||||
If None is specified then ``Config.get('device_id')`` will be used.
|
|
||||||
:param listeners: If specified then the MQTT backend will also listen for
|
|
||||||
messages on the additional configured message queues. This parameter
|
|
||||||
is a list of maps where each item supports the same arguments passed
|
|
||||||
to the main backend configuration (host, port, topic, password etc.).
|
|
||||||
Note that the message queue configured on the main configuration
|
|
||||||
will expect valid Platypush messages that then can execute, while
|
|
||||||
message queues registered to the listeners will accept any message. Example::
|
|
||||||
|
|
||||||
listeners:
|
|
||||||
- host: localhost
|
|
||||||
topics:
|
|
||||||
- topic1
|
|
||||||
- topic2
|
|
||||||
- topic3
|
|
||||||
- host: sensors
|
|
||||||
topics:
|
|
||||||
- topic4
|
|
||||||
- topic5
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
if host:
|
|
||||||
self.host = host
|
|
||||||
self.port = port
|
|
||||||
self.tls_cafile = self._expandpath(tls_cafile) if tls_cafile else None
|
|
||||||
self.tls_certfile = self._expandpath(tls_certfile) if tls_certfile else None
|
|
||||||
self.tls_keyfile = self._expandpath(tls_keyfile) if tls_keyfile else None
|
|
||||||
self.tls_version = MQTTPlugin.get_tls_version(tls_version)
|
|
||||||
self.tls_ciphers = tls_ciphers
|
|
||||||
self.tls_insecure = tls_insecure
|
|
||||||
self.username = username
|
|
||||||
self.password = password
|
|
||||||
self.client_id: str = client_id or Config.get('device_id')
|
|
||||||
else:
|
|
||||||
client = get_plugin('mqtt')
|
|
||||||
assert (
|
|
||||||
client.host
|
|
||||||
), 'No host specified on backend.mqtt nor mqtt configuration'
|
|
||||||
|
|
||||||
self.host = client.host
|
|
||||||
self.port = client.port
|
|
||||||
self.tls_cafile = client.tls_cafile
|
|
||||||
self.tls_certfile = client.tls_certfile
|
|
||||||
self.tls_keyfile = client.tls_keyfile
|
|
||||||
self.tls_version = client.tls_version
|
|
||||||
self.tls_ciphers = client.tls_ciphers
|
|
||||||
self.tls_insecure = client.tls_insecure
|
|
||||||
self.username = client.username
|
|
||||||
self.password = client.password
|
|
||||||
self.client_id = client_id or client.client_id
|
|
||||||
|
|
||||||
self.topic = f'{topic}/{self.device_id}'
|
|
||||||
self.subscribe_default_topic = subscribe_default_topic
|
|
||||||
self._listeners: Dict[str, MqttClient] = {} # client_id -> MqttClient map
|
|
||||||
self.listeners_conf = listeners or []
|
|
||||||
|
|
||||||
def send_message(self, msg, *_, topic: Optional[str] = None, **kwargs):
|
|
||||||
try:
|
|
||||||
client = get_plugin('mqtt')
|
|
||||||
client.send_message(
|
|
||||||
topic=topic or self.topic,
|
|
||||||
msg=msg,
|
|
||||||
host=self.host,
|
|
||||||
port=self.port,
|
|
||||||
username=self.username,
|
|
||||||
password=self.password,
|
|
||||||
tls_cafile=self.tls_cafile,
|
|
||||||
tls_certfile=self.tls_certfile,
|
|
||||||
tls_keyfile=self.tls_keyfile,
|
|
||||||
tls_version=self.tls_version,
|
|
||||||
tls_insecure=self.tls_insecure,
|
|
||||||
tls_ciphers=self.tls_ciphers,
|
|
||||||
**kwargs,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.exception(e)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _expandpath(path: str) -> str:
|
|
||||||
return os.path.abspath(os.path.expanduser(path)) if path else path
|
|
||||||
|
|
||||||
def add_listeners(self, *listeners):
|
|
||||||
for i, listener in enumerate(listeners):
|
|
||||||
host = listener.get('host', self.host)
|
|
||||||
port = listener.get('port', self.port)
|
|
||||||
username = listener.get('username', self.username)
|
|
||||||
password = listener.get('password', self.password)
|
|
||||||
tls_cafile = self._expandpath(listener.get('tls_cafile', self.tls_cafile))
|
|
||||||
tls_certfile = self._expandpath(
|
|
||||||
listener.get('tls_certfile', self.tls_certfile)
|
|
||||||
)
|
|
||||||
tls_keyfile = self._expandpath(
|
|
||||||
listener.get('tls_keyfile', self.tls_keyfile)
|
|
||||||
)
|
|
||||||
tls_version = MQTTPlugin.get_tls_version(
|
|
||||||
listener.get('tls_version', self.tls_version)
|
|
||||||
)
|
|
||||||
tls_ciphers = listener.get('tls_ciphers', self.tls_ciphers)
|
|
||||||
tls_insecure = listener.get('tls_insecure', self.tls_insecure)
|
|
||||||
topics = listener.get('topics')
|
|
||||||
|
|
||||||
if not topics:
|
|
||||||
self.logger.warning(
|
|
||||||
'No list of topics specified for listener n.%d', i + 1
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
client = self._get_client(
|
|
||||||
host=host,
|
|
||||||
port=port,
|
|
||||||
topics=topics,
|
|
||||||
username=username,
|
|
||||||
password=password,
|
|
||||||
client_id=self.client_id,
|
|
||||||
tls_cafile=tls_cafile,
|
|
||||||
tls_certfile=tls_certfile,
|
|
||||||
tls_keyfile=tls_keyfile,
|
|
||||||
tls_version=tls_version,
|
|
||||||
tls_ciphers=tls_ciphers,
|
|
||||||
tls_insecure=tls_insecure,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not client.is_alive():
|
|
||||||
client.start()
|
|
||||||
|
|
||||||
def _get_client_id(
|
|
||||||
self,
|
|
||||||
host: str,
|
|
||||||
port: int,
|
|
||||||
topics: Optional[List[str]] = None,
|
|
||||||
client_id: Optional[str] = None,
|
|
||||||
on_message: Optional[Callable[[MqttClient, Any, mqtt.MQTTMessage], Any]] = None,
|
|
||||||
) -> str:
|
|
||||||
client_id = client_id or self.client_id
|
|
||||||
client_hash = hashlib.sha1(
|
|
||||||
'|'.join(
|
|
||||||
[
|
|
||||||
host,
|
|
||||||
str(port),
|
|
||||||
json.dumps(sorted(topics or [])),
|
|
||||||
str(id(on_message)),
|
|
||||||
]
|
|
||||||
).encode()
|
|
||||||
).hexdigest()
|
|
||||||
|
|
||||||
return f'{client_id}-{client_hash}'
|
|
||||||
|
|
||||||
def _get_client(
|
|
||||||
self,
|
|
||||||
host: str,
|
|
||||||
port: int,
|
|
||||||
topics: Optional[List[str]] = None,
|
|
||||||
username: Optional[str] = None,
|
|
||||||
password: Optional[str] = None,
|
|
||||||
client_id: Optional[str] = None,
|
|
||||||
tls_cafile: Optional[str] = None,
|
|
||||||
tls_certfile: Optional[str] = None,
|
|
||||||
tls_keyfile: Optional[str] = None,
|
|
||||||
tls_version=None,
|
|
||||||
tls_ciphers=None,
|
|
||||||
tls_insecure: bool = False,
|
|
||||||
on_message: Optional[Callable] = None,
|
|
||||||
) -> MqttClient:
|
|
||||||
on_message = on_message or self.on_mqtt_message()
|
|
||||||
client_id = self._get_client_id(
|
|
||||||
host=host,
|
|
||||||
port=port,
|
|
||||||
topics=topics,
|
|
||||||
client_id=client_id,
|
|
||||||
on_message=on_message,
|
|
||||||
)
|
|
||||||
client = self._listeners.get(client_id)
|
|
||||||
|
|
||||||
if not (client and client.is_alive()):
|
|
||||||
client = self._listeners[client_id] = MqttClient(
|
|
||||||
host=host,
|
|
||||||
port=port,
|
|
||||||
topics=topics,
|
|
||||||
username=username,
|
|
||||||
password=password,
|
|
||||||
client_id=client_id,
|
|
||||||
tls_cafile=tls_cafile,
|
|
||||||
tls_certfile=tls_certfile,
|
|
||||||
tls_keyfile=tls_keyfile,
|
|
||||||
tls_version=tls_version,
|
|
||||||
tls_ciphers=tls_ciphers,
|
|
||||||
tls_insecure=tls_insecure,
|
|
||||||
on_message=on_message,
|
|
||||||
)
|
|
||||||
|
|
||||||
if topics:
|
|
||||||
client.subscribe(*topics)
|
|
||||||
|
|
||||||
return client
|
|
||||||
|
|
||||||
def on_mqtt_message(self):
|
|
||||||
def handler(client: MqttClient, _, msg: mqtt.MQTTMessage):
|
|
||||||
data = msg.payload
|
|
||||||
try:
|
|
||||||
data = data.decode('utf-8')
|
|
||||||
data = json.loads(data)
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.debug(str(e))
|
|
||||||
|
|
||||||
self.bus.post(
|
|
||||||
MQTTMessageEvent(
|
|
||||||
host=client.host, port=client.port, topic=msg.topic, msg=data
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return handler
|
|
||||||
|
|
||||||
def on_exec_message(self):
|
|
||||||
def handler(_, __, msg: mqtt.MQTTMessage):
|
|
||||||
def response_thread(msg):
|
|
||||||
response = self.get_message_response(msg)
|
|
||||||
if not response:
|
|
||||||
return
|
|
||||||
response_topic = f'{self.topic}/responses/{msg.id}'
|
|
||||||
|
|
||||||
self.logger.info(
|
|
||||||
'Processing response on the MQTT topic %s: %s',
|
|
||||||
response_topic,
|
|
||||||
response,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.send_message(response, topic=response_topic)
|
|
||||||
|
|
||||||
msg = msg.payload.decode('utf-8')
|
|
||||||
try:
|
|
||||||
msg = json.loads(msg)
|
|
||||||
msg = Message.build(msg)
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.debug(str(e))
|
|
||||||
|
|
||||||
if not msg:
|
|
||||||
return
|
|
||||||
|
|
||||||
self.logger.info('Received message on the MQTT backend: %s', msg)
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.on_message(msg)
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.exception(e)
|
|
||||||
return
|
|
||||||
|
|
||||||
if isinstance(msg, Request):
|
|
||||||
threading.Thread(
|
|
||||||
target=response_thread,
|
|
||||||
name='MQTTProcessorResponseThread',
|
|
||||||
args=(msg,),
|
|
||||||
).start()
|
|
||||||
|
|
||||||
return handler
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
super().run()
|
|
||||||
|
|
||||||
if self.host and self.subscribe_default_topic:
|
|
||||||
topics = [self.topic]
|
|
||||||
client = self._get_client(
|
|
||||||
host=self.host,
|
|
||||||
port=self.port,
|
|
||||||
topics=topics,
|
|
||||||
username=self.username,
|
|
||||||
password=self.password,
|
|
||||||
client_id=self.client_id,
|
|
||||||
tls_cafile=self.tls_cafile,
|
|
||||||
tls_certfile=self.tls_certfile,
|
|
||||||
tls_keyfile=self.tls_keyfile,
|
|
||||||
tls_version=self.tls_version,
|
|
||||||
tls_ciphers=self.tls_ciphers,
|
|
||||||
tls_insecure=self.tls_insecure,
|
|
||||||
on_message=self.on_exec_message(),
|
|
||||||
)
|
|
||||||
|
|
||||||
client.start()
|
|
||||||
self.logger.info(
|
|
||||||
'Initialized MQTT backend on host %s:%d, topic=%s',
|
|
||||||
self.host,
|
|
||||||
self.port,
|
|
||||||
self.topic,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.add_listeners(*self.listeners_conf)
|
|
||||||
|
|
||||||
def on_stop(self):
|
|
||||||
self.logger.info('Received STOP event on the MQTT backend')
|
|
||||||
|
|
||||||
for listener in self._listeners.values():
|
|
||||||
try:
|
|
||||||
listener.stop()
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.warning('Could not stop MQTT listener: %s', e)
|
|
||||||
|
|
||||||
self.logger.info('MQTT backend terminated')
|
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
|
|
@ -1,17 +0,0 @@
|
||||||
manifest:
|
|
||||||
events:
|
|
||||||
platypush.message.event.mqtt.MQTTMessageEvent: when a newmessage is received on
|
|
||||||
one of the custom listeners
|
|
||||||
install:
|
|
||||||
apk:
|
|
||||||
- py3-paho-mqtt
|
|
||||||
dnf:
|
|
||||||
- python-paho-mqtt
|
|
||||||
pacman:
|
|
||||||
- python-paho-mqtt
|
|
||||||
apt:
|
|
||||||
- python3-paho-mqtt
|
|
||||||
pip:
|
|
||||||
- paho-mqtt
|
|
||||||
package: platypush.backend.mqtt
|
|
||||||
type: backend
|
|
|
@ -5,11 +5,20 @@ import threading
|
||||||
import websocket
|
import websocket
|
||||||
|
|
||||||
from platypush.backend import Backend
|
from platypush.backend import Backend
|
||||||
from platypush.message.event.music import MusicPlayEvent, MusicPauseEvent, \
|
from platypush.message.event.music import (
|
||||||
MusicStopEvent, NewPlayingTrackEvent, PlaylistChangeEvent, VolumeChangeEvent, \
|
MusicPlayEvent,
|
||||||
PlaybackConsumeModeChangeEvent, PlaybackSingleModeChangeEvent, \
|
MusicPauseEvent,
|
||||||
PlaybackRepeatModeChangeEvent, PlaybackRandomModeChangeEvent, \
|
MusicStopEvent,
|
||||||
MuteChangeEvent, SeekChangeEvent
|
NewPlayingTrackEvent,
|
||||||
|
PlaylistChangeEvent,
|
||||||
|
VolumeChangeEvent,
|
||||||
|
PlaybackConsumeModeChangeEvent,
|
||||||
|
PlaybackSingleModeChangeEvent,
|
||||||
|
PlaybackRepeatModeChangeEvent,
|
||||||
|
PlaybackRandomModeChangeEvent,
|
||||||
|
MuteChangeEvent,
|
||||||
|
SeekChangeEvent,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# noinspection PyUnusedLocal
|
# noinspection PyUnusedLocal
|
||||||
|
@ -22,20 +31,10 @@ class MusicMopidyBackend(Backend):
|
||||||
solution if you're not running Mopidy or your instance has the websocket
|
solution if you're not running Mopidy or your instance has the websocket
|
||||||
interface or web port disabled.
|
interface or web port disabled.
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
* :class:`platypush.message.event.music.MusicPlayEvent` if the playback state changed to play
|
|
||||||
* :class:`platypush.message.event.music.MusicPauseEvent` if the playback state changed to pause
|
|
||||||
* :class:`platypush.message.event.music.MusicStopEvent` if the playback state changed to stop
|
|
||||||
* :class:`platypush.message.event.music.NewPlayingTrackEvent` if a new track is being played
|
|
||||||
* :class:`platypush.message.event.music.PlaylistChangeEvent` if the main playlist has changed
|
|
||||||
* :class:`platypush.message.event.music.VolumeChangeEvent` if the main volume has changed
|
|
||||||
* :class:`platypush.message.event.music.MuteChangeEvent` if the mute status has changed
|
|
||||||
* :class:`platypush.message.event.music.SeekChangeEvent` if a track seek event occurs
|
|
||||||
|
|
||||||
Requires:
|
Requires:
|
||||||
|
|
||||||
* Mopidy installed and the HTTP service enabled
|
* A Mopidy instance running with the HTTP service enabled.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, host='localhost', port=6680, **kwargs):
|
def __init__(self, host='localhost', port=6680, **kwargs):
|
||||||
|
@ -77,8 +76,11 @@ class MusicMopidyBackend(Backend):
|
||||||
conv_track['album'] = conv_track['album']['name']
|
conv_track['album'] = conv_track['album']['name']
|
||||||
|
|
||||||
if 'length' in conv_track:
|
if 'length' in conv_track:
|
||||||
conv_track['time'] = conv_track['length']/1000 \
|
conv_track['time'] = (
|
||||||
if conv_track['length'] else conv_track['length']
|
conv_track['length'] / 1000
|
||||||
|
if conv_track['length']
|
||||||
|
else conv_track['length']
|
||||||
|
)
|
||||||
del conv_track['length']
|
del conv_track['length']
|
||||||
|
|
||||||
if pos is not None:
|
if pos is not None:
|
||||||
|
@ -90,7 +92,6 @@ class MusicMopidyBackend(Backend):
|
||||||
return conv_track
|
return conv_track
|
||||||
|
|
||||||
def _communicate(self, msg):
|
def _communicate(self, msg):
|
||||||
|
|
||||||
if isinstance(msg, str):
|
if isinstance(msg, str):
|
||||||
msg = json.loads(msg)
|
msg = json.loads(msg)
|
||||||
|
|
||||||
|
@ -107,14 +108,10 @@ class MusicMopidyBackend(Backend):
|
||||||
|
|
||||||
def _get_tracklist_status(self):
|
def _get_tracklist_status(self):
|
||||||
return {
|
return {
|
||||||
'repeat': self._communicate({
|
'repeat': self._communicate({'method': 'core.tracklist.get_repeat'}),
|
||||||
'method': 'core.tracklist.get_repeat'}),
|
'random': self._communicate({'method': 'core.tracklist.get_random'}),
|
||||||
'random': self._communicate({
|
'single': self._communicate({'method': 'core.tracklist.get_single'}),
|
||||||
'method': 'core.tracklist.get_random'}),
|
'consume': self._communicate({'method': 'core.tracklist.get_consume'}),
|
||||||
'single': self._communicate({
|
|
||||||
'method': 'core.tracklist.get_single'}),
|
|
||||||
'consume': self._communicate({
|
|
||||||
'method': 'core.tracklist.get_consume'}),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def _on_msg(self):
|
def _on_msg(self):
|
||||||
|
@ -133,19 +130,25 @@ class MusicMopidyBackend(Backend):
|
||||||
track = self._parse_track(track)
|
track = self._parse_track(track)
|
||||||
if not track:
|
if not track:
|
||||||
return
|
return
|
||||||
self.bus.post(MusicPauseEvent(status=status, track=track, plugin_name='music.mpd'))
|
self.bus.post(
|
||||||
|
MusicPauseEvent(status=status, track=track, plugin_name='music.mpd')
|
||||||
|
)
|
||||||
elif event == 'track_playback_resumed':
|
elif event == 'track_playback_resumed':
|
||||||
status['state'] = 'play'
|
status['state'] = 'play'
|
||||||
track = self._parse_track(track)
|
track = self._parse_track(track)
|
||||||
if not track:
|
if not track:
|
||||||
return
|
return
|
||||||
self.bus.post(MusicPlayEvent(status=status, track=track, plugin_name='music.mpd'))
|
self.bus.post(
|
||||||
|
MusicPlayEvent(status=status, track=track, plugin_name='music.mpd')
|
||||||
|
)
|
||||||
elif event == 'track_playback_ended' or (
|
elif event == 'track_playback_ended' or (
|
||||||
event == 'playback_state_changed'
|
event == 'playback_state_changed' and msg.get('new_state') == 'stopped'
|
||||||
and msg.get('new_state') == 'stopped'):
|
):
|
||||||
status['state'] = 'stop'
|
status['state'] = 'stop'
|
||||||
track = self._parse_track(track)
|
track = self._parse_track(track)
|
||||||
self.bus.post(MusicStopEvent(status=status, track=track, plugin_name='music.mpd'))
|
self.bus.post(
|
||||||
|
MusicStopEvent(status=status, track=track, plugin_name='music.mpd')
|
||||||
|
)
|
||||||
elif event == 'track_playback_started':
|
elif event == 'track_playback_started':
|
||||||
track = self._parse_track(track)
|
track = self._parse_track(track)
|
||||||
if not track:
|
if not track:
|
||||||
|
@ -154,9 +157,13 @@ class MusicMopidyBackend(Backend):
|
||||||
status['state'] = 'play'
|
status['state'] = 'play'
|
||||||
status['position'] = 0.0
|
status['position'] = 0.0
|
||||||
status['time'] = track.get('time')
|
status['time'] = track.get('time')
|
||||||
self.bus.post(NewPlayingTrackEvent(status=status, track=track, plugin_name='music.mpd'))
|
self.bus.post(
|
||||||
|
NewPlayingTrackEvent(
|
||||||
|
status=status, track=track, plugin_name='music.mpd'
|
||||||
|
)
|
||||||
|
)
|
||||||
elif event == 'stream_title_changed':
|
elif event == 'stream_title_changed':
|
||||||
m = re.match('^\s*(.+?)\s+-\s+(.*)\s*$', msg.get('title', ''))
|
m = re.match(r'^\s*(.+?)\s+-\s+(.*)\s*$', msg.get('title', ''))
|
||||||
if not m:
|
if not m:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -164,35 +171,78 @@ class MusicMopidyBackend(Backend):
|
||||||
track['title'] = m.group(2)
|
track['title'] = m.group(2)
|
||||||
status['state'] = 'play'
|
status['state'] = 'play'
|
||||||
status['position'] = 0.0
|
status['position'] = 0.0
|
||||||
self.bus.post(NewPlayingTrackEvent(status=status, track=track, plugin_name='music.mpd'))
|
self.bus.post(
|
||||||
|
NewPlayingTrackEvent(
|
||||||
|
status=status, track=track, plugin_name='music.mpd'
|
||||||
|
)
|
||||||
|
)
|
||||||
elif event == 'volume_changed':
|
elif event == 'volume_changed':
|
||||||
status['volume'] = msg.get('volume')
|
status['volume'] = msg.get('volume')
|
||||||
self.bus.post(VolumeChangeEvent(volume=status['volume'], status=status, track=track,
|
self.bus.post(
|
||||||
plugin_name='music.mpd'))
|
VolumeChangeEvent(
|
||||||
|
volume=status['volume'],
|
||||||
|
status=status,
|
||||||
|
track=track,
|
||||||
|
plugin_name='music.mpd',
|
||||||
|
)
|
||||||
|
)
|
||||||
elif event == 'mute_changed':
|
elif event == 'mute_changed':
|
||||||
status['mute'] = msg.get('mute')
|
status['mute'] = msg.get('mute')
|
||||||
self.bus.post(MuteChangeEvent(mute=status['mute'], status=status, track=track,
|
self.bus.post(
|
||||||
plugin_name='music.mpd'))
|
MuteChangeEvent(
|
||||||
|
mute=status['mute'],
|
||||||
|
status=status,
|
||||||
|
track=track,
|
||||||
|
plugin_name='music.mpd',
|
||||||
|
)
|
||||||
|
)
|
||||||
elif event == 'seeked':
|
elif event == 'seeked':
|
||||||
status['position'] = msg.get('time_position')/1000
|
status['position'] = msg.get('time_position') / 1000
|
||||||
self.bus.post(SeekChangeEvent(position=status['position'], status=status, track=track,
|
self.bus.post(
|
||||||
plugin_name='music.mpd'))
|
SeekChangeEvent(
|
||||||
|
position=status['position'],
|
||||||
|
status=status,
|
||||||
|
track=track,
|
||||||
|
plugin_name='music.mpd',
|
||||||
|
)
|
||||||
|
)
|
||||||
elif event == 'tracklist_changed':
|
elif event == 'tracklist_changed':
|
||||||
tracklist = [self._parse_track(t, pos=i)
|
tracklist = [
|
||||||
for i, t in enumerate(self._communicate({
|
self._parse_track(t, pos=i)
|
||||||
'method': 'core.tracklist.get_tl_tracks'}))]
|
for i, t in enumerate(
|
||||||
|
self._communicate({'method': 'core.tracklist.get_tl_tracks'})
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
self.bus.post(PlaylistChangeEvent(changes=tracklist, plugin_name='music.mpd'))
|
self.bus.post(
|
||||||
|
PlaylistChangeEvent(changes=tracklist, plugin_name='music.mpd')
|
||||||
|
)
|
||||||
elif event == 'options_changed':
|
elif event == 'options_changed':
|
||||||
new_status = self._get_tracklist_status()
|
new_status = self._get_tracklist_status()
|
||||||
if new_status['random'] != self._latest_status.get('random'):
|
if new_status['random'] != self._latest_status.get('random'):
|
||||||
self.bus.post(PlaybackRandomModeChangeEvent(state=new_status['random'], plugin_name='music.mpd'))
|
self.bus.post(
|
||||||
|
PlaybackRandomModeChangeEvent(
|
||||||
|
state=new_status['random'], plugin_name='music.mpd'
|
||||||
|
)
|
||||||
|
)
|
||||||
if new_status['repeat'] != self._latest_status['repeat']:
|
if new_status['repeat'] != self._latest_status['repeat']:
|
||||||
self.bus.post(PlaybackRepeatModeChangeEvent(state=new_status['repeat'], plugin_name='music.mpd'))
|
self.bus.post(
|
||||||
|
PlaybackRepeatModeChangeEvent(
|
||||||
|
state=new_status['repeat'], plugin_name='music.mpd'
|
||||||
|
)
|
||||||
|
)
|
||||||
if new_status['single'] != self._latest_status['single']:
|
if new_status['single'] != self._latest_status['single']:
|
||||||
self.bus.post(PlaybackSingleModeChangeEvent(state=new_status['single'], plugin_name='music.mpd'))
|
self.bus.post(
|
||||||
|
PlaybackSingleModeChangeEvent(
|
||||||
|
state=new_status['single'], plugin_name='music.mpd'
|
||||||
|
)
|
||||||
|
)
|
||||||
if new_status['consume'] != self._latest_status['consume']:
|
if new_status['consume'] != self._latest_status['consume']:
|
||||||
self.bus.post(PlaybackConsumeModeChangeEvent(state=new_status['consume'], plugin_name='music.mpd'))
|
self.bus.post(
|
||||||
|
PlaybackConsumeModeChangeEvent(
|
||||||
|
state=new_status['consume'], plugin_name='music.mpd'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
self._latest_status = new_status
|
self._latest_status = new_status
|
||||||
|
|
||||||
|
@ -204,7 +254,7 @@ class MusicMopidyBackend(Backend):
|
||||||
try:
|
try:
|
||||||
self._connect()
|
self._connect()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning('Error on websocket reconnection: '.format(str(e)))
|
self.logger.warning('Error on websocket reconnection: %s', e)
|
||||||
|
|
||||||
self._connected_event.wait(timeout=10)
|
self._connected_event.wait(timeout=10)
|
||||||
|
|
||||||
|
@ -244,17 +294,23 @@ class MusicMopidyBackend(Backend):
|
||||||
|
|
||||||
def _connect(self):
|
def _connect(self):
|
||||||
if not self._ws:
|
if not self._ws:
|
||||||
self._ws = websocket.WebSocketApp(self.url,
|
self._ws = websocket.WebSocketApp(
|
||||||
on_open=self._on_open(),
|
self.url,
|
||||||
on_message=self._on_msg(),
|
on_open=self._on_open(),
|
||||||
on_error=self._on_error(),
|
on_message=self._on_msg(),
|
||||||
on_close=self._on_close())
|
on_error=self._on_error(),
|
||||||
|
on_close=self._on_close(),
|
||||||
|
)
|
||||||
|
|
||||||
self._ws.run_forever()
|
self._ws.run_forever()
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
super().run()
|
super().run()
|
||||||
self.logger.info('Started tracking Mopidy events backend on {}:{}'.format(self.host, self.port))
|
self.logger.info(
|
||||||
|
'Started tracking Mopidy events backend on {}:{}'.format(
|
||||||
|
self.host, self.port
|
||||||
|
)
|
||||||
|
)
|
||||||
self._connect()
|
self._connect()
|
||||||
|
|
||||||
def on_stop(self):
|
def on_stop(self):
|
||||||
|
|
|
@ -2,28 +2,28 @@ import time
|
||||||
|
|
||||||
from platypush.backend import Backend
|
from platypush.backend import Backend
|
||||||
from platypush.context import get_plugin
|
from platypush.context import get_plugin
|
||||||
from platypush.message.event.music import MusicPlayEvent, MusicPauseEvent, \
|
from platypush.message.event.music import (
|
||||||
MusicStopEvent, NewPlayingTrackEvent, PlaylistChangeEvent, VolumeChangeEvent, \
|
MusicPlayEvent,
|
||||||
PlaybackConsumeModeChangeEvent, PlaybackSingleModeChangeEvent, \
|
MusicPauseEvent,
|
||||||
PlaybackRepeatModeChangeEvent, PlaybackRandomModeChangeEvent
|
MusicStopEvent,
|
||||||
|
NewPlayingTrackEvent,
|
||||||
|
PlaylistChangeEvent,
|
||||||
|
VolumeChangeEvent,
|
||||||
|
PlaybackConsumeModeChangeEvent,
|
||||||
|
PlaybackSingleModeChangeEvent,
|
||||||
|
PlaybackRepeatModeChangeEvent,
|
||||||
|
PlaybackRandomModeChangeEvent,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class MusicMpdBackend(Backend):
|
class MusicMpdBackend(Backend):
|
||||||
"""
|
"""
|
||||||
This backend listens for events on a MPD/Mopidy music server.
|
This backend listens for events on a MPD/Mopidy music server.
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
* :class:`platypush.message.event.music.MusicPlayEvent` if the playback state changed to play
|
|
||||||
* :class:`platypush.message.event.music.MusicPauseEvent` if the playback state changed to pause
|
|
||||||
* :class:`platypush.message.event.music.MusicStopEvent` if the playback state changed to stop
|
|
||||||
* :class:`platypush.message.event.music.NewPlayingTrackEvent` if a new track is being played
|
|
||||||
* :class:`platypush.message.event.music.PlaylistChangeEvent` if the main playlist has changed
|
|
||||||
* :class:`platypush.message.event.music.VolumeChangeEvent` if the main volume has changed
|
|
||||||
|
|
||||||
Requires:
|
Requires:
|
||||||
* **python-mpd2** (``pip install python-mpd2``)
|
|
||||||
* The :mod:`platypush.plugins.music.mpd` plugin to be configured
|
* :class:`platypush.plugins.music.mpd.MusicMpdPlugin` configured
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, server='localhost', port=6600, poll_seconds=3, **kwargs):
|
def __init__(self, server='localhost', port=6600, poll_seconds=3, **kwargs):
|
||||||
|
@ -81,11 +81,23 @@ class MusicMpdBackend(Backend):
|
||||||
|
|
||||||
if state != last_state:
|
if state != last_state:
|
||||||
if state == 'stop':
|
if state == 'stop':
|
||||||
self.bus.post(MusicStopEvent(status=status, track=track, plugin_name='music.mpd'))
|
self.bus.post(
|
||||||
|
MusicStopEvent(
|
||||||
|
status=status, track=track, plugin_name='music.mpd'
|
||||||
|
)
|
||||||
|
)
|
||||||
elif state == 'pause':
|
elif state == 'pause':
|
||||||
self.bus.post(MusicPauseEvent(status=status, track=track, plugin_name='music.mpd'))
|
self.bus.post(
|
||||||
|
MusicPauseEvent(
|
||||||
|
status=status, track=track, plugin_name='music.mpd'
|
||||||
|
)
|
||||||
|
)
|
||||||
elif state == 'play':
|
elif state == 'play':
|
||||||
self.bus.post(MusicPlayEvent(status=status, track=track, plugin_name='music.mpd'))
|
self.bus.post(
|
||||||
|
MusicPlayEvent(
|
||||||
|
status=status, track=track, plugin_name='music.mpd'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if playlist != last_playlist:
|
if playlist != last_playlist:
|
||||||
if last_playlist:
|
if last_playlist:
|
||||||
|
@ -97,31 +109,66 @@ class MusicMpdBackend(Backend):
|
||||||
last_playlist = playlist
|
last_playlist = playlist
|
||||||
|
|
||||||
if state == 'play' and track != last_track:
|
if state == 'play' and track != last_track:
|
||||||
self.bus.post(NewPlayingTrackEvent(status=status, track=track, plugin_name='music.mpd'))
|
self.bus.post(
|
||||||
|
NewPlayingTrackEvent(
|
||||||
|
status=status, track=track, plugin_name='music.mpd'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if last_status.get('volume', None) != status['volume']:
|
if last_status.get('volume') != status['volume']:
|
||||||
self.bus.post(VolumeChangeEvent(volume=int(status['volume']), status=status, track=track,
|
self.bus.post(
|
||||||
plugin_name='music.mpd'))
|
VolumeChangeEvent(
|
||||||
|
volume=int(status['volume']),
|
||||||
|
status=status,
|
||||||
|
track=track,
|
||||||
|
plugin_name='music.mpd',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if last_status.get('random', None) != status['random']:
|
if last_status.get('random') != status['random']:
|
||||||
self.bus.post(PlaybackRandomModeChangeEvent(state=bool(int(status['random'])), status=status,
|
self.bus.post(
|
||||||
track=track, plugin_name='music.mpd'))
|
PlaybackRandomModeChangeEvent(
|
||||||
|
state=bool(int(status['random'])),
|
||||||
|
status=status,
|
||||||
|
track=track,
|
||||||
|
plugin_name='music.mpd',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if last_status.get('repeat', None) != status['repeat']:
|
if last_status.get('repeat') != status['repeat']:
|
||||||
self.bus.post(PlaybackRepeatModeChangeEvent(state=bool(int(status['repeat'])), status=status,
|
self.bus.post(
|
||||||
track=track, plugin_name='music.mpd'))
|
PlaybackRepeatModeChangeEvent(
|
||||||
|
state=bool(int(status['repeat'])),
|
||||||
|
status=status,
|
||||||
|
track=track,
|
||||||
|
plugin_name='music.mpd',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if last_status.get('consume', None) != status['consume']:
|
if last_status.get('consume') != status['consume']:
|
||||||
self.bus.post(PlaybackConsumeModeChangeEvent(state=bool(int(status['consume'])), status=status,
|
self.bus.post(
|
||||||
track=track, plugin_name='music.mpd'))
|
PlaybackConsumeModeChangeEvent(
|
||||||
|
state=bool(int(status['consume'])),
|
||||||
|
status=status,
|
||||||
|
track=track,
|
||||||
|
plugin_name='music.mpd',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if last_status.get('single', None) != status['single']:
|
if last_status.get('single') != status['single']:
|
||||||
self.bus.post(PlaybackSingleModeChangeEvent(state=bool(int(status['single'])), status=status,
|
self.bus.post(
|
||||||
track=track, plugin_name='music.mpd'))
|
PlaybackSingleModeChangeEvent(
|
||||||
|
state=bool(int(status['single'])),
|
||||||
|
status=status,
|
||||||
|
track=track,
|
||||||
|
plugin_name='music.mpd',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
last_status = status
|
last_status = status
|
||||||
last_state = state
|
last_state = state
|
||||||
last_track = track
|
last_track = track
|
||||||
time.sleep(self.poll_seconds)
|
time.sleep(self.poll_seconds)
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
|
@ -21,19 +21,7 @@ from platypush.message.event.music.snapcast import (
|
||||||
class MusicSnapcastBackend(Backend):
|
class MusicSnapcastBackend(Backend):
|
||||||
"""
|
"""
|
||||||
Backend that listens for notification and status changes on one or more
|
Backend that listens for notification and status changes on one or more
|
||||||
[Snapcast](https://github.com/badaix/snapcast) servers.
|
`Snapcast <https://github.com/badaix/snapcast>`_ servers.
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
* :class:`platypush.message.event.music.snapcast.ClientConnectedEvent`
|
|
||||||
* :class:`platypush.message.event.music.snapcast.ClientDisconnectedEvent`
|
|
||||||
* :class:`platypush.message.event.music.snapcast.ClientVolumeChangeEvent`
|
|
||||||
* :class:`platypush.message.event.music.snapcast.ClientLatencyChangeEvent`
|
|
||||||
* :class:`platypush.message.event.music.snapcast.ClientNameChangeEvent`
|
|
||||||
* :class:`platypush.message.event.music.snapcast.GroupMuteChangeEvent`
|
|
||||||
* :class:`platypush.message.event.music.snapcast.GroupStreamChangeEvent`
|
|
||||||
* :class:`platypush.message.event.music.snapcast.StreamUpdateEvent`
|
|
||||||
* :class:`platypush.message.event.music.snapcast.ServerUpdateEvent`
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_DEFAULT_SNAPCAST_PORT = 1705
|
_DEFAULT_SNAPCAST_PORT = 1705
|
||||||
|
|
|
@ -7,8 +7,14 @@ from typing import Optional, Dict, Any
|
||||||
from platypush.backend import Backend
|
from platypush.backend import Backend
|
||||||
from platypush.common.spotify import SpotifyMixin
|
from platypush.common.spotify import SpotifyMixin
|
||||||
from platypush.config import Config
|
from platypush.config import Config
|
||||||
from platypush.message.event.music import MusicPlayEvent, MusicPauseEvent, MusicStopEvent, \
|
from platypush.message.event.music import (
|
||||||
NewPlayingTrackEvent, SeekChangeEvent, VolumeChangeEvent
|
MusicPlayEvent,
|
||||||
|
MusicPauseEvent,
|
||||||
|
MusicStopEvent,
|
||||||
|
NewPlayingTrackEvent,
|
||||||
|
SeekChangeEvent,
|
||||||
|
VolumeChangeEvent,
|
||||||
|
)
|
||||||
from platypush.utils import get_redis
|
from platypush.utils import get_redis
|
||||||
|
|
||||||
from .event import status_queue
|
from .event import status_queue
|
||||||
|
@ -21,53 +27,47 @@ class MusicSpotifyBackend(Backend, SpotifyMixin):
|
||||||
stream Spotify through the Platypush host. After the backend has started, you should see a new entry in the
|
stream Spotify through the Platypush host. After the backend has started, you should see a new entry in the
|
||||||
Spotify Connect devices list in your app.
|
Spotify Connect devices list in your app.
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
* :class:`platypush.message.event.music.MusicPlayEvent` if the playback state changed to play
|
|
||||||
* :class:`platypush.message.event.music.MusicPauseEvent` if the playback state changed to pause
|
|
||||||
* :class:`platypush.message.event.music.MusicStopEvent` if the playback state changed to stop
|
|
||||||
* :class:`platypush.message.event.music.NewPlayingTrackEvent` if a new track is being played
|
|
||||||
* :class:`platypush.message.event.music.VolumeChangeEvent` if the volume changes
|
|
||||||
|
|
||||||
Requires:
|
Requires:
|
||||||
|
|
||||||
* **librespot**. Consult the `README <https://github.com/librespot-org/librespot>`_ for instructions.
|
* **librespot**. Consult the `README <https://github.com/librespot-org/librespot>`_ for instructions.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(
|
||||||
librespot_path: str = 'librespot',
|
self,
|
||||||
device_name: Optional[str] = None,
|
librespot_path: str = 'librespot',
|
||||||
device_type: str = 'speaker',
|
device_name: Optional[str] = None,
|
||||||
audio_backend: str = 'alsa',
|
device_type: str = 'speaker',
|
||||||
audio_device: Optional[str] = None,
|
audio_backend: str = 'alsa',
|
||||||
mixer: str = 'softvol',
|
audio_device: Optional[str] = None,
|
||||||
mixer_name: str = 'PCM',
|
mixer: str = 'softvol',
|
||||||
mixer_card: str = 'default',
|
mixer_name: str = 'PCM',
|
||||||
mixer_index: int = 0,
|
mixer_card: str = 'default',
|
||||||
volume: int = 100,
|
mixer_index: int = 0,
|
||||||
volume_ctrl: str = 'linear',
|
volume: int = 100,
|
||||||
bitrate: int = 160,
|
volume_ctrl: str = 'linear',
|
||||||
autoplay: bool = False,
|
bitrate: int = 160,
|
||||||
disable_gapless: bool = False,
|
autoplay: bool = False,
|
||||||
username: Optional[str] = None,
|
disable_gapless: bool = False,
|
||||||
password: Optional[str] = None,
|
username: Optional[str] = None,
|
||||||
client_id: Optional[str] = None,
|
password: Optional[str] = None,
|
||||||
client_secret: Optional[str] = None,
|
client_id: Optional[str] = None,
|
||||||
proxy: Optional[str] = None,
|
client_secret: Optional[str] = None,
|
||||||
ap_port: Optional[int] = None,
|
proxy: Optional[str] = None,
|
||||||
disable_discovery: bool = False,
|
ap_port: Optional[int] = None,
|
||||||
cache_dir: Optional[str] = None,
|
disable_discovery: bool = False,
|
||||||
system_cache_dir: Optional[str] = None,
|
cache_dir: Optional[str] = None,
|
||||||
disable_audio_cache=False,
|
system_cache_dir: Optional[str] = None,
|
||||||
enable_volume_normalization: bool = False,
|
disable_audio_cache=False,
|
||||||
normalization_method: str = 'dynamic',
|
enable_volume_normalization: bool = False,
|
||||||
normalization_pre_gain: Optional[float] = None,
|
normalization_method: str = 'dynamic',
|
||||||
normalization_threshold: float = -1.,
|
normalization_pre_gain: Optional[float] = None,
|
||||||
normalization_attack: int = 5,
|
normalization_threshold: float = -1.0,
|
||||||
normalization_release: int = 100,
|
normalization_attack: int = 5,
|
||||||
normalization_knee: float = 1.,
|
normalization_release: int = 100,
|
||||||
**kwargs):
|
normalization_knee: float = 1.0,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
:param librespot_path: Librespot path/executable name (default: ``librespot``).
|
:param librespot_path: Librespot path/executable name (default: ``librespot``).
|
||||||
:param device_name: Device name (default: same as configured Platypush ``device_id`` or hostname).
|
:param device_name: Device name (default: same as configured Platypush ``device_id`` or hostname).
|
||||||
|
@ -121,17 +121,36 @@ class MusicSpotifyBackend(Backend, SpotifyMixin):
|
||||||
SpotifyMixin.__init__(self, client_id=client_id, client_secret=client_secret)
|
SpotifyMixin.__init__(self, client_id=client_id, client_secret=client_secret)
|
||||||
self.device_name = device_name or Config.get('device_id')
|
self.device_name = device_name or Config.get('device_id')
|
||||||
self._librespot_args = [
|
self._librespot_args = [
|
||||||
librespot_path, '--name', self.device_name, '--backend', audio_backend,
|
librespot_path,
|
||||||
'--device-type', device_type, '--mixer', mixer, '--alsa-mixer-control', mixer_name,
|
'--name',
|
||||||
'--initial-volume', str(volume), '--volume-ctrl', volume_ctrl, '--bitrate', str(bitrate),
|
self.device_name,
|
||||||
'--emit-sink-events', '--onevent', 'python -m platypush.backend.music.spotify.event',
|
'--backend',
|
||||||
|
audio_backend,
|
||||||
|
'--device-type',
|
||||||
|
device_type,
|
||||||
|
'--mixer',
|
||||||
|
mixer,
|
||||||
|
'--alsa-mixer-control',
|
||||||
|
mixer_name,
|
||||||
|
'--initial-volume',
|
||||||
|
str(volume),
|
||||||
|
'--volume-ctrl',
|
||||||
|
volume_ctrl,
|
||||||
|
'--bitrate',
|
||||||
|
str(bitrate),
|
||||||
|
'--emit-sink-events',
|
||||||
|
'--onevent',
|
||||||
|
'python -m platypush.backend.music.spotify.event',
|
||||||
]
|
]
|
||||||
|
|
||||||
if audio_device:
|
if audio_device:
|
||||||
self._librespot_args += ['--alsa-mixer-device', audio_device]
|
self._librespot_args += ['--alsa-mixer-device', audio_device]
|
||||||
else:
|
else:
|
||||||
self._librespot_args += [
|
self._librespot_args += [
|
||||||
'--alsa-mixer-device', mixer_card, '--alsa-mixer-index', str(mixer_index)
|
'--alsa-mixer-device',
|
||||||
|
mixer_card,
|
||||||
|
'--alsa-mixer-index',
|
||||||
|
str(mixer_index),
|
||||||
]
|
]
|
||||||
if autoplay:
|
if autoplay:
|
||||||
self._librespot_args += ['--autoplay']
|
self._librespot_args += ['--autoplay']
|
||||||
|
@ -148,17 +167,30 @@ class MusicSpotifyBackend(Backend, SpotifyMixin):
|
||||||
if cache_dir:
|
if cache_dir:
|
||||||
self._librespot_args += ['--cache', os.path.expanduser(cache_dir)]
|
self._librespot_args += ['--cache', os.path.expanduser(cache_dir)]
|
||||||
if system_cache_dir:
|
if system_cache_dir:
|
||||||
self._librespot_args += ['--system-cache', os.path.expanduser(system_cache_dir)]
|
self._librespot_args += [
|
||||||
|
'--system-cache',
|
||||||
|
os.path.expanduser(system_cache_dir),
|
||||||
|
]
|
||||||
if enable_volume_normalization:
|
if enable_volume_normalization:
|
||||||
self._librespot_args += [
|
self._librespot_args += [
|
||||||
'--enable-volume-normalisation', '--normalisation-method', normalization_method,
|
'--enable-volume-normalisation',
|
||||||
'--normalisation-threshold', str(normalization_threshold), '--normalisation-attack',
|
'--normalisation-method',
|
||||||
str(normalization_attack), '--normalisation-release', str(normalization_release),
|
normalization_method,
|
||||||
'--normalisation-knee', str(normalization_knee),
|
'--normalisation-threshold',
|
||||||
|
str(normalization_threshold),
|
||||||
|
'--normalisation-attack',
|
||||||
|
str(normalization_attack),
|
||||||
|
'--normalisation-release',
|
||||||
|
str(normalization_release),
|
||||||
|
'--normalisation-knee',
|
||||||
|
str(normalization_knee),
|
||||||
]
|
]
|
||||||
|
|
||||||
if normalization_pre_gain:
|
if normalization_pre_gain:
|
||||||
self._librespot_args += ['--normalisation-pregain', str(normalization_pre_gain)]
|
self._librespot_args += [
|
||||||
|
'--normalisation-pregain',
|
||||||
|
str(normalization_pre_gain),
|
||||||
|
]
|
||||||
|
|
||||||
self._librespot_dump_args = self._librespot_args.copy()
|
self._librespot_dump_args = self._librespot_args.copy()
|
||||||
if username and password:
|
if username and password:
|
||||||
|
@ -227,11 +259,21 @@ class MusicSpotifyBackend(Backend, SpotifyMixin):
|
||||||
|
|
||||||
def _process_status_msg(self, status):
|
def _process_status_msg(self, status):
|
||||||
event_type = status.get('PLAYER_EVENT')
|
event_type = status.get('PLAYER_EVENT')
|
||||||
volume = int(status['VOLUME'])/655.35 if status.get('VOLUME') is not None else None
|
volume = (
|
||||||
|
int(status['VOLUME']) / 655.35 if status.get('VOLUME') is not None else None
|
||||||
|
)
|
||||||
track_id = status.get('TRACK_ID')
|
track_id = status.get('TRACK_ID')
|
||||||
old_track_id = status.get('OLD_TRACK_ID', self.track['id'])
|
old_track_id = status.get('OLD_TRACK_ID', self.track['id'])
|
||||||
duration = int(status['DURATION_MS'])/1000. if status.get('DURATION_MS') is not None else None
|
duration = (
|
||||||
elapsed = int(status['POSITION_MS'])/1000. if status.get('POSITION_MS') is not None else None
|
int(status['DURATION_MS']) / 1000.0
|
||||||
|
if status.get('DURATION_MS') is not None
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
elapsed = (
|
||||||
|
int(status['POSITION_MS']) / 1000.0
|
||||||
|
if status.get('POSITION_MS') is not None
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
if volume is not None:
|
if volume is not None:
|
||||||
self.status['volume'] = volume
|
self.status['volume'] = volume
|
||||||
|
@ -275,7 +317,7 @@ class MusicSpotifyBackend(Backend, SpotifyMixin):
|
||||||
self._librespot_proc.terminate()
|
self._librespot_proc.terminate()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._librespot_proc.wait(timeout=5.)
|
self._librespot_proc.wait(timeout=5.0)
|
||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
self.logger.warning('Librespot has not yet terminated: killing it')
|
self.logger.warning('Librespot has not yet terminated: killing it')
|
||||||
self._librespot_proc.kill()
|
self._librespot_proc.kill()
|
||||||
|
|
|
@ -11,71 +11,76 @@ class NextcloudBackend(Backend):
|
||||||
"""
|
"""
|
||||||
This backend triggers events when new activities occur on a NextCloud instance.
|
This backend triggers events when new activities occur on a NextCloud instance.
|
||||||
|
|
||||||
Triggers:
|
The field ``activity_type`` in the triggered :class:`platypush.message.event.nextcloud.NextCloudActivityEvent`
|
||||||
|
events identifies the activity type (e.g. ``file_created``, ``file_deleted``,
|
||||||
|
``file_changed``). Example in the case of the creation of new files:
|
||||||
|
|
||||||
- :class:`platypush.message.event.nextcloud.NextCloudActivityEvent` when new activity occurs on the instance.
|
.. code-block:: json
|
||||||
The field ``activity_type`` identifies the activity type (e.g. ``file_created``, ``file_deleted``,
|
|
||||||
``file_changed``). Example in the case of the creation of new files:
|
|
||||||
|
|
||||||
.. code-block:: json
|
{
|
||||||
|
"activity_id": 387,
|
||||||
{
|
"app": "files",
|
||||||
"activity_id": 387,
|
"activity_type": "file_created",
|
||||||
"app": "files",
|
"user": "your-user",
|
||||||
"activity_type": "file_created",
|
"subject": "You created InstantUpload/Camera/IMG_0100.jpg",
|
||||||
"user": "your-user",
|
"subject_rich": [
|
||||||
"subject": "You created InstantUpload/Camera/IMG_0100.jpg, InstantUpload/Camera/IMG_0101.jpg and InstantUpload/Camera/IMG_0102.jpg",
|
"You created {file3}, {file2} and {file1}",
|
||||||
"subject_rich": [
|
{
|
||||||
"You created {file3}, {file2} and {file1}",
|
"file1": {
|
||||||
{
|
"type": "file",
|
||||||
"file1": {
|
"id": "41994",
|
||||||
"type": "file",
|
"name": "IMG_0100.jpg",
|
||||||
"id": "41994",
|
"path": "InstantUpload/Camera/IMG_0100.jpg",
|
||||||
"name": "IMG_0100.jpg",
|
"link": "https://your-domain/nextcloud/index.php/f/41994"
|
||||||
"path": "InstantUpload/Camera/IMG_0100.jpg",
|
},
|
||||||
"link": "https://your-domain/nextcloud/index.php/f/41994"
|
"file2": {
|
||||||
},
|
"type": "file",
|
||||||
"file2": {
|
"id": "42005",
|
||||||
"type": "file",
|
"name": "IMG_0101.jpg",
|
||||||
"id": "42005",
|
"path": "InstantUpload/Camera/IMG_0102.jpg",
|
||||||
"name": "IMG_0101.jpg",
|
"link": "https://your-domain/nextcloud/index.php/f/42005"
|
||||||
"path": "InstantUpload/Camera/IMG_0102.jpg",
|
},
|
||||||
"link": "https://your-domain/nextcloud/index.php/f/42005"
|
"file3": {
|
||||||
},
|
"type": "file",
|
||||||
"file3": {
|
"id": "42014",
|
||||||
"type": "file",
|
"name": "IMG_0102.jpg",
|
||||||
"id": "42014",
|
"path": "InstantUpload/Camera/IMG_0102.jpg",
|
||||||
"name": "IMG_0102.jpg",
|
"link": "https://your-domain/nextcloud/index.php/f/42014"
|
||||||
"path": "InstantUpload/Camera/IMG_0102.jpg",
|
|
||||||
"link": "https://your-domain/nextcloud/index.php/f/42014"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"message": "",
|
|
||||||
"message_rich": [
|
|
||||||
"",
|
|
||||||
[]
|
|
||||||
],
|
|
||||||
"object_type": "files",
|
|
||||||
"object_id": 41994,
|
|
||||||
"object_name": "/InstantUpload/Camera/IMG_0102.jpg",
|
|
||||||
"objects": {
|
|
||||||
"42014": "/InstantUpload/Camera/IMG_0100.jpg",
|
|
||||||
"42005": "/InstantUpload/Camera/IMG_0101.jpg",
|
|
||||||
"41994": "/InstantUpload/Camera/IMG_0102.jpg"
|
|
||||||
},
|
|
||||||
"link": "https://your-domain/nextcloud/index.php/apps/files/?dir=/InstantUpload/Camera",
|
|
||||||
"icon": "https://your-domain/nextcloud/apps/files/img/add-color.svg",
|
|
||||||
"datetime": "2020-09-07T17:04:29+00:00"
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"message": "",
|
||||||
|
"message_rich": [
|
||||||
|
"",
|
||||||
|
[]
|
||||||
|
],
|
||||||
|
"object_type": "files",
|
||||||
|
"object_id": 41994,
|
||||||
|
"object_name": "/InstantUpload/Camera/IMG_0102.jpg",
|
||||||
|
"objects": {
|
||||||
|
"42014": "/InstantUpload/Camera/IMG_0100.jpg",
|
||||||
|
"42005": "/InstantUpload/Camera/IMG_0101.jpg",
|
||||||
|
"41994": "/InstantUpload/Camera/IMG_0102.jpg"
|
||||||
|
},
|
||||||
|
"link": "https://your-domain/nextcloud/index.php/apps/files/?dir=/InstantUpload/Camera",
|
||||||
|
"icon": "https://your-domain/nextcloud/apps/files/img/add-color.svg",
|
||||||
|
"datetime": "2020-09-07T17:04:29+00:00"
|
||||||
|
}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_LAST_ACTIVITY_VARNAME = '_NEXTCLOUD_LAST_ACTIVITY_ID'
|
_LAST_ACTIVITY_VARNAME = '_NEXTCLOUD_LAST_ACTIVITY_ID'
|
||||||
|
|
||||||
def __init__(self, url: Optional[str] = None, username: Optional[str] = None, password: Optional[str] = None,
|
def __init__(
|
||||||
object_type: Optional[str] = None, object_id: Optional[int] = None,
|
self,
|
||||||
poll_seconds: Optional[float] = 60., **kwargs):
|
url: Optional[str] = None,
|
||||||
|
username: Optional[str] = None,
|
||||||
|
password: Optional[str] = None,
|
||||||
|
object_type: Optional[str] = None,
|
||||||
|
object_id: Optional[int] = None,
|
||||||
|
poll_seconds: Optional[float] = 60.0,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
:param url: NextCloud instance URL (default: same as the :class:`platypush.plugins.nextcloud.NextCloudPlugin`).
|
:param url: NextCloud instance URL (default: same as the :class:`platypush.plugins.nextcloud.NextCloudPlugin`).
|
||||||
:param username: NextCloud username (default: same as the :class:`platypush.plugins.nextcloud.NextCloudPlugin`).
|
:param username: NextCloud username (default: same as the :class:`platypush.plugins.nextcloud.NextCloudPlugin`).
|
||||||
|
@ -106,14 +111,17 @@ class NextcloudBackend(Backend):
|
||||||
self.username = username if username else self.username
|
self.username = username if username else self.username
|
||||||
self.password = password if password else self.password
|
self.password = password if password else self.password
|
||||||
|
|
||||||
assert self.url and self.username and self.password, \
|
assert (
|
||||||
'No configuration provided neither for the NextCloud plugin nor the backend'
|
self.url and self.username and self.password
|
||||||
|
), 'No configuration provided neither for the NextCloud plugin nor the backend'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def last_seen_id(self) -> Optional[int]:
|
def last_seen_id(self) -> Optional[int]:
|
||||||
if self._last_seen_id is None:
|
if self._last_seen_id is None:
|
||||||
variables: VariablePlugin = get_plugin('variable')
|
variables: VariablePlugin = get_plugin('variable')
|
||||||
last_seen_id = variables.get(self._LAST_ACTIVITY_VARNAME).output.get(self._LAST_ACTIVITY_VARNAME)
|
last_seen_id = variables.get(self._LAST_ACTIVITY_VARNAME).output.get(
|
||||||
|
self._LAST_ACTIVITY_VARNAME
|
||||||
|
)
|
||||||
self._last_seen_id = last_seen_id
|
self._last_seen_id = last_seen_id
|
||||||
|
|
||||||
return self._last_seen_id
|
return self._last_seen_id
|
||||||
|
@ -133,8 +141,14 @@ class NextcloudBackend(Backend):
|
||||||
new_last_seen_id = int(last_seen_id)
|
new_last_seen_id = int(last_seen_id)
|
||||||
plugin: NextcloudPlugin = get_plugin('nextcloud')
|
plugin: NextcloudPlugin = get_plugin('nextcloud')
|
||||||
# noinspection PyUnresolvedReferences
|
# noinspection PyUnresolvedReferences
|
||||||
activities = plugin.get_activities(sort='desc', url=self.url, username=self.username, password=self.password,
|
activities = plugin.get_activities(
|
||||||
object_type=self.object_type, object_id=self.object_id).output
|
sort='desc',
|
||||||
|
url=self.url,
|
||||||
|
username=self.username,
|
||||||
|
password=self.password,
|
||||||
|
object_type=self.object_type,
|
||||||
|
object_id=self.object_id,
|
||||||
|
).output
|
||||||
|
|
||||||
events = []
|
events = []
|
||||||
for activity in activities:
|
for activity in activities:
|
||||||
|
|
|
@ -14,18 +14,6 @@ class NfcBackend(Backend):
|
||||||
"""
|
"""
|
||||||
Backend to detect NFC card events from a compatible reader.
|
Backend to detect NFC card events from a compatible reader.
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
* :class:`platypush.message.event.nfc.NFCDeviceConnectedEvent` when an NFC reader/writer is connected
|
|
||||||
* :class:`platypush.message.event.nfc.NFCDeviceDisconnectedEvent` when an NFC reader/writer is disconnected
|
|
||||||
* :class:`platypush.message.event.nfc.NFCTagDetectedEvent` when an NFC tag is detected
|
|
||||||
* :class:`platypush.message.event.nfc.NFCTagRemovedEvent` when an NFC tag is removed
|
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **nfcpy** >= 1.0 (``pip install 'nfcpy>=1.0'``)
|
|
||||||
* **ndef** (``pip install ndeflib``)
|
|
||||||
|
|
||||||
Run the following to check if your device is compatible with nfcpy and the right permissions are set::
|
Run the following to check if your device is compatible with nfcpy and the right permissions are set::
|
||||||
|
|
||||||
python -m nfc
|
python -m nfc
|
||||||
|
|
|
@ -13,11 +13,6 @@ class NoderedBackend(Backend):
|
||||||
used in your flows. This block will accept JSON requests as input in the format
|
used in your flows. This block will accept JSON requests as input in the format
|
||||||
``{"type":"request", "action":"plugin.name.action_name", "args": {...}}`` and return the output
|
``{"type":"request", "action":"plugin.name.action_name", "args": {...}}`` and return the output
|
||||||
of the action as block output, or raise an exception if the action failed.
|
of the action as block output, or raise an exception if the action failed.
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **pynodered** (``pip install pynodered``)
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, port: int = 5051, *args, **kwargs):
|
def __init__(self, port: int = 5051, *args, **kwargs):
|
||||||
|
@ -27,7 +22,8 @@ class NoderedBackend(Backend):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.port = port
|
self.port = port
|
||||||
self._runner_path = os.path.join(
|
self._runner_path = os.path.join(
|
||||||
os.path.dirname(inspect.getfile(self.__class__)), 'runner.py')
|
os.path.dirname(inspect.getfile(self.__class__)), 'runner.py'
|
||||||
|
)
|
||||||
self._server = None
|
self._server = None
|
||||||
|
|
||||||
def on_stop(self):
|
def on_stop(self):
|
||||||
|
@ -40,8 +36,16 @@ class NoderedBackend(Backend):
|
||||||
super().run()
|
super().run()
|
||||||
self.register_service(port=self.port, name='node')
|
self.register_service(port=self.port, name='node')
|
||||||
|
|
||||||
self._server = subprocess.Popen([sys.executable, '-m', 'pynodered.server',
|
self._server = subprocess.Popen(
|
||||||
'--port', str(self.port), self._runner_path])
|
[
|
||||||
|
sys.executable,
|
||||||
|
'-m',
|
||||||
|
'pynodered.server',
|
||||||
|
'--port',
|
||||||
|
str(self.port),
|
||||||
|
self._runner_path,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
self.logger.info('Started Node-RED backend on port {}'.format(self.port))
|
self.logger.info('Started Node-RED backend on port {}'.format(self.port))
|
||||||
self._server.wait()
|
self._server.wait()
|
||||||
|
|
|
@ -11,12 +11,6 @@ from platypush.utils.workers import Worker, Workers
|
||||||
class PingBackend(Backend):
|
class PingBackend(Backend):
|
||||||
"""
|
"""
|
||||||
This backend allows you to ping multiple remote hosts at regular intervals.
|
This backend allows you to ping multiple remote hosts at regular intervals.
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
- :class:`platypush.message.event.ping.HostDownEvent` if a host stops responding ping requests
|
|
||||||
- :class:`platypush.message.event.ping.HostUpEvent` if a host starts responding ping requests
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class Pinger(Worker):
|
class Pinger(Worker):
|
||||||
|
@ -30,7 +24,15 @@ class PingBackend(Backend):
|
||||||
response = pinger.ping(host, timeout=self.timeout, count=self.count).output
|
response = pinger.ping(host, timeout=self.timeout, count=self.count).output
|
||||||
return host, response['success'] is True
|
return host, response['success'] is True
|
||||||
|
|
||||||
def __init__(self, hosts: List[str], timeout: float = 5.0, interval: float = 60.0, count: int = 1, *args, **kwargs):
|
def __init__(
|
||||||
|
self,
|
||||||
|
hosts: List[str],
|
||||||
|
timeout: float = 5.0,
|
||||||
|
interval: float = 60.0,
|
||||||
|
count: int = 1,
|
||||||
|
*args,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
:param hosts: List of IP addresses or host names to monitor.
|
:param hosts: List of IP addresses or host names to monitor.
|
||||||
:param timeout: Ping timeout.
|
:param timeout: Ping timeout.
|
||||||
|
@ -47,7 +49,9 @@ class PingBackend(Backend):
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
super().run()
|
super().run()
|
||||||
self.logger.info('Starting ping backend with {} hosts to monitor'.format(len(self.hosts)))
|
self.logger.info(
|
||||||
|
'Starting ping backend with {} hosts to monitor'.format(len(self.hosts))
|
||||||
|
)
|
||||||
|
|
||||||
while not self.should_stop():
|
while not self.should_stop():
|
||||||
workers = Workers(10, self.Pinger, timeout=self.timeout, count=self.count)
|
workers = Workers(10, self.Pinger, timeout=self.timeout, count=self.count)
|
||||||
|
|
|
@ -14,19 +14,16 @@ class PushbulletBackend(Backend):
|
||||||
Pushbullet app and/or through Tasker), synchronize clipboards, send pictures
|
Pushbullet app and/or through Tasker), synchronize clipboards, send pictures
|
||||||
and files to other devices etc. You can also wrap Platypush messages as JSON
|
and files to other devices etc. You can also wrap Platypush messages as JSON
|
||||||
into a push body to execute them.
|
into a push body to execute them.
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
* :class:`platypush.message.event.pushbullet.PushbulletEvent` if a new push is received
|
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **pushbullet.py** (``pip install git+https://github.com/pushbullet.py/pushbullet.py``)
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, token: str, device: str = 'Platypush', proxy_host: Optional[str] = None,
|
def __init__(
|
||||||
proxy_port: Optional[int] = None, **kwargs):
|
self,
|
||||||
|
token: str,
|
||||||
|
device: str = 'Platypush',
|
||||||
|
proxy_host: Optional[str] = None,
|
||||||
|
proxy_port: Optional[int] = None,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
:param token: Your Pushbullet API token, see https://docs.pushbullet.com/#authentication
|
:param token: Your Pushbullet API token, see https://docs.pushbullet.com/#authentication
|
||||||
:param device: Name of the virtual device for Platypush (default: Platypush)
|
:param device: Name of the virtual device for Platypush (default: Platypush)
|
||||||
|
@ -47,12 +44,15 @@ class PushbulletBackend(Backend):
|
||||||
def _initialize(self):
|
def _initialize(self):
|
||||||
# noinspection PyPackageRequirements
|
# noinspection PyPackageRequirements
|
||||||
from pushbullet import Pushbullet
|
from pushbullet import Pushbullet
|
||||||
|
|
||||||
self.pb = Pushbullet(self.token)
|
self.pb = Pushbullet(self.token)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.device = self.pb.get_device(self.device_name)
|
self.device = self.pb.get_device(self.device_name)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.info(f'Device {self.device_name} does not exist: {e}. Creating it')
|
self.logger.info(
|
||||||
|
f'Device {self.device_name} does not exist: {e}. Creating it'
|
||||||
|
)
|
||||||
self.device = self.pb.new_device(self.device_name)
|
self.device = self.pb.new_device(self.device_name)
|
||||||
|
|
||||||
self.pb_device_id = self.get_device_id()
|
self.pb_device_id = self.get_device_id()
|
||||||
|
@ -98,8 +98,10 @@ class PushbulletBackend(Backend):
|
||||||
body = json.loads(body)
|
body = json.loads(body)
|
||||||
self.on_message(body)
|
self.on_message(body)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.debug('Unexpected message received on the ' +
|
self.logger.debug(
|
||||||
f'Pushbullet backend: {e}. Message: {body}')
|
'Unexpected message received on the '
|
||||||
|
+ f'Pushbullet backend: {e}. Message: {body}'
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.exception(e)
|
self.logger.exception(e)
|
||||||
return
|
return
|
||||||
|
@ -111,8 +113,12 @@ class PushbulletBackend(Backend):
|
||||||
try:
|
try:
|
||||||
return self.pb.get_device(self.device_name).device_iden
|
return self.pb.get_device(self.device_name).device_iden
|
||||||
except Exception:
|
except Exception:
|
||||||
device = self.pb.new_device(self.device_name, model='Platypush virtual device',
|
device = self.pb.new_device(
|
||||||
manufacturer='platypush', icon='system')
|
self.device_name,
|
||||||
|
model='Platypush virtual device',
|
||||||
|
manufacturer='platypush',
|
||||||
|
icon='system',
|
||||||
|
)
|
||||||
|
|
||||||
self.logger.info(f'Created Pushbullet device {self.device_name}')
|
self.logger.info(f'Created Pushbullet device {self.device_name}')
|
||||||
return device.device_iden
|
return device.device_iden
|
||||||
|
@ -158,14 +164,18 @@ class PushbulletBackend(Backend):
|
||||||
def run_listener(self):
|
def run_listener(self):
|
||||||
from .listener import Listener
|
from .listener import Listener
|
||||||
|
|
||||||
self.logger.info(f'Initializing Pushbullet backend - device_id: {self.device_name}')
|
self.logger.info(
|
||||||
self.listener = Listener(account=self.pb,
|
f'Initializing Pushbullet backend - device_id: {self.device_name}'
|
||||||
on_push=self.on_push(),
|
)
|
||||||
on_open=self.on_open(),
|
self.listener = Listener(
|
||||||
on_close=self.on_close(),
|
account=self.pb,
|
||||||
on_error=self.on_error(),
|
on_push=self.on_push(),
|
||||||
http_proxy_host=self.proxy_host,
|
on_open=self.on_open(),
|
||||||
http_proxy_port=self.proxy_port)
|
on_close=self.on_close(),
|
||||||
|
on_error=self.on_error(),
|
||||||
|
http_proxy_host=self.proxy_host,
|
||||||
|
http_proxy_port=self.proxy_port,
|
||||||
|
)
|
||||||
|
|
||||||
self.listener.run_forever()
|
self.listener.run_forever()
|
||||||
|
|
||||||
|
|
|
@ -9,23 +9,18 @@ class ScardBackend(Backend):
|
||||||
|
|
||||||
Extend this backend to implement more advanced communication with custom
|
Extend this backend to implement more advanced communication with custom
|
||||||
smart cards.
|
smart cards.
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
* :class:`platypush.message.event.scard.SmartCardDetectedEvent` when a smart card is detected
|
|
||||||
* :class:`platypush.message.event.scard.SmartCardRemovedEvent` when a smart card is removed
|
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **pyscard** (``pip install pyscard``)
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, atr=None, *args, **kwargs):
|
def __init__(self, atr=None, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
:param atr: If set, the backend will trigger events only for card(s) with the specified ATR(s). It can be either an ATR string (space-separated hex octects) or a list of ATR strings. Default: none (any card will be detected)
|
:param atr: If set, the backend will trigger events only for card(s)
|
||||||
|
with the specified ATR(s). It can be either an ATR string
|
||||||
|
(space-separated hex octects) or a list of ATR strings. Default:
|
||||||
|
none (any card will be detected).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from smartcard.CardType import AnyCardType, ATRCardType
|
from smartcard.CardType import AnyCardType, ATRCardType
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.ATRs = []
|
self.ATRs = []
|
||||||
|
|
||||||
|
@ -35,9 +30,10 @@ class ScardBackend(Backend):
|
||||||
elif isinstance(atr, list):
|
elif isinstance(atr, list):
|
||||||
self.ATRs = atr
|
self.ATRs = atr
|
||||||
else:
|
else:
|
||||||
raise RuntimeError("Unsupported ATR: \"{}\" - type: {}, " +
|
raise RuntimeError(
|
||||||
"supported types: string, list".format(
|
f"Unsupported ATR: \"{atr}\" - type: {type(atr)}, "
|
||||||
atr, type(atr)))
|
+ "supported types: string, list"
|
||||||
|
)
|
||||||
|
|
||||||
self.cardtype = ATRCardType(*[self._to_bytes(atr) for atr in self.ATRs])
|
self.cardtype = ATRCardType(*[self._to_bytes(atr) for atr in self.ATRs])
|
||||||
else:
|
else:
|
||||||
|
@ -56,8 +52,9 @@ class ScardBackend(Backend):
|
||||||
|
|
||||||
super().run()
|
super().run()
|
||||||
|
|
||||||
self.logger.info('Initialized smart card reader backend - ATR filter: {}'.
|
self.logger.info(
|
||||||
format(self.ATRs))
|
'Initialized smart card reader backend - ATR filter: {}'.format(self.ATRs)
|
||||||
|
)
|
||||||
|
|
||||||
prev_atr = None
|
prev_atr = None
|
||||||
reader = None
|
reader = None
|
||||||
|
@ -72,17 +69,19 @@ class ScardBackend(Backend):
|
||||||
atr = toHexString(cardservice.connection.getATR())
|
atr = toHexString(cardservice.connection.getATR())
|
||||||
|
|
||||||
if atr != prev_atr:
|
if atr != prev_atr:
|
||||||
self.logger.info('Smart card detected on reader {}, ATR: {}'.
|
self.logger.info(
|
||||||
format(reader, atr))
|
'Smart card detected on reader {}, ATR: {}'.format(reader, atr)
|
||||||
|
)
|
||||||
|
|
||||||
self.bus.post(SmartCardDetectedEvent(atr=atr, reader=reader))
|
self.bus.post(SmartCardDetectedEvent(atr=atr, reader=reader))
|
||||||
prev_atr = atr
|
prev_atr = atr
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if isinstance(e, NoCardException) or isinstance(e, CardConnectionException):
|
if isinstance(e, (NoCardException, CardConnectionException)):
|
||||||
self.bus.post(SmartCardRemovedEvent(atr=prev_atr, reader=reader))
|
self.bus.post(SmartCardRemovedEvent(atr=prev_atr, reader=reader))
|
||||||
else:
|
else:
|
||||||
self.logger.exception(e)
|
self.logger.exception(e)
|
||||||
|
|
||||||
prev_atr = None
|
prev_atr = None
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
|
@ -14,11 +14,6 @@ class SensorIrZeroborgBackend(Backend):
|
||||||
remote by running the scan utility::
|
remote by running the scan utility::
|
||||||
|
|
||||||
python -m platypush.backend.sensor.ir.zeroborg.scan
|
python -m platypush.backend.sensor.ir.zeroborg.scan
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
* :class:`platypush.message.event.sensor.ir.IrKeyDownEvent` when a key is pressed
|
|
||||||
* :class:`platypush.message.event.sensor.ir.IrKeyUpEvent` when a key is released
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
last_message = None
|
last_message = None
|
||||||
|
@ -40,20 +35,29 @@ class SensorIrZeroborgBackend(Backend):
|
||||||
if self.zb.HasNewIrMessage():
|
if self.zb.HasNewIrMessage():
|
||||||
message = self.zb.GetIrMessage()
|
message = self.zb.GetIrMessage()
|
||||||
if message != self.last_message:
|
if message != self.last_message:
|
||||||
self.logger.info('Received key down event on the IR sensor: {}'.format(message))
|
self.logger.info(
|
||||||
|
'Received key down event on the IR sensor: {}'.format(
|
||||||
|
message
|
||||||
|
)
|
||||||
|
)
|
||||||
self.bus.post(IrKeyDownEvent(message=message))
|
self.bus.post(IrKeyDownEvent(message=message))
|
||||||
|
|
||||||
self.last_message = message
|
self.last_message = message
|
||||||
self.last_message_timestamp = time.time()
|
self.last_message_timestamp = time.time()
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
self.logger.warning('Failed reading IR sensor status: {}: {}'.format(type(e), str(e)))
|
self.logger.warning(
|
||||||
|
'Failed reading IR sensor status: {}: {}'.format(type(e), str(e))
|
||||||
|
)
|
||||||
|
|
||||||
if self.last_message_timestamp and \
|
if (
|
||||||
time.time() - self.last_message_timestamp > self.no_message_timeout:
|
self.last_message_timestamp
|
||||||
|
and time.time() - self.last_message_timestamp > self.no_message_timeout
|
||||||
|
):
|
||||||
self.logger.info('Received key up event on the IR sensor')
|
self.logger.info('Received key up event on the IR sensor')
|
||||||
self.bus.post(IrKeyUpEvent(message=self.last_message))
|
self.bus.post(IrKeyUpEvent(message=self.last_message))
|
||||||
|
|
||||||
self.last_message = None
|
self.last_message = None
|
||||||
self.last_message_timestamp = None
|
self.last_message_timestamp = None
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
|
@ -7,8 +7,13 @@ import Leap
|
||||||
|
|
||||||
from platypush.backend import Backend
|
from platypush.backend import Backend
|
||||||
from platypush.context import get_backend
|
from platypush.context import get_backend
|
||||||
from platypush.message.event.sensor.leap import LeapFrameEvent, \
|
from platypush.message.event.sensor.leap import (
|
||||||
LeapFrameStartEvent, LeapFrameStopEvent, LeapConnectEvent, LeapDisconnectEvent
|
LeapFrameEvent,
|
||||||
|
LeapFrameStartEvent,
|
||||||
|
LeapFrameStopEvent,
|
||||||
|
LeapConnectEvent,
|
||||||
|
LeapDisconnectEvent,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SensorLeapBackend(Backend):
|
class SensorLeapBackend(Backend):
|
||||||
|
@ -26,40 +31,38 @@ class SensorLeapBackend(Backend):
|
||||||
|
|
||||||
Requires:
|
Requires:
|
||||||
|
|
||||||
* The Redis backend enabled
|
* The Leap Motion SDK compiled with Python 3 support, see my port at
|
||||||
* The Leap Motion SDK compiled with Python 3 support, see my port at https://github.com:BlackLight/leap-sdk-python3.git
|
https://github.com:BlackLight/leap-sdk-python3.git
|
||||||
* The `leapd` daemon to be running and your Leap Motion connected
|
* The ``leapd`` daemon to be running and your Leap Motion connected
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
* :class:`platypush.message.event.sensor.leap.LeapFrameEvent` when a new frame is received
|
|
||||||
* :class:`platypush.message.event.sensor.leap.LeapFrameStartEvent` when a new sequence of frame starts
|
|
||||||
* :class:`platypush.message.event.sensor.leap.LeapFrameStopEvent` when a sequence of frame stops
|
|
||||||
* :class:`platypush.message.event.sensor.leap.LeapConnectEvent` when a Leap Motion device is connected
|
|
||||||
* :class:`platypush.message.event.sensor.leap.LeapDisconnectEvent` when a Leap Motion device disconnects
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_listener_proc = None
|
_listener_proc = None
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(
|
||||||
position_ranges=None,
|
self,
|
||||||
position_tolerance=0.0, # Position variation tolerance in %
|
position_ranges=None,
|
||||||
frames_throttle_secs=None,
|
position_tolerance=0.0, # Position variation tolerance in %
|
||||||
*args, **kwargs):
|
frames_throttle_secs=None,
|
||||||
|
*args,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
:param position_ranges: It specifies how wide the hand space (x, y and z axes) should be in millimiters.
|
:param position_ranges: It specifies how wide the hand space (x, y and
|
||||||
|
z axes) should be in millimiters.
|
||||||
|
|
||||||
Default::
|
Default::
|
||||||
|
|
||||||
[
|
[
|
||||||
[-300.0, 300.0], # x axis
|
[-300.0, 300.0], # x axis
|
||||||
[25.0, 600.0], # y axis
|
[25.0, 600.0], # y axis
|
||||||
[-300.0, 300.0], # z axis
|
[-300.0, 300.0], # z axis
|
||||||
]
|
]
|
||||||
|
|
||||||
:type position_ranges: list[list[float]]
|
:type position_ranges: list[list[float]]
|
||||||
|
|
||||||
:param position_tolerance: % of change between a frame and the next to really consider the next frame as a new one (default: 0)
|
:param position_tolerance: % of change between a frame and the next to
|
||||||
|
really consider the next frame as a new one (default: 0).
|
||||||
:type position_tolerance: float
|
:type position_tolerance: float
|
||||||
|
|
||||||
:param frames_throttle_secs: If set, the frame events will be throttled
|
:param frames_throttle_secs: If set, the frame events will be throttled
|
||||||
|
@ -87,16 +90,20 @@ class SensorLeapBackend(Backend):
|
||||||
super().run()
|
super().run()
|
||||||
|
|
||||||
def _listener_process():
|
def _listener_process():
|
||||||
listener = LeapListener(position_ranges=self.position_ranges,
|
listener = LeapListener(
|
||||||
position_tolerance=self.position_tolerance,
|
position_ranges=self.position_ranges,
|
||||||
frames_throttle_secs=self.frames_throttle_secs,
|
position_tolerance=self.position_tolerance,
|
||||||
logger=self.logger)
|
frames_throttle_secs=self.frames_throttle_secs,
|
||||||
|
logger=self.logger,
|
||||||
|
)
|
||||||
|
|
||||||
controller = Leap.Controller()
|
controller = Leap.Controller()
|
||||||
|
|
||||||
if not controller:
|
if not controller:
|
||||||
raise RuntimeError('No Leap Motion controller found - is your ' +
|
raise RuntimeError(
|
||||||
'device connected and is leapd running?')
|
'No Leap Motion controller found - is your '
|
||||||
|
+ 'device connected and is leapd running?'
|
||||||
|
)
|
||||||
|
|
||||||
controller.add_listener(listener)
|
controller.add_listener(listener)
|
||||||
self.logger.info('Leap Motion backend initialized')
|
self.logger.info('Leap Motion backend initialized')
|
||||||
|
@ -120,12 +127,14 @@ class LeapFuture(Timer):
|
||||||
def _callback_wrapper(self):
|
def _callback_wrapper(self):
|
||||||
def _callback():
|
def _callback():
|
||||||
self.listener._send_event(self.event)
|
self.listener._send_event(self.event)
|
||||||
|
|
||||||
return _callback
|
return _callback
|
||||||
|
|
||||||
|
|
||||||
class LeapListener(Leap.Listener):
|
class LeapListener(Leap.Listener):
|
||||||
def __init__(self, position_ranges, position_tolerance, logger,
|
def __init__(
|
||||||
frames_throttle_secs=None):
|
self, position_ranges, position_tolerance, logger, frames_throttle_secs=None
|
||||||
|
):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
self.prev_frame = None
|
self.prev_frame = None
|
||||||
|
@ -138,8 +147,11 @@ class LeapListener(Leap.Listener):
|
||||||
def _send_event(self, event):
|
def _send_event(self, event):
|
||||||
backend = get_backend('redis')
|
backend = get_backend('redis')
|
||||||
if not backend:
|
if not backend:
|
||||||
self.logger.warning('Redis backend not configured, I cannot propagate the following event: {}'.
|
self.logger.warning(
|
||||||
format(event))
|
'Redis backend not configured, I cannot propagate the following event: {}'.format(
|
||||||
|
event
|
||||||
|
)
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
backend.send_message(event)
|
backend.send_message(event)
|
||||||
|
@ -147,8 +159,9 @@ class LeapListener(Leap.Listener):
|
||||||
def send_event(self, event):
|
def send_event(self, event):
|
||||||
if self.frames_throttle_secs:
|
if self.frames_throttle_secs:
|
||||||
if not self.running_future or not self.running_future.is_alive():
|
if not self.running_future or not self.running_future.is_alive():
|
||||||
self.running_future = LeapFuture(seconds=self.frames_throttle_secs,
|
self.running_future = LeapFuture(
|
||||||
listener=self, event=event)
|
seconds=self.frames_throttle_secs, listener=self, event=event
|
||||||
|
)
|
||||||
self.running_future.start()
|
self.running_future.start()
|
||||||
else:
|
else:
|
||||||
self._send_event(event)
|
self._send_event(event)
|
||||||
|
@ -193,23 +206,38 @@ class LeapListener(Leap.Listener):
|
||||||
'id': hand.id,
|
'id': hand.id,
|
||||||
'is_left': hand.is_left,
|
'is_left': hand.is_left,
|
||||||
'is_right': hand.is_right,
|
'is_right': hand.is_right,
|
||||||
'palm_normal': [hand.palm_normal[0], hand.palm_normal[1], hand.palm_normal[2]],
|
'palm_normal': [
|
||||||
|
hand.palm_normal[0],
|
||||||
|
hand.palm_normal[1],
|
||||||
|
hand.palm_normal[2],
|
||||||
|
],
|
||||||
'palm_position': self._normalize_position(hand.palm_position),
|
'palm_position': self._normalize_position(hand.palm_position),
|
||||||
'palm_velocity': [hand.palm_velocity[0], hand.palm_velocity[1], hand.palm_velocity[2]],
|
'palm_velocity': [
|
||||||
|
hand.palm_velocity[0],
|
||||||
|
hand.palm_velocity[1],
|
||||||
|
hand.palm_velocity[2],
|
||||||
|
],
|
||||||
'palm_width': hand.palm_width,
|
'palm_width': hand.palm_width,
|
||||||
'sphere_center': [hand.sphere_center[0], hand.sphere_center[1], hand.sphere_center[2]],
|
'sphere_center': [
|
||||||
|
hand.sphere_center[0],
|
||||||
|
hand.sphere_center[1],
|
||||||
|
hand.sphere_center[2],
|
||||||
|
],
|
||||||
'sphere_radius': hand.sphere_radius,
|
'sphere_radius': hand.sphere_radius,
|
||||||
'stabilized_palm_position': self._normalize_position(hand.stabilized_palm_position),
|
'stabilized_palm_position': self._normalize_position(
|
||||||
|
hand.stabilized_palm_position
|
||||||
|
),
|
||||||
'time_visible': hand.time_visible,
|
'time_visible': hand.time_visible,
|
||||||
'wrist_position': self._normalize_position(hand.wrist_position),
|
'wrist_position': self._normalize_position(hand.wrist_position),
|
||||||
}
|
}
|
||||||
for i, hand in enumerate(frame.hands)
|
for i, hand in enumerate(frame.hands)
|
||||||
if hand.is_valid and (
|
if hand.is_valid
|
||||||
len(frame.hands) != len(self.prev_frame.hands) or
|
and (
|
||||||
self._position_changed(
|
len(frame.hands) != len(self.prev_frame.hands)
|
||||||
|
or self._position_changed(
|
||||||
old_position=self.prev_frame.hands[i].stabilized_palm_position,
|
old_position=self.prev_frame.hands[i].stabilized_palm_position,
|
||||||
new_position=hand.stabilized_palm_position)
|
new_position=hand.stabilized_palm_position,
|
||||||
|
)
|
||||||
if self.prev_frame
|
if self.prev_frame
|
||||||
else True
|
else True
|
||||||
)
|
)
|
||||||
|
@ -220,25 +248,38 @@ class LeapListener(Leap.Listener):
|
||||||
# having x_range = z_range = [-100, 100], y_range = [0, 100]
|
# having x_range = z_range = [-100, 100], y_range = [0, 100]
|
||||||
|
|
||||||
return [
|
return [
|
||||||
self._scale_scalar(value=position[0], range=self.position_ranges[0], new_range=[-100.0, 100.0]),
|
self._scale_scalar(
|
||||||
self._scale_scalar(value=position[1], range=self.position_ranges[1], new_range=[0.0, 100.0]),
|
value=position[0],
|
||||||
self._scale_scalar(value=position[2], range=self.position_ranges[2], new_range=[-100.0, 100.0]),
|
range=self.position_ranges[0],
|
||||||
|
new_range=[-100.0, 100.0],
|
||||||
|
),
|
||||||
|
self._scale_scalar(
|
||||||
|
value=position[1], range=self.position_ranges[1], new_range=[0.0, 100.0]
|
||||||
|
),
|
||||||
|
self._scale_scalar(
|
||||||
|
value=position[2],
|
||||||
|
range=self.position_ranges[2],
|
||||||
|
new_range=[-100.0, 100.0],
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _scale_scalar(value, range, new_range):
|
def _scale_scalar(value, range, new_range):
|
||||||
if value < range[0]:
|
if value < range[0]:
|
||||||
value=range[0]
|
value = range[0]
|
||||||
if value > range[1]:
|
if value > range[1]:
|
||||||
value=range[1]
|
value = range[1]
|
||||||
|
|
||||||
return ((new_range[1]-new_range[0])/(range[1]-range[0]))*(value-range[0]) + new_range[0]
|
return ((new_range[1] - new_range[0]) / (range[1] - range[0])) * (
|
||||||
|
value - range[0]
|
||||||
|
) + new_range[0]
|
||||||
|
|
||||||
def _position_changed(self, old_position, new_position):
|
def _position_changed(self, old_position, new_position):
|
||||||
return (
|
return (
|
||||||
abs(old_position[0]-new_position[0]) > self.position_tolerance or
|
abs(old_position[0] - new_position[0]) > self.position_tolerance
|
||||||
abs(old_position[1]-new_position[1]) > self.position_tolerance or
|
or abs(old_position[1] - new_position[1]) > self.position_tolerance
|
||||||
abs(old_position[2]-new_position[2]) > self.position_tolerance)
|
or abs(old_position[2] - new_position[2]) > self.position_tolerance
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
|
@ -10,7 +10,18 @@ from platypush.message import Message
|
||||||
|
|
||||||
class TcpBackend(Backend):
|
class TcpBackend(Backend):
|
||||||
"""
|
"""
|
||||||
Backend that reads messages from a configured TCP port
|
Backend that reads messages from a configured TCP port.
|
||||||
|
|
||||||
|
You can use this backend to send messages to Platypush from any TCP client, for example:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ echo '{"type": "request", "action": "shell.exec", "args": {"cmd": "ls /"}}' | nc localhost 1234
|
||||||
|
|
||||||
|
.. warning:: Be **VERY** careful when exposing this backend to the Internet. Unlike the HTTP backend, this backend
|
||||||
|
doesn't implement any authentication mechanisms, so anyone who can connect to the TCP port will be able to
|
||||||
|
execute commands on your Platypush instance.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Maximum length of a request to be processed
|
# Maximum length of a request to be processed
|
||||||
|
|
|
@ -3,31 +3,21 @@ import time
|
||||||
|
|
||||||
from platypush.backend import Backend
|
from platypush.backend import Backend
|
||||||
from platypush.context import get_plugin
|
from platypush.context import get_plugin
|
||||||
from platypush.message.event.todoist import NewItemEvent, RemovedItemEvent, ModifiedItemEvent, CheckedItemEvent, \
|
from platypush.message.event.todoist import (
|
||||||
ItemContentChangeEvent, TodoistSyncRequiredEvent
|
NewItemEvent,
|
||||||
|
RemovedItemEvent,
|
||||||
|
ModifiedItemEvent,
|
||||||
|
CheckedItemEvent,
|
||||||
|
ItemContentChangeEvent,
|
||||||
|
TodoistSyncRequiredEvent,
|
||||||
|
)
|
||||||
|
|
||||||
from platypush.plugins.todoist import TodoistPlugin
|
from platypush.plugins.todoist import TodoistPlugin
|
||||||
|
|
||||||
|
|
||||||
class TodoistBackend(Backend):
|
class TodoistBackend(Backend):
|
||||||
"""
|
"""
|
||||||
This backend listens for events on a remote Todoist account.
|
This backend listens for events on a Todoist account.
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **todoist-python** (``pip install todoist-python``)
|
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
* :class:`platypush.message.event.todoist.NewItemEvent` when a new item is created.
|
|
||||||
* :class:`platypush.message.event.todoist.RemovedItemEvent` when an item is removed.
|
|
||||||
* :class:`platypush.message.event.todoist.CheckedItemEvent` when an item is checked.
|
|
||||||
* :class:`platypush.message.event.todoist.ItemContentChangeEvent` when the content of an item is changed.
|
|
||||||
* :class:`platypush.message.event.todoist.ModifiedItemEvent` when an item is changed and the change
|
|
||||||
doesn't fall into the categories above.
|
|
||||||
* :class:`platypush.message.event.todoist.TodoistSyncRequiredEvent` when an update has occurred that doesn't
|
|
||||||
fall into the categories above and a sync is required to get up-to-date.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, api_token: str = None, **kwargs):
|
def __init__(self, api_token: str = None, **kwargs):
|
||||||
|
@ -35,7 +25,9 @@ class TodoistBackend(Backend):
|
||||||
self._plugin: TodoistPlugin = get_plugin('todoist')
|
self._plugin: TodoistPlugin = get_plugin('todoist')
|
||||||
|
|
||||||
if not api_token:
|
if not api_token:
|
||||||
assert self._plugin and self._plugin.api_token, 'No api_token specified either on Todoist backend or plugin'
|
assert (
|
||||||
|
self._plugin and self._plugin.api_token
|
||||||
|
), 'No api_token specified either on Todoist backend or plugin'
|
||||||
self.api_token = self._plugin.api_token
|
self.api_token = self._plugin.api_token
|
||||||
else:
|
else:
|
||||||
self.api_token = api_token
|
self.api_token = api_token
|
||||||
|
@ -97,16 +89,15 @@ class TodoistBackend(Backend):
|
||||||
import websocket
|
import websocket
|
||||||
|
|
||||||
if not self._ws:
|
if not self._ws:
|
||||||
self._ws = websocket.WebSocketApp(self.url,
|
self._ws = websocket.WebSocketApp(
|
||||||
on_message=self._on_msg(),
|
self.url,
|
||||||
on_error=self._on_error(),
|
on_message=self._on_msg(),
|
||||||
on_close=self._on_close())
|
on_error=self._on_error(),
|
||||||
|
on_close=self._on_close(),
|
||||||
|
)
|
||||||
|
|
||||||
def _refresh_items(self):
|
def _refresh_items(self):
|
||||||
new_items = {
|
new_items = {i['id']: i for i in self._plugin.get_items().output}
|
||||||
i['id']: i
|
|
||||||
for i in self._plugin.get_items().output
|
|
||||||
}
|
|
||||||
|
|
||||||
if self._todoist_initialized:
|
if self._todoist_initialized:
|
||||||
for id, item in new_items.items():
|
for id, item in new_items.items():
|
||||||
|
|
|
@ -34,13 +34,6 @@ class TrelloBackend(Backend):
|
||||||
|
|
||||||
* The :class:`platypush.plugins.trello.TrelloPlugin` configured.
|
* The :class:`platypush.plugins.trello.TrelloPlugin` configured.
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
* :class:`platypush.message.event.trello.NewCardEvent` when a card is created.
|
|
||||||
* :class:`platypush.message.event.trello.MoveCardEvent` when a card is moved.
|
|
||||||
* :class:`platypush.message.event.trello.ArchivedCardEvent` when a card is archived/closed.
|
|
||||||
* :class:`platypush.message.event.trello.UnarchivedCardEvent` when a card is un-archived/opened.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_websocket_url_base = 'wss://trello.com/1/Session/socket?token={token}'
|
_websocket_url_base = 'wss://trello.com/1/Session/socket?token={token}'
|
||||||
|
|
|
@ -2,7 +2,10 @@ import time
|
||||||
|
|
||||||
from platypush.backend import Backend
|
from platypush.backend import Backend
|
||||||
from platypush.context import get_plugin
|
from platypush.context import get_plugin
|
||||||
from platypush.message.event.weather import NewWeatherConditionEvent, NewPrecipitationForecastEvent
|
from platypush.message.event.weather import (
|
||||||
|
NewWeatherConditionEvent,
|
||||||
|
NewPrecipitationForecastEvent,
|
||||||
|
)
|
||||||
from platypush.plugins.weather.buienradar import WeatherBuienradarPlugin
|
from platypush.plugins.weather.buienradar import WeatherBuienradarPlugin
|
||||||
|
|
||||||
|
|
||||||
|
@ -10,10 +13,6 @@ class WeatherBuienradarBackend(Backend):
|
||||||
"""
|
"""
|
||||||
Buienradar weather forecast backend. Listens for new weather or precipitation updates.
|
Buienradar weather forecast backend. Listens for new weather or precipitation updates.
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
* :class:`platypush.message.event.weather.NewWeatherConditionEvent` when there is a weather condition update
|
|
||||||
|
|
||||||
Requires:
|
Requires:
|
||||||
|
|
||||||
* The :mod:`platypush.plugins.weather.buienradar` plugin configured
|
* The :mod:`platypush.plugins.weather.buienradar` plugin configured
|
||||||
|
@ -37,16 +36,24 @@ class WeatherBuienradarBackend(Backend):
|
||||||
del weather['measured']
|
del weather['measured']
|
||||||
|
|
||||||
if precip != self.last_precip:
|
if precip != self.last_precip:
|
||||||
self.bus.post(NewPrecipitationForecastEvent(plugin_name='weather.buienradar',
|
self.bus.post(
|
||||||
average=precip.get('average'),
|
NewPrecipitationForecastEvent(
|
||||||
total=precip.get('total'),
|
plugin_name='weather.buienradar',
|
||||||
time_frame=precip.get('time_frame')))
|
average=precip.get('average'),
|
||||||
|
total=precip.get('total'),
|
||||||
|
time_frame=precip.get('time_frame'),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if weather != self.last_weather:
|
if weather != self.last_weather:
|
||||||
self.bus.post(NewWeatherConditionEvent(**{
|
self.bus.post(
|
||||||
**weather,
|
NewWeatherConditionEvent(
|
||||||
'plugin_name': 'weather.buienradar',
|
**{
|
||||||
}))
|
**weather,
|
||||||
|
'plugin_name': 'weather.buienradar',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
self.last_weather = weather
|
self.last_weather = weather
|
||||||
self.last_precip = precip
|
self.last_precip = precip
|
||||||
|
|
|
@ -5,10 +5,6 @@ class WeatherDarkskyBackend(WeatherBackend):
|
||||||
"""
|
"""
|
||||||
Weather forecast backend that leverages the DarkSky API.
|
Weather forecast backend that leverages the DarkSky API.
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
* :class:`platypush.message.event.weather.NewWeatherConditionEvent` when there is a weather condition update
|
|
||||||
|
|
||||||
Requires:
|
Requires:
|
||||||
|
|
||||||
* The :class:`platypush.plugins.weather.darksky.WeatherDarkskyPlugin` plugin configured
|
* The :class:`platypush.plugins.weather.darksky.WeatherDarkskyPlugin` plugin configured
|
||||||
|
@ -19,7 +15,9 @@ class WeatherDarkskyBackend(WeatherBackend):
|
||||||
"""
|
"""
|
||||||
:param poll_seconds: How often the backend should check for updates (default: every 5 minutes).
|
:param poll_seconds: How often the backend should check for updates (default: every 5 minutes).
|
||||||
"""
|
"""
|
||||||
super().__init__(plugin_name='weather.darksky', poll_seconds=poll_seconds, **kwargs)
|
super().__init__(
|
||||||
|
plugin_name='weather.darksky', poll_seconds=poll_seconds, **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
|
@ -5,10 +5,6 @@ class WeatherOpenweathermapBackend(WeatherBackend):
|
||||||
"""
|
"""
|
||||||
Weather forecast backend that leverages the OpenWeatherMap API.
|
Weather forecast backend that leverages the OpenWeatherMap API.
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
* :class:`platypush.message.event.weather.NewWeatherConditionEvent` when there is a weather condition update
|
|
||||||
|
|
||||||
Requires:
|
Requires:
|
||||||
|
|
||||||
* The :class:`platypush.plugins.weather.openweathermap.WeatherOpenWeatherMapPlugin` plugin configured
|
* The :class:`platypush.plugins.weather.openweathermap.WeatherOpenWeatherMapPlugin` plugin configured
|
||||||
|
@ -19,7 +15,9 @@ class WeatherOpenweathermapBackend(WeatherBackend):
|
||||||
"""
|
"""
|
||||||
:param poll_seconds: How often the backend should check for updates (default: every minute).
|
:param poll_seconds: How often the backend should check for updates (default: every minute).
|
||||||
"""
|
"""
|
||||||
super().__init__(plugin_name='weather.openweathermap', poll_seconds=poll_seconds, **kwargs)
|
super().__init__(
|
||||||
|
plugin_name='weather.openweathermap', poll_seconds=poll_seconds, **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
|
@ -13,14 +13,10 @@ class WiimoteBackend(Backend):
|
||||||
"""
|
"""
|
||||||
Backend to communicate with a Nintendo WiiMote controller
|
Backend to communicate with a Nintendo WiiMote controller
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
* :class:`platypush.message.event.wiimote.WiimoteEvent` \
|
|
||||||
when the state of the Wiimote (battery, buttons, acceleration etc.) changes
|
|
||||||
|
|
||||||
Requires:
|
Requires:
|
||||||
|
|
||||||
* **python3-wiimote** (follow instructions at https://github.com/azzra/python3-wiimote)
|
* **python3-wiimote** (follow instructions at https://github.com/azzra/python3-wiimote)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_wiimote = None
|
_wiimote = None
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
manifest:
|
manifest:
|
||||||
events:
|
events:
|
||||||
platypush.message.event.wiimote.WiimoteEvent: when the state of the Wiimote (battery,
|
- platypush.message.event.wiimote.WiimoteEvent
|
||||||
buttons, acceleration etc.) changes
|
|
||||||
install:
|
install:
|
||||||
apt:
|
apt:
|
||||||
- libcwiid1
|
- libcwiid1
|
||||||
|
|
|
@ -1,34 +0,0 @@
|
||||||
import warnings
|
|
||||||
|
|
||||||
from platypush.backend import Backend
|
|
||||||
|
|
||||||
|
|
||||||
class ZwaveMqttBackend(Backend):
|
|
||||||
"""
|
|
||||||
Listen for events on a zwave2mqtt service.
|
|
||||||
|
|
||||||
**WARNING**: This backend is **DEPRECATED** and it will be removed in a
|
|
||||||
future version.
|
|
||||||
|
|
||||||
It has been merged with
|
|
||||||
:class:`platypush.plugins.zwave.mqtt.ZwaveMqttPlugin`.
|
|
||||||
|
|
||||||
Now you can simply configure the `zwave.mqtt` plugin in order to enable
|
|
||||||
the Zwave integration - no need to enable both the plugin and the backend.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
super().run()
|
|
||||||
warnings.warn(
|
|
||||||
'''
|
|
||||||
The zwave.mqtt backend has been merged into the zwave.mqtt plugin.
|
|
||||||
It is now deprecated and it will be removed in a future version.
|
|
||||||
Please remove any references to it from your configuration.
|
|
||||||
''',
|
|
||||||
DeprecationWarning,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.wait_stop()
|
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
|
|
@ -1,28 +0,0 @@
|
||||||
manifest:
|
|
||||||
events:
|
|
||||||
platypush.message.event.zwave.ZwaveNodeAddedEvent: when a node is added to the
|
|
||||||
network.
|
|
||||||
platypush.message.event.zwave.ZwaveNodeAsleepEvent: when a node goes into sleep
|
|
||||||
mode.
|
|
||||||
platypush.message.event.zwave.ZwaveNodeAwakeEvent: when a node goes back into
|
|
||||||
awake mode.
|
|
||||||
platypush.message.event.zwave.ZwaveNodeEvent: when a node attribute changes.
|
|
||||||
platypush.message.event.zwave.ZwaveNodeReadyEvent: when a node is ready.
|
|
||||||
platypush.message.event.zwave.ZwaveNodeRemovedEvent: when a node is removed from
|
|
||||||
the network.
|
|
||||||
platypush.message.event.zwave.ZwaveNodeRenamedEvent: when a node is renamed.
|
|
||||||
platypush.message.event.zwave.ZwaveValueChangedEvent: when the value of a node
|
|
||||||
on the networkchanges.
|
|
||||||
install:
|
|
||||||
apk:
|
|
||||||
- py3-paho-mqtt
|
|
||||||
dnf:
|
|
||||||
- python-paho-mqtt
|
|
||||||
pacman:
|
|
||||||
- python-paho-mqtt
|
|
||||||
apt:
|
|
||||||
- python3-paho-mqtt
|
|
||||||
pip:
|
|
||||||
- paho-mqtt
|
|
||||||
package: platypush.backend.zwave.mqtt
|
|
||||||
type: backend
|
|
|
@ -18,11 +18,6 @@ class AdafruitIoPlugin(Plugin):
|
||||||
You can send values to feeds on your Adafruit IO account and read the
|
You can send values to feeds on your Adafruit IO account and read the
|
||||||
values of those feeds as well through any device.
|
values of those feeds as well through any device.
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **adafruit-io** (``pip install adafruit-io``)
|
|
||||||
* Redis server running and Redis backend configured if you want to enable throttling
|
|
||||||
|
|
||||||
Some example usages::
|
Some example usages::
|
||||||
|
|
||||||
# Send the temperature value for a connected sensor to the "temperature" feed
|
# Send the temperature value for a connected sensor to the "temperature" feed
|
||||||
|
@ -63,6 +58,7 @@ class AdafruitIoPlugin(Plugin):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from Adafruit_IO import Client
|
from Adafruit_IO import Client
|
||||||
|
|
||||||
global data_throttler_lock
|
global data_throttler_lock
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
@ -109,15 +105,19 @@ class AdafruitIoPlugin(Plugin):
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
new_data = ast.literal_eval(
|
new_data = ast.literal_eval(
|
||||||
redis.blpop(self._DATA_THROTTLER_QUEUE)[1].decode('utf-8'))
|
redis.blpop(self._DATA_THROTTLER_QUEUE)[1].decode('utf-8')
|
||||||
|
)
|
||||||
|
|
||||||
for (key, value) in new_data.items():
|
for (key, value) in new_data.items():
|
||||||
data.setdefault(key, []).append(value)
|
data.setdefault(key, []).append(value)
|
||||||
except QueueTimeoutError:
|
except QueueTimeoutError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if data and (last_processed_batch_timestamp is None or
|
if data and (
|
||||||
time.time() - last_processed_batch_timestamp >= self.throttle_seconds):
|
last_processed_batch_timestamp is None
|
||||||
|
or time.time() - last_processed_batch_timestamp
|
||||||
|
>= self.throttle_seconds
|
||||||
|
):
|
||||||
last_processed_batch_timestamp = time.time()
|
last_processed_batch_timestamp = time.time()
|
||||||
self.logger.info('Processing feeds batch for Adafruit IO')
|
self.logger.info('Processing feeds batch for Adafruit IO')
|
||||||
|
|
||||||
|
@ -128,8 +128,10 @@ class AdafruitIoPlugin(Plugin):
|
||||||
try:
|
try:
|
||||||
self.send(feed, value, enqueue=False)
|
self.send(feed, value, enqueue=False)
|
||||||
except ThrottlingError:
|
except ThrottlingError:
|
||||||
self.logger.warning('Adafruit IO throttling threshold hit, taking a nap ' +
|
self.logger.warning(
|
||||||
'before retrying')
|
'Adafruit IO throttling threshold hit, taking a nap '
|
||||||
|
+ 'before retrying'
|
||||||
|
)
|
||||||
time.sleep(self.throttle_seconds)
|
time.sleep(self.throttle_seconds)
|
||||||
|
|
||||||
data = {}
|
data = {}
|
||||||
|
@ -184,11 +186,15 @@ class AdafruitIoPlugin(Plugin):
|
||||||
:type value: Numeric or string
|
:type value: Numeric or string
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.aio.send_data(feed=feed, value=value, metadata={
|
self.aio.send_data(
|
||||||
'lat': lat,
|
feed=feed,
|
||||||
'lon': lon,
|
value=value,
|
||||||
'ele': ele,
|
metadata={
|
||||||
})
|
'lat': lat,
|
||||||
|
'lon': lon,
|
||||||
|
'ele': ele,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _cast_value(cls, value):
|
def _cast_value(cls, value):
|
||||||
|
@ -205,9 +211,12 @@ class AdafruitIoPlugin(Plugin):
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
attr: self._cast_value(getattr(i, attr))
|
attr: self._cast_value(getattr(i, attr))
|
||||||
if attr == 'value' else getattr(i, attr)
|
if attr == 'value'
|
||||||
for attr in DATA_FIELDS if getattr(i, attr) is not None
|
else getattr(i, attr)
|
||||||
} for i in data
|
for attr in DATA_FIELDS
|
||||||
|
if getattr(i, attr) is not None
|
||||||
|
}
|
||||||
|
for i in data
|
||||||
]
|
]
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
|
|
@ -58,17 +58,6 @@ class ArduinoPlugin(SensorPlugin):
|
||||||
Download and flash the
|
Download and flash the
|
||||||
`Standard Firmata <https://github.com/firmata/arduino/blob/master/examples/StandardFirmata/StandardFirmata.ino>`_
|
`Standard Firmata <https://github.com/firmata/arduino/blob/master/examples/StandardFirmata/StandardFirmata.ino>`_
|
||||||
firmware to the Arduino in order to use this plugin.
|
firmware to the Arduino in order to use this plugin.
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **pyfirmata2** (``pip install pyfirmata2``)
|
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
* :class:`platypush.message.event.sensor.SensorDataAboveThresholdEvent`
|
|
||||||
* :class:`platypush.message.event.sensor.SensorDataBelowThresholdEvent`
|
|
||||||
* :class:`platypush.message.event.sensor.SensorDataChangeEvent`
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|
|
@ -25,18 +25,6 @@ class AssistantEchoPlugin(AssistantPlugin):
|
||||||
4. Log in to your Amazon account
|
4. Log in to your Amazon account
|
||||||
5. The required credentials will be stored to ~/.avs.json
|
5. The required credentials will be stored to ~/.avs.json
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
* :class:`platypush.message.event.assistant.ConversationStartEvent`
|
|
||||||
when a new conversation starts
|
|
||||||
* :class:`platypush.message.event.assistant.SpeechRecognizedEvent`
|
|
||||||
when a new voice command is recognized
|
|
||||||
* :class:`platypush.message.event.assistant.ConversationEndEvent`
|
|
||||||
when a new conversation ends
|
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **avs** (``pip install avs``)
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|
|
@ -20,22 +20,6 @@ from platypush.plugins.assistant import AssistantPlugin
|
||||||
class AssistantGooglePushtotalkPlugin(AssistantPlugin):
|
class AssistantGooglePushtotalkPlugin(AssistantPlugin):
|
||||||
"""
|
"""
|
||||||
Plugin for the Google Assistant push-to-talk API.
|
Plugin for the Google Assistant push-to-talk API.
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
* :class:`platypush.message.event.assistant.ConversationStartEvent`
|
|
||||||
when a new conversation starts
|
|
||||||
* :class:`platypush.message.event.assistant.SpeechRecognizedEvent`
|
|
||||||
when a new voice command is recognized
|
|
||||||
* :class:`platypush.message.event.assistant.ConversationEndEvent`
|
|
||||||
when a new conversation ends
|
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **tenacity** (``pip install tenacity``)
|
|
||||||
* **google-assistant-sdk** (``pip install google-assistant-sdk[samples]``)
|
|
||||||
* **google-auth** (``pip install google-auth``)
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
api_endpoint = 'embeddedassistant.googleapis.com'
|
api_endpoint = 'embeddedassistant.googleapis.com'
|
||||||
|
|
|
@ -1,3 +1,650 @@
|
||||||
from ._plugin import BluetoothPlugin
|
import base64
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from queue import Empty, Queue
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from typing import (
|
||||||
|
Any,
|
||||||
|
Collection,
|
||||||
|
Dict,
|
||||||
|
Final,
|
||||||
|
List,
|
||||||
|
Optional,
|
||||||
|
Union,
|
||||||
|
Type,
|
||||||
|
)
|
||||||
|
|
||||||
|
from platypush.common import StoppableThread
|
||||||
|
from platypush.context import get_bus, get_plugin
|
||||||
|
from platypush.entities import (
|
||||||
|
EnumSwitchEntityManager,
|
||||||
|
get_entities_engine,
|
||||||
|
)
|
||||||
|
from platypush.entities.bluetooth import BluetoothDevice, BluetoothService
|
||||||
|
from platypush.message.event.bluetooth import (
|
||||||
|
BluetoothScanPausedEvent,
|
||||||
|
BluetoothScanResumedEvent,
|
||||||
|
)
|
||||||
|
from platypush.plugins import RunnablePlugin, action
|
||||||
|
from platypush.plugins.db import DbPlugin
|
||||||
|
|
||||||
|
from ._ble import BLEManager
|
||||||
|
from ._cache import EntityCache
|
||||||
|
from ._legacy import LegacyManager
|
||||||
|
from ._types import DevicesBlacklist, RawServiceClass
|
||||||
|
from ._manager import BaseBluetoothManager
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=too-many-ancestors
|
||||||
|
class BluetoothPlugin(RunnablePlugin, EnumSwitchEntityManager):
|
||||||
|
"""
|
||||||
|
Plugin to interact with Bluetooth devices.
|
||||||
|
|
||||||
|
This plugin uses `Bleak <https://github.com/hbldh/bleak>`_ to interact
|
||||||
|
with the Bluetooth stack and `Theengs <https://github.com/theengs/decoder>`_
|
||||||
|
to map the services exposed by the devices into native entities.
|
||||||
|
|
||||||
|
The full list of devices natively supported can be found
|
||||||
|
`here <https://decoder.theengs.io/devices/devices_by_brand.html>`_.
|
||||||
|
|
||||||
|
It also supports legacy Bluetooth services, as well as the transfer of
|
||||||
|
files.
|
||||||
|
|
||||||
|
Note that the support for Bluetooth low-energy devices requires a Bluetooth
|
||||||
|
adapter compatible with the Bluetooth 5.0 specification or higher.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_default_connect_timeout: Final[int] = 20
|
||||||
|
""" Default connection timeout (in seconds) """
|
||||||
|
|
||||||
|
_default_scan_duration: Final[float] = 10.0
|
||||||
|
""" Default duration of a discovery session (in seconds) """
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
interface: Optional[str] = None,
|
||||||
|
connect_timeout: float = _default_connect_timeout,
|
||||||
|
service_uuids: Optional[Collection[RawServiceClass]] = None,
|
||||||
|
scan_paused_on_start: bool = False,
|
||||||
|
poll_interval: float = _default_scan_duration,
|
||||||
|
exclude_known_noisy_beacons: bool = True,
|
||||||
|
ignored_device_addresses: Optional[Collection[str]] = None,
|
||||||
|
ignored_device_names: Optional[Collection[str]] = None,
|
||||||
|
ignored_device_manufacturers: Optional[Collection[str]] = None,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
:param interface: Name of the Bluetooth interface to use (e.g. ``hci0``
|
||||||
|
on Linux). Default: first available interface.
|
||||||
|
:param connect_timeout: Timeout in seconds for the connection to a
|
||||||
|
Bluetooth device. Default: 20 seconds.
|
||||||
|
:param service_uuids: List of service UUIDs to discover.
|
||||||
|
Default: all.
|
||||||
|
:param scan_paused_on_start: If ``True``, the plugin will not the
|
||||||
|
scanning thread until :meth:`.scan_resume` is called (default:
|
||||||
|
``False``).
|
||||||
|
:param exclude_known_noisy_beacons: Exclude BLE beacons from devices
|
||||||
|
known for being very noisy. It mainly includes tracking services on
|
||||||
|
Google, Apple, Microsoft and Samsung devices. These devices are
|
||||||
|
also known for refreshing their MAC address very frequently, which
|
||||||
|
may result in a large (and constantly increasing) list of devices.
|
||||||
|
Disable this flag if you need to track BLE beacons from these
|
||||||
|
devices, but beware that you may need periodically clean up your
|
||||||
|
list of scanned devices.
|
||||||
|
:param ignored_device_addresses: List of device addresses to ignore.
|
||||||
|
:param ignored_device_names: List of device names to ignore.
|
||||||
|
:param ignored_device_manufacturers: List of device manufacturers to
|
||||||
|
ignore.
|
||||||
|
"""
|
||||||
|
kwargs['poll_interval'] = poll_interval
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
self._interface: Optional[str] = interface
|
||||||
|
""" Default Bluetooth interface to use """
|
||||||
|
self._connect_timeout: float = connect_timeout
|
||||||
|
""" Connection timeout in seconds """
|
||||||
|
self._service_uuids: Collection[RawServiceClass] = service_uuids or []
|
||||||
|
""" UUIDs to discover """
|
||||||
|
self._scan_lock = threading.RLock()
|
||||||
|
""" Lock to synchronize scanning access to the Bluetooth device """
|
||||||
|
self._scan_enabled = threading.Event()
|
||||||
|
""" Event used to enable/disable scanning """
|
||||||
|
self._device_queue: Queue[BluetoothDevice] = Queue()
|
||||||
|
"""
|
||||||
|
Queue used by the Bluetooth managers to published the discovered
|
||||||
|
Bluetooth devices.
|
||||||
|
"""
|
||||||
|
self._device_cache = EntityCache()
|
||||||
|
"""
|
||||||
|
Cache of the devices discovered by the plugin.
|
||||||
|
"""
|
||||||
|
self._excluded_known_noisy_beacons = exclude_known_noisy_beacons
|
||||||
|
""" Exclude known noisy BLE beacons. """
|
||||||
|
|
||||||
|
self._blacklist = DevicesBlacklist(
|
||||||
|
addresses=set(ignored_device_addresses or []),
|
||||||
|
names=set(ignored_device_names or []),
|
||||||
|
manufacturers=set(ignored_device_manufacturers or []),
|
||||||
|
)
|
||||||
|
""" Blacklist rules for the devices to ignore. """
|
||||||
|
|
||||||
|
self._managers: Dict[Type[BaseBluetoothManager], BaseBluetoothManager] = {}
|
||||||
|
"""
|
||||||
|
Bluetooth managers threads, one for BLE devices and one for non-BLE
|
||||||
|
devices.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._scan_controller_timer: Optional[threading.Timer] = None
|
||||||
|
""" Timer used to temporarily pause the discovery process """
|
||||||
|
|
||||||
|
if not scan_paused_on_start:
|
||||||
|
self._scan_enabled.set()
|
||||||
|
|
||||||
|
def _refresh_cache(self) -> None:
|
||||||
|
# Wait for the entities engine to start
|
||||||
|
get_entities_engine().wait_start()
|
||||||
|
|
||||||
|
with get_plugin(DbPlugin).get_session(
|
||||||
|
autoflush=False, autocommit=False, expire_on_commit=False
|
||||||
|
) as session:
|
||||||
|
existing_devices = [d.copy() for d in session.query(BluetoothDevice).all()]
|
||||||
|
|
||||||
|
for dev in existing_devices:
|
||||||
|
self._device_cache.add(dev)
|
||||||
|
|
||||||
|
def _init_bluetooth_managers(self):
|
||||||
|
"""
|
||||||
|
Initializes the Bluetooth managers threads.
|
||||||
|
"""
|
||||||
|
manager_args = {
|
||||||
|
'interface': self._interface,
|
||||||
|
'poll_interval': self.poll_interval,
|
||||||
|
'connect_timeout': self._connect_timeout,
|
||||||
|
'stop_event': self._should_stop,
|
||||||
|
'scan_lock': self._scan_lock,
|
||||||
|
'scan_enabled': self._scan_enabled,
|
||||||
|
'device_queue': self._device_queue,
|
||||||
|
'service_uuids': list(map(BluetoothService.to_uuid, self._service_uuids)),
|
||||||
|
'device_cache': self._device_cache,
|
||||||
|
'exclude_known_noisy_beacons': self._excluded_known_noisy_beacons,
|
||||||
|
'blacklist': self._blacklist,
|
||||||
|
}
|
||||||
|
|
||||||
|
self._managers = {
|
||||||
|
BLEManager: BLEManager(**manager_args),
|
||||||
|
LegacyManager: LegacyManager(**manager_args),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _scan_state_set(self, state: bool, duration: Optional[float] = None):
|
||||||
|
"""
|
||||||
|
Set the state of the scanning process.
|
||||||
|
|
||||||
|
:param state: ``True`` to enable the scanning process, ``False`` to
|
||||||
|
disable it.
|
||||||
|
:param duration: The duration of the pause (in seconds) or ``None``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def timer_callback():
|
||||||
|
if state:
|
||||||
|
self.scan_pause()
|
||||||
|
else:
|
||||||
|
self.scan_resume()
|
||||||
|
|
||||||
|
self._scan_controller_timer = None
|
||||||
|
|
||||||
|
with self._scan_lock:
|
||||||
|
if not state and self._scan_enabled.is_set():
|
||||||
|
get_bus().post(BluetoothScanPausedEvent(duration=duration))
|
||||||
|
elif state and not self._scan_enabled.is_set():
|
||||||
|
get_bus().post(BluetoothScanResumedEvent(duration=duration))
|
||||||
|
|
||||||
|
if state:
|
||||||
|
self._scan_enabled.set()
|
||||||
|
else:
|
||||||
|
self._scan_enabled.clear()
|
||||||
|
|
||||||
|
if duration and not self._scan_controller_timer:
|
||||||
|
self._scan_controller_timer = threading.Timer(duration, timer_callback)
|
||||||
|
self._scan_controller_timer.start()
|
||||||
|
|
||||||
|
def _cancel_scan_controller_timer(self):
|
||||||
|
"""
|
||||||
|
Cancels a scan controller timer if scheduled.
|
||||||
|
"""
|
||||||
|
if self._scan_controller_timer:
|
||||||
|
self._scan_controller_timer.cancel()
|
||||||
|
|
||||||
|
def _manager_by_device(
|
||||||
|
self,
|
||||||
|
device: BluetoothDevice,
|
||||||
|
port: Optional[int] = None,
|
||||||
|
service_uuid: Optional[Union[str, RawServiceClass]] = None,
|
||||||
|
) -> BaseBluetoothManager:
|
||||||
|
"""
|
||||||
|
:param device: A discovered Bluetooth device.
|
||||||
|
:param port: The port to connect to.
|
||||||
|
:param service_uuid: The UUID of the service to connect to.
|
||||||
|
:return: The manager associated with the device (BLE or legacy).
|
||||||
|
"""
|
||||||
|
# No port nor service UUID -> use the BLE manager for direct connection
|
||||||
|
if not (port or service_uuid):
|
||||||
|
return self._managers[BLEManager]
|
||||||
|
|
||||||
|
uuid = BluetoothService.to_uuid(service_uuid) if service_uuid else None
|
||||||
|
matching_services = (
|
||||||
|
[srv for srv in device.services if srv.port == port]
|
||||||
|
if port
|
||||||
|
else [srv for srv in device.services if srv.uuid == uuid]
|
||||||
|
)
|
||||||
|
|
||||||
|
if not matching_services:
|
||||||
|
# It could be a GATT characteristic, so try BLE
|
||||||
|
return self._managers[BLEManager]
|
||||||
|
|
||||||
|
srv = matching_services[0]
|
||||||
|
return (
|
||||||
|
self._managers[BLEManager] if srv.is_ble else self._managers[LegacyManager]
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_device(self, device: str, _fail_if_not_cached=False) -> BluetoothDevice:
|
||||||
|
"""
|
||||||
|
Get a device by its address or name, and scan for it if it's not
|
||||||
|
cached.
|
||||||
|
"""
|
||||||
|
# If device is a compound entity ID in the format
|
||||||
|
# ``<mac_address>:<service>``, then split the MAC address part
|
||||||
|
m = re.match(r'^(([0-9a-f]{2}:){6}):.*', device, re.IGNORECASE)
|
||||||
|
if m:
|
||||||
|
device = m.group(1).rstrip(':')
|
||||||
|
|
||||||
|
dev = self._device_cache.get(device)
|
||||||
|
if dev:
|
||||||
|
return dev
|
||||||
|
|
||||||
|
assert not _fail_if_not_cached, f'Device {device} not found'
|
||||||
|
self.logger.info('Scanning for unknown device %s', device)
|
||||||
|
self.scan()
|
||||||
|
return self._get_device(device, _fail_if_not_cached=True)
|
||||||
|
|
||||||
|
@action
|
||||||
|
def connect(
|
||||||
|
self,
|
||||||
|
device: str,
|
||||||
|
port: Optional[int] = None,
|
||||||
|
service_uuid: Optional[Union[RawServiceClass, str]] = None,
|
||||||
|
interface: Optional[str] = None,
|
||||||
|
timeout: Optional[float] = None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Pair and connect to a device by address or name.
|
||||||
|
|
||||||
|
:param device: The device address or name.
|
||||||
|
:param port: The port to connect to. Either ``port`` or
|
||||||
|
``service_uuid`` is required for non-BLE devices.
|
||||||
|
:param service_uuid: The UUID of the service to connect to. Either
|
||||||
|
``port`` or ``service_uuid`` is required for non-BLE devices.
|
||||||
|
:param interface: The Bluetooth interface to use (it overrides the
|
||||||
|
default ``interface``).
|
||||||
|
:param timeout: The connection timeout in seconds (it overrides the
|
||||||
|
default ``connect_timeout``).
|
||||||
|
"""
|
||||||
|
dev = self._get_device(device)
|
||||||
|
manager = self._manager_by_device(dev, port=port, service_uuid=service_uuid)
|
||||||
|
uuid = BluetoothService.to_uuid(service_uuid) if service_uuid else None
|
||||||
|
manager.connect(
|
||||||
|
dev.address,
|
||||||
|
port=port,
|
||||||
|
service_uuid=uuid,
|
||||||
|
interface=interface,
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
|
||||||
|
@action
|
||||||
|
def disconnect(
|
||||||
|
self,
|
||||||
|
device: str,
|
||||||
|
port: Optional[int] = None,
|
||||||
|
service_uuid: Optional[RawServiceClass] = None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Close an active connection to a device.
|
||||||
|
|
||||||
|
Note that this method can only close connections that have been
|
||||||
|
initiated by the application. It can't close connections owned by
|
||||||
|
other applications or agents.
|
||||||
|
|
||||||
|
:param device: The device address or name.
|
||||||
|
:param port: If connected to a non-BLE device, the optional port to
|
||||||
|
disconnect.
|
||||||
|
:param service_uuid: The optional UUID of the service to disconnect
|
||||||
|
from, for non-BLE devices.
|
||||||
|
"""
|
||||||
|
dev = self._get_device(device)
|
||||||
|
uuid = BluetoothService.to_uuid(service_uuid) if service_uuid else None
|
||||||
|
err = None
|
||||||
|
success = False
|
||||||
|
|
||||||
|
for manager in self._managers.values():
|
||||||
|
try:
|
||||||
|
manager.disconnect(dev.address, port=port, service_uuid=uuid)
|
||||||
|
success = True
|
||||||
|
except Exception as e:
|
||||||
|
err = e
|
||||||
|
|
||||||
|
assert success, f'Could not disconnect from {device}: {err}'
|
||||||
|
|
||||||
|
@action
|
||||||
|
def scan_pause(self, duration: Optional[float] = None):
|
||||||
|
"""
|
||||||
|
Pause the scanning thread.
|
||||||
|
|
||||||
|
:param duration: For how long the scanning thread should be paused
|
||||||
|
(default: null = indefinitely).
|
||||||
|
"""
|
||||||
|
self._scan_state_set(False, duration)
|
||||||
|
|
||||||
|
@action
|
||||||
|
def scan_resume(self, duration: Optional[float] = None):
|
||||||
|
"""
|
||||||
|
Resume the scanning thread, if inactive.
|
||||||
|
|
||||||
|
:param duration: For how long the scanning thread should be running
|
||||||
|
(default: null = indefinitely).
|
||||||
|
"""
|
||||||
|
self._scan_state_set(True, duration)
|
||||||
|
|
||||||
|
@action
|
||||||
|
def scan(
|
||||||
|
self,
|
||||||
|
duration: Optional[float] = None,
|
||||||
|
devices: Optional[Collection[str]] = None,
|
||||||
|
service_uuids: Optional[Collection[RawServiceClass]] = None,
|
||||||
|
) -> List[BluetoothDevice]:
|
||||||
|
"""
|
||||||
|
Scan for Bluetooth devices nearby and return the results as a list of
|
||||||
|
entities.
|
||||||
|
|
||||||
|
:param duration: Scan duration in seconds (default: same as the plugin's
|
||||||
|
`poll_interval` configuration parameter)
|
||||||
|
:param devices: List of device addresses or names to scan for.
|
||||||
|
:param service_uuids: List of service UUIDs to discover. Default: all.
|
||||||
|
"""
|
||||||
|
scanned_device_addresses = set()
|
||||||
|
duration = duration or self.poll_interval or self._default_scan_duration
|
||||||
|
uuids = {BluetoothService.to_uuid(uuid) for uuid in (service_uuids or [])}
|
||||||
|
|
||||||
|
for manager in self._managers.values():
|
||||||
|
scanned_device_addresses.update(
|
||||||
|
[
|
||||||
|
device.address
|
||||||
|
for device in manager.scan(duration=duration // len(self._managers))
|
||||||
|
if (not uuids or any(srv.uuid in uuids for srv in device.services))
|
||||||
|
and (
|
||||||
|
not devices
|
||||||
|
or device.address in devices
|
||||||
|
or device.name in devices
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
with get_plugin(DbPlugin).get_session(
|
||||||
|
autoflush=False, autocommit=False, expire_on_commit=False
|
||||||
|
) as session:
|
||||||
|
return [
|
||||||
|
d.copy()
|
||||||
|
for d in session.query(BluetoothDevice).all()
|
||||||
|
if d.address in scanned_device_addresses
|
||||||
|
]
|
||||||
|
|
||||||
|
@action
|
||||||
|
def read(
|
||||||
|
self,
|
||||||
|
device: str,
|
||||||
|
service_uuid: RawServiceClass,
|
||||||
|
interface: Optional[str] = None,
|
||||||
|
connect_timeout: Optional[float] = None,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Read a message from a device.
|
||||||
|
|
||||||
|
:param device: Name or address of the device to read from.
|
||||||
|
:param service_uuid: Service UUID.
|
||||||
|
:param interface: Bluetooth adapter name to use (default configured if None).
|
||||||
|
:param connect_timeout: Connection timeout in seconds (default: same as the
|
||||||
|
configured `connect_timeout`).
|
||||||
|
:return: The base64-encoded response received from the device.
|
||||||
|
"""
|
||||||
|
dev = self._get_device(device)
|
||||||
|
uuid = BluetoothService.to_uuid(service_uuid)
|
||||||
|
manager = self._manager_by_device(dev, service_uuid=uuid)
|
||||||
|
data = manager.read(
|
||||||
|
dev.address, uuid, interface=interface, connect_timeout=connect_timeout
|
||||||
|
)
|
||||||
|
return base64.b64encode(data).decode()
|
||||||
|
|
||||||
|
@action
|
||||||
|
def write(
|
||||||
|
self,
|
||||||
|
device: str,
|
||||||
|
data: str,
|
||||||
|
service_uuid: RawServiceClass,
|
||||||
|
interface: Optional[str] = None,
|
||||||
|
connect_timeout: Optional[float] = None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Writes data to a device
|
||||||
|
|
||||||
|
:param device: Name or address of the device to read from.
|
||||||
|
:param data: Data to be written, as a base64-encoded string.
|
||||||
|
:param service_uuid: Service UUID.
|
||||||
|
:param interface: Bluetooth adapter name to use (default configured if None)
|
||||||
|
:param connect_timeout: Connection timeout in seconds (default: same as the
|
||||||
|
configured `connect_timeout`).
|
||||||
|
"""
|
||||||
|
binary_data = base64.b64decode(data.encode())
|
||||||
|
dev = self._get_device(device)
|
||||||
|
uuid = BluetoothService.to_uuid(service_uuid)
|
||||||
|
manager = self._manager_by_device(dev, service_uuid=uuid)
|
||||||
|
manager.write(
|
||||||
|
dev.address,
|
||||||
|
binary_data,
|
||||||
|
service_uuid=uuid,
|
||||||
|
interface=interface,
|
||||||
|
connect_timeout=connect_timeout,
|
||||||
|
)
|
||||||
|
|
||||||
|
@action
|
||||||
|
def set(self, entity: str, value: Any, **_):
|
||||||
|
"""
|
||||||
|
Set the value of an entity.
|
||||||
|
|
||||||
|
This is currently only supported for SwitchBot devices, where the value
|
||||||
|
can be one among ``on``, ``off`` and ``press``.
|
||||||
|
|
||||||
|
:param entity: The entity to set the value for. It can be the full
|
||||||
|
entity ID in the format ``<mac-address>::<service>``, or just
|
||||||
|
the MAC address if the plugin supports it.
|
||||||
|
:param value: The value to set the entity to.
|
||||||
|
"""
|
||||||
|
device = self._get_device(entity)
|
||||||
|
matching_plugin = next(
|
||||||
|
iter(
|
||||||
|
plugin
|
||||||
|
for manager in self._managers.values()
|
||||||
|
for plugin in manager.plugins
|
||||||
|
if plugin.supports_device(device)
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
matching_plugin is not None
|
||||||
|
), f'Action `set` not supported on device {entity}'
|
||||||
|
|
||||||
|
method = getattr(matching_plugin, 'set', None)
|
||||||
|
assert method, f'The plugin {matching_plugin} does not support `set`'
|
||||||
|
return method(device, value)
|
||||||
|
|
||||||
|
@action
|
||||||
|
def send_file(
|
||||||
|
self,
|
||||||
|
file: str,
|
||||||
|
device: str,
|
||||||
|
data: Optional[Union[str, bytes, bytearray]] = None,
|
||||||
|
binary: bool = False,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Send a file to a device that exposes an OBEX Object Push service.
|
||||||
|
|
||||||
|
:param file: Path of the file to be sent. If ``data`` is specified
|
||||||
|
then ``file`` should include the proposed file on the
|
||||||
|
receiving host.
|
||||||
|
:param data: Alternatively to a file on disk you can send raw (string
|
||||||
|
or binary) content.
|
||||||
|
:param device: Device address or name.
|
||||||
|
:param binary: Set to true if data is a base64-encoded binary string.
|
||||||
|
"""
|
||||||
|
from ._file import FileSender
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
file = os.path.abspath(os.path.expanduser(file))
|
||||||
|
with open(file, 'rb') as f:
|
||||||
|
binary_data = f.read()
|
||||||
|
elif binary:
|
||||||
|
binary_data = base64.b64decode(
|
||||||
|
data.encode() if isinstance(data, str) else data
|
||||||
|
)
|
||||||
|
elif isinstance(data, str):
|
||||||
|
binary_data = data.encode()
|
||||||
|
else:
|
||||||
|
binary_data = data
|
||||||
|
|
||||||
|
sender = FileSender(self._managers[LegacyManager]) # type: ignore
|
||||||
|
sender.send_file(file, device, binary_data)
|
||||||
|
|
||||||
|
@action
|
||||||
|
def status(
|
||||||
|
self,
|
||||||
|
*_,
|
||||||
|
duration: Optional[float] = None,
|
||||||
|
devices: Optional[Collection[str]] = None,
|
||||||
|
service_uuids: Optional[Collection[RawServiceClass]] = None,
|
||||||
|
**__,
|
||||||
|
) -> List[BluetoothDevice]:
|
||||||
|
"""
|
||||||
|
Retrieve the status of all the devices, or the matching
|
||||||
|
devices/services.
|
||||||
|
|
||||||
|
If scanning is currently disabled, it will enable it and perform a
|
||||||
|
scan.
|
||||||
|
|
||||||
|
The differences between this method and :meth:`.scan` are:
|
||||||
|
|
||||||
|
1. :meth:`.status` will return the status of all the devices known
|
||||||
|
to the application, while :meth:`.scan` will return the status
|
||||||
|
only of the devices discovered in the provided time window.
|
||||||
|
|
||||||
|
2. :meth:`.status` will not initiate a new scan if scanning is
|
||||||
|
already enabled (it will only return the status of the known
|
||||||
|
devices), while :meth:`.scan` will initiate a new scan.
|
||||||
|
|
||||||
|
:param duration: Scan duration in seconds, if scanning is disabled
|
||||||
|
(default: same as the plugin's `poll_interval` configuration
|
||||||
|
parameter)
|
||||||
|
:param devices: List of device addresses or names to filter for.
|
||||||
|
Default: all.
|
||||||
|
:param service_uuids: List of service UUIDs to filter for. Default:
|
||||||
|
all.
|
||||||
|
"""
|
||||||
|
if not self._scan_enabled.is_set():
|
||||||
|
self.scan(
|
||||||
|
duration=duration,
|
||||||
|
devices=devices,
|
||||||
|
service_uuids=service_uuids,
|
||||||
|
)
|
||||||
|
|
||||||
|
with get_plugin(DbPlugin).get_session(
|
||||||
|
autoflush=False, autocommit=False, expire_on_commit=False
|
||||||
|
) as session:
|
||||||
|
known_devices = [
|
||||||
|
d.copy()
|
||||||
|
for d in session.query(BluetoothDevice).all()
|
||||||
|
if (not devices or d.address in devices or d.name in devices)
|
||||||
|
and (
|
||||||
|
not service_uuids
|
||||||
|
or any(str(srv.uuid) in service_uuids for srv in d.services)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
# Send entity update events to keep any asynchronous clients in sync
|
||||||
|
get_entities_engine().notify(*known_devices)
|
||||||
|
return known_devices
|
||||||
|
|
||||||
|
def transform_entities(
|
||||||
|
self, entities: Collection[BluetoothDevice]
|
||||||
|
) -> Collection[BluetoothDevice]:
|
||||||
|
return super().transform_entities(entities)
|
||||||
|
|
||||||
|
def main(self):
|
||||||
|
self._refresh_cache()
|
||||||
|
self._init_bluetooth_managers()
|
||||||
|
|
||||||
|
for manager in self._managers.values():
|
||||||
|
manager.start()
|
||||||
|
|
||||||
|
try:
|
||||||
|
while not self.should_stop():
|
||||||
|
try:
|
||||||
|
device = self._device_queue.get(timeout=1)
|
||||||
|
except Empty:
|
||||||
|
continue
|
||||||
|
|
||||||
|
device = self._device_cache.add(device)
|
||||||
|
self.publish_entities([device], callback=self._device_cache.add)
|
||||||
|
finally:
|
||||||
|
self.stop()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""
|
||||||
|
Upon stop request, it stops any pending scans and closes all active
|
||||||
|
connections.
|
||||||
|
"""
|
||||||
|
super().stop()
|
||||||
|
|
||||||
|
self._cancel_scan_controller_timer()
|
||||||
|
self._stop_threads(self._managers.values())
|
||||||
|
|
||||||
|
def _stop_threads(self, threads: Collection[StoppableThread], timeout: float = 5):
|
||||||
|
"""
|
||||||
|
Set the stop events on active threads and wait for them to stop.
|
||||||
|
"""
|
||||||
|
# Set the stop events and call `.stop`
|
||||||
|
for thread in threads:
|
||||||
|
if thread and thread.is_alive():
|
||||||
|
self.logger.info('Waiting for %s to stop', thread.name)
|
||||||
|
try:
|
||||||
|
thread.stop()
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.exception('Error while stopping %s: %s', thread.name, e)
|
||||||
|
|
||||||
|
# Wait for the manager threads to stop
|
||||||
|
wait_start = time.time()
|
||||||
|
|
||||||
|
for thread in threads:
|
||||||
|
if (
|
||||||
|
thread
|
||||||
|
and thread.ident != threading.current_thread().ident
|
||||||
|
and thread.is_alive()
|
||||||
|
):
|
||||||
|
thread.join(timeout=max(0, int(timeout - (time.time() - wait_start))))
|
||||||
|
|
||||||
|
if thread and thread.is_alive():
|
||||||
|
self.logger.warning(
|
||||||
|
'Timeout while waiting for %s to stop', thread.name
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["BluetoothPlugin"]
|
__all__ = ["BluetoothPlugin"]
|
||||||
|
|
||||||
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
|
@ -1,674 +0,0 @@
|
||||||
import base64
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
from queue import Empty, Queue
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
from typing import (
|
|
||||||
Any,
|
|
||||||
Collection,
|
|
||||||
Dict,
|
|
||||||
Final,
|
|
||||||
List,
|
|
||||||
Optional,
|
|
||||||
Union,
|
|
||||||
Type,
|
|
||||||
)
|
|
||||||
|
|
||||||
from platypush.common import StoppableThread
|
|
||||||
from platypush.context import get_bus, get_plugin
|
|
||||||
from platypush.entities import (
|
|
||||||
EnumSwitchEntityManager,
|
|
||||||
get_entities_engine,
|
|
||||||
)
|
|
||||||
from platypush.entities.bluetooth import BluetoothDevice, BluetoothService
|
|
||||||
from platypush.message.event.bluetooth import (
|
|
||||||
BluetoothScanPausedEvent,
|
|
||||||
BluetoothScanResumedEvent,
|
|
||||||
)
|
|
||||||
from platypush.plugins import RunnablePlugin, action
|
|
||||||
from platypush.plugins.db import DbPlugin
|
|
||||||
|
|
||||||
from ._ble import BLEManager
|
|
||||||
from ._cache import EntityCache
|
|
||||||
from ._legacy import LegacyManager
|
|
||||||
from ._types import DevicesBlacklist, RawServiceClass
|
|
||||||
from ._manager import BaseBluetoothManager
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-ancestors
|
|
||||||
class BluetoothPlugin(RunnablePlugin, EnumSwitchEntityManager):
|
|
||||||
"""
|
|
||||||
Plugin to interact with Bluetooth devices.
|
|
||||||
|
|
||||||
This plugin uses `_Bleak_ <https://github.com/hbldh/bleak>`_ to interact
|
|
||||||
with the Bluetooth stack and `_Theengs_ <https://github.com/theengs/decoder>`_
|
|
||||||
to map the services exposed by the devices into native entities.
|
|
||||||
|
|
||||||
The full list of devices natively supported can be found
|
|
||||||
`here <https://decoder.theengs.io/devices/devices_by_brand.html>`_.
|
|
||||||
|
|
||||||
It also supports legacy Bluetooth services, as well as the transfer of
|
|
||||||
files.
|
|
||||||
|
|
||||||
Note that the support for Bluetooth low-energy devices requires a Bluetooth
|
|
||||||
adapter compatible with the Bluetooth 5.0 specification or higher.
|
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **bleak** (``pip install bleak``)
|
|
||||||
* **bluetooth-numbers** (``pip install bluetooth-numbers``)
|
|
||||||
* **TheengsDecoder** (``pip install TheengsDecoder``)
|
|
||||||
* **pydbus** (``pip install pydbus``)
|
|
||||||
* **pybluez** (``pip install git+https://github.com/pybluez/pybluez``)
|
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
* :class:`platypush.message.event.bluetooth.BluetoothConnectionFailedEvent`
|
|
||||||
* :class:`platypush.message.event.bluetooth.BluetoothDeviceConnectedEvent`
|
|
||||||
* :class:`platypush.message.event.bluetooth.BluetoothDeviceDisconnectedEvent`
|
|
||||||
* :class:`platypush.message.event.bluetooth.BluetoothDeviceFoundEvent`
|
|
||||||
* :class:`platypush.message.event.bluetooth.BluetoothDeviceLostEvent`
|
|
||||||
* :class:`platypush.message.event.bluetooth.BluetoothFileReceivedEvent`
|
|
||||||
* :class:`platypush.message.event.bluetooth.BluetoothFileSentEvent`
|
|
||||||
* :class:`platypush.message.event.bluetooth.BluetoothFileTransferStartedEvent`
|
|
||||||
* :class:`platypush.message.event.bluetooth.BluetoothScanPausedEvent`
|
|
||||||
* :class:`platypush.message.event.bluetooth.BluetoothScanResumedEvent`
|
|
||||||
* :class:`platypush.message.event.entities.EntityUpdateEvent`
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
_default_connect_timeout: Final[int] = 20
|
|
||||||
""" Default connection timeout (in seconds) """
|
|
||||||
|
|
||||||
_default_scan_duration: Final[float] = 10.0
|
|
||||||
""" Default duration of a discovery session (in seconds) """
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
interface: Optional[str] = None,
|
|
||||||
connect_timeout: float = _default_connect_timeout,
|
|
||||||
service_uuids: Optional[Collection[RawServiceClass]] = None,
|
|
||||||
scan_paused_on_start: bool = False,
|
|
||||||
poll_interval: float = _default_scan_duration,
|
|
||||||
exclude_known_noisy_beacons: bool = True,
|
|
||||||
ignored_device_addresses: Optional[Collection[str]] = None,
|
|
||||||
ignored_device_names: Optional[Collection[str]] = None,
|
|
||||||
ignored_device_manufacturers: Optional[Collection[str]] = None,
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
:param interface: Name of the Bluetooth interface to use (e.g. ``hci0``
|
|
||||||
on Linux). Default: first available interface.
|
|
||||||
:param connect_timeout: Timeout in seconds for the connection to a
|
|
||||||
Bluetooth device. Default: 20 seconds.
|
|
||||||
:param service_uuids: List of service UUIDs to discover.
|
|
||||||
Default: all.
|
|
||||||
:param scan_paused_on_start: If ``True``, the plugin will not the
|
|
||||||
scanning thread until :meth:`.scan_resume` is called (default:
|
|
||||||
``False``).
|
|
||||||
:param exclude_known_noisy_beacons: Exclude BLE beacons from devices
|
|
||||||
known for being very noisy. It mainly includes tracking services on
|
|
||||||
Google, Apple, Microsoft and Samsung devices. These devices are
|
|
||||||
also known for refreshing their MAC address very frequently, which
|
|
||||||
may result in a large (and constantly increasing) list of devices.
|
|
||||||
Disable this flag if you need to track BLE beacons from these
|
|
||||||
devices, but beware that you may need periodically clean up your
|
|
||||||
list of scanned devices.
|
|
||||||
:param ignored_device_addresses: List of device addresses to ignore.
|
|
||||||
:param ignored_device_names: List of device names to ignore.
|
|
||||||
:param ignored_device_manufacturers: List of device manufacturers to
|
|
||||||
ignore.
|
|
||||||
"""
|
|
||||||
kwargs['poll_interval'] = poll_interval
|
|
||||||
super().__init__(**kwargs)
|
|
||||||
|
|
||||||
self._interface: Optional[str] = interface
|
|
||||||
""" Default Bluetooth interface to use """
|
|
||||||
self._connect_timeout: float = connect_timeout
|
|
||||||
""" Connection timeout in seconds """
|
|
||||||
self._service_uuids: Collection[RawServiceClass] = service_uuids or []
|
|
||||||
""" UUIDs to discover """
|
|
||||||
self._scan_lock = threading.RLock()
|
|
||||||
""" Lock to synchronize scanning access to the Bluetooth device """
|
|
||||||
self._scan_enabled = threading.Event()
|
|
||||||
""" Event used to enable/disable scanning """
|
|
||||||
self._device_queue: Queue[BluetoothDevice] = Queue()
|
|
||||||
"""
|
|
||||||
Queue used by the Bluetooth managers to published the discovered
|
|
||||||
Bluetooth devices.
|
|
||||||
"""
|
|
||||||
self._device_cache = EntityCache()
|
|
||||||
"""
|
|
||||||
Cache of the devices discovered by the plugin.
|
|
||||||
"""
|
|
||||||
self._excluded_known_noisy_beacons = exclude_known_noisy_beacons
|
|
||||||
""" Exclude known noisy BLE beacons. """
|
|
||||||
|
|
||||||
self._blacklist = DevicesBlacklist(
|
|
||||||
addresses=set(ignored_device_addresses or []),
|
|
||||||
names=set(ignored_device_names or []),
|
|
||||||
manufacturers=set(ignored_device_manufacturers or []),
|
|
||||||
)
|
|
||||||
""" Blacklist rules for the devices to ignore. """
|
|
||||||
|
|
||||||
self._managers: Dict[Type[BaseBluetoothManager], BaseBluetoothManager] = {}
|
|
||||||
"""
|
|
||||||
Bluetooth managers threads, one for BLE devices and one for non-BLE
|
|
||||||
devices.
|
|
||||||
"""
|
|
||||||
|
|
||||||
self._scan_controller_timer: Optional[threading.Timer] = None
|
|
||||||
""" Timer used to temporarily pause the discovery process """
|
|
||||||
|
|
||||||
if not scan_paused_on_start:
|
|
||||||
self._scan_enabled.set()
|
|
||||||
|
|
||||||
def _refresh_cache(self) -> None:
|
|
||||||
# Wait for the entities engine to start
|
|
||||||
get_entities_engine().wait_start()
|
|
||||||
|
|
||||||
with get_plugin(DbPlugin).get_session(
|
|
||||||
autoflush=False, autocommit=False, expire_on_commit=False
|
|
||||||
) as session:
|
|
||||||
existing_devices = [d.copy() for d in session.query(BluetoothDevice).all()]
|
|
||||||
|
|
||||||
for dev in existing_devices:
|
|
||||||
self._device_cache.add(dev)
|
|
||||||
|
|
||||||
def _init_bluetooth_managers(self):
|
|
||||||
"""
|
|
||||||
Initializes the Bluetooth managers threads.
|
|
||||||
"""
|
|
||||||
manager_args = {
|
|
||||||
'interface': self._interface,
|
|
||||||
'poll_interval': self.poll_interval,
|
|
||||||
'connect_timeout': self._connect_timeout,
|
|
||||||
'stop_event': self._should_stop,
|
|
||||||
'scan_lock': self._scan_lock,
|
|
||||||
'scan_enabled': self._scan_enabled,
|
|
||||||
'device_queue': self._device_queue,
|
|
||||||
'service_uuids': list(map(BluetoothService.to_uuid, self._service_uuids)),
|
|
||||||
'device_cache': self._device_cache,
|
|
||||||
'exclude_known_noisy_beacons': self._excluded_known_noisy_beacons,
|
|
||||||
'blacklist': self._blacklist,
|
|
||||||
}
|
|
||||||
|
|
||||||
self._managers = {
|
|
||||||
BLEManager: BLEManager(**manager_args),
|
|
||||||
LegacyManager: LegacyManager(**manager_args),
|
|
||||||
}
|
|
||||||
|
|
||||||
def _scan_state_set(self, state: bool, duration: Optional[float] = None):
|
|
||||||
"""
|
|
||||||
Set the state of the scanning process.
|
|
||||||
|
|
||||||
:param state: ``True`` to enable the scanning process, ``False`` to
|
|
||||||
disable it.
|
|
||||||
:param duration: The duration of the pause (in seconds) or ``None``.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def timer_callback():
|
|
||||||
if state:
|
|
||||||
self.scan_pause()
|
|
||||||
else:
|
|
||||||
self.scan_resume()
|
|
||||||
|
|
||||||
self._scan_controller_timer = None
|
|
||||||
|
|
||||||
with self._scan_lock:
|
|
||||||
if not state and self._scan_enabled.is_set():
|
|
||||||
get_bus().post(BluetoothScanPausedEvent(duration=duration))
|
|
||||||
elif state and not self._scan_enabled.is_set():
|
|
||||||
get_bus().post(BluetoothScanResumedEvent(duration=duration))
|
|
||||||
|
|
||||||
if state:
|
|
||||||
self._scan_enabled.set()
|
|
||||||
else:
|
|
||||||
self._scan_enabled.clear()
|
|
||||||
|
|
||||||
if duration and not self._scan_controller_timer:
|
|
||||||
self._scan_controller_timer = threading.Timer(duration, timer_callback)
|
|
||||||
self._scan_controller_timer.start()
|
|
||||||
|
|
||||||
def _cancel_scan_controller_timer(self):
|
|
||||||
"""
|
|
||||||
Cancels a scan controller timer if scheduled.
|
|
||||||
"""
|
|
||||||
if self._scan_controller_timer:
|
|
||||||
self._scan_controller_timer.cancel()
|
|
||||||
|
|
||||||
def _manager_by_device(
|
|
||||||
self,
|
|
||||||
device: BluetoothDevice,
|
|
||||||
port: Optional[int] = None,
|
|
||||||
service_uuid: Optional[Union[str, RawServiceClass]] = None,
|
|
||||||
) -> BaseBluetoothManager:
|
|
||||||
"""
|
|
||||||
:param device: A discovered Bluetooth device.
|
|
||||||
:param port: The port to connect to.
|
|
||||||
:param service_uuid: The UUID of the service to connect to.
|
|
||||||
:return: The manager associated with the device (BLE or legacy).
|
|
||||||
"""
|
|
||||||
# No port nor service UUID -> use the BLE manager for direct connection
|
|
||||||
if not (port or service_uuid):
|
|
||||||
return self._managers[BLEManager]
|
|
||||||
|
|
||||||
uuid = BluetoothService.to_uuid(service_uuid) if service_uuid else None
|
|
||||||
matching_services = (
|
|
||||||
[srv for srv in device.services if srv.port == port]
|
|
||||||
if port
|
|
||||||
else [srv for srv in device.services if srv.uuid == uuid]
|
|
||||||
)
|
|
||||||
|
|
||||||
if not matching_services:
|
|
||||||
# It could be a GATT characteristic, so try BLE
|
|
||||||
return self._managers[BLEManager]
|
|
||||||
|
|
||||||
srv = matching_services[0]
|
|
||||||
return (
|
|
||||||
self._managers[BLEManager] if srv.is_ble else self._managers[LegacyManager]
|
|
||||||
)
|
|
||||||
|
|
||||||
def _get_device(self, device: str, _fail_if_not_cached=False) -> BluetoothDevice:
|
|
||||||
"""
|
|
||||||
Get a device by its address or name, and scan for it if it's not
|
|
||||||
cached.
|
|
||||||
"""
|
|
||||||
# If device is a compound entity ID in the format
|
|
||||||
# ``<mac_address>:<service>``, then split the MAC address part
|
|
||||||
m = re.match(r'^(([0-9a-f]{2}:){6}):.*', device, re.IGNORECASE)
|
|
||||||
if m:
|
|
||||||
device = m.group(1).rstrip(':')
|
|
||||||
|
|
||||||
dev = self._device_cache.get(device)
|
|
||||||
if dev:
|
|
||||||
return dev
|
|
||||||
|
|
||||||
assert not _fail_if_not_cached, f'Device {device} not found'
|
|
||||||
self.logger.info('Scanning for unknown device %s', device)
|
|
||||||
self.scan()
|
|
||||||
return self._get_device(device, _fail_if_not_cached=True)
|
|
||||||
|
|
||||||
@action
|
|
||||||
def connect(
|
|
||||||
self,
|
|
||||||
device: str,
|
|
||||||
port: Optional[int] = None,
|
|
||||||
service_uuid: Optional[Union[RawServiceClass, str]] = None,
|
|
||||||
interface: Optional[str] = None,
|
|
||||||
timeout: Optional[float] = None,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Pair and connect to a device by address or name.
|
|
||||||
|
|
||||||
:param device: The device address or name.
|
|
||||||
:param port: The port to connect to. Either ``port`` or
|
|
||||||
``service_uuid`` is required for non-BLE devices.
|
|
||||||
:param service_uuid: The UUID of the service to connect to. Either
|
|
||||||
``port`` or ``service_uuid`` is required for non-BLE devices.
|
|
||||||
:param interface: The Bluetooth interface to use (it overrides the
|
|
||||||
default ``interface``).
|
|
||||||
:param timeout: The connection timeout in seconds (it overrides the
|
|
||||||
default ``connect_timeout``).
|
|
||||||
"""
|
|
||||||
dev = self._get_device(device)
|
|
||||||
manager = self._manager_by_device(dev, port=port, service_uuid=service_uuid)
|
|
||||||
uuid = BluetoothService.to_uuid(service_uuid) if service_uuid else None
|
|
||||||
manager.connect(
|
|
||||||
dev.address,
|
|
||||||
port=port,
|
|
||||||
service_uuid=uuid,
|
|
||||||
interface=interface,
|
|
||||||
timeout=timeout,
|
|
||||||
)
|
|
||||||
|
|
||||||
@action
|
|
||||||
def disconnect(
|
|
||||||
self,
|
|
||||||
device: str,
|
|
||||||
port: Optional[int] = None,
|
|
||||||
service_uuid: Optional[RawServiceClass] = None,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Close an active connection to a device.
|
|
||||||
|
|
||||||
Note that this method can only close connections that have been
|
|
||||||
initiated by the application. It can't close connections owned by
|
|
||||||
other applications or agents.
|
|
||||||
|
|
||||||
:param device: The device address or name.
|
|
||||||
:param port: If connected to a non-BLE device, the optional port to
|
|
||||||
disconnect.
|
|
||||||
:param service_uuid: The optional UUID of the service to disconnect
|
|
||||||
from, for non-BLE devices.
|
|
||||||
"""
|
|
||||||
dev = self._get_device(device)
|
|
||||||
uuid = BluetoothService.to_uuid(service_uuid) if service_uuid else None
|
|
||||||
err = None
|
|
||||||
success = False
|
|
||||||
|
|
||||||
for manager in self._managers.values():
|
|
||||||
try:
|
|
||||||
manager.disconnect(dev.address, port=port, service_uuid=uuid)
|
|
||||||
success = True
|
|
||||||
except Exception as e:
|
|
||||||
err = e
|
|
||||||
|
|
||||||
assert success, f'Could not disconnect from {device}: {err}'
|
|
||||||
|
|
||||||
@action
|
|
||||||
def scan_pause(self, duration: Optional[float] = None):
|
|
||||||
"""
|
|
||||||
Pause the scanning thread.
|
|
||||||
|
|
||||||
:param duration: For how long the scanning thread should be paused
|
|
||||||
(default: null = indefinitely).
|
|
||||||
"""
|
|
||||||
self._scan_state_set(False, duration)
|
|
||||||
|
|
||||||
@action
|
|
||||||
def scan_resume(self, duration: Optional[float] = None):
|
|
||||||
"""
|
|
||||||
Resume the scanning thread, if inactive.
|
|
||||||
|
|
||||||
:param duration: For how long the scanning thread should be running
|
|
||||||
(default: null = indefinitely).
|
|
||||||
"""
|
|
||||||
self._scan_state_set(True, duration)
|
|
||||||
|
|
||||||
@action
|
|
||||||
def scan(
|
|
||||||
self,
|
|
||||||
duration: Optional[float] = None,
|
|
||||||
devices: Optional[Collection[str]] = None,
|
|
||||||
service_uuids: Optional[Collection[RawServiceClass]] = None,
|
|
||||||
) -> List[BluetoothDevice]:
|
|
||||||
"""
|
|
||||||
Scan for Bluetooth devices nearby and return the results as a list of
|
|
||||||
entities.
|
|
||||||
|
|
||||||
:param duration: Scan duration in seconds (default: same as the plugin's
|
|
||||||
`poll_interval` configuration parameter)
|
|
||||||
:param devices: List of device addresses or names to scan for.
|
|
||||||
:param service_uuids: List of service UUIDs to discover. Default: all.
|
|
||||||
"""
|
|
||||||
scanned_device_addresses = set()
|
|
||||||
duration = duration or self.poll_interval or self._default_scan_duration
|
|
||||||
uuids = {BluetoothService.to_uuid(uuid) for uuid in (service_uuids or [])}
|
|
||||||
|
|
||||||
for manager in self._managers.values():
|
|
||||||
scanned_device_addresses.update(
|
|
||||||
[
|
|
||||||
device.address
|
|
||||||
for device in manager.scan(duration=duration // len(self._managers))
|
|
||||||
if (not uuids or any(srv.uuid in uuids for srv in device.services))
|
|
||||||
and (
|
|
||||||
not devices
|
|
||||||
or device.address in devices
|
|
||||||
or device.name in devices
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
with get_plugin(DbPlugin).get_session(
|
|
||||||
autoflush=False, autocommit=False, expire_on_commit=False
|
|
||||||
) as session:
|
|
||||||
return [
|
|
||||||
d.copy()
|
|
||||||
for d in session.query(BluetoothDevice).all()
|
|
||||||
if d.address in scanned_device_addresses
|
|
||||||
]
|
|
||||||
|
|
||||||
@action
|
|
||||||
def read(
|
|
||||||
self,
|
|
||||||
device: str,
|
|
||||||
service_uuid: RawServiceClass,
|
|
||||||
interface: Optional[str] = None,
|
|
||||||
connect_timeout: Optional[float] = None,
|
|
||||||
) -> str:
|
|
||||||
"""
|
|
||||||
Read a message from a device.
|
|
||||||
|
|
||||||
:param device: Name or address of the device to read from.
|
|
||||||
:param service_uuid: Service UUID.
|
|
||||||
:param interface: Bluetooth adapter name to use (default configured if None).
|
|
||||||
:param connect_timeout: Connection timeout in seconds (default: same as the
|
|
||||||
configured `connect_timeout`).
|
|
||||||
:return: The base64-encoded response received from the device.
|
|
||||||
"""
|
|
||||||
dev = self._get_device(device)
|
|
||||||
uuid = BluetoothService.to_uuid(service_uuid)
|
|
||||||
manager = self._manager_by_device(dev, service_uuid=uuid)
|
|
||||||
data = manager.read(
|
|
||||||
dev.address, uuid, interface=interface, connect_timeout=connect_timeout
|
|
||||||
)
|
|
||||||
return base64.b64encode(data).decode()
|
|
||||||
|
|
||||||
@action
|
|
||||||
def write(
|
|
||||||
self,
|
|
||||||
device: str,
|
|
||||||
data: str,
|
|
||||||
service_uuid: RawServiceClass,
|
|
||||||
interface: Optional[str] = None,
|
|
||||||
connect_timeout: Optional[float] = None,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Writes data to a device
|
|
||||||
|
|
||||||
:param device: Name or address of the device to read from.
|
|
||||||
:param data: Data to be written, as a base64-encoded string.
|
|
||||||
:param service_uuid: Service UUID.
|
|
||||||
:param interface: Bluetooth adapter name to use (default configured if None)
|
|
||||||
:param connect_timeout: Connection timeout in seconds (default: same as the
|
|
||||||
configured `connect_timeout`).
|
|
||||||
"""
|
|
||||||
binary_data = base64.b64decode(data.encode())
|
|
||||||
dev = self._get_device(device)
|
|
||||||
uuid = BluetoothService.to_uuid(service_uuid)
|
|
||||||
manager = self._manager_by_device(dev, service_uuid=uuid)
|
|
||||||
manager.write(
|
|
||||||
dev.address,
|
|
||||||
binary_data,
|
|
||||||
service_uuid=uuid,
|
|
||||||
interface=interface,
|
|
||||||
connect_timeout=connect_timeout,
|
|
||||||
)
|
|
||||||
|
|
||||||
@action
|
|
||||||
def set(self, entity: str, value: Any, **_):
|
|
||||||
"""
|
|
||||||
Set the value of an entity.
|
|
||||||
|
|
||||||
This is currently only supported for Switchbot devices, where the value
|
|
||||||
can be one among ``on``, ``off`` and ``press``.
|
|
||||||
|
|
||||||
:param entity: The entity to set the value for. It can be the full
|
|
||||||
entity ID in the format ``<mac-address>::<service>``, or just
|
|
||||||
the MAC address if the plugin supports it.
|
|
||||||
:param value: The value to set the entity to.
|
|
||||||
"""
|
|
||||||
device = self._get_device(entity)
|
|
||||||
matching_plugin = next(
|
|
||||||
iter(
|
|
||||||
plugin
|
|
||||||
for manager in self._managers.values()
|
|
||||||
for plugin in manager.plugins
|
|
||||||
if plugin.supports_device(device)
|
|
||||||
),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert (
|
|
||||||
matching_plugin is not None
|
|
||||||
), f'Action `set` not supported on device {entity}'
|
|
||||||
|
|
||||||
method = getattr(matching_plugin, 'set', None)
|
|
||||||
assert method, f'The plugin {matching_plugin} does not support `set`'
|
|
||||||
return method(device, value)
|
|
||||||
|
|
||||||
@action
|
|
||||||
def send_file(
|
|
||||||
self,
|
|
||||||
file: str,
|
|
||||||
device: str,
|
|
||||||
data: Optional[Union[str, bytes, bytearray]] = None,
|
|
||||||
binary: bool = False,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Send a file to a device that exposes an OBEX Object Push service.
|
|
||||||
|
|
||||||
:param file: Path of the file to be sent. If ``data`` is specified
|
|
||||||
then ``file`` should include the proposed file on the
|
|
||||||
receiving host.
|
|
||||||
:param data: Alternatively to a file on disk you can send raw (string
|
|
||||||
or binary) content.
|
|
||||||
:param device: Device address or name.
|
|
||||||
:param binary: Set to true if data is a base64-encoded binary string.
|
|
||||||
"""
|
|
||||||
from ._file import FileSender
|
|
||||||
|
|
||||||
if not data:
|
|
||||||
file = os.path.abspath(os.path.expanduser(file))
|
|
||||||
with open(file, 'rb') as f:
|
|
||||||
binary_data = f.read()
|
|
||||||
elif binary:
|
|
||||||
binary_data = base64.b64decode(
|
|
||||||
data.encode() if isinstance(data, str) else data
|
|
||||||
)
|
|
||||||
elif isinstance(data, str):
|
|
||||||
binary_data = data.encode()
|
|
||||||
else:
|
|
||||||
binary_data = data
|
|
||||||
|
|
||||||
sender = FileSender(self._managers[LegacyManager]) # type: ignore
|
|
||||||
sender.send_file(file, device, binary_data)
|
|
||||||
|
|
||||||
@action
|
|
||||||
def status(
|
|
||||||
self,
|
|
||||||
*_,
|
|
||||||
duration: Optional[float] = None,
|
|
||||||
devices: Optional[Collection[str]] = None,
|
|
||||||
service_uuids: Optional[Collection[RawServiceClass]] = None,
|
|
||||||
**__,
|
|
||||||
) -> List[BluetoothDevice]:
|
|
||||||
"""
|
|
||||||
Retrieve the status of all the devices, or the matching
|
|
||||||
devices/services.
|
|
||||||
|
|
||||||
If scanning is currently disabled, it will enable it and perform a
|
|
||||||
scan.
|
|
||||||
|
|
||||||
The differences between this method and :meth:`.scan` are:
|
|
||||||
|
|
||||||
1. :meth:`.status` will return the status of all the devices known
|
|
||||||
to the application, while :meth:`.scan` will return the status
|
|
||||||
only of the devices discovered in the provided time window.
|
|
||||||
|
|
||||||
2. :meth:`.status` will not initiate a new scan if scanning is
|
|
||||||
already enabled (it will only returned the status of the known
|
|
||||||
devices), while :meth:`.scan` will initiate a new scan.
|
|
||||||
|
|
||||||
:param duration: Scan duration in seconds, if scanning is disabled
|
|
||||||
(default: same as the plugin's `poll_interval` configuration
|
|
||||||
parameter)
|
|
||||||
:param devices: List of device addresses or names to filter for.
|
|
||||||
Default: all.
|
|
||||||
:param service_uuids: List of service UUIDs to filter for. Default:
|
|
||||||
all.
|
|
||||||
"""
|
|
||||||
if not self._scan_enabled.is_set():
|
|
||||||
self.scan(
|
|
||||||
duration=duration,
|
|
||||||
devices=devices,
|
|
||||||
service_uuids=service_uuids,
|
|
||||||
)
|
|
||||||
|
|
||||||
with get_plugin(DbPlugin).get_session(
|
|
||||||
autoflush=False, autocommit=False, expire_on_commit=False
|
|
||||||
) as session:
|
|
||||||
known_devices = [
|
|
||||||
d.copy()
|
|
||||||
for d in session.query(BluetoothDevice).all()
|
|
||||||
if (not devices or d.address in devices or d.name in devices)
|
|
||||||
and (
|
|
||||||
not service_uuids
|
|
||||||
or any(str(srv.uuid) in service_uuids for srv in d.services)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
# Send entity update events to keep any asynchronous clients in sync
|
|
||||||
get_entities_engine().notify(*known_devices)
|
|
||||||
return known_devices
|
|
||||||
|
|
||||||
def transform_entities(
|
|
||||||
self, entities: Collection[BluetoothDevice]
|
|
||||||
) -> Collection[BluetoothDevice]:
|
|
||||||
return super().transform_entities(entities)
|
|
||||||
|
|
||||||
def main(self):
|
|
||||||
self._refresh_cache()
|
|
||||||
self._init_bluetooth_managers()
|
|
||||||
|
|
||||||
for manager in self._managers.values():
|
|
||||||
manager.start()
|
|
||||||
|
|
||||||
try:
|
|
||||||
while not self.should_stop():
|
|
||||||
try:
|
|
||||||
device = self._device_queue.get(timeout=1)
|
|
||||||
except Empty:
|
|
||||||
continue
|
|
||||||
|
|
||||||
device = self._device_cache.add(device)
|
|
||||||
self.publish_entities([device], callback=self._device_cache.add)
|
|
||||||
finally:
|
|
||||||
self.stop()
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
"""
|
|
||||||
Upon stop request, it stops any pending scans and closes all active
|
|
||||||
connections.
|
|
||||||
"""
|
|
||||||
super().stop()
|
|
||||||
|
|
||||||
self._cancel_scan_controller_timer()
|
|
||||||
self._stop_threads(self._managers.values())
|
|
||||||
|
|
||||||
def _stop_threads(self, threads: Collection[StoppableThread], timeout: float = 5):
|
|
||||||
"""
|
|
||||||
Set the stop events on active threads and wait for them to stop.
|
|
||||||
"""
|
|
||||||
# Set the stop events and call `.stop`
|
|
||||||
for thread in threads:
|
|
||||||
if thread and thread.is_alive():
|
|
||||||
self.logger.info('Waiting for %s to stop', thread.name)
|
|
||||||
try:
|
|
||||||
thread.stop()
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.exception('Error while stopping %s: %s', thread.name, e)
|
|
||||||
|
|
||||||
# Wait for the manager threads to stop
|
|
||||||
wait_start = time.time()
|
|
||||||
|
|
||||||
for thread in threads:
|
|
||||||
if (
|
|
||||||
thread
|
|
||||||
and thread.ident != threading.current_thread().ident
|
|
||||||
and thread.is_alive()
|
|
||||||
):
|
|
||||||
thread.join(timeout=max(0, timeout - (time.time() - wait_start)))
|
|
||||||
|
|
||||||
if thread and thread.is_alive():
|
|
||||||
self.logger.warning(
|
|
||||||
'Timeout while waiting for %s to stop', thread.name
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["BluetoothPlugin"]
|
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
|
|
@ -9,11 +9,6 @@ from platypush.plugins.calendar import CalendarInterface
|
||||||
class CalendarIcalPlugin(Plugin, CalendarInterface):
|
class CalendarIcalPlugin(Plugin, CalendarInterface):
|
||||||
"""
|
"""
|
||||||
iCal calendars plugin. Interact with remote calendars in iCal format.
|
iCal calendars plugin. Interact with remote calendars in iCal format.
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **icalendar** (``pip install icalendar``)
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, url, *args, **kwargs):
|
def __init__(self, url, *args, **kwargs):
|
||||||
|
|
|
@ -60,25 +60,6 @@ class CameraPlugin(Plugin, ABC):
|
||||||
|
|
||||||
Both the endpoints support the same parameters of the constructor of this class (e.g. ``device``, ``warmup_frames``,
|
Both the endpoints support the same parameters of the constructor of this class (e.g. ``device``, ``warmup_frames``,
|
||||||
``duration`` etc.) as ``GET`` parameters.
|
``duration`` etc.) as ``GET`` parameters.
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **Pillow** (``pip install Pillow``) [optional] default handler for image transformations.
|
|
||||||
* **wxPython** (``pip install wxPython``) [optional] default handler for camera previews (``ffplay`` will be
|
|
||||||
used as a fallback if ``wxPython`` is not installed).
|
|
||||||
* **ffmpeg** (see installation instructions for your OS) for rendering/streaming videos.
|
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
* :class:`platypush.message.event.camera.CameraRecordingStartedEvent`
|
|
||||||
when a new video recording/photo burst starts
|
|
||||||
* :class:`platypush.message.event.camera.CameraRecordingStoppedEvent`
|
|
||||||
when a video recording/photo burst ends
|
|
||||||
* :class:`platypush.message.event.camera.CameraVideoRenderedEvent`
|
|
||||||
when a sequence of captured is successfully rendered into a video
|
|
||||||
* :class:`platypush.message.event.camera.CameraPictureTakenEvent`
|
|
||||||
when a snapshot is captured and stored to an image file
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_camera_class = Camera
|
_camera_class = Camera
|
||||||
|
|
|
@ -7,16 +7,15 @@ from platypush.plugins.camera.model.writer.cv import CvFileWriter
|
||||||
class CameraCvPlugin(CameraPlugin):
|
class CameraCvPlugin(CameraPlugin):
|
||||||
"""
|
"""
|
||||||
Plugin to control generic cameras over OpenCV.
|
Plugin to control generic cameras over OpenCV.
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **opencv** (``pip install opencv-python``)
|
|
||||||
* **Pillow** (``pip install Pillow``)
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, color_transform: Optional[str] = 'COLOR_BGR2RGB', video_type: str = 'XVID',
|
def __init__(
|
||||||
video_writer: str = 'ffmpeg', **kwargs):
|
self,
|
||||||
|
color_transform: Optional[str] = 'COLOR_BGR2RGB',
|
||||||
|
video_type: str = 'XVID',
|
||||||
|
video_writer: str = 'ffmpeg',
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
:param device: Device ID (0 for the first camera, 1 for the second etc.) or path (e.g. ``/dev/video0``).
|
:param device: Device ID (0 for the first camera, 1 for the second etc.) or path (e.g. ``/dev/video0``).
|
||||||
:param video_type: Default video type to use when exporting captured frames to camera (default: 0, infers the
|
:param video_type: Default video type to use when exporting captured frames to camera (default: 0, infers the
|
||||||
|
@ -38,7 +37,9 @@ class CameraCvPlugin(CameraPlugin):
|
||||||
|
|
||||||
:param kwargs: Extra arguments to be passed up to :class:`platypush.plugins.camera.CameraPlugin`.
|
:param kwargs: Extra arguments to be passed up to :class:`platypush.plugins.camera.CameraPlugin`.
|
||||||
"""
|
"""
|
||||||
super().__init__(color_transform=color_transform, video_type=video_type, **kwargs)
|
super().__init__(
|
||||||
|
color_transform=color_transform, video_type=video_type, **kwargs
|
||||||
|
)
|
||||||
if video_writer == 'cv':
|
if video_writer == 'cv':
|
||||||
self._video_writer_class = CvFileWriter
|
self._video_writer_class = CvFileWriter
|
||||||
|
|
||||||
|
@ -60,12 +61,15 @@ class CameraCvPlugin(CameraPlugin):
|
||||||
def capture_frame(self, camera: Camera, *args, **kwargs):
|
def capture_frame(self, camera: Camera, *args, **kwargs):
|
||||||
import cv2
|
import cv2
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
ret, frame = camera.object.read()
|
ret, frame = camera.object.read()
|
||||||
assert ret, 'Cannot retrieve frame from {}'.format(camera.info.device)
|
assert ret, 'Cannot retrieve frame from {}'.format(camera.info.device)
|
||||||
|
|
||||||
color_transform = camera.info.color_transform
|
color_transform = camera.info.color_transform
|
||||||
if isinstance(color_transform, str):
|
if isinstance(color_transform, str):
|
||||||
color_transform = getattr(cv2, color_transform or self.camera_info.color_transform)
|
color_transform = getattr(
|
||||||
|
cv2, color_transform or self.camera_info.color_transform
|
||||||
|
)
|
||||||
if color_transform:
|
if color_transform:
|
||||||
frame = cv2.cvtColor(frame, color_transform)
|
frame = cv2.cvtColor(frame, color_transform)
|
||||||
|
|
||||||
|
|
|
@ -12,18 +12,18 @@ from platypush.plugins.camera.ffmpeg.model import FFmpegCamera, FFmpegCameraInfo
|
||||||
class CameraFfmpegPlugin(CameraPlugin):
|
class CameraFfmpegPlugin(CameraPlugin):
|
||||||
"""
|
"""
|
||||||
Plugin to interact with a camera over FFmpeg.
|
Plugin to interact with a camera over FFmpeg.
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **ffmpeg** package installed on the system.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_camera_class = FFmpegCamera
|
_camera_class = FFmpegCamera
|
||||||
_camera_info_class = FFmpegCameraInfo
|
_camera_info_class = FFmpegCameraInfo
|
||||||
|
|
||||||
def __init__(self, device: Optional[str] = '/dev/video0', input_format: str = 'v4l2', ffmpeg_args: Tuple[str] = (),
|
def __init__(
|
||||||
**opts):
|
self,
|
||||||
|
device: Optional[str] = '/dev/video0',
|
||||||
|
input_format: str = 'v4l2',
|
||||||
|
ffmpeg_args: Tuple[str] = (),
|
||||||
|
**opts
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
:param device: Path to the camera device (default: ``/dev/video0``).
|
:param device: Path to the camera device (default: ``/dev/video0``).
|
||||||
:param input_format: FFmpeg input format for the the camera device (default: ``v4l2``).
|
:param input_format: FFmpeg input format for the the camera device (default: ``v4l2``).
|
||||||
|
@ -35,10 +35,25 @@ class CameraFfmpegPlugin(CameraPlugin):
|
||||||
|
|
||||||
def prepare_device(self, camera: FFmpegCamera) -> subprocess.Popen:
|
def prepare_device(self, camera: FFmpegCamera) -> subprocess.Popen:
|
||||||
warmup_seconds = self._get_warmup_seconds(camera)
|
warmup_seconds = self._get_warmup_seconds(camera)
|
||||||
ffmpeg = [camera.info.ffmpeg_bin, '-y', '-f', camera.info.input_format, '-i', camera.info.device, '-s',
|
ffmpeg = [
|
||||||
'{}x{}'.format(*camera.info.resolution), '-ss', str(warmup_seconds),
|
camera.info.ffmpeg_bin,
|
||||||
*(('-r', str(camera.info.fps)) if camera.info.fps else ()),
|
'-y',
|
||||||
'-pix_fmt', 'rgb24', '-f', 'rawvideo', *camera.info.ffmpeg_args, '-']
|
'-f',
|
||||||
|
camera.info.input_format,
|
||||||
|
'-i',
|
||||||
|
camera.info.device,
|
||||||
|
'-s',
|
||||||
|
'{}x{}'.format(*camera.info.resolution),
|
||||||
|
'-ss',
|
||||||
|
str(warmup_seconds),
|
||||||
|
*(('-r', str(camera.info.fps)) if camera.info.fps else ()),
|
||||||
|
'-pix_fmt',
|
||||||
|
'rgb24',
|
||||||
|
'-f',
|
||||||
|
'rawvideo',
|
||||||
|
*camera.info.ffmpeg_args,
|
||||||
|
'-',
|
||||||
|
]
|
||||||
|
|
||||||
self.logger.info('Running FFmpeg: {}'.format(' '.join(ffmpeg)))
|
self.logger.info('Running FFmpeg: {}'.format(' '.join(ffmpeg)))
|
||||||
proc = subprocess.Popen(ffmpeg, stdout=subprocess.PIPE)
|
proc = subprocess.Popen(ffmpeg, stdout=subprocess.PIPE)
|
||||||
|
@ -46,7 +61,9 @@ class CameraFfmpegPlugin(CameraPlugin):
|
||||||
proc.send_signal(signal.SIGSTOP)
|
proc.send_signal(signal.SIGSTOP)
|
||||||
return proc
|
return proc
|
||||||
|
|
||||||
def start_camera(self, camera: FFmpegCamera, preview: bool = False, *args, **kwargs):
|
def start_camera(
|
||||||
|
self, camera: FFmpegCamera, preview: bool = False, *args, **kwargs
|
||||||
|
):
|
||||||
super().start_camera(*args, camera=camera, preview=preview, **kwargs)
|
super().start_camera(*args, camera=camera, preview=preview, **kwargs)
|
||||||
if camera.object:
|
if camera.object:
|
||||||
camera.object.send_signal(signal.SIGCONT)
|
camera.object.send_signal(signal.SIGCONT)
|
||||||
|
@ -65,7 +82,9 @@ class CameraFfmpegPlugin(CameraPlugin):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning('Error on FFmpeg capture wait: {}'.format(str(e)))
|
self.logger.warning('Error on FFmpeg capture wait: {}'.format(str(e)))
|
||||||
|
|
||||||
def capture_frame(self, camera: FFmpegCamera, *args, **kwargs) -> Optional[ImageType]:
|
def capture_frame(
|
||||||
|
self, camera: FFmpegCamera, *args, **kwargs
|
||||||
|
) -> Optional[ImageType]:
|
||||||
raw_size = camera.info.resolution[0] * camera.info.resolution[1] * 3
|
raw_size = camera.info.resolution[0] * camera.info.resolution[1] * 3
|
||||||
data = camera.object.stdout.read(raw_size)
|
data = camera.object.stdout.read(raw_size)
|
||||||
if len(data) < raw_size:
|
if len(data) < raw_size:
|
||||||
|
|
|
@ -11,20 +11,6 @@ from platypush.common.gstreamer import Pipeline
|
||||||
class CameraGstreamerPlugin(CameraPlugin):
|
class CameraGstreamerPlugin(CameraPlugin):
|
||||||
"""
|
"""
|
||||||
Plugin to interact with a camera over GStreamer.
|
Plugin to interact with a camera over GStreamer.
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **gst-python**
|
|
||||||
* **pygobject**
|
|
||||||
|
|
||||||
On Debian and derived systems:
|
|
||||||
|
|
||||||
* ``[sudo] apt-get install python3-gi python3-gst-1.0``
|
|
||||||
|
|
||||||
On Arch and derived systems:
|
|
||||||
|
|
||||||
* ``[sudo] pacman -S gst-python``
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_camera_class = GStreamerCamera
|
_camera_class = GStreamerCamera
|
||||||
|
|
|
@ -25,15 +25,15 @@ class CameraIrMlx90640Plugin(CameraPlugin):
|
||||||
$ make bcm2835
|
$ make bcm2835
|
||||||
$ make examples/rawrgb I2C_MODE=LINUX
|
$ make examples/rawrgb I2C_MODE=LINUX
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **mlx90640-library** installation (see instructions above)
|
|
||||||
* **PIL** image library (``pip install Pillow``)
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, rawrgb_path: Optional[str] = None, resolution: Tuple[int, int] = (32, 24),
|
def __init__(
|
||||||
warmup_frames: Optional[int] = 5, **kwargs):
|
self,
|
||||||
|
rawrgb_path: Optional[str] = None,
|
||||||
|
resolution: Tuple[int, int] = (32, 24),
|
||||||
|
warmup_frames: Optional[int] = 5,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
:param rawrgb_path: Specify it if the rawrgb executable compiled from
|
:param rawrgb_path: Specify it if the rawrgb executable compiled from
|
||||||
https://github.com/pimoroni/mlx90640-library is in another folder than
|
https://github.com/pimoroni/mlx90640-library is in another folder than
|
||||||
|
@ -42,14 +42,22 @@ class CameraIrMlx90640Plugin(CameraPlugin):
|
||||||
:param warmup_frames: Number of frames to be skipped on sensor initialization/warmup (default: 2).
|
:param warmup_frames: Number of frames to be skipped on sensor initialization/warmup (default: 2).
|
||||||
:param kwargs: Extra parameters to be passed to :class:`platypush.plugins.camera.CameraPlugin`.
|
:param kwargs: Extra parameters to be passed to :class:`platypush.plugins.camera.CameraPlugin`.
|
||||||
"""
|
"""
|
||||||
super().__init__(device='mlx90640', resolution=resolution, warmup_frames=warmup_frames, **kwargs)
|
super().__init__(
|
||||||
|
device='mlx90640',
|
||||||
|
resolution=resolution,
|
||||||
|
warmup_frames=warmup_frames,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
if not rawrgb_path:
|
if not rawrgb_path:
|
||||||
rawrgb_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'lib', 'examples', 'rawrgb')
|
rawrgb_path = os.path.join(
|
||||||
|
os.path.dirname(os.path.abspath(__file__)), 'lib', 'examples', 'rawrgb'
|
||||||
|
)
|
||||||
rawrgb_path = os.path.abspath(os.path.expanduser(rawrgb_path))
|
rawrgb_path = os.path.abspath(os.path.expanduser(rawrgb_path))
|
||||||
|
|
||||||
assert os.path.isfile(rawrgb_path),\
|
assert os.path.isfile(
|
||||||
'rawrgb executable not found. Please follow the documentation of this plugin to build it'
|
rawrgb_path
|
||||||
|
), 'rawrgb executable not found. Please follow the documentation of this plugin to build it'
|
||||||
|
|
||||||
self.rawrgb_path = rawrgb_path
|
self.rawrgb_path = rawrgb_path
|
||||||
self._capture_proc = None
|
self._capture_proc = None
|
||||||
|
@ -59,8 +67,11 @@ class CameraIrMlx90640Plugin(CameraPlugin):
|
||||||
|
|
||||||
def prepare_device(self, device: Camera):
|
def prepare_device(self, device: Camera):
|
||||||
if not self._is_capture_running():
|
if not self._is_capture_running():
|
||||||
self._capture_proc = subprocess.Popen([self.rawrgb_path, '{}'.format(device.info.fps)],
|
self._capture_proc = subprocess.Popen(
|
||||||
stdin=subprocess.PIPE, stdout=subprocess.PIPE)
|
[self.rawrgb_path, '{}'.format(device.info.fps)],
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
)
|
||||||
|
|
||||||
return self._capture_proc
|
return self._capture_proc
|
||||||
|
|
||||||
|
@ -77,11 +88,14 @@ class CameraIrMlx90640Plugin(CameraPlugin):
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
camera = self.prepare_device(device)
|
camera = self.prepare_device(device)
|
||||||
frame = camera.stdout.read(device.info.resolution[0] * device.info.resolution[1] * 3)
|
frame = camera.stdout.read(
|
||||||
|
device.info.resolution[0] * device.info.resolution[1] * 3
|
||||||
|
)
|
||||||
return Image.frombytes('RGB', device.info.resolution, frame)
|
return Image.frombytes('RGB', device.info.resolution, frame)
|
||||||
|
|
||||||
def to_grayscale(self, image):
|
def to_grayscale(self, image):
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
new_image = Image.new('L', image.size)
|
new_image = Image.new('L', image.size)
|
||||||
|
|
||||||
for i in range(0, image.size[0]):
|
for i in range(0, image.size[0]):
|
||||||
|
|
|
@ -12,30 +12,45 @@ class CameraPiPlugin(CameraPlugin):
|
||||||
"""
|
"""
|
||||||
Plugin to control a Pi camera.
|
Plugin to control a Pi camera.
|
||||||
|
|
||||||
Requires:
|
.. warning::
|
||||||
|
This plugin is **DEPRECATED**, as it relies on the old ``picamera`` module.
|
||||||
* **picamera** (``pip install picamera``)
|
On recent systems, it should be possible to access the Pi Camera through
|
||||||
* **numpy** (``pip install numpy``)
|
the ffmpeg or gstreamer integrations.
|
||||||
* **Pillow** (``pip install Pillow``)
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_camera_class = PiCamera
|
_camera_class = PiCamera
|
||||||
_camera_info_class = PiCameraInfo
|
_camera_info_class = PiCameraInfo
|
||||||
|
|
||||||
def __init__(self, device: int = 0, fps: float = 30., warmup_seconds: float = 2., sharpness: int = 0,
|
def __init__(
|
||||||
contrast: int = 0, brightness: int = 50, video_stabilization: bool = False, iso: int = 0,
|
self,
|
||||||
exposure_compensation: int = 0, exposure_mode: str = 'auto', meter_mode: str = 'average',
|
device: int = 0,
|
||||||
awb_mode: str = 'auto', image_effect: str = 'none', led_pin: Optional[int] = None,
|
fps: float = 30.0,
|
||||||
color_effects: Optional[Union[str, List[str]]] = None,
|
warmup_seconds: float = 2.0,
|
||||||
zoom: Tuple[float, float, float, float] = (0.0, 0.0, 1.0, 1.0), **camera):
|
sharpness: int = 0,
|
||||||
|
contrast: int = 0,
|
||||||
|
brightness: int = 50,
|
||||||
|
video_stabilization: bool = False,
|
||||||
|
iso: int = 0,
|
||||||
|
exposure_compensation: int = 0,
|
||||||
|
exposure_mode: str = 'auto',
|
||||||
|
meter_mode: str = 'average',
|
||||||
|
awb_mode: str = 'auto',
|
||||||
|
image_effect: str = 'none',
|
||||||
|
led_pin: Optional[int] = None,
|
||||||
|
color_effects: Optional[Union[str, List[str]]] = None,
|
||||||
|
zoom: Tuple[float, float, float, float] = (0.0, 0.0, 1.0, 1.0),
|
||||||
|
**camera
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
See https://www.raspberrypi.org/documentation/usage/camera/python/README.md
|
See https://www.raspberrypi.org/documentation/usage/camera/python/README.md
|
||||||
for a detailed reference about the Pi camera options.
|
for a detailed reference about the Pi camera options.
|
||||||
|
|
||||||
:param camera: Options for the base camera plugin (see :class:`platypush.plugins.camera.CameraPlugin`).
|
:param camera: Options for the base camera plugin (see :class:`platypush.plugins.camera.CameraPlugin`).
|
||||||
"""
|
"""
|
||||||
super().__init__(device=device, fps=fps, warmup_seconds=warmup_seconds, **camera)
|
super().__init__(
|
||||||
|
device=device, fps=fps, warmup_seconds=warmup_seconds, **camera
|
||||||
|
)
|
||||||
|
|
||||||
self.camera_info.sharpness = sharpness
|
self.camera_info.sharpness = sharpness
|
||||||
self.camera_info.contrast = contrast
|
self.camera_info.contrast = contrast
|
||||||
|
@ -56,8 +71,12 @@ class CameraPiPlugin(CameraPlugin):
|
||||||
# noinspection PyUnresolvedReferences
|
# noinspection PyUnresolvedReferences
|
||||||
import picamera
|
import picamera
|
||||||
|
|
||||||
camera = picamera.PiCamera(camera_num=device.info.device, resolution=device.info.resolution,
|
camera = picamera.PiCamera(
|
||||||
framerate=device.info.fps, led_pin=device.info.led_pin)
|
camera_num=device.info.device,
|
||||||
|
resolution=device.info.resolution,
|
||||||
|
framerate=device.info.fps,
|
||||||
|
led_pin=device.info.led_pin,
|
||||||
|
)
|
||||||
|
|
||||||
camera.hflip = device.info.horizontal_flip
|
camera.hflip = device.info.horizontal_flip
|
||||||
camera.vflip = device.info.vertical_flip
|
camera.vflip = device.info.vertical_flip
|
||||||
|
@ -97,9 +116,11 @@ class CameraPiPlugin(CameraPlugin):
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
shape = (camera.info.resolution[1] + (camera.info.resolution[1] % 16),
|
shape = (
|
||||||
camera.info.resolution[0] + (camera.info.resolution[0] % 32),
|
camera.info.resolution[1] + (camera.info.resolution[1] % 16),
|
||||||
3)
|
camera.info.resolution[0] + (camera.info.resolution[0] % 32),
|
||||||
|
3,
|
||||||
|
)
|
||||||
|
|
||||||
frame = np.empty(shape, dtype=np.uint8)
|
frame = np.empty(shape, dtype=np.uint8)
|
||||||
camera.object.capture(frame, 'rgb')
|
camera.object.capture(frame, 'rgb')
|
||||||
|
@ -121,7 +142,9 @@ class CameraPiPlugin(CameraPlugin):
|
||||||
self.logger.warning(str(e))
|
self.logger.warning(str(e))
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def capture_preview(self, duration: Optional[float] = None, n_frames: Optional[int] = None, **camera) -> dict:
|
def capture_preview(
|
||||||
|
self, duration: Optional[float] = None, n_frames: Optional[int] = None, **camera
|
||||||
|
) -> dict:
|
||||||
camera = self.open_device(**camera)
|
camera = self.open_device(**camera)
|
||||||
self.start_preview(camera)
|
self.start_preview(camera)
|
||||||
|
|
||||||
|
@ -132,11 +155,15 @@ class CameraPiPlugin(CameraPlugin):
|
||||||
|
|
||||||
return self.status()
|
return self.status()
|
||||||
|
|
||||||
def streaming_thread(self, camera: PiCamera, stream_format: str, duration: Optional[float] = None):
|
def streaming_thread(
|
||||||
|
self, camera: PiCamera, stream_format: str, duration: Optional[float] = None
|
||||||
|
):
|
||||||
server_socket = self._prepare_server_socket(camera)
|
server_socket = self._prepare_server_socket(camera)
|
||||||
sock = None
|
sock = None
|
||||||
streaming_started_time = time.time()
|
streaming_started_time = time.time()
|
||||||
self.logger.info('Starting streaming on port {}'.format(camera.info.listen_port))
|
self.logger.info(
|
||||||
|
'Starting streaming on port {}'.format(camera.info.listen_port)
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while camera.stream_event.is_set():
|
while camera.stream_event.is_set():
|
||||||
|
@ -161,7 +188,9 @@ class CameraPiPlugin(CameraPlugin):
|
||||||
try:
|
try:
|
||||||
sock.close()
|
sock.close()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning('Error while closing client socket: {}'.format(str(e)))
|
self.logger.warning(
|
||||||
|
'Error while closing client socket: {}'.format(str(e))
|
||||||
|
)
|
||||||
|
|
||||||
self.close_device(camera)
|
self.close_device(camera)
|
||||||
finally:
|
finally:
|
||||||
|
@ -169,7 +198,9 @@ class CameraPiPlugin(CameraPlugin):
|
||||||
self.logger.info('Stopped camera stream')
|
self.logger.info('Stopped camera stream')
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def start_streaming(self, duration: Optional[float] = None, stream_format: str = 'h264', **camera) -> dict:
|
def start_streaming(
|
||||||
|
self, duration: Optional[float] = None, stream_format: str = 'h264', **camera
|
||||||
|
) -> dict:
|
||||||
camera = self.open_device(stream_format=stream_format, **camera)
|
camera = self.open_device(stream_format=stream_format, **camera)
|
||||||
return self._start_streaming(camera, duration, stream_format)
|
return self._start_streaming(camera, duration, stream_format)
|
||||||
|
|
||||||
|
|
|
@ -18,32 +18,6 @@ class ChatIrcPlugin(RunnablePlugin, ChatPlugin):
|
||||||
|
|
||||||
This plugin allows you to easily create IRC bots with custom logic that reacts to IRC events
|
This plugin allows you to easily create IRC bots with custom logic that reacts to IRC events
|
||||||
and interact with IRC sessions.
|
and interact with IRC sessions.
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
* :class:`platypush.message.event.irc.IRCChannelJoinEvent` when a user joins a channel.
|
|
||||||
* :class:`platypush.message.event.irc.IRCChannelKickEvent` when a user is kicked from a channel.
|
|
||||||
* :class:`platypush.message.event.irc.IRCModeEvent` when a user/channel mode change event occurs.
|
|
||||||
* :class:`platypush.message.event.irc.IRCPartEvent` when a user parts a channel.
|
|
||||||
* :class:`platypush.message.event.irc.IRCQuitEvent` when a user quits.
|
|
||||||
* :class:`platypush.message.event.irc.IRCNickChangeEvent` when a user nick changes.
|
|
||||||
* :class:`platypush.message.event.irc.IRCConnectEvent` when the bot connects to a server.
|
|
||||||
* :class:`platypush.message.event.irc.IRCDisconnectEvent` when the bot disconnects from a server.
|
|
||||||
* :class:`platypush.message.event.irc.IRCPrivateMessageEvent` when a private message is received.
|
|
||||||
* :class:`platypush.message.event.irc.IRCPublicMessageEvent` when a public message is received.
|
|
||||||
* :class:`platypush.message.event.irc.IRCDCCRequestEvent` when a DCC connection request is received.
|
|
||||||
* :class:`platypush.message.event.irc.IRCDCCMessageEvent` when a DCC message is received.
|
|
||||||
* :class:`platypush.message.event.irc.IRCCTCPMessageEvent` when a CTCP message is received.
|
|
||||||
* :class:`platypush.message.event.irc.IRCDCCFileRequestEvent` when a DCC file request is received.
|
|
||||||
* :class:`platypush.message.event.irc.IRCDCCFileRecvCompletedEvent` when a DCC file download is completed.
|
|
||||||
* :class:`platypush.message.event.irc.IRCDCCFileRecvCancelledEvent` when a DCC file download is cancelled.
|
|
||||||
* :class:`platypush.message.event.irc.IRCDCCFileSendCompletedEvent` when a DCC file upload is completed.
|
|
||||||
* :class:`platypush.message.event.irc.IRCDCCFileSendCancelledEvent` when a DCC file upload is cancelled.
|
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **irc** (``pip install irc``)
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, servers: Sequence[dict], **kwargs):
|
def __init__(self, servers: Sequence[dict], **kwargs):
|
||||||
|
|
|
@ -4,21 +4,28 @@ import os
|
||||||
from threading import RLock
|
from threading import RLock
|
||||||
from typing import Optional, Union
|
from typing import Optional, Union
|
||||||
|
|
||||||
# noinspection PyPackageRequirements
|
|
||||||
from telegram.ext import Updater
|
from telegram.ext import Updater
|
||||||
# noinspection PyPackageRequirements
|
|
||||||
from telegram.message import Message as TelegramMessage
|
from telegram.message import Message as TelegramMessage
|
||||||
# noinspection PyPackageRequirements
|
|
||||||
from telegram.user import User as TelegramUser
|
from telegram.user import User as TelegramUser
|
||||||
|
|
||||||
from platypush.message.response.chat.telegram import TelegramMessageResponse, TelegramFileResponse, \
|
from platypush.message.response.chat.telegram import (
|
||||||
TelegramChatResponse, TelegramUserResponse, TelegramUsersResponse
|
TelegramMessageResponse,
|
||||||
|
TelegramFileResponse,
|
||||||
|
TelegramChatResponse,
|
||||||
|
TelegramUserResponse,
|
||||||
|
TelegramUsersResponse,
|
||||||
|
)
|
||||||
from platypush.plugins import action
|
from platypush.plugins import action
|
||||||
from platypush.plugins.chat import ChatPlugin
|
from platypush.plugins.chat import ChatPlugin
|
||||||
|
|
||||||
|
|
||||||
class Resource:
|
class Resource:
|
||||||
def __init__(self, file_id: Optional[int] = None, url: Optional[str] = None, path: Optional[str] = None):
|
def __init__(
|
||||||
|
self,
|
||||||
|
file_id: Optional[int] = None,
|
||||||
|
url: Optional[str] = None,
|
||||||
|
path: Optional[str] = None,
|
||||||
|
):
|
||||||
assert file_id or url or path, 'You need to specify either file_id, url or path'
|
assert file_id or url or path, 'You need to specify either file_id, url or path'
|
||||||
self.file_id = file_id
|
self.file_id = file_id
|
||||||
self.url = url
|
self.url = url
|
||||||
|
@ -27,12 +34,14 @@ class Resource:
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
if self.path:
|
if self.path:
|
||||||
self._file = open(os.path.abspath(os.path.expanduser(self.path)), 'rb')
|
self._file = open( # noqa
|
||||||
|
os.path.abspath(os.path.expanduser(self.path)), 'rb'
|
||||||
|
)
|
||||||
return self._file
|
return self._file
|
||||||
|
|
||||||
return self.file_id or self.url
|
return self.file_id or self.url
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
def __exit__(self, *_, **__):
|
||||||
if self._file:
|
if self._file:
|
||||||
self._file.close()
|
self._file.close()
|
||||||
|
|
||||||
|
@ -47,10 +56,6 @@ class ChatTelegramPlugin(ChatPlugin):
|
||||||
3. Copy the provided API token in the configuration of this plugin.
|
3. Copy the provided API token in the configuration of this plugin.
|
||||||
4. Open a conversation with your newly created bot.
|
4. Open a conversation with your newly created bot.
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **python-telegram-bot** (``pip install python-telegram-bot``)
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, api_token: str, **kwargs):
|
def __init__(self, api_token: str, **kwargs):
|
||||||
|
@ -117,7 +122,7 @@ class ChatTelegramPlugin(ChatPlugin):
|
||||||
contact_user_id=msg.contact.user_id if msg.contact else None,
|
contact_user_id=msg.contact.user_id if msg.contact else None,
|
||||||
contact_vcard=msg.contact.vcard if msg.contact else None,
|
contact_vcard=msg.contact.vcard if msg.contact else None,
|
||||||
link=msg.link,
|
link=msg.link,
|
||||||
media_group_id=msg.media_group_id
|
media_group_id=msg.media_group_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -129,13 +134,19 @@ class ChatTelegramPlugin(ChatPlugin):
|
||||||
first_name=user.first_name,
|
first_name=user.first_name,
|
||||||
last_name=user.last_name,
|
last_name=user.last_name,
|
||||||
language_code=user.language_code,
|
language_code=user.language_code,
|
||||||
link=user.link
|
link=user.link,
|
||||||
)
|
)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def send_message(self, chat_id: Union[str, int], text: str, parse_mode: Optional[str] = None,
|
def send_message(
|
||||||
disable_web_page_preview: bool = False, disable_notification: bool = False,
|
self,
|
||||||
reply_to_message_id: Optional[int] = None) -> TelegramMessageResponse:
|
chat_id: Union[str, int],
|
||||||
|
text: str,
|
||||||
|
parse_mode: Optional[str] = None,
|
||||||
|
disable_web_page_preview: bool = False,
|
||||||
|
disable_notification: bool = False,
|
||||||
|
reply_to_message_id: Optional[int] = None,
|
||||||
|
) -> TelegramMessageResponse:
|
||||||
"""
|
"""
|
||||||
Send a message to a chat.
|
Send a message to a chat.
|
||||||
|
|
||||||
|
@ -152,25 +163,30 @@ class ChatTelegramPlugin(ChatPlugin):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
telegram = self.get_telegram()
|
telegram = self.get_telegram()
|
||||||
msg = telegram.bot.send_message(chat_id=chat_id,
|
msg = telegram.bot.send_message(
|
||||||
text=text,
|
chat_id=chat_id,
|
||||||
parse_mode=parse_mode,
|
text=text,
|
||||||
disable_web_page_preview=disable_web_page_preview,
|
parse_mode=parse_mode,
|
||||||
disable_notification=disable_notification,
|
disable_web_page_preview=disable_web_page_preview,
|
||||||
reply_to_message_id=reply_to_message_id)
|
disable_notification=disable_notification,
|
||||||
|
reply_to_message_id=reply_to_message_id,
|
||||||
|
)
|
||||||
|
|
||||||
return self.parse_msg(msg)
|
return self.parse_msg(msg)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def send_photo(self, chat_id: Union[str, int],
|
def send_photo(
|
||||||
file_id: Optional[int] = None,
|
self,
|
||||||
url: Optional[str] = None,
|
chat_id: Union[str, int],
|
||||||
path: Optional[str] = None,
|
file_id: Optional[int] = None,
|
||||||
caption: Optional[str] = None,
|
url: Optional[str] = None,
|
||||||
parse_mode: Optional[str] = None,
|
path: Optional[str] = None,
|
||||||
disable_notification: bool = False,
|
caption: Optional[str] = None,
|
||||||
reply_to_message_id: Optional[int] = None,
|
parse_mode: Optional[str] = None,
|
||||||
timeout: int = 20) -> TelegramMessageResponse:
|
disable_notification: bool = False,
|
||||||
|
reply_to_message_id: Optional[int] = None,
|
||||||
|
timeout: int = 20,
|
||||||
|
) -> TelegramMessageResponse:
|
||||||
"""
|
"""
|
||||||
Send a picture to a chat.
|
Send a picture to a chat.
|
||||||
|
|
||||||
|
@ -198,28 +214,34 @@ class ChatTelegramPlugin(ChatPlugin):
|
||||||
telegram = self.get_telegram()
|
telegram = self.get_telegram()
|
||||||
|
|
||||||
with Resource(file_id=file_id, url=url, path=path) as resource:
|
with Resource(file_id=file_id, url=url, path=path) as resource:
|
||||||
msg = telegram.bot.send_photo(chat_id=chat_id,
|
msg = telegram.bot.send_photo(
|
||||||
photo=resource,
|
chat_id=chat_id,
|
||||||
caption=caption,
|
photo=resource,
|
||||||
disable_notification=disable_notification,
|
caption=caption,
|
||||||
reply_to_message_id=reply_to_message_id,
|
disable_notification=disable_notification,
|
||||||
timeout=timeout, parse_mode=parse_mode)
|
reply_to_message_id=reply_to_message_id,
|
||||||
|
timeout=timeout,
|
||||||
|
parse_mode=parse_mode,
|
||||||
|
)
|
||||||
|
|
||||||
return self.parse_msg(msg)
|
return self.parse_msg(msg)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def send_audio(self, chat_id: Union[str, int],
|
def send_audio(
|
||||||
file_id: Optional[int] = None,
|
self,
|
||||||
url: Optional[str] = None,
|
chat_id: Union[str, int],
|
||||||
path: Optional[str] = None,
|
file_id: Optional[int] = None,
|
||||||
caption: Optional[str] = None,
|
url: Optional[str] = None,
|
||||||
performer: Optional[str] = None,
|
path: Optional[str] = None,
|
||||||
title: Optional[str] = None,
|
caption: Optional[str] = None,
|
||||||
duration: Optional[float] = None,
|
performer: Optional[str] = None,
|
||||||
parse_mode: Optional[str] = None,
|
title: Optional[str] = None,
|
||||||
disable_notification: bool = False,
|
duration: Optional[float] = None,
|
||||||
reply_to_message_id: Optional[int] = None,
|
parse_mode: Optional[str] = None,
|
||||||
timeout: int = 20) -> TelegramMessageResponse:
|
disable_notification: bool = False,
|
||||||
|
reply_to_message_id: Optional[int] = None,
|
||||||
|
timeout: int = 20,
|
||||||
|
) -> TelegramMessageResponse:
|
||||||
"""
|
"""
|
||||||
Send audio to a chat.
|
Send audio to a chat.
|
||||||
|
|
||||||
|
@ -250,30 +272,35 @@ class ChatTelegramPlugin(ChatPlugin):
|
||||||
telegram = self.get_telegram()
|
telegram = self.get_telegram()
|
||||||
|
|
||||||
with Resource(file_id=file_id, url=url, path=path) as resource:
|
with Resource(file_id=file_id, url=url, path=path) as resource:
|
||||||
msg = telegram.bot.send_audio(chat_id=chat_id,
|
msg = telegram.bot.send_audio(
|
||||||
audio=resource,
|
chat_id=chat_id,
|
||||||
caption=caption,
|
audio=resource,
|
||||||
disable_notification=disable_notification,
|
caption=caption,
|
||||||
performer=performer,
|
disable_notification=disable_notification,
|
||||||
title=title,
|
performer=performer,
|
||||||
duration=duration,
|
title=title,
|
||||||
reply_to_message_id=reply_to_message_id,
|
duration=duration,
|
||||||
timeout=timeout,
|
reply_to_message_id=reply_to_message_id,
|
||||||
parse_mode=parse_mode)
|
timeout=timeout,
|
||||||
|
parse_mode=parse_mode,
|
||||||
|
)
|
||||||
|
|
||||||
return self.parse_msg(msg)
|
return self.parse_msg(msg)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def send_document(self, chat_id: Union[str, int],
|
def send_document(
|
||||||
file_id: Optional[int] = None,
|
self,
|
||||||
url: Optional[str] = None,
|
chat_id: Union[str, int],
|
||||||
path: Optional[str] = None,
|
file_id: Optional[int] = None,
|
||||||
filename: Optional[str] = None,
|
url: Optional[str] = None,
|
||||||
caption: Optional[str] = None,
|
path: Optional[str] = None,
|
||||||
parse_mode: Optional[str] = None,
|
filename: Optional[str] = None,
|
||||||
disable_notification: bool = False,
|
caption: Optional[str] = None,
|
||||||
reply_to_message_id: Optional[int] = None,
|
parse_mode: Optional[str] = None,
|
||||||
timeout: int = 20) -> TelegramMessageResponse:
|
disable_notification: bool = False,
|
||||||
|
reply_to_message_id: Optional[int] = None,
|
||||||
|
timeout: int = 20,
|
||||||
|
) -> TelegramMessageResponse:
|
||||||
"""
|
"""
|
||||||
Send a document to a chat.
|
Send a document to a chat.
|
||||||
|
|
||||||
|
@ -302,30 +329,35 @@ class ChatTelegramPlugin(ChatPlugin):
|
||||||
telegram = self.get_telegram()
|
telegram = self.get_telegram()
|
||||||
|
|
||||||
with Resource(file_id=file_id, url=url, path=path) as resource:
|
with Resource(file_id=file_id, url=url, path=path) as resource:
|
||||||
msg = telegram.bot.send_document(chat_id=chat_id,
|
msg = telegram.bot.send_document(
|
||||||
document=resource,
|
chat_id=chat_id,
|
||||||
filename=filename,
|
document=resource,
|
||||||
caption=caption,
|
filename=filename,
|
||||||
disable_notification=disable_notification,
|
caption=caption,
|
||||||
reply_to_message_id=reply_to_message_id,
|
disable_notification=disable_notification,
|
||||||
timeout=timeout,
|
reply_to_message_id=reply_to_message_id,
|
||||||
parse_mode=parse_mode)
|
timeout=timeout,
|
||||||
|
parse_mode=parse_mode,
|
||||||
|
)
|
||||||
|
|
||||||
return self.parse_msg(msg)
|
return self.parse_msg(msg)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def send_video(self, chat_id: Union[str, int],
|
def send_video(
|
||||||
file_id: Optional[int] = None,
|
self,
|
||||||
url: Optional[str] = None,
|
chat_id: Union[str, int],
|
||||||
path: Optional[str] = None,
|
file_id: Optional[int] = None,
|
||||||
duration: Optional[int] = None,
|
url: Optional[str] = None,
|
||||||
caption: Optional[str] = None,
|
path: Optional[str] = None,
|
||||||
width: Optional[int] = None,
|
duration: Optional[int] = None,
|
||||||
height: Optional[int] = None,
|
caption: Optional[str] = None,
|
||||||
parse_mode: Optional[str] = None,
|
width: Optional[int] = None,
|
||||||
disable_notification: bool = False,
|
height: Optional[int] = None,
|
||||||
reply_to_message_id: Optional[int] = None,
|
parse_mode: Optional[str] = None,
|
||||||
timeout: int = 20) -> TelegramMessageResponse:
|
disable_notification: bool = False,
|
||||||
|
reply_to_message_id: Optional[int] = None,
|
||||||
|
timeout: int = 20,
|
||||||
|
) -> TelegramMessageResponse:
|
||||||
"""
|
"""
|
||||||
Send a video to a chat.
|
Send a video to a chat.
|
||||||
|
|
||||||
|
@ -356,32 +388,37 @@ class ChatTelegramPlugin(ChatPlugin):
|
||||||
telegram = self.get_telegram()
|
telegram = self.get_telegram()
|
||||||
|
|
||||||
with Resource(file_id=file_id, url=url, path=path) as resource:
|
with Resource(file_id=file_id, url=url, path=path) as resource:
|
||||||
msg = telegram.bot.send_video(chat_id=chat_id,
|
msg = telegram.bot.send_video(
|
||||||
video=resource,
|
chat_id=chat_id,
|
||||||
duration=duration,
|
video=resource,
|
||||||
caption=caption,
|
duration=duration,
|
||||||
width=width,
|
caption=caption,
|
||||||
height=height,
|
width=width,
|
||||||
disable_notification=disable_notification,
|
height=height,
|
||||||
reply_to_message_id=reply_to_message_id,
|
disable_notification=disable_notification,
|
||||||
timeout=timeout,
|
reply_to_message_id=reply_to_message_id,
|
||||||
parse_mode=parse_mode)
|
timeout=timeout,
|
||||||
|
parse_mode=parse_mode,
|
||||||
|
)
|
||||||
|
|
||||||
return self.parse_msg(msg)
|
return self.parse_msg(msg)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def send_animation(self, chat_id: Union[str, int],
|
def send_animation(
|
||||||
file_id: Optional[int] = None,
|
self,
|
||||||
url: Optional[str] = None,
|
chat_id: Union[str, int],
|
||||||
path: Optional[str] = None,
|
file_id: Optional[int] = None,
|
||||||
duration: Optional[int] = None,
|
url: Optional[str] = None,
|
||||||
caption: Optional[str] = None,
|
path: Optional[str] = None,
|
||||||
width: Optional[int] = None,
|
duration: Optional[int] = None,
|
||||||
height: Optional[int] = None,
|
caption: Optional[str] = None,
|
||||||
parse_mode: Optional[str] = None,
|
width: Optional[int] = None,
|
||||||
disable_notification: bool = False,
|
height: Optional[int] = None,
|
||||||
reply_to_message_id: Optional[int] = None,
|
parse_mode: Optional[str] = None,
|
||||||
timeout: int = 20) -> TelegramMessageResponse:
|
disable_notification: bool = False,
|
||||||
|
reply_to_message_id: Optional[int] = None,
|
||||||
|
timeout: int = 20,
|
||||||
|
) -> TelegramMessageResponse:
|
||||||
"""
|
"""
|
||||||
Send an animation (GIF or H.264/MPEG-4 AVC video without sound) to a chat.
|
Send an animation (GIF or H.264/MPEG-4 AVC video without sound) to a chat.
|
||||||
|
|
||||||
|
@ -412,30 +449,35 @@ class ChatTelegramPlugin(ChatPlugin):
|
||||||
telegram = self.get_telegram()
|
telegram = self.get_telegram()
|
||||||
|
|
||||||
with Resource(file_id=file_id, url=url, path=path) as resource:
|
with Resource(file_id=file_id, url=url, path=path) as resource:
|
||||||
msg = telegram.bot.send_animation(chat_id=chat_id,
|
msg = telegram.bot.send_animation(
|
||||||
animation=resource,
|
chat_id=chat_id,
|
||||||
duration=duration,
|
animation=resource,
|
||||||
caption=caption,
|
duration=duration,
|
||||||
width=width,
|
caption=caption,
|
||||||
height=height,
|
width=width,
|
||||||
disable_notification=disable_notification,
|
height=height,
|
||||||
reply_to_message_id=reply_to_message_id,
|
disable_notification=disable_notification,
|
||||||
timeout=timeout,
|
reply_to_message_id=reply_to_message_id,
|
||||||
parse_mode=parse_mode)
|
timeout=timeout,
|
||||||
|
parse_mode=parse_mode,
|
||||||
|
)
|
||||||
|
|
||||||
return self.parse_msg(msg)
|
return self.parse_msg(msg)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def send_voice(self, chat_id: Union[str, int],
|
def send_voice(
|
||||||
file_id: Optional[int] = None,
|
self,
|
||||||
url: Optional[str] = None,
|
chat_id: Union[str, int],
|
||||||
path: Optional[str] = None,
|
file_id: Optional[int] = None,
|
||||||
caption: Optional[str] = None,
|
url: Optional[str] = None,
|
||||||
duration: Optional[float] = None,
|
path: Optional[str] = None,
|
||||||
parse_mode: Optional[str] = None,
|
caption: Optional[str] = None,
|
||||||
disable_notification: bool = False,
|
duration: Optional[float] = None,
|
||||||
reply_to_message_id: Optional[int] = None,
|
parse_mode: Optional[str] = None,
|
||||||
timeout: int = 20) -> TelegramMessageResponse:
|
disable_notification: bool = False,
|
||||||
|
reply_to_message_id: Optional[int] = None,
|
||||||
|
timeout: int = 20,
|
||||||
|
) -> TelegramMessageResponse:
|
||||||
"""
|
"""
|
||||||
Send audio to a chat as a voice file. For this to work, your audio must be in an .ogg file encoded with OPUS
|
Send audio to a chat as a voice file. For this to work, your audio must be in an .ogg file encoded with OPUS
|
||||||
(other formats may be sent as Audio or Document).
|
(other formats may be sent as Audio or Document).
|
||||||
|
@ -465,25 +507,31 @@ class ChatTelegramPlugin(ChatPlugin):
|
||||||
telegram = self.get_telegram()
|
telegram = self.get_telegram()
|
||||||
|
|
||||||
with Resource(file_id=file_id, url=url, path=path) as resource:
|
with Resource(file_id=file_id, url=url, path=path) as resource:
|
||||||
msg = telegram.bot.send_voice(chat_id=chat_id,
|
msg = telegram.bot.send_voice(
|
||||||
voice=resource,
|
chat_id=chat_id,
|
||||||
caption=caption,
|
voice=resource,
|
||||||
disable_notification=disable_notification,
|
caption=caption,
|
||||||
duration=duration,
|
disable_notification=disable_notification,
|
||||||
reply_to_message_id=reply_to_message_id,
|
duration=duration,
|
||||||
timeout=timeout, parse_mode=parse_mode)
|
reply_to_message_id=reply_to_message_id,
|
||||||
|
timeout=timeout,
|
||||||
|
parse_mode=parse_mode,
|
||||||
|
)
|
||||||
|
|
||||||
return self.parse_msg(msg)
|
return self.parse_msg(msg)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def send_video_note(self, chat_id: Union[str, int],
|
def send_video_note(
|
||||||
file_id: Optional[int] = None,
|
self,
|
||||||
url: Optional[str] = None,
|
chat_id: Union[str, int],
|
||||||
path: Optional[str] = None,
|
file_id: Optional[int] = None,
|
||||||
duration: Optional[int] = None,
|
url: Optional[str] = None,
|
||||||
disable_notification: bool = False,
|
path: Optional[str] = None,
|
||||||
reply_to_message_id: Optional[int] = None,
|
duration: Optional[int] = None,
|
||||||
timeout: int = 20) -> TelegramMessageResponse:
|
disable_notification: bool = False,
|
||||||
|
reply_to_message_id: Optional[int] = None,
|
||||||
|
timeout: int = 20,
|
||||||
|
) -> TelegramMessageResponse:
|
||||||
"""
|
"""
|
||||||
Send a video note to a chat. As of v.4.0, Telegram clients support rounded square mp4 videos of up to
|
Send a video note to a chat. As of v.4.0, Telegram clients support rounded square mp4 videos of up to
|
||||||
1 minute long.
|
1 minute long.
|
||||||
|
@ -511,22 +559,27 @@ class ChatTelegramPlugin(ChatPlugin):
|
||||||
telegram = self.get_telegram()
|
telegram = self.get_telegram()
|
||||||
|
|
||||||
with Resource(file_id=file_id, url=url, path=path) as resource:
|
with Resource(file_id=file_id, url=url, path=path) as resource:
|
||||||
msg = telegram.bot.send_video_note(chat_id=chat_id,
|
msg = telegram.bot.send_video_note(
|
||||||
video=resource,
|
chat_id=chat_id,
|
||||||
duration=duration,
|
video=resource,
|
||||||
disable_notification=disable_notification,
|
duration=duration,
|
||||||
reply_to_message_id=reply_to_message_id,
|
disable_notification=disable_notification,
|
||||||
timeout=timeout)
|
reply_to_message_id=reply_to_message_id,
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
|
||||||
return self.parse_msg(msg)
|
return self.parse_msg(msg)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def send_location(self, chat_id: Union[str, int],
|
def send_location(
|
||||||
latitude: float,
|
self,
|
||||||
longitude: float,
|
chat_id: Union[str, int],
|
||||||
disable_notification: bool = False,
|
latitude: float,
|
||||||
reply_to_message_id: Optional[int] = None,
|
longitude: float,
|
||||||
timeout: int = 20) -> TelegramMessageResponse:
|
disable_notification: bool = False,
|
||||||
|
reply_to_message_id: Optional[int] = None,
|
||||||
|
timeout: int = 20,
|
||||||
|
) -> TelegramMessageResponse:
|
||||||
"""
|
"""
|
||||||
Send a location to a chat.
|
Send a location to a chat.
|
||||||
|
|
||||||
|
@ -543,26 +596,31 @@ class ChatTelegramPlugin(ChatPlugin):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
telegram = self.get_telegram()
|
telegram = self.get_telegram()
|
||||||
msg = telegram.bot.send_location(chat_id=chat_id,
|
msg = telegram.bot.send_location(
|
||||||
latitude=latitude,
|
chat_id=chat_id,
|
||||||
longitude=longitude,
|
latitude=latitude,
|
||||||
disable_notification=disable_notification,
|
longitude=longitude,
|
||||||
reply_to_message_id=reply_to_message_id,
|
disable_notification=disable_notification,
|
||||||
timeout=timeout)
|
reply_to_message_id=reply_to_message_id,
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
|
||||||
return self.parse_msg(msg)
|
return self.parse_msg(msg)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def send_venue(self, chat_id: Union[str, int],
|
def send_venue(
|
||||||
latitude: float,
|
self,
|
||||||
longitude: float,
|
chat_id: Union[str, int],
|
||||||
title: str,
|
latitude: float,
|
||||||
address: str,
|
longitude: float,
|
||||||
foursquare_id: Optional[str] = None,
|
title: str,
|
||||||
foursquare_type: Optional[str] = None,
|
address: str,
|
||||||
disable_notification: bool = False,
|
foursquare_id: Optional[str] = None,
|
||||||
reply_to_message_id: Optional[int] = None,
|
foursquare_type: Optional[str] = None,
|
||||||
timeout: int = 20) -> TelegramMessageResponse:
|
disable_notification: bool = False,
|
||||||
|
reply_to_message_id: Optional[int] = None,
|
||||||
|
timeout: int = 20,
|
||||||
|
) -> TelegramMessageResponse:
|
||||||
"""
|
"""
|
||||||
Send the address of a venue to a chat.
|
Send the address of a venue to a chat.
|
||||||
|
|
||||||
|
@ -583,28 +641,33 @@ class ChatTelegramPlugin(ChatPlugin):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
telegram = self.get_telegram()
|
telegram = self.get_telegram()
|
||||||
msg = telegram.bot.send_venue(chat_id=chat_id,
|
msg = telegram.bot.send_venue(
|
||||||
latitude=latitude,
|
chat_id=chat_id,
|
||||||
longitude=longitude,
|
latitude=latitude,
|
||||||
title=title,
|
longitude=longitude,
|
||||||
address=address,
|
title=title,
|
||||||
foursquare_id=foursquare_id,
|
address=address,
|
||||||
foursquare_type=foursquare_type,
|
foursquare_id=foursquare_id,
|
||||||
disable_notification=disable_notification,
|
foursquare_type=foursquare_type,
|
||||||
reply_to_message_id=reply_to_message_id,
|
disable_notification=disable_notification,
|
||||||
timeout=timeout)
|
reply_to_message_id=reply_to_message_id,
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
|
||||||
return self.parse_msg(msg)
|
return self.parse_msg(msg)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def send_contact(self, chat_id: Union[str, int],
|
def send_contact(
|
||||||
phone_number: str,
|
self,
|
||||||
first_name: str,
|
chat_id: Union[str, int],
|
||||||
last_name: Optional[str] = None,
|
phone_number: str,
|
||||||
vcard: Optional[str] = None,
|
first_name: str,
|
||||||
disable_notification: bool = False,
|
last_name: Optional[str] = None,
|
||||||
reply_to_message_id: Optional[int] = None,
|
vcard: Optional[str] = None,
|
||||||
timeout: int = 20) -> TelegramMessageResponse:
|
disable_notification: bool = False,
|
||||||
|
reply_to_message_id: Optional[int] = None,
|
||||||
|
timeout: int = 20,
|
||||||
|
) -> TelegramMessageResponse:
|
||||||
"""
|
"""
|
||||||
Send a contact to a chat.
|
Send a contact to a chat.
|
||||||
|
|
||||||
|
@ -623,14 +686,16 @@ class ChatTelegramPlugin(ChatPlugin):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
telegram = self.get_telegram()
|
telegram = self.get_telegram()
|
||||||
msg = telegram.bot.send_contact(chat_id=chat_id,
|
msg = telegram.bot.send_contact(
|
||||||
phone_number=phone_number,
|
chat_id=chat_id,
|
||||||
first_name=first_name,
|
phone_number=phone_number,
|
||||||
last_name=last_name,
|
first_name=first_name,
|
||||||
vcard=vcard,
|
last_name=last_name,
|
||||||
disable_notification=disable_notification,
|
vcard=vcard,
|
||||||
reply_to_message_id=reply_to_message_id,
|
disable_notification=disable_notification,
|
||||||
timeout=timeout)
|
reply_to_message_id=reply_to_message_id,
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
|
||||||
return self.parse_msg(msg)
|
return self.parse_msg(msg)
|
||||||
|
|
||||||
|
@ -645,10 +710,14 @@ class ChatTelegramPlugin(ChatPlugin):
|
||||||
|
|
||||||
telegram = self.get_telegram()
|
telegram = self.get_telegram()
|
||||||
file = telegram.bot.get_file(file_id, timeout=timeout)
|
file = telegram.bot.get_file(file_id, timeout=timeout)
|
||||||
return TelegramFileResponse(file_id=file.file_id, file_path=file.file_path, file_size=file.file_size)
|
return TelegramFileResponse(
|
||||||
|
file_id=file.file_id, file_path=file.file_path, file_size=file.file_size
|
||||||
|
)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def get_chat(self, chat_id: Union[int, str], timeout: int = 20) -> TelegramChatResponse:
|
def get_chat(
|
||||||
|
self, chat_id: Union[int, str], timeout: int = 20
|
||||||
|
) -> TelegramChatResponse:
|
||||||
"""
|
"""
|
||||||
Get the info about a Telegram chat.
|
Get the info about a Telegram chat.
|
||||||
|
|
||||||
|
@ -658,18 +727,22 @@ class ChatTelegramPlugin(ChatPlugin):
|
||||||
|
|
||||||
telegram = self.get_telegram()
|
telegram = self.get_telegram()
|
||||||
chat = telegram.bot.get_chat(chat_id, timeout=timeout)
|
chat = telegram.bot.get_chat(chat_id, timeout=timeout)
|
||||||
return TelegramChatResponse(chat_id=chat.id,
|
return TelegramChatResponse(
|
||||||
link=chat.link,
|
chat_id=chat.id,
|
||||||
username=chat.username,
|
link=chat.link,
|
||||||
invite_link=chat.invite_link,
|
username=chat.username,
|
||||||
title=chat.title,
|
invite_link=chat.invite_link,
|
||||||
description=chat.description,
|
title=chat.title,
|
||||||
type=chat.type,
|
description=chat.description,
|
||||||
first_name=chat.first_name,
|
type=chat.type,
|
||||||
last_name=chat.last_name)
|
first_name=chat.first_name,
|
||||||
|
last_name=chat.last_name,
|
||||||
|
)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def get_chat_user(self, chat_id: Union[int, str], user_id: int, timeout: int = 20) -> TelegramUserResponse:
|
def get_chat_user(
|
||||||
|
self, chat_id: Union[int, str], user_id: int, timeout: int = 20
|
||||||
|
) -> TelegramUserResponse:
|
||||||
"""
|
"""
|
||||||
Get the info about a user connected to a chat.
|
Get the info about a user connected to a chat.
|
||||||
|
|
||||||
|
@ -680,16 +753,20 @@ class ChatTelegramPlugin(ChatPlugin):
|
||||||
|
|
||||||
telegram = self.get_telegram()
|
telegram = self.get_telegram()
|
||||||
user = telegram.bot.get_chat_member(chat_id, user_id, timeout=timeout)
|
user = telegram.bot.get_chat_member(chat_id, user_id, timeout=timeout)
|
||||||
return TelegramUserResponse(user_id=user.user.id,
|
return TelegramUserResponse(
|
||||||
link=user.user.link,
|
user_id=user.user.id,
|
||||||
username=user.user.username,
|
link=user.user.link,
|
||||||
first_name=user.user.first_name,
|
username=user.user.username,
|
||||||
last_name=user.user.last_name,
|
first_name=user.user.first_name,
|
||||||
is_bot=user.user.is_bot,
|
last_name=user.user.last_name,
|
||||||
language_code=user.user.language_code)
|
is_bot=user.user.is_bot,
|
||||||
|
language_code=user.user.language_code,
|
||||||
|
)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def get_chat_administrators(self, chat_id: Union[int, str], timeout: int = 20) -> TelegramUsersResponse:
|
def get_chat_administrators(
|
||||||
|
self, chat_id: Union[int, str], timeout: int = 20
|
||||||
|
) -> TelegramUsersResponse:
|
||||||
"""
|
"""
|
||||||
Get the list of the administrators of a chat.
|
Get the list of the administrators of a chat.
|
||||||
|
|
||||||
|
@ -699,20 +776,25 @@ class ChatTelegramPlugin(ChatPlugin):
|
||||||
|
|
||||||
telegram = self.get_telegram()
|
telegram = self.get_telegram()
|
||||||
admins = telegram.bot.get_chat_administrators(chat_id, timeout=timeout)
|
admins = telegram.bot.get_chat_administrators(chat_id, timeout=timeout)
|
||||||
return TelegramUsersResponse([
|
return TelegramUsersResponse(
|
||||||
TelegramUserResponse(
|
[
|
||||||
user_id=user.user.id,
|
TelegramUserResponse(
|
||||||
link=user.user.link,
|
user_id=user.user.id,
|
||||||
username=user.user.username,
|
link=user.user.link,
|
||||||
first_name=user.user.first_name,
|
username=user.user.username,
|
||||||
last_name=user.user.last_name,
|
first_name=user.user.first_name,
|
||||||
is_bot=user.user.is_bot,
|
last_name=user.user.last_name,
|
||||||
language_code=user.user.language_code,
|
is_bot=user.user.is_bot,
|
||||||
) for user in admins
|
language_code=user.user.language_code,
|
||||||
])
|
)
|
||||||
|
for user in admins
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def get_chat_members_count(self, chat_id: Union[int, str], timeout: int = 20) -> int:
|
def get_chat_members_count(
|
||||||
|
self, chat_id: Union[int, str], timeout: int = 20
|
||||||
|
) -> int:
|
||||||
"""
|
"""
|
||||||
Get the number of users in a chat.
|
Get the number of users in a chat.
|
||||||
|
|
||||||
|
@ -723,10 +805,13 @@ class ChatTelegramPlugin(ChatPlugin):
|
||||||
return telegram.bot.get_chat_members_count(chat_id, timeout=timeout)
|
return telegram.bot.get_chat_members_count(chat_id, timeout=timeout)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def kick_chat_member(self, chat_id: Union[str, int],
|
def kick_chat_member(
|
||||||
user_id: int,
|
self,
|
||||||
until_date: Optional[datetime.datetime] = None,
|
chat_id: Union[str, int],
|
||||||
timeout: int = 20):
|
user_id: int,
|
||||||
|
until_date: Optional[datetime.datetime] = None,
|
||||||
|
timeout: int = 20,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Kick a user from a chat.
|
Kick a user from a chat.
|
||||||
|
|
||||||
|
@ -742,15 +827,13 @@ class ChatTelegramPlugin(ChatPlugin):
|
||||||
|
|
||||||
telegram = self.get_telegram()
|
telegram = self.get_telegram()
|
||||||
telegram.bot.kick_chat_member(
|
telegram.bot.kick_chat_member(
|
||||||
chat_id=chat_id,
|
chat_id=chat_id, user_id=user_id, until_date=until_date, timeout=timeout
|
||||||
user_id=user_id,
|
)
|
||||||
until_date=until_date,
|
|
||||||
timeout=timeout)
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def unban_chat_member(self, chat_id: Union[str, int],
|
def unban_chat_member(
|
||||||
user_id: int,
|
self, chat_id: Union[str, int], user_id: int, timeout: int = 20
|
||||||
timeout: int = 20):
|
):
|
||||||
"""
|
"""
|
||||||
Lift the ban from a chat member.
|
Lift the ban from a chat member.
|
||||||
|
|
||||||
|
@ -765,22 +848,24 @@ class ChatTelegramPlugin(ChatPlugin):
|
||||||
|
|
||||||
telegram = self.get_telegram()
|
telegram = self.get_telegram()
|
||||||
telegram.bot.unban_chat_member(
|
telegram.bot.unban_chat_member(
|
||||||
chat_id=chat_id,
|
chat_id=chat_id, user_id=user_id, timeout=timeout
|
||||||
user_id=user_id,
|
)
|
||||||
timeout=timeout)
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def promote_chat_member(self, chat_id: Union[str, int],
|
def promote_chat_member(
|
||||||
user_id: int,
|
self,
|
||||||
can_change_info: Optional[bool] = None,
|
chat_id: Union[str, int],
|
||||||
can_post_messages: Optional[bool] = None,
|
user_id: int,
|
||||||
can_edit_messages: Optional[bool] = None,
|
can_change_info: Optional[bool] = None,
|
||||||
can_delete_messages: Optional[bool] = None,
|
can_post_messages: Optional[bool] = None,
|
||||||
can_invite_users: Optional[bool] = None,
|
can_edit_messages: Optional[bool] = None,
|
||||||
can_restrict_members: Optional[bool] = None,
|
can_delete_messages: Optional[bool] = None,
|
||||||
can_promote_members: Optional[bool] = None,
|
can_invite_users: Optional[bool] = None,
|
||||||
can_pin_messages: Optional[bool] = None,
|
can_restrict_members: Optional[bool] = None,
|
||||||
timeout: int = 20):
|
can_promote_members: Optional[bool] = None,
|
||||||
|
can_pin_messages: Optional[bool] = None,
|
||||||
|
timeout: int = 20,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Promote or demote a member.
|
Promote or demote a member.
|
||||||
|
|
||||||
|
@ -813,12 +898,11 @@ class ChatTelegramPlugin(ChatPlugin):
|
||||||
can_restrict_members=can_restrict_members,
|
can_restrict_members=can_restrict_members,
|
||||||
can_promote_members=can_promote_members,
|
can_promote_members=can_promote_members,
|
||||||
can_pin_messages=can_pin_messages,
|
can_pin_messages=can_pin_messages,
|
||||||
timeout=timeout)
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def set_chat_title(self, chat_id: Union[str, int],
|
def set_chat_title(self, chat_id: Union[str, int], title: str, timeout: int = 20):
|
||||||
title: str,
|
|
||||||
timeout: int = 20):
|
|
||||||
"""
|
"""
|
||||||
Set the title of a channel/group.
|
Set the title of a channel/group.
|
||||||
|
|
||||||
|
@ -832,15 +916,12 @@ class ChatTelegramPlugin(ChatPlugin):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
telegram = self.get_telegram()
|
telegram = self.get_telegram()
|
||||||
telegram.bot.set_chat_title(
|
telegram.bot.set_chat_title(chat_id=chat_id, description=title, timeout=timeout)
|
||||||
chat_id=chat_id,
|
|
||||||
description=title,
|
|
||||||
timeout=timeout)
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def set_chat_description(self, chat_id: Union[str, int],
|
def set_chat_description(
|
||||||
description: str,
|
self, chat_id: Union[str, int], description: str, timeout: int = 20
|
||||||
timeout: int = 20):
|
):
|
||||||
"""
|
"""
|
||||||
Set the description of a channel/group.
|
Set the description of a channel/group.
|
||||||
|
|
||||||
|
@ -855,14 +936,11 @@ class ChatTelegramPlugin(ChatPlugin):
|
||||||
|
|
||||||
telegram = self.get_telegram()
|
telegram = self.get_telegram()
|
||||||
telegram.bot.set_chat_description(
|
telegram.bot.set_chat_description(
|
||||||
chat_id=chat_id,
|
chat_id=chat_id, description=description, timeout=timeout
|
||||||
description=description,
|
)
|
||||||
timeout=timeout)
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def set_chat_photo(self, chat_id: Union[str, int],
|
def set_chat_photo(self, chat_id: Union[str, int], path: str, timeout: int = 20):
|
||||||
path: str,
|
|
||||||
timeout: int = 20):
|
|
||||||
"""
|
"""
|
||||||
Set the photo of a channel/group.
|
Set the photo of a channel/group.
|
||||||
|
|
||||||
|
@ -879,13 +957,11 @@ class ChatTelegramPlugin(ChatPlugin):
|
||||||
|
|
||||||
with Resource(path=path) as resource:
|
with Resource(path=path) as resource:
|
||||||
telegram.bot.set_chat_photo(
|
telegram.bot.set_chat_photo(
|
||||||
chat_id=chat_id,
|
chat_id=chat_id, photo=resource, timeout=timeout
|
||||||
photo=resource,
|
)
|
||||||
timeout=timeout)
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def delete_chat_photo(self, chat_id: Union[str, int],
|
def delete_chat_photo(self, chat_id: Union[str, int], timeout: int = 20):
|
||||||
timeout: int = 20):
|
|
||||||
"""
|
"""
|
||||||
Delete the photo of a channel/group.
|
Delete the photo of a channel/group.
|
||||||
|
|
||||||
|
@ -898,15 +974,16 @@ class ChatTelegramPlugin(ChatPlugin):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
telegram = self.get_telegram()
|
telegram = self.get_telegram()
|
||||||
telegram.bot.delete_chat_photo(
|
telegram.bot.delete_chat_photo(chat_id=chat_id, timeout=timeout)
|
||||||
chat_id=chat_id,
|
|
||||||
timeout=timeout)
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def pin_chat_message(self, chat_id: Union[str, int],
|
def pin_chat_message(
|
||||||
message_id: int,
|
self,
|
||||||
disable_notification: Optional[bool] = None,
|
chat_id: Union[str, int],
|
||||||
timeout: int = 20):
|
message_id: int,
|
||||||
|
disable_notification: Optional[bool] = None,
|
||||||
|
timeout: int = 20,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Pin a message in a chat.
|
Pin a message in a chat.
|
||||||
|
|
||||||
|
@ -925,11 +1002,11 @@ class ChatTelegramPlugin(ChatPlugin):
|
||||||
chat_id=chat_id,
|
chat_id=chat_id,
|
||||||
message_id=message_id,
|
message_id=message_id,
|
||||||
disable_notification=disable_notification,
|
disable_notification=disable_notification,
|
||||||
timeout=timeout)
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def unpin_chat_message(self, chat_id: Union[str, int],
|
def unpin_chat_message(self, chat_id: Union[str, int], timeout: int = 20):
|
||||||
timeout: int = 20):
|
|
||||||
"""
|
"""
|
||||||
Unpin the message of a chat.
|
Unpin the message of a chat.
|
||||||
|
|
||||||
|
@ -942,13 +1019,10 @@ class ChatTelegramPlugin(ChatPlugin):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
telegram = self.get_telegram()
|
telegram = self.get_telegram()
|
||||||
telegram.bot.unpin_chat_message(
|
telegram.bot.unpin_chat_message(chat_id=chat_id, timeout=timeout)
|
||||||
chat_id=chat_id,
|
|
||||||
timeout=timeout)
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def leave_chat(self, chat_id: Union[str, int],
|
def leave_chat(self, chat_id: Union[str, int], timeout: int = 20):
|
||||||
timeout: int = 20):
|
|
||||||
"""
|
"""
|
||||||
Leave a chat.
|
Leave a chat.
|
||||||
|
|
||||||
|
@ -961,9 +1035,7 @@ class ChatTelegramPlugin(ChatPlugin):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
telegram = self.get_telegram()
|
telegram = self.get_telegram()
|
||||||
telegram.bot.leave_chat(
|
telegram.bot.leave_chat(chat_id=chat_id, timeout=timeout)
|
||||||
chat_id=chat_id,
|
|
||||||
timeout=timeout)
|
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
|
@ -10,15 +10,6 @@ class ClipboardPlugin(RunnablePlugin):
|
||||||
"""
|
"""
|
||||||
Plugin to programmatically copy strings to your system clipboard,
|
Plugin to programmatically copy strings to your system clipboard,
|
||||||
monitor and get the current clipboard content.
|
monitor and get the current clipboard content.
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
- **pyclip** (``pip install pyclip``)
|
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
- :class:`platypush.message.event.clipboard.ClipboardEvent` on clipboard update.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
|
@ -2,7 +2,7 @@ import enum
|
||||||
import json
|
import json
|
||||||
from typing import Set, Dict, Optional, Iterable, Callable, Union
|
from typing import Set, Dict, Optional, Iterable, Callable, Union
|
||||||
|
|
||||||
from gi.repository import GLib # type: ignore
|
from gi.repository import GLib # type: ignore
|
||||||
from pydbus import SessionBus, SystemBus
|
from pydbus import SessionBus, SystemBus
|
||||||
from pydbus.bus import Bus
|
from pydbus.bus import Bus
|
||||||
from defusedxml import ElementTree
|
from defusedxml import ElementTree
|
||||||
|
@ -27,7 +27,7 @@ class BusType(enum.Enum):
|
||||||
SESSION = 'session'
|
SESSION = 'session'
|
||||||
|
|
||||||
|
|
||||||
class DBusService():
|
class DBusService:
|
||||||
"""
|
"""
|
||||||
<node>
|
<node>
|
||||||
<interface name="org.platypush.Bus">
|
<interface name="org.platypush.Bus">
|
||||||
|
@ -94,21 +94,14 @@ class DbusPlugin(RunnablePlugin):
|
||||||
* It can be used to execute methods exponsed by D-Bus objects through the
|
* It can be used to execute methods exponsed by D-Bus objects through the
|
||||||
:meth:`.execute` method.
|
:meth:`.execute` method.
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **pydbus** (``pip install pydbus``)
|
|
||||||
* **defusedxml** (``pip install defusedxml``)
|
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
* :class:`platypush.message.event.dbus.DbusSignalEvent` when a signal is received.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, signals: Optional[Iterable[dict]] = None,
|
self,
|
||||||
service_name: Optional[str] = _default_service_name,
|
signals: Optional[Iterable[dict]] = None,
|
||||||
service_path: Optional[str] = _default_service_path, **kwargs
|
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
|
:param signals: Specify this if you want to subscribe to specific DBus
|
||||||
|
@ -138,8 +131,7 @@ class DbusPlugin(RunnablePlugin):
|
||||||
self._loop = None
|
self._loop = None
|
||||||
self._signals = DbusSignalSchema().load(signals or [], many=True)
|
self._signals = DbusSignalSchema().load(signals or [], many=True)
|
||||||
self._signal_handlers = [
|
self._signal_handlers = [
|
||||||
self._get_signal_handler(**signal)
|
self._get_signal_handler(**signal) for signal in self._signals
|
||||||
for signal in self._signals
|
|
||||||
]
|
]
|
||||||
|
|
||||||
self.service_name = service_name
|
self.service_name = service_name
|
||||||
|
@ -150,8 +142,12 @@ class DbusPlugin(RunnablePlugin):
|
||||||
def handler(sender, path, interface, signal, params):
|
def handler(sender, path, interface, signal, params):
|
||||||
get_bus().post(
|
get_bus().post(
|
||||||
DbusSignalEvent(
|
DbusSignalEvent(
|
||||||
bus=bus, signal=signal, path=path,
|
bus=bus,
|
||||||
interface=interface, sender=sender, params=params
|
signal=signal,
|
||||||
|
path=path,
|
||||||
|
interface=interface,
|
||||||
|
sender=sender,
|
||||||
|
params=params,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -201,7 +197,9 @@ class DbusPlugin(RunnablePlugin):
|
||||||
def _get_bus_names(bus: Bus) -> Set[str]:
|
def _get_bus_names(bus: Bus) -> Set[str]:
|
||||||
return {str(name) for name in bus.dbus.ListNames() if not name.startswith(':')}
|
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):
|
def path_names(
|
||||||
|
self, bus: Bus, service: str, object_path='/', paths=None, service_dict=None
|
||||||
|
):
|
||||||
if paths is None:
|
if paths is None:
|
||||||
paths = {}
|
paths = {}
|
||||||
if service_dict is None:
|
if service_dict is None:
|
||||||
|
@ -212,10 +210,14 @@ class DbusPlugin(RunnablePlugin):
|
||||||
obj = bus.get(service, object_path)
|
obj = bus.get(service, object_path)
|
||||||
interface = obj['org.freedesktop.DBus.Introspectable']
|
interface = obj['org.freedesktop.DBus.Introspectable']
|
||||||
except GLib.GError as e:
|
except GLib.GError as e:
|
||||||
self.logger.warning(f'Could not inspect D-Bus object {service}, path={object_path}: {e}')
|
self.logger.warning(
|
||||||
|
f'Could not inspect D-Bus object {service}, path={object_path}: {e}'
|
||||||
|
)
|
||||||
return {}
|
return {}
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
self.logger.warning(f'Could not get interfaces on the D-Bus object {service}, path={object_path}: {e}')
|
self.logger.warning(
|
||||||
|
f'Could not get interfaces on the D-Bus object {service}, path={object_path}: {e}'
|
||||||
|
)
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
xml_string = interface.Introspect()
|
xml_string = interface.Introspect()
|
||||||
|
@ -226,7 +228,9 @@ class DbusPlugin(RunnablePlugin):
|
||||||
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']))
|
||||||
self.path_names(bus, service, new_path, paths, service_dict=service_dict)
|
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 = '/'
|
||||||
|
@ -253,8 +257,9 @@ class DbusPlugin(RunnablePlugin):
|
||||||
return service_dict
|
return service_dict
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def query(self, service: Optional[str] = None, bus=tuple(t.value for t in BusType)) \
|
def query(
|
||||||
-> Dict[str, dict]:
|
self, service: Optional[str] = None, bus=tuple(t.value for t in BusType)
|
||||||
|
) -> 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.
|
||||||
|
|
||||||
|
@ -427,13 +432,13 @@ class DbusPlugin(RunnablePlugin):
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def execute(
|
def execute(
|
||||||
self,
|
self,
|
||||||
service: str,
|
service: str,
|
||||||
interface: str,
|
interface: str,
|
||||||
method_name: str,
|
method_name: str,
|
||||||
bus: str = BusType.SESSION.value,
|
bus: str = BusType.SESSION.value,
|
||||||
path: str = '/',
|
path: str = '/',
|
||||||
args: Optional[list] = None
|
args: Optional[list] = None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Execute a method exposed on DBus.
|
Execute a method exposed on DBus.
|
||||||
|
|
|
@ -7,10 +7,6 @@ from platypush.plugins import Plugin, action
|
||||||
class DropboxPlugin(Plugin):
|
class DropboxPlugin(Plugin):
|
||||||
"""
|
"""
|
||||||
Plugin to manage a Dropbox account and its files and folders.
|
Plugin to manage a Dropbox account and its files and folders.
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **dropbox** (``pip install dropbox``)
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, access_token, **kwargs):
|
def __init__(self, access_token, **kwargs):
|
||||||
|
@ -101,15 +97,26 @@ class DropboxPlugin(Plugin):
|
||||||
for item in files:
|
for item in files:
|
||||||
entry = {
|
entry = {
|
||||||
attr: getattr(item, attr)
|
attr: getattr(item, attr)
|
||||||
for attr in ['id', 'name', 'path_display', 'path_lower',
|
for attr in [
|
||||||
'parent_shared_folder_id', 'property_groups']
|
'id',
|
||||||
|
'name',
|
||||||
|
'path_display',
|
||||||
|
'path_lower',
|
||||||
|
'parent_shared_folder_id',
|
||||||
|
'property_groups',
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
if item.sharing_info:
|
if item.sharing_info:
|
||||||
entry['sharing_info'] = {
|
entry['sharing_info'] = {
|
||||||
attr: getattr(item.sharing_info, attr)
|
attr: getattr(item.sharing_info, attr)
|
||||||
for attr in ['no_access', 'parent_shared_folder_id', 'read_only',
|
for attr in [
|
||||||
'shared_folder_id', 'traverse_only']
|
'no_access',
|
||||||
|
'parent_shared_folder_id',
|
||||||
|
'read_only',
|
||||||
|
'shared_folder_id',
|
||||||
|
'traverse_only',
|
||||||
|
]
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
entry['sharing_info'] = {}
|
entry['sharing_info'] = {}
|
||||||
|
@ -118,7 +125,13 @@ class DropboxPlugin(Plugin):
|
||||||
entry['client_modified'] = item.client_modified.isoformat()
|
entry['client_modified'] = item.client_modified.isoformat()
|
||||||
entry['server_modified'] = item.server_modified.isoformat()
|
entry['server_modified'] = item.server_modified.isoformat()
|
||||||
|
|
||||||
for attr in ['content_hash', 'has_explicit_shared_members', 'is_downloadable', 'rev', 'size']:
|
for attr in [
|
||||||
|
'content_hash',
|
||||||
|
'has_explicit_shared_members',
|
||||||
|
'is_downloadable',
|
||||||
|
'rev',
|
||||||
|
'size',
|
||||||
|
]:
|
||||||
if hasattr(item, attr):
|
if hasattr(item, attr):
|
||||||
entry[attr] = getattr(item, attr)
|
entry[attr] = getattr(item, attr)
|
||||||
|
|
||||||
|
@ -127,8 +140,14 @@ class DropboxPlugin(Plugin):
|
||||||
return entries
|
return entries
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def copy(self, from_path: str, to_path: str, allow_shared_folder=True, autorename=False,
|
def copy(
|
||||||
allow_ownership_transfer=False):
|
self,
|
||||||
|
from_path: str,
|
||||||
|
to_path: str,
|
||||||
|
allow_shared_folder=True,
|
||||||
|
autorename=False,
|
||||||
|
allow_ownership_transfer=False,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Copy a file or folder to a different location in the user's Dropbox. If the source path is a folder all
|
Copy a file or folder to a different location in the user's Dropbox. If the source path is a folder all
|
||||||
its contents will be copied.
|
its contents will be copied.
|
||||||
|
@ -148,12 +167,23 @@ class DropboxPlugin(Plugin):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
dbx = self._get_instance()
|
dbx = self._get_instance()
|
||||||
dbx.files_copy_v2(from_path, to_path, allow_shared_folder=allow_shared_folder,
|
dbx.files_copy_v2(
|
||||||
autorename=autorename, allow_ownership_transfer=allow_ownership_transfer)
|
from_path,
|
||||||
|
to_path,
|
||||||
|
allow_shared_folder=allow_shared_folder,
|
||||||
|
autorename=autorename,
|
||||||
|
allow_ownership_transfer=allow_ownership_transfer,
|
||||||
|
)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def move(self, from_path: str, to_path: str, allow_shared_folder=True, autorename=False,
|
def move(
|
||||||
allow_ownership_transfer=False):
|
self,
|
||||||
|
from_path: str,
|
||||||
|
to_path: str,
|
||||||
|
allow_shared_folder=True,
|
||||||
|
autorename=False,
|
||||||
|
allow_ownership_transfer=False,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Move a file or folder to a different location in the user's Dropbox. If the source path is a folder all its
|
Move a file or folder to a different location in the user's Dropbox. If the source path is a folder all its
|
||||||
contents will be moved.
|
contents will be moved.
|
||||||
|
@ -173,8 +203,13 @@ class DropboxPlugin(Plugin):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
dbx = self._get_instance()
|
dbx = self._get_instance()
|
||||||
dbx.files_move_v2(from_path, to_path, allow_shared_folder=allow_shared_folder,
|
dbx.files_move_v2(
|
||||||
autorename=autorename, allow_ownership_transfer=allow_ownership_transfer)
|
from_path,
|
||||||
|
to_path,
|
||||||
|
allow_shared_folder=allow_shared_folder,
|
||||||
|
autorename=autorename,
|
||||||
|
allow_ownership_transfer=allow_ownership_transfer,
|
||||||
|
)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def delete(self, path: str):
|
def delete(self, path: str):
|
||||||
|
@ -251,7 +286,9 @@ class DropboxPlugin(Plugin):
|
||||||
|
|
||||||
if download_path:
|
if download_path:
|
||||||
if os.path.isdir(download_path):
|
if os.path.isdir(download_path):
|
||||||
download_path = os.path.join(download_path, result.metadata.name + '.zip')
|
download_path = os.path.join(
|
||||||
|
download_path, result.metadata.name + '.zip'
|
||||||
|
)
|
||||||
|
|
||||||
with open(download_path, 'wb') as f:
|
with open(download_path, 'wb') as f:
|
||||||
f.write(response.content)
|
f.write(response.content)
|
||||||
|
@ -350,8 +387,13 @@ class DropboxPlugin(Plugin):
|
||||||
from dropbox.files import SearchMode
|
from dropbox.files import SearchMode
|
||||||
|
|
||||||
dbx = self._get_instance()
|
dbx = self._get_instance()
|
||||||
response = dbx.files_search(query=query, path=path, start=start, max_results=max_results,
|
response = dbx.files_search(
|
||||||
mode=SearchMode.filename_and_content if content else SearchMode.filename)
|
query=query,
|
||||||
|
path=path,
|
||||||
|
start=start,
|
||||||
|
max_results=max_results,
|
||||||
|
mode=SearchMode.filename_and_content if content else SearchMode.filename,
|
||||||
|
)
|
||||||
|
|
||||||
results = [self._parse_metadata(match.metadata) for match in response.matches]
|
results = [self._parse_metadata(match.metadata) for match in response.matches]
|
||||||
|
|
||||||
|
@ -397,8 +439,12 @@ class DropboxPlugin(Plugin):
|
||||||
else:
|
else:
|
||||||
raise SyntaxError('Please specify either a file or text to be uploaded')
|
raise SyntaxError('Please specify either a file or text to be uploaded')
|
||||||
|
|
||||||
metadata = dbx.files_upload(content, path, autorename=autorename,
|
metadata = dbx.files_upload(
|
||||||
mode=WriteMode.overwrite if overwrite else WriteMode.add)
|
content,
|
||||||
|
path,
|
||||||
|
autorename=autorename,
|
||||||
|
mode=WriteMode.overwrite if overwrite else WriteMode.add,
|
||||||
|
)
|
||||||
|
|
||||||
return self._parse_metadata(metadata)
|
return self._parse_metadata(metadata)
|
||||||
|
|
||||||
|
|
|
@ -9,15 +9,11 @@ from platypush.plugins import Plugin, action
|
||||||
class FfmpegPlugin(Plugin):
|
class FfmpegPlugin(Plugin):
|
||||||
"""
|
"""
|
||||||
Generic FFmpeg plugin to interact with media files and devices.
|
Generic FFmpeg plugin to interact with media files and devices.
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **ffmpeg-python** (``pip install ffmpeg-python``)
|
|
||||||
* The **ffmpeg** package installed on the system.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, ffmpeg_cmd: str = 'ffmpeg', ffprobe_cmd: str = 'ffprobe', **kwargs):
|
def __init__(
|
||||||
|
self, ffmpeg_cmd: str = 'ffmpeg', ffprobe_cmd: str = 'ffprobe', **kwargs
|
||||||
|
):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.ffmpeg_cmd = ffmpeg_cmd
|
self.ffmpeg_cmd = ffmpeg_cmd
|
||||||
self.ffprobe_cmd = ffprobe_cmd
|
self.ffprobe_cmd = ffprobe_cmd
|
||||||
|
@ -102,14 +98,19 @@ class FfmpegPlugin(Plugin):
|
||||||
"""
|
"""
|
||||||
# noinspection PyPackageRequirements
|
# noinspection PyPackageRequirements
|
||||||
import ffmpeg
|
import ffmpeg
|
||||||
|
|
||||||
filename = os.path.abspath(os.path.expanduser(filename))
|
filename = os.path.abspath(os.path.expanduser(filename))
|
||||||
info = ffmpeg.probe(filename, cmd=self.ffprobe_cmd, **kwargs)
|
info = ffmpeg.probe(filename, cmd=self.ffprobe_cmd, **kwargs)
|
||||||
return info
|
return info
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _poll_thread(proc: subprocess.Popen, packet_size: int, on_packet: Callable[[bytes], None],
|
def _poll_thread(
|
||||||
on_open: Optional[Callable[[], None]] = None,
|
proc: subprocess.Popen,
|
||||||
on_close: Optional[Callable[[], None]] = None):
|
packet_size: int,
|
||||||
|
on_packet: Callable[[bytes], None],
|
||||||
|
on_open: Optional[Callable[[], None]] = None,
|
||||||
|
on_close: Optional[Callable[[], None]] = None,
|
||||||
|
):
|
||||||
try:
|
try:
|
||||||
if on_open:
|
if on_open:
|
||||||
on_open()
|
on_open()
|
||||||
|
@ -122,25 +123,49 @@ class FfmpegPlugin(Plugin):
|
||||||
on_close()
|
on_close()
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def start(self, pipeline: List[dict], pipe_stdin: bool = False, pipe_stdout: bool = False,
|
def start(
|
||||||
pipe_stderr: bool = False, quiet: bool = False, overwrite_output: bool = False,
|
self,
|
||||||
on_packet: Callable[[bytes], None] = None, packet_size: int = 4096):
|
pipeline: List[dict],
|
||||||
|
pipe_stdin: bool = False,
|
||||||
|
pipe_stdout: bool = False,
|
||||||
|
pipe_stderr: bool = False,
|
||||||
|
quiet: bool = False,
|
||||||
|
overwrite_output: bool = False,
|
||||||
|
on_packet: Callable[[bytes], None] = None,
|
||||||
|
packet_size: int = 4096,
|
||||||
|
):
|
||||||
# noinspection PyPackageRequirements
|
# noinspection PyPackageRequirements
|
||||||
import ffmpeg
|
import ffmpeg
|
||||||
|
|
||||||
stream = ffmpeg
|
stream = ffmpeg
|
||||||
|
|
||||||
for step in pipeline:
|
for step in pipeline:
|
||||||
args = step.pop('args') if 'args' in step else []
|
args = step.pop('args') if 'args' in step else []
|
||||||
stream = getattr(stream, step.pop('method'))(*args, **step)
|
stream = getattr(stream, step.pop('method'))(*args, **step)
|
||||||
|
|
||||||
self.logger.info('Executing {cmd} {args}'.format(cmd=self.ffmpeg_cmd, args=stream.get_args()))
|
self.logger.info(
|
||||||
proc = stream.run_async(cmd=self.ffmpeg_cmd, pipe_stdin=pipe_stdin, pipe_stdout=pipe_stdout,
|
'Executing {cmd} {args}'.format(cmd=self.ffmpeg_cmd, args=stream.get_args())
|
||||||
pipe_stderr=pipe_stderr, quiet=quiet, overwrite_output=overwrite_output)
|
)
|
||||||
|
proc = stream.run_async(
|
||||||
|
cmd=self.ffmpeg_cmd,
|
||||||
|
pipe_stdin=pipe_stdin,
|
||||||
|
pipe_stdout=pipe_stdout,
|
||||||
|
pipe_stderr=pipe_stderr,
|
||||||
|
quiet=quiet,
|
||||||
|
overwrite_output=overwrite_output,
|
||||||
|
)
|
||||||
|
|
||||||
if on_packet:
|
if on_packet:
|
||||||
with self._thread_lock:
|
with self._thread_lock:
|
||||||
self._threads[self._next_thread_id] = threading.Thread(target=self._poll_thread, kwargs=dict(
|
self._threads[self._next_thread_id] = threading.Thread(
|
||||||
proc=proc, on_packet=on_packet, packet_size=packet_size))
|
target=self._poll_thread,
|
||||||
|
kwargs={
|
||||||
|
'proc': proc,
|
||||||
|
'on_packet': on_packet,
|
||||||
|
'packet_size': packet_size,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
self._threads[self._next_thread_id].start()
|
self._threads[self._next_thread_id].start()
|
||||||
self._next_thread_id += 1
|
self._next_thread_id += 1
|
||||||
|
|
||||||
|
|
|
@ -22,11 +22,6 @@ class GooglePlugin(Plugin):
|
||||||
python -m platypush.plugins.google.credentials \
|
python -m platypush.plugins.google.credentials \
|
||||||
'https://www.googleapis.com/auth/gmail.compose' ~/client_secret.json
|
'https://www.googleapis.com/auth/gmail.compose' ~/client_secret.json
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **google-api-python-client** (``pip install google-api-python-client``)
|
|
||||||
* **oauth2client** (``pip install oauth2client``)
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, scopes=None, **kwargs):
|
def __init__(self, scopes=None, **kwargs):
|
||||||
|
|
|
@ -7,13 +7,7 @@ from platypush.plugins.calendar import CalendarInterface
|
||||||
|
|
||||||
class GoogleCalendarPlugin(GooglePlugin, CalendarInterface):
|
class GoogleCalendarPlugin(GooglePlugin, CalendarInterface):
|
||||||
"""
|
"""
|
||||||
Google calendar plugin.
|
Google Calendar plugin.
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **google-api-python-client** (``pip install google-api-python-client``)
|
|
||||||
* **oauth2client** (``pip install oauth2client``)
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
scopes = ['https://www.googleapis.com/auth/calendar.readonly']
|
scopes = ['https://www.googleapis.com/auth/calendar.readonly']
|
||||||
|
|
|
@ -10,17 +10,13 @@ from platypush.message.response.google.drive import GoogleDriveFile
|
||||||
class GoogleDrivePlugin(GooglePlugin):
|
class GoogleDrivePlugin(GooglePlugin):
|
||||||
"""
|
"""
|
||||||
Google Drive plugin.
|
Google Drive plugin.
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **google-api-python-client** (``pip install google-api-python-client``)
|
|
||||||
* **oauth2client** (``pip install oauth2client``)
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
scopes = ['https://www.googleapis.com/auth/drive',
|
scopes = [
|
||||||
'https://www.googleapis.com/auth/drive.appfolder',
|
'https://www.googleapis.com/auth/drive',
|
||||||
'https://www.googleapis.com/auth/drive.photos.readonly']
|
'https://www.googleapis.com/auth/drive.appfolder',
|
||||||
|
'https://www.googleapis.com/auth/drive.photos.readonly',
|
||||||
|
]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(scopes=self.scopes, *args, **kwargs)
|
super().__init__(scopes=self.scopes, *args, **kwargs)
|
||||||
|
@ -30,13 +26,15 @@ class GoogleDrivePlugin(GooglePlugin):
|
||||||
|
|
||||||
# noinspection PyShadowingBuiltins
|
# noinspection PyShadowingBuiltins
|
||||||
@action
|
@action
|
||||||
def files(self,
|
def files(
|
||||||
filter: Optional[str] = None,
|
self,
|
||||||
folder_id: Optional[str] = None,
|
filter: Optional[str] = None,
|
||||||
limit: Optional[int] = 100,
|
folder_id: Optional[str] = None,
|
||||||
drive_id: Optional[str] = None,
|
limit: Optional[int] = 100,
|
||||||
spaces: Optional[Union[str, List[str]]] = None,
|
drive_id: Optional[str] = None,
|
||||||
order_by: Optional[Union[str, List[str]]] = None) -> Union[GoogleDriveFile, List[GoogleDriveFile]]:
|
spaces: Optional[Union[str, List[str]]] = None,
|
||||||
|
order_by: Optional[Union[str, List[str]]] = None,
|
||||||
|
) -> Union[GoogleDriveFile, List[GoogleDriveFile]]:
|
||||||
"""
|
"""
|
||||||
Get the list of files.
|
Get the list of files.
|
||||||
|
|
||||||
|
@ -90,25 +88,32 @@ class GoogleDrivePlugin(GooglePlugin):
|
||||||
filter += "'{}' in parents".format(folder_id)
|
filter += "'{}' in parents".format(folder_id)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
results = service.files().list(
|
results = (
|
||||||
q=filter,
|
service.files()
|
||||||
driveId=drive_id,
|
.list(
|
||||||
pageSize=limit,
|
q=filter,
|
||||||
orderBy=order_by,
|
driveId=drive_id,
|
||||||
fields="nextPageToken, files(id, name, kind, mimeType)",
|
pageSize=limit,
|
||||||
pageToken=page_token,
|
orderBy=order_by,
|
||||||
spaces=spaces,
|
fields="nextPageToken, files(id, name, kind, mimeType)",
|
||||||
).execute()
|
pageToken=page_token,
|
||||||
|
spaces=spaces,
|
||||||
|
)
|
||||||
|
.execute()
|
||||||
|
)
|
||||||
|
|
||||||
page_token = results.get('nextPageToken')
|
page_token = results.get('nextPageToken')
|
||||||
files.extend([
|
files.extend(
|
||||||
GoogleDriveFile(
|
[
|
||||||
id=f.get('id'),
|
GoogleDriveFile(
|
||||||
name=f.get('name'),
|
id=f.get('id'),
|
||||||
type=f.get('kind').split('#')[1],
|
name=f.get('name'),
|
||||||
mime_type=f.get('mimeType'),
|
type=f.get('kind').split('#')[1],
|
||||||
) for f in results.get('files', [])
|
mime_type=f.get('mimeType'),
|
||||||
])
|
)
|
||||||
|
for f in results.get('files', [])
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
if not page_token or (limit and len(files) >= limit):
|
if not page_token or (limit and len(files) >= limit):
|
||||||
break
|
break
|
||||||
|
@ -131,14 +136,16 @@ class GoogleDrivePlugin(GooglePlugin):
|
||||||
)
|
)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def upload(self,
|
def upload(
|
||||||
path: str,
|
self,
|
||||||
mime_type: Optional[str] = None,
|
path: str,
|
||||||
name: Optional[str] = None,
|
mime_type: Optional[str] = None,
|
||||||
description: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
parents: Optional[List[str]] = None,
|
description: Optional[str] = None,
|
||||||
starred: bool = False,
|
parents: Optional[List[str]] = None,
|
||||||
target_mime_type: Optional[str] = None) -> GoogleDriveFile:
|
starred: bool = False,
|
||||||
|
target_mime_type: Optional[str] = None,
|
||||||
|
) -> GoogleDriveFile:
|
||||||
"""
|
"""
|
||||||
Upload a file to Google Drive.
|
Upload a file to Google Drive.
|
||||||
|
|
||||||
|
@ -171,11 +178,11 @@ class GoogleDrivePlugin(GooglePlugin):
|
||||||
|
|
||||||
media = MediaFileUpload(path, mimetype=mime_type)
|
media = MediaFileUpload(path, mimetype=mime_type)
|
||||||
service = self.get_service()
|
service = self.get_service()
|
||||||
file = service.files().create(
|
file = (
|
||||||
body=metadata,
|
service.files()
|
||||||
media_body=media,
|
.create(body=metadata, media_body=media, fields='*')
|
||||||
fields='*'
|
.execute()
|
||||||
).execute()
|
)
|
||||||
|
|
||||||
return GoogleDriveFile(
|
return GoogleDriveFile(
|
||||||
type=file.get('kind').split('#')[1],
|
type=file.get('kind').split('#')[1],
|
||||||
|
@ -216,12 +223,14 @@ class GoogleDrivePlugin(GooglePlugin):
|
||||||
return path
|
return path
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def create(self,
|
def create(
|
||||||
name: str,
|
self,
|
||||||
description: Optional[str] = None,
|
name: str,
|
||||||
mime_type: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
parents: Optional[List[str]] = None,
|
mime_type: Optional[str] = None,
|
||||||
starred: bool = False) -> GoogleDriveFile:
|
parents: Optional[List[str]] = None,
|
||||||
|
starred: bool = False,
|
||||||
|
) -> GoogleDriveFile:
|
||||||
"""
|
"""
|
||||||
Create a file.
|
Create a file.
|
||||||
|
|
||||||
|
@ -242,10 +251,7 @@ class GoogleDrivePlugin(GooglePlugin):
|
||||||
metadata['mimeType'] = mime_type
|
metadata['mimeType'] = mime_type
|
||||||
|
|
||||||
service = self.get_service()
|
service = self.get_service()
|
||||||
file = service.files().create(
|
file = service.files().create(body=metadata, fields='*').execute()
|
||||||
body=metadata,
|
|
||||||
fields='*'
|
|
||||||
).execute()
|
|
||||||
|
|
||||||
return GoogleDriveFile(
|
return GoogleDriveFile(
|
||||||
type=file.get('kind').split('#')[1],
|
type=file.get('kind').split('#')[1],
|
||||||
|
@ -255,15 +261,17 @@ class GoogleDrivePlugin(GooglePlugin):
|
||||||
)
|
)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def update(self,
|
def update(
|
||||||
file_id: str,
|
self,
|
||||||
name: Optional[str] = None,
|
file_id: str,
|
||||||
description: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
add_parents: Optional[List[str]] = None,
|
description: Optional[str] = None,
|
||||||
remove_parents: Optional[List[str]] = None,
|
add_parents: Optional[List[str]] = None,
|
||||||
mime_type: Optional[str] = None,
|
remove_parents: Optional[List[str]] = None,
|
||||||
starred: bool = None,
|
mime_type: Optional[str] = None,
|
||||||
trashed: bool = None) -> GoogleDriveFile:
|
starred: bool = None,
|
||||||
|
trashed: bool = None,
|
||||||
|
) -> GoogleDriveFile:
|
||||||
"""
|
"""
|
||||||
Update the metadata or the content of a file.
|
Update the metadata or the content of a file.
|
||||||
|
|
||||||
|
@ -293,11 +301,9 @@ class GoogleDrivePlugin(GooglePlugin):
|
||||||
metadata['trashed'] = trashed
|
metadata['trashed'] = trashed
|
||||||
|
|
||||||
service = self.get_service()
|
service = self.get_service()
|
||||||
file = service.files().update(
|
file = (
|
||||||
fileId=file_id,
|
service.files().update(fileId=file_id, body=metadata, fields='*').execute()
|
||||||
body=metadata,
|
)
|
||||||
fields='*'
|
|
||||||
).execute()
|
|
||||||
|
|
||||||
return GoogleDriveFile(
|
return GoogleDriveFile(
|
||||||
type=file.get('kind').split('#')[1],
|
type=file.get('kind').split('#')[1],
|
||||||
|
|
|
@ -5,20 +5,16 @@ from platypush.plugins.google import GooglePlugin
|
||||||
class GoogleFitPlugin(GooglePlugin):
|
class GoogleFitPlugin(GooglePlugin):
|
||||||
"""
|
"""
|
||||||
Google Fit plugin.
|
Google Fit plugin.
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **google-api-python-client** (``pip install google-api-python-client``)
|
|
||||||
* **oauth2client** (``pip install oauth2client``)
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
scopes = ['https://www.googleapis.com/auth/fitness.activity.read',
|
scopes = [
|
||||||
'https://www.googleapis.com/auth/fitness.body.read',
|
'https://www.googleapis.com/auth/fitness.activity.read',
|
||||||
'https://www.googleapis.com/auth/fitness.body_temperature.read',
|
'https://www.googleapis.com/auth/fitness.body.read',
|
||||||
'https://www.googleapis.com/auth/fitness.heart_rate.read',
|
'https://www.googleapis.com/auth/fitness.body_temperature.read',
|
||||||
'https://www.googleapis.com/auth/fitness.sleep.read',
|
'https://www.googleapis.com/auth/fitness.heart_rate.read',
|
||||||
'https://www.googleapis.com/auth/fitness.location.read']
|
'https://www.googleapis.com/auth/fitness.sleep.read',
|
||||||
|
'https://www.googleapis.com/auth/fitness.location.read',
|
||||||
|
]
|
||||||
|
|
||||||
def __init__(self, user_id='me', *args, **kwargs):
|
def __init__(self, user_id='me', *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
@ -30,7 +26,6 @@ class GoogleFitPlugin(GooglePlugin):
|
||||||
super().__init__(scopes=self.scopes, *args, **kwargs)
|
super().__init__(scopes=self.scopes, *args, **kwargs)
|
||||||
self.user_id = user_id
|
self.user_id = user_id
|
||||||
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def get_data_sources(self, user_id=None):
|
def get_data_sources(self, user_id=None):
|
||||||
"""
|
"""
|
||||||
|
@ -38,8 +33,9 @@ class GoogleFitPlugin(GooglePlugin):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
service = self.get_service(service='fitness', version='v1')
|
service = self.get_service(service='fitness', version='v1')
|
||||||
sources = service.users().dataSources(). \
|
sources = (
|
||||||
list(userId=user_id or self.user_id).execute()
|
service.users().dataSources().list(userId=user_id or self.user_id).execute()
|
||||||
|
)
|
||||||
|
|
||||||
return sources['dataSource']
|
return sources['dataSource']
|
||||||
|
|
||||||
|
@ -64,11 +60,19 @@ class GoogleFitPlugin(GooglePlugin):
|
||||||
kwargs['limit'] = limit
|
kwargs['limit'] = limit
|
||||||
data_points = []
|
data_points = []
|
||||||
|
|
||||||
for data_point in service.users().dataSources().dataPointChanges(). \
|
for data_point in (
|
||||||
list(**kwargs).execute().get('insertedDataPoint', []):
|
service.users()
|
||||||
data_point['startTime'] = float(data_point.pop('startTimeNanos'))/1e9
|
.dataSources()
|
||||||
data_point['endTime'] = float(data_point.pop('endTimeNanos'))/1e9
|
.dataPointChanges()
|
||||||
data_point['modifiedTime'] = float(data_point.pop('modifiedTimeMillis'))/1e6
|
.list(**kwargs)
|
||||||
|
.execute()
|
||||||
|
.get('insertedDataPoint', [])
|
||||||
|
):
|
||||||
|
data_point['startTime'] = float(data_point.pop('startTimeNanos')) / 1e9
|
||||||
|
data_point['endTime'] = float(data_point.pop('endTimeNanos')) / 1e9
|
||||||
|
data_point['modifiedTime'] = (
|
||||||
|
float(data_point.pop('modifiedTimeMillis')) / 1e6
|
||||||
|
)
|
||||||
values = []
|
values = []
|
||||||
|
|
||||||
for value in data_point.pop('value'):
|
for value in data_point.pop('value'):
|
||||||
|
@ -81,9 +85,11 @@ class GoogleFitPlugin(GooglePlugin):
|
||||||
elif value.get('mapVal'):
|
elif value.get('mapVal'):
|
||||||
value = {
|
value = {
|
||||||
v['key']: v['value'].get(
|
v['key']: v['value'].get(
|
||||||
'intVal', v['value'].get(
|
'intVal',
|
||||||
'fpVal', v['value'].get('stringVal')))
|
v['value'].get('fpVal', v['value'].get('stringVal')),
|
||||||
for v in value['mapVal'] }
|
)
|
||||||
|
for v in value['mapVal']
|
||||||
|
}
|
||||||
|
|
||||||
values.append(value)
|
values.append(value)
|
||||||
|
|
||||||
|
|
|
@ -17,12 +17,6 @@ from platypush.plugins.google import GooglePlugin
|
||||||
class GoogleMailPlugin(GooglePlugin):
|
class GoogleMailPlugin(GooglePlugin):
|
||||||
"""
|
"""
|
||||||
GMail plugin. It allows you to programmatically compose and (TODO) get emails
|
GMail plugin. It allows you to programmatically compose and (TODO) get emails
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **google-api-python-client** (``pip install google-api-python-client``)
|
|
||||||
* **oauth2client** (``pip install oauth2client``)
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
scopes = ['https://www.googleapis.com/auth/gmail.modify']
|
scopes = ['https://www.googleapis.com/auth/gmail.modify']
|
||||||
|
|
|
@ -14,12 +14,6 @@ datetime_types = Union[str, int, float, datetime]
|
||||||
class GoogleMapsPlugin(GooglePlugin):
|
class GoogleMapsPlugin(GooglePlugin):
|
||||||
"""
|
"""
|
||||||
Plugins that provides utilities to interact with Google Maps API services.
|
Plugins that provides utilities to interact with Google Maps API services.
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **google-api-python-client** (``pip install google-api-python-client``)
|
|
||||||
* **oauth2client** (``pip install oauth2client``)
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
scopes = []
|
scopes = []
|
||||||
|
|
|
@ -19,19 +19,13 @@ class GooglePubsubPlugin(Plugin):
|
||||||
3. Download the JSON service credentials file. By default platypush will look for the credentials file under
|
3. Download the JSON service credentials file. By default platypush will look for the credentials file under
|
||||||
~/.credentials/platypush/google/pubsub.json.
|
~/.credentials/platypush/google/pubsub.json.
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **google-api-python-client** (``pip install google-api-python-client``)
|
|
||||||
* **oauth2client** (``pip install oauth2client``)
|
|
||||||
* **google-cloud-pubsub** (``pip install google-cloud-pubsub``)
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
publisher_audience = 'https://pubsub.googleapis.com/google.pubsub.v1.Publisher'
|
publisher_audience = 'https://pubsub.googleapis.com/google.pubsub.v1.Publisher'
|
||||||
subscriber_audience = 'https://pubsub.googleapis.com/google.pubsub.v1.Subscriber'
|
subscriber_audience = 'https://pubsub.googleapis.com/google.pubsub.v1.Subscriber'
|
||||||
default_credentials_file = os.path.join(os.path.expanduser('~'),
|
default_credentials_file = os.path.join(
|
||||||
'.credentials', 'platypush', 'google', 'pubsub.json')
|
os.path.expanduser('~'), '.credentials', 'platypush', 'google', 'pubsub.json'
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, credentials_file: str = default_credentials_file, **kwargs):
|
def __init__(self, credentials_file: str = default_credentials_file, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
@ -43,13 +37,15 @@ class GooglePubsubPlugin(Plugin):
|
||||||
self.project_id = self.get_project_id()
|
self.project_id = self.get_project_id()
|
||||||
|
|
||||||
def get_project_id(self):
|
def get_project_id(self):
|
||||||
credentials = json.load(open(self.credentials_file))
|
with open(self.credentials_file) as f:
|
||||||
return credentials.get('project_id')
|
return json.load(f).get('project_id')
|
||||||
|
|
||||||
def get_credentials(self, audience: str):
|
def get_credentials(self, audience: str):
|
||||||
# noinspection PyPackageRequirements
|
|
||||||
from google.auth import jwt
|
from google.auth import jwt
|
||||||
return jwt.Credentials.from_service_account_file(self.credentials_file, audience=audience)
|
|
||||||
|
return jwt.Credentials.from_service_account_file(
|
||||||
|
self.credentials_file, audience=audience
|
||||||
|
)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def send_message(self, topic: str, msg, **kwargs):
|
def send_message(self, topic: str, msg, **kwargs):
|
||||||
|
@ -63,9 +59,7 @@ class GooglePubsubPlugin(Plugin):
|
||||||
:param msg: Message to be sent. It can be a list, a dict, or a Message object
|
:param msg: Message to be sent. It can be a list, a dict, or a Message object
|
||||||
:param kwargs: Extra arguments to be passed to .publish()
|
:param kwargs: Extra arguments to be passed to .publish()
|
||||||
"""
|
"""
|
||||||
# noinspection PyPackageRequirements
|
|
||||||
from google.cloud import pubsub_v1
|
from google.cloud import pubsub_v1
|
||||||
# noinspection PyPackageRequirements
|
|
||||||
from google.api_core.exceptions import AlreadyExists
|
from google.api_core.exceptions import AlreadyExists
|
||||||
|
|
||||||
credentials = self.get_credentials(self.publisher_audience)
|
credentials = self.get_credentials(self.publisher_audience)
|
||||||
|
@ -79,9 +73,9 @@ class GooglePubsubPlugin(Plugin):
|
||||||
except AlreadyExists:
|
except AlreadyExists:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if isinstance(msg, int) or isinstance(msg, float):
|
if isinstance(msg, (int, float)):
|
||||||
msg = str(msg)
|
msg = str(msg)
|
||||||
if isinstance(msg, dict) or isinstance(msg, list):
|
if isinstance(msg, (dict, list)):
|
||||||
msg = json.dumps(msg)
|
msg = json.dumps(msg)
|
||||||
if isinstance(msg, str):
|
if isinstance(msg, str):
|
||||||
msg = msg.encode()
|
msg = msg.encode()
|
||||||
|
|
|
@ -24,19 +24,19 @@ class GoogleTranslatePlugin(Plugin):
|
||||||
4. Create a new private JSON key for the service account and download it. By default platypush will look for the
|
4. Create a new private JSON key for the service account and download it. By default platypush will look for the
|
||||||
credentials file under ``~/.credentials/platypush/google/translate.json``.
|
credentials file under ``~/.credentials/platypush/google/translate.json``.
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **google-api-python-client** (``pip install google-api-python-client``)
|
|
||||||
* **oauth2client** (``pip install oauth2client``)
|
|
||||||
* **google-cloud-translate** (``pip install google-cloud-translate``)
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_maximum_text_length = 2000
|
_maximum_text_length = 2000
|
||||||
default_credentials_file = os.path.join(os.path.expanduser('~'), '.credentials', 'platypush', 'google',
|
default_credentials_file = os.path.join(
|
||||||
'translate.json')
|
os.path.expanduser('~'), '.credentials', 'platypush', 'google', 'translate.json'
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, target_language: str = 'en', credentials_file: Optional[str] = None, **kwargs):
|
def __init__(
|
||||||
|
self,
|
||||||
|
target_language: str = 'en',
|
||||||
|
credentials_file: Optional[str] = None,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
:param target_language: Default target language (default: 'en').
|
:param target_language: Default target language (default: 'en').
|
||||||
:param credentials_file: Google service account JSON credentials file. If none is specified then the plugin will
|
:param credentials_file: Google service account JSON credentials file. If none is specified then the plugin will
|
||||||
|
@ -50,7 +50,9 @@ class GoogleTranslatePlugin(Plugin):
|
||||||
self.credentials_file = None
|
self.credentials_file = None
|
||||||
|
|
||||||
if credentials_file:
|
if credentials_file:
|
||||||
self.credentials_file = os.path.abspath(os.path.expanduser(credentials_file))
|
self.credentials_file = os.path.abspath(
|
||||||
|
os.path.expanduser(credentials_file)
|
||||||
|
)
|
||||||
elif os.path.isfile(self.default_credentials_file):
|
elif os.path.isfile(self.default_credentials_file):
|
||||||
self.credentials_file = self.default_credentials_file
|
self.credentials_file = self.default_credentials_file
|
||||||
|
|
||||||
|
@ -59,11 +61,11 @@ class GoogleTranslatePlugin(Plugin):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _nearest_delimiter_index(text: str, pos: int) -> int:
|
def _nearest_delimiter_index(text: str, pos: int) -> int:
|
||||||
for i in range(min(pos, len(text)-1), -1, -1):
|
for i in range(min(pos, len(text) - 1), -1, -1):
|
||||||
if text[i] in [' ', '\t', ',', '.', ')', '>']:
|
if text[i] in [' ', '\t', ',', '.', ')', '>']:
|
||||||
return i
|
return i
|
||||||
elif text[i] in ['(', '<']:
|
elif text[i] in ['(', '<']:
|
||||||
return i-1 if i > 0 else 0
|
return i - 1 if i > 0 else 0
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
@ -77,17 +79,22 @@ class GoogleTranslatePlugin(Plugin):
|
||||||
parts.append(text)
|
parts.append(text)
|
||||||
text = ''
|
text = ''
|
||||||
else:
|
else:
|
||||||
part = text[:i+1]
|
part = text[: i + 1]
|
||||||
if part:
|
if part:
|
||||||
parts.append(part.strip())
|
parts.append(part.strip())
|
||||||
text = text[i+1:]
|
text = text[i + 1 :]
|
||||||
|
|
||||||
return parts
|
return parts
|
||||||
|
|
||||||
# noinspection PyShadowingBuiltins
|
# noinspection PyShadowingBuiltins
|
||||||
@action
|
@action
|
||||||
def translate(self, text: str, target_language: Optional[str] = None, source_language: Optional[str] = None,
|
def translate(
|
||||||
format: Optional[str] = None) -> TranslateResponse:
|
self,
|
||||||
|
text: str,
|
||||||
|
target_language: Optional[str] = None,
|
||||||
|
source_language: Optional[str] = None,
|
||||||
|
format: Optional[str] = None,
|
||||||
|
) -> TranslateResponse:
|
||||||
"""
|
"""
|
||||||
Translate a piece of text or HTML.
|
Translate a piece of text or HTML.
|
||||||
|
|
||||||
|
|
|
@ -5,12 +5,6 @@ from platypush.plugins.google import GooglePlugin
|
||||||
class GoogleYoutubePlugin(GooglePlugin):
|
class GoogleYoutubePlugin(GooglePlugin):
|
||||||
"""
|
"""
|
||||||
YouTube plugin.
|
YouTube plugin.
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **google-api-python-client** (``pip install google-api-python-client``)
|
|
||||||
* **oauth2client** (``pip install oauth2client``)
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
scopes = ['https://www.googleapis.com/auth/youtube.readonly']
|
scopes = ['https://www.googleapis.com/auth/youtube.readonly']
|
||||||
|
|
|
@ -17,11 +17,6 @@ class GotifyPlugin(RunnablePlugin):
|
||||||
|
|
||||||
`Gotify <https://gotify.net>`_ allows you process messages and notifications asynchronously
|
`Gotify <https://gotify.net>`_ allows you process messages and notifications asynchronously
|
||||||
over your own devices without relying on 3rd-party cloud services.
|
over your own devices without relying on 3rd-party cloud services.
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
* :class:`platypush.message.event.gotify.GotifyMessageEvent` when a new message is received.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, server_url: str, app_token: str, client_token: str, **kwargs):
|
def __init__(self, server_url: str, app_token: str, client_token: str, **kwargs):
|
||||||
|
@ -47,11 +42,13 @@ class GotifyPlugin(RunnablePlugin):
|
||||||
rs = getattr(requests, method)(
|
rs = getattr(requests, method)(
|
||||||
f'{self.server_url}/{endpoint}',
|
f'{self.server_url}/{endpoint}',
|
||||||
headers={
|
headers={
|
||||||
'X-Gotify-Key': self.app_token if method == 'post' else self.client_token,
|
'X-Gotify-Key': self.app_token
|
||||||
|
if method == 'post'
|
||||||
|
else self.client_token,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
**kwargs.pop('headers', {}),
|
**kwargs.pop('headers', {}),
|
||||||
},
|
},
|
||||||
**kwargs
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
rs.raise_for_status()
|
rs.raise_for_status()
|
||||||
|
@ -65,7 +62,9 @@ class GotifyPlugin(RunnablePlugin):
|
||||||
stop_events = []
|
stop_events = []
|
||||||
|
|
||||||
while not any(stop_events):
|
while not any(stop_events):
|
||||||
stop_events = self._should_stop.wait(timeout=1), self._disconnected_event.wait(timeout=1)
|
stop_events = self._should_stop.wait(
|
||||||
|
timeout=1
|
||||||
|
), self._disconnected_event.wait(timeout=1)
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
if self._ws_app:
|
if self._ws_app:
|
||||||
|
@ -78,7 +77,9 @@ class GotifyPlugin(RunnablePlugin):
|
||||||
self._ws_listener.join(5)
|
self._ws_listener.join(5)
|
||||||
|
|
||||||
if self._ws_listener and self._ws_listener.is_alive():
|
if self._ws_listener and self._ws_listener.is_alive():
|
||||||
self.logger.warning('Terminating the websocket process failed, killing the process')
|
self.logger.warning(
|
||||||
|
'Terminating the websocket process failed, killing the process'
|
||||||
|
)
|
||||||
self._ws_listener.kill()
|
self._ws_listener.kill()
|
||||||
|
|
||||||
if self._ws_listener:
|
if self._ws_listener:
|
||||||
|
@ -92,13 +93,18 @@ class GotifyPlugin(RunnablePlugin):
|
||||||
if self.should_stop() or self._connected_event.is_set():
|
if self.should_stop() or self._connected_event.is_set():
|
||||||
return
|
return
|
||||||
|
|
||||||
ws_url = '/'.join([self.server_url.split('/')[0].replace('http', 'ws'), *self.server_url.split('/')[1:]])
|
ws_url = '/'.join(
|
||||||
|
[
|
||||||
|
self.server_url.split('/')[0].replace('http', 'ws'),
|
||||||
|
*self.server_url.split('/')[1:],
|
||||||
|
]
|
||||||
|
)
|
||||||
self._ws_app = websocket.WebSocketApp(
|
self._ws_app = websocket.WebSocketApp(
|
||||||
f'{ws_url}/stream?token={self.client_token}',
|
f'{ws_url}/stream?token={self.client_token}',
|
||||||
on_open=self._on_open(),
|
on_open=self._on_open(),
|
||||||
on_message=self._on_msg(),
|
on_message=self._on_msg(),
|
||||||
on_error=self._on_error(),
|
on_error=self._on_error(),
|
||||||
on_close=self._on_close()
|
on_close=self._on_close(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def server():
|
def server():
|
||||||
|
@ -144,7 +150,13 @@ class GotifyPlugin(RunnablePlugin):
|
||||||
return hndl
|
return hndl
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def send_message(self, message: str, title: Optional[str] = None, priority: int = 0, extras: Optional[dict] = None):
|
def send_message(
|
||||||
|
self,
|
||||||
|
message: str,
|
||||||
|
title: Optional[str] = None,
|
||||||
|
priority: int = 0,
|
||||||
|
extras: Optional[dict] = None,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Send a message to the server.
|
Send a message to the server.
|
||||||
|
|
||||||
|
@ -155,12 +167,16 @@ class GotifyPlugin(RunnablePlugin):
|
||||||
:return: .. schema:: gotify.GotifyMessageSchema
|
:return: .. schema:: gotify.GotifyMessageSchema
|
||||||
"""
|
"""
|
||||||
return GotifyMessageSchema().dump(
|
return GotifyMessageSchema().dump(
|
||||||
self._execute('post', 'message', json={
|
self._execute(
|
||||||
'message': message,
|
'post',
|
||||||
'title': title,
|
'message',
|
||||||
'priority': priority,
|
json={
|
||||||
'extras': extras or {},
|
'message': message,
|
||||||
})
|
'title': title,
|
||||||
|
'priority': priority,
|
||||||
|
'extras': extras or {},
|
||||||
|
},
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
@ -174,11 +190,14 @@ class GotifyPlugin(RunnablePlugin):
|
||||||
"""
|
"""
|
||||||
return GotifyMessageSchema().dump(
|
return GotifyMessageSchema().dump(
|
||||||
self._execute(
|
self._execute(
|
||||||
'get', 'message', params={
|
'get',
|
||||||
|
'message',
|
||||||
|
params={
|
||||||
'limit': limit,
|
'limit': limit,
|
||||||
**({'since': since} if since else {}),
|
**({'since': since} if since else {}),
|
||||||
}
|
},
|
||||||
).get('messages', []), many=True
|
).get('messages', []),
|
||||||
|
many=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
|
|
@ -10,16 +10,6 @@ class GpioPlugin(RunnablePlugin):
|
||||||
"""
|
"""
|
||||||
This plugin can be used to interact with custom electronic devices
|
This plugin can be used to interact with custom electronic devices
|
||||||
connected to a Raspberry Pi (or compatible device) over GPIO pins.
|
connected to a Raspberry Pi (or compatible device) over GPIO pins.
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **RPi.GPIO** (``pip install RPi.GPIO``)
|
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
* :class:`platypush.message.event.gpio.GPIOEvent` when the value of a
|
|
||||||
monitored PIN changes.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|
|
@ -22,12 +22,6 @@ class GpioZeroborgPlugin(Plugin):
|
||||||
ZeroBorg plugin. It allows you to control a ZeroBorg
|
ZeroBorg plugin. It allows you to control a ZeroBorg
|
||||||
(https://www.piborg.org/motor-control-1135/zeroborg) motor controller and
|
(https://www.piborg.org/motor-control-1135/zeroborg) motor controller and
|
||||||
infrared sensor circuitry for Raspberry Pi
|
infrared sensor circuitry for Raspberry Pi
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
* :class:`platypush.message.event.zeroborg.ZeroborgDriveEvent` when motors direction changes
|
|
||||||
* :class:`platypush.message.event.zeroborg.ZeroborgStopEvent` upon motors stop
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, directions: Dict[str, List[float]] = None, **kwargs):
|
def __init__(self, directions: Dict[str, List[float]] = None, **kwargs):
|
||||||
|
@ -72,6 +66,7 @@ class GpioZeroborgPlugin(Plugin):
|
||||||
directions = {}
|
directions = {}
|
||||||
|
|
||||||
import platypush.plugins.gpio.zeroborg.lib as ZeroBorg
|
import platypush.plugins.gpio.zeroborg.lib as ZeroBorg
|
||||||
|
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
self.directions = directions
|
self.directions = directions
|
||||||
|
@ -109,13 +104,19 @@ class GpioZeroborgPlugin(Plugin):
|
||||||
if self._direction in self.directions:
|
if self._direction in self.directions:
|
||||||
self._motors = self.directions[self._direction]
|
self._motors = self.directions[self._direction]
|
||||||
else:
|
else:
|
||||||
self.logger.warning('Invalid direction {}: stopping motors'.format(self._direction))
|
self.logger.warning(
|
||||||
|
'Invalid direction {}: stopping motors'.format(
|
||||||
|
self._direction
|
||||||
|
)
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error('Error on _get_direction_from_sensors: {}'.format(str(e)))
|
self.logger.error(
|
||||||
|
'Error on _get_direction_from_sensors: {}'.format(str(e))
|
||||||
|
)
|
||||||
break
|
break
|
||||||
|
|
||||||
for i, power in enumerate(self._motors):
|
for i, power in enumerate(self._motors):
|
||||||
method = getattr(self.zb, 'SetMotor{}'.format(i+1))
|
method = getattr(self.zb, 'SetMotor{}'.format(i + 1))
|
||||||
method(power)
|
method(power)
|
||||||
finally:
|
finally:
|
||||||
self.zb.MotorsOff()
|
self.zb.MotorsOff()
|
||||||
|
@ -129,7 +130,11 @@ class GpioZeroborgPlugin(Plugin):
|
||||||
drive_thread.start()
|
drive_thread.start()
|
||||||
self._drive_thread = drive_thread
|
self._drive_thread = drive_thread
|
||||||
|
|
||||||
get_bus().post(ZeroborgDriveEvent(direction=self._direction, motors=self.directions[self._direction]))
|
get_bus().post(
|
||||||
|
ZeroborgDriveEvent(
|
||||||
|
direction=self._direction, motors=self.directions[self._direction]
|
||||||
|
)
|
||||||
|
)
|
||||||
return {'status': 'running', 'direction': direction}
|
return {'status': 'running', 'direction': direction}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
@ -163,7 +168,9 @@ class GpioZeroborgPlugin(Plugin):
|
||||||
return {
|
return {
|
||||||
'status': 'running' if self._direction else 'stopped',
|
'status': 'running' if self._direction else 'stopped',
|
||||||
'direction': self._direction,
|
'direction': self._direction,
|
||||||
'motors': [getattr(self.zb, 'GetMotor{}'.format(i+1))() for i in range(4)],
|
'motors': [
|
||||||
|
getattr(self.zb, 'GetMotor{}'.format(i + 1))() for i in range(4)
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -46,15 +46,6 @@ class HidPlugin(RunnablePlugin):
|
||||||
|
|
||||||
# udevadm control --reload && udevadm trigger
|
# udevadm control --reload && udevadm trigger
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
* :class:`platypush.message.event.hid.HidDeviceConnectedEvent` when a
|
|
||||||
device is connected
|
|
||||||
* :class:`platypush.message.event.hid.HidDeviceDisconnectedEvent` when
|
|
||||||
a previously available device is disconnected
|
|
||||||
* :class:`platypush.message.event.hid.HidDeviceDataEvent` when a
|
|
||||||
monitored device sends some data
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
from platypush.plugins import action
|
|
||||||
from platypush.plugins.http.request import HttpRequestPlugin
|
|
||||||
|
|
||||||
|
|
||||||
class HttpRequestRssPlugin(HttpRequestPlugin):
|
|
||||||
"""
|
|
||||||
Plugin to programmatically retrieve and parse an RSS feed URL.
|
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **feedparser** (``pip install feedparser``)
|
|
||||||
"""
|
|
||||||
|
|
||||||
@action
|
|
||||||
def get(self, url, **_):
|
|
||||||
import feedparser
|
|
||||||
|
|
||||||
response = super().get(url, output='text').output
|
|
||||||
feed = feedparser.parse(response)
|
|
||||||
return feed.entries
|
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
|
|
@ -1,15 +0,0 @@
|
||||||
manifest:
|
|
||||||
events: {}
|
|
||||||
install:
|
|
||||||
apk:
|
|
||||||
- py3-feedparser
|
|
||||||
apt:
|
|
||||||
- python3-feedparser
|
|
||||||
dnf:
|
|
||||||
- python-feedparser
|
|
||||||
pacman:
|
|
||||||
- python-feedparser
|
|
||||||
pip:
|
|
||||||
- feedparser
|
|
||||||
package: platypush.plugins.http.request.rss
|
|
||||||
type: plugin
|
|
|
@ -71,8 +71,6 @@ class HttpWebpagePlugin(Plugin):
|
||||||
|
|
||||||
Requires:
|
Requires:
|
||||||
|
|
||||||
* **weasyprint** (``pip install weasyprint``), optional, for HTML->PDF conversion
|
|
||||||
* **node** and **npm** installed on your system (to use the mercury-parser interface)
|
|
||||||
* The mercury-parser library installed (``npm install -g @postlight/mercury-parser``)
|
* The mercury-parser library installed (``npm install -g @postlight/mercury-parser``)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -7,23 +7,20 @@ class InputsPlugin(Plugin):
|
||||||
"""
|
"""
|
||||||
This plugin emulates user input on a keyboard/mouse. It requires the a graphical server (X server or Mac/Win
|
This plugin emulates user input on a keyboard/mouse. It requires the a graphical server (X server or Mac/Win
|
||||||
interface) to be running - it won't work in console mode.
|
interface) to be running - it won't work in console mode.
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **pyuserinput** (``pip install pyuserinput``)
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_keyboard():
|
def _get_keyboard():
|
||||||
# noinspection PyPackageRequirements
|
# noinspection PyPackageRequirements
|
||||||
from pykeyboard import PyKeyboard
|
from pykeyboard import PyKeyboard
|
||||||
|
|
||||||
return PyKeyboard()
|
return PyKeyboard()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_mouse():
|
def _get_mouse():
|
||||||
# noinspection PyPackageRequirements
|
# noinspection PyPackageRequirements
|
||||||
from pymouse import PyMouse
|
from pymouse import PyMouse
|
||||||
|
|
||||||
return PyMouse()
|
return PyMouse()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
@ -8,14 +8,6 @@ from platypush.plugins import Plugin, action
|
||||||
class KafkaPlugin(Plugin):
|
class KafkaPlugin(Plugin):
|
||||||
"""
|
"""
|
||||||
Plugin to send messages to an Apache Kafka instance (https://kafka.apache.org/)
|
Plugin to send messages to an Apache Kafka instance (https://kafka.apache.org/)
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
* :class:`platypush.message.event.kafka.KafkaMessageEvent` when a new message is received on the consumer topic.
|
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **kafka** (``pip install kafka-python``)
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, server=None, port=9092, **kwargs):
|
def __init__(self, server=None, port=9092, **kwargs):
|
||||||
|
@ -30,8 +22,9 @@ class KafkaPlugin(Plugin):
|
||||||
|
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
self.server = '{server}:{port}'.format(server=server, port=port) \
|
self.server = (
|
||||||
if server else None
|
'{server}:{port}'.format(server=server, port=port) if server else None
|
||||||
|
)
|
||||||
|
|
||||||
self.producer = None
|
self.producer = None
|
||||||
|
|
||||||
|
@ -60,13 +53,15 @@ class KafkaPlugin(Plugin):
|
||||||
kafka_backend = get_backend('kafka')
|
kafka_backend = get_backend('kafka')
|
||||||
server = kafka_backend.server
|
server = kafka_backend.server
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise RuntimeError(f'No Kafka server nor default server specified: {str(e)}')
|
raise RuntimeError(
|
||||||
|
f'No Kafka server nor default server specified: {str(e)}'
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
server = self.server
|
server = self.server
|
||||||
|
|
||||||
if isinstance(msg, dict) or isinstance(msg, list):
|
if isinstance(msg, (dict, list)):
|
||||||
msg = json.dumps(msg)
|
msg = json.dumps(msg)
|
||||||
msg = str(msg).encode('utf-8')
|
msg = str(msg).encode()
|
||||||
|
|
||||||
producer = KafkaProducer(bootstrap_servers=server)
|
producer = KafkaProducer(bootstrap_servers=server)
|
||||||
producer.send(topic, msg)
|
producer.send(topic, msg)
|
||||||
|
|
|
@ -8,10 +8,6 @@ class LastfmPlugin(Plugin):
|
||||||
"""
|
"""
|
||||||
Plugin to interact with your Last.FM (https://last.fm) account, update your
|
Plugin to interact with your Last.FM (https://last.fm) account, update your
|
||||||
current track and your scrobbles.
|
current track and your scrobbles.
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **pylast** (``pip install pylast``)
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, api_key, api_secret, username, password):
|
def __init__(self, api_key, api_secret, username, password):
|
||||||
|
|
|
@ -7,13 +7,8 @@ from platypush.plugins import Plugin, action
|
||||||
class LcdPlugin(Plugin, ABC):
|
class LcdPlugin(Plugin, ABC):
|
||||||
"""
|
"""
|
||||||
Abstract class for plugins to communicate with LCD displays.
|
Abstract class for plugins to communicate with LCD displays.
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **RPLCD** (``pip install RPLCD``)
|
|
||||||
* **RPi.GPIO** (``pip install RPi.GPIO``)
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.lcd = None
|
self.lcd = None
|
||||||
|
@ -21,9 +16,12 @@ class LcdPlugin(Plugin, ABC):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_pin_mode(pin_mode: str) -> int:
|
def _get_pin_mode(pin_mode: str) -> int:
|
||||||
import RPi.GPIO
|
import RPi.GPIO
|
||||||
|
|
||||||
pin_modes = ['BOARD', 'BCM']
|
pin_modes = ['BOARD', 'BCM']
|
||||||
pin_mode = pin_mode.upper()
|
pin_mode = pin_mode.upper()
|
||||||
assert pin_mode in pin_modes, 'Invalid pin_mode: {}. Supported modes: {}'.format(pin_mode, pin_modes)
|
assert (
|
||||||
|
pin_mode in pin_modes
|
||||||
|
), 'Invalid pin_mode: {}. Supported modes: {}'.format(pin_mode, pin_modes)
|
||||||
return getattr(RPi.GPIO, pin_mode).value
|
return getattr(RPi.GPIO, pin_mode).value
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
@ -105,7 +103,8 @@ class LcdPlugin(Plugin, ABC):
|
||||||
modes = ['left', 'right']
|
modes = ['left', 'right']
|
||||||
mode = mode.lower()
|
mode = mode.lower()
|
||||||
assert mode in modes, 'Unsupported text mode: {}. Supported modes: {}'.format(
|
assert mode in modes, 'Unsupported text mode: {}. Supported modes: {}'.format(
|
||||||
mode, modes)
|
mode, modes
|
||||||
|
)
|
||||||
|
|
||||||
self._init_lcd()
|
self._init_lcd()
|
||||||
self.lcd.text_align_mode = mode
|
self.lcd.text_align_mode = mode
|
||||||
|
|
|
@ -6,23 +6,26 @@ from platypush.plugins.lcd import LcdPlugin
|
||||||
class LcdGpioPlugin(LcdPlugin):
|
class LcdGpioPlugin(LcdPlugin):
|
||||||
"""
|
"""
|
||||||
Plugin to write to an LCD display connected via GPIO.
|
Plugin to write to an LCD display connected via GPIO.
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **RPLCD** (``pip install RPLCD``)
|
|
||||||
* **RPi.GPIO** (``pip install RPi.GPIO``)
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, pin_rs: int, pin_e: int, pins_data: List[int],
|
def __init__(
|
||||||
pin_rw: Optional[int] = None, pin_mode: str = 'BOARD',
|
self,
|
||||||
pin_backlight: Optional[int] = None,
|
pin_rs: int,
|
||||||
cols: int = 16, rows: int = 2,
|
pin_e: int,
|
||||||
backlight_enabled: bool = True,
|
pins_data: List[int],
|
||||||
backlight_mode: str = 'active_low',
|
pin_rw: Optional[int] = None,
|
||||||
dotsize: int = 8, charmap: str = 'A02',
|
pin_mode: str = 'BOARD',
|
||||||
auto_linebreaks: bool = True,
|
pin_backlight: Optional[int] = None,
|
||||||
compat_mode: bool = False, **kwargs):
|
cols: int = 16,
|
||||||
|
rows: int = 2,
|
||||||
|
backlight_enabled: bool = True,
|
||||||
|
backlight_mode: str = 'active_low',
|
||||||
|
dotsize: int = 8,
|
||||||
|
charmap: str = 'A02',
|
||||||
|
auto_linebreaks: bool = True,
|
||||||
|
compat_mode: bool = False,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
:param pin_rs: Pin for register select (RS).
|
:param pin_rs: Pin for register select (RS).
|
||||||
:param pin_e: Pin to start data read or write (E).
|
:param pin_e: Pin to start data read or write (E).
|
||||||
|
@ -70,15 +73,23 @@ class LcdGpioPlugin(LcdPlugin):
|
||||||
|
|
||||||
def _get_lcd(self):
|
def _get_lcd(self):
|
||||||
from RPLCD.gpio import CharLCD
|
from RPLCD.gpio import CharLCD
|
||||||
return CharLCD(cols=self.cols, rows=self.rows, pin_rs=self.pin_rs,
|
|
||||||
pin_e=self.pin_e, pins_data=self.pins_data,
|
return CharLCD(
|
||||||
numbering_mode=self.pin_mode, pin_rw=self.pin_rw,
|
cols=self.cols,
|
||||||
pin_backlight=self.pin_backlight,
|
rows=self.rows,
|
||||||
backlight_enabled=self.backlight_enabled,
|
pin_rs=self.pin_rs,
|
||||||
backlight_mode=self.backlight_mode,
|
pin_e=self.pin_e,
|
||||||
dotsize=self.dotsize, charmap=self.charmap,
|
pins_data=self.pins_data,
|
||||||
auto_linebreaks=self.auto_linebreaks,
|
numbering_mode=self.pin_mode,
|
||||||
compat_mode=self.compat_mode)
|
pin_rw=self.pin_rw,
|
||||||
|
pin_backlight=self.pin_backlight,
|
||||||
|
backlight_enabled=self.backlight_enabled,
|
||||||
|
backlight_mode=self.backlight_mode,
|
||||||
|
dotsize=self.dotsize,
|
||||||
|
charmap=self.charmap,
|
||||||
|
auto_linebreaks=self.auto_linebreaks,
|
||||||
|
compat_mode=self.compat_mode,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
|
@ -8,47 +8,63 @@ class LcdI2cPlugin(LcdPlugin):
|
||||||
Plugin to write to an LCD display connected via I2C.
|
Plugin to write to an LCD display connected via I2C.
|
||||||
Adafruit I2C/SPI LCD Backback is supported.
|
Adafruit I2C/SPI LCD Backback is supported.
|
||||||
|
|
||||||
Warning: You might need a level shifter (that supports i2c)
|
Warning: You might need a level shifter (that supports i2c) between the
|
||||||
between the SCL/SDA connections on the MCP chip / backpack and the Raspberry Pi.
|
SCL/SDA connections on the MCP chip / backpack and the Raspberry Pi.
|
||||||
Or you might damage the Pi and possibly any other 3.3V i2c devices
|
|
||||||
connected on the i2c bus. Or cause reliability issues. The SCL/SDA are rated 0.7*VDD
|
Otherwise, you might damage the Pi and possibly any other 3.3V i2c devices
|
||||||
on the MCP23008, so it needs 3.5V on the SCL/SDA when 5V is applied to drive the LCD.
|
connected on the i2c bus. Or cause reliability issues.
|
||||||
The MCP23008 and MCP23017 needs to be connected exactly the same way as the backpack.
|
|
||||||
|
The SCL/SDA are rated 0.7*VDD on the MCP23008, so it needs 3.5V on the
|
||||||
|
SCL/SDA when 5V is applied to drive the LCD.
|
||||||
|
|
||||||
|
The MCP23008 and MCP23017 needs to be connected exactly the same way as the
|
||||||
|
backpack.
|
||||||
|
|
||||||
For complete schematics see the adafruit page at:
|
For complete schematics see the adafruit page at:
|
||||||
https://learn.adafruit.com/i2c-spi-lcd-backpack/
|
https://learn.adafruit.com/i2c-spi-lcd-backpack/
|
||||||
4-bit operation. I2C only supported.
|
|
||||||
|
4-bit operations. I2C only supported.
|
||||||
|
|
||||||
Pin mapping::
|
Pin mapping::
|
||||||
|
|
||||||
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0
|
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0
|
||||||
BL | D7 | D6 | D5 | D4 | E | RS | -
|
BL | D7 | D6 | D5 | D4 | E | RS | -
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **RPLCD** (``pip install RPLCD``)
|
|
||||||
* **RPi.GPIO** (``pip install RPi.GPIO``)
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, i2c_expander: str, address: int,
|
def __init__(
|
||||||
expander_params: Optional[dict] = None,
|
self,
|
||||||
port: int = 1, cols: int = 16, rows: int = 2,
|
i2c_expander: str,
|
||||||
backlight_enabled: bool = True,
|
address: int,
|
||||||
dotsize: int = 8, charmap: str = 'A02',
|
expander_params: Optional[dict] = None,
|
||||||
auto_linebreaks: bool = True, **kwargs):
|
port: int = 1,
|
||||||
|
cols: int = 16,
|
||||||
|
rows: int = 2,
|
||||||
|
backlight_enabled: bool = True,
|
||||||
|
dotsize: int = 8,
|
||||||
|
charmap: str = 'A02',
|
||||||
|
auto_linebreaks: bool = True,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
:param i2c_expander: Set your I²C chip type. Supported: "PCF8574", "MCP23008", "MCP23017".
|
:param i2c_expander: Set your I²C chip type. Supported: "PCF8574",
|
||||||
|
"MCP23008", "MCP23017".
|
||||||
:param address: The I2C address of your LCD.
|
:param address: The I2C address of your LCD.
|
||||||
:param expander_params: Parameters for expanders, in a dictionary. Only needed for MCP23017
|
:param expander_params: Parameters for expanders, in a dictionary. Only
|
||||||
gpio_bank - This must be either ``A`` or ``B``. If you have a HAT, A is usually marked 1 and B is 2.
|
needed for MCP23017 gpio_bank - This must be either ``A`` or ``B``.
|
||||||
Example: ``expander_params={'gpio_bank': 'A'}``
|
If you have a HAT, A is usually marked 1 and B is 2. Example:
|
||||||
|
``expander_params={'gpio_bank': 'A'}``
|
||||||
:param port: The I2C port number. Default: ``1``.
|
:param port: The I2C port number. Default: ``1``.
|
||||||
:param cols: Number of columns per row (usually 16 or 20). Default: ``16``.
|
:param cols: Number of columns per row (usually 16 or 20). Default: ``16``.
|
||||||
:param rows: Number of display rows (usually 1, 2 or 4). Default: ``2``.
|
:param rows: Number of display rows (usually 1, 2 or 4). Default: ``2``.
|
||||||
:param backlight_enabled: Whether the backlight is enabled initially. Default: ``True``. Has no effect if pin_backlight is ``None``
|
:param backlight_enabled: Whether the backlight is enabled initially.
|
||||||
:param dotsize: Some 1 line displays allow a font height of 10px. Allowed: ``8`` or ``10``. Default: ``8``.
|
Default: ``True``. Has no effect if pin_backlight is ``None``
|
||||||
:param charmap: The character map used. Depends on your LCD. This must be either ``A00`` or ``A02`` or ``ST0B``. Default: ``A02``.
|
:param dotsize: Some 1 line displays allow a font height of 10px.
|
||||||
:param auto_linebreaks: Whether or not to automatically insert line breaks. Default: ``True``.
|
Allowed: ``8`` or ``10``. Default: ``8``.
|
||||||
|
:param charmap: The character map used. Depends on your LCD. This must
|
||||||
|
be either ``A00`` or ``A02`` or ``ST0B``. Default: ``A02``.
|
||||||
|
:param auto_linebreaks: Whether or not to automatically insert line
|
||||||
|
breaks. Default: ``True``.
|
||||||
"""
|
"""
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
@ -65,12 +81,18 @@ class LcdI2cPlugin(LcdPlugin):
|
||||||
|
|
||||||
def _get_lcd(self):
|
def _get_lcd(self):
|
||||||
from RPLCD.i2c import CharLCD
|
from RPLCD.i2c import CharLCD
|
||||||
return CharLCD(cols=self.cols, rows=self.rows,
|
|
||||||
i2c_expander=self.i2c_expander,
|
return CharLCD(
|
||||||
address=self.address, port=self.port,
|
cols=self.cols,
|
||||||
backlight_enabled=self.backlight_enabled,
|
rows=self.rows,
|
||||||
dotsize=self.dotsize, charmap=self.charmap,
|
i2c_expander=self.i2c_expander,
|
||||||
auto_linebreaks=self.auto_linebreaks)
|
address=self.address,
|
||||||
|
port=self.port,
|
||||||
|
backlight_enabled=self.backlight_enabled,
|
||||||
|
dotsize=self.dotsize,
|
||||||
|
charmap=self.charmap,
|
||||||
|
auto_linebreaks=self.auto_linebreaks,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class LcdI2CPlugin(LcdI2cPlugin):
|
class LcdI2CPlugin(LcdI2cPlugin):
|
||||||
|
|
|
@ -34,18 +34,6 @@ from platypush.plugins import RunnablePlugin, action
|
||||||
class LightHuePlugin(RunnablePlugin, LightEntityManager):
|
class LightHuePlugin(RunnablePlugin, LightEntityManager):
|
||||||
"""
|
"""
|
||||||
Philips Hue lights plugin.
|
Philips Hue lights plugin.
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **phue** (``pip install phue``)
|
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
- :class:`platypush.message.event.light.LightAnimationStartedEvent` when an animation is started.
|
|
||||||
- :class:`platypush.message.event.light.LightAnimationStoppedEvent` when an animation is stopped.
|
|
||||||
- :class:`platypush.message.event.light.LightStatusChangeEvent` when the status of a lightbulb
|
|
||||||
changes.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
MAX_BRI = 255
|
MAX_BRI = 255
|
||||||
|
@ -88,7 +76,7 @@ class LightHuePlugin(RunnablePlugin, LightEntityManager):
|
||||||
"""
|
"""
|
||||||
:param bridge: Bridge address or hostname
|
:param bridge: Bridge address or hostname
|
||||||
:param lights: Default lights to be controlled (default: all)
|
:param lights: Default lights to be controlled (default: all)
|
||||||
:param groups Default groups to be controlled (default: all)
|
:param groups: Default groups to be controlled (default: all)
|
||||||
:param poll_interval: How often the plugin should check the bridge for light
|
:param poll_interval: How often the plugin should check the bridge for light
|
||||||
updates (default: 20 seconds).
|
updates (default: 20 seconds).
|
||||||
:param config_file: Path to the phue configuration file containing the
|
:param config_file: Path to the phue configuration file containing the
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue