Merge branch 'master' into snyk-upgrade-87b37fbfcf86065d749fecde13c6979c

This commit is contained in:
Fabio Manganiello 2024-03-03 23:00:23 +01:00 committed by GitHub
commit 3993a51aed
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
744 changed files with 28983 additions and 14794 deletions

View File

@ -62,7 +62,8 @@ steps:
# Backup the original git configuration before changing attributes
- export GIT_CONF=$PWD/.git/config
- cp $GIT_CONF /tmp/git.config.orig
- export TMP_GIT_CONF=/tmp/git.config.orig
- cp $GIT_CONF $TMP_GIT_CONF
- git config --global --add safe.directory $PWD
# Install the SSH and PGP keys
@ -95,7 +96,7 @@ steps:
- git checkout master
# Restore the original git configuration
- mv /tmp/git.config.orig $GIT_CONF
- mv $TMP_GIT_CONF $GIT_CONF
when:
event:
@ -114,8 +115,8 @@ steps:
commands:
- echo "Installing required build dependencies"
- apk add --update --no-cache git make py3-sphinx py3-myst-parser py3-pip $(cat platypush/install/requirements/alpine.txt)
- pip install -U sphinx-rtd-theme sphinx-book-theme
- pip install .
- pip install -U sphinx-rtd-theme sphinx-book-theme --break-system-packages
- pip install . --break-system-packages
- mkdir -p /docs/current
- export APPDIR=$PWD
- rm -rf "$APPDIR/docs/build"
@ -150,8 +151,8 @@ steps:
image: alpine
commands:
- apk add --update --no-cache $(cat platypush/install/requirements/alpine.txt)
- pip install .
- pip install -r requirements-tests.txt
- pip install . --break-system-packages
- pip install -r requirements-tests.txt --break-system-packages
- pytest tests
###
@ -181,11 +182,14 @@ steps:
- tests
commands:
- export SKIPCI="$PWD/.skipci"
- rm -rf "$SKIPCI"
- apk add --update --no-cache git
# Backup the original git configuration before changing attributes
- export GIT_CONF=$PWD/.git/config
- cp $GIT_CONF /tmp/git.config.orig
- export TMP_GIT_CONF=/tmp/git.config.orig
- cp $GIT_CONF $TMP_GIT_CONF
- git config --global --add safe.directory $PWD
- cd platypush/backend/http/webapp
@ -201,7 +205,7 @@ steps:
exit 0
fi
- rm -rf node_modules
- rm -rf dist node_modules
- npm install
- npm run build
- |
@ -210,6 +214,10 @@ steps:
exit 0
fi
# Create a .skipci file to mark the fact that the next steps should be skipped
# (we're going to do another push anyway, so another pipeline will be triggered)
- touch "$SKIPCI"
- apk add --update --no-cache bash gnupg openssh
- mkdir -p ~/.ssh
- |
@ -236,7 +244,95 @@ steps:
- git push -f origin master
# Restore the original git configuration
- mv /tmp/git.config.orig $GIT_CONF
- mv $TMP_GIT_CONF $GIT_CONF
###
### Regenerate the components.json cache
###
- name: update-components-cache
image: alpine
environment:
SSH_PUBKEY:
from_secret: ssh_pubkey
SSH_PRIVKEY:
from_secret: ssh_privkey
PGP_KEY:
from_secret: pgp_key
PGP_KEY_ID:
from_secret: pgp_key_id
when:
branch:
- master
event:
- push
depends_on:
- build-ui
commands:
- export SKIPCI="$PWD/.skipci"
- export CACHEFILE="$PWD/platypush/components.json.gz"
- |
[ -f "$SKIPCI" ] && exit 0
# Only regenerate the components cache if either the plugins, backends,
# events or schemas folders have some changes (excluding the webapp files).
- apk add --update --no-cache git
- |
if [ -z "$(git log --pretty=oneline $DRONE_COMMIT_AFTER...$DRONE_COMMIT_BEFORE -- platypush/backend platypush/plugins platypush/schemas platypush/message/event ':(exclude)platypush/backend/http/webapp')" ]; then
echo 'No changes to the components file'
exit 0
fi
- echo 'Updating components cache'
- apk add --update --no-cache bash gnupg openssh $(cat platypush/install/requirements/alpine.txt)
- pip install . --break-system-packages
- |
python - <<EOF
from platypush import get_plugin
get_plugin('inspect').refresh_cache(force=True)
EOF
# Backup the original git configuration before changing attributes
- export GIT_CONF=$PWD/.git/config
- export TMP_GIT_CONF=/tmp/git.config.orig
- cp $GIT_CONF $TMP_GIT_CONF
- git config --global --add safe.directory $PWD
# Create a .skipci file to mark the fact that the next steps should be skipped
# (we're going to do another push anyway, so another pipeline will be triggered)
- touch "$SKIPCI"
- mkdir -p ~/.ssh
- |
cat <<EOF | gpg --import --armor
$PGP_KEY
EOF
- echo $SSH_PUBKEY > ~/.ssh/id_rsa.pub
- |
cat <<EOF > ~/.ssh/id_rsa
$SSH_PRIVKEY
EOF
- chmod 0600 ~/.ssh/id_rsa
- ssh-keyscan git.platypush.tech >> ~/.ssh/known_hosts 2>/dev/null
- git config user.name "Platypush CI/CD Automation"
- git config user.email "admin@platypush.tech"
- git config commit.gpgsign true
- git config user.signingkey $PGP_KEY_ID
- git add "$CACHEFILE"
- git commit "$CACHEFILE" -S -m "[Automatic] Updated components cache" --no-verify
- git remote rm origin
- git remote add origin git@git.platypush.tech:platypush/platypush.git
- git push -f origin master
# Restore the original git configuration
- mv $TMP_GIT_CONF $GIT_CONF
###
### Update the Arch packages
@ -258,9 +354,12 @@ steps:
- push
depends_on:
- build-ui
- update-components-cache
commands:
- |
[ -f .skipci ] && exit 0
- echo "-- Installing dependencies"
- apk add --update --no-cache curl git openssh pacman sudo
@ -269,7 +368,7 @@ steps:
- git pull --rebase origin master --tags
- export VERSION=$(python setup.py --version)
- export HEAD=$(git log --pretty=format:%h HEAD...HEAD~1 | head -1)
- export GIT_VERSION="$VERSION.r$(git log --pretty=oneline HEAD...v$VERSION | wc -l).$HEAD"
- export GIT_VERSION="$VERSION.r$(git log --pretty=oneline HEAD...v$VERSION | wc -l).g$${HEAD}"
- export TAG_URL="https://git.platypush.tech/platypush/platypush/archive/v$VERSION.tar.gz"
- echo "--- Preparing environment"
@ -355,9 +454,12 @@ steps:
- push
depends_on:
- build-ui
- update-components-cache
commands:
- |
[ -f .skipci ] && exit 0
- echo "-- Installing dependencies"
- apt update
- apt install -y curl dpkg-dev gpg git python3 python3-pip
@ -369,12 +471,13 @@ steps:
- cd "$SRCDIR"
- echo "--- Parsing metadata"
- git config --global --add safe.directory $PWD
- git config --global --add safe.directory "$PWD"
- git pull --rebase origin master --tags
- export VERSION=$(python3 setup.py --version)
- export GIT_VERSION="$VERSION-$(( $(git log --pretty=oneline HEAD...v$VERSION | wc -l) + 1))"
- export GIT_VERSION="$VERSION-$(git log --pretty=oneline HEAD...v$VERSION | wc -l)"
- export GIT_BUILD_DIR="$WORKDIR/$${PKG_NAME}_$${GIT_VERSION}_all"
- export GIT_DEB="$WORKDIR/$${PKG_NAME}_$${GIT_VERSION}_all.deb"
- export POOL_PATH="$APT_ROOT/pool/$DEB_VERSION/dev"
- echo "--- Building git package"
- pip install --prefix="$GIT_BUILD_DIR/usr" --no-cache --no-deps .
@ -395,13 +498,17 @@ steps:
Homepage: https://platypush.tech
Description: Universal command executor and automation hub.
EOF
- mkdir -p "$POOL_PATH"
- rm -f "$POOL_PATH/"*.deb
- dpkg --build "$GIT_BUILD_DIR"
- echo "--- Copying package to the repo"
- mkdir -p "$APT_ROOT/pool/$DEB_VERSION/dev"
- cp "$GIT_DEB" "$APT_ROOT/pool/$DEB_VERSION/dev/"
- echo "--- Copying $GIT_DEB to $POOL_PATH"
- cp "$GIT_DEB" "$POOL_PATH"
# If main/all/Packages doesn't exist, then we should create the first main release
- |
[ $(ls "$APT_ROOT/pool/$DEB_VERSION/main/$${PKG_NAME}_$${VERSION}-"*"_all.deb" 2>/dev/null | wc -l) -eq 0 ] && export UPDATE_STABLE_PKG=1
- export PKGURL="https://apt.platypush.tech/dists/$DEB_VERSION/main/all/Packages"
- |
[ -z "$UPDATE_STABLE_PKG" ] &&
@ -446,9 +553,12 @@ steps:
- push
depends_on:
- build-ui
- update-components-cache
commands:
- |
[ -f .skipci ] && exit 0
- echo "-- Copying source directory"
- mkdir -p "$WORKDIR"
- export SRCDIR="$WORKDIR/src"
@ -460,12 +570,13 @@ steps:
- apt install -y curl dpkg-dev gpg git python3 python3-pip
- echo "--- Parsing metadata"
- git config --global --add safe.directory $PWD
- git config --global --add safe.directory "$PWD"
- git pull --rebase origin master --tags
- export VERSION=$(python3 setup.py --version)
- export GIT_VERSION="$VERSION-$(( $(git log --pretty=oneline HEAD...v$VERSION | wc -l) + 1))"
- export GIT_VERSION="$VERSION-$(git log --pretty=oneline HEAD...v$VERSION | wc -l)"
- export GIT_BUILD_DIR="$WORKDIR/$${PKG_NAME}_$${GIT_VERSION}_all"
- export GIT_DEB="$WORKDIR/$${PKG_NAME}_$${GIT_VERSION}_all.deb"
- export POOL_PATH="$APT_ROOT/pool/$DEB_VERSION/dev"
- echo "--- Building git package"
- pip install --prefix="$GIT_BUILD_DIR/usr" --no-cache --no-deps .
@ -486,13 +597,17 @@ steps:
Homepage: https://platypush.tech
Description: Universal command executor and automation hub.
EOF
- mkdir -p "$POOL_PATH"
- rm -f "$POOL_PATH/"*.deb
- dpkg --build "$GIT_BUILD_DIR"
- echo "--- Copying package to the repo"
- mkdir -p "$APT_ROOT/pool/$DEB_VERSION/dev"
- cp "$GIT_DEB" "$APT_ROOT/pool/$DEB_VERSION/dev/"
- echo "--- Copying $GIT_DEB to $POOL_PATH"
- cp "$GIT_DEB" "$POOL_PATH"
# If main/all/Packages doesn't exist, then we should create the first main release
- |
[ $(ls "$APT_ROOT/pool/$DEB_VERSION/main/$${PKG_NAME}_$${VERSION}-"*"_all.deb" 2>/dev/null | wc -l) -eq 0 ] && export UPDATE_STABLE_PKG=1
- export PKGURL="https://apt.platypush.tech/dists/$DEB_VERSION/main/all/Packages"
- |
[ -z "$UPDATE_STABLE_PKG" ] &&
@ -543,6 +658,9 @@ steps:
- update-debian-oldstable-packages
commands:
- |
[ -f .skipci ] && exit 0
- echo "-- Installing dependencies"
- apt update
- apt install -y dpkg-dev gpg
@ -564,13 +682,16 @@ steps:
echo "stable\noldstable" | while read distro; do
echo "main\ndev" | while read branch; do
branch_dir="$TMP_APT_ROOT/pool/$distro/$branch"
[ -d "$branch_dir" ] || continue
echo "Checking pool folder: $branch_dir"
[ -d "$branch_dir" ] || mkdir -p "$branch_dir"
dist_dir="$TMP_APT_ROOT/dists/$distro/$branch/all"
mkdir -p "$dist_dir"
pkg_file="$dist_dir/Packages"
dpkg-scanpackages --arch all "$branch_dir" > "$pkg_file"
sed -i "$pkg_file" -re "s|^Filename: $TMP_APT_ROOT/|Filename: |"
cat "$pkg_file" | gzip -9 > "$pkg_file.gz"
echo "Generated Packages file: $pkg_file"
cat "$pkg_file"
done
done
@ -616,6 +737,28 @@ steps:
add_hashes "$dist_dir" "sha256sum" "SHA256" >> "$release_file"
done
- echo "-- Generating list files"
- mkdir -p "$TMP_APT_ROOT/lists"
- |
cat <<EOF > "$TMP_APT_ROOT/lists/platypush-stable-main.list"
deb https://apt.platypush.tech/ stable main
EOF
- |
cat <<EOF > "$TMP_APT_ROOT/lists/platypush-stable-dev.list"
deb https://apt.platypush.tech/ stable dev
EOF
- |
cat <<EOF > "$TMP_APT_ROOT/lists/platypush-oldstable-main.list"
deb https://apt.platypush.tech/ oldstable main
EOF
- |
cat <<EOF > "$TMP_APT_ROOT/lists/platypush-oldstable-dev.list"
deb https://apt.platypush.tech/ oldstable dev
EOF
- echo "-- Updating index file"
- |
cat <<EOF > "$TMP_APT_ROOT/index.txt"
@ -633,15 +776,15 @@ steps:
1. Add this repository's PGP key to your apt keyring
====================================================
$ sudo wget -q -O \
/etc/apt/trusted.gpg.d/platypush.asc \
$ sudo wget -q -O \\\
/etc/apt/trusted.gpg.d/platypush.asc \\\
https://apt.platypush.tech/pubkey.txt
2. Add the repository to your sources
=====================================
$ sudo wget -q -O \
/etc/apt/sources.list.d/platypush.list \
$ sudo wget -q -O \\\
/etc/apt/sources.list.d/platypush.list \\\
https://apt.platypush.tech/lists/platypush-<deb_version>-<branch>.list
Where:
@ -653,8 +796,8 @@ steps:
For example, to install the latest stable tags on Debian stable:
$ sudo wget -q -O \
/etc/apt/sources.list.d/platypush.list \
$ sudo wget -q -O \\\
/etc/apt/sources.list.d/platypush.list \\\
https://apt.platypush.tech/lists/platypush-stable-main.list
3. Update your repos
@ -686,12 +829,9 @@ steps:
- echo "-- Updating the apt repo root"
- export OLD_APT_ROOT="$REPOS_ROOT/oldapt"
- |
if [ ! -d "$OLD_APT_ROOT" ]; then
mv "$APT_ROOT" "$OLD_APT_ROOT"
mv "$TMP_APT_ROOT" "$APT_ROOT"
rm -rf "$OLD_APT_ROOT"
fi
- rm -rf "$OLD_APT_ROOT"
- mv "$APT_ROOT" "$OLD_APT_ROOT"
- mv "$TMP_APT_ROOT" "$APT_ROOT"
- chmod -R a+r "$APT_ROOT"
- chmod a+x "$APT_ROOT"
@ -724,9 +864,12 @@ steps:
- push
depends_on:
- build-ui
- update-components-cache
commands:
- |
[ -f .skipci ] && exit 0
- echo "-- Installing dependencies"
- yum install -y createrepo rpm-build rpm-sign gpg wget yum-utils git python python-pip
@ -741,7 +884,7 @@ steps:
- git config --global --add safe.directory $PWD
- git pull --rebase origin master --tags
- export VERSION=$(python3 setup.py --version)
- export RELNUM=$(git log --pretty=oneline HEAD...v$VERSION | wc -l)
- export RELNUM="$(git log --pretty=oneline HEAD...v$VERSION | wc -l)"
- export SPECFILE="$WORKDIR/$PKG_NAME.spec"
- export BUILD_DIR="$WORKDIR/build"
- export TMP_RPM_ROOT="$WORKDIR/repo"

