#platypush — Public Fediverse posts
Live and recent posts from across the Fediverse tagged #platypush, aggregated by home.social.
-
If you’re tired of getting spied through your #weather app, but you still want all the features of your commercial/spyware weather apps, then #Platypush has a few solutions to get you covered.
-
Turn Platypush into your favourite weather app
Mobile app, web app and custom handling of weather events, all in one
#weather apps are one of those things that nowadays we take for granted, and most of us consider a largely solved problem.
After all, a weather app mostly consists in a simple UI that fetches the weather conditions from some API, optionally by retrieving your current location, and then it displays the information to the user.
Optionally, it can provide little perks like weather notifications.
The average usual app is simple and boring, and it's usually nothing that a decent student at the last year of engineering college wouldn't be able to put together in a weekend.
And yet, because of how easy it is to build a simple weather app, how pervasive these apps are on everyone's phones, and the amount of data that they collect about the user (which vastly justifies the small development cost), weather apps have become a favourite vector for tracking users.
They are also favourite ads delivery instruments.
The American government itself uses weather apps to track its citizens, and ICE buys data from weather apps to track potential targets.
After all, weather apps are among the few apps that:
- Are easy to build and deploy
- Are usually installed by everyone who has a phone
- Continuously collect location data about users
What if it could be different?
Of course, there are many weather apps on F-Droid that actually do a decent job respecting users' privacy, and if you want something simple that only runs on your phone many of them may match your needs. But in my opinion that only solves part of the problem.
Those apps still need to be installed on each of your mobile or tablet devices. And on desktop you'll need a different service anyway.
And any weather notifications will be limited to the mobile device that receives them, which limits what you can do with them - what if I want to use my weather app to record all the weather measurements in a certain area? What if I also want to send a message on my family's messaging group if it starts snowing in my area?
This is where a weather solution based on Platypush comes handy.
Pros:
-
Fully self-hosted. Run the #platypush service on any device that can run a #python interpreter, from a cheap VPS to a RaspberryPi, expose it over an HTTPS URL, and any device can use the app.
-
No mobile apps required. Platypush provides a Progressive Web Application (PWA), which means that you can open the Web interface from your browser, install the PWA directly from your browser, and have it on your home screen just like a native app.
-
Full control over weather notifications. Weather updates are handled as standard Platypush events, which means that you can write your own custom hooks to deliver notifications, store weather measurements, send notifications over other channels, and so on.
Getting started
The first step is to get an OpenWeatherMap API key.
Then create a simple configuration for Platypush under
/your/platypush/config/config.yaml:# Enable the Web server backend.http: port: 8008 # Enable the OpenWeatherMap plugin weather.openweathermap: token: YOUR_OPENWEATHERMAP_API_KEY # Specify a location by name location: Amsterdam,NL # Or by coordinates # lat: 52.372829 # long: 4.893680 # How often the plugin should check for new data, in seconds poll_interval: 300 # metric or imperial units # units: metricThen install Platypush, or run it directly through the Docker image:
docker run --rm \ --name platypush \ -p 8008:8008 \ -v /your/platypush/config:/etc/platypush \ -v /your/platypush/data:/var/lib/platypush \ -e PLATYPUSH_DEVICE_ID=WeatherApp \ quay.io/platypush/platypushOnce started, you can open the Web interface at http://localhost:8008 to register your user.
Once logged in, you can click on the
weather.openweathermaptab from the left menu to immediately access your weather forecast:HTTPS configuration
A PWA requires an HTTPS connection, or the Web service to be installed on
localhost.The
localhostinstallation of Platypush is also possible on Android via Termux, but it's out of the scope of this article.We can use a reverse proxy and Certbot to make the Web interface available at e.g.
https://weather.platypush.example.com:This requires:
- A registered domain name (e.g.
weather.platypush.example.com) - A box with a public IP (e.g. a VPS)
- A reverse proxy (e.g. nginx) installed on the box
- Certbot installed on the box to generate and maintain the SSL certificate
[Optional] Set up a VPN for the reverse proxy
This step is optional.
You can also run the Platypush weather service on the same box as the reverse proxy, and in such cases you don't need to set up a VPN for the reverse proxy.
If your Platypush service runs on the same machine as the reverse proxy, you can skip to Reverse proxy configuration.
Otherwise, It's recommended if you want your reverse proxy to tunnel HTTP requests to e.g. your RaspberryPi or old Android tablet at home that runs the Platypush service, without exposing those IP addresses directly to the Internet.
A quick solution involves setting up your machine with a public IP to also run a Wireguard tunnel to your local machine, so the reverse proxy can directly access your local Platypush installation without leaking your own IP to the Internet.
Server configuration
A common set up, if your machine runs Linux with systemd, involves using the
wg-quickutility to create a Wireguard tunnel, and then setting up a systemd service to start the tunnel at boot time.wg-quickis usually provided by thewireguard-toolspackage on most of the UNIX-like installations.Wireguard peers authenticate each other through public keys. In this example:
- The VPS/reverse proxy gets the VPN address
10.0.0.1. - The Platypush machine gets the VPN address
10.0.0.2. - The server only accepts one client, the one whose public key is listed in the server's
[Peer]section.
Start by generating the server keypair on the VPS:
sudo install -d -m 700 /etc/wireguard # Generate the server key wg genkey | sudo tee /etc/wireguard/private.key > /dev/null sudo chmod 600 /etc/wireguard/private.key # Generate a public key sudo cat /etc/wireguard/private.key | wg pubkey | sudo tee /etc/wireguard/public.key # Print this value. You will need it in the client configuration. sudo cat /etc/wireguard/public.keyThen generate the client keypair on the Platypush machine:
sudo install -d -m 700 /etc/wireguard # Generate the client key wg genkey | sudo tee /etc/wireguard/private.key > /dev/null sudo chmod 600 /etc/wireguard/private.key # Generate a public key sudo cat /etc/wireguard/private.key | wg pubkey | sudo tee /etc/wireguard/public.key # Print this value. You will need it in the server configuration. sudo cat /etc/wireguard/public.keyNow go back to the VPS and create the Wireguard server configuration.
Replace
<The public key of the client>with the public key printed by the Platypush machine in the previous step:CLIENT_PUBLIC_KEY="<The public key of the client>" SERVER_PRIVATE_KEY="$(sudo cat /etc/wireguard/private.key)" sudo tee /etc/wireguard/wg0.conf > /dev/null <<EOF [Interface] PrivateKey = ${SERVER_PRIVATE_KEY} Address = 10.0.0.1/32 ListenPort = 9929 [Peer] PublicKey = ${CLIENT_PUBLIC_KEY} AllowedIPs = 10.0.0.2/32 EOF unset SERVER_PRIVATE_KEY sudo chmod 600 /etc/wireguard/wg0.confThe
AllowedIPs = 10.0.0.2/32line is important: it tells Wireguard that only the peer with the configured client public key is allowed to use the10.0.0.2tunnel address. Unknown clients, or clients with a different private key, won't be able to complete the tunnel handshake.If your VPS firewall blocks inbound traffic by default, allow the Wireguard UDP port. For example, with
ufw:sudo ufw allow 9929/udp comment 'Wireguard'Now start the tunnel on the VPS:
# Enable and start the tunnel sudo systemctl enable --now wg-quick@wg0 # Verify that the tunnel is up sudo wg show sudo systemctl status wg-quick@wg0Client configuration
The Platypush machine now needs a configuration that points back to the VPS.
Run the following commands on your Platypush machine:
SERVER_PUBLIC_KEY="<The public key of the server>" SERVER_ENDPOINT="<The public IP or DNS name of the server>" CLIENT_PRIVATE_KEY="$(sudo cat /etc/wireguard/private.key)" sudo tee /etc/wireguard/wg0.conf > /dev/null <<EOF [Interface] PrivateKey = ${CLIENT_PRIVATE_KEY} Address = 10.0.0.2/32 [Peer] PublicKey = ${SERVER_PUBLIC_KEY} AllowedIPs = 10.0.0.1/32 Endpoint = ${SERVER_ENDPOINT}:9929 PersistentKeepalive = 25 EOF unset CLIENT_PRIVATE_KEY sudo chmod 600 /etc/wireguard/wg0.conf sudo systemctl enable --now wg-quick@wg0 # Verify the tunnel and the route to the reverse proxy. sudo wg show ping -c 3 10.0.0.1AllowedIPs = 10.0.0.1/32keeps the client configuration narrow: only traffic for the VPN address of the reverse proxy goes through this tunnel.PersistentKeepalive = 25is useful when the Platypush machine is behind a home router or mobile NAT, because it keeps the tunnel mapping alive so the reverse proxy can reach10.0.0.2:8008.Once the client is up, verify from the VPS that the reverse proxy can reach the Platypush Web service through the tunnel:
ping -c 3 10.0.0.2 curl -I http://10.0.0.2:8008/Reverse proxy
Assuming that these conditions are met, proceed with creating a reverse proxy configuration for the Platypush Web interface:
upstream platypush-weather { # Or just 127.0.0.1:8008 if the Platypush service runs on the same box server 10.0.0.2:8008; } server { # Replace with your own domain # NOTE: The DNS entry of this domain must point to the reverse proxy server_name weather.platypush.example.com; listen 80; # Standard HTTP resources location / { 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_pass http://platypush-weather; } # WebSockets location /ws/ { client_max_body_size 5M; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_redirect off; proxy_http_version 1.1; proxy_set_header Host $http_host; proxy_pass http://platypush-weather; } }Apply the configuration and reload your reverse proxy:
sudo nginx -t sudo nginx -s reloadThen verify that the reverse proxy can reach the Platypush Web service through the tunnel:
curl -I http://weather.platypush.example.com/Generate a certificate
Run the following commands on your reverse proxy machine:
sudo certbot --nginx -d weather.platypush.example.comThen verify that the reverse proxy can reach the Platypush Web service through the tunnel over HTTPS:
curl -I https://weather.platypush.example.com/Installing the mobile app
Open
https://<weather.platypush.example.com>/plugin/weather.openweathermapfrom your browser on a mobile device.Tap on your browser's menu. You should see an entry like "Add to Home Screen" or "Install app".
Select to install the app and add it to your home screen.
You can search for other locations directly in the search bar, or use the GPS button to find your current location.
The GPS access permissions are optional, they are requested directly in your browser and only when you use the app, and there's nothing monitoring your location in the background (unless you actually want to monitor it explicitly).
[Optional] Handling weather events
If you only need a weather app that works on desktop and mobile when you want to check it, then you can skip this section.
If instead you would also like to handle events from the weather service, then you can create Platypush event hooks to handle
NewWeatherConditionEvent.Every time the Platypush service processed a weather update, it will emit events with the following payload:
{ "type": "event", "target": "WeatherApp", "origin": "WeatherApp", "id": "f7c26249a0b4eec5c04831fbfe11cdcd", "args": { "type": "platypush.message.event.weather.NewWeatherConditionEvent", "plugin_name": "weather.openweathermap", "summary": "Clear", "icon": "01d", "precip_intensity": 0, "precip_type": null, "temperature": 25.46, "apparent_temperature": 25.81, "humidity": 67, "pressure": 1020, "wind_speed": 3.58, "wind_gust": 6.71, "wind_direction": 344, "cloud_cover": 5, "visibility": 10000.0, "sunrise": "2026-07-11T03:31:24+00:00", "sunset": "2026-07-11T19:59:52+00:00", "units": "metric", "time": "2026-07-11T18:52:05" } }Values:
temperature: temperature in degrees Celsius (ifunits: metric) or degrees Fahrenheit (ifunits: imperial)apparent_temperature: apparent temperature in degrees Celsius (ifunits: metric) or degrees Fahrenheit (ifunits: imperial)wind_speed: wind speed in meters per secondwind_gust: wind gust in meters per secondvisibility: visibility in meterspressure: pressure in hPacloud_cover: cloud cover in percentage (0-100)precip_intensity: precipitation intensity in mm/hprecip_type: rain, snow or hail
You can subscribe to these events, for example, for:
- Sending a notification to your mobile when it starts raining
- Sending an alert on an XMPP or Matrix channel if the visibility drops below a certain threshold
- Sending a message to your family's messaging group if it starts snowing in your area
- Storing the weather data in a database for later analysis
ntfy
We'll use
ntfyto send notifications to your mobile device, paired with the Platypushntfyplugin.You can install the Android (F-Droid link) or iOS app to receive notifications on your mobile device.
By default, the app and the Platypush plugin will connect to the default ntfy server (
https://ntfy.sh/app).That's an option, but in that case make sure to always use authentication or topic names with randomized strings.
Otherwise, you can run your own ntfy instance to send and receive your notifications on a fully self-hosted setup.
The easiest way is perhaps through Docker:
docker run \ -v /your/ntfy/config:/etc/ntfy \ -v /your/ntfy/cache:/var/cache/ntfy \ -p 8080:80 \ -it \ binwiederhier/ntfy \ serve \ --cache-file /var/cache/ntfy/cache.dbThen create a reverse proxy configuration with a certificate like shown in Reverse proxy configuration.
Plugin configuration
Add the
ntfyplugin to yourconfig.yamlfile for Platypush:ntfy: # Optional, if using a custom server, otherwise ntfy.sh is used # server_url: https://ntfy.example.comNotifying of precipitation events
Create a Platypush event hook to send notifications to your mobile device:
# Content of /your/platypush/config/scripts/weather_notifications.py import json from functools import wraps from time import time from platypush import Variable, cron, run from platypush.events.weather import NewWeatherConditionEvent # The weather notifications topic. Replace `1234` with a random string weather_notifications_topic = "weather-notifications-1234" # A Platypush variable to persist the last weather notification timestamp last_weather_state_notification_timestamp = Variable( "LAST_WEATHER_STATE_NOTIFICATION_TIMESTAMP" ) # A Platypush variable to persist the latest notified weather state, # so notifications will be delivered when the state changes, even if # they are within the `weather_state_notification_timeout` last_notified_weather_state = Variable("LAST_NOTIFIED_WEATHER_STATE") # The maximum time between weather notifications weather_state_notification_timeout = ( 60 * 60 * 60 # At most one weather notification every 30 minutes ) def notification_throttled(func): """ Prevents the decorated function from running too often, unless the weather state has changed since the last notification. """ @wraps(func) def wrapper(*args, **kwargs): event = args[0] weather_state = json.dumps(event.args, sort_keys=True) last_weather_state = last_notified_weather_state.get() notification_is_throttled = ( time() - float(last_weather_state_notification_timestamp.get() or 0) <= weather_state_notification_timeout ) if notification_is_throttled and weather_state == last_weather_state: return result = func(*args, **kwargs) last_weather_state_notification_timestamp.set(time()) last_notified_weather_state.set(weather_state) return result return wrapper def precipitation_notification(event: NewWeatherConditionEvent) -> dict | None: """ Returns a notification about precipitation for ntfy, if any. """ if (event.precip_intensity or 0) < 0.1: return # Negligible or absent precipitation precip_type = (event.precip_type or "rain").lower() if event.precip_intensity < 2: precip_intensity = "light" priority = 2 elif event.precip_intensity < 5: precip_intensity = "moderate" priority = 3 else: precip_intensity = "heavy" priority = 4 if precip_type == "snow": precip_icon = "❄️" elif precip_type == "hail": precip_icon = "🌧️" else: precip_icon = "💧" run( "ntfy.send_message", topic=weather_notifications_topic, ) return { "title": f"{precip_icon} Weather update", "message": f"{precip_intensity.capitalize()} {precip_type} in your area", "priority": priority, } @hook(NewWeatherConditionEvent) @notification_throttled def on_weather_update(event: NewWeatherConditionEvent, *_, **__): precip = precipitation_notification(event) if precip: run( "ntfy.send_message", topic=weather_notifications_topic, **precip, )Then install the ntfy app on your mobile device or use the Web interface, and subscribe to the
weather-notifications-1234topic on the configured server.You'll be notified whenever there's precipitation in your area.
Of course, you can modify your hook to deliver any kind of relevant notifications in your area - about wind, temperature, humidity, etc.
And actions are not limited to ntfy. If you prefer, you can deliver the notification over email, ActivityPub, Matrix, Telegram, SMS, XMPP or anything that has a Platypush plugin.
Weather summaries
Another useful feature of many mainstream weather apps is that of a daily summary (usually in the morning) of the weather in a certain location, so you can plan your day accordingly.
This can be easily achieved too through a Platypush cronjob.
# Content of /your/platypush/config/scripts/weather_report.py from datetime import datetime from platypush import cron, procedure, run weather_plugin = "weather.openweathermap" # The weather notifications topic. Replace `1234` with a random string weather_notifications_topic = "weather-notifications-1234" @procedure("deliver_weather_report") def deliver_weather_report(): """ Procedure that sends a weather report to ntfy. """ forecast = [ { "icon": weather["icon"], "temperature": weather["temperature"], "summary": weather["summary"], "time": weather["time"], } for weather in run(f"{weather_plugin}.get_forecast") if datetime.fromisoformat(weather["time"]).day == datetime.now().day ] current_weather = { "time": datetime.now().isoformat(), **run(f"{weather_plugin}.get_current_weather"), } body = "" for forecast_entry in [current_weather, *forecast]: body += ( f"`{datetime.fromisoformat(forecast_entry['time']).strftime('%H:%M')}`\n" f"**{round(forecast_entry['temperature'])}°** " f"![{forecast_entry['summary']}](https://openweathermap.org/payload/api/media/file/{forecast_entry['icon']}.png)\n\n" ) run( "ntfy.send_message", topic=weather_notifications_topic, title=f"{round(current_weather['temperature'])}° - {current_weather['summary']}", message=body, icon=f"https://openweathermap.org/payload/api/media/file/{current_weather['icon']}.png", markdown=True, ) @cron("0 6 * * *") def daily_weather_report(): """ Run the `deliver_weather_report` procedure every day at 6 AM. """ run("procedure.deliver_weather_report")Restart the Platypush service, and every day at 6 AM, you'll get a summary of the weather in your area.
[Optional] Voice Assistant
A common use-case for voice assistant is to ask information about the weather, and Platypush can cover that too by running a voice assistant directly on your hardware.
The linked article describes how to run a fully local voice assistant, with local speech-to-text and text-to-speech engines.
But things also work if you decide to use remote models through the
assistant.openaiortts.openaiplugins.You can use the
assistant-samplerepository to quickly get started with a Docker image with a Platypush installation configured to run a voice assistant.Some sample configuration, using
assistant.openwakewordfor hotword detection together withassistant.openaiandtts.openai:assistant.openwakeword: detection_sensitivity: 0.3 models: - alexa openai: model: gpt-5.5 api_key: <YOUR-OPENAI-API-KEY> assistant.openai: tts_plugin: tts.openai conversation_start_sound: /usr/share/sounds/assistant.mp3 input_volume: 110 tts.openai: output_volume: 85You can then add a script with an event hook on
SpeechRecognizedEventand reacts to weather requests:# Content of /your/platypush/config/scripts/weather_assistant.py import json from dataclasses import dataclass from datetime import datetime from time import time import requests from platypush import Config, run, __version__ as platypush_version from platypush.events.assistant import ( HotwordDetectedEvent, SpeechRecognizedEvent, ) ai_plugin = "openai" assistant_plugin = "assistant.openai" weather_plugin = "weather.openweathermap" location_cache: dict[str, tuple[float, float]] = {} @dataclass class DefaultLocation: """ A utility class to automatically and lazy manage the default weather location. """ _location: str | None = None @property def name(self) -> str | None: if self._location: return self._location # Get the weather plugin configuration cfg = Config.get().get(weather_plugin) if not cfg: return None lat = cfg.get("lat") long = cfg.get("long") location = cfg.get("location") # If a location name is configured, use that if location: return location # Otherwise, reverse lookup the location from latitude and longitude self._location = ( run(f"{weather_plugin}.reverse_lookup_location", lat=lat, long=long) or {} ).get("name") return self._location default_location = DefaultLocation() @dataclass class WeatherRequest: """ A weather forecast request. """ location: str delta_days: int def __post_init__(self): if self.delta_days < 0: raise ValueError("delta_days must be positive") def get_location_coords(self) -> tuple[float, float] | None: """ Get the coordinates of the location. """ # Cache lookup coord = location_cache.get(self.location) if coord: return coord # Reverse geo lookup response = requests.get( f"https://nominatim.openstreetmap.org/search?q={self.location}&format=json&limit=1", headers={ "User-Agent": ( f"Mozilla/5.0 (compatible; Platypush/{platypush_version}; " "+https://git.platypush.tech/platypush/platypush)" ), }, timeout=10, ) response.raise_for_status() geojson = response.json() or [] if not geojson: return None lat, lng = (geojson[0] or {}).get("lat"), (geojson[0] or {}).get("lon") if not (lat and lng): return None location_cache[self.location] = lat, lng return lat, lng def get_time_range(self) -> tuple[datetime, datetime]: return ( datetime.fromtimestamp(time() + self.delta_days * 24 * 60 * 60), datetime.fromtimestamp(time() + (self.delta_days + 1) * 24 * 60 * 60), ) def get_structured_weather_forecast(prompt: str) -> dict: """ Get a structured weather forecast given a free text prompt. """ # Parse the free-text request request = parse_weather_request(prompt) response: dict = {} if not request: return response # Retrive the location for the weather request coord = request.get_location_coords() if not coord: return response # Get the weather forecast for the specified location and time frame lat, lng = coord time_range = request.get_time_range() forecast = [ { "location": request.location, **weather, } for weather in run( f"{weather_plugin}.get_forecast", lat=lat, long=lng, ) if time_range[0] <= datetime.fromisoformat(weather["time"]) <= time_range[1] ] # If the user requested the current weather, add it to the response is_today_forecast = request.delta_days == 0 current_weather = None if is_today_forecast: current_weather = forecast[0] if current_weather: response["now"] = current_weather # Construct the response if is_today_forecast: key = "today" elif request.delta_days == 1: key = "tomorrow" else: key = f"{request.delta_days}days" response[key] = forecast return response def parse_weather_request(request: str) -> WeatherRequest | None: """ Parse a weather request given a free text prompt. """ # Use the OpenAI plugin to parse the free-text user request into a # structured request request_dict = ( run( f"{ai_plugin}.get_response", context=[ { "role": "system", "content": ( "You are a voice assistant provided with weather requests as free text.\n" "Given the prompt, return a structured JSON representation of the request in the following format: " '{ "type": "weather", "delta_days": 1, "location": "San Francisco" }, ' 'where both delta_days and location are optional (e.g. if the user simply asks "How\'s the weather?".\n' 'If the prompt doesn\'t seem to contain a weather request, return { "type": null }' ), } ], prompt=request, ) or {} ) if request_dict.get("type") != "weather": return None weather_request = WeatherRequest( location=request_dict.get("location", default_location.name), delta_days=request_dict.get("delta_days", 0), ) return weather_request def get_weather_report(user_request: str) -> str | None: """ Get a weather free-text report given a free text prompt. """ structured_response = get_structured_weather_forecast(user_request) response = None if structured_response: # Use the OpenAI plugin to translate the structured weather forecast # response into a free-text report response = run( f"{ai_plugin}.get_response", prompt=json.dumps(structured_response), context=[ # General weather assistant system prompt { "role": "system", "content": ( "You are a weather voice assistant that translates JSON weather reports " "into more informal weather reports. The output reports should be brief and to the " "point, but not miss relevant details from the original report." ), }, # JSON structure system prompt { "role": "system", "content": ( "If the JSON report contains a 'now' key, that's the current weather. " "Otherwise, it may contain either 'today', 'tomorrow' or '<n>days' in the future. " ), }, # Avoid abbreviations { "role": "system", "content": ( "Do not use abbreviations in your output text, always use the full text units. " "Also, strip any Markdown formatting from the output text. " "Keep in mind that your output text will be rendered verbatim by a text-to-speech engine." ), }, # Degrees and wind speed system prompt { "role": "system", "content": ( "Do not say 'Celsius' or 'Fahreneit', only 'degrees'. " "Wind speed is reported in km/h, but don't report the absolute number - " "just if the wind is absent, weak, medium or strong." ), }, # Precipitation system prompt { "role": "system", "content": ( "Precipitation intensity is expressed in mm/h. Do not report the absolute number. " "Only say if the precipitation is low, medium, high or very intense. " "If absent, don't say anything about precipitations. " "Otherwise, mention the precipitation type too (rain, snow, hail...). " "If any precipitations are present in the forecast, mention their intensity and " "around what time of the day they will happen." ), }, # Cloud cover system prompt { "role": "system", "content": ( "Cloud cover is reported as a percentage between 0 and 100. " "Do not report the absolute number - only a description of the cloud cover. " "In the forecast, mention a cloud cover description that matches a reasonable average among the data points." ), }, ], ) return response @when(HotwordDetectedEvent) def on_hotword_detected(): """ Start the conversation when the hotword is detected. """ run(f"{assistant_plugin}.start_conversation") @when(SpeechRecognizedEvent, phrase=".*weather.*") def on_weather_request(event): """ Respond to weather requests by intercepting `SpeechRecognizedEvent` events that contain a weather request (regex). """ response = ( get_weather_report(event.phrase) or "Sorry, I couldn't find any weather information for that location." ) event.assistant.render_response(response)Voice weather assistant flow
A full demo of how it looks and sounds like:
-
A local #RaspberryPi friendly voice assistant setup with #Platypush.
The setup (hotword detection, speech-to-text, text-to-speech, local plugins, #AI inference) can run completely on-device.
Ingredients:
openwakeword(hotword detection)vosk(local speech-to-text models)piper(local text-to-speech models)- [optional]
openai(speech-to-intent processing and free text responses). It can also plug to a local model served via e.g.ollama serve
-
Build a fully local voice assistant in 2026
A practical setup for a Raspberry Pi-friendly voice assistant based on Platypush.
Those who have followed me for a while know of my personal obsession with self-built voice assistants.
My experiments over the years can be summarized as it follows:
-
2007: Voxifera, my very first attempt at building a primitive voice assistant using Hidden Markov models. Definitely not good for general-purpose usage, but good enough in 2007 to distinguish between a dozen of simple voice commands.
-
2019: First voice assistant built on top of Platypush. It used the now deprecated Google Assistant Library on top of a Raspberry Pi with a microphone and a speaker, and it could hook any automation routines and custom commands to it through event hooks.
-
2020: Second iteration on #platypush, this time supporting other assistant plugins too - Alexa (integration now removed), Snowboy (also removed, since the project is dead), Mozilla DeepSpeech (also removed now, since Mozilla discontinued it), PicoVoice, and mimic3 (the text-to-speech engine built on top of Mycroft, now bankrupt).
-
2024: Third iteration on Platypush, this time with an enhanced PicoVoice integration and new speech-to-text and text-to-speech plugins based on the OpenAI APIs.
But it's now 2026, and perhaps both the hardware and the software are now mature enough for fully on-device voice assistants based on fully open solutions likely to stick around for a while.
In this article we'll wire that gap closed with Platypush:
assistant.openwakewordlistens for the wake word locally.assistant.vosktranscribes the command locally.tts.piperspeaks the answer locally.openaiis used only where a language model is useful: turning messy speech into intent, or answering general questions.- Existing home automation plugins such as
light.hue,music.mpdorweather.openweathermapto provide the actions.
The result is not another cloud assistant with a different coat of paint. The hotword engine, speech recognition, command dispatch and speech synthesis can all run on-device. If the
openaistep points to a local OpenAI-compatible server, then the whole pipeline can stay on your LAN too.The pipeline
The architecture can be summarized as follows:
Hotword detection ("OK Google", "Alexa" etc.) is a continuous, low-latency workload, and it should not need the network.
Speech-to-text is also a good fit for local inference: Vosk models are small enough to run on modest hardware, including Raspberry Pis, and they are perfectly adequate for short home automation commands.
Text-to-speech is another place where local models are good enough nowadays: Piper voices are fast, small and much nicer than the old robotic
espeak-style fallback.The only optional network-shaped piece is the language model.
But that is a policy choice, not a requirement of the voice stack.
Setup
Clone the assistant sample repository:
git clone https://git.platypush.tech/platypush/assistant-sample cd assistant-sampleModels
The next step is to download the voice models used by the voice stack.
Hotword Detection
When the service starts the first time, it will automatically download all the available models.
You can then use the following command to list the available models once the service is running:
curl -s -XPOST \ -H 'Content-type: application/json' \ -H "Authorization: Bearer $PLATYPUSH_TOKEN" \ -d '{"type":"request", "action":"assistant.openwakeword.list_models"}' \ http://localhost:8008/executeWhere
$PLATYPUSH_TOKENis the token of the user that is running the service.You can retrieve it by connecting to
http://localhost:8008when the service starts for the first time. Create your credentials, then select Settings -> Tokens -> Generate API Token.Speech-to-text
A full list of the Vosk voice models is available here.
Some feedback about the quality of the English models:
Model Size Notesvosk-model-small-en-us-0.1540 MB Very fast and lightweight model that can also run on an old Raspberry Pi, but accuracy can be low.vosk-model-en-us-0.22-lgraph128 MB Reasonably accurate on clear speech and with native speakers, but still small enough to run fine even on a Raspberry Pi.vosk-model-en-us-0.221.8 GB Accurate generic US English model. Fast on an laptop or x86 processor, but it may be a bit heavy on a Raspberry Pi.Download the selected model to the Docker volume working directory:
mkdir -p ./workdir/assistant.vosk/models cd ./workdir/assistant.vosk/models wget "https://alphacephei.com/vosk/models/vosk-model-en-us-0.22-lgraph.zip" unzip "vosk-model-en-us-0.22-lgraph.zip" rm "vosk-model-en-us-0.22-lgraph.zip"Text-to-speech
Download a speech synthesis model from here.
Audio samples are also available to get an idea of the type of voice before downloading.
The model usually consists of a
*.onnxand a*.onnx.jsonfile. Download both of them to the Docker volume working directory:mkdir -p ./workdir/piper_tts cd ./workdir/piper_tts wget "https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/hfc_female/medium/en_US-hfc_female-medium.onnx" wget "https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/hfc_female/medium/en_US-hfc_female-medium.onnx.json"Configuration
Copy and edit the example configuration file.
cp config/config.example.yaml config/config.yamlHome automation plugins
The assistant becomes useful once recognized speech can reach the rest of the house.
For example, Hue lights:
light.hue: bridge: hue groups: - Living RoomAnd MPD/Mopidy for music:
music.mopidy: host: localhost music.mpd: host: localhost poll_interval: nullThose are just regular Platypush plugins.
The assistant does not need special knowledge about Hue, MPD, Chromecast, Zigbee, MQTT or anything else.
It only needs to emit events; your hooks decide what to do with them.
Build
Build the container image for the assistant service:
docker build -t platypush-voice .Run
The assistant needs access to the host microphone and speakers. The container routes ALSA through PulseAudio, so the examples below connect it to a PulseAudio server running on the host.
Linux
With PulseAudio or
pipewire-pulseaudioinstalled:docker run --rm \ -e PULSE_SERVER=unix:/run/pulse/native \ -v /run/user/$(id -u)/pulse/native:/run/pulse/native \ --name voice-assistant \ -p 8008:8008 \ -v ./config:/etc/platypush \ -v ./workdir:/var/lib/platypush \ platypush-voicemacOS
Install and start PulseAudio on the host:
brew install pulseaudio pulseaudio --daemonize=yes --exit-idle-time=-1 pactl load-module module-native-protocol-tcp \ auth-anonymous=1 \ listen=0.0.0.0 \ port=4713Then start the container:
docker run --rm \ -e PULSE_SERVER=tcp:host.docker.internal:4713 \ --name voice-assistant \ -p 8008:8008 \ -v "$(pwd)/config:/etc/platypush" \ -v "$(pwd)/workdir:/var/lib/platypush" \ platypush-voiceIf
pactl load-modulereports that the module is already loaded, you can keep using the existing PulseAudio daemon.Windows
Install PulseAudio for Windows, then create a
default.pafile in the same directory aspulseaudio.exe:load-module module-waveout sink_name=output source_name=input record=1 load-module module-native-protocol-tcp auth-anonymous=1 listen=0.0.0.0 port=4713 set-default-sink output set-default-source inputStart PulseAudio from PowerShell:
.\pulseaudio.exe -F .\default.pa --exit-idle-time=-1Then start the container from the repository directory:
docker run --rm ` -e PULSE_SERVER=tcp:host.docker.internal:4713 ` --name voice-assistant ` -p 8008:8008 ` -v "${PWD}/config:/etc/platypush" ` -v "${PWD}/workdir:/var/lib/platypush" ` platypush-voiceMake sure microphone access is enabled for desktop applications under Windows privacy settings, and allow PulseAudio through the firewall if prompted.
Usage
Once the service is running, you can start interact with it with voice commands (the default activation word is "Alexa").
Any questions about the weather will be resolved by the weather plugin if it's been enabled.
If the music or lights plugins are enabled, they can be controlled with voice commands ("stop the music", "turn on the lights", etc.)
Otherwise, the assistant will use the
openaiplugin to respond to your questions, with follow-up turns when the response from OpenAI is also a question.Extending the Assistant
The assistant logic is modeled through simple Platypush hooks under
config/scripts.You can extend it as you like by defining your own hooks or modifying the existing ones.
Starting a conversation
Conversations are started by hooking to the
HotwordDetectedEvent.import logging from platypush import run, when from platypush.events.assistant import HotwordDetectedEvent logger = logging.getLogger(__name__) ai_plugin = "openai" assistant_plugin = "assistant.vosk" @when(HotwordDetectedEvent) def on_hotword_detected(event: HotwordDetectedEvent): """ When the hotword is detected, start a conversation. """ logger.info(f"Hotword {event.hotword} detected") run(f"{assistant_plugin}.start_conversation")Deterministic commands
For common home automation commands, regular event hooks are still the best tool. They are fast, inspectable, and they do not hallucinate.
from platypush import run, when from platypush.events.assistant import SpeechRecognizedEvent @when(SpeechRecognizedEvent, phrase="turn on (the)? lights") def turn_on_lights(): """ Hook run when the user says "turn on the lights" (regex) """ run("light.hue.on") @when(SpeechRecognizedEvent, phrase="play (the)? music") def play_music(): """ Hook run when the user says "play the music" (regex) """ run("music.mpd.play") @when(SpeechRecognizedEvent, phrase="set the music volume (to|on|at) ${volume}") def set_volume(volume: int): """ Hook run when the user says "set the music volume to ${volume}" (regex with parameter). """ run("music.mpd.set_volume", volume=volume)AI Commands
If the
openaiplugin is enabled, you can use it to help you answer questions.There are two generic use-cases for voice assistants where an AI plugin is beneficial:
- Speech to Intent
- Response fallback
Speech to Intent
You may want this for general questions, for commands that do not fit a neat regular expression, or for transforming a raw sentence such as:
make it a bit darker and reduce the music volume
into a structured action plan like.
[ { "action": "light.hue.set_lights", "args": { "bri": 50 } }, { "action": "music.mpd.set_volume", "args": { "volume": 20 } } ]An example provided in the assistant sample is that of weather forecasting.
Note in particular the usage of
openai.get_responsewith a well crafted system prompt that turns a natural language request like:What's the weather tomorrow in San Francisco?
Into:
{ "type": "weather", "delta_days": 1, "location": "San Francisco" }def parse_weather_request(request: str) -> WeatherRequest | None: request_dict = ( run( "openai.get_response", context=[ { "role": "system", "content": ( "You are a voice assistant provided with weather requests as free text.\n" "Given the prompt, return a structured JSON representation of the request in the following format: " '{ "type": "weather", "delta_days": 1, "location": "San Francisco" }, ' 'where both delta_days and location are optional (e.g. if the user simply asks "How\'s the weather?".\n' 'If the prompt doesn\'t seem to contain a weather request, return { "type": null }' ), } ], prompt=request, ) or {} ) if request_dict.get("type") != "weather": return None weather_request = WeatherRequest( location=request_dict.get("location", default_location), delta_days=request_dict.get("delta_days", 0), ) return weather_requestYou can also use the model for intermediate transformation instead of direct answers. For example, ask it to return a tiny JSON object with
actionandargs, then dispatch only actions you explicitly allow:ALLOWED_ACTIONS = { "lights.on": "light.hue.on", "lights.off": "light.hue.off", "music.play": "music.mpd.play", "music.stop": "music.mpd.stop", } @when(SpeechRecognizedEvent) def on_fuzzy_command(event): plan = run( "openai.get_response", prompt=event.phrase, context=[ { "role": "system", "content": ( "Map the user command to JSON only: " '{"action": "...", "args": {...}}. ' f"Allowed actions: {', '.join(ALLOWED_ACTIONS)}. " "If none match, return {\"action\": null, \"args\": {}}." ), } ], ) # Parse `plan` as JSON here, validate it, then run only an allow-listed action.That last validation step matters. A model may be useful for interpretation, but it should not get arbitrary access to
run().Response fallback
If a request doesn't match any of the commands you have defined, you can use a generic
SpeechRecognizedEventhook to forward the request to an AI plugin, and render the response as speech through the text-to-speech plugin.import logging from platypush import run, when from platypush.events.assistant import SpeechRecognizedEvent logger = logging.getLogger(__name__) ai_plugin = "openai" assistant_plugin = "assistant.vosk" @when(SpeechRecognizedEvent, plugin=assistant_plugin) def on_speech_recognized(event: SpeechRecognizedEvent): """ Generic handler for speech recognition events received by the configured assistant plugin. """ logger.info("Recognized speech: %s", event.phrase) # Forward the request to OpenAI and render the response as speech response = run( f"{ai_plugin}.get_response", prompt=event.phrase, context=[ { "role": "system", "content": ( "You are a voice assistant that can answer questions and perform actions. " "Keep in mind that prompts are transcriptions of user speech and they may " "contain misspellings or errors. Try and interpret them as best as possible. " "When possible, keep your answers short and concise." ), } ], ) # If the response is not empty, render it using the TTS plugin if response: event.assistant.render_response(response)When a response from the LLM ends with a question mark, the assistant will automatically listen for a follow-up command and fire a new
SpeechRecognizedEvent.Pausing music while listening
One nice touch is to pause the music when a conversation starts and resume it after the assistant is done.
from platypush import run, when from platypush.events.assistant import ( ConversationEndEvent, ConversationStartEvent, ) @when(ConversationStartEvent) def on_conversation_start(event): run("music.mpd.pause_if_playing") @when(ConversationEndEvent) def on_conversation_end(): run( "utils.set_timeout", name="ConversationEndTimeout", seconds=5, actions=[{"action": "music.mpd.play_if_paused"}], )That makes the interaction feel much less clumsy: wake word, music ducks or pauses, command is recognized, answer is spoken, music resumes a few seconds later.
Going fully local
With the configuration above, hotword detection, speech-to-text, automation and text-to-speech are already local. The only non-local component is the
openaiplugin, if it points to OpenAI's servers.To make the last step local too, run a model server that exposes an OpenAI-compatible API. Ollama, llama.cpp server, vLLM and LocalAI can all expose some version of
/v1/chat/completions.For example, with Ollama:
ollama pull llama3.1:8b ollama serveThe OpenAI-compatible endpoint is then usually available at:
http://127.0.0.1:11434/v1/chat/completionsIf your Platypush
openaiplugin version supports a custom API base URL, the configuration is the whole change:openai: model: llama3.1:8b base_url: http://127.0.0.1:11434/v1If it does not, keep the rest of the assistant exactly the same and replace only the fallback action with a tiny local request:
That is enough to turn the assistant into a fully local stack:
On a Raspberry Pi, I would still keep expectations realistic. Hotword detection, Vosk and Piper are fine on small machines. Local LLMs are the heavy piece. A Pi 5 with enough RAM can run small quantized models, but latency will not feel like a cloud model or a GPU-backed workstation. For many home automation workflows, that is acceptable because the LLM is only the fallback; the frequent commands stay deterministic.
Why this architecture ages well
Voice assistants have been a graveyard of abandoned SDKs and cloud products. Snowboy is gone. Mycroft is gone. The old Google Assistant SDK is deprecated. Vendor assistants are increasingly shaped around vendor ecosystems rather than user-controlled automation.
The safer long-term bet is not one monolithic assistant. It is a pipeline of small replaceable parts:
- Swap the hotword model without touching the automation logic.
- Swap Vosk for another STT engine without touching Hue or MPD.
- Swap OpenAI for a local OpenAI-compatible model without touching the wake word, TTS or command hooks.
- Swap Piper voices without touching the assistant flow.
Platypush is a good fit for this because its event system is already the boundary between perception and action. Speech recognition emits an event. Hooks decide what to do. Plugins execute the actions.
That separation is what makes the assistant inspectable. It is also what makes it possible to keep most of it on a Raspberry Pi in your house, instead of outsourcing the entire audio loop to a cloud service that may disappear, get worse, or decide one day that your use case is no longer part of the roadmap.
Final notes
The minimal version of this setup is small:
assistant.openwakewordfor the always-on wake word.assistant.voskfor local command transcription.- A few
@when(SpeechRecognizedEvent, phrase=...)hooks for deterministic commands. light.hue,music.mpdor any other Platypush plugin for actions.tts.piperfor local spoken responses.openai.get_responseonly where language understanding is worth the cost.
Start with the deterministic commands. Add the model fallback later. That way the assistant stays fast for the commands you use every day, while still being flexible enough to answer questions or interpret messy speech when you need it.
-
-
#ActivityPub support in #Madblog
https://blog.fabiomanganiello.com/article/Madblog-federated-blogging-from-markdown
I am glad to announce that Madblog has now officially joined the #Fediverse family.
If you want to test it out, search for this URL on your Fediverse client.
Madblog has already supported #Webmentions for the past couple of weeks, allowing your blog posts to be mentioned by other sites with Webmentions support (WordPress, Lemmy, HackerNews…) and get those mentions directly rendered on your page.
It now adds ActivityPub support too, using #Pubby, another little Python library that I’ve put together myself (just like Webmentions) as a mean to quickly plug ActivityPub support to any Python Web app.
Webmentions and Pubby follow similar principles and implement a similar API, and you can easily use them to add federation support to your existing Web applications - a single
bind_webmentionsorbind_activitypubcall to your existing Flask/FastAPI/Tornado application should suffice for most of the cases.Madblog may have now become the easiest way to publish a federated blog - and perhaps the only way that doesn’t require a database, everything is based on plain Markdown files.
If you have a registered domain and a certificate, then hosting your federated blog is now just a matter of:
mkdir -p ~/madblog/markdown cat <<EOF > ~/madblog/markdown/hello-world.md # My first post This is my first post on [Madblog](https://git.fabiomanganiello.com/madblog)! EOF docker run -it \ -p 8000:8000 \ -v "$HOME/madblog:/data" \ quay.io/blacklight/madblogAnd Markdown files can be hosted wherever you like - a Git folder, an Obsidian Vault, a Nextcloud Notes installation, a folder on your phone synchronized over SyncThing…
Federation support is also at a quite advanced state compared to e.g. #WriteFreely. It currently supports:
Interactions rendered on the articles: if you like, boost, quote or reply to an article, all interactions are rendered directly at the bottom of the article (interactions with WriteFreely through federated accounts were kind of lost in the void instead)
Guestbook support (optional): mentions to the federated Madblog handle that are not in response to articles are now rendered on a separate
/guestbookrouteEmail notifications: all interactions can have email notifications
Support for quotes, also on Mastodon
Support for mentions, just drop a
@[email protected]in your Markdown file and Joe will get a notificationSupport for hashtag federation
Support for split-domain configurations, you can host your blog on
blog.example.combut have a Fediverse handle like@[email protected]. Search by direct post URL on Mastodon will work with both casesSupport for custom profile fields, all rendered on Mastodon, with verification support
Support for moderation, either through blocklist or allowlist, with support for rules on handles/usernames, URLs, domains or regular expressions
A partial (but comprehensive for the provided features) implementation of the Mastodon API
If you want you can follow both the profiles of my blogs - they are now both federated:
My personal blog: @fabio (it used to run WriteFreely before, so if you followed it you may need to unfollow it and re-follow it)
The #Platypush blog: @blog
-
📰 New blog article
Self-host your own multi-service #music server on #Android
How to replace your music streaming apps with a setup that supports multiple streaming services, multiple devices and multiple outputs from a single Webapp.
#mopidy #platypush #termux #ntfy #Tasker #python
https://blog.platypush.tech/article/Self-host-your-music-experience-on-mobile
-
A self-hosted solution to create unlimited private email aliases. Featuring:
- #Postfix (mail server)
- #ntfy (pub/sub over HTTP)
- #Platypush (to listen to alias requests, create them and notify the clients)
- #Tasker (to conveniently wrap the service on Android into a simple app)
-
With the release of Trixie, the CI/CD pipeline of #Platypush has started releasing Debian packages for the
devbranch that target either Trixie:# /etc/apt/sources.list.d/platypush.list deb https://apt.platypush.tech/ stable devOr Bookworm:
# /etc/apt/sources.list.d/platypush.list deb https://apt.platypush.tech/ oldstable devWhich means that if you use an older version an
apt upgradewill probably break your Platypush installation.At that’s indeed been the case for me, since I still run a bunch of RaspberryPis on older versions of Debian that I’m too lazy to upgrade.
I don’t feel like creating new time/resource-consuming CI/CD processes to target each of the past versions of Debian and Ubuntu, but if you use an old version you should be able to make Platypush work after the installation by simply creating a symbolic link:
# If you configured the oldstable repo on Bullseye # Otherwise change the target directory with the one that matches your Python version sudo ln -s /usr/local/lib/python3.11/dist-packages/platypush /usr/local/lib/python3.9/dist-packagesThis is guaranteed to work as long as you use a version of Python released any time in the past decade. I strive to always keep Platypush compatible with Python >= 3.6, and with any versions of the dependencies released in that timeframe, and most of the core logic compatible even with Python 3.5 - even if that means keeping a lot of extra logic to check libraries versions, or avoiding all constructs introduced in recent versions of the language.
-
📰 New blog article
I’ve recently put together an improved version of my #Platypush + #Wallabag + #MercuryReader + #InternetArchive solution to scrape and archive articles.
So far it seems to work with nearly any major news website and blogging platform that I regularly read, so it’s the most likely candidate to replace solutions like 12ft / 13ft that have been broken for a while, at least for me (maybe I could put all these scripts together and call it 14ft?)
I don’t support piracy when it harms creators, but if buying ain’t owning then piracy ain’t stealing. But always remember to support your creators through other means.
https://blog.platypush.tech/article/Building-a-better-digital-reading-experience
-
📦 I have just released the version 0.2.0 of the #Platypush web extension - which hadn’t seen any commits since 2020, and too much pending work was long overdue.
The changes:
The extension builds again with modern node dependencies. Some of the dependencies had rotten (for example a version of node-gyp was used that still relied on python2) and fixing the build was the first priority.
Simpler configuration. The configuration form was very out-of-date, and it still required many settings that are no longer required in Platypush. It’s now possible to add Platypush services by simply providing the URL, and log in via token or browser flow.
Better README
Removed Chromium support. Sorry, no MV2, no party. I want the ability to run custom JS from strings through the
executeScriptAPI, so users have a GreaseMonkey-like API to interact with their browser and also access to the Platypush APIs. I want the ability to perform custom HTTP requests in my extension so I can connect to multiple Platypush devices without having to go crazy with the declarative approach. MV3 was a disgrace, I don’t have enough time to invest to complicate my life just to support a mutilated version of this extension on Chromium, and if it means that the Platypush extension will only target <5% of the browser market then let it be.
Btw, this partial rewrite of the extension was motivated by some tinkering I’ve been doing lately to save web pages to Wallabag also when I go through the Web Archive (which has added Cloudflare gateways that prevent the Wallabag scrapers from saving archived pages). Since my flow to save web pages is managed through a script in the Platypush extension, I’ve modified it to scrape the DOM on archive.is on the client side instead, save the scraped HTML on a temporary web server, and let Wallabag save that URL instead (so no CAPTCHAs and no Cloudflare).
The set up is still a bit messy at the current stage, but after refactoring a bit I may write a new article on how to replicate it.
-
It looks like runtime context evaluation in YAML procedures/hooks in #Platypush is broken as of #Python 3.13 - reference issue.
I’ve noticed it today after upgrading one of my nodes to Python 3.13, and its hooks suddenly stopped working.
As of Python 3.13, it’s no longer allowed to set local variables via
locals(), orexec()/eval(), so the following code breaks on the latest version:>>> def f(): ... exec('foo="bar"') ... print(foo) ... >>> f() Traceback (most recent call last): File "<python-input-3>", line 1, in <module> f() ~^^ File "<python-input-2>", line 3, in f print(foo) ^^^ NameError: name 'foo' is not definedWhereas it works on previous versions:
>>> def f(): ... exec('foo="bar"') ... print(foo) ... >>> f() barI know that manually setting local variables by assigning keys to
locals()or viaexec/evalisn’t pretty, it’s a quite nasty abuse of a side effect, and it makes the code harder to debug.But there are legit cases too. For example, Platypush allows to define event hooks in YAML this way:
event.hook.LogLatLongUpdate: if: type: platypush.message.event.geo.LatLongUpdateEvent then: - action: procedure.store_location_data args: latitude: ${latitude} longitude: ${longitude} altitude: ${altitude}Where
latitude,longitudeandaltitude, in this case, are parsed from the incoming event, assigned at runtime to local variables, and then the execution flow can just expand them on the fly.Apparently this flow is no longer allowed.
I’ll investigate for workarounds (
globals()doesn’t seem to be impacted, but I definitely don’t want to mess up with global variables), because at the current stage all those occurrences need to be replaced with e.g.${context["event"].latitude}- still ok-ish, but definitely less readable.I’m also a bit disappointed that such a breaking change wasn’t properly announced - sure, people shouldn’t mess up with the local variables of the interpreter at runtime, but there are legit cases that were worth discussing, and alternatives could have been provided, rather than seeing some code break from a day to another and getting a “sorry, you were never supposed to do this” as an answer.
-
The main feature of this release is the support for multiple backends in the
youtubeplugin.It allows you to watch ad-free YouTube videos on any supported media player and manage your playlists and subscriptions through multiple YouTube implementations.
Support for multiple YouTube backendsEarlier only #Piped was supported, but given the state of the project and most instances (all the ones I’ve tested, including my own, are still blocked by #YouTube’s new restrictions) I’ve added support for #Invidious too, and that’s currently the recommended backend.
The in-browser YouTube player now plays videos using the Invidious embedded player if you configure the
invidiousbackend, so the UI can be used as a full alternative frontend for Invidious.I’ve also added a new
googlebackend that leverages the official YouTube Data API to search and fetch your playlists and subscriptions, but keep in mind that:It requires you to register a project on the Google Cloud developers console.
It doesn’t support the
get_feed()action (YouTube has removed the endpoint in v3), so you won’t be able to get the latest videos published by your subscribed channel.All searches and activities will be logged on your Google account, so it’s probably not the best option if you are looking for a privacy-aware experience (but video streaming will still be ad-free).
The
Leveraging the support for multiple backends to migrate your data aroundyoutubeplugin should work in tandem with any supported Platypush media integrations (tested withmedia.mpv,media.vlc,media.gstreamer,media.kodiandmedia.chromecast), butmedia.mpvis recommended. The reason is thatmpvprovides the--ytdloption out of the box to leverageyt-dlpto download and stream videos on the fly, while other media plugins will have to first download the full video locally before streaming it (I’ve tried to implement my own real-time media streaming server, but I’m still not very happy with its stability).I’ve always been baffled by the fact that there isn’t a standard format to export playlists/subscriptions across different backend implementations (even among alternative backends, such as Piped and Invidious).
As someone who has migrated through different YouTube backends and apps depending on the state of restrictions implemented by Google, I’ve often had to write my own scripts to convert CSV/JSON exports from one platform or app to a format understood by the new solution.
Since the Platypush
youtubeplugin exposes the same API regardless of the backend, it is possible to configure multiple backends, and write a small script that fetches all playlists and subscriptions from one and imports them into another:from platypush import run # Get all the playlists from e.g. the Piped backend piped_playlists = run("youtube.get_playlists", backend="piped") piped_playlists_with_videos = { pl["id"]: { item["id"] for item in run( "youtube.get_playlist", id=pl["id"], backend="piped" ) } for pl in piped_playlists } # Create the playlists on Invidious and populate them for pl in piped_playlists: invidious_playlist = run( "youtube.create_playlist", name=pl["name"], backend="invidious" ) run( "youtube.add_to_playlist", playlist_id=invidious_playlist["id"], item_ids=piped_playlists_with_videos[pl["id"]] or [], backend="invidious" )Note that the simple script above doesn’t handle merge of existing playlists and items, but it can be easily adapted - if there’s enough interest I may write a small blog article with a more complete example.
Other release featuresThe full changelog of the new release is here. Besides the
youtubeintegration changes, this release includes the following features:Many stability/performance improvements for the
music.mopidyintegration - especially in handling connection recoveries.Support for ungrouped lights in the
light.hueplugin.Added a new Application tab to the UI, which allows you to monitor all events and requests handled by the service.
Adapted
ssllayer to Python 3.12 (which has deprecatedssl.wrap_socket()).Migrated the
kafkaintegration tokafka-python-nginstead ofkafka, which is currently broken and basically unmaintained.
-
@Nelfan I use #NewPipe on Android, but unfortunately it doesn’t come with a web version.
After self-hosting #Piped for a while I’ve recently switched to #Indivious (Piped isn’t seeing much development and it’s much easier to get blocked by YouTube by using it), and I must say that, hosted on a residential address and with IPv6 rotation, it does its job quite well.
For everything else (streaming on TV, Chromecast etc.) #Platypush with the YouTube plugin and MPV/VLC does a very good job, as long as yt-dlp is up-to-date (of course, being the main developer of it I’m a bit biased here).
I really hope that yt-dlp keeps working, and I’d direct my efforts towards keeping that alive, because yt-dlp functioning properly (and not only for YouTube) means that a lot of projects downstream will keep functioning.
-
@77nn I’m still quite passionate about IoT, and being the main developer of #Platypush I still try my best to have a fully on-device automation experience.
There are many applications where automation actually makes life easier (like turning off all lights and appliances automatically when you exit your home, or turning lights on/off depending on the luminosity in the room or when motion is detected, or setting the temperature of your house dynamically, or monitoring the consumption of my washing machine/smart tv, not to mention anything related to media center events).
Voice commands are also not always required if you set up a bunch of smart buttons that communicate over standard protocols, or even a bunch of IR receivers for traditional remotes. But sure voice interaction is still the weak point in the chain - things got better than a couple of years ago, and it’s now become more feasible to run on-device voice models also for devices that don’t have bleeding edge specs, but there still a lot of progress to be made. I still piggyback on the (deprecated) Google Assistant SDK for most of my voice needs on most of my RPis.
TL;DR: things are slowly getting better, and I think that using a local MQTT broker, a gateway like zigbee2mqtt or zwavejs2mqtt, maybe a bunch of custom devices with Arduino or ESP, and a local automation platform like HomeAssistant, OpenHUB or Platypush to tie things together now you can get a decent local home automation experience even without being connected to the Internet. But there’s still some progress to do, and sure the learning curve is still steeper than the alternative “buy our bridge and download our app” approach.
-
It looks like streaming #YouTube videos in #Platypush is currently broken because of new checks put in place by #Google.
#mpv with
--ytdlis also affected, and so are static downloads through the yt-dlp command of course.It seems that providing an API token to the yt-dlp configuration may mitigate the issue, but of course you lose anonimity.
I’ll test things with an auth token and document the new process this week.
-
This release turns procedures into first-class citizens.
Procedures are the bread-and-butter of Platypush customizations. They allow you to specify some custom logic that can run within the application, either from structured requests or code snippets - both #Python and #YAML scripts are supported. They are akin to recipes in #IFTTT and tasks in #Tasker.
You can call them from an event hook when a certain condition is met, from a cronjob or an alarm, from stand-alone scripts, from other procedures, and so on.
This release improves the integration of procedures into the UI, turning them into entities that can be controlled from the entities panel - and, soon, embedded into custom dashboards.
It also introduces a new powerful procedure editor, which allows you to visually create your automation routines through an intuitive UI with drag-and-drop support (a big part of this release has been about getting drag-and-drop to work nicely on mobile too). The new UI supports nested if/for/while blocks, break/continue/return, setting context variables, variable name autocompletion, export to YAML (if you prefer to have your procedures stored in the configuration rather than the db), and more.
This UI has been inspired by the job done by Joao Dias on Tasker (I’ve always wanted to have a similarly powerful block-based UI to create custom routines also on desktop/server), and in part by IFTTT’s recipe editor.
Python procedures can also be easily managed through a new file editor component that allows you to precisely navigate to their definition.
This release also includes a new file browser component. You can now browse your files on the Platypush instance, create/edit/delete/upload/download them directly from the Platypush UI, even if you don’t have SSH access to the machine.
Happy hacking!
-
Adding #YouTube videos to #Piped playlists seems to be broken now (or at least it only works sometimes).
It seems that even videos metadata (besides the actual media) now can’t be fetched via the YouTube API if YouTube decides for whatever reason to throttle your IP.
Which means that the
youtubeintegration in #Platypush will probably have to implement its own backend to save playlists and subscriptions instead of piggybacking on Piped.Switching the backend to #Invidious probably won’t help either, as most of the Invidious instances are now broken too.
At this point it almost looks like #Google is going down with its aggressive policy against 3rd-party YouTube clients to the point that they’re ok to even break their own APIs.
The only working solutions that I currently have to watch YouTube videos without going through their app/site are:
Platypush’ YouTube integration paired with a media plugin (like vlc, mpv or gstreamer) or Kodi/a Chromecast-compatible device. Platypush under the hood uses yt-dlp to do the magic, so if you run a Platypush instance in your home network your IP will still be visible to Google, but it’s still better than the alternative (watch endless ads or access their app/site while logged in with your Google account). Support for user playlists and subscriptions is currently buggy though, since it relies on the Piped API, and basically all Piped instances are currently broken.
Firefox’ Open With extension paired with #mpv, which comes with native yt-dlp support. That extension is unmaintained, and it doesn’t support ways to automatically open URLs with an external app (you have to explicitly open the URL with the extension), but so far it’s the only extension that I’ve managed to get to work with an external player - I haven’t had much luck with the External Application Launcher yet, which in theory should also support auto-opening specific URLs with the external app rather than the browser.
No solutions on mobile yet, but I’m working on empowering the streaming capabilities in Platypush so a “Play in browser” option for YouTube videos can be a thing.
-
I have a little gift for all the frontend folks out there who have struggled to get drag-and-drop interfaces to work both on desktop and mobile.
The source of all problems is that touch-based browsers usually don’t listen to higher-level drag/drop events like pointer-based interfaces. A long click on a component usually means that I want to drag that component. That doesn’t apply however to touch-based interfaces, where I may just want to drag my thumb to scroll, zoom, pinch or do any kind of touch gesture.
I’ve spent the past couple of days working on a UI component for #Platypush that allows users to modify procedures by dragging actions around, rendered as tiles. I want the dragging experience to feel the same both on desktop and phone. But I don’t want the touch-based experience to turn into a dragging hell where simply scrolling over the container results in blocks being randomly dragged around. Ideally, I’d like it if #JavaScript on mobile natively implemented a long-ish press on a
draggablecomponent to enter the dragging state, while atouchstartevent followed bytouchmoveall over the screen usually means that I’m scrolling or doing something else.Since such API isn’t there, and I couldn’t find much around either, I’ve decided put together two #Vue components,
DraggableandDroppable, that do exactly that.They can be imported anywhere in your template, linked to a DOM element that you want to be able to drag or accept dragged elements respectively, and they’ll install all the right event handlers and classes and proxy the events in order to expose a consistent API both on desktop and mobile.
They’ll also take care of little details to make the experience consistent across devices, such as scrolling the nearest scrollable parent if you drag a component above or below its container, or generating a thumbnail version of the dragged component that will be displayed while the cursor moves around.
By default on mobile it will initiate the dragging state only if the draggable component is touched for more than half a second, but you can customize this behaviour by setting the
touch-drag-start-thresholdproperty on theDraggablecomponent - a value of 0 will start dragging as soon as you touch a draggable item.The commit message is quite self-explanatory, and it also shows how to customize the style of the dragged/active/selected components.
The commit is quite self-contained and the two files come with no dependencies, so you can literally drop them in a Vue project and import them. But if there’s sufficient interest I could put together an #npm module for this.
Now that the (technical) UI problem is mostly solved, I still see a UX issue: how to make it clear to mobile users that a certain component is draggable? It’s usually quite straightforward on desktop - turn the cursor icon into a dragging hand or moveable arrows. But folks just aren’t used to drag-and-drop experiences on mobile (outside of native apps), and unless they accidentally stumble upon it while scrolling around they may never know that an interface supports it. Outside of invasive help popups and UI tours that take time and screen space, would there be some obvious way (like an icon or some other kind of decoration) to visually annotate a component that can be dragged, both on mobile and desktop?
https://git.platypush.tech/platypush/platypush/commit/1316af9553cdbcb619b4433ba50eea3911fd3ea8
-
If implementing a properly working drag and drop interface in #JavaScript wasn’t already hard enough, imagine realizing that drag events aren’t usually implemented on mobile interfaces, and that you have to reinvent the whole dragging logic on top of the basic
touchstart/touchmove/touchendevents.I’m currently implementing a portable drag-and-drop logic in #Platypush as reusable components, so you have an API like this:
<template> <div ref="draggable">...</div> <div ref="droppable">...</div> <Draggable :element="$refs.draggable" @drag="onDrag" @drop="onDrop" /> <Droppable :element="$refs.droppable" @drop="onDrop" @dragover="onDragOver" /> </template>The
DraggableandDroppablecomponents will then be in charge of registering the right listener and properties on the specified DOM elements, and do it in such a way that works both with thedrag*andtouch*APIs.Something tells me that this could be a quite common problem, so odds are that I may release this as a tiny separate npm module if there’s enough interest - not sure how common are the use-cases of drag-and-drop Web interfaces that need to work both on desktop and touchscreens.
-
@kuketzblog you may want to check the latest updates on #Platypush: https://blog.platypush.tech/article/Play-all-media-everywhere.
The latest version basically takes care of the transcoding and streaming of content from yt-dlp too, it's already compatible with the Piped API (so you can easily access your playlists and subscriptions there too, by enabling the YouTube plugin), and it supports a wide range of players - VLC, MPV, mplayer, gstreamer, Kodi and Chromecasts. -
📦 #Platypush 1.2.3 is out!
The main focus of this release is on the #media side.
In particular, Platypush now supports streaming/playing/downloading any media compatible with youtube-dl / yt-dlp, even if the upstream audio/video files are split - yay!
This means that it’s again compatible with #YouTube URLs (the integration broke recently after YouTube migrated all of its media to split video+audio tracks), and a lot of other sources that have been using this practice for a while - Facebook, Instagram, X, TikTok etc.
It means that you can play anything that yt-dlp can digest to any supported media plugin - VLC, mpv, mplayer, gstreamer, Kodi or Chromecast/Miracast.
Note however that mileage may vary depending on the player.
Things work fine out of the box if you use
media.mpv. MPV comes with native youtube-dl support, and the right stuff will be used to play the video smoothly if youtube-dl or yt-dlp are present on the system.media.vlcandmedia.gstreamernow provide two different play modes for YouTube-compatible content: play on the fly and play with cache. In play-on-the-fly mode (default) audio and video content will be mixed on the fly over ffmpeg and piped to the player process. This means shorter load times, it’s a good fit for live streams and large files, but it also means potentially lower media quality, high chances of media jitters in case of gaps in the stream being transcoded, and reduced ability to seek through the media. In play-with-cache mode the transcoded content will be cached to disk instead. It means waiting a bit longer for the video to load, and higher disk usage in case of large streams, but also a more robust and smooth playback experience.However I’m investigating a way to pass both an audio and a video URLs to GStreamer (it doesn’t seem to be easily feasible with VLC), so the player can do its own tuned mixed playback without me having to reinvent the wheel. If I can sort it out, and manage to avoid big audio offsets in the playback process, then this could be the default mode for GStreamer.
media.mplayeronly supports play-with-cache mode. The plugin already uses the player’s stdin to communicate commands, and AFAIK MPlayer doesn’t support sending both commands and media bytes to the player. Same goes formedia.kodi.media.chromecastmileage may vary depending on the model of Chromecast. I haven’t had much luck playing audio+video simultaneously when Platypush streams YouTube content to 1st-gen Chromecasts because the new video codecs used by YouTube videos apparently aren’t available on those devices. I’ve had mixed results by forcing the container to transcode the video track to H264 (and that is also the new default configuration forytdl_argsfor themedia.chromecastintegration), but there’s still a 50/50 chance that the Chromecast will only play the audio. I’ve had better luck with more recent Chromecast models though. And I believe that things should work just fine if you use any modern Miracast/DLNA-compatible device/dongle. Given the deprecation status of the Chromecast, and the dubious compatibility with whatever the Google TV folks are planning next, I’m not even sure if it’s worth investing further energies in for the Chromecast compatibility.media.chromecastnow also provides ause_ytdlconfiguration flag - it’s set to true by default, but you can disable if you want to stream YouTube/Facebook/TikTok etc. URLs to your Chromecast bypassing the Platypush streaming service. This means higher chances that the content will play fine, but it also means that it’ll be played by whatever compatible app (if available) runs on your Chromecast (i.e. ads/tracking/account limitations/geo limitations etc.).https://blog.platypush.tech/article/Play-all-media-everywhere
-
#YouTube playback is now broken on most of the #Piped instances out there, included mine. Google has decided to get tighter on IP checks and most of the videos are now replaced by a “Please sign in to confirm that you’re not a bot” text.
In the meantime, YouTube playback over external media plugins is currently also broken on #Platypush. YouTube has apparently completed the migration of all of its media to split audio+video tracks to be transcoded on the client, so the old trick of getting the audio+video direct URL via yt-dlp and passing it to vlc/mpv/Chromecast no longer works - you’ll mostly likely get an audio-only or video-only track.
A new version of Platypush that fixes the issue is on the way, but it comes with a trade-off, which is now embodied by the new
cache_streamsmedia plugin setting:cache_stream=false(default) means that transcoding and streaming will occur in memory. This means less clutter on disk and playback that can start almost immediately. It also means however that the media quality may experience temporary jitters while it downloads, especially at the beginning. And it also means that seeking, depending on the player, may not (always) be available. It also results in higher memory usage for large files, although usually ffmpeg takes care of buffered memory quite efficiently.cache_stream=truemeans that the transcoded media file will be temporarily cached on disk. This usually results in better media quality and robust support for seeking within the file. It also means however higher disk usage, especially if you’re watching large videos or live streams. And it also means a longer waiting time before playback, as at least one of the part files must be fully downloaded before streaming can start.
With such changes however media playback (with audio+video) via external player is now supported in Platypush also with other yt-dlp compatible sources (such as Facebook, Instagram, Twitter and TikTok URLs) that already had separate video+audio tracks.
Stay tuned for the new release!
-
Anyone knows of something like #ffplay, but with support for external commands to control playback?
Maintaining four different local media integrations in #Platypush (vlc, mpv, mplayer and omxplayer) is tiring, and each of those players comes with its own overhead, caveats and API quirks.
I’d be much easier if I could just pipe any stream to
ffplay -and call it a day, and have a way to easily pass play/pause/stop/volume etc. commands to ffplay - over stdin, over socket, over signals, anything works.Platypush could then be “its own” media player solely based on ffmpeg. Anything would be piped to ffplay over stdin.
Unfortunately, I can’t find a single way to programmatically control ffplay during playback that works - outside of hacks with keystroke emulation that are unlikely to work in Wayland anyway.
And so far I’ve been very tempted from meddling with GStreamer unless really required - first because it’d be a Linux-only solution, and second because it depends on the dbus+GLib and carries a whole lot of dependencies with it, while ffplay needs basically only the ffmpeg package.
Anyone who knows how to get this to work, or even a simple stand-alone command line player or media framework that can be externally controlled and uses as a base, feel free to share!
-
📦 #Platypush 1.2.1 is out!
There’s a large changelog, in a nutshell:
2FA support is here.
Added randomly generated API tokens alongside JWT tokens.
Added
bind_socketoption tobackend.http- now the Webapp can also be exposed on a UNIX socket rather than TCP.Support for per-plugin PWAs. You can now open
<platypush-host>/plugin/<plugin-name>from your browser on mobile, and install the PWA associated only to that plugin view. This is similar to what NextCloud offers - if you install the PWA from the calendar page it’ll install only the calendar app, if you install it from the notes page it’ll only install the notes app, and so on.Added support for custom Redis executables on
--start-redisvia--redis-bin- now tested also with alternative Redis implementations, namely Valkey and Redict.Fullscreen video/photo support for the camera views.
Migrated the project from
setup.pytopyproject.toml(it was about time).
The backlog is already filling up for the next release.
As usual, feel free to report your suggestions and feature requests on the issues page.
-
I’ve taken quite a deep dive in these days into the WebPush implementation and the details of the #VAPID specification for authenticated push notifications through #PWA.
I’m now testing things in the Platypush web app - aiming to support custom notifications providers too, so dispatching notifications to your mobile devices through your ntfy or NextPush server can also be supported.
The next release of Platypush may finally include native Web notifications through the PWA layer.
You should then be able to get notifications for all the media playing on your devices, and control them just like you would do with a Spotify/YouTube media notification, without having to use intermediary layers such as Tasker, Termux, Pushbullet or ntfy. Or get mobile notifications for the interactions with your custom voice assistants. Or create your custom push notifications (e.g. on your event hooks, custom procedures or crons) and dispatch them securely to your mobile devices through your own ntfy server. All (hopefully) without the need of a native Android app - power of Web pushes!
I only wish that the tools to implement WebPush/VAPID in #Python applications were as mature as those available for the JS ecosystem. py-vapid seems reasonably well designed, but it’s still a bit of an early project and it’s not even available on any major package managers (which is a big no-no for core Platypush features). And it only takes care of signing VAPID claims, not of packing and delivering WebPush requests end-to-end. I’ve eventually resorted to doing my own implementation with
ecdsa, plusjoseto take care of the JWT encryption boilerplate. I may write a little blog article if it ends up working. -
#Platypush 1.1.3 is out 📦
The main focus of this release is on the #YouTube integration and the media UI.
A new built-in download manager is now available for all the
mediaplugins.The main beneficiary is the
youtubeintegration, which now supports local media downloads (and audio-only downloads) for any YouTube content - and any other URL compatible with yt-dlp, e.g. Facebook, TikTok, Twitter etc.The YouTube integration also got more #Piped features, and it can now be used a full alternative Piped UI, including:
- Playlists support - search playlists, create/remove and add/remove items and navigate playlists directly from search results/library items.
- Channels support - search channels, subscribe/unsubscribe and navigate channels from search results/library items.
Add the existing media integrations features on top, and you have a system where you can easily download almost any URL from the Web, or play it on any compatible player - vlc/mpv/mplayer, Chromecasts, Kodi etc.
The release also includes a new architecture for the core Redis messaging system - now using a connection pool and a pub/sub mechanism instead of a static queue, which makes the service much faster and greatly reduces the chances of Redis deadlocks.
Happy hacking!
-
📢 New #blog article
Buiding custom voice assistants, 2024 edition
This is the third article I write on #voice #technology in #Platypush. I wrote the latest one in 2020, but unfortunately all the voice products I described there (Snowboy, DeepSpeech, Mycroft, AVS…) are now gone.
assistant.googleis still there (but I don’t know for how long, as the underlying Assistant library was deprecated back in 2019), but Platypush 1.0 has also added two more integrations to the mix:assistant.openaiassistant.picovoice
These are based on the OpenAI voice+GPT APIs and the Picovoice products respectively, and can be combined to provide a very modern and flexible voice assistant interface.
The article also describes how to create your custom action hooks to control your devices and services, train assistants with context, customize voice models, play custom conversation sounds, handle voice intents and offline transcriptions, and run multiple assistants on the same device.
Happy read!
https://blog.platypush.tech/article/The-state-of-voice-assistant-integrations-in-2024
-
#Platypush 1.0 is out!
It’s been 10 months and 1049 commits since the latest release of Platypush, 7 years since the first commit, and 10 years since the first release of its ancestor,
evesp.The past few months have been quite hectic and I have nearly rewritten the whole codebase, but I feel like the software is now at a stage where it’s mature and stable enough to be used by a larger audience.
The changelog is quite big, but it doesn’t even cover all the changes, as many integrations have been completely rewritten.
The biggest (breaking) change is the merge between plugins and backends. Now, except for those integrations that actually listen for messages and execute them (like HTTP and Redis), all the other integrations are plugins. This greatly simplifies the configuration and removes a lot of confusion for new users.
The Docker support has been greatly improved too. There are now officially supported multi-arch images for Alpine, Debian, Ubuntu and Fedora, an official
docker-compose.ymlfile, and both theplatydockandplatyvenvutilities have been almost completely rewritten to seamlessly automate the creation and configuration of containers and virtual environments (respectively) starting from a singleconfig.yaml.And the Python API has become much simpler and consistent. No more
__init__.pyfiles that the user had to manually create in each subfolder ofscripts, just drop a.pyfile with your automation in the scripts dir and it’ll be picked up. Moreover, the most common imports are now available on top level as well, and there’s no more need to create procedures/hooks/crons with varargs:from platypush import run, when from platypush.events.sun import SunsetEvent @when(SunsetEvent) def sunset_lights_on(): run('light.hue.on')There’s also a revamped documentation portal, which now includes both the wiki and the plugin reference.
Most of the integrations have been rewritten at different degrees, and in the process many bugs have been squashed, many features added and many APIs updated to be more consistent, so make sure to check the documentation pages of your integrations in order to migrate.
And if you have more requests or questions, feel free to open a ticket, a PR or ask on the Lemmy server.
-
@theendismeh #Platypush comes with several camera plugins. It works with any camera supported either by ffmpeg or gstreamer, and it has plugins also for PiCamera. If you choose this option I’d advise you to use the git version rather than the one on p PyPI - it’s a bit old and I’m planning a new release.
If you use a RPi with a PiCamera then you can use RPi Camera Viewer, a free Android app. Otherwise, for any other USB camera with ffmpeg/gstreamer, you can either view the stream from the Platypush webapp, or open the URL in VLC or anything compatible (support for RTSP is on my backlog).
I have 6 RPis with various cameras scattered around the house and I’ve never needed any other solution.
I’m just unsure about the “easy for non techies” requirement - I mean, the integration can be set up from a simple YAML configuration, but even as I’m working to lower entry barriers, Platypush is still mainly geared towards DIY.
-
Is anyone aware of ways to control #ffplay programmatically in any form, without having to focus the window and having to emulate keyboard/mouse bindings in it?
ffplay is amazing, light, fast, and it’s a player that comes with any #ffmpeg installation.
#Platypush supports media players such as VLC, mpv, mplayer, omxplayer and gstreamer, but they all come with their bags of issues - the VLC libraries seem to break too often on Wayland, mpv has too many API breaking changes across versions and controlling it only works if the version of the library and the player are carefully aligned, mplayer is an unmaintained dumpster fire with a messy control API, working with gstreamer in Python requires the user to install the whole fat GLib luggage and MBs of plugins, and omxplayer is basically dead.
ffplay would be my favourite pick for a portable and lightweight default media player. But the fact that it apparently can’t be controlled in non-interactive ways really puzzles me.