Merge branch 'master' into snyk-upgrade-503e414934e3e9df4999abbd15eed244
This commit is contained in:
commit
912dddd3da
34 changed files with 1078 additions and 574 deletions
13
README.md
13
README.md
|
@ -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}
|
||||
}):
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
``camera.android``
|
||||
=============================================
|
||||
|
||||
.. automodule:: platypush.message.response.camera.android
|
||||
:members:
|
|
@ -1,5 +0,0 @@
|
|||
``camera``
|
||||
=====================================
|
||||
|
||||
.. automodule:: platypush.message.response.camera
|
||||
:members:
|
|
@ -1,5 +0,0 @@
|
|||
``pihole``
|
||||
=====================================
|
||||
|
||||
.. automodule:: platypush.message.response.pihole
|
||||
:members:
|
|
@ -1,5 +0,0 @@
|
|||
``stt``
|
||||
==================================
|
||||
|
||||
.. automodule:: platypush.message.response.stt
|
||||
:members:
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
1
platypush/backend/http/webapp/dist/static/css/3924.e7e714bc.css
vendored
Normal file
1
platypush/backend/http/webapp/dist/static/css/3924.e7e714bc.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
platypush/backend/http/webapp/dist/static/js/3924.9592f111.js
vendored
Normal file
2
platypush/backend/http/webapp/dist/static/js/3924.9592f111.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
platypush/backend/http/webapp/dist/static/js/3924.9592f111.js.map
vendored
Normal file
1
platypush/backend/http/webapp/dist/static/js/3924.9592f111.js.map
vendored
Normal file
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
155
platypush/backend/http/webapp/package-lock.json
generated
155
platypush/backend/http/webapp/package-lock.json
generated
|
@ -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": "*"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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.
|
@ -1,8 +0,0 @@
|
|||
from platypush.message.response import Response
|
||||
|
||||
|
||||
class CameraResponse(Response):
|
||||
pass
|
||||
|
||||
|
||||
# vim:sw=4:ts=4:et:
|
|
@ -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:
|
|
@ -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:
|
|
@ -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:
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
0
platypush/schemas/camera/android/__init__.py
Normal file
0
platypush/schemas/camera/android/__init__.py
Normal file
332
platypush/schemas/camera/android/ipcam.py
Normal file
332
platypush/schemas/camera/android/ipcam.py
Normal 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
140
platypush/schemas/pihole.py
Normal 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',
|
||||
},
|
||||
)
|
Loading…
Reference in a new issue