forked from platypush/platypush
Merge branch 'master' into snyk-upgrade-f6fc128f6839afbdb22ffdb72a07ffb9
This commit is contained in:
commit
938b8761e7
717 changed files with 26686 additions and 13718 deletions
219
.drone.yml
219
.drone.yml
|
@ -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
2
.gitignore
vendored
|
@ -26,3 +26,5 @@ Session.vim
|
|||
/package.json
|
||||
/Dockerfile
|
||||
/docs/source/wiki
|
||||
/.skipci
|
||||
dump.rdb
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -6,44 +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.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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -39,6 +39,7 @@ Wiki
|
|||
wiki/Configuration
|
||||
wiki/Installing-extensions
|
||||
wiki/A-configuration-example
|
||||
wiki/The-Web-interface
|
||||
|
||||
Reference
|
||||
=========
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
``adafruit.io``
|
||||
=================================
|
||||
|
||||
.. automodule:: platypush.backend.adafruit.io
|
||||
:members:
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
``alarm``
|
||||
===========================
|
||||
|
||||
.. automodule:: platypush.backend.alarm
|
||||
:members:
|
|
@ -1,6 +0,0 @@
|
|||
``button.flic``
|
||||
=================================
|
||||
|
||||
.. automodule:: platypush.backend.button.flic
|
||||
:members:
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
``camera.pi``
|
||||
===============================
|
||||
|
||||
.. automodule:: platypush.backend.camera.pi
|
||||
:members:
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
``chat.telegram``
|
||||
===================================
|
||||
|
||||
.. automodule:: platypush.backend.chat.telegram
|
||||
:members:
|
|
@ -1,5 +0,0 @@
|
|||
``file.monitor``
|
||||
==================================
|
||||
|
||||
.. automodule:: platypush.backend.file.monitor
|
||||
:members:
|
|
@ -1,5 +0,0 @@
|
|||
``foursquare``
|
||||
================================
|
||||
|
||||
.. automodule:: platypush.backend.foursquare
|
||||
:members:
|
|
@ -1,5 +0,0 @@
|
|||
``github``
|
||||
============================
|
||||
|
||||
.. automodule:: platypush.backend.github
|
||||
:members:
|
|
@ -1,6 +0,0 @@
|
|||
``google.fit``
|
||||
================================
|
||||
|
||||
.. automodule:: platypush.backend.google.fit
|
||||
:members:
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
``google.pubsub``
|
||||
===================================
|
||||
|
||||
.. automodule:: platypush.backend.google.pubsub
|
||||
:members:
|
|
@ -1,6 +0,0 @@
|
|||
``gps``
|
||||
=========================
|
||||
|
||||
.. automodule:: platypush.backend.gps
|
||||
:members:
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
``joystick.jstest``
|
||||
=====================================
|
||||
|
||||
.. automodule:: platypush.backend.joystick.jstest
|
||||
:members:
|
|
@ -1,5 +0,0 @@
|
|||
``joystick.linux``
|
||||
====================================
|
||||
|
||||
.. automodule:: platypush.backend.joystick.linux
|
||||
:members:
|
|
@ -1,6 +0,0 @@
|
|||
``joystick``
|
||||
==============================
|
||||
|
||||
.. automodule:: platypush.backend.joystick
|
||||
:members:
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
``kafka``
|
||||
===========================
|
||||
|
||||
.. automodule:: platypush.backend.kafka
|
||||
:members:
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
``log.http``
|
||||
==============================
|
||||
|
||||
.. automodule:: platypush.backend.log.http
|
||||
:members:
|
|
@ -1,5 +0,0 @@
|
|||
``mail``
|
||||
==========================
|
||||
|
||||
.. automodule:: platypush.backend.mail
|
||||
:members:
|
|
@ -1,6 +0,0 @@
|
|||
``music.mpd``
|
||||
===============================
|
||||
|
||||
.. automodule:: platypush.backend.music.mpd
|
||||
:members:
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
``nextcloud``
|
||||
===============================
|
||||
|
||||
.. automodule:: platypush.backend.nextcloud
|
||||
:members:
|
|
@ -1,6 +0,0 @@
|
|||
``nfc``
|
||||
=========================
|
||||
|
||||
.. automodule:: platypush.backend.nfc
|
||||
:members:
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
``ping``
|
||||
==========================
|
||||
|
||||
.. automodule:: platypush.backend.ping
|
||||
:members:
|
|
@ -1,6 +0,0 @@
|
|||
``pushbullet``
|
||||
================================
|
||||
|
||||
.. automodule:: platypush.backend.pushbullet
|
||||
:members:
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
``scard``
|
||||
===========================
|
||||
|
||||
.. automodule:: platypush.backend.scard
|
||||
:members:
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
``sensor.ir.zeroborg``
|
||||
========================================
|
||||
|
||||
.. automodule:: platypush.backend.sensor.ir.zeroborg
|
||||
:members:
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
``sensor.leap``
|
||||
=================================
|
||||
|
||||
.. automodule:: platypush.backend.sensor.leap
|
||||
:members:
|
||||
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
``todoist``
|
||||
=============================
|
||||
|
||||
.. automodule:: platypush.backend.todoist
|
||||
:members:
|
|
@ -1,5 +0,0 @@
|
|||
``trello``
|
||||
============================
|
||||
|
||||
.. automodule:: platypush.backend.trello
|
||||
:members:
|
|
@ -1,5 +0,0 @@
|
|||
``weather.buienradar``
|
||||
========================================
|
||||
|
||||
.. automodule:: platypush.backend.weather.buienradar
|
||||
:members:
|
|
@ -1,5 +0,0 @@
|
|||
``weather.darksky``
|
||||
=====================================
|
||||
|
||||
.. automodule:: platypush.backend.weather.darksky
|
||||
:members:
|
|
@ -1,5 +0,0 @@
|
|||
``weather.openweathermap``
|
||||
============================================
|
||||
|
||||
.. automodule:: platypush.backend.weather.openweathermap
|
||||
:members:
|
|
@ -1,6 +0,0 @@
|
|||
``wiimote``
|
||||
=============================
|
||||
|
||||
.. automodule:: platypush.backend.wiimote
|
||||
:members:
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
``button.flic``
|
||||
=======================================
|
||||
|
||||
.. automodule:: platypush.message.event.button.flic
|
||||
:members:
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
``chat.telegram``
|
||||
=========================================
|
||||
|
||||
.. automodule:: platypush.message.event.chat.telegram
|
||||
:members:
|
5
docs/source/platypush/events/flic.rst
Normal file
5
docs/source/platypush/events/flic.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``flic``
|
||||
========
|
||||
|
||||
.. automodule:: platypush.message.event.flic
|
||||
:members:
|
5
docs/source/platypush/events/telegram.rst
Normal file
5
docs/source/platypush/events/telegram.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``telegram``
|
||||
============
|
||||
|
||||
.. automodule:: platypush.message.event.telegram
|
||||
:members:
|
|
@ -1,5 +0,0 @@
|
|||
``todoist``
|
||||
===================================
|
||||
|
||||
.. automodule:: platypush.message.event.todoist
|
||||
:members:
|
5
docs/source/platypush/plugins/camera.pi.legacy.rst
Normal file
5
docs/source/platypush/plugins/camera.pi.legacy.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``camera.pi.legacy``
|
||||
====================
|
||||
|
||||
.. automodule:: platypush.plugins.camera.pi.legacy
|
||||
:members:
|
|
@ -1,5 +0,0 @@
|
|||
``chat.irc``
|
||||
============
|
||||
|
||||
.. automodule:: platypush.plugins.chat.irc
|
||||
:members:
|
|
@ -1,5 +0,0 @@
|
|||
``chat.telegram``
|
||||
===================================
|
||||
|
||||
.. automodule:: platypush.plugins.chat.telegram
|
||||
:members:
|
5
docs/source/platypush/plugins/file.monitor.rst
Normal file
5
docs/source/platypush/plugins/file.monitor.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``file.monitor``
|
||||
================
|
||||
|
||||
.. automodule:: platypush.plugins.file.monitor
|
||||
:members:
|
5
docs/source/platypush/plugins/flic.rst
Normal file
5
docs/source/platypush/plugins/flic.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``flic``
|
||||
========
|
||||
|
||||
.. automodule:: platypush.plugins.flic
|
||||
:members:
|
5
docs/source/platypush/plugins/github.rst
Normal file
5
docs/source/platypush/plugins/github.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``github``
|
||||
==========
|
||||
|
||||
.. automodule:: platypush.plugins.github
|
||||
:members:
|
5
docs/source/platypush/plugins/gps.rst
Normal file
5
docs/source/platypush/plugins/gps.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``gps``
|
||||
=======
|
||||
|
||||
.. automodule:: platypush.plugins.gps
|
||||
:members:
|
|
@ -1,7 +0,0 @@
|
|||
``http.request``
|
||||
==================================
|
||||
|
||||
.. automodule:: platypush.plugins.http.request
|
||||
:members:
|
||||
|
||||
|
5
docs/source/platypush/plugins/http.rst
Normal file
5
docs/source/platypush/plugins/http.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``http``
|
||||
========
|
||||
|
||||
.. automodule:: platypush.plugins.http
|
||||
:members:
|
|
@ -1,5 +0,0 @@
|
|||
``inputs``
|
||||
============================
|
||||
|
||||
.. automodule:: platypush.plugins.inputs
|
||||
:members:
|
5
docs/source/platypush/plugins/irc.rst
Normal file
5
docs/source/platypush/plugins/irc.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``irc``
|
||||
=======
|
||||
|
||||
.. automodule:: platypush.plugins.irc
|
||||
:members:
|
5
docs/source/platypush/plugins/joystick.rst
Normal file
5
docs/source/platypush/plugins/joystick.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``joystick``
|
||||
============
|
||||
|
||||
.. automodule:: platypush.plugins.joystick
|
||||
:members:
|
5
docs/source/platypush/plugins/leap.rst
Normal file
5
docs/source/platypush/plugins/leap.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``leap``
|
||||
========
|
||||
|
||||
.. automodule:: platypush.plugins.leap
|
||||
:members:
|
5
docs/source/platypush/plugins/log.http.rst
Normal file
5
docs/source/platypush/plugins/log.http.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``log.http``
|
||||
============
|
||||
|
||||
.. automodule:: platypush.plugins.log.http
|
||||
:members:
|
|
@ -1,5 +0,0 @@
|
|||
``mail.imap``
|
||||
===============================
|
||||
|
||||
.. automodule:: platypush.plugins.mail.imap
|
||||
:members:
|
5
docs/source/platypush/plugins/mail.rst
Normal file
5
docs/source/platypush/plugins/mail.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``mail``
|
||||
========
|
||||
|
||||
.. automodule:: platypush.plugins.mail
|
||||
:members:
|
|
@ -1,5 +0,0 @@
|
|||
``mail.smtp``
|
||||
===============================
|
||||
|
||||
.. automodule:: platypush.plugins.mail.smtp
|
||||
:members:
|
5
docs/source/platypush/plugins/nfc.rst
Normal file
5
docs/source/platypush/plugins/nfc.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``nfc``
|
||||
=======
|
||||
|
||||
.. automodule:: platypush.plugins.nfc
|
||||
:members:
|
5
docs/source/platypush/plugins/telegram.rst
Normal file
5
docs/source/platypush/plugins/telegram.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``telegram``
|
||||
============
|
||||
|
||||
.. automodule:: platypush.plugins.telegram
|
||||
:members:
|
|
@ -1,5 +0,0 @@
|
|||
``weather.darksky``
|
||||
=====================================
|
||||
|
||||
.. automodule:: platypush.plugins.weather.darksky
|
||||
:members:
|
|
@ -1,6 +0,0 @@
|
|||
``wiimote``
|
||||
=============================
|
||||
|
||||
.. automodule:: platypush.plugins.wiimote
|
||||
:members:
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
``chat.telegram``
|
||||
============================================
|
||||
|
||||
.. automodule:: platypush.message.response.chat.telegram
|
||||
:members:
|
|
@ -1,5 +0,0 @@
|
|||
``ping``
|
||||
===================================
|
||||
|
||||
.. automodule:: platypush.message.response.ping
|
||||
:members:
|
|
@ -1,5 +0,0 @@
|
|||
``todoist``
|
||||
======================================
|
||||
|
||||
.. automodule:: platypush.message.response.todoist
|
||||
:members:
|
|
@ -1,5 +0,0 @@
|
|||
``trello``
|
||||
=====================================
|
||||
|
||||
.. automodule:: platypush.message.response.trello
|
||||
:members:
|
|
@ -1,5 +0,0 @@
|
|||
``weather.buienradar``
|
||||
=================================================
|
||||
|
||||
.. automodule:: platypush.message.response.weather.buienradar
|
||||
:members:
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
|
@ -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
|
|
@ -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:
|
|
@ -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
|
|
@ -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:
|
|
@ -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()
|
|
@ -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
|
|
@ -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:
|
|
@ -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:
|
|
@ -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
|
|
@ -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')
|
|
@ -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
|
|
@ -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:
|
|
@ -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
|
|
@ -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
|
|
@ -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:
|
|
@ -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
|
|
@ -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:
|
|
@ -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
|
|
@ -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:
|
|
@ -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
|
|
@ -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):
|
||||
|
|
|
@ -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}'
|
||||
)
|
||||
|
|
|
@ -1,152 +0,0 @@
|
|||
import logging
|
||||
import re
|
||||
from threading import Thread
|
||||
import time
|
||||
|
||||
import requests
|
||||
from frozendict import frozendict
|
||||
|
||||
from platypush.message.event.http import HttpEvent
|
||||
|
||||
|
||||
class HttpRequest:
|
||||
"""
|
||||
Backend used for polling HTTP resources.
|
||||
"""
|
||||
|
||||
poll_seconds = 60
|
||||
timeout = 5
|
||||
|
||||
class HttpRequestArguments:
|
||||
"""
|
||||
Models the properties of an HTTP request.
|
||||
"""
|
||||
|
||||
def __init__(self, url, *args, method='get', **kwargs):
|
||||
self.method = method.lower()
|
||||
self.url = url
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
|
||||
def __init__(
|
||||
self, args, bus=None, poll_seconds=None, timeout=None, skip_first_call=True, **_
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
self.poll_seconds = poll_seconds or self.poll_seconds
|
||||
self.timeout = timeout or self.timeout
|
||||
self.bus = bus
|
||||
self.skip_first_call = skip_first_call
|
||||
self.last_request_timestamp = 0
|
||||
self.logger = logging.getLogger('platypush')
|
||||
|
||||
if isinstance(args, self.HttpRequestArguments):
|
||||
self.args = args
|
||||
elif isinstance(args, dict):
|
||||
self.args = self.HttpRequestArguments(**args)
|
||||
else:
|
||||
raise RuntimeError('{} is neither a dictionary nor an HttpRequest')
|
||||
|
||||
if 'timeout' not in self.args.kwargs:
|
||||
self.args.kwargs['timeout'] = self.timeout
|
||||
|
||||
self.request_args = {
|
||||
'method': self.args.method,
|
||||
'url': self.args.url,
|
||||
**self.args.kwargs,
|
||||
}
|
||||
|
||||
def execute(self):
|
||||
def _thread_func():
|
||||
is_first_call = self.last_request_timestamp == 0
|
||||
self.last_request_timestamp = time.time()
|
||||
|
||||
try:
|
||||
method = getattr(requests, self.args.method.lower())
|
||||
response = method(self.args.url, *self.args.args, **self.args.kwargs)
|
||||
new_items = self.get_new_items(response)
|
||||
|
||||
if isinstance(new_items, HttpEvent):
|
||||
event = new_items
|
||||
new_items = event.args['response']
|
||||
else:
|
||||
event = HttpEvent(dict(self), new_items)
|
||||
|
||||
if (
|
||||
new_items
|
||||
and self.bus
|
||||
and (
|
||||
not self.skip_first_call
|
||||
or (self.skip_first_call and not is_first_call)
|
||||
)
|
||||
):
|
||||
self.bus.post(event)
|
||||
|
||||
response.raise_for_status()
|
||||
except Exception as e:
|
||||
self.logger.exception(e)
|
||||
self.logger.warning(
|
||||
'Encountered an error while retrieving %s: %s', self.args.url, e
|
||||
)
|
||||
|
||||
Thread(target=_thread_func, name='HttpPoll').start()
|
||||
|
||||
def get_new_items(self, response):
|
||||
"""Gets new items out of a response"""
|
||||
raise NotImplementedError(
|
||||
"get_new_items must be implemented in a derived class"
|
||||
)
|
||||
|
||||
def __iter__(self):
|
||||
"""
|
||||
:return: The ``request_args`` as key-value pairs.
|
||||
"""
|
||||
for key, value in self.request_args.items():
|
||||
yield key, value
|
||||
|
||||
|
||||
class JsonHttpRequest(HttpRequest):
|
||||
"""
|
||||
Specialization of the HttpRequest class for JSON requests.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, path=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.path = path
|
||||
self.seen_entries = set()
|
||||
|
||||
def get_new_items(self, response):
|
||||
response = response.json()
|
||||
new_entries = []
|
||||
|
||||
if self.path:
|
||||
m = re.match(r'\${\s*(.*)\s*}', self.path)
|
||||
if m:
|
||||
response = eval(m.group(1)) # pylint: disable=eval-used
|
||||
|
||||
for entry in response:
|
||||
flattened_entry = deep_freeze(entry)
|
||||
if flattened_entry not in self.seen_entries:
|
||||
new_entries.append(entry)
|
||||
self.seen_entries.add(flattened_entry)
|
||||
|
||||
return new_entries
|
||||
|
||||
|
||||
def deep_freeze(x):
|
||||
"""
|
||||
Deep freezes a Python object - works for strings, dictionaries, sets and
|
||||
iterables.
|
||||
"""
|
||||
|
||||
if isinstance(x, str) or not hasattr(x, "__len__"):
|
||||
return x
|
||||
if hasattr(x, "keys") and hasattr(x, "values"):
|
||||
return frozendict({deep_freeze(k): deep_freeze(v) for k, v in x.items()})
|
||||
if hasattr(x, "__getitem__"):
|
||||
return tuple(map(deep_freeze, x))
|
||||
|
||||
return frozenset(map(deep_freeze, x))
|
||||
|
||||
|
||||
# vim:sw=4:ts=4:et:
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue