Settings reference
Every user-adjustable setting, its YAML key, its environment variable and its value in the Docker configuration (config/docker.yaml over config/home.yaml and the printer model). tests/test_docker_release.py derives the inventory from the settings schema and fails when a setting lacks a row here or an .env.example line, or when a documented name is not a real setting.
- YAML: put keys in a file and select it with
PRINT_HANDY_CONFIG. In Docker, mount the file and pointPRINT_HANDY_CONFIGat its container path. - Environment:
PRINT_HANDY__SECTION__KEY=valueoverrides any file. Booleans accepttrue/false,1/0,yes/no,on/off. Integer settings need integer spelling (12, not12.0). Rows marked file only are lists or tables and can only come from YAML or legacy TOML; the one list exception isdelivery.methods, which also accepts a comma- or space-separated environment value (PRINT_HANDY__DELIVERY__METHODS=discord,bambu_handoff,printer, #270). - Precedence, later wins: vendor and printer model defaults (model),
config/home.yaml, the selected config file, environment. Settings are read once at start, so restart the service after a change. - A typo or invalid value stops startup and names the setting. YAML holds no secrets; see Secrets.
Schema, validation limits and TOML migration: CONFIGURATION-YAML.md. Standalone bot settings: STANDALONE.md. Printer vendor profiles: PRINTER-PROFILES.md.
| YAML key | Environment variable | Docker value | What it does |
|---|---|---|---|
layout.single_copy_edge_mm | PRINT_HANDY__LAYOUT__SINGLE_COPY_EDGE_MM | 1.0 model | Bed-edge clearance for one copy |
layout.multi_copy_edge_mm | PRINT_HANDY__LAYOUT__MULTI_COPY_EDGE_MM | 3.0 model | Bed-edge clearance for several copies |
layout.merge_edge_margin_mm | PRINT_HANDY__LAYOUT__MERGE_EDGE_MARGIN_MM | 8.0 model | Edge margin when merging plates |
layout.merge_spacing_mm | PRINT_HANDY__LAYOUT__MERGE_SPACING_MM | 10.0 model | Gap between merged plates' parts |
layout.max_merged_hours | PRINT_HANDY__LAYOUT__MAX_MERGED_HOURS | 12 | Merged plates longer than this stay separate |
prime_tower.size_mm | PRINT_HANDY__PRIME_TOWER__SIZE_MM | 45.0 model | Prime tower size |
prime_tower.part_clearance_mm | PRINT_HANDY__PRIME_TOWER__PART_CLEARANCE_MM | 8.0 model | Prime tower to part clearance |
prime_tower.edge_mm | PRINT_HANDY__PRIME_TOWER__EDGE_MM | 10.0 model | Prime tower to bed edge clearance |
prime_tower.merge_slack_mm | PRINT_HANDY__PRIME_TOWER__MERGE_SLACK_MM | 5.0 model | Extra room kept for the tower when merging |
printer.firmware | PRINT_HANDY__PRINTER__FIRMWARE | ZMod Klipper model | Firmware label shown in job summaries |
printer.start_macro | PRINT_HANDY__PRINTER__START_MACRO | START_PRINT model | Klipper start macro for stock Klipper models |
printer.end_macro | PRINT_HANDY__PRINTER__END_MACRO | END_PRINT model | Klipper end macro for stock Klipper models |
printer.name | PRINT_HANDY__PRINTER__NAME | Flashforge AD5X model | Printer name shown in job summaries |
printer.bed_mm | file only | [220.0, 220.0] model | Usable print area [x, y] in mm |
printer.max_print_height_mm | PRINT_HANDY__PRINTER__MAX_PRINT_HEIGHT_MM | 220.0 model | Tallest allowed print |
printer.nozzle_diameter_mm | PRINT_HANDY__PRINTER__NOZZLE_DIAMETER_MM | 0.4 model | Installed nozzle; slicing refuses any other |
printer.ifs_slots | PRINT_HANDY__PRINTER__IFS_SLOTS | 4 model | Filament slots; 1 for a single extruder |
printer.tool_count | PRINT_HANDY__PRINTER__TOOL_COUNT | 1 model | Independent toolheads on a tool-changer model (Creator 5 / 5 Pro: 4); slicer tool i is toolhead i, never an IFS slot |
printer.model | PRINT_HANDY__PRINTER__MODEL | flashforge.ad5x | Printer brand and model: flashforge.ad5x, sovol.sv06, klipper.generic, octoprint.marlin, bambulab.a1, bambulab.p1s (Bambu Lab models are offline only). Sets every model default marked model |
printer.vendor | PRINT_HANDY__PRINTER__VENDOR | flashforge model | Vendor part of the model key, set by the model; vendor plus a bare printer name is also accepted |
printer.slicer | PRINT_HANDY__PRINTER__SLICER | orca model | Slicer: orca everywhere; Bambu Lab printers default to bambu_studio and may pick orca |
slicer.flash_studio_path | PRINT_HANDY__SLICER__FLASH_STUDIO_PATH | empty | Optional path to a Flash Studio executable the user already installed. Empty keeps OrcaSlicer. A set but missing or non-executable path is an error, not a fallback. Never downloaded or bundled. |
printer.nozzle_type | PRINT_HANDY__PRINTER__NOZZLE_TYPE | unset | Optional nozzle material, limited to the model's supported types |
orca.templates | PRINT_HANDY__ORCA__TEMPLATES | the model's folder model | Folder with the Orca machine, process and filament JSON |
orca.command | file only | ["/usr/local/bin/orca-slicer"] | How to start the OrcaSlicer CLI; PRINT_HANDY_ORCA_CHANNEL selects stable (default) or alpha; both binaries exist only when WITH_ORCA=true |
delivery.methods | PRINT_HANDY__DELIVERY__METHODS | ["printer"] model | Post-slice options offered, from the vendor profile (for example printer); comma- or space-separated in the environment, an empty value keeps the file value |
bambu.command | file only | ["/usr/bin/bambu-studio"] | Bambu Studio CLI: the default slicer for Bambu Lab printers, Orca's one-shot fallback elsewhere; exists only in a WITH_BAMBU=true image |
bambu.lan_host | PRINT_HANDY__BAMBU__LAN_HOST | "" | Bambu Lab printer LAN address for read-only status (#187); must be set together with lan_serial; empty disables LAN status |
bambu.lan_serial | PRINT_HANDY__BAMBU__LAN_SERIAL | "" | The Bambu Lab printer's serial number (stored upper case); used in the MQTT topics, not as a credential |
bambu.lan_access_code_env | PRINT_HANDY__BAMBU__LAN_ACCESS_CODE_ENV | BAMBU_LAN_PASSWORD | Name of the environment variable holding the printer's LAN access code; the code itself is never a setting |
bambu.start_confirm_s | PRINT_HANDY__BAMBU__START_CONFIRM_S | 90 | Bambu Lab LAN print start (#271): how long, in whole seconds (1-300), the printer is polled every 10 s to report the submitted file as preparing or running before the start is left as uncertain; keep it well under the bot's 420 s start budget, which also covers the upload |
flashforge_stock.host | PRINT_HANDY__FLASHFORGE_STOCK__HOST | "" | Stock Flashforge printer LAN address; empty leaves the printer offline. The serial and check code stay in AD5X_SERIAL and AD5X_CHECK_CODE, never in a setting |
prusa.link_host | PRINT_HANDY__PRUSA__LINK_HOST | "" | Prusa printer LAN address for PrusaLink; empty leaves the printer offline (slicing, checks and Discord delivery only). Prusa models only |
elegoo_sdcp.host | PRINT_HANDY__ELEGOO_SDCP__HOST | "" | Centauri printer LAN address for SDCP; empty leaves the printer offline (slicing, checks and Discord delivery only). Elegoo SDCP models only. SDCP has no authentication; there is no key to configure |
prusa.api_key_env | PRINT_HANDY__PRUSA__API_KEY_ENV | "PRUSA_LINK_API_KEY" | Name of the environment variable holding the PrusaLink API key. The key itself is never a setting |
paths.hermes_home | PRINT_HANDY__PATHS__HERMES_HOME | /data/hermes | State folder (state/, logs/); the name is historical, Hermes is not required |
paths.archive | PRINT_HANDY__PATHS__ARCHIVE | /data/archive | Job archive |
paths.records_vault | PRINT_HANDY__PATHS__RECORDS_VAULT | /data/records | Print records: one JSON file per print in Prints/ |
paths.cache | PRINT_HANDY__PATHS__CACHE | /data/cache | Regenerable cache |
paths.hermes_bin | PRINT_HANDY__PATHS__HERMES_BIN | /usr/local/bin/hermes | Hermes CLI; Hermes installs only, not in the image |
paths.ffmpeg | PRINT_HANDY__PATHS__FFMPEG | ffmpeg | ffmpeg binary or name on PATH |
paths.hermes_agent | PRINT_HANDY__PATHS__HERMES_AGENT | /data/hermes/hermes-agent | Hermes agent checkout (Hermes installs only) |
paths.hermes_restart_helper | PRINT_HANDY__PATHS__HERMES_RESTART_HELPER | /data/hermes/restart_hermes.py | Hermes restart helper (Hermes installs only) |
services.moonraker_url | PRINT_HANDY__SERVICES__MOONRAKER_URL | empty | Klipper printers: Moonraker HTTP URL, e.g. http://printer.example:7125. Required by the listener |
services.moonraker_ws_url | PRINT_HANDY__SERVICES__MOONRAKER_WS_URL | empty | Moonraker websocket; derived from moonraker_url when empty |
services.camera_source | PRINT_HANDY__SERVICES__CAMERA_SOURCE | auto | Which camera still images come from (#294): auto follows the printer vendor's rule, or pin moonraker, bambu, go2rtc, or none |
services.camera_snapshot_url | PRINT_HANDY__SERVICES__CAMERA_SNAPSHOT_URL | empty | Camera snapshot URL; derived from the Moonraker host when empty |
services.spoolman_url | PRINT_HANDY__SERVICES__SPOOLMAN_URL | empty | Spoolman URL. The Docker config and Compose default to http://127.0.0.1:8000, the bundled Spoolman API in the same container. Set another URL, plus PRINT_HANDY_BUNDLED_SPOOLMAN=false, to use an existing external Spoolman instead |
services.openviking_url | PRINT_HANDY__SERVICES__OPENVIKING_URL | empty | Optional OpenViking memory store |
services.vision_chat_url | PRINT_HANDY__SERVICES__VISION_CHAT_URL | empty | OpenAI-compatible endpoint for camera checks and roasts; empty disables both |
services.stock_api_port | PRINT_HANDY__SERVICES__STOCK_API_PORT | 8898 | Flashforge stock API port for IFS presence |
services.octoprint_url | PRINT_HANDY__SERVICES__OCTOPRINT_URL | empty | OctoPrint printers (octoprint.marlin): OctoPrint URL. API key: OCTOPRINT_API_KEY secret |
discord.chat_id | PRINT_HANDY__DISCORD__CHAT_ID | empty | Discord channel ID |
discord.approval_user_id | PRINT_HANDY__DISCORD__APPROVAL_USER_ID | empty | Discord user ID allowed to approve |
discord.upload_max_bytes | PRINT_HANDY__DISCORD__UPLOAD_MAX_BYTES | 9500000 | Largest upload per file on Discord: video is shrunk to it, and a sliced file or Bambu package over it is reported with its host path instead of offered for sending (see PRINTER-PROFILES.md). Keep it under your server's Discord cap (10 MB unboosted). This row governs Discord only — Telegram and Matrix posts are sized to their own upload_max_bytes rows below |
discord.post_print_quiet_s | PRINT_HANDY__DISCORD__POST_PRINT_QUIET_S | 7200 | Retired (#524): the bot no longer resets chat sessions after prints. Kept in the schema so existing configs still load; session resets are the agent host's own setting |
discord.daily_reset_hour | PRINT_HANDY__DISCORD__DAILY_RESET_HOUR | 4 | Retired (#524): see post_print_quiet_s. The shipped Hermes profile template still resets its sessions daily at 4 AM on its own config |
discord.timezone | PRINT_HANDY__DISCORD__TIMEZONE | America/New_York | Display and reset time zone (IANA name) |
discord.bot_name | PRINT_HANDY__DISCORD__BOT_NAME | Print Handy | Name the bot uses for itself |
discord.question_timeout_s | PRINT_HANDY__DISCORD__QUESTION_TIMEOUT_S | 1800 | How long a Discord question waits for a button |
discord.safety_confirm_timeout_s | PRINT_HANDY__DISCORD__SAFETY_CONFIRM_TIMEOUT_S | 600 | How long a safety confirmation waits |
discord.upload_wait_s | PRINT_HANDY__DISCORD__UPLOAD_WAIT_S | 3600 | How long an upload-wait freeze stays active |
discord.cold_pull_outcome_s | PRINT_HANDY__DISCORD__COLD_PULL_OUTCOME_S | 1800 | How long a cold-pull outcome prompt stays open |
discord.token_env | PRINT_HANDY__DISCORD__TOKEN_ENV | DISCORD_BOT_TOKEN | Name of the environment variable holding the bot token |
discord.allowed_user_ids | PRINT_HANDY__DISCORD__ALLOWED_USER_IDS | empty | Optional whitelist of Discord user IDs for the standalone bot, comma or space separated. Empty (the default) lets everyone in the print channel use the bot — the channel is the access boundary (#386) |
discord.denied_user_ids | PRINT_HANDY__DISCORD__DENIED_USER_IDS | empty | Optional deny list of Discord user IDs, comma or space separated. A denied user is refused even when whitelisted or the approval user; a refusal is logged at INFO and answered in channel once per user per hour. Applies to bot messages, commands and button presses; the live camera Activity keeps its own allowlist (approval_user_id plus allowed_user_ids) |
discord.chat_llm_enabled | PRINT_HANDY__DISCORD__CHAT_LLM_ENABLED | true | Discord's chat-LLM switch (#466). false runs Discord as a pure bot: no stray-chat model call of any kind (no [llm] call, no [agent] lane call), unmatched messages get the short not-understood reply, even while [llm] stays configured for /print-design (which inherits it). The roast and the camera watchdog use the separate vision endpoint and their own switches, so they are unaffected. bot check names this switch on its llm and agent rows when it is off. The switch means the same thing in Hermes mode (#524 stage 3): a final plugin hook claims unmatched Discord text for the bot's own lanes — with the switch off the Hermes profile agent never sees the text, and with a lane configured the bot's lane answers instead of the in-process agent; with the switch on and no lane configured, the profile agent keeps answering as before |
discord.clarify_ui | PRINT_HANDY__DISCORD__CLARIFY_UI | false | Who draws Print Handy's questions on a Hermes install (#524 stage 2). false (the default): Hermes' adapter renders them, exactly as before. true: Print Handy activates its own prompt registry and posts questions itself by direct Discord REST, resolving clicks and typed answers itself — one renderer in both modes, matching the standalone rendering and the README mockups. The standalone bot always uses its own renderer either way. Read when the plugin registers; restart the gateway to change it. bot check reports the renderer on its clarify ui row |
telegram.token_env | PRINT_HANDY__TELEGRAM__TOKEN_ENV | TELEGRAM_BOT_TOKEN | Name of the environment variable holding the Telegram bot token |
telegram.chat_id | PRINT_HANDY__TELEGRAM__CHAT_ID | empty | Telegram chat (group) ID; group and channel IDs are negative |
telegram.allowed_user_ids | PRINT_HANDY__TELEGRAM__ALLOWED_USER_IDS | empty | Telegram user IDs allowed to use the standalone bot, comma or space separated |
telegram.approval_user_id | PRINT_HANDY__TELEGRAM__APPROVAL_USER_ID | empty | Telegram user ID allowed to approve |
telegram.upload_max_bytes | PRINT_HANDY__TELEGRAM__UPLOAD_MAX_BYTES | 50000000 | Largest file the Telegram bot uploads (the Bot API cloud limit); timelapse video is shrunk to this value, not to Discord's smaller cap |
matrix.homeserver_url | PRINT_HANDY__MATRIX__HOMESERVER_URL | empty | Matrix homeserver base URL, for example https://matrix.org or a self-hosted server |
matrix.access_token_env | PRINT_HANDY__MATRIX__ACCESS_TOKEN_ENV | MATRIX_ACCESS_TOKEN | Name of the environment variable holding the Matrix access token |
matrix.room_id | PRINT_HANDY__MATRIX__ROOM_ID | empty | Matrix room ID of the print room (starts with !); the room must be unencrypted |
matrix.allowed_user_ids | PRINT_HANDY__MATRIX__ALLOWED_USER_IDS | empty | Matrix user IDs allowed to use the standalone bot, full @user:server form, comma or space separated |
matrix.approval_user_id | PRINT_HANDY__MATRIX__APPROVAL_USER_ID | empty | Matrix user ID allowed to approve |
matrix.bot_user_id | PRINT_HANDY__MATRIX__BOT_USER_ID | empty | The bot's own Matrix user ID; empty asks the homeserver (whoami) at startup |
matrix.upload_max_bytes | PRINT_HANDY__MATRIX__UPLOAD_MAX_BYTES | 50000000 | Largest file the Matrix bot uploads; lowered to the homeserver's media limit when the server reports a smaller one. Timelapse video is shrunk to that resulting limit; if even the smallest shrink cannot fit it, the completion post says the timelapse was recorded but is too large for the server instead of attaching the video |
bot.mode | PRINT_HANDY__BOT__MODE | standalone | standalone or hermes; an explicit hermes in an existing config is unchanged — only an unset mode defaults to standalone (#524) |
bot.platform | PRINT_HANDY__BOT__PLATFORM | discord | Chat platform for the standalone bot (#164): discord, telegram or matrix |
bot.health_port | PRINT_HANDY__BOT__HEALTH_PORT | 0 | Serve GET /healthz on this port; 0 turns the HTTP check off |
bot.health_stale_s | PRINT_HANDY__BOT__HEALTH_STALE_S | 120 | Bot health fails when its heartbeat is older than this |
bot.sync_commands | PRINT_HANDY__BOT__SYNC_COMMANDS | true | Register the slash commands with Discord at startup |
llm.chat_url | PRINT_HANDY__LLM__CHAT_URL | empty | Optional OpenAI-compatible chat URL; empty: unmatched text gets the command list |
llm.model | PRINT_HANDY__LLM__MODEL | empty | LLM model name, required when chat_url is set |
llm.api_key_env | PRINT_HANDY__LLM__API_KEY_ENV | LLM_API_KEY | Name of the environment variable holding the LLM API key |
llm.timeout_s | PRINT_HANDY__LLM__TIMEOUT_S | 20 | LLM request timeout |
agent.chat_url | PRINT_HANDY__AGENT__CHAT_URL | empty | Stray-chat agent lane (#524 stage 3): an agent endpoint — any OpenAI-compatible chat completions URL, e.g. a Hermes agent behind its API — answers Discord free text the handlers did not take, ahead of the [llm] call. Empty: no agent lane; stray chat uses [llm] if configured, else the short not-understood reply. The endpoint answers in its own voice; its replies are posted as-is and can never run a command. discord.chat_llm_enabled=false turns this lane off too |
agent.model | PRINT_HANDY__AGENT__MODEL | empty | Agent model name; optional, unlike llm.model — empty omits model from the request, for endpoints that are not model-addressable |
agent.api_key_env | PRINT_HANDY__AGENT__API_KEY_ENV | AGENT_API_KEY | Name of the environment variable holding the agent endpoint's API key |
agent.timeout_s | PRINT_HANDY__AGENT__TIMEOUT_S | 120 | Agent request timeout; one attempt, no retries. Longer than llm.timeout_s because an agent turn does real work |
design.enabled | PRINT_HANDY__DESIGN__ENABLED | unset (auto) | /print-design / /design AI design (#388). Unset = auto: on whenever a design/LLM endpoint is configured (like roasts); false forces off (the command answers "not set up"); true forces on (an unconfigured endpoint is then reported, not silent) |
design.chat_url | PRINT_HANDY__DESIGN__CHAT_URL | empty (inherit) | Design model endpoint; empty uses llm.chat_url. Wins in Hermes mode too — the profile's auxiliary.print_design block is the fallback only when no endpoint is configured |
design.model | PRINT_HANDY__DESIGN__MODEL | empty (inherit) | Design model name; empty uses llm.model. Point this at a stronger model than chat if the endpoint offers one |
design.api_key_env | PRINT_HANDY__DESIGN__API_KEY_ENV | empty (inherit) | Environment variable holding the design endpoint's key; empty uses llm.api_key_env |
design.timeout_s | PRINT_HANDY__DESIGN__TIMEOUT_S | 180 | Design model-call timeout in seconds; also the OpenSCAD render wall-clock limit |
design.repair_attempts | PRINT_HANDY__DESIGN__REPAIR_ATTEMPTS | 3 | How many times a failed render is sent back to the model with the error before the turn gives up |
listener.tick_s | PRINT_HANDY__LISTENER__TICK_S | 600 | Listener housekeeping interval |
listener.ifs_presence_poll_s | PRINT_HANDY__LISTENER__IFS_PRESENCE_POLL_S | 15 | IFS spool presence poll |
listener.spool_nag_s | PRINT_HANDY__LISTENER__SPOOL_NAG_S | 600 | Reminder interval for an unanswered spool question |
listener.start_claim_ttl_s | PRINT_HANDY__LISTENER__START_CLAIM_TTL_S | 1800 | How long a start button stays claimed |
vision.model | PRINT_HANDY__VISION__MODEL | gemini-latest | Model for camera checks |
vision.roast_model | PRINT_HANDY__VISION__ROAST_MODEL | luna-latest | Model for the print-start roast |
vision.roast_enabled | PRINT_HANDY__VISION__ROAST_ENABLED | true | The one-line roast on the print-started post only (never on the completion post), on every platform (Telegram included). On by default: it runs whenever an LLM/vision endpoint is available (services.vision_chat_url plus CLIPROXY_API_KEY); false turns roasts off without disabling camera checks |
vision.auto_light_off_minutes | PRINT_HANDY__VISION__AUTO_LIGHT_OFF_MINUTES | 0 | Auto light-off (#516) for Moonraker, Bambu Lab and Elegoo SDCP printers: 0 is off (the default). When set (30 is the suggested value), the light turns on at print start and off again after the plate has been seen empty for that many minutes. Needs a vision endpoint, like the camera watchdog; on SDCP the turn-on works but auto-off waits on a verified camera source; see Auto light-off |
vision.light_object | PRINT_HANDY__VISION__LIGHT_OBJECT | empty | The Moonraker light object the auto light-off switches: led <name>, neopixel <name> or output_pin <name> (for example led chamber_led). Empty auto-discovers: the printer's light object is used only when it lists exactly one; with several, the listener logs the candidates and the feature stays off until one is named here |
visual_watch.sensitivity | PRINT_HANDY__VISUAL_WATCH__SENSITIVITY | 3 | Camera watchdog sensitivity, 1 (quietest) to 5 (most alerts); also sets the periodic check interval and stall threshold, see the project reference |
visual_watch.enabled | PRINT_HANDY__VISUAL_WATCH__ENABLED | true | Camera monitoring switch; the watchdog is alert-only and never controls the printer |
timelapse.mode | PRINT_HANDY__TIMELAPSE__MODE | derived | Timelapse mode (#246/#295): rendered (the printer renders it, moonraker-timelapse), container (in-container capture) or off. Empty in a file derives: rendered when a Moonraker URL is configured, else off |
timelapse.encoder | PRINT_HANDY__TIMELAPSE__ENCODER | cpu | ffmpeg encoder for the timelapse sizing shrink (#314): cpu (libx264), vaapi, qsv or nvenc. Hardware choices need the GPU passed through to the container (see DOCKER.md); a hardware encode that fails falls back to cpu automatically, so a completion post is never lost to the encoder |
timelapse.archive_host | PRINT_HANDY__TIMELAPSE__ARCHIVE_HOST | empty | SSH host for the timelapse archive; empty disables it |
timelapse.archive_dir | PRINT_HANDY__TIMELAPSE__ARCHIVE_DIR | empty | Archive folder on that host |
timelapse.ssh_key | PRINT_HANDY__TIMELAPSE__SSH_KEY | empty | Path of the SSH key in the container (mount it read-only) |
timelapse.render_wait_s | PRINT_HANDY__TIMELAPSE__RENDER_WAIT_S | 900 | Wait for the printer to render the timelapse. Delivery contract (#373): when archiving is on and the rendered video doesn't arrive within the wait (or the marker can't be matched to the job), the completion post itself carries a "Timelapse not attached" line with the reason instead of the video vanishing silently; and if the listener restarts during the wait, the pending completion re-drives the retrieval for the persisted job rather than posting the text without the video |
timelapse.render_poll_s | PRINT_HANDY__TIMELAPSE__RENDER_POLL_S | 10 | Poll interval while waiting |
timelapse.max_bytes | PRINT_HANDY__TIMELAPSE__MAX_BYTES | 209715200 | Largest timelapse kept |
timelapse.local_retention_days | PRINT_HANDY__TIMELAPSE__LOCAL_RETENTION_DAYS | 1 | Local copy retention |
timelapse.plan_clip_retention_days | PRINT_HANDY__TIMELAPSE__PLAN_CLIP_RETENTION_DAYS | 14 | Plan clip retention |
timelapse.vertical_short | PRINT_HANDY__TIMELAPSE__VERTICAL_SHORT | false | Also post a 9:16 vertical version of each finished timelapse (#317), alongside the timelapse itself |
timelapse.vertical_short_mode | PRINT_HANDY__TIMELAPSE__VERTICAL_SHORT_MODE | pad | pad fills the 9:16 frame with a blurred background; crop centre-crops |
timelapse.vertical_short_overlay | PRINT_HANDY__TIMELAPSE__VERTICAL_SHORT_OVERLAY | true | Burn the print title / printer / filament / time overlay into the vertical short |
timelapse.capture_enabled | PRINT_HANDY__TIMELAPSE__CAPTURE_ENABLED | false | In-container timelapse capture (#246): starts the bundled go2rtc (full Docker image only) to normalise the camera source on loopback. Off keeps today's printer-rendered behaviour. On stock Flashforge there is no go2rtc leg: the launched-print monitor captures from the camera source the printer advertises in /detail (see FLASHFORGE-STOCK.md) |
timelapse.capture_source_url | PRINT_HANDY__TIMELAPSE__CAPTURE_SOURCE_URL | empty | Explicit capture camera source (MJPEG/RTSP URL or a go2rtc ffmpeg: source). Empty derives it from services.camera_snapshot_url / the Moonraker webcam |
timelapse.capture_trigger | PRINT_HANDY__TIMELAPSE__CAPTURE_TRIGGER | interval | Stage 2 frame capture trigger (#246): interval (a frame every capture_interval_s), layer (a frame on each Moonraker layer change) or both |
timelapse.capture_interval_s | PRINT_HANDY__TIMELAPSE__CAPTURE_INTERVAL_S | 5.0 | Seconds between captured frames for the interval trigger (above 0, at most 3600) |
timelapse.capture_max_frames | PRINT_HANDY__TIMELAPSE__CAPTURE_MAX_FRAMES | 3600 | Per-job cap on captured frames; capture stops at the cap and keeps the frames already taken. 0 disables the cap |
timelapse.capture_max_bytes | PRINT_HANDY__TIMELAPSE__CAPTURE_MAX_BYTES | 536870912 | Per-job cap on captured frame bytes; capture stops at the cap and keeps the frames already taken. 0 disables the cap |
timelapse.capture_encoder | PRINT_HANDY__TIMELAPSE__CAPTURE_ENCODER | auto | Encoder preference for the Stage 3 assembly encode (#246): auto (VAAPI → NVENC → QSV → CPU), vaapi, nvenc, qsv or cpu. A failing or unavailable hardware encoder falls back to CPU; GPU paths are not verified on real hardware |
webui.enabled | PRINT_HANDY__WEBUI__ENABLED | false | Optional Web UI (#286) switch; off by default, and nothing listens while disabled |
webui.host | PRINT_HANDY__WEBUI__HOST | 127.0.0.1 | Host the Web UI binds to when enabled; a plain IP address or hostname |
webui.port | PRINT_HANDY__WEBUI__PORT | 0 | Port the Web UI binds to when enabled; 0 means no port has been chosen yet |
webui.access_token_env | PRINT_HANDY__WEBUI__ACCESS_TOKEN_ENV | WEBUI_ACCESS_TOKEN | Name of the environment variable holding the optional Web UI access token; the token itself is never a setting |
live_stream.enabled | PRINT_HANDY__LIVE_STREAM__ENABLED | false | Live camera viewing in Discord (#311) switch; off by default, and /print-live only reports that while disabled |
live_stream.voice_channel_id | PRINT_HANDY__LIVE_STREAM__VOICE_CHANNEL_ID | empty | The one Discord voice channel the live Activity is launched in; the bot never names another channel. Keep it a private channel (see DISCORD-BOT-SETUP.md) |
live_stream.auto_start_on_print | PRINT_HANDY__LIVE_STREAM__AUTO_START_ON_PRINT | false | Offer the live view automatically when a print starts; the viewing session itself still starts only from Discord |
live_stream.max_height | PRINT_HANDY__LIVE_STREAM__MAX_HEIGHT | 720 | Live feed height cap in pixels (144-2160); width follows the camera's aspect |
live_stream.fps | PRINT_HANDY__LIVE_STREAM__FPS | 15 | Live feed frame-rate cap (1-60) |
live_stream.max_bitrate_kbps | PRINT_HANDY__LIVE_STREAM__MAX_BITRATE_KBPS | 2500 | Live feed bitrate cap in kbit/s (100-20000) |
live_stream.encoder | PRINT_HANDY__LIVE_STREAM__ENCODER | auto | Encoder preference for the live feed: auto, vaapi, qsv, nvenc or cpu (the plan in print_handy/live_stream.py) |
live_stream.empty_timeout_s | PRINT_HANDY__LIVE_STREAM__EMPTY_TIMEOUT_S | 300 | Seconds the voice channel may stay empty of viewers before the live session ends itself |
live_stream.activity_origin | PRINT_HANDY__LIVE_STREAM__ACTIVITY_ORIGIN | empty | Public https origin the Discord Activity page is served from: the named Cloudflare Tunnel's hostname, mapped in the Discord Developer Portal (see DISCORD-BOT-SETUP.md); empty until deployed |
live_stream.port | PRINT_HANDY__LIVE_STREAM__PORT | 0 | Loopback port the supervised Activity server binds inside the deployment; 0 means no port has been chosen yet, so the server does not start |
logging.level | PRINT_HANDY__LOGGING__LEVEL | INFO | DEBUG, INFO, WARNING or ERROR |
logging.format | PRINT_HANDY__LOGGING__FORMAT | text | text (one readable line per event) or json (one JSON object per line) |
logging.file | PRINT_HANDY__LOGGING__FILE | empty | Also write a rotating log file (5 MiB, 3 backups) at this path, for example under /data/hermes/logs; empty logs to stderr only |
logging.levels | file only | empty | Per-subsystem verbosity (#390): a table of logger name to level, for example levels = { "print_handy.core.orca_adapter" = "DEBUG" } in TOML, so one subsystem can be raised without a wall of debug output; applied at process start |
maintenance.quick_hours | PRINT_HANDY__MAINTENANCE__QUICK_HOURS | 25 | Quick maintenance due after these print hours |
maintenance.full_hours | PRINT_HANDY__MAINTENANCE__FULL_HOURS | 75 | Full maintenance due after these print hours |
maintenance.full_days | PRINT_HANDY__MAINTENANCE__FULL_DAYS | 30 | Full maintenance due after these days |
nozzle_temp_offsets.<type> | PRINT_HANDY__NOZZLE_TEMP_OFFSETS__<TYPE> | per type: hardened_steel and stainless_steel 5.0, others 0.0 | Temperature offset, −10 to +15 °C, for brass, hardened_steel, stainless_steel, ruby, tungsten_carbide, diamond. The steel defaults compensate for lower thermal conductivity and apply on top of every filament's baseline temperature (all materials, not one); set a type here to override its default |
material_temps.<MATERIAL> | file only | see config/home.yaml | {nozzle, bed} fallback temperatures for PLA, PETG, ABS, ASA, TPU, PA, PC; Spoolman filament values win |
spool.prompt_brand | PRINT_HANDY__SPOOL__PROMPT_BRAND | false | Ask for the brand/vendor during the conversational new-spool walkthrough (#512). Off by default: the walkthrough skips brand entirely and the spool is created without one. Set true only if you track brands |
Material variant adjustments (#512). A named variant within a family — Silk PLA, PLA+, Matte PLA, and the fibre-reinforced *-CF / *-GF variants of any family — adjusts the family fallback above: Silk PLA +10 °C nozzle and 0.7× outer-wall speed when slicing (its gloss needs the slower wall), PLA+ / Matte PLA / reinforced variants +5 °C nozzle (PA-CF +10 °C), Matte PLA 0.9× outer-wall speed. The adjustment applies only to the material-default fallback: a temperature recorded on the Spoolman filament itself is explicit data and is never adjusted, and plain family labels (PLA, PETG, ABS, ASA, TPU, PA, PC) are never adjusted.
Photo label intake (#514). When any label-reading route is available, the new-spool walkthrough's first step also offers Use a photo of the label. The operator sends one photo of the spool's box or sticker as the next message in the channel, and the routes are tried in a fixed order: (1) the label's own barcode/QR code, decoded locally with the zxing-cpp library (a runtime dependency) — deterministic data the manufacturer printed; (2) the vision endpoint — the same availability rule the camera watchdog uses: PRINT_VISION_URL set, or services.vision_chat_url with its key; (3) local OCR with the tesseract binary (installed in the shipped container image; elsewhere, install tesseract-ocr to enable it), matched against the known filament types and the material/colour/brand vocabulary labels are printed from. The first route that reads a complete spec wins; partial results are never merged across routes. Every route extracts the same fields — brand, material (with sub-type), colour/blend, net weight and, when printed, the recommended nozzle/bed temperatures — and the values pre-fill the walkthrough at its confirmation step, which names the route that read them. Nothing is created from a photo without that confirmation, and Edit details walks the normal steps instead. A photo no route can use refuses with the reason and falls back to the manual steps. Label temperatures are recorded on the Spoolman filament the spool is created with, so they win over material defaults exactly like temperatures entered in Spoolman itself, for every material; a filament type that already exists keeps the temperatures it already has. The photo itself is never stored or recorded: it goes only to the one route that reads it (a local decoder, a local binary, or the configured vision endpoint). With no route available the button is not offered and the walkthrough is unchanged. The guided /spool flow does not offer the photo step: its gateway prompts answer with buttons or text and cannot carry an attachment. | cold_pull.<profile> | file only | from the vendor profile model | Hot and cold cold-pull temperatures; profile names are yours |
Custom vision endpoint (PRINT_VISION_URL)
The camera watchdog normally judges frames through the OpenAI-compatible services.vision_chat_url endpoint (with CLIPROXY_API_KEY). Setting the plain environment variable PRINT_VISION_URL — it is not a PRINT_HANDY__ setting and has no config-file form — points the watchdog at your own detector instead: a Frigate-adjacent sidecar, an Obico-style adapter, a small ONNX service, anything that can answer one HTTP call. When it is set, it is the route used for every camera check: the OpenAI-compatible route is not consulted, no API key is needed, and the camera watch counts as having a vision endpoint. It serves camera checks and spool-label photo reads (#514) — the print-start roast still needs services.vision_chat_url.
The contract is deliberately small:
- Request:
POSTto thePRINT_VISION_URLURL with headerContent-Type: application/jsonand noAuthorizationheader. The body is a JSON object:{"image": "<base64>", "prompt": "<text>"}.imageis the camera frame's JPEG bytes, standard base64, nodata:prefix.promptis the watchdog's question text (today: "Is there plastic on the bed under the nozzle, or is there spaghetti/detachment?"). Unlike the OpenAI-compatible route, no format instructions are prepended — the endpoint itself maps its analysis onto the verdict vocabulary below. - Response: HTTP 200 with a JSON object body:
{"verdict": "...", "reason": "..."}.verdictis matched case-insensitively (Print Handy lowercases it). The vocabulary isok,air,spaghetti,detached,empty_idle,uncertain; the watch also recognisessafe(healthy, likeok) andno/unsafe(suspicious). A missingverdictcounts asuncertain; any other value is passed through and matches neither the alert nor the healthy set.reasonis free text shown with the alert; a missing one counts as empty, and it is truncated to 300 characters. - How verdicts map:
ok/safeare healthy (atvisual_watch.sensitivity1–2 a healthy verdict can veto a pixel-diff stall alert).air,spaghettianddetachedalert.uncertainalso alerts — a frame that cannot be judged is treated as needing review, never as a pass.empty_idleis neither: an idle printer with an empty bed is expected. - Timeout and failures: Print Handy waits at most 30 seconds for the answer. Anything that goes wrong — endpoint unreachable, timeout, an HTTP error status, or a body that is not a JSON object — degrades to verdict
uncertainwith the detail "the vision check could not be completed", so a broken endpoint produces visible alerts instead of silently passing frames. The underlying error is written to the log (vision check failed via PRINT_VISION_URL: ...), which is where a mis-configured URL is diagnosed. - Label reads (#514): the same endpoint may also receive the spool-label prompt (the photo intake above): the request shape is identical, the
imageis the operator's label photo, and the response body should be the JSON object the prompt describes (brand / material / color / grams / nozzle_temp / bed_temp) instead of a verdict object. An endpoint that only answers verdicts will simply fail the label read — the walkthrough refuses with the reason and falls back to its manual steps.
Auto light-off
vision.auto_light_off_minutes (#516) is an opt-in for Moonraker, Bambu Lab and Elegoo SDCP printers: the printer's light turns on when a print starts, and off again once the plate has been seen empty for that many minutes after a print ends. It is off by default (0); 30 is the suggested value. It is the one deliberate exception to the camera watchdog's alert-only posture (#455), and it extends to exactly one accessory — the light. It never touches motion, heaters, pause/stop or a print in progress. The other families (PrusaLink, OctoPrint, stock FlashForge, offline) have no light to switch: for those printers the setting has no effect and bot check says so.
How it works:
- On is a state event, never a vision call. The listener's print-start transition sends the on command. That has to be so: once the light is off the camera frame is dark, so vision could never be the on-trigger.
- Off needs both a state gate and sustained vision. After a print reaches a terminal state, an idle window opens. While the printer stays idle, the plate is checked about every 5 minutes through the same camera snapshot and verdict path as the watchdog. Only verdicts of
empty_idle, sustained across the full configured minutes, turn the light off. Any other verdict resets the wait —uncertainincluded, so darkness or ambiguity never turns a light off, and a part left on the plate keeps the light on indefinitely (there is no long-idle override). A new print cancels the window. After lights-out, checking stops until the next print. - A verdict endpoint is required — the same gate as the camera watchdog (
services.vision_chat_urlplus its key, orPRINT_VISION_URL). With none configured the feature stays off, andbot checksays so on its auto light row. - The light target is discovered, never hardcoded. On Moonraker, the listener reads the printer's object list and considers
led,neopixelandoutput_pinobjects. Name one explicitly withvision.light_object(for exampleled chamber_led); with the setting empty, auto-discovery acts only when the printer lists exactly one candidate — with several, the listener logs the candidate names once and the feature stays off rather than guessing which light is "the" light. - On Bambu Lab the target is the chamber light — a fixed protocol target (MQTT
system/ledctrl,led_node: chamber_light), sovision.light_objectdoes not apply. Whether a model has one is read from the printer's own telemetry (lights_report): a model that reports no chamber light is never switched, and the listener logs the feature as unavailable for it once. - On Elegoo SDCP the target is the chamber light too (Cmd 403,
LightStatus.SecondLight), andvision.light_objectdoes not apply. Print-start turn-on works, but auto-off cannot run on SDCP yet: no SDCP camera source is verified (see ELEGOO.md), so the idle empty-plate check has no frame to judge and the light stays on after a print until it is switched some other way.bot checknames this on its auto light row rather than letting the setting look fully on. - Switching on Moonraker happens in Moonraker (
SET_LED/SET_PINthrough Moonraker's G-code endpoint). Home Assistant's Moonraker integration exposes the same light, so its state in Home Assistant follows automatically — Print Handy does not talk to Home Assistant for this. Flipping the light in Home Assistant is just a Moonraker state change like any other; the automation never polls or fights it — it acts only at print start and at the end of an empty window. - Failures are quiet and final for that transition. A failed light command is logged once and never blocks, delays or fails a print.
Printers list
By default Print Handy runs one printer, described by the [printer], [services], [bambu], [prusa], [elegoo_sdcp] and [flashforge_stock] settings above. A config file may instead declare a list of printers — printers: in YAML, [[printers]] in TOML — one entry per printer, each carrying its own connection keys (#333; design: MULTI-PRINTER-DESIGN.md). The list itself is file only: it cannot come from the environment.
The first entry is the default printer: the legacy single-printer settings are populated from it, so everything that reads [printer], the [services] URLs or a vendor section sees the first entry. When a list is declared, the legacy per-printer locations must be absent or empty in the same file and in the environment — setting both is refused at startup, with the offending locations named. A config with no list behaves exactly as before: loading synthesises a one-entry roster from the legacy settings, its id the slug of printer.name (or printer when there is no name).
Each entry is validated against its own model with the same rules as the legacy sections: a Bambu entry may not carry a Moonraker URL, lan_host and lan_serial come as a pair, a host belongs only to Elegoo SDCP and stock Flashforge entries, and so on. Two entries may not share an id, and two entries may not claim the same chat_id (an entry without one falls back to discord.chat_id).
Scalar keys of a declared entry can be overridden from the environment with the indexed spelling PRINT_HANDY__PRINTERS__<ID>__<KEY> — the entry's id uppercased in the <ID> position, the key in the <KEY> position. The environment can never create an entry, and id and model cannot be overridden: they are the entry's identity in the file.
Each printer's runtime state lives in its own folder, state/printers/<id>/ under the profile's state directory (#334): its hardware record (printer_hardware.json), its launch database (jobs.db — the print lease and consumed bed-clear approvals), and its maintenance record (maintenance.json). Job folders stay in the shared archive; a job's JOB.json records the printer_id it belongs to. On the first start after upgrading, the default printer adopts the previous root-level state files — a held lease and the hardware record carry over intact, new writes go to the per-printer folder, and the old files are left in place for one release so a rollback still finds them.
Channel routing (#335). Each printer is driven from its own channel. An entry's chat_id is that printer's channel on whichever platform the bot runs — a Discord channel ID, a Telegram chat ID, or a Matrix room ID — and an entry without one uses the platform's global chat (discord.chat_id, telegram.chat_id or matrix.room_id), which is therefore the default printer's channel unless the default entry sets its own. A message, command or button press in a printer's channel is handled for that printer end to end: intake stamps the job with that printer, status, stop, resume, spool, macro and maintenance commands read and act on that printer, and its prompts are answered only in its own channel. A channel no printer claims gets silence — there is no fallback to the default printer for unmapped channels, because an approval button carries no typed target: the channel is the target, and the default printer is named in output wherever a refusal or listing stands in for it. Two entries claiming one channel refuse startup with the channel and both printers named (the settings load already refuses the Discord case; the bot re-checks for the platform it runs). Delivery methods stay global in v1; they are not per-printer.
Listeners (#336). Every printer whose connection needs the Moonraker listener gets its own listener instance — one child process per printer under the Docker supervisor, one runner per printer in the native service — started with that printer's per-printer view: its Moonraker URL, its state folder, its spool and its camera. A listener that dies is restarted on its own backoff and never takes the bot or a sibling listener down; the aggregated health covers every listener, and both the bot status reply and the Web UI status page report each one.
Setting a roster up (#337). The CLI setup wizard asks "Add another printer?" after the first printer and writes the roster for you, and the in-chat guided setup offers the same at its review step (reply add). Both refuse to guess at a config they cannot rewrite faithfully: the wizard only rewrites an existing config/local.yaml it can parse and re-emit unchanged-in-meaning, and leaves an un-round-trippable file byte-for-byte untouched with instructions instead. The Web UI does not edit the roster in this version — its field catalog addresses global fields only — but the status page lists the roster read-only (id, model, connection, channel), and /api/status carries the same rows under printers.
| YAML key | Environment variable | Docker value | What it does |
|---|---|---|---|
printers.id | file only | — | Required. The printer's stable identity: a slug of a-z, 0-9, - and _ starting with a letter or digit, unique in the list. Renaming an id makes it a different printer as far as its state is concerned |
printers.model | file only | — | Required. A registry model key, spelled exactly as printer.model |
printers.name | PRINT_HANDY__PRINTERS__<ID>__NAME | — | Display name for this printer; defaults to the name the model's own defaults give printer.name |
printers.chat_id | PRINT_HANDY__PRINTERS__<ID>__CHAT_ID | — | This printer's channel on the platform the bot runs (Discord channel ID, Telegram chat ID, or Matrix room ID); empty falls back to that platform's global chat. Two entries may not claim the same channel |
printers.moonraker_url | PRINT_HANDY__PRINTERS__<ID>__MOONRAKER_URL | — | This entry's services.moonraker_url; must be empty for offline, Bambu LAN, stock Flashforge, PrusaLink and SDCP models |
printers.moonraker_ws_url | PRINT_HANDY__PRINTERS__<ID>__MOONRAKER_WS_URL | — | This entry's services.moonraker_ws_url; derived from moonraker_url when empty |
printers.octoprint_url | PRINT_HANDY__PRINTERS__<ID>__OCTOPRINT_URL | — | This entry's services.octoprint_url, for OctoPrint models |
printers.camera_snapshot_url | PRINT_HANDY__PRINTERS__<ID>__CAMERA_SNAPSHOT_URL | — | This entry's services.camera_snapshot_url; derived from moonraker_url when empty |
printers.lan_host | PRINT_HANDY__PRINTERS__<ID>__LAN_HOST | — | This entry's bambu.lan_host (Bambu Lab models only); set together with lan_serial |
printers.lan_serial | PRINT_HANDY__PRINTERS__<ID>__LAN_SERIAL | — | This entry's bambu.lan_serial (Bambu Lab models only); set together with lan_host |
printers.lan_access_code_env | PRINT_HANDY__PRINTERS__<ID>__LAN_ACCESS_CODE_ENV | — | This entry's bambu.lan_access_code_env: the environment variable holding the printer's LAN access code |
printers.start_confirm_s | PRINT_HANDY__PRINTERS__<ID>__START_CONFIRM_S | — | This entry's bambu.start_confirm_s |
printers.link_host | PRINT_HANDY__PRINTERS__<ID>__LINK_HOST | — | This entry's prusa.link_host (Prusa models only) |
printers.api_key_env | PRINT_HANDY__PRINTERS__<ID>__API_KEY_ENV | — | This entry's prusa.api_key_env: the environment variable holding the PrusaLink API key |
printers.host | PRINT_HANDY__PRINTERS__<ID>__HOST | — | This entry's elegoo_sdcp.host or flashforge_stock.host, chosen by the entry's model; no other model takes a host |
printers.nozzle_type | PRINT_HANDY__PRINTERS__<ID>__NOZZLE_TYPE | — | This entry's printer.nozzle_type override, limited to the model's supported types |
printers.tool_count | PRINT_HANDY__PRINTERS__<ID>__TOOL_COUNT | — | This entry's printer.tool_count override, for multi-tool models |
Compose variables
These configure the Compose deployment, not Print Handy itself. Set them in .env.
| Variable | Default | What it does |
|---|---|---|
PRINT_HANDY_IMAGE | none, required | Image the combined container runs: a locally built tag or a tested immutable digest. No published image exists yet |
PRINT_HANDY_DATA | ./data | Host folder holding the hermes, archive, records and cache volumes |
PRINT_HANDY_SPOOLMAN_DATA | ./data/spoolman | Host folder for the bundled Spoolman database, config, logs and backups |
PRINT_HANDY_CONFIG | /app/config/docker.yaml | Settings file Print Handy reads (YAML, or legacy .toml) |
DISCORD_CLIENT_ID | empty | The Discord application's ID for the live view (#311): a plain identifier (it appears in invite URLs), not a secret. The Activity server refuses to start without it when live viewing is enabled |
CLOUDFLARED_IMAGE | none, required for the sidecar | Image the optional cloudflared sidecar runs (Compose profile live-stream): a locally tested tag or immutable digest of the Cloudflare Tunnel connector |
TZ | UTC | Container timezone (IANA name), used by the bundled Spoolman; Print Handy itself uses discord.timezone |
Runtime environment
| Variable | Default | What it does |
|---|---|---|
PRINT_HANDY_ORCA_CHANNEL | stable | Selects the bundled stable or alpha OrcaSlicer at startup; invalid values stop the container |
PRINT_HANDY_BUNDLED_SPOOLMAN | true | true runs the bundled Spoolman API; false uses services.spoolman_url only |
SPOOLMAN_HOST | 127.0.0.1 | Bundled Spoolman bind address; widen it only together with an approved port mapping, see docs/DOCKER.md |
PRINT_VISION_URL | empty | Custom vision endpoint for camera checks; takes precedence over services.vision_chat_url and needs no key. See Custom vision endpoint |
WITH_ORCA=false (default) omits both Orca channels; slicing fails closed without either binary.
Build arguments
Image build inputs (docker build --build-arg NAME=value), not runtime settings. PRINT_HANDY_ORCA_CHANNEL is a runtime environment variable (default stable, accepts alpha) and requires an image built with WITH_ORCA=true.
| Argument | Default | What it does |
|---|---|---|
APP_CHANNEL | development | Application provenance label, stable or development; it does not change the source checkout |
WITH_ORCA | false | true installs the pinned official prebuilt OrcaSlicer release on both channels (stable and alpha both pin v2.5.0-alpha since #356, each SHA256 verified in docker/orca-prebuilt.sh); OrcaSlicer is never built from source; false omits it and slicing fails closed |
WITH_GO2RTC | true | true installs the pinned official go2rtc prebuilt (v1.9.15, SHA256 verified in docker/go2rtc-prebuilt.sh) and the VAAPI driver stack for in-container timelapse capture (#246); the published images set it; false omits both |
SPOOLMAN_REF | bed3f14ee77a3a571d31b14215d54b0b88a7519c | Pinned Spoolman v0.27.0 source commit; only its Python API backend is installed, never the web UI |
ORCA_BUILD_JOBS | auto | Bambu Studio source build only: parallel compile jobs, auto (memory-aware, below the CPU count) or a whole number from 1 to 64 |
ORCA_BUILD_MEM_PER_JOB_MIB | 2560 | Bambu Studio source build only: memory budgeted per job by auto, at least 256 |
ORCA_CCACHE_MAXSIZE | 5G | Bambu Studio source build only: size cap of the compiler cache inside the build cache |
WITH_BAMBU | false | true builds Bambu Studio from pinned source into /usr/bin/bambu-studio; false omits it and Bambu Lab slicing fails closed |
BAMBU_REF | 926a7192574bcb9b3a732e1ec59a46d79cb45466 | Pinned Bambu Studio 2.8.2 source commit |
BAMBU_BUILD_PROFILE | release-static-fhs | Names the Bambu Studio build options; rename it to reset the build trees |
Secrets
Secrets come from the environment only (Docker: .env, never committed or shared), never from YAML or the image.
| Variable | Used for |
|---|---|
DISCORD_BOT_TOKEN | Standalone bot token. The variable name is discord.token_env |
TELEGRAM_BOT_TOKEN | Telegram standalone bot token. The variable name is telegram.token_env |
MATRIX_ACCESS_TOKEN | Matrix standalone bot access token. The variable name is matrix.access_token_env |
WEBUI_ACCESS_TOKEN | Optional Web UI (#286) access token, required when it binds beyond loopback. The variable name is webui.access_token_env |
DISCORD_CLIENT_SECRET | Live camera in Discord (#311): the Discord application's client secret, used server-side for the Activity OAuth exchange. Never a setting |
LIVE_STREAM_SESSION_SECRET | Live camera in Discord (#311): signs the Activity session markers. Never a setting |
TUNNEL_TOKEN | Live camera in Discord (#311): the named Cloudflare Tunnel's token. Read only by the optional cloudflared sidecar; Print Handy itself never uses it |
LLM_API_KEY | Optional key for llm.chat_url. The variable name is llm.api_key_env |
AGENT_API_KEY | Optional key for agent.chat_url. The variable name is agent.api_key_env |
OCTOPRINT_API_KEY | octoprint.marlin printers: OctoPrint application key |
BAMBU_LAN_PASSWORD | Bambu Lab printers: the LAN access code for read-only status. The variable name is bambu.lan_access_code_env |
MOONRAKER_API_KEY | Moonraker API key, only when Moonraker requires one |
CLIPROXY_API_KEY | Key for services.vision_chat_url |
OPENVIKING_API_KEY | Key for services.openviking_url |
AD5X_SERIAL | Stock Flashforge printers: the printer serial number for the stock LAN API. Never a setting; set it together with flashforge_stock.host |
AD5X_CHECK_CODE | Stock Flashforge printers: the printer check code for the stock LAN API. Never a setting |
The MakerWorld account token is not an environment variable. It is the file ~/.config/bambu/token (Docker: /data/.config/bambu/token), needed only for MakerWorld links; see MakerWorld account token.
Legacy environment names
The names below predate PRINT_HANDY__SECTION__KEY and still work; new installs should use the long form. The full map is LEGACY_ENV in print_handy/settings.py, for example MOONRAKER_URL → services.moonraker_url, SPOOLMAN_URL → services.spoolman_url, PRINT_DISCORD_CHAT_ID → discord.chat_id.
