Files
meshcore-bot/config.ini.example
agessaman 5b1bcac2a0 Implement temperature formatting in weather commands and services
- Added new configuration options in `config.ini.example` for customizing the display of daily high/low temperatures.
- Introduced `format_temperature_high_low` function in `utils.py` to format temperature strings based on user-defined templates.
- Updated `WxCommand`, `GlobalWxCommand`, and `WeatherService` classes to utilize the new formatting function, enhancing the presentation of temperature data in weather forecasts.
- Refactored existing high/low temperature handling to improve code clarity and maintainability.

These changes enhance the flexibility and readability of temperature displays in weather-related outputs.
2026-03-29 21:22:00 -07:00

1747 lines
71 KiB
Plaintext
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
[Connection]
# Connection type: serial, ble, or tcp
# serial: Connect via USB serial port
# ble: Connect via Bluetooth Low Energy
# tcp: Connect via TCP/IP
connection_type = serial
# Serial port (for serial connection)
# Common ports: /dev/ttyUSB0, /dev/tty.usbserial-*, COM3 (Windows)
serial_port = /dev/ttyUSB0
# BLE device name (for BLE connection)
# Leave commented out for auto-detection, or specify exact device name
#ble_device_name = MeshCore
# TCP hostname or IP address (for TCP connection)
#hostname = 192.168.1.60
# TCP port (for TCP connection)
#tcp_port = 5000
# Connection timeout in seconds
timeout = 30
[Bot]
# Bot name for identification and logging
bot_name = MeshCoreBot
# Automatically update device name to match bot_name on startup
# true: Device name will be updated to match bot_name if they differ (default)
# false: Device name will not be updated, keep device name separate from config
auto_update_device_name = true
# RF Data Correlation Settings
# Time window for correlating RF data with messages (seconds)
rf_data_timeout = 15.0
# Time to wait for RF data correlation (seconds)
message_correlation_timeout = 10.0
# Enable enhanced correlation strategies
enable_enhanced_correlation = true
# Bot node ID (leave empty for auto-assignment)
node_id =
# Command prefix (optional)
# If set, all commands must start with this prefix (e.g., "!", ".", "b", "abc")
# Examples: "!" for !ping, "." for .ping, "b" for bping, "abc" for abcping
# Leave empty or unset to allow commands without prefix (backward compatible)
#command_prefix =
# Enable/disable bot responses
# true: Bot will respond to keywords and commands
# false: Bot will only listen and log messages
enabled = true
# Passive mode (only listen, don't respond)
# true: Bot will not send any messages
# false: Bot will respond normally
passive_mode = false
# Rate limiting in seconds between messages
# Prevents spam by limiting how often the bot can send messages
rate_limit_seconds = 10
# Bot transmission rate limit in seconds between bot messages
# Prevents bot from overwhelming the mesh network
bot_tx_rate_limit_seconds = 1.0
# Per-user rate limit: minimum seconds between bot replies to the same user
# User is identified by public key when available (DMs and channel when provided), else sender name
# Channel senders are often matched by name only. Set to 0 or disable to effectively turn off per-user limiting
per_user_rate_limit_seconds = 5
# Enable or disable per-user rate limiting (true/false)
per_user_rate_limit_enabled = true
# Transmission delay in milliseconds before sending messages
# Helps prevent message collisions on the mesh network
# Recommended: 100-500ms for busy networks, 0 for quiet networks
tx_delay_ms = 250
# DM retry settings for improved reliability (meshcore-2.1.6+)
# Maximum number of retry attempts for failed DM sends
dm_max_retries = 3
# Maximum flood attempts (when path reset is needed)
dm_max_flood_attempts = 2
# Number of attempts before switching to flood mode
dm_flood_after = 2
# Timezone for bot operations
# Use standard timezone names (e.g., America/New_York, Europe/London, UTC)
# Leave empty to use system timezone
timezone =
# Bot location for geographic proximity calculations and astronomical data
# Default latitude for bot location (decimal degrees)
# Example: 40.7128 for New York City, 48.50 for Victoria BC
bot_latitude = 40.7128
# Default longitude for bot location (decimal degrees)
# Example: -74.0060 for New York City, -123.00 for Victoria BC
bot_longitude = -74.0060
# Maximum number of channels to fetch from MeshCore node
# MeshCore supports up to 40 channels (default: 40)
# Set to a lower value if you want to limit channel fetching for performance
max_channels = 12
# Interval-based advertising settings
# Send periodic flood adverts at specified intervals
# 0: Disabled (default)
# >0: Send flood advert every N hours
advert_interval_hours = 0
# Send startup advert when bot finishes initializing
# false: No startup advert (default)
# zero-hop: Send local broadcast advert
# flood: Send network-wide flood advert
startup_advert = false
# Auto-manage contact list when new contacts are discovered
# device: Device handles auto-addition using standard auto-discovery mode, bot manages contact list capacity (purge old contacts when near limits)
# bot: Bot automatically adds new companion contacts to device, bot manages contact list capacity (purge old contacts when near limits)
# false: Manual mode - no automatic actions, use !repeater commands to manage contacts (default)
auto_manage_contacts = bot
# Database path for main bot database
# Default: meshcore_bot.db
db_path = meshcore_bot.db
# Local plugins directory (optional)
# Directory containing commands/, service_plugins/, and optional config.ini.
# Relative paths are resolved from the bot root; absolute paths used as-is.
# Default: local (i.e. bot_root/local). Changing this requires a bot restart.
#local_dir_path = local
# Seconds to wait after a failed service restart before retrying (default: 300)
service_restart_backoff_seconds = 300
[Channels]
# Channels to monitor (comma-separated)
# Bot will only respond to messages on these channels
# Use exact channel names as configured on your MeshCore node
# Use hashtags for hashtag channels (i.e. #mybotchannel, #weather, etc.)
monitor_channels = #mybotchannel
# Enable DM responses
# true: Bot will respond to direct messages
# false: Bot will ignore direct messages
respond_to_dms = true
# Limit channel responses to certain keywords (optional)
# When set, only these triggers (command/keyword names) are answered in channels;
# DMs always get all triggers. Use to reduce channel floods by making heavy
# triggers (wx, satpass, joke, etc.) DM-only. Names are case-insensitive.
# Leave empty or omit to allow all triggers in monitored channels.
# Interaction: Commands with per-command "channels = " (empty) are already DM-only;
# channel_keywords is a global whitelist for channel. Both can be used together.
# Example: channel_keywords = help,ping,test,hello
# channel_keywords =
# Set a custom prefix length for the public keys to identify repeaters
# 1 = 2 hex chars (e.g. 7E), 2 = 4 hex chars (e.g. 7E42). Also used as the mesh
# graph key length; if the mesh often uses 2-byte paths, set to 2 to avoid
# conflating distinct links (see Path Command / path-command-config.md).
prefix_bytes = 1
# Flood scope for channel messages (MeshCore regions; optional)
# Empty, * or 0: classic flood (default). Set to a region name (e.g. west) to limit
# channel sends to that scope. Dynamic "reply with same scope as sender" is not
# supported until the protocol exposes scope on received channel messages.
# flood_scope =
[Banned_Users]
# List of banned sender names (comma-separated). Matching is prefix (starts-with):
# "Awful Username" also matches "Awful Username 🍆". No bot responses in channels or DMs.
banned_users =
[Localization]
# Language code for bot responses (en, es, es-MX, es-ES, fr, de, ja, etc.)
# Default: en (English)
# The bot will use translations from translations/{language}.json
# Supports locale codes:
# - Simple codes: en, es, fr, de, ja
# - Locale codes: es-MX (Mexican Spanish), es-ES (Spain Spanish), fr-CA (Canadian French)
# If locale-specific file not found, falls back to base language (e.g., es.json)
# If translation file is missing or key not found, falls back to English
language = en
# Path to translation files directory (relative to bot root)
# Default: translations/
translation_path = translations/
[Admin_ACL]
# Admin Access Control List (ACL) for restricted commands
# Only users with public keys listed here can execute admin commands
#
# SECURITY IMPORTANT:
# - Public keys MUST be exactly 64 hexadecimal characters (ed25519 format)
# - Invalid formats will be rejected with error logs
# - Empty or whitespace-only values disable admin access
# - Keys are case-insensitive (normalized to lowercase)
#
# Format: comma-separated list of 64-character hex public keys (without spaces)
# Example: f5d2b56d19b24412756933e917d4632e088cdd5daeadc9002feca73bf5d2b56d,1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef
#
# IMPORTANT: Leave blank to disable all admin commands. Set your actual admin pubkey(s) here.
admin_pubkeys =
# Commands that require admin access (comma-separated)
# These commands will only work for users in the admin_pubkeys list
# reload: Reload configuration without restarting (radio settings cannot be changed)
# channelpause: DM-only; channelpause / channelresume — pause or resume bot responses on channels (not persisted)
admin_commands = repeater,webviewer,reload,channelpause
[Plugin_Overrides]
# Plugin Overrides - Use alternative plugin implementations
# Format: command_name = alternative_file_name
# The alternative_file_name should be the name of a Python file (without .py extension)
# in the modules/commands/alternatives/ directory
#
# Example: To use an alternative weather plugin for international users:
# wx = wx_international
#
# This will replace the default wx command with the plugin from
# modules/commands/alternatives/wx_international.py
#
# Note: The alternative plugin must have the same 'name' metadata as the command
# it's replacing, or the override will use the alternative plugin's name instead.
#
# Local plugins: You can add your own command and service plugins without editing
# bot code. Put command plugins in local/commands/ and service plugins in
# local/service_plugins/. Use local/config.ini for their settings. See docs/local-plugins.md.
[Companion_Purge]
# Enable companion contact purging
# true: Purge inactive companions when contact list is full
# false: Never purge companions (default: false for safety)
companion_purge_enabled = false
# Days since last DM to consider companion inactive
# Companions who haven't DM'd the bot in this many days may be purged
companion_dm_threshold_days = 30
# Days since last advert to consider companion inactive
# Companions who haven't adverted in this many days may be purged
companion_advert_threshold_days = 30
# Minimum days since last activity (DM or advert) before purge
# Companions must be inactive for at least this many days
companion_min_inactive_days = 30
[Keywords]
# Available placeholders (message-based):
# {sender} - Name/ID of message sender
# {connection_info} - Path info, SNR, and RSSI combined (e.g., "01,5f (2 hops) | SNR: 15 dB | RSSI: -120 dBm")
# {snr} - Signal-to-noise ratio in dB
# {rssi} - Received signal strength indicator in dBm
# {timestamp} - Message timestamp in HH:MM:SS format
# {path} - Message routing path (e.g., "01,5f (2 hops)")
# {hops} - Total hop count only (e.g., "2" or "0"); same value as in path/connection_info, for use without the path string
# {hops_label} - Same as hops with word "hop"/"hops" and pluralization (e.g., "1 hop", "2 hops", or "?" when unknown)
# {path_distance} - Total distance between all hops in path with locations (e.g., "123.4km (3 segs, 1 no-loc)")
# {firstlast_distance} - Distance between first and last repeater in path (e.g., "45.6km" or empty if locations missing)
# {elapsed} - Elapsed time (e.g. 1234ms) or "Sync Device Clock" when device clock is invalid (use {elapsed} only; do not append ms)
#
# To add newlines in responses, use \n (single backslash + n):
# Example: test = "Line 1\nLine 2\nLine 3"
# This will output:
# Line 1
# Line 2
# Line 3
#
# To use a literal backslash + n, use \\n (double backslash + n)
# Other escape sequences: \t (tab), \r (carriage return), \\ (literal backslash)
#
# Available placeholders (mesh network info - same as Scheduled_Messages):
# Total counts (ever heard):
# {total_contacts} - Total number of contacts ever heard
# {total_repeaters} - Total number of repeater devices ever heard
# {total_companions} - Total number of companion devices ever heard
# {total_roomservers} - Total number of roomserver devices ever heard
# {total_sensors} - Total number of sensor devices ever heard
#
# Recent activity:
# {recent_activity_24h} - Number of unique users active in last 24 hours
#
# Active in last 30 days (last_heard):
# {total_contacts_30d} - Total contacts active (last_heard) in last 30 days
# {total_repeaters_30d} - Total repeaters active (last_heard) in last 30 days
# {total_companions_30d} - Total companions active (last_heard) in last 30 days
# {total_roomservers_30d} - Total roomservers active (last_heard) in last 30 days
# {total_sensors_30d} - Total sensors active (last_heard) in last 30 days
#
# New devices (first heard in last 7 days):
# {new_companions_7d} - New companion devices first heard in last 7 days
# {new_repeaters_7d} - New repeater devices first heard in last 7 days
# {new_roomservers_7d} - New roomserver devices first heard in last 7 days
# {new_sensors_7d} - New sensor devices first heard in last 7 days
#
# Legacy placeholders (for backward compatibility):
# {repeaters} - Same as {total_repeaters}
# {companions} - Same as {total_companions}
test = "ack @[{sender}]{phrase_part} | {connection_info} | Received at: {timestamp}"
ping = "Pong!"
pong = "Ping!"
# Override 'help' command output
# help = "Bot Help: test (or t), ping, help, hello, cmd, advert, wx, aqi, sun, moon, solar, hfcond, satpass, prefix, path, sports, dice, roll, stats | More: 'help <command>'"
# Override 'cmd' command output
# cmd = "Available commands: test (or t), ping, help, hello, cmd, advert, wx, aqi, sun, moon, solar, hfcond, satpass, prefix, path, sports, dice, roll, stats"
[RandomLine]
# Configurable command to act on a trigger word and respond with a random line from its file
# triggers.<key> = csv list of trigger words
# file.<key> = path to text file
# prefix.<key> = string prepended to the chosen line (often an emoji)
# channel.<key> or channels.<key> = comma-separated channel names; if set, trigger only works in those channels (e.g. channel.momjoke = #jokes)
# default prefix (blank = no prefix)
prefix.default =
# Mom Jokes (only in #jokes)
triggers.momjoke = momjoke,momjokes,mom joke,mom jokes,mom-joke,mom-jokes
file.momjoke = data/randomlines/momjokes.txt
prefix.momjoke = 🥸
# channel.momjoke = #jokes
# Fun Facts
triggers.funfact = funfact,funfacts,fun fact,fun facts,fun-fact,fun-facts
file.funfact = data/randomlines/funfacts.txt
prefix.funfact = 💡
[Scheduled_Messages]
# Scheduled message format: HHMM = channel:message
# Time format: HHMM (24-hour, no colon)
# Bot will send these messages at the specified times daily
# Example: 0800 = general:Good morning! Weather update coming soon.
#
# Newlines: use \n in the message for a line break (e.g. general:Line one\nLine two).
# Literal backslash: use \\n for backslash+n; \\t for tab.
#
# Available placeholders for mesh network information:
#
# Total counts (ever heard):
# {total_contacts} - Total number of contacts ever heard
# {total_repeaters} - Total number of repeater devices ever heard
# {total_companions} - Total number of companion devices ever heard
# {total_roomservers} - Total number of roomserver devices ever heard
# {total_sensors} - Total number of sensor devices ever heard
#
# Recent activity:
# {recent_activity_24h} - Number of unique users active in last 24 hours
#
# Active in last 30 days (last_heard):
# {total_contacts_30d} - Total contacts active (last_heard) in last 30 days
# {total_repeaters_30d} - Total repeaters active (last_heard) in last 30 days
# {total_companions_30d} - Total companions active (last_heard) in last 30 days
# {total_roomservers_30d} - Total roomservers active (last_heard) in last 30 days
# {total_sensors_30d} - Total sensors active (last_heard) in last 30 days
#
# New devices (first heard in last 7 days):
# {new_companions_7d} - New companion devices first heard in last 7 days
# {new_repeaters_7d} - New repeater devices first heard in last 7 days
# {new_roomservers_7d} - New roomserver devices first heard in last 7 days
# {new_sensors_7d} - New sensor devices first heard in last 7 days
#
# Legacy placeholders (for backward compatibility):
# {repeaters} - Same as {total_repeaters}
# {companions} - Same as {total_companions}
#
# Example with placeholders:
# 0800 = Public:Good morning! Network: {total_contacts} total ({total_repeaters} repeaters, {total_companions} companions). {new_repeaters_7d} new repeaters, {new_companions_7d} new companions in last 7d. {recent_activity_24h} active in 24h.
# Example with 30-day active devices and new devices in 7d:
# 0900 = Public:{total_contacts_30d} active in last 30d: ({total_repeaters_30d} repeaters, {total_companions_30d} companions). {new_repeaters_7d} new repeaters, {new_companions_7d} new companions in last 7d. {recent_activity_24h} users active in last 24h.
#0800 = Public:Good morning! Bot is online and ready.
#1200 = Public:Midday status check - all systems operational.
#1800 = Public:Evening update - bot status: Good
[Schedule_Command]
# Show scheduled messages and advert interval via the 'schedule' command
# Responds with a compact list of scheduled times, channels, and message previews
# Default: enabled, DM-only (exposes bot configuration — restrict access)
enabled = true
# dm_only = true: only respond to direct messages (recommended)
# dm_only = false: allow in monitored channels
dm_only = true
# aliases = comma-separated list of additional trigger words for this command
# aliases = s, sched
[Logging]
# Log level: DEBUG, INFO, WARNING, ERROR, CRITICAL
# DEBUG: Most verbose, shows all details
# INFO: Standard logging level
# WARNING: Only warnings and errors
# ERROR: Only errors
# CRITICAL: Only critical errors
log_level = INFO
# Log file path (leave empty for console only)
# Bot will write logs to this file in addition to console
# Logs rotate at 5 MB with up to 3 backup files (e.g. meshcore_bot.log.1, .2, .3)
log_file = meshcore_bot.log
# Enable colored console output
# true: Use colors in console output
# false: Plain text output
colored_output = true
# MeshCore library log level (separate from bot log level)
# Controls debug output from the meshcore library itself
# Options: DEBUG, INFO, WARNING, ERROR, CRITICAL
meshcore_log_level = INFO
# Structured JSON logging mode for log aggregation pipelines (Loki, Elasticsearch, Splunk, etc.)
# When enabled, each log line is a JSON object:
# {"timestamp":"2026-03-14T12:00:00.000Z","level":"INFO","logger":"MeshCoreBot","message":"..."}
# colored_output is ignored when json_logging = true.
json_logging = false
# Log rotation settings (can also be changed at runtime via the web viewer Configuration tab)
# log_max_bytes: rotate when the log file exceeds this size (default 5 MB = 5242880)
log_max_bytes = 5242880
# log_backup_count: number of rotated backup files to keep (e.g. meshcore_bot.log.1 … .3)
log_backup_count = 3
[Custom_Syntax]
# Custom syntax patterns for special message formats
# Format: pattern = "response_format"
# Available fields: {sender}, {phrase}, {connection_info}, {snr}, {timestamp}, {path}, {path_distance}, {firstlast_distance}
# {phrase}: The text after the trigger (for custom syntax patterns)
# {path_distance}: Total distance between all hops in path with locations (e.g., "123.4km (3 segs, 1 no-loc)")
# {firstlast_distance}: Distance between first and last repeater in path (e.g., "45.6km" or empty if locations missing)
#
# Note: The "t" command is now handled by the test command as an alias
# "t phrase" works the same as "test phrase" - both use the test response format
# Example: "t hello world" -> "ack {sender}: hello world | {connection_info}"
[External_Data]
# URL shortener API base (v.gd / is.gd-compatible create.php). Default: https://v.gd
# See https://v.gd/apishorteningreference.php
short_url_website = https://v.gd
# Optional API key for alternate shortener hosts (unused for public v.gd/is.gd)
short_url_website_api_key =
# Weather API key (future feature)
weather_api_key =
# Weather update interval in seconds (future feature)
weather_update_interval = 3600
# Tide API key (future feature)
tide_api_key =
# Tide update interval in seconds (future feature)
tide_update_interval = 1800
# N2YO API key for satellite pass information
# Get free key at: https://www.n2yo.com/login/
n2yo_api_key =
# AirNow API key for AQI data
# Get free key at: https://docs.airnowapi.org/
airnow_api_key =
# Forecast.Solar API key for solar forecast data
# Get key at: https://forecast.solar/ (free tier works without key, paid tier for 3+ day forecasts)
# Free tier: 2-day forecast, 1-hour resolution
# Paid tier (14 EUR/year): 3-6 day forecast, 15-30 minute resolution
forecast_solar_api_key =
# Repeater prefix API URL for prefix command
# Leave empty to disable prefix command functionality
# Configure your own regional API endpoint
repeater_prefix_api_url =
# Repeater prefix cache duration in hours
# How long to cache prefix data before refreshing from API
# Recommended: 1-6 hours (data doesn't change frequently)
repeater_prefix_cache_hours = 1
[Prefix_Command]
# Enable or disable repeater geolocation in prefix command
# true: Show city names with repeaters when location data is available
# false: Show only repeater names without location information
show_repeater_locations = true
# Use reverse geocoding for coordinates without city names
# true: Automatically look up city names from GPS coordinates
# false: Only show coordinates if no city name is available
use_reverse_geocoding = true
# Hide prefix source information
# true: Hide "Source: domain.com" line from prefix command output
# false: Show source information (default)
hide_source = false
# Prefix heard time window (days)
# Number of days to look back when showing prefix results (default command behavior)
# Only repeaters heard within this window will be shown by default
# Use "prefix XX all" to show all repeaters regardless of time
prefix_heard_days = 7
# Prefix free time window (days)
# Number of days to look back when determining which prefixes are "free"
# Only repeaters heard within this window will be considered as using a prefix
# Repeaters not heard in this window will be excluded from used prefixes list
prefix_free_days = 7
# Maximum range for prefix filtering (kilometers)
# Repeaters beyond this distance from bot location will be excluded from prefix lookups
# This prevents prefix collisions from far-away repeaters from affecting local prefix availability
# Set to 0 to disable range limiting
max_prefix_range = 200
# Prefix best location feature
# Enable "prefix best <location>" to suggest best prefix (true/false)
prefix_best_enabled = true
# Minimum edge observations for neighbor detection
# Edges in the mesh graph must have at least this many observations to be considered
# Higher values = more conservative (requires more evidence of neighbor relationship)
# Default: 2
prefix_best_min_edge_observations = 2
# Maximum edge age for neighbor detection (days)
# Edges not observed within this many days are ignored when finding neighbors
# Helps filter out stale connections from repeaters that may have moved or gone offline
# Default: 30
prefix_best_max_edge_age_days = 30
# Location search radius (kilometers)
# When finding repeaters at a location, search within this radius
# Larger radius = more repeaters considered, but may include distant repeaters
# Default: 50
prefix_best_location_radius_km = 50
# Prefixes to never suggest (comma-separated)
# List of prefixes that should never be suggested by the "prefix best" command
# Useful for excluding major infrastructure repeaters or reserved prefixes
# Example: prefix_best_do_not_suggest = 00,FF,01,02
# Leave empty to allow all prefixes
prefix_best_do_not_suggest =
[Weather]
# NOTE: Unit settings are used by both the wx and gwx commands and Weather_Service plugin
# Weather provider selection
# Options: noaa, openmeteo
# noaa: Use NOAA API (US-focused, better for US locations, includes weather alerts)
# openmeteo: Use Open-Meteo API (global coverage, works worldwide)
# Default: noaa
weather_provider = noaa
# Open-Meteo model selection (used by gwx and Weather_Service when provider is openmeteo)
# Leave unset to default to "best_match"
# Supported model names: https://open-meteo.com/en/docs
# Set to an empty value (weather_model =) to omit model selection and let Open-Meteo auto-select
#weather_model =
# Default state for city name disambiguation
# When users type "wx seattle", it will search for "seattle, WA, USA"
# Use 2-letter state abbreviation (e.g., WA, CA, NY, TX)
default_state = WA
# Default country for city name disambiguation (for international weather plugin)
# Use 2-letter country code (e.g., US, CA, GB, AU)
default_country = US
# When true, a bare "wx" or "gwx" (no location) uses bot_latitude/bot_longitude from [Bot]
# if there is no default WXSIM source and no companion location in the database.
# Default: false (show usage instead)
use_bot_location_when_no_location = false
# Temperature unit for weather display
# Options: fahrenheit, celsius
# Default: fahrenheit
temperature_unit = fahrenheit
# Wind speed unit for weather display
# Options: mph, kmh, ms (meters per second)
# Default: mph
wind_speed_unit = mph
# Precipitation unit for weather display
# Options: inch, mm
# Default: inch
precipitation_unit = inch
# How to show daily high/low temperatures (wx, gwx, Weather_Service).
# Placeholders: {high} {low} {units} ({units} is °F or °C)
temperature_high_low_format = H:{high}{units} L:{low}{units}
temperature_high_only_format = H:{high}{units}
temperature_low_only_format = L:{low}{units}
# Examples:
# temperature_high_low_format = ↓{low}°↑{high}{units}
# temperature_high_low_format = H:{high}{units} L:{low}{units}
# temperature_high_low_format = {high}{units}/{low}{units}
# Custom weather sources (extensible pattern, similar to Channels_List)
# Format: custom.<provider>.<name> = <source_url>
# custom.wxsim.default = <url> - Default source used when 'wx' is called without a location
# custom.wxsim.<name> = <url> - Named source accessible via 'wx <name>'
#
# Example:
# custom.wxsim.default = https://westlethbridgeweather.com/latest.txt
# custom.wxsim.lethbridge = https://westlethbridgeweather.com/latest.txt
# custom.wxsim.seattle = https://seattleweather.example.com/plaintext.txt
#
# Note: Only one 'default' source can be set per provider type.
# Future providers (e.g., custom.pwaweather.location) will follow the same pattern.
[Solar_Config]
# URL timeout for external API calls (seconds)
url_timeout = 10
# Use Zulu/UTC time for astronomical data
# true: Use 24-hour UTC format
# false: Use 12-hour local format
use_zulu_time = false
[Aurora_Command]
# Enable or disable the aurora command
enabled = true
# Optional: default coordinates when user does not specify a location
# default_lat = 48.08
# default_lon = -121.97
[Channels_List]
# Common hashtag channels for the region
# Format: channel_name = description
# These channels will be listed when users use the 'channels' command
# The bot will automatically add the '#' prefix when displaying channels
# General channels (no category prefix)
weather = Weather updates and conditions
emergency = Emergency communications and alerts
# Sub-command channels (format: subcommand.channel_name = description)
# Example: channels sports -> sports.sounders = Seattle Sounders FC
# Sports-focused channels
sports.sounders = Seattle Sounders FC
sports.kraken = Seattle Kraken
sports.mariners = Seattle Mariners
sports.seahawks = Seattle Seahawks
sports.reign = OL Reign
sports.storm = Seattle Storm
sports.huskies = Washington Huskies
# Local area channels
local.capitolhill = Capitol Hill neighborhood
local.ballard = Ballard neighborhood
local.fremont = Fremont neighborhood
local.queenanne = Queen Anne neighborhood
# Technology channels
tech.mesh = Mesh networking and technical discussions
tech.hamradio = Amateur radio and ham radio topics
tech.programming = Programming and development
tech.iot = Internet of Things projects
# Emergency-focused channels
emergency.emergency = Emergency communications and alerts
emergency.weather = Weather updates and conditions
emergency.traffic = Traffic updates and road conditions
emergency.hamradio = Amateur radio emergency net
[Sports_Command]
# Enable or disable the sports command (true/false)
enabled = true
# Default teams to show when 'sports' command is used without arguments
# Comma-separated list of team names (use lowercase)
teams = seahawks,mariners,sounders,kraken
# Channels where sports command is allowed
# IMPORTANT: Leave commented out (or omit entirely) to use global monitor_channels (default behavior)
# If uncommented with empty value (channels = ), command will be DM-only
# Comma-separated list to restrict to specific channels (only sports command works there)
# Example: channels = #sounders,#seahawks (only sports command works in these channels)
# channels = general,#bot,#sounders,#seahawks
# Channel overrides for sports command
# Format: channel_name = default_team
# Allows sports command to work in specific channels with default team shortcuts
# Example: #sounders = sounders (sports in #sounders becomes "sports sounders")
channel_override = #sounders=sounders,#seahawks=seahawks,#kraken=kraken,#mariners=mariners
# ESPN API timeout in seconds
api_timeout = 10
[Stats_Command]
# Enable or disable the stats command (true/false)
enabled = true
# Data retention settings
# Number of days to keep stats data (older data will be automatically cleaned up)
# Recommended: 7-30 days to balance storage usage with historical data
data_retention_days = 7
# Enable automatic cleanup of old stats data
# true: Automatically clean up old data based on data_retention_days
# false: Manual cleanup only
auto_cleanup = true
# Stats collection settings
# Track all incoming messages (not just commands)
# true: Record all messages for comprehensive stats
# false: Only record command executions
track_all_messages = true
# Track command execution details
# true: Record detailed command execution info
# false: Basic command tracking only
track_command_details = true
# Privacy settings
# Anonymize user data in stats
# true: Replace user IDs with anonymous identifiers
# false: Keep actual user IDs in stats
anonymize_users = false
[Data_Retention]
# Data retention controls how long the bot keeps data in the database.
# The scheduler runs cleanup daily so retention is enforced even when the
# standalone web viewer is not running. Shorter retention reduces DB size.
#
# Packet stream (web viewer real-time display and transmission_tracker)
# 2-3 days is enough for most deployments; 7 days if you need longer history.
packet_stream_retention_days = 3
#
# Repeater/stats tables: daily_stats, unique_advert_packets, observed_paths
daily_stats_retention_days = 90
observed_paths_retention_days = 90
#
# Purging log (audit trail for repeater purges)
purging_log_retention_days = 90
#
# Mesh connections (path graph edges). Should be >= Path_Command graph_edge_expiration_days.
mesh_connections_retention_days = 7
[Path_Command]
# Enable or disable the path command
enabled = true
# Enable "p" shortcut for path command (similar to "t" for test command)
# true: Respond to just "p" or "p <path_data>" as a shortcut for "path" (default)
# false: Only respond to "path", "decode", or "route" keywords
enable_p_shortcut = true
# Path Selection Preset
# Choose a preset that configures multiple related settings:
# - balanced: Balanced approach using both graph evidence and geographic proximity (default)
# - geographic: Prioritize geographic proximity over graph evidence (better for local networks)
# - graph: Prioritize graph evidence over geographic proximity (better for well-connected networks)
# Individual settings below can override preset values
# See docs/path-command-config.md for detailed documentation
path_selection_preset = balanced
# Basic Settings
# Geographic proximity calculation method
# simple: Use proximity to bot location
# path: Use proximity to previous/next nodes in the path for more realistic routing (default)
proximity_method = path
# Enable path proximity fallback
# When path proximity can't be calculated (missing location data), fall back to simple proximity
path_proximity_fallback = true
# Maximum range for geographic proximity guessing (kilometers, 0 = disabled)
# Typical LoRa transmission: < 30km, Long LoRa transmission: up to 200km
# Repeaters beyond this distance will have reduced confidence or be rejected
max_proximity_range = 200
# Maximum age for repeater data in path matching (days, 0 = disabled)
# Only include repeaters that have been heard within this many days
max_repeater_age_days = 14
# Recency vs Proximity weighting (0.0 to 1.0)
# 0.0 = 100% proximity (only distance matters)
# 1.0 = 100% recency (only when last heard matters)
# 0.4 = 40% recency, 60% proximity (balanced)
recency_weight = 0.4
# Recency decay half-life in hours (for longer advert intervals)
# Default: 24 hours. For 48-72 hour advert intervals, use 36-48 hours.
recency_decay_half_life_hours = 24
# Enable graph-based path validation
# When enabled, uses observed mesh connections to improve path guessing accuracy
# The graph learns from message paths to validate repeater selections
graph_based_validation = true
# Minimum edge observations required for graph confidence
# Edges with fewer observations are not considered for path validation
# Higher values = more conservative (requires more evidence)
min_edge_observations = 3
# Graph edge expiration (days without observation)
# Edges not observed for this many days will be ignored
# Helps filter out stale routing information
graph_edge_expiration_days = 7
# Graph persistence write strategy: 'immediate', 'batched', or 'hybrid'
# - immediate: Write each edge update to database immediately (safer for frequent restarts, higher I/O)
# - batched: Accumulate updates, flush periodically (better performance, risk of data loss on crash)
# - hybrid: Immediate for new edges, batched for observation count increments (balanced)
# Recommended: 'hybrid' for development (frequent restarts), 'batched' for production
graph_write_strategy = hybrid
# Batch write interval in seconds (only used if strategy is 'batched' or 'hybrid')
# How often to flush pending edge updates to database
# Lower values = more frequent writes (safer but more I/O)
# For frequent restarts during development, use 5-10 seconds
graph_batch_interval_seconds = 30
# Maximum pending updates before forcing a flush (only used if strategy is 'batched' or 'hybrid')
# If this many updates are pending, flush immediately even if interval hasn't elapsed
# Prevents unbounded memory growth during high message volume
graph_batch_max_pending = 100
# Enhanced graph features for improved path accuracy
# Enable bidirectional edge bonus (check if reverse edges exist for higher confidence)
graph_use_bidirectional = true
# Enable hop position validation (validate candidate appears in expected position based on avg_hop_position)
graph_use_hop_position = true
# Enable multi-hop path inference (find intermediate nodes when direct edges don't exist)
graph_multi_hop_enabled = true
# Maximum hops for multi-hop path inference (default: 2, can be increased to 3 for fallback)
graph_multi_hop_max_hops = 2
# Combine graph and geographic scores into weighted average (default: false, uses graph-first fallback)
# When enabled, combines graph and geographic scores instead of choosing one or the other
graph_geographic_combined = false
# Weight for graph score when combining with geographic (0.0-1.0, default: 0.7)
# Higher values give more weight to graph evidence, lower values favor geographic proximity
graph_geographic_weight = 0.7
# Minimum graph confidence threshold to override geographic selection (0.0-1.0, default: 0.7)
# When graph confidence >= this value, graph selection overrides geographic even if geographic prefers closer repeaters
# Lower values (e.g., 0.5) = geographic gets more consideration, higher values (e.g., 0.9) = graph gets more priority
# Set to 1.0 to always prefer geographic when available, or 0.0 to always prefer graph
graph_confidence_override_threshold = 0.7
# Enable distance penalties for intermediate hops in graph selection (default: true)
# When enabled, graph scores are penalized for candidates that create long-distance hops (>50km)
# This prevents graph from selecting very distant repeaters even if they have strong graph evidence
graph_distance_penalty_enabled = true
# Maximum reasonable hop distance in km before applying penalty (default: 50)
# Hops longer than this distance will have their graph score penalized
# Lower values = more aggressive penalty for long hops
graph_max_reasonable_hop_distance_km = 50
# Distance penalty strength (0.0-1.0, default: 0.3)
# How much to penalize graph scores for long-distance hops
# 0.3 = 30% penalty for hops beyond max_reasonable_hop_distance
# Higher values = stronger penalty, lower values = weaker penalty
graph_distance_penalty_strength = 0.3
# Zero-hop advert bonus for graph selection (0.0-1.0, default: 0.4)
# Repeaters that have been heard directly by the bot (zero-hop adverts) get this bonus
# This is strong evidence the repeater is close, even for intermediate hops
# Higher values = stronger preference for repeaters heard directly
graph_zero_hop_bonus = 0.4
# Prefer candidates that match stored public keys in graph edges (default: true)
# When enabled, candidates whose public key matches the stored public key in an edge
# get a significant confidence boost, as stored keys indicate high confidence in the edge
# This aligns path command selection with what the graph visualization shows
graph_prefer_stored_keys = true
# Enable bot location proximity consideration for final hop in graph selection (default: true)
# When enabled, for the final hop (last repeater before bot), candidates closer to bot location
# get a proximity bonus added to their graph score
graph_final_hop_proximity_enabled = true
# Weight for proximity score when combining with graph score for final hop (0.0-1.0, default: 0.25)
# Higher values give more weight to proximity, lower values favor graph evidence
# 0.25 means 25% proximity, 75% graph score
graph_final_hop_proximity_weight = 0.25
# Maximum distance in km for final hop proximity consideration (default: 0 = no limit)
# Repeaters beyond this distance from bot will not receive proximity bonus
# Set to 0 to disable distance limiting
graph_final_hop_max_distance = 0
# Distance normalization for final hop proximity scoring (km, default: 200)
# Closer repeaters get higher proximity scores. Lower values = more aggressive scoring
# 200km (long LoRa range) means repeaters within 200km get full scoring range, beyond that scores decrease
graph_final_hop_proximity_normalization_km = 200
# Very close distance threshold for boosted proximity weight (km, default: 10)
# Repeaters within this distance get 2x proximity weight (up to 0.6 max)
graph_final_hop_very_close_threshold_km = 10
# Close distance threshold for boosted proximity weight (km, default: 30)
# Repeaters within this distance get 1.5x proximity weight (up to 0.5 max). Typical LoRa range.
graph_final_hop_close_threshold_km = 30
# Maximum proximity weight for very close repeaters (0.0-1.0, default: 0.6)
# When a repeater is within very_close_threshold_km, proximity weight is boosted up to this value
graph_final_hop_max_proximity_weight = 0.6
# Path validation bonus settings
# Maximum bonus for path validation matches (0.0-1.0, default: 0.3)
# Higher values give more weight to stored path matches when resolving prefix collisions
graph_path_validation_max_bonus = 0.3
# Observation count divisor for path validation bonus (default: 50.0)
# Lower values = stronger bonus from observation count. 50.0 means 50 observations = 0.15 bonus
graph_path_validation_obs_divisor = 50.0
# Load only recent edges on startup (days, 0 = load all historical edges)
# Edges older than this are skipped at startup to bound initial memory usage.
# The in-code default is 14 days when this setting is absent from config.ini.
# Recommended values:
# 0 - Load all historical edges (servers with ample RAM, e.g. x86 VM)
# 14 - Good balance of coverage vs. memory (default for unconfigured installs)
# 7 - Reduced memory footprint for Raspberry Pi Zero 2 W
# Note: edges older than graph_edge_expiration_days are never loaded regardless of this value.
graph_startup_load_days = 0
# Enable graph data capture from incoming packets (default: true)
# When true, the bot observes routing paths from advertisements, messages, and trace
# packets and stores edges in the mesh graph.
# When false, NO new edge data is collected and the background batch writer thread is
# not started — reducing both CPU and RAM overhead. Any edges already in the database
# are still available for graph_based_validation if that is also enabled.
# Set to false on devices that don't use the path command and want minimal overhead.
graph_capture_enabled = true
# Star bias multiplier for path command
# When a contact is starred in the web viewer, multiply its selection score by this value
# Higher values = stronger preference for starred repeaters
# Default: 2.5 (starred repeaters get 2.5x their normal score)
# Set to 1.0 to disable star bias
star_bias_multiplier = 2.5
[Joke_Command]
# Enable or disable the joke command (true/false)
enabled = true
# Enable seasonal joke defaults (October: spooky, December: Christmas)
# true: Seasonal defaults are applied (default)
# false: No seasonal defaults (always random)
seasonal_jokes = true
# Handle long jokes (over 130 characters)
# false: Fetch new jokes until we get a short one (default)
# true: Split long jokes into multiple messages
long_jokes = false
# Channels where joke command is allowed (omit to use global monitor_channels)
# channels = #bot,#jokes
[DadJoke_Command]
# Enable or disable the dad joke command (true/false)
enabled = true
# Handle long jokes (over 130 characters)
# false: Fetch new jokes until we get a short one (default)
# true: Split long jokes into multiple messages
long_jokes = false
# Channels where dadjoke command is allowed (omit to use global monitor_channels)
# channels = #bot,#jokes
[Hacker_Command]
# Enable or disable the hacker command (true/false; responds to Linux commands with supervillain mainframe errors)
enabled = false
[Multitest_Command]
# Response format for multitest command results
# Available fields: {sender}, {path_count}, {paths}, {listening_duration}
# {sender}: Name/ID of message sender
# {path_count}: Number of unique paths found
# {paths}: Newline-separated list of paths
# {listening_duration}: Listening window duration in seconds
# Leave empty to use default format
# Example: "Found {path_count} unique path(s) for @[{sender}]:\n{paths}"
response_format = @[{sender}] found {path_count} unique path(s):\n{paths}
# When true, {paths} uses shared-prefix lines ending with ┐ U+2510 when branching, then ├ U+251C / └ U+2514 (nested rows use   U+3000 before ├/└)
condense_paths = false
[Greeter_Command]
# Enable greeter to greet users on first channel message (true/false)
enabled = false
# Channels where greetings should occur (comma-separated)
# IMPORTANT: Leave commented out (or omit entirely) to use global monitor_channels (default behavior)
# If uncommented with empty value (channels = ), command will be DM-only
# Comma-separated list to restrict to specific channels (only greeter command works there)
# Example: channels = general,welcome,newbies
# If not specified, uses the channels from [Channels] monitor_channels setting
# channels =
# Greeting message template (default for all channels)
# Available fields: {sender} - the user's name/ID
# For multi-part greetings, separate messages with pipe (|)
# Example (single): "Welcome to the mesh, @[{sender}]!"
# Example (multi-part): "Welcome to the mesh, @[{sender}]!|This is a great place to chat.|Use !help for commands."
greeting_message = Welcome to the mesh, @[{sender}]!
# Channel-specific greeting messages (optional)
# Format: channel_name:greeting_message,channel_name2:greeting_message2
# If a channel has a specific greeting, it will be used instead of the default greeting_message
# Example: Public:Welcome to Public channel, @[{sender}]!|general:Welcome to general, @[{sender}]!
# Multi-part greetings are supported per channel using pipe (|) separator
# Leave empty to use greeting_message for all channels
channel_greetings =
# Per-channel greetings (tracking behavior)
# false: Greet each user only once globally (default - user gets one greeting total)
# true: Greet each user once per channel (user can be greeted on each channel separately)
# Note: This controls tracking, not the greeting message itself. Use channel_greetings for different messages.
per_channel_greetings = false
# Include mesh network information in greeting
# true: Add mesh statistics to greeting (total contacts, repeaters, etc.)
# false: Only send the greeting message
include_mesh_info = true
# Mesh info format template
# Available fields: {total_contacts}, {repeaters}, {companions}, {recent_activity_24h}
# Example: "\n\nMesh Info: {total_contacts} contacts, {repeaters} repeaters"
# Note: Mesh info is appended to the last greeting message part
mesh_info_format = \n\nMesh Info: {total_contacts} contacts, {repeaters} repeaters, {recent_activity_24h} active in last 24h
# Rollout period in days
# When greeter is first enabled on an active mesh, this sets how many days
# to listen and mark all active users as already greeted before beginning
# to greet new users. This prevents greeting everyone on an established mesh.
# Set to 0 to disable rollout (will greet all new users immediately)
# Note: Use auto_backfill to mark historical users and shorten/eliminate rollout period
rollout_days = 7
# Auto-backfill from historical message_stats data
# true: Automatically mark all users who have posted on public channels in the past
# false: Only mark users during rollout period (default)
# This allows shortening or eliminating the rollout period by using existing data
auto_backfill = false
# Backfill lookback period in days
# Number of days to look back when auto-backfilling (0 = all time)
# Only used if auto_backfill = true
# Example: 30 = only mark users who posted in last 30 days
# Example: 0 = mark all users who have ever posted (all time)
backfill_lookback_days = 30
[Alert_Command]
# Enable or disable the alert command (true/false)
enabled = false
# PulsePoint agency IDs by county/region
# Format: agency.<county_name> = comma-separated agency IDs
# Get agency IDs from: https://web.pulsepoint.org/ or PulsePoint searchagencies API
# You can use any naming convention that makes sense for your region
# Examples: agency.county1, agency.city1, agency.region1, etc.
# Example: County agencies
# agency.county1 =
# Example: City agencies (can have multiple entries for the same region)
# agency.city1 =
# agency.city1_alias =
# Example: Combined region (all counties/agencies)
# agency.region_all =
[Announcements_Command]
# Enable or disable the announcements command (true/false)
enabled = false
# Announcements Access Control List (ACL)
# Only users with public keys listed here can send announcements
# This ACL automatically inherits all members from the Admin_ACL
# Format: comma-separated list of 64-character hex public keys (without spaces)
# Example: f5d2b56d19b24412756933e917d4632e088cdd5daeadc9002feca73bf5d2b56d
# Leave empty to only use Admin_ACL members
announcements_acl =
# Default channel for announcements when no channel is specified
# Announcements will be sent to this channel if no channel is provided
default_announcement_channel = Public
# Announcement cooldown in minutes
# Prevents the same announcement from being sent too frequently
# Default: 60 minutes
announcement_cooldown = 60
# Announcement triggers
# Format: announce.<trigger_name> = <announcement_text>
# Users can send: announce <trigger_name> [channel]
# If channel is not specified, uses default_announcement_channel
# Example triggers:
announce.default = This is the default announcement.
announce.bots = This is an announcement with the list of bots.
announce.other = This is a different announcement on another topic.
[Airplanes_Command]
# Enable or disable the airplanes command (true/false)
enabled = true
# API endpoint URL for ADS-B aircraft data
# Default: airplanes.live API
# Supports any standardized ADS-B API using readsb/airplanes.live format
# Examples:
# http://api.airplanes.live/v2/ (default)
# https://adsbexchange-com1.p.rapidapi.com/v2/ (if compatible)
# http://localhost:8080/data/ (local readsb instance)
api_url = http://api.airplanes.live/v2/
# Default search radius in nautical miles
# Maximum: 250 nautical miles
default_radius = 25
# Maximum number of aircraft to return in results
# Maximum: 50
max_results = 10
# API request timeout in seconds
url_timeout = 10
# Command stubs (enable/channels only; omit to use global monitor_channels)
[Wx_Command]
enabled = true
# channels =
# aliases = comma-separated additional trigger words, e.g.: aliases = weather, w
[WebViewer_Command]
enabled = true
# channels =
[Sun_Command]
enabled = true
# channels =
[Solar_Command]
enabled = true
# channels =
[Solarforecast_Command]
enabled = true
# channels =
[Satpass_Command]
enabled = true
# channels =
[Roll_Command]
enabled = true
# channels =
[Repeater_Command]
enabled = true
# channels =
[Ping_Command]
enabled = true
# channels =
[Moon_Command]
enabled = true
# channels =
[Magic8_Command]
enabled = true
# channels =
[Hfcond_Command]
enabled = true
# channels =
[Help_Command]
enabled = true
# channels =
[Hello_Command]
enabled = true
# channels =
[Dice_Command]
enabled = true
# channels =
[Cmd_Command]
enabled = true
# channels =
[Channels_Command]
enabled = true
# channels =
[Catfact_Command]
enabled = true
# channels =
[Aqi_Command]
enabled = true
# channels =
[Advert_Command]
enabled = true
# channels =
[Test_Command]
enabled = true
# channels =
[Trace_Command]
# Enable or disable the trace/tracer commands (link diagnostics)
enabled = true
# Maximum path length (hops) for manual or reciprocal path; paths longer than this are capped
maximum_hops = 5
# Trace mode: one_byte (default) or two_byte (when firmware supports it)
trace_mode = one_byte
# Timeout: total = timeout_base_seconds + (path hops * timeout_per_hop_seconds); typical 6 hops ~1s, 10 ~2s
timeout_base_seconds = 1.0
timeout_per_hop_seconds = 0.5
# Retries: max attempts (default 2 = try once, then once more after delay)
trace_retry_count = 2
# Seconds to wait before retrying a failed trace
trace_retry_delay_seconds = 1.0
# Update mesh graph with bidirectional link when 1-byte trace succeeds
update_graph_one_byte = true
# Update mesh graph with 2-byte link identification when 2-byte trace succeeds (when supported)
update_graph_two_byte = true
# Optional: single emoji or string for bot in trace output; if unset, uses [Bot]
# bot_label = 🤖
# Output format: inline (default) or vertical
# output_format = inline
# channels =
[Feed_Command]
enabled = false
# channels =
####################################################################################################
# #
# Web Viewer Configuration #
# #
# Settings for web administration backend #
# #
####################################################################################################
[Web_Viewer]
# Enable or disable the web data viewer
# Enable web viewer (true/false). false recommended unless you have configured web_viewer_password.
enabled = false
# Password to protect the web viewer UI (BUG-001 fix)
# If set, a login page is shown and all routes + SocketIO connections require authentication.
# If left empty, the web viewer is accessible without any authentication.
# Recommendation: always set a password when host != 127.0.0.1
# web_viewer_password = changeme
# Web viewer host address
# SECURITY WARNING:
# - 127.0.0.1: Only accessible from localhost (SECURE - recommended)
# - 0.0.0.0: Accessible from any network interface (use only with web_viewer_password set)
#
# Using 0.0.0.0 without web_viewer_password will expose:
# - All messages and their contents
# - Contact list and repeater information
# - Real-time packet stream
# - Bot configuration details
host = 127.0.0.1
# Web viewer port
# Must be between 1024-65535 (non-privileged ports)
# Default: 8080
port = 8080
# Enable debug mode for the web viewer
# true: Enable Flask debug mode (auto-reload on changes)
# false: Production mode (recommended)
debug = false
# Auto-start web viewer with bot
# true: Start web viewer automatically when bot starts
# false: Start web viewer manually (recommended)
auto_start = false
# Optional: database path for web viewer. If unset, the viewer uses [Bot] db_path (recommended).
# Only set this if you use a separate database for the viewer; you will see a startup warning.
# See docs/web-viewer.md for migrating from a separate database.
# db_path = meshcore_bot.db
# Additional hashtag channels to decode in the packet stream
# The web viewer can decrypt GroupText messages from hashtag channels
# without adding them to the radio. Enter channel names (with or without #)
# as a comma-separated list. The Public channel is always included.
#
# Example: #CQ,#ARES,#EmComm
# This will allow decryption of messages on #CQ, #ARES, and #EmComm channels
# in addition to any channels configured on your radio.
#
# Note: Only hashtag channels work here - custom channels with private keys
# must be added to the radio itself.
decode_hashtag_channels =
####################################################################################################
# #
# Service Plugins Config #
# #
# Settings for non-interactive bot services and plugins (weather, alerts, etc.) #
# #
####################################################################################################
[PacketCapture]
# Enable packet capture service (true/false)
enabled = false
# Output file for packet data (optional)
# Leave empty to disable file output
# Packets will be written as JSON lines
output_file =
# Verbose output (show JSON packet data in logs)
# true: Show packet data in logs
# false: Minimal logging
verbose = false
# Debug output (show detailed debugging info)
# true: Show all debugging information
# false: Standard logging
debug = false
# Owner information (for packet analyzer registration)
# Owner public key (64-character hex string)
owner_public_key =
# Owner email address
owner_email =
# Private key file path for auth token generation (fallback if device signing unavailable)
# Optional - on-device signing is preferred
# Supports 64-byte orlp format (128 hex chars) or 32-byte seed (64 hex chars)
# Required only if device doesn't support on-device signing or auth_token_method = python
# Note: If not provided and auth_token_method = python, the service will attempt to fetch
# the private key from the device automatically
private_key_path =
# Auth token signing method
# device: Try on-device signing first, fallback to Python signing (default, recommended)
# python: Use Python signing only (requires private_key_path or device key export capability)
auth_token_method = device
# Location Code (IATA code for your location)
# Used in topic templates and auth tokens
# Example: SEA, LAX, JFK, etc.
# Default: XYZ (invalid if not set)
iata = XYZ
# MQTT Broker Configuration
# You can configure multiple MQTT brokers by using mqtt1_*, mqtt2_*, mqtt3_*, etc.
# Each broker can have independent settings for transport, TLS, authentication, and topics.
#
# Broker-specific options (replace N with broker number: 1, 2, 3, etc.):
# mqttN_enabled = true/false # Enable/disable this broker
# mqttN_server = hostname # MQTT broker hostname or IP
# mqttN_port = 1883 # MQTT broker port (1883 for TCP, 443 for WSS)
# mqttN_transport = tcp/websockets # Transport type (tcp or websockets)
# mqttN_use_tls = true/false # Enable TLS/SSL (required for WSS)
# mqttN_websocket_path = /mqtt # WebSocket path (for websockets transport)
# mqttN_username = # MQTT username (optional, auto-generated for auth tokens)
# mqttN_password = # MQTT password (optional, auto-generated for auth tokens)
# mqttN_use_auth_token = true/false # Use JWT auth token instead of username/password
# mqttN_token_audience = # JWT audience (usually the broker hostname)
# mqttN_topic_status = # Status topic template (uses placeholders below)
# mqttN_topic_packets = # Packets topic template (uses placeholders below)
# mqttN_topic_prefix = # Legacy topic prefix (fallback if topic_status/topic_packets not set)
# mqttN_client_id = # MQTT client ID (optional, auto-generated from bot name)
# mqttN_upload_packet_types = # Comma-separated packet types to upload (e.g. 2,4); empty = all
#
# Topic template placeholders:
# {IATA} - Uppercase IATA code (e.g., SEA)
# {iata} - Lowercase IATA code (e.g., sea)
# {PUBLIC_KEY} - Uppercase device public key (64 hex chars)
# {public_key} - Lowercase device public key (64 hex chars)
# MQTT Broker 1 - Let's Mesh Analyzer (US)
mqtt1_enabled = true
mqtt1_server = mqtt-us-v1.letsmesh.net
mqtt1_port = 443
mqtt1_transport = websockets
mqtt1_use_tls = true
mqtt1_use_auth_token = true
mqtt1_token_audience = mqtt-us-v1.letsmesh.net
mqtt1_topic_status = meshcore/{IATA}/{PUBLIC_KEY}/status
mqtt1_topic_packets = meshcore/{IATA}/{PUBLIC_KEY}/packets
mqtt1_websocket_path = /mqtt
mqtt1_client_id =
mqtt1_upload_packet_types =
# MQTT Broker 2 - Let's Mesh Analyzer (EU)
mqtt2_enabled = true
mqtt2_server = mqtt-eu-v1.letsmesh.net
mqtt2_port = 443
mqtt2_transport = websockets
mqtt2_use_tls = true
mqtt2_use_auth_token = true
mqtt2_token_audience = mqtt-eu-v1.letsmesh.net
mqtt2_topic_status = meshcore/{IATA}/{PUBLIC_KEY}/status
mqtt2_topic_packets = meshcore/{IATA}/{PUBLIC_KEY}/packets
mqtt2_websocket_path = /mqtt
mqtt2_client_id =
mqtt2_upload_packet_types =
# Stats and status publishing
# Enable stats in status messages
stats_in_status_enabled = true
# Stats refresh interval (seconds)
stats_refresh_interval = 300
# JWT renewal interval (seconds, 0 = disabled)
jwt_renewal_interval = 86400
# Health check interval (seconds, 0 = disabled)
health_check_interval = 30
# Health check grace period (consecutive failures before warning)
health_check_grace_period = 2
[MapUploader]
# Enable map uploader (true/false). Uploads node adverts (repeaters, room servers, sensors) to map.meshcore.dev
# CHAT adverts skipped; adverts without GPS or with 0.0 coords skipped. Uses device radio parameters
enabled = false
# API endpoint URL
# The map.meshcore.dev API endpoint for uploading node adverts
# Default: https://map.meshcore.dev/api/v1/uploader/node
api_url = https://map.meshcore.dev/api/v1/uploader/node
# Private key file path (optional)
# Path to file containing device private key for signing uploads
# If not provided, the service will attempt to fetch the private key from the device
# Supports 64-byte orlp format (128 hex chars) or 32-byte seed (64 hex chars)
# Required only if device doesn't support private key export
private_key_path =
# Minimum time between re-uploads of same node (seconds)
# Prevents uploading the same node too frequently to avoid API spam
# Only nodes with timestamps newer than last upload + this interval will be uploaded
# Default: 3600 (1 hour)
min_reupload_interval = 3600
# Verbose logging
# Enable detailed debug logging including upload data and signature details
# true: Show detailed debug information (useful for troubleshooting)
# false: Standard logging (default)
verbose = false
[Weather_Service]
# Enable weather service for scheduled forecasts and alert monitoring (true/false)
enabled = false
# Daily weather forecast time
# Format: HH:MM (24-hour format, e.g., "6:00" for 6 AM)
# Or use "sunrise" or "sunset" for dynamic times based on your location
# Bot will send daily weather forecast at this time
weather_alarm = 6:00
# NOTE: Temperature, wind speed, and precipitation units are inherited from [Weather] section
# See [Weather] section below for temperature_unit, wind_speed_unit, and precipitation_unit settings
# Bot position for weather forecasts and alerts
# Latitude in decimal degrees
my_position_lat =
# Longitude in decimal degrees
my_position_lon =
# Channel for daily weather forecasts
# Weather forecasts will be sent to this channel
weather_channel = #weather
# Channel for weather alerts
# Weather alerts will be sent to this channel
alerts_channel = #weather
# Weather alert polling interval (milliseconds)
# How often to check for new weather alerts
# Default: 600000 (10 minutes)
poll_weather_alerts_interval = 600000
# Thunder/storm data collection interval (milliseconds)
# How often to aggregate thunder data for evaluation
# Default: 600000 (10 minutes)
# Note: Storm detection area must be configured for this to work
blitz_collection_interval = 600000
# Storm detection area (optional)
# Only report storms detected within this area
# Leave empty to disable area filtering
# Format: decimal degrees
# blitz_area_min_lat = 47.51
# blitz_area_min_lon = 15.54
# blitz_area_max_lat = 48.76
# blitz_area_max_lon = 18.62
[Earthquake_Service]
# Enable earthquake alert service (true/false)
# Polls USGS Earthquake API and posts alerts to a channel when quakes occur in the configured region
enabled = false
# Channel to post earthquake alerts to
channel = #general
# Poll interval (milliseconds). How often to check USGS for new earthquakes
# Default: 60000 (1 minute)
poll_interval = 60000
# Time window (minutes). Only earthquakes in the last N minutes are queried
# Default: 10
time_window_minutes = 10
# Minimum magnitude to report (e.g. 3.0 for M3.0+)
# Default: 3.0
min_magnitude = 3.0
# Region bounding box (decimal degrees). Defaults are California
# Southern and northern latitude bounds
minlatitude = 32.5
maxlatitude = 42.0
# Western and eastern longitude bounds (negative = West)
minlongitude = -124.5
maxlongitude = -114.0
# Send USGS event link in a separate message following the alert (true/false)
# When true: notification message then link-only message. When false: no link sent.
# Default: true
send_link = true
[Rate_Limits]
# Per-channel rate limiting: minimum seconds between bot messages on a specific channel.
# Format: channel.<channel_name>_seconds = <seconds>
# Channels without an explicit entry are unrestricted.
# Example: limit "BotCmds" channel to one bot response every 15 seconds:
#channel.BotCmds_seconds = 15
# Example: limit the default channel to 10 seconds between responses:
#channel.BroadcastCh_seconds = 10
[Webhook]
# Inbound webhook receiver — accept HTTP POST requests and relay them as MeshCore messages.
# Useful for integrating external systems (alerts, monitoring, scripts) with the mesh.
#
# Enable the webhook HTTP server (true/false). Default: false
enabled = false
# Bind address. Use 127.0.0.1 to accept connections only from localhost (recommended).
# Use 0.0.0.0 to accept from any interface (ensure your firewall restricts access).
host = 127.0.0.1
# Listen port (must not conflict with the web viewer port, default 8080).
port = 8765
# Optional shared secret. If set, every request must include one of:
# Authorization: Bearer <secret_token>
# X-Webhook-Token: <secret_token>
# Leave empty to disable authentication (NOT recommended for production).
# secret_token = changeme
# Maximum message length in characters (excess is silently truncated). Default: 200
# max_message_length = 200
# Comma-separated channel whitelist. If set, only these channels may be posted to.
# Leave empty to allow any channel.
# allowed_channels = general,alerts
# ---------------------------------------------------------------------------
# HTTP API
# ---------------------------------------------------------------------------
# POST http://<host>:<port>/webhook
# Content-Type: application/json
# Authorization: Bearer <secret_token>
#
# Channel message:
# {"channel": "general", "message": "Hello from webhook!"}
#
# Direct message:
# {"dm_to": "SomeUser", "message": "Private message"}
#
# Response: {"ok": true} or {"error": "..."}
# ---------------------------------------------------------------------------
[RepeaterPrefixCollision_Service]
# Enable repeater prefix collision notifications (true/false)
# Watches NEW_CONTACT events; after the repeater is stored + geolocated in the database,
# posts a message when a newly heard repeater shares a prefix with an existing repeater.
enabled = false
# Channels to post alerts to (comma-separated).
# Prefer this option if you want multiple channels.
channels = #general
# Fallback single channel option (used only when channels= is not set).
# channel = #general
# Notify on duplicate prefix matches for 1, 2, or 3 bytes.
# 1 byte = 2 hex chars (01), 2 bytes = 4 hex chars (0101), 3 bytes = 6 hex chars (010101)
notify_on_prefix_bytes = 1
# Only treat an existing prefix as "in use" if the repeater was heard within this window (days).
heard_window_days = 30
# Used for the "{prefixes_free} free prefixes remain" count (days).
# Set to 0 to count all historical prefixes.
prefix_free_days = 30
# Delay/poll timing to allow contact storage + reverse geocoding to complete.
post_process_delay_seconds = 0.5
post_process_timeout_seconds = 15.0
# Seconds between DB polls while waiting for the row (default 0.2).
# post_process_poll_interval_seconds = 0.2
# Include: "Type 'prefix free' to find one." (only applies to 1-byte notifications).
include_prefix_free_hint = true
# Cooldown (minutes) to reduce alert spam for the same prefix (and repeated NEW_CONTACT events).
cooldown_minutes_per_prefix = 60
# Eligibility (strict, not configurable): alerts only when first_heard is today (local) and
# unique_advert_packets has exactly one distinct packet_hash for this public_key today—so same-day
# re-adverts or double-ingest skip rather than spam. See docs/repeater-prefix-collision-service.md.
[DiscordBridge]
# Enable Discord bridge service
# Enable Discord bridge (true/false). One-way, read-only webhooks
enabled = false
# IMPORTANT: Direct messages (DMs) are NEVER bridged to Discord
# This is hardcoded for privacy and cannot be changed
# Discord Rate Limits:
# - Webhooks are limited to 30 messages per minute per webhook URL
# - The service will log warnings if approaching rate limit (within 20% of exhaustion)
# - If rate limited, Discord returns HTTP 429 with retry-after header
# Avatar generation style
# Controls how user avatars are generated in Discord
# Options:
# color - Generate colored Discord default avatars based on username hash (default, no external API)
# fun-emoji - Fun emoji-style avatars from DiceBear API
# avataaars - Cartoon avatar faces from DiceBear API
# bottts - Robot avatars from DiceBear API
# identicon - Geometric patterns (GitHub-style) from DiceBear API
# pixel-art - Retro pixel-style avatars from DiceBear API
# adventurer - Adventure-themed avatars from DiceBear API
# initials - User initials on colored background from DiceBear API
# Default: color
avatar_style = color
# Profanity handling for bridged message content and usernames: drop (default), censor, or off
# drop = do not bridge messages that contain profanity; censor = replace with **** and bridge; off = no filtering
# filter_profanity = drop
# Bridge the bot's own channel responses (e.g. command replies) to Discord. true (default) = bridge; false = only bridge other users' messages
# bridge_bot_responses = true
# Channel mappings: bridge.<meshcore_channel> = <discord_webhook_url>[, <discord_webhook_url>...]
# Only channels explicitly listed here will be bridged to Discord
# Get webhook URLs from Discord: Channel Settings → Integrations → Webhooks → Create Webhook
#
# SECURITY WARNING: Webhook URLs contain authentication tokens
# - Keep this config file secure and never commit real webhook URLs to version control
# - Anyone with the webhook URL can post to your Discord channel
# - Rotate webhook URLs immediately if exposed
#
# Example mappings:
# bridge.general = https://discord.com/api/webhooks/123456789012345678/abcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHIJKLMNOPQRST
# bridge.emergency = https://discord.com/api/webhooks/987654321098765432/ZYXWVUTSRQPONMLKJIHGFEDCBA0987654321zyxwvutsrqponmlkjihgf
# bridge.wx = https://discord.com/api/webhooks/111222333444555666/WeatherChannelWebhookTokenGoesHere1234567890abcdefghijklmno
#
# Fan out a single MeshCore channel to multiple Discord servers
# bridge.Public = https://discord.com/api/webhooks/AAA/aaa..., https://discord.com/api/webhooks/BBB/bbb...
[TelegramBridge]
# Enable Telegram bridge service
# Posts MeshCore channel messages to Telegram via the Bot API (one-way, read-only)
# true: Enable Telegram bridge
# false: Telegram bridge disabled (default)
enabled = false
# IMPORTANT: Direct messages (DMs) are NEVER bridged to Telegram (hardcoded for privacy)
# Bot API token from @BotFather. Can also be set via TELEGRAM_BOT_TOKEN env var (env takes precedence).
# Rotate token in @BotFather if it was ever exposed.
# api_token = YOUR_BOT_TOKEN_HERE
# Optional: parse_mode for message formatting (HTML, Markdown, or MarkdownV2). Default: HTML
# parse_mode = HTML
# Optional: disable link previews in bridged messages (true/false). Default: false
# disable_web_page_preview = false
# Optional: max message length (1-4096). Telegram limit is 4096. Default: 4096
# max_message_length = 4096
# Profanity handling for bridged message content and usernames: drop (default), censor, or off
# drop = do not bridge messages that contain profanity; censor = replace with **** and bridge; off = no filtering
# filter_profanity = drop
# Bridge the bot's own channel responses (e.g. command replies) to Telegram. true (default) = bridge; false = only bridge other users' messages
# bridge_bot_responses = true
# Channel mappings: bridge.<meshcore_channel> = <telegram_chat_id>
# chat_id: @channelusername for public channels, or numeric ID (e.g. -100xxxxxxxxxx) for private
# Add bot to channel as Administrator with "Post Messages" permission
# See docs/telegram-bridge.md for setup
# bridge.Public = @YourChannelName
# bridge.emergency = -1001234567890
# -----------------------------------------------------------------------------
# Check-in service (local plugin: local/service_plugins/checkin_service.py)
# Put [CheckIn] in local/config.ini to keep main config clean. See docs/checkin-api.md for API contract.
# -----------------------------------------------------------------------------
# [CheckIn]
# enabled = true
# Channel to collect check-ins from (default: #meshmonday)
# channel = #meshmonday
# When to collect: monday (only Mondays) or daily
# check_in_days = monday
# If any_message_counts = true, any message in the channel counts as a check-in.
# If false, only messages containing require_phrase (case-insensitive) count.
# any_message_counts = false
# require_phrase = check in
# Time of day to flush collected check-ins and submit to API (HH:MM or HHMM). Uses [Bot] timezone.
# flush_time = 23:59
# Optional: submit check-ins to a web API (POST). If set, api_key or CHECKIN_API_KEY env is required.
# api_url = https://example.com/checkins
# api_key = YOUR_API_KEY
# Or set CHECKIN_API_KEY in the environment (takes precedence).