Initial commit: LAN Checker

Network health monitoring script with MQTT reporting for Home Assistant.
- Ping, HTTP, and SNMP checkers
- MQTT Discovery for automatic entity creation
- Configurable check intervals

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-26 16:22:55 +01:00
commit 02b14979bc
11 changed files with 536 additions and 0 deletions

218
lan_checker.py Normal file
View File

@@ -0,0 +1,218 @@
#!/usr/bin/env python3
"""
LAN Checker - Network health monitoring with MQTT reporting for Home Assistant.
"""
import argparse
import json
import logging
import signal
import sys
import time
from pathlib import Path
import yaml
import paho.mqtt.client as mqtt
from checkers import CHECKERS
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)
class LanChecker:
def __init__(self, config_path: str):
self.config = self._load_config(config_path)
self.mqtt_client: mqtt.Client | None = None
self.running = False
self.checks = []
def _load_config(self, config_path: str) -> dict:
path = Path(config_path)
if not path.exists():
raise FileNotFoundError(f"Configuration file not found: {config_path}")
with open(path, "r") as f:
return yaml.safe_load(f)
def _setup_mqtt(self):
mqtt_config = self.config["mqtt"]
self.mqtt_client = mqtt.Client(
mqtt.CallbackAPIVersion.VERSION2,
client_id=mqtt_config.get("client_id", "lan_checker")
)
if mqtt_config.get("username"):
self.mqtt_client.username_pw_set(
mqtt_config["username"],
mqtt_config.get("password", "")
)
self.mqtt_client.on_connect = self._on_mqtt_connect
self.mqtt_client.on_disconnect = self._on_mqtt_disconnect
self.mqtt_client.connect(
mqtt_config["host"],
mqtt_config.get("port", 1883),
keepalive=60
)
self.mqtt_client.loop_start()
def _on_mqtt_connect(self, client, userdata, flags, reason_code, properties):
if reason_code == 0:
logger.info("Connected to MQTT broker")
self._publish_discovery()
else:
logger.error(f"MQTT connection failed: {reason_code}")
def _on_mqtt_disconnect(self, client, userdata, flags, reason_code, properties):
logger.warning(f"Disconnected from MQTT broker: {reason_code}")
def _publish_discovery(self):
"""Publish MQTT Discovery messages for Home Assistant."""
for check in self.config["checks"]:
device_id = check["id"]
device_name = check["name"]
# Binary sensor for online/offline status
status_config = {
"name": f"{device_name} Status",
"unique_id": f"lan_checker_{device_id}_status",
"state_topic": f"lan_checker/{device_id}/state",
"value_template": "{{ value_json.state }}",
"payload_on": "online",
"payload_off": "offline",
"device_class": "connectivity",
"device": {
"identifiers": [f"lan_checker_{device_id}"],
"name": device_name,
"manufacturer": "LAN Checker",
},
}
self.mqtt_client.publish(
f"homeassistant/binary_sensor/lan_checker_{device_id}/config",
json.dumps(status_config),
retain=True
)
# Sensor for response time
latency_config = {
"name": f"{device_name} Latency",
"unique_id": f"lan_checker_{device_id}_latency",
"state_topic": f"lan_checker/{device_id}/state",
"value_template": "{{ value_json.response_time | default('unavailable') }}",
"unit_of_measurement": "ms",
"device_class": "duration",
"state_class": "measurement",
"device": {
"identifiers": [f"lan_checker_{device_id}"],
"name": device_name,
"manufacturer": "LAN Checker",
},
}
self.mqtt_client.publish(
f"homeassistant/sensor/lan_checker_{device_id}_latency/config",
json.dumps(latency_config),
retain=True
)
logger.info(f"Published discovery for: {device_name}")
def _setup_checks(self):
"""Initialize check instances from configuration."""
for check_config in self.config["checks"]:
check_type = check_config["type"]
if check_type not in CHECKERS:
logger.error(f"Unknown check type: {check_type}")
continue
checker_class = CHECKERS[check_type]
checker = checker_class(check_config["name"], check_config)
self.checks.append({
"id": check_config["id"],
"name": check_config["name"],
"checker": checker,
"interval": check_config.get("interval", self.config.get("default_interval", 60)),
"last_check": 0,
})
def _run_check(self, check: dict):
"""Execute a single check and publish results."""
result = check["checker"].check()
state = "online" if result.success else "offline"
payload = {
"state": state,
"message": result.message,
"response_time": round(result.response_time, 2) if result.response_time else None,
"last_check": time.strftime("%Y-%m-%dT%H:%M:%S"),
}
if result.details:
payload["details"] = result.details
topic = f"lan_checker/{check['id']}/state"
self.mqtt_client.publish(topic, json.dumps(payload), retain=True)
log_level = logging.INFO if result.success else logging.WARNING
logger.log(log_level, f"{check['name']}: {state} - {result.message}")
def run(self):
"""Main loop."""
self._setup_mqtt()
self._setup_checks()
self.running = True
logger.info(f"Starting LAN Checker with {len(self.checks)} checks")
while self.running:
current_time = time.time()
for check in self.checks:
if current_time - check["last_check"] >= check["interval"]:
self._run_check(check)
check["last_check"] = current_time
time.sleep(1)
def stop(self):
"""Stop the checker gracefully."""
logger.info("Stopping LAN Checker...")
self.running = False
if self.mqtt_client:
self.mqtt_client.loop_stop()
self.mqtt_client.disconnect()
def main():
parser = argparse.ArgumentParser(description="LAN Checker - Network health monitoring")
parser.add_argument(
"-c", "--config",
default="config.yaml",
help="Path to configuration file (default: config.yaml)"
)
args = parser.parse_args()
checker = LanChecker(args.config)
def signal_handler(sig, frame):
checker.stop()
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
try:
checker.run()
except Exception as e:
logger.error(f"Fatal error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()