Compare commits

...

818 Commits

Author SHA1 Message Date
Fabio Manganiello 14e7b44f86
Updated dist files 2023-05-20 02:36:24 +02:00
Fabio Manganiello ca342ee3ea
Removed XML header from the generated SVG 2023-05-20 02:34:07 +02:00
Fabio Manganiello 14d6924338
Added beforeinstallprompt listener 2023-05-20 02:33:26 +02:00
Fabio Manganiello 9ba7ad9402
Removed XML header from the generated SVG 2023-05-20 02:32:48 +02:00
Fabio Manganiello d3dde80269
Display the logo directly as an SVG on the login/registration page. 2023-05-20 02:31:26 +02:00
Fabio Manganiello 47395f0b03
Updated icon files 2023-05-20 02:21:35 +02:00
Fabio Manganiello 075efde58c
Added route to dynamically generate logo.svg. 2023-05-20 01:18:48 +02:00
Fabio Manganiello c5aee0a65d
Updated webapp dist files 2023-05-18 03:17:24 +02:00
Fabio Manganiello 795754f858
Added PWA support 2023-05-18 03:12:48 +02:00
Fabio Manganiello 27d4a20418
Use reflection to infer the arguments of a Python user procedure 2023-05-17 17:17:59 +02:00
Fabio Manganiello 0a1209fe6e
Updated webapp dist files 2023-05-17 10:56:37 +02:00
Fabio Manganiello 33e2879413
Various UI improvements for the execute tab. 2023-05-17 10:41:02 +02:00
Fabio Manganiello 91daec579d
Reverted to the previous style for entities on mobile.
Better to use screen width wisely and avoid unnecessary padding.
2023-05-17 01:13:09 +02:00
Fabio Manganiello 61ea3d79e4
Large refactor for the `inspect` plugin.
More common logic has been extracted and all the methods and classes
have been documented and black'd.
2023-05-17 00:05:22 +02:00
Fabio Manganiello 2cba504e3b
Improvements for the autocomplete component. 2023-05-14 15:07:54 +02:00
Fabio Manganiello 8447f9a854
Improved rendering of actions/arguments documentation.
The frontend now calls `utils.rst_to_html` to render the docstrings as
HTML instead of dumping them as raw text.

Also, actions and arguments are now cached to improve performance.
2023-05-14 15:06:34 +02:00
Fabio Manganiello 5f2d6dfeb5
Added `utils.rst_to_html` action. 2023-05-14 15:05:24 +02:00
Fabio Manganiello 3c83e7f412
A faster implementation for the `inspect.get_*` methods.
Plugin/backend lookup is now done by inspecting the manifest files
instead of searching all the subpackages.
2023-05-13 13:44:46 +02:00
Fabio Manganiello 72797e73ff
Changed the Tornado paradigm to start the WSGI workers.
Use `bind_sockets`/`fork_processes` instead of reinventing the wheel
with our own multiprocessing handling.
2023-05-13 12:35:20 +02:00
Fabio Manganiello ac4fe4447e
Revert "Added a multi-worker approach to the Tornado WSGI container."
This reverts commit 71401a4936.

Temporarily reverted this commit because the `reuse_address` on the
application's `listen` method has only been implemented in Tornado 6.2 -
and Debian stable still shipts Tornado 6.1.
2023-05-13 02:36:20 +02:00
Fabio Manganiello 71401a4936
Added a multi-worker approach to the Tornado WSGI container.
The WSGI container is a good option to wrap a multi-modal webapp
(Flask + websocket routes), but it's constrained to a single-process
approach and queued/pre-buffered requests. That makes performance poor
when handling requests that may take a few seconds to complete.
2023-05-13 01:26:18 +02:00
Fabio Manganiello b7b93edbae
Updated dist files 2023-05-12 03:52:33 +02:00
Fabio Manganiello a15191d4ca
Updated dist files 2023-05-12 03:51:59 +02:00
Fabio Manganiello d4f8e51caf
A less blocking implementation of the entities loading UI logic. 2023-05-12 03:49:20 +02:00
Fabio Manganiello 62d846ddda
Updated dist files 2023-05-12 03:42:17 +02:00
Fabio Manganiello 23a5e90e2e
Updated dist files 2023-05-12 03:26:55 +02:00
Fabio Manganiello 6cd9cb6e76
Better entities caching on the frontend. 2023-05-12 03:18:22 +02:00
Fabio Manganiello 79871e0fa1
Fixed devServer routes in vue.config.js.
Use `127.0.0.1` instead of `localhost` for the Vue proxy.
2023-05-12 02:57:14 +02:00
Fabio Manganiello cfedcd701e
Performance improvements when loading the Tensorflow plugin.
The Tensorflow module may take a few seconds to load the first time and
slow down the first scan of the plugins.

All the Tensorflow imports should therefore be placed close to where
they are used instead of being defined at the top of the module.
2023-05-11 19:48:22 +02:00
Fabio Manganiello f49ad4c349
Updated dist files 2023-05-10 02:52:24 +02:00
Fabio Manganiello 6b0f0883ee
A proper way to proxy websocket calls using the Vue devServer. 2023-05-10 02:27:01 +02:00
Fabio Manganiello 78c12212c6
[#260] A simple entities caching mechanism using the browser storage. 2023-05-10 02:26:06 +02:00
Fabio Manganiello 74ab884b7a
Proper redirects upon `/execute` failure.
If a call to `/execute` fails with a 401 or 412 status, then redirect
the user to `/register` or `/login`.
2023-05-10 02:24:50 +02:00
Fabio Manganiello 0de56ad52e
Added nginx sample configuration. 2023-05-10 00:59:41 +02:00
Fabio Manganiello 1395c472c0
`docutils` moved to required dependencies. 2023-05-09 21:59:36 +02:00
Fabio Manganiello 41233138ff
Blackened `inspect` module and extracted model defs to adjacent module. 2023-05-09 21:58:02 +02:00
Fabio Manganiello b91aedc553
Updated README toc format 2023-05-09 02:56:44 +02:00
Fabio Manganiello 5415f0ccf3
Updated README 2023-05-09 02:54:02 +02:00
Fabio Manganiello ab2425ebd0
[#260] Removed legacy `backend.websocket`.
It has now been replaced by the `/ws/events` and `/ws/requests`
websocket routes under `backend.http`.
2023-05-09 02:46:43 +02:00
Fabio Manganiello a069d23bb7
[#260] Added ``/ws/requests`` websocket route. 2023-05-09 02:40:32 +02:00
Fabio Manganiello 7716a416e9
[#260] Support for sending events via websocket over `/ws/events`. 2023-05-09 02:18:58 +02:00
Fabio Manganiello edb7197f71
[#260] Implemented authentication for websocket routes.
Plus, refactored the `backend.http.app.utils` module by breaking it down
into multiple components, as the module was starting to get too large.
2023-05-09 00:03:11 +02:00
Fabio Manganiello 2d4b179879
Include the path info in the logging messages in WSRoute. 2023-05-08 12:35:54 +02:00
Fabio Manganiello 3fc622e296
Remove legacy references to the websocket HTTP port and backend in platydock. 2023-05-08 12:25:09 +02:00
Fabio Manganiello f5fcccb0bd
Refactored the new websocket routes.
Defined a `platypush.backend.http.ws` package with all the routes, a
base `WSRoute` class that all the websocket routes can extend, and a
logic in the HTTP backend to automatically scan the package to register
exposed websocket routes.
2023-05-08 11:45:14 +02:00
Fabio Manganiello 56dc8d0972
Migrated the webapp to Tornado.
It was just too painful to find a combination of versions of  gunicorn,
gevent, eventlet, pyuwsgi etc. that could work on all of my systems.

On the other hand, Tornado works out of the box with no headaches.

Also in this commit:

- Updated a bunch of outdated/required integration dependencies.
- Black'd and LINTed a couple of old plugins.
2023-05-08 02:06:45 +02:00
Fabio Manganiello f81e9061a3
`lifespan=on` is actually not required on Flask config level. 2023-05-07 16:30:41 +02:00
Fabio Manganiello 692180c653
Back to uvicorn workers from eventlet.
The eventlet API has way too many dependency issues with gunicorn.

Still TODO: Fix or at least mitigate the WSGI workers timeout issue when
they handle websocket connections.
2023-05-07 15:37:58 +02:00
Fabio Manganiello 8102178ba4
Updated dist files 2023-05-07 13:00:48 +02:00
Fabio Manganiello 29c0a11c37
Forced to use the gunicorn version on Github.
Reason: gunicorn maintainers no longer give a fuck about their project
and they aren't letting anybody take over either - see
https://github.com/benoitc/gunicorn/pull/2581

This is not how a FOSS project should be run. A project with 9k stars
and countless usages shouldn't end up in a situation where users beg for
two years for a new release that fixes a bad regression and a bad
security vulnerability. The way gunicorn is maintained and run is an
insult to the whole FOSS community.
2023-05-07 12:54:50 +02:00
Fabio Manganiello c0a948f8ce
Removed remaining references to websocket port. 2023-05-07 12:54:13 +02:00
Fabio Manganiello bdbbd24e6f
Only include /ws/events as a proxied websocket, without messing with the /ws route exposed by the Vue debugger 2023-05-07 12:22:51 +02:00
Fabio Manganiello 059fff8558
Updated dist files 2023-05-07 12:19:13 +02:00
Fabio Manganiello f9b0bc905e
Migrated websocket service.
The websocket service is no longer provided by a different service,
controlled by a different thread running on another port.

Instead, it's now exposed directly over Flask routes, using
WSGI+eventlet+simple_websocket.

Also, the SSL context options have been removed from `backend.http`, for
sake of simplicity. If you want to enable SSL, you can serve Platypush
through a reverse proxy like nginx.
2023-05-07 12:08:28 +02:00
Fabio Manganiello 3aefc9607d
Migrated from waitress to gunicorn.
`waitress`, unlike `gunicorn`, doesn't provide an easy way to plug into
a WSGI socket that can be used for the websocket interface.
2023-05-07 00:42:57 +02:00
Fabio Manganiello ca65db016e
Added description line to `conf.py`. 2023-05-06 23:26:07 +02:00
Fabio Manganiello 9951d62511
Added logic to automatically generate the secret key for Flask. 2023-05-06 22:04:48 +02:00
Fabio Manganiello d1f0e1976c
Exclude squashfs/loopback mounts from `system.disk_info`. 2023-05-06 18:53:16 +02:00
Fabio Manganiello e33a391d25
Updated dist files 2023-05-06 12:37:00 +02:00
Fabio Manganiello 4f78d61223
Improved UI on mobile. 2023-05-06 12:34:27 +02:00
Fabio Manganiello 6e939bbe62
Close modals and dropdown when ESC is pressed. 2023-05-05 20:46:42 +02:00
Fabio Manganiello e9e59c857a
Updated dist files 2023-05-05 02:51:33 +02:00
Fabio Manganiello 59bf1c2aa0
Added close button to the modal's header. 2023-05-05 02:48:41 +02:00
Fabio Manganiello 8af3ae17b8
A more efficient way of detecting the entity groups to display.
Instead of iterating over each of the entities in a grouping to find out
which groups should be displayed based on the selector's policy, the
selector can directly keep its `selectedGroups` attribute in sync with
the index.
2023-05-05 02:33:34 +02:00
Fabio Manganiello 373788377b
Created two separate actions under `variable` to delete/unset.
`delete` will actually remove the record from the database (same as
`unset`'s new behaviour), while `unset` will set it to null without
deleting it (same as the `unset`'s previous behaviour).
2023-05-05 02:21:18 +02:00
Fabio Manganiello 98b9d31dd4
Updated dist files 2023-05-05 01:10:24 +02:00
Fabio Manganiello 4383dbb2b4
Bluetooth UI toggle aligned to the right - like all other toggles. 2023-05-05 01:04:39 +02:00
Fabio Manganiello 285f3941d9
Always use an external uWSGI server to run the web service.
Added `waitress` dependency. For performance and security reasons, it's
better to always run the Flask application inside of a uWSGI server.

`waitress` also makes things easier by avoiding to ask the user to
manually provide the external executable arguments, as it was the case
with `uwsgi` and `gunicorn`.
2023-05-05 00:07:13 +02:00
Fabio Manganiello 2c254e8eb9
numpy and PIL should be required dependencies for all camera plugins. 2023-05-04 23:44:42 +02:00
Fabio Manganiello 99311a6e71
Updated dist files 2023-05-04 02:23:24 +02:00
Fabio Manganiello 7db09276ca
Some small style improvements. 2023-05-04 02:20:40 +02:00
Fabio Manganiello 2398cac572
A more efficient and clean logic for `selectedEntities` calculation. 2023-05-04 02:19:55 +02:00
Fabio Manganiello 394e27eaf2
Refactored style for UI dropdowns. 2023-05-04 02:19:09 +02:00
Fabio Manganiello 9fd7f7db04 Fixed compatibility with new Sphinx version 2023-05-04 01:05:27 +02:00
Fabio Manganiello c690230930
An `AssistantEvent` should not fail initialization if the assistant integration isn't found. 2023-05-04 00:28:50 +02:00
Fabio Manganiello 04b1dad6d8
Removed `test_cron_execution_upon_system_clock_change`.
The test is too brittle as it depends on small mocked clock skews and it
can easily fail.
2023-05-04 00:11:11 +02:00
Fabio Manganiello 91d1d33ab6
Exclude `tests` from pip installation. 2023-05-03 21:45:02 +02:00
Fabio Manganiello 5d1c8cf8e9
Additional null check on this.searchTerm 2023-05-03 03:33:34 +02:00
Fabio Manganiello 3482c29679
Updated dist files 2023-05-03 03:18:44 +02:00
Fabio Manganiello a06d0ef6a1
Merged all the items in the entities panel's header in the Selector component. 2023-05-03 03:14:46 +02:00
Fabio Manganiello 7c7818dd76
Fixed entity search.
It was broken by the previous refactor of the entities panel, which no
longer triggers the `watch` callback on the upstream `entityGroups`.

The new approach listens for entity updates on the frontend bus and
dynamically creates the entity groupings in `selectedGroups` if they are
missing.
2023-05-03 02:12:14 +02:00
Fabio Manganiello 9922305ac5
Fixed grouping for `entityGroups.id`.
Unlike the other entity groupings, which are 4-layered (`grouping ->
group -> entity_id -> entity`), the grouping by ID only needs 3 layers
(`grouping -> entity_id -> entity`).
2023-05-03 02:09:51 +02:00
Fabio Manganiello cc2ec1db7f
The HTTP Zeroconf service should be registered before the server starts. 2023-05-02 21:24:50 +02:00
Fabio Manganiello 55cb87d14f
Updated dist files 2023-05-02 10:24:11 +02:00
Fabio Manganiello 68359b88a9
More performance improvements for the entities page.
- Don't recalculate entity groups every time. Instead, keep them in sync
  every time an entity is added or removed.

- Removed `computedChildren` from the entity component - no null nodes
  are guaranteed to be passed now, so there's no need for another
  iteration on the list of children.

- `childrenByParentId` now only looks in the scope of the entity's
  children instead of searching all the entities.
2023-05-02 10:14:03 +02:00
Fabio Manganiello 0fc0a22cd7
Reintroduced loading icon spin animation. 2023-05-02 10:08:36 +02:00
Fabio Manganiello 3febfabdd7
Bluetooth LE blacklisted device notices moved `info -> debug`. 2023-05-01 22:10:06 +02:00
Fabio Manganiello 998990aabc
Made `Entity.children_ids` resilient against deleted objects. 2023-05-01 22:09:16 +02:00
Fabio Manganiello 9d82ce6ea9
Noisy beacons notice back to debug level.
There's just too many of them and it ends up polluting the logs.
2023-05-01 21:25:42 +02:00
Fabio Manganiello ce248ccfbb
Added `children_ids` to the entity attributes serialized in `to_json`. 2023-05-01 19:59:13 +02:00
Fabio Manganiello de76c2b6a8
Updated dist files 2023-05-01 10:12:43 +02:00
Fabio Manganiello 835ad9f2dc
Use plugin as a default entity grouping instead of category 2023-05-01 10:06:11 +02:00
Fabio Manganiello 85ecdcb0cb
Removed entity icon loading animation.
The animation has a big impact on page loading performance when the
system includes a high number of entities that all need their loading
animation to be render.
2023-05-01 09:49:34 +02:00
Fabio Manganiello 24c6b7b377
Updated dist files 2023-05-01 01:25:27 +02:00
Fabio Manganiello b7cf1a42de
Use a proxy value in the `variable` component for the textbox.
Otherwise the value may keep being overwritten while the user is typing
a new one.
2023-05-01 01:22:20 +02:00
Fabio Manganiello a3839e637d
Set a max-width: 600px for group containers. 2023-05-01 01:22:02 +02:00
Fabio Manganiello 1e43866978
Moved more entity common CSS out of the Vue component. 2023-05-01 01:21:18 +02:00
Fabio Manganiello de84a65a22
Show prettified entity type when hover the entity icon. 2023-05-01 01:20:31 +02:00
Fabio Manganiello 7906ee2c49
Entity components style improvements.
Multiple style improvements for the entity components. Among these:

- A more consistent style for entity values and toggler buttons.
- Fixed overflowing/underflowing entities on smaller/larger screen
  sizes.
- Simplified the stylesheets for many entities as many component classes
  have now been moved to `common.scss`.
2023-04-30 22:32:50 +02:00
Fabio Manganiello d1066ba624
Use Math.round instead of parseInt when parsing durations. 2023-04-30 16:22:37 +02:00
Fabio Manganiello 5d4bffa119
Fixed retrieval of `entities` plugin. 2023-04-30 10:42:05 +02:00
Fabio Manganiello 94a493580c
Updated dist files 2023-04-30 01:36:13 +02:00
Fabio Manganiello 0b853e0a54
Apply `word-break: break-all` to entities' names and values.
The entity name and value in the component header may be arbitrarily
long and rendered on small screens.

We therefore need to ensure that the text won't overflow the screen
width.
2023-04-30 01:17:54 +02:00
Fabio Manganiello 3d7755159f
Improved compatibility for `traceback.format_exception`.
The new syntax, that only requires an `Exception` instance to be passed
to the function, is only compatible with Python >= 3.10.
2023-04-30 00:38:17 +02:00
Fabio Manganiello 12cca4991a
Fixed paths for Alembic's `package_data`. 2023-04-29 23:48:06 +02:00
Fabio Manganiello 6b28d16ccf
Exclude more noisy Bluetooth beacons.
Exclude any beacons from devices with no name, no children other than
services, and with none of those services being public/known.
2023-04-29 23:34:24 +02:00
Fabio Manganiello f764d1b4fb
Noisy Bluetooth beacons notices should be logged on info level. 2023-04-29 23:18:12 +02:00
Fabio Manganiello 52f036dc1d
Updated dist files 2023-04-29 22:49:35 +02:00
Fabio Manganiello cbf0ea8a19
Style fixes for mobile screens. 2023-04-29 22:45:10 +02:00
Fabio Manganiello 9ebdaf620e Merge pull request '[#255] Model variables as entities' (#256) from 255-model-variables-as-entities into master
Reviewed-on: platypush/platypush#256
Closes: #255
2023-04-29 18:24:24 +02:00
Fabio Manganiello e96885a805
Delete the entity on `variable.unset` instead of setting it to null. 2023-04-29 18:21:57 +02:00
Fabio Manganiello b4048002b9
Updated dist files 2023-04-29 18:21:32 +02:00
Fabio Manganiello 6d9c34f06f
Added VariableModal to set variables from the dashboard. 2023-04-29 18:20:41 +02:00
Fabio Manganiello a3888be216
The robustness check in case of missing fields should also apply to other system entities. 2023-04-29 16:08:38 +02:00
Fabio Manganiello 8c9768b05e
Robustness check for system disk entities.
When the system information is still loading it may happen that the
device associated to the disk hasn't been loaded yet.
2023-04-29 16:04:57 +02:00
Fabio Manganiello a20065c649
Exposed `_entities` utility property in `Plugin`.
It can be used by other plugins to easily access the `entities` plugin,
along the lines of `db` and `redis`.
2023-04-29 15:50:31 +02:00
Fabio Manganiello 68d8befa34
Removed some vestigial commented code. 2023-04-29 15:28:44 +02:00
Fabio Manganiello 23b851e9d7
`variable.status` robustness fix.
`entities.transform_entities` will pass back an empty list instead of an
empty dict if no entities were found, and the function should be able to
handle it.
2023-04-29 15:24:58 +02:00
Fabio Manganiello e919bf95ad
Print the full stack trace if a plugin failed in `entities.scan` 2023-04-29 15:14:13 +02:00
Fabio Manganiello 38c87ef39f
Added frontend component for the `Variable` entity. 2023-04-29 11:37:21 +02:00
Fabio Manganiello f40f956507
Migrated `variable` table to the new entities framework. 2023-04-29 11:36:55 +02:00
Fabio Manganiello 8fe61217ce
Added `_db` and `_redis` properties to the Plugin class.
Plugins can now access the database and Redis APIs directly without
having to run their own `get_plugin` validation logic.
2023-04-29 11:35:57 +02:00
Fabio Manganiello a8d2261f32
Added `core_plugins` to the configuration.
These plugins (only including `variable` for now) are a core part of the
application and should always be explicitly enabled.
2023-04-29 11:34:34 +02:00
Fabio Manganiello 78cee5d9b0
Added support for automatic database migrations.
Added Alembic environment and `run_db_migrations` logic to the entities
engine so database schema changes can be processed as soon as the
application is started.
2023-04-29 11:32:31 +02:00
Fabio Manganiello ff9b76477d
Fixed arguments naming. 2023-04-28 11:04:33 +02:00
Fabio Manganiello 38262e245e Merge pull request '[#253] Support for relational filters on event hooks' (#254) from 253-support-for-relational-filters-on-event-hooks into master
Reviewed-on: platypush/platypush#254
Closes: #253
2023-04-27 22:08:37 +02:00
Fabio Manganiello 162904f281
[#253] Added support for relational filters on event hooks. 2023-04-27 22:07:02 +02:00
Fabio Manganiello 87db5ca5f3 Exclude all iBeacon devices by default (it's not only Apple, it's everyone) 2023-04-26 14:17:59 +02:00
Fabio Manganiello 7685521e2b
Always use the default configuration values for MQTT listeners if not specified 2023-04-26 03:30:05 +02:00
Fabio Manganiello 10d587efd0
FIX: Possible assert evaluation error.
Some versions/configurations of Python may throw `Boolean value of this
clause is not defined` here.
2023-04-26 02:25:28 +02:00
Fabio Manganiello 339786b123 Merge pull request 'Support for nested/partial event hook filters' (#252) from 251-nested-filters-on-event-hooks into master
Reviewed-on: platypush/platypush#252
Closes: #251
2023-04-26 01:55:27 +02:00
Fabio Manganiello 245472a4c5
Better event hooks filters.
- Support for nested attributes on event hook conditions. Things like
  these are now possible:

```
from platypush.event.hook import hook
from platypush.message.event.entities import EntityUpdateEvent

@hook(EntityUpdateEvent, entity={"external_id": "system:cpu"})
def on_cpu_update_event(event: EntityUpdateEvent, **_):
    print(event.args["entity"]["percent"])
```

- The scoring/regex extraction/partial string match logic in
  `_matches_argument` is actually only needed for
  `SpeechRecognizedEvent`. Other events don't need these features, and
  event hooks may be actually triggered unexpectedly in case of partial
  matches. Therefore, the "complex" `_matches_argument` has been moved
  as an override only for `SpeechRecognizedEvent`, and all the other
  events will perform simple key-value matching.
2023-04-26 01:45:58 +02:00
Fabio Manganiello ee54e0edbf
Use a font-awesome spinner instead of an animated gif when loading entities 2023-04-25 16:42:01 +02:00
Fabio Manganiello cb288deb71
Exclude more noisy BLE beacons.
Excluding Apple iBeacons and devices with no name and no services.
2023-04-25 16:19:11 +02:00
Fabio Manganiello 99382e4505 Merge pull request 'Fixed compatibility with SQLAlchemy >= 2.0' (#250) from 239-sqlalchemy-2-compatibility into master
Reviewed-on: platypush/platypush#250
Closes: #239
2023-04-25 10:47:27 +02:00
Fabio Manganiello 9c93b793e3
Merge branch 'master' into 239-sqlalchemy-2-compatibility 2023-04-25 10:44:31 +02:00
Fabio Manganiello dd60b8924d
Wrap the `PRAGMA` statement in `sqlalchemy.text`.
SQLAlchemy 2 no longer supports raw strings passed to `.execute()`
methods.
2023-04-25 10:41:37 +02:00
Fabio Manganiello 440d70d9cf
LINT/format fixes. 2023-04-25 10:36:27 +02:00
Fabio Manganiello 4cc88fcf5f
Rewritten the `variable` plugin to use SQLAlchemy's ORM.
This removes the need for raw SQL statements and CREATE TABLE statements
that may be engine-specific.
2023-04-25 10:35:12 +02:00
Fabio Manganiello e1cd22121a
Removed `connection.begin()` pattern from the `db` plugin.
SQLAlchemy should automatically begin a transaction on
connection/session creation. Plus, `.begin()` messes up things with
SQLAlchemy 2, which has `autobegin` enabled with no easy way of
disabling it.
2023-04-25 10:31:49 +02:00
Fabio Manganiello f4e13d0cb0
No need for `session.begin` in `db.create_all`. 2023-04-24 23:57:47 +02:00
Fabio Manganiello 37722d12cd
No need for `session.begin` in `db.create_all`. 2023-04-24 23:55:50 +02:00
Fabio Manganiello 6fa179e769
LINT fixes 2023-04-24 23:49:31 +02:00
Fabio Manganiello d33d760361
Better way to import `declarative_base` from SQLAlchemy.
Import `declarative_base` in a way that is compatible with any
SQLAlchemy version between 1.3 and 2.x.
2023-04-24 23:23:55 +02:00
Fabio Manganiello 91df18f7b5
Better way to import `declarative_base` from SQLAlchemy.
Import `declarative_base` in a way that is compatible with any
SQLAlchemy version between 1.3 and 2.x.
2023-04-24 23:21:39 +02:00
Fabio Manganiello 87889142e0
Fixed compatibility with SQLAlchemy >= 2.0 in the `db` plugin. 2023-04-24 22:52:17 +02:00
Fabio Manganiello 8478245cde
Removed `Mapped[Entity]` type annotation.
`Mapped` has been introduced only in SQLAlchemy 1.4, while Debian stable
still ships 1.3.

Removing the type annotation doesn't come with a big cost, but it keeps
Platypush compatible with Debian stable.
2023-04-24 21:48:52 +02:00
Fabio Manganiello e955ffc018
Be more resilient in DateTimeWeather widget about custom temperature/humidity names or non-numeric data 2023-04-24 12:48:51 +02:00
Fabio Manganiello 5638c567ff
Show temperature and humidity on the DateTimeWeather widget upon sensor events only if the sensor data is numeric. 2023-04-24 10:59:45 +02:00
Fabio Manganiello bfa296e7c5
Fixed dataclass JSON serialization 2023-04-24 01:18:33 +02:00
Fabio Manganiello 9c03b028d7 Be a bit more resilient if an upstream integration sent some empty entities 2023-04-24 00:44:16 +02:00
Fabio Manganiello 6711b26137
Support dataclass serialization in the standard message serializer. 2023-04-24 00:43:06 +02:00
Fabio Manganiello dc3392c11d
Disk I/O stats are not always available and should therefore be optional. 2023-04-23 22:25:24 +02:00
Fabio Manganiello 8e7d444c02
Updated CHANGELOG 2023-04-23 21:19:31 +02:00
Fabio Manganiello 0cd28f1040
libbluetooth-dev is a required dependency to build pybluez on Debian-derived distros 2023-04-23 18:59:37 +02:00
Fabio Manganiello 9c1855e4c0
Fixed docstring for `zigbee.mqtt` plugin. 2023-04-23 13:03:10 +02:00
Fabio Manganiello 0fc05135df
Updated docs 2023-04-23 02:14:57 +02:00
Fabio Manganiello 512ced3e94
Updated dist files 2023-04-23 02:13:48 +02:00
Fabio Manganiello 6439e235d2
Updated caniuse dependency 2023-04-23 02:11:21 +02:00
Fabio Manganiello 27b1048789
Converted `system.processes` to the new data model. 2023-04-23 02:08:43 +02:00
Fabio Manganiello 387616ea96
Convert `system.connected_users` to the new data model. 2023-04-23 01:12:07 +02:00
Fabio Manganiello 259b42bdd6
Removed legacy `backend.sensor.battery`. 2023-04-23 00:44:03 +02:00
Fabio Manganiello 763d9e06ec
Increased default `poll_interval` for `system` plugin to 60 seconds. 2023-04-23 00:42:44 +02:00
Fabio Manganiello a72c32cb00
Added battery entity support to `system` plugin. 2023-04-23 00:41:21 +02:00
Fabio Manganiello b3440ab96b
Added support for fan sensors on the `system` plugin. 2023-04-23 00:08:27 +02:00
Fabio Manganiello 45d5f439be
Added support for system temperature sensor entities. 2023-04-22 22:42:11 +02:00
Fabio Manganiello 1b048e1952
s/net_connections/network_connections/g 2023-04-22 17:19:24 +02:00
Fabio Manganiello 374f936c1f
Merged `network_stats` into `NetworkInterface` model. 2023-04-22 17:19:24 +02:00
Fabio Manganiello f4036be52b
Extracted and refactored more common elements of the Entity components. 2023-04-22 17:19:23 +02:00
Fabio Manganiello e213941791
s/net_io_counters/network_info/g 2023-04-22 17:19:23 +02:00
Fabio Manganiello 977b55dea9
Merged network addresses into `NetworkInterface` model. 2023-04-22 17:19:23 +02:00
Fabio Manganiello ebe79ac29a
Refactored system schema dataclasses.
- `percent_field` should be declared on `platypush.schemas.dataclasses`
  level, since it's not specific to the `system` plugin.
- Added a common `SystemBaseSchema` that takes care of calling
  `_asdict()` if the object is passed as a `psutil` object instead of a
  dict.
2023-04-22 17:19:23 +02:00
Fabio Manganiello 2d618188c8
Print the full exception stack trace if `.status` fails. 2023-04-22 17:19:23 +02:00
Fabio Manganiello b3a0896485
Converted `NetworkConnection` schema/response. 2023-04-22 17:19:22 +02:00
Fabio Manganiello d473b5d836 Make the recursive entity merger/column set logic more resilient against ObjectDeletedError 2023-04-22 10:40:30 +02:00
Fabio Manganiello 98a300c4b1
Added `NetworkInterface` entities to `system` plugin.
Plus, `platypush.schemas.system` has now been split into multiple
submodules to avoid a single-file mega-module with all the system
schemas definitions.
2023-04-21 00:45:15 +02:00
Fabio Manganiello 44b8fd4b34
Support for `disk` entities in the `system` integration. 2023-04-20 16:26:51 +02:00
Fabio Manganiello 6b03451386
Better responsive alignment for the collapse toggler. 2023-04-20 16:26:05 +02:00
Fabio Manganiello e8c96ad35d
Added `convertTime` utility function 2023-04-20 02:27:58 +02:00
Fabio Manganiello 153d03d43f
Moved CPU percentage on the level of the CPU entity instead of a child entity. 2023-04-19 01:48:05 +02:00
Fabio Manganiello 4ebfbf3851
Added memory stats entities. 2023-04-19 01:31:11 +02:00
Fabio Manganiello 0073239a40
Support for CPU `load_average` entity. 2023-04-18 18:26:02 +02:00
Fabio Manganiello 1cee0459cf
Added `CpuFrequency` entity to `system`. 2023-04-18 01:49:36 +02:00
Fabio Manganiello a5b0a524f6
Added `CpuStats` entity to `system`. 2023-04-18 01:19:06 +02:00
Fabio Manganiello b4fbd3e915
Added `percent` entity to `cpu`. 2023-04-17 02:25:04 +02:00
Fabio Manganiello 711cc2b239
Removed (now unused) `CpuTimesResponse`. 2023-04-17 02:25:03 +02:00
Fabio Manganiello b9286f50b0
Added support for `CpuTimes` as an entity of the `system` plugin.
Also, there is now a single `Cpu` entity being exported, with a nested
hierarchy structured like:

```
cpu
  -> cpu_info
  -> cpu_times
    -> idle
    -> user
    -> system
    -> ...
  -> cpu_load
    -> ...
```
2023-04-17 02:25:03 +02:00
Fabio Manganiello 4842c1911b
Frontend entities should have a reference to `allEntities`.
There are probably more optimal ways of achieving this other than
passing a reference to the full list of entities to each of the
entities, such as running a BFS to recursively expand all the entities
within the child hierarchy of an entity.

This is needed because the entity needs to know which entities aren't
direct children, but are two or more layers down in the hierarchy, so
they should be passed to their own child entities.
2023-04-17 02:25:03 +02:00
Fabio Manganiello 6e65783feb
Added schemas for `CpuTimes`. 2023-04-17 02:25:03 +02:00
Fabio Manganiello e810025a6d
Added `Cpu` and `CpuTimes` entities. 2023-04-17 02:25:03 +02:00
Fabio Manganiello 65481dc6b4
Added `PercentSensor` entity type. 2023-04-17 02:25:02 +02:00
Fabio Manganiello e7f64843a5
Added `include_children` parameter to `_merge_columns`.
We need to recursively merge the columns of children entities if a child
entity isn't a leaf node.
2023-04-17 02:25:02 +02:00
Fabio Manganiello b43017ef01
Refactoring the `system` plugin to support entities. 2023-04-17 02:25:02 +02:00
Fabio Manganiello 3e3c48d779
Defined new entity and schema for CpuInfo. 2023-04-17 02:25:02 +02:00
Fabio Manganiello 186a21f715
Added CpuInfo entity frontend components. 2023-04-17 02:25:01 +02:00
Fabio Manganiello 74aeca5c34
Trigger a sensor event only if abs(old_data - new_data) > tolerance
Not if abs(old_data - new_data) >= tolerance, otherwise events will
always be triggered when tolerance=0, even if the data hasn't changed.
2023-04-17 02:25:01 +02:00
Fabio Manganiello 4c19535612 A more resilient logic on entity copy/serialization to prevent ObjectDeletedError 2023-04-13 17:16:21 +02:00
Fabio Manganiello a499b7bc2f
Deprecated `poll_seconds` in `light.hue`.
For sake of naming consistency with other plugins, we should use
`poll_interval` instead.
2023-04-03 01:36:12 +02:00
Fabio Manganiello 10955dad72
Fixed some documentation glitches in `switchbot`. 2023-04-03 01:36:12 +02:00
Fabio Manganiello f9ce4b75e8
Updated docs 2023-04-03 01:36:12 +02:00
Fabio Manganiello d5de38975d
generate_missing_docs 2.0 2023-04-03 01:36:12 +02:00
Fabio Manganiello 6e5f746dbe
Removed deprecated `gpio.sensor` base plugin.
Now all the plugins that used to implement it have been moved to
`SensorPlugin`.
2023-04-03 01:36:12 +02:00
Fabio Manganiello 8852cb8db4
Fixed new class name for `sensor.mcp3008` plugin. 2023-04-03 01:36:12 +02:00
Fabio Manganiello d5ddc0c65e
Migrated `arduino` integration to the new `SensorPlugin` API. 2023-04-03 01:36:12 +02:00
Fabio Manganiello cf16076bce
Added icons for new entity sensor sources. 2023-04-03 01:36:11 +02:00
Fabio Manganiello ac2ec58f89
Migrated `mcp3008` integration to the new `SensorPlugin` API. 2023-04-03 01:36:11 +02:00
Fabio Manganiello 45e5ca47e7 Fallback for sensor._has_changes 2023-04-02 15:38:49 +02:00
Fabio Manganiello 962c55937d
Migrated `sensor.distance` integration.
Remove `backend.sensor.distance` and `gpio.sensor.distance`. They are
now replaced by the `sensor.hcsr04` integration, which is compatible
with the new `SensorPlugin` API.
2023-04-02 14:20:12 +02:00
Fabio Manganiello 92578a17c9
Added small docstring portion 2023-04-02 13:55:00 +02:00
Fabio Manganiello beff88986a
Migrated `dht` integration.
Removed `backend.sensor.dht` and `gpio.sensor.dht`. They have been
merged into the new `sensor.dht` integration, which supports the new
`SensorPlugin` API.
2023-04-02 13:38:53 +02:00
Fabio Manganiello 8f604445a2
Migrated old `sensor.accelerometer` integration.
Removed `backend.sensor.accelerometer` and `gpio.sensor.accelerometer`.
The logic has now been merged in the new `sensor.lis3dh` integration,
which is compatible with the new `SensorPlugin` API.
2023-04-02 13:22:28 +02:00
Fabio Manganiello 44cf25271c
Migrated `pmw3901` integration.
Removed legacy `backend.sensor.motion.pmw3901` and
`gpio.sensor.motion.pmw3901`. They have been merged in the new
`sensor.pmw3901` integration, compatible with the new `SensorPlugin`
API.
2023-04-02 12:36:08 +02:00
Fabio Manganiello fcdda40c4a
Update the `_last_measurement` only if some events were processed from the new data. 2023-04-02 12:09:45 +02:00
Fabio Manganiello 88784985e1
Should be `abs(old_data - new_data) >= tolerance`.
Not `abs(old_data - new_data) > tolerance`.
2023-04-02 12:08:40 +02:00
Fabio Manganiello a3f4b21478
Updated dist files 2023-04-02 03:24:11 +02:00
Fabio Manganiello e6e5dec088
Updated dist files 2023-04-02 02:56:09 +02:00
Fabio Manganiello 7697c1c6ad
Migrated `envirophat` to the new `SensorPlugin` API.
Removed `backend.sensor.envirophat` and `gpio.sensor.envirophat` plugin.
They have now been merged into the new `sensor.envirophat` plugin.
2023-04-02 02:49:08 +02:00
Fabio Manganiello 3cd42c9e45
`Entity` should use `Message.Encoder` as a JSON serializer. 2023-04-02 02:44:19 +02:00
Fabio Manganiello 31f411868c
`Message.Encoder` should serialize binary data to `0x`-led hex strings. 2023-04-02 02:43:06 +02:00
Fabio Manganiello 9e5ad0e0b1
`Entity.to_dict` now takes into account columns mapped to properties.
No more `_value` in the JSON output instead of the `value` property.

If a column is marked as private, and there's an associated property
mapped to its public name, then we should use and serialize that value.
2023-04-02 02:22:40 +02:00
Fabio Manganiello 8d4aa310f4
Support for values passed in dict format to `ThreeAxisSensor` 2023-04-02 02:02:08 +02:00
Fabio Manganiello 5a6f4bcf57
Added 3-axis sensor, accelerometer and magnetometer entities 2023-04-02 01:13:22 +02:00
Fabio Manganiello d964167631
`s/TimeDurationSensor/TimeDuration/g` 2023-04-02 00:57:48 +02:00
Fabio Manganiello 839c6108a0
Added `sensor.*` icon classes 2023-04-02 00:40:50 +02:00
Fabio Manganiello 429893ddbf
Updated dist files 2023-04-01 23:58:28 +02:00
Fabio Manganiello f24d0773d1
No need for `sensor.vl53l1x.transform_entities` to call the parent. 2023-04-01 23:54:43 +02:00
Fabio Manganiello 99572f9731
Sanity check to prevent empty objects from being propagated to `sensor.transform_entities` 2023-04-01 23:41:28 +02:00
Fabio Manganiello 3f3726c50a
Fixed another occurrence of "Subscripted generics cannot be used" etc. error 2023-04-01 23:34:22 +02:00
Fabio Manganiello e2e73d0fdb
Fix another Python < 3.10 subscripted generic issue. 2023-04-01 23:23:51 +02:00
Fabio Manganiello c1d0f21ead
Migrated `ltr559` integration to the new API.
Merged `backend.sensor.ltr559` and `gpio.sensor.ltr559` into the new
`sensor.ltr559` plugin, which extends the new `SensorPlugin` API.
2023-04-01 23:16:03 +02:00
Fabio Manganiello 8e0f88ea16
Don't swap the argument of `SensorPlugin.publish_entities` with a list if not required 2023-04-01 23:06:37 +02:00
Fabio Manganiello 0047d85b9d
Dirty fix for "Subscripted generics cannot be used with class and instance checks" on Python < 3.10 2023-04-01 22:52:24 +02:00
Fabio Manganiello 98ec018292
Replaced `NoneType` reference.
`types.NoneType` is not always available on all Python versions, so we
have to make our own type for it.
2023-04-01 22:42:13 +02:00
Fabio Manganiello 5dabfed365
Migrated `sensor.bme280` to the new `SensorPlugin` interface.
Removed the old `backend.sensor.bme280` and the old `gpio.sensor.bme280`
plugin. They have now been merged into the new `sensor.bme280` runnable
plugin, which extends the `SensorPlugin` API and supports entities.
2023-04-01 22:31:24 +02:00
Fabio Manganiello 6f237a1500
Support the deprecated `poll_seconds` option on `RunnablePlugin` 2023-04-01 22:02:59 +02:00
Fabio Manganiello c23e8867e2
Added `enabled_sensors` to the `sensor` plugin 2023-04-01 21:56:56 +02:00
Fabio Manganiello 7912a59ff8
`vl53l1x` plugin migrated to the new `SensorPlugin` interface. 2023-04-01 19:31:13 +02:00
Fabio Manganiello 6a5a5de03e
`serial` plugin migrated to the new `SensorPlugin` interface. 2023-04-01 19:29:56 +02:00
Fabio Manganiello bf4db76830
Legacy `sensor` backend replaced by an extended `sensor` runnable plugin. 2023-04-01 19:24:35 +02:00
Fabio Manganiello bf75eb73ac
Added an abstract base `SensorDataEvent` for sensor events. 2023-03-31 22:51:35 +02:00
Fabio Manganiello 6a3ade3304
Added `common.sensors` package.
The package contains the base types and constants shared across
sensor-based integrations.
2023-03-31 22:50:47 +02:00
Fabio Manganiello 42d468c895
`get_lock` should raise a TimeoutError if `lock.acquire` is False 2023-03-31 22:31:32 +02:00
Fabio Manganiello 9693becb9e
Removed LGTM badges from the README.
LGTM is now merged into Github and the badges are no longer available.
2023-03-31 14:31:45 +02:00
Fabio Manganiello 7bdd877e49
Support the `binary` flag both on `serial.read` and `serial.write`. 2023-03-31 14:31:45 +02:00
Fabio Manganiello 1efaff878e
Rewritten `serial` plugin.
`backend.serial` has been removed and the polling logic merged into the
`serial` plugin.

The `serial` plugin now supports the new entity engine as well.
2023-03-31 14:31:45 +02:00
Fabio Manganiello 4f15758de9
black fixes 2023-03-31 14:31:38 +02:00
Fabio Manganiello 2a8a3f4394 Removed legacy sensor.distance.vl53l1x backend 2023-03-31 14:26:14 +02:00
Fabio Manganiello a3e8c7c155 Rewritten vl53l1x integration as a runnable plugin with entity support 2023-03-31 14:25:05 +02:00
Fabio Manganiello 226034946f Added `distance_sensor` entity 2023-03-31 14:22:28 +02:00
Fabio Manganiello 6fb362a6fb gpio.sensor.distance.vl53l1x -> sensor.distance.vl53l1x 2023-03-31 14:21:48 +02:00
Fabio Manganiello e198f2a175 Replaced `.title` in `get_plugin` with `.upper` on the first character.
`str.title` capitalizes any alphabetic letter after any non-alphabetic
letter. That's a problem for Platypush plugins' naming convention,
because plugins like `sensor.distance.vl53l1x` may be broken into
`sensor.distance.vl53.l1.x`.
2023-03-31 14:09:43 +02:00
Fabio Manganiello c2f9ebf4ed
Updated dist files 2023-03-27 01:47:29 +02:00
Fabio Manganiello 2781eb1fb1
Merge branch 'master' into 29-generic-entities-support 2023-03-27 00:36:50 +02:00
Fabio Manganiello 792a65df8b Merge pull request '[#240] Migrated `clipboard` plugin from `pyperclip` to `pyclip`' (#241) from 240-migrate-clipboard-integration-to-pyclip into master
Reviewed-on: platypush/platypush#241
2023-03-26 23:56:51 +02:00
Fabio Manganiello 7a368ebbb8
[#240] Migrated `clipboard` plugin from `pyperclip` to `pyclip`.
Closes: #240
2023-03-26 23:52:15 +02:00
Fabio Manganiello bce2fdee25
Replaced deprecated `asyncio.wait([])` with `asyncio.gather(*[])`. 2023-03-26 23:15:53 +02:00
Fabio Manganiello cf91ab90df
Increased default width of `nav` on desktop+ 2023-03-26 23:10:46 +02:00
Fabio Manganiello c0251ef2f7
`s/instance/instance_name/g` in `LinodeInstanceStatusChanged`.
For sake of consistency - we also have `instance_id` and having the
instance name assigned to the `instance` attribute is quite ambiguous.
2023-03-26 22:58:20 +02:00
Fabio Manganiello efe400f921
Fixed `maxdepth` attribute in generate docs. 2023-03-26 22:55:22 +02:00
Fabio Manganiello 6d674fef21
Fixed small JSON syntax error in the docstring of `ntfy.send_message`. 2023-03-26 22:53:42 +02:00
Fabio Manganiello 30124e7cef
Fixed docstring of `Event.__init__`. 2023-03-26 22:53:11 +02:00
Fabio Manganiello 276aff757b
Removed circular dependency.
Workaround for the circular dependency between
`platypush.entities.bluetooth` and `platypush.plugins.bluetooth.model`.

Unentangling the circular dependency would require way too much work,
since the entity model provides several helpers and properties that
depend on the plugin's model.

The workaround in this commit is to simply push those imports down in
the methods that use them, so they won't be imported until those methods
are called, as well as removing some type annotations that depended on
those objects.
2023-03-26 15:30:57 +02:00
Fabio Manganiello 3bb2336b3a
Updated docs 2023-03-26 15:13:48 +02:00
Fabio Manganiello 89bc54da22
Updated dist files 2023-03-26 12:30:46 +02:00
Fabio Manganiello 295758bb20
Added frontend components for cloud instances. 2023-03-26 12:27:17 +02:00
Fabio Manganiello bc2730c841
Rewritten `linode` integration.
- Support for cloud instances as native entities.
- Using Marshmallow dataclasses+schemas instead of custom `Response`
  objects.
- Merge `linode` backend into `linode` plugin.
2023-03-26 11:23:33 +02:00
Fabio Manganiello 4b9c5a0203
Support for schema `EnumField`. 2023-03-26 03:48:32 +02:00
Fabio Manganiello 026662f6b6
Added base schema for Marshmallow dataclasses. 2023-03-26 03:47:44 +02:00
Fabio Manganiello 7bbae55e44
`platypush.entities._managers` -> `platypush.entities.managers`.
It's better for entity managers to be stored in their own public
package, instead of cluttering too much the namespace of their parent
package.
2023-03-26 03:46:06 +02:00
Fabio Manganiello f5d9895521
Added `marshmallow_dataclass` to the requirements. 2023-03-26 03:44:57 +02:00
Fabio Manganiello 89d85baa6d
Support for implicit serialization of Enum values in JSONAble. 2023-03-26 03:43:04 +02:00
Fabio Manganiello a71017df33
Updated web app files 2023-03-24 16:45:55 +01:00
Fabio Manganiello 567e9d4e21
Removed legacy `bluetooth` backends.
No replacements have been made for the OBEX backends (push and file
services). PyOBEX is too broken and unmaintained, and there are too many
poorly documented steps required to get an unprivileged user to run an
SDP service.
2023-03-24 16:41:30 +01:00
Fabio Manganiello 3c355352c5
Using the new `StoppableThread` API. 2023-03-24 16:39:30 +01:00
Fabio Manganiello 5ebf4e912e
Added `wait_stop` and `shoud_stop` methods to `StoppableThread`. 2023-03-24 16:05:18 +01:00
Fabio Manganiello 998793e94f
Added `OBEX_FILE_TRANSFER` constant to `directory` stub. 2023-03-24 15:41:20 +01:00
Fabio Manganiello 4b4db5b3c7
Added `StoppableThread` common interface. 2023-03-24 15:40:16 +01:00
Fabio Manganiello 2f49ddf33a
Fallback logic that uses DBus to disconnect from a BT device.
This logic will be used if the connection wasn't opened by the current
process and therefore a call to DBus is required to terminate it.
2023-03-24 01:57:05 +01:00
Fabio Manganiello 913ef6f8cd
Refresh `BluetoothDevice.reachable` when a device is found/lost. 2023-03-24 01:56:19 +01:00
Fabio Manganiello d46d4e2300
Added support for Bluetooth devices blacklist.
Based on device address, name or manufacturer.
2023-03-24 01:52:39 +01:00
Fabio Manganiello 0cebcf4f9b
`switchbot.bluetooth` integration migrated to a `bluetooth` plugin. 2023-03-23 17:46:54 +01:00
Fabio Manganiello 4fac110bb8
Added `bluetooth.set` method, whose execution is delegated to the plugins. 2023-03-23 17:45:02 +01:00
Fabio Manganiello cd219f44c4
Pass the list of plugins when creating Bluetooth managers. 2023-03-23 17:42:16 +01:00
Fabio Manganiello 43289a3b55
Scan always at least for 10 seconds before failing on `get_device`. 2023-03-23 17:41:37 +01:00
Fabio Manganiello 6267943786
Wrap `BleakError` exceptions into `AssertionError`. 2023-03-23 17:40:30 +01:00
Fabio Manganiello d6805a8b18
Added support for custom Bluetooth device plugins. 2023-03-23 17:10:37 +01:00
Fabio Manganiello af125347d6
If no matching services are found when connecting to a device, default to BLEManager.
GATT characteristics are not necessarily exposed as services.
2023-03-23 13:00:26 +01:00
Fabio Manganiello d1cd6dd2af
get_plugin with reload=True should stop the existing plugin if it's running 2023-03-23 01:11:54 +01:00
Fabio Manganiello a2a5fce6cb
Added `Apple Continuity` to the list of blacklisted manufacturers/models 2023-03-22 22:55:19 +01:00
Fabio Manganiello e71c312133
Always read an entity's parent through get_parent when climbing up.
This should avoid the risk of `DetachedInstanceError` by retrieving the
object into the session if it's not available.
2023-03-22 22:41:09 +01:00
Fabio Manganiello 5c23d3aa87
metadata and rssi fields on BLEDevice have been deprecated.
Changed the BLE beacon parsing logic to read those fields from
`AdvertisementData` instead of `BLEDevice`.
2023-03-22 22:39:01 +01:00
Fabio Manganiello 65bc3ae06d
Noisy beacons device configuration should look both at manufacturer and model. 2023-03-22 22:37:46 +01:00
Fabio Manganiello f49b866a51
Focus the <input> element when a <NameEditor> element is created. 2023-03-22 21:28:21 +01:00
Fabio Manganiello dd80dc998c
Show entity icon and type in the list of children entities on EntityModal. 2023-03-22 21:26:59 +01:00
Fabio Manganiello 239dd17f23
Exclude from the list of display children on EntityModal those with no name or that are configuration values. 2023-03-22 16:38:38 +01:00
Fabio Manganiello e10bec88c0
Noisy beacons logging trace moved from info to debug. 2023-03-22 16:31:57 +01:00
Fabio Manganiello 5dd95362a1
Include links both to the parent and children entities in EntityModal. 2023-03-22 16:20:29 +01:00
Fabio Manganiello 99cfd247a5
A more effective logic to exclude noisy BLE beacons.
This includes BLE beacons sent from all Google/Apple/Microsoft/Samsung
beacon networks in all of their variants.
2023-03-22 15:35:02 +01:00
Fabio Manganiello 01d323fad0
Passing a boolean `exclude_known_noisy_beacons` to `bluetooth` plugin.
The logic to exclude BLE beacons from randomized devices needs to be a
bit more granular and not limited only to the reported device
manufacturer.
2023-03-22 15:29:19 +01:00
Fabio Manganiello f6e09d34e4
A more clever logic of parsing the manufacturer for BLE devices.
1. Check the manufacturer parsed via Bleak/Theengs
2. Check the MAC address prefix in the oui numbers table
3. Check from the reported `manufacturer_data`
2023-03-22 14:16:00 +01:00
Fabio Manganiello f7e8cfe5a7
Don't include `unit` in BLE sensors when they are matched against the native type.
It's likely to just include the native type name anyway.
2023-03-22 14:14:59 +01:00
Fabio Manganiello 486f37a45e
Support sensor value reported both on `value` as well as `_value` fields. 2023-03-22 14:11:13 +01:00
Fabio Manganiello bfc87e0f7b
Display arrays and objects in the entity modal as prettified JSON. 2023-03-22 13:50:35 +01:00
Fabio Manganiello c750d83188
Prevent name collisions on `bluetooth.ServiceClass`. 2023-03-22 03:27:25 +01:00
Fabio Manganiello 174b1ee6a9
Use a default list of excluded Bluetooth manufacturers. 2023-03-21 16:03:01 +01:00
Fabio Manganiello e9abb5cb9a
Implemented support for child entities in entity modals. 2023-03-21 16:02:02 +01:00
Fabio Manganiello b1cb7ef847
Added a list of `excluded_manufacturers` to `BluetoothPlugin`. 2023-03-21 14:32:45 +01:00
Fabio Manganiello 718e0434ba
Display all available entity attributes on EntityModal. 2023-03-20 14:32:03 +01:00
Fabio Manganiello 78bbe71be1
Another .pull-right fix. 2023-03-20 02:04:32 +01:00
Fabio Manganiello 3743ee4f00
s/TheengsGateway/TheengsDecoder/g now that the pip package has been published. 2023-03-20 01:41:21 +01:00
Fabio Manganiello 431dedf3eb
BluetoothDevice moved to its own component, with device connect support. 2023-03-20 01:28:12 +01:00
Fabio Manganiello 0a4b22c12e
Implemented connect/disconnect call on BluetoothService component. 2023-03-20 01:27:47 +01:00
Fabio Manganiello 714f853751
Pass the list of children to the entity component. 2023-03-20 01:27:21 +01:00
Fabio Manganiello a011de890b
Better .pull-right class implementation. 2023-03-20 01:26:48 +01:00
Fabio Manganiello 2b5596820b
Made Types.objectsEqual method more robust against null input 2023-03-19 22:50:23 +01:00
Fabio Manganiello 71a3481560
Better style for the sidebar/nav 2023-03-19 22:23:37 +01:00
Fabio Manganiello 12096f2dbe
Don't fail hard when device disconnection fails. 2023-03-19 12:56:53 +01:00
Fabio Manganiello 40f81b105f
Set the connected flag when connecting/disconnecting from a service. 2023-03-19 12:56:31 +01:00
Fabio Manganiello 9d66b63266
BluetoothService attributes fixes.
BluetoothService IDs should always be in the format `address::uuid` and
the name should always be in title format.
2023-03-19 12:55:14 +01:00
Fabio Manganiello 6e9263c4e4
A more elegant logic to infer the manufacturer name. 2023-03-19 12:54:52 +01:00
Fabio Manganiello b568876474
Use a service's UUID as a name instead of Unknown if the service is unknown. 2023-03-19 12:54:09 +01:00
Fabio Manganiello aa04741daa
Added BluetoothService UI component 2023-03-19 12:53:23 +01:00
Fabio Manganiello f74fab795d
Added `parent` component value to `Entity`. 2023-03-19 12:50:45 +01:00
Fabio Manganiello 243de15813
Added `connected` flag to `BluetoothService`. 2023-03-19 12:49:38 +01:00
Fabio Manganiello 256d9adbf2
Removed `children` from `BluetoothDevice.to_json` - it makes events too verbose 2023-03-19 12:48:11 +01:00
Fabio Manganiello 4144e4f842
Fixed self._ip_to_dev expansion 2023-03-19 12:47:07 +01:00
Fabio Manganiello 878fe91155
Big rewrite/refactor of the entities merger 2023-03-19 12:40:48 +01:00
Fabio Manganiello 2411b961e8
[WIP] Big, big refactor of the Bluetooth integration.
- Merged together Bluetooth legacy and BLE plugins and scanners.
- Introduced Theengs as a dependency to infer BLE device types and
  create sub-entities appropriately.
- Using `BluetoothDevice` and `BluetoothService` entities as the bread
  and butter for all the Bluetooth plugin's components.
- Using a shared cache of devices and services between the legacy and
  BLE integrations, with merging/coalescing logic included.
- Extended list of discoverable services to include all those officially
  supported by the Bluetooth specs.
- Instantiate a separate pool of workers to discover services.
- Refactor of the Bluetooth events - all of them are now instantiated
  from a single `BluetoothDevice` object.
2023-03-13 02:31:21 +01:00
Fabio Manganiello 4bc61133c5
The Entity object should also have a `to_json` method. 2023-03-12 23:01:51 +01:00
Fabio Manganiello 4a8da80c7c
Don't join self._thread on stop in RunnablePlugin if self._thread = current_thread 2023-03-11 23:45:46 +01:00
Fabio Manganiello 31552963c4
`EntitiesDb.upsert` should return a deep copy of the upserted entities.
Not the upserted entities themselves, no matter if expunged or made transient.

Reminder to my future self: returning the flushed entities and then using them
outside of the session or in another thread opens a big can of worms when using
SQLAlchemy.
2023-03-10 12:06:36 +01:00
Fabio Manganiello f45e47363d
Use lazy='joined' instead of lazy='selectin' on Entity.parent.
That's the best way to ensure that all the columns are fetched eagerly and
prevent errors later when trying to access lazily loaded attributes outside
of the session/thread.
2023-03-10 12:01:23 +01:00
Fabio Manganiello 8ccf3e804d
Added utility get_entities_engine() function. 2023-03-10 11:49:23 +01:00
Fabio Manganiello 60da930e4b
Added support for get_plugin(MyPlugin) besides get_plugin('my'). 2023-03-10 11:47:39 +01:00
Fabio Manganiello 3fcc9957d1
A dirty fix to prevent DetachedInstanceError when accessing the parent entity. 2023-03-10 11:45:44 +01:00
Fabio Manganiello ceb7a2f098
EntitiesEngine synchronization improvements.
- Added `wait_start()` method that other threads can use to synchronize
  with the engine and wait before performing db operations.

- Callback logic wrapped in a try/except block to prevent custom
  integrations with buggy callbacks from crashing the engine.
2023-03-10 00:57:24 +01:00
Fabio Manganiello 73dc2463f1
Added auto_commit=False to entities.save() 2023-03-10 00:40:51 +01:00
Fabio Manganiello 7e92d5f244
Added recursive `.copy()` method to `Entity`. 2023-03-09 22:35:31 +01:00
Fabio Manganiello 9f9ee575f1
Added _import_error_ignored_modules.
ImportErrors on these entity modules will be ignored when dynamically
loading them, since they have optional external dependencies and we
shouldn't throw an error if we can't import them.
2023-03-09 01:40:35 +01:00
Fabio Manganiello c4efec6832
Several fixes and improvements on the entities engine.
- Support for an optional callback on `publish_entities` to get notified
  when the published object are flushed to the db.

- Use `lazy='selectin'` for the entity parent -> children relationship -
  it is more efficient and it ensures that all the data the application
  needs is loaded upfront.

- `Entity.entity_key` rolled back to `<external_id, plugin>`. The
  fallback logic on `<id, plugin>` created more problems than those it
  as supposed to solve.

- Added `expire_on_commit=False` to the entities engine session to make
  sure that we don't get errors on detached/expired instances.
2023-03-09 01:16:04 +01:00
Fabio Manganiello 1781a19a79
s/Entity.to_json/Entity.to_dict/g
stuff
2023-03-06 23:46:33 +01:00
Fabio Manganiello b9efa9fa30
entity_key should coalesce (entity.external_id or entity.id) 2023-03-06 16:54:02 +01:00
Fabio Manganiello 72c55c03f2
[WIP] Refactoring/extending models and parsers for Bluetooth entities. 2023-03-03 02:10:11 +01:00
Fabio Manganiello a688e7102e
Changed default `poll_interval` for `RunnablePlugin`.
30 -> 15 seconds.
2023-03-03 02:00:48 +01:00
Fabio Manganiello ead4513915
Added optional `unit` column to `RawSensor` entity. 2023-03-03 01:59:27 +01:00
Fabio Manganiello 94c4e52154
Mock PyOBEX.client in readthedocs conf.py 2023-03-03 01:58:32 +01:00
Fabio Manganiello 7be55e446f
Convert UUID objects to strings when serializing to JSON. 2023-03-02 21:58:26 +01:00
Fabio Manganiello 15fadb93bb
Added stand-alone `connect` and `disconnect` actions to `bluetooth`. 2023-02-25 01:59:35 +01:00
Fabio Manganiello 70d1bb893c
A cleaner way of calculating the `success` response attribute. 2023-02-25 01:58:09 +01:00
Fabio Manganiello 2dfb389630
Added remaining `bluetooth` entity types in `_mappers.py`. 2023-02-23 21:20:41 +01:00
Fabio Manganiello a0556d3a42
Added `PresenceSensor` entities. 2023-02-23 01:42:26 +01:00
Fabio Manganiello 886b930e2f
Support for `bluetooth` devices with multiple temperature sensors. 2023-02-23 01:27:31 +01:00
Fabio Manganiello 56d693032a
Added `DewPointSensor` entities. 2023-02-23 01:23:04 +01:00
Fabio Manganiello d212276247
Added `PressureSensor` entities. 2023-02-23 01:12:27 +01:00
Fabio Manganiello dd3f683006
Added `unit` to `bluetooth` mappers whenever available. 2023-02-23 01:04:33 +01:00
Fabio Manganiello d961e2a997
Added `TimeDurationSensor` entity. 2023-02-23 01:02:13 +01:00
Fabio Manganiello c3e16f9f9d
Added support for heart rate sensor entities. 2023-02-23 00:55:55 +01:00
Fabio Manganiello 3dab94c346
Added `StepsSensor` detection to `bluetooth`. 2023-02-23 00:50:06 +01:00
Fabio Manganiello e1b3d52706
Added `StepsSensor` entity. 2023-02-23 00:45:58 +01:00
Fabio Manganiello dcab766cef
Only scan for the configured Bluetooth service UUIDs. 2023-02-22 03:36:16 +01:00
Fabio Manganiello d8c429f4a8
Major improvements on the entities engine.
- Better logic to recursively link parent/children entities, so partial
  updates won't get lost.

- Removed `EntitiesCache` - it was too much to maintain while keeping
  consistent with the ORM, and it was a perennial fight against
  SQLAlchemy's own cache.

- Removed `EntityNotifier` - with no need to merge cached entities, the
  `notify` method has become much simpler and it's simply been merged
  in the `EntitiesRepository`.
2023-02-22 02:53:45 +01:00
Fabio Manganiello 9776921836
Better way of handling with `RawSensor` in `bluetooth` integration. 2023-02-22 02:26:51 +01:00
Fabio Manganiello a5a923a752
Added `BluetoothDeviceNewDataEvent`.
These events handle the case where a Bluetooth device only publishes new
service data without advertising any additional updated properties.
2023-02-22 02:23:11 +01:00
Fabio Manganiello dc7cbe743d
Refactored/improved `RawSensor` entity.
It will now automatically deal with most of the native types and convert
them to strings on the db.
2023-02-22 02:19:19 +01:00
Fabio Manganiello b2ffc08c89
s/MultiValueSensor/CompositeSensor/g on `smartthings` 2023-02-22 02:18:12 +01:00
Fabio Manganiello 340fd08064
Removed some old `type: ignore` comments. 2023-02-22 01:29:51 +01:00
Fabio Manganiello cf219d5a48
Added some more docstrings to entities. 2023-02-22 01:02:26 +01:00
Fabio Manganiello 7fa545d7f8
Merge branch 'master' into 29-generic-entities-support 2023-02-22 00:46:33 +01:00
Fabio Manganiello c645ce6bb8
Bump version: 0.24.4 → 0.24.5 2023-02-22 00:32:57 +01:00
Fabio Manganiello 2b8a5fee88
Updated CHANGELOG 2023-02-22 00:32:39 +01:00
Fabio Manganiello 26d9aaa5b1
(Temporarily) specify `sqlalchemy<2.0.0`.
SQLAlchemy 2 has introduced several breaking changes that can break
several things in the application - especially where the code uses
`connection.execute()` with raw SQL statements.

We need to temporarily force the installation of versions from the 1.x
branch, while migrating the existing code to the new version.
2023-02-22 00:25:57 +01:00
Fabio Manganiello bbc9647cb0
s/MultiValueSensor/CompositeSensor/g 2023-02-21 23:14:10 +01:00
Fabio Manganiello 2fa45fc5a3
Documentation and LINT fixes for sensor entities. 2023-02-21 23:10:05 +01:00
Fabio Manganiello b4627ecd04 Removed deprecated use_unicode parameter from MPDClient 2023-02-20 20:35:33 +01:00
Fabio Manganiello aa0b909fff
Use the TheengsDecoder to parse Bluetooth packets and map services to native entities. 2023-02-20 20:27:17 +01:00
Fabio Manganiello 73bf2446bd
Wrap `bluetooth.connect` in a per-device locked section. 2023-02-19 23:11:19 +01:00
Fabio Manganiello 9112239ac3
Better exception management in `AsyncRunnablePlugin`.
Exceptions that cause the termination of the plugin's loop should always
be logged as such, unless the plugin is supposed to stop and various
exceptions may occur upon teardown.
2023-02-19 23:03:27 +01:00
Fabio Manganiello a6c36fa1c1
Added brand, model and model_id columns to `BluetoothDevice`. 2023-02-19 23:02:04 +01:00
Fabio Manganiello 68e6b271c1
Updated dist files 2023-02-19 22:58:20 +01:00
Fabio Manganiello cb9b01c89f
Added raw_sensor metadata 2023-02-19 22:57:50 +01:00
Fabio Manganiello 72a9a9dfcf
LINT/type fixes 2023-02-19 22:56:45 +01:00
Fabio Manganiello 8aedc3c233
Recursively normalize child entities in `EntityManager._normalize_entities` 2023-02-18 17:51:57 +01:00
Fabio Manganiello 613e32e7c1
Extended number of supported events and data fields in Bluetooth integration. 2023-02-18 01:15:10 +01:00
Fabio Manganiello 7adae272a4
Merge branch 'master' into 29-generic-entities-support 2023-02-15 22:24:41 +01:00
Fabio Manganiello 08553f84b9
Added `timeout` parameter to `websocket.send`. 2023-02-15 22:23:15 +01:00
Fabio Manganiello 45664be44b
Removed deprecated `backend.bluetooth.scanner`.
Scan capabilities are now implemented on the `bluetooth` plugin itself.
2023-02-13 23:13:51 +01:00
Fabio Manganiello 471bc1fd3d
Updated dist files 2023-02-13 23:13:32 +01:00
Fabio Manganiello a3aa186ddf
- Added support for `scan_pause`/`scan_resume` on `bluetooth` integration.
- Added `BluetoothDevice` as its own entity type.
2023-02-13 23:12:25 +01:00
Fabio Manganiello 1d0be5c929
- Simplified prototype for `EntityManager.set`
- Added small documentation/annotations notes to the `Plugin` module.

- Small LINT fixes
2023-02-11 21:35:00 +01:00
Fabio Manganiello 575635fd6b
Defined `set` as a base method for all plugins that implement writeable entities 2023-02-11 04:04:21 +01:00
Fabio Manganiello 4365352331
[WIP] s/set_value/set/g for entities 2023-02-11 03:57:23 +01:00
Fabio Manganiello b0cc80ceb0
Rewriting `bluetooth.ble` plugin to use `bleak` instead of `gattlib`. 2023-02-10 17:40:20 +01:00
Fabio Manganiello f30e077a5a
Support for custom Bluetooth adapter on `switchbot.bluetooth`. 2023-02-08 23:01:05 +01:00
Fabio Manganiello 8469a1027f
Migrated/refactored `switchbot.bluetooth` integration.
- Out `gattlib` + `pybluez`, in `bleak`. It's not platform-dependent, it doesn't
  require libboost and other heavy build dependencies, and it doesn't require the
  user that runs the service from having special privileges to access raw
  Bluetooth sockets.

- Better integration with Platypush native entities. The devices are now mapped
  to write-only `EnumSwitch` entities, and the status returns the serialized
  representation of those entities instead of the previous intermediate
  representation.
2023-02-08 22:42:00 +01:00
Fabio Manganiello 35719b0da9
Let `publish_entities` return the list of transformed_entities 2023-02-08 02:09:34 +01:00
Fabio Manganiello e04870209e
More LINT fixes 2023-02-08 01:50:54 +01:00
Fabio Manganiello a98a5f0980
typo fix 2023-02-08 01:09:25 +01:00
Fabio Manganiello e49a0aec4d
Various improvements.
- Better synchronization logic on stop for `AsyncRunnablePlugin`.
- Fixed several thread names by dropping `prctl.set_name` in favour of
  specifying the name directly on thread creation.
- Several LINT fixes.
2023-02-08 00:46:50 +01:00
Fabio Manganiello 9d028af524
Removed last reference of `SwitchPlugin` 2023-02-05 23:10:35 +01:00
Fabio Manganiello 419a0cec61
More LINTing
Better prototype for `MultiLevelSwitchEntityManager.set_value`
2023-02-05 23:07:43 +01:00
Fabio Manganiello fde834c1b1
More LINT fixes + refactors 2023-02-05 22:00:50 +01:00
Fabio Manganiello 4849e14414
LINT fixes for the `utils` module + additional documentation 2023-02-05 18:05:41 +01:00
Fabio Manganiello b8fca97891
Default poll_interval for `RunnablePlugin` set to 30 seconds 2023-02-05 17:31:43 +01:00
Fabio Manganiello 06dfd1a152
Added support for more entities in `switchbot` 2023-02-05 15:34:50 +01:00
Fabio Manganiello 64e9bf17cf
Updated dist files 2023-02-05 14:53:36 +01:00
Fabio Manganiello 2047b9b76c
[WIP] Refactoring `switchbot` plugin as a runnable plugin + entity manager 2023-02-04 22:22:51 +01:00
Fabio Manganiello 65827aa0cd
Updated dist files 2023-02-04 17:36:46 +01:00
Fabio Manganiello b96838a856
Major LINT fixes/refactor for the `Config` class 2023-02-04 17:35:48 +01:00
Fabio Manganiello db5846d296
Add the unit to the `Dimmer` display value if it's available 2023-02-04 17:28:54 +01:00
Fabio Manganiello 0311d87bc3
The `switch.wemo` integration now extends `SwitchEntityManager` 2023-02-04 00:58:28 +01:00
Fabio Manganiello de2849546a
LINT fixes 2023-02-04 00:26:48 +01:00
Fabio Manganiello a160d3217e
Removed legacy `get_sensor_plugins` and `get_switch_plugins` actions 2023-02-03 22:54:42 +01:00
Fabio Manganiello a8fcbef1b5
gitignore 2023-02-03 22:49:50 +01:00
Fabio Manganiello b6814b4f16
Removed legacy Switches integration [frontend] 2023-02-03 22:49:09 +01:00
Fabio Manganiello 6ef2feea71
LINT fixes for `utils` plugin 2023-02-03 18:08:19 +01:00
Fabio Manganiello 3db9c58d31
[WIP] Converted `switch.tplink` plugin.
`switch.tplink` converted to a `RunnablePlugin` that implements
`SwitchEntityManager`.
2023-02-03 02:20:20 +01:00
Fabio Manganiello be3b99326f
[WIP] Refactoring `@manages` annotation into a proper `EntityManager` hierarchy 2023-02-02 23:21:12 +01:00
Fabio Manganiello 63d6920716
Updated dist files 2023-02-02 18:07:44 +01:00
Fabio Manganiello 59eb0742a1
s/warnings/logger.debug/ if publish_entities is called with no engine registered 2023-01-29 21:52:12 +01:00
Fabio Manganiello 8aff181956
Merged `zwave.mqtt` backend into the `zwave.mqtt` plugin 2023-01-29 02:34:48 +01:00
Fabio Manganiello 0e56d0fff6
Double-check if self._thread != None on stop on the ntfy thread
Race conditions may occur here
2023-01-27 22:12:34 +01:00
Fabio Manganiello 341e749d23
Merged the `zigbee.mqtt` backend into the plugin.
- Deprecated the old `zigbee.mqtt` backend
- Black style for the `mqtt` backend
2023-01-27 01:59:57 +01:00
Fabio Manganiello afdeb91f66
Implemented remaining supported entities for the `smartthings` integration 2023-01-26 22:10:02 +01:00
Fabio Manganiello 334ccc35a2
Don't serialize I/O wrappers
This removes warnings on `config.get`, where the `logging` configuration
key may also contain the current logging stream and we end up with a
JSONDecodeError when trying to serialize it.
2023-01-25 00:52:37 +01:00
Fabio Manganiello ba31dff06a
Major refactor + fixes for `smartthings` 2023-01-24 23:56:28 +01:00
Fabio Manganiello 147f36c86c
All `Sensor` instances should have `is_read_only=True` by default 2023-01-22 21:05:14 +01:00
Fabio Manganiello fd76642082
Added `Volume` and `Muted` entities 2023-01-22 21:04:46 +01:00
Fabio Manganiello bb637a1411
Defined a unique `stop_timeout` (default=5) for RunnablePlugin 2023-01-22 14:28:16 +01:00
Fabio Manganiello 6d4cf64253
More work on `smartthings`.
- Added support for `Battery` entities
- Fixed saturation range for `Light` entities
- Parsing `min`/`max`/`unit` from the status attributes, if available
2023-01-22 01:01:47 +01:00
Fabio Manganiello ddd516a677
Added polling/RunnablePlugin logic to `smartthings` 2023-01-22 00:09:10 +01:00
Fabio Manganiello dabbe031ab
Don't show the entity modal unless the user clicks on the name or icon 2023-01-21 23:46:38 +01:00
Fabio Manganiello 32e4e60579
A more robust handling of events in the `zwave.mqtt` backend 2023-01-21 23:44:51 +01:00
Fabio Manganiello 3940288396
Use the new bus notification helpers 2023-01-21 16:59:18 +01:00
Fabio Manganiello 241670c9d0
Handle parent/child update events through broadcast bus events 2023-01-21 16:58:28 +01:00
Fabio Manganiello 3923a09831
- Expose methods on the bus module to publish/subscribe to notifications and entity updates
- Removed some redundant `pass` statements in Z-Wave derived event classes
2023-01-21 16:56:27 +01:00
Fabio Manganiello fb562bb415
Propagate the @update event to the parent entities 2023-01-21 14:55:06 +01:00
Fabio Manganiello 4d762b81dc
EntityUpdateEvent traces can now be logged on INFO level
The EntityUpdateEvents generated by light.hue are now less noisy.
2023-01-21 14:50:05 +01:00
Fabio Manganiello 247912799f
Refactored light.hue integration so EntityUpdateEvents won't be triggered on every call to _get_lights 2023-01-21 14:48:33 +01:00
Fabio Manganiello dfb13127ee
Added MotionSensor entities 2023-01-21 14:47:18 +01:00
Fabio Manganiello a892bad34c
Refactoring smartthings plugin to support more entity types 2023-01-21 14:09:26 +01:00
Fabio Manganiello 22b8b03cb2
Refactored EntityIcon component 2023-01-15 20:02:50 +01:00
Fabio Manganiello 9a5e2899e8
Support for external_url and image_url on entities 2023-01-15 20:01:47 +01:00
Fabio Manganiello 2cc5e3f726
UI tweaks 2023-01-15 15:46:25 +01:00
Fabio Manganiello 9e4fbc6a21
Defined the collapsed data property on EntityMixin level 2023-01-15 15:29:26 +01:00
Fabio Manganiello 78e250186b
Deallocate the color converter when the light component is unmounted 2023-01-15 15:25:04 +01:00
Fabio Manganiello e9371ac5d0
Improved entity collapse logic
- Toggle collapsed state also if clicked on the gap between the entity
  name and the right edge, instead of opening the entity modal. The
  entity configuration modal should open only when clicking on the
  entity name or icon (and these should be highlighted on hover as links
  as well).

- The collapsed state update should be propagated to the wrapped
  component as well, if applicable.
2023-01-15 15:03:53 +01:00
Fabio Manganiello dbf5ed3b85
s/expanded/collapsed/g (for naming consistency) 2023-01-15 14:26:44 +01:00
Fabio Manganiello bb483fd1b1
Using a nice gradient for hover-bg 2023-01-15 12:34:18 +01:00
Fabio Manganiello cda03887d4
Updated dist files 2023-01-15 12:34:02 +01:00
Fabio Manganiello 9df4d5b5b8
Zigbee entities should be marked as unreachable also if they are currently being interviewed 2023-01-14 22:35:17 +01:00
Fabio Manganiello afd9a1d6bf
Don't load entities that only have non-queriable children 2023-01-14 22:33:53 +01:00
Fabio Manganiello 2778357a9e
Wrapped dynamic Vue components in shallowRef.
The performance of the page is heavily degraded by components loaded
dynamically via defineAsyncComponent that recursively carry behind the
whole Vue machinery.

By wrapping defineAsyncComponent calls in shallowRef we make sure that
we only wire the root level of the newly created dynamic component.
2023-01-14 22:31:48 +01:00
Fabio Manganiello fd2d83c80b
Renamed Notification mixin's warn and error methods.
Renamed to `notifyWarning` and `notifyError` respectively.

Those names can often clash with other properties defined on components
that extend the mixin (like entities).
2023-01-14 22:27:43 +01:00
Fabio Manganiello aa22507f50
DropdownItem.className should not be enforced to be a string.
It can also be a class -> boolean object.
2023-01-14 22:11:05 +01:00
Fabio Manganiello a58caa17e2
Decreased time of EventQueue (2 -> 1 second) to make entity events more responsive 2023-01-13 23:28:58 +01:00
Fabio Manganiello 68497e6388
Normalize the light devices' IEEE addresses before retrieving the associated devices 2023-01-13 23:28:12 +01:00
Fabio Manganiello 22a566a88b
More refactors and fixes for `zigbee.mqtt` 2023-01-13 02:58:47 +01:00
Fabio Manganiello 38438230d7
The batch of entities currently being processed should have no duplicate keys 2023-01-11 01:22:56 +01:00
Fabio Manganiello 4a2851231c
Large refactor of `zigbee.mqtt`
- Support for device options as children configuration entities
- Refactored switches management, removed legacy `switches` plugin
  integration, and supporting multiple binary switches for one device
2023-01-09 01:02:49 +01:00
Fabio Manganiello 27b23b7fae
Normalize array/dict options for values on EnumSwitch 2023-01-09 01:01:35 +01:00
Fabio Manganiello e9c84ff5d4
Support units on dimmer entities 2023-01-09 01:01:05 +01:00
Fabio Manganiello 32330ca7a8
Merge branch 'master' into 29-generic-entities-support 2023-01-08 23:26:08 +01:00
Fabio Manganiello a7a107e5fb
Merge pull request #343 from BlackLight/dependabot/npm_and_yarn/platypush/backend/http/webapp/minimatch-3.1.2
Bump minimatch from 3.0.4 to 3.1.2 in /platypush/backend/http/webapp
2023-01-08 23:25:17 +01:00
dependabot[bot] a0e45c38a5
Bump minimatch from 3.0.4 to 3.1.2 in /platypush/backend/http/webapp
Bumps [minimatch](https://github.com/isaacs/minimatch) from 3.0.4 to 3.1.2.
- [Release notes](https://github.com/isaacs/minimatch/releases)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.0.4...v3.1.2)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-08 22:24:41 +00:00
Fabio Manganiello 1ae92dca92
Merge pull request #357 from BlackLight/dependabot/npm_and_yarn/platypush/backend/http/webapp/json5-1.0.2
Bump json5 from 1.0.1 to 1.0.2 in /platypush/backend/http/webapp
2023-01-08 23:23:16 +01:00
dependabot[bot] 379c822588
Bump json5 from 1.0.1 to 1.0.2 in /platypush/backend/http/webapp
Bumps [json5](https://github.com/json5/json5) from 1.0.1 to 1.0.2.
- [Release notes](https://github.com/json5/json5/releases)
- [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md)
- [Commits](https://github.com/json5/json5/compare/v1.0.1...v1.0.2)

---
updated-dependencies:
- dependency-name: json5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-08 22:22:52 +00:00
Fabio Manganiello 309643dcc6
Merge pull request #345 from BlackLight/dependabot/npm_and_yarn/platypush/backend/http/webapp/loader-utils-1.4.2
Bump loader-utils from 1.4.0 to 1.4.2 in /platypush/backend/http/webapp
2023-01-08 23:21:54 +01:00
dependabot[bot] 47c3a24def
Bump loader-utils from 1.4.0 to 1.4.2 in /platypush/backend/http/webapp
Bumps [loader-utils](https://github.com/webpack/loader-utils) from 1.4.0 to 1.4.2.
- [Release notes](https://github.com/webpack/loader-utils/releases)
- [Changelog](https://github.com/webpack/loader-utils/blob/v1.4.2/CHANGELOG.md)
- [Commits](https://github.com/webpack/loader-utils/compare/v1.4.0...v1.4.2)

---
updated-dependencies:
- dependency-name: loader-utils
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-08 15:53:12 +00:00
Fabio Manganiello 66000a0774
Merge branch 'master' into 29-generic-entities-support 2023-01-08 00:23:57 +01:00
Fabio Manganiello 2893cb1cc4
Replaced deprecated `missing` marshmallow parameter with `load_default` 2023-01-08 00:15:24 +01:00
Fabio Manganiello 7d90b274ae
Make sure that any existing device monitor is terminated upon disconnection 2023-01-07 23:48:02 +01:00
Fabio Manganiello 72454a6583
Merge branch 'master' into 29-generic-entities-support 2023-01-07 23:31:31 +01:00
Fabio Manganiello 40bdc3b7f3
Always wait 5 seconds (regardless of the poll interval) in case of errors.
Also, print the error only on the first occurrence, to prevent log
spamming.
2023-01-07 23:21:59 +01:00
Fabio Manganiello e8f767d819
Take into account the notify_only_if_changed parameter 2023-01-07 23:11:34 +01:00
Fabio Manganiello c5cf9803ff
Take into account the notify_only_if_changed parameter 2023-01-07 23:09:42 +01:00
Fabio Manganiello 6630873e2c
Merge branch 'master' into 29-generic-entities-support 2023-01-07 22:39:46 +01:00
Fabio Manganiello 2ee810bdc4
Added missing event to documentation 2023-01-07 22:39:23 +01:00
Fabio Manganiello b7f266cd92
Merge branch 'master' into 29-generic-entities-support 2023-01-07 22:31:36 +01:00
Fabio Manganiello a77206800d
Added HID plugin to support interaction with generic HID devices 2023-01-07 22:30:32 +01:00
Fabio Manganiello c215c693f5
Only pass children that aren't configuration values to the entities 2023-01-03 23:25:43 +01:00
Fabio Manganiello 7868d6fe37
Support for nested configuration objects on entity modals 2023-01-03 23:16:14 +01:00
Fabio Manganiello 13eb515f87
Select current display value by default on EnumSwitch 2023-01-03 23:14:57 +01:00
Fabio Manganiello 01727f53bc
Support for is_configuration flag on `zwave` entities 2023-01-03 23:13:34 +01:00
Fabio Manganiello c32aecece3
Added is_configuration flag to entities 2023-01-03 23:12:27 +01:00
Fabio Manganiello 81fee3ea2a
Style tweaks 2023-01-03 23:11:39 +01:00
Fabio Manganiello 71ed545cc7
Fancier gradient background for the navigator header 2023-01-02 23:28:53 +01:00
Fabio Manganiello 0f60bc2131
Don't delete external_id and data attributes unless they are set 2023-01-02 21:01:46 +01:00
Fabio Manganiello b0671354ea
(Tentative) support for zwave light colors 2023-01-02 12:50:01 +01:00
Fabio Manganiello 4f75cbc8b4
Updated frontend dist files 2023-01-01 23:23:18 +01:00
Fabio Manganiello 80c2c77272
Support for entities with children on the frontend 2023-01-01 23:06:40 +01:00
Fabio Manganiello 772ba6adb0
Merge branch 'master' into 29-generic-entities-support 2023-01-01 13:19:31 +01:00
Fabio Manganiello dd3c4b10c7
Run register_service in a separate thread in `backend.http`.
The Zeroconf registration part may randomly get stuck, resulting in the
web server not being properly started.

It's therefore better to run the Zeroconf registration process
asynchronously, for it's not strictly required for the web server to
execute.
2023-01-01 13:19:11 +01:00
Fabio Manganiello edddc90d73
Run register_service in a separate thread in `backend.http`.
The Zeroconf registration part may randomly get stuck, resulting in the
web server not being properly started.

It's therefore better to run the Zeroconf registration process
asynchronously, for it's not strictly required for the web server to
execute.
2023-01-01 13:16:46 +01:00
Fabio Manganiello 1c811a490f
Don't serialize children_ids in Entity.to_json()
Serializing both children_ids and parent_id can result in nasty
SQLAlchemy bugs, especially when handling objects that haven't been
flushed yet.
2023-01-01 12:47:15 +01:00
Fabio Manganiello f9b6799a18
A more robust and scalable way of merging/handling the currentValue/targetValue duality in zwave.mqtt 2023-01-01 12:45:41 +01:00
Fabio Manganiello 0513339be7
Merge branch 'master' into 29-generic-entities-support 2022-12-20 23:06:19 +01:00
Fabio Manganiello 84ce31cab0
Bump version: 0.24.3 → 0.24.4 2022-12-20 23:05:42 +01:00
Fabio Manganiello d0d333e8f4
FIX: Clear the cronjob event after receiving a TIME_SYNC.
When a cronjob receives a TIME_SYNC event (because the system clock has
changed/drifted and the cronjobs are expected to recalculate their next
run slot) it should also clear the event.

Otherwise, the next `wait` will be skipped and the cronjob will be
executed even if it wasn't scheduled.
2022-12-20 23:01:03 +01:00
Fabio Manganiello 2feaba7bf4
Add children_ids attribute to entities when converted to JSON 2022-12-18 21:03:12 +01:00
Fabio Manganiello 3615a269fe
Use Float instead of Numeric on NumericSensor table.
The Numeric type may have casting/rounding issues with SQLite.
2022-12-18 15:25:22 +01:00
Fabio Manganiello 5763c5e0ba
Don't use the entities cache when upserting entities.
This may make things a bit less optimal, but it's probably the only
possible solution that preserves my sanity.

Managing upserts of cached instances that were previously made transient
and expunged from the session is far from easy, and the management of
recursive parent/children relationships only add one more layer of
complexity (and that management is already complex enough in its current
implementation).
2022-12-18 15:13:21 +01:00
Fabio Manganiello b0464219d3
Large refactor of the entities engine. 2022-12-17 21:41:23 +01:00
Fabio Manganiello 9ddebb920f
Merge branch 'master' into 29-generic-entities-support 2022-12-17 00:51:51 +01:00
Fabio Manganiello 6666f5581c
Bump version: 0.24.2 → 0.24.3 2022-12-17 00:31:22 +01:00
Fabio Manganiello 3279a6ca53 Merge pull request 'Add author and tags attributes to RSS feed entries' (#238) from 236-add-author-and-tags-columns-to-feeds into master
Reviewed-on: platypush/platypush#238
2022-12-17 00:25:09 +01:00
Fabio Manganiello 152ebdf737
[#236] Added `author` and `tags` attributes to new feed entry event and schema objects. 2022-12-17 00:21:32 +01:00
Fabio Manganiello cd569c76aa
Changed deprecated format of description_file in setup.cfg 2022-12-16 23:20:26 +01:00
Fabio Manganiello b044fa4acf
s/disable_logging/logging_level/g on entity events. 2022-12-11 11:58:49 +01:00
Fabio Manganiello 3e41418742
Merge branch 'master' into 29-generic-entities-support 2022-12-11 11:47:12 +01:00
Fabio Manganiello 2ee2a1d7b5
Replaced `disable_logging` with a more generic `logging_level`.
The `disable_logging` attribute was only available on events and
responses, and it could only either entirely disable or enable logging
for all the events of a certain type.

The new flag allows more customization by setting the default logging
level used for any message of a certain type (or `None` to disable
logging). This makes it possible to e.g. set some verbose events to
debug level, and the user can see them if they configure the application
in debug mode.

It also delegates the logging logic to the message itself, instead of
having different parts of the application handling their own logic.
2022-12-11 11:46:37 +01:00
Fabio Manganiello d4b540dd67
Replaced `disable_logging` with a more generic `logging_level`.
The `disable_logging` attribute was only available on events and
responses, and it could only either entirely disable or enable logging
for all the events of a certain type.

The new flag allows more customization by setting the default logging
level used for any message of a certain type (or `None` to disable
logging). This makes it possible to e.g. set some verbose events to
debug level, and the user can see them if they configure the application
in debug mode.

It also delegates the logging logic to the message itself, instead of
having different parts of the application handling their own logic.
2022-12-11 11:39:38 +01:00
Fabio Manganiello dff54a5246
Merge branch 'master' into 29-generic-entities-support 2022-12-11 10:59:58 +01:00
Fabio Manganiello aa3479abeb
Added [-v|--verbose] and --version options to the command line. 2022-12-11 10:59:12 +01:00
Fabio Manganiello a1d3724b8d
Added [-v|--verbose] and --version options to the command line. 2022-12-11 10:54:03 +01:00
Fabio Manganiello cf9d34d38e
A more robust logic to parse `zwave.mqtt` value attributes. 2022-12-10 16:21:29 +01:00
Fabio Manganiello c4f649a0d5
`autoflush` should be passed as an option to `db.get_session`. 2022-12-10 16:20:14 +01:00
Fabio Manganiello 6a2a3100f8
LINT fixes for `zwave.mqtt` backend 2022-12-10 16:16:23 +01:00
Fabio Manganiello 5a47308516
Merge branch 'master' into 29-generic-entities-support 2022-12-10 15:57:28 +01:00
Fabio Manganiello 4c8190ac14
Bump version: 0.24.1 → 0.24.2 2022-12-10 15:37:49 +01:00
Fabio Manganiello 6713bf6994
Fixed `backend.zwave` event logic dispatch for recent versions of ZWaveJS.
ZWaveJS has broken back-compatibility with zwavejs2mqtt when it comes to
events format.

Only a partial representation of the node and value objects is
forwarded, and that's often not sufficient to infer the full state of
the node with its values.

The `_dispatch_event` logic has therefore been modified to accommodate
both the implementation.

This means that we have to go conservative in order to preserve
back-compatibility and not over-complicate things, even if it (slightly)
comes at the expense of performance.
2022-12-10 15:35:09 +01:00
Fabio Manganiello 313105f014
Fixed `backend.zwave` event logic dispatch for recent versions of ZWaveJS.
ZWaveJS has broken back-compatibility with zwavejs2mqtt when it comes to
events format.

Only a partial representation of the node and value objects is
forwarded, and that's often not sufficient to infer the full state of
the node with its values.

The `_dispatch_event` logic has therefore been modified to accommodate
both the implementation.

This means that we have to go conservative in order to preserve
back-compatibility and not over-complicate things, even if it (slightly)
comes at the expense of performance.
2022-12-10 14:52:10 +01:00
Fabio Manganiello a17bc3c474 `main.db` should use the configured `workdir` when not specified.' (#235)
Reviewed-on: platypush/platypush#235
Closes: platypush/platypush#234
2022-12-09 23:41:07 +01:00
Fabio Manganiello 219a0a99ca `main.db` should use the configured `workdir` when not specified.
Closes: #234
Reviewed-On: platypush/platypush#234
2022-12-09 23:37:10 +01:00
Fabio Manganiello 3b1147eaae Bump version: 0.24.0 → 0.24.1 2022-12-08 12:33:34 +01:00
Fabio Manganiello 5ba3fa1b5b FIX: Parenthesized context managers are only available in Python >= 3.10
Since Parenthesized context managers are only supported on very recent
versions of Python (thanks black for breaking back-compatibility), we
should still use the old multiline syntax - it's not worth breaking
compatibility with Python >= 3.6 and < 3.10 just to avoid typing a
backslash.
2022-12-08 12:28:36 +01:00
Fabio Manganiello 00fca6b187
Merge branch 'master' into 29-generic-entities-support 2022-12-04 20:58:06 +01:00
Fabio Manganiello 00a918dd20
Support for the new way of reporting events on ZWaveJS-UI.
The most recent versions of ZwaveJS-UI don't send the `hexId` of the
node on node change events. We have therefore to infer it from the
reported `dbLink`.
2022-12-04 20:56:52 +01:00
Fabio Manganiello 3a92bf59ca
Support for the new way of reporting events on ZWaveJS-UI.
The most recent versions of ZwaveJS-UI don't send the `hexId` of the
node on node change events. We have therefore to infer it from the
reported `dbLink`.
2022-12-04 20:48:42 +01:00
Fabio Manganiello ecba72935f
Check for table metadata existance in `Base.metadata` instead of having a separate entity registry 2022-12-04 16:28:46 +01:00
Fabio Manganiello 1ab85f99d9
Support for illuminance sensor entities on `zigbee.mqtt` 2022-11-30 02:16:56 +01:00
Fabio Manganiello 09d70e2ff1
The `zwavejs2mqtt` project has been renamed `zwave-js-ui`
Change the documentation accordingly
2022-11-30 02:04:48 +01:00
Fabio Manganiello b6370b51da
Extended humidity sensors detection for zigbee.mqtt 2022-11-30 01:24:35 +01:00
Fabio Manganiello 16c24d799d
Removed custom formatting for child zigbee/zwave entity names
The parent->child relationship is now modelled on the database itself,
so we no longer need value names specifically formatted as
`[DeviceName] ValueName` - the UI will take care of it.
2022-11-30 01:02:25 +01:00
Fabio Manganiello 080b21ab70
Added support for reachable flag on zwave.mqtt child entities 2022-11-30 01:01:45 +01:00
Fabio Manganiello 2b532c1947
Implemented parent/child support for zigbee.mqtt entities 2022-11-30 00:55:04 +01:00
Fabio Manganiello abaeabea22
Implemented recursive merges of parent/child relationships in entities 2022-11-30 00:50:53 +01:00
Fabio Manganiello cc156a53a1
Support for parent/children relationships on `zwave.mqtt` entities 2022-11-28 21:42:11 +01:00
Fabio Manganiello 0edd73690b
Modelling of parent/children relationships on entity level 2022-11-28 21:36:00 +01:00
Fabio Manganiello 0e0c90f0f2
zwave.mqtt additions
- Infer entity types on the basis of their semantic type (bool, decimal,
  list) and read-only attribute (read-only bool is `BinarySensor`,
  read-write bool is `Switch`, read-only decimal is `NumericSensor`,
  read-write decimal is `Dimmer`, etc.) instead of trying to infer it
  from the command class. Only a small set of command classes (like
  configurations, vendor-specific or internal values) will be excluded.
  This should greatly increase the number of supported values.

- Added support for `EnumSwitch` entities.

- Added inference for illuminance and humidity sensors.
2022-11-27 22:53:53 +01:00
Fabio Manganiello 78c59f437a
Added support for illuminance sensor entities 2022-11-27 22:38:58 +01:00
Fabio Manganiello 03d1c554ea
Updated webapp dist files 2022-11-27 14:23:30 +01:00
Fabio Manganiello b1a7a7d915
Fixed little overlap between the entities' header and the navigator 2022-11-27 12:56:39 +01:00
Fabio Manganiello b5653e070e
Style improvements for the main navigator 2022-11-27 12:56:17 +01:00
Fabio Manganiello 681f307d04
A more self-explanatory icon for entity grouping selections 2022-11-27 00:56:47 +01:00
Fabio Manganiello bba582875a
The `data` attribute on `EntityUpdateEvent` shouldn't be taken into account for flashing updates 2022-11-27 00:56:23 +01:00
Fabio Manganiello e8d6717fcb
Added input box for <Dimmer> entities 2022-11-27 00:56:01 +01:00
Fabio Manganiello bd59a5eefd
Support for range labels on <Slider> 2022-11-27 00:55:19 +01:00
Fabio Manganiello f8aaab20f5
Updated webapp dist files 2022-11-27 00:53:58 +01:00
Fabio Manganiello faa8295469
White background for main nav 2022-11-26 01:52:42 +01:00
Fabio Manganiello d29723ea41
Keep the main menu items vertically aligned to the center also on tablets 2022-11-26 01:28:20 +01:00
Fabio Manganiello d0c8a8edf9
A bit of padding for LightHue on mobile 2022-11-26 01:19:52 +01:00
Fabio Manganiello 37254cad1a
Mobile UI improvements 2022-11-26 01:16:07 +01:00
Fabio Manganiello f28f08dd1a
Keep the main menu open on page load by default on >= desktop 2022-11-26 01:15:03 +01:00
Fabio Manganiello fecd96f64c
Solved issue with main menu shrinking a bit when the main panel has too much wide content 2022-11-26 00:32:11 +01:00
Fabio Manganiello 33cc055249
Switched expanded main menu to light colors 2022-11-26 00:31:36 +01:00
Fabio Manganiello a57e67b96f
Better style for the settings' users and token panels 2022-11-25 23:16:16 +01:00
Fabio Manganiello 21c1c96f2e
Use Dropdown for the settings menu 2022-11-25 23:15:41 +01:00
Fabio Manganiello 292ed2abff
Better style for dropdown items.
- Larger icon div to prevent text overlapping with icons
- Support for `selected` class
2022-11-25 23:14:28 +01:00
Fabio Manganiello 73f6712f7a
Bump version: 0.23.6 → 0.24.0 2022-11-22 00:12:25 +01:00
Fabio Manganiello c0dd91838b
Merge branch 'master' into 29-generic-entities-support 2022-11-21 22:13:47 +01:00
Fabio Manganiello d95baac74e Add user credentials on the encrypted JWT token.
Adding the credentials ensures that tokens associated to non-existing
users, or users with an invalid password, won't be accepted, even if
they were correctly encrypted using the host's keypair.

This adds an additional layer of security in case the host's keypair
gets compromised.
2022-11-21 13:16:09 +01:00
Fabio Manganiello 98d7c95aa7 Removed two unrequired `return` statements 2022-11-21 13:04:48 +01:00
Fabio Manganiello ba1681fc22 Merge branch 'master' into 29-generic-entities-support 2022-11-21 12:36:01 +01:00
Fabio Manganiello a2c8e27bd8 Removed PyJWT dependency.
PyJWT is a very brittle and cumbersome dependency that expects several
cryptography libraries to be already installed on the system, and it can
lead to hard-to-debug errors when ported to different systems.

Moreover, it installs the whole `cryptography` package, which is several
MBs in size, takes time to compile, and it requires a Rust compiler to
be present on the target machine.

Platypush will now use the Python-native `rsa` module to handle JWT
tokens.
2022-11-21 12:30:38 +01:00
Fabio Manganiello 02f89258b8
FIX: Task.set_name only works on Python >= 3.8 2022-11-21 09:49:57 +01:00
Fabio Manganiello ae17a12c12
FIX: `UserManager.get_users`
`UserManager.get_users` should not return a reference to the query
object, since the query object will be invalidated as soon as the
connection is closed.

Instead, it should return directly the list of `User` objects.
2022-11-21 00:57:00 +01:00
Fabio Manganiello e579fb3417
Don't display sensors with null value 2022-11-21 00:05:19 +01:00
Fabio Manganiello b9e6614b04
Added support for `EnumSensor` entities 2022-11-21 00:04:07 +01:00
Fabio Manganiello d171000a0e
Initial support for sensor entities in `zwave.mqtt` 2022-11-14 22:08:15 +01:00
Fabio Manganiello a7bc4f443c
Imports order 2022-11-14 21:30:43 +01:00
Fabio Manganiello 45d0e4445b
Sorted entity type names 2022-11-14 00:46:58 +01:00
Fabio Manganiello 96ce4729f9
Updated webapp dist files 2022-11-14 00:46:40 +01:00
Fabio Manganiello b7757d17cc
Updated webapp dist files 2022-11-14 00:06:41 +01:00
Fabio Manganiello 7fac5392b8
Blink entities only if their values have actually changed 2022-11-13 23:52:21 +01:00
Fabio Manganiello 211372e472
Added support for dimmers on `zigbee.mqtt` 2022-11-13 18:48:36 +01:00
Fabio Manganiello 833d908a32
Blink entities body upon update 2022-11-13 01:39:40 +01:00
Fabio Manganiello 24f5a8283c
Added `PRAGMA foreign_keys = ON` before deleting entities on SQLite
SQLite doesn't enable foreign keys cascade on delete by default.
2022-11-13 01:18:45 +01:00
Fabio Manganiello f90d84a3d4
Don't wait for UI updates for entities that are not queriable 2022-11-13 00:54:37 +01:00
Fabio Manganiello fb594cb8b1
Updated webapp dist files 2022-11-12 16:31:35 +01:00
Fabio Manganiello 69e097707d
Don't lock read session from the main database 2022-11-12 16:10:57 +01:00
Fabio Manganiello 86edd70d93
Fixed session/concurrency management on the main SQLite db
- The `declarative_base` instance should be shared
- Database `session_locks` should be stored at module, not instance
  level
- Better isolation of scoped sessions
- Enclapsulated `get_session` method in `UserManager`
2022-11-12 15:36:17 +01:00
Fabio Manganiello bfeb0a08c4
Encapsulate `_get_session` in `EntityManager` 2022-11-12 15:14:10 +01:00
Fabio Manganiello 8450129858
LINT fixes 2022-11-12 11:39:12 +01:00
Fabio Manganiello 8a894d0989
`user_manager` should be a global object instead of being initialized on-demand 2022-11-12 11:38:40 +01:00
Fabio Manganiello 6b7933cd33
Using a different SQLite database for entities
This prevents multiprocessing/concurrency issues when modifying the same
database file both from the main process and from the web server process
2022-11-12 02:00:55 +01:00
Fabio Manganiello 3fc94181b7
LINT fixes 2022-11-11 22:02:36 +01:00
Fabio Manganiello 26f869b6e4
LINT fixes 2022-11-11 21:49:38 +01:00
Fabio Manganiello 02a4c9f667
Added is_query_disabled attribute to entities 2022-11-11 20:40:36 +01:00
Fabio Manganiello 84bb77bd5b
Replaced ambiguous logger variable name 2022-11-11 20:37:39 +01:00
Fabio Manganiello 00a43dd1f8
Implemented `EnumSwitch` entity type
Done for `zigbee.mqtt`, other plugins will follow
2022-11-11 01:46:38 +01:00
Fabio Manganiello 801ed05684
Added support for binary sensors (in zigbee.mqtt for now) 2022-11-05 01:47:50 +01:00
Fabio Manganiello 6454f9d018
Propert snake case -> camel case conversion for backend entities -> frontend components 2022-11-04 22:53:24 +01:00
Fabio Manganiello 0f19104512
Improved zigbee.mqtt node property queries.
Now handling cases of nodes with values having multiple levels (> 1) of
nested properties.
2022-11-04 22:51:40 +01:00
Fabio Manganiello 5ca3c06f96
Normalize device names in set_lights 2022-11-02 23:32:21 +01:00
Fabio Manganiello d5f8d55b4b
Fixed zigbee.mqtt light entity conversion 2022-11-02 23:07:12 +01:00
Fabio Manganiello 636d1ced3a
A more robust way of splitting devices provided in the <ieee_address:value> format 2022-11-02 22:49:19 +01:00
Fabio Manganiello 7db84acd34
Notify of entity scan timeouts on the console instead of creating tons of notifications 2022-11-02 22:24:06 +01:00
Fabio Manganiello 02abef71e3
Fixes for zigbee devices polling
- Don't publish a `get` request if the device has no exposed queriable
  attributes.

- Perform the recursive build of the `get` request payload before
  checking for the `access` attribute.
2022-11-02 21:54:47 +01:00
Fabio Manganiello 64513be6b8
Initial implementation of sensor entities.
Implemented (at least in `zigbee.mqtt`, for now):

- `TemperatureSensor`
- `HumiditySensor`
- `VoltageSensor`
- `CurrentSensor`
- `EnergySensor`
- `PowerSensor`
- `NumericSensor` (generic fallback 1)
- `RawSensor` (generic fallback 2)
- `Sensor` (root class)
2022-11-02 16:38:17 +01:00
Fabio Manganiello 440cd60d6e
A (slightly) smarter way to infer the plural spelling of singular entity names 2022-11-02 16:35:20 +01:00
Fabio Manganiello 3d1a08f7af
Changed default entity grouping on the frontend.
Changed from `type` to `category`, which is basically the `name_plural`
attribute of the associated entity type metadata.

This allows us to define distinct entity metadata entries that we still
want to share the same grouping - for instance, `temperature_sensor`,
`humidity_sensor` and `battery` should all be grouped under `Sensors` on
the frontend.
2022-11-02 16:33:12 +01:00
Fabio Manganiello 68dd09e8ae
Removed unused `expanded` data attribute 2022-11-02 16:31:50 +01:00
Fabio Manganiello d7214c4c83
Fix for `No converter available` warnings on zigbee2mqtt
Only include readable (not state-only) properties on the payload sent to
to `zigbee2mqtt/<device>/get`.
2022-10-31 00:51:26 +01:00
Fabio Manganiello a1cf671334
Added support for link_quality entities to `zigbee.mqtt` 2022-10-30 11:03:22 +01:00
Fabio Manganiello 78dc8416fb
Snake case -> camel case for backend -> frontend entity types conversion 2022-10-30 11:01:46 +01:00
Fabio Manganiello 691d109fb7
Expunge entities after session commit to ensure that the ORM objects can be reused 2022-10-30 11:00:09 +01:00
Fabio Manganiello 71ccf6d04a
Support for battery sensors on zigbee.mqtt 2022-10-29 18:16:38 +02:00
Fabio Manganiello 42651e937b
LINT fixes on zigbee.mqtt plugin 2022-10-29 14:09:44 +02:00
Fabio Manganiello d61b053f72
Support for battery entities 2022-10-29 13:38:42 +02:00
Fabio Manganiello cdacf50fc7
Support for decimal.Decimal type JSON serialization 2022-10-29 13:35:52 +02:00
Fabio Manganiello b8215d2736 A more robust cron start logic
If may happen (usually because of a race condition) that a cronjob has
already been started, but it hasn't yet changed its status from IDLE to
RUNNING when the scheduler checks it.

This fix guards the application against such events. If they occur, we
should just report them and move on, not terminate the whole scheduler.
2022-10-27 10:45:59 +02:00
Fabio Manganiello 486cd66885
More LINTs 2022-10-23 21:23:19 +02:00
Fabio Manganiello 72c7444a45
LINT 2022-10-23 18:23:20 +02:00
Fabio Manganiello 951950c864
Added dimmer entities 2022-10-23 00:30:32 +02:00
Fabio Manganiello d7278857e5
Ensure that no records with duplicate key exist within an SQLAlchemy session before flushing 2022-10-23 00:28:42 +02:00
Fabio Manganiello 3e6ebdd23b
Don't store/show the state of write-only toggle switches 2022-10-23 00:28:01 +02:00
Fabio Manganiello 8cd5cb3338
The Slider should only react to @input events 2022-10-23 00:26:59 +02:00
Fabio Manganiello 1af7ece881
Added deprecation notice for `zwave` plugin and backend (use `zwave.mqtt` instead) 2022-10-22 19:17:58 +02:00
Fabio Manganiello 5c68365188
Better management for entity error icons 2022-10-14 23:37:36 +02:00
Fabio Manganiello 7f575bacaa
Implemented the new zwavejs2mqtt features for adding and removing nodes 2022-10-14 23:28:02 +02:00
Fabio Manganiello 5995d045e1
Merge branch 'master' into 29-generic-entities-support 2022-10-14 20:57:13 +02:00
Fabio Manganiello a5db599268
FIX: Skip empty lines on `config.include` 2022-10-14 20:56:18 +02:00
Fabio Manganiello c89ed24f4b
Updated webapp dist files 2022-10-12 03:07:17 +02:00
Fabio Manganiello 1b791156bd
Proper support for color zigbee lights 2022-10-12 03:00:42 +02:00
Fabio Manganiello e617fc75d4
Fixed slider ranges and label 2022-10-12 02:59:50 +02:00
Fabio Manganiello 041f64c80f
Dirty workaround to prevent redefinition of SQLAlchemy ORM model classes 2022-10-10 01:38:15 +02:00
Fabio Manganiello aa5b52db2f
FIX: Still redirect to /register by default if no users have been created 2022-10-10 01:36:28 +02:00
Fabio Manganiello 5f09d449f4
`extend_existing=True` for entity tables 2022-10-09 23:15:50 +02:00
Fabio Manganiello 6ec8a991df
Fixed tests 2022-10-08 15:18:26 +02:00
Fabio Manganiello 958ef6b987
Better entity modal padding 2022-10-07 11:12:30 +02:00
Fabio Manganiello 16c55b45f6
updated dist files 2022-10-07 11:12:13 +02:00
Fabio Manganiello b9b7404230
Web panel improvements.
- Don't return a redirect to the login page if an authentication failed
  over a JSON endpoint - instead, return a JSON payload with the error.

- Added support for additional fonts.

- Re-designed the login/registration page.

- Updated caniuse database.
2022-10-07 02:24:29 +02:00
Fabio Manganiello c0ffea681f
updated dist files 2022-10-07 02:23:12 +02:00
Fabio Manganiello 2aab1d090d
Increased maxkb limit 2022-10-07 02:23:04 +02:00
Fabio Manganiello 2cc80e7f16
Merge branch 'master' into 191-support-for-general-entities-backend-and-plugin 2022-10-07 00:05:54 +02:00
Fabio Manganiello b88983f055
Added `qos` argument to `mqtt.publish`. 2022-10-05 01:13:47 +02:00
Fabio Manganiello 85f583a0ad
Reduced :maxdepth: of toctree in documentation.
Recent versions of Sphinx get a bit too zealous about generating deeply
nested toctrees.
2022-09-30 11:47:19 +02:00
Fabio Manganiello fed7c2c6ff
Fixed typo in schema path 2022-09-30 11:30:57 +02:00
Fabio Manganiello 1d78c3e753
FIX: Broken docstring 2022-09-30 10:56:08 +02:00
Fabio Manganiello 00d47731c5 Merge pull request 'Mimic3 integration' (#227) from 226-mimic3-integration into master
Reviewed-on: platypush/platypush#227
2022-09-30 10:52:53 +02:00
Fabio Manganiello ae226a5b01
Added `tts.mimic3` integration.
Closes: #226
2022-09-30 10:51:17 +02:00
Fabio Manganiello fef7aff245
LINT fixes for mpv plugin 2022-09-30 10:41:56 +02:00
Fabio Manganiello 82ab7face2
A more robust logic to detect the webserver local bind address 2022-09-30 03:10:37 +02:00
Fabio Manganiello 3ed10092ae Merge pull request 'Wallabag integration' (#225) from 222-wallabag-integration into master
Reviewed-on: platypush/platypush#225
2022-09-29 10:52:16 +02:00
Fabio Manganiello 4bab9d2607
[#224] Implemented Wallabag integration 2022-09-29 10:51:16 +02:00
Fabio Manganiello deb25196d2
Merge branch 'master' into 191-support-for-general-entities-backend-and-plugin 2022-09-28 02:17:10 +02:00
Fabio Manganiello a0575ed6de
Bump version: 0.23.5 → 0.23.6 2022-09-19 20:41:02 +02:00
Fabio Manganiello 3d74f0a11f
Updated CHANGELOG 2022-09-19 20:40:54 +02:00
Fabio Manganiello 09baceab4b
Include album_id and the list of tracks in music.tidal.get_album 2022-09-19 20:39:21 +02:00
Fabio Manganiello c2a3f2f4f3
Bump version: 0.23.4 → 0.23.5 2022-09-18 19:55:05 +02:00
Fabio Manganiello 36dd645209
Use session.playlist instead of session.user.playlist to query playlists 2022-09-18 06:04:53 +02:00
Fabio Manganiello 61cda60751
Proper implementation for Tidal's add_to_playlist and remove_from_playlist methods
- Using tidalapi's `UserPlaylist.add` and `UserPlaylist.delete` methods
  instead of defining my own through `_api_request`, so we won't have to
  deal with the logic to set the ETag header.

- Added `remove_from_playlist` method.
2022-09-18 05:22:12 +02:00
Fabio Manganiello 7c610adc84
FIX: Apply expanduser to the credentials_file setting in music.tidal 2022-09-17 06:30:20 +02:00
Fabio Manganiello a9ebb4805a
Fixed doc warnings 2022-09-17 06:25:28 +02:00
Fabio Manganiello 1b405de0d5
Added missing docs 2022-09-17 06:09:39 +02:00
Fabio Manganiello e1aa214bad tidal-integration (#223)
Reviewed-on: platypush/platypush#223
2022-09-16 21:48:09 +02:00
Fabio Manganiello 41acf4b253
Generate event ID as true random strings, not MD5 hashes of UUIDs 2022-09-05 03:08:39 +02:00
Fabio Manganiello c77746e278 If the output of a hook is null, make sure to normalize it an empty string before pushing it to Redis 2022-09-04 16:16:02 +02:00
Fabio Manganiello 4682fb4210
Throw an assertion error when on_duplicate_update is specified on db.insert with no key_columns 2022-09-04 16:02:37 +02:00
Fabio Manganiello 0143dac216
Improved support for bulk database statements
- Wrapped insert/update/delete operations in transactions
- Proper (and much more efficient) bulk logic
- Better upsert logic
- Return inserted/updated records if the engine supports it
2022-09-04 13:30:35 +02:00
Fabio Manganiello a90aa2cb2e Make sure that a webhook function never returns a null response 2022-09-04 00:52:41 +02:00
Fabio Manganiello 1ea53a6f50
Support for query placeholders in `db.select` 2022-09-04 00:28:08 +02:00
Fabio Manganiello e77d6a4ad4 Merge pull request 'Add support for OPML import and export in the RSS plugin' (#220) from 219-opml-import-export into master
Reviewed-on: platypush/platypush#220
2022-09-02 00:24:37 +02:00
Fabio Manganiello 61c96612bc Merge branch 'master' into 219-opml-import-export 2022-09-02 00:23:57 +02:00
Fabio Manganiello 6c6e68b512
Added support for OPML import and export in the RSS plugin.
[closes #219]
2022-09-02 00:21:40 +02:00
Fabio Manganiello a286cf5000 Updated PopcornTime base URL 2022-09-01 11:13:16 +02:00
Fabio Manganiello c5b12403d0
Implemented support for returning richer HTTP responses on webhooks.
A `WebhookEvent` hook can now return a tuple in the format `(data,
http_code, headers)` in order to customize the HTTP status code and the
headers of a response.
2022-09-01 01:37:18 +02:00
Fabio Manganiello 96b2ad148c
A smarter way of building and matching the event condition 2022-08-31 02:19:21 +02:00
Fabio Manganiello 67413c02cd
Handle the case where the condition is a serialized dictionary 2022-08-31 01:55:21 +02:00
Fabio Manganiello db45d7ecbf
FIX: More robust logic against section configurations that may not be maps 2022-08-31 01:27:53 +02:00
Fabio Manganiello a675fe6a92
Updated CHANGELOG 2022-08-31 00:49:08 +02:00
Fabio Manganiello c3fa3315f5
Implemented synchronization with webhook responses.
When a client triggers a `WebhookEvent` by calling a configured webhook
over `/hook/<hook_name>`, the server will now wait for the configured
`@hook` function to complete and it will return the returned response
back to the client.

This makes webhooks much more powerful, as they can be used to proxy
HTTP calls or other services, and in general return something to the
client instead of just executing actions.
2022-08-30 23:35:19 +02:00
Fabio Manganiello 1880a99052
Merge branch 'master' into 191-support-for-general-entities-backend-and-plugin 2022-08-29 01:41:47 +02:00
Fabio Manganiello e08947a3b7
Merge pull request #311 from BlackLight/dependabot/npm_and_yarn/platypush/backend/http/webapp/terser-5.14.2
Bump terser from 5.12.1 to 5.14.2 in /platypush/backend/http/webapp
2022-08-29 00:59:55 +02:00
Fabio Manganiello 6d63d2fc74
Merge pull request #305 from BlackLight/dependabot/npm_and_yarn/platypush/backend/http/webapp/shell-quote-1.7.3
Bump shell-quote from 1.7.2 to 1.7.3 in /platypush/backend/http/webapp
2022-08-29 00:59:19 +02:00
Fabio Manganiello 540a7d469e
- Fixed documentation errors and warnings
- Split Matrix integration into `plugin` and `client` files.
2022-08-29 00:55:46 +02:00
Fabio Manganiello b11a0e8bbb
Bump version: 0.23.3 → 0.23.4 2022-08-28 15:27:54 +02:00
Fabio Manganiello f4360dc0e0 Merge pull request 'Matrix Integration' (#217) from matrix-integration into master
Reviewed-on: platypush/platypush#217

Closes: #2
2022-08-28 15:21:05 +02:00
Fabio Manganiello ba68341d28 Merge branch 'master' into matrix-integration 2022-08-28 15:19:58 +02:00
Fabio Manganiello 4308024eef
Added missing docs 2022-08-28 15:18:23 +02:00
Fabio Manganiello c417d2f692
Implemented last Matrix integration features.
- Added presence, typing and seen receipt events.
- Added set display_name and avatar methods.
2022-08-28 15:17:11 +02:00
Fabio Manganiello e479ca7e3e
Completing the Matrix plugin integration
Newly implemented actions:

- `get_messages`
- `get_room_members`
- `update_device`
- `delete_devices`
- `room_alias_to_id`
- `add_room_alias`
- `delete_room_alias`
- `kick`
- `ban`
- `unban`
- `forget`
2022-08-28 12:26:27 +02:00
Fabio Manganiello 0e3cabc5f6
Support `attribute` parameter on `Function` schema fields. 2022-08-28 11:55:30 +02:00
Fabio Manganiello d890b6cbe8
Added create_room action 2022-08-27 23:26:42 +02:00
Fabio Manganiello 912168626c
Added join_room, leave_room and invite_to_room and extended handling on invitation events 2022-08-27 21:50:48 +02:00
Fabio Manganiello 513195b396
Implemented support for file upload 2022-08-27 15:12:50 +02:00
Fabio Manganiello 48ec6ef68b
Implemented proper support for encrypted media and added download method 2022-08-26 23:48:29 +02:00
Fabio Manganiello e4eb4cd7dc
More granular control over trusted devices, and added global synchronization event 2022-08-25 00:34:01 +02:00
Fabio Manganiello 550f026e13
Cleaner logging for assertion errors in plugin actions 2022-08-25 00:30:53 +02:00
Fabio Manganiello c89c712928
Fixed device trust process 2022-08-24 01:49:43 +02:00
Fabio Manganiello 05908e1a77
Fixing key verification process 2022-08-17 10:28:31 +02:00
Fabio Manganiello c04bc8d2bc
The matrix plugin joins the AsyncRunnablePlugin family too 2022-08-15 02:18:29 +02:00
Fabio Manganiello 2797ffbe53
The websocket plugin now extends AsyncRunnablePlugin too 2022-08-15 02:18:29 +02:00
Fabio Manganiello 770a14daae
ntfy plugin migrated to AsyncRunnablePlugin.
This commit removes a lot of the loop management boilerplate.
2022-08-15 02:18:29 +02:00
Fabio Manganiello dba03d3e33
Added AsyncRunnablePlugin class.
This class handles runnable plugins that have their own asyncio event
loop, without the pain usually caused by the management of multiple
threads + asyncio loops.
2022-08-15 02:18:28 +02:00
Fabio Manganiello f4672ce5c3
Refactored concurrency model in ntfy plugin 2022-08-15 02:18:28 +02:00
Fabio Manganiello 9e2b4a0043
Removed references to deprecated websockets attributes 2022-08-15 02:18:28 +02:00
Fabio Manganiello 4e3c6a5c16
The websocket plugin now extends AsyncRunnablePlugin too 2022-08-15 02:17:05 +02:00
Fabio Manganiello e17e65a703
ntfy plugin migrated to AsyncRunnablePlugin.
This commit removes a lot of the loop management boilerplate.
2022-08-15 02:17:05 +02:00
Fabio Manganiello 3b1ab78268
Added AsyncRunnablePlugin class.
This class handles runnable plugins that have their own asyncio event
loop, without the pain usually caused by the management of multiple
threads + asyncio loops.
2022-08-15 02:17:05 +02:00
Fabio Manganiello 4043878afd
Refactored concurrency model in ntfy plugin 2022-08-15 02:16:25 +02:00
Fabio Manganiello 2e7f3d8868
Removed references to deprecated websockets attributes 2022-08-12 15:22:04 +02:00
Fabio Manganiello dc7ba881f1
Merge branch 'master' into matrix-integration 2022-08-12 14:39:13 +02:00
Fabio Manganiello 4e1e6da67e
Added recv action on websocket plugin 2022-08-12 14:16:01 +02:00
Fabio Manganiello 354f3906f9
Changed autojoin_on_invite default value 2022-08-12 00:11:15 +02:00
Fabio Manganiello 7ab02e705d
Removed redundant _action_wrapper decorator 2022-08-05 19:04:43 +02:00
Fabio Manganiello cbe2e7bbfe
[WIP] 2022-08-04 03:08:54 +02:00
Fabio Manganiello c17d0080b5
Merge branch 'master' into matrix-integration 2022-08-04 02:14:22 +02:00
Fabio Manganiello 55671f4aff
If a request on a RunnablePlugin throws an exception then we should also restart the plugin upon reload
Plus some Black/LINT chores
2022-07-25 00:41:08 +02:00
Fabio Manganiello c32142c8b5
Added wait_stop() method to RunnablePlugin 2022-07-23 17:33:23 +02:00
Fabio Manganiello 32be4df11c
More robust way to retrieve an object's attribute on schemas 2022-07-23 17:32:14 +02:00
dependabot[bot] c7927a3d2f
Bump terser from 5.12.1 to 5.14.2 in /platypush/backend/http/webapp
Bumps [terser](https://github.com/terser/terser) from 5.12.1 to 5.14.2.
- [Release notes](https://github.com/terser/terser/releases)
- [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/terser/terser/commits)

---
updated-dependencies:
- dependency-name: terser
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-21 00:35:29 +00:00
Fabio Manganiello 3edb8352b4
Support sections with empty bodies in the YAML configuration files. 2022-07-16 02:09:22 +02:00
Fabio Manganiello cc29136db7
[#2] Support for caching rooms info and exposing them in the events 2022-07-15 00:37:21 +02:00
Fabio Manganiello 719bd4fddf
[#217 WIP] Initial plugin implementation.
- Added initial synchronization and users cache.
- Added loop to poll for new events (TODO: use websocket after the first sync)
- Added login, sync and join actions
2022-07-14 01:50:46 +02:00
Fabio Manganiello 3513ee3e1c
Merge branch 'master' into 191-support-for-general-entities-backend-and-plugin 2022-07-08 23:13:36 +02:00
dependabot[bot] 06168d4ebd
Bump shell-quote from 1.7.2 to 1.7.3 in /platypush/backend/http/webapp
Bumps [shell-quote](https://github.com/substack/node-shell-quote) from 1.7.2 to 1.7.3.
- [Release notes](https://github.com/substack/node-shell-quote/releases)
- [Changelog](https://github.com/substack/node-shell-quote/blob/master/CHANGELOG.md)
- [Commits](https://github.com/substack/node-shell-quote/compare/v1.7.2...1.7.3)

---
updated-dependencies:
- dependency-name: shell-quote
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-23 11:29:13 +00:00
Fabio Manganiello 0d0995d71d
Merge branch 'master' into 191-support-for-general-entities-backend-and-plugin 2022-06-02 20:58:34 +02:00
Fabio Manganiello 2898a33752
s/click_url/url/g in ntfy message definitions 2022-06-02 00:36:14 +02:00
Fabio Manganiello 0919a0055d
Merge branch 'master' into 191-support-for-general-entities-backend-and-plugin 2022-06-02 00:13:43 +02:00
Fabio Manganiello 5b3e1317f4
Only refresh entities that are visible on the interface 2022-05-30 09:23:25 +02:00
Fabio Manganiello 1df71cb54a
Proper support for light entities on smartthings 2022-05-30 09:23:05 +02:00
Fabio Manganiello 0689e05e96
Apply the light color to the icon fill instead of the bulb icon itself 2022-05-30 09:18:19 +02:00
Fabio Manganiello 89560e7c38
Only include entities associated to enabled plugins or with no plugins in `entities.get` 2022-05-29 23:59:46 +02:00
Fabio Manganiello 30dfdeecb0
Merge branch 'master' into 191-support-for-general-entities-backend-and-plugin 2022-05-25 10:11:57 +02:00
Fabio Manganiello f57f940d57
Made _is_switch more resilient against rogue Z-Wave values 2022-05-01 22:18:46 +02:00
Fabio Manganiello 117f92e5b4
Deprecated the `light.hue` backend
The polling logic has been moved to the `light.hue` plugin itself
instead, so it's no longer required to have both a plugin and a backend
enabled in order to fully manage a Hue bridge.
2022-05-01 21:55:35 +02:00
Fabio Manganiello a5541c33b0
Added support for light entities in zigbee.mqtt
TODO: Support for colors (I don't have a color Zigbee bulb to test it on yet)
2022-05-01 21:10:54 +02:00
Fabio Manganiello b23f45f45e
Process a zigbee entity update event with all the properties, not only the ones that have been changed 2022-05-01 21:09:13 +02:00
Fabio Manganiello 088cf23958
Do not emit input event from the light component upon update
It may be an incomplete update that breaks the UI, and it will be
overwritten by the backend event anyway
2022-05-01 21:08:02 +02:00
Fabio Manganiello e8f4b7c10e
CSS adjustments 2022-05-01 15:44:57 +02:00
Fabio Manganiello dd12d57552
Added light UI entity component 2022-05-01 15:35:20 +02:00
Fabio Manganiello 5aa3750807
Re-sync the list of entities when the entities component is mounted 2022-05-01 15:34:45 +02:00
Fabio Manganiello f760d44224
Refactored/simplified UI code for entities management 2022-05-01 15:34:15 +02:00
Fabio Manganiello 8d91fec771
Better implementation for light.hue.set_lights 2022-05-01 15:33:12 +02:00
Fabio Manganiello c22c17a55d
More flexible implementation for LightPlugin abstract methods 2022-05-01 15:31:45 +02:00
Fabio Manganiello 46df3a6a98
FIX: `reachable` is an attribute of `state` 2022-05-01 01:58:05 +02:00
Fabio Manganiello 8e06b8c727
Fixed range scaling on Slider component 2022-04-30 23:40:14 +02:00
Fabio Manganiello 30a024befb
Manage hue/sat/bri/ct light ranges on the light entity object itself 2022-04-30 19:38:50 +02:00
Fabio Manganiello b16af0a97f
Include entity `data` attributes in the entity info modal 2022-04-30 16:39:37 +02:00
Fabio Manganiello c7970842d7
Disable logging by default for entity events (they can be quite spammy) 2022-04-30 02:13:20 +02:00
Fabio Manganiello 7df67aca82
updated_at should have utcnow() onupdate, not now() 2022-04-30 01:48:55 +02:00
Fabio Manganiello d29b377cf1
Exclude deleted lights/groups/scenes from the returned lists 2022-04-30 01:39:39 +02:00
Fabio Manganiello 8d57cf06c2
Major refactor for the `light.hue` plugin.
- Added support for lights as native platform entities.
- Improved performance by using the JSON API objects whenever possible
  to interact with the bridge instead of the native Python objects,
  which perform a bunch of lazy API calls under the hood resulting in
  degraded performance.
- Fixed lights animation attributes by setting only the ones actually
  supported by a light.
- Several LINT fixes.
2022-04-30 01:07:00 +02:00
Fabio Manganiello 975d37c562
Added relevant attributes to `light` entities 2022-04-29 23:29:04 +02:00
Fabio Manganiello 90f067de61
Added `reachable` flag to device entities 2022-04-29 23:27:35 +02:00
Fabio Manganiello f45df5d4d3
Explictly cast entity IDs to strings when coalescing entity updates
Some plugins may represent entity IDs as integers, while the database
maps external IDs to strings. This may result in entities being
incorrectly mapped during merging. Casting to string prevents these
type-related ambiguities.
2022-04-29 23:24:28 +02:00
Fabio Manganiello 975991ba69
Merge branch 'master' into 191-support-for-general-entities-backend-and-plugin 2022-04-29 16:53:41 +02:00
Fabio Manganiello d22fbcd9db
Merge branch 'master' into 191-support-for-general-entities-backend-and-plugin 2022-04-28 01:58:24 +02:00
Fabio Manganiello 47f8520f3b
Added support for description/read_only/write_only on entity level 2022-04-24 22:18:29 +02:00
Fabio Manganiello d261b9bb9b
Frontend support for entities deletion 2022-04-24 21:40:10 +02:00
Fabio Manganiello 9981cc4746
Backend support for entities deletion 2022-04-24 21:38:45 +02:00
Fabio Manganiello 3e4b13d20f
Added standard Vue component for confirm dialogs 2022-04-24 21:34:39 +02:00
Fabio Manganiello 321a61d06d
Align .section.right content to the right 2022-04-24 11:30:52 +02:00
Fabio Manganiello b22df768eb
Fixed entity icon alignment on mobile 2022-04-24 01:42:14 +02:00
Fabio Manganiello 8e2154f2b5
Do not overwrite an entity's state from an event if the state was not sampled 2022-04-24 01:41:45 +02:00
Fabio Manganiello a9751f21f1
`entities` should be the default view when the web panel is opened 2022-04-24 01:40:34 +02:00
Fabio Manganiello 135965176d
Support for entity icon color change 2022-04-23 17:52:21 +02:00
Fabio Manganiello ef6b57df31
Added entity info modal and (partial) support for renaming entities 2022-04-23 01:01:14 +02:00
Fabio Manganiello 7d4bd20df0
Support for individual entity group refresh 2022-04-19 23:56:49 +02:00
Fabio Manganiello e6bfa1c50f
Better dynamic entities discovery 2022-04-13 11:25:14 +02:00
Fabio Manganiello 332c91252c
zwave.mqtt.status renamed to controller_status, while status should return the current state of the values 2022-04-12 23:44:14 +02:00
Fabio Manganiello b35c761a43
Fixed entities panel mobile layout 2022-04-12 22:24:19 +02:00
Fabio Manganiello 08c0779347
<style> on entity components should be scoped 2022-04-12 16:00:31 +02:00
Fabio Manganiello 595ebe49ca
Support for entity scan timeout errors and visual error handling 2022-04-12 15:58:19 +02:00
Fabio Manganiello 548d487e73
Publish a switch entity from zigbee.mqtt only if the update includes its state 2022-04-12 14:41:21 +02:00
Fabio Manganiello 20530c2b6d
Loading events are now synchronized both ways upon entity action/refresh 2022-04-12 01:10:09 +02:00
Fabio Manganiello 9ddcf5eaeb
Implemented entities refresh on the UI 2022-04-12 00:43:22 +02:00
Fabio Manganiello 2aa8778078
Do not process EntityUpdateEvents only in case of payload changes
The UI relies on these events upon refresh to detect if a device is
still reacheable. Therefore, we shouldn't mask them if we don't detect
any changes with the current entity configuration/state.
2022-04-12 00:41:20 +02:00
Fabio Manganiello 72617b4b75
Handle EntityUpdateEvents on the UI 2022-04-11 23:16:29 +02:00
Fabio Manganiello be4d1e8e01
Proper support for native entities in zigbee.mqtt integration 2022-04-11 21:16:45 +02:00
Fabio Manganiello db4ad5825e
Fire an EntityUpdateEvent when the zwave.mqtt backend gets a value changed message 2022-04-11 01:40:49 +02:00
Fabio Manganiello 4471001110
smartthings.toggle should properly publish the updated entity 2022-04-11 00:43:31 +02:00
Fabio Manganiello f17245e8c7
Send an EntityUpdateEvent only if an entity has already been persisted
If an event comes from an entity that hasn't been persisted yet on the
internal storage then we wait for the entity record to be committed
before firing an event. It's better to wait a couple of seconds for the
database to synchronize rather than dealing with entity events with
incomplete objects.
2022-04-11 00:38:11 +02:00
Fabio Manganiello 67ff585f6c
Entities engine improvements
- Added cache support to prevent duplicate EntityUpdateEvents
- The cache is smartly pre-populated and kept up-to-date, so it's
  possible to trigger events as soon as the entities are published by
  the plugin (not only when the records are flushed to the internal db)
2022-04-11 00:01:21 +02:00
Fabio Manganiello 17615ff028
Support for multiple entity types/plugins filter on entities.get 2022-04-10 21:23:03 +02:00
Fabio Manganiello 532217be12
Support for filtering entities by search string 2022-04-10 17:57:51 +02:00
Fabio Manganiello f301fd7e69
Added standard NoItems component to handle visualization of no-results divs 2022-04-10 14:27:32 +02:00
Fabio Manganiello 58861afb1c
Added entities panel 2022-04-10 13:07:36 +02:00
Fabio Manganiello 8ec9c8f203
Added standard component for icons 2022-04-10 13:07:01 +02:00
Fabio Manganiello 3435f591eb
Support for keep-open-on-item-click and icon URLs on dropdown elements 2022-04-10 01:57:39 +02:00
Fabio Manganiello 19223bbbe1
Added SmartThings icon 2022-04-10 01:56:47 +02:00
Fabio Manganiello 453652ef76
Updated plugin icons 2022-04-10 01:50:45 +02:00
Fabio Manganiello b2ff66aa62
Added mixins to capitalize/prettify text 2022-04-10 01:50:13 +02:00
Fabio Manganiello 655d56f4da
Upgraded font-awesome to 6.x 2022-04-10 01:49:14 +02:00
Fabio Manganiello f52b556219
- icon_class should not be part of the backend model
- Interaction with entities should occur through the `entities.action`
  method, not by implementing native methods on each of the model
  objects
2022-04-08 16:49:47 +02:00
Fabio Manganiello 947b50b937
Added meta as a JSON field on the Entity table
Metadata attributes can now be defined and overridden on the object
itself, as well as on the database. Note that db settings will always
take priority in case of value conflicts.
2022-04-07 22:11:31 +02:00
Fabio Manganiello db7c2095ea
Implemented meta property for entities (for now it only include `icon_class`) 2022-04-07 18:09:25 +02:00
Fabio Manganiello e40b668380
Added missing docs 2022-04-07 01:49:13 +02:00
Fabio Manganiello d3dc86a5e2
Added documentation for plugin/entity type registry 2022-04-07 01:47:42 +02:00
Fabio Manganiello 28026b0428
Trigger an EntityUpdateEvent when an entity state changes 2022-04-07 01:46:37 +02:00
Fabio Manganiello 44707731a8
Normalize UTC timezone on all the entity timestamps 2022-04-07 01:13:29 +02:00
Fabio Manganiello 948f37afd4
Filter by configured/enabled plugins when returning the entity/plugin registry 2022-04-07 01:04:06 +02:00
Fabio Manganiello 3b4f7d3dad
Added entities plugin to query/action entities 2022-04-07 00:22:54 +02:00
Fabio Manganiello 2eeb1d4fea
Entity objects are now JSON-able 2022-04-07 00:21:54 +02:00
Fabio Manganiello 26ffc0b0e1
Use Redis instead of an in-process map to store the entity/plugin registry
This is particularly useful when we want to access the registry from
another process, like the web server or an external script.
2022-04-07 00:18:11 +02:00
Fabio Manganiello 7b1a63e287
Make sure that flake8 and black don't step on each other's toes 2022-04-07 00:17:39 +02:00
Fabio Manganiello 1c6ff2fa49
(actually, the other way around is better) 2022-04-06 23:56:10 +02:00
Fabio Manganiello d311629403
black validation should run before flake8 2022-04-06 23:48:27 +02:00
Fabio Manganiello d52ae2fb80
Implemented RunnablePlugin.wait_stop() utility method 2022-04-05 23:33:02 +02:00
Fabio Manganiello 061268cdaf
Support for direct actions on native entities [WIP] 2022-04-05 23:22:54 +02:00
Fabio Manganiello 91ff47167b
Don't terminate the entities engine thread if a batch of entity records fails 2022-04-05 23:04:57 +02:00
Fabio Manganiello fe0f3202fe
columns should be a property of the Entity object 2022-04-05 23:04:19 +02:00
Fabio Manganiello 8a70f1d38e
Replaced deprecated sqlalchemy.ext.declarative with sqlalchemy.orm 2022-04-05 22:47:44 +02:00
Fabio Manganiello 4b7eeaa4ed
Smarter merging of entities with the same key before they are committed 2022-04-05 21:17:58 +02:00
Fabio Manganiello b43ed169c7
Added support for switches as native entities to zwave.mqtt plugin 2022-04-05 20:22:47 +02:00
Fabio Manganiello 0dac2c0e92
Fixed handling of possible null device definition in zigbee.mqtt 2022-04-05 00:31:04 +02:00
Fabio Manganiello 28b3672432
Added native support for switch entities to the zigbee.mqtt plugin. 2022-04-05 00:07:55 +02:00
Fabio Manganiello 9f2793118b
black fix 2022-04-04 22:43:04 +02:00
Fabio Manganiello 9d9ec1dc59
Added native support for switch entities to the smartthings plugin 2022-04-04 22:41:04 +02:00
Fabio Manganiello b9c78ad913
Added native support for switch entities to switchbot.bluetooth plugin 2022-04-04 21:12:59 +02:00
Fabio Manganiello 91ff8d811f
Added native entities support in switchbot plugin 2022-04-04 20:56:28 +02:00
Fabio Manganiello 783238642d
Skip string and underscore normalization in black 2022-04-04 20:56:28 +02:00
Fabio Manganiello 53da19b638
Added entities engine support to WeMo switch plugin 2022-04-04 17:22:55 +02:00
Fabio Manganiello 7459f0115b
Added more pre-commit hooks 2022-04-04 17:22:54 +02:00
Fabio Manganiello 2c4c27855d
Added `.exception` action to logger plugin 2022-04-04 17:22:54 +02:00
Fabio Manganiello 9c25a131fa
get_bus() should return a default RedisBus() instance if the main bus is not registered 2022-04-04 17:22:54 +02:00
Fabio Manganiello 4ee7e4db29
Basic support for entities on the local db and implemented support for switch entities on the tplink plugin 2022-04-04 16:50:17 +02:00
1140 changed files with 41275 additions and 13361 deletions

2
.gitignore vendored
View File

@ -22,3 +22,5 @@ platypush/requests
.coverage
coverage.xml
Session.vim
/jsconfig.json
/package.json

View File

@ -6,11 +6,12 @@ repos:
hooks:
# - id: trailing-whitespace
# - id: end-of-file-fixer
- id: check-yaml
- id: check-json
- id: check-xml
- id: check-symlinks
- id: check-added-large-files
- id: check-yaml
- id: check-json
- id: check-xml
- id: check-symlinks
- id: check-added-large-files
args: ['--maxkb=1500']
- repo: https://github.com/Lucas-C/pre-commit-hooks-nodejs
rev: v1.1.2

View File

@ -1,10 +1,133 @@
# Changelog
All notable changes to this project will be documented in this file.
Given the high speed of development in the first phase, changes are being reported only starting from v0.20.2.
Given the high speed of development in the first phase, changes are being
reported only starting from v0.20.2.
## [Unreleased]
### Added
- Migrated many integrations to the new [entities
framework](https://git.platypush.tech/platypush/platypush/pulls/230).
This is a very large change to the foundations of the platform, and there's
still a lot of work in progress. A detailed description of all the changes
will follow shortly.
### Fixed
- Migrated the `clipboard` integration from `pyperclip` to `pyclip` (see
[#240](https://git.platypush.tech/platypush/platypush/issues/240)).
`pyperclip` is unmaintained and largely broken, and `pyclip` seems to be a
viable drop-in alternative.
## [0.24.5] - 2023-02-22
### Added
- Added `hid` plugin to support discoverability and data interaction with
generic HID devices - like Bluetooth/USB peripherals, joysticks, dongles and
any other type of devices that supports the HID interface.
- Added `timeout` parameter to `websocket.send` to prevent messages sent on a
non-responsive websocket from getting the websocket loop stuck
### Fixed
- Running the Zeroconf registration logic in another thread in `backend.http`,
so failures in the Zeroconf logic don't affect the startup of the web server.
- (Temporarily) introduced `sqlalchemy < 2.0.0` as a requirement - a PR with a
migration to the new stable version of SQLAlchemy is in TODO.
## [0.24.4] - 2022-12-20
### Fixed
- Fixed cronjobs potentially being triggered even if it wasn't their slot in
case of clock synchronization events.
## [0.24.3] - 2022-12-17
### Added
- Added `[-v|--verbose]` command-line option to override the default logging
configuration and enable debug logging.
- Added `--version` command-line option to print the current version and exit.
- [[#236](https://git.platypush.tech/platypush/platypush/issues/236)] Added
support for `author` and `tags` attributes on feed entries.
## [0.24.2] - 2022-12-10
### Fixed
- The `main.db` configuration should use the configured `workdir` when no
values are specified.
- The `zwave.mqtt` is now compatible both with older (i.e. `zwavejs2mqtt`) and
newer (i.e. `ZwaveJS`) versions of the backend.
## [0.24.1] - 2022-12-08
### Fixed
- Removed a parenthesized context manager that broke compatibility with
Python &lt; 3.10.
## [0.24.0] - 2022-11-22
### Added
- Added [Wallabag integration](https://git.platypush.tech/platypush/platypush/issues/224).
- Added [Mimic3 TTS integration](https://git.platypush.tech/platypush/platypush/issues/226).
- Added `qos` attribute to `mqtt.publish` and all the plugins derived from `mqtt`.
### Changed
- Replaced PyJWT dependency with the Python-native `rsa` package. This will
make the installation much lighter, compatible with more systems and less
dependent on the platform-specific libraries required by `cryptography`.
> **NOTE**: This is a breaking change for those who use the `backend.http` API
> with JWT tokens. The new logic encrypts and encodes the payload in a
> different format, therefore previously generated tokens are no longer
> compatible.
## [0.23.6] - 2022-09-19
### Fixed
- Fixed album_id and list of tracks on `music.tidal.get_album`.
## [0.23.5] - 2022-09-18
### Added
- Added support for web hooks returning their hook method responses back to the
HTTP client.
- Added [Tidal integration](https://git.platypush.tech/platypush/platypush/pulls/223)
- Added support for [OPML
subscriptions](https://git.platypush.tech/platypush/platypush/pulls/220) to
the `rss` plugin.
- Better support for bulk database operations on the `db` plugin.
### Fixed
- Now supporting YAML sections with empty configurations.
## [0.23.4] - 2022-08-28
### Added
- Added `matrix` integration
([issue](https://git.platypush.tech/platypush/platypush/issues/2),
[PR](https://git.platypush.tech/platypush/platypush/pulls/217)).
### Changed
- Removed `clipboard` backend. Enabling the `clipboard` plugin will also enable
clipboard monitoring, with no need for an additional backend.

737
README.md
View File

@ -4,24 +4,16 @@ Platypush
[![Build Status](https://ci.platypush.tech/status.svg)](https://ci.platypush.tech/latest.log)
[![Documentation Status](https://ci.platypush.tech/docs/status.svg)](https://ci.platypush.tech/docs/latest.log)
[![pip version](https://img.shields.io/pypi/v/platypush.svg?style=flat)](https://pypi.python.org/pypi/platypush/)
[![License](https://img.shields.io/github/license/BlackLight/platypush.svg)](https://git.platypush.tech/platypush/platypush/-/blob/master/LICENSE.txt)
[![Last Commit](https://img.shields.io/github/last-commit/BlackLight/platypush.svg)](https://git.platypush.tech/platypush/platypush/-/commits/master/)
[![License](https://img.shields.io/github/license/BlackLight/platypush.svg)](https://git.platypush.tech/platypush/platypush/src/branch/master/LICENSE.txt)
[![Last Commit](https://img.shields.io/github/last-commit/BlackLight/platypush.svg)](https://git.platypush.tech/platypush/platypush/commits/branch/master)
[![Join chat on Matrix](https://img.shields.io/matrix/:platypush?server_fqdn=matrix.platypush.tech)](https://matrix.to/#/#platypush:matrix.platypush.tech)
[![Contributions](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://git.platypush.tech/platypush/platypush/-/blob/master/CONTRIBUTING.md)
[![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/BlackLight/platypush.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/BlackLight/platypush/context:python)
[![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/BlackLight/platypush.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/BlackLight/platypush/context:javascript)
[![Contributions](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://git.platypush.tech/platypush/platypush/src/branch/master/CONTRIBUTING.md)
<!-- toc -->
- [Architecture](#architecture)
* [Plugins](#plugins)
* [Actions](#actions)
* [Backends](#backends)
* [Events](#events)
* [Hooks](#hooks)
* [Procedures](#procedures)
* [Cronjobs](#cronjobs)
* [The web interface](#the-web-interface)
- [Useful links](#useful-links)
- [Introduction](#introduction)
+ [What it can do](#what-it-can-do)
- [Installation](#installation)
* [System installation](#system-installation)
+ [Install through `pip`](#install-through-pip)
@ -33,17 +25,27 @@ Platypush
+ [Check the instructions reported in the documentation](#check-the-instructions-reported-in-the-documentation)
* [Virtual environment installation](#virtual-environment-installation)
* [Docker installation](#docker-installation)
- [Architecture](#architecture)
* [Plugins](#plugins)
* [Actions](#actions)
* [Backends](#backends)
* [Events](#events)
* [Hooks](#hooks)
* [Procedures](#procedures)
* [Cronjobs](#cronjobs)
* [Entities](#entities)
* [The web interface](#the-web-interface)
- [Mobile app](#mobile-app)
- [Tests](#tests)
- [Funding](#funding)
<!-- tocstop -->
## Useful links
- Recommended read: [**Getting started with Platypush**](https://blog.platypush.tech/article/Ultimate-self-hosted-automation-with-Platypush).
- The [blog](https://blog.platypush.tech) is in general a good place to get
more insights on what you can build with it and inspiration about possible
usages.
- The [blog](https://blog.platypush.tech) is a good place to get more insights
and inspiration on what you can build.
- The [wiki](https://git.platypush.tech/platypush/platypush/wiki) also
contains many resources on getting started.
@ -51,19 +53,19 @@ Platypush
- Extensive documentation for all the available integrations and messages [is
available](https://docs.platypush.tech/).
- If you have issues/feature requests/enhancement ideas please [create an
issue](https://git.platypush.tech/platypush/platypush/-/issues).
- A [Reddit channel](https://www.reddit.com/r/platypush) is also available for
more general questions.
- If you have issues/feature requests/enhancements please [create an
issue](https://git.platypush.tech/platypush/platypush/issues).
- A [Matrix instance](https://matrix.to/#/#platypush:matrix.platypush.tech) is
also available if you are looking for more interactive support.
available if you are looking for interactive support.
---
- A [Reddit channel](https://www.reddit.com/r/platypush) is available for
general questions.
Platypush is a general-purpose extensible platform for automation and
integration across multiple services and devices.
## Introduction
Platypush is a general-purpose extensible platform for automation across
multiple services and devices.
It enables users to create their own self-hosted pieces of automation based on
events (*if this happens then do that*)
@ -72,16 +74,15 @@ everything you need to visualize and control under one roof.
It takes some concepts from [IFTTT](https://ifttt.com),
[Tasker](https://tasker.joaoapps.com/), [Microsoft
Flow](https://flow.microsoft.com), [PushBullet](https://pushbullet.com) and
[Home Assistant](https://www.home-assistant.io/) to provide an environment
where the user can easily connect things together.
Flow](https://flow.microsoft.com) and [Home
Assistant](https://www.home-assistant.io/) to provide an environment where the
user can easily connect things together.
Its ideal home is a single-board computer like a RaspberryPi that you can
configure to orchestrate any home automation and cloud automation in your own
living room or garage, but it can easily run on any device that can run a
Python interpreter, and the bar for the hardware requirements is very low as
well - I use it to run pieces of automation on devices as powerful as a
RaspberryPi Zero or an old Nokia N900 with Linux.
It's built with compatibility and flexibility in mind, and it can easily run on
any device that can run a Python interpreter - from a Raspberry Pi zero, to an
old smartphone, to a beefy server.
#### What it can do
You can use Platypush to do things like:
@ -112,325 +113,7 @@ You can use Platypush to do things like:
- [Create a custom single hub for Zigbee and Z-Wave smart devices](https://blog.platypush.tech/article/Transform-a-RaspberryPi-into-a-universal-Zigbee-and-Z-Wave-bridge)
- Build your own web dashboard with calendar, weather, news and music controls
(basically, anything that has a Platypush web widget)
- ...and much more (basically, anything that comes with a [Platypush plugin](https://docs.platypush.tech/en/latest/plugins.html))
## Architecture
The architecture of Platypush consists of a few simple pieces, orchestrated by
a configuration file stored by default under
[`~/.config/platypush/config.yaml`](https://git.platypush.tech/platypush/platypush/-/blob/master/examples/conf/config.yaml):
### Plugins
[Full list](https://docs.platypush.tech/en/latest/plugins.html)
Plugins are integrations that do things - like [modify
files](https://docs.platypush.tech/en/latest/platypush/plugins/file.html),
[train and evaluate machine learning
models](https://docs.platypush.tech/en/latest/platypush/plugins/tensorflow.html),
[control
cameras](https://docs.platypush.tech/en/latest/platypush/plugins/camera.pi.html),
[read
sensors](https://docs.platypush.tech/en/latest/platypush/plugins/gpio.sensor.dht.html),
[parse a web
page](https://docs.platypush.tech/en/latest/platypush/plugins/http.webpage.html),
[control
lights](https://docs.platypush.tech/en/latest/platypush/plugins/light.hue.html),
[send
emails](https://docs.platypush.tech/en/latest/platypush/plugins/mail.smtp.html),
[control
Chromecasts](https://docs.platypush.tech/en/latest/platypush/plugins/media.chromecast.html),
[run voice
queries](https://docs.platypush.tech/en/latest/platypush/plugins/assistant.google.html),
[handle torrent
transfers](https://docs.platypush.tech/en/latest/platypush/plugins/torrent.html)
or control
[Zigbee](https://docs.platypush.tech/en/latest/platypush/plugins/zigbee.mqtt.html)
or [Z-Wave](https://docs.platypush.tech/en/latest/platypush/plugins/zwave.html)
devices.
The configuration of a plugin matches one-on-one that of its documented class
constructor, so it's very straightforward to write a configuration for a plugin
by reading its documentation:
```yaml
light.hue:
# Groups that will be controlled by default
groups:
- Living Room
- Hall
```
### Actions
Plugins expose *actions*, that match one-on-one the plugin class methods
denoted by `@action`, so it's very straightforward to invoke plugin actions by
just reading the plugin documentation. They can be invoked directly from your
own scripts or they can be sent to the platform through any supported channel
as simple JSON messages:
```json
{
"type": "request",
"action": "light.hue.on",
"args": {
"lights": ["Entrance Bulb"]
}
}
```
### Backends
[Full list](https://docs.platypush.tech/en/latest/backends.html)
They are background services that either listen for messages on channels (like
an [HTTP
backend](https://docs.platypush.tech/en/latest/platypush/backend/http.html), an
[MQTT
instance](https://docs.platypush.tech/en/latest/platypush/backend/mqtt.html), a
[Kafka
instance](https://docs.platypush.tech/en/latest/platypush/backend/kafka.html),
a [Websocket
service](https://docs.platypush.tech/en/latest/platypush/backend/websocket.html),
[Pushbullet](https://docs.platypush.tech/en/latest/platypush/backend/pushbullet.html)
etc.) or monitor a device or a service for events (like a
[sensor](https://docs.platypush.tech/en/latest/platypush/backend/sensor.html),
a custom [voice
assistant](https://docs.platypush.tech/en/latest/platypush/backend/assistant.google.html),
a bridge running on a
[Zigbee](https://docs.platypush.tech/en/latest/platypush/backend/zigbee.mqtt.html)
or
[Z-Wave](https://docs.platypush.tech/en/latest/platypush/backend/zwave.html),
an [NFC card
reader](https://docs.platypush.tech/en/latest/platypush/backend/nfc.html), a
[MIDI
device](https://docs.platypush.tech/en/latest/platypush/backend/midi.html), a
[Telegram
channel](https://docs.platypush.tech/en/latest/platypush/backend/chat.telegram.html),
a [Bluetooth
scanner](https://docs.platypush.tech/en/latest/platypush/backend/bluetooth.scanner.ble.html)
etc.).
If a backend supports the execution of requests (e.g. HTTP, MQTT, Kafka,
Websocket and TCP) then you can send requests to these services in JSON format.
For example, in the case of the HTTP backend:
```shell
# Get a token
curl -XPOST -H 'Content-Type: application/json' -d '
{
"username": "$YOUR_USER",
"password": "$YOUR_PASSWORD"
}' http://host:8008/auth
# Execute a request
curl -XPOST -H 'Content-Type: application/json' \
-H "Authorization: Bearer $YOUR_TOKEN" -d '
{
"type": "request",
"action": "tts.say",
"args": {
"text": "This is a test"
}
}' http://host:8008/execute
```
### Events
[Full list](https://docs.platypush.tech/en/latest/events.html)
When a certain event occurs (e.g. a JSON request is received, or a [Bluetooth
device is
connected](https://docs.platypush.tech/en/latest/platypush/events/bluetooth.html#platypush.message.event.bluetooth.BluetoothDeviceConnectedEvent),
or a [Flic button is
pressed](https://docs.platypush.tech/en/latest/platypush/events/button.flic.html#platypush.message.event.button.flic.FlicButtonEvent),
or some [speech is detected on the voice assistant
service](https://docs.platypush.tech/en/latest/platypush/events/assistant.html#platypush.message.event.assistant.SpeechRecognizedEvent),
or an [RSS feed has new
items](https://docs.platypush.tech/en/latest/platypush/events/http.rss.html#platypush.message.event.http.rss.NewFeedEvent),
or a [new email is
received](https://docs.platypush.tech/en/latest/platypush/events/mail.html#platypush.message.event.mail.MailReceivedEvent),
or a [new track is
played](https://docs.platypush.tech/en/latest/platypush/events/music.html#platypush.message.event.music.NewPlayingTrackEvent),
or an [NFC tag is
detected](https://docs.platypush.tech/en/latest/platypush/events/nfc.html#platypush.message.event.nfc.NFCTagDetectedEvent),
or [new sensor data is
available](https://docs.platypush.tech/en/latest/platypush/events/sensor.html#platypush.message.event.sensor.SensorDataChangeEvent),
or [a value of a Zigbee device
changes](https://docs.platypush.tech/en/latest/platypush/events/zigbee.mqtt.html#platypush.message.event.zigbee.mqtt.ZigbeeMqttDevicePropertySetEvent),
etc.), the associated backend will trigger an
[event](https://docs.platypush.tech/en/latest/events.html).
### Hooks
Event hooks are custom pieces of logic that will be run when a certain event is
triggered. Hooks are the glue that connects events to actions, exposing a
paradigm similar to IFTTT (_if a certain event happens then run these
actions_). They can declared as:
- Sections of the [`config.yaml`](https://git.platypush.tech/platypush/platypush/-/blob/master/examples/conf/config.yaml).
Example:
```yaml
event.hook.SearchSongVoiceCommand:
if:
type: platypush.message.event.assistant.SpeechRecognizedEvent
phrase: "play ${title} by ${artist}"
then:
- action: music.mpd.clear
- action: music.mpd.search
args:
filter:
artist: ${artist}
title: ${title}
- if ${len(output)}:
- action: music.mpd.play
args:
resource: ${output[0]['file']}
```
- Stand-alone Python scripts stored under `~/.config/platypush/scripts` and
will be dynamically imported at start time.
[Example](https://git.platypush.tech/platypush/platypush/-/blob/master/examples/conf/hook.py):
```python
from platypush.event.hook import hook
from platypush.utils import run
from platypush.message.event.assistant import SpeechRecognizedEvent
@hook(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,
'title': title,
})
if results:
run('music.mpd.play', results[0]['file'])
```
### Procedures
Procedures are pieces of custom logic that can be executed as atomic actions
using `procedure.<name>` as an action name.
They can be defined either in the `config.yaml` or as Python scripts stored
under `~/.config/platypush/scripts` - provided that the procedure is also
imported in `~/.config/platypush/scripts/__init__.py` so it can be discovered
by the service.
YAML example for a procedure that can be executed when we arrive home and turns
on the lights if the luminosity is lower that a certain thresholds, says a
welcome home message using the TTS engine and starts playing the music:
```yaml
procedure.at_home:
# Get luminosity data from a sensor - e.g. LTR559
- action: gpio.sensor.ltr559.get_data
# If it's lower than a certain threshold, turn on the lights
- if ${int(light or 0) < 110}:
- action: light.hue.on
# Say a welcome home message
- action: tts.google.say
args:
text: Welcome home
# Play the music
- action: music.mpd.play
```
Python example:
```python
# Content of ~/.config/platypush/scripts/home.py
from platypush.procedure import procedure
from platypush.utils import run
@procedure
def at_home(**context):
sensor_data = run('gpio.sensor.ltr559.get_data')
if sensor_data['light'] < 110:
run('light.hue.on')
run('tts.google.say', text='Welcome home')
run('music.mpd.play')
```
In either case, you can easily trigger the at-home procedure by sending an
action request message to a backend - for example, over the HTTP backend:
```shell
curl -XPOST -H 'Content-Type: application/json' \
-H "Authorization: Bearer $YOUR_TOKEN" -d '
{
"type": "request",
"action": "procedure.at_home"
}' http://host:8008/execute
```
### Cronjobs
Cronjobs are pieces of logic that will be run at regular intervals, expressed
in crontab-compatible syntax. They can be defined either in the `config.yaml`
or as Python scripts stored under `~/.config/platypush/scripts` as functions
labelled by the `@cron` decorator.
Note that seconds are also supported (unlike the standard crontab definition),
but, for back-compatibility with the standard crontab format, they are at the
end of the cron expression, so the expression is actually in the format
`<minute> <hour> <day_of_month> <month> <day_of_week> <second>`.
YAML example for a cronjob that is executed every 30 seconds and checks if a
Bluetooth device is nearby:
```yaml
cron.check_bt_device:
cron_expression: '* * * * * */30'
actions:
- action: bluetooth.lookup_name
args:
addr: XX:XX:XX:XX:XX:XX
- if ${name}:
- action: procedure.on_device_on
- else:
- action: procedure.on_device_off
```
Python example:
```python
# Content of ~/.config/platypush/scripts/bt_cron.py
from platypush.cron import cron
from platypush.utils import run
@cron('* * * * * */30')
def check_bt_device(**context):
name = run('bluetooth.lookup_name').get('name')
if name:
# on_device_on logic here
else:
# on_device_off logic here
```
### The web interface
If
[`backend.http`](https://docs.platypush.tech/en/latest/platypush/backend/http.html)
is enabled then a web interface will be provided by default on
`http://host:8008/`. Besides using the `/execute` endpoint for running
requests, the built-in web server also provides a full-featured interface that
groups together the controls for most of the plugins - e.g. sensors, switches,
music controls and search, media library and torrent management, lights,
Zigbee/Z-Wave devices and so on. The UI is responsive and mobile-friendly.
The web service also provides means for the user to create [custom
dashboards](https://git.platypush.tech/platypush/platypush/-/blob/master/examples/conf/dashboard.xml)
that can be used to show information from multiple sources on a large screen.
- ...and much more (basically, anything that comes with a [Platypush plugin](https://docs.platypush.tech))
## Installation
@ -439,10 +122,10 @@ that can be used to show information from multiple sources on a large screen.
Platypush uses Redis to deliver and store requests and temporary messages:
```yaml
# Example for Debian-based distributions
# Example for Debian-based distributions
[sudo] apt-get install redis-server
# Enable and start the service
# Enable and start the service
[sudo] systemctl enable redis
[sudo] systemctl start redis
```
@ -479,7 +162,7 @@ or tags.
git clone https://git.platypush.tech/platypush/platypush.git
cd platypush
[sudo] pip install .
# Or
# Or
[sudo] python3 setup.py install
```
@ -493,7 +176,7 @@ ways to check the dependencies required by an extension:
All the extensions that require extra dependencies are listed in the
[`extras_require` section under
`setup.py`](https://git.platypush.tech/platypush/platypush/-/blob/master/setup.py#L72).
`setup.py`](https://git.platypush.tech/platypush/platypush/src/branch/master/setup.py#L84).
#### Install via `manifest.yaml`
@ -536,7 +219,7 @@ platypush
It's advised to run it as a systemd service though - simply copy the provided
[`.service`
file](https://git.platypush.tech/platypush/platypush/-/blob/master/examples/systemd/platypush.service)
file](https://git.platypush.tech/platypush/platypush/src/branch/master/examples/systemd/platypush.service)
to `~/.config/systemd/user`, check if the path of `platypush` matches the path
where it's installed on your system, and start the service via `systemctl`:
@ -617,6 +300,336 @@ directory in the same folder as the `config.yaml`.
[Wiki instructions](https://git.platypush.tech/platypush/platypush/wiki/Run-platypush-in-a-container)
## Architecture
The architecture of Platypush consists of a few simple pieces, orchestrated by
a configuration file stored by default under
[`~/.config/platypush/config.yaml`](https://git.platypush.tech/platypush/platypush/src/branch/master/examples/conf/config.yaml):
### Plugins
[Full list](https://docs.platypush.tech/en/latest/plugins.html)
Plugins are integrations that do things - like [modify
files](https://docs.platypush.tech/en/latest/platypush/plugins/file.html),
[train and evaluate machine learning
models](https://docs.platypush.tech/en/latest/platypush/plugins/tensorflow.html),
[control
cameras](https://docs.platypush.tech/en/latest/platypush/plugins/camera.pi.html),
[read
sensors](https://docs.platypush.tech/en/latest/platypush/plugins/gpio.sensor.dht.html),
[parse a web
page](https://docs.platypush.tech/en/latest/platypush/plugins/http.webpage.html),
[control
lights](https://docs.platypush.tech/en/latest/platypush/plugins/light.hue.html),
[send
emails](https://docs.platypush.tech/en/latest/platypush/plugins/mail.smtp.html),
[control
Chromecasts](https://docs.platypush.tech/en/latest/platypush/plugins/media.chromecast.html),
[run voice
queries](https://docs.platypush.tech/en/latest/platypush/plugins/assistant.google.html),
[handle torrent
transfers](https://docs.platypush.tech/en/latest/platypush/plugins/torrent.html)
or control
[Zigbee](https://docs.platypush.tech/en/latest/platypush/plugins/zigbee.mqtt.html)
or [Z-Wave](https://docs.platypush.tech/en/latest/platypush/plugins/zwave.html)
devices.
The configuration of a plugin matches one-on-one that of its documented class
constructor, so it's very straightforward to write a configuration for a plugin
by reading its documentation:
```yaml
light.hue:
# Groups that will be controlled by default
groups:
- Living Room
- Hall
```
### Actions
Plugins expose *actions*, that match one-on-one the plugin class methods
denoted by `@action`, so it's very straightforward to invoke plugin actions by
just reading the plugin documentation. They can be invoked directly from your
own scripts or they can be sent to the platform through any supported channel
as simple JSON messages:
```json
{
"type": "request",
"action": "light.hue.on",
"args": {
"lights": ["Entrance Bulb"]
}
}
```
### Backends
[Full list](https://docs.platypush.tech/en/latest/backends.html)
They are background services that listen for messages on channels (like
an [HTTP
backend](https://docs.platypush.tech/en/latest/platypush/backend/http.html), an
[MQTT
instance](https://docs.platypush.tech/en/latest/platypush/backend/mqtt.html), a
[Kafka
instance](https://docs.platypush.tech/en/latest/platypush/backend/kafka.html),
[Pushbullet](https://docs.platypush.tech/en/latest/platypush/backend/pushbullet.html)
etc.).
If a backend supports the execution of requests (e.g. HTTP, MQTT, Kafka,
Websocket and TCP) then you can send requests to these services in JSON format.
For example, in the case of the HTTP backend:
```shell
# Get a token
curl -XPOST -H 'Content-Type: application/json' -d '
{
"username": "$YOUR_USER",
"password": "$YOUR_PASSWORD"
}' http://host:8008/auth
# Execute a request
curl -XPOST -H 'Content-Type: application/json' \
-H "Authorization: Bearer $YOUR_TOKEN" -d '
{
"type": "request",
"action": "tts.say",
"args": {
"text": "This is a test"
}
}' http://host:8008/execute
```
### Events
[Full list](https://docs.platypush.tech/en/latest/events.html)
When a certain event occurs (e.g. a JSON request is received, or a [Bluetooth
device is
connected](https://docs.platypush.tech/en/latest/platypush/events/bluetooth.html#platypush.message.event.bluetooth.BluetoothDeviceConnectedEvent),
or a [Flic button is
pressed](https://docs.platypush.tech/en/latest/platypush/events/button.flic.html#platypush.message.event.button.flic.FlicButtonEvent),
or some [speech is detected on the voice assistant
service](https://docs.platypush.tech/en/latest/platypush/events/assistant.html#platypush.message.event.assistant.SpeechRecognizedEvent),
or an [RSS feed has new
items](https://docs.platypush.tech/en/latest/platypush/events/http.rss.html#platypush.message.event.http.rss.NewFeedEvent),
or a [new email is
received](https://docs.platypush.tech/en/latest/platypush/events/mail.html#platypush.message.event.mail.MailReceivedEvent),
or a [new track is
played](https://docs.platypush.tech/en/latest/platypush/events/music.html#platypush.message.event.music.NewPlayingTrackEvent),
or an [NFC tag is
detected](https://docs.platypush.tech/en/latest/platypush/events/nfc.html#platypush.message.event.nfc.NFCTagDetectedEvent),
or [new sensor data is
available](https://docs.platypush.tech/en/latest/platypush/events/sensor.html#platypush.message.event.sensor.SensorDataChangeEvent),
or [a value of a Zigbee device
changes](https://docs.platypush.tech/en/latest/platypush/events/zigbee.mqtt.html#platypush.message.event.zigbee.mqtt.ZigbeeMqttDevicePropertySetEvent),
etc.), the associated backend will trigger an
[event](https://docs.platypush.tech/en/latest/events.html).
### Hooks
Event hooks are custom pieces of logic that will be run when a certain event is
triggered. Hooks are the glue that connects events to actions, exposing a
paradigm similar to IFTTT (_if a certain event happens then run these
actions_). They can declared as:
- Sections of the [`config.yaml`](https://git.platypush.tech/platypush/platypush/src/branch/master/examples/conf/config.yaml).
Example:
```yaml
event.hook.SearchSongVoiceCommand:
if:
type: platypush.message.event.assistant.SpeechRecognizedEvent
phrase: "play ${title} by ${artist}"
then:
- action: music.mpd.clear
- action: music.mpd.search
args:
filter:
artist: ${artist}
title: ${title}
- if ${len(output)}:
- action: music.mpd.play
args:
resource: ${output[0]['file']}
```
- Stand-alone Python scripts stored under `~/.config/platypush/scripts` and
will be dynamically imported at start time.
[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.message.event.assistant import SpeechRecognizedEvent
@hook(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,
'title': title,
})
if results:
run('music.mpd.play', results[0]['file'])
```
### Procedures
Procedures are pieces of custom logic that can be executed as atomic actions
using `procedure.<name>` as an action name.
They can be defined either in the `config.yaml` or as Python scripts stored
under `~/.config/platypush/scripts` - provided that the procedure is also
imported in `~/.config/platypush/scripts/__init__.py` so it can be discovered
by the service.
YAML example for a procedure that can be executed when we arrive home and turns
on the lights if the luminosity is lower that a certain thresholds, says a
welcome home message using the TTS engine and starts playing the music:
```yaml
procedure.at_home:
# Get luminosity data from a sensor - e.g. LTR559
- action: gpio.sensor.ltr559.get_data
# If it's lower than a certain threshold, turn on the lights
- if ${int(light or 0) < 110}:
- action: light.hue.on
# Say a welcome home message
- action: tts.google.say
args:
text: Welcome home
# Play the music
- action: music.mpd.play
```
Python example:
```python
# Content of ~/.config/platypush/scripts/home.py
from platypush.procedure import procedure
from platypush.utils import run
@procedure
def at_home(**context):
sensor_data = run('gpio.sensor.ltr559.get_data')
if sensor_data['light'] < 110:
run('light.hue.on')
run('tts.google.say', text='Welcome home')
run('music.mpd.play')
```
In either case, you can easily trigger the at-home procedure by sending an
action request message to a backend - for example, over the HTTP backend:
```shell
curl -XPOST -H 'Content-Type: application/json' \
-H "Authorization: Bearer $YOUR_TOKEN" -d '
{
"type": "request",
"action": "procedure.at_home"
}' http://host:8008/execute
```
### Cronjobs
Cronjobs are pieces of logic that will be run at regular intervals, expressed
in crontab-compatible syntax. They can be defined either in the `config.yaml`
or as Python scripts stored under `~/.config/platypush/scripts` as functions
labelled by the `@cron` decorator.
Note that seconds are also supported (unlike the standard crontab definition),
but, for back-compatibility with the standard crontab format, they are at the
end of the cron expression, so the expression is actually in the format
`<minute> <hour> <day_of_month> <month> <day_of_week> <second>`.
YAML example for a cronjob that is executed every 30 seconds and checks if a
Bluetooth device is nearby:
```yaml
cron.check_bt_device:
cron_expression: '* * * * * */30'
actions:
- action: bluetooth.lookup_name
args:
addr: XX:XX:XX:XX:XX:XX
- if ${name}:
- action: procedure.on_device_on
- else:
- action: procedure.on_device_off
```
Python example:
```python
# Content of ~/.config/platypush/scripts/bt_cron.py
from platypush.cron import cron
from platypush.utils import run
@cron('* * * * * */30')
def check_bt_device(**context):
name = run('bluetooth.lookup_name').get('name')
if name:
# on_device_on logic here
else:
# on_device_off logic here
```
### Entities
Entities are a fundamental building block of Platypush. Most of the
integrations will store their state or connected devices in the form of
entities - e.g. the sensors detected by the Z-Wave/Zigbee/Bluetooth
integration, or the lights connected to a Hue bridge, or your cloud nodes, or
your custom Arduino/ESP machinery, and so on.
Entities provide a consistent interface to interact with your integrations
regardless of their type and the plugin that handles them. For instance, all
temperature sensors will expose the same interface, regardless if they are
Bluetooth or Zigbee sensors, and all the media plugins will expose the same
interface, regardless if they manage Chromecasts, Kodi, Plex, Jellyfin or a
local VLC player.
Once you enable the HTTP backend and a few integrations that export entities
and register a user, you can query the detected entities via:
```shell
curl -XPOST -H 'Content-Type: application/json' \
-H "Authorization: Bearer $YOUR_TOKEN" \
-d '{"type":"request", "action":"entities.get"}' \
http://localhost:8008/execute
```
All the entities expose the same interface and can be manipulated through the
same API. Also, when an entity is updated it always emits an
[`EntityUpdateEvent`](https://docs.platypush.tech/platypush/events/entities.html#platypush.message.event.entities.EntityUpdateEvent),
so you can easily create hooks that react to these events and act on multiple
types of entities.
### The web interface
If
[`backend.http`](https://docs.platypush.tech/en/latest/platypush/backend/http.html)
is enabled then a web interface will be provided by default on
`http://host:8008/`. Besides using the `/execute` endpoint for running
requests, the built-in web server also provides a full-featured interface that
groups together the controls for most of the plugins - e.g. sensors, switches,
music controls and search, media library and torrent management, lights,
Zigbee/Z-Wave devices and so on. The UI is responsive and mobile-friendly.
The web service also provides means for the user to create [custom
dashboards](https://git.platypush.tech/platypush/platypush/src/branch/master/examples/conf/dashboard.xml)
that can be used to show information from multiple sources on a large screen.
## Mobile app
An [official Android
@ -628,11 +641,7 @@ of Platypush to your fingertips.
## Tests
To run the tests simply run `pytest` either from the project root folder or the
`tests/` folder. Or run the following command from the project root folder:
```shell
python -m tests
```
`tests/` folder.
---

View File

@ -3,17 +3,13 @@ Backends
========
.. toctree::
:maxdepth: 2
:maxdepth: 1
:caption: Backends:
platypush/backend/adafruit.io.rst
platypush/backend/alarm.rst
platypush/backend/assistant.google.rst
platypush/backend/assistant.snowboy.rst
platypush/backend/bluetooth.fileserver.rst
platypush/backend/bluetooth.pushserver.rst
platypush/backend/bluetooth.scanner.rst
platypush/backend/bluetooth.scanner.ble.rst
platypush/backend/button.flic.rst
platypush/backend/camera.pi.rst
platypush/backend/chat.telegram.rst
@ -32,7 +28,6 @@ Backends
platypush/backend/joystick.linux.rst
platypush/backend/kafka.rst
platypush/backend/light.hue.rst
platypush/backend/linode.rst
platypush/backend/log.http.rst
platypush/backend/mail.rst
platypush/backend/midi.rst
@ -48,20 +43,8 @@ Backends
platypush/backend/pushbullet.rst
platypush/backend/redis.rst
platypush/backend/scard.rst
platypush/backend/sensor.accelerometer.rst
platypush/backend/sensor.arduino.rst
platypush/backend/sensor.battery.rst
platypush/backend/sensor.bme280.rst
platypush/backend/sensor.dht.rst
platypush/backend/sensor.distance.rst
platypush/backend/sensor.distance.vl53l1x.rst
platypush/backend/sensor.envirophat.rst
platypush/backend/sensor.ir.zeroborg.rst
platypush/backend/sensor.leap.rst
platypush/backend/sensor.ltr559.rst
platypush/backend/sensor.mcp3008.rst
platypush/backend/sensor.motion.pmw3901.rst
platypush/backend/sensor.serial.rst
platypush/backend/stt.deepspeech.rst
platypush/backend/stt.picovoice.hotword.rst
platypush/backend/stt.picovoice.speech.rst
@ -72,8 +55,6 @@ Backends
platypush/backend/weather.buienradar.rst
platypush/backend/weather.darksky.rst
platypush/backend/weather.openweathermap.rst
platypush/backend/websocket.rst
platypush/backend/wiimote.rst
platypush/backend/zigbee.mqtt.rst
platypush/backend/zwave.rst
platypush/backend/zwave.mqtt.rst

View File

@ -71,7 +71,7 @@ master_doc = 'index'
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
language = 'en'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
@ -103,6 +103,7 @@ html_domain_indices = True
html_theme_options = {
'toc_title': 'Platypush documentation',
'repository_url': 'https://git.platypush.tech/platypush/platypush',
'repository_provider': 'github',
'use_repository_button': True,
'use_issues_button': True,
'use_fullscreen_button': True,
@ -138,15 +139,12 @@ latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
@ -156,8 +154,7 @@ latex_elements = {
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'platypush.tex', 'platypush Documentation',
'BlackLight', 'manual'),
(master_doc, 'platypush.tex', 'platypush Documentation', 'BlackLight', 'manual'),
]
@ -165,10 +162,7 @@ latex_documents = [
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'platypush', 'platypush Documentation',
[author], 1)
]
man_pages = [(master_doc, 'platypush', 'platypush Documentation', [author], 1)]
# -- Options for Texinfo output ----------------------------------------------
@ -177,9 +171,15 @@ man_pages = [
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'platypush', 'platypush Documentation',
author, 'platypush', 'One line description of project.',
'Miscellaneous'),
(
master_doc,
'platypush',
'platypush Documentation',
author,
'platypush',
'A general-purpose platform for automation.',
'Miscellaneous',
),
]
@ -188,7 +188,7 @@ texinfo_documents = [
# -- Options for intersphinx extension ---------------------------------------
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {'https://docs.python.org/': None}
intersphinx_mapping = {'python': ('https://docs.python.org/3', None)}
# -- Options for todo extension ----------------------------------------------
@ -196,102 +196,117 @@ intersphinx_mapping = {'https://docs.python.org/': None}
todo_include_todos = True
autodoc_default_options = {
'inherited-members': True,
'members': True,
'show-inheritance': True,
}
autodoc_mock_imports = ['googlesamples.assistant.grpc.audio_helpers',
'google.assistant.embedded',
'google.assistant.library',
'google.assistant.library.event',
'google.assistant.library.file_helpers',
'google.oauth2.credentials',
'oauth2client',
'apiclient',
'tenacity',
'smartcard',
'Leap',
'oauth2client',
'rtmidi',
'bluetooth',
'gevent.wsgi',
'Adafruit_IO',
'pyperclip',
'pydbus',
'inputs',
'inotify',
'omxplayer',
'plexapi',
'cwiid',
'sounddevice',
'soundfile',
'numpy',
'cv2',
'nfc',
'ndef',
'bcrypt',
'google',
'feedparser',
'kafka',
'googlesamples',
'icalendar',
'httplib2',
'mpd',
'serial',
'pyHS100',
'grpc',
'envirophat',
'gps',
'picamera',
'pmw3901',
'PIL',
'croniter',
'pyaudio',
'avs',
'PyOBEX',
'todoist',
'trello',
'telegram',
'telegram.ext',
'pyfirmata2',
'cups',
'graphyte',
'cpuinfo',
'psutil',
'openzwave',
'deepspeech',
'wave',
'pvporcupine ',
'pvcheetah',
'pyotp',
'linode_api4',
'pyzbar',
'tensorflow',
'keras',
'pandas',
'samsungtvws',
'paramiko',
'luma',
'zeroconf',
'dbus',
'gi',
'gi.repository',
'twilio',
'Adafruit_Python_DHT',
'RPi.GPIO',
'RPLCD',
'imapclient',
'pysmartthings',
'aiohttp',
'watchdog',
'pyngrok',
'irc',
'irc.bot',
'irc.strings',
'irc.client',
'irc.connection',
'irc.events',
'defusedxml',
]
autodoc_mock_imports = [
'gunicorn',
'googlesamples.assistant.grpc.audio_helpers',
'google.assistant.embedded',
'google.assistant.library',
'google.assistant.library.event',
'google.assistant.library.file_helpers',
'google.oauth2.credentials',
'oauth2client',
'apiclient',
'tenacity',
'smartcard',
'Leap',
'oauth2client',
'rtmidi',
'bluetooth',
'gevent.wsgi',
'Adafruit_IO',
'pyclip',
'pydbus',
'inputs',
'inotify',
'omxplayer',
'plexapi',
'cwiid',
'sounddevice',
'soundfile',
'numpy',
'cv2',
'nfc',
'ndef',
'bcrypt',
'google',
'feedparser',
'kafka',
'googlesamples',
'icalendar',
'httplib2',
'mpd',
'serial',
'pyHS100',
'grpc',
'envirophat',
'gps',
'picamera',
'pmw3901',
'PIL',
'croniter',
'pyaudio',
'avs',
'PyOBEX',
'PyOBEX.client',
'todoist',
'trello',
'telegram',
'telegram.ext',
'pyfirmata2',
'cups',
'graphyte',
'cpuinfo',
'psutil',
'openzwave',
'deepspeech',
'wave',
'pvporcupine ',
'pvcheetah',
'pyotp',
'linode_api4',
'pyzbar',
'tensorflow',
'keras',
'pandas',
'samsungtvws',
'paramiko',
'luma',
'zeroconf',
'dbus',
'gi',
'gi.repository',
'twilio',
'Adafruit_Python_DHT',
'RPi.GPIO',
'RPLCD',
'imapclient',
'pysmartthings',
'aiohttp',
'watchdog',
'pyngrok',
'irc',
'irc.bot',
'irc.strings',
'irc.client',
'irc.connection',
'irc.events',
'defusedxml',
'nio',
'aiofiles',
'aiofiles.os',
'async_lru',
'bleak',
'bluetooth_numbers',
'TheengsDecoder',
'simple_websocket',
'uvicorn',
'websockets',
'docutils',
]
sys.path.insert(0, os.path.abspath('../..'))

View File

@ -3,7 +3,7 @@ Events
======
.. toctree::
:maxdepth: 2
:maxdepth: 1
:caption: Events:
platypush/events/adafruit.rst
@ -20,6 +20,7 @@ Events
platypush/events/custom.rst
platypush/events/dbus.rst
platypush/events/distance.rst
platypush/events/entities.rst
platypush/events/file.rst
platypush/events/foursquare.rst
platypush/events/geo.rst
@ -30,6 +31,7 @@ Events
platypush/events/gotify.rst
platypush/events/gpio.rst
platypush/events/gps.rst
platypush/events/hid.rst
platypush/events/http.rst
platypush/events/http.hook.rst
platypush/events/http.rss.rst
@ -41,11 +43,13 @@ Events
platypush/events/linode.rst
platypush/events/log.http.rst
platypush/events/mail.rst
platypush/events/matrix.rst
platypush/events/media.rst
platypush/events/midi.rst
platypush/events/mqtt.rst
platypush/events/music.rst
platypush/events/music.snapcast.rst
platypush/events/music.tidal.rst
platypush/events/nextcloud.rst
platypush/events/nfc.rst
platypush/events/ngrok.rst
@ -72,6 +76,7 @@ Events
platypush/events/weather.rst
platypush/events/web.rst
platypush/events/web.widget.rst
platypush/events/websocket.rst
platypush/events/wiimote.rst
platypush/events/zeroborg.rst
platypush/events/zeroconf.rst

View File

@ -16,7 +16,7 @@ For more information on Platypush check out:
.. _Blog articles: https://blog.platypush.tech
.. toctree::
:maxdepth: 3
:maxdepth: 2
:caption: Contents:
backends

View File

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

View File

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

View File

@ -1,5 +0,0 @@
``bluetooth.scanner.ble``
===========================================
.. automodule:: platypush.backend.bluetooth.scanner.ble
:members:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +0,0 @@
``sensor.mcp3008``
====================================
.. automodule:: platypush.backend.sensor.mcp3008
:members:

View File

@ -1,5 +0,0 @@
``sensor.motion.pmw3901``
=========================
.. automodule:: platypush.backend.sensor.motion.pmw3901
:members:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,4 +2,4 @@
==========================
.. automodule:: platypush.plugins.dbus
:members:
:exclude-members: DBusService, BusType

View File

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

View File

@ -1,6 +0,0 @@
``gpio.sensor.accelerometer``
===============================================
.. automodule:: platypush.plugins.gpio.sensor.accelerometer
:members:

View File

@ -1,6 +0,0 @@
``gpio.sensor.bme280``
========================================
.. automodule:: platypush.plugins.gpio.sensor.bme280
:members:

View File

@ -1,5 +0,0 @@
``gpio.sensor.dht``
=====================================
.. automodule:: platypush.plugins.gpio.sensor.dht
:members:

View File

@ -1,6 +0,0 @@
``gpio.sensor.distance``
==========================================
.. automodule:: platypush.plugins.gpio.sensor.distance
:members:

View File

@ -1,6 +0,0 @@
``gpio.sensor.distance.vl53l1x``
==================================================
.. automodule:: platypush.plugins.gpio.sensor.distance.vl53l1x
:members:

View File

@ -1,6 +0,0 @@
``gpio.sensor.envirophat``
============================================
.. automodule:: platypush.plugins.gpio.sensor.envirophat
:members:

View File

@ -1,6 +0,0 @@
``gpio.sensor.ltr559``
========================================
.. automodule:: platypush.plugins.gpio.sensor.ltr559
:members:

View File

@ -1,7 +0,0 @@
``gpio.sensor.mcp3008``
=========================================
.. automodule:: platypush.plugins.gpio.sensor.mcp3008
:members:

View File

@ -1,5 +0,0 @@
``gpio.sensor.motion.pmw3901``
==============================
.. automodule:: platypush.plugins.gpio.sensor.motion.pmw3901
:members:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
``sensor.distance.vl53l1x``
===========================
.. automodule:: platypush.plugins.sensor.distance.vl53l1x
:members:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@ Plugins
=======
.. toctree::
:maxdepth: 2
:maxdepth: 1
:caption: Plugins:
platypush/plugins/adafruit.io.rst
@ -14,7 +14,6 @@ Plugins
platypush/plugins/assistant.google.pushtotalk.rst
platypush/plugins/autoremote.rst
platypush/plugins/bluetooth.rst
platypush/plugins/bluetooth.ble.rst
platypush/plugins/calendar.rst
platypush/plugins/calendar.ical.rst
platypush/plugins/camera.android.ipcam.rst
@ -32,6 +31,7 @@ Plugins
platypush/plugins/db.rst
platypush/plugins/dbus.rst
platypush/plugins/dropbox.rst
platypush/plugins/entities.rst
platypush/plugins/esp.rst
platypush/plugins/ffmpeg.rst
platypush/plugins/file.rst
@ -46,17 +46,9 @@ Plugins
platypush/plugins/google.youtube.rst
platypush/plugins/gotify.rst
platypush/plugins/gpio.rst
platypush/plugins/gpio.sensor.accelerometer.rst
platypush/plugins/gpio.sensor.bme280.rst
platypush/plugins/gpio.sensor.dht.rst
platypush/plugins/gpio.sensor.distance.rst
platypush/plugins/gpio.sensor.distance.vl53l1x.rst
platypush/plugins/gpio.sensor.envirophat.rst
platypush/plugins/gpio.sensor.ltr559.rst
platypush/plugins/gpio.sensor.mcp3008.rst
platypush/plugins/gpio.sensor.motion.pmw3901.rst
platypush/plugins/gpio.zeroborg.rst
platypush/plugins/graphite.rst
platypush/plugins/hid.rst
platypush/plugins/http.request.rst
platypush/plugins/http.request.rss.rst
platypush/plugins/http.webpage.rst
@ -75,6 +67,7 @@ Plugins
platypush/plugins/mail.smtp.rst
platypush/plugins/mailgun.rst
platypush/plugins/mastodon.rst
platypush/plugins/matrix.rst
platypush/plugins/media.chromecast.rst
platypush/plugins/media.gstreamer.rst
platypush/plugins/media.jellyfin.rst
@ -93,6 +86,7 @@ Plugins
platypush/plugins/music.mpd.rst
platypush/plugins/music.snapcast.rst
platypush/plugins/music.spotify.rst
platypush/plugins/music.tidal.rst
platypush/plugins/nextcloud.rst
platypush/plugins/ngrok.rst
platypush/plugins/nmap.rst
@ -107,6 +101,15 @@ Plugins
platypush/plugins/redis.rst
platypush/plugins/rss.rst
platypush/plugins/rtorrent.rst
platypush/plugins/sensor.bme280.rst
platypush/plugins/sensor.dht.rst
platypush/plugins/sensor.distance.vl53l1x.rst
platypush/plugins/sensor.envirophat.rst
platypush/plugins/sensor.hcsr04.rst
platypush/plugins/sensor.lis3dh.rst
platypush/plugins/sensor.ltr559.rst
platypush/plugins/sensor.mcp3008.rst
platypush/plugins/sensor.pmw3901.rst
platypush/plugins/serial.rst
platypush/plugins/shell.rst
platypush/plugins/slack.rst
@ -120,7 +123,6 @@ Plugins
platypush/plugins/switch.tplink.rst
platypush/plugins/switch.wemo.rst
platypush/plugins/switchbot.rst
platypush/plugins/switchbot.bluetooth.rst
platypush/plugins/system.rst
platypush/plugins/tcp.rst
platypush/plugins/tensorflow.rst
@ -130,12 +132,14 @@ Plugins
platypush/plugins/trello.rst
platypush/plugins/tts.rst
platypush/plugins/tts.google.rst
platypush/plugins/tts.mimic3.rst
platypush/plugins/tv.samsung.ws.rst
platypush/plugins/twilio.rst
platypush/plugins/udp.rst
platypush/plugins/user.rst
platypush/plugins/utils.rst
platypush/plugins/variable.rst
platypush/plugins/wallabag.rst
platypush/plugins/weather.buienradar.rst
platypush/plugins/weather.darksky.rst
platypush/plugins/weather.openweathermap.rst

View File

@ -3,22 +3,19 @@ Responses
=========
.. toctree::
:maxdepth: 2
:maxdepth: 1
:caption: Responses:
platypush/responses/bluetooth.rst
platypush/responses/camera.rst
platypush/responses/camera.android.rst
platypush/responses/chat.telegram.rst
platypush/responses/google.drive.rst
platypush/responses/linode.rst
platypush/responses/pihole.rst
platypush/responses/ping.rst
platypush/responses/printer.cups.rst
platypush/responses/qrcode.rst
platypush/responses/ssh.rst
platypush/responses/stt.rst
platypush/responses/system.rst
platypush/responses/tensorflow.rst
platypush/responses/todoist.rst
platypush/responses/translate.rst

View File

@ -109,8 +109,6 @@ calendar:
backend.http:
# Listening port
port: 8008
# Websocket port
websocket_port: 8009
# Through resource_dirs you can specify external folders whose content can be accessed on
# the web server through a custom URL. In the case below we have a Dropbox folder containing
@ -165,10 +163,6 @@ backend.mqtt:
#backend.tcp:
# port: 3333
# Websocket backend. Install required dependencies through 'pip install "platypush[http]"'
#backend.websocket:
# port: 8765
## --
## Assistant configuration examples
## --

View File

@ -0,0 +1,38 @@
# An nginx configuration that can be used to reverse proxy connections to your
# Platypush' HTTP service.
server {
server_name my-platypush-host.domain.com;
# Proxy standard HTTP connections to your Platypush IP
location / {
proxy_pass http://my-platypush-host:8008/;
client_max_body_size 5M;
proxy_read_timeout 60;
proxy_connect_timeout 60;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Ssl on;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# Proxy websocket connections
location ~ ^/ws/(.*)$ {
proxy_pass http://10.0.0.2:8008/ws/$1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_redirect off;
proxy_http_version 1.1;
client_max_body_size 200M;
proxy_set_header Host $http_host;
}
# Optional SSL configuration - using Let's Encrypt certificates in this case
# listen 443 ssl;
# ssl_certificate /etc/letsencrypt/live/my-platypush-host.domain.com/fullchain.pem;
# ssl_certificate_key /etc/letsencrypt/live/my-platypush-host.domain.com/privkey.pem;
# include /etc/letsencrypt/options-ssl-nginx.conf;
# ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}

View File

@ -1,4 +1,5 @@
import os
from typing import Iterable, Optional
from platypush.backend import Backend
from platypush.context import get_plugin
@ -13,21 +14,11 @@ def _get_inspect_plugin():
def get_all_plugins():
manifests = {mf.component_name for mf in get_manifests(Plugin)}
return {
plugin_name: plugin_info
for plugin_name, plugin_info in _get_inspect_plugin().get_all_plugins().output.items()
if plugin_name in manifests
}
return sorted([mf.component_name for mf in get_manifests(Plugin)])
def get_all_backends():
manifests = {mf.component_name for mf in get_manifests(Backend)}
return {
backend_name: backend_info
for backend_name, backend_info in _get_inspect_plugin().get_all_backends().output.items()
if backend_name in manifests
}
return sorted([mf.component_name for mf in get_manifests(Backend)])
def get_all_events():
@ -38,142 +29,122 @@ def get_all_responses():
return _get_inspect_plugin().get_all_responses().output
# noinspection DuplicatedCode
def _generate_components_doc(
index_name: str,
package_name: str,
components: Iterable[str],
doc_dir: Optional[str] = None,
):
if not doc_dir:
doc_dir = index_name
index_file = os.path.join(
os.path.dirname(os.path.abspath(__file__)),
'docs',
'source',
f'{index_name}.rst',
)
docs_dir = os.path.join(
os.path.dirname(os.path.abspath(__file__)),
'docs',
'source',
'platypush',
doc_dir,
)
for comp in components:
comp_file = os.path.join(docs_dir, comp + '.rst')
if not os.path.exists(comp_file):
comp = f'platypush.{package_name}.{comp}'
header = '``' + '.'.join(comp.split('.')[2:]) + '``'
divider = '=' * len(header)
body = f'\n.. automodule:: {comp}\n :members:\n'
out = '\n'.join([header, divider, body])
with open(comp_file, 'w') as f:
f.write(out)
with open(index_file, 'w') as f:
f.write(
f'''
{index_name.title()}
{''.join(['='] * len(index_name))}
.. toctree::
:maxdepth: 1
:caption: {index_name.title()}:
'''
)
for comp in components:
f.write(f' platypush/{doc_dir}/{comp}.rst\n')
_cleanup_removed_components_docs(docs_dir, components)
def _cleanup_removed_components_docs(docs_dir: str, components: Iterable[str]):
new_components = set(components)
existing_files = {
os.path.join(root, file)
for root, _, files in os.walk(docs_dir)
for file in files
if file.endswith('.rst')
}
files_to_remove = {
file
for file in existing_files
if os.path.basename(file).removesuffix('.rst') not in new_components
}
for file in files_to_remove:
print(f'Removing unlinked component {file}')
os.unlink(file)
def generate_plugins_doc():
plugins_index = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'docs', 'source', 'plugins.rst')
plugins_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'docs', 'source', 'platypush', 'plugins')
all_plugins = sorted(plugin for plugin in get_all_plugins().keys())
for plugin in all_plugins:
plugin_file = os.path.join(plugins_dir, plugin + '.rst')
if not os.path.exists(plugin_file):
plugin = 'platypush.plugins.' + plugin
header = '``{}``'.format('.'.join(plugin.split('.')[2:]))
divider = '=' * len(header)
body = '\n.. automodule:: {}\n :members:\n'.format(plugin)
out = '\n'.join([header, divider, body])
with open(plugin_file, 'w') as f:
f.write(out)
with open(plugins_index, 'w') as f:
f.write('''
Plugins
=======
.. toctree::
:maxdepth: 2
:caption: Plugins:
''')
for plugin in all_plugins:
f.write(' platypush/plugins/' + plugin + '.rst\n')
_generate_components_doc(
index_name='plugins', package_name='plugins', components=get_all_plugins()
)
# noinspection DuplicatedCode
def generate_backends_doc():
backends_index = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'docs', 'source', 'backends.rst')
backends_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'docs', 'source', 'platypush', 'backend')
all_backends = sorted(backend for backend in get_all_backends().keys())
for backend in all_backends:
backend_file = os.path.join(backends_dir, backend + '.rst')
if not os.path.exists(backend_file):
backend = 'platypush.backend.' + backend
header = '``{}``'.format('.'.join(backend.split('.')[2:]))
divider = '=' * len(header)
body = '\n.. automodule:: {}\n :members:\n'.format(backend)
out = '\n'.join([header, divider, body])
with open(backend_file, 'w') as f:
f.write(out)
with open(backends_index, 'w') as f:
f.write('''
Backends
========
.. toctree::
:maxdepth: 2
:caption: Backends:
''')
for backend in all_backends:
f.write(' platypush/backend/' + backend + '.rst\n')
_generate_components_doc(
index_name='backends',
package_name='backend',
components=get_all_backends(),
doc_dir='backend',
)
# noinspection DuplicatedCode
def generate_events_doc():
from platypush.message import event as event_module
events_index = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'docs', 'source', 'events.rst')
events_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'docs', 'source', 'platypush', 'events')
all_events = sorted(event for event in get_all_events().keys() if event)
for event in all_events:
event_file = os.path.join(events_dir, event + '.rst')
if not os.path.exists(event_file):
header = '``{}``'.format(event)
divider = '=' * len(header)
body = '\n.. automodule:: {}.{}\n :members:\n'.format(event_module.__name__, event)
out = '\n'.join([header, divider, body])
with open(event_file, 'w') as f:
f.write(out)
with open(events_index, 'w') as f:
f.write('''
Events
======
.. toctree::
:maxdepth: 2
:caption: Events:
''')
for event in all_events:
f.write(' platypush/events/' + event + '.rst\n')
_generate_components_doc(
index_name='events',
package_name='message.event',
components=sorted(event for event in get_all_events().keys() if event),
)
# noinspection DuplicatedCode
def generate_responses_doc():
from platypush.message import response as response_module
responses_index = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'docs', 'source', 'responses.rst')
responses_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'docs', 'source', 'platypush', 'responses')
all_responses = sorted(response for response in get_all_responses().keys() if response)
for response in all_responses:
response_file = os.path.join(responses_dir, response + '.rst')
if not os.path.exists(response_file):
header = '``{}``'.format(response)
divider = '=' * len(header)
body = '\n.. automodule:: {}.{}\n :members:\n'.format(response_module.__name__, response)
out = '\n'.join([header, divider, body])
with open(response_file, 'w') as f:
f.write(out)
with open(responses_index, 'w') as f:
f.write('''
Responses
=========
.. toctree::
:maxdepth: 2
:caption: Responses:
''')
for response in all_responses:
f.write(' platypush/responses/' + response + '.rst\n')
_generate_components_doc(
index_name='responses',
package_name='message.response',
components=sorted(
response for response in get_all_responses().keys() if response
),
)
generate_plugins_doc()
generate_backends_doc()
generate_events_doc()
generate_responses_doc()
def main():
generate_plugins_doc()
generate_backends_doc()
generate_events_doc()
generate_responses_doc()
if __name__ == '__main__':
main()
# vim:sw=4:ts=4:et:

View File

@ -9,11 +9,13 @@ import argparse
import logging
import os
import sys
from typing import Optional
from .bus.redis import RedisBus
from .config import Config
from .context import register_backends, register_plugins
from .cron.scheduler import CronScheduler
from .entities import init_entities_engine, EntitiesEngine
from .event.processor import EventProcessor
from .logger import Logger
from .message.event import Event
@ -23,9 +25,9 @@ from .message.response import Response
from .utils import set_thread_name, get_enabled_plugins
__author__ = 'Fabio Manganiello <info@fabiomanganiello.com>'
__version__ = '0.23.3'
__version__ = '0.24.5'
logger = logging.getLogger('platypush')
log = logging.getLogger('platypush')
class Daemon:
@ -59,6 +61,7 @@ class Daemon:
no_capture_stdout=False,
no_capture_stderr=False,
redis_queue=None,
verbose=False,
):
"""
Constructor
@ -74,6 +77,7 @@ class Daemon:
no_capture_stderr -- Set to true if you want to disable the stderr
capture by the logging system
redis_queue -- Name of the (Redis) queue used for dispatching messages (default: platypush/bus).
verbose -- Enable debug/verbose logging, overriding the stored configuration (default: False).
"""
if pidfile:
@ -84,7 +88,10 @@ class Daemon:
self.redis_queue = redis_queue or self._default_redis_queue
self.config_file = config_file
Config.init(self.config_file)
logging.basicConfig(**Config.get('logging'))
logging_conf = Config.get('logging') or {}
if verbose:
logging_conf['level'] = logging.DEBUG
logging.basicConfig(**logging_conf)
redis_conf = Config.get('backend.redis') or {}
self.bus = RedisBus(
@ -96,6 +103,7 @@ class Daemon:
self.no_capture_stdout = no_capture_stdout
self.no_capture_stderr = no_capture_stderr
self.event_processor = EventProcessor()
self.entities_engine: Optional[EntitiesEngine] = None
self.requests_to_process = requests_to_process
self.processed_requests = 0
self.cron_scheduler = None
@ -116,6 +124,21 @@ class Daemon:
default=None,
help=cls.config_file.__doc__,
)
parser.add_argument(
'--version',
dest='version',
required=False,
action='store_true',
help="Print the current version and exit",
)
parser.add_argument(
'--verbose',
'-v',
dest='verbose',
required=False,
action='store_true',
help="Enable verbose/debug logging",
)
parser.add_argument(
'--pidfile',
'-P',
@ -154,12 +177,18 @@ class Daemon:
)
opts, args = parser.parse_known_args(args)
if opts.version:
print(__version__)
sys.exit(0)
return cls(
config_file=opts.config,
pidfile=opts.pidfile,
no_capture_stdout=opts.no_capture_stdout,
no_capture_stderr=opts.no_capture_stderr,
redis_queue=opts.redis_queue,
verbose=opts.verbose,
)
def on_message(self):
@ -178,7 +207,7 @@ class Daemon:
try:
msg.execute(n_tries=self.n_tries)
except PermissionError:
logger.info('Dropped unauthorized request: {}'.format(msg))
log.info('Dropped unauthorized request: {}'.format(msg))
self.processed_requests += 1
if (
@ -187,10 +216,9 @@ class Daemon:
):
self.stop_app()
elif isinstance(msg, Response):
logger.info('Received response: {}'.format(msg))
msg.log()
elif isinstance(msg, Event):
if not msg.disable_logging:
logger.info('Received event: {}'.format(msg))
msg.log()
self.event_processor.process_event(msg)
return _f
@ -199,26 +227,35 @@ class Daemon:
"""Stops the backends and the bus"""
from .plugins import RunnablePlugin
for backend in self.backends.values():
backend.stop()
if self.backends:
for backend in self.backends.values():
backend.stop()
for plugin in get_enabled_plugins().values():
if isinstance(plugin, RunnablePlugin):
plugin.stop()
self.bus.stop()
if self.bus:
self.bus.stop()
self.bus = None
if self.cron_scheduler:
self.cron_scheduler.stop()
self.cron_scheduler = None
if self.entities_engine:
self.entities_engine.stop()
self.entities_engine = None
def run(self):
"""Start the daemon"""
if not self.no_capture_stdout:
sys.stdout = Logger(logger.info)
sys.stdout = Logger(log.info)
if not self.no_capture_stderr:
sys.stderr = Logger(logger.warning)
sys.stderr = Logger(log.warning)
set_thread_name('platypush')
logger.info('---- Starting platypush v.{}'.format(__version__))
log.info('---- Starting platypush v.{}'.format(__version__))
# Initialize the backends and link them to the bus
self.backends = register_backends(bus=self.bus, global_scope=True)
@ -230,18 +267,22 @@ class Daemon:
# Initialize the plugins
register_plugins(bus=self.bus)
# Initialize the entities engine
self.entities_engine = init_entities_engine()
# Start the cron scheduler
if Config.get_cronjobs():
self.cron_scheduler = CronScheduler(jobs=Config.get_cronjobs())
self.cron_scheduler.start()
assert self.bus, 'The bus is not running'
self.bus.post(ApplicationStartedEvent())
# Poll for messages on the bus
try:
self.bus.poll()
except KeyboardInterrupt:
logger.info('SIGINT received, terminating application')
log.info('SIGINT received, terminating application')
finally:
self.stop_app()

View File

@ -8,11 +8,22 @@ import os
import time
from platypush.backend.assistant import AssistantBackend
from platypush.message.event.assistant import \
ConversationStartEvent, ConversationEndEvent, ConversationTimeoutEvent, \
ResponseEvent, NoResponseEvent, SpeechRecognizedEvent, AlarmStartedEvent, \
AlarmEndEvent, TimerStartedEvent, TimerEndEvent, AlertStartedEvent, \
AlertEndEvent, MicMutedEvent, MicUnmutedEvent
from platypush.message.event.assistant import (
ConversationStartEvent,
ConversationEndEvent,
ConversationTimeoutEvent,
ResponseEvent,
NoResponseEvent,
SpeechRecognizedEvent,
AlarmStartedEvent,
AlarmEndEvent,
TimerStartedEvent,
TimerEndEvent,
AlertStartedEvent,
AlertEndEvent,
MicMutedEvent,
MicUnmutedEvent,
)
class AssistantGoogleBackend(AssistantBackend):
@ -57,22 +68,30 @@ class AssistantGoogleBackend(AssistantBackend):
* **google-assistant-library** (``pip install google-assistant-library``)
* **google-assistant-sdk[samples]** (``pip install google-assistant-sdk[samples]``)
* **google-auth** (``pip install google-auth``)
"""
def __init__(self,
credentials_file=os.path.join(
os.path.expanduser('~/.config'),
'google-oauthlib-tool', 'credentials.json'),
device_model_id='Platypush', **kwargs):
_default_credentials_file = os.path.join(
os.path.expanduser('~/.config'), 'google-oauthlib-tool', 'credentials.json'
)
def __init__(
self,
credentials_file=_default_credentials_file,
device_model_id='Platypush',
**kwargs
):
"""
:param credentials_file: Path to the Google OAuth credentials file \
(default: ~/.config/google-oauthlib-tool/credentials.json). \
See https://developers.google.com/assistant/sdk/guides/library/python/embed/install-sample#generate_credentials \
:param credentials_file: Path to the Google OAuth credentials file
(default: ~/.config/google-oauthlib-tool/credentials.json).
See
https://developers.google.com/assistant/sdk/guides/library/python/embed/install-sample#generate_credentials
for instructions to get your own credentials file.
:type credentials_file: str
:param device_model_id: Device model ID to use for the assistant \
:param device_model_id: Device model ID to use for the assistant
(default: Platypush)
:type device_model_id: str
"""
@ -102,17 +121,23 @@ class AssistantGoogleBackend(AssistantBackend):
self.bus.post(ConversationTimeoutEvent(assistant=self))
elif event.type == EventType.ON_NO_RESPONSE:
self.bus.post(NoResponseEvent(assistant=self))
elif hasattr(EventType, 'ON_RENDER_RESPONSE') and \
event.type == EventType.ON_RENDER_RESPONSE:
self.bus.post(ResponseEvent(assistant=self, response_text=event.args.get('text')))
elif (
hasattr(EventType, 'ON_RENDER_RESPONSE')
and event.type == EventType.ON_RENDER_RESPONSE
):
self.bus.post(
ResponseEvent(assistant=self, response_text=event.args.get('text'))
)
tts, args = self._get_tts_plugin()
if tts and 'text' in event.args:
self.stop_conversation()
tts.say(text=event.args['text'], **args)
elif hasattr(EventType, 'ON_RESPONDING_STARTED') and \
event.type == EventType.ON_RESPONDING_STARTED and \
event.args.get('is_error_response', False) is True:
elif (
hasattr(EventType, 'ON_RESPONDING_STARTED')
and event.type == EventType.ON_RESPONDING_STARTED
and event.args.get('is_error_response', False) is True
):
self.logger.warning('Assistant response error')
elif event.type == EventType.ON_RECOGNIZING_SPEECH_FINISHED:
phrase = event.args['text'].lower().strip()
@ -144,12 +169,12 @@ class AssistantGoogleBackend(AssistantBackend):
self.bus.post(event)
def start_conversation(self):
""" Starts an assistant conversation """
"""Starts an assistant conversation"""
if self.assistant:
self.assistant.start_conversation()
def stop_conversation(self):
""" Stops an assistant conversation """
"""Stops an assistant conversation"""
if self.assistant:
self.assistant.stop_conversation()
@ -177,7 +202,9 @@ class AssistantGoogleBackend(AssistantBackend):
super().run()
with open(self.credentials_file, 'r') as f:
self.credentials = google.oauth2.credentials.Credentials(token=None, **json.load(f))
self.credentials = google.oauth2.credentials.Credentials(
token=None, **json.load(f)
)
while not self.should_stop():
self._has_error = False
@ -186,12 +213,16 @@ class AssistantGoogleBackend(AssistantBackend):
self.assistant = assistant
for event in assistant.start():
if not self.is_detecting():
self.logger.info('Assistant event received but detection is currently paused')
self.logger.info(
'Assistant event received but detection is currently paused'
)
continue
self._process_event(event)
if self._has_error:
self.logger.info('Restarting the assistant after an unrecoverable error')
self.logger.info(
'Restarting the assistant after an unrecoverable error'
)
time.sleep(5)
break

View File

@ -22,5 +22,6 @@ manifest:
pip:
- google-assistant-library
- google-assistant-sdk[samples]
- google-auth
package: platypush.backend.assistant.google
type: backend

View File

@ -1,99 +0,0 @@
import os
import time
# noinspection PyPackageRequirements
from PyOBEX import headers, requests, responses
# noinspection PyPackageRequirements
from PyOBEX.server import Server
from platypush.backend import Backend
from platypush.message.event.bluetooth import BluetoothDeviceConnectedEvent, BluetoothFileReceivedEvent, \
BluetoothDeviceDisconnectedEvent, BluetoothFilePutRequestEvent
class BluetoothBackend(Backend, Server):
_sleep_on_error = 10.0
def __init__(self, address: str = '', port: int = None, directory: str = None, whitelisted_addresses=None,
**kwargs):
Backend.__init__(self, **kwargs)
Server.__init__(self, address=address)
self.port = port
self.directory = os.path.join(os.path.expanduser(directory))
self.whitelisted_addresses = whitelisted_addresses or []
self._sock = None
def run(self):
self.logger.info('Starting bluetooth service [address={}] [port={}]'.format(
self.address, self.port))
while not self.should_stop():
try:
# noinspection PyArgumentList
self._sock = self.start_service(self.port)
self.serve(self._sock)
except Exception as e:
self.logger.error('Error on bluetooth connection [address={}] [port={}]: {}'.format(
self.address, self.port, str(e)))
time.sleep(self._sleep_on_error)
finally:
self.stop()
def stop(self):
if self._sock:
self.stop_service(self._sock)
self._sock = None
def put(self, socket, request):
name = ""
body = ""
while True:
for header in request.header_data:
if isinstance(header, headers.Name):
name = header.decode()
self.logger.info("Receiving {}".format(name))
elif isinstance(header, headers.Length):
length = header.decode()
self.logger.info("Content length: {} bytes".format(length))
elif isinstance(header, headers.Body):
body += header.decode()
elif isinstance(header, headers.End_Of_Body):
body += header.decode()
if request.is_final():
break
# Ask for more data.
Server.send_response(self, socket, responses.Continue())
# Get the next part of the data.
request = self.request_handler.decode(socket)
Server.send_response(self, socket, responses.Success())
name = os.path.basename(name.strip("\x00"))
path = os.path.join(self.directory, name)
self.logger.info("Writing file {}" .format(path))
open(path, "wb").write(body.encode())
self.bus.post(BluetoothFileReceivedEvent(path=path))
def process_request(self, connection, request, *address):
if isinstance(request, requests.Connect):
self.connect(connection, request)
self.bus.post(BluetoothDeviceConnectedEvent(address=address[0], port=address[1]))
elif isinstance(request, requests.Disconnect):
self.disconnect(connection, request)
self.bus.post(BluetoothDeviceDisconnectedEvent(address=address[0], port=address[1]))
elif isinstance(request, requests.Put):
self.bus.post(BluetoothFilePutRequestEvent(address=address[0], port=address[1]))
self.put(connection, request)
else:
self._reject(connection)
self.bus.post(BluetoothFilePutRequestEvent(address=address[0], port=address[1]))
def accept_connection(self, address, port):
return address in self.whitelisted_addresses
# vim:sw=4:ts=4:et:

View File

@ -1,92 +0,0 @@
import os
import stat
# noinspection PyPackageRequirements
from PyOBEX import requests, responses, headers
# noinspection PyPackageRequirements
from PyOBEX.server import BrowserServer
from platypush.backend.bluetooth import BluetoothBackend
from platypush.message.event.bluetooth import BluetoothFileGetRequestEvent
class BluetoothFileserverBackend(BluetoothBackend, BrowserServer):
"""
Bluetooth OBEX file server.
Enable it to allow bluetooth devices to browse files on this machine.
If you run platypush as a non-root user (and you should) then you to change the group owner of the
service discovery protocol file (/var/run/sdp) and add your user to that group. See
`here <https://stackoverflow.com/questions/34599703/rfcomm-bluetooth-permission-denied-error-raspberry-pi>`_
for details.
Requires:
* **pybluez** (``pip install pybluez``)
* **pyobex** (``pip install git+https://github.com/BlackLight/PyOBEX``)
"""
def __init__(self, port: int, address: str = '', directory: str = os.path.expanduser('~'),
whitelisted_addresses: list = None, **kwargs):
"""
:param port: Bluetooth listen port
:param address: Bluetooth address to bind the server to (default: any)
:param directory: Directory to share (default: HOME directory)
:param whitelisted_addresses: If set then only accept connections from the listed device addresses
"""
BluetoothBackend.__init__(self, address=address, port=port, directory=directory,
whitelisted_addresses=whitelisted_addresses, **kwargs)
if not os.path.isdir(self.directory):
raise FileNotFoundError(self.directory)
def process_request(self, socket, request, *address):
if isinstance(request, requests.Get):
self.bus.post(BluetoothFileGetRequestEvent(address=address[0], port=address[1]))
self.get(socket, request)
else:
super().process_request(socket, request, *address)
def get(self, socket, request):
name = ""
req_type = ""
for header in request.header_data:
if isinstance(header, headers.Name):
name = header.decode().strip("\x00")
self.logger.info("Receiving request for {}".format(name))
elif isinstance(header, headers.Type):
req_type = header.decode().strip("\x00")
self.logger.info("Request type: {}".format(req_type))
path = os.path.abspath(os.path.join(self.directory, name))
if os.path.isdir(path) or req_type == "x-obex/folder-listing":
if path.startswith(self.directory):
filelist = os.listdir(path)
s = '<?xml version="1.0"?>\n<folder-listing>\n'
for i in filelist:
objpath = os.path.join(path, i)
if os.path.isdir(objpath):
s += ' <folder name="{}" created="{}" />'.format(i, os.stat(objpath)[stat.ST_CTIME])
else:
s += ' <file name="{}" created="{}" size="{}" />'.format(
i, os.stat(objpath)[stat.ST_CTIME], os.stat(objpath)[stat.ST_SIZE])
s += "</folder-listing>\n"
self.logger.debug('Bluetooth get XML output:\n' + s)
response = responses.Success()
response_headers = [headers.Name(name.encode("utf8")),
headers.Length(len(s)),
headers.Body(s.encode("utf8"))]
BrowserServer.send_response(self, socket, response, response_headers)
else:
self._reject(socket)
else:
self._reject(socket)
# vim:sw=4:ts=4:et:

View File

@ -1,8 +0,0 @@
manifest:
events: {}
install:
pip:
- pybluez
- pyobex
package: platypush.backend.bluetooth.fileserver
type: backend

View File

@ -1,47 +0,0 @@
import os
# noinspection PyPackageRequirements
from PyOBEX.server import PushServer
from platypush.backend.bluetooth import BluetoothBackend
class BluetoothPushserverBackend(BluetoothBackend, PushServer):
"""
Bluetooth OBEX push server.
Enable it to allow bluetooth file transfers from other devices.
If you run platypush as a non-root user (and you should) then you to change the group owner of the
service discovery protocol file (/var/run/sdp) and add your user to that group. See
`here <https://stackoverflow.com/questions/34599703/rfcomm-bluetooth-permission-denied-error-raspberry-pi>`_
for details.
Requires:
* **pybluez** (``pip install pybluez``)
* **pyobex** (``pip install git+https://github.com/BlackLight/PyOBEX``)
"""
_sleep_on_error = 10.0
def __init__(self, port: int, address: str = '',
directory: str = os.path.join(os.path.expanduser('~'), 'bluetooth'),
whitelisted_addresses: list = None, **kwargs):
"""
:param port: Bluetooth listen port
:param address: Bluetooth address to bind the server to (default: any)
:param directory: Destination directory where files will be downloaded (default: ~/bluetooth)
:param whitelisted_addresses: If set then only accept connections from the listed device addresses
"""
BluetoothBackend.__init__(self, address=address, port=port, directory=directory,
whitelisted_addresses=whitelisted_addresses, **kwargs)
def run(self):
if not os.path.isdir(self.directory):
os.makedirs(self.directory, exist_ok=True)
super().run()
# vim:sw=4:ts=4:et:

View File

@ -1,8 +0,0 @@
manifest:
events: {}
install:
pip:
- pybluez
- pyobex
package: platypush.backend.bluetooth.pushserver
type: backend

View File

@ -1,109 +0,0 @@
import time
from threading import Thread, RLock
from typing import Dict, Optional, List
from platypush.backend.sensor import SensorBackend
from platypush.context import get_plugin
from platypush.message.event.bluetooth import BluetoothDeviceFoundEvent, BluetoothDeviceLostEvent
class BluetoothScannerBackend(SensorBackend):
"""
This backend periodically scans for available bluetooth devices and returns events when a devices enter or exits
the range.
Triggers:
* :class:`platypush.message.event.bluetooth.BluetoothDeviceFoundEvent` when a new bluetooth device is found.
* :class:`platypush.message.event.bluetooth.BluetoothDeviceLostEvent` when a bluetooth device is lost.
Requires:
* The :class:`platypush.plugins.bluetooth.BluetoothPlugin` plugin working.
"""
def __init__(self, device_id: Optional[int] = None, scan_duration: int = 10,
track_devices: Optional[List[str]] = None, **kwargs):
"""
:param device_id: Bluetooth adapter ID to use (default configured on the ``bluetooth`` plugin if None).
:param scan_duration: How long the scan should run (default: 10 seconds).
:param track_devices: List of addresses of devices to actively track, even if they aren't discoverable.
"""
super().__init__(plugin='bluetooth', plugin_args={
'device_id': device_id,
'duration': scan_duration,
}, **kwargs)
self._last_seen_devices = {}
self._tracking_thread: Optional[Thread] = None
self._bt_lock = RLock()
self.track_devices = set(track_devices or [])
self.scan_duration = scan_duration
def _add_last_seen_device(self, dev):
addr = dev.pop('addr')
if addr not in self._last_seen_devices:
self.bus.post(BluetoothDeviceFoundEvent(address=addr, **dev))
self._last_seen_devices[addr] = {'addr': addr, **dev}
def _remove_last_seen_device(self, addr: str):
dev = self._last_seen_devices.get(addr)
if not dev:
return
self.bus.post(BluetoothDeviceLostEvent(address=addr, **dev))
del self._last_seen_devices[addr]
def _addr_tracker(self, addr):
with self._bt_lock:
name = get_plugin('bluetooth').lookup_name(addr, timeout=self.scan_duration).name
if name is None:
self._remove_last_seen_device(addr)
else:
self._add_last_seen_device({'addr': addr, 'name': name})
def _bt_tracker(self):
self.logger.info('Starting Bluetooth tracker')
while not self.should_stop():
trackers = []
for addr in self.track_devices:
tracker = Thread(target=self._addr_tracker, args=(addr,))
tracker.start()
trackers.append(tracker)
for tracker in trackers:
tracker.join(timeout=self.scan_duration)
time.sleep(self.scan_duration)
self.logger.info('Bluetooth tracker stopped')
def get_measurement(self):
with self._bt_lock:
return super().get_measurement()
def process_data( # lgtm [py/inheritance/signature-mismatch]
self, data: Dict[str, dict], new_data: Optional[Dict[str, dict]] = None, **_
):
for addr, dev in data.items():
self._add_last_seen_device(dev)
for addr, dev in self._last_seen_devices.copy().items():
if addr not in data and addr not in self.track_devices:
self._remove_last_seen_device(addr)
def run(self):
self._tracking_thread = Thread(target=self._bt_tracker)
self._tracking_thread.start()
super().run()
def on_stop(self):
super().on_stop()
if self._tracking_thread and self._tracking_thread.is_alive():
self.logger.info('Waiting for the Bluetooth tracking thread to stop')
self._tracking_thread.join(timeout=self.scan_duration)
# vim:sw=4:ts=4:et:

View File

@ -1,33 +0,0 @@
from typing import Optional
from platypush.backend.bluetooth.scanner import BluetoothScannerBackend
class BluetoothBleScannerBackend(BluetoothScannerBackend):
"""
This backend periodically scans for available bluetooth low-energy devices and returns events when a devices enter
or exits the range.
Triggers:
* :class:`platypush.message.event.bluetooth.BluetoothDeviceFoundEvent` when a new bluetooth device is found.
* :class:`platypush.message.event.bluetooth.BluetoothDeviceLostEvent` when a bluetooth device is lost.
Requires:
* The :class:`platypush.plugins.bluetooth.BluetoothBlePlugin` plugin working.
"""
def __init__(self, interface: Optional[int] = None, scan_duration: int = 10, **kwargs):
"""
:param interface: Bluetooth adapter name to use (default configured on the ``bluetooth.ble`` plugin if None).
:param scan_duration: How long the scan should run (default: 10 seconds).
"""
super().__init__(plugin='bluetooth.ble', plugin_args={
'interface': interface,
'duration': scan_duration,
}, **kwargs)
# vim:sw=4:ts=4:et:

View File

@ -1,10 +0,0 @@
manifest:
events:
platypush.message.event.bluetooth.BluetoothDeviceFoundEvent: when a new bluetooth
device is found.
platypush.message.event.bluetooth.BluetoothDeviceLostEvent: when a bluetooth device
is lost.
install:
pip: []
package: platypush.backend.bluetooth.scanner.ble
type: backend

View File

@ -1,10 +0,0 @@
manifest:
events:
platypush.message.event.bluetooth.BluetoothDeviceFoundEvent: when a new bluetooth
device is found.
platypush.message.event.bluetooth.BluetoothDeviceLostEvent: when a bluetooth device
is lost.
install:
pip: []
package: platypush.backend.bluetooth.scanner
type: backend

View File

@ -3,5 +3,7 @@ manifest:
install:
pip:
- picamera
- numpy
- Pillow
package: platypush.backend.camera.pi
type: backend

View File

@ -4,9 +4,9 @@ from typing import Optional, Union, List, Dict, Any
from sqlalchemy import create_engine, Column, Integer, String, DateTime
from sqlalchemy.orm import sessionmaker, scoped_session
from sqlalchemy.ext.declarative import declarative_base
from platypush.backend import Backend
from platypush.common.db import declarative_base
from platypush.config import Config
from platypush.context import get_plugin
from platypush.message.event.covid19 import Covid19UpdateEvent
@ -17,10 +17,10 @@ Session = scoped_session(sessionmaker())
class Covid19Update(Base):
""" Models the Covid19Data table """
"""Models the Covid19Data table"""
__tablename__ = 'covid19data'
__table_args__ = ({'sqlite_autoincrement': True})
__table_args__ = {'sqlite_autoincrement': True}
country = Column(String, primary_key=True)
confirmed = Column(Integer, nullable=False, default=0)
@ -40,7 +40,12 @@ class Covid19Backend(Backend):
"""
# noinspection PyProtectedMember
def __init__(self, country: Optional[Union[str, List[str]]], poll_seconds: Optional[float] = 3600.0, **kwargs):
def __init__(
self,
country: Optional[Union[str, List[str]]],
poll_seconds: Optional[float] = 3600.0,
**kwargs
):
"""
:param country: Default country (or list of countries) to retrieve the stats for. It can either be the full
country name or the country code. Special values:
@ -56,7 +61,9 @@ class Covid19Backend(Backend):
super().__init__(poll_seconds=poll_seconds, **kwargs)
self._plugin: Covid19Plugin = get_plugin('covid19')
self.country: List[str] = self._plugin._get_countries(country)
self.workdir = os.path.join(os.path.expanduser(Config.get('workdir')), 'covid19')
self.workdir = os.path.join(
os.path.expanduser(Config.get('workdir')), 'covid19'
)
self.dbfile = os.path.join(self.workdir, 'data.db')
os.makedirs(self.workdir, exist_ok=True)
@ -67,22 +74,30 @@ class Covid19Backend(Backend):
self.logger.info('Stopped Covid19 backend')
def _process_update(self, summary: Dict[str, Any], session: Session):
update_time = datetime.datetime.fromisoformat(summary['Date'].replace('Z', '+00:00'))
update_time = datetime.datetime.fromisoformat(
summary['Date'].replace('Z', '+00:00')
)
self.bus.post(Covid19UpdateEvent(
country=summary['Country'],
country_code=summary['CountryCode'],
confirmed=summary['TotalConfirmed'],
deaths=summary['TotalDeaths'],
recovered=summary['TotalRecovered'],
update_time=update_time,
))
self.bus.post(
Covid19UpdateEvent(
country=summary['Country'],
country_code=summary['CountryCode'],
confirmed=summary['TotalConfirmed'],
deaths=summary['TotalDeaths'],
recovered=summary['TotalRecovered'],
update_time=update_time,
)
)
session.merge(Covid19Update(country=summary['CountryCode'],
confirmed=summary['TotalConfirmed'],
deaths=summary['TotalDeaths'],
recovered=summary['TotalRecovered'],
last_updated_at=update_time))
session.merge(
Covid19Update(
country=summary['CountryCode'],
confirmed=summary['TotalConfirmed'],
deaths=summary['TotalDeaths'],
recovered=summary['TotalRecovered'],
last_updated_at=update_time,
)
)
def loop(self):
# noinspection PyUnresolvedReferences
@ -90,23 +105,30 @@ class Covid19Backend(Backend):
if not summaries:
return
engine = create_engine('sqlite:///{}'.format(self.dbfile), connect_args={'check_same_thread': False})
engine = create_engine(
'sqlite:///{}'.format(self.dbfile),
connect_args={'check_same_thread': False},
)
Base.metadata.create_all(engine)
Session.configure(bind=engine)
session = Session()
last_records = {
record.country: record
for record in session.query(Covid19Update).filter(Covid19Update.country.in_(self.country)).all()
for record in session.query(Covid19Update)
.filter(Covid19Update.country.in_(self.country))
.all()
}
for summary in summaries:
country = summary['CountryCode']
last_record = last_records.get(country)
if not last_record or \
summary['TotalConfirmed'] != last_record.confirmed or \
summary['TotalDeaths'] != last_record.deaths or \
summary['TotalRecovered'] != last_record.recovered:
if (
not last_record
or summary['TotalConfirmed'] != last_record.confirmed
or summary['TotalDeaths'] != last_record.deaths
or summary['TotalRecovered'] != last_record.recovered
):
self._process_update(summary=summary, session=session)
session.commit()

View File

@ -6,15 +6,29 @@ from typing import Optional, List
import requests
from sqlalchemy import create_engine, Column, String, DateTime
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, scoped_session
from platypush.backend import Backend
from platypush.common.db import declarative_base
from platypush.config import Config
from platypush.message.event.github import GithubPushEvent, GithubCommitCommentEvent, GithubCreateEvent, \
GithubDeleteEvent, GithubEvent, GithubForkEvent, GithubWikiEvent, GithubIssueCommentEvent, GithubIssueEvent, \
GithubMemberEvent, GithubPublicEvent, GithubPullRequestEvent, GithubPullRequestReviewCommentEvent, \
GithubReleaseEvent, GithubSponsorshipEvent, GithubWatchEvent
from platypush.message.event.github import (
GithubPushEvent,
GithubCommitCommentEvent,
GithubCreateEvent,
GithubDeleteEvent,
GithubEvent,
GithubForkEvent,
GithubWikiEvent,
GithubIssueCommentEvent,
GithubIssueEvent,
GithubMemberEvent,
GithubPublicEvent,
GithubPullRequestEvent,
GithubPullRequestReviewCommentEvent,
GithubReleaseEvent,
GithubSponsorshipEvent,
GithubWatchEvent,
)
Base = declarative_base()
Session = scoped_session(sessionmaker())
@ -71,8 +85,17 @@ class GithubBackend(Backend):
_base_url = 'https://api.github.com'
def __init__(self, user: str, user_token: str, repos: Optional[List[str]] = None, org: Optional[str] = None,
poll_seconds: int = 60, max_events_per_scan: Optional[int] = 10, *args, **kwargs):
def __init__(
self,
user: str,
user_token: str,
repos: Optional[List[str]] = None,
org: Optional[str] = None,
poll_seconds: int = 60,
max_events_per_scan: Optional[int] = 10,
*args,
**kwargs
):
"""
If neither ``repos`` nor ``org`` is specified then the backend will monitor all new events on user level.
@ -102,17 +125,23 @@ class GithubBackend(Backend):
def _request(self, uri: str, method: str = 'get') -> dict:
method = getattr(requests, method.lower())
return method(self._base_url + uri, auth=(self.user, self.user_token),
headers={'Accept': 'application/vnd.github.v3+json'}).json()
return method(
self._base_url + uri,
auth=(self.user, self.user_token),
headers={'Accept': 'application/vnd.github.v3+json'},
).json()
def _init_db(self):
engine = create_engine('sqlite:///{}'.format(self.dbfile), connect_args={'check_same_thread': False})
engine = create_engine(
'sqlite:///{}'.format(self.dbfile),
connect_args={'check_same_thread': False},
)
Base.metadata.create_all(engine)
Session.configure(bind=engine)
@staticmethod
def _to_datetime(time_string: str) -> datetime.datetime:
""" Convert ISO 8061 string format with leading 'Z' into something understandable by Python """
"""Convert ISO 8061 string format with leading 'Z' into something understandable by Python"""
return datetime.datetime.fromisoformat(time_string[:-1] + '+00:00')
@staticmethod
@ -128,7 +157,11 @@ class GithubBackend(Backend):
def _get_last_event_time(self, uri: str):
with self.db_lock:
record = self._get_or_create_resource(uri=uri, session=Session())
return record.last_updated_at.replace(tzinfo=datetime.timezone.utc) if record.last_updated_at else None
return (
record.last_updated_at.replace(tzinfo=datetime.timezone.utc)
if record.last_updated_at
else None
)
def _update_last_event_time(self, uri: str, last_updated_at: datetime.datetime):
with self.db_lock:
@ -158,9 +191,18 @@ class GithubBackend(Backend):
'WatchEvent': GithubWatchEvent,
}
event_type = event_mapping[event['type']] if event['type'] in event_mapping else GithubEvent
return event_type(event_type=event['type'], actor=event['actor'], repo=event.get('repo', {}),
payload=event['payload'], created_at=cls._to_datetime(event['created_at']))
event_type = (
event_mapping[event['type']]
if event['type'] in event_mapping
else GithubEvent
)
return event_type(
event_type=event['type'],
actor=event['actor'],
repo=event.get('repo', {}),
payload=event['payload'],
created_at=cls._to_datetime(event['created_at']),
)
def _events_monitor(self, uri: str, method: str = 'get'):
def thread():
@ -175,7 +217,10 @@ class GithubBackend(Backend):
fired_events = []
for event in events:
if self.max_events_per_scan and len(fired_events) >= self.max_events_per_scan:
if (
self.max_events_per_scan
and len(fired_events) >= self.max_events_per_scan
):
break
event_time = self._to_datetime(event['created_at'])
@ -189,14 +234,19 @@ class GithubBackend(Backend):
for event in fired_events:
self.bus.post(event)
self._update_last_event_time(uri=uri, last_updated_at=new_last_event_time)
self._update_last_event_time(
uri=uri, last_updated_at=new_last_event_time
)
except Exception as e:
self.logger.warning('Encountered exception while fetching events from {}: {}'.format(
uri, str(e)))
self.logger.warning(
'Encountered exception while fetching events from {}: {}'.format(
uri, str(e)
)
)
self.logger.exception(e)
finally:
if self.wait_stop(timeout=self.poll_seconds):
break
if self.wait_stop(timeout=self.poll_seconds):
break
return thread
@ -206,12 +256,30 @@ class GithubBackend(Backend):
if self.repos:
for repo in self.repos:
monitors.append(threading.Thread(target=self._events_monitor('/networks/{repo}/events'.format(repo=repo))))
monitors.append(
threading.Thread(
target=self._events_monitor(
'/networks/{repo}/events'.format(repo=repo)
)
)
)
if self.org:
monitors.append(threading.Thread(target=self._events_monitor('/orgs/{org}/events'.format(org=self.org))))
monitors.append(
threading.Thread(
target=self._events_monitor(
'/orgs/{org}/events'.format(org=self.org)
)
)
)
if not (self.repos or self.org):
monitors.append(threading.Thread(target=self._events_monitor('/users/{user}/events'.format(user=self.user))))
monitors.append(
threading.Thread(
target=self._events_monitor(
'/users/{user}/events'.format(user=self.user)
)
)
)
for monitor in monitors:
monitor.start()
@ -222,4 +290,5 @@ class GithubBackend(Backend):
self.logger.info('Github backend terminated')
# vim:sw=4:ts=4:et:

View File

@ -1,19 +1,27 @@
import asyncio
import os
import subprocess
import pathlib
import secrets
import threading
from multiprocessing import Process
from time import time
from typing import List, Mapping, Optional
from tornado.httpserver import HTTPServer
try:
from websockets.exceptions import ConnectionClosed
from websockets import serve as websocket_serve
except ImportError:
from websockets import ConnectionClosed, serve as websocket_serve
from tornado.netutil import bind_sockets
from tornado.process import cpu_count, fork_processes
from tornado.wsgi import WSGIContainer
from tornado.web import Application, FallbackHandler
from platypush.backend import Backend
from platypush.backend.http.app import application
from platypush.context import get_or_create_event_loop
from platypush.utils import get_ssl_server_context, set_thread_name
from platypush.backend.http.app.utils import get_ws_routes
from platypush.backend.http.app.ws.events import events_redis_topic
from platypush.bus.redis import RedisBus
from platypush.config import Config
from platypush.utils import get_redis
class HttpBackend(Backend):
@ -27,8 +35,6 @@ class HttpBackend(Backend):
backend.http:
# Default HTTP listen port
port: 8008
# Default websocket port
websocket_port: 8009
# External folders that will be exposed over `/resources/<name>`
resource_dirs:
photos: /mnt/hd/photos
@ -39,9 +45,11 @@ class HttpBackend(Backend):
* To execute Platypush commands via HTTP calls. In order to do so:
* Register a user to Platypush through the web panel (usually served on ``http://host:8008/``).
* Register a user to Platypush through the web panel (usually
served on ``http://host:8008/``).
* Generate a token for your user, either through the web panel (Settings -> Generate Token) or via API:
* Generate a token for your user, either through the web panel
(Settings -> Generate Token) or via API:
.. code-block:: shell
@ -64,16 +72,35 @@ class HttpBackend(Backend):
}
}' http://host:8008/execute
* To interact with your system (and control plugins and backends) through the Platypush web panel,
by default available on ``http://host:8008/``. Any configured plugin that has an available panel
plugin will be automatically added to the web panel.
* To interact with your system (and control plugins and backends)
through the Platypush web panel, by default available on
``http://host:8008/``. Any configured plugin that has an available
panel plugin will be automatically added to the web panel.
* To create asynchronous integrations with Platypush over websockets.
Two routes are available:
* ``/ws/events`` - Subscribe to this websocket to receive the
events generated by the application.
* ``/ws/requests`` - Subscribe to this websocket to send commands
to Platypush and receive the response asynchronously.
You will have to authenticate your connection to these websockets,
just like the ``/execute`` endpoint. In both cases, you can pass the
token either via ``Authorization: Bearer``, via the ``token`` query
string or body parameter, or leverage ``Authorization: Basic`` with
username and password (not advised), or use a valid ``session_token``
cookie from an authenticated web panel session.
* To display a fullscreen dashboard with custom widgets.
* Widgets are available as Vue.js components under ``platypush/backend/http/webapp/src/components/widgets``.
* Widgets are available as Vue.js components under
``platypush/backend/http/webapp/src/components/widgets``.
* Explore their options (some may require some plugins or backends to be configured in order to work) and
create a new dashboard template under ``~/.config/platypush/dashboards``- e.g. ``main.xml``:
* Explore their options (some may require some plugins or backends
to be configured in order to work) and create a new dashboard
template under ``~/.config/platypush/dashboards``- e.g.
``main.xml``:
.. code-block:: xml
@ -109,13 +136,17 @@ class HttpBackend(Backend):
</Row>
</Dashboard>
* The dashboard will be accessible under ``http://host:8008/dashboard/<name>``, where ``name=main`` if for
example you stored your template under ``~/.config/platypush/dashboards/main.xml``.
* The dashboard will be accessible under
``http://host:8008/dashboard/<name>``, where ``name=main`` if for
example you stored your template under
``~/.config/platypush/dashboards/main.xml``.
* To expose custom endpoints that can be called as web hooks by other applications and run some custom logic.
All you have to do in this case is to create a hook on a
:class:`platypush.message.event.http.hook.WebhookEvent` with the endpoint that you want to expose and store
it under e.g. ``~/.config/platypush/scripts/hooks.py``:
* To expose custom endpoints that can be called as web hooks by other
applications and run some custom logic. All you have to do in this case
is to create a hook on a
:class:`platypush.message.event.http.hook.WebhookEvent` with the
endpoint that you want to expose and store it under e.g.
``~/.config/platypush/scripts/hooks.py``:
.. code-block:: python
@ -140,117 +171,54 @@ class HttpBackend(Backend):
module can expose lists of routes to the main webapp through the
``__routes__`` object (a list of Flask blueprints).
Security: Access to the endpoints requires at least one user to be registered. Access to the endpoints is regulated
in the following ways (with the exception of event hooks, whose logic is up to the user):
Security: Access to the endpoints requires at least one user to be
registered. Access to the endpoints is regulated in the following ways
(with the exception of event hooks, whose logic is up to the user):
* **Simple authentication** - i.e. registered username and password.
* **JWT token** provided either over as ``Authorization: Bearer`` header or ``GET`` ``?token=<TOKEN>``
parameter. A JWT token can be generated either through the web panel or over the ``/auth`` endpoint.
* **Global platform token**, usually configured on the root of the ``config.yaml`` as ``token: <VALUE>``.
It can provided either over on the ``X-Token`` header or as a ``GET`` ``?token=<TOKEN>`` parameter.
* **Session token**, generated upon login, it can be used to authenticate requests through the ``Cookie`` header
(cookie name: ``session_token``).
Requires:
* **gunicorn** (``pip install gunicorn``) - optional, to run the Platypush webapp over uWSGI.
By default the Platypush web server will run in a
process spawned on the fly by the HTTP backend. However, being a
Flask app, it will serve clients in a single thread and it won't
support many features of a full-blown web server. gunicorn allows
you to easily spawn the web server in a uWSGI wrapper, separate
from the main Platypush daemon, and the uWSGI layer can be easily
exposed over an nginx/lighttpd web server.
Command to run the web server over a gunicorn uWSGI wrapper::
gunicorn -w <n_workers> -b <bind_address>:8008 platypush.backend.http.uwsgi
* **JWT token** provided either over as ``Authorization: Bearer``
header or ``GET`` ``?token=<TOKEN>`` parameter. A JWT token can be
generated either through the web panel or over the ``/auth``
endpoint.
* **Global platform token**, usually configured on the root of the
``config.yaml`` as ``token: <VALUE>``. It can provided either over on
the ``X-Token`` header or as a ``GET`` ``?token=<TOKEN>`` parameter.
* **Session token**, generated upon login, it can be used to
authenticate requests through the ``Cookie`` header (cookie name:
``session_token``).
"""
_DEFAULT_HTTP_PORT = 8008
_DEFAULT_WEBSOCKET_PORT = 8009
"""The default listen port for the webserver."""
def __init__(
self,
port=_DEFAULT_HTTP_PORT,
websocket_port=_DEFAULT_WEBSOCKET_PORT,
bind_address='0.0.0.0',
disable_websocket=False,
resource_dirs=None,
ssl_cert=None,
ssl_key=None,
ssl_cafile=None,
ssl_capath=None,
maps=None,
run_externally=False,
uwsgi_args=None,
**kwargs
port: int = _DEFAULT_HTTP_PORT,
bind_address: str = '0.0.0.0',
resource_dirs: Optional[Mapping[str, str]] = None,
secret_key_file: Optional[str] = None,
num_workers: Optional[int] = None,
**kwargs,
):
"""
:param port: Listen port for the web server (default: 8008)
:type port: int
:param websocket_port: Listen port for the websocket server (default: 8009)
:type websocket_port: int
:param bind_address: Address/interface to bind to (default: 0.0.0.0, accept connection from any IP)
:type bind_address: str
:param disable_websocket: Disable the websocket interface (default: False)
:type disable_websocket: bool
:param ssl_cert: Set it to the path of your certificate file if you want to enable HTTPS (default: None)
:type ssl_cert: str
:param ssl_key: Set it to the path of your key file if you want to enable HTTPS (default: None)
:type ssl_key: str
:param ssl_cafile: Set it to the path of your certificate authority file if you want to enable HTTPS
(default: None)
:type ssl_cafile: str
:param ssl_capath: Set it to the path of your certificate authority directory if you want to enable HTTPS
(default: None)
:type ssl_capath: str
:param resource_dirs: Static resources directories that will be
accessible through ``/resources/<path>``. It is expressed as a map
where the key is the relative path under ``/resources`` to expose and
the value is the absolute path to expose.
:type resource_dirs: dict[str, str]
:param run_externally: If set, then the HTTP backend will not directly
spawn the web server. Set this option if you plan to run the webapp
in a separate web server (recommended), like uwsgi or uwsgi+nginx.
:type run_externally: bool
:param uwsgi_args: If ``run_externally`` is set and you would like the
HTTP backend to directly spawn and control the uWSGI application
server instance, then pass the list of uWSGI arguments through
this parameter. Some examples include::
# Start uWSGI instance listening on HTTP port 8008 with 4
# processes
['--plugin', 'python', '--http-socket', ':8008', '--master', '--processes', '4']
# Start uWSGI instance listening on uWSGI socket on port 3031.
# You can then use another full-blown web server, like nginx
# or Apache, to communicate with the uWSGI instance
['--plugin', 'python', '--socket', '127.0.0.1:3031', '--master', '--processes', '4']
:type uwsgi_args: list[str]
:param secret_key_file: Path to the file containing the secret key that will be used by Flask
(default: ``~/.local/share/platypush/flask.secret.key``).
:param num_workers: Number of worker processes to use (default: ``(cpu_count * 2) + 1``).
"""
super().__init__(**kwargs)
self.port = port
self.websocket_port = websocket_port
self.maps = maps or {}
self.server_proc = None
self.disable_websocket = disable_websocket
self.websocket_thread = None
self._websocket_loop = None
self._server_proc: Optional[Process] = None
self._workers: List[Process] = []
self._service_registry_thread = None
self.bind_address = bind_address
if resource_dirs:
@ -261,36 +229,14 @@ class HttpBackend(Backend):
else:
self.resource_dirs = {}
self.active_websockets = set()
self.run_externally = run_externally
self.uwsgi_args = uwsgi_args or []
self.ssl_context = (
get_ssl_server_context(
ssl_cert=ssl_cert,
ssl_key=ssl_key,
ssl_cafile=ssl_cafile,
ssl_capath=ssl_capath,
)
if ssl_cert
else None
self.secret_key_file = os.path.expanduser(
secret_key_file
or os.path.join(Config.get('workdir'), 'flask.secret.key') # type: ignore
)
self.local_base_url = f'http://localhost:{self.port}'
self.num_workers = num_workers or (cpu_count() * 2) + 1
if self.uwsgi_args:
self.uwsgi_args = [str(_) for _ in self.uwsgi_args] + [
'--module',
'platypush.backend.http.uwsgi',
'--enable-threads',
]
self.local_base_url = '{proto}://localhost:{port}'.format(
proto=('https' if ssl_cert else 'http'), port=self.port
)
self._websocket_lock_timeout = 10
self._websocket_lock = threading.RLock()
self._websocket_locks = {}
def send_message(self, msg, **kwargs):
def send_message(self, *_, **__):
self.logger.warning('Use cURL or any HTTP client to query the HTTP backend')
def on_stop(self):
@ -298,190 +244,120 @@ class HttpBackend(Backend):
super().on_stop()
self.logger.info('Received STOP event on HttpBackend')
if self.server_proc:
if isinstance(self.server_proc, subprocess.Popen):
self.server_proc.kill()
self.server_proc.wait(timeout=10)
if self.server_proc.poll() is not None:
self.logger.info(
'HTTP server process may be still alive at termination'
)
else:
self.logger.info('HTTP server process terminated')
else:
self.server_proc.terminate()
self.server_proc.join(timeout=10)
if self.server_proc.is_alive():
self.server_proc.kill()
if self.server_proc.is_alive():
self.logger.info(
'HTTP server process may be still alive at termination'
)
else:
self.logger.info('HTTP server process terminated')
start_time = time()
timeout = 5
workers = self._workers.copy()
if (
self.websocket_thread
and self.websocket_thread.is_alive()
and self._websocket_loop
):
self._websocket_loop.stop()
self.logger.info('HTTP websocket service terminated')
for i, worker in enumerate(workers[::-1]):
if worker and worker.is_alive():
worker.terminate()
worker.join(timeout=max(0, start_time + timeout - time()))
def _acquire_websocket_lock(self, ws):
try:
acquire_ok = self._websocket_lock.acquire(
timeout=self._websocket_lock_timeout
)
if not acquire_ok:
raise TimeoutError('Websocket lock acquire timeout')
if worker and worker.is_alive():
worker.kill()
self._workers.pop(i)
addr = ws.remote_address
if addr not in self._websocket_locks:
self._websocket_locks[addr] = threading.RLock()
finally:
self._websocket_lock.release()
if self._server_proc:
self._server_proc.terminate()
self._server_proc.join(timeout=5)
self._server_proc = None
acquire_ok = self._websocket_locks[addr].acquire(
timeout=self._websocket_lock_timeout
)
if not acquire_ok:
raise TimeoutError(
'Websocket on address {} not ready to receive data'.format(addr)
)
if self._server_proc and self._server_proc.is_alive():
self._server_proc.kill()
def _release_websocket_lock(self, ws):
try:
acquire_ok = self._websocket_lock.acquire(
timeout=self._websocket_lock_timeout
)
if not acquire_ok:
raise TimeoutError('Websocket lock acquire timeout')
self._server_proc = None
self.logger.info('HTTP server terminated')
addr = ws.remote_address
if addr in self._websocket_locks:
self._websocket_locks[addr].release()
except Exception as e:
self.logger.warning(
'Unhandled exception while releasing websocket lock: {}'.format(str(e))
)
finally:
self._websocket_lock.release()
if self._service_registry_thread and self._service_registry_thread.is_alive():
self._service_registry_thread.join(timeout=5)
self._service_registry_thread = None
def notify_web_clients(self, event):
"""Notify all the connected web clients (over websocket) of a new event"""
get_redis().publish(events_redis_topic, str(event))
async def send_event(ws):
try:
self._acquire_websocket_lock(ws)
await ws.send(str(event))
except Exception as e:
self.logger.warning('Error on websocket send_event: {}'.format(e))
finally:
self._release_websocket_lock(ws)
def _get_secret_key(self, _create=False):
if _create:
self.logger.info('Creating web server secret key')
pathlib.Path(self.secret_key_file).parent.mkdir(parents=True, exist_ok=True)
with open(self.secret_key_file, 'w') as f:
f.write(secrets.token_urlsafe(32))
loop = get_or_create_event_loop()
wss = self.active_websockets.copy()
os.chmod(self.secret_key_file, 0o600)
return secrets.token_urlsafe(32)
for _ws in wss:
try:
loop.run_until_complete(send_event(_ws))
except ConnectionClosed:
self.logger.warning(
'Websocket client {} connection lost'.format(_ws.remote_address)
)
self.active_websockets.remove(_ws)
if _ws.remote_address in self._websocket_locks:
del self._websocket_locks[_ws.remote_address]
try:
with open(self.secret_key_file, 'r') as f:
return f.read()
except IOError as e:
if not _create:
return self._get_secret_key(_create=True)
def websocket(self):
"""Websocket main server"""
set_thread_name('WebsocketServer')
raise e
async def register_websocket(websocket, path):
address = (
websocket.remote_address
if websocket.remote_address
else '<unknown client>'
)
self.logger.info(
'New websocket connection from {} on path {}'.format(address, path)
)
self.active_websockets.add(websocket)
try:
await websocket.recv()
except ConnectionClosed:
self.logger.info(
'Websocket client {} closed connection'.format(address)
)
self.active_websockets.remove(websocket)
if address in self._websocket_locks:
del self._websocket_locks[address]
websocket_args = {}
if self.ssl_context:
websocket_args['ssl'] = self.ssl_context
self._websocket_loop = get_or_create_event_loop()
self._websocket_loop.run_until_complete(
websocket_serve(
register_websocket,
self.bind_address,
self.websocket_port,
**websocket_args
)
)
self._websocket_loop.run_forever()
def _start_web_server(self):
def proc():
self.logger.info('Starting local web server on port {}'.format(self.port))
kwargs = {
'host': self.bind_address,
'port': self.port,
'use_reloader': False,
'debug': False,
}
application.config['redis_queue'] = self.bus.redis_queue
if self.ssl_context:
kwargs['ssl_context'] = self.ssl_context
application.run(**kwargs)
return proc
def run(self):
super().run()
def _register_service(self):
try:
self.register_service(port=self.port)
except Exception as e:
self.logger.warning('Could not register the Zeroconf service')
self.logger.exception(e)
if not self.disable_websocket:
self.logger.info('Initializing websocket interface')
self.websocket_thread = threading.Thread(target=self.websocket)
self.websocket_thread.start()
def _start_zeroconf_service(self):
self._service_registry_thread = threading.Thread(
target=self._register_service,
name='ZeroconfService',
)
self._service_registry_thread.start()
if not self.run_externally:
self.server_proc = Process(
target=self._start_web_server(), name='WebServer'
)
self.server_proc.start()
self.server_proc.join()
elif self.uwsgi_args:
uwsgi_cmd = ['uwsgi'] + self.uwsgi_args
self.logger.info('Starting uWSGI with arguments {}'.format(uwsgi_cmd))
self.server_proc = subprocess.Popen(uwsgi_cmd)
else:
self.logger.info(
'The web server is configured to be launched externally but '
+ 'no uwsgi_args were provided. Make sure that you run another external service'
+ 'for the webserver (e.g. nginx)'
)
async def _post_fork_main(self, sockets):
assert isinstance(
self.bus, RedisBus
), 'The HTTP backend only works if backed by a Redis bus'
application.config['redis_queue'] = self.bus.redis_queue
application.secret_key = self._get_secret_key()
container = WSGIContainer(application)
tornado_app = Application(
[
*[(route.path(), route) for route in get_ws_routes()],
(r'.*', FallbackHandler, {'fallback': container}),
]
)
server = HTTPServer(tornado_app)
server.add_sockets(sockets)
try:
await asyncio.Event().wait()
except (asyncio.CancelledError, KeyboardInterrupt):
return
def _web_server_proc(self):
self.logger.info(
'Starting local web server on port %s with %d service workers',
self.port,
self.num_workers,
)
sockets = bind_sockets(self.port, address=self.bind_address, reuse_port=True)
try:
fork_processes(self.num_workers)
future = self._post_fork_main(sockets)
asyncio.run(future)
except (asyncio.CancelledError, KeyboardInterrupt):
return
def _start_web_server(self):
self._server_proc = Process(target=self._web_server_proc)
self._server_proc.start()
self._server_proc.join()
def run(self):
super().run()
self._start_zeroconf_service()
self._start_web_server()
# vim:sw=4:ts=4:et:

View File

@ -50,7 +50,6 @@ def auth_endpoint():
except Exception as e:
log.warning('Invalid payload passed to the auth endpoint: ' + str(e))
abort(400)
return jsonify({'token': None})
expiry_days = payload.get('expiry_days')
expires_at = None
@ -65,4 +64,3 @@ def auth_endpoint():
})
except UserException as e:
abort(401, str(e))
return jsonify({'token': None})

View File

@ -1,7 +1,7 @@
from flask import Blueprint, render_template
from platypush.backend.http.app import template_folder
from platypush.backend.http.app.utils import authenticate, get_websocket_port
from platypush.backend.http.app.utils import authenticate
from platypush.backend.http.utils import HttpUtils
dashboard = Blueprint('dashboard', __name__, template_folder=template_folder)
@ -16,10 +16,11 @@ __routes__ = [
@dashboard.route('/dashboard/<name>', methods=['GET'])
@authenticate()
def render_dashboard(name):
""" Route for the dashboard """
return render_template('index.html',
utils=HttpUtils,
websocket_port=get_websocket_port())
"""Route for the dashboard"""
return render_template(
'index.html',
utils=HttpUtils,
)
# vim:sw=4:ts=4:et:

View File

@ -1,6 +1,7 @@
import json
from flask import Blueprint, abort, request, Response
from flask import Blueprint, abort, request
from flask.wrappers import Response
from platypush.backend.http.app import template_folder
from platypush.backend.http.app.utils import authenticate, logger, send_message
@ -14,8 +15,8 @@ __routes__ = [
@execute.route('/execute', methods=['POST'])
@authenticate()
def execute():
@authenticate(json=True)
def execute_route():
"""Endpoint to execute commands"""
try:
msg = json.loads(request.data.decode('utf-8'))

View File

@ -1,9 +1,11 @@
import json
from flask import Blueprint, abort, request, Response
from flask import Blueprint, abort, request, make_response
from platypush.backend.http.app import template_folder
from platypush.backend.http.app.utils import logger, send_message
from platypush.config import Config
from platypush.event.hook import EventCondition
from platypush.message.event.http.hook import WebhookEvent
@ -15,9 +17,23 @@ __routes__ = [
]
@hook.route('/hook/<hook_name>', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])
def _hook(hook_name):
""" Endpoint for custom webhooks """
def matches_condition(event: WebhookEvent, hook):
if isinstance(hook, dict):
if_ = hook['if'].copy()
if_['type'] = '.'.join([event.__module__, event.__class__.__qualname__])
condition = EventCondition.build(if_)
else:
condition = hook.condition
return event.matches_condition(condition)
@hook.route(
'/hook/<hook_name>', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS']
)
def hook_route(hook_name):
"""Endpoint for custom webhooks"""
event_args = {
'hook': hook_name,
@ -28,20 +44,54 @@ def _hook(hook_name):
}
if event_args['data']:
# noinspection PyBroadException
try:
event_args['data'] = json.loads(event_args['data'])
except Exception as e:
logger().warning('Not a valid JSON string: {}: {}'.format(event_args['data'], str(e)))
logger().warning(
'Not a valid JSON string: %s: %s', event_args['data'], str(e)
)
event = WebhookEvent(**event_args)
matching_hooks = [
hook
for hook in Config.get_event_hooks().values()
if matches_condition(event, hook)
]
try:
send_message(event)
return Response(json.dumps({'status': 'ok', **event_args}), mimetype='application/json')
rs = default_rs = make_response(json.dumps({'status': 'ok', **event_args}))
headers = {}
status_code = 200
# If there are matching hooks, wait for their completion before returning
if matching_hooks:
rs = event.wait_response(timeout=60)
try:
rs = json.loads(rs.decode()) # type: ignore
except Exception:
pass
if isinstance(rs, dict) and '___data___' in rs:
# data + http_code + custom_headers return format
headers = rs.get('___headers___', {})
status_code = rs.get('___code___', status_code)
rs = rs['___data___']
if rs is None:
rs = default_rs
headers = {'Content-Type': 'application/json'}
rs = make_response(rs)
else:
headers = {'Content-Type': 'application/json'}
rs.status_code = status_code
rs.headers.update(headers)
return rs
except Exception as e:
logger().exception(e)
logger().error('Error while dispatching webhook event {}: {}'.format(event, str(e)))
logger().error('Error while dispatching webhook event %s: %s', event, str(e))
abort(500, str(e))

View File

@ -0,0 +1,185 @@
from dataclasses import dataclass
import math
from typing import Tuple
from flask import Blueprint, make_response, request
logo = Blueprint('logo', __name__)
# Declare routes list
__routes__ = [
logo,
]
@dataclass
class Gear:
"""
A utility class used to model the gears in the application's logo.
"""
center: Tuple[float, float]
outer_radius: float
inner_radius: float
color: str = "currentColor"
background: str = ""
num_spikes: int = 0
spike_max_base: float = 0
spike_min_base: float = 0
spike_height: float = 0
alpha_offset: float = 0
def to_svg(self) -> str:
"""
Returns the SVG representation of a gear.
"""
# Generate the basic circle
svg = f"""
<circle
cx="{self.center[0]}" cy="{self.center[1]}"
r="{self.outer_radius - (self.inner_radius / math.pi)}"
stroke-width="{self.inner_radius}"
stroke="{self.color}"
fill="none" />
"""
# Generate the spikes
for i in range(self.num_spikes):
# Iterate for alpha -> [0, 2*pi]
alpha = (2 * math.pi * i) / self.num_spikes
# Calculate the base angle for the major base of the gear polygon
maj_delta_alpha = math.asin(self.spike_max_base / (2 * self.outer_radius))
# Calculate the points of the gear polygon's major base
maj_base = (
(
self.center[0]
+ self.outer_radius
* math.cos(alpha + maj_delta_alpha + self.alpha_offset),
self.center[1]
+ self.outer_radius
* math.sin(alpha + maj_delta_alpha + self.alpha_offset),
),
(
self.center[0]
+ self.outer_radius
* math.cos(alpha - maj_delta_alpha + self.alpha_offset),
self.center[1]
+ self.outer_radius
* math.sin(alpha - maj_delta_alpha + self.alpha_offset),
),
)
# Height of the gear relative to the circle's center
h = self.outer_radius * math.cos(maj_delta_alpha) + self.spike_height
# Calculate the base angle for the minor base of the gear polygon
min_delta_alpha = math.asin(self.spike_min_base / (2 * h))
# Calculate the points of the gear polygon's minor base
min_base = (
(
self.center[0]
+ h * math.cos(alpha - min_delta_alpha + self.alpha_offset),
self.center[1]
+ h * math.sin(alpha - min_delta_alpha + self.alpha_offset),
),
(
self.center[0]
+ h * math.cos(alpha + min_delta_alpha + self.alpha_offset),
self.center[1]
+ h * math.sin(alpha + min_delta_alpha + self.alpha_offset),
),
)
# Flatten the polygon's points
svg_points = " ".join(
[f"{point[0]},{point[1]}" for point in [*maj_base, *min_base]]
)
# Serialize the gear polygon to SVG
svg += f"""
<polygon points="{svg_points}" stroke="{self.color}" fill="{self.color}" />"""
return svg
# Properties of the two gears on the logo
gears = [
Gear(
center=(32.9, 34.5),
outer_radius=22.6,
inner_radius=12.4,
num_spikes=12,
spike_max_base=9,
spike_min_base=4.3,
spike_height=10.16,
),
Gear(
center=(65.5, 70.5),
outer_radius=14.4,
inner_radius=8.5,
num_spikes=7,
spike_max_base=9,
spike_min_base=4.3,
spike_height=7.5,
alpha_offset=math.pi / 6.6,
),
]
template_start = """
<svg version="1.1"
width="{width}" height="{height}"
viewBox="0 0 100 100"
preserveAspectRatio="none"
xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="triangleGradient">
<stop offset="0%" stop-color="#8acb45" />
<stop offset="50%" stop-color="#6bbb4c" />
<stop offset="100%" stop-color="#5cb450" />
</linearGradient>
</defs>
<rect width="100%" height="100%" fill="{bg_color}" />
"""
template_end = "\n</svg>"
@logo.route('/logo.svg', methods=['GET'])
def logo_path():
"""
This path dynamically generates the logo image as a parametrizable vector SVG.
Parameters:
- ``size``: Size of the image in pixels (default: 256)
- ``bg``: Background color (default: "none")
- ``fg``: Foreground color (default: "currentColor")
"""
size = request.args.get("size", 256)
bg = request.args.get("bg", "none")
fg = request.args.get("fg", "currentColor")
svg = template_start.format(
width=size,
height=size,
bg_color=bg,
)
for gear in gears:
gear.color = fg
gear.background = bg
svg += gear.to_svg()
# "Play" triangle on the logo
svg += """\n\t\t<polygon points="67,47 67,3 99,25.3" fill="url(#triangleGradient)" />"""
svg += template_end
rs = make_response(svg)
rs.headers.update({"Content-Type": "image/svg+xml"})
return rs
# vim:sw=4:ts=4:et:

View File

@ -0,0 +1,46 @@
import requests
from urllib.parse import urljoin
from flask import abort, request, Blueprint
from platypush.backend.http.app import template_folder
mimic3 = Blueprint('mimic3', __name__, template_folder=template_folder)
# Declare routes list
__routes__ = [
mimic3,
]
@mimic3.route('/tts/mimic3/say', methods=['GET'])
def proxy_tts_request():
"""
This route is used to proxy the POST request to the Mimic3 TTS server
through a GET, so it can be easily processed as a URL through a media
plugin.
"""
required_args = {
'text',
'server_url',
'voice',
}
missing_args = required_args.difference(set(request.args.keys()))
if missing_args:
abort(400, f'Missing parameters: {missing_args}')
args = {arg: request.args[arg] for arg in required_args}
rs = requests.post(
urljoin(args['server_url'], '/api/tts'),
data=args['text'],
params={
'voice': args['voice'],
},
)
return rs.content
# vim:sw=4:ts=4:et:

View File

@ -0,0 +1,113 @@
from flask import Blueprint, jsonify, send_from_directory
from platypush.config import Config
from platypush.backend.http.app import template_folder
pwa = Blueprint('pwa', __name__, template_folder=template_folder)
# Declare routes list
__routes__ = [
pwa,
]
@pwa.route('/manifest.json', methods=['GET'])
def manifest_json():
"""Generated manifest file for the PWA"""
return jsonify(
{
"name": f'Platypush @ {Config.get("device_id")}',
"short_name": Config.get('device_id'),
"icons": [
{
"src": "/img/icons/favicon-16x16.png",
"sizes": "16x16",
"type": "image/png",
},
{
"src": "/img/icons/favicon-32x32.png",
"sizes": "32x32",
"type": "image/png",
},
{
"src": "/img/icons/apple-touch-icon-60x60.png",
"sizes": "60x60",
"type": "image/png",
},
{
"src": "/img/icons/apple-touch-icon-76x76.png",
"sizes": "76x76",
"type": "image/png",
},
{
"src": "/img/icons/apple-touch-icon-120x120.png",
"sizes": "120x120",
"type": "image/png",
},
{
"src": "/img/icons/msapplication-icon-144x144.png",
"sizes": "144x144",
"type": "image/png",
},
{
"src": "/img/icons/mstile-150x150.png",
"sizes": "150x150",
"type": "image/png",
},
{
"src": "/img/icons/apple-touch-icon-152x152.png",
"sizes": "152x152",
"type": "image/png",
},
{
"src": "/img/icons/apple-touch-icon-180x180.png",
"sizes": "180x180",
"type": "image/png",
},
{
"src": "/img/icons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png",
},
{
"src": "/img/icons/android-chrome-maskable-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable",
},
{
"src": "/img/icons/logo-256x256.png",
"sizes": "256x256",
"type": "image/png",
},
{
"src": "/img/icons/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png",
},
{
"src": "/img/icons/android-chrome-maskable-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable",
},
],
"gcm_sender_id": "",
"gcm_user_visible_only": True,
"start_url": "/",
"permissions": ["gcm"],
"orientation": "portrait",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff",
}
)
@pwa.route('/service-worker.js', methods=['GET'])
def service_worker_js():
"""URL that serves the service worker for the PWA"""
return send_from_directory(template_folder, 'service-worker.js')
# vim:sw=4:ts=4:et:

View File

@ -8,22 +8,27 @@ from platypush.backend.http.app import template_folder
img_folder = os.path.join(template_folder, 'img')
fonts_folder = os.path.join(template_folder, 'fonts')
icons_folder = os.path.join(template_folder, 'icons')
resources = Blueprint('resources', __name__, template_folder=template_folder)
favicon = Blueprint('favicon', __name__, template_folder=template_folder)
img = Blueprint('img', __name__, template_folder=template_folder)
icons = Blueprint('icons', __name__, template_folder=template_folder)
fonts = Blueprint('fonts', __name__, template_folder=template_folder)
# Declare routes list
__routes__ = [
resources,
favicon,
img,
icons,
fonts,
]
@resources.route('/resources/<path:path>', methods=['GET'])
def resources_path(path):
""" Custom static resources """
"""Custom static resources"""
path_tokens = path.split('/')
http_conf = Config.get('backend.http')
resource_dirs = http_conf.get('resource_dirs', {})
@ -42,9 +47,11 @@ def resources_path(path):
real_path = real_base_path
file_path = [
s for s in re.sub(
r'^{}(.*)$'.format(base_path), '\\1', path # lgtm [py/regex-injection]
).split('/') if s
s
for s in re.sub(
r'^{}(.*)$'.format(base_path), '\\1', path # lgtm [py/regex-injection]
).split('/')
if s
]
for p in file_path[:-1]:
@ -61,20 +68,26 @@ def resources_path(path):
@favicon.route('/favicon.ico', methods=['GET'])
def serve_favicon():
""" favicon.ico icon """
"""favicon.ico icon"""
return send_from_directory(template_folder, 'favicon.ico')
@img.route('/img/<path:path>', methods=['GET'])
def imgpath(path):
""" Default static images """
"""Default static images"""
return send_from_directory(img_folder, path)
@img.route('/icons/<path:path>', methods=['GET'])
@icons.route('/icons/<path:path>', methods=['GET'])
def iconpath(path):
""" Default static icons """
"""Default static icons"""
return send_from_directory(icons_folder, path)
@fonts.route('/fonts/<path:path>', methods=['GET'])
def fontpath(path):
"""Default fonts"""
return send_from_directory(fonts_folder, path)
# vim:sw=4:ts=4:et:

View File

@ -1,274 +0,0 @@
import importlib
import logging
import os
from functools import wraps
from flask import abort, request, redirect, Response, current_app
from redis import Redis
# NOTE: The HTTP service will *only* work on top of a Redis bus. The default
# internal bus service won't work as the web server will run in a different process.
from platypush.bus.redis import RedisBus
from platypush.config import Config
from platypush.message import Message
from platypush.message.request import Request
from platypush.user import UserManager
from platypush.utils import get_redis_queue_name_by_message, get_ip_or_hostname
_bus = None
_logger = None
def bus():
global _bus
if _bus is None:
_bus = RedisBus(redis_queue=current_app.config.get('redis_queue'))
return _bus
def logger():
global _logger
if not _logger:
log_args = {
'level': logging.INFO,
'format': '%(asctime)-15s|%(levelname)5s|%(name)s|%(message)s',
}
level = (Config.get('backend.http') or {}).get('logging') or \
(Config.get('logging') or {}).get('level')
filename = (Config.get('backend.http') or {}).get('filename')
if level:
log_args['level'] = getattr(logging, level.upper()) \
if isinstance(level, str) else level
if filename:
log_args['filename'] = filename
logging.basicConfig(**log_args)
_logger = logging.getLogger('platypush:web')
return _logger
def get_message_response(msg):
redis = Redis(**bus().redis_args)
response = redis.blpop(get_redis_queue_name_by_message(msg), timeout=60)
if response and len(response) > 1:
response = Message.build(response[1])
else:
response = None
return response
# noinspection PyProtectedMember
def get_http_port():
from platypush.backend.http import HttpBackend
http_conf = Config.get('backend.http')
return http_conf.get('port', HttpBackend._DEFAULT_HTTP_PORT)
# noinspection PyProtectedMember
def get_websocket_port():
from platypush.backend.http import HttpBackend
http_conf = Config.get('backend.http')
return http_conf.get('websocket_port', HttpBackend._DEFAULT_WEBSOCKET_PORT)
def send_message(msg, wait_for_response=True):
msg = Message.build(msg)
if isinstance(msg, Request):
msg.origin = 'http'
if Config.get('token'):
msg.token = Config.get('token')
bus().post(msg)
if isinstance(msg, Request) and wait_for_response:
response = get_message_response(msg)
logger().debug('Processing response on the HTTP backend: {}'.
format(response))
return response
def send_request(action, wait_for_response=True, **kwargs):
msg = {
'type': 'request',
'action': action
}
if kwargs:
msg['args'] = kwargs
return send_message(msg, wait_for_response=wait_for_response)
def _authenticate_token():
token = Config.get('token')
user_manager = UserManager()
if 'X-Token' in request.headers:
user_token = request.headers['X-Token']
elif 'Authorization' in request.headers and request.headers['Authorization'].startswith('Bearer '):
user_token = request.headers['Authorization'][len('Bearer '):]
elif 'token' in request.args:
user_token = request.args.get('token')
else:
return False
try:
user_manager.validate_jwt_token(user_token)
return True
except Exception as e:
logger().debug(str(e))
return token and user_token == token
def _authenticate_http():
user_manager = UserManager()
if not request.authorization:
return False
username = request.authorization.username
password = request.authorization.password
return user_manager.authenticate_user(username, password)
def _authenticate_session():
user_manager = UserManager()
user_session_token = None
user = None
if 'X-Session-Token' in request.headers:
user_session_token = request.headers['X-Session-Token']
elif 'session_token' in request.args:
user_session_token = request.args.get('session_token')
elif 'session_token' in request.cookies:
user_session_token = request.cookies.get('session_token')
if user_session_token:
user, session = user_manager.authenticate_user_session(user_session_token)
return user is not None
def _authenticate_csrf_token():
user_manager = UserManager()
user_session_token = None
if 'X-Session-Token' in request.headers:
user_session_token = request.headers['X-Session-Token']
elif 'session_token' in request.args:
user_session_token = request.args.get('session_token')
elif 'session_token' in request.cookies:
user_session_token = request.cookies.get('session_token')
if user_session_token:
user, session = user_manager.authenticate_user_session(user_session_token)
else:
return False
if user is None:
return False
return session.csrf_token is None or request.form.get('csrf_token') == session.csrf_token
def authenticate(redirect_page='', skip_auth_methods=None, check_csrf_token=False):
def decorator(f):
@wraps(f)
def wrapper(*args, **kwargs):
user_manager = UserManager()
n_users = user_manager.get_user_count()
skip_methods = skip_auth_methods or []
# User/pass HTTP authentication
http_auth_ok = True
if n_users > 0 and 'http' not in skip_methods:
http_auth_ok = _authenticate_http()
if http_auth_ok:
return f(*args, **kwargs)
# Token-based authentication
token_auth_ok = True
if 'token' not in skip_methods:
token_auth_ok = _authenticate_token()
if token_auth_ok:
return f(*args, **kwargs)
# Session token based authentication
session_auth_ok = True
if n_users > 0 and 'session' not in skip_methods:
session_auth_ok = _authenticate_session()
if session_auth_ok:
return f(*args, **kwargs)
return redirect('/login?redirect=' + (redirect_page or request.url), 307)
# CSRF token check
if check_csrf_token:
csrf_check_ok = _authenticate_csrf_token()
if not csrf_check_ok:
return abort(403, 'Invalid or missing csrf_token')
if n_users == 0 and 'session' not in skip_methods:
return redirect('/register?redirect=' + (redirect_page or request.url), 307)
if ('http' not in skip_methods and http_auth_ok) or \
('token' not in skip_methods and token_auth_ok) or \
('session' not in skip_methods and session_auth_ok):
return f(*args, **kwargs)
return Response('Authentication required', 401,
{'WWW-Authenticate': 'Basic realm="Login required"'})
return wrapper
return decorator
def get_routes():
routes_dir = os.path.join(
os.path.dirname(os.path.abspath(__file__)), 'routes')
routes = []
base_module = '.'.join(__name__.split('.')[:-1])
for path, dirs, files in os.walk(routes_dir):
for f in files:
if f.endswith('.py'):
mod_name = '.'.join(
(base_module + '.' + os.path.join(path, f).replace(
os.path.dirname(__file__), '')[1:].replace(os.sep, '.')).split('.')
[:(-2 if f == '__init__.py' else -1)])
try:
mod = importlib.import_module(mod_name)
if hasattr(mod, '__routes__'):
routes.extend(mod.__routes__)
except Exception as e:
logger().warning('Could not import routes from {}/{}: {}: {}'.
format(path, f, type(e), str(e)))
return routes
def get_local_base_url():
http_conf = Config.get('backend.http') or {}
return '{proto}://localhost:{port}'.format(
proto=('https' if http_conf.get('ssl_cert') else 'http'),
port=get_http_port())
def get_remote_base_url():
http_conf = Config.get('backend.http') or {}
return '{proto}://{host}:{port}'.format(
proto=('https' if http_conf.get('ssl_cert') else 'http'),
host=get_ip_or_hostname(), port=get_http_port())
# vim:sw=4:ts=4:et:

View File

@ -0,0 +1,37 @@
from .auth import (
authenticate,
authenticate_token,
authenticate_user_pass,
get_auth_status,
)
from .bus import bus, get_message_response, send_message, send_request
from .logger import logger
from .routes import (
get_http_port,
get_ip_or_hostname,
get_local_base_url,
get_remote_base_url,
get_routes,
)
from .ws import get_ws_routes
__all__ = [
'authenticate',
'authenticate_token',
'authenticate_user_pass',
'bus',
'get_auth_status',
'get_http_port',
'get_ip_or_hostname',
'get_local_base_url',
'get_message_response',
'get_remote_base_url',
'get_routes',
'get_ws_routes',
'logger',
'send_message',
'send_request',
]
# vim:sw=4:ts=4:et:

View File

@ -0,0 +1,196 @@
import base64
from functools import wraps
from typing import Optional
from flask import request, redirect, jsonify
from flask.wrappers import Response
from platypush.config import Config
from platypush.user import UserManager
from ..logger import logger
from .status import AuthStatus
user_manager = UserManager()
def get_arg(req, name: str) -> Optional[str]:
# The Flask way
if hasattr(req, 'args'):
return req.args.get(name)
# The Tornado way
if hasattr(req, 'arguments'):
arg = req.arguments.get(name)
if arg:
return arg[0].decode()
return None
def get_cookie(req, name: str) -> Optional[str]:
cookie = req.cookies.get(name)
if not cookie:
return None
# The Flask way
if isinstance(cookie, str):
return cookie
# The Tornado way
return cookie.value
def authenticate_token(req):
token = Config.get('token')
user_token = None
if 'X-Token' in req.headers:
user_token = req.headers['X-Token']
elif 'Authorization' in req.headers and req.headers['Authorization'].startswith(
'Bearer '
):
user_token = req.headers['Authorization'][7:]
else:
user_token = get_arg(req, 'token')
if not user_token:
return False
try:
user_manager.validate_jwt_token(user_token)
return True
except Exception as e:
logger().debug(str(e))
return bool(token and user_token == token)
def authenticate_user_pass(req):
# Flask populates request.authorization
if hasattr(req, 'authorization'):
if not req.authorization:
return False
username = req.authorization.username
password = req.authorization.password
# Otherwise, check the Authorization header
elif 'Authorization' in req.headers and req.headers['Authorization'].startswith(
'Basic '
):
auth = req.headers['Authorization'][6:]
try:
auth = base64.b64decode(auth)
except ValueError:
pass
username, password = auth.decode().split(':', maxsplit=1)
else:
return False
return user_manager.authenticate_user(username, password)
def authenticate_session(req):
user = None
# Check the X-Session-Token header
user_session_token = req.headers.get('X-Session-Token')
# Check the `session_token` query/body parameter
if not user_session_token:
user_session_token = get_arg(req, 'session_token')
# Check the `session_token` cookie
if not user_session_token:
user_session_token = get_cookie(req, 'session_token')
if user_session_token:
user, _ = user_manager.authenticate_user_session(user_session_token)
return user is not None
def authenticate(
redirect_page='',
skip_auth_methods=None,
json=False,
):
"""
Authentication decorator for Flask routes.
"""
def decorator(f):
@wraps(f)
def wrapper(*args, **kwargs):
auth_status = get_auth_status(
request,
skip_auth_methods=skip_auth_methods,
)
if auth_status == AuthStatus.OK:
return f(*args, **kwargs)
if json:
return jsonify(auth_status.to_dict()), auth_status.value.code
if auth_status == AuthStatus.NO_USERS:
return redirect(
f'/register?redirect={redirect_page or request.url}', 307
)
if auth_status == AuthStatus.UNAUTHORIZED:
return redirect(f'/login?redirect={redirect_page or request.url}', 307)
return Response(
'Authentication required',
401,
{'WWW-Authenticate': 'Basic realm="Login required"'},
)
return wrapper
return decorator
# pylint: disable=too-many-return-statements
def get_auth_status(req, skip_auth_methods=None) -> AuthStatus:
"""
Check against the available authentication methods (except those listed in
``skip_auth_methods``) if the user is properly authenticated.
"""
n_users = user_manager.get_user_count()
skip_methods = skip_auth_methods or []
# User/pass HTTP authentication
http_auth_ok = True
if n_users > 0 and 'http' not in skip_methods:
http_auth_ok = authenticate_user_pass(req)
if http_auth_ok:
return AuthStatus.OK
# Token-based authentication
token_auth_ok = True
if 'token' not in skip_methods:
token_auth_ok = authenticate_token(req)
if token_auth_ok:
return AuthStatus.OK
# Session token based authentication
session_auth_ok = True
if n_users > 0 and 'session' not in skip_methods:
return AuthStatus.OK if authenticate_session(req) else AuthStatus.UNAUTHORIZED
# At least a user should be created before accessing an authenticated resource
if n_users == 0 and 'session' not in skip_methods:
return AuthStatus.NO_USERS
if ( # pylint: disable=too-many-boolean-expressions
('http' not in skip_methods and http_auth_ok)
or ('token' not in skip_methods and token_auth_ok)
or ('session' not in skip_methods and session_auth_ok)
):
return AuthStatus.OK
return AuthStatus.UNAUTHORIZED

View File

@ -0,0 +1,21 @@
from collections import namedtuple
from enum import Enum
StatusValue = namedtuple('StatusValue', ['code', 'message'])
class AuthStatus(Enum):
"""
Models the status of the authentication.
"""
OK = StatusValue(200, 'OK')
UNAUTHORIZED = StatusValue(401, 'Unauthorized')
NO_USERS = StatusValue(412, 'Please create a user first')
def to_dict(self):
return {
'code': self.value[0],
'message': self.value[1],
}

View File

@ -0,0 +1,64 @@
from redis import Redis
from platypush.bus.redis import RedisBus
from platypush.config import Config
from platypush.context import get_backend
from platypush.message import Message
from platypush.message.request import Request
from platypush.utils import get_redis_queue_name_by_message
from .logger import logger
_bus = None
def bus():
global _bus
if _bus is None:
redis_queue = get_backend('http').bus.redis_queue # type: ignore
_bus = RedisBus(redis_queue=redis_queue)
return _bus
def send_message(msg, wait_for_response=True):
msg = Message.build(msg)
if msg is None:
return
if isinstance(msg, Request):
msg.origin = 'http'
if Config.get('token'):
msg.token = Config.get('token')
bus().post(msg)
if isinstance(msg, Request) and wait_for_response:
response = get_message_response(msg)
logger().debug('Processing response on the HTTP backend: {}'.format(response))
return response
def send_request(action, wait_for_response=True, **kwargs):
msg = {'type': 'request', 'action': action}
if kwargs:
msg['args'] = kwargs
return send_message(msg, wait_for_response=wait_for_response)
def get_message_response(msg):
redis = Redis(**bus().redis_args)
redis_queue = get_redis_queue_name_by_message(msg)
if not redis_queue:
return
response = redis.blpop(redis_queue, timeout=60)
if response and len(response) > 1:
response = Message.build(response[1])
else:
response = None
return response

View File

@ -0,0 +1,31 @@
import logging
from platypush.config import Config
_logger = None
def logger():
global _logger
if not _logger:
log_args = {
'level': logging.INFO,
'format': '%(asctime)-15s|%(levelname)5s|%(name)s|%(message)s',
}
level = (Config.get('backend.http') or {}).get('logging') or (
Config.get('logging') or {}
).get('level')
filename = (Config.get('backend.http') or {}).get('filename')
if level:
log_args['level'] = (
getattr(logging, level.upper()) if isinstance(level, str) else level
)
if filename:
log_args['filename'] = filename
logging.basicConfig(**log_args)
_logger = logging.getLogger('platypush:web')
return _logger

View File

@ -0,0 +1,59 @@
import importlib
import inspect
import os
import pkgutil
from platypush.backend import Backend
from platypush.config import Config
from platypush.utils import get_ip_or_hostname
from .logger import logger
def get_http_port():
from platypush.backend.http import HttpBackend
http_conf = Config.get('backend.http') or {}
return http_conf.get('port', HttpBackend._DEFAULT_HTTP_PORT)
def get_routes():
base_pkg = '.'.join([Backend.__module__, 'http', 'app', 'routes'])
base_dir = os.path.join(
os.path.dirname(inspect.getfile(Backend)), 'http', 'app', 'routes'
)
routes = []
for _, mod_name, _ in pkgutil.walk_packages([base_dir], prefix=base_pkg + '.'):
try:
module = importlib.import_module(mod_name)
if hasattr(module, '__routes__'):
routes.extend(module.__routes__)
except Exception as e:
logger.warning('Could not import module %s', mod_name)
logger.exception(e)
continue
return routes
def get_local_base_url():
http_conf = Config.get('backend.http') or {}
bind_address = http_conf.get('bind_address')
if not bind_address or bind_address == '0.0.0.0':
bind_address = 'localhost'
return '{proto}://{host}:{port}'.format(
proto=('https' if http_conf.get('ssl_cert') else 'http'),
host=bind_address,
port=get_http_port(),
)
def get_remote_base_url():
http_conf = Config.get('backend.http') or {}
return '{proto}://{host}:{port}'.format(
proto=('https' if http_conf.get('ssl_cert') else 'http'),
host=get_ip_or_hostname(),
port=get_http_port(),
)

View File

@ -0,0 +1,37 @@
import os
import importlib
import inspect
from typing import List, Type
import pkgutil
from ..ws import WSRoute, logger
def get_ws_routes() -> List[Type[WSRoute]]:
"""
Scans for websocket route objects.
"""
from platypush.backend.http import HttpBackend
base_pkg = '.'.join([HttpBackend.__module__, 'app', 'ws'])
base_dir = os.path.join(os.path.dirname(inspect.getfile(HttpBackend)), 'app', 'ws')
routes = []
for _, mod_name, _ in pkgutil.walk_packages([base_dir], prefix=base_pkg + '.'):
try:
module = importlib.import_module(mod_name)
except Exception as e:
logger.warning('Could not import module %s', mod_name)
logger.exception(e)
continue
for _, obj in inspect.getmembers(module):
if (
inspect.isclass(obj)
and not inspect.isabstract(obj)
and issubclass(obj, WSRoute)
):
routes.append(obj)
return routes

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