2
.gitignore vendored
View File

@ -26,3 +26,5 @@ Session.vim
/package.json
/Dockerfile
/docs/source/wiki
/.skipci
dump.rdb

View File

@ -3,3 +3,4 @@ recursive-include platypush/install *
include platypush/plugins/http/webpage/mercury-parser.js
include platypush/config/*.yaml
global-include manifest.yaml
global-include components.json.gz

View File

@ -419,9 +419,7 @@ backend](https://docs.platypush.tech/en/latest/platypush/backend/http.html), an
[MQTT
instance](https://docs.platypush.tech/en/latest/platypush/backend/mqtt.html), a
[Kafka
instance](https://docs.platypush.tech/en/latest/platypush/backend/kafka.html),
[Pushbullet](https://docs.platypush.tech/en/latest/platypush/backend/pushbullet.html)
etc.).
instance](https://docs.platypush.tech/en/latest/platypush/backend/kafka.html).
If a backend supports the execution of requests (e.g. HTTP, MQTT, Kafka,
Websocket and TCP) then you can send requests to these services in JSON format.
@ -707,8 +705,6 @@ groups together the controls for most of the plugins - e.g. sensors, switches,
music controls and search, media library and torrent management, lights,
Zigbee/Z-Wave devices and so on. The UI is responsive and mobile-friendly.
Some screenshots:
#### The main web panel
This is the default panel available at `http://<host>:<port>` after

View File

@ -6,45 +6,13 @@ Backends
:maxdepth: 1
:caption: Backends:
platypush/backend/adafruit.io.rst
platypush/backend/alarm.rst
platypush/backend/button.flic.rst
platypush/backend/camera.pi.rst
platypush/backend/chat.telegram.rst
platypush/backend/file.monitor.rst
platypush/backend/foursquare.rst
platypush/backend/github.rst
platypush/backend/google.fit.rst
platypush/backend/google.pubsub.rst
platypush/backend/gps.rst
platypush/backend/http.rst
platypush/backend/joystick.rst
platypush/backend/joystick.jstest.rst
platypush/backend/joystick.linux.rst
platypush/backend/kafka.rst
platypush/backend/log.http.rst
platypush/backend/mail.rst
platypush/backend/midi.rst
platypush/backend/music.mopidy.rst
platypush/backend/music.mpd.rst
platypush/backend/music.snapcast.rst
platypush/backend/music.spotify.rst
platypush/backend/nextcloud.rst
platypush/backend/nfc.rst
platypush/backend/nodered.rst
platypush/backend/ping.rst
platypush/backend/pushbullet.rst
platypush/backend/redis.rst
platypush/backend/scard.rst
platypush/backend/sensor.ir.zeroborg.rst
platypush/backend/sensor.leap.rst
platypush/backend/stt.deepspeech.rst
platypush/backend/stt.picovoice.hotword.rst
platypush/backend/stt.picovoice.speech.rst
platypush/backend/tcp.rst
platypush/backend/todoist.rst
platypush/backend/trello.rst
platypush/backend/weather.buienradar.rst
platypush/backend/weather.darksky.rst
platypush/backend/weather.openweathermap.rst
platypush/backend/wiimote.rst

View File

@ -11,16 +11,15 @@ Events
platypush/events/application.rst
platypush/events/assistant.rst
platypush/events/bluetooth.rst
platypush/events/button.flic.rst
platypush/events/camera.rst
platypush/events/chat.slack.rst
platypush/events/chat.telegram.rst
platypush/events/clipboard.rst
platypush/events/custom.rst
platypush/events/dbus.rst
platypush/events/distance.rst
platypush/events/entities.rst
platypush/events/file.rst
platypush/events/flic.rst
platypush/events/foursquare.rst
platypush/events/geo.rst
platypush/events/github.rst
@ -66,8 +65,8 @@ Events
platypush/events/sound.rst
platypush/events/stt.rst
platypush/events/sun.rst
platypush/events/telegram.rst
platypush/events/tensorflow.rst
platypush/events/todoist.rst
platypush/events/torrent.rst
platypush/events/trello.rst
platypush/events/video.rst

View File

@ -39,6 +39,7 @@ Wiki
wiki/Configuration
wiki/Installing-extensions
wiki/A-configuration-example
wiki/The-Web-interface
Reference
=========

View File

@ -1,6 +0,0 @@
``adafruit.io``
=================================
.. automodule:: platypush.backend.adafruit.io
:members:

View File

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

View File

@ -1,6 +0,0 @@
``button.flic``
=================================
.. automodule:: platypush.backend.button.flic
:members:

View File

@ -1,6 +0,0 @@
``camera.pi``
===============================
.. automodule:: platypush.backend.camera.pi
:members:

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +0,0 @@
``google.fit``
================================
.. automodule:: platypush.backend.google.fit
:members:

View File

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

View File

@ -1,6 +0,0 @@
``gps``
=========================
.. automodule:: platypush.backend.gps
:members:

View File

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

View File

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

View File

@ -1,6 +0,0 @@
``joystick``
==============================
.. automodule:: platypush.backend.joystick
:members:

View File

@ -1,6 +0,0 @@
``kafka``
===========================
.. automodule:: platypush.backend.kafka
:members:

View File

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

View File

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

View File

@ -1,6 +0,0 @@
``music.mpd``
===============================
.. automodule:: platypush.backend.music.mpd
:members:

View File

@ -1,6 +0,0 @@
``music.snapcast``
====================================
.. automodule:: platypush.backend.music.snapcast
:members:

View File

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

View File

@ -1,6 +0,0 @@
``nfc``
=========================
.. automodule:: platypush.backend.nfc
:members:

View File

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

View File

@ -1,6 +0,0 @@
``pushbullet``
================================
.. automodule:: platypush.backend.pushbullet
:members:

View File

@ -1,6 +0,0 @@
``scard``
===========================
.. automodule:: platypush.backend.scard
:members:

View File

@ -1,6 +0,0 @@
``sensor.ir.zeroborg``
========================================
.. automodule:: platypush.backend.sensor.ir.zeroborg
:members:

View File

@ -1,7 +0,0 @@
``sensor.leap``
=================================
.. automodule:: platypush.backend.sensor.leap
:members:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +0,0 @@
``wiimote``
=============================
.. automodule:: platypush.backend.wiimote
:members:

View File

@ -1,6 +0,0 @@
``button.flic``
=======================================
.. automodule:: platypush.message.event.button.flic
:members:

View File

@ -1,5 +0,0 @@
``chat.telegram``
=========================================
.. automodule:: platypush.message.event.chat.telegram
:members:

View File

@ -0,0 +1,5 @@
``flic``
========
.. automodule:: platypush.message.event.flic
:members:

View File

@ -0,0 +1,5 @@
``telegram``
============
.. automodule:: platypush.message.event.telegram
:members:

View File

@ -1,5 +0,0 @@
``todoist``
===================================
.. automodule:: platypush.message.event.todoist
:members:

View File

@ -0,0 +1,5 @@
``camera.pi.legacy``
====================
.. automodule:: platypush.plugins.camera.pi.legacy
:members:

View File

@ -1,5 +0,0 @@
``chat.irc``
============
.. automodule:: platypush.plugins.chat.irc
:members:

View File

@ -1,5 +0,0 @@
``chat.telegram``
===================================
.. automodule:: platypush.plugins.chat.telegram
:members:

View File

@ -0,0 +1,5 @@
``file.monitor``
================
.. automodule:: platypush.plugins.file.monitor
:members:

View File

@ -0,0 +1,5 @@
``flic``
========
.. automodule:: platypush.plugins.flic
:members:

View File

@ -0,0 +1,5 @@
``github``
==========
.. automodule:: platypush.plugins.github
:members:

View File

@ -0,0 +1,5 @@
``gps``
=======
.. automodule:: platypush.plugins.gps
:members:

View File

@ -1,7 +0,0 @@
``http.request``
==================================
.. automodule:: platypush.plugins.http.request
:members:

View File

@ -0,0 +1,5 @@
``http``
========
.. automodule:: platypush.plugins.http
:members:

View File

@ -1,5 +0,0 @@
``inputs``
============================
.. automodule:: platypush.plugins.inputs
:members:

View File

@ -0,0 +1,5 @@
``irc``
=======
.. automodule:: platypush.plugins.irc
:members:

View File

@ -0,0 +1,5 @@
``joystick``
============
.. automodule:: platypush.plugins.joystick
:members:

View File

@ -0,0 +1,5 @@
``leap``
========
.. automodule:: platypush.plugins.leap
:members:

View File

@ -0,0 +1,5 @@
``log.http``
============
.. automodule:: platypush.plugins.log.http
:members:

View File

@ -1,5 +0,0 @@
``mail.imap``
===============================
.. automodule:: platypush.plugins.mail.imap
:members:

View File

@ -0,0 +1,5 @@
``mail``
========
.. automodule:: platypush.plugins.mail
:members:

View File

@ -1,5 +0,0 @@
``mail.smtp``
===============================
.. automodule:: platypush.plugins.mail.smtp
:members:

View File

@ -0,0 +1,5 @@
``nfc``
=======
.. automodule:: platypush.plugins.nfc
:members:

View File

@ -0,0 +1,5 @@
``telegram``
============
.. automodule:: platypush.plugins.telegram
:members:

View File

@ -1,5 +0,0 @@
``weather.darksky``
=====================================
.. automodule:: platypush.plugins.weather.darksky
:members:

View File

@ -1,6 +0,0 @@
``wiimote``
=============================
.. automodule:: platypush.plugins.wiimote
:members:

View File

@ -1,5 +0,0 @@
``chat.telegram``
============================================
.. automodule:: platypush.message.response.chat.telegram
:members:

View File

@ -1,5 +0,0 @@
``ping``
===================================
.. automodule:: platypush.message.response.ping
:members:

View File

@ -1,5 +0,0 @@
``todoist``
======================================
.. automodule:: platypush.message.response.todoist
:members:

View File

@ -1,5 +0,0 @@
``trello``
=====================================
.. automodule:: platypush.message.response.trello
:members:

View File

@ -1,5 +0,0 @@
``weather.buienradar``
=================================================
.. automodule:: platypush.message.response.weather.buienradar
:members:

View File

@ -21,8 +21,7 @@ Plugins
platypush/plugins/camera.gstreamer.rst
platypush/plugins/camera.ir.mlx90640.rst
platypush/plugins/camera.pi.rst
platypush/plugins/chat.irc.rst
platypush/plugins/chat.telegram.rst
platypush/plugins/camera.pi.legacy.rst
platypush/plugins/clipboard.rst
platypush/plugins/config.rst
platypush/plugins/csv.rst
@ -33,7 +32,10 @@ Plugins
platypush/plugins/esp.rst
platypush/plugins/ffmpeg.rst
platypush/plugins/file.rst
platypush/plugins/file.monitor.rst
platypush/plugins/flic.rst
platypush/plugins/foursquare.rst
platypush/plugins/github.rst
platypush/plugins/google.calendar.rst
platypush/plugins/google.drive.rst
platypush/plugins/google.fit.rst
@ -44,23 +46,26 @@ Plugins
platypush/plugins/gotify.rst
platypush/plugins/gpio.rst
platypush/plugins/gpio.zeroborg.rst
platypush/plugins/gps.rst
platypush/plugins/graphite.rst
platypush/plugins/hid.rst
platypush/plugins/http.request.rst
platypush/plugins/http.rst
platypush/plugins/http.webpage.rst
platypush/plugins/ifttt.rst
platypush/plugins/inputs.rst
platypush/plugins/inspect.rst
platypush/plugins/irc.rst
platypush/plugins/joystick.rst
platypush/plugins/kafka.rst
platypush/plugins/lastfm.rst
platypush/plugins/lcd.gpio.rst
platypush/plugins/lcd.i2c.rst
platypush/plugins/leap.rst
platypush/plugins/light.hue.rst
platypush/plugins/linode.rst
platypush/plugins/log.http.rst
platypush/plugins/logger.rst
platypush/plugins/luma.oled.rst
platypush/plugins/mail.imap.rst
platypush/plugins/mail.smtp.rst
platypush/plugins/mail.rst
platypush/plugins/mailgun.rst
platypush/plugins/mastodon.rst
platypush/plugins/matrix.rst
@ -84,6 +89,7 @@ Plugins
platypush/plugins/music.spotify.rst
platypush/plugins/music.tidal.rst
platypush/plugins/nextcloud.rst
platypush/plugins/nfc.rst
platypush/plugins/ngrok.rst
platypush/plugins/nmap.rst
platypush/plugins/ntfy.rst
@ -121,6 +127,7 @@ Plugins
platypush/plugins/switchbot.rst
platypush/plugins/system.rst
platypush/plugins/tcp.rst
platypush/plugins/telegram.rst
platypush/plugins/tensorflow.rst
platypush/plugins/todoist.rst
platypush/plugins/torrent.rst
@ -136,10 +143,8 @@ Plugins
platypush/plugins/variable.rst
platypush/plugins/wallabag.rst
platypush/plugins/weather.buienradar.rst
platypush/plugins/weather.darksky.rst
platypush/plugins/weather.openweathermap.rst
platypush/plugins/websocket.rst
platypush/plugins/wiimote.rst
platypush/plugins/xmpp.rst
platypush/plugins/youtube.rst
platypush/plugins/zeroconf.rst

View File

@ -8,16 +8,11 @@ Responses
platypush/responses/camera.rst
platypush/responses/camera.android.rst
platypush/responses/chat.telegram.rst
platypush/responses/google.drive.rst
platypush/responses/pihole.rst
platypush/responses/ping.rst
platypush/responses/printer.cups.rst
platypush/responses/qrcode.rst
platypush/responses/ssh.rst
platypush/responses/stt.rst
platypush/responses/tensorflow.rst
platypush/responses/todoist.rst
platypush/responses/translate.rst
platypush/responses/trello.rst
platypush/responses/weather.buienradar.rst

View File

@ -274,6 +274,10 @@ class Application:
backend.stop()
for plugin in runnable_plugins:
# This is required because some plugins may redefine the `stop` method.
# In that case, at the very least the _should_stop event should be
# set to notify the plugin to stop.
plugin._should_stop.set() # pylint: disable=protected-access
plugin.stop()
for backend in backends:

View File

@ -1,97 +0,0 @@
from typing import Optional
from platypush.backend import Backend
from platypush.context import get_plugin
from platypush.message.event.adafruit import (
ConnectedEvent,
DisconnectedEvent,
FeedUpdateEvent,
)
class AdafruitIoBackend(Backend):
"""
Backend that listens to messages received over the Adafruit IO message queue
Requires:
* The :class:`platypush.plugins.adafruit.io.AdafruitIoPlugin` plugin to
be active and configured.
"""
def __init__(self, feeds, *args, **kwargs):
"""
:param feeds: List of feed IDs to monitor
:type feeds: list[str]
"""
super().__init__(*args, **kwargs)
from Adafruit_IO import MQTTClient
self.feeds = feeds
self._client: Optional[MQTTClient] = None
def _init_client(self):
if self._client:
return
from Adafruit_IO import MQTTClient
plugin = get_plugin('adafruit.io')
if not plugin:
raise RuntimeError('Adafruit IO plugin not configured')
# noinspection PyProtectedMember
self._client = MQTTClient(plugin._username, plugin._key)
self._client.on_connect = self.on_connect()
self._client.on_disconnect = self.on_disconnect()
self._client.on_message = self.on_message()
def on_connect(self):
def _handler(client):
for feed in self.feeds:
client.subscribe(feed)
self.bus.post(ConnectedEvent())
return _handler
def on_disconnect(self):
def _handler(*_, **__):
self.bus.post(DisconnectedEvent())
return _handler
def on_message(self, *_, **__):
# noinspection PyUnusedLocal
def _handler(client, feed, data):
try:
data = float(data)
except Exception as e:
self.logger.debug('Not a number: {}: {}'.format(data, e))
self.bus.post(FeedUpdateEvent(feed=feed, data=data))
return _handler
def run(self):
super().run()
self.logger.info(
('Initialized Adafruit IO backend, listening on ' + 'feeds {}').format(
self.feeds
)
)
while not self.should_stop():
try:
self._init_client()
# noinspection PyUnresolvedReferences
self._client.connect()
# noinspection PyUnresolvedReferences
self._client.loop_blocking()
except Exception as e:
self.logger.exception(e)
self._client = None
# vim:sw=4:ts=4:et:

View File

@ -1,12 +0,0 @@
manifest:
events:
platypush.message.event.adafruit.ConnectedEvent: when thebackend connects to the
Adafruit queue
platypush.message.event.adafruit.DisconnectedEvent: when thebackend disconnects
from the Adafruit queue
platypush.message.event.adafruit.FeedUpdateEvent: when anupdate event is received
on a monitored feed
install:
pip: []
package: platypush.backend.adafruit.io
type: backend

View File

@ -1,351 +0,0 @@
import datetime
import enum
import os
import time
import threading
from typing import Optional, Union, Dict, Any, List
import croniter
from dateutil.tz import gettz
from platypush.backend import Backend
from platypush.context import get_bus, get_plugin
from platypush.message.event.alarm import (
AlarmStartedEvent,
AlarmDismissedEvent,
AlarmSnoozedEvent,
)
from platypush.plugins.media import MediaPlugin, PlayerState
from platypush.procedure import Procedure
class AlarmState(enum.IntEnum):
WAITING = 1
RUNNING = 2
DISMISSED = 3
SNOOZED = 4
SHUTDOWN = 5
class Alarm:
_alarms_count = 0
_id_lock = threading.RLock()
def __init__(
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,
snooze_interval: float = 300.0,
enabled: bool = True,
):
with self._id_lock:
self._alarms_count += 1
self.id = self._alarms_count
self.when = when
self.name = name or 'Alarm_{}'.format(self.id)
self.audio_file = None
if 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
)
self.audio_plugin = audio_plugin
self.audio_volume = audio_volume
self.snooze_interval = snooze_interval
self.state: Optional[AlarmState] = None
self.timer: Optional[threading.Timer] = None
self.actions = Procedure.build(
name=name, _async=False, requests=actions or [], id=self.id
)
self._enabled = enabled
self._runtime_snooze_interval = snooze_interval
def get_next(self) -> float:
now = datetime.datetime.now().replace(
tzinfo=gettz()
) # lgtm [py/call-to-non-callable]
try:
cron = croniter.croniter(self.when, now)
return cron.get_next()
except (AttributeError, croniter.CroniterBadCronError):
try:
timestamp = datetime.datetime.fromisoformat(self.when).replace(
tzinfo=gettz()
) # lgtm [py/call-to-non-callable]
except (TypeError, ValueError):
timestamp = datetime.datetime.now().replace(
tzinfo=gettz()
) + datetime.timedelta( # lgtm [py/call-to-non-callable]
seconds=int(self.when)
)
return timestamp.timestamp() if timestamp >= now else None
def is_enabled(self):
return self._enabled
def disable(self):
self._enabled = False
def enable(self):
self._enabled = True
def dismiss(self):
self.state = AlarmState.DISMISSED
self.stop_audio()
get_bus().post(AlarmDismissedEvent(name=self.name))
def snooze(self, interval: Optional[float] = None):
self._runtime_snooze_interval = interval or self.snooze_interval
self.state = AlarmState.SNOOZED
self.stop_audio()
get_bus().post(
AlarmSnoozedEvent(name=self.name, interval=self._runtime_snooze_interval)
)
def start(self):
if self.timer:
self.timer.cancel()
if self.get_next() is None:
return
interval = self.get_next() - time.time()
self.timer = threading.Timer(interval, self.callback())
self.timer.start()
self.state = AlarmState.WAITING
def stop(self):
self.state = AlarmState.SHUTDOWN
if self.timer:
self.timer.cancel()
self.timer = None
def _get_audio_plugin(self) -> MediaPlugin:
return get_plugin(self.audio_plugin)
def play_audio(self):
def thread():
self._get_audio_plugin().play(self.audio_file)
if self.audio_volume is not None:
self._get_audio_plugin().set_volume(self.audio_volume)
self.state = AlarmState.RUNNING
audio_thread = threading.Thread(target=thread)
audio_thread.start()
def stop_audio(self):
self._get_audio_plugin().stop()
def callback(self):
def _callback():
while True:
if self.state == AlarmState.SHUTDOWN:
break
if self.is_enabled():
get_bus().post(AlarmStartedEvent(name=self.name))
if self.audio_plugin and self.audio_file:
self.play_audio()
self.actions.execute()
time.sleep(10)
sleep_time = None
if self.state == AlarmState.RUNNING:
while True:
state = self._get_audio_plugin().status().output.get('state')
if state == PlayerState.STOP.value:
if self.state == AlarmState.SNOOZED:
sleep_time = self._runtime_snooze_interval
else:
self.state = AlarmState.WAITING
break
else:
time.sleep(10)
if self.state == AlarmState.SNOOZED:
sleep_time = self._runtime_snooze_interval
elif self.get_next() is None:
self.state = AlarmState.SHUTDOWN
break
if not sleep_time:
sleep_time = (
self.get_next() - time.time() if self.get_next() else 10
)
time.sleep(sleep_time)
return _callback
def to_dict(self):
return {
'name': self.name,
'id': self.id,
'when': self.when,
'next_run': self.get_next(),
'enabled': self.is_enabled(),
'state': self.state.name,
}
class AlarmBackend(Backend):
"""
Backend to handle user-configured alarms.
"""
def __init__(
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:
.. code-block:: yaml
morning_alarm:
when: '0 7 * * 1-5' # Cron expression format: run every weekday at 7 AM
audio_file: ~/path/your_ringtone.mp3
audio_plugin: media.mplayer
audio_volume: 10 # 10%
snooze_interval: 300 # 5 minutes snooze
actions:
- action: tts.say
args:
text: Good morning
- action: light.hue.bri
args:
value: 1
- action: light.hue.bri
args:
value: 140
transitiontime: 150
one_shot_alarm:
when: '2020-02-18T07:00:00.000000' # One-shot execution, with timestamp in ISO format
audio_file: ~/path/your_ringtone.mp3
actions:
- action: light.hue.on
:param audio_plugin: Media plugin (instance of :class:`platypush.plugins.media.MediaPlugin`) that will be
used to play the alarm audio (default: ``media.mplayer``).
"""
super().__init__(*args, **kwargs)
alarms = alarms or []
if isinstance(alarms, dict):
alarms = [{'name': name, **alarm} for name, alarm in alarms.items()]
self.audio_plugin = audio_plugin
alarms = [
Alarm(**{'audio_plugin': self.audio_plugin, **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,
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:
self.logger.info('Overwriting existing alarm {}'.format(alarm.name))
self.alarms[alarm.name].stop()
self.alarms[alarm.name] = alarm
self.alarms[alarm.name].start()
return self.alarms[alarm.name]
def _get_alarm(self, name) -> Alarm:
assert name in self.alarms, 'Alarm {} does not exist'.format(name)
return self.alarms[name]
def enable_alarm(self, name: str):
self._get_alarm(name).enable()
def disable_alarm(self, name: str):
self._get_alarm(name).disable()
def dismiss_alarm(self):
alarm = self.get_running_alarm()
if not alarm:
self.logger.info('No alarm is running')
return
alarm.dismiss()
def snooze_alarm(self, interval: Optional[str] = None):
alarm = self.get_running_alarm()
if not alarm:
self.logger.info('No alarm is running')
return
alarm.snooze(interval=interval)
def get_alarms(self) -> List[Alarm]:
return sorted(
self.alarms.values(),
key=lambda alarm: alarm.get_next(),
)
def get_running_alarm(self) -> Optional[Alarm]:
running_alarms = [
alarm for alarm in self.alarms.values() if alarm.state == AlarmState.RUNNING
]
return running_alarms[0] if running_alarms else None
def __enter__(self):
for alarm in self.alarms.values():
alarm.stop()
alarm.start()
self.logger.info(
'Initialized alarm backend with {} alarms'.format(len(self.alarms))
)
def __exit__(self, *_, **__):
for alarm in self.alarms.values():
alarm.stop()
self.logger.info('Alarm backend terminated')
def loop(self):
for name, alarm in self.alarms.copy().items():
if not alarm.timer or (
not alarm.timer.is_alive() and alarm.state == AlarmState.SHUTDOWN
):
del self.alarms[name]
time.sleep(10)
# vim:sw=4:ts=4:et:

View File

@ -1,10 +0,0 @@
manifest:
events:
platypush.message.event.alarm.AlarmDismissedEvent: when an alarm is dismissed.
platypush.message.event.alarm.AlarmSnoozedEvent: when an alarm is snoozed.
platypush.message.event.alarm.AlarmStartedEvent: when an alarm starts.
platypush.message.event.alarm.AlarmTimeoutEvent: when an alarm times out.
install:
pip: []
package: platypush.backend.alarm
type: backend

View File

@ -1,131 +0,0 @@
from threading import Timer
from time import time
from platypush.backend import Backend
from platypush.message.event.button.flic import FlicButtonEvent
from .fliclib.fliclib import FlicClient, ButtonConnectionChannel, ClickType
class ButtonFlicBackend(Backend):
"""
Backend that listen for events from the Flic (https://flic.io/) bluetooth
smart buttons.
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.
"""
_long_press_timeout = 0.3
_btn_timeout = 0.5
ShortPressEvent = "ShortPressEvent"
LongPressEvent = "LongPressEvent"
def __init__(
self,
server='localhost',
long_press_timeout=_long_press_timeout,
btn_timeout=_btn_timeout,
**kwargs
):
"""
:param server: flicd server host (default: localhost)
: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)
: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)
:type btn_timeout: float
"""
super().__init__(**kwargs)
self.server = server
self.client = FlicClient(server)
self.client.get_info(self._received_info())
self.client.on_new_verified_button = self._got_button()
self._long_press_timeout = long_press_timeout
self._btn_timeout = btn_timeout
self._btn_timer = None
self._btn_addr = None
self._down_pressed_time = None
self._cur_sequence = []
self.logger.info('Initialized Flic buttons backend on %s', self.server)
def _got_button(self):
def _f(bd_addr):
cc = ButtonConnectionChannel(bd_addr)
cc.on_button_up_or_down = (
lambda channel, click_type, was_queued, time_diff: self._on_event()(
bd_addr, channel, click_type, was_queued, time_diff
)
)
self.client.add_connection_channel(cc)
return _f
def _received_info(self):
def _f(items):
for bd_addr in items["bd_addr_of_verified_buttons"]:
self._got_button()(bd_addr)
return _f
def _on_btn_timeout(self):
def _f():
self.logger.info(
'Flic event triggered from %s: %s', self._btn_addr, self._cur_sequence
)
self.bus.post(
FlicButtonEvent(btn_addr=self._btn_addr, sequence=self._cur_sequence)
)
self._cur_sequence = []
return _f
def _on_event(self):
# _ = channel
# __ = time_diff
def _f(bd_addr, _, click_type, was_queued, __):
if was_queued:
return
if self._btn_timer:
self._btn_timer.cancel()
if click_type == ClickType.ButtonDown:
self._down_pressed_time = time()
return
btn_event = self.ShortPressEvent
if self._down_pressed_time:
if time() - self._down_pressed_time >= self._long_press_timeout:
btn_event = self.LongPressEvent
self._down_pressed_time = None
self._cur_sequence.append(btn_event)
self._btn_addr = bd_addr
self._btn_timer = Timer(self._btn_timeout, self._on_btn_timeout())
self._btn_timer.start()
return _f
def run(self):
super().run()
self.client.handle_events()
# vim:sw=4:ts=4:et:

View File

@ -1,609 +0,0 @@
"""Flic client library for python
Requires python 3.3 or higher.
For detailed documentation, see the protocol documentation.
Notes on the data type used in this python implementation compared to the protocol documentation:
All kind of integers are represented as python integers.
Booleans use the Boolean type.
Enums use the defined python enums below.
Bd addr are represented as standard python strings, e.g. "aa:bb:cc:dd:ee:ff".
"""
from enum import Enum
from collections import namedtuple
import time
import socket
import select
import struct
import itertools
import queue
import threading
class CreateConnectionChannelError(Enum):
NoError = 0
MaxPendingConnectionsReached = 1
class ConnectionStatus(Enum):
Disconnected = 0
Connected = 1
Ready = 2
class DisconnectReason(Enum):
Unspecified = 0
ConnectionEstablishmentFailed = 1
TimedOut = 2
BondingKeysMismatch = 3
class RemovedReason(Enum):
RemovedByThisClient = 0
ForceDisconnectedByThisClient = 1
ForceDisconnectedByOtherClient = 2
ButtonIsPrivate = 3
VerifyTimeout = 4
InternetBackendError = 5
InvalidData = 6
CouldntLoadDevice = 7
class ClickType(Enum):
ButtonDown = 0
ButtonUp = 1
ButtonClick = 2
ButtonSingleClick = 3
ButtonDoubleClick = 4
ButtonHold = 5
class BdAddrType(Enum):
PublicBdAddrType = 0
RandomBdAddrType = 1
class LatencyMode(Enum):
NormalLatency = 0
LowLatency = 1
HighLatency = 2
class BluetoothControllerState(Enum):
Detached = 0
Resetting = 1
Attached = 2
class ScanWizardResult(Enum):
WizardSuccess = 0
WizardCancelledByUser = 1
WizardFailedTimeout = 2
WizardButtonIsPrivate = 3
WizardBluetoothUnavailable = 4
WizardInternetBackendError = 5
WizardInvalidData = 6
class ButtonScanner:
"""ButtonScanner class.
Usage:
scanner = ButtonScanner()
scanner.on_advertisement_packet = lambda scanner, bd_addr, name, rssi, is_private, already_verified: ...
client.add_scanner(scanner)
"""
_cnt = itertools.count()
def __init__(self):
self._scan_id = next(ButtonScanner._cnt)
self.on_advertisement_packet = lambda scanner, bd_addr, name, rssi, is_private, already_verified: None
class ScanWizard:
"""ScanWizard class
Usage:
wizard = ScanWizard()
wizard.on_found_private_button = lambda scan_wizard: ...
wizard.on_found_public_button = lambda scan_wizard, bd_addr, name: ...
wizard.on_button_connected = lambda scan_wizard, bd_addr, name: ...
wizard.on_completed = lambda scan_wizard, result, bd_addr, name: ...
client.add_scan_wizard(wizard)
"""
_cnt = itertools.count()
def __init__(self):
self._scan_wizard_id = next(ScanWizard._cnt)
self._bd_addr = None
self._name = None
self.on_found_private_button = lambda scan_wizard: None
self.on_found_public_button = lambda scan_wizard, bd_addr, name: None
self.on_button_connected = lambda scan_wizard, bd_addr, name: None
self.on_completed = lambda scan_wizard, result, bd_addr, name: None
class ButtonConnectionChannel:
"""ButtonConnectionChannel class.
This class represents a connection channel to a Flic button.
Add this button connection channel to a FlicClient by executing client.add_connection_channel(connection_channel).
You may only have this connection channel added to one FlicClient at a time.
Before you add the connection channel to the client, you should set up your callback functions by assigning
the corresponding properties to this object with a function. Each callback function has a channel parameter as the first one,
referencing this object.
Available properties and the function parameters are:
on_create_connection_channel_response: channel, error, connection_status
on_removed: channel, removed_reason
on_connection_status_changed: channel, connection_status, disconnect_reason
on_button_up_or_down / on_button_click_or_hold / on_button_single_or_double_click / on_button_single_or_double_click_or_hold: channel, click_type, was_queued, time_diff
"""
_cnt = itertools.count()
def __init__(self, bd_addr, latency_mode = LatencyMode.NormalLatency, auto_disconnect_time = 511):
self._conn_id = next(ButtonConnectionChannel._cnt)
self._bd_addr = bd_addr
self._latency_mode = latency_mode
self._auto_disconnect_time = auto_disconnect_time
self._client = None
self.on_create_connection_channel_response = lambda channel, error, connection_status: None
self.on_removed = lambda channel, removed_reason: None
self.on_connection_status_changed = lambda channel, connection_status, disconnect_reason: None
self.on_button_up_or_down = lambda channel, click_type, was_queued, time_diff: None
self.on_button_click_or_hold = lambda channel, click_type, was_queued, time_diff: None
self.on_button_single_or_double_click = lambda channel, click_type, was_queued, time_diff: None
self.on_button_single_or_double_click_or_hold = lambda channel, click_type, was_queued, time_diff: None
@property
def bd_addr(self):
return self._bd_addr
@property
def latency_mode(self):
return self._latency_mode
@latency_mode.setter
def latency_mode(self, latency_mode):
if self._client is None:
self._latency_mode = latency_mode
return
with self._client._lock:
self._latency_mode = latency_mode
if not self._client._closed:
self._client._send_command("CmdChangeModeParameters", {"conn_id": self._conn_id, "latency_mode": self._latency_mode, "auto_disconnect_time": self._auto_disconnect_time})
@property
def auto_disconnect_time(self):
return self._auto_disconnect_time
@auto_disconnect_time.setter
def auto_disconnect_time(self, auto_disconnect_time):
if self._client is None:
self._auto_disconnect_time = auto_disconnect_time
return
with self._client._lock:
self._auto_disconnect_time = auto_disconnect_time
if not self._client._closed:
self._client._send_command("CmdChangeModeParameters", {"conn_id": self._conn_id, "latency_mode": self._latency_mode, "auto_disconnect_time": self._auto_disconnect_time})
class FlicClient:
"""FlicClient class.
When this class is constructed, a socket connection is established.
You may then send commands to the server and set timers.
Once you are ready with the initialization you must call the handle_events() method which is a main loop that never exits, unless the socket is closed.
For a more detailed description of all commands, events and enums, check the protocol specification.
All commands are wrapped in more high level functions and events are reported using callback functions.
All methods called on this class will take effect only if you eventually call the handle_events() method.
The ButtonScanner is used to set up a handler for advertisement packets.
The ButtonConnectionChannel is used to interact with connections to flic buttons and receive their events.
Other events are handled by the following callback functions that can be assigned to this object (and a list of the callback function parameters):
on_new_verified_button: bd_addr
on_no_space_for_new_connection: max_concurrently_connected_buttons
on_got_space_for_new_connection: max_concurrently_connected_buttons
on_bluetooth_controller_state_change: state
"""
_EVENTS = [
("EvtAdvertisementPacket", "<I6s17pb??", "scan_id bd_addr name rssi is_private already_verified"),
("EvtCreateConnectionChannelResponse", "<IBB", "conn_id error connection_status"),
("EvtConnectionStatusChanged", "<IBB", "conn_id connection_status disconnect_reason"),
("EvtConnectionChannelRemoved", "<IB", "conn_id removed_reason"),
("EvtButtonUpOrDown", "<IBBI", "conn_id click_type was_queued time_diff"),
("EvtButtonClickOrHold", "<IBBI", "conn_id click_type was_queued time_diff"),
("EvtButtonSingleOrDoubleClick", "<IBBI", "conn_id click_type was_queued time_diff"),
("EvtButtonSingleOrDoubleClickOrHold", "<IBBI", "conn_id click_type was_queued time_diff"),
("EvtNewVerifiedButton", "<6s", "bd_addr"),
("EvtGetInfoResponse", "<B6sBBhBBH", "bluetooth_controller_state my_bd_addr my_bd_addr_type max_pending_connections max_concurrently_connected_buttons current_pending_connections currently_no_space_for_new_connection nb_verified_buttons"),
("EvtNoSpaceForNewConnection", "<B", "max_concurrently_connected_buttons"),
("EvtGotSpaceForNewConnection", "<B", "max_concurrently_connected_buttons"),
("EvtBluetoothControllerStateChange", "<B", "state"),
("EvtPingResponse", "<I", "ping_id"),
("EvtGetButtonUUIDResponse", "<6s16s", "bd_addr uuid"),
("EvtScanWizardFoundPrivateButton", "<I", "scan_wizard_id"),
("EvtScanWizardFoundPublicButton", "<I6s17p", "scan_wizard_id bd_addr name"),
("EvtScanWizardButtonConnected", "<I", "scan_wizard_id"),
("EvtScanWizardCompleted", "<IB", "scan_wizard_id result")
]
_EVENT_STRUCTS = list(map(lambda x: None if x is None else struct.Struct(x[1]), _EVENTS))
_EVENT_NAMED_TUPLES = list(map(lambda x: None if x is None else namedtuple(x[0], x[2]), _EVENTS))
_COMMANDS = [
("CmdGetInfo", "", ""),
("CmdCreateScanner", "<I", "scan_id"),
("CmdRemoveScanner", "<I", "scan_id"),
("CmdCreateConnectionChannel", "<I6sBh", "conn_id bd_addr latency_mode auto_disconnect_time"),
("CmdRemoveConnectionChannel", "<I", "conn_id"),
("CmdForceDisconnect", "<6s", "bd_addr"),
("CmdChangeModeParameters", "<IBh", "conn_id latency_mode auto_disconnect_time"),
("CmdPing", "<I", "ping_id"),
("CmdGetButtonUUID", "<6s", "bd_addr"),
("CmdCreateScanWizard", "<I", "scan_wizard_id"),
("CmdCancelScanWizard", "<I", "scan_wizard_id")
]
_COMMAND_STRUCTS = list(map(lambda x: struct.Struct(x[1]), _COMMANDS))
_COMMAND_NAMED_TUPLES = list(map(lambda x: namedtuple(x[0], x[2]), _COMMANDS))
_COMMAND_NAME_TO_OPCODE = dict((x[0], i) for i, x in enumerate(_COMMANDS))
@staticmethod
def _bdaddr_bytes_to_string(bdaddr_bytes):
return ":".join(map(lambda x: "%02x" % x, reversed(bdaddr_bytes)))
@staticmethod
def _bdaddr_string_to_bytes(bdaddr_string):
return bytearray.fromhex("".join(reversed(bdaddr_string.split(":"))))
def __init__(self, host, port = 5551):
self._sock = socket.create_connection((host, port), None)
self._lock = threading.RLock()
self._scanners = {}
self._scan_wizards = {}
self._connection_channels = {}
self._get_info_response_queue = queue.Queue()
self._get_button_uuid_queue = queue.Queue()
self._timers = queue.PriorityQueue()
self._handle_event_thread_ident = None
self._closed = False
self.on_new_verified_button = lambda bd_addr: None
self.on_no_space_for_new_connection = lambda max_concurrently_connected_buttons: None
self.on_got_space_for_new_connection = lambda max_concurrently_connected_buttons: None
self.on_bluetooth_controller_state_change = lambda state: None
def close(self):
"""Closes the client. The handle_events() method will return."""
with self._lock:
if self._closed:
return
if threading.get_ident() != self._handle_event_thread_ident:
self._send_command("CmdPing", {"ping_id": 0}) # To unblock socket select
self._closed = True
def add_scanner(self, scanner):
"""Add a ButtonScanner object.
The scan will start directly once the scanner is added.
"""
with self._lock:
if scanner._scan_id in self._scanners:
return
self._scanners[scanner._scan_id] = scanner
self._send_command("CmdCreateScanner", {"scan_id": scanner._scan_id})
def remove_scanner(self, scanner):
"""Remove a ButtonScanner object.
You will no longer receive advertisement packets.
"""
with self._lock:
if scanner._scan_id not in self._scanners:
return
del self._scanners[scanner._scan_id]
self._send_command("CmdRemoveScanner", {"scan_id": scanner._scan_id})
def add_scan_wizard(self, scan_wizard):
"""Add a ScanWizard object.
The scan wizard will start directly once the scan wizard is added.
"""
with self._lock:
if scan_wizard._scan_wizard_id in self._scan_wizards:
return
self._scan_wizards[scan_wizard._scan_wizard_id] = scan_wizard
self._send_command("CmdCreateScanWizard", {"scan_wizard_id": scan_wizard._scan_wizard_id})
def cancel_scan_wizard(self, scan_wizard):
"""Cancel a ScanWizard.
Note: The effect of this command will take place at the time the on_completed event arrives on the scan wizard object.
If cancelled due to this command, "result" in the on_completed event will be "WizardCancelledByUser".
"""
with self._lock:
if scan_wizard._scan_wizard_id not in self._scan_wizards:
return
self._send_command("CmdCancelScanWizard", {"scan_wizard_id": scan_wizard._scan_wizard_id})
def add_connection_channel(self, channel):
"""Adds a connection channel to a specific Flic button.
This will start listening for a specific Flic button's connection and button events.
Make sure the Flic is either in public mode (by holding it down for 7 seconds) or already verified before calling this method.
The on_create_connection_channel_response callback property will be called on the
connection channel after this command has been received by the server.
You may have as many connection channels as you wish for a specific Flic Button.
"""
with self._lock:
if channel._conn_id in self._connection_channels:
return
channel._client = self
self._connection_channels[channel._conn_id] = channel
self._send_command("CmdCreateConnectionChannel", {"conn_id": channel._conn_id, "bd_addr": channel.bd_addr, "latency_mode": channel._latency_mode, "auto_disconnect_time": channel._auto_disconnect_time})
def remove_connection_channel(self, channel):
"""Remove a connection channel.
This will stop listening for new events for a specific connection channel that has previously been added.
Note: The effect of this command will take place at the time the on_removed event arrives on the connection channel object.
"""
with self._lock:
if channel._conn_id not in self._connection_channels:
return
self._send_command("CmdRemoveConnectionChannel", {"conn_id": channel._conn_id})
def force_disconnect(self, bd_addr):
"""Force disconnection or cancel pending connection of a specific Flic button.
This removes all connection channels for all clients connected to the server for this specific Flic button.
"""
self._send_command("CmdForceDisconnect", {"bd_addr": bd_addr})
def get_info(self, callback):
"""Get info about the current state of the server.
The server will send back its information directly and the callback will be called once the response arrives.
The callback takes only one parameter: info. This info parameter is a dictionary with the following objects:
bluetooth_controller_state, my_bd_addr, my_bd_addr_type, max_pending_connections, max_concurrently_connected_buttons,
current_pending_connections, currently_no_space_for_new_connection, bd_addr_of_verified_buttons (a list of bd addresses).
"""
self._get_info_response_queue.put(callback)
self._send_command("CmdGetInfo", {})
def get_button_uuid(self, bd_addr, callback):
"""Get button uuid for a verified button.
The server will send back its information directly and the callback will be called once the response arrives.
Responses will arrive in the same order as requested.
The callback takes two parameters: bd_addr, uuid (hex string of 32 characters).
Note: if the button isn't verified, the uuid sent to the callback will rather be None.
"""
with self._lock:
self._get_button_uuid_queue.put(callback)
self._send_command("CmdGetButtonUUID", {"bd_addr": bd_addr})
def set_timer(self, timeout_millis, callback):
"""Set a timer
This timer callback will run after the specified timeout_millis on the thread that handles the events.
"""
point_in_time = time.monotonic() + timeout_millis / 1000.0
self._timers.put((point_in_time, callback))
if threading.get_ident() != self._handle_event_thread_ident:
self._send_command("CmdPing", {"ping_id": 0}) # To unblock socket select
def run_on_handle_events_thread(self, callback):
"""Run a function on the thread that handles the events."""
if threading.get_ident() == self._handle_event_thread_ident:
callback()
else:
self.set_timer(0, callback)
def _send_command(self, name, items):
for key, value in items.items():
if isinstance(value, Enum):
items[key] = value.value
if "bd_addr" in items:
items["bd_addr"] = FlicClient._bdaddr_string_to_bytes(items["bd_addr"])
opcode = FlicClient._COMMAND_NAME_TO_OPCODE[name]
data_bytes = FlicClient._COMMAND_STRUCTS[opcode].pack(*FlicClient._COMMAND_NAMED_TUPLES[opcode](**items))
bytes = bytearray(3)
bytes[0] = (len(data_bytes) + 1) & 0xff
bytes[1] = (len(data_bytes) + 1) >> 8
bytes[2] = opcode
bytes += data_bytes
with self._lock:
if not self._closed:
self._sock.sendall(bytes)
def _dispatch_event(self, data):
if len(data) == 0:
return
opcode = data[0]
if opcode >= len(FlicClient._EVENTS) or FlicClient._EVENTS[opcode] is None:
return
event_name = FlicClient._EVENTS[opcode][0]
data_tuple = FlicClient._EVENT_STRUCTS[opcode].unpack(data[1 : 1 + FlicClient._EVENT_STRUCTS[opcode].size])
items = FlicClient._EVENT_NAMED_TUPLES[opcode]._make(data_tuple)._asdict()
# Process some kind of items whose data type is not supported by struct
if "bd_addr" in items:
items["bd_addr"] = FlicClient._bdaddr_bytes_to_string(items["bd_addr"])
if "name" in items:
items["name"] = items["name"].decode("utf-8")
if event_name == "EvtCreateConnectionChannelResponse":
items["error"] = CreateConnectionChannelError(items["error"])
items["connection_status"] = ConnectionStatus(items["connection_status"])
if event_name == "EvtConnectionStatusChanged":
items["connection_status"] = ConnectionStatus(items["connection_status"])
items["disconnect_reason"] = DisconnectReason(items["disconnect_reason"])
if event_name == "EvtConnectionChannelRemoved":
items["removed_reason"] = RemovedReason(items["removed_reason"])
if event_name.startswith("EvtButton"):
items["click_type"] = ClickType(items["click_type"])
if event_name == "EvtGetInfoResponse":
items["bluetooth_controller_state"] = BluetoothControllerState(items["bluetooth_controller_state"])
items["my_bd_addr"] = FlicClient._bdaddr_bytes_to_string(items["my_bd_addr"])
items["my_bd_addr_type"] = BdAddrType(items["my_bd_addr_type"])
items["bd_addr_of_verified_buttons"] = []
pos = FlicClient._EVENT_STRUCTS[opcode].size
for i in range(items["nb_verified_buttons"]):
items["bd_addr_of_verified_buttons"].append(FlicClient._bdaddr_bytes_to_string(data[1 + pos : 1 + pos + 6]))
pos += 6
if event_name == "EvtBluetoothControllerStateChange":
items["state"] = BluetoothControllerState(items["state"])
if event_name == "EvtGetButtonUUIDResponse":
items["uuid"] = "".join(map(lambda x: "%02x" % x, items["uuid"]))
if items["uuid"] == "00000000000000000000000000000000":
items["uuid"] = None
if event_name == "EvtScanWizardCompleted":
items["result"] = ScanWizardResult(items["result"])
# Process event
if event_name == "EvtAdvertisementPacket":
scanner = self._scanners.get(items["scan_id"])
if scanner is not None:
scanner.on_advertisement_packet(scanner, items["bd_addr"], items["name"], items["rssi"], items["is_private"], items["already_verified"])
if event_name == "EvtCreateConnectionChannelResponse":
channel = self._connection_channels[items["conn_id"]]
if items["error"] != CreateConnectionChannelError.NoError:
del self._connection_channels[items["conn_id"]]
channel.on_create_connection_channel_response(channel, items["error"], items["connection_status"])
if event_name == "EvtConnectionStatusChanged":
channel = self._connection_channels[items["conn_id"]]
channel.on_connection_status_changed(channel, items["connection_status"], items["disconnect_reason"])
if event_name == "EvtConnectionChannelRemoved":
channel = self._connection_channels[items["conn_id"]]
del self._connection_channels[items["conn_id"]]
channel.on_removed(channel, items["removed_reason"])
if event_name == "EvtButtonUpOrDown":
channel = self._connection_channels[items["conn_id"]]
channel.on_button_up_or_down(channel, items["click_type"], items["was_queued"], items["time_diff"])
if event_name == "EvtButtonClickOrHold":
channel = self._connection_channels[items["conn_id"]]
channel.on_button_click_or_hold(channel, items["click_type"], items["was_queued"], items["time_diff"])
if event_name == "EvtButtonSingleOrDoubleClick":
channel = self._connection_channels[items["conn_id"]]
channel.on_button_single_or_double_click(channel, items["click_type"], items["was_queued"], items["time_diff"])
if event_name == "EvtButtonSingleOrDoubleClickOrHold":
channel = self._connection_channels[items["conn_id"]]
channel.on_button_single_or_double_click_or_hold(channel, items["click_type"], items["was_queued"], items["time_diff"])
if event_name == "EvtNewVerifiedButton":
self.on_new_verified_button(items["bd_addr"])
if event_name == "EvtGetInfoResponse":
self._get_info_response_queue.get()(items)
if event_name == "EvtNoSpaceForNewConnection":
self.on_no_space_for_new_connection(items["max_concurrently_connected_buttons"])
if event_name == "EvtGotSpaceForNewConnection":
self.on_got_space_for_new_connection(items["max_concurrently_connected_buttons"])
if event_name == "EvtBluetoothControllerStateChange":
self.on_bluetooth_controller_state_change(items["state"])
if event_name == "EvtGetButtonUUIDResponse":
self._get_button_uuid_queue.get()(items["bd_addr"], items["uuid"])
if event_name == "EvtScanWizardFoundPrivateButton":
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
scan_wizard.on_found_private_button(scan_wizard)
if event_name == "EvtScanWizardFoundPublicButton":
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
scan_wizard._bd_addr = items["bd_addr"]
scan_wizard._name = items["name"]
scan_wizard.on_found_public_button(scan_wizard, scan_wizard._bd_addr, scan_wizard._name)
if event_name == "EvtScanWizardButtonConnected":
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
scan_wizard.on_button_connected(scan_wizard, scan_wizard._bd_addr, scan_wizard._name)
if event_name == "EvtScanWizardCompleted":
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
del self._scan_wizards[items["scan_wizard_id"]]
scan_wizard.on_completed(scan_wizard, items["result"], scan_wizard._bd_addr, scan_wizard._name)
def _handle_one_event(self):
if len(self._timers.queue) > 0:
current_timer = self._timers.queue[0]
timeout = max(current_timer[0] - time.monotonic(), 0)
if timeout == 0:
self._timers.get()[1]()
return True
if len(select.select([self._sock], [], [], timeout)[0]) == 0:
return True
len_arr = bytearray(2)
view = memoryview(len_arr)
toread = 2
while toread > 0:
nbytes = self._sock.recv_into(view, toread)
if nbytes == 0:
return False
view = view[nbytes:]
toread -= nbytes
packet_len = len_arr[0] | (len_arr[1] << 8)
data = bytearray(packet_len)
view = memoryview(data)
toread = packet_len
while toread > 0:
nbytes = self._sock.recv_into(view, toread)
if nbytes == 0:
return False
view = view[nbytes:]
toread -= nbytes
self._dispatch_event(data)
return True
def handle_events(self):
"""Start the main loop for this client.
This method will not return until the socket has been closed.
Once it has returned, any use of this FlicClient is illegal.
"""
self._handle_event_thread_ident = threading.get_ident()
while not self._closed:
if not self._handle_one_event():
break
self._sock.close()

View File

@ -1,9 +0,0 @@
manifest:
events:
platypush.message.event.button.flic.FlicButtonEvent: when a button is pressed.The
event will also contain the press sequence(e.g. ``["ShortPressEvent", "LongPressEvent",
"ShortPressEvent"]``)
install:
pip: []
package: platypush.backend.button.flic
type: backend

View File

@ -1,214 +0,0 @@
import json
import socket
from enum import Enum
from threading import Thread
from platypush.backend import Backend
from platypush.context import get_backend
class CameraPiBackend(Backend):
"""
Backend to interact with a Raspberry Pi camera. It can start and stop
recordings and take pictures. It can be programmatically controlled through
the :class:`platypush.plugins.camera.pi` plugin. Note that the Redis backend
must be configured and running to enable camera control.
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
on :class:`platypush.message.event.application.ApplicationStartedEvent` that runs ``camera.pi.start_streaming``.
"""
class CameraAction(Enum):
START_RECORDING = 'START_RECORDING'
STOP_RECORDING = 'STOP_RECORDING'
TAKE_PICTURE = 'TAKE_PICTURE'
def __eq__(self, other):
return self.value == other
# noinspection PyUnresolvedReferences,PyPackageRequirements
def __init__(
self,
listen_port,
bind_address='0.0.0.0',
x_resolution=640,
y_resolution=480,
redis_queue='platypush/camera/pi',
start_recording_on_startup=True,
framerate=24,
hflip=False,
vflip=False,
sharpness=0,
contrast=0,
brightness=50,
video_stabilization=False,
iso=0,
exposure_compensation=0,
exposure_mode='auto',
meter_mode='average',
awb_mode='auto',
image_effect='none',
color_effects=None,
rotation=0,
crop=(0.0, 0.0, 1.0, 1.0),
**kwargs
):
"""
See https://www.raspberrypi.org/documentation/usage/camera/python/README.md
for a detailed reference about the Pi camera options.
:param listen_port: Port where the camera process will provide the video output while recording
:type listen_port: int
:param bind_address: Bind address (default: 0.0.0.0).
:type bind_address: str
"""
super().__init__(**kwargs)
self.bind_address = bind_address
self.listen_port = listen_port
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.listen(0)
import picamera
self.camera = picamera.PiCamera()
self.camera.resolution = (x_resolution, y_resolution)
self.camera.framerate = framerate
self.camera.hflip = hflip
self.camera.vflip = vflip
self.camera.sharpness = sharpness
self.camera.contrast = contrast
self.camera.brightness = brightness
self.camera.video_stabilization = video_stabilization
self.camera.ISO = iso
self.camera.exposure_compensation = exposure_compensation
self.camera.exposure_mode = exposure_mode
self.camera.meter_mode = meter_mode
self.camera.awb_mode = awb_mode
self.camera.image_effect = image_effect
self.camera.color_effects = color_effects
self.camera.rotation = rotation
self.camera.crop = crop
self.start_recording_on_startup = start_recording_on_startup
self.redis = None
self.redis_queue = redis_queue
self._recording_thread = None
def send_camera_action(self, action, **kwargs):
action = {'action': action.value, **kwargs}
self.redis.send_message(msg=json.dumps(action), queue_name=self.redis_queue)
def take_picture(self, image_file):
"""
Take a picture.
:param image_file: Output image file
:type image_file: str
"""
self.logger.info('Capturing camera snapshot to {}'.format(image_file))
self.camera.capture(image_file)
self.logger.info('Captured camera snapshot to {}'.format(image_file))
# noinspection PyShadowingBuiltins
def start_recording(self, video_file=None, format='h264'):
"""
Start a recording.
:param video_file: Output video file. If specified, the video will be recorded to file, otherwise it will be
served via TCP/IP on the listen_port. Use ``stop_recording`` to stop the recording.
:type video_file: str
:param format: Video format (default: h264)
:type format: str
"""
# noinspection PyBroadException
def recording_thread():
if video_file:
self.camera.start_recording(video_file, format=format)
while True:
self.camera.wait_recording(2)
else:
while not self.should_stop():
connection = self.server_socket.accept()[0].makefile('wb')
self.logger.info(
'Accepted client connection on port {}'.format(self.listen_port)
)
try:
self.camera.start_recording(connection, format=format)
while True:
self.camera.wait_recording(2)
except ConnectionError:
self.logger.info('Client closed connection')
try:
self.stop_recording()
except Exception as e:
self.logger.warning(
'Could not stop recording: {}'.format(str(e))
)
try:
connection.close()
except Exception as e:
self.logger.warning(
'Could not close connection: {}'.format(str(e))
)
self.send_camera_action(self.CameraAction.START_RECORDING)
if self._recording_thread:
self.logger.info('Recording already running')
return
self.logger.info('Starting camera recording')
self._recording_thread = Thread(
target=recording_thread, name='PiCameraRecorder'
)
self._recording_thread.start()
def stop_recording(self):
"""Stops recording"""
self.logger.info('Stopping camera recording')
try:
self.camera.stop_recording()
except Exception as e:
self.logger.warning('Failed to stop recording')
self.logger.exception(e)
def run(self):
super().run()
if not self.redis:
self.redis = get_backend('redis')
if self.start_recording_on_startup:
self.send_camera_action(self.CameraAction.START_RECORDING)
self.logger.info('Initialized Pi camera backend')
while not self.should_stop():
try:
msg = self.redis.get_message(self.redis_queue)
if msg.get('action') == self.CameraAction.START_RECORDING:
self.start_recording()
elif msg.get('action') == self.CameraAction.STOP_RECORDING:
self.stop_recording()
elif msg.get('action') == self.CameraAction.TAKE_PICTURE:
self.take_picture(image_file=msg.get('image_file'))
except Exception as e:
self.logger.exception(e)
# vim:sw=4:ts=4:et:

View File

@ -1,164 +0,0 @@
import re
from typing import Type, Optional, Union, List
from platypush.backend import Backend
from platypush.context import get_plugin
from platypush.message.event.chat.telegram import (
MessageEvent,
CommandMessageEvent,
TextMessageEvent,
PhotoMessageEvent,
VideoMessageEvent,
ContactMessageEvent,
DocumentMessageEvent,
LocationMessageEvent,
GroupChatCreatedEvent,
)
from platypush.plugins.chat.telegram import ChatTelegramPlugin
class ChatTelegramBackend(Backend):
"""
Telegram bot that listens for messages and updates.
Requires:
* The :class:`platypush.plugins.chat.telegram.ChatTelegramPlugin` plugin configured
"""
def __init__(
self, authorized_chat_ids: Optional[List[Union[str, int]]] = None, **kwargs
):
"""
:param authorized_chat_ids: Optional list of chat_id/user_id which are authorized to send messages to
the bot. If nothing is specified then no restrictions are applied.
"""
super().__init__(**kwargs)
self.authorized_chat_ids = set(authorized_chat_ids or [])
self._plugin: ChatTelegramPlugin = get_plugin('chat.telegram') # type: ignore
def _authorize(self, msg):
if not self.authorized_chat_ids:
return
if msg.chat.type == 'private' and msg.chat.id not in self.authorized_chat_ids:
self.logger.info(
'Received message from unauthorized chat_id %s', msg.chat.id
)
self._plugin.send_message(
chat_id=msg.chat.id,
text='You are not allowed to send messages to this bot',
)
raise PermissionError
def _msg_hook(self, cls: Type[MessageEvent]):
# noinspection PyUnusedLocal
def hook(update, _):
msg = update.effective_message
try:
self._authorize(msg)
self.bus.post(
cls(
chat_id=update.effective_chat.id,
message=self._plugin.parse_msg(msg).output,
user=self._plugin.parse_user(update.effective_user).output,
)
)
except PermissionError:
pass
return hook
def _group_hook(self):
def hook(update, context):
msg = update.effective_message
if msg.group_chat_created:
self.bus.post(
GroupChatCreatedEvent(
chat_id=update.effective_chat.id,
message=self._plugin.parse_msg(msg).output,
user=self._plugin.parse_user(update.effective_user).output,
)
)
elif msg.photo:
self._msg_hook(PhotoMessageEvent)(update, context)
elif msg.video:
self._msg_hook(VideoMessageEvent)(update, context)
elif msg.contact:
self._msg_hook(ContactMessageEvent)(update, context)
elif msg.location:
self._msg_hook(LocationMessageEvent)(update, context)
elif msg.document:
self._msg_hook(DocumentMessageEvent)(update, context)
elif msg.text:
if msg.text.startswith('/'):
self._command_hook()(update, context)
else:
self._msg_hook(TextMessageEvent)(update, context)
return hook
def _command_hook(self):
def hook(update, _):
msg = update.effective_message
m = re.match(r'\s*/([0-9a-zA-Z_-]+)\s*(.*)', msg.text)
if not m:
self.logger.warning('Invalid command: %s', msg.text)
return
cmd = m.group(1).lower()
args = [arg for arg in re.split(r'\s+', m.group(2)) if len(arg)]
try:
self._authorize(msg)
self.bus.post(
CommandMessageEvent(
chat_id=update.effective_chat.id,
command=cmd,
cmdargs=args,
message=self._plugin.parse_msg(msg).output,
user=self._plugin.parse_user(update.effective_user).output,
)
)
except PermissionError:
pass
return hook
def run(self):
from telegram.ext import MessageHandler, Filters
super().run()
telegram = self._plugin.get_telegram()
dispatcher = telegram.dispatcher
dispatcher.add_handler(MessageHandler(Filters.group, self._group_hook()))
dispatcher.add_handler(
MessageHandler(Filters.text, self._msg_hook(TextMessageEvent))
)
dispatcher.add_handler(
MessageHandler(Filters.photo, self._msg_hook(PhotoMessageEvent))
)
dispatcher.add_handler(
MessageHandler(Filters.video, self._msg_hook(VideoMessageEvent))
)
dispatcher.add_handler(
MessageHandler(Filters.contact, self._msg_hook(ContactMessageEvent))
)
dispatcher.add_handler(
MessageHandler(Filters.location, self._msg_hook(LocationMessageEvent))
)
dispatcher.add_handler(
MessageHandler(Filters.document, self._msg_hook(DocumentMessageEvent))
)
dispatcher.add_handler(MessageHandler(Filters.command, self._command_hook()))
self.logger.info('Initialized Telegram backend')
telegram.start_polling()
# vim:sw=4:ts=4:et:

