Merge branch 'master' into snyk-upgrade-503e414934e3e9df4999abbd15eed244

This commit is contained in:
Fabio Manganiello 2024-05-09 14:14:07 +02:00 committed by GitHub
commit 912dddd3da
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 1078 additions and 574 deletions

View file

@ -505,11 +505,10 @@ event.hook.SearchSongVoiceCommand:
[Example](https://git.platypush.tech/platypush/platypush/src/branch/master/examples/conf/hook.py):
```python
from platypush.event.hook import hook
from platypush.utils import run
from platypush import run, when
from platypush.message.event.assistant import SpeechRecognizedEvent
@hook(SpeechRecognizedEvent, phrase='play ${title} by ${artist}')
@when(SpeechRecognizedEvent, phrase='play ${title} by ${artist}')
def on_music_play_command(event, title=None, artist=None, **context):
results = run('music.mpd.search', filter={
'artist': artist,
@ -527,22 +526,22 @@ against partial event arguments are also possible, and relational operators are
supported as well. For example:
```python
from platypush.event.hook import hook
from platypush import hook
from platypush.message.event.sensor import SensorDataChangeEvent
@hook(SensorDataChangeEvent, data=1):
@when(SensorDataChangeEvent, data=1):
def hook_1(event):
"""
Triggered when event.data == 1
"""
@hook(SensorDataChangeEvent, data={'state': 1}):
@when(SensorDataChangeEvent, data={'state': 1}):
def hook_2(event):
"""
Triggered when event.data['state'] == 1
"""
@hook(SensorDataChangeEvent, data={
@when(SensorDataChangeEvent, data={
'temperature': {'$gt': 25},
'humidity': {'$le': 15}
}):

View file

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

View file

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

View file

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

View file

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

View file

@ -6,13 +6,9 @@ Responses
:maxdepth: 1
:caption: Responses:
platypush/responses/camera.rst
platypush/responses/camera.android.rst
platypush/responses/google.drive.rst
platypush/responses/pihole.rst
platypush/responses/printer.cups.rst
platypush/responses/qrcode.rst
platypush/responses/ssh.rst
platypush/responses/stt.rst
platypush/responses/tensorflow.rst
platypush/responses/translate.rst

View file

@ -3,13 +3,10 @@
# which event type they should be called, and optionally on which event attribute values.
#
# Event hooks should be stored in Python files under `~/.config/platypush/scripts`. All the functions that use the
# @hook decorator will automatically be discovered and imported as event hooks into the platform at runtime.
# @when decorator will automatically be discovered and imported as event hooks into the platform at runtime.
# `run` is a utility function that runs a request by name (e.g. `light.hue.on`).
from platypush.utils import run
# @hook decorator
from platypush.event.hook import hook
from platypush import when, run
# Event types that you want to react to
from platypush.message.event.assistant import (
@ -18,7 +15,7 @@ from platypush.message.event.assistant import (
)
@hook(SpeechRecognizedEvent, phrase='play ${title} by ${artist}')
@when(SpeechRecognizedEvent, phrase='play ${title} by ${artist}')
def on_music_play_command(event, title=None, artist=None, **context):
"""
This function will be executed when a SpeechRecognizedEvent with `phrase="play the music"` is triggered.
@ -40,7 +37,7 @@ def on_music_play_command(event, title=None, artist=None, **context):
run('tts.say', "I can't find any music matching your query")
@hook(ConversationStartEvent)
@when(ConversationStartEvent)
def on_conversation_start(event, **context):
"""
A simple hook that gets invoked when a new conversation starts with a voice assistant and simply pauses the music

View file

@ -17,6 +17,10 @@ from .procedure import procedure
from .runner import main
from .utils import run
# Alias for platypush.event.hook.hook,
# see https://git.platypush.tech/platypush/platypush/issues/399
when = hook
__author__ = 'Fabio Manganiello <fabio@manganiello.tech>'
__version__ = '0.50.3'
@ -35,6 +39,7 @@ __all__ = [
'main',
'procedure',
'run',
'when',
]

View file

@ -153,14 +153,13 @@ class HttpBackend(Backend):
.. code-block:: python
from platypush.context import get_plugin
from platypush.event.hook import hook
from platypush import get_plugin, when
from platypush.message.event.http.hook import WebhookEvent
hook_token = 'abcdefabcdef'
# Expose the hook under the /hook/lights_toggle endpoint
@hook(WebhookEvent, hook='lights_toggle')
@when(WebhookEvent, hook='lights_toggle')
def lights_toggle(event, **context):
# Do any checks on the request
assert event.headers.get('X-Token') == hook_token, 'Unauthorized'

View file

@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><!--[if IE]><link rel="icon" href="/favicon.ico"><![endif]--><link rel="stylesheet" href="/fonts/poppins.css"><title>platypush</title><script defer="defer" src="/static/js/chunk-vendors.89d8d8a7.js"></script><script defer="defer" src="/static/js/app.0268780d.js"></script><link href="/static/css/chunk-vendors.a2412607.css" rel="stylesheet"><link href="/static/css/app.0e655fed.css" rel="stylesheet"><link rel="icon" type="image/svg+xml" href="/img/icons/favicon.svg"><link rel="icon" type="image/png" sizes="32x32" href="/img/icons/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="/img/icons/favicon-16x16.png"><link rel="manifest" href="/manifest.json"><meta name="theme-color" content="#ffffff"><meta name="apple-mobile-web-app-capable" content="no"><meta name="apple-mobile-web-app-status-bar-style" content="default"><meta name="apple-mobile-web-app-title" content="Platypush"><link rel="apple-touch-icon" href="/img/icons/apple-touch-icon-152x152.png"><link rel="mask-icon" href="/img/icons/safari-pinned-tab.svg" color="#ffffff"><meta name="msapplication-TileImage" content="/img/icons/msapplication-icon-144x144.png"><meta name="msapplication-TileColor" content="#000000"></head><body><noscript><strong>We're sorry but platypush doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><!--[if IE]><link rel="icon" href="/favicon.ico"><![endif]--><link rel="stylesheet" href="/fonts/poppins.css"><title>platypush</title><script defer="defer" src="/static/js/chunk-vendors.89d8d8a7.js"></script><script defer="defer" src="/static/js/app.50f2ef87.js"></script><link href="/static/css/chunk-vendors.a2412607.css" rel="stylesheet"><link href="/static/css/app.0e655fed.css" rel="stylesheet"><link rel="icon" type="image/svg+xml" href="/img/icons/favicon.svg"><link rel="icon" type="image/png" sizes="32x32" href="/img/icons/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="/img/icons/favicon-16x16.png"><link rel="manifest" href="/manifest.json"><meta name="theme-color" content="#ffffff"><meta name="apple-mobile-web-app-capable" content="no"><meta name="apple-mobile-web-app-status-bar-style" content="default"><meta name="apple-mobile-web-app-title" content="Platypush"><link rel="apple-touch-icon" href="/img/icons/apple-touch-icon-152x152.png"><link rel="mask-icon" href="/img/icons/safari-pinned-tab.svg" color="#ffffff"><meta name="msapplication-TileImage" content="/img/icons/msapplication-icon-144x144.png"><meta name="msapplication-TileColor" content="#000000"></head><body><noscript><strong>We're sorry but platypush doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -16,9 +16,9 @@
"lato-font": "^3.0.0",
"mitt": "^2.1.0",
"register-service-worker": "^1.7.2",
"sass": "^1.71.0",
"sass": "^1.75.0",
"sass-loader": "^10.5.2",
"vue": "^3.4.19",
"vue": "^3.4.23",
"vue-router": "^4.3.0",
"vue-skycons": "^4.2.0",
"w3css": "^2.7.0"
@ -492,9 +492,9 @@
}
},
"node_modules/@babel/parser": {
"version": "7.24.0",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.0.tgz",
"integrity": "sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==",
"version": "7.24.5",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz",
"integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==",
"bin": {
"parser": "bin/babel-parser.js"
},
@ -3080,15 +3080,15 @@
"dev": true
},
"node_modules/@vue/compiler-core": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.19.tgz",
"integrity": "sha512-gj81785z0JNzRcU0Mq98E56e4ltO1yf8k5PQ+tV/7YHnbZkrM0fyFyuttnN8ngJZjbpofWE/m4qjKBiLl8Ju4w==",
"version": "3.4.23",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.23.tgz",
"integrity": "sha512-HAFmuVEwNqNdmk+w4VCQ2pkLk1Vw4XYiiyxEp3z/xvl14aLTUBw2OfVH3vBcx+FtGsynQLkkhK410Nah1N2yyQ==",
"dependencies": {
"@babel/parser": "^7.23.9",
"@vue/shared": "3.4.19",
"@babel/parser": "^7.24.1",
"@vue/shared": "3.4.23",
"entities": "^4.5.0",
"estree-walker": "^2.0.2",
"source-map-js": "^1.0.2"
"source-map-js": "^1.2.0"
}
},
"node_modules/@vue/compiler-core/node_modules/entities": {
@ -3103,37 +3103,37 @@
}
},
"node_modules/@vue/compiler-dom": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.19.tgz",
"integrity": "sha512-vm6+cogWrshjqEHTzIDCp72DKtea8Ry/QVpQRYoyTIg9k7QZDX6D8+HGURjtmatfgM8xgCFtJJaOlCaRYRK3QA==",
"version": "3.4.23",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.23.tgz",
"integrity": "sha512-t0b9WSTnCRrzsBGrDd1LNR5HGzYTr7LX3z6nNBG+KGvZLqrT0mY6NsMzOqlVMBKKXKVuusbbB5aOOFgTY+senw==",
"dependencies": {
"@vue/compiler-core": "3.4.19",
"@vue/shared": "3.4.19"
"@vue/compiler-core": "3.4.23",
"@vue/shared": "3.4.23"
}
},
"node_modules/@vue/compiler-sfc": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.19.tgz",
"integrity": "sha512-LQ3U4SN0DlvV0xhr1lUsgLCYlwQfUfetyPxkKYu7dkfvx7g3ojrGAkw0AERLOKYXuAGnqFsEuytkdcComei3Yg==",
"version": "3.4.23",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.23.tgz",
"integrity": "sha512-fSDTKTfzaRX1kNAUiaj8JB4AokikzStWgHooMhaxyjZerw624L+IAP/fvI4ZwMpwIh8f08PVzEnu4rg8/Npssw==",
"dependencies": {
"@babel/parser": "^7.23.9",
"@vue/compiler-core": "3.4.19",
"@vue/compiler-dom": "3.4.19",
"@vue/compiler-ssr": "3.4.19",
"@vue/shared": "3.4.19",
"@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",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.6",
"postcss": "^8.4.33",
"source-map-js": "^1.0.2"
"magic-string": "^0.30.8",
"postcss": "^8.4.38",
"source-map-js": "^1.2.0"
}
},
"node_modules/@vue/compiler-ssr": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.19.tgz",
"integrity": "sha512-P0PLKC4+u4OMJ8sinba/5Z/iDT84uMRRlrWzadgLA69opCpI1gG4N55qDSC+dedwq2fJtzmGald05LWR5TFfLw==",
"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==",
"dependencies": {
"@vue/compiler-dom": "3.4.19",
"@vue/shared": "3.4.19"
"@vue/compiler-dom": "3.4.23",
"@vue/shared": "3.4.23"
}
},
"node_modules/@vue/component-compiler-utils": {
@ -3206,48 +3206,48 @@
"integrity": "sha512-LgPscpE3Vs0x96PzSSB4IGVSZXZBZHpfxs+ZA1d+VEPwHdOXowy/Y2CsvCAIFrf+ssVU1pD1jidj505EpUnfbA=="
},
"node_modules/@vue/reactivity": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.19.tgz",
"integrity": "sha512-+VcwrQvLZgEclGZRHx4O2XhyEEcKaBi50WbxdVItEezUf4fqRh838Ix6amWTdX0CNb/b6t3Gkz3eOebfcSt+UA==",
"version": "3.4.23",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.23.tgz",
"integrity": "sha512-GlXR9PL+23fQ3IqnbSQ8OQKLodjqCyoCrmdLKZk3BP7jN6prWheAfU7a3mrltewTkoBm+N7qMEb372VHIkQRMQ==",
"dependencies": {
"@vue/shared": "3.4.19"
"@vue/shared": "3.4.23"
}
},
"node_modules/@vue/runtime-core": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.19.tgz",
"integrity": "sha512-/Z3tFwOrerJB/oyutmJGoYbuoadphDcJAd5jOuJE86THNZji9pYjZroQ2NFsZkTxOq0GJbb+s2kxTYToDiyZzw==",
"version": "3.4.23",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.23.tgz",
"integrity": "sha512-FeQ9MZEXoFzFkFiw9MQQ/FWs3srvrP+SjDKSeRIiQHIhtkzoj0X4rWQlRNHbGuSwLra6pMyjAttwixNMjc/xLw==",
"dependencies": {
"@vue/reactivity": "3.4.19",
"@vue/shared": "3.4.19"
"@vue/reactivity": "3.4.23",
"@vue/shared": "3.4.23"
}
},
"node_modules/@vue/runtime-dom": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.19.tgz",
"integrity": "sha512-IyZzIDqfNCF0OyZOauL+F4yzjMPN2rPd8nhqPP2N1lBn3kYqJpPHHru+83Rkvo2lHz5mW+rEeIMEF9qY3PB94g==",
"version": "3.4.23",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.23.tgz",
"integrity": "sha512-RXJFwwykZWBkMiTPSLEWU3kgVLNAfActBfWFlZd0y79FTUxexogd0PLG4HH2LfOktjRxV47Nulygh0JFXe5f9A==",
"dependencies": {
"@vue/runtime-core": "3.4.19",
"@vue/shared": "3.4.19",
"@vue/runtime-core": "3.4.23",
"@vue/shared": "3.4.23",
"csstype": "^3.1.3"
}
},
"node_modules/@vue/server-renderer": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.19.tgz",
"integrity": "sha512-eAj2p0c429RZyyhtMRnttjcSToch+kTWxFPHlzGMkR28ZbF1PDlTcmGmlDxccBuqNd9iOQ7xPRPAGgPVj+YpQw==",
"version": "3.4.23",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.23.tgz",
"integrity": "sha512-LDwGHtnIzvKFNS8dPJ1SSU5Gvm36p2ck8wCZc52fc3k/IfjKcwCyrWEf0Yag/2wTFUBXrqizfhK9c/mC367dXQ==",
"dependencies": {
"@vue/compiler-ssr": "3.4.19",
"@vue/shared": "3.4.19"
"@vue/compiler-ssr": "3.4.23",
"@vue/shared": "3.4.23"
},
"peerDependencies": {
"vue": "3.4.19"
"vue": "3.4.23"
}
},
"node_modules/@vue/shared": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.19.tgz",
"integrity": "sha512-/KliRRHMF6LoiThEy+4c1Z4KB/gbPrGjWwJR+crg2otgrf/egKzRaCPvJ51S5oetgsgXLfc4Rm5ZgrKHZrtMSw=="
"version": "3.4.23",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.23.tgz",
"integrity": "sha512-wBQ0gvf+SMwsCQOyusNw/GoXPV47WGd1xB5A1Pgzy0sQ3Bi5r5xm3n+92y3gCnB3MWqnRDdvfkRGxhKtbBRNgg=="
},
"node_modules/@vue/vue-loader-v15": {
"name": "vue-loader",
@ -8376,14 +8376,11 @@
}
},
"node_modules/magic-string": {
"version": "0.30.8",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz",
"integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==",
"version": "0.30.10",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz",
"integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15"
},
"engines": {
"node": ">=12"
}
},
"node_modules/make-dir": {
@ -9338,9 +9335,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.35",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz",
"integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==",
"version": "8.4.38",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
"integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
"funding": [
{
"type": "opencollective",
@ -9358,7 +9355,7 @@
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
"source-map-js": "^1.2.0"
},
"engines": {
"node": "^10 || ^12 || >=14"
@ -10554,9 +10551,9 @@
"dev": true
},
"node_modules/sass": {
"version": "1.71.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.71.0.tgz",
"integrity": "sha512-HKKIKf49Vkxlrav3F/w6qRuPcmImGVbIXJ2I3Kg0VMA+3Bav+8yE9G5XmP5lMj6nl4OlqbPftGAscNaNu28b8w==",
"version": "1.75.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.75.0.tgz",
"integrity": "sha512-ShMYi3WkrDWxExyxSZPst4/okE9ts46xZmJDSawJQrnte7M1V9fScVB+uNXOVKRBt0PggHOwoZcn8mYX4trnBw==",
"dependencies": {
"chokidar": ">=3.0.0 <4.0.0",
"immutable": "^4.0.0",
@ -11023,9 +11020,9 @@
}
},
"node_modules/source-map-js": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
"engines": {
"node": ">=0.10.0"
}
@ -11979,15 +11976,15 @@
}
},
"node_modules/vue": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.4.19.tgz",
"integrity": "sha512-W/7Fc9KUkajFU8dBeDluM4sRGc/aa4YJnOYck8dkjgZoXtVsn3OeTGni66FV1l3+nvPA7VBFYtPioaGKUmEADw==",
"version": "3.4.23",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.4.23.tgz",
"integrity": "sha512-X1y6yyGJ28LMUBJ0k/qIeKHstGd+BlWQEOT40x3auJFTmpIhpbKLgN7EFsqalnJXq1Km5ybDEsp6BhuWKciUDg==",
"dependencies": {
"@vue/compiler-dom": "3.4.19",
"@vue/compiler-sfc": "3.4.19",
"@vue/runtime-dom": "3.4.19",
"@vue/server-renderer": "3.4.19",
"@vue/shared": "3.4.19"
"@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"
},
"peerDependencies": {
"typescript": "*"

View file

@ -16,9 +16,9 @@
"lato-font": "^3.0.0",
"mitt": "^2.1.0",
"register-service-worker": "^1.7.2",
"sass": "^1.71.0",
"sass": "^1.75.0",
"sass-loader": "^10.5.2",
"vue": "^3.4.19",
"vue": "^3.4.23",
"vue-router": "^4.3.0",
"vue-skycons": "^4.2.0",
"w3css": "^2.7.0"

View file

@ -87,8 +87,30 @@ export default {
},
computed: {
config() {
return this.$root.config['camera.android.ipcam']
configuredCameras() {
const config = this.$root.config['camera.android.ipcam']
let cameras = config.cameras || []
if (!cameras.length) {
const name = config.name || config.host
cameras[name] = {
'name': name,
'host': config.host,
'port': config.port,
'username': config.username,
'password': config.password,
'timeout': config.timeout,
'ssl': config.ssl,
}
} else {
cameras = cameras.reduce((cameras, cam) => {
const name = cam.name || cam.host
cameras[name] = cam
return cameras
}, {})
}
return cameras
},
},
@ -160,7 +182,7 @@ export default {
cam[attr] = cam[attr].replace('https://', 'http://')
}
if (cam.name in this.config.cameras && this.config.cameras[cam.name].username) {
if (cam.name in this.configuredCameras && this.configuredCameras[cam.name].username) {
cam[attr] = 'http://' + this.config.cameras[cam.name].username + ':' +
this.config.cameras[cam.name].password + '@' + cam[attr].substr(7)
}

Binary file not shown.

View file

@ -1,8 +0,0 @@
from platypush.message.response import Response
class CameraResponse(Response):
pass
# vim:sw=4:ts=4:et:

View file

@ -1,191 +0,0 @@
from typing import List
from platypush.message.response.camera import CameraResponse
class AndroidCameraStatusResponse(CameraResponse):
"""
Example response:
.. code-block:: json
{
"stream_url": "https://192.168.1.30:8080/video",
"image_url": "https://192.168.1.30:8080/photo.jpg",
"audio_url": "https://192.168.1.30:8080/audio.wav",
"orientation": "landscape",
"idle": "off",
"audio_only": "off",
"overlay": "off",
"quality": "49",
"focus_homing": "off",
"ip_address": "192.168.1.30",
"motion_limit": "250",
"adet_limit": "200",
"night_vision": "off",
"night_vision_average": "2",
"night_vision_gain": "1.0",
"motion_detect": "off",
"motion_display": "off",
"video_chunk_len": "60",
"gps_active": "off",
"video_size": "1920x1080",
"mirror_flip": "none",
"ffc": "off",
"rtsp_video_formats": "",
"rtsp_audio_formats": "",
"video_connections": "0",
"audio_connections": "0",
"ivideon_streaming": "off",
"zoom": "100",
"crop_x": "50",
"crop_y": "50",
"coloreffect": "none",
"scenemode": "auto",
"focusmode": "continuous-video",
"whitebalance": "auto",
"flashmode": "off",
"antibanding": "off",
"torch": "off",
"focus_distance": "0.0",
"focal_length": "4.25",
"aperture": "1.7",
"filter_density": "0.0",
"exposure_ns": "9384",
"frame_duration": "33333333",
"iso": "100",
"manual_sensor": "off",
"photo_size": "1920x1080"
}
"""
attrs = {
'orientation', 'idle', 'audio_only', 'overlay', 'quality', 'focus_homing',
'ip_address', 'motion_limit', 'adet_limit', 'night_vision',
'night_vision_average', 'night_vision_gain', 'motion_detect',
'motion_display', 'video_chunk_len', 'gps_active', 'video_size',
'mirror_flip', 'ffc', 'rtsp_video_formats', 'rtsp_audio_formats',
'video_connections', 'audio_connections', 'ivideon_streaming', 'zoom',
'crop_x', 'crop_y', 'coloreffect', 'scenemode', 'focusmode',
'whitebalance', 'flashmode', 'antibanding', 'torch', 'focus_distance',
'focal_length', 'aperture', 'filter_density', 'exposure_ns',
'frame_duration', 'iso', 'manual_sensor', 'photo_size',
}
def __init__(self, *args,
name: str = None,
stream_url: str = None,
image_url: str = None,
audio_url: str = None,
orientation: str = None,
idle: str = None,
audio_only: str = None,
overlay: str = None,
quality: str = None,
focus_homing: str = None,
ip_address: str = None,
motion_limit: str = None,
adet_limit: str = None,
night_vision: str = None,
night_vision_average: str = None,
night_vision_gain: str = None,
motion_detect: str = None,
motion_display: str = None,
video_chunk_len: str = None,
gps_active: str = None,
video_size: str = None,
mirror_flip: str = None,
ffc: str = None,
rtsp_video_formats: str = None,
rtsp_audio_formats: str = None,
video_connections: str = None,
audio_connections: str = None,
ivideon_streaming: str = None,
zoom: str = None,
crop_x: str = None,
crop_y: str = None,
coloreffect: str = None,
scenemode: str = None,
focusmode: str = None,
whitebalance: str = None,
flashmode: str = None,
antibanding: str = None,
torch: str = None,
focus_distance: str = None,
focal_length: str = None,
aperture: str = None,
filter_density: str = None,
exposure_ns: str = None,
frame_duration: str = None,
iso: str = None,
manual_sensor: str = None,
photo_size: str = None,
**kwargs):
self.status = {
"name": name,
"stream_url": stream_url,
"image_url": image_url,
"audio_url": audio_url,
"orientation": orientation,
"idle": True if idle == "on" else False,
"audio_only": True if audio_only == "on" else False,
"overlay": True if overlay == "on" else False,
"quality": int(quality or 0),
"focus_homing": True if focus_homing == "on" else False,
"ip_address": ip_address,
"motion_limit": int(motion_limit or 0),
"adet_limit": int(adet_limit or 0),
"night_vision": True if night_vision == "on" else False,
"night_vision_average": float(night_vision_average or 0),
"night_vision_gain": float(night_vision_gain or 0),
"motion_detect": True if motion_detect == "on" else False,
"motion_display": True if motion_display == "on" else False,
"video_chunk_len": int(video_chunk_len or 0),
"gps_active": True if gps_active == "on" else False,
"video_size": video_size,
"mirror_flip": mirror_flip,
"ffc": True if ffc == "on" else False,
"rtsp_video_formats": rtsp_video_formats,
"rtsp_audio_formats": rtsp_audio_formats,
"video_connections": int(video_connections or 0),
"audio_connections": int(audio_connections or 0),
"ivideon_streaming": True if ivideon_streaming == "on" else False,
"zoom": int(zoom or 0),
"crop_x": int(crop_x or 0),
"crop_y": int(crop_y or 0),
"coloreffect": coloreffect,
"scenemode": scenemode,
"focusmode": focusmode,
"whitebalance": whitebalance,
"flashmode": True if flashmode == "on" else False,
"antibanding": True if antibanding == "on" else False,
"torch": True if torch == "on" else False,
"focus_distance": float(focus_distance or 0),
"focal_length": float(focal_length or 0),
"aperture": float(aperture or 0),
"filter_density": float(filter_density or 0),
"exposure_ns": int(exposure_ns or 0),
"frame_duration": int(frame_duration or 0),
"iso": int(iso or 0),
"manual_sensor": True if manual_sensor == "on" else False,
"photo_size": photo_size,
}
super().__init__(*args, output=self.status, **kwargs)
class AndroidCameraStatusListResponse(CameraResponse):
def __init__(self, status: List[AndroidCameraStatusResponse], **kwargs):
self.status = [s.status for s in status]
super().__init__(output=self.status, **kwargs)
class AndroidCameraPictureResponse(CameraResponse):
def __init__(self, image_file: str, *args, **kwargs):
self.image_file = image_file
super().__init__(*args, output={'image_file': image_file}, **kwargs)
# vim:sw=4:ts=4:et:

View file

@ -1,38 +0,0 @@
from platypush.message.response import Response
class PiholeStatusResponse(Response):
def __init__(self,
server: str,
status: str,
ads_percentage: float,
blocked: int,
cached: int,
domain_count: int,
forwarded: int,
queries: int,
total_clients: int,
total_queries: int,
unique_clients: int,
unique_domains: int,
version: str,
*args,
**kwargs):
super().__init__(*args, output={
'server': server,
'status': status,
'ads_percentage': ads_percentage,
'blocked': blocked,
'cached': cached,
'domain_count': domain_count,
'forwarded': forwarded,
'queries': queries,
'total_clients': total_clients,
'total_queries': total_queries,
'unique_clients': unique_clients,
'unique_domains': unique_domains,
'version': version,
}, **kwargs)
# vim:sw=4:ts=4:et:

View file

@ -1,11 +0,0 @@
from platypush.message.response import Response
class SpeechDetectedResponse(Response):
def __init__(self, *args, speech: str, **kwargs):
super().__init__(*args, output={
'speech': speech
}, **kwargs)
# vim:sw=4:ts=4:et:

View file

@ -86,11 +86,11 @@ class AssistantPicovoicePlugin(AssistantPlugin, RunnablePlugin):
import time
from platypush import hook, run
from platypush import when, run
from platypush.message.event.assistant import HotwordDetectedEvent
# Turn on a light for 5 seconds when the hotword "Alexa" is detected
@hook(HotwordDetectedEvent, hotword='Alexa')
@when(HotwordDetectedEvent, hotword='Alexa')
def on_hotword_detected(event: HotwordDetectedEvent, **context):
run("light.hue.on", lights=["Living Room"])
time.sleep(5)
@ -109,12 +109,12 @@ class AssistantPicovoicePlugin(AssistantPlugin, RunnablePlugin):
.. code-block:: python
from platypush import hook, run
from platypush import when, run
from platypush.message.event.assistant import HotwordDetectedEvent
# Start a conversation using the Italian language model when the
# "Buongiorno" hotword is detected
@hook(HotwordDetectedEvent, hotword='Buongiorno')
@when(HotwordDetectedEvent, hotword='Buongiorno')
def on_it_hotword_detected(event: HotwordDetectedEvent, **context):
event.assistant.start_conversation(model_file='path/to/it.pv')
@ -136,7 +136,7 @@ class AssistantPicovoicePlugin(AssistantPlugin, RunnablePlugin):
.. code-block:: python
from platypush import hook, run
from platypush import when, run
from platypush.message.event.assistant import SpeechRecognizedEvent
# Turn on a light when the phrase "turn on the lights" is detected.
@ -144,7 +144,7 @@ class AssistantPicovoicePlugin(AssistantPlugin, RunnablePlugin):
# flexible when matching the phrases. For example, the following hook
# will be matched when the user says "turn on the lights", "turn on
# lights", "lights on", "lights on please", "turn on light" etc.
@hook(SpeechRecognizedEvent, phrase='turn on (the)? lights?')
@when(SpeechRecognizedEvent, phrase='turn on (the)? lights?')
def on_turn_on_lights(event: SpeechRecognizedEvent, **context):
run("light.hue.on")
@ -154,10 +154,10 @@ class AssistantPicovoicePlugin(AssistantPlugin, RunnablePlugin):
.. code-block:: python
from platypush import hook, run
from platypush import when, run
from platypush.message.event.assistant import SpeechRecognizedEvent
@hook(SpeechRecognizedEvent, phrase='play ${title} by ${artist}')
@when(SpeechRecognizedEvent, phrase='play ${title} by ${artist}')
def on_play_track_command(
event: SpeechRecognizedEvent, title: str, artist: str, **context
):
@ -227,10 +227,10 @@ class AssistantPicovoicePlugin(AssistantPlugin, RunnablePlugin):
.. code-block:: python
from platypush import hook, run
from platypush import when, run
from platypush.message.event.assistant import IntentRecognizedEvent
@hook(IntentRecognizedEvent, intent='lights_ctrl', slots={'state': 'on'})
@when(IntentRecognizedEvent, intent='lights_ctrl', slots={'state': 'on'})
def on_turn_on_lights(event: IntentRecognizedEvent, **context):
room = event.slots.get('room')
if room:
@ -255,10 +255,10 @@ class AssistantPicovoicePlugin(AssistantPlugin, RunnablePlugin):
.. code-block:: python
from platypush import hook, run
from platypush import when, run
from platypush.message.event.assistant import SpeechRecognizedEvent
@hook(SpeechRecognizedEvent, phrase='turn ${state} (the)? ${room} lights?')
@when(SpeechRecognizedEvent, phrase='turn ${state} (the)? ${room} lights?')
def on_turn_on_lights(event: SpeechRecognizedEvent, phrase, room, **context):
if room:
run("light.hue.on", groups=[room])
@ -331,7 +331,7 @@ class AssistantPicovoicePlugin(AssistantPlugin, RunnablePlugin):
(re.compile(r".*"), ai_assist),
)
@hook(SpeechRecognizedEvent)
@when(SpeechRecognizedEvent)
def on_speech_recognized(event, **kwargs):
for pattern, command in hooks:
if pattern.search(event.phrase):
@ -339,7 +339,7 @@ class AssistantPicovoicePlugin(AssistantPlugin, RunnablePlugin):
command(event, **kwargs)
break
@hook(ResponseEndEvent)
@when(ResponseEndEvent)
def on_response_end(event: ResponseEndEvent, **__):
# Check if the response is a question and start a follow-on turn if so.
# Note that the ``openai`` plugin by default is configured to keep

View file

@ -3,18 +3,29 @@ import os
import requests
from requests.auth import HTTPBasicAuth
from typing import Optional, Union, Dict, List, Any
from typing import Optional, Sequence, Union, Dict, List, Any
from platypush.message.response.camera.android import AndroidCameraStatusResponse, AndroidCameraStatusListResponse, \
AndroidCameraPictureResponse
from platypush.plugins import Plugin, action
from platypush.schemas.camera.android.ipcam import CameraStatusSchema
class AndroidIpcam:
"""
IPCam camera configuration.
"""
args = {}
def __init__(self, name: str, host: str, port: int = 8080, username: Optional[str] = None,
password: Optional[str] = None, timeout: int = 10, ssl: bool = True):
def __init__(
self,
name: str,
host: str,
port: int = 8080,
username: Optional[str] = None,
password: Optional[str] = None,
timeout: int = 10,
ssl: bool = True,
):
self.args = {
'name': name,
'host': host,
@ -41,12 +52,13 @@ class AndroidIpcam:
self.args[key] = value
def __str__(self):
return json.dumps(getattr(self, 'args') or {})
return json.dumps(self.args or {})
@property
def base_url(self) -> str:
return 'http{ssl}://{host}:{port}/'.format(
ssl=('s' if self.ssl else ''), host=self.host, port=self.port)
ssl=('s' if self.ssl else ''), host=self.host, port=self.port
)
@property
def stream_url(self) -> str:
@ -67,16 +79,20 @@ class CameraAndroidIpcamPlugin(Plugin):
`IPCam <https://play.google.com/store/apps/details?id=com.pas.webcam>`_.
"""
def __init__(self,
host: Optional[str] = None,
port: Optional[int] = 8080,
username: Optional[str] = None,
password: Optional[str] = None,
timeout: int = 10,
ssl: bool = True,
cameras: Optional[Dict[str, Dict[str, Any]]] = None,
**kwargs):
def __init__(
self,
name: Optional[str] = None,
host: Optional[str] = None,
port: int = 8080,
username: Optional[str] = None,
password: Optional[str] = None,
timeout: int = 10,
ssl: bool = True,
cameras: Optional[Sequence[dict]] = None,
**kwargs,
):
"""
:param name: Custom name for the default camera (default: IP/hostname)
:param host: Camera host name or address
:param port: Camera port
:param username: Camera username, if set
@ -84,28 +100,44 @@ class CameraAndroidIpcamPlugin(Plugin):
:param timeout: Connection timeout
:param ssl: Use HTTPS instead of HTTP
:param cameras: Alternatively, you can specify a list of IPCam cameras as a
name->dict mapping. The keys will be unique names used to identify your
cameras, the values will contain dictionaries containing `host, `port`,
`username`, `password`, `timeout` and `ssl` attributes for each camera.
list of objects with ``name``, ``host``, ``port``, ``username``,
``password``, ``timeout`` and ``ssl`` attributes.
"""
super().__init__(**kwargs)
self.cameras: List[AndroidIpcam] = []
self._camera_name_to_idx: Dict[str, int] = {}
if not cameras:
camera = AndroidIpcam(name=host, host=host, port=port, username=username,
password=password, timeout=timeout, ssl=ssl)
assert host, 'You need to specify at least one camera'
name = name or host
camera = AndroidIpcam(
name=name,
host=host,
port=port,
username=username,
password=password,
timeout=timeout,
ssl=ssl,
)
self.cameras.append(camera)
self._camera_name_to_idx[host] = 0
self._camera_name_to_idx[name] = 0
else:
for name, camera in cameras.items():
camera = AndroidIpcam(name=name, host=camera['host'], port=camera.get('port', port),
username=camera.get('username'), password=camera.get('password'),
timeout=camera.get('timeout', timeout), ssl=camera.get('ssl', ssl))
for camera in cameras:
assert 'host' in camera, 'You need to specify the host for each camera'
name = camera.get('name', camera['host'])
camera = AndroidIpcam(
name=name,
host=camera['host'],
port=camera.get('port', port),
username=camera.get('username'),
password=camera.get('password'),
timeout=camera.get('timeout', timeout),
ssl=camera.get('ssl', ssl),
)
self._camera_name_to_idx[name] = len(self.cameras)
self.cameras.append(camera)
def _get_camera(self, camera: Union[int, str] = None) -> AndroidIpcam:
def _get_camera(self, camera: Optional[Union[int, str]] = None) -> AndroidIpcam:
if not camera:
camera = 0
@ -114,10 +146,14 @@ class CameraAndroidIpcamPlugin(Plugin):
return self.cameras[self._camera_name_to_idx[camera]]
def _exec(self, url: str, camera: Union[int, str] = None, *args, **kwargs) -> Union[Dict[str, Any], bool]:
def _exec(
self, url: str, *args, camera: Optional[Union[int, str]] = None, **kwargs
) -> Union[Dict[str, Any], bool]:
cam = self._get_camera(camera)
url = cam.base_url + url
response = requests.get(url, auth=cam.auth, timeout=cam.timeout, verify=False, *args, **kwargs)
response = requests.get(
url, auth=cam.auth, timeout=cam.timeout, verify=False, *args, **kwargs
)
response.raise_for_status()
if response.headers.get('content-type') == 'application/json':
@ -125,87 +161,138 @@ class CameraAndroidIpcamPlugin(Plugin):
return response.text.find('Ok') != -1
@action
def change_setting(self, key: str, value: Union[str, int, bool], camera: Union[int, str] = None) -> bool:
"""
Change a setting.
:param key: Setting name
:param value: Setting value
:param camera: Camera index or configured name
:return: True on success, False otherwise
"""
def _change_setting(
self,
key: str,
value: Union[str, int, bool],
camera: Optional[Union[int, str]] = None,
) -> bool:
if isinstance(value, bool):
payload = "on" if value else "off"
else:
payload = value
return self._exec("settings/{key}?set={payload}".format(key=key, payload=payload), camera=camera)
return bool(
self._exec(
"settings/{key}?set={payload}".format(key=key, payload=payload),
camera=camera,
)
)
@action
def status(self, camera: Union[int, str] = None) -> AndroidCameraStatusListResponse:
def change_setting(
self,
key: str,
value: Union[str, int, bool],
camera: Optional[Union[int, str]] = None,
) -> bool:
"""
Change a setting.
:param key: Setting name
:param value: Setting value
:param camera: Camera index or configured name
:return: True on success, False otherwise
"""
return self._change_setting(key, value, camera=camera)
@action
def status(self, camera: Optional[Union[int, str]] = None) -> List[dict]:
"""
:param camera: Camera index or name (default: status of all the cameras)
:return: True if the camera is available, False otherwise
:return: .. schema:: camera.android.ipcam.CameraStatusSchema(many=True)
"""
cameras = self._camera_name_to_idx.keys() if camera is None else [camera]
statuses = []
for c in cameras:
try:
if isinstance(camera, int):
print('****** HERE ******')
print(self._camera_name_to_idx)
if isinstance(c, int):
cam = self.cameras[c]
else:
cam = self.cameras[self._camera_name_to_idx[c]]
status_data = self._exec('status.json', params={'show_avail': 1}, camera=cam.name).get('curvals', {})
status = AndroidCameraStatusResponse(
name=cam.name,
stream_url=cam.stream_url,
image_url=cam.image_url,
audio_url=cam.audio_url,
**{k: v for k, v in status_data.items()
if k in AndroidCameraStatusResponse.attrs})
response = self._exec(
'status.json', params={'show_avail': 1}, camera=cam.name
)
assert isinstance(response, dict), f'Invalid response: {response}'
status_data = response.get('curvals', {})
status = CameraStatusSchema().dump(
{
'name': cam.name,
'stream_url': cam.stream_url,
'image_url': cam.image_url,
'audio_url': cam.audio_url,
**status_data,
}
)
statuses.append(status)
except Exception as e:
self.logger.warning('Could not get the status of {}: {}'.format(c, str(e)))
self.logger.warning(
'Could not get the status of %s: %s: %s', c, type(e), e
)
return AndroidCameraStatusListResponse(statuses)
return statuses
@action
def set_front_facing_camera(self, activate: bool = True, camera: Union[int, str] = None) -> bool:
def set_front_facing_camera(
self, activate: bool = True, camera: Optional[Union[int, str]] = None
) -> bool:
"""Enable/disable the front-facing camera."""
return self.change_setting('ffc', activate, camera=camera)
return self._change_setting('ffc', activate, camera=camera)
@action
def set_torch(self, activate: bool = True, camera: Union[int, str] = None) -> bool:
def set_torch(
self, activate: bool = True, camera: Optional[Union[int, str]] = None
) -> bool:
"""Enable/disable the torch."""
url = 'enabletorch' if activate else 'disabletorch'
return self._exec(url, camera=camera)
return bool(self._exec(url, camera=camera))
@action
def set_focus(self, activate: bool = True, camera: Union[int, str] = None) -> bool:
def set_focus(
self, activate: bool = True, camera: Optional[Union[int, str]] = None
) -> bool:
"""Enable/disable the focus."""
url = 'focus' if activate else 'nofocus'
return self._exec(url, camera=camera)
return bool(self._exec(url, camera=camera))
@action
def start_recording(self, tag: Optional[str] = None, camera: Union[int, str] = None) -> bool:
def start_recording(
self, tag: Optional[str] = None, camera: Optional[Union[int, str]] = None
) -> bool:
"""Start recording."""
params = {'force': 1}
if tag:
params['tag'] = tag
return self._exec('startvideo', params=params, camera=camera)
params = {
'force': 1,
**({'tag': tag} if tag else {}),
}
return bool(self._exec('startvideo', params=params, camera=camera))
@action
def stop_recording(self, camera: Union[int, str] = None) -> bool:
def stop_recording(self, camera: Optional[Union[int, str]] = None) -> bool:
"""Stop recording."""
return self._exec('stopvideo', params={'force': 1}, camera=camera)
return bool(self._exec('stopvideo', params={'force': 1}, camera=camera))
@action
def take_picture(self, image_file: str, camera: Union[int, str] = None) -> AndroidCameraPictureResponse:
"""Take a picture and save it on the local device."""
def take_picture(
self, image_file: str, camera: Optional[Union[int, str]] = None
) -> dict:
"""
Take a picture and save it on the local device.
:return: dict
.. code-block:: json
{
"image_file": "/path/to/image.jpg"
}
"""
cam = self._get_camera(camera)
image_file = os.path.abspath(os.path.expanduser(image_file))
os.makedirs(os.path.dirname(image_file), exist_ok=True)
@ -214,47 +301,64 @@ class CameraAndroidIpcamPlugin(Plugin):
with open(image_file, 'wb') as f:
f.write(response.content)
return AndroidCameraPictureResponse(image_file=image_file)
return {'image_file': image_file}
@action
def set_night_vision(self, activate: bool = True, camera: Union[int, str] = None) -> bool:
def set_night_vision(
self, activate: bool = True, camera: Optional[Union[int, str]] = None
) -> bool:
"""Enable/disable night vision."""
return self.change_setting('night_vision', activate, camera=camera)
return self._change_setting('night_vision', activate, camera=camera)
@action
def set_overlay(self, activate: bool = True, camera: Union[int, str] = None) -> bool:
def set_overlay(
self, activate: bool = True, camera: Optional[Union[int, str]] = None
) -> bool:
"""Enable/disable video overlay."""
return self.change_setting('overlay', activate, camera=camera)
return self._change_setting('overlay', activate, camera=camera)
@action
def set_gps(self, activate: bool = True, camera: Union[int, str] = None) -> bool:
def set_gps(
self, activate: bool = True, camera: Optional[Union[int, str]] = None
) -> bool:
"""Enable/disable GPS."""
return self.change_setting('gps_active', activate, camera=camera)
return self._change_setting('gps_active', activate, camera=camera)
@action
def set_quality(self, quality: int = 100, camera: Union[int, str] = None) -> bool:
def set_quality(
self, quality: int = 100, camera: Optional[Union[int, str]] = None
) -> bool:
"""Set video quality."""
return self.change_setting('quality', int(quality), camera=camera)
return self._change_setting('quality', int(quality), camera=camera)
@action
def set_motion_detect(self, activate: bool = True, camera: Union[int, str] = None) -> bool:
def set_motion_detect(
self, activate: bool = True, camera: Optional[Union[int, str]] = None
) -> bool:
"""Enable/disable motion detect."""
return self.change_setting('motion_detect', activate, camera=camera)
return self._change_setting('motion_detect', activate, camera=camera)
@action
def set_orientation(self, orientation: str = 'landscape', camera: Union[int, str] = None) -> bool:
def set_orientation(
self, orientation: str = 'landscape', camera: Optional[Union[int, str]] = None
) -> bool:
"""Set video orientation."""
return self.change_setting('orientation', orientation, camera=camera)
return self._change_setting('orientation', orientation, camera=camera)
@action
def set_zoom(self, zoom: float, camera: Union[int, str] = None) -> bool:
def set_zoom(self, zoom: float, camera: Optional[Union[int, str]] = None) -> bool:
"""Set the zoom level."""
return self._exec('settings/ptz', params={'zoom': float(zoom)}, camera=camera)
return bool(
self._exec('settings/ptz', params={'zoom': float(zoom)}, camera=camera)
)
@action
def set_scenemode(self, scenemode: str = 'auto', camera: Union[int, str] = None) -> bool:
def set_scenemode(
self, scenemode: str = 'auto', camera: Optional[Union[int, str]] = None
) -> bool:
"""Set video orientation."""
return self.change_setting('scenemode', scenemode, camera=camera)
return self._change_setting('scenemode', scenemode, camera=camera)
# vim:sw=4:ts=4:et:

View file

@ -1,25 +1,27 @@
import hashlib
import requests
from enum import Enum
from typing import Optional, Union, List
from typing import Any, Dict, Optional, Union, List
from platypush.message.response.pihole import PiholeStatusResponse
from platypush.schemas.pihole import PiholeStatusSchema
from platypush.plugins import Plugin, action
class PiholeStatus(Enum):
ENABLED = 'enabled'
DISABLED = 'disabled'
class PiholePlugin(Plugin):
"""
Plugin for interacting with a `Pi-Hole <https://pi-hole.net>`_ DNS server for advertisement and content blocking.
"""
def __init__(self, server: Optional[str] = None, password: Optional[str] = None, api_key: Optional[str] = None,
ssl: bool = False, verify_ssl: bool = True, **kwargs):
def __init__(
self,
server: Optional[str] = None,
password: Optional[str] = None,
api_key: Optional[str] = None,
ssl: bool = False,
verify_ssl: bool = True,
timeout: int = 10,
**kwargs,
):
"""
:param server: Default Pi-hole server IP address.
:param password: Password for the default Pi-hole server.
@ -27,6 +29,7 @@ class PiholePlugin(Plugin):
http://pi-hole-server/admin/scripts/pi-hole/php/api_token.php
:param ssl: Set to true if the host uses HTTPS (default: False).
:param verify_ssl: Set to False to disable SSL certificate check.
:param timeout: Default timeout for the HTTP requests.
"""
super().__init__(**kwargs)
self.server = server
@ -34,23 +37,36 @@ class PiholePlugin(Plugin):
self.api_key = api_key
self.ssl = ssl
self.verify_ssl = verify_ssl
self.timeout = timeout
@staticmethod
def _get_token(password: Optional[str] = None, api_key: Optional[str] = None) -> str:
def _get_token(
password: Optional[str] = None, api_key: Optional[str] = None
) -> str:
if not password:
return api_key or ''
return hashlib.sha256(hashlib.sha256(str(password).encode()).hexdigest().encode()).hexdigest() # lgtm [py/weak-sensitive-data-hashing]
return hashlib.sha256(
hashlib.sha256(str(password).encode()).hexdigest().encode()
).hexdigest() # lgtm [py/weak-sensitive-data-hashing]
def _get_url(self, name: str, server: Optional[str] = None, password: Optional[str] = None,
ssl: Optional[bool] = None, api_key: Optional[str] = None, **kwargs) -> str:
def _get_url(
self,
name: str,
server: Optional[str] = None,
password: Optional[str] = None,
ssl: Optional[bool] = None,
api_key: Optional[str] = None,
**kwargs,
) -> str:
if not server:
server = self.server
password = password or self.password
api_key = api_key or self.api_key
ssl = ssl if ssl is not None else self.ssl
args = '&'.join(['{key}={value}'.format(key=key, value=value) for key, value in kwargs.items()
if value is not None])
args = '&'.join(
[f'{key}={value}' for key, value in kwargs.items() if value is not None]
)
if args:
args = '&' + args
@ -59,17 +75,20 @@ class PiholePlugin(Plugin):
if token:
token = '&auth=' + token
return 'http{ssl}://{server}/admin/api.php?{name}{token}{args}'.format(
ssl='s' if ssl else '', server=server, name=name, token=token, args=args)
return f'http{"s" if ssl else ""}://{server}/admin/api.php?{name}{token}{args}'
@staticmethod
def _normalize_number(n: Union[str, int]):
return int(str(n).replace(',', ''))
@action
def status(self, server: Optional[str] = None, password: Optional[str] = None, api_key: Optional[str] = None,
ssl: bool = None) \
-> PiholeStatusResponse:
def status(
self,
server: Optional[str] = None,
password: Optional[str] = None,
api_key: Optional[str] = None,
ssl: Optional[bool] = None,
) -> dict:
"""
Get the status and statistics of a running Pi-hole server.
@ -77,32 +96,64 @@ class PiholePlugin(Plugin):
:param password: Server password (default: default configured ``password`` value).
:param api_key: Server API key (default: default configured ``api_key`` value).
:param ssl: Set to True if the server uses SSL (default: False).
:return: :class:`platypush.message.response.pihole.PiholeStatusResponse`
:return: .. schema:: pihole.PiholeStatusSchema
"""
status = requests.get(self._get_url('summary', server=server, password=password, api_key=api_key,
ssl=ssl), verify=self.verify_ssl).json()
version = requests.get(self._get_url('versions', server=server, password=password, api_key=api_key,
ssl=ssl), verify=self.verify_ssl).json()
status = requests.get(
self._get_url(
'summary', server=server, password=password, api_key=api_key, ssl=ssl
),
verify=self.verify_ssl,
timeout=self.timeout,
).json()
return PiholeStatusResponse(
server=server or self.server,
status=PiholeStatus(status.get('status')).value,
ads_percentage=float(status.get('ads_percentage_today')),
blocked=self._normalize_number(status.get('ads_blocked_today')),
cached=self._normalize_number(status.get('queries_cached')),
domain_count=self._normalize_number(status.get('domains_being_blocked')),
forwarded=self._normalize_number(status.get('queries_forwarded')),
queries=self._normalize_number(status.get('dns_queries_today')),
total_clients=self._normalize_number(status.get('clients_ever_seen')),
total_queries=self._normalize_number(status.get('dns_queries_all_types')),
unique_clients=self._normalize_number(status.get('unique_clients')),
unique_domains=self._normalize_number(status.get('unique_domains')),
version=version.get('core_current'),
version = requests.get(
self._get_url(
'versions', server=server, password=password, api_key=api_key, ssl=ssl
),
verify=self.verify_ssl,
timeout=self.timeout,
).json()
return dict(
PiholeStatusSchema().dump(
{
'server': server or self.server,
'status': status.get('status'),
'ads_percentage': float(status.get('ads_percentage_today')),
'blocked': self._normalize_number(status.get('ads_blocked_today')),
'cached': self._normalize_number(status.get('queries_cached')),
'domain_count': self._normalize_number(
status.get('domains_being_blocked')
),
'forwarded': self._normalize_number(
status.get('queries_forwarded')
),
'queries': self._normalize_number(status.get('dns_queries_today')),
'total_clients': self._normalize_number(
status.get('clients_ever_seen')
),
'total_queries': self._normalize_number(
status.get('dns_queries_all_types')
),
'unique_clients': self._normalize_number(
status.get('unique_clients')
),
'unique_domains': self._normalize_number(
status.get('unique_domains')
),
'version': version.get('core_current'),
}
)
)
@action
def enable(self, server: Optional[str] = None, password: Optional[str] = None, api_key: Optional[str] = None,
ssl: bool = None):
def enable(
self,
server: Optional[str] = None,
password: Optional[str] = None,
api_key: Optional[str] = None,
ssl: Optional[bool] = None,
):
"""
Enable a Pi-hole server.
@ -111,20 +162,31 @@ class PiholePlugin(Plugin):
:param api_key: Server API key (default: default configured ``api_key`` value).
:param ssl: Set to True if the server uses SSL (default: False).
"""
response = requests.get(self._get_url('enable', server=server, password=password, api_key=api_key, ssl=ssl),
verify=self.verify_ssl)
response = requests.get(
self._get_url(
'enable', server=server, password=password, api_key=api_key, ssl=ssl
),
verify=self.verify_ssl,
timeout=self.timeout,
)
try:
status = (response.json() or {}).get('status')
assert status == 'enabled', 'Wrong credentials'
except Exception as e:
raise AssertionError('Could not enable the server: {}'.format(response.text or str(e)))
raise AssertionError(f'Could not enable the server: {response.text or e}')
return response.json()
@action
def disable(self, server: Optional[str] = None, password: Optional[str] = None, api_key: Optional[str] = None,
seconds: Optional[int] = None, ssl: bool = None):
def disable(
self,
server: Optional[str] = None,
password: Optional[str] = None,
api_key: Optional[str] = None,
seconds: Optional[int] = None,
ssl: Optional[bool] = None,
):
"""
Disable a Pi-hole server.
@ -135,46 +197,79 @@ class PiholePlugin(Plugin):
:param ssl: Set to True if the server uses SSL (default: False).
"""
if seconds:
response = requests.get(self._get_url('', server=server, password=password, api_key=api_key,
ssl=ssl, disable=seconds), verify=self.verify_ssl)
response = requests.get(
self._get_url(
'',
server=server,
password=password,
api_key=api_key,
ssl=ssl,
disable=seconds,
),
verify=self.verify_ssl,
timeout=self.timeout,
)
else:
response = requests.get(self._get_url('disable', server=server, password=password, api_key=api_key,
ssl=ssl), verify=self.verify_ssl)
response = requests.get(
self._get_url(
'disable',
server=server,
password=password,
api_key=api_key,
ssl=ssl,
),
verify=self.verify_ssl,
timeout=self.timeout,
)
try:
status = (response.json() or {}).get('status')
assert status == 'disabled', 'Wrong credentials'
except Exception as e:
raise AssertionError('Could not disable the server: {}'.format(response.text or str(e)))
raise AssertionError(f'Could not disable the server: {response.text or e}')
return response.json()
def _list_manage(self, domain: str, list_name: str, endpoint: str, server: Optional[str] = None,
password: Optional[str] = None, api_key: Optional[str] = None, ssl: bool = None):
data = {
'list': list_name,
'domain': domain
}
def _list_manage(
self,
domain: str,
list_name: str,
endpoint: str,
server: Optional[str] = None,
password: Optional[str] = None,
api_key: Optional[str] = None,
ssl: Optional[bool] = None,
):
data: Dict[str, Any] = {'list': list_name, 'domain': domain}
if password or self.password:
data['pw'] = password or self.password
elif api_key or self.api_key:
data['auth'] = api_key or self.api_key
base_url = "http{ssl}://{host}/admin/scripts/pi-hole/php/{endpoint}.php".format(
ssl='s' if ssl or self.ssl else '', host=server or self.server, endpoint=endpoint
base_url = (
f"http{'s' if ssl or (ssl is None and self.ssl) else ''}://"
f"{server or self.server}/admin/scripts/pi-hole/php/{endpoint}.php"
)
with requests.session() as s:
s.get(base_url, verify=self.verify_ssl)
response = requests.post(base_url, data=data, verify=self.verify_ssl).text.strip()
response = requests.post(
base_url, data=data, verify=self.verify_ssl, timeout=self.timeout
).text.strip()
return {'response': response}
def _list_get(self, list_name: str, server: Optional[str] = None, ssl: bool = None) -> List[str]:
response = requests.get("http{ssl}://{host}/admin/scripts/pi-hole/php/get.php?list={list}".format(
ssl='s' if ssl or self.ssl else '', host=server or self.server, list=list_name
), verify=self.verify_ssl).json()
def _list_get(
self, list_name: str, server: Optional[str] = None, ssl: Optional[bool] = None
) -> List[str]:
response = requests.get(
f"http{'s' if ssl or (ssl is None and self.ssl) else ''}"
f"://{server or self.server}/admin/scripts/pi-hole/php/"
f"get.php?list={list_name}",
verify=self.verify_ssl,
timeout=10,
).json()
ret = set()
for ll in response:
@ -182,7 +277,9 @@ class PiholePlugin(Plugin):
return list(ret)
@action
def get_blacklist(self, server: Optional[str] = None, ssl: bool = None) -> List[str]:
def get_blacklist(
self, server: Optional[str] = None, ssl: Optional[bool] = None
) -> List[str]:
"""
Get the content of the blacklist.
@ -192,7 +289,9 @@ class PiholePlugin(Plugin):
return self._list_get(list_name='black', server=server, ssl=ssl)
@action
def get_whitelist(self, server: Optional[str] = None, ssl: bool = None) -> List[str]:
def get_whitelist(
self, server: Optional[str] = None, ssl: Optional[bool] = None
) -> List[str]:
"""
Get the content of the whitelist.
@ -202,7 +301,9 @@ class PiholePlugin(Plugin):
return self._list_get(list_name='white', server=server, ssl=ssl)
@action
def get_list(self, list_name: str, server: Optional[str] = None, ssl: bool = None) -> List[str]:
def get_list(
self, list_name: str, server: Optional[str] = None, ssl: Optional[bool] = None
) -> List[str]:
"""
Get the content of a list stored on the server.
@ -213,8 +314,14 @@ class PiholePlugin(Plugin):
return self._list_get(list_name=list_name, server=server, ssl=ssl)
@action
def blacklist_add(self, domain: str, server: Optional[str] = None, password: Optional[str] = None,
api_key: Optional[str] = None, ssl: bool = None):
def blacklist_add(
self,
domain: str,
server: Optional[str] = None,
password: Optional[str] = None,
api_key: Optional[str] = None,
ssl: Optional[bool] = None,
):
"""
Add a domain to the blacklist.
@ -224,12 +331,25 @@ class PiholePlugin(Plugin):
:param api_key: Server API key (default: default configured ``api_key`` value).
:param ssl: Set to True if the server uses SSL (default: False).
"""
return self._list_manage(domain=domain, list_name='black', endpoint='add', server=server, password=password,
api_key=api_key, ssl=ssl)
return self._list_manage(
domain=domain,
list_name='black',
endpoint='add',
server=server,
password=password,
api_key=api_key,
ssl=ssl,
)
@action
def blacklist_remove(self, domain: str, server: Optional[str] = None, password: Optional[str] = None,
api_key: Optional[str] = None, ssl: bool = None):
def blacklist_remove(
self,
domain: str,
server: Optional[str] = None,
password: Optional[str] = None,
api_key: Optional[str] = None,
ssl: Optional[bool] = None,
):
"""
Remove a domain from the blacklist.
@ -239,12 +359,25 @@ class PiholePlugin(Plugin):
:param api_key: Server API key (default: default configured ``api_key`` value).
:param ssl: Set to True if the server uses SSL (default: False).
"""
return self._list_manage(domain=domain, list_name='black', endpoint='sub', server=server, password=password,
api_key=api_key, ssl=ssl)
return self._list_manage(
domain=domain,
list_name='black',
endpoint='sub',
server=server,
password=password,
api_key=api_key,
ssl=ssl,
)
@action
def whitelist_add(self, domain: str, server: Optional[str] = None, password: Optional[str] = None,
api_key: Optional[str] = None, ssl: bool = None):
def whitelist_add(
self,
domain: str,
server: Optional[str] = None,
password: Optional[str] = None,
api_key: Optional[str] = None,
ssl: Optional[bool] = None,
):
"""
Add a domain to the whitelist.
@ -254,12 +387,25 @@ class PiholePlugin(Plugin):
:param api_key: Server API key (default: default configured ``api_key`` value).
:param ssl: Set to True if the server uses SSL (default: False).
"""
return self._list_manage(domain=domain, list_name='white', endpoint='add', server=server, password=password,
api_key=api_key, ssl=ssl)
return self._list_manage(
domain=domain,
list_name='white',
endpoint='add',
server=server,
password=password,
api_key=api_key,
ssl=ssl,
)
@action
def whitelist_remove(self, domain: str, server: Optional[str] = None, password: Optional[str] = None,
api_key: Optional[str] = None, ssl: bool = None):
def whitelist_remove(
self,
domain: str,
server: Optional[str] = None,
password: Optional[str] = None,
api_key: Optional[str] = None,
ssl: Optional[bool] = None,
):
"""
Remove a domain from the whitelist.
@ -269,12 +415,26 @@ class PiholePlugin(Plugin):
:param api_key: Server API key (default: default configured ``api_key`` value).
:param ssl: Set to True if the server uses SSL (default: False).
"""
return self._list_manage(domain=domain, list_name='white', endpoint='sub', server=server, password=password,
api_key=api_key, ssl=ssl)
return self._list_manage(
domain=domain,
list_name='white',
endpoint='sub',
server=server,
password=password,
api_key=api_key,
ssl=ssl,
)
@action
def list_add(self, list_name: str, domain: str, server: Optional[str] = None, password: Optional[str] = None,
api_key: Optional[str] = None, ssl: bool = None):
def list_add(
self,
list_name: str,
domain: str,
server: Optional[str] = None,
password: Optional[str] = None,
api_key: Optional[str] = None,
ssl: Optional[bool] = None,
):
"""
Add a domain to a custom list stored on the server.
@ -285,12 +445,26 @@ class PiholePlugin(Plugin):
:param api_key: Server API key (default: default configured ``api_key`` value).
:param ssl: Set to True if the server uses SSL (default: False).
"""
return self._list_manage(domain=domain, list_name=list_name, endpoint='add', server=server, password=password,
api_key=api_key, ssl=ssl)
return self._list_manage(
domain=domain,
list_name=list_name,
endpoint='add',
server=server,
password=password,
api_key=api_key,
ssl=ssl,
)
@action
def list_remove(self, list_name: str, domain: str, server: Optional[str] = None, password: Optional[str] = None,
api_key: Optional[str] = None, ssl: bool = None):
def list_remove(
self,
list_name: str,
domain: str,
server: Optional[str] = None,
password: Optional[str] = None,
api_key: Optional[str] = None,
ssl: Optional[bool] = None,
):
"""
Remove a domain from a custom list stored on the server.
@ -301,8 +475,15 @@ class PiholePlugin(Plugin):
:param api_key: Server API key (default: default configured ``api_key`` value).
:param ssl: Set to True if the server uses SSL (default: False).
"""
return self._list_manage(domain=domain, list_name=list_name, endpoint='sub', server=server, password=password,
api_key=api_key, ssl=ssl)
return self._list_manage(
domain=domain,
list_name=list_name,
endpoint='sub',
server=server,
password=password,
api_key=api_key,
ssl=ssl,
)
# vim:sw=4:ts=4:et:

View file

@ -0,0 +1,332 @@
from marshmallow import EXCLUDE, fields, pre_dump
from marshmallow.schema import Schema
from marshmallow.validate import OneOf
from platypush.schemas import StrippedString
class CameraStatusSchema(Schema):
"""
Schema for the camera status.
"""
class Meta: # type: ignore
"""
Exclude unknown fields.
"""
unknown = EXCLUDE
name = StrippedString(
required=True,
metadata={
'description': 'Name or IP of the camera',
'example': 'Front Door',
},
)
stream_url = fields.Url(
required=True,
metadata={
'description': 'URL to the video stream',
'example': 'http://192.168.1.10:8080/video',
},
)
image_url = fields.Url(
required=True,
metadata={
'description': 'URL to get a snapshot from the camera',
'example': 'http://192.168.1.10:8080/photo.jpg',
},
)
audio_url = fields.Url(
required=True,
metadata={
'description': 'URL to get audio from the camera',
'example': 'http://192.168.1.10:8080/audio.wav',
},
)
orientation = fields.Str(
required=True,
metadata={
'description': 'Orientation of the camera',
'example': 'landscape',
'validate': OneOf(['landscape', 'portrait']),
},
)
idle = fields.Bool(
required=True,
metadata={
'description': 'Idle status of the camera',
'example': False,
},
)
audio_only = fields.Bool(
required=True,
metadata={
'description': 'Whether the camera is in audio-only mode',
'example': False,
},
)
overlay = fields.Bool(
required=True,
metadata={
'description': 'Whether the camera is in overlay mode',
'example': False,
},
)
quality = fields.Int(
required=True,
metadata={
'description': 'Quality of the video stream, in percent',
'example': 49,
},
)
night_vision = fields.Bool(
required=True,
metadata={
'description': 'Whether night vision is enabled',
'example': False,
},
)
night_vision_average = fields.Int(
required=True,
metadata={
'description': 'Average brightness for night vision',
'example': 2,
},
)
night_vision_gain = fields.Float(
required=True,
metadata={
'description': 'Brightness gain for night vision',
'example': 1.0,
},
)
ip_address = fields.Str(
required=True,
metadata={
'description': 'IP address of the camera',
'example': '192.168.1.10',
},
)
motion_limit = fields.Int(
required=True,
metadata={
'description': 'Motion limit',
'example': 250,
},
)
motion_detect = fields.Bool(
required=True,
metadata={
'description': 'Whether motion detection is enabled',
'example': False,
},
)
motion_display = fields.Bool(
required=True,
metadata={
'description': 'Whether motion display is enabled',
'example': False,
},
)
gps_active = fields.Bool(
required=True,
metadata={
'description': 'Whether GPS is active',
'example': False,
},
)
video_size = fields.Str(
required=True,
metadata={
'description': 'Size of the video stream',
'example': '1920x1080',
},
)
photo_size = fields.Str(
required=True,
metadata={
'description': 'Size of the photo',
'example': '1920x1080',
},
)
mirror_flip = fields.Str(
required=True,
metadata={
'description': 'Mirror/flip mode',
'example': 'none',
'validate': OneOf(['none', 'horizontal', 'vertical', 'both']),
},
)
video_connections = fields.Int(
required=True,
metadata={
'description': 'Number of active video connections',
'example': 0,
},
)
audio_connections = fields.Int(
required=True,
metadata={
'description': 'Number of active audio connections',
'example': 0,
},
)
zoom = fields.Int(
required=True,
metadata={
'description': 'Zoom level, as a percentage',
'example': 100,
},
)
crop_x = fields.Int(
required=True,
metadata={
'description': 'Crop X, as a percentage',
'example': 50,
},
)
crop_y = fields.Int(
required=True,
metadata={
'description': 'Crop Y, as a percentage',
'example': 50,
},
)
coloreffect = fields.Str(
required=True,
metadata={
'description': 'Color effect',
'example': 'none',
},
)
scenemode = fields.Str(
required=True,
metadata={
'description': 'Scene mode',
'example': 'auto',
},
)
focusmode = fields.Str(
required=True,
metadata={
'description': 'Focus mode',
'example': 'continuous-video',
},
)
whitebalance = fields.Str(
required=True,
metadata={
'description': 'White balance',
'example': 'auto',
},
)
flashmode = fields.Str(
required=True,
validate=OneOf(['off', 'on', 'auto']),
metadata={
'description': 'Flash mode',
'example': 'off',
},
)
torch = fields.Bool(
required=True,
metadata={
'description': 'Whether the torch is enabled',
'example': False,
},
)
focus_distance = fields.Float(
required=True,
metadata={
'description': 'Focus distance',
'example': 0.0,
},
)
focal_length = fields.Float(
required=True,
metadata={
'description': 'Focal length',
'example': 4.25,
},
)
aperture = fields.Float(
required=True,
metadata={
'description': 'Aperture',
'example': 1.7,
},
)
filter_density = fields.Float(
required=True,
metadata={
'description': 'Filter density',
'example': 0.0,
},
)
exposure_ns = fields.Int(
required=True,
metadata={
'description': 'Exposure time in nanoseconds',
'example': 9384,
},
)
iso = fields.Int(
required=True,
metadata={
'description': 'ISO',
'example': 100,
},
)
manual_sensor = fields.Bool(
required=True,
metadata={
'description': 'Whether the sensor is in manual mode',
'example': False,
},
)
@pre_dump
def normalize_bools(self, data, **_):
for k, v in data.items():
if k != 'flashmode' and isinstance(v, str) and v.lower() in ['on', 'off']:
data[k] = v.lower() == 'on'
return data

140
platypush/schemas/pihole.py Normal file
View file

@ -0,0 +1,140 @@
from marshmallow import EXCLUDE, fields
from marshmallow.schema import Schema
from marshmallow.validate import OneOf
from platypush.schemas import StrippedString
class PiholeStatusSchema(Schema):
"""
Schema for a Pi-hole status response.
"output": {
"server": "dns.fabiomanganiello.com",
"status": "enabled",
"ads_percentage": 6.7,
"blocked": 37191,
"cached": 361426,
"domain_count": 1656690,
"forwarded": 150187,
"queries": 552076,
"total_clients": 57,
"total_queries": 552076,
"unique_clients": 41,
"unique_domains": 39348,
"version": "5.18.2"
},
"""
class Meta: # type: ignore
"""
Exclude unknown fields.
"""
unknown = EXCLUDE
server = StrippedString(
required=True,
metadata={
'description': 'Hostname or IP of the Pi-hole server',
'example': '192.168.1.254',
},
)
status = fields.String(
required=True,
validate=OneOf(['enabled', 'disabled']),
metadata={
'description': 'Status of the Pi-hole server',
'example': 'enabled',
},
)
ads_percentage = fields.Float(
required=True,
metadata={
'description': 'Percentage of ads blocked by the Pi-hole server',
'example': 6.7,
},
)
blocked = fields.Integer(
required=True,
metadata={
'description': 'Number of blocked queries',
'example': 37191,
},
)
cached = fields.Integer(
required=True,
metadata={
'description': 'Number of cached queries',
'example': 361426,
},
)
domain_count = fields.Integer(
required=True,
metadata={
'description': 'Number of domains resolved the Pi-hole server',
'example': 1656690,
},
)
forwarded = fields.Integer(
required=True,
metadata={
'description': 'Number of forwarded queries',
'example': 150187,
},
)
queries = fields.Integer(
required=True,
metadata={
'description': 'Number of processed queries since the latest restart',
'example': 552076,
},
)
total_clients = fields.Integer(
required=True,
metadata={
'description': 'Number of connected clients',
'example': 57,
},
)
total_queries = fields.Integer(
required=True,
metadata={
'description': 'Total number of queries processed by the Pi-hole server',
'example': 552076,
},
)
unique_clients = fields.Integer(
required=True,
metadata={
'description': 'Number of unique IP addresses connected to the Pi-hole server',
'example': 41,
},
)
unique_domains = fields.Integer(
required=True,
metadata={
'description': 'Number of unique domains resolved by the Pi-hole server',
'example': 39348,
},
)
version = StrippedString(
required=True,
metadata={
'description': 'Version of the Pi-hole server',
'example': '5.18.2',
},
)