# HTTP listen address. Port 80 keeps the hostname-only URL (pictureframe-XXXX.local) # and is required for captive-portal probes; the kiosk-backend service gets # CAP_NET_BIND_SERVICE to bind it. Dev configs use :8080 so `go run` works unprivileged. addr = ":80" # HCI device to use for BLE sensors. Run "hciconfig -a" to list available # adapters. Default: "hci0" (built-in on most Pi models). # bluetooth_adapter = "hci0" # Display [display] # Blank the screen after this much time with no motion detected. # Set to "0s" to disable idle-blank entirely, recommended when you have no # motion sensor, so the screen stays on until turned off (e.g. via the HA switch). blank_after = "20m" # Screen-power backend: # "wlopm" (default): full KMS (vc4-kms-v3d), via the labwc compositor. # "vcgencmd": legacy fkms (vc4-fkms-v3d), no compositor. # backend = "wlopm" # # Connector wlopm targets (run `wlopm` to list; ignored by vcgencmd). # output = "HDMI-A-1" # # BCP-47 locale the kiosk uses to format the clock and date (e.g. "en-US", # "en-GB", "hu-HU"). Drives both the date wording and 12h/24h time. Default "en-US". # locale = "en-US" # # Captions shown under the readings on the frame, in your own words (e.g. # "Outside" / "Kint" / "Garden"). Empty or omitted hides that caption. # [display.labels] # outside = "Outside" # inside = "Inside" # humidity = "Humidity" # # Turn the screen off and on at the same times every day, as 24-hour "HH:MM" in # the display timezone. Leave one out to skip that half. # [display.schedule] # off_at = "23:00" # on_at = "07:00" # Slideshow [slideshow] # How long each image is shown before advancing to the next. interval = "120s" # Root for image storage. The fs backend writes/reads here directly; remote # backends use a subfolder (e.g. images/immich/), so `rm -rf images/immich/` # clears the remote cache without touching local uploads. images_dir = "images" # Randomize the slideshow order on each full cycle. randomize = false # Library backend [library] # "fs" (default): local uploads via the admin page only. # "immich": sync from a shared Immich album; admin page becomes read-only. backend = "fs" # Immich backend (only used when backend = "immich"). # Create a shared link in the Immich UI; password is optional but recommended. # Both link shapes work: /share/, or /s/ if it has a custom URL. # On Immich >= 2.6.0 the password is exchanged for a session token (kept out of # URLs and logs); older servers fall back to a query parameter. Either way, keep # the server behind TLS and don't put a high-trust password here. # # [library.immich] # share_url = "https://photos.example.com/share/" # share_password = "" # leave empty if the share has no password # sync_interval = "15m" # how often to reconcile with the album # Sensors, multiple [[sensor]] blocks allowed; each needs a unique id and type. # BLE sensor (example, uncomment and edit to enable; needs Bluetooth + the # service user in the 'bluetooth' group). A fresh frame ships with no sensors. # [[sensor]] # id = "living_room" # type = "ble" # role = "inside" # display role: kiosk indexes this sensor's readings as "inside:temperature" etc. # mac = "AA:BB:CC:DD:EE:FF" # address_type = "random" # "random" or "public" # Each characteristic maps a GATT UUID to a reading kind and decoder. # [[sensor.characteristic]] # uuid = "00002a6e-0000-1000-8000-00805f9b34fb" # kind = "temperature" # decoder = "int16le_div100" # [[sensor.characteristic]] # uuid = "00002a6f-0000-1000-8000-00805f9b34fb" # kind = "humidity" # decoder = "uint16be_div10" # [[sensor.characteristic]] # uuid = "0000a001-0000-1000-8000-00805f9b34fb" # kind = "motion" # decoder = "bool_nonzero" # poll_interval: how often to read all characteristics as a fallback/heartbeat. # Default: 80s # poll_interval = "80s" # reset_after: power-cycle the Bluetooth adapter if connecting has been failing # for this long. Useful when BlueZ wedges silently. Omit or set to "0s" to # disable, recommended if you have other BLE devices that must not be # interrupted. # Default: disabled # reset_after = "5m" # MQTT-subscriber sensor # Turns any MQTT state topic (Home Assistant, Zigbee2MQTT, Ecowitt → HA, etc.) # into a sensor reading. Requires [mqtt].broker to be set below. # # Each (role, kind) pair must be unique across all sensors, two sensors # claiming the same key (e.g. role="inside" + kind="temperature") would race # on the overlay. Configure separate kinds per topic. # # Decoder choices for `parser`: # raw_float, payload is a number, e.g. "23.4" # raw_int, payload is an integer # onoff_to_bool, "ON"/"on"/"true"/"1" → 1, else 0 (HA binary_sensor) # bool_nonzero, any non-zero byte → 1 (BLE-style binary) # # Bundled JSON payloads (one topic carrying many fields), set `json_field` to # a dotted path; the leaf value is fed to `parser`. Object navigation only: # arrays aren't indexable (e.g. "items.0.value" won't work). If you need to # pull from an array, point the publisher at a flatter topic instead. # # WARNING: do NOT subscribe to topics owned by this app's own bridge # (anything under base_topic, e.g. "picture-frame/sensor/.../..."), you'd # loop your own published readings back into the kiosk. # # Flat (HA standard sensor exposure, one topic per measurement): # [[sensor]] # id = "ha_living_room_temp" # type = "mqtt-subscriber" # role = "inside" # topic = "homeassistant/sensor/living_room_temperature/state" # kind = "temperature" # parser = "raw_float" # # [[sensor]] # id = "ha_living_room_humidity" # type = "mqtt-subscriber" # role = "inside" # topic = "homeassistant/sensor/living_room_humidity/state" # kind = "humidity" # parser = "raw_float" # # [[sensor]] # id = "ha_living_room_motion" # type = "mqtt-subscriber" # role = "inside" # topic = "homeassistant/binary_sensor/living_room_motion/state" # kind = "motion" # parser = "onoff_to_bool" # # Bundled JSON (one topic, dotted-path extraction): # [[sensor]] # id = "wittboy_outside_temp" # type = "mqtt-subscriber" # role = "outside" # the kiosk overlay prefers this over OWM for temp # topic = "ecowitt/wittboy" # json_field = "tempinf" # dotted paths supported, e.g. "main.temp" # kind = "temperature" # parser = "raw_float" # Mock sensor # Emits readings on a configurable interval. Useful for local development and # Playwright tests, no hardware required. # delta is added to value after every full cycle; omit or set to 0 for constant. # [[sensor]] # id = "mock_inside" # type = "mock" # role = "inside" # poll_interval = "3s" # # [[sensor.mock_reading]] # kind = "temperature" # value = 22.0 # delta = 0.5 # 22.0 → 22.5 → 23.0 → … # # [[sensor.mock_reading]] # kind = "humidity" # value = 48.0 # delta = -1.0 # 48.0 → 47.0 → 46.0 → … # # [[sensor.mock_reading]] # kind = "motion" # value = 1.0 # Weather (OpenWeatherMap) [weather] api_key = "" # OWM API key, leave empty to disable weather lat = 0.0 lon = 0.0 poll_interval = "10m" retry_interval = "30s" # first delay after a failed poll; backs off up to poll_interval (0 = retry only at poll_interval) units = "metric" # "standard" (K), "metric" (°C), or "imperial" (°F) # MQTT, connection settings (shared by [mqtt.bridge] and mqtt-subscriber sensors). # A connection is opened iff [mqtt.bridge].enabled is true OR any sensor of # type "mqtt-subscriber" is configured. # # [mqtt] # broker = "tcp://192.168.1.10:1883" # tcp:// or ssl://, host:port # username = "" # password = "" # client_id = "picture-frame" # must be unique per running instance, duplicates # # cause the broker to disconnect one of them # # [mqtt.bridge], outbound Home Assistant auto-discovery. # Publishes your BLE sensors' readings and a screen switch to your broker. # Only BLE sensors are bridged: mock sensors are a dev aid and mqtt-subscriber # readings already come from MQTT (republishing would loop). Disabled by default. # # [mqtt.bridge] # enabled = true # node_id = "picture_frame" # this frame's identity: HA device id + # # unique_id prefix + discovery topic group. # # Use distinct values for multiple frames. # base_topic = "picture-frame" # namespaces state/availability/command topics # discovery_prefix = "homeassistant" # must match HA's MQTT discovery prefix # stale_after = "10m" # mark a sensor unavailable in HA if no # # reading arrives within this window; # # set "0s" to disable freshness expiry # # (sensor stays online once first seen) # # Topics produced (node_id "picture_frame", base_topic "picture-frame", # sensor id "living_room"): # discovery: homeassistant/sensor/picture_frame/living_room_temperature/config # state: picture-frame/sensor/living_room/temperature # motion state: picture-frame/sensor/living_room/motion ("ON"/"OFF") # screen: picture-frame/switch/screen/state + .../switch/screen/set # availability: picture-frame/availability (bridge, LWT) and # picture-frame/sensor/living_room/availability (per-sensor freshness) # An entity is shown available in HA only when BOTH its bridge and per-sensor # availability are online, so a dead frame or a stale sensor both surface # correctly instead of leaving the last value looking live. # # Screen switch: ON re-enables motion auto-wake (auto mode), OFF forces the # screen off and suppresses auto-wake. Motion-driven blank/wake does not move # the switch. With no motion sensor (set blank_after = "0s"), ON/OFF is a plain # screen on/off. # WiFi AP fallback, moved-device recovery # When the configured WiFi disappears, the Pi raises its own AP so you can # reconnect via a phone without SSH. Active by default (ap_ssid is set). # Clear ap_ssid to disable the AP fallback entirely. [wifi] ap_ssid = "PictureFrame" # hotspot name; clear to disable AP fallback # ap_password = "" # WPA2 passphrase; leave absent for open AP ap_timeout_minutes = 3 # minutes disconnected before raising AP scan_interval_minutes = 5 # minutes between AP-mode scans for known networks # In-app self-updater. Checks always run; auto_update gates the nightly apply. # On by default, only same-major (non-breaking minor/patch) releases auto-apply; # a new major is surfaced but applied manually from the admin UI. [updater] auto_update = true # nightly auto-apply of same-major releases update_hour = 2 # device-local hour (0–23) for the scheduled check+apply # github_repo = "" # override the release source ("owner/name") for forks # github_token = "" # auth for a private release source (PF_GITHUB_TOKEN env also works)