View File

@ -1,19 +0,0 @@
manifest:
events:
platypush.message.event.chat.telegram.CommandMessageEvent: when a command message
is received.
platypush.message.event.chat.telegram.ContactMessageEvent: when a contact is received.
platypush.message.event.chat.telegram.DocumentMessageEvent: when a document is
received.
platypush.message.event.chat.telegram.GroupChatCreatedEvent: when the bot is invited
to a new group.
platypush.message.event.chat.telegram.LocationMessageEvent: when a location is
received.
platypush.message.event.chat.telegram.PhotoMessageEvent: when a photo is received.
platypush.message.event.chat.telegram.TextMessageEvent: when a text message is
received.
platypush.message.event.chat.telegram.VideoMessageEvent: when a video is received.
install:
pip: []
package: platypush.backend.chat.telegram
type: backend

View File

@ -1,127 +0,0 @@
from typing import Iterable, Dict, Union, Any
from watchdog.observers import Observer
from platypush.backend import Backend
from .entities.handlers import EventHandler
from .entities.resources import MonitoredResource, MonitoredPattern, MonitoredRegex
class FileMonitorBackend(Backend):
"""
This backend monitors changes to local files and directories using the Watchdog API.
"""
class EventHandlerFactory:
"""
Create a file system event handler from a string, dictionary or ``MonitoredResource`` resource.
"""
@staticmethod
def from_resource(
resource: Union[str, Dict[str, Any], MonitoredResource]
) -> EventHandler:
if isinstance(resource, str):
resource = MonitoredResource(resource)
elif isinstance(resource, dict):
if 'regexes' in resource or 'ignore_regexes' in resource:
resource = MonitoredRegex(**resource)
elif (
'patterns' in resource
or 'ignore_patterns' in resource
or 'ignore_directories' in resource
):
resource = MonitoredPattern(**resource)
else:
resource = MonitoredResource(**resource)
return EventHandler.from_resource(resource)
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:
- Simple strings. In this case, paths will be interpreted as absolute references to a file or a directory
to monitor. Example:
.. code-block:: yaml
backend.file.monitor:
paths:
# Monitor changes on the /tmp folder
- /tmp
# Monitor changes on /etc/passwd
- /etc/passwd
- Path with monitoring properties expressed as a key-value object. Example showing the supported attributes:
.. code-block:: yaml
backend.file.monitor:
paths:
# Monitor changes on the /tmp folder and its subfolders
- path: /tmp
recursive: True
- Path with pattern-based search criteria for the files to monitor and exclude. Example:
.. code-block:: yaml
backend.file.monitor:
paths:
# Recursively monitor changes on the ~/my-project folder that include all
# *.py files, excluding those whose name starts with tmp_ and
# all the files contained in the __pycache__ folders
- path: ~/my-project
recursive: True
patterns:
- "*.py"
ignore_patterns:
- "tmp_*"
ignore_directories:
- "__pycache__"
- Path with regex-based search criteria for the files to monitor and exclude. Example:
.. code-block:: yaml
backend.file.monitor:
paths:
# Recursively monitor changes on the ~/my-images folder that include all
# the files matching a JPEG extension in case-insensitive mode,
# excluding those whose name starts with tmp_ and
# all the files contained in the __MACOSX folders
- path: ~/my-images
recursive: True
case_sensitive: False
regexes:
- '.*\\.jpe?g$'
ignore_patterns:
- '^tmp_.*'
ignore_directories:
- '__MACOSX'
"""
super().__init__(**kwargs)
self._observer = Observer()
for path in paths:
handler = self.EventHandlerFactory.from_resource(path)
self._observer.schedule(
handler, handler.resource.path, recursive=handler.resource.recursive
)
def run(self):
super().run()
self.logger.info('Initializing file monitor backend')
self._observer.start()
self.wait_stop()
def on_stop(self):
self.logger.info('Stopping file monitor backend')
self._observer.stop()
self._observer.join()
self.logger.info('Stopped file monitor backend')

View File

@ -1,18 +0,0 @@
manifest:
events:
platypush.message.event.file.FileSystemCreateEvent: if a resource is created.
platypush.message.event.file.FileSystemDeleteEvent: if a resource is removed.
platypush.message.event.file.FileSystemModifyEvent: if a resource is modified.
install:
apk:
- py3-watchdog
apt:
- python3-watchdog
dnf:
- python-watchdog
pacman:
- python-watchdog
pip:
- watchdog
package: platypush.backend.file.monitor
type: backend

View File

@ -1,54 +0,0 @@
from typing import Optional
from platypush.backend import Backend
from platypush.context import get_plugin
from platypush.message.event.foursquare import FoursquareCheckinEvent
class FoursquareBackend(Backend):
"""
This backend polls for new check-ins on the user's Foursquare account and triggers an event when a new check-in
occurs.
Requires:
* The :class:`platypush.plugins.foursquare.FoursquarePlugin` plugin configured and enabled.
"""
_last_created_at_varname = '_foursquare_checkin_last_created_at'
def __init__(self, poll_seconds: Optional[float] = 60.0, *args, **kwargs):
"""
:param poll_seconds: How often the backend should check for new check-ins (default: one minute).
"""
super().__init__(*args, poll_seconds=poll_seconds, **kwargs)
self._last_created_at = None
def __enter__(self):
self._last_created_at = int(
get_plugin('variable')
.get(self._last_created_at_varname)
.output.get(self._last_created_at_varname)
or 0
)
self.logger.info('Started Foursquare backend')
def loop(self):
checkins = get_plugin('foursquare').get_checkins().output
if not checkins:
return
last_checkin = checkins[0]
last_checkin_created_at = last_checkin.get('createdAt', 0)
if self._last_created_at and last_checkin_created_at <= self._last_created_at:
return
self.bus.post(FoursquareCheckinEvent(checkin=last_checkin))
self._last_created_at = last_checkin_created_at
get_plugin('variable').set(
**{self._last_created_at_varname: self._last_created_at}
)
# vim:sw=4:ts=4:et:

View File

@ -1,8 +0,0 @@
manifest:
events:
platypush.message.event.foursquare.FoursquareCheckinEvent: when a new check-in
occurs.
install:
pip: []
package: platypush.backend.foursquare
type: backend

View File

@ -1,32 +0,0 @@
manifest:
events:
platypush.message.event.github.GithubCommitCommentEvent: when a new commit comment
is created.
platypush.message.event.github.GithubCreateEvent: when a tag or branch is created.
platypush.message.event.github.GithubDeleteEvent: when a tag or branch is deleted.
platypush.message.event.github.GithubEvent: for any event that doesn't fall in
the above categories(``event_type`` will be set accordingly).
platypush.message.event.github.GithubForkEvent: when a user forks a repository.
platypush.message.event.github.GithubIssueCommentEvent: when new activity happens
on an issue comment.
platypush.message.event.github.GithubIssueEvent: when new repository issue activity
happens.
platypush.message.event.github.GithubMemberEvent: when new repository collaborators
activity happens.
platypush.message.event.github.GithubPublicEvent: when a repository goes public.
platypush.message.event.github.GithubPullRequestEvent: when new pull request related
activity happens.
platypush.message.event.github.GithubPullRequestReviewCommentEvent: when activity
happens on a pullrequest commit.
platypush.message.event.github.GithubPushEvent: when a new push is created.
platypush.message.event.github.GithubReleaseEvent: when a new release happens.
platypush.message.event.github.GithubSponsorshipEvent: when new sponsorship related
activity happens.
platypush.message.event.github.GithubWatchEvent: when someone stars/starts watching
a repository.
platypush.message.event.github.GithubWikiEvent: when new activity happens on a
repository wiki.
install:
pip: []
package: platypush.backend.github
type: backend

View File

@ -1,129 +0,0 @@
import datetime
import time
from platypush.backend import Backend
from platypush.context import get_plugin
from platypush.message.event.google.fit import GoogleFitEvent
from platypush.utils import camel_case_to_snake_case
class GoogleFitBackend(Backend):
"""
This backend will listen for new Google Fit events (e.g. new weight/height
measurements, new fitness activities etc.) on the specified data streams and
fire an event upon new data.
Requires:
* The **google.fit** plugin
(:class:`platypush.plugins.google.fit.GoogleFitPlugin`) enabled.
"""
_default_poll_seconds = 60
_default_user_id = 'me'
_last_timestamp_varname = '_GOOGLE_FIT_LAST_TIMESTAMP_'
def __init__(
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
get a list of the available data sources through the
:meth:`platypush.plugins.google.fit.GoogleFitPlugin.get_data_sources`
action
:type data_sources: list[str]
:param user_id: Google user ID to track (default: 'me')
:type user_id: str
:param poll_seconds: How often the backend will query the data sources
for new data points (default: 60 seconds)
:type poll_seconds: float
"""
super().__init__(*args, **kwargs)
self.data_sources = data_sources
self.user_id = user_id
self.poll_seconds = poll_seconds
def run(self):
super().run()
self.logger.info(
'Started Google Fit backend on data sources {}'.format(self.data_sources)
)
while not self.should_stop():
try:
for data_source in self.data_sources:
varname = self._last_timestamp_varname + data_source
last_timestamp = float(
get_plugin('variable').get(varname).output.get(varname) or 0
)
new_last_timestamp = last_timestamp
self.logger.info(
'Processing new entries from data source {}, last timestamp: {}'.format(
data_source,
str(datetime.datetime.fromtimestamp(last_timestamp)),
)
)
data_points = (
get_plugin('google.fit')
.get_data(user_id=self.user_id, data_source_id=data_source)
.output
)
new_data_points = 0
for dp in data_points:
dp_time = dp.pop('startTime', 0)
if 'dataSourceId' in dp:
del dp['dataSourceId']
if dp_time > last_timestamp:
self.bus.post(
GoogleFitEvent(
user_id=self.user_id,
data_source_id=data_source,
data_type=dp.pop('dataTypeName'),
start_time=dp_time,
end_time=dp.pop('endTime'),
modified_time=dp.pop('modifiedTime'),
values=dp.pop('values'),
**{
camel_case_to_snake_case(k): v
for k, v in dp.items()
}
)
)
new_data_points += 1
new_last_timestamp = max(dp_time, new_last_timestamp)
last_timestamp = new_last_timestamp
self.logger.info(
'Got {} new entries from data source {}, last timestamp: {}'.format(
new_data_points,
data_source,
str(datetime.datetime.fromtimestamp(last_timestamp)),
)
)
get_plugin('variable').set(**{varname: last_timestamp})
except Exception as e:
self.logger.warning('Exception while processing Fit data')
self.logger.exception(e)
continue
time.sleep(self.poll_seconds)
# vim:sw=4:ts=4:et:

