mqttwebstuff
A tiny live web view onto arbitrary MQTT streams: FastAPI subscribes to the
broker, a mapper plugin (a plain, mountable Python file or module)
filters/maps every message, and the browser receives rendered HTML fragments via
Server-Sent Events, displayed with htmx (+ SSE extension) on PicoCSS.
No frontend build, no framework; the only hand-written JavaScript is a dozen
lines in base.html.j2 preserving open <details> states across SSE swaps.
Modules
serve.py: Typer CLI (mqttweb-serve). Loads the mapper plugin (or the generic fallback), connects to the broker and runs the web app with uvicorn. The MQTT connection tolerates an unreachable broker at startup: paho keeps retrying in the background while the web app is already up (empty board,/healthzshowingmqtt_connected: false).hub.py:ViewHub, the last-value cache between MQTT and the browsers. It keeps the last rendered item per(panel, key)and fans updates out to all SSE clients. Messages are submitted thread-safely from paho's network thread.plugin_api.py: the contract between core and plugin.ViewEventdescribes one mapped message (panel, key, data, template, sort, group, title, ttl);load_pluginvalidates a mapper given as a file path or dotted module.webapp.py: the FastAPI app and the Jinja2 environment (including thehhmmfilter).plugins/oepnv_view.py: reference mapper rendering theoepnvstuffGTFS-realtime stream as a departure board (departures, feed status, per-line coverage, service alerts). It follows the publisher's topic base viaOEPNV_MQTT_BASE_TOPIC(defaultoepnv; multi-level bases likehome/oepnvwork).templates/,static/: built-in Jinja2 templates (generic JSON card, indented topic tree) and vendored htmx, htmx-ext-sse and PicoCSS, so the server serves everything itself (no CDN egress).
Logging (loguru, with an intercept for stdlib logging from paho and
uvicorn) and the startup banner are shared via somestuff.mqttwebstuff.
MQTT broker ──> paho thread ──> ViewHub (last-value cache) ──> SSE ──> htmx swaps panels
│
mapper plugin
filter / panel / key / template / ttl
Why a cache in the middle?
Streams such as the oepnv one publish with retain=False. The hub keeps the
last rendered item per (panel, key), so a freshly opened tab gets the full
current board server-rendered on GET /, then only deltas over GET /stream.
Items carry a TTL, so topics that stop being published disappear from the
board, mirroring the non-retained semantics.
Endpoints
| Route | Purpose |
|---|---|
/ |
Full board, server-rendered from the cache |
/stream |
SSE: panel:<name> repaints, board on new panels, keepalives |
/healthz |
{"ok": true, "mqtt_connected": ...} |
Deep links
Group boxes (e.g. one stop on the oepnv board) and tree rows carry stable,
ASCII-slugged anchor ids: /#g-<panel>-<label> and /#t-<topic>, e.g.
/#g-departures-ellerbek-moordamm or /#t-nodered-hydrostatics-hydrostatic1.
Hovering a label reveals its self-link (#); the jump target is highlighted.
Since ids survive repaints and restarts, the links are bookmarkable.
Installation
pip install somestuff-mqttwebstuff
somestuff is a PEP 420 namespace; the package is imported as
somestuff.mqttwebstuff and shares the namespace with the other somestuff-*
distributions.
Usage
The console script and module invocation are equivalent:
mqttweb-serve --mapper somestuff.mqttwebstuff.plugins.oepnv_view --mqtt-host broker.example.org
python3 -m somestuff.mqttwebstuff.serve --mapper somestuff.mqttwebstuff.plugins.oepnv_view --mqtt-host broker.example.org
--mapper accepts a dotted module name on the import path (as above) or the
path of a .py file, e.g. --mapper /config/my_view.py. In a repository
checkout, --mapper somestuff/mqttwebstuff/plugins/oepnv_view.py works as well.
Without a mapper, a generic view shows every message of the --topics patterns:
# default layout: indented hierarchical topic tree (branch rows per level,
# scalars inline, JSON collapsible)
mqttweb-serve --topics 'ecowitt/#,oepnv/status' --mqtt-host broker.example.org
# ad-hoc: peek into a whole topic tree and keep items forever
mqttweb-serve --mapper "" --topics 'nodered/#' --item-ttl 0 --title "Node-RED Live"
# one JSON card per topic instead of the tree
mqttweb-serve --mapper "" --topics 'nodered/#' --view flat
--mapper "" explicitly overrides a mapper configured via environment or env
file; a configured mapper otherwise wins over --topics. --item-ttl defaults
to 900 seconds (an item without a new message disappears), 0 keeps items
forever. mqttweb-serve --help lists all options.
Configuration
Every option is also an environment variable (CLI wins over environment). The
tool additionally loads mqttweb.local.env from the current working directory
and from the package directory; precedence is real environment, then the CWD
file, then the package-directory file, with CLI options over all.
| Variable | CLI option | Default | Meaning |
|---|---|---|---|
MQTTWEB_MAPPER |
--mapper |
empty (generic view) | Mapper plugin: .py file path or dotted module name |
MQTTWEB_TOPICS |
--topics |
# |
Comma-separated subscribe patterns, generic view only |
MQTTWEB_ITEM_TTL |
--item-ttl |
900 |
Generic view: seconds until an unrefreshed card disappears, 0 = forever |
MQTTWEB_VIEW |
--view |
hierarchical |
Generic view layout: hierarchical or flat |
MQTTWEB_TITLE |
--title |
empty (plugin's TITLE) |
Page title override |
MQTTWEB_LISTEN_HOST |
--listen-host |
0.0.0.0 |
Bind address |
MQTTWEB_LISTEN_PORT |
--listen-port |
8080 |
Bind port |
MQTTWEB_LIMIT_CONCURRENCY |
--limit-concurrency |
0 (unlimited) |
Reject new connections with 503 above N concurrent ones. Every open tab holds one SSE stream and a page load briefly needs several slots, so size generously (100+), not per user |
MQTTWEB_MQTT_HOST |
--mqtt-host |
mosquitto.mosquitto.svc.cluster.local |
MQTT broker host |
MQTTWEB_MQTT_PORT |
--mqtt-port |
1883 |
MQTT broker port |
MQTTWEB_MQTT_USER |
--mqtt-user |
empty (anonymous) | MQTT username |
MQTTWEB_MQTT_PASSWORD |
--mqtt-password |
empty | MQTT password |
MQTTWEB_MQTT_TLS |
--mqtt-tls |
false |
Encrypt the MQTT connection; the port is not switched automatically, set --mqtt-port (usually 8883) |
MQTTWEB_MQTT_TLS_CA |
--mqtt-tls-ca |
empty (system CA store) | CA certificate path |
MQTTWEB_MQTT_TLS_CERT |
--mqtt-tls-cert |
empty | Client certificate path (mutual TLS) |
MQTTWEB_MQTT_TLS_KEY |
--mqtt-tls-key |
empty | Client key path (mutual TLS) |
MQTTWEB_MQTT_TLS_INSECURE |
--mqtt-tls-insecure |
false |
Skip TLS hostname verification (self-signed certs); encrypted but MITM-able |
MQTTWEB_VERBOSE |
--verbose, -v |
false |
DEBUG logging, including the uvicorn access log |
--topics, --item-ttl and --view are ignored when a mapper is set.
Plugin-specific settings are plain environment variables the plugin reads
itself; the oepnv plugin reads OEPNV_MQTT_BASE_TOPIC at load time.
The repository additionally contains an annotated mqttweb.env.example, a
Kubernetes deployment example and a secret example (not part of the installed
package): https://github.com/vroomfondel/somestuff/tree/main/somestuff/mqttwebstuff/
Writing a mapper plugin
A plugin is one Python file (or an importable module), no packaging needed.
Required attributes: SUBSCRIPTIONS and map_message. Optional: TITLE,
PANELS (panel name to heading, fixes the board order; an empty heading means a
plain panel without chrome) and TEMPLATE_DIR. payload is already decoded: a
dict/list when the raw payload parses as JSON, otherwise the raw string.
Library
from somestuff.mqttwebstuff.plugin_api import ViewEvent
TITLE = "My Stream"
SUBSCRIPTIONS = ["mystream/#"]
PANELS = {"main": "Main"} # optional: fixes panel order + headings
# May also return a list of ViewEvents (one message -> several board items,
# e.g. a leaf plus synthesized ancestor rows, as the generic hierarchical view
# does) or None to drop the message.
def map_message(topic: str, payload) -> ViewEvent | None:
if not isinstance(payload, dict):
return None # filter
return ViewEvent(
panel="main", # board section
key=topic, # stable identity -> idempotent replace
data=payload, # template context
template=None, # None = built-in generic JSON card
sort="", # ordering within the panel/group ("" = by key)
group="", # optional sub-heading label (e.g. stop name);
# items cluster under it, "" = ungrouped
ttl=300.0, # vanish after 5 min without re-publish
)
Save it as my_view.py and run mqttweb-serve --mapper my_view.py --mqtt-host broker.example.org.
Templates: put Jinja2 files (*.html.j2) into a templates/ directory next to
the plugin file (or set TEMPLATE_DIR, relative to the plugin's directory). The
plugin's directory is searched first, the built-ins second, so a plugin can also
override base.html.j2 or add styles via an extra_head.html.j2 include. The
hhmm filter formats ISO timestamps as HH:MM.
In Kubernetes, mount the plugin (+ templates) from a ConfigMap and point
MQTTWEB_MAPPER at it; see the deployment example in the repository (link
above). The oepnv reference plugin is part of the package, so the oepnv board
needs no mount at all.
Notes
- Requires:
loguru,typer,fastapi,uvicorn,jinja2,python-dotenv,tabulate,mqttstuff. - Licensed under the LGPL, see
LICENSE.mdin the repository root.
Metadata
Release files for somestuff-mqttwebstuff 0.1.4
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| somestuff_mqttwebstuff-0.1.4.tar.gz | 67.8 kB | Without provenance | Details for somestuff_mqttwebstuff-0.1.4.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| somestuff_mqttwebstuff-0.1.4-py3-none-any.whl | Python 3 | none | any | Details for somestuff_mqttwebstuff-0.1.4-py3-none-any.whl |
Total release size: 136.1 kB
Release files / somestuff_mqttwebstuff-0.1.4.tar.gz
| Download URL | somestuff_mqttwebstuff-0.1.4.tar.gz |
|---|---|
| Size | 67.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
fe445a686ec0ef6d9ef12879f5a6e4664c682e723a558d56e25276dfac123790
|
|
BLAKE2b-256 checksum How to use checksums |
2f706653449301c1bae3a80ddcbe7e73f942da944fc5c7f9faa09a5bd582b1cf
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
Hatch/1.18.2 {"ci":null,"cpu":"x86_64","distro":{"id":"resolute","libc":{"lib":"glibc","version":"2.43"},"name":"Ubuntu","version":"26.04"},"implementation":{"name":"CPython","version":"3.14.4"},"installer":{"name":"hatch","version":"1.18.2"},"openssl_version":"OpenSSL 3.5.5 27 Jan 2026","python":"3.14.4","system":{"name":"Linux","release":"7.2.10-custom"}} HTTPX2/2.16.0
|
Release files / somestuff_mqttwebstuff-0.1.4-py3-none-any.whl
| Download URL | somestuff_mqttwebstuff-0.1.4-py3-none-any.whl |
|---|---|
| Size | 68.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c89e264e44288067711dbf5855ae8efad657f1af33ab24166586979a2959a087
|
|
BLAKE2b-256 checksum How to use checksums |
617bc098da3572af9f4cea0ab42a362504e5b1895b84d15b70be7d78e7564f18
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
Hatch/1.18.2 {"ci":null,"cpu":"x86_64","distro":{"id":"resolute","libc":{"lib":"glibc","version":"2.43"},"name":"Ubuntu","version":"26.04"},"implementation":{"name":"CPython","version":"3.14.4"},"installer":{"name":"hatch","version":"1.18.2"},"openssl_version":"OpenSSL 3.5.5 27 Jan 2026","python":"3.14.4","system":{"name":"Linux","release":"7.2.10-custom"}} HTTPX2/2.16.0
|
Security for somestuff-mqttwebstuff 0.1.4
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.