Features • Quick Start • Roadmap • Issues
Deutsche Version / German version
Flask server for an ESC/POS thermal receipt printer (tested with an Epson TM-T88V) on a Raspberry Pi Zero (2) W. Mobile web UI (German/English) for printing lists (shopping, to-do, task/kanban cards), status messages, weather reports, images, system reports, and offline pen-and-paper games (Sudoku, a dice score sheet, Tic-Tac-Toe boards) - most print types optionally prefixed with a small logo - plus a generic automation webhook (compatible with Home Assistant, Node-RED, n8n, or any tool that can do an HTTP POST) and other automation triggers (GitHub stars, Fritz!Box guest network, storm warnings).
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
cp config.example.py config.py
# Fill config.py with real values (Home Assistant token, Netatmo entities, Fritz!Box credentials, ...)
python3 app.pyThe server then runs on port 5000, web UI at http://<pi-hostname>:5000.
python3 app.py uses Flask's development server - for production the
service runs via Gunicorn with gunicorn.conf.py and exactly 1 worker, which is required because the print queue is process-local.
Most settings (quiet-hour rules, weather locations, GitHub-watched repos,
SSH targets for the system report, per-module logos, UI language) are
configured entirely through the web UI's Settings page - no need to touch
config.py for those. Only bootstrap-level values (secret key, API token,
printer USB IDs, real credentials such as the Home Assistant token or
Fritz!Box password) stay in config.py, since ReceiptPi currently has no
login/auth for the web UI itself.
receiptpi/
├── app.py Create the Flask app, register blueprints
├── gunicorn.conf.py on_starting hook for the boot greeting
├── print_queue.py central print queue + print-rule checks
├── printer.py printer hardware access (USB)
├── security.py CSRF protection, API token protection, JSON parsing
├── secrets_crypto.py Fernet encryption for system-report SSH passwords and the recipe manager API token (key in STATE_DIR, not in git)
├── settings_store.py central settings (JSON in STATE_DIR, not in the project folder)
├── history_store.py SQLite print history (STATE_DIR), auto-pruned after 180 days
├── lists_store.py in-progress list/task-card drafts (STATE_DIR), cleared once printed
├── pending_store.py blocked/failed print jobs (STATE_DIR, 0600), see "Pending jobs" below
├── logos.py per-print-type logo resolution, upload validation, seeding
├── i18n.py minimal translation lookup (JSON files, no Flask-Babel)
├── module_catalog.py registry of the toggleable home-page modules (key, icon, URL) + their effective on/off state
├── text_style.py Easy-Read text-size scaling, shared by print jobs and the web UI
├── themes.py registry of the selectable web UI color themes
├── version.py reads VERSION, minimal comparison for ReceiptPi's own version scheme
├── VERSION single source of truth for the running version (e.g. "0.1.9alpha")
├── config.example.py template for config.py (fill in locally)
├── modules/
│ ├── lists/ shopping lists, to-do lists, task/kanban cards
│ ├── message/ status messages (free title + text)
│ ├── images/ print images
│ ├── wifi/ guest wifi slip (text + QR code)
│ ├── weather/ weather report (DWD + Netatmo)
│ ├── system/ system report (Proxmox/PBS via SSH, plus custom SSH commands for other hosts)
│ ├── games/ offline games (Sudoku, dice score sheet, Tic-Tac-Toe)
│ ├── recipes/ recipe search/print + shopping list import (Mealie)
│ ├── automation/ generic automation webhook
│ ├── history/ print history dashboard
│ ├── pending/ review/reprint/discard blocked or failed print jobs
│ └── settings/ settings pages (web UI) + settings API
├── watchers/
│ ├── github_star_watch.py cron: prints on a new GitHub star
│ ├── fritzbox_wifi_watch.py cron: prints a wifi slip once the guest network is enabled
│ ├── storm_warning_watch.py cron: prints on a new active storm warning
│ ├── update_check_watch.py cron (every 12h): caches whether a newer GitHub release exists, for the footer
│ └── crontab.example ready-to-adjust cron template for all four watchers above
├── assets/example-logos/ bundled starter logo set (outline icons)
├── static/
│ ├── style.css design system (spacing/colors/themes, card/tile/icon styles)
│ ├── draft-autosave.js generic auto-save for the lists/task-card forms
│ ├── mealie-import.js Mealie shopping list import on the shopping list page
│ ├── manifest.json web app manifest (home-screen name/icons/colors)
│ ├── sw.js service worker (served at /sw.js, HTTPS only)
│ └── icons/ local Lucide SVG icon set (see LICENSE in that folder)
└── templates/ shared layout and module pages
├── base.html
├── home.html
├── shopping.html
├── lists_*.html lists overview, to-do page, task/kanban card page
├── message.html
├── images.html
├── wifi.html
├── weather.html
├── system.html
├── history.html
├── games_*.html games overview + one page per game
├── recipes*.html recipe search/random picker + recipe preview
└── settings_*.html settings overview + one sub-page per area
Each feature is implemented as its own Flask blueprint. Unhandled exceptions are isolated to the current request; modules share the same process, print queue and settings store.
The web UI is mobile-first (a single narrow column, no separate desktop layout) and built as a small card/tile-based design system rather than plain forms:
- Home/dashboard (
/) - a status strip (printer reachability, polled live via/healthevery 60 seconds; how many of the toggleable modules are currently active; total print count from the history store), followed by a tile grid linking to every currently enabled module. - Navigation - a topbar wordmark, a small printer-status dot, and a hamburger menu (History, Settings).
- Themes - 6 selectable color themes (Warm [default], Forrest, Dark
Lime, Frost, Butter Bean, White Purple), picked under
/settings/designand applied via adata-themeattribute plus CSS custom properties. Warm is the only theme that also follows the system/browser light-dark preference automatically. - Icons - a local set of Lucide SVG icons under
static/icons/(no external icon font or CDN), rendered via CSSmask-imageso every icon automatically follows the active theme's accent color instead of being hardcoded to one color. See Third-Party Licenses. - Easy-Read - one text-size switch scales both the web UI and the receipt output, see Easy-Read below.
- Footer - shows the running version and a link to the GitHub repo on
every page; if
watchers/update_check_watch.py(cron, every 12h) has found a newer GitHub release (including alpha/prereleases), the footer links directly to it. Information only, no auto-update.
Each page shows a small icon matching its content in the topbar (a printer icon on the home page, a message icon on the message page, and so on). There is no browser-tab favicon yet.
Home screen / installable web app - static/manifest.json plus a
theme-color and an apple-touch-icon in templates/base.html let the
web UI be added to a phone's home screen with its own name and icon
(iOS: Safari share menu -> "Add to Home Screen"; Android: browser menu).
When the UI is served over HTTPS (e.g. behind a reverse proxy), a
service worker (static/sw.js, served at /sw.js) is registered as
well, so Chrome on Android offers "Install app". Over plain HTTP
(http://<pi>:5000) no service worker is registered - the home-screen
shortcut still works.
The service worker only caches same-origin /static/ files that carry a
?v=<content hash> (style sheet, fonts, icons, manifest) - these URLs
change whenever their content does, so nothing stale can be served. Pages,
/health, /print/*, /ui/*, /settings/* and every other request always
go to the network. The cache is named after the VERSION file; a new
release replaces the worker and deletes the old cache. It is not an
offline mode - the UI still needs the Pi to be reachable. The icons under
static/icons/ (icon-192.png, icon-512.png, icon-maskable-512.png,
apple-touch-icon.png) are an interim solution derived from the
existing logo - a proper icon set will follow.
/settings is an overview page (tile grid, grouped into General /
Printing / Integrations / System) linking to a dedicated sub-page per
area, instead of one long form:
/settings/language- UI language (German/English)/settings/design- web UI color theme/settings/modules- enable/disable individual home-page modules, see Module toggles/settings/print-rules- quiet-hour rules (multiple independent rules, each with its own weekdays and time window) + rate limiting + duplicate suppression + the Easy-Read text-size switch/settings/logos- global logo toggle, default logo, and a per-print-type toggle/upload/preview (falls back to the default logo if no custom one is set)/settings/weather- weather report provider (DWD or Open-Meteo), weather locations, and an independent storm-warning provider (DWD, MeteoAlarm, or NWS) with its own enable toggle and an optional "ignore quiet hours" override/settings/github-watch- watched GitHub repositories/settings/system-report- a free-form list of SSH targets for the system report, see System report below/settings/recipes- recipe manager (Off / Mealie), Mealie base URL, encrypted API token, connection test, see Recipes below
All /print/* and /settings/* endpoints require the X-Api-Token: <value>
header once API_TOKEN is set in config.py (empty = no protection).
Every print job also passes through the central print rules: quiet hours, rate limiting and duplicate suppression.
A blocked job responds with 429.
Printing
POST /print/message-{ "title": "...", "text": "..." }POST /print/list-{ "title": "...", "items": ["..."] }- shopping list, unchanged since before the lists module existed, kept fully backward compatiblePOST /print/todo-{ "title": "...", "items": ["..."] }- to-do list, same shape as/print/listPOST /print/task-{ "title": "...", "description": "optional", "items": ["optional checklist"], "priority": "low"|"medium"|"high", "due_date": "YYYY-MM-DD" }- printable task/kanban card; onlytitleis requiredPOST /print/image- multipart upload (max. 6000px per side, 12MB)POST /print/wifi-{ "ssid": "...", "password": "...", "auth_type": "WPA" }POST /print/weather- optional{ "location": "Berlin" }, otherwise the default locationPOST /print/system- no body neededPOST /print/automation-{ "title": "optional", "text": "..." }- generic automation webhook (compatible with Home Assistant, Node-RED, n8n, or any tool that can do an HTTP POST)GET /health- check printer reachability (no token needed, but runs through the same queue; excluded from print history)
Settings API
GET /settings/api- current print rules + weather locations (JSON)POST /settings/print_rules- change rate limit/duplicate windowGET|POST /settings/quiet_hours/rules,POST /settings/quiet_hours/rules/<id>/toggle,DELETE /settings/quiet_hours/rules/<id>GET|POST /settings/weather/locations,DELETE /settings/weather/locations/<name>GET /settings/system_report- current list of system-report hosts (JSON, passwords redacted to ahas_passwordflag)POST /settings/system_report-{ "name": "...", "role": "proxmox"|"pbs"|"custom", "host": "...", "user": "...", "command": "required for role=custom", "password": "optional" }- adds one hostDELETE /settings/system_report/<id>- removes one hostGET|POST /settings/github_watch/repos,DELETE /settings/github_watch/repos/<owner>/<repo>GET|POST /settings/logos/config- global/per-module logo togglesPOST /settings/logos/upload/<slot>,DELETE /settings/logos/upload/<slot>- logo image upload/removal (base64),slotisdefaultor a module key
Recipes (web UI routes)
The recipes module has no /print/* JSON API and no settings API - its
routes serve the web UI and need no X-Api-Token. All of them answer
404 while the module is inactive (see Module toggles).
GET /recipes?q=<term>- search page with resultsGET /recipes/view/<slug>- recipe previewGET /recipes/random?ingredients=<a,b>&match=all- random recipe;ingredientsoptional (max. 5),match=allrequires all of them instead of anyPOST /ui/recipes/print- form post (CSRF-protected), fieldslugGET /recipes/import/lists- Mealie shopping lists as JSON (used by the import on/shopping)GET /recipes/import/lists/<id>- unchecked items of one list as JSON, formatted as shopping list lines
pip freeze > requirements.lock.txtFreezes the actually installed versions - requirements.txt itself
deliberately has no version pins (no ==x.y.z), requirements.lock.txt
is only a reference in case an update ever breaks something.
The four scripts under watchers/ (GitHub star notifications, guest
Wi-Fi slip, storm warnings, update check) only run when something
schedules them - they are never started by app.py/Gunicorn itself.
See watchers/crontab.example for a ready-to-adjust template covering
all four with the interval each script's own docstring recommends;
install it with crontab watchers/crontab.example after editing the
PYTHON/PROJECT_DIR placeholders, or copy individual lines into an
existing crontab (crontab -e).
- Lists: shopping lists, to-do lists, and printable task/kanban cards, see Lists below
- Free-form messages
- Image uploads and API image printing
- Guest Wi-Fi credentials and QR codes
- Weather reports (DWD or Open-Meteo, selectable, + optional Netatmo), with an optional storm-warning watcher (DWD, MeteoAlarm, or NWS, prints only when a warning is actually active)
- System reports: Proxmox/PBS via SSH, plus a custom-command role for any other host (e.g. a NAS), with optional encrypted password auth, see System report below
- Offline games (Sudoku, a dice score sheet, Tic-Tac-Toe), see Games below
- Recipes from Mealie: search, preview and print recipes, random recipe by ingredient, and import a Mealie shopping list into the shopping list, see Recipes below
- Print history dashboard (stats + paginated log, SQLite-backed)
- Pending jobs: review/reprint/discard print jobs blocked by quiet hours/rate limit or failed with a real printer error, see "Pending jobs" below
- Optional per-print-type logos with a global default fallback
- Individually toggleable home-page modules, see Module toggles below
- Web-based settings, split into per-area sub-pages and grouped by topic
- 6 selectable web UI color themes
- Easy-Read: one switch for larger text on both the receipt and the web UI, see Easy-Read below
- German/English UI, including receipt content itself (not just the UI chrome)
- USB-connected ESC/POS printer (python-escpos + pyusb, Epson TM-T88V profile)
- Version footer with GitHub release update notice (checked every 12h, no auto-update)
Grew out of the original shopping-list module; all three list types share the same print queue, history logging, rate limiting and quiet hours as every other module.
- Shopping list (
/shopping, also reachable at/lists/shopping) - a plain checklist. The/print/listAPI endpoint is unchanged since before the lists module existed, kept fully backward compatible for existing integrations. This is the only list type with a logo slot (reuses the existing shopping logo slot) - to-do lists and task cards never show a logo. - To-Do list (
/lists/todo) - the same checklist mechanism as the shopping list, printed and labelled separately.POST /print/todoJSON API. - Task/kanban card (
/lists/task) - a title plus optional description, optional checklist, optional priority (low/medium/high) and optional due date; only fields actually set are printed, no empty placeholders on the receipt.POST /print/taskJSON API - suitable for a plaincurlcall, a Home Assistantrest_command, or any other automation that can do an HTTP POST, with the sameX-Api-Tokenauth, print queue, quiet hours, rate limit and history logging as every other/print/*endpoint. - Drafts - all three forms auto-save to the server (
lists_store.py,~1safter typing stops) so an in-progress list survives navigating away or closing the tab; a draft is only cleared once actually printed. A "Discard" button clears it back to a blank form.
/settings/system-report manages a free-form list of SSH targets
instead of a fixed set - each entry has a name, a role, a host and a
user, and connects either via SSH key (default) or an optional
password.
- Proxmox and PBS are structured roles with their own fixed set of SSH commands (CPU/RAM/LXC/VM list for Proxmox, recent backup tasks for PBS).
- Custom command runs an arbitrary user-supplied SSH command verbatim on the target host - the general-purpose role for anything that isn't Proxmox/PBS, e.g. a NAS of any kind. No NAS-specific software is assumed anywhere in the codebase.
- An update check (
apt list --upgradable) runs across every configured host regardless of role. - Password auth: an optional password field is the alternative to
SSH-key auth, for a host with its own restricted, password-based
user. Connects via
paramikoinstead of the systemsshCLI (avoids passing the password through a process argument, unlikesshpass). The password is encrypted at rest with Fernet (cryptographypackage) before being written tosettings.json; the encryption key lives in its own file underSTATE_DIR(secret.key, mode 0600), generated on first use, never in git and never insettings.jsonitself.
Three offline games, printed through the same central print queue as
every other module. Unlike most modules they are UI-only - there is no
/print/games/* JSON API and they are not wired into the automation
webhook - and they have no logo slot.
- Sudoku (
/games/sudoku) - a locally generated 9x9 puzzle with three difficulty levels (easy/medium/hard), with an option to also print the solution as a second receipt. - Dice score sheet (
/games/dice) - a generic five-dice score pad (upper section ones-sixes with subtotal/bonus, lower section three-/four-of-a-kind, full house, small/large straight, five-alike, chance, plus upper/lower/grand totals). One receipt per player, 1-12 players per print. - Tic-Tac-Toe (
/games/tictactoe) - printable blank 3x3 boards to fill in by hand, 3/6/9 rounds per print.
Connects to a self-hosted Mealie instance (tested
with v3.27.0). Set it up under /settings/recipes: base URL + an API
token (created in Mealie under Profile -> API Tokens). The token is stored
Fernet-encrypted (same mechanism as the system report SSH passwords) and
never shown again; leaving the token field empty when saving keeps the
stored one (only while the base URL stays the same - a changed URL needs
the token entered again, so the stored token is never sent to a new
address), and "Remove token" deletes it. "Test connection" checks both
the URL and the token (via /api/users/self - Mealie's /api/app/about
answers without authentication, so it alone can't validate a token)
without saving anything.
The module is only active when its toggle under /settings/modules is on
and a recipe manager is selected - with the recipe manager set to
"Off" it behaves like a disabled module (no tile, 404 on all its
routes, no import on /shopping), see Module toggles.
- Search + print (
/recipes) - searches recipe titles/descriptions (Mealie's search does not cover ingredients), shows a preview, prints title, total time, ingredients and instructions. - Random recipe - optionally filtered by one or more ingredients (any/all). Ingredient names must match a Mealie food exactly (case-insensitive, singular or plural) - otherwise close matches are suggested.
- Shopping list import (on
/shopping) - appends the unchecked items of a Mealie shopping list to the current list. Append-only, and read-only towards Mealie (nothing is checked off there).
UI-only (no /print/* JSON API, see Endpoints), no logo
slot. Fraction characters in Mealie's ingredient text (e.g. ¹/₂) are
printed as plain ASCII (1/2).
A single switch under /settings/print-rules ("text size": Normal /
Large) scales both the receipt output and the web UI from the same
setting:
- Receipt: headings print at double width/double height, body text at double width (which halves the usable line width, so body text wraps more often); the footer timestamp and section separators always stay at normal size.
- Web UI: the base font size grows from 16px to 19px, and the home status strip and module tile grid drop from a multi-column to a single-column layout so nothing gets squeezed into a cramped column.
It is a plain text-size switch, not a full accessibility overhaul - there is no separate screen-reader markup or contrast mode tied to it.
A print job blocked by quiet hours/rate limit, or one that fails with a
real printer error (device unreachable, USB error, ...), doesn't just
vanish - its content is saved so it can be reviewed and manually
reprinted or discarded later, reachable via the burger menu. Applies
across every source (web UI, JSON API, automation webhook, watchers)
equally, since it hooks into enqueue_print() centrally rather than
each module individually. Duplicate-suppression blocks are deliberately
excluded - that reflects an already-accepted earlier submission of the
same content, not lost content.
Check the box next to the entries you want, then choose "Print
selected" or "Discard selected"; "Print all"/"Discard all" skip the
selection entirely. A reprint bypasses quiet hours (an explicit,
reviewed action) and duplicate suppression, but the rate limit still
applies. An entry is only removed once its reprint is CONFIRMED
successful - a failed retry leaves it in place, and never creates a
second pending entry for the same content. /settings/print-rules has
an optional auto-cleanup setting (0 = never, otherwise a number of
days).
Not every job type is included - weather/system reports re-fetch live data on reprint anyway (no staleness concern), but a raw image upload is stored as its already-downscaled/dithered print-ready version (a few KB, not the original photo) rather than the full upload.
/settings/modules lets each of the 8 catalog modules (lists,
message, weather, images, wifi, system, games, recipes) be switched on or
off individually:
- Disabling a module removes its tile from the home page and blocks
all of its routes - web UI pages,
/ui/*form posts, and/print/*API endpoints alike - with a 404, not just hiding the tile. - A module can have an additional precondition on top of its toggle:
recipes is only active while a recipe manager is selected under
/settings/recipes. With the recipe manager set to "Off" it is treated exactly like a disabled module; its checkbox here stays as saved and shows a note explaining why the module is hidden. - The home page's status strip shows how many of these 8 modules are currently active (toggle and precondition).
- History, the Settings pages themselves, and the generic automation webhook are outside this toggle system and stay reachable regardless.
- Two of the three watchers respect this:
fritzbox_wifi_watch.pyandstorm_warning_watch.pycheck thewifi/weathermodule state and skip cleanly if disabled.github_star_watch.pydoes not check it and will simply fail its print call if themessagemodule is disabled.
Next up: NFC-tag-triggered printing (just a URL in the tag, no app needed), Tandoor as a second recipe manager next to Mealie, and a proper icon set replacing the interim home-screen icons. The roadmap may change as ReceiptPi is tested on more hardware.
Issues and pull requests are welcome. New modules should use Flask Blueprints, submit print jobs through the central queue and avoid direct writes to the printer or settings files.
Raspberry Pi is a trademark of the Raspberry Pi Foundation. This project is not affiliated with, endorsed by, or sponsored by the Raspberry Pi Foundation.
Epson and TM-T88V are trademarks of Seiko Epson Corporation. This project is not affiliated with, endorsed by, or sponsored by Epson.
ReceiptPi is an independent, community-built project designed to run on Raspberry Pi hardware and to be compatible with Epson ESC/POS thermal printers.
The web UI's icons are from Lucide (static/icons/),
used under the ISC License - see static/icons/LICENSE
for the full text.
The web UI's typefaces are IBM Plex Sans and IBM Plex Mono
(static/fonts/), self-hosted rather than loaded from Google Fonts, used
under the SIL Open Font License 1.1 - see
static/fonts/LICENSE.txt for the full text.
MIT - see LICENSE.