View File

@ -1,8 +0,0 @@
manifest:
events:
platypush.message.event.google.fit.GoogleFitEvent: when a newdata point is received
on one of the registered streams.
install:
pip: []
package: platypush.backend.google.fit
type: backend

View File

@ -1,88 +0,0 @@
import json
from typing import Optional, List
from platypush.backend import Backend
from platypush.context import get_plugin
from platypush.message.event.google.pubsub import GooglePubsubMessageEvent
class GooglePubsubBackend(Backend):
"""
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
project and credentials file.
"""
def __init__(
self, topics: List[str], credentials_file: Optional[str] = None, *args, **kwargs
):
"""
:param topics: List of topics to subscribe. You can either specify the full topic name in the format
``projects/<project_id>/topics/<topic_name>``, where ``<project_id>`` must be the ID of your
Google Pub/Sub project, or just ``<topic_name>`` - in such case it's implied that you refer to the
``topic_name`` under the ``project_id`` of your service credentials.
:param credentials_file: Path to the Pub/Sub service credentials file (default: value configured on the
``google.pubsub`` plugin or ``~/.credentials/platypush/google/pubsub.json``).
"""
super().__init__(*args, name='GooglePubSub', **kwargs)
self.topics = topics
if credentials_file:
self.credentials_file = credentials_file
else:
plugin = self._get_plugin()
self.credentials_file = plugin.credentials_file
@staticmethod
def _get_plugin():
plugin = get_plugin('google.pubsub')
assert plugin, 'google.pubsub plugin not enabled'
return plugin
def _message_callback(self, topic):
def callback(msg):
data = msg.data.decode()
try:
data = json.loads(data)
except Exception as e:
self.logger.debug('Not a valid JSON: %s: %s', data, e)
msg.ack()
self.bus.post(GooglePubsubMessageEvent(topic=topic, msg=data))
return callback
def run(self):
# noinspection PyPackageRequirements
from google.cloud import pubsub_v1
# noinspection PyPackageRequirements
from google.api_core.exceptions import AlreadyExists
super().run()
plugin = self._get_plugin()
project_id = plugin.get_project_id()
credentials = plugin.get_credentials(plugin.subscriber_audience)
subscriber = pubsub_v1.SubscriberClient(credentials=credentials)
for topic in self.topics:
prefix = f'projects/{project_id}/topics/'
if not topic.startswith(prefix):
topic = f'{prefix}{topic}'
subscription_name = '/'.join(
[*topic.split('/')[:2], 'subscriptions', topic.split('/')[-1]]
)
try:
subscriber.create_subscription(name=subscription_name, topic=topic)
except AlreadyExists:
pass
subscriber.subscribe(subscription_name, self._message_callback(topic))
self.wait_stop()
# vim:sw=4:ts=4:et:

