Initial commit with cleaned history and environment configuration

This commit is contained in:
Space-Banane
2026-03-12 15:47:58 +01:00
commit 66ec4945d5
5 changed files with 203 additions and 0 deletions

11
.env.example Normal file
View File

@@ -0,0 +1,11 @@
# AdGuard Configuration
ADGUARD_URL=http://your-adguard-ip
ADGUARD_USER=your-username
ADGUARD_PASSWORD=your-password
# Home Assistant Configuration
HASS_URL=https://your-hass-instance.com/api/services/notify/mobile_app_your_phone
HASS_TOKEN=your-long-lived-access-token
# Monitored Clients (JSON Format: {"IP": "Nickname"})
CLIENTS='{"192.168.1.100": "My Laptop", "192.168.1.101": "My Phone"}'

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
.env
*.log
__pycache__/
*.pyc

37
README.md Normal file
View File

@@ -0,0 +1,37 @@
# AdGuard Monitor
Monitoring service for AdGuard DNS queries with notifications sent to Home Assistant.
## Who is this for?
> This is for me mainly, so let's see if you need it too.
Adguard Monitor is designed for openclaw, which should usually NEVER make a request that needs to be blocked. If it does, I want to know about it immediately.
## Features
- **Multi-Client Support**: Monitor specific IP addresses and receive nicknamed notifications (e.g., "BLOCKED: My Laptop").
- **Strict Configuration**: Fails fast if environment variables are missing.
- **Home Assistant Notifications**: Native mobile push messages for blocked queries.
- **Structured Logging**: UTF-8 encoded logging for unicode emoji support.
## Setup
1. **Clone the repository.**
2. **Install dependencies**:
```bash
pip install -r requirements.txt
```
3. **Configure environment**:
Copy `.env.example` to `.env` and fill in your details:
```bash
cp .env.example .env
```
4. **Run the monitor**:
```bash
python monitor.py --interval 15
```
## Environment Variables
- `ADGUARD_URL`: Full URL of your AdGuard instance (e.g., `http://192.168.1.50`).
- `ADGUARD_USER/PASSWORD`: API credentials.
- `CLIENTS`: A JSON-formatted dictionary mapping IPs to display names.
- `HASS_URL/TOKEN`: Home Assistant mobile notification endpoint and Long-Lived Access Token.

149
monitor.py Normal file
View File

@@ -0,0 +1,149 @@
import logging
import requests
import json
import time
import argparse
import os
from dotenv import load_dotenv
from requests.auth import HTTPBasicAuth
from datetime import datetime
# Load environment variables
load_dotenv()
def get_env_or_fail(key):
value = os.getenv(key)
if not value or value.strip() == "":
raise EnvironmentError(f"CRITICAL: Missing required environment variable: {key}")
return value
# --- Configuration ---
try:
ADGUARD_URL = get_env_or_fail("ADGUARD_URL")
QUERY_LOG_URL = f"{ADGUARD_URL}/control/querylog"
USERNAME = get_env_or_fail("ADGUARD_USER")
PASSWORD = get_env_or_fail("ADGUARD_PASSWORD")
# Clients configuration via JSON string in .env: {"IP": "NAME"}
CLIENTS_RAW = get_env_or_fail("CLIENTS")
CLIENTS = json.loads(CLIENTS_RAW)
# --- Home Assistant Configuration ---
HASS_URL = get_env_or_fail("HASS_URL")
HASS_TOKEN = get_env_or_fail("HASS_TOKEN")
HASS_HEADERS = {
"Authorization": f"Bearer {HASS_TOKEN}",
"Content-Type": "application/json",
}
except (EnvironmentError, json.JSONDecodeError) as e:
print(f"Error during startup: {e}")
exit(1)
def notify_hass(message,title):
payload = {
"message": message,
"title": title,
"data": {
"push": {
"sound": "default",
"badge": 1,
},
"icon_url": "https://adguard.com/favicon.ico",
}
}
try:
requests.post(HASS_URL, headers=HASS_HEADERS, json=payload, timeout=5)
except Exception as e:
logger.error(f"Failed to send HASS notification: {e}")
# --- Logging Setup ---
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s',
handlers=[
logging.FileHandler("adguard_monitor.log", encoding='utf-8'),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
# --- State Management ---
class MonitorStats:
def __init__(self):
self.api_calls = 0
self.total_blocked = 0
self.last_log_id = None
def log_api_call(self):
self.api_calls += 1
def log_blocked(self):
self.total_blocked += 1
def get_summary(self):
return f"API Polls: {self.api_calls} | Total Blocked: {self.total_blocked}"
stats = MonitorStats()
def get_auth_session():
session = requests.Session()
session.auth = HTTPBasicAuth(USERNAME, PASSWORD)
return session
def fetch_and_analyze(session):
stats.log_api_call()
try:
response = session.get(QUERY_LOG_URL, params={'limit': 50}, timeout=10)
response.raise_for_status()
data = response.json()
logs = data.get('data', [])
# Log pull event
logger.info(f"Pulled {len(logs)} entries from API")
if not logs:
return
# Process logs in reverse order (oldest to newest) to maintain sequence
new_items_processed = 0
for log in reversed(logs):
log_id = log.get('time')
if stats.last_log_id and log_id <= stats.last_log_id:
continue
client_ip = log.get('client')
if client_ip not in CLIENTS:
continue
stats.last_log_id = log_id
new_items_processed += 1
reason = log.get('reason')
host = log.get('question', {}).get('name')
client_name = CLIENTS[client_ip]
if reason in ["FilteredBlackList", "Rewrite"]:
stats.log_blocked()
msg = f"🚫 Blocked: {host}\nReason: {reason}"
logger.warning(msg)
notify_hass(msg, f"BLOCKED: {client_name}")
logger.info(f"Analyzed {new_items_processed} new entries since last pull.")
logger.debug(f"Status Update: {stats.get_summary()}")
except requests.exceptions.RequestException as e:
logger.error(f"API Error: {e}")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="AdGuard Monitor Service")
parser.add_argument("--interval", type=int, default=15, help="Update interval in seconds")
args = parser.parse_args()
logger.info(f"AdGuard Monitor Service Started. Interval: {args.interval}s")
session = get_auth_session()
while True:
fetch_and_analyze(session)
time.sleep(args.interval)

2
requirements.txt Normal file
View File

@@ -0,0 +1,2 @@
requests
python-dotenv