Auto Responder Scripting Guide β
Overview β
MeshMonitor's Auto Responder feature supports executing custom scripts in response to mesh messages. This enables advanced automation, dynamic content generation, external API integration, and complex logic beyond simple text responses.
Scripts can be written in Node.js, Python, or Shell and are executed in the MeshMonitor container with full access to message context via environment variables.
Supported Languages β
| Language | Extensions | Interpreter | Version |
|---|---|---|---|
| Node.js | .js, .mjs | /usr/local/bin/node | v22.21.1 |
| Python | .py | /usr/bin/python3 | 3.12.12 |
| Shell | .sh | /bin/sh | BusyBox ash (Alpine) |
Quick Start β
1. Create a Script β
Example: hello.js
#!/usr/bin/env node
const name = process.env.PARAM_name || 'stranger';
const response = {
response: `Hello ${name}! You sent: ${process.env.MESSAGE}`
};
console.log(JSON.stringify(response));2. Deploy to Container β
Option A: Using the UI (Recommended)
- Navigate to Settings β Automation β Auto Responder
- Scroll down to Script Management section
- Click Import Script
- Select your script file (
.js,.mjs,.py, or.sh) - The script will be automatically uploaded and made executable
Option B: Manual Deployment
# Copy script to container
docker cp hello.js meshmonitor:/data/scripts/
# Make executable
docker exec meshmonitor chmod +x /data/scripts/hello.jsOption C: Docker Compose (for scripts requiring environment variables)
Use the Configurator
The easiest way to set up scripts volume mounting is using the Docker Compose Configurator - just check "Mount Auto Responder Scripts Directory" under Additional Settings!
For scripts that need environment variables (e.g., API keys) or easier script management:
services:
meshmonitor:
image: yeraze/meshmonitor:latest
container_name: meshmonitor
restart: unless-stopped
ports:
- "3001:3001"
volumes:
# Mount scripts directory for easy script management
- ./scripts:/data/scripts
# Mount data directory for persistence
- ./data:/data
environment:
# Timezone configuration (for timezone-aware scripts)
- TZ=America/New_York
# Example: API keys for scripts (e.g., Pirate Weather)
- PIRATE_WEATHER_API_KEY=your_api_key_here
# Other MeshMonitor environment variables
- MESHTASTIC_NODE_IP=192.168.1.100
- MESHTASTIC_TCP_PORT=4403Benefits:
- Volume Mapping:
./scripts:/data/scriptsallows editing scripts locally without copying into container - Environment Variables: Scripts can access all environment variables via
process.env(Node.js) oros.environ(Python) - Timezone: Set
TZfor timezone-aware scripts (see Timezone Support)
3. Configure Auto Responder β
- Navigate to Settings β Automation β Auto Responder
- Click Add Trigger
- Configure:
- Trigger:
hello {name} - Type:
Script - Response:
/data/scripts/hello.js(select from dropdown)
- Trigger:
- Click Save Changes
4. Test β
Send a direct message to your node:
hello AliceExpected response:
Hello Alice! You sent: hello AliceScript Requirements β
Must Have β
β
Location: Scripts must be in /data/scripts/ directory β
Extension: .js, .mjs, .py, or .sh β
Output: Valid JSON to stdout with response field β
Timeout: Complete within 10 seconds β
Executable: Have execute permissions (chmod +x)
JSON Output Format β
Scripts must print JSON to stdout:
{
"response": "Your response text (max 200 characters)"
}Optional fields (reserved for future use):
{
"response": "Your response text",
"actions": {
"notify": false,
"log": true
}
}Environment Variables β
All scripts receive these environment variables:
| Variable | Description | Example |
|---|---|---|
MESSAGE | Full message text received | "weather miami" |
FROM_NODE | Sender's node number | "123456789" |
PACKET_ID | Message packet ID | "987654321" |
TRIGGER | Trigger pattern that matched | "weather {location}" |
PARAM_* | Extracted parameters | PARAM_location="miami" |
TZ | Server timezone (IANA timezone name) | "America/New_York" |
Timezone Support β
Scripts receive the TZ environment variable containing the server's configured timezone (IANA timezone name). This allows scripts to perform timezone-aware time operations.
Configuration:
The timezone is configured via the TZ environment variable in your docker-compose.yaml:
environment:
- TZ=America/New_YorkExample: Python Script
#!/usr/bin/env python3
import os
import json
from datetime import datetime
tz = os.environ.get('TZ', 'UTC')
now = datetime.now()
print(json.dumps({
"response": f"Current time in {tz}: {now.strftime('%Y-%m-%d %H:%M:%S %Z')}"
}))Example: JavaScript Script
#!/usr/bin/env node
const tz = process.env.TZ || 'UTC';
const now = new Date();
console.log(JSON.stringify({
response: `Current time in ${tz}: ${now.toLocaleString('en-US', { timeZone: tz })}`
}));Example: Shell Script
#!/bin/sh
TZ="${TZ:-UTC}"
NOW=$(TZ="$TZ" date '+%Y-%m-%d %H:%M:%S %Z')
echo "{\"response\": \"Current time in $TZ: $NOW\"}"Common IANA Timezone Names:
America/New_York- Eastern TimeAmerica/Chicago- Central TimeAmerica/Denver- Mountain TimeAmerica/Los_Angeles- Pacific TimeEurope/London- UK TimeUTC- Coordinated Universal Time
For a complete list, see IANA Time Zone Database.
Parameter Extraction β
Parameters are extracted from trigger patterns using {paramName} syntax:
Trigger: weather {location}Message: weather miamiEnvironment: PARAM_location="miami"
Trigger: forecast {city},{state}Message: forecast austin,txEnvironment:
PARAM_city="austin"PARAM_state="tx"
Custom Regex Patterns (Advanced) β
You can specify custom regex patterns for parameters using {paramName:regex} syntax. This allows for more precise matching and validation:
Basic Regex Examples:
Trigger: w {zip:\d{5}}Message: w 33076Environment: PARAM_zip="33076"Note: Only matches 5-digit zip codes
Trigger: temp {value:\d+}Message: temp 72Environment: PARAM_value="72"Note: Only matches numeric values
Trigger: coords {lat:-?\d+\.?\d*},{lon:-?\d+\.?\d*}Message: coords 40.7128,-74.0060Environment:
PARAM_lat="40.7128"PARAM_lon="-74.0060"Note: Matches decimal coordinates (positive or negative)
More Regex Examples:
Multi-word Parameters:Trigger: weather {location:[\w\s]+}Message: weather new yorkEnvironment: PARAM_location="new york"Note: Matches locations with spaces using [\w\s]+ pattern
Everything Pattern:Trigger: alert {message:.+}Message: alert Hello, world!Environment: PARAM_message="Hello, world!"Note: Matches everything including punctuation using .+ pattern
Common Regex Patterns:
\d+- One or more digits (e.g.,{value:\d+})\d{5}- Exactly 5 digits (e.g.,{zip:\d{5}})[\w\s]+- Word characters and spaces (e.g.,{location:[\w\s]+}).+- Any character including spaces and punctuation (e.g.,{message:.+})-?\d+\.?\d*- Optional negative, digits, optional decimal (e.g.,{temp:-?\d+\.?\d*})
Default Behavior: If no regex pattern is specified, parameters default to matching non-whitespace characters ([^\s]+)
Escaping Special Characters: Remember to escape special regex characters if they appear in your pattern: \ . + * ? ^ $ { } [ ] ( ) |
Multiple Patterns Per Trigger β
You can specify multiple patterns for a single trigger by separating them with commas. This is useful when you want one trigger to handle different message formats (e.g., a command with or without parameters):
Example: Ask Command with Optional Message
Trigger: ask, ask {message}Messages:
askβ Matches first pattern, no parameters extractedask how are youβ Matches second pattern,PARAM_message="how are you"
Script Example:
#!/usr/bin/env python3
import os
import json
message = os.environ.get('PARAM_message', '').strip()
if not message:
# No message provided - show help
response = {
"response": "Ask me anything! Usage: ask {your question}"
}
else:
# Process the question
response = {
"response": f"You asked: {message}. Processing..."
}
print(json.dumps(response))Example: Help Command with Optional Command Name
Trigger: help, help {command}Messages:
helpβ Shows general helphelp weatherβ Shows help for weather command
Example: Temperature with Optional Value
Trigger: temp, temp {value:\d+}Messages:
tempβ Shows current temperaturetemp 72β Sets temperature to 72 (only numeric values accepted due to\d+pattern)
Example: Weather Bot with Help
Trigger: weather, weather {location}Messages:
weatherβ Shows help text with usage examplesweather 90210β Gets weather for zip code 90210weather "New York, NY"β Gets weather for New York
Script Example (PirateWeather.py):
#!/usr/bin/env python3
import os
import json
location = os.environ.get('PARAM_location', '').strip()
if not location:
# No location - show help (triggered by "weather" pattern)
response = {
"response": "Weather Bot:\nβ’ weather {location} - Get weather\nExamples:\nβ’ weather 90210\nβ’ weather \"New York, NY\""
}
else:
# Get weather for location (API call logic here)
response = {
"response": f"Weather for {location}: ..."
}
print(json.dumps(response))Usage: Enter patterns separated by commas in the trigger field. The first matching pattern will be used, and parameters will be extracted from that pattern.
Language-Specific Examples β
Node.js β
Basic Example:
#!/usr/bin/env node
const response = {
response: `Hello from Node.js v${process.version}!`
};
console.log(JSON.stringify(response));With Environment Variables:
#!/usr/bin/env node
const location = process.env.PARAM_location || 'Unknown';
const message = process.env.MESSAGE;
const fromNode = process.env.FROM_NODE;
const response = {
response: `Weather for ${location} requested by node ${fromNode}`
};
console.log(JSON.stringify(response));With External API (using fetch):
#!/usr/bin/env node
const location = process.env.PARAM_location || 'Unknown';
async function getWeather() {
try {
const url = `https://wttr.in/${encodeURIComponent(location)}?format=3`;
const response = await fetch(url, { signal: AbortSignal.timeout(5000) });
const weather = await response.text();
return {
response: weather.trim()
};
} catch (error) {
return {
response: `Failed to get weather for ${location}`
};
}
}
getWeather().then(result => {
console.log(JSON.stringify(result));
}).catch(error => {
console.log(JSON.stringify({ response: 'Error: ' + error.message }));
});With Error Handling:
#!/usr/bin/env node
try {
const name = process.env.PARAM_name;
if (!name) {
throw new Error('Name parameter required');
}
const response = {
response: `Hello ${name}!`
};
console.log(JSON.stringify(response));
} catch (error) {
console.error('Error:', error.message); // Goes to container logs
console.log(JSON.stringify({
response: 'Error processing request'
}));
}Python β
Basic Example:
#!/usr/bin/env python3
import os
import json
name = os.environ.get('PARAM_name', 'stranger')
response = {
"response": f"Hello {name} from Python!"
}
print(json.dumps(response))With External API:
#!/usr/bin/env python3
import os
import json
import urllib.request
import sys
location = os.environ.get('PARAM_location', 'Unknown')
try:
url = f"https://wttr.in/{location}?format=3"
with urllib.request.urlopen(url, timeout=5) as response:
weather = response.read().decode('utf-8').strip()
output = {"response": weather}
except Exception as e:
print(f"Error: {e}", file=sys.stderr) # Goes to container logs
output = {"response": f"Weather unavailable for {location}"}
print(json.dumps(output))With Apprise Integration:
#!/usr/bin/env python3
import os
import json
import sys
# Access Apprise virtual environment
sys.path.insert(0, '/opt/apprise-venv/lib/python3.12/site-packages')
try:
import apprise
message = os.environ.get('MESSAGE', 'No message')
from_node = os.environ.get('FROM_NODE', 'Unknown')
# Send notification
apobj = apprise.Apprise()
apobj.add('mailto://user:pass@gmail.com')
apobj.notify(
body=f'Message from node {from_node}: {message}',
title='Mesh Message'
)
output = {"response": "Notification sent!"}
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
output = {"response": "Notification failed"}
print(json.dumps(output))Shell β
Basic Example:
#!/bin/sh
NAME="${PARAM_name:-stranger}"
cat <<EOF
{
"response": "Hello ${NAME} from Shell!"
}
EOFWith System Commands:
#!/bin/sh
# Get system uptime
UPTIME=$(uptime | awk '{print $3}')
# Get load average
LOAD=$(uptime | awk -F'load average:' '{print $2}' | xargs)
cat <<EOF
{
"response": "Uptime: ${UPTIME}, Load: ${LOAD}"
}
EOFWith Environment Variables:
#!/bin/sh
MESSAGE="${MESSAGE}"
FROM_NODE="${FROM_NODE}"
LOCATION="${PARAM_location:-Unknown}"
cat <<EOF
{
"response": "Location: ${LOCATION}, From: ${FROM_NODE}"
}
EOFAdvanced Patterns β
Database Queries β
Python with SQLite:
#!/usr/bin/env python3
import os
import json
import sqlite3
node_id = os.environ.get('PARAM_nodeid', 'Unknown')
try:
conn = sqlite3.connect('/data/meshmonitor.db')
cursor = conn.cursor()
cursor.execute(
"SELECT longName, lastSeen FROM nodes WHERE nodeId = ?",
(node_id,)
)
result = cursor.fetchone()
conn.close()
if result:
output = {
"response": f"{result[0]} last seen {result[1]}"
}
else:
output = {"response": f"Node {node_id} not found"}
except Exception as e:
output = {"response": "Database error"}
print(json.dumps(output))Multi-Step Logic β
Node.js with Conditional Responses:
#!/usr/bin/env node
const command = process.env.PARAM_command;
const arg = process.env.PARAM_arg;
let response;
switch (command) {
case 'status':
response = `System status: OK`;
break;
case 'info':
response = `Node info: ${process.env.FROM_NODE}`;
break;
case 'weather':
response = `Weather for ${arg}: Checking...`;
break;
default:
response = `Unknown command: ${command}`;
}
console.log(JSON.stringify({ response }));Caching Results β
Python with File Cache:
#!/usr/bin/env python3
import os
import json
import time
CACHE_FILE = '/data/scripts/.cache/weather.json'
CACHE_TTL = 300 # 5 minutes
location = os.environ.get('PARAM_location', 'Unknown')
# Check cache
try:
if os.path.exists(CACHE_FILE):
age = time.time() - os.path.getmtime(CACHE_FILE)
if age < CACHE_TTL:
with open(CACHE_FILE, 'r') as f:
cached = json.load(f)
if cached.get('location') == location:
print(json.dumps({"response": cached['data']}))
exit(0)
except Exception:
pass
# Fetch fresh data (implement API call here)
weather_data = f"Weather for {location}: Sunny, 72Β°F"
# Save to cache
try:
os.makedirs(os.path.dirname(CACHE_FILE), exist_ok=True)
with open(CACHE_FILE, 'w') as f:
json.dump({'location': location, 'data': weather_data}, f)
except Exception:
pass
print(json.dumps({"response": weather_data}))Debugging β
View Execution Logs β
# Tail container logs in real-time
docker logs -f meshmonitor
# Search for script errors
docker logs meshmonitor 2>&1 | grep -i "script"
# View last 100 lines
docker logs meshmonitor --tail 100Script Debug Output β
Node.js:
console.error('Debug:', someVariable); // Appears in container logs
console.log(JSON.stringify({response: 'OK'})); // Sent to meshPython:
print(f'Debug: {some_variable}', file=sys.stderr) # Logs
print(json.dumps({"response": "OK"})) # ResponseShell:
echo "Debug: $VARIABLE" >&2 # Logs
cat <<EOF # Response
{"response": "OK"}
EOFTest Scripts Locally β
# Test Node.js script
docker exec meshmonitor sh -c 'export MESSAGE="test" PARAM_name="Alice" && /usr/local/bin/node /data/scripts/hello.js'
# Test Python script
docker exec meshmonitor sh -c 'export MESSAGE="weather miami" PARAM_location="miami" && /usr/bin/python3 /data/scripts/weather.py'
# Test Shell script
docker exec meshmonitor sh -c 'export MESSAGE="info" FROM_NODE="123" && /bin/sh /data/scripts/info.sh'Security Considerations β
Sandboxing β
β
Scripts run as node user (not root) β
Limited to /data/scripts/ directory β
Path traversal attempts (..) are blocked β
10-second execution timeout β
Output limited to 1MB
Best Practices β
DO:
- β Validate all parameters before use
- β Handle errors gracefully
- β Use timeout for external API calls
- β Sanitize user input
- β Log errors to stderr for debugging
DON'T:
- β Trust user input without validation
- β Execute arbitrary commands from parameters
- β Store secrets in script files
- β Make unbounded API calls
- β Ignore error handling
Example: Input Validation β
#!/usr/bin/env node
const location = process.env.PARAM_location || '';
// Validate input
if (!/^[a-zA-Z0-9\s,-]{1,50}$/.test(location)) {
console.log(JSON.stringify({
response: 'Invalid location format'
}));
process.exit(0);
}
// Safe to use location
const response = {
response: `Weather for ${location}: ...`
};
console.log(JSON.stringify(response));Performance Tips β
Optimize Script Execution β
Keep scripts fast (< 1 second preferred)
- Cache external API results
- Use efficient algorithms
- Minimize disk I/O
Use async I/O for network requests
- Node.js: Use
fetchwith timeout - Python: Use
urllibwith timeout - Shell: Use
curlwith--max-time
- Node.js: Use
Implement caching when appropriate
- File-based cache for API responses
- Memory cache for frequently accessed data
- Respect cache TTL
Test scripts locally before deployment
- Verify JSON output format
- Test error handling
- Measure execution time
Example: Efficient API Call β
#!/usr/bin/env node
const location = process.env.PARAM_location;
async function getWeather() {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 3000);
try {
const response = await fetch(
`https://api.example.com/weather/${location}`,
{
signal: controller.signal,
headers: { 'User-Agent': 'MeshMonitor' }
}
);
if (!response.ok) throw new Error('API error');
const data = await response.json();
return { response: data.summary };
} catch (error) {
return { response: 'Weather unavailable' };
} finally {
clearTimeout(timeout);
}
}
getWeather().then(result => console.log(JSON.stringify(result)));Troubleshooting β
Common Issues β
Script doesn't appear in dropdown:
- Verify file is in
/data/scripts/ - Check file extension (
.js,.mjs,.py,.sh) - Refresh the Auto Responder page
Script executes but no response:
- Check JSON output format (must have
responsefield) - Verify stdout (not stderr) is used
- Check for script errors in logs:
docker logs meshmonitor
Timeout errors:
- Reduce external API timeout
- Optimize slow operations
- Check for infinite loops
Permission denied:
- Make script executable:
chmod +x /data/scripts/script.py - Verify file ownership is correct
Parameters not extracted:
- Verify trigger pattern uses
{paramName}syntax (or{paramName:regex}for custom patterns) - Check environment variable names match (case-sensitive)
- Ensure custom regex patterns are valid and match the expected input
- Test trigger patterns using the "Test Trigger Matching" feature in the UI
- Remember: default pattern matches non-whitespace
[^\s]+, custom patterns override this
Example Scripts Repository β
Complete example scripts are available in the MeshMonitor repository:
GitHub: examples/auto-responder-scripts/
hello.js- Simple Node.js greeting scriptweather.py- Python weather lookup templatePirateWeather.py- Complete Pirate Weather API integration with Nominatim geocodinginfo.sh- Shell system information scriptREADME.md- Detailed examples and usage
API Reference β
/api/scripts Endpoint β
Method: GET Authentication: None (public endpoint) Response:
{
"scripts": [
"/data/scripts/hello.js",
"/data/scripts/info.sh",
"/data/scripts/weather.py"
]
}This endpoint is called automatically by the Auto Responder UI to populate the script dropdown.
Version Compatibility β
| MeshMonitor Version | Feature |
|---|---|
| v2.18.0+ | Script execution support |
| v2.17.8 | Text and HTTP responses only |
Support β
For issues, questions, or feature requests:
- GitHub Issues: https://github.com/MeshAddicts/meshmonitor/issues
- Documentation: https://meshmonitor.org/features/automation#auto-responder
- Examples: https://github.com/MeshAddicts/meshmonitor/tree/main/examples/auto-responder-scripts
License β
MeshMonitor is licensed under the MIT License. See LICENSE for details.