mirror of
https://github.com/jorijn/meshcore-stats.git
synced 2026-03-28 17:42:55 +01:00
feat: auto-load config from meshcore.conf
Simplify setup by having Python automatically load configuration from meshcore.conf at module import time. This eliminates the need to source config files in cron jobs or use direnv. - Add _load_config_file() to env.py that parses shell-style config - Environment variables always take precedence (Docker-friendly) - Rename .envrc.example to meshcore.conf.example (no direnv dependency) - Update cron examples to use flock for USB serial locking - Simplify documentation to use traditional .venv/ virtualenv BREAKING CHANGE: Configuration file renamed from .envrc to meshcore.conf. Users must copy meshcore.conf.example to meshcore.conf and migrate their settings. The new file format is the same (shell-style exports) but without the direnv-specific "layout python3" command. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -15,6 +15,7 @@ build/
|
||||
# Environment
|
||||
.envrc
|
||||
.env
|
||||
meshcore.conf
|
||||
|
||||
# Data directories (keep structure, ignore content)
|
||||
data/snapshots/companion/**/*.json
|
||||
|
||||
55
CLAUDE.md
55
CLAUDE.md
@@ -12,21 +12,18 @@
|
||||
| HTML templates | `src/meshmon/templates/*.html` | `out/*.html` |
|
||||
| JavaScript | `src/meshmon/templates/*.js` | `out/*.js` |
|
||||
|
||||
Always edit the source templates, then regenerate with `direnv exec . python scripts/render_site.py`.
|
||||
Always edit the source templates, then regenerate with `python scripts/render_site.py`.
|
||||
|
||||
## Running Commands
|
||||
|
||||
**IMPORTANT**: Always use `direnv exec .` to run Python scripts in this project. This ensures the correct virtualenv and environment variables are loaded.
|
||||
|
||||
```bash
|
||||
# Correct way to run scripts
|
||||
direnv exec . python scripts/render_site.py
|
||||
|
||||
# NEVER use these (virtualenv won't be loaded correctly):
|
||||
# source .envrc && python ...
|
||||
# .direnv/python-3.12/bin/python ...
|
||||
cd /path/to/meshcore-stats
|
||||
source .venv/bin/activate
|
||||
python scripts/render_site.py
|
||||
```
|
||||
|
||||
Configuration is automatically loaded from `meshcore.conf` (if it exists). Environment variables always take precedence over the config file.
|
||||
|
||||
## Commit Message Guidelines
|
||||
|
||||
This project uses [Conventional Commits](https://www.conventionalcommits.org/) with [release-please](https://github.com/googleapis/release-please) for automated releases. **Commit messages directly control versioning and changelog generation.**
|
||||
@@ -236,12 +233,13 @@ meshcore-stats/
|
||||
│ │ └── MM/
|
||||
│ │ └── index.html, report.txt, report.json # Monthly
|
||||
│ └── companion/ # Same structure as repeater
|
||||
└── .envrc # Environment configuration
|
||||
├── meshcore.conf.example # Example configuration
|
||||
└── meshcore.conf # Your configuration (auto-loaded by scripts)
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
All configuration via environment variables (see `.envrc`):
|
||||
All configuration via `meshcore.conf` or environment variables. The config file is automatically loaded by scripts; environment variables take precedence.
|
||||
|
||||
### Connection Settings
|
||||
- `MESH_TRANSPORT`: "serial" (default), "tcp", or "ble"
|
||||
@@ -390,16 +388,13 @@ Current migrations:
|
||||
|
||||
## Running the Scripts
|
||||
|
||||
Always source the environment first:
|
||||
|
||||
```bash
|
||||
# Using direnv (automatic)
|
||||
cd /path/to/meshcore-stats
|
||||
|
||||
# Or manually
|
||||
source .envrc 2>/dev/null
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
Configuration is automatically loaded from `meshcore.conf`.
|
||||
|
||||
### Data Collection
|
||||
|
||||
```bash
|
||||
@@ -593,31 +588,37 @@ meshcore-cli -s /dev/ttyACM0 reset_path "repeater name"
|
||||
- Have routing issues (asymmetric path)
|
||||
- Be offline or rebooted
|
||||
|
||||
2. **Environment variables not loaded**: Scripts must be run with direnv active or manually source `.envrc`
|
||||
2. **Configuration not loaded**: Ensure `meshcore.conf` exists in the project root, or set environment variables directly.
|
||||
|
||||
3. **Empty charts**: Need at least 2 data points to display meaningful data.
|
||||
|
||||
## Cron Setup (Example)
|
||||
|
||||
**Important**: Stagger companion and repeater collection to avoid USB serial conflicts.
|
||||
Use `flock` to prevent USB serial conflicts when companion and repeater collection overlap.
|
||||
|
||||
```cron
|
||||
# Companion: every minute at :00
|
||||
* * * * * cd /path/to/meshcore-stats && .direnv/python-3.12/bin/python scripts/collect_companion.py
|
||||
MESHCORE=/path/to/meshcore-stats
|
||||
|
||||
# Repeater: every 15 minutes at :01, :16, :31, :46 (offset by 1 min to avoid USB conflict)
|
||||
1,16,31,46 * * * * cd /path/to/meshcore-stats && .direnv/python-3.12/bin/python scripts/collect_repeater.py
|
||||
# Companion: every minute
|
||||
* * * * * cd $MESHCORE && flock -w 60 /tmp/meshcore.lock .venv/bin/python scripts/collect_companion.py
|
||||
|
||||
# Repeater: every 15 minutes (offset by 1 min for staggering)
|
||||
1,16,31,46 * * * * cd $MESHCORE && flock -w 60 /tmp/meshcore.lock .venv/bin/python scripts/collect_repeater.py
|
||||
|
||||
# Charts: every 5 minutes (generates SVG charts from database)
|
||||
*/5 * * * * cd /path/to/meshcore-stats && .direnv/python-3.12/bin/python scripts/render_charts.py
|
||||
*/5 * * * * cd $MESHCORE && .venv/bin/python scripts/render_charts.py
|
||||
|
||||
# HTML: every 5 minutes
|
||||
*/5 * * * * cd /path/to/meshcore-stats && .direnv/python-3.12/bin/python scripts/render_site.py
|
||||
*/5 * * * * cd $MESHCORE && .venv/bin/python scripts/render_site.py
|
||||
|
||||
# Reports: daily at midnight (historical stats don't change often)
|
||||
0 0 * * * cd /path/to/meshcore-stats && .direnv/python-3.12/bin/python scripts/render_reports.py
|
||||
0 0 * * * cd $MESHCORE && .venv/bin/python scripts/render_reports.py
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- `cd $MESHCORE` is required because paths in the config are relative to the project root
|
||||
- `flock -w 60` waits up to 60 seconds for the lock, preventing USB serial conflicts
|
||||
|
||||
## Adding New Metrics
|
||||
|
||||
With the EAV schema, adding new metrics is simple:
|
||||
@@ -653,7 +654,7 @@ To change a metric from gauge to counter (or vice versa):
|
||||
|
||||
1. Update `METRIC_CONFIG` in `src/meshmon/metrics.py` - change the `type` field
|
||||
2. Update `scale` if needed (counters often use scale=60 for per-minute display)
|
||||
3. Regenerate charts: `direnv exec . python scripts/render_charts.py`
|
||||
3. Regenerate charts: `python scripts/render_charts.py`
|
||||
|
||||
## Database Maintenance
|
||||
|
||||
|
||||
52
README.md
52
README.md
@@ -41,16 +41,16 @@ source .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 2. Configure Environment Variables
|
||||
### 2. Configure
|
||||
|
||||
Copy the example configuration file and customize it:
|
||||
|
||||
```bash
|
||||
cp .envrc.example .envrc
|
||||
# Edit .envrc with your settings
|
||||
cp meshcore.conf.example meshcore.conf
|
||||
# Edit meshcore.conf with your settings
|
||||
```
|
||||
|
||||
The `.envrc.example` file contains all available configuration options with documentation. Key settings to configure:
|
||||
The configuration file is automatically loaded by the scripts. Key settings to configure:
|
||||
|
||||
- **Connection**: `MESH_SERIAL_PORT`, `MESH_TRANSPORT`
|
||||
- **Repeater Identity**: `REPEATER_NAME`, `REPEATER_PASSWORD`
|
||||
@@ -59,16 +59,16 @@ The `.envrc.example` file contains all available configuration options with docu
|
||||
- **Hardware Info**: `REPEATER_HARDWARE`, `COMPANION_HARDWARE`
|
||||
- **Radio Config**: `RADIO_FREQUENCY`, `RADIO_BANDWIDTH`, etc. (includes presets for different regions)
|
||||
|
||||
If using direnv:
|
||||
```bash
|
||||
direnv allow
|
||||
```
|
||||
See `meshcore.conf.example` for all available options with documentation.
|
||||
|
||||
## Usage
|
||||
|
||||
### Manual Execution
|
||||
|
||||
```bash
|
||||
cd /path/to/meshcore-stats
|
||||
source .venv/bin/activate
|
||||
|
||||
# Collect companion data
|
||||
python scripts/collect_companion.py
|
||||
|
||||
@@ -82,32 +82,46 @@ python scripts/render_site.py
|
||||
python scripts/render_reports.py
|
||||
```
|
||||
|
||||
The configuration is automatically loaded from `meshcore.conf`.
|
||||
|
||||
### Cron Setup
|
||||
|
||||
Add these entries to your crontab (`crontab -e`):
|
||||
|
||||
```cron
|
||||
# MeshCore Stats - adjust paths as needed
|
||||
SHELL=/bin/bash
|
||||
MESHCORE_STATS=/home/user/meshcore-stats
|
||||
DIRENV=/usr/bin/direnv
|
||||
# MeshCore Stats - adjust path as needed
|
||||
MESHCORE=/home/user/meshcore-stats
|
||||
|
||||
# Every minute: collect companion data
|
||||
* * * * * cd $MESHCORE_STATS && $DIRENV exec . python scripts/collect_companion.py
|
||||
* * * * * cd $MESHCORE && flock -w 60 /tmp/meshcore.lock .venv/bin/python scripts/collect_companion.py
|
||||
|
||||
# Every 15 minutes: collect repeater data
|
||||
*/15 * * * * cd $MESHCORE_STATS && $DIRENV exec . python scripts/collect_repeater.py
|
||||
1,16,31,46 * * * * cd $MESHCORE && flock -w 60 /tmp/meshcore.lock .venv/bin/python scripts/collect_repeater.py
|
||||
|
||||
# Every 5 minutes: render site
|
||||
*/5 * * * * cd $MESHCORE_STATS && $DIRENV exec . python scripts/render_site.py
|
||||
*/5 * * * * cd $MESHCORE && .venv/bin/python scripts/render_site.py
|
||||
|
||||
# Daily at midnight: generate reports
|
||||
0 0 * * * cd $MESHCORE_STATS && $DIRENV exec . python scripts/render_reports.py
|
||||
0 0 * * * cd $MESHCORE && .venv/bin/python scripts/render_reports.py
|
||||
|
||||
# Monthly at 3 AM on the 1st: database maintenance
|
||||
0 3 1 * * cd $MESHCORE_STATS && ./scripts/db_maintenance.sh
|
||||
0 3 1 * * $MESHCORE/scripts/db_maintenance.sh
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- `cd $MESHCORE` is required because paths in the config are relative to the project root
|
||||
- `flock` prevents USB serial conflicts when companion and repeater collection overlap
|
||||
|
||||
### Docker / Container Usage
|
||||
|
||||
When running in Docker, you can skip the config file and pass environment variables directly:
|
||||
|
||||
```bash
|
||||
docker run -e MESH_SERIAL_PORT=/dev/ttyUSB0 -e REPEATER_NAME="My Repeater" ...
|
||||
```
|
||||
|
||||
Environment variables always take precedence over `meshcore.conf`.
|
||||
|
||||
### Serving the Site
|
||||
|
||||
The static site is generated in the `out/` directory. You can serve it with any web server:
|
||||
@@ -125,8 +139,8 @@ cd out && python3 -m http.server 8080
|
||||
meshcore-stats/
|
||||
├── requirements.txt
|
||||
├── README.md
|
||||
├── .envrc.example # Example configuration (copy to .envrc)
|
||||
├── .envrc # Your configuration (create this)
|
||||
├── meshcore.conf.example # Example configuration
|
||||
├── meshcore.conf # Your configuration (create this)
|
||||
├── src/meshmon/
|
||||
│ ├── __init__.py
|
||||
│ ├── env.py # Environment variable parsing
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
# MeshCore Stats Configuration
|
||||
# Copy this file to .envrc and customize for your setup:
|
||||
# cp .envrc.example .envrc
|
||||
# Copy this file to meshcore.conf and customize for your setup:
|
||||
# cp meshcore.conf.example meshcore.conf
|
||||
#
|
||||
# If using direnv, it will automatically load when you cd into this directory.
|
||||
# Otherwise, source it manually: source .envrc
|
||||
|
||||
layout python3
|
||||
# This file is automatically loaded by the scripts. No need to source it manually.
|
||||
# Environment variables always take precedence over this file (useful for Docker).
|
||||
|
||||
# =============================================================================
|
||||
# Connection Settings
|
||||
@@ -1,10 +1,91 @@
|
||||
"""Environment variable parsing and configuration."""
|
||||
|
||||
import os
|
||||
import re
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def _parse_config_value(value: str) -> str:
|
||||
"""Parse a shell-style value, handling quotes and inline comments."""
|
||||
value = value.strip()
|
||||
|
||||
if not value:
|
||||
return ""
|
||||
|
||||
# Handle double-quoted strings
|
||||
if value.startswith('"'):
|
||||
end = value.find('"', 1)
|
||||
if end != -1:
|
||||
return value[1:end]
|
||||
return value[1:] # No closing quote
|
||||
|
||||
# Handle single-quoted strings
|
||||
if value.startswith("'"):
|
||||
end = value.find("'", 1)
|
||||
if end != -1:
|
||||
return value[1:end]
|
||||
return value[1:]
|
||||
|
||||
# Unquoted value - strip inline comments (# preceded by whitespace)
|
||||
comment_match = re.search(r"\s+#", value)
|
||||
if comment_match:
|
||||
value = value[: comment_match.start()]
|
||||
|
||||
return value.strip()
|
||||
|
||||
|
||||
def _load_config_file() -> None:
|
||||
"""Load meshcore.conf if it exists. Env vars take precedence.
|
||||
|
||||
The config file is expected in the project root (three levels up from this module).
|
||||
Scripts should be run from the project directory via cron: cd $MESHCORE && .venv/bin/python ...
|
||||
"""
|
||||
config_path = Path(__file__).resolve().parent.parent.parent / "meshcore.conf"
|
||||
|
||||
if not config_path.exists():
|
||||
return
|
||||
|
||||
try:
|
||||
with open(config_path, encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
|
||||
# Skip comments and empty lines
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
|
||||
# Remove 'export ' prefix
|
||||
if line.startswith("export "):
|
||||
line = line[7:].lstrip()
|
||||
|
||||
# Must have KEY=value format
|
||||
if "=" not in line:
|
||||
continue
|
||||
|
||||
key, _, value = line.partition("=")
|
||||
key = key.strip()
|
||||
|
||||
# Validate key is a valid shell identifier
|
||||
if not key or not re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", key):
|
||||
continue
|
||||
|
||||
# Parse value (handles quotes, inline comments)
|
||||
value = _parse_config_value(value)
|
||||
|
||||
# Only set if not already in environment
|
||||
if key not in os.environ:
|
||||
os.environ[key] = value
|
||||
|
||||
except (OSError, UnicodeDecodeError) as e:
|
||||
warnings.warn(f"Failed to load {config_path}: {e}")
|
||||
|
||||
|
||||
# Load config file at module import time, before Config is instantiated
|
||||
_load_config_file()
|
||||
|
||||
|
||||
def get_str(key: str, default: Optional[str] = None) -> Optional[str]:
|
||||
"""Get string env var."""
|
||||
return os.environ.get(key, default)
|
||||
|
||||
Reference in New Issue
Block a user