Compare commits
91 Commits
Author | SHA1 | Date |
---|---|---|
Fabio Manganiello | ed697c0ad2 | |
Fabio Manganiello | 22cfe777fa | |
Fabio Manganiello | 3f2832a077 | |
Fabio Manganiello | 6f8eb397d2 | |
Fabio Manganiello | 3163721bf3 | |
Fabio Manganiello | d79b8a1de5 | |
Fabio Manganiello | 3afc6b2271 | |
Fabio Manganiello | 17b6b02986 | |
Fabio Manganiello | 87a902bfa3 | |
Fabio Manganiello | 421feffd3e | |
Fabio Manganiello | 518dc146d6 | |
Fabio Manganiello | 6b11db7afb | |
Fabio Manganiello | 8814859abc | |
Fabio Manganiello | 0ccd029ff1 | |
Fabio Manganiello | e52f5e06f4 | |
snyk-bot | 066d71faa3 | |
Fabio Manganiello | 4f19b45975 | |
Fabio Manganiello | 7d6ffc76fb | |
Fabio Manganiello | 7a8f30e5e0 | |
Fabio Manganiello | 3528b3646f | |
Fabio Manganiello | 9cca928d4b | |
Fabio Manganiello | f356fcd844 | |
Fabio Manganiello | fcae7aa3ad | |
Fabio Manganiello | c7d640a1d2 | |
Fabio Manganiello | 1cc2aaf5a4 | |
Fabio Manganiello | 2acf6ef3e9 | |
Fabio Manganiello | 1107e526f7 | |
Fabio Manganiello | 5fc9c1199b | |
Fabio Manganiello | b067430cd5 | |
Fabio Manganiello | ff60896625 | |
Fabio Manganiello | 67b6e3a608 | |
Fabio Manganiello | c61a1b89d6 | |
Fabio Manganiello | c9a5c29a4a | |
Fabio Manganiello | 1067ab04d9 | |
Fabio Manganiello | 709b90fa4b | |
Fabio Manganiello | 06f0ac4545 | |
Fabio Manganiello | 944fd45f9f | |
Fabio Manganiello | 6acdde6164 | |
Fabio Manganiello | 3583dafbc3 | |
Fabio Manganiello | 4513bb9569 | |
Fabio Manganiello | 4e82dd17bb | |
Fabio Manganiello | e982c02524 | |
Fabio Manganiello | d9a5ea1e53 | |
Fabio Manganiello | 23e02de1d7 | |
Fabio Manganiello | 4d0b63a155 | |
Fabio Manganiello | ce1525e786 | |
Fabio Manganiello | 67478e7ca1 | |
Fabio Manganiello | c18768e61f | |
Fabio Manganiello | 30362b89e3 | |
Fabio Manganiello | 826a3fa55c | |
Fabio Manganiello | 3986549326 | |
Fabio Manganiello | fa318882a5 | |
Fabio Manganiello | d6185ddb1e | |
snyk-bot | fee12951d6 | |
Fabio Manganiello | d2caa989ac | |
Fabio Manganiello | fa3c804b71 | |
Fabio Manganiello | 4cd0761e78 | |
Fabio Manganiello | 16f7b7e12a | |
Fabio Manganiello | 22222fab65 | |
Fabio Manganiello | 5b3c0ad1cf | |
Fabio Manganiello | 3758a8d759 | |
Fabio Manganiello | de2bbc53c6 | |
Fabio Manganiello | a4a776986b | |
Fabio Manganiello | 9fef73a746 | |
Fabio Manganiello | 0f6f119089 | |
Fabio Manganiello | c64ff40dd3 | |
Fabio Manganiello | 5c0f85c311 | |
Fabio Manganiello | e6702398dc | |
Fabio Manganiello | 983bcc240a | |
Fabio Manganiello | d6d9d7a8e7 | |
Fabio Manganiello | 8d26721040 | |
Fabio Manganiello | 96f265a4a2 | |
Fabio Manganiello | 0ffff854d3 | |
Fabio Manganiello | 020804fd1c | |
Fabio Manganiello | 42174b31bc | |
Fabio Manganiello | 3642d1ffa4 | |
Fabio Manganiello | 104457a302 | |
Fabio Manganiello | 0445087699 | |
Fabio Manganiello | 751d719b04 | |
Fabio Manganiello | bef027fc07 | |
Fabio Manganiello | cc670f9d4a | |
Fabio Manganiello | 86674ddc28 | |
Fabio Manganiello | ee3933dc77 | |
Fabio Manganiello | e23664b5e7 | |
Fabio Manganiello | 0537815721 | |
Fabio Manganiello | a2ec20bb3a | |
Fabio Manganiello | d3562f4d20 | |
Fabio Manganiello | bf5aece08b | |
Fabio Manganiello | 2f20580498 | |
snyk-bot | 46da373637 | |
snyk-bot | 092f5b607c |
57
.drone.yml
57
.drone.yml
|
@ -29,29 +29,6 @@ steps:
|
|||
commands:
|
||||
- . .drone/github-mirror.sh
|
||||
|
||||
###
|
||||
### Synchronize the `stable` branch to the new release
|
||||
###
|
||||
|
||||
- name: sync-stable-branch
|
||||
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
|
||||
|
||||
commands:
|
||||
- . .drone/sync-stable-branch.sh
|
||||
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
###
|
||||
### Rebuild the docs
|
||||
###
|
||||
|
@ -69,9 +46,6 @@ steps:
|
|||
event:
|
||||
- tag
|
||||
|
||||
depends_on:
|
||||
- sync-stable-branch
|
||||
|
||||
###
|
||||
### Run the tests
|
||||
###
|
||||
|
@ -220,6 +194,34 @@ steps:
|
|||
commands:
|
||||
- . .drone/update-deb-packages.sh
|
||||
|
||||
###
|
||||
### Update the Ubuntu (latest) packages
|
||||
###
|
||||
|
||||
- name: update-ubuntu-packages
|
||||
image: ubuntu:latest
|
||||
volumes:
|
||||
- name: repos
|
||||
path: /repos
|
||||
|
||||
environment:
|
||||
DEB_VERSION: ubuntu
|
||||
WORKDIR: /tmp/workdir
|
||||
APT_ROOT: /repos/apt
|
||||
PKG_NAME: platypush
|
||||
|
||||
when:
|
||||
branch:
|
||||
- master
|
||||
event:
|
||||
- push
|
||||
|
||||
depends_on:
|
||||
- update-components-cache
|
||||
|
||||
commands:
|
||||
- . .drone/update-deb-packages.sh
|
||||
|
||||
###
|
||||
### Updates the APT repository after new packages have been pushed
|
||||
###
|
||||
|
@ -247,6 +249,7 @@ steps:
|
|||
depends_on:
|
||||
- update-debian-stable-packages
|
||||
- update-debian-oldstable-packages
|
||||
- update-ubuntu-packages
|
||||
|
||||
commands:
|
||||
- . .drone/update-apt-repo.sh
|
||||
|
@ -289,7 +292,7 @@ steps:
|
|||
###
|
||||
|
||||
- name: update-pip-package
|
||||
image: python:3.11-alpine
|
||||
image: alpine
|
||||
environment:
|
||||
TWINE_USERNAME:
|
||||
from_secret: pypi_user
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
#!/bin/sh
|
||||
|
||||
export SRCDIR="$PWD"
|
||||
export WEBAPP_DIR="$SRCDIR/platypush/backend/http/webapp"
|
||||
export SKIPCI="$PWD/.skipci"
|
||||
rm -rf "$SKIPCI"
|
||||
|
||||
. .drone/macros/configure-git.sh
|
||||
|
||||
cd platypush/backend/http/webapp
|
||||
cd "$WEBAPP_DIR"
|
||||
if [ $(git log --pretty=oneline $DRONE_COMMIT_AFTER...$DRONE_COMMIT_BEFORE . | wc -l) -eq 0 ]; then
|
||||
echo "No UI changes detected, skipping build"
|
||||
exit 0
|
||||
|
@ -29,12 +31,13 @@ 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"
|
||||
cd "$SRCDIR"
|
||||
|
||||
. .drone/macros/configure-ssh.sh
|
||||
. .drone/macros/configure-gpg.sh
|
||||
|
||||
git add dist
|
||||
git commit dist -S -m "[Automatic] Updated UI files" --no-verify
|
||||
git add "${WEBAPP_DIR}/dist"
|
||||
git commit "${WEBAPP_DIR}/dist" -S -m "[Automatic] Updated UI files" --no-verify
|
||||
git remote rm origin
|
||||
git remote add origin git@git.platypush.tech:platypush/platypush.git
|
||||
git push -f origin master
|
||||
|
|
|
@ -14,7 +14,6 @@ git clone 'https://git.platypush.tech/platypush/platypush.wiki.git' wiki
|
|||
|
||||
echo "Linking the wiki to the Sphinx index"
|
||||
cd wiki
|
||||
ln -s Home.md index.md
|
||||
cd "$APPDIR/docs"
|
||||
make html
|
||||
rm -f config*.yaml
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
#!/bin/sh
|
||||
|
||||
. .drone/macros/configure-git.sh
|
||||
. .drone/macros/configure-ssh.sh
|
||||
. .drone/macros/configure-gpg.sh
|
||||
|
||||
# Git configuration
|
||||
git remote rm origin
|
||||
git remote add origin git@git.platypush.tech:platypush/platypush.git
|
||||
|
||||
# Merge and push to the `stable` branch
|
||||
git checkout stable
|
||||
git rebase master
|
||||
git push -u origin stable
|
||||
git checkout master
|
||||
|
||||
# Restore the original git configuration
|
||||
mv "$TMP_GIT_CONF" "$GIT_CONF"
|
|
@ -1,6 +1,5 @@
|
|||
#!/bin/sh
|
||||
|
||||
|
||||
[ -f .skipci ] && exit 0
|
||||
|
||||
echo "-- Installing dependencies"
|
||||
|
@ -21,7 +20,7 @@ done
|
|||
|
||||
echo "-- Updating Packages files"
|
||||
|
||||
echo "stable\noldstable" | while read distro; do
|
||||
echo "stable\noldstable\nubuntu" | while read distro; do
|
||||
echo "main\ndev" | while read branch; do
|
||||
branch_dir="$TMP_APT_ROOT/pool/$distro/$branch"
|
||||
echo "Checking pool folder: $branch_dir"
|
||||
|
@ -58,7 +57,7 @@ add_hashes() {
|
|||
done
|
||||
}
|
||||
|
||||
echo "stable\noldstable" | while read distro; do
|
||||
echo "stable\noldstable\nubuntu" | while read distro; do
|
||||
dist_dir="$TMP_APT_ROOT/dists/$distro"
|
||||
components=$(find "$dist_dir" -name Packages | awk -F '/' '{print $(NF-2)}' | uniq | tr '\n' ' ')
|
||||
release_file="$dist_dir/Release"
|
||||
|
@ -82,7 +81,7 @@ done
|
|||
echo "-- Generating list files"
|
||||
mkdir -p "$TMP_APT_ROOT/lists"
|
||||
|
||||
for distro in stable oldstable; do
|
||||
for distro in stable oldstable ubuntu; do
|
||||
for branch in main dev; do
|
||||
echo "deb https://apt.platypush.tech/ $distro $branch" > "$TMP_APT_ROOT/lists/platypush-$distro-$branch.list"
|
||||
done
|
||||
|
@ -105,39 +104,43 @@ Steps:
|
|||
1. Add this repository's PGP key to your apt keyring
|
||||
====================================================
|
||||
|
||||
$ sudo wget -q -O \\\
|
||||
# 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 \\\
|
||||
# wget -q -O \\\
|
||||
/etc/apt/sources.list.d/platypush.list \\\
|
||||
https://apt.platypush.tech/lists/platypush-<deb_version>-<branch>.list
|
||||
|
||||
Where:
|
||||
|
||||
- deb_version can be either *stable* (for the current Debian stable version) or
|
||||
*oldstable* (for the previous Debian stable version)
|
||||
- branch can be either *main* (for the latest releases) or *dev* (for a package
|
||||
that is always in sync with the git version)
|
||||
- deb_version can be:
|
||||
- *stable* - current Debian stable version
|
||||
- *oldstable* - previous Debian stable version
|
||||
- *ubuntu* - latest Ubuntu version
|
||||
|
||||
- branch can be either:
|
||||
- *main* - latest stable release
|
||||
- *dev* a package always in sync with the git version
|
||||
|
||||
For example, to install the latest stable tags on Debian stable:
|
||||
|
||||
$ sudo wget -q -O \\\
|
||||
# wget -q -O \\\
|
||||
/etc/apt/sources.list.d/platypush.list \\\
|
||||
https://apt.platypush.tech/lists/platypush-stable-main.list
|
||||
|
||||
3. Update your repos
|
||||
====================
|
||||
|
||||
$ sudo apt update
|
||||
# apt update
|
||||
|
||||
4. Install Platypush
|
||||
====================
|
||||
|
||||
$ sudo apt install platypush
|
||||
# apt install platypush
|
||||
EOF
|
||||
|
||||
echo "-- Importing and refreshing PGP key"
|
||||
|
|
|
@ -9,6 +9,7 @@ cp -r "$PWD" "$SRCDIR"
|
|||
cd "$SRCDIR"
|
||||
|
||||
echo "-- Installing dependencies"
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt update
|
||||
apt install -y curl dpkg-dev gpg git python3 python3-pip python3-setuptools
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/sh
|
||||
|
||||
apk add --update --no-cache py3-twine
|
||||
apk add --update --no-cache py3-twine py3-setuptools py3-wheel py3-pip
|
||||
python setup.py sdist bdist_wheel
|
||||
twine upload dist/platypush-$(python setup.py --version).tar.gz
|
||||
|
|
|
@ -235,16 +235,16 @@ Steps:
|
|||
1. Add the repository to your sources
|
||||
=====================================
|
||||
|
||||
$ sudo yum config-manager --add-repo https://rpm.platypush.tech/platypush.repo
|
||||
# yum config-manager --add-repo https://rpm.platypush.tech/platypush.repo
|
||||
|
||||
2. Install Platypush
|
||||
====================
|
||||
|
||||
$ sudo yum install platypush
|
||||
# yum install platypush
|
||||
|
||||
Or, if you want to install a version always up-to-date with the git repo:
|
||||
|
||||
$ sudo yum install platypush-git
|
||||
# yum install platypush-git
|
||||
EOF
|
||||
|
||||
cat <<EOF > "$TMP_RPM_ROOT/pubkey.txt"
|
||||
|
|
58
CHANGELOG.md
58
CHANGELOG.md
|
@ -1,6 +1,62 @@
|
|||
# Changelog
|
||||
|
||||
## [1.0.0 release candidate]
|
||||
## [Unreleased]
|
||||
|
||||
- [[#281](https://git.platypush.tech/platypush/platypush/issues/281)]
|
||||
replaced `warnings.warn` with `logging.warning`, as there is no easy and
|
||||
reliable way of routing `warnings.warn` to `logging`.
|
||||
|
||||
## [1.1.0] - 2024-06-06
|
||||
|
||||
- [[#405](https://git.platypush.tech/platypush/platypush/issues/405)] Fixed
|
||||
timezone/timestamp rendering issues for `calendar.ical` events.
|
||||
- [[#403]((https://git.platypush.tech/platypush/platypush/issues/403))]
|
||||
Included inherited actions in plugins docs.
|
||||
|
||||
## [1.0.7] - 2024-06-02
|
||||
|
||||
- [[#384]((https://git.platypush.tech/platypush/platypush/issues/384))] Added
|
||||
`assistant.openai` and `tts.openai` plugins.
|
||||
|
||||
## [1.0.6] - 2024-06-01
|
||||
|
||||
- 🐛 Bug fix on one of the entities modules that prevented the application from
|
||||
loading when `.` is part of `PYTHONPATH`.
|
||||
|
||||
## [1.0.5] - 2024-06-01
|
||||
|
||||
- A proper solution for the `utcnow()` issue.
|
||||
|
||||
It was a bit trickier than expected to solve, but now Platypush uses a
|
||||
`utcnow()` facade that always returns a UTC datetime in a timezone-aware
|
||||
representation.
|
||||
|
||||
The code should however also handle the case of timestamps stored on the db in
|
||||
the old format.
|
||||
|
||||
## [1.0.4] - 2024-05-31
|
||||
|
||||
- Fixed regression introduced by
|
||||
[c18768e61fef62924f4c1fac3089ecfb83666dab](https://git.platypush.tech/platypush/platypush/commit/c18768e61fef62924f4c1fac3089ecfb83666dab).
|
||||
Python seems to have introduced a breaking change from the version 3.12 -
|
||||
`datetime.utcnow()` is not deprecated, but `datetime.UTC`, the suggested
|
||||
alternative, isn't available on older versions of Python. Added a workaround
|
||||
that makes Platypush compatible with both the implementations.
|
||||
|
||||
## [1.0.3] - 2024-05-31
|
||||
|
||||
- [[#368](https://git.platypush.tech/platypush/platypush/issues/368)] Added
|
||||
Ubuntu packages.
|
||||
|
||||
- Fixed bug that didn't get hooks to match events imported through the new
|
||||
`platypush.events` symlinked module.
|
||||
|
||||
## [1.0.2] - 2024-05-26
|
||||
|
||||
- Fixed regression introduced by the support of custom names through the
|
||||
`@procedure` decorator.
|
||||
|
||||
## [1.0.0] - 2024-05-26
|
||||
|
||||
Many, many changes for the first major release of Platypush after so many
|
||||
years.
|
||||
|
|
64
README.md
64
README.md
|
@ -795,51 +795,49 @@ or tags.
|
|||
|
||||
#### Debian/Ubuntu
|
||||
|
||||
Currently the following releases are supported:
|
||||
1. Add the Platypush APT key to your trusted keyring:
|
||||
|
||||
1. The current Debian `stable`
|
||||
2. Debian `oldstable`
|
||||
```
|
||||
# wget -q -O \
|
||||
/etc/apt/trusted.gpg.d/platypush.asc \
|
||||
https://apt.platypush.tech/pubkey.txt
|
||||
```
|
||||
|
||||
Ubuntu supported [to be added
|
||||
soon](https://git.platypush.tech/platypush/platypush/issues/368).
|
||||
2. Add the Platypush repository to your APT sources:
|
||||
|
||||
- Add the Platypush APT key to your trusted keyring:
|
||||
```
|
||||
# wget -q -O \
|
||||
/etc/apt/sources.list.d/platypush.list \
|
||||
https://apt.platypush.tech/lists/platypush-<deb_version>-<branch>.list
|
||||
```
|
||||
|
||||
```
|
||||
# wget -q -O \
|
||||
/etc/apt/trusted.gpg.d/platypush.asc \
|
||||
https://apt.platypush.tech/pubkey.txt
|
||||
```
|
||||
Where:
|
||||
|
||||
- Add the Platypush repository to your APT sources:
|
||||
- `deb_version` can be either:
|
||||
|
||||
```
|
||||
# wget -q -O \
|
||||
/etc/apt/sources.list.d/platypush.list \
|
||||
https://apt.platypush.tech/lists/platypush-<deb_version>-<branch>.list
|
||||
```
|
||||
- `stable`: current Debian stable
|
||||
- `oldstable`: previous Debian stable
|
||||
- `ubuntu`: latest Ubuntu release
|
||||
|
||||
Where:
|
||||
- `branch` can be either:
|
||||
|
||||
- `deb_version` can be either *stable* (for the current Debian stable version) or
|
||||
*oldstable* (for the previous Debian stable version)
|
||||
- `branch` can be either *main* (for the latest releases) or *dev* (for a package
|
||||
that is always in sync with the git version)
|
||||
- `main`: latest stable release
|
||||
- `dev`: a package always in sync with the latest git version
|
||||
|
||||
For example, to install the latest stable tags on Debian stable:
|
||||
For example, to install the latest stable tags on Debian stable:
|
||||
|
||||
```
|
||||
# wget -q -O \
|
||||
/etc/apt/sources.list.d/platypush.list \
|
||||
https://apt.platypush.tech/lists/platypush-stable-main.list
|
||||
```
|
||||
```
|
||||
# wget -q -O \
|
||||
/etc/apt/sources.list.d/platypush.list \
|
||||
https://apt.platypush.tech/lists/platypush-stable-main.list
|
||||
```
|
||||
|
||||
- Update your repos and install Platypush:
|
||||
3. Update your repos and install Platypush:
|
||||
|
||||
```
|
||||
# apt update
|
||||
# apt install platypush
|
||||
```
|
||||
```
|
||||
# apt update
|
||||
# apt install platypush
|
||||
```
|
||||
|
||||
#### Fedora
|
||||
|
||||
|
|
|
@ -152,7 +152,7 @@ const generateComponentsGrid = () => {
|
|||
return
|
||||
}
|
||||
|
||||
if (window.location.pathname.endsWith('/index.html')) {
|
||||
if (window.location.pathname === '/' || window.location.pathname.endsWith('/index.html')) {
|
||||
if (tocWrappers.length < 2) {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ sys.path.insert(0, os.path.abspath("./_ext"))
|
|||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = 'Platypush'
|
||||
copyright = '2017-2023, Fabio Manganiello'
|
||||
copyright = '2017-2024, Fabio Manganiello'
|
||||
author = 'Fabio Manganiello <fabio@manganiello.tech>'
|
||||
|
||||
# The short X.Y version
|
||||
|
@ -199,6 +199,7 @@ intersphinx_mapping = {'python': ('https://docs.python.org/3', None)}
|
|||
autodoc_default_options = {
|
||||
'members': True,
|
||||
'show-inheritance': True,
|
||||
'inherited-members': True,
|
||||
}
|
||||
|
||||
sys.path.insert(0, os.path.abspath('../..'))
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
``assistant.openai``
|
||||
====================
|
||||
|
||||
.. automodule:: platypush.plugins.assistant.openai
|
||||
:members:
|
|
@ -0,0 +1,5 @@
|
|||
``tts.openai``
|
||||
==============
|
||||
|
||||
.. automodule:: platypush.plugins.tts.openai
|
||||
:members:
|
|
@ -11,6 +11,7 @@ Plugins
|
|||
platypush/plugins/application.rst
|
||||
platypush/plugins/arduino.rst
|
||||
platypush/plugins/assistant.google.rst
|
||||
platypush/plugins/assistant.openai.rst
|
||||
platypush/plugins/assistant.picovoice.rst
|
||||
platypush/plugins/autoremote.rst
|
||||
platypush/plugins/bluetooth.rst
|
||||
|
@ -134,6 +135,7 @@ Plugins
|
|||
platypush/plugins/tts.rst
|
||||
platypush/plugins/tts.google.rst
|
||||
platypush/plugins/tts.mimic3.rst
|
||||
platypush/plugins/tts.openai.rst
|
||||
platypush/plugins/tts.picovoice.rst
|
||||
platypush/plugins/tv.samsung.ws.rst
|
||||
platypush/plugins/twilio.rst
|
||||
|
|
|
@ -23,7 +23,7 @@ when = hook
|
|||
|
||||
|
||||
__author__ = 'Fabio Manganiello <fabio@manganiello.tech>'
|
||||
__version__ = '0.99.0'
|
||||
__version__ = '1.1.0'
|
||||
__all__ = [
|
||||
'Application',
|
||||
'Variable',
|
||||
|
|
|
@ -6,6 +6,7 @@ from flask import Blueprint, request, redirect, render_template, make_response
|
|||
from platypush.backend.http.app import template_folder
|
||||
from platypush.backend.http.utils import HttpUtils
|
||||
from platypush.user import UserManager
|
||||
from platypush.utils import utcnow
|
||||
|
||||
login = Blueprint('login', __name__, template_folder=template_folder)
|
||||
|
||||
|
@ -17,7 +18,7 @@ __routes__ = [
|
|||
|
||||
@login.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
""" Login page """
|
||||
"""Login page"""
|
||||
user_manager = UserManager()
|
||||
session_token = request.cookies.get('session_token')
|
||||
|
||||
|
@ -37,11 +38,11 @@ def login():
|
|||
username = request.form.get('username')
|
||||
password = request.form.get('password')
|
||||
remember = request.form.get('remember')
|
||||
expires = datetime.datetime.utcnow() + datetime.timedelta(days=365) \
|
||||
if remember else None
|
||||
expires = utcnow() + datetime.timedelta(days=365) if remember else None
|
||||
|
||||
session = user_manager.create_user_session(username=username, password=password,
|
||||
expires_at=expires)
|
||||
session = user_manager.create_user_session(
|
||||
username=username, password=password, expires_at=expires
|
||||
)
|
||||
|
||||
if session:
|
||||
redirect_target = redirect(redirect_page, 302) # lgtm [py/url-redirection]
|
||||
|
|
|
@ -6,6 +6,7 @@ from flask import Blueprint, request, redirect, render_template, make_response,
|
|||
from platypush.backend.http.app import template_folder
|
||||
from platypush.backend.http.utils import HttpUtils
|
||||
from platypush.user import UserManager
|
||||
from platypush.utils import utcnow
|
||||
|
||||
register = Blueprint('register', __name__, template_folder=template_folder)
|
||||
|
||||
|
@ -17,7 +18,7 @@ __routes__ = [
|
|||
|
||||
@register.route('/register', methods=['GET', 'POST'])
|
||||
def register():
|
||||
""" Registration page """
|
||||
"""Registration page"""
|
||||
user_manager = UserManager()
|
||||
redirect_page = request.args.get('redirect')
|
||||
if not redirect_page:
|
||||
|
@ -34,7 +35,9 @@ def register():
|
|||
return redirect(redirect_page, 302) # lgtm [py/url-redirection]
|
||||
|
||||
if user_manager.get_user_count() > 0:
|
||||
return redirect('/login?redirect=' + redirect_page, 302) # lgtm [py/url-redirection]
|
||||
return redirect(
|
||||
'/login?redirect=' + redirect_page, 302
|
||||
) # lgtm [py/url-redirection]
|
||||
|
||||
if request.form:
|
||||
username = request.form.get('username')
|
||||
|
@ -44,12 +47,18 @@ def register():
|
|||
|
||||
if password == confirm_password:
|
||||
user_manager.create_user(username=username, password=password)
|
||||
session = user_manager.create_user_session(username=username, password=password,
|
||||
expires_at=datetime.datetime.utcnow() + datetime.timedelta(days=1)
|
||||
if not remember else None)
|
||||
session = user_manager.create_user_session(
|
||||
username=username,
|
||||
password=password,
|
||||
expires_at=(
|
||||
utcnow() + datetime.timedelta(days=1) if not remember else None
|
||||
),
|
||||
)
|
||||
|
||||
if session:
|
||||
redirect_target = redirect(redirect_page, 302) # lgtm [py/url-redirection]
|
||||
redirect_target = redirect(
|
||||
redirect_page, 302
|
||||
) # lgtm [py/url-redirection]
|
||||
response = make_response(redirect_target)
|
||||
response.set_cookie('session_token', session.session_token)
|
||||
return response
|
||||
|
|
|
@ -10,15 +10,15 @@
|
|||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.5.2",
|
||||
"axios": "^1.6.8",
|
||||
"core-js": "^3.37.0",
|
||||
"cronstrue": "^2.49.0",
|
||||
"core-js": "^3.37.1",
|
||||
"cronstrue": "^2.50.0",
|
||||
"highlight.js": "^11.9.0",
|
||||
"lato-font": "^3.0.0",
|
||||
"mitt": "^2.1.0",
|
||||
"register-service-worker": "^1.7.2",
|
||||
"sass": "^1.75.0",
|
||||
"sass": "^1.76.0",
|
||||
"sass-loader": "^10.5.2",
|
||||
"vue": "^3.4.23",
|
||||
"vue": "^3.4.24",
|
||||
"vue-router": "^4.3.2",
|
||||
"vue-skycons": "^4.3.4",
|
||||
"w3css": "^2.7.0"
|
||||
|
@ -3080,12 +3080,12 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/@vue/compiler-core": {
|
||||
"version": "3.4.23",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.23.tgz",
|
||||
"integrity": "sha512-HAFmuVEwNqNdmk+w4VCQ2pkLk1Vw4XYiiyxEp3z/xvl14aLTUBw2OfVH3vBcx+FtGsynQLkkhK410Nah1N2yyQ==",
|
||||
"version": "3.4.24",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.24.tgz",
|
||||
"integrity": "sha512-vbW/tgbwJYj62N/Ww99x0zhFTkZDTcGh3uwJEuadZ/nF9/xuFMC4693P9r+3sxGXISABpDKvffY5ApH9pmdd1A==",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.24.1",
|
||||
"@vue/shared": "3.4.23",
|
||||
"@babel/parser": "^7.24.4",
|
||||
"@vue/shared": "3.4.24",
|
||||
"entities": "^4.5.0",
|
||||
"estree-walker": "^2.0.2",
|
||||
"source-map-js": "^1.2.0"
|
||||
|
@ -3103,37 +3103,37 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-dom": {
|
||||
"version": "3.4.23",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.23.tgz",
|
||||
"integrity": "sha512-t0b9WSTnCRrzsBGrDd1LNR5HGzYTr7LX3z6nNBG+KGvZLqrT0mY6NsMzOqlVMBKKXKVuusbbB5aOOFgTY+senw==",
|
||||
"version": "3.4.24",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.24.tgz",
|
||||
"integrity": "sha512-4XgABML/4cNndVsQndG6BbGN7+EoisDwi3oXNovqL/4jdNhwvP8/rfRMTb6FxkxIxUUtg6AI1/qZvwfSjxJiWA==",
|
||||
"dependencies": {
|
||||
"@vue/compiler-core": "3.4.23",
|
||||
"@vue/shared": "3.4.23"
|
||||
"@vue/compiler-core": "3.4.24",
|
||||
"@vue/shared": "3.4.24"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-sfc": {
|
||||
"version": "3.4.23",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.23.tgz",
|
||||
"integrity": "sha512-fSDTKTfzaRX1kNAUiaj8JB4AokikzStWgHooMhaxyjZerw624L+IAP/fvI4ZwMpwIh8f08PVzEnu4rg8/Npssw==",
|
||||
"version": "3.4.24",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.24.tgz",
|
||||
"integrity": "sha512-nRAlJUK02FTWfA2nuvNBAqsDZuERGFgxZ8sGH62XgFSvMxO2URblzulExsmj4gFZ8e+VAyDooU9oAoXfEDNxTA==",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.24.1",
|
||||
"@vue/compiler-core": "3.4.23",
|
||||
"@vue/compiler-dom": "3.4.23",
|
||||
"@vue/compiler-ssr": "3.4.23",
|
||||
"@vue/shared": "3.4.23",
|
||||
"@babel/parser": "^7.24.4",
|
||||
"@vue/compiler-core": "3.4.24",
|
||||
"@vue/compiler-dom": "3.4.24",
|
||||
"@vue/compiler-ssr": "3.4.24",
|
||||
"@vue/shared": "3.4.24",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.30.8",
|
||||
"magic-string": "^0.30.10",
|
||||
"postcss": "^8.4.38",
|
||||
"source-map-js": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-ssr": {
|
||||
"version": "3.4.23",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.23.tgz",
|
||||
"integrity": "sha512-hb6Uj2cYs+tfqz71Wj6h3E5t6OKvb4MVcM2Nl5i/z1nv1gjEhw+zYaNOV+Xwn+SSN/VZM0DgANw5TuJfxfezPg==",
|
||||
"version": "3.4.24",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.24.tgz",
|
||||
"integrity": "sha512-ZsAtr4fhaUFnVcDqwW3bYCSDwq+9Gk69q2r/7dAHDrOMw41kylaMgOP4zRnn6GIEJkQznKgrMOGPMFnLB52RbQ==",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.4.23",
|
||||
"@vue/shared": "3.4.23"
|
||||
"@vue/compiler-dom": "3.4.24",
|
||||
"@vue/shared": "3.4.24"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/component-compiler-utils": {
|
||||
|
@ -3206,48 +3206,48 @@
|
|||
"integrity": "sha512-LgPscpE3Vs0x96PzSSB4IGVSZXZBZHpfxs+ZA1d+VEPwHdOXowy/Y2CsvCAIFrf+ssVU1pD1jidj505EpUnfbA=="
|
||||
},
|
||||
"node_modules/@vue/reactivity": {
|
||||
"version": "3.4.23",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.23.tgz",
|
||||
"integrity": "sha512-GlXR9PL+23fQ3IqnbSQ8OQKLodjqCyoCrmdLKZk3BP7jN6prWheAfU7a3mrltewTkoBm+N7qMEb372VHIkQRMQ==",
|
||||
"version": "3.4.24",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.24.tgz",
|
||||
"integrity": "sha512-nup3fSYg4i4LtNvu9slF/HF/0dkMQYfepUdORBcMSsankzRPzE7ypAFurpwyRBfU1i7Dn1kcwpYsE1wETSh91g==",
|
||||
"dependencies": {
|
||||
"@vue/shared": "3.4.23"
|
||||
"@vue/shared": "3.4.24"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-core": {
|
||||
"version": "3.4.23",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.23.tgz",
|
||||
"integrity": "sha512-FeQ9MZEXoFzFkFiw9MQQ/FWs3srvrP+SjDKSeRIiQHIhtkzoj0X4rWQlRNHbGuSwLra6pMyjAttwixNMjc/xLw==",
|
||||
"version": "3.4.24",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.24.tgz",
|
||||
"integrity": "sha512-c7iMfj6cJMeAG3s5yOn9Rc5D9e2/wIuaozmGf/ICGCY3KV5H7mbTVdvEkd4ZshTq7RUZqj2k7LMJWVx+EBiY1g==",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.4.23",
|
||||
"@vue/shared": "3.4.23"
|
||||
"@vue/reactivity": "3.4.24",
|
||||
"@vue/shared": "3.4.24"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-dom": {
|
||||
"version": "3.4.23",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.23.tgz",
|
||||
"integrity": "sha512-RXJFwwykZWBkMiTPSLEWU3kgVLNAfActBfWFlZd0y79FTUxexogd0PLG4HH2LfOktjRxV47Nulygh0JFXe5f9A==",
|
||||
"version": "3.4.24",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.24.tgz",
|
||||
"integrity": "sha512-uXKzuh/Emfad2Y7Qm0ABsLZZV6H3mAJ5ZVqmAOlrNQRf+T5mxpPGZBfec1hkP41t6h6FwF6RSGCs/gd8WbuySQ==",
|
||||
"dependencies": {
|
||||
"@vue/runtime-core": "3.4.23",
|
||||
"@vue/shared": "3.4.23",
|
||||
"@vue/runtime-core": "3.4.24",
|
||||
"@vue/shared": "3.4.24",
|
||||
"csstype": "^3.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/server-renderer": {
|
||||
"version": "3.4.23",
|
||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.23.tgz",
|
||||
"integrity": "sha512-LDwGHtnIzvKFNS8dPJ1SSU5Gvm36p2ck8wCZc52fc3k/IfjKcwCyrWEf0Yag/2wTFUBXrqizfhK9c/mC367dXQ==",
|
||||
"version": "3.4.24",
|
||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.24.tgz",
|
||||
"integrity": "sha512-H+DLK4sQF6sRgzKyofmlEVBIV/9KrQU6HIV7nt6yIwSGGKvSwlV8pqJlebUKLpbXaNHugdSfAbP6YmXF69lxow==",
|
||||
"dependencies": {
|
||||
"@vue/compiler-ssr": "3.4.23",
|
||||
"@vue/shared": "3.4.23"
|
||||
"@vue/compiler-ssr": "3.4.24",
|
||||
"@vue/shared": "3.4.24"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "3.4.23"
|
||||
"vue": "3.4.24"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/shared": {
|
||||
"version": "3.4.23",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.23.tgz",
|
||||
"integrity": "sha512-wBQ0gvf+SMwsCQOyusNw/GoXPV47WGd1xB5A1Pgzy0sQ3Bi5r5xm3n+92y3gCnB3MWqnRDdvfkRGxhKtbBRNgg=="
|
||||
"version": "3.4.24",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.24.tgz",
|
||||
"integrity": "sha512-BW4tajrJBM9AGAknnyEw5tO2xTmnqgup0VTnDAMcxYmqOX0RG0b9aSUGAbEKolD91tdwpA6oCwbltoJoNzpItw=="
|
||||
},
|
||||
"node_modules/@vue/vue-loader-v15": {
|
||||
"name": "vue-loader",
|
||||
|
@ -4655,10 +4655,11 @@
|
|||
}
|
||||
},
|
||||
"node_modules/core-js": {
|
||||
"version": "3.37.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.37.0.tgz",
|
||||
"integrity": "sha512-fu5vHevQ8ZG4og+LXug8ulUtVxjOcEYvifJr7L5Bfq9GOztVqsKd9/59hUk2ZSbCrS3BqUr3EpaYGIYzq7g3Ug==",
|
||||
"version": "3.37.1",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.37.1.tgz",
|
||||
"integrity": "sha512-Xn6qmxrQZyB0FFY8E3bgRXei3lWDJHhvI+u0q9TKIYM49G8pAr0FgnnrFRAmsbptZL1yxRADVXn+x5AGsbBfyw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/core-js"
|
||||
|
@ -4700,9 +4701,10 @@
|
|||
}
|
||||
},
|
||||
"node_modules/cronstrue": {
|
||||
"version": "2.49.0",
|
||||
"resolved": "https://registry.npmjs.org/cronstrue/-/cronstrue-2.49.0.tgz",
|
||||
"integrity": "sha512-FWZBqdStQaPR8ZTBQGALh1EK9Hl1HcG70dyGvD1rKLPafFO3H73o38dz/e8YkIlbLn3JxmBI/f6Doe3Nh+DcEQ==",
|
||||
"version": "2.50.0",
|
||||
"resolved": "https://registry.npmjs.org/cronstrue/-/cronstrue-2.50.0.tgz",
|
||||
"integrity": "sha512-ULYhWIonJzlScCCQrPUG5uMXzXxSixty4djud9SS37DoNxDdkeRocxzHuAo4ImRBUK+mAuU5X9TSwEDccnnuPg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"cronstrue": "bin/cli.js"
|
||||
}
|
||||
|
@ -10551,9 +10553,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.75.0",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.75.0.tgz",
|
||||
"integrity": "sha512-ShMYi3WkrDWxExyxSZPst4/okE9ts46xZmJDSawJQrnte7M1V9fScVB+uNXOVKRBt0PggHOwoZcn8mYX4trnBw==",
|
||||
"version": "1.76.0",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.76.0.tgz",
|
||||
"integrity": "sha512-nc3LeqvF2FNW5xGF1zxZifdW3ffIz5aBb7I7tSvOoNu7z1RQ6pFt9MBuiPtjgaI62YWrM/txjWlOCFiGtf2xpw==",
|
||||
"dependencies": {
|
||||
"chokidar": ">=3.0.0 <4.0.0",
|
||||
"immutable": "^4.0.0",
|
||||
|
@ -11976,15 +11978,15 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vue": {
|
||||
"version": "3.4.23",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.4.23.tgz",
|
||||
"integrity": "sha512-X1y6yyGJ28LMUBJ0k/qIeKHstGd+BlWQEOT40x3auJFTmpIhpbKLgN7EFsqalnJXq1Km5ybDEsp6BhuWKciUDg==",
|
||||
"version": "3.4.24",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.4.24.tgz",
|
||||
"integrity": "sha512-NPdx7dLGyHmKHGRRU5bMRYVE+rechR+KDU5R2tSTNG36PuMwbfAJ+amEvOAw7BPfZp5sQulNELSLm5YUkau+Sg==",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.4.23",
|
||||
"@vue/compiler-sfc": "3.4.23",
|
||||
"@vue/runtime-dom": "3.4.23",
|
||||
"@vue/server-renderer": "3.4.23",
|
||||
"@vue/shared": "3.4.23"
|
||||
"@vue/compiler-dom": "3.4.24",
|
||||
"@vue/compiler-sfc": "3.4.24",
|
||||
"@vue/runtime-dom": "3.4.24",
|
||||
"@vue/server-renderer": "3.4.24",
|
||||
"@vue/shared": "3.4.24"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "*"
|
||||
|
|
|
@ -10,15 +10,15 @@
|
|||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.5.2",
|
||||
"axios": "^1.6.8",
|
||||
"core-js": "^3.37.0",
|
||||
"cronstrue": "^2.49.0",
|
||||
"core-js": "^3.37.1",
|
||||
"cronstrue": "^2.50.0",
|
||||
"highlight.js": "^11.9.0",
|
||||
"lato-font": "^3.0.0",
|
||||
"mitt": "^2.1.0",
|
||||
"register-service-worker": "^1.7.2",
|
||||
"sass": "^1.75.0",
|
||||
"sass": "^1.76.0",
|
||||
"sass-loader": "^10.5.2",
|
||||
"vue": "^3.4.23",
|
||||
"vue": "^3.4.24",
|
||||
"vue-router": "^4.3.2",
|
||||
"vue-skycons": "^4.3.4",
|
||||
"w3css": "^2.7.0"
|
||||
|
|
|
@ -8,6 +8,9 @@
|
|||
"assistant.google": {
|
||||
"class": "fas fa-microphone-lines"
|
||||
},
|
||||
"assistant.openai": {
|
||||
"class": "fas fa-microphone-lines"
|
||||
},
|
||||
"assistant.picovoice": {
|
||||
"class": "fas fa-microphone-lines"
|
||||
},
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
from collections import namedtuple
|
||||
from dataclasses import dataclass, field
|
||||
from logging import getLogger
|
||||
from queue import Full, Queue
|
||||
from threading import Event, RLock
|
||||
from threading import Event
|
||||
from time import time
|
||||
from typing import Optional
|
||||
|
||||
|
@ -10,63 +8,7 @@ import sounddevice as sd
|
|||
|
||||
from platypush.utils import wait_for_either
|
||||
|
||||
|
||||
AudioFrame = namedtuple('AudioFrame', ['data', 'timestamp'])
|
||||
|
||||
|
||||
@dataclass
|
||||
class PauseState:
|
||||
"""
|
||||
Data class to hold the boilerplate (state + synchronization events) for the
|
||||
audio recorder pause API.
|
||||
"""
|
||||
|
||||
_paused_event: Event = field(default_factory=Event)
|
||||
_recording_event: Event = field(default_factory=Event)
|
||||
_state_lock: RLock = field(default_factory=RLock)
|
||||
|
||||
@property
|
||||
def paused(self):
|
||||
with self._state_lock:
|
||||
return self._paused_event.is_set()
|
||||
|
||||
def pause(self):
|
||||
"""
|
||||
Pause the audio recorder.
|
||||
"""
|
||||
with self._state_lock:
|
||||
self._paused_event.set()
|
||||
self._recording_event.clear()
|
||||
|
||||
def resume(self):
|
||||
"""
|
||||
Resume the audio recorder.
|
||||
"""
|
||||
with self._state_lock:
|
||||
self._paused_event.clear()
|
||||
self._recording_event.set()
|
||||
|
||||
def toggle(self):
|
||||
"""
|
||||
Toggle the audio recorder pause state.
|
||||
"""
|
||||
with self._state_lock:
|
||||
if self.paused:
|
||||
self.resume()
|
||||
else:
|
||||
self.pause()
|
||||
|
||||
def wait_paused(self, timeout: Optional[float] = None):
|
||||
"""
|
||||
Wait until the audio recorder is paused.
|
||||
"""
|
||||
self._paused_event.wait(timeout=timeout)
|
||||
|
||||
def wait_recording(self, timeout: Optional[float] = None):
|
||||
"""
|
||||
Wait until the audio recorder is resumed.
|
||||
"""
|
||||
self._recording_event.wait(timeout=timeout)
|
||||
from ._state import AudioFrame, PauseState
|
||||
|
||||
|
||||
class AudioRecorder:
|
||||
|
@ -112,9 +54,7 @@ class AudioRecorder:
|
|||
"""
|
||||
Start the audio stream.
|
||||
"""
|
||||
self._stop_event.clear()
|
||||
self.stream.start()
|
||||
return self
|
||||
return self.start()
|
||||
|
||||
def __exit__(self, *_):
|
||||
"""
|
||||
|
@ -145,6 +85,14 @@ class AudioRecorder:
|
|||
self.logger.debug('Audio queue is empty')
|
||||
return None
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
Start the audio stream.
|
||||
"""
|
||||
self._stop_event.clear()
|
||||
self.stream.start()
|
||||
return self
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Stop the audio stream.
|
||||
|
@ -186,6 +134,6 @@ class AudioRecorder:
|
|||
wait_for_either(
|
||||
self._stop_event,
|
||||
self._upstream_stop_event,
|
||||
self._paused_state._recording_event,
|
||||
self._paused_state._recording_event, # pylint: disable=protected-access
|
||||
timeout=timeout,
|
||||
)
|
|
@ -0,0 +1,61 @@
|
|||
from collections import namedtuple
|
||||
from dataclasses import dataclass, field
|
||||
from threading import Event, RLock
|
||||
from typing import Optional
|
||||
|
||||
AudioFrame = namedtuple('AudioFrame', ['data', 'timestamp'])
|
||||
|
||||
|
||||
@dataclass
|
||||
class PauseState:
|
||||
"""
|
||||
Data class to hold the boilerplate (state + synchronization events) for the
|
||||
audio recorder pause API.
|
||||
"""
|
||||
|
||||
_paused_event: Event = field(default_factory=Event)
|
||||
_recording_event: Event = field(default_factory=Event)
|
||||
_state_lock: RLock = field(default_factory=RLock)
|
||||
|
||||
@property
|
||||
def paused(self):
|
||||
with self._state_lock:
|
||||
return self._paused_event.is_set()
|
||||
|
||||
def pause(self):
|
||||
"""
|
||||
Pause the audio recorder.
|
||||
"""
|
||||
with self._state_lock:
|
||||
self._paused_event.set()
|
||||
self._recording_event.clear()
|
||||
|
||||
def resume(self):
|
||||
"""
|
||||
Resume the audio recorder.
|
||||
"""
|
||||
with self._state_lock:
|
||||
self._paused_event.clear()
|
||||
self._recording_event.set()
|
||||
|
||||
def toggle(self):
|
||||
"""
|
||||
Toggle the audio recorder pause state.
|
||||
"""
|
||||
with self._state_lock:
|
||||
if self.paused:
|
||||
self.resume()
|
||||
else:
|
||||
self.pause()
|
||||
|
||||
def wait_paused(self, timeout: Optional[float] = None):
|
||||
"""
|
||||
Wait until the audio recorder is paused.
|
||||
"""
|
||||
self._paused_event.wait(timeout=timeout)
|
||||
|
||||
def wait_recording(self, timeout: Optional[float] = None):
|
||||
"""
|
||||
Wait until the audio recorder is resumed.
|
||||
"""
|
||||
self._recording_event.wait(timeout=timeout)
|
Binary file not shown.
|
@ -355,7 +355,7 @@ class Config:
|
|||
prefix = modname + '.' if prefix is None else prefix
|
||||
self.procedures.update(
|
||||
**{
|
||||
getattr(obj, 'procedure_name', f'{prefix}.{name}'): obj
|
||||
(getattr(obj, 'procedure_name', None) or f'{prefix}{name}'): obj
|
||||
for name, obj in inspect.getmembers(module)
|
||||
if is_functional_procedure(obj)
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from threading import Event
|
||||
from typing import Collection, Optional
|
||||
|
||||
from platypush.utils import utcnow
|
||||
|
||||
from ._base import (
|
||||
Entity,
|
||||
EntityKey,
|
||||
|
@ -45,8 +46,8 @@ def get_entities_engine(timeout: Optional[float] = None) -> EntitiesEngine:
|
|||
|
||||
:param timeout: Timeout in seconds (default: None).
|
||||
"""
|
||||
time_start = datetime.utcnow()
|
||||
while not timeout or (datetime.utcnow() - time_start < timedelta(seconds=timeout)):
|
||||
time_start = utcnow().timestamp()
|
||||
while not timeout or (utcnow().timestamp() - time_start < timeout):
|
||||
if _engine:
|
||||
break
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ import platypush
|
|||
from platypush.config import Config
|
||||
from platypush.common.db import Base, is_defined
|
||||
from platypush.message import JSONAble, Message
|
||||
from platypush.utils import utcnow
|
||||
|
||||
EntityRegistryType = Dict[str, Type['Entity']]
|
||||
entities_registry: EntityRegistryType = {}
|
||||
|
@ -82,13 +83,11 @@ if not is_defined('entity'):
|
|||
external_url = Column(String)
|
||||
image_url = Column(String)
|
||||
|
||||
created_at = Column(
|
||||
DateTime(timezone=False), default=datetime.utcnow(), nullable=False
|
||||
)
|
||||
created_at = Column(DateTime(timezone=False), default=utcnow(), nullable=False)
|
||||
updated_at = Column(
|
||||
DateTime(timezone=False),
|
||||
default=datetime.utcnow(),
|
||||
onupdate=datetime.utcnow(),
|
||||
default=utcnow(),
|
||||
onupdate=utcnow(),
|
||||
)
|
||||
|
||||
parent = relationship(
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import inspect
|
||||
import json
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import datetime
|
||||
from typing import Any, Optional, Dict, Collection, Type
|
||||
|
||||
from platypush.config import Config
|
||||
from platypush.entities._base import Entity, EntitySavedCallback
|
||||
from platypush.utils import get_plugin_name_by_class, get_redis
|
||||
from platypush.utils import get_plugin_name_by_class, get_redis, utcnow
|
||||
|
||||
_entity_registry_varname = '_platypush/plugin_entity_registry'
|
||||
|
||||
|
@ -77,7 +76,7 @@ class EntityManager(ABC):
|
|||
entity.id = None # type: ignore
|
||||
|
||||
entity.plugin = get_plugin_name_by_class(self.__class__) # type: ignore
|
||||
entity.updated_at = datetime.utcnow() # type: ignore
|
||||
entity.updated_at = utcnow() # type: ignore
|
||||
entity.children = self._normalize_entities(entity.children)
|
||||
|
||||
return entities
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import copy
|
||||
import json
|
||||
import importlib
|
||||
import logging
|
||||
import threading
|
||||
from functools import wraps
|
||||
|
@ -46,8 +47,7 @@ class EventCondition:
|
|||
kwargs -- Fields rules as a key-value (e.g. source_button=btn_id
|
||||
or recognized_phrase='Your phrase')
|
||||
"""
|
||||
|
||||
self.type = type or Event.__class__ # type: ignore
|
||||
self.type = self._get_event_type(type)
|
||||
self.args = {}
|
||||
self.parsed_args = {}
|
||||
self.priority = priority
|
||||
|
@ -55,6 +55,23 @@ class EventCondition:
|
|||
for key, value in kwargs.items():
|
||||
self.args[key] = value
|
||||
|
||||
@staticmethod
|
||||
def _get_event_type(type: Optional[Type[Event]] = None) -> Type[Event]:
|
||||
if not type:
|
||||
return Event
|
||||
|
||||
# The package alias `platypush.events` -> `platypush.message.event` is
|
||||
# supported
|
||||
if type.__module__.startswith('platypush.events'):
|
||||
module = importlib.import_module(
|
||||
'platypush.message.event' + type.__module__[len('platypush.events') :]
|
||||
)
|
||||
|
||||
type = getattr(module, type.__name__)
|
||||
assert type, f'Invalid event type: {type}'
|
||||
|
||||
return type
|
||||
|
||||
@classmethod
|
||||
def build(cls, rule):
|
||||
"""
|
||||
|
|
|
@ -19,14 +19,12 @@ class AssistantEvent(Event):
|
|||
"""
|
||||
assistant = assistant or kwargs.get('assistant')
|
||||
if assistant:
|
||||
assistant = (
|
||||
kwargs['plugin'] = kwargs['_assistant'] = (
|
||||
assistant
|
||||
if isinstance(assistant, str)
|
||||
else get_plugin_name_by_class(assistant.__class__)
|
||||
)
|
||||
|
||||
kwargs['_assistant'] = assistant
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@property
|
||||
|
@ -149,7 +147,12 @@ class SpeechRecognizedEvent(AssistantEvent):
|
|||
"""
|
||||
|
||||
result = super().matches_condition(condition)
|
||||
if result.is_match and self.assistant and 'phrase' in condition.args:
|
||||
if (
|
||||
result.is_match
|
||||
and condition.args.get('phrase')
|
||||
and self.assistant
|
||||
and self.assistant.stop_conversation_on_speech_match
|
||||
):
|
||||
self.assistant.stop_conversation()
|
||||
|
||||
return result
|
||||
|
@ -244,6 +247,21 @@ class IntentRecognizedEvent(AssistantEvent):
|
|||
"""
|
||||
super().__init__(*args, intent=intent, slots=slots or {}, **kwargs)
|
||||
|
||||
def matches_condition(self, condition):
|
||||
"""
|
||||
Overrides matches condition, and stops the conversation to prevent the
|
||||
default assistant response if the event matched some event hook condition.
|
||||
"""
|
||||
result = super().matches_condition(condition)
|
||||
if (
|
||||
result.is_match
|
||||
and self.assistant
|
||||
and self.assistant.stop_conversation_on_speech_match
|
||||
):
|
||||
self.assistant.stop_conversation()
|
||||
|
||||
return result
|
||||
|
||||
def _matches_argument(
|
||||
self, argname, condition_value, event_args, result: EventMatchResult
|
||||
):
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import asyncio
|
||||
import logging
|
||||
import threading
|
||||
import warnings
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from functools import wraps
|
||||
|
@ -168,11 +167,7 @@ class RunnablePlugin(Plugin):
|
|||
self._thread: Optional[threading.Thread] = None
|
||||
|
||||
if kwargs.get('poll_seconds') is not None:
|
||||
warnings.warn(
|
||||
'poll_seconds is deprecated, use poll_interval instead',
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
self.logger.warning('poll_seconds is deprecated, use poll_interval instead')
|
||||
|
||||
if self.poll_interval is None:
|
||||
self.poll_interval = kwargs['poll_seconds']
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from abc import ABC
|
||||
from dataclasses import asdict, dataclass
|
||||
from enum import Enum
|
||||
import os
|
||||
|
@ -13,7 +13,6 @@ from platypush.plugins import Plugin, action
|
|||
from platypush.utils import get_plugin_name_by_class
|
||||
|
||||
|
||||
@dataclass
|
||||
class AlertType(Enum):
|
||||
"""
|
||||
Enum representing the type of an alert.
|
||||
|
@ -51,7 +50,7 @@ class AssistantPlugin(Plugin, AssistantEntityManager, ABC):
|
|||
tts_plugin: Optional[str] = None,
|
||||
tts_plugin_args: Optional[Dict[str, Any]] = None,
|
||||
conversation_start_sound: Optional[str] = None,
|
||||
stop_conversation_on_speech_match: bool = False,
|
||||
stop_conversation_on_speech_match: bool = True,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
|
@ -67,17 +66,25 @@ class AssistantPlugin(Plugin, AssistantEntityManager, ABC):
|
|||
on the default audio output device. If not set, the assistant won't
|
||||
play any sound when it detects a speech.
|
||||
|
||||
:param stop_conversation_on_speech_match: If set, the plugin will close the
|
||||
conversation if the latest recognized speech matches a registered
|
||||
:class:`platypush.message.event.assistant.SpeechRecognizedEvent` hook
|
||||
with a phrase. This is usually set to ``True`` for
|
||||
:class:`platypush.plugins.assistant.google.GoogleAssistantPlugin`,
|
||||
as it overrides the default assistant response when a speech event is
|
||||
actually handled on the application side.
|
||||
:param stop_conversation_on_speech_match: If set, the plugin will
|
||||
prevent the default assistant response when a
|
||||
:class:`platypush.message.event.assistant.SpeechRecognizedEvent`
|
||||
matches a user hook with a condition on a ``phrase`` field. This is
|
||||
useful to prevent the assistant from responding with a default "*I'm
|
||||
sorry, I can't help you with that*" when e.g. you say "*play the
|
||||
music*", and you have a hook that matches the phrase "*play the
|
||||
music*" and handles it with a custom action. If set, and you wish
|
||||
the assistant to also provide an answer if an event matches one of
|
||||
your hooks, then you should call the :meth:`render_response` method
|
||||
in your hook handler. If not set, then the assistant will always try
|
||||
and respond with a default message, even if a speech event matches
|
||||
the phrase of one of your hooks. In this case, if you want to prevent
|
||||
the default response, you should call :meth:`stop_conversation`
|
||||
explicitly from your hook handler. Default: True.
|
||||
"""
|
||||
super().__init__(*args, **kwargs)
|
||||
self.tts_plugin = tts_plugin
|
||||
self.tts_plugin_args = tts_plugin_args or {}
|
||||
self.tts_plugin_args = {'join': True, **(tts_plugin_args or {})}
|
||||
self.stop_conversation_on_speech_match = stop_conversation_on_speech_match
|
||||
self._conversation_start_sound = None
|
||||
if conversation_start_sound:
|
||||
|
@ -104,19 +111,17 @@ class AssistantPlugin(Plugin, AssistantEntityManager, ABC):
|
|||
alert_state=self._cur_alert_type.value if self._cur_alert_type else None,
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
def start_conversation(self, *_, **__):
|
||||
"""
|
||||
Programmatically starts a conversation.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
@action
|
||||
def stop_conversation(self, *_, **__):
|
||||
"""
|
||||
Programmatically stops a conversation.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
self._stop_conversation()
|
||||
|
||||
def _stop_conversation(self, *_, **__):
|
||||
tts = self._get_tts_plugin()
|
||||
if tts:
|
||||
tts.stop()
|
||||
|
||||
@action
|
||||
def pause_detection(self, *_, **__):
|
||||
|
@ -166,15 +171,38 @@ class AssistantPlugin(Plugin, AssistantEntityManager, ABC):
|
|||
return asdict(self._state)
|
||||
|
||||
@action
|
||||
def render_response(self, text: str, *_, **__):
|
||||
def render_response(
|
||||
self, text: str, *_, with_follow_on_turn: Optional[bool] = None, **__
|
||||
) -> bool:
|
||||
"""
|
||||
Render a response text as audio over the configured TTS plugin.
|
||||
|
||||
:param text: Text to render.
|
||||
:param with_follow_on_turn: If set, the assistant will wait for a follow-up.
|
||||
By default, ``with_follow_on_turn`` will be automatically set to true if
|
||||
the ``text`` ends with a question mark.
|
||||
:return: True if the assistant is waiting for a follow-up, False otherwise.
|
||||
"""
|
||||
self._on_response_render_start(text)
|
||||
if not text:
|
||||
self._on_no_response()
|
||||
return False
|
||||
|
||||
follow_up = (
|
||||
bool(text and text.strip().endswith('?'))
|
||||
if with_follow_on_turn is None
|
||||
else with_follow_on_turn
|
||||
)
|
||||
|
||||
self._on_response_render_start(text, with_follow_on_turn=follow_up)
|
||||
self._render_response(text)
|
||||
self._on_response_render_end()
|
||||
self._on_response_render_end(with_follow_on_turn=follow_up)
|
||||
|
||||
if follow_up:
|
||||
self.start_conversation()
|
||||
else:
|
||||
self.stop_conversation()
|
||||
|
||||
return follow_up
|
||||
|
||||
def _get_tts_plugin(self):
|
||||
if not self.tts_plugin:
|
||||
|
@ -229,22 +257,37 @@ class AssistantPlugin(Plugin, AssistantEntityManager, ABC):
|
|||
self._conversation_running.clear()
|
||||
self._send_event(NoResponseEvent)
|
||||
|
||||
def _on_response_render_start(self, text: Optional[str]):
|
||||
def _on_response_render_start(
|
||||
self, text: Optional[str], with_follow_on_turn: bool = False
|
||||
):
|
||||
from platypush.message.event.assistant import ResponseEvent
|
||||
|
||||
self._last_response = text
|
||||
self._send_event(ResponseEvent, response_text=text)
|
||||
self._send_event(
|
||||
ResponseEvent, response_text=text, with_follow_on_turn=with_follow_on_turn
|
||||
)
|
||||
|
||||
def _render_response(self, text: Optional[str]):
|
||||
tts = self._get_tts_plugin()
|
||||
if tts and text:
|
||||
self.stop_conversation()
|
||||
tts.say(text=text, **self.tts_plugin_args)
|
||||
if not text:
|
||||
return
|
||||
|
||||
def _on_response_render_end(self):
|
||||
tts = self._get_tts_plugin()
|
||||
if not tts:
|
||||
self.logger.warning(
|
||||
'Got a response to render, but no TTS plugin is configured: %s', text
|
||||
)
|
||||
return
|
||||
|
||||
tts.say(text=text, **self.tts_plugin_args)
|
||||
|
||||
def _on_response_render_end(self, with_follow_on_turn: bool = False):
|
||||
from platypush.message.event.assistant import ResponseEndEvent
|
||||
|
||||
self._send_event(ResponseEndEvent, response_text=self._last_response)
|
||||
self._send_event(
|
||||
ResponseEndEvent,
|
||||
response_text=self._last_response,
|
||||
with_follow_on_turn=with_follow_on_turn,
|
||||
)
|
||||
|
||||
def _on_hotword_detected(self, hotword: Optional[str]):
|
||||
from platypush.message.event.assistant import HotwordDetectedEvent
|
||||
|
|
|
@ -76,7 +76,6 @@ class AssistantGooglePlugin(AssistantPlugin, RunnablePlugin):
|
|||
self,
|
||||
credentials_file: Optional[str] = None,
|
||||
device_model_id: str = 'Platypush',
|
||||
stop_conversation_on_speech_match: bool = True,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
|
@ -95,20 +94,8 @@ class AssistantGooglePlugin(AssistantPlugin, RunnablePlugin):
|
|||
:param device_model_id: The device model ID that identifies the device
|
||||
where the assistant is running (default: Platypush). It can be a
|
||||
custom string.
|
||||
|
||||
:param stop_conversation_on_speech_match: If set, the plugin will close the
|
||||
conversation if the latest recognized speech matches a registered
|
||||
:class:`platypush.message.event.assistant.SpeechRecognizedEvent` hook
|
||||
with a phrase. This is usually set to ``True`` for
|
||||
:class:`platypush.plugins.assistant.google.GoogleAssistantPlugin`,
|
||||
as it overrides the default assistant response when a speech event is
|
||||
actually handled on the application side.
|
||||
"""
|
||||
|
||||
super().__init__(
|
||||
stop_conversation_on_speech_match=stop_conversation_on_speech_match,
|
||||
**kwargs,
|
||||
)
|
||||
super().__init__(**kwargs)
|
||||
self._credentials_file = credentials_file
|
||||
self.device_model_id = device_model_id
|
||||
self.credentials = None
|
||||
|
@ -155,7 +142,7 @@ class AssistantGooglePlugin(AssistantPlugin, RunnablePlugin):
|
|||
hasattr(EventType, 'ON_RENDER_RESPONSE')
|
||||
and event.type == EventType.ON_RENDER_RESPONSE
|
||||
):
|
||||
self._on_reponse_rendered(event.args.get('text'))
|
||||
self._on_response_render_start(event.args.get('text'))
|
||||
elif (
|
||||
hasattr(EventType, 'ON_RESPONDING_STARTED')
|
||||
and event.type == EventType.ON_RESPONDING_STARTED
|
||||
|
@ -194,8 +181,7 @@ class AssistantGooglePlugin(AssistantPlugin, RunnablePlugin):
|
|||
if self.assistant:
|
||||
self.assistant.start_conversation()
|
||||
|
||||
@action
|
||||
def stop_conversation(self, *_, **__):
|
||||
def _stop_conversation(self, *_, **__):
|
||||
"""
|
||||
Programmatically stop a running conversation with the assistant
|
||||
"""
|
||||
|
|
|
@ -0,0 +1,447 @@
|
|||
from io import BytesIO
|
||||
from threading import Event
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
from pydub import AudioSegment
|
||||
|
||||
from platypush.common.assistant import AudioRecorder
|
||||
from platypush.context import get_plugin
|
||||
from platypush.plugins import RunnablePlugin, action
|
||||
from platypush.plugins.assistant import AssistantPlugin
|
||||
from platypush.plugins.openai import OpenaiPlugin
|
||||
|
||||
from ._state import RecordingState
|
||||
|
||||
|
||||
# pylint: disable=too-many-ancestors
|
||||
class AssistantOpenaiPlugin(AssistantPlugin, RunnablePlugin):
|
||||
"""
|
||||
A voice assistant based on the OpenAI API.
|
||||
|
||||
It requires the :class:`platypush.plugins.openai.OpenaiPlugin` plugin to be
|
||||
configured with an OpenAI API key.
|
||||
|
||||
Hotword detection
|
||||
-----------------
|
||||
|
||||
This plugin doesn't have hotword detection, as OpenAI doesn't provide
|
||||
an API for that. Instead, the assistant can be started and stopped
|
||||
programmatically through the :meth:`.start_conversation` action.
|
||||
|
||||
If you want to implement hotword detection, you can use a separate plugin
|
||||
such as
|
||||
:class:`platypush.plugins.assistant.picovoice.AssistantPicovoicePlugin`.
|
||||
|
||||
The configuration in this case would be like:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
assistant.picovoice:
|
||||
access_key: YOUR_PICOVOICE_ACCESS_KEY
|
||||
|
||||
# List of hotwords to listen for
|
||||
keywords:
|
||||
- alexa
|
||||
- computer
|
||||
- ok google
|
||||
|
||||
# Disable speech-to-text and intent recognition, only use hotword
|
||||
# detection
|
||||
stt_enabled: false
|
||||
hotword_enabled: true
|
||||
|
||||
conversation_start_sound: /sound/to/play/when/the/conversation/starts.mp3
|
||||
# speech_model_path: /mnt/hd/models/picovoice/cheetah/custom-en.pv
|
||||
# intent_model_path: /mnt/hd/models/picovoice/rhino/custom-en-x86.rhn
|
||||
|
||||
openai:
|
||||
api_key: YOUR_OPENAI_API_KEY
|
||||
|
||||
# Customize your assistant's context and knowledge base to your
|
||||
# liking
|
||||
context:
|
||||
- role: system
|
||||
content: >
|
||||
You are a 16th century noble lady who talks in
|
||||
Shakespearean English to her peers.
|
||||
|
||||
# Enable the assistant plugin
|
||||
assistant.openai:
|
||||
|
||||
# Enable the text-to-speech plugin
|
||||
tts.openai:
|
||||
# Customize the voice model
|
||||
voice: nova
|
||||
|
||||
Then you can call :meth:`.start_conversation` when the hotword is detected
|
||||
:class:`platypush.message.event.assistant.HotwordDetectedEvent` is
|
||||
triggered:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from platypush import run, when
|
||||
from platypush.message.event.assistant import HotwordDetectedEvent
|
||||
|
||||
@when(HotwordDetectedEvent)
|
||||
# You can also customize it by running a different assistant logic
|
||||
# depending on the hotword
|
||||
# @when(HotwordDetectedEvent, hotword='computer')
|
||||
def on_hotword_detected():
|
||||
run("assistant.openai.start_conversation")
|
||||
|
||||
This configuration will:
|
||||
|
||||
1. Start the hotword detection when the application starts.
|
||||
2. Start the OpenAI assistant when the hotword is detected.
|
||||
|
||||
AI responses
|
||||
------------
|
||||
|
||||
By default (unless you set ``stop_conversation_on_speech_match`` to ``False``),
|
||||
the plugin will:
|
||||
|
||||
1. Process the speech through the OpenAI API (the GPT model to be is
|
||||
configurable in the OpenAI plugin ``model`` configuration).
|
||||
|
||||
2. Render the response through the configured ``tts_plugin`` (default:
|
||||
``tts.openai``). If ``tts_plugin`` is not set, then the response will
|
||||
be returned as a string.
|
||||
|
||||
Custom speech processing
|
||||
------------------------
|
||||
|
||||
You can create custom hooks on
|
||||
:class:`platypush.message.event.assistant.SpeechRecognizedEvent` with
|
||||
custom ``phrase`` strings or (regex) patterns. For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from platypush import run, when
|
||||
from platypush.message.event.assistant import SpeechRecognizedEvent
|
||||
|
||||
# Matches any phrase that contains either "play music" or "play the
|
||||
# music"
|
||||
@when(SpeechRecognizedEvent, phrase='play (the)? music')
|
||||
def play_music():
|
||||
run('music.mpd.play')
|
||||
|
||||
If at least a custom hook with a non-empty ``phrase`` string is matched,
|
||||
then the default response will be disabled. If you still want the assistant
|
||||
to say something when the event is handled, you can call
|
||||
``event.assistant.render_response`` on the hook:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from datetime import datetime
|
||||
from textwrap import dedent
|
||||
from time import time
|
||||
|
||||
from platypush import run, when
|
||||
from platypush.message.event.assistant import SpeechRecognizedEvent
|
||||
|
||||
@when(SpeechRecognizedEvent, phrase='weather today')
|
||||
def weather_forecast(event: SpeechRecognizedEvent):
|
||||
limit = time() + 24 * 60 * 60 # 24 hours from now
|
||||
forecast = [
|
||||
weather
|
||||
for weather in run("weather.openweathermap.get_forecast")
|
||||
if datetime.fromisoformat(weather["time"]).timestamp() < limit
|
||||
]
|
||||
|
||||
min_temp = round(
|
||||
min(weather["temperature"] for weather in forecast)
|
||||
)
|
||||
max_temp = round(
|
||||
max(weather["temperature"] for weather in forecast)
|
||||
)
|
||||
max_wind_gust = round(
|
||||
(max(weather["wind_gust"] for weather in forecast)) * 3.6
|
||||
)
|
||||
summaries = [weather["summary"] for weather in forecast]
|
||||
most_common_summary = max(summaries, key=summaries.count)
|
||||
avg_cloud_cover = round(
|
||||
sum(weather["cloud_cover"] for weather in forecast) / len(forecast)
|
||||
)
|
||||
|
||||
event.assistant.render_response(
|
||||
dedent(
|
||||
f\"\"\"
|
||||
The forecast for today is: {most_common_summary}, with
|
||||
a minimum of {min_temp} and a maximum of {max_temp}
|
||||
degrees, wind gust of {max_wind_gust} km/h, and an
|
||||
average cloud cover of {avg_cloud_cover}%.
|
||||
\"\"\"
|
||||
)
|
||||
)
|
||||
|
||||
Conversation follow-up
|
||||
----------------------
|
||||
|
||||
A conversation will have a follow-up (i.e. the assistant will listen for a
|
||||
phrase after rendering a response) if the response is not empty and ends
|
||||
with a question mark. If you want to force a follow-up even if the response
|
||||
doesn't end with a question mark, you can call :meth:`.start_conversation`
|
||||
programmatically from your hooks.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model: str = "whisper-1",
|
||||
tts_plugin: Optional[str] = "tts.openai",
|
||||
min_silence_secs: float = 1.0,
|
||||
silence_threshold: int = -22,
|
||||
sample_rate: int = 16000,
|
||||
frame_size: int = 16384,
|
||||
channels: int = 1,
|
||||
conversation_start_timeout: float = 5.0,
|
||||
conversation_end_timeout: float = 1.0,
|
||||
conversation_max_duration: float = 15.0,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
:param model: OpenAI model to use for audio transcription (default:
|
||||
``whisper-1``).
|
||||
:param tts_plugin: Name of the TTS plugin to use for rendering the responses
|
||||
(default: ``tts.openai``).
|
||||
:param min_silence_secs: Minimum silence duration in seconds to detect
|
||||
the end of a conversation (default: 1.0 seconds).
|
||||
:param silence_threshold: Silence threshold in dBFS (default: -22).
|
||||
The value of 0 is the maximum amplitude, and -120 is associated to
|
||||
a silent or nearly silent audio, thus the higher the value, the more
|
||||
sensitive the silence detection will be (default: -22).
|
||||
:param sample_rate: Recording sample rate in Hz (default: 16000).
|
||||
:param frame_size: Recording frame size in samples (default: 16384).
|
||||
Note that it's important to make sure that ``frame_size`` /
|
||||
``sample_rate`` isn't smaller than the minimum silence duration,
|
||||
otherwise the silence detection won't work properly.
|
||||
:param channels: Number of recording channels (default: 1).
|
||||
:param conversation_start_timeout: How long to wait for the
|
||||
conversation to start (i.e. the first non-silent audio frame to be
|
||||
detected) before giving up and stopping the recording (default: 5.0
|
||||
seconds).
|
||||
:param conversation_end_timeout: How many seconds of silence to wait
|
||||
after the last non-silent audio frame before stopping the recording
|
||||
(default: 1.5 seconds).
|
||||
:param conversation_max_duration: Maximum conversation duration in seconds
|
||||
(default: 15.0 seconds).
|
||||
"""
|
||||
kwargs["tts_plugin"] = tts_plugin
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self._model = model
|
||||
self._min_silence_secs = min_silence_secs
|
||||
self._silence_threshold = silence_threshold
|
||||
self._sample_rate = sample_rate
|
||||
self._frame_size = frame_size
|
||||
self._channels = channels
|
||||
self._conversation_start_timeout = conversation_start_timeout
|
||||
self._conversation_end_timeout = conversation_end_timeout
|
||||
self._conversation_max_duration = conversation_max_duration
|
||||
self._start_recording_event = Event()
|
||||
self._disable_default_response = False
|
||||
self._recording_state = RecordingState(
|
||||
sample_rate=sample_rate,
|
||||
channels=channels,
|
||||
min_silence_secs=min_silence_secs,
|
||||
silence_threshold=silence_threshold,
|
||||
)
|
||||
|
||||
self._recorder: Optional[AudioRecorder] = None
|
||||
|
||||
def _to_audio_segment(self, data: np.ndarray) -> AudioSegment:
|
||||
return AudioSegment(
|
||||
data.tobytes(),
|
||||
frame_rate=self._sample_rate,
|
||||
sample_width=data.dtype.itemsize,
|
||||
channels=self._channels,
|
||||
)
|
||||
|
||||
def _is_conversation_ended(self):
|
||||
# End if the recording has been stopped
|
||||
if not self._recorder or self._recorder.should_stop():
|
||||
return True
|
||||
|
||||
# End if we reached the max conversation duration
|
||||
if self._recording_state.duration >= self._conversation_max_duration:
|
||||
return True
|
||||
|
||||
# End if the conversation hasn't started yet and we reached the
|
||||
# conversation start timeout
|
||||
if (
|
||||
not self._recording_state.conversation_started
|
||||
and self._recording_state.duration >= self._conversation_start_timeout
|
||||
):
|
||||
return True
|
||||
|
||||
# End if the conversation has started and the user has been silent for
|
||||
# more than the conversation end timeout
|
||||
if (
|
||||
self._recording_state.conversation_started
|
||||
and self._recording_state.silence_duration >= self._conversation_end_timeout
|
||||
):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@property
|
||||
def _openai(self) -> OpenaiPlugin:
|
||||
openai: Optional[OpenaiPlugin] = get_plugin("openai")
|
||||
assert openai, (
|
||||
"OpenAI plugin not found. "
|
||||
"Please configure the `openai` plugin to use `assistant.openai`"
|
||||
)
|
||||
return openai
|
||||
|
||||
def _get_prediction(self, audio: BytesIO) -> str:
|
||||
return self._openai.transcribe_raw(
|
||||
audio.getvalue(), extension='mp3', model=self._model
|
||||
)
|
||||
|
||||
def _capture_audio(self, recorder: AudioRecorder):
|
||||
while not self.should_stop() and not self._is_conversation_ended():
|
||||
audio_data = recorder.read()
|
||||
if not audio_data:
|
||||
continue
|
||||
|
||||
self._recording_state.add_audio(audio_data)
|
||||
|
||||
def _audio_loop(self):
|
||||
while not self.should_stop():
|
||||
self._wait_recording_start()
|
||||
self._recording_state.reset()
|
||||
self._on_conversation_start()
|
||||
|
||||
try:
|
||||
with AudioRecorder(
|
||||
stop_event=self._should_stop,
|
||||
sample_rate=self._sample_rate,
|
||||
frame_size=self._frame_size,
|
||||
channels=self._channels,
|
||||
) as self._recorder:
|
||||
self._capture_audio(self._recorder)
|
||||
finally:
|
||||
if self._recorder:
|
||||
try:
|
||||
self._recorder.stream.close()
|
||||
except Exception as e:
|
||||
self.logger.warning("Error closing the audio stream: %s", e)
|
||||
|
||||
self._recorder = None
|
||||
|
||||
if self._recording_state.is_silent():
|
||||
self._on_conversation_timeout()
|
||||
else:
|
||||
audio = self._recording_state.export_audio()
|
||||
text = self._get_prediction(audio)
|
||||
self._on_speech_recognized(text)
|
||||
|
||||
def _wait_recording_start(self):
|
||||
self._start_recording_event.wait()
|
||||
self._start_recording_event.clear()
|
||||
|
||||
def _start_conversation(self, *_, **__):
|
||||
self._disable_default_response = False
|
||||
self._recording_state.reset()
|
||||
self._start_recording_event.set()
|
||||
|
||||
def _stop_conversation(self, *_, **__):
|
||||
self._disable_default_response = True
|
||||
super()._stop_conversation()
|
||||
self._recording_state.reset()
|
||||
if self._recorder:
|
||||
self._recorder.stop()
|
||||
|
||||
self._on_conversation_end()
|
||||
|
||||
def _on_speech_recognized(self, phrase: Optional[str]):
|
||||
super()._on_speech_recognized(phrase)
|
||||
|
||||
# Dirty hack: wait a bit before stopping the conversation to make sure
|
||||
# that there aren't event hooks triggered in other threads that are
|
||||
# supposed to handle.
|
||||
if self.stop_conversation_on_speech_match:
|
||||
self.wait_stop(0.5)
|
||||
if self.should_stop():
|
||||
return
|
||||
|
||||
if self._disable_default_response:
|
||||
self.logger.debug("Default response disabled, skipping response")
|
||||
return
|
||||
|
||||
response = self._openai.get_response(phrase).output
|
||||
if response:
|
||||
self.render_response(response)
|
||||
else:
|
||||
self._on_no_response()
|
||||
|
||||
@action
|
||||
def start_conversation(self, *_, **__):
|
||||
"""
|
||||
Start a conversation with the assistant. The conversation will be
|
||||
automatically stopped after ``conversation_max_duration`` seconds of
|
||||
audio, or after ``conversation_start_timeout`` seconds of silence
|
||||
with no audio detected, or after ``conversation_end_timeout`` seconds
|
||||
after the last non-silent audio frame has been detected, or when the
|
||||
:meth:`.stop_conversation` method is called.
|
||||
"""
|
||||
self._start_conversation()
|
||||
|
||||
@action
|
||||
def mute(self, *_, **__):
|
||||
"""
|
||||
.. note:: This plugin has no hotword detection, thus no continuous
|
||||
audio detection. Speech processing is done on-demand through the
|
||||
:meth:`.start_conversation` and :meth:`.stop_conversation` methods.
|
||||
Therefore, the :meth:`.mute` and :meth:`.unmute` methods are not
|
||||
implemented.
|
||||
"""
|
||||
self.logger.warning(
|
||||
"assistant.openai.mute is not implemented because this plugin "
|
||||
"has no hotword detection, and the only way to stop a conversation "
|
||||
"is by calling stop_conversation()"
|
||||
)
|
||||
|
||||
@action
|
||||
def unmute(self, *_, **__):
|
||||
"""
|
||||
.. note:: This plugin has no hotword detection, thus no continuous
|
||||
audio detection. Speech processing is done on-demand through the
|
||||
:meth:`.start_conversation` and :meth:`.stop_conversation` methods.
|
||||
Therefore, the :meth:`.mute` and :meth:`.unmute` methods are not
|
||||
implemented.
|
||||
"""
|
||||
self.logger.warning(
|
||||
"assistant.openai.unmute is not implemented because this plugin "
|
||||
"has no hotword detection, and the only way to start a conversation "
|
||||
"is by calling start_conversation()"
|
||||
)
|
||||
|
||||
@action
|
||||
def send_text_query(self, text: str, *_, **__):
|
||||
"""
|
||||
If the ``tts_plugin`` configuration is set, then the assistant will
|
||||
process the given text query through
|
||||
:meth:`platypush.plugins.openai.OpenaiPlugin.get_response` and render
|
||||
the response through the specified TTS plugin.
|
||||
|
||||
:return: The response received from
|
||||
:meth:`platypush.plugins.openai.OpenaiPlugin.get_response`.
|
||||
"""
|
||||
response = self._openai.get_response(text).output
|
||||
self.render_response(response)
|
||||
return response
|
||||
|
||||
def main(self):
|
||||
while not self.should_stop():
|
||||
try:
|
||||
self._audio_loop()
|
||||
except Exception as e:
|
||||
self.logger.error("Audio loop error: %s", e, exc_info=True)
|
||||
self.wait_stop(5)
|
||||
finally:
|
||||
self.stop_conversation()
|
||||
|
||||
def stop(self):
|
||||
self._stop_conversation()
|
||||
super().stop()
|
|
@ -0,0 +1,80 @@
|
|||
from io import BytesIO
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List
|
||||
|
||||
import numpy as np
|
||||
from pydub import AudioSegment, silence
|
||||
|
||||
from platypush.common.assistant import AudioFrame
|
||||
|
||||
|
||||
@dataclass
|
||||
class RecordingState:
|
||||
"""
|
||||
Current state of the audio recording.
|
||||
"""
|
||||
|
||||
sample_rate: int
|
||||
channels: int
|
||||
min_silence_secs: float
|
||||
silence_threshold: int
|
||||
silence_duration: float = 0.0
|
||||
audio_segments: List[AudioSegment] = field(default_factory=list)
|
||||
duration: float = 0.0
|
||||
conversation_started: bool = False
|
||||
|
||||
def _silence_duration(self, audio: AudioSegment) -> float:
|
||||
silent_frames = [
|
||||
(start / 1000, stop / 1000)
|
||||
for start, stop in silence.detect_silence(
|
||||
audio,
|
||||
min_silence_len=int(self.min_silence_secs * 1000),
|
||||
silence_thresh=int(self.silence_threshold),
|
||||
)
|
||||
]
|
||||
|
||||
return sum(stop - start for start, stop in silent_frames)
|
||||
|
||||
def _to_audio_segment(self, data: np.ndarray) -> AudioSegment:
|
||||
return AudioSegment(
|
||||
data.tobytes(),
|
||||
frame_rate=self.sample_rate,
|
||||
sample_width=data.dtype.itemsize,
|
||||
channels=self.channels,
|
||||
)
|
||||
|
||||
def _add_audio_segment(self, audio: AudioSegment):
|
||||
self.audio_segments.append(audio)
|
||||
self.duration += audio.duration_seconds
|
||||
silence_duration = self._silence_duration(audio)
|
||||
is_mostly_silent = silence_duration >= audio.duration_seconds * 0.75
|
||||
|
||||
if is_mostly_silent:
|
||||
self.silence_duration += silence_duration
|
||||
else:
|
||||
self.conversation_started = True
|
||||
self.silence_duration = 0.0
|
||||
|
||||
def is_silent(self) -> bool:
|
||||
return self.silence_duration >= self.duration
|
||||
|
||||
def add_audio(self, audio: AudioFrame):
|
||||
self._add_audio_segment(self._to_audio_segment(audio.data))
|
||||
|
||||
def export_audio(self) -> BytesIO:
|
||||
buffer = BytesIO()
|
||||
if not self.audio_segments:
|
||||
return buffer
|
||||
|
||||
audio = self.audio_segments[0]
|
||||
for segment in self.audio_segments[1:]:
|
||||
audio += segment
|
||||
|
||||
audio.export(buffer, format="mp3", bitrate='92')
|
||||
return buffer
|
||||
|
||||
def reset(self):
|
||||
self.audio_segments.clear()
|
||||
self.duration = 0.0
|
||||
self.silence_duration = 0.0
|
||||
self.conversation_started = False
|
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
"manifest": {
|
||||
"package": "platypush.plugins.assistant.openai",
|
||||
"type": "plugin",
|
||||
"events": [
|
||||
"platypush.message.event.assistant.ConversationEndEvent",
|
||||
"platypush.message.event.assistant.ConversationStartEvent",
|
||||
"platypush.message.event.assistant.ConversationTimeoutEvent",
|
||||
"platypush.message.event.assistant.HotwordDetectedEvent",
|
||||
"platypush.message.event.assistant.IntentRecognizedEvent",
|
||||
"platypush.message.event.assistant.MicMutedEvent",
|
||||
"platypush.message.event.assistant.MicUnmutedEvent",
|
||||
"platypush.message.event.assistant.NoResponseEvent",
|
||||
"platypush.message.event.assistant.ResponseEndEvent",
|
||||
"platypush.message.event.assistant.ResponseEvent",
|
||||
"platypush.message.event.assistant.SpeechRecognizedEvent"
|
||||
],
|
||||
"install": {
|
||||
"apk": [
|
||||
"ffmpeg",
|
||||
"py3-numpy"
|
||||
],
|
||||
"apt": [
|
||||
"ffmpeg",
|
||||
"python3-numpy",
|
||||
"python3-pydub"
|
||||
],
|
||||
"dnf": [
|
||||
"ffmpeg",
|
||||
"python-numpy"
|
||||
],
|
||||
"pacman": [
|
||||
"ffmpeg",
|
||||
"python-numpy",
|
||||
"python-sounddevice"
|
||||
],
|
||||
"pip": [
|
||||
"numpy",
|
||||
"pydub",
|
||||
"sounddevice"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -530,12 +530,12 @@ class AssistantPicovoicePlugin(AssistantPlugin, RunnablePlugin):
|
|||
def _get_tts_plugin(self) -> TtsPicovoicePlugin:
|
||||
return self.tts
|
||||
|
||||
def _on_response_render_start(self, text: Optional[str]):
|
||||
def _on_response_render_start(self, text: Optional[str], *_, **__):
|
||||
if self._assistant:
|
||||
self._assistant.set_responding(True)
|
||||
return super()._on_response_render_start(text)
|
||||
|
||||
def _on_response_render_end(self):
|
||||
def _on_response_render_end(self, *_, **__):
|
||||
if self._assistant:
|
||||
self._assistant.set_responding(False)
|
||||
|
||||
|
@ -562,11 +562,8 @@ class AssistantPicovoicePlugin(AssistantPlugin, RunnablePlugin):
|
|||
self._assistant.override_speech_model(model_file)
|
||||
self._assistant.state = AssistantState.DETECTING_SPEECH
|
||||
|
||||
@action
|
||||
def stop_conversation(self, *_, **__):
|
||||
"""
|
||||
Programmatically stop a running conversation with the assistant
|
||||
"""
|
||||
def _stop_conversation(self, *_, **__):
|
||||
super()._stop_conversation()
|
||||
if not self._assistant:
|
||||
self.logger.warning('Assistant not initialized')
|
||||
return
|
||||
|
|
|
@ -7,6 +7,7 @@ from typing import Any, Dict, Optional, Sequence
|
|||
|
||||
import pvporcupine
|
||||
|
||||
from platypush.common.assistant import AudioRecorder
|
||||
from platypush.context import get_plugin
|
||||
from platypush.message.event.assistant import (
|
||||
AssistantEvent,
|
||||
|
@ -16,8 +17,6 @@ from platypush.message.event.assistant import (
|
|||
SpeechRecognizedEvent,
|
||||
)
|
||||
from platypush.plugins.tts.picovoice import TtsPicovoicePlugin
|
||||
|
||||
from ._recorder import AudioRecorder
|
||||
from ._speech import SpeechProcessor
|
||||
from ._state import AssistantState
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from datetime import datetime, timedelta
|
||||
from datetime import timedelta, timezone
|
||||
from logging import getLogger
|
||||
from queue import Queue
|
||||
from typing import Callable, Collection, Dict, Final, List, Optional, Type
|
||||
|
@ -16,6 +16,7 @@ from platypush.message.event.bluetooth import (
|
|||
BluetoothDeviceSignalUpdateEvent,
|
||||
BluetoothDeviceEvent,
|
||||
)
|
||||
from platypush.utils import utcnow
|
||||
|
||||
from .._cache import EntityCache
|
||||
from .._model import ServiceClass
|
||||
|
@ -98,7 +99,7 @@ event_matchers: Dict[
|
|||
)
|
||||
and (
|
||||
not (old and old.updated_at)
|
||||
or datetime.utcnow() - old.updated_at
|
||||
or utcnow() - old.updated_at.replace(tzinfo=timezone.utc)
|
||||
>= timedelta(seconds=_rssi_update_interval)
|
||||
)
|
||||
),
|
||||
|
@ -227,9 +228,11 @@ class EventHandler:
|
|||
return True
|
||||
|
||||
mapped_uuids = [
|
||||
int(str(srv.uuid).split('-', maxsplit=1)[0], 16) & 0xFFFF
|
||||
if isinstance(srv.uuid, UUID)
|
||||
else srv.uuid
|
||||
(
|
||||
int(str(srv.uuid).split('-', maxsplit=1)[0], 16) & 0xFFFF
|
||||
if isinstance(srv.uuid, UUID)
|
||||
else srv.uuid
|
||||
)
|
||||
for srv in device.services
|
||||
]
|
||||
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import datetime
|
||||
import requests
|
||||
from typing import Optional
|
||||
|
||||
import requests
|
||||
from dateutil.tz import gettz
|
||||
|
||||
from platypush.plugins import Plugin, action
|
||||
from platypush.plugins.calendar import CalendarInterface
|
||||
from platypush.utils import utcnow
|
||||
|
||||
|
||||
class CalendarIcalPlugin(Plugin, CalendarInterface):
|
||||
|
@ -20,19 +23,20 @@ class CalendarIcalPlugin(Plugin, CalendarInterface):
|
|||
self.url = url
|
||||
|
||||
@staticmethod
|
||||
def _convert_timestamp(event, attribute: str) -> Optional[str]:
|
||||
def _convert_timestamp(event: dict, attribute: str) -> Optional[str]:
|
||||
t = event.get(attribute)
|
||||
if not t:
|
||||
return
|
||||
return None
|
||||
|
||||
if type(t.dt) == datetime.date:
|
||||
if isinstance(t.dt, datetime.date) and not isinstance(t.dt, datetime.datetime):
|
||||
return datetime.datetime(
|
||||
t.dt.year, t.dt.month, t.dt.day, tzinfo=datetime.timezone.utc
|
||||
t.dt.year, t.dt.month, t.dt.day, tzinfo=gettz()
|
||||
).isoformat()
|
||||
|
||||
return (
|
||||
datetime.datetime.utcfromtimestamp(t.dt.timestamp())
|
||||
.replace(tzinfo=datetime.timezone.utc)
|
||||
datetime.datetime.fromtimestamp(t.dt.timestamp())
|
||||
.replace(tzinfo=t.dt.tzinfo or gettz())
|
||||
.astimezone(datetime.timezone.utc)
|
||||
.isoformat()
|
||||
)
|
||||
|
||||
|
@ -42,21 +46,23 @@ class CalendarIcalPlugin(Plugin, CalendarInterface):
|
|||
'id': str(event.get('uid')) if event.get('uid') else None,
|
||||
'kind': 'calendar#event',
|
||||
'summary': str(event.get('summary')) if event.get('summary') else None,
|
||||
'description': str(event.get('description'))
|
||||
if event.get('description')
|
||||
else None,
|
||||
'description': (
|
||||
str(event.get('description')) if event.get('description') else None
|
||||
),
|
||||
'status': str(event.get('status')).lower() if event.get('status') else None,
|
||||
'responseStatus': str(event.get('partstat')).lower()
|
||||
if event.get('partstat')
|
||||
else None,
|
||||
'responseStatus': (
|
||||
str(event.get('partstat')).lower() if event.get('partstat') else None
|
||||
),
|
||||
'location': str(event.get('location')) if event.get('location') else None,
|
||||
'htmlLink': str(event.get('url')) if event.get('url') else None,
|
||||
'organizer': {
|
||||
'email': str(event.get('organizer')).replace('MAILTO:', ''),
|
||||
'displayName': event.get('organizer').params.get('cn'),
|
||||
}
|
||||
if event.get('organizer')
|
||||
else None,
|
||||
'organizer': (
|
||||
{
|
||||
'email': str(event.get('organizer')).replace('MAILTO:', ''),
|
||||
'displayName': event.get('organizer').params.get('cn'),
|
||||
}
|
||||
if event.get('organizer')
|
||||
else None
|
||||
),
|
||||
'created': cls._convert_timestamp(event, 'created'),
|
||||
'updated': cls._convert_timestamp(event, 'last-modified'),
|
||||
'start': {
|
||||
|
@ -79,10 +85,10 @@ class CalendarIcalPlugin(Plugin, CalendarInterface):
|
|||
from icalendar import Calendar
|
||||
|
||||
events = []
|
||||
response = requests.get(self.url)
|
||||
assert response.ok, "HTTP error while getting events from {}: {}".format(
|
||||
self.url, response.text
|
||||
)
|
||||
response = requests.get(self.url, timeout=20)
|
||||
assert (
|
||||
response.ok
|
||||
), f"HTTP error while getting events from {self.url}: {response.text}"
|
||||
|
||||
calendar = Calendar.from_ical(response.text)
|
||||
for event in calendar.walk():
|
||||
|
@ -94,10 +100,8 @@ class CalendarIcalPlugin(Plugin, CalendarInterface):
|
|||
if (
|
||||
event['status'] != 'cancelled'
|
||||
and event['end'].get('dateTime')
|
||||
and event['end']['dateTime']
|
||||
>= datetime.datetime.utcnow()
|
||||
.replace(tzinfo=datetime.timezone.utc)
|
||||
.isoformat()
|
||||
and datetime.datetime.fromisoformat(event['end']['dateTime'])
|
||||
>= utcnow()
|
||||
and (
|
||||
(
|
||||
only_participating
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import datetime
|
||||
|
||||
from platypush.plugins import action
|
||||
from platypush.plugins.google import GooglePlugin
|
||||
from platypush.plugins.calendar import CalendarInterface
|
||||
from platypush.utils import utcnow
|
||||
|
||||
|
||||
class GoogleCalendarPlugin(GooglePlugin, CalendarInterface):
|
||||
|
@ -72,7 +71,7 @@ class GoogleCalendarPlugin(GooglePlugin, CalendarInterface):
|
|||
:meth:`platypush.plugins.calendar.CalendarPlugin.get_upcoming_events`.
|
||||
"""
|
||||
|
||||
now = datetime.datetime.utcnow().isoformat() + 'Z'
|
||||
now = utcnow().replace(tzinfo=None).isoformat() + 'Z'
|
||||
service = self.get_service('calendar', 'v3')
|
||||
result = (
|
||||
service.events()
|
||||
|
|
|
@ -17,8 +17,6 @@ from typing import (
|
|||
Set,
|
||||
Union,
|
||||
)
|
||||
import warnings
|
||||
|
||||
from platypush.config import Config
|
||||
from platypush.context import get_bus
|
||||
from platypush.entities import Entity, LightEntityManager
|
||||
|
@ -86,11 +84,7 @@ class LightHuePlugin(RunnablePlugin, LightEntityManager):
|
|||
|
||||
poll_seconds = kwargs.pop('poll_seconds', None)
|
||||
if poll_seconds is not None:
|
||||
warnings.warn(
|
||||
'poll_seconds is deprecated, use poll_interval instead',
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
self.logger.warning('poll_seconds is deprecated, use poll_interval instead')
|
||||
|
||||
if poll_interval is None:
|
||||
poll_interval = poll_seconds
|
||||
|
@ -1156,12 +1150,16 @@ class LightHuePlugin(RunnablePlugin, LightEntityManager):
|
|||
temperature=entity.get('state', {}).get('ct'),
|
||||
colormode=entity.get('colormode'),
|
||||
reachable=entity.get('state', {}).get('reachable'),
|
||||
x=entity['state']['xy'][0]
|
||||
if entity.get('state', {}).get('xy')
|
||||
else None,
|
||||
y=entity['state']['xy'][1]
|
||||
if entity.get('state', {}).get('xy')
|
||||
else None,
|
||||
x=(
|
||||
entity['state']['xy'][0]
|
||||
if entity.get('state', {}).get('xy')
|
||||
else None
|
||||
),
|
||||
y=(
|
||||
entity['state']['xy'][1]
|
||||
if entity.get('state', {}).get('xy')
|
||||
else None
|
||||
),
|
||||
effect=entity.get('state', {}).get('effect'),
|
||||
**(
|
||||
{
|
||||
|
|
|
@ -3,7 +3,7 @@ from dataclasses import dataclass
|
|||
from datetime import datetime as dt
|
||||
from enum import Enum
|
||||
from threading import RLock
|
||||
from typing import Iterable, List, Optional
|
||||
from typing import IO, Iterable, List, Optional
|
||||
|
||||
import requests
|
||||
|
||||
|
@ -49,7 +49,18 @@ class OpenaiPlugin(Plugin):
|
|||
"""
|
||||
Plugin to interact with OpenAI services.
|
||||
|
||||
So far only ChatGPT is supported.
|
||||
Currently supported:
|
||||
|
||||
- :meth:`get_response`: Get a response to a prompt/question using the
|
||||
GPT API. It supports custom contexts and environment settings.
|
||||
|
||||
- :meth:`transcribe`: Perform speech-to-text on an audio file. This API
|
||||
is also leveraged by the
|
||||
:class:`platypush.plugins.assistant.openai.OpenaiPlugin` to provide a
|
||||
full-fledged voice assistant.
|
||||
|
||||
- Through the :class:`platypush.plugins.tts.openai.OpenaiPlugin` plugin,
|
||||
text-to-speech is also supported.
|
||||
|
||||
Contexts
|
||||
--------
|
||||
|
@ -265,6 +276,84 @@ class OpenaiPlugin(Plugin):
|
|||
self._update_context(msg)
|
||||
return msg["content"]
|
||||
|
||||
def _process_transcribe_response(self, resp: requests.Response) -> str:
|
||||
rs_json = None
|
||||
|
||||
try:
|
||||
rs_json = resp.json()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.logger.debug("OpenAI response: %s", rs_json)
|
||||
resp.raise_for_status()
|
||||
return (rs_json or {}).get("text", "")
|
||||
|
||||
def transcribe_file(
|
||||
self,
|
||||
f: IO,
|
||||
model: Optional[str] = 'whisper-1',
|
||||
timeout: Optional[float] = None,
|
||||
) -> str:
|
||||
resp = requests.post(
|
||||
"https://api.openai.com/v1/audio/transcriptions",
|
||||
timeout=timeout or self.timeout,
|
||||
headers={
|
||||
"Authorization": f"Bearer {self._api_key}",
|
||||
},
|
||||
files={
|
||||
"file": f,
|
||||
},
|
||||
data={
|
||||
"model": model or self.model,
|
||||
},
|
||||
)
|
||||
|
||||
return self._process_transcribe_response(resp)
|
||||
|
||||
def transcribe_raw(
|
||||
self,
|
||||
audio: bytes,
|
||||
extension: str,
|
||||
model: Optional[str] = 'whisper-1',
|
||||
timeout: Optional[float] = None,
|
||||
) -> str:
|
||||
resp = requests.post(
|
||||
"https://api.openai.com/v1/audio/transcriptions",
|
||||
timeout=timeout or self.timeout,
|
||||
headers={
|
||||
"Authorization": f"Bearer {self._api_key}",
|
||||
},
|
||||
files={
|
||||
"file": (f"audio.{extension}", audio),
|
||||
},
|
||||
data={
|
||||
"model": model or self.model,
|
||||
},
|
||||
)
|
||||
|
||||
return self._process_transcribe_response(resp)
|
||||
|
||||
@action
|
||||
def transcribe(
|
||||
self,
|
||||
audio: str,
|
||||
model: Optional[str] = 'whisper-1',
|
||||
timeout: Optional[float] = None,
|
||||
) -> str:
|
||||
"""
|
||||
Perform speech-to-text on an audio file.
|
||||
|
||||
:param audio: The audio file to transcribe.
|
||||
:param model: The model to use for speech-to-text. Default:
|
||||
``whisper-1``. If not set, the configured default model will be
|
||||
used.
|
||||
:param timeout: Timeout for the API request. If not set, the default
|
||||
timeout will be used.
|
||||
:return: The transcribed text.
|
||||
"""
|
||||
with open(os.path.expanduser(audio), "rb") as f:
|
||||
return self.transcribe_file(f, model=model, timeout=timeout)
|
||||
|
||||
def _update_context(self, *entries: dict):
|
||||
"""
|
||||
Update the context with a new entry.
|
||||
|
|
|
@ -16,6 +16,7 @@ from platypush.message.event.rss import NewFeedEntryEvent
|
|||
from platypush.plugins import RunnablePlugin, action
|
||||
from platypush.plugins.variable import VariablePlugin
|
||||
from platypush.schemas.rss import RssFeedEntrySchema
|
||||
from platypush.utils import utcnow
|
||||
|
||||
|
||||
def _variable() -> VariablePlugin:
|
||||
|
@ -289,7 +290,7 @@ class RssPlugin(RunnablePlugin):
|
|||
title = ElementTree.Element('title')
|
||||
title.text = 'Platypush feed subscriptions'
|
||||
created = ElementTree.Element('dateCreated')
|
||||
created.text = self._datetime_to_string(datetime.datetime.utcnow())
|
||||
created.text = self._datetime_to_string(utcnow())
|
||||
head.append(title)
|
||||
head.append(created)
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
from collections.abc import Collection
|
||||
import time
|
||||
from typing import List, Optional, Union
|
||||
import warnings
|
||||
|
||||
from platypush.context import get_bus
|
||||
from platypush.entities.distance import DistanceSensor
|
||||
|
@ -45,10 +44,8 @@ class SensorHcsr04Plugin(GpioPlugin, SensorPlugin):
|
|||
|
||||
measurement_interval = kwargs.pop('measurement_interval', None)
|
||||
if measurement_interval is not None:
|
||||
warnings.warn(
|
||||
self.logger.warning(
|
||||
'measurement_interval is deprecated, use poll_interval instead',
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
poll_interval = measurement_interval
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
from dataclasses import asdict
|
||||
import warnings
|
||||
from typing import Iterable, List, Optional, Union
|
||||
|
||||
from platypush.plugins import RunnablePlugin, action
|
||||
|
@ -184,10 +183,8 @@ class SoundPlugin(RunnablePlugin):
|
|||
blocksize = blocksize or self.output_blocksize
|
||||
|
||||
if file:
|
||||
warnings.warn(
|
||||
self.logger.warning(
|
||||
'file is deprecated, use resource instead',
|
||||
DeprecationWarning,
|
||||
stacklevel=1,
|
||||
)
|
||||
if not resource:
|
||||
resource = file
|
||||
|
@ -232,10 +229,8 @@ class SoundPlugin(RunnablePlugin):
|
|||
"""
|
||||
Deprecated alias for :meth:`.record`.
|
||||
"""
|
||||
warnings.warn(
|
||||
self.logger.warning(
|
||||
'sound.stream_recording is deprecated, use sound.record instead',
|
||||
DeprecationWarning,
|
||||
stacklevel=1,
|
||||
)
|
||||
|
||||
return self.record(*args, **kwargs)
|
||||
|
@ -319,10 +314,8 @@ class SoundPlugin(RunnablePlugin):
|
|||
"""
|
||||
Deprecated alias for :meth:`.record`.
|
||||
"""
|
||||
warnings.warn(
|
||||
self.logger.warning(
|
||||
'sound.recordplay is deprecated, use sound.record with `play_audio=True` instead',
|
||||
DeprecationWarning,
|
||||
stacklevel=1,
|
||||
)
|
||||
|
||||
kwargs['play_audio'] = True
|
||||
|
@ -398,10 +391,8 @@ class SoundPlugin(RunnablePlugin):
|
|||
Deprecated alias for :meth:`.status`.
|
||||
"""
|
||||
|
||||
warnings.warn(
|
||||
self.logger.warning(
|
||||
'sound.query_streams is deprecated, use sound.status instead',
|
||||
DeprecationWarning,
|
||||
stacklevel=1,
|
||||
)
|
||||
|
||||
return self.status()
|
||||
|
|
|
@ -7,6 +7,7 @@ from dateutil.tz import gettz
|
|||
from platypush.message.event.sun import SunriseEvent, SunsetEvent
|
||||
from platypush.plugins import RunnablePlugin, action
|
||||
from platypush.schemas.sun import SunEventsSchema
|
||||
from platypush.utils import utcnow
|
||||
|
||||
|
||||
class SunPlugin(RunnablePlugin):
|
||||
|
@ -63,11 +64,11 @@ class SunPlugin(RunnablePlugin):
|
|||
|
||||
@staticmethod
|
||||
def _convert_time(t: str) -> datetime.datetime:
|
||||
now = datetime.datetime.now(datetime.UTC).replace(microsecond=0)
|
||||
now = utcnow().replace(microsecond=0)
|
||||
dt = datetime.datetime.strptime(
|
||||
f'{now.year}-{now.month:02d}-{now.day:02d} {t}',
|
||||
'%Y-%m-%d %I:%M:%S %p',
|
||||
).replace(tzinfo=datetime.UTC)
|
||||
).replace(tzinfo=datetime.timezone.utc)
|
||||
|
||||
if dt < now:
|
||||
dt += datetime.timedelta(days=1)
|
||||
|
|
|
@ -55,5 +55,18 @@ class TtsPlugin(Plugin):
|
|||
|
||||
self._playback(url, **player_args)
|
||||
|
||||
@action
|
||||
def stop(self):
|
||||
"""
|
||||
Stop the playback.
|
||||
"""
|
||||
try:
|
||||
audio = get_plugin('sound')
|
||||
except Exception:
|
||||
return
|
||||
|
||||
if audio:
|
||||
audio.stop_playback()
|
||||
|
||||
|
||||
# vim:sw=4:ts=4:et:
|
||||
|
|
|
@ -0,0 +1,150 @@
|
|||
import os
|
||||
import tempfile
|
||||
from contextlib import contextmanager
|
||||
from multiprocessing import Process
|
||||
from typing import Generator, Optional
|
||||
|
||||
import requests
|
||||
|
||||
from platypush.context import get_plugin
|
||||
from platypush.plugins import action
|
||||
from platypush.plugins.openai import OpenaiPlugin
|
||||
from platypush.plugins.tts import TtsPlugin
|
||||
|
||||
|
||||
class TtsOpenaiPlugin(TtsPlugin):
|
||||
r"""
|
||||
This plugin provides an interface to the `OpenAI text-to-speech API
|
||||
<https://platform.openai.com/docs/guides/text-to-speech>`_.
|
||||
|
||||
It requires the :class:`platypush.plugins.openai.OpenaiPlugin` plugin to be
|
||||
configured.
|
||||
"""
|
||||
|
||||
_BUFSIZE = 1024
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model: str = 'tts-1',
|
||||
voice: str = 'nova',
|
||||
timeout: float = 10,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
:param model: Model to be used for the text-to-speech conversion.
|
||||
See the `OpenAI API models documentation
|
||||
<https://platform.openai.com/docs/models/tts>`_ for the list of
|
||||
available models (default: ``tts-1``).
|
||||
:param voice: Default voice to be used. See the `OpenAI API
|
||||
voices documentation
|
||||
<https://platform.openai.com/docs/guides/text-to-speech/voice-options>`_
|
||||
for the list of available voices (default: ``nova``).
|
||||
:param timeout: Default timeout for the API requests (default: 10s).
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
openai = get_plugin('openai')
|
||||
assert openai, 'openai plugin not configured'
|
||||
|
||||
self.openai: OpenaiPlugin = openai
|
||||
self.model = model
|
||||
self.voice = voice
|
||||
self.timeout = timeout
|
||||
self._audio_proc: Optional[Process] = None
|
||||
|
||||
def _process_response(
|
||||
self,
|
||||
response: requests.Response,
|
||||
audio_file: str,
|
||||
) -> Process:
|
||||
def proc_fn():
|
||||
try:
|
||||
with open(audio_file, 'wb') as file:
|
||||
for chunk in response.iter_content(chunk_size=self._BUFSIZE):
|
||||
if chunk:
|
||||
file.write(chunk)
|
||||
file.flush()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
self._audio_proc = Process(target=proc_fn, name='openai-tts-response-processor')
|
||||
self._audio_proc.start()
|
||||
return self._audio_proc
|
||||
|
||||
def _make_request(
|
||||
self,
|
||||
text: str,
|
||||
model: Optional[str] = None,
|
||||
voice: Optional[str] = None,
|
||||
) -> requests.Response:
|
||||
rs = requests.post(
|
||||
"https://api.openai.com/v1/audio/speech",
|
||||
timeout=self.timeout,
|
||||
stream=True,
|
||||
headers={
|
||||
"Authorization": f"Bearer {self.openai._api_key}", # pylint: disable=protected-access
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
json={
|
||||
"model": model or self.model,
|
||||
"voice": voice or self.voice,
|
||||
"input": text,
|
||||
},
|
||||
)
|
||||
|
||||
rs.raise_for_status()
|
||||
return rs
|
||||
|
||||
@contextmanager
|
||||
def _audio_fifo(self) -> Generator[str, None, None]:
|
||||
fifo_dir = tempfile.mkdtemp()
|
||||
fifo_path = os.path.join(fifo_dir, 'platypush-tts-openai-fifo')
|
||||
os.mkfifo(fifo_path)
|
||||
yield fifo_path
|
||||
|
||||
os.unlink(fifo_path)
|
||||
os.rmdir(fifo_dir)
|
||||
|
||||
@action
|
||||
def say(
|
||||
self,
|
||||
text: str,
|
||||
*_,
|
||||
model: Optional[str] = None,
|
||||
voice: Optional[str] = None,
|
||||
**player_args,
|
||||
):
|
||||
"""
|
||||
Say some text.
|
||||
|
||||
:param text: Text to say.
|
||||
:param model: Default ``model`` override.
|
||||
:param voice: Default ``voice`` override.
|
||||
:param player_args: Extends the additional arguments to be passed to
|
||||
:meth:`platypush.plugins.sound.SoundPlugin.play` (like volume,
|
||||
duration, channels etc.).
|
||||
"""
|
||||
response_processor: Optional[Process] = None
|
||||
|
||||
try:
|
||||
response = self._make_request(text, model=model, voice=voice)
|
||||
|
||||
with self._audio_fifo() as audio_file:
|
||||
response_processor = self._process_response(
|
||||
response=response, audio_file=audio_file
|
||||
)
|
||||
self._playback(audio_file, **player_args)
|
||||
response_processor.join()
|
||||
response_processor = None
|
||||
finally:
|
||||
if response_processor:
|
||||
response_processor.terminate()
|
||||
|
||||
@action
|
||||
def stop(self):
|
||||
super().stop()
|
||||
if self._audio_proc and self._audio_proc.is_alive():
|
||||
self._audio_proc.terminate()
|
||||
self._audio_proc.join()
|
||||
|
||||
|
||||
# vim:sw=4:ts=4:et:
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"manifest": {
|
||||
"events": {},
|
||||
"install": {
|
||||
"apk": [
|
||||
"ffmpeg",
|
||||
"portaudio-dev",
|
||||
"py3-numpy"
|
||||
],
|
||||
"apt": [
|
||||
"ffmpeg",
|
||||
"portaudio19-dev",
|
||||
"python3-numpy"
|
||||
],
|
||||
"dnf": [
|
||||
"ffmpeg",
|
||||
"portaudio-devel",
|
||||
"python-numpy"
|
||||
],
|
||||
"pacman": [
|
||||
"ffmpeg",
|
||||
"portaudio",
|
||||
"python-numpy",
|
||||
"python-sounddevice"
|
||||
],
|
||||
"pip": [
|
||||
"numpy",
|
||||
"sounddevice"
|
||||
]
|
||||
},
|
||||
"package": "platypush.plugins.tts.openai",
|
||||
"type": "plugin"
|
||||
}
|
||||
}
|
|
@ -178,7 +178,7 @@ class TtsPicovoicePlugin(TtsPlugin):
|
|||
self._play_audio(
|
||||
orca=orca,
|
||||
pcm=np.array(
|
||||
orca.synthesize(text, speech_rate=speech_rate),
|
||||
orca.synthesize(text, speech_rate=speech_rate)[0],
|
||||
dtype='int16',
|
||||
),
|
||||
)
|
||||
|
|
|
@ -377,9 +377,11 @@ class ZwaveMqttPlugin(
|
|||
'device_id': device_id.replace('0x', ''),
|
||||
'name': node.get('name'),
|
||||
'capabilities': capabilities,
|
||||
'manufacturer_id': f'0x{node["manufacturerId"]:04x}'
|
||||
if node.get('manufacturerId')
|
||||
else None,
|
||||
'manufacturer_id': (
|
||||
f'0x{node["manufacturerId"]:04x}'
|
||||
if node.get('manufacturerId')
|
||||
else None
|
||||
),
|
||||
'manufacturer_name': node.get('manufacturer'),
|
||||
'location': node.get('loc'),
|
||||
'status': node.get('status'),
|
||||
|
@ -397,12 +399,12 @@ class ZwaveMqttPlugin(
|
|||
'is_security_device': node.get('supportsSecurity'),
|
||||
'is_sleeping': node.get('ready') and node.get('status') == 'Asleep',
|
||||
'last_update': cls._convert_timestamp(node.get('lastActive')),
|
||||
'product_id': f'0x{node["productId"]:04x}'
|
||||
if node.get('productId')
|
||||
else None,
|
||||
'product_type': f'0x{node["productType"]:04x}'
|
||||
if node.get('productType')
|
||||
else None,
|
||||
'product_id': (
|
||||
f'0x{node["productId"]:04x}' if node.get('productId') else None
|
||||
),
|
||||
'product_type': (
|
||||
f'0x{node["productType"]:04x}' if node.get('productType') else None
|
||||
),
|
||||
'product_name': ' '.join(
|
||||
[node.get('productLabel', ''), node.get('productDescription', '')]
|
||||
),
|
||||
|
@ -1048,7 +1050,7 @@ class ZwaveMqttPlugin(
|
|||
"""
|
||||
Get the current status of the Z-Wave values.
|
||||
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish`
|
||||
(default: query the default configured device).
|
||||
"""
|
||||
return self._filter_values(**kwargs)
|
||||
|
@ -1058,7 +1060,7 @@ class ZwaveMqttPlugin(
|
|||
"""
|
||||
Get the status of the controller.
|
||||
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish`
|
||||
(default: query the default configured device).
|
||||
"""
|
||||
msg_queue: queue.Queue = queue.Queue()
|
||||
|
@ -1105,7 +1107,7 @@ class ZwaveMqttPlugin(
|
|||
:param do_security: Whether to initialize the Network Key on the device if it supports the Security CC
|
||||
:param timeout: How long the inclusion process should last, in seconds (default: 30). Specify zero or null
|
||||
for no timeout.
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish`
|
||||
(default: query the default configured device).
|
||||
"""
|
||||
self._api_request(
|
||||
|
@ -1128,7 +1130,7 @@ class ZwaveMqttPlugin(
|
|||
Remove a node from the network (or, better, start the exclusion process).
|
||||
|
||||
:param timeout: How long the exclusion process should last, in seconds (default: 30).
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish`
|
||||
(default: query the default configured device).
|
||||
"""
|
||||
self._api_request('startExclusion', **kwargs)
|
||||
|
@ -1144,7 +1146,7 @@ class ZwaveMqttPlugin(
|
|||
|
||||
:param node_id: Filter by node_id.
|
||||
:param node_name: Filter by node name.
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish`
|
||||
(default: query the default configured device).
|
||||
"""
|
||||
if node_name:
|
||||
|
@ -1162,7 +1164,7 @@ class ZwaveMqttPlugin(
|
|||
|
||||
:param node_id: Filter by node_id.
|
||||
:param node_name: Filter by node name.
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish`
|
||||
(default: query the default configured device).
|
||||
"""
|
||||
if node_name:
|
||||
|
@ -1180,7 +1182,7 @@ class ZwaveMqttPlugin(
|
|||
|
||||
:param node_id: Filter by node_id.
|
||||
:param node_name: Filter by node name.
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish`
|
||||
(default: query the default configured device).
|
||||
"""
|
||||
if node_name:
|
||||
|
@ -1194,7 +1196,7 @@ class ZwaveMqttPlugin(
|
|||
"""
|
||||
Request a neighbours list update.
|
||||
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish`
|
||||
(default: query the default configured device).
|
||||
"""
|
||||
self._api_request('refreshNeighbors', **kwargs)
|
||||
|
@ -1208,7 +1210,7 @@ class ZwaveMqttPlugin(
|
|||
|
||||
:param node_id: Filter by node_id.
|
||||
:param node_name: Filter by node name.
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish`
|
||||
(default: query the default configured device).
|
||||
|
||||
:return: List of paired devices. Example output:
|
||||
|
@ -1362,7 +1364,7 @@ class ZwaveMqttPlugin(
|
|||
}
|
||||
}
|
||||
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish`
|
||||
(default: query the default configured device).
|
||||
"""
|
||||
if node_id or node_name:
|
||||
|
@ -1386,7 +1388,7 @@ class ZwaveMqttPlugin(
|
|||
:param new_name: New name for the node.
|
||||
:param node_id: Filter by node_id.
|
||||
:param node_name: Filter by current node name.
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish`
|
||||
(default: query the default configured device).
|
||||
"""
|
||||
if node_name:
|
||||
|
@ -1417,7 +1419,7 @@ class ZwaveMqttPlugin(
|
|||
:param location: Node location.
|
||||
:param node_id: Filter by node_id.
|
||||
:param node_name: Filter by current node name.
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish`
|
||||
(default: query the default configured device).
|
||||
"""
|
||||
if node_name:
|
||||
|
@ -1442,7 +1444,7 @@ class ZwaveMqttPlugin(
|
|||
Heal network by requesting nodes rediscover their neighbours.
|
||||
|
||||
:param timeout: Duration of the healing process in seconds (default: 60). Set to zero or null for no timeout.
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish`
|
||||
(default: query the default configured device).
|
||||
"""
|
||||
self._api_request('beginHealingNetwork', **kwargs)
|
||||
|
@ -1469,7 +1471,7 @@ class ZwaveMqttPlugin(
|
|||
:param value_label: Select value by [node_id/node_name, value_label]
|
||||
:param node_id: Select value by [node_id/node_name, value_label]
|
||||
:param node_name: Select value by [node_id/node_name, value_label]
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish`
|
||||
(default: query the default configured device).
|
||||
"""
|
||||
return self._get_value(
|
||||
|
@ -1503,7 +1505,7 @@ class ZwaveMqttPlugin(
|
|||
:param value_label: Select value by [node_id/node_name, value_label]
|
||||
:param node_id: Select value by [node_id/node_name, value_label]
|
||||
:param node_name: Select value by [node_id/node_name, value_label]
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish`
|
||||
(default: query the default configured device).
|
||||
"""
|
||||
# Compatibility layer with the .set_value format used by
|
||||
|
@ -1558,7 +1560,7 @@ class ZwaveMqttPlugin(
|
|||
|
||||
:param node_id: Select node by node_id.
|
||||
:param node_name: Select node by label.
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish`
|
||||
(default: query the default configured device).
|
||||
"""
|
||||
if node_name:
|
||||
|
@ -1575,7 +1577,7 @@ class ZwaveMqttPlugin(
|
|||
|
||||
:param node_id: Select node by node_id.
|
||||
:param node_name: Select node by label.
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish`
|
||||
(default: query the default configured device).
|
||||
"""
|
||||
self.node_heal(node_id=node_id, node_name=node_name, **kwargs)
|
||||
|
@ -1590,7 +1592,7 @@ class ZwaveMqttPlugin(
|
|||
|
||||
:param node_id: Select node by node_id.
|
||||
:param node_name: Select node by label.
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish`
|
||||
(default: query the default configured device).
|
||||
"""
|
||||
self.node_heal(node_id=node_id, node_name=node_name, **kwargs)
|
||||
|
@ -1604,7 +1606,7 @@ class ZwaveMqttPlugin(
|
|||
|
||||
:param node_id: Select node by node_id.
|
||||
:param node_name: Select node by label.
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish`
|
||||
(default: query the default configured device).
|
||||
"""
|
||||
if node_name:
|
||||
|
@ -1620,7 +1622,7 @@ class ZwaveMqttPlugin(
|
|||
|
||||
:param node_id: Select node by node_id.
|
||||
:param node_name: Select node by label.
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish`
|
||||
(default: query the default configured device).
|
||||
"""
|
||||
return self._filter_values(
|
||||
|
@ -1639,7 +1641,7 @@ class ZwaveMqttPlugin(
|
|||
|
||||
:param node_id: Select node by node_id.
|
||||
:param node_name: Select node by label.
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish`
|
||||
(default: query the default configured device).
|
||||
"""
|
||||
return self._filter_values(
|
||||
|
@ -1663,7 +1665,7 @@ class ZwaveMqttPlugin(
|
|||
|
||||
:param node_id: Select node by node_id.
|
||||
:param node_name: Select node by name.
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish`
|
||||
(default: query the default configured device).
|
||||
"""
|
||||
return self._filter_values(
|
||||
|
@ -1679,7 +1681,7 @@ class ZwaveMqttPlugin(
|
|||
|
||||
:param node_id: Select node by node_id.
|
||||
:param node_name: Select node by name.
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish`
|
||||
(default: query the default configured device).
|
||||
"""
|
||||
return self._filter_values(
|
||||
|
@ -1695,7 +1697,7 @@ class ZwaveMqttPlugin(
|
|||
|
||||
:param node_id: Select node by node_id.
|
||||
:param node_name: Select node by name.
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish`
|
||||
(default: query the default configured device).
|
||||
"""
|
||||
return self._filter_values(
|
||||
|
@ -1715,7 +1717,7 @@ class ZwaveMqttPlugin(
|
|||
|
||||
:param node_id: Select node by node_id.
|
||||
:param node_name: Select node by name.
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish`
|
||||
(default: query the default configured device).
|
||||
"""
|
||||
return self._filter_values(
|
||||
|
@ -1735,7 +1737,7 @@ class ZwaveMqttPlugin(
|
|||
|
||||
:param node_id: Select node by node_id.
|
||||
:param node_name: Select node by name.
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish`
|
||||
(default: query the default configured device).
|
||||
"""
|
||||
return self._filter_values(
|
||||
|
@ -1755,7 +1757,7 @@ class ZwaveMqttPlugin(
|
|||
|
||||
:param node_id: Select node by node_id.
|
||||
:param node_name: Select node by name.
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish`
|
||||
(default: query the default configured device).
|
||||
"""
|
||||
return self._filter_values(
|
||||
|
@ -1771,7 +1773,7 @@ class ZwaveMqttPlugin(
|
|||
|
||||
:param node_id: Select node by node_id.
|
||||
:param node_name: Select node by name.
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish`
|
||||
(default: query the default configured device).
|
||||
"""
|
||||
return self._filter_values(
|
||||
|
@ -1787,7 +1789,7 @@ class ZwaveMqttPlugin(
|
|||
|
||||
:param node_id: Select node by node_id.
|
||||
:param node_name: Select node by name.
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish`
|
||||
(default: query the default configured device).
|
||||
"""
|
||||
return self._filter_values(
|
||||
|
@ -1803,7 +1805,7 @@ class ZwaveMqttPlugin(
|
|||
|
||||
:param node_id: Select node by node_id.
|
||||
:param node_name: Select node by name.
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish`
|
||||
(default: query the default configured device).
|
||||
"""
|
||||
return self._filter_values(
|
||||
|
@ -1830,7 +1832,7 @@ class ZwaveMqttPlugin(
|
|||
|
||||
:param node_id: Select node by node_id.
|
||||
:param node_name: Select node by name.
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish`
|
||||
(default: query the default configured device).
|
||||
"""
|
||||
return self._filter_values(
|
||||
|
@ -1842,7 +1844,7 @@ class ZwaveMqttPlugin(
|
|||
"""
|
||||
Get the groups on the network.
|
||||
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish`
|
||||
(default: query the default configured device).
|
||||
|
||||
:return: A list of the available groups. Example:
|
||||
|
@ -1885,7 +1887,7 @@ class ZwaveMqttPlugin(
|
|||
"""
|
||||
Get the scenes configured on the network.
|
||||
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish`
|
||||
(default: query the default configured device).
|
||||
:return: dict with the following format:
|
||||
|
||||
|
@ -1919,7 +1921,7 @@ class ZwaveMqttPlugin(
|
|||
Create a new scene.
|
||||
|
||||
:param label: Scene label.
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish`
|
||||
(default: query the default configured device).
|
||||
"""
|
||||
self._api_request('_createScene', label, **kwargs)
|
||||
|
@ -1936,7 +1938,7 @@ class ZwaveMqttPlugin(
|
|||
|
||||
:param scene_id: Select by scene_id.
|
||||
:param scene_label: Select by scene label.
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish`
|
||||
(default: query the default configured device).
|
||||
"""
|
||||
scene = self._get_scene(scene_id=scene_id, scene_label=scene_label, **kwargs)
|
||||
|
@ -1954,7 +1956,7 @@ class ZwaveMqttPlugin(
|
|||
|
||||
:param scene_id: Select by scene_id.
|
||||
:param scene_label: Select by scene label.
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish`
|
||||
(default: query the default configured device).
|
||||
"""
|
||||
scene = self._get_scene(scene_id=scene_id, scene_label=scene_label, **kwargs)
|
||||
|
@ -1984,7 +1986,7 @@ class ZwaveMqttPlugin(
|
|||
:param node_name: Select value by [node_id/node_name, value_label]
|
||||
:param scene_id: Select scene by scene_id.
|
||||
:param scene_label: Select scene by scene label.
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish`
|
||||
(default: query the default configured device).
|
||||
"""
|
||||
value = self._get_value(
|
||||
|
@ -2032,7 +2034,7 @@ class ZwaveMqttPlugin(
|
|||
:param node_name: Select value by [node_id/node_name, value_label]
|
||||
:param scene_id: Select scene by scene_id.
|
||||
:param scene_label: Select scene by scene label.
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish`
|
||||
(default: query the default configured device).
|
||||
"""
|
||||
value = self._get_value(
|
||||
|
@ -2058,7 +2060,7 @@ class ZwaveMqttPlugin(
|
|||
|
||||
:param scene_id: Select by scene_id.
|
||||
:param scene_label: Select by scene label.
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish`
|
||||
(default: query the default configured device).
|
||||
"""
|
||||
scene = self._get_scene(scene_id=scene_id, scene_label=scene_label, **kwargs)
|
||||
|
@ -2078,7 +2080,7 @@ class ZwaveMqttPlugin(
|
|||
:param group_id: Group ID.
|
||||
:param node_id: Node ID to be added.
|
||||
:param endpoint: Add a specific endpoint of the node to the group (default: add a node association).
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish`
|
||||
(default: query the default configured device).
|
||||
"""
|
||||
group = self._get_group(group_id, **kwargs)
|
||||
|
@ -2102,7 +2104,7 @@ class ZwaveMqttPlugin(
|
|||
:param group_id: Group ID.
|
||||
:param node_id: Node ID to be added.
|
||||
:param endpoint: Node endpoint to remove (default: remove node association).
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish`
|
||||
(default: query the default configured device).
|
||||
"""
|
||||
group = self._get_group(group_id, **kwargs)
|
||||
|
@ -2128,7 +2130,7 @@ class ZwaveMqttPlugin(
|
|||
Turn on a switch on a device.
|
||||
|
||||
:param device: ``id_on_network`` of the value to be switched on.
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish`
|
||||
(default: query the default configured device).
|
||||
"""
|
||||
self.set_value(data=True, id_on_network=device, **kwargs)
|
||||
|
@ -2139,7 +2141,7 @@ class ZwaveMqttPlugin(
|
|||
Turn off a switch on a device.
|
||||
|
||||
:param device: ``id_on_network`` of the value to be switched off.
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish`
|
||||
(default: query the default configured device).
|
||||
"""
|
||||
self.set_value(data=False, id_on_network=device, **kwargs)
|
||||
|
@ -2152,7 +2154,7 @@ class ZwaveMqttPlugin(
|
|||
Toggle a switch on a device.
|
||||
|
||||
:param device: ``id_on_network`` of the value to be toggled.
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish`
|
||||
(default: query the default configured device).
|
||||
"""
|
||||
value = self._get_value(id_on_network=device, use_cache=False, **kwargs)
|
||||
|
|
|
@ -558,7 +558,9 @@ class IfProcedure(Procedure):
|
|||
return response
|
||||
|
||||
|
||||
def procedure(name: Optional[str] = None):
|
||||
def procedure(name_or_func: Optional[str] = None, *upper_args, **upper_kwargs):
|
||||
name = name_or_func if isinstance(name_or_func, str) else None
|
||||
|
||||
def func_wrapper(f):
|
||||
"""
|
||||
Public decorator to mark a function as a procedure.
|
||||
|
@ -569,10 +571,15 @@ def procedure(name: Optional[str] = None):
|
|||
|
||||
@wraps(f)
|
||||
def _execute_procedure(*args, **kwargs):
|
||||
args = [*upper_args, *args]
|
||||
kwargs = {**upper_kwargs, **kwargs}
|
||||
return exec_wrapper(f, *args, **kwargs)
|
||||
|
||||
return _execute_procedure
|
||||
|
||||
if callable(name_or_func):
|
||||
return func_wrapper(name_or_func)
|
||||
|
||||
return func_wrapper
|
||||
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import warnings
|
||||
import logging
|
||||
|
||||
from marshmallow import Schema, fields, pre_dump, post_dump
|
||||
|
||||
|
@ -6,6 +6,8 @@ from platypush.context import get_plugin
|
|||
|
||||
from . import MediaArtistSchema, MediaCollectionSchema, MediaVideoSchema
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class JellyfinSchema(Schema):
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
@ -20,9 +22,10 @@ class JellyfinSchema(Schema):
|
|||
@post_dump
|
||||
def gen_img_url(self, data: dict, **_) -> dict:
|
||||
if 'image' in self.fields:
|
||||
plugin = get_plugin('media.jellyfin')
|
||||
assert plugin, 'The media.jellyfin plugin is not configured'
|
||||
data['image'] = (
|
||||
get_plugin('media.jellyfin').server
|
||||
+ f'/Items/{data["id"]}' # type: ignore
|
||||
plugin.server + f'/Items/{data["id"]}' # type: ignore
|
||||
'/Images/Primary?fillHeight=333&fillWidth=222&quality=96'
|
||||
)
|
||||
|
||||
|
@ -43,9 +46,8 @@ class JellyfinSchema(Schema):
|
|||
|
||||
if not video_format:
|
||||
if not available_containers:
|
||||
warnings.warn(
|
||||
f'The media ID {data["Id"]} has no available video containers',
|
||||
stacklevel=2,
|
||||
logger.warning(
|
||||
'The media ID %s has no available video containers', data["Id"]
|
||||
)
|
||||
|
||||
return data
|
||||
|
|
|
@ -18,7 +18,7 @@ from platypush.exceptions.user import (
|
|||
InvalidJWTTokenException,
|
||||
InvalidCredentialsException,
|
||||
)
|
||||
from platypush.utils import get_or_generate_jwt_rsa_key_pair
|
||||
from platypush.utils import get_or_generate_jwt_rsa_key_pair, utcnow
|
||||
|
||||
|
||||
class UserManager:
|
||||
|
@ -78,7 +78,7 @@ class UserManager:
|
|||
),
|
||||
password_salt=password_salt.hex(),
|
||||
hmac_iterations=hmac_iterations,
|
||||
created_at=datetime.datetime.utcnow(),
|
||||
created_at=utcnow(),
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
@ -115,10 +115,13 @@ class UserManager:
|
|||
.first()
|
||||
)
|
||||
|
||||
if not user_session or (
|
||||
user_session.expires_at
|
||||
and user_session.expires_at < datetime.datetime.utcnow()
|
||||
):
|
||||
expires_at = (
|
||||
user_session.expires_at.replace(tzinfo=datetime.timezone.utc)
|
||||
if user_session and user_session.expires_at
|
||||
else None
|
||||
)
|
||||
|
||||
if not user_session or (expires_at and expires_at < utcnow()):
|
||||
return None, None
|
||||
|
||||
user = session.query(User).filter_by(user_id=user_session.user_id).first()
|
||||
|
@ -171,7 +174,7 @@ class UserManager:
|
|||
user_id=user.user_id,
|
||||
session_token=self.generate_session_token(),
|
||||
csrf_token=self.generate_session_token(),
|
||||
created_at=datetime.datetime.utcnow(),
|
||||
created_at=utcnow(),
|
||||
expires_at=expires_at,
|
||||
)
|
||||
|
||||
|
|
|
@ -814,4 +814,12 @@ def wait_for_either(*events, timeout: Optional[float] = None, cls: Type = Event)
|
|||
return OrEvent(*events, cls=cls).wait(timeout=timeout)
|
||||
|
||||
|
||||
def utcnow():
|
||||
"""
|
||||
utcnow() without tears. It always returns a datetime object in UTC
|
||||
timezone.
|
||||
"""
|
||||
return datetime.datetime.now(datetime.timezone.utc)
|
||||
|
||||
|
||||
# vim:sw=4:ts=4:et:
|
||||
|
|
|
@ -28,6 +28,7 @@ mock_imports = [
|
|||
"dbus",
|
||||
"deepspeech",
|
||||
"defusedxml",
|
||||
"dns",
|
||||
"docutils",
|
||||
"envirophat",
|
||||
"feedparser",
|
||||
|
@ -92,6 +93,7 @@ mock_imports = [
|
|||
"pychromecast",
|
||||
"pyclip",
|
||||
"pydbus",
|
||||
"pydub",
|
||||
"pyfirmata2",
|
||||
"pyngrok",
|
||||
"pyotp",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
[bumpversion]
|
||||
current_version = 0.99.0
|
||||
current_version = 1.1.0
|
||||
commit = True
|
||||
tag = True
|
||||
|
||||
|
|
5
setup.py
5
setup.py
|
@ -34,8 +34,7 @@ def parse_deps(deps):
|
|||
ret = []
|
||||
for dep in deps:
|
||||
if dep.startswith('git+'):
|
||||
repo_name = dep.split('/')[-1].split('.git')[0]
|
||||
dep = f'{repo_name} @ {dep}'
|
||||
continue # Don't include git dependencies in the setup.py, or Twine will complain
|
||||
|
||||
ret.append(dep)
|
||||
|
||||
|
@ -67,7 +66,7 @@ backend = pkg_files('platypush/backend')
|
|||
|
||||
setup(
|
||||
name="platypush",
|
||||
version="0.50.3",
|
||||
version="1.1.0",
|
||||
author="Fabio Manganiello",
|
||||
author_email="fabio@manganiello.tech",
|
||||
description="Platypush service",
|
||||
|
|
Loading…
Reference in New Issue