forked from platypush/platypush
[#311] Docs deps autogen sphinx plugin.
Added an `add_dependencies` plugin to the Sphinx build process that parses the manifest files of the scanned backends and plugins and automatically generates the documentation for the required dependencies and triggered events. This means that those dependencies are no longer required to be listed in the docstring of the class itself. Also in this commit: - Black/LINT for some integrations that hadn't been touched in a long time. - Deleted some leftovers from previous refactors (deprecated `backend.mqtt`, `backend.zwave.mqtt`, `backend.http.request.rss`). - Deleted deprecated `inotify` backend - replaced by `file.monitor` (see #289).
This commit is contained in:
parent
27340f2889
commit
c3337ccc6c
165 changed files with 5131 additions and 4691 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
|
||||||
|
|
100
docs/source/_ext/add_dependencies.py
Normal file
100
docs/source/_ext/add_dependencies.py
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from sphinx.application import Sphinx
|
||||||
|
|
||||||
|
|
||||||
|
def add_events(source: list[str], manifest: dict, idx: int) -> int:
|
||||||
|
events = manifest.get('events', [])
|
||||||
|
if not events:
|
||||||
|
return idx
|
||||||
|
|
||||||
|
source.insert(
|
||||||
|
idx,
|
||||||
|
'Triggered events\n----------------\n\n'
|
||||||
|
+ '\n'.join(f'\t- :class:`{event}`' for event in events)
|
||||||
|
+ '\n\n',
|
||||||
|
)
|
||||||
|
|
||||||
|
return idx + 1
|
||||||
|
|
||||||
|
|
||||||
|
def add_install_deps(source: list[str], manifest: dict, idx: int) -> int:
|
||||||
|
install_deps = manifest.get('install', {})
|
||||||
|
install_cmds = {
|
||||||
|
'pip': 'pip install',
|
||||||
|
'Alpine': 'apk add',
|
||||||
|
'Arch Linux': 'pacman -S',
|
||||||
|
'Debian': 'apt install',
|
||||||
|
'Fedora': 'yum install',
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed_deps = {
|
||||||
|
'pip': install_deps.get('pip', []),
|
||||||
|
'Alpine': install_deps.get('apk', []),
|
||||||
|
'Arch Linux': install_deps.get('pacman', []),
|
||||||
|
'Debian': install_deps.get('apt', []),
|
||||||
|
'Fedora': install_deps.get('dnf', install_deps.get('yum', [])),
|
||||||
|
}
|
||||||
|
|
||||||
|
if not any(parsed_deps.values()):
|
||||||
|
return idx
|
||||||
|
|
||||||
|
source.insert(idx, 'Dependencies\n^^^^^^^^^^^^\n\n')
|
||||||
|
idx += 1
|
||||||
|
|
||||||
|
for env, deps in parsed_deps.items():
|
||||||
|
if deps:
|
||||||
|
install_cmd = install_cmds[env]
|
||||||
|
source.insert(
|
||||||
|
idx,
|
||||||
|
f'**{env}**\n\n'
|
||||||
|
+ '.. code-block:: bash\n\n\t'
|
||||||
|
+ f'{install_cmd} '
|
||||||
|
+ ' '.join(deps)
|
||||||
|
+ '\n\n',
|
||||||
|
)
|
||||||
|
|
||||||
|
idx += 1
|
||||||
|
|
||||||
|
return idx
|
||||||
|
|
||||||
|
|
||||||
|
def parse_dependencies(_: 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
|
||||||
|
|
||||||
|
base_path = os.path.abspath(
|
||||||
|
os.path.join(os.path.dirname(os.path.relpath(__file__)), '..', '..', '..')
|
||||||
|
)
|
||||||
|
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 open(manifest_file) as f:
|
||||||
|
manifest: dict = yaml.safe_load(f).get('manifest', {})
|
||||||
|
|
||||||
|
idx = add_install_deps(src, manifest, idx=3)
|
||||||
|
add_events(src, manifest, idx=idx)
|
||||||
|
source[0] = '\n'.join(src)
|
||||||
|
|
||||||
|
|
||||||
|
def setup(app: Sphinx):
|
||||||
|
app.connect('source-read', parse_dependencies)
|
||||||
|
|
||||||
|
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
|
|
||||||
|
|
|
@ -52,6 +52,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.
|
||||||
|
|
|
@ -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,
|
||||||
|
when: str,
|
||||||
|
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,
|
audio_volume: Optional[Union[int, float]] = None,
|
||||||
snooze_interval: float = 300.0, enabled: bool = True):
|
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(
|
||||||
|
|
|
@ -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__(
|
||||||
|
self,
|
||||||
|
listen_port,
|
||||||
|
bind_address='0.0.0.0',
|
||||||
|
x_resolution=640,
|
||||||
|
y_resolution=480,
|
||||||
redis_queue='platypush/camera/pi',
|
redis_queue='platypush/camera/pi',
|
||||||
start_recording_on_startup=True,
|
start_recording_on_startup=True,
|
||||||
framerate=24, hflip=False, vflip=False,
|
framerate=24,
|
||||||
sharpness=0, contrast=0, brightness=50,
|
hflip=False,
|
||||||
video_stabilization=False, iso=0, exposure_compensation=0,
|
vflip=False,
|
||||||
exposure_mode='auto', meter_mode='average', awb_mode='auto',
|
sharpness=0,
|
||||||
image_effect='none', color_effects=None, rotation=0,
|
contrast=0,
|
||||||
crop=(0.0, 0.0, 1.0, 1.0), **kwargs):
|
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(
|
||||||
|
user_id=self.user_id,
|
||||||
|
data_source_id=data_source,
|
||||||
data_type=dp.pop('dataTypeName'),
|
data_type=dp.pop('dataTypeName'),
|
||||||
start_time=dp_time,
|
start_time=dp_time,
|
||||||
end_time=dp.pop('endTime'),
|
end_time=dp.pop('endTime'),
|
||||||
modified_time=dp.pop('modifiedTime'),
|
modified_time=dp.pop('modifiedTime'),
|
||||||
values=dp.pop('values'),
|
values=dp.pop('values'),
|
||||||
**{camel_case_to_snake_case(k): v
|
**{
|
||||||
for k, v in dp.items()}
|
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(
|
||||||
|
release=report.get('release'),
|
||||||
rev=report.get('rev'),
|
rev=report.get('rev'),
|
||||||
proto_major=report.get('proto_major'),
|
proto_major=report.get('proto_major'),
|
||||||
proto_minor=report.get('proto_minor'))
|
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__(
|
||||||
|
self,
|
||||||
device: str = '/dev/input/js0',
|
device: str = '/dev/input/js0',
|
||||||
jstest_path: str = '/usr/bin/jstest',
|
jstest_path: str = '/usr/bin/jstest',
|
||||||
**kwargs):
|
**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,13 +153,16 @@ 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
|
||||||
|
|
||||||
|
try:
|
||||||
# Joystick event loop
|
# Joystick event loop
|
||||||
while not self.should_stop():
|
while not self.should_stop():
|
||||||
try:
|
try:
|
||||||
|
@ -172,9 +177,15 @@ class JoystickLinuxBackend(Backend):
|
||||||
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 = (
|
||||||
|
JoystickButtonPressedEvent
|
||||||
|
if value
|
||||||
|
else JoystickButtonReleasedEvent
|
||||||
|
)
|
||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
self.bus.post(evt_class(device=self.device, button=button))
|
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]
|
||||||
|
@ -182,8 +193,14 @@ class JoystickLinuxBackend(Backend):
|
||||||
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(
|
||||||
|
JoystickAxisEvent(
|
||||||
|
device=self.device, axis=axis, value=fvalue
|
||||||
|
)
|
||||||
|
)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
self.logger.warning(f'Connection to {self.device} lost: {e}')
|
self.logger.warning(f'Connection to {self.device} lost: {e}')
|
||||||
self.bus.post(JoystickDisconnectedEvent(device=self.device))
|
self.bus.post(JoystickDisconnectedEvent(device=self.device))
|
||||||
break
|
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(
|
||||||
|
self.url,
|
||||||
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(),
|
||||||
|
)
|
||||||
|
|
||||||
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,21 +27,14 @@ 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__(
|
||||||
|
self,
|
||||||
librespot_path: str = 'librespot',
|
librespot_path: str = 'librespot',
|
||||||
device_name: Optional[str] = None,
|
device_name: Optional[str] = None,
|
||||||
device_type: str = 'speaker',
|
device_type: str = 'speaker',
|
||||||
|
@ -63,11 +62,12 @@ class MusicSpotifyBackend(Backend, SpotifyMixin):
|
||||||
enable_volume_normalization: bool = False,
|
enable_volume_normalization: bool = False,
|
||||||
normalization_method: str = 'dynamic',
|
normalization_method: str = 'dynamic',
|
||||||
normalization_pre_gain: Optional[float] = None,
|
normalization_pre_gain: Optional[float] = None,
|
||||||
normalization_threshold: float = -1.,
|
normalization_threshold: float = -1.0,
|
||||||
normalization_attack: int = 5,
|
normalization_attack: int = 5,
|
||||||
normalization_release: int = 100,
|
normalization_release: int = 100,
|
||||||
normalization_knee: float = 1.,
|
normalization_knee: float = 1.0,
|
||||||
**kwargs):
|
**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,10 +11,8 @@ 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``,
|
||||||
- :class:`platypush.message.event.nextcloud.NextCloudActivityEvent` when new activity occurs on the instance.
|
|
||||||
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:
|
``file_changed``). Example in the case of the creation of new files:
|
||||||
|
|
||||||
.. code-block:: json
|
.. code-block:: json
|
||||||
|
@ -24,7 +22,7 @@ class NextcloudBackend(Backend):
|
||||||
"app": "files",
|
"app": "files",
|
||||||
"activity_type": "file_created",
|
"activity_type": "file_created",
|
||||||
"user": "your-user",
|
"user": "your-user",
|
||||||
"subject": "You created InstantUpload/Camera/IMG_0100.jpg, InstantUpload/Camera/IMG_0101.jpg and InstantUpload/Camera/IMG_0102.jpg",
|
"subject": "You created InstantUpload/Camera/IMG_0100.jpg",
|
||||||
"subject_rich": [
|
"subject_rich": [
|
||||||
"You created {file3}, {file2} and {file1}",
|
"You created {file3}, {file2} and {file1}",
|
||||||
{
|
{
|
||||||
|
@ -73,9 +71,16 @@ class NextcloudBackend(Backend):
|
||||||
|
|
||||||
_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}'
|
||||||
|
)
|
||||||
|
self.listener = Listener(
|
||||||
|
account=self.pb,
|
||||||
on_push=self.on_push(),
|
on_push=self.on_push(),
|
||||||
on_open=self.on_open(),
|
on_open=self.on_open(),
|
||||||
on_close=self.on_close(),
|
on_close=self.on_close(),
|
||||||
on_error=self.on_error(),
|
on_error=self.on_error(),
|
||||||
http_proxy_host=self.proxy_host,
|
http_proxy_host=self.proxy_host,
|
||||||
http_proxy_port=self.proxy_port)
|
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,28 +31,25 @@ 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__(
|
||||||
|
self,
|
||||||
position_ranges=None,
|
position_ranges=None,
|
||||||
position_tolerance=0.0, # Position variation tolerance in %
|
position_tolerance=0.0, # Position variation tolerance in %
|
||||||
frames_throttle_secs=None,
|
frames_throttle_secs=None,
|
||||||
*args, **kwargs):
|
*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::
|
||||||
|
|
||||||
|
@ -59,7 +61,8 @@ class SensorLeapBackend(Backend):
|
||||||
|
|
||||||
: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_ranges=self.position_ranges,
|
||||||
position_tolerance=self.position_tolerance,
|
position_tolerance=self.position_tolerance,
|
||||||
frames_throttle_secs=self.frames_throttle_secs,
|
frames_throttle_secs=self.frames_throttle_secs,
|
||||||
logger=self.logger)
|
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(
|
||||||
|
self.url,
|
||||||
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 _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(
|
||||||
|
NewPrecipitationForecastEvent(
|
||||||
|
plugin_name='weather.buienradar',
|
||||||
average=precip.get('average'),
|
average=precip.get('average'),
|
||||||
total=precip.get('total'),
|
total=precip.get('total'),
|
||||||
time_frame=precip.get('time_frame')))
|
time_frame=precip.get('time_frame'),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if weather != self.last_weather:
|
if weather != self.last_weather:
|
||||||
self.bus.post(NewWeatherConditionEvent(**{
|
self.bus.post(
|
||||||
|
NewWeatherConditionEvent(
|
||||||
|
**{
|
||||||
**weather,
|
**weather,
|
||||||
'plugin_name': 'weather.buienradar',
|
'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(
|
||||||
|
feed=feed,
|
||||||
|
value=value,
|
||||||
|
metadata={
|
||||||
'lat': lat,
|
'lat': lat,
|
||||||
'lon': lon,
|
'lon': lon,
|
||||||
'ele': ele,
|
'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'
|
||||||
|
|
|
@ -53,29 +53,6 @@ class BluetoothPlugin(RunnablePlugin, EnumSwitchEntityManager):
|
||||||
|
|
||||||
Note that the support for Bluetooth low-energy devices requires a Bluetooth
|
Note that the support for Bluetooth low-energy devices requires a Bluetooth
|
||||||
adapter compatible with the Bluetooth 5.0 specification or higher.
|
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_connect_timeout: Final[int] = 20
|
||||||
|
|
|
@ -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,
|
||||||
|
'-y',
|
||||||
|
'-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 ()),
|
*(('-r', str(camera.info.fps)) if camera.info.fps else ()),
|
||||||
'-pix_fmt', 'rgb24', '-f', 'rawvideo', *camera.info.ffmpeg_args, '-']
|
'-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,44 @@ 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 the FFmpeg or GStreamer integrations.
|
||||||
* **numpy** (``pip install numpy``)
|
|
||||||
* **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,
|
||||||
|
warmup_seconds: float = 2.0,
|
||||||
|
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,
|
color_effects: Optional[Union[str, List[str]]] = None,
|
||||||
zoom: Tuple[float, float, float, float] = (0.0, 0.0, 1.0, 1.0), **camera):
|
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 +70,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 +115,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[1] + (camera.info.resolution[1] % 16),
|
||||||
camera.info.resolution[0] + (camera.info.resolution[0] % 32),
|
camera.info.resolution[0] + (camera.info.resolution[0] % 32),
|
||||||
3)
|
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 +141,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 +154,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 +187,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 +197,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,17 +163,21 @@ 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(
|
||||||
|
chat_id=chat_id,
|
||||||
text=text,
|
text=text,
|
||||||
parse_mode=parse_mode,
|
parse_mode=parse_mode,
|
||||||
disable_web_page_preview=disable_web_page_preview,
|
disable_web_page_preview=disable_web_page_preview,
|
||||||
disable_notification=disable_notification,
|
disable_notification=disable_notification,
|
||||||
reply_to_message_id=reply_to_message_id)
|
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(
|
||||||
|
self,
|
||||||
|
chat_id: Union[str, int],
|
||||||
file_id: Optional[int] = None,
|
file_id: Optional[int] = None,
|
||||||
url: Optional[str] = None,
|
url: Optional[str] = None,
|
||||||
path: Optional[str] = None,
|
path: Optional[str] = None,
|
||||||
|
@ -170,7 +185,8 @@ class ChatTelegramPlugin(ChatPlugin):
|
||||||
parse_mode: Optional[str] = None,
|
parse_mode: Optional[str] = None,
|
||||||
disable_notification: bool = False,
|
disable_notification: bool = False,
|
||||||
reply_to_message_id: Optional[int] = None,
|
reply_to_message_id: Optional[int] = None,
|
||||||
timeout: int = 20) -> TelegramMessageResponse:
|
timeout: int = 20,
|
||||||
|
) -> TelegramMessageResponse:
|
||||||
"""
|
"""
|
||||||
Send a picture to a chat.
|
Send a picture to a chat.
|
||||||
|
|
||||||
|
@ -198,17 +214,22 @@ 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(
|
||||||
|
chat_id=chat_id,
|
||||||
photo=resource,
|
photo=resource,
|
||||||
caption=caption,
|
caption=caption,
|
||||||
disable_notification=disable_notification,
|
disable_notification=disable_notification,
|
||||||
reply_to_message_id=reply_to_message_id,
|
reply_to_message_id=reply_to_message_id,
|
||||||
timeout=timeout, parse_mode=parse_mode)
|
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(
|
||||||
|
self,
|
||||||
|
chat_id: Union[str, int],
|
||||||
file_id: Optional[int] = None,
|
file_id: Optional[int] = None,
|
||||||
url: Optional[str] = None,
|
url: Optional[str] = None,
|
||||||
path: Optional[str] = None,
|
path: Optional[str] = None,
|
||||||
|
@ -219,7 +240,8 @@ class ChatTelegramPlugin(ChatPlugin):
|
||||||
parse_mode: Optional[str] = None,
|
parse_mode: Optional[str] = None,
|
||||||
disable_notification: bool = False,
|
disable_notification: bool = False,
|
||||||
reply_to_message_id: Optional[int] = None,
|
reply_to_message_id: Optional[int] = None,
|
||||||
timeout: int = 20) -> TelegramMessageResponse:
|
timeout: int = 20,
|
||||||
|
) -> TelegramMessageResponse:
|
||||||
"""
|
"""
|
||||||
Send audio to a chat.
|
Send audio to a chat.
|
||||||
|
|
||||||
|
@ -250,7 +272,8 @@ 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(
|
||||||
|
chat_id=chat_id,
|
||||||
audio=resource,
|
audio=resource,
|
||||||
caption=caption,
|
caption=caption,
|
||||||
disable_notification=disable_notification,
|
disable_notification=disable_notification,
|
||||||
|
@ -259,12 +282,15 @@ class ChatTelegramPlugin(ChatPlugin):
|
||||||
duration=duration,
|
duration=duration,
|
||||||
reply_to_message_id=reply_to_message_id,
|
reply_to_message_id=reply_to_message_id,
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
parse_mode=parse_mode)
|
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(
|
||||||
|
self,
|
||||||
|
chat_id: Union[str, int],
|
||||||
file_id: Optional[int] = None,
|
file_id: Optional[int] = None,
|
||||||
url: Optional[str] = None,
|
url: Optional[str] = None,
|
||||||
path: Optional[str] = None,
|
path: Optional[str] = None,
|
||||||
|
@ -273,7 +299,8 @@ class ChatTelegramPlugin(ChatPlugin):
|
||||||
parse_mode: Optional[str] = None,
|
parse_mode: Optional[str] = None,
|
||||||
disable_notification: bool = False,
|
disable_notification: bool = False,
|
||||||
reply_to_message_id: Optional[int] = None,
|
reply_to_message_id: Optional[int] = None,
|
||||||
timeout: int = 20) -> TelegramMessageResponse:
|
timeout: int = 20,
|
||||||
|
) -> TelegramMessageResponse:
|
||||||
"""
|
"""
|
||||||
Send a document to a chat.
|
Send a document to a chat.
|
||||||
|
|
||||||
|
@ -302,19 +329,23 @@ 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(
|
||||||
|
chat_id=chat_id,
|
||||||
document=resource,
|
document=resource,
|
||||||
filename=filename,
|
filename=filename,
|
||||||
caption=caption,
|
caption=caption,
|
||||||
disable_notification=disable_notification,
|
disable_notification=disable_notification,
|
||||||
reply_to_message_id=reply_to_message_id,
|
reply_to_message_id=reply_to_message_id,
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
parse_mode=parse_mode)
|
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(
|
||||||
|
self,
|
||||||
|
chat_id: Union[str, int],
|
||||||
file_id: Optional[int] = None,
|
file_id: Optional[int] = None,
|
||||||
url: Optional[str] = None,
|
url: Optional[str] = None,
|
||||||
path: Optional[str] = None,
|
path: Optional[str] = None,
|
||||||
|
@ -325,7 +356,8 @@ class ChatTelegramPlugin(ChatPlugin):
|
||||||
parse_mode: Optional[str] = None,
|
parse_mode: Optional[str] = None,
|
||||||
disable_notification: bool = False,
|
disable_notification: bool = False,
|
||||||
reply_to_message_id: Optional[int] = None,
|
reply_to_message_id: Optional[int] = None,
|
||||||
timeout: int = 20) -> TelegramMessageResponse:
|
timeout: int = 20,
|
||||||
|
) -> TelegramMessageResponse:
|
||||||
"""
|
"""
|
||||||
Send a video to a chat.
|
Send a video to a chat.
|
||||||
|
|
||||||
|
@ -356,7 +388,8 @@ 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(
|
||||||
|
chat_id=chat_id,
|
||||||
video=resource,
|
video=resource,
|
||||||
duration=duration,
|
duration=duration,
|
||||||
caption=caption,
|
caption=caption,
|
||||||
|
@ -365,12 +398,15 @@ class ChatTelegramPlugin(ChatPlugin):
|
||||||
disable_notification=disable_notification,
|
disable_notification=disable_notification,
|
||||||
reply_to_message_id=reply_to_message_id,
|
reply_to_message_id=reply_to_message_id,
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
parse_mode=parse_mode)
|
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(
|
||||||
|
self,
|
||||||
|
chat_id: Union[str, int],
|
||||||
file_id: Optional[int] = None,
|
file_id: Optional[int] = None,
|
||||||
url: Optional[str] = None,
|
url: Optional[str] = None,
|
||||||
path: Optional[str] = None,
|
path: Optional[str] = None,
|
||||||
|
@ -381,7 +417,8 @@ class ChatTelegramPlugin(ChatPlugin):
|
||||||
parse_mode: Optional[str] = None,
|
parse_mode: Optional[str] = None,
|
||||||
disable_notification: bool = False,
|
disable_notification: bool = False,
|
||||||
reply_to_message_id: Optional[int] = None,
|
reply_to_message_id: Optional[int] = None,
|
||||||
timeout: int = 20) -> TelegramMessageResponse:
|
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,7 +449,8 @@ 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(
|
||||||
|
chat_id=chat_id,
|
||||||
animation=resource,
|
animation=resource,
|
||||||
duration=duration,
|
duration=duration,
|
||||||
caption=caption,
|
caption=caption,
|
||||||
|
@ -421,12 +459,15 @@ class ChatTelegramPlugin(ChatPlugin):
|
||||||
disable_notification=disable_notification,
|
disable_notification=disable_notification,
|
||||||
reply_to_message_id=reply_to_message_id,
|
reply_to_message_id=reply_to_message_id,
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
parse_mode=parse_mode)
|
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(
|
||||||
|
self,
|
||||||
|
chat_id: Union[str, int],
|
||||||
file_id: Optional[int] = None,
|
file_id: Optional[int] = None,
|
||||||
url: Optional[str] = None,
|
url: Optional[str] = None,
|
||||||
path: Optional[str] = None,
|
path: Optional[str] = None,
|
||||||
|
@ -435,7 +476,8 @@ class ChatTelegramPlugin(ChatPlugin):
|
||||||
parse_mode: Optional[str] = None,
|
parse_mode: Optional[str] = None,
|
||||||
disable_notification: bool = False,
|
disable_notification: bool = False,
|
||||||
reply_to_message_id: Optional[int] = None,
|
reply_to_message_id: Optional[int] = None,
|
||||||
timeout: int = 20) -> TelegramMessageResponse:
|
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(
|
||||||
|
chat_id=chat_id,
|
||||||
voice=resource,
|
voice=resource,
|
||||||
caption=caption,
|
caption=caption,
|
||||||
disable_notification=disable_notification,
|
disable_notification=disable_notification,
|
||||||
duration=duration,
|
duration=duration,
|
||||||
reply_to_message_id=reply_to_message_id,
|
reply_to_message_id=reply_to_message_id,
|
||||||
timeout=timeout, 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_note(self, chat_id: Union[str, int],
|
def send_video_note(
|
||||||
|
self,
|
||||||
|
chat_id: Union[str, int],
|
||||||
file_id: Optional[int] = None,
|
file_id: Optional[int] = None,
|
||||||
url: Optional[str] = None,
|
url: Optional[str] = None,
|
||||||
path: Optional[str] = None,
|
path: Optional[str] = None,
|
||||||
duration: Optional[int] = None,
|
duration: Optional[int] = None,
|
||||||
disable_notification: bool = False,
|
disable_notification: bool = False,
|
||||||
reply_to_message_id: Optional[int] = None,
|
reply_to_message_id: Optional[int] = None,
|
||||||
timeout: int = 20) -> TelegramMessageResponse:
|
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(
|
||||||
|
chat_id=chat_id,
|
||||||
video=resource,
|
video=resource,
|
||||||
duration=duration,
|
duration=duration,
|
||||||
disable_notification=disable_notification,
|
disable_notification=disable_notification,
|
||||||
reply_to_message_id=reply_to_message_id,
|
reply_to_message_id=reply_to_message_id,
|
||||||
timeout=timeout)
|
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(
|
||||||
|
self,
|
||||||
|
chat_id: Union[str, int],
|
||||||
latitude: float,
|
latitude: float,
|
||||||
longitude: float,
|
longitude: float,
|
||||||
disable_notification: bool = False,
|
disable_notification: bool = False,
|
||||||
reply_to_message_id: Optional[int] = None,
|
reply_to_message_id: Optional[int] = None,
|
||||||
timeout: int = 20) -> TelegramMessageResponse:
|
timeout: int = 20,
|
||||||
|
) -> TelegramMessageResponse:
|
||||||
"""
|
"""
|
||||||
Send a location to a chat.
|
Send a location to a chat.
|
||||||
|
|
||||||
|
@ -543,17 +596,21 @@ 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(
|
||||||
|
chat_id=chat_id,
|
||||||
latitude=latitude,
|
latitude=latitude,
|
||||||
longitude=longitude,
|
longitude=longitude,
|
||||||
disable_notification=disable_notification,
|
disable_notification=disable_notification,
|
||||||
reply_to_message_id=reply_to_message_id,
|
reply_to_message_id=reply_to_message_id,
|
||||||
timeout=timeout)
|
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(
|
||||||
|
self,
|
||||||
|
chat_id: Union[str, int],
|
||||||
latitude: float,
|
latitude: float,
|
||||||
longitude: float,
|
longitude: float,
|
||||||
title: str,
|
title: str,
|
||||||
|
@ -562,7 +619,8 @@ class ChatTelegramPlugin(ChatPlugin):
|
||||||
foursquare_type: Optional[str] = None,
|
foursquare_type: Optional[str] = None,
|
||||||
disable_notification: bool = False,
|
disable_notification: bool = False,
|
||||||
reply_to_message_id: Optional[int] = None,
|
reply_to_message_id: Optional[int] = None,
|
||||||
timeout: int = 20) -> TelegramMessageResponse:
|
timeout: int = 20,
|
||||||
|
) -> TelegramMessageResponse:
|
||||||
"""
|
"""
|
||||||
Send the address of a venue to a chat.
|
Send the address of a venue to a chat.
|
||||||
|
|
||||||
|
@ -583,7 +641,8 @@ 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(
|
||||||
|
chat_id=chat_id,
|
||||||
latitude=latitude,
|
latitude=latitude,
|
||||||
longitude=longitude,
|
longitude=longitude,
|
||||||
title=title,
|
title=title,
|
||||||
|
@ -592,19 +651,23 @@ class ChatTelegramPlugin(ChatPlugin):
|
||||||
foursquare_type=foursquare_type,
|
foursquare_type=foursquare_type,
|
||||||
disable_notification=disable_notification,
|
disable_notification=disable_notification,
|
||||||
reply_to_message_id=reply_to_message_id,
|
reply_to_message_id=reply_to_message_id,
|
||||||
timeout=timeout)
|
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(
|
||||||
|
self,
|
||||||
|
chat_id: Union[str, int],
|
||||||
phone_number: str,
|
phone_number: str,
|
||||||
first_name: str,
|
first_name: str,
|
||||||
last_name: Optional[str] = None,
|
last_name: Optional[str] = None,
|
||||||
vcard: Optional[str] = None,
|
vcard: Optional[str] = None,
|
||||||
disable_notification: bool = False,
|
disable_notification: bool = False,
|
||||||
reply_to_message_id: Optional[int] = None,
|
reply_to_message_id: Optional[int] = None,
|
||||||
timeout: int = 20) -> TelegramMessageResponse:
|
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(
|
||||||
|
chat_id=chat_id,
|
||||||
phone_number=phone_number,
|
phone_number=phone_number,
|
||||||
first_name=first_name,
|
first_name=first_name,
|
||||||
last_name=last_name,
|
last_name=last_name,
|
||||||
vcard=vcard,
|
vcard=vcard,
|
||||||
disable_notification=disable_notification,
|
disable_notification=disable_notification,
|
||||||
reply_to_message_id=reply_to_message_id,
|
reply_to_message_id=reply_to_message_id,
|
||||||
timeout=timeout)
|
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,7 +727,8 @@ 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(
|
||||||
|
chat_id=chat.id,
|
||||||
link=chat.link,
|
link=chat.link,
|
||||||
username=chat.username,
|
username=chat.username,
|
||||||
invite_link=chat.invite_link,
|
invite_link=chat.invite_link,
|
||||||
|
@ -666,10 +736,13 @@ class ChatTelegramPlugin(ChatPlugin):
|
||||||
description=chat.description,
|
description=chat.description,
|
||||||
type=chat.type,
|
type=chat.type,
|
||||||
first_name=chat.first_name,
|
first_name=chat.first_name,
|
||||||
last_name=chat.last_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(
|
||||||
|
user_id=user.user.id,
|
||||||
link=user.user.link,
|
link=user.user.link,
|
||||||
username=user.user.username,
|
username=user.user.username,
|
||||||
first_name=user.user.first_name,
|
first_name=user.user.first_name,
|
||||||
last_name=user.user.last_name,
|
last_name=user.user.last_name,
|
||||||
is_bot=user.user.is_bot,
|
is_bot=user.user.is_bot,
|
||||||
language_code=user.user.language_code)
|
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,7 +776,8 @@ 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(
|
TelegramUserResponse(
|
||||||
user_id=user.user.id,
|
user_id=user.user.id,
|
||||||
link=user.user.link,
|
link=user.user.link,
|
||||||
|
@ -708,11 +786,15 @@ class ChatTelegramPlugin(ChatPlugin):
|
||||||
last_name=user.user.last_name,
|
last_name=user.user.last_name,
|
||||||
is_bot=user.user.is_bot,
|
is_bot=user.user.is_bot,
|
||||||
language_code=user.user.language_code,
|
language_code=user.user.language_code,
|
||||||
) for user in admins
|
)
|
||||||
])
|
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(
|
||||||
|
self,
|
||||||
|
chat_id: Union[str, int],
|
||||||
user_id: int,
|
user_id: int,
|
||||||
until_date: Optional[datetime.datetime] = None,
|
until_date: Optional[datetime.datetime] = None,
|
||||||
timeout: int = 20):
|
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,12 +848,13 @@ 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(
|
||||||
|
self,
|
||||||
|
chat_id: Union[str, int],
|
||||||
user_id: int,
|
user_id: int,
|
||||||
can_change_info: Optional[bool] = None,
|
can_change_info: Optional[bool] = None,
|
||||||
can_post_messages: Optional[bool] = None,
|
can_post_messages: Optional[bool] = None,
|
||||||
|
@ -780,7 +864,8 @@ class ChatTelegramPlugin(ChatPlugin):
|
||||||
can_restrict_members: Optional[bool] = None,
|
can_restrict_members: Optional[bool] = None,
|
||||||
can_promote_members: Optional[bool] = None,
|
can_promote_members: Optional[bool] = None,
|
||||||
can_pin_messages: Optional[bool] = None,
|
can_pin_messages: Optional[bool] = None,
|
||||||
timeout: int = 20):
|
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(
|
||||||
|
self,
|
||||||
|
chat_id: Union[str, int],
|
||||||
message_id: int,
|
message_id: int,
|
||||||
disable_notification: Optional[bool] = None,
|
disable_notification: Optional[bool] = None,
|
||||||
timeout: int = 20):
|
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):
|
||||||
|
|
|
@ -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,
|
||||||
|
signals: Optional[Iterable[dict]] = None,
|
||||||
service_name: Optional[str] = _default_service_name,
|
service_name: Optional[str] = _default_service_name,
|
||||||
service_path: Optional[str] = _default_service_path, **kwargs
|
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.
|
||||||
|
|
||||||
|
@ -433,7 +438,7 @@ class DbusPlugin(RunnablePlugin):
|
||||||
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(
|
||||||
|
proc: subprocess.Popen,
|
||||||
|
packet_size: int,
|
||||||
|
on_packet: Callable[[bytes], None],
|
||||||
on_open: Optional[Callable[[], None]] = None,
|
on_open: Optional[Callable[[], None]] = None,
|
||||||
on_close: 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',
|
||||||
'https://www.googleapis.com/auth/drive.appfolder',
|
'https://www.googleapis.com/auth/drive.appfolder',
|
||||||
'https://www.googleapis.com/auth/drive.photos.readonly']
|
'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(
|
||||||
|
self,
|
||||||
filter: Optional[str] = None,
|
filter: Optional[str] = None,
|
||||||
folder_id: Optional[str] = None,
|
folder_id: Optional[str] = None,
|
||||||
limit: Optional[int] = 100,
|
limit: Optional[int] = 100,
|
||||||
drive_id: Optional[str] = None,
|
drive_id: Optional[str] = None,
|
||||||
spaces: Optional[Union[str, List[str]]] = None,
|
spaces: Optional[Union[str, List[str]]] = None,
|
||||||
order_by: Optional[Union[str, List[str]]] = None) -> Union[GoogleDriveFile, List[GoogleDriveFile]]:
|
order_by: Optional[Union[str, List[str]]] = None,
|
||||||
|
) -> Union[GoogleDriveFile, List[GoogleDriveFile]]:
|
||||||
"""
|
"""
|
||||||
Get the list of files.
|
Get the list of files.
|
||||||
|
|
||||||
|
@ -90,7 +88,9 @@ class GoogleDrivePlugin(GooglePlugin):
|
||||||
filter += "'{}' in parents".format(folder_id)
|
filter += "'{}' in parents".format(folder_id)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
results = service.files().list(
|
results = (
|
||||||
|
service.files()
|
||||||
|
.list(
|
||||||
q=filter,
|
q=filter,
|
||||||
driveId=drive_id,
|
driveId=drive_id,
|
||||||
pageSize=limit,
|
pageSize=limit,
|
||||||
|
@ -98,17 +98,22 @@ class GoogleDrivePlugin(GooglePlugin):
|
||||||
fields="nextPageToken, files(id, name, kind, mimeType)",
|
fields="nextPageToken, files(id, name, kind, mimeType)",
|
||||||
pageToken=page_token,
|
pageToken=page_token,
|
||||||
spaces=spaces,
|
spaces=spaces,
|
||||||
).execute()
|
)
|
||||||
|
.execute()
|
||||||
|
)
|
||||||
|
|
||||||
page_token = results.get('nextPageToken')
|
page_token = results.get('nextPageToken')
|
||||||
files.extend([
|
files.extend(
|
||||||
|
[
|
||||||
GoogleDriveFile(
|
GoogleDriveFile(
|
||||||
id=f.get('id'),
|
id=f.get('id'),
|
||||||
name=f.get('name'),
|
name=f.get('name'),
|
||||||
type=f.get('kind').split('#')[1],
|
type=f.get('kind').split('#')[1],
|
||||||
mime_type=f.get('mimeType'),
|
mime_type=f.get('mimeType'),
|
||||||
) for f in results.get('files', [])
|
)
|
||||||
])
|
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(
|
||||||
|
self,
|
||||||
path: str,
|
path: str,
|
||||||
mime_type: Optional[str] = None,
|
mime_type: Optional[str] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
parents: Optional[List[str]] = None,
|
parents: Optional[List[str]] = None,
|
||||||
starred: bool = False,
|
starred: bool = False,
|
||||||
target_mime_type: Optional[str] = None) -> GoogleDriveFile:
|
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(
|
||||||
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
mime_type: Optional[str] = None,
|
mime_type: Optional[str] = None,
|
||||||
parents: Optional[List[str]] = None,
|
parents: Optional[List[str]] = None,
|
||||||
starred: bool = False) -> GoogleDriveFile:
|
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,7 +261,8 @@ class GoogleDrivePlugin(GooglePlugin):
|
||||||
)
|
)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def update(self,
|
def update(
|
||||||
|
self,
|
||||||
file_id: str,
|
file_id: str,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
|
@ -263,7 +270,8 @@ class GoogleDrivePlugin(GooglePlugin):
|
||||||
remove_parents: Optional[List[str]] = None,
|
remove_parents: Optional[List[str]] = None,
|
||||||
mime_type: Optional[str] = None,
|
mime_type: Optional[str] = None,
|
||||||
starred: bool = None,
|
starred: bool = None,
|
||||||
trashed: bool = None) -> GoogleDriveFile:
|
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.activity.read',
|
||||||
'https://www.googleapis.com/auth/fitness.body.read',
|
'https://www.googleapis.com/auth/fitness.body.read',
|
||||||
'https://www.googleapis.com/auth/fitness.body_temperature.read',
|
'https://www.googleapis.com/auth/fitness.body_temperature.read',
|
||||||
'https://www.googleapis.com/auth/fitness.heart_rate.read',
|
'https://www.googleapis.com/auth/fitness.heart_rate.read',
|
||||||
'https://www.googleapis.com/auth/fitness.sleep.read',
|
'https://www.googleapis.com/auth/fitness.sleep.read',
|
||||||
'https://www.googleapis.com/auth/fitness.location.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(
|
||||||
|
'post',
|
||||||
|
'message',
|
||||||
|
json={
|
||||||
'message': message,
|
'message': message,
|
||||||
'title': title,
|
'title': title,
|
||||||
'priority': priority,
|
'priority': priority,
|
||||||
'extras': extras or {},
|
'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_rs: int,
|
||||||
|
pin_e: int,
|
||||||
|
pins_data: List[int],
|
||||||
|
pin_rw: Optional[int] = None,
|
||||||
|
pin_mode: str = 'BOARD',
|
||||||
pin_backlight: Optional[int] = None,
|
pin_backlight: Optional[int] = None,
|
||||||
cols: int = 16, rows: int = 2,
|
cols: int = 16,
|
||||||
|
rows: int = 2,
|
||||||
backlight_enabled: bool = True,
|
backlight_enabled: bool = True,
|
||||||
backlight_mode: str = 'active_low',
|
backlight_mode: str = 'active_low',
|
||||||
dotsize: int = 8, charmap: str = 'A02',
|
dotsize: int = 8,
|
||||||
|
charmap: str = 'A02',
|
||||||
auto_linebreaks: bool = True,
|
auto_linebreaks: bool = True,
|
||||||
compat_mode: bool = False, **kwargs):
|
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,
|
||||||
|
rows=self.rows,
|
||||||
|
pin_rs=self.pin_rs,
|
||||||
|
pin_e=self.pin_e,
|
||||||
|
pins_data=self.pins_data,
|
||||||
|
numbering_mode=self.pin_mode,
|
||||||
|
pin_rw=self.pin_rw,
|
||||||
pin_backlight=self.pin_backlight,
|
pin_backlight=self.pin_backlight,
|
||||||
backlight_enabled=self.backlight_enabled,
|
backlight_enabled=self.backlight_enabled,
|
||||||
backlight_mode=self.backlight_mode,
|
backlight_mode=self.backlight_mode,
|
||||||
dotsize=self.dotsize, charmap=self.charmap,
|
dotsize=self.dotsize,
|
||||||
|
charmap=self.charmap,
|
||||||
auto_linebreaks=self.auto_linebreaks,
|
auto_linebreaks=self.auto_linebreaks,
|
||||||
compat_mode=self.compat_mode)
|
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__(
|
||||||
|
self,
|
||||||
|
i2c_expander: str,
|
||||||
|
address: int,
|
||||||
expander_params: Optional[dict] = None,
|
expander_params: Optional[dict] = None,
|
||||||
port: int = 1, cols: int = 16, rows: int = 2,
|
port: int = 1,
|
||||||
|
cols: int = 16,
|
||||||
|
rows: int = 2,
|
||||||
backlight_enabled: bool = True,
|
backlight_enabled: bool = True,
|
||||||
dotsize: int = 8, charmap: str = 'A02',
|
dotsize: int = 8,
|
||||||
auto_linebreaks: bool = True, **kwargs):
|
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,
|
|
||||||
|
return CharLCD(
|
||||||
|
cols=self.cols,
|
||||||
|
rows=self.rows,
|
||||||
i2c_expander=self.i2c_expander,
|
i2c_expander=self.i2c_expander,
|
||||||
address=self.address, port=self.port,
|
address=self.address,
|
||||||
|
port=self.port,
|
||||||
backlight_enabled=self.backlight_enabled,
|
backlight_enabled=self.backlight_enabled,
|
||||||
dotsize=self.dotsize, charmap=self.charmap,
|
dotsize=self.dotsize,
|
||||||
auto_linebreaks=self.auto_linebreaks)
|
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
|
||||||
|
|
|
@ -26,14 +26,6 @@ class LinodePlugin(RunnablePlugin, CloudInstanceEntityManager, EnumSwitchEntityM
|
||||||
- Go to My Profile -> API Tokens -> Add a Personal Access Token.
|
- Go to My Profile -> API Tokens -> Add a Personal Access Token.
|
||||||
- Select the scopes that you want to provide to your new token.
|
- Select the scopes that you want to provide to your new token.
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **linode_api4** (``pip install linode_api4``)
|
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
* :class:`platypush.message.event.linode.LinodeInstanceStatusChanged` when the status of an instance changes.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, token: str, poll_interval: float = 60.0, **kwargs):
|
def __init__(self, token: str, poll_interval: float = 60.0, **kwargs):
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue