501 lines
21 KiB
Markdown
501 lines
21 KiB
Markdown
Platypush
|
|
=========
|
|
|
|
[![Build Status](https://ci.platypush.tech/status.svg)](https://ci.platypush.tech)
|
|
[![Documentation Status](https://readthedocs.org/projects/platypush/badge/?version=latest)](https://docs.platypush.tech/en/latest/)
|
|
[![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/)
|
|
[![Contributions](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://git.platypush.tech/platypush/platypush/-/issues)
|
|
|
|
- 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 [wiki](https://git.platypush.tech/platypush/platypush/-/wikis/home) also contains many resources on getting started.
|
|
|
|
- Extensive documentation for all the available integrations and messages is available on [ReadTheDocs](https://docs.platypush.tech/en/latest/).
|
|
|
|
- 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.
|
|
|
|
---
|
|
|
|
Platypush is a general-purpose extensible platform for automation and integration 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*)
|
|
and it provides a comprehensive and customizable user interface that collects 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.
|
|
|
|
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.
|
|
|
|
You can use Platypush to do things like:
|
|
|
|
- [Control your smart home lights](https://blog.platypush.tech/article/Ultimate-self-hosted-automation-with-Platypush)
|
|
- [Control your music and synchronize it to multiple devices](https://blog.platypush.tech/article/Build-your-open-source-multi-room-and-multi-provider-sound-server-with-Platypush-Mopidy-and-Snapcast)
|
|
- [Create custom and privacy-secure voice assistants that run custom hooks on your phrases](https://blog.platypush.tech/article/Build-custom-voice-assistants)
|
|
- Build integrations between [sensors](https://docs.platypush.tech/en/latest/platypush/backend/sensor.html),
|
|
[cameras](https://docs.platypush.tech/en/latest/platypush/plugins/camera.pi.html),
|
|
[microphones](https://docs.platypush.tech/en/latest/platypush/plugins/sound.html) and
|
|
[machine learning models](https://docs.platypush.tech/en/latest/platypush/plugins/tensorflow.html) to create smart
|
|
pieces of automation for e.g.
|
|
[people detection](https://blog.platypush.tech/article/Detect-people-with-a-RaspberryPi-a-thermal-camera-Platypush-and-a-pinch-of-machine-learning)
|
|
or [sound detection](https://blog.platypush.tech/article/Create-your-smart-baby-monitor-with-Platypush-and-Tensorflow)
|
|
- [Get events from your Google or Facebook calendars](https://docs.platypush.tech/en/latest/platypush/plugins/calendar.html)
|
|
- [Read data from your sensors and trigger custom events whenever they go above or below some custom thresholds](https://blog.platypush.tech/article/How-to-build-your-personal-infrastructure-for-data-collection-and-visualization)
|
|
- [Control and automate a self-built robot](https://docs.platypush.tech/en/latest/platypush/plugins/gpio.zeroborg.html)
|
|
- [Deliver automated newsletters from custom RSS digests](https://blog.platypush.tech/article/Deliver-customized-newsletters-from-RSS-feeds-with-Platypush)
|
|
- [Synchronize the clipboards on your devices](https://docs.platypush.tech/en/latest/platypush/plugins/clipboard.html)
|
|
- [Control your smart switches](https://docs.platypush.tech/en/latest/platypush/plugins/switch.html)
|
|
- [Implement automated custom text-to-speech routines](https://docs.platypush.tech/en/latest/platypush/plugins/tts.html)
|
|
- [Build any kind of interactions and automation routines with your Android device using Tasker](https://blog.platypush.tech/article/How-to-build-your-personal-infrastructure-for-data-collection-and-visualization)
|
|
- Play [local videos](https://docs.platypush.tech/en/latest/platypush/plugins/media.mpv.html), YouTube videos and torrent media from any device and service, to any device
|
|
- [Get weather forecast events for your location and build automation routines on them](https://docs.platypush.tech/en/latest/platypush/plugins/weather.darksky.html)
|
|
- [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](https://docs.platypush.tech/en/latest/plugins.html)
|
|
|
|
They 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](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](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.
|
|
|
|
## Installation
|
|
|
|
### System installation
|
|
|
|
Platypush uses Redis to deliver and store requests and temporary messages:
|
|
|
|
```yaml
|
|
# Example for Debian-based distributions
|
|
[sudo] apt-get install redis-server
|
|
|
|
# Enable and start the service
|
|
[sudo] systemctl enable redis
|
|
[sudo] systemctl start redis
|
|
```
|
|
|
|
To install the core platform:
|
|
|
|
* The `pip` way:
|
|
|
|
```shell
|
|
[sudo] pip3 install platypush
|
|
```
|
|
|
|
* The sources way:
|
|
|
|
```shell
|
|
git clone https://git.platypush.tech/platypush/platypush.git
|
|
cd platypush
|
|
[sudo] python3 setup.py install
|
|
```
|
|
|
|
Then install the extensions that you wish to use. There are a few ways to check the dependencies required by an
|
|
extension:
|
|
|
|
#### Check their `extras` name in [`extras_require` under `setup.py`](https://git.platypush.tech/platypush/platypush/-/blob/master/setup.py#L72).
|
|
|
|
If you follow this route then you can install the extra dependencies in one of the following ways:
|
|
|
|
1. `pip` installation:
|
|
|
|
```shell
|
|
[sudo] pip3 install 'platypush[extra1,extra2,extra3]'
|
|
```
|
|
|
|
2. Sources installation:
|
|
|
|
```shell
|
|
cd $DIR_TO_PLATYPUSH
|
|
[sudo] pip3 install '.[extra1,extra2,extra3]'
|
|
```
|
|
|
|
#### Check the dependencies/installation instructions reported under the plugin/backend documentation.
|
|
|
|
If you follow this route then simply run the commands listed in the plugin/backend documentation to get the dependencies
|
|
installed.
|
|
|
|
#### Check/uncomment the associated lines in [`requirements.txt`](https://git.platypush.tech/platypush/platypush/-/blob/master/requirements.txt).
|
|
|
|
If you follow this route then uncomment the lines in
|
|
[`requirements.txt`](https://git.platypush.tech/platypush/platypush/-/blob/master/requirements.txt) associated to the
|
|
plugins/backends that you want to use and run:
|
|
|
|
```shell
|
|
[sudo] pip3 install -r requirements.txt
|
|
```
|
|
|
|
After installing the dependencies, create a configuration file under `~/.config/platypush/config.yaml` (the application
|
|
can load the configuration from another location through the `-c` option) containing the configuration of the backend
|
|
and plugins that you want to use, and add any hooks and procedures for your use case.
|
|
|
|
You can then start the service by simply running:
|
|
|
|
```shell
|
|
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) 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`:
|
|
|
|
```shell
|
|
systemctl --user start platypush
|
|
```
|
|
|
|
### [Virtual environment installation](https://git.platypush.tech/platypush/platypush/-/wikis/Run-platypush-in-a-virtual-environment)
|
|
|
|
Platypush provides a script named `platyvenv` that can parse a `config.yaml` and automatically create a virtual
|
|
environment (under `~/.local/share/platypush/venv/<device_id>`) with all the dependencies required by the configured
|
|
integrations.
|
|
|
|
1. Create the environment from a configuration file:
|
|
|
|
```shell
|
|
platyvenv build -c /path/to/config.yaml
|
|
```
|
|
|
|
2. Start the service from the virtual environment:
|
|
|
|
```shell
|
|
# device_id matches either the hostname or the device_id in config.yaml
|
|
platyvenv start device_id
|
|
```
|
|
|
|
3. Stop the instance:
|
|
|
|
```shell
|
|
platyvenv stop device_id
|
|
```
|
|
|
|
4. Remove the instance:
|
|
|
|
```shell
|
|
platyvenv rm device_id
|
|
```
|
|
|
|
### [Docker installation](https://git.platypush.tech/platypush/platypush/-/wikis/Run-platypush-in-a-container)
|
|
|
|
You can also install Platypush in a container - the application provides a script named `platydock` that automatically
|
|
creates a container instance from a `config.yaml`:
|
|
|
|
1. Create the container from a configuration file:
|
|
|
|
```shell
|
|
platydock build -c /path/to/config.yaml
|
|
```
|
|
|
|
2. Start the container:
|
|
|
|
```shell
|
|
# device_id matches either the hostname or the device_id in config.yaml
|
|
platydock start device_id
|
|
```
|
|
|
|
3. Stop the instance:
|
|
|
|
```shell
|
|
platydock stop device_id
|
|
```
|
|
|
|
4. Remove the instance:
|
|
|
|
```shell
|
|
platyvdock rm device_id
|
|
```
|
|
|
|
## 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
|
|
```
|
|
|
|
---
|
|
|
|
## Funding
|
|
|
|
If you use and love Platypush, please consider [buying me a coffee/beer](https://paypal.me/fabiomanganiello).
|
|
|
|
I've been working on Platypush all by myself in my spare time for the past few years, and I've made sure that it remains
|
|
open and free.
|
|
|
|
If you like this product, please consider supporting - I'm definitely not planning to get rich with this project, but
|
|
I'd love to have at least the costs for the server covered by users.
|
|
|
|
Issues and requests opened by donors will also be given priority over others.
|