View File

@ -1,9 +0,0 @@
manifest:
events:
platypush.message.event.google.pubsub.GooglePubsubMessageEvent: when a new message
is received ona subscribed topic.
install:
pip:
- google-cloud-pubsub
package: platypush.backend.google.pubsub
type: backend

View File

@ -1,145 +0,0 @@
import threading
import time
from platypush.backend import Backend
from platypush.message.event.gps import GPSVersionEvent, GPSDeviceEvent, GPSUpdateEvent
class GpsBackend(Backend):
"""
This backend can interact with a GPS device and listen for events.
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::
[sudo] gpsd /dev/ttyUSB0 -F /var/run/gpsd.sock
The best option is probably to run gpsd at startup as a systemd service.
"""
_fail_sleep_time = 5.0
_lat_lng_tolerance = 1e-5
_alt_tolerance = 0.5
def __init__(self, gpsd_server='localhost', gpsd_port=2947, **kwargs):
"""
:param gpsd_server: gpsd daemon server name/address (default: localhost)
:type gpsd_server: str
:param gpsd_port: Port of the gpsd daemon (default: 2947)
:type gpsd_port: int or str
"""
super().__init__(**kwargs)
self.gpsd_server = gpsd_server
self.gpsd_port = gpsd_port
self._session = None
self._session_lock = threading.RLock()
self._devices = {}
def _get_session(self):
import gps
with self._session_lock:
if not self._session:
self._session = gps.gps(
host=self.gpsd_server, port=self.gpsd_port, reconnect=True
)
self._session.stream(gps.WATCH_ENABLE | gps.WATCH_NEWSTYLE)
return self._session
def _gps_report_to_event(self, report):
if report.get('class').lower() == 'version':
return GPSVersionEvent(
release=report.get('release'),
rev=report.get('rev'),
proto_major=report.get('proto_major'),
proto_minor=report.get('proto_minor'),
)
if report.get('class').lower() == 'devices':
for device in report.get('devices', []):
if device.get(
'path'
) not in self._devices or device != self._devices.get('path'):
# noinspection DuplicatedCode
self._devices[device.get('path')] = device
return GPSDeviceEvent(
path=device.get('path'),
activated=device.get('activated'),
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':
# noinspection DuplicatedCode
self._devices[report.get('path')] = report
return GPSDeviceEvent(
path=report.get('path'),
activated=report.get('activated'),
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':
return GPSUpdateEvent(
device=report.get('device'),
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):
super().run()
self.logger.info(
'Initialized GPS backend on {}:{}'.format(self.gpsd_server, self.gpsd_port)
)
last_event = None
while not self.should_stop():
try:
session = self._get_session()
report = session.next()
event = self._gps_report_to_event(report)
if event and (
last_event is None
or abs(
(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)
last_event = event
except Exception as e:
if isinstance(e, StopIteration):
self.logger.warning(
'GPS service connection lost, check that gpsd is running'
)
else:
self.logger.exception(e)
self._session = None
time.sleep(self._fail_sleep_time)
# vim:sw=4:ts=4:et:

View File

@ -1,20 +0,0 @@
manifest:
events:
platypush.message.event.gps.GPSDeviceEvent: when a GPS device is connected or
updated
platypush.message.event.gps.GPSUpdateEvent: when a GPS device has new data
platypush.message.event.gps.GPSVersionEvent: when a GPS device advertises its
version data
install:
apk:
- gpsd
apt:
- gpsd
dnf:
- gpsd
pacman:
- gpsd
pip:
- gps
package: platypush.backend.gps
type: backend

View File

@ -65,8 +65,7 @@ class PubSubMixin:
with self._pubsub_lock:
# Close and free the pub/sub object if it has no active subscriptions.
if self._pubsub is not None and len(self._subscriptions) == 0:
self._pubsub.close()
self._pubsub = None
self._pubsub_close()
@staticmethod
def _serialize(data: MessageType) -> bytes:
@ -126,7 +125,7 @@ class PubSubMixin:
continue
yield Message(data=msg.get('data', b''), channel=channel)
except (AttributeError, RedisConnectionError):
except (AttributeError, ValueError, RedisConnectionError):
return
def _pubsub_close(self):

View File

@ -1,6 +1,7 @@
from abc import ABC, abstractmethod
import hashlib
import logging
import os
from typing import Generator, Optional
from platypush.message import JSONAble
@ -56,6 +57,9 @@ class MediaHandler(JSONAble, ABC):
logging.exception(e)
errors[hndl_class.__name__] = str(e)
if os.path.exists(source):
source = f'file://{source}'
raise AttributeError(
f'The source {source} has no handlers associated. Errors: {errors}'
)

Some files were not shown because too many files have changed in this diff Show More