# digest-engine configuration template. # # Copy this to the container host as (for example) /opt/smart-home/digest/digest-engine.env, # fill in the real values, and `chmod 600` it. That file is what the compose # service loads via env_file; it must never be committed (the repo .gitignore # already covers .env / *.env, keeping this in line with the restic-password # convention in hosts/container-host/scripts/setup-container-host.sh). # # Every toggle below is the string "true" or "false", matching the ENABLE_X # convention used throughout this repo. # --------------------------------------------------------------------------- # Run behaviour # --------------------------------------------------------------------------- # How far back each source looks. Should match the digest cadence (4x/day = 6h). DIGEST_LOOKBACK_HOURS=6 DIGEST_OUTPUT_DIR=/output LOG_LEVEL=INFO # Which slot a run belongs to is derived from the container's local wall clock — # it is NOT passed in by systemd, because the timer runs a bare # `docker compose run --rm digest-engine` and a manual run is the same command. # Keep DIGEST_SCHEDULE in step with DIGEST_SCHEDULE in # hosts/container-host/scripts/setup-container-host.sh, which is what actually # sets the timer's OnCalendar. DIGEST_SCHEDULE=00,06,12,18 # The "I'm done working" slot. Only this run gets the recipe + shopping-list # suggestion in the household section; the other three omit it entirely. Must be # one of the DIGEST_SCHEDULE hours or no run will ever qualify. DIGEST_EVENING_HOUR=18 # Testing override — makes any run behave as the evening run, so the recipe # feature can be checked without waiting for 18:00. Leave false in production. DIGEST_FORCE_EVENING=false # --------------------------------------------------------------------------- # LLM synthesis — the existing Phase 3 Ollama host # --------------------------------------------------------------------------- OLLAMA_HOST=http://llm-host:11434 OLLAMA_MODEL=qwen2.5:14b-instruct OLLAMA_TIMEOUT=600 OLLAMA_TEMPERATURE=0.4 # --------------------------------------------------------------------------- # "Was the last digest viewed?" — see viewed_tracker.py. Same Mosquitto broker # every HA/thinclient-agent already uses; digest-engine only ever subscribes, # never publishes, to the smarthome/digest/viewed topic. Defaults assume the # same-Docker-network reachability every other container-host service relies on. # --------------------------------------------------------------------------- MQTT_BROKER_HOST=mosquitto MQTT_BROKER_PORT=1883 MQTT_USERNAME= MQTT_PASSWORD= # How long to wait for a retained message before giving up and assuming "viewed" # (the safe default — see viewed_tracker.last_viewed_at()'s docstring). MQTT_VIEWED_WAIT_SECONDS=3 # --------------------------------------------------------------------------- # Ingestion toggles — every source is off until you have provisioned its # credentials. A disabled or misconfigured source logs a warning and contributes # nothing; it never fails the run. # --------------------------------------------------------------------------- ENABLE_EMAIL_INGEST=false ENABLE_SIGNAL_INGEST=false ENABLE_TELEGRAM_INGEST=false ENABLE_DISCORD_INGEST=false # Off by default — read digest-engine/README.md before enabling, real ban risk # even with the headful-Chromium mitigation. Use a secondary/non-critical number. ENABLE_WHATSAPP_INGEST=false ENABLE_NEWS_INGEST=true ENABLE_FINANCIAL_INGEST=true # Off by default on purpose — read the OpenSky terms-of-use note further down # before turning this on, it is a licensing decision, not a technical one. ENABLE_FLIGHT_TRAFFIC_INGEST=false # Off by default because it does nothing without AISSTREAM_API_KEY. ENABLE_NAVAL_TRAFFIC_INGEST=false # Off by default on purpose — this one talks to your firewall. Read the OPNsense # section at the bottom of this file and IDSconf.json.example before enabling. ENABLE_OPNSENSE_IDS_INGEST=false # The two household sources. Off until you have made a Grocy API key and a # Nextcloud app password — see their sections below. ENABLE_CALDAV_INGEST=false ENABLE_GROCY_INGEST=false # --------------------------------------------------------------------------- # Email (IMAP) # --------------------------------------------------------------------------- # Auth is an app-specific password, NOT OAuth2. For Gmail: enable 2FA, then # generate one at myaccount.google.com -> Security -> App passwords. Other # providers have an equivalent. The mailbox is opened read-only, so nothing is # ever marked \Seen, archived or deleted. EMAIL_IMAP_HOST=imap.gmail.com EMAIL_IMAP_PORT=993 EMAIL_USERNAME=you@example.com EMAIL_PASSWORD= EMAIL_FOLDER=INBOX # Overrides DIGEST_LOOKBACK_HOURS for mail only; leave unset to inherit it. #EMAIL_LOOKBACK_HOURS=6 # --------------------------------------------------------------------------- # Signal # --------------------------------------------------------------------------- # digest-engine does NOT run signal-cli. Add your own signal-cli JSON-RPC daemon # as a separate compose service on the same network (linked as a secondary device # to your account) and point this at it. See # https://github.com/AsamK/signal-cli/blob/master/man/signal-cli-jsonrpc.5.adoc SIGNAL_CLI_URL=http://signal-cli:8080 # Your registered number in E.164 form; only needed if the daemon serves more # than one account. SIGNAL_ACCOUNT= SIGNAL_RECEIVE_TIMEOUT=10 # --------------------------------------------------------------------------- # Telegram (Telethon / MTProto userbot) # --------------------------------------------------------------------------- # Get the API id/hash from https://my.telegram.org -> API development tools. # The session file is created ONCE, interactively: # docker compose run --rm --entrypoint python digest-engine ingest/telegram_login.py TELEGRAM_API_ID= TELEGRAM_API_HASH= TELEGRAM_SESSION_PATH=/data/telegram.session TELEGRAM_MAX_DIALOGS=25 TELEGRAM_MAX_MESSAGES_PER_DIALOG=50 # --------------------------------------------------------------------------- # Discord # --------------------------------------------------------------------------- # A bot token from https://discord.com/developers/applications, with the # "Message Content" privileged gateway intent enabled. The bot can only ever see # guilds it was invited to — Discord enforces that, not this code. DISCORD_BOT_TOKEN= DISCORD_MAX_MESSAGES_PER_CHANNEL=50 DISCORD_TIMEOUT=120 # --------------------------------------------------------------------------- # WhatsApp (whatsapp-bridge sidecar) # --------------------------------------------------------------------------- # Path, inside the digest-engine container, of the JSON-lines file the bridge # writes. Drained (renamed, read, deleted) once per run. WHATSAPP_MESSAGES_PATH=/data/whatsapp-bridge/messages.jsonl # --------------------------------------------------------------------------- # News # --------------------------------------------------------------------------- NEWS_OPML_PATH=/app/feeds/curated-feeds.opml NEWS_MAX_ENTRIES_PER_FEED=15 # --------------------------------------------------------------------------- # Financial # --------------------------------------------------------------------------- # Free FRED API key: https://fred.stlouisfed.org/docs/api/api_key.html FRED_API_KEY= # Comma-separated FRED series IDs. Default UNRATE = US unemployment rate. # Browse more at https://fred.stlouisfed.org (e.g. UNRATE,CPIAUCSL,FEDFUNDS). FRED_SERIES=UNRATE # Comma-separated Stooq symbols (keyless CSV, no rate cap worth worrying about). # ^spx / ^dax / ^ndq = indices, cl.f = WTI crude futures, gc.f = gold, # aapl.us style = individual US equities. STOOQ_SYMBOLS=^spx,^dax,cl.f # --------------------------------------------------------------------------- # Air traffic (OpenSky Network) # --------------------------------------------------------------------------- # Extra evidence for the political section, not a section of its own. # # LICENSING — decide this before setting ENABLE_FLIGHT_TRAFFIC_INGEST=true. # OpenSky's Terms of Use (https://opensky-network.org/about/terms-of-use) license # the data for non-profit research/education and personal use, and say that using # the REST API "in any operational capacity — including integration into a live # product, service, or automated system (even if only internal)" needs a prior # written agreement. A timer-driven digest is arguably such a system. Personal, # non-commercial household use is the intended case here; if that is not you, # mail contact@opensky-network.org first. Attribution to OpenSky is required. # # RATE LIMITS — a daily credit budget per endpoint. Anonymous (by IP): 400/day, # current state only, 10s resolution. With client credentials: 4,000/day, 5s # resolution. Running an ADS-B feeder: 8,000/day. A /states/all call costs 1 # credit for a box <=25 sq deg, 2 for 25-100, 3 for 100-400, 4 for global. The # default region list costs ~12 credits/run, ~48/day at 4 runs/day — fine even # anonymously, which is why the credentials below are optional. # # Optional. Create an API client at https://opensky-network.org/my-opensky (the # account page). Username/password basic auth stopped working on 2026-03-18. OPENSKY_CLIENT_ID= OPENSKY_CLIENT_SECRET= # REVIEW THIS LIST. Semicolon-separated `Name:lamin,lomin,lamax,lomax` boxes in # decimal degrees. What ships below is a placeholder set of currently-tense # regions, in the same spirit as the placeholder outlets in curated-feeds.opml — # conflict zones move and nothing here updates itself. Keep boxes small: cost per # call and context size both scale with area. FLIGHT_REGIONS=Eastern Mediterranean / Levant:30,32,37,37;Black Sea / Ukraine:44,29,53,41;Persian Gulf / Strait of Hormuz:24,50,30,60;Red Sea / Bab el-Mandeb:12,38,20,45;Taiwan Strait:21,117,27,124;Baltic / Kaliningrad:53,17,60,28 # Callsign prefixes treated as military. A heuristic that only catches aircraft # flying under published national callsigns (REACH/ASCOT/CANFORCE and friends); # anything with its transponder off is invisible to ADS-B entirely. FLIGHT_MILITARY_CALLSIGN_PREFIXES=RCH,CNV,RRR,CFC,GAF,IAM,FAF,BAF,NAF,PLF,HAF,NATO,SVF FLIGHT_MAX_MILITARY_PER_REGION=20 # --------------------------------------------------------------------------- # Naval traffic (aisstream.io) # --------------------------------------------------------------------------- # Free key, no card, from https://aisstream.io after signing in (GitHub etc.). # The service is WebSocket-only and self-describes as beta with no uptime SLA, so # a run that returns nothing is normal rather than broken. # # What this measures: civil merchant traffic through chokepoints. What it does # NOT measure: naval force posture. Warships sail with AIS off as a matter of # routine and the AIS "military ops" type code is self-declared, so a quiet box # here means nothing at all about warships. It is useful for the opposite # reading — merchant traffic abandoning a route, which shows up in freight and # insurance costs and pairs with the financial indicators above. AISSTREAM_API_KEY= # Same `Name:lamin,lomin,lamax,lomax` format as FLIGHT_REGIONS. NAVAL_REGIONS=Red Sea / Bab el-Mandeb:12,38,20,45;Strait of Hormuz:24,54,28,58;Black Sea:41,27,47,42;Taiwan Strait:21,117,27,124;Suez Canal approaches:29,32,32,34 # Seconds the socket is held open per run. This is a density sample, not a # census; raising it makes a oneshot digest sit on a socket for longer. NAVAL_SAMPLE_SECONDS=20 # --------------------------------------------------------------------------- # Household calendar (Nextcloud CalDAV, Phase 8) # --------------------------------------------------------------------------- # Auth is a Nextcloud APP PASSWORD, not your account password and not OAuth2 — # same reasoning as the IMAP section above. Create one at Nextcloud -> Settings -> # Security -> Devices & sessions -> Create new app password. It is mandatory once # two-factor authentication is enabled (the DAV endpoints cannot prompt for a # second factor) and it is revocable on its own if this host is ever compromised. # # Point CALDAV_URL at the DAV root; the client discovers the principal and its # calendars from there. The per-user form # https://cloud.example.com/remote.php/dav/principals/users// also works. # # Read-only: this only ever issues CalDAV read requests. It never creates, moves # or deletes an event. CALDAV_URL=https://cloud.example.com/remote.php/dav CALDAV_USERNAME= CALDAV_PASSWORD= # Comma-separated calendar display names to include. Empty = every calendar the # account can see (which includes ones shared with you). CALDAV_CALENDARS= # The calendar window is asymmetric: DIGEST_LOOKBACK_HOURS backwards (to catch # what already happened today and anything still in progress) and this forwards. CALDAV_LOOKAHEAD_HOURS=48 CALDAV_MAX_EVENTS=100 # Only set false for a self-signed internal cert, and understand that it sends # the app password over an unverified session. CALDAV_VERIFY_TLS=true # --------------------------------------------------------------------------- # Kitchen inventory (Grocy, Phase 7) # --------------------------------------------------------------------------- # GROCY_URL is the container name on the shared compose network — Grocy listens # on port 80 inside the container (published to the LAN as 9283, which this does # not need to go through). Use http://:9283 only if Grocy runs elsewhere. # # The API key is per user: Grocy -> Settings -> Manage API keys -> add. Note that # a Grocy key is NOT scoped — it carries that user's full read and write rights, # so give this one its own Grocy user. The read-only guarantee comes from # ingest/grocy.py issuing GETs and nothing else; see the invariant at the top of # that file, which also names the write endpoints it deliberately never calls. # # Nothing in this component ever adds to Grocy's shopping list, consumes stock, # executes a chore or charges a battery — including the evening recipe feature, # whose shopping list is a suggestion printed in the digest for you to act on. GROCY_URL=http://grocy GROCY_API_KEY= # Days ahead a product counts as "due soon" (Grocy's /stock/volatile default is 5). GROCY_DUE_SOON_DAYS=5 # Days ahead a chore or battery charge is worth mentioning. GROCY_TASK_HORIZON_DAYS=7 # Cap on the in-stock product list, which is only sent on the evening run so the # recipe's shopping list can tell "already have it" from "buy it". Raise it if # your pantry is bigger than this, or the shopping list will suggest things you # already own (a warning is logged when the cap bites). GROCY_MAX_STOCK_ITEMS=200 # --------------------------------------------------------------------------- # Home network intrusion detection (OPNsense + Suricata) # --------------------------------------------------------------------------- # Feeds a short "is anything wrong with the home network" item into the # HOUSEHOLD section. Everything except the toggle and the path below lives in # IDSconf.json, because that file carries an API key/secret — copy # IDSconf.json.example to the digest data volume, chmod 600 it, and read its # _readme keys; they cover creating the OPNsense API key and scoping the user. # # Suricata is part of OPNsense core (no os-suricata plugin to install), but it # is OFF on a stock install — enable it at Services -> Intrusion Detection and # download a ruleset first, or every run will honestly report that the IDS was # not running. # # This is a pure pull over HTTPS: GET /api/ids/service/status and POST # /api/ids/service/query_alerts (a POST that only reads /var/log/suricata/ # eve.json), and nothing else, ever. It never starts a packet capture, never # reconfigures or restarts Suricata, and never clears an alert log. # # POSTURE NOTE: the container host and the firewall sit on the same flat LAN — # no VLAN segmentation is implemented in this project yet (docs/project-plan.md # only plans one for cameras). Scope the OPNsense user to the single "Services: # Intrusion Detection" privilege and nothing else. Be aware that OPNsense ACLs # are page-level: that privilege still covers the IDS start/stop endpoints, so # the read-only guarantee comes from this code, not from the firewall. # # Path INSIDE the container. /data is the bind-mounted digest data volume. OPNSENSE_IDS_CONF_PATH=/data/IDSconf.json