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

9
checkers/__init__.py Normal file
View File

@@ -0,0 +1,9 @@
from .ping import PingChecker
from .http import HttpChecker
from .snmp import SnmpChecker
CHECKERS = {
"ping": PingChecker,
"http": HttpChecker,
"snmp": SnmpChecker,
}

21
checkers/base.py Normal file
View File

@@ -0,0 +1,21 @@
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any
@dataclass
class CheckResult:
success: bool
message: str
response_time: float | None = None
details: dict[str, Any] | None = None
class BaseChecker(ABC):
def __init__(self, name: str, config: dict):
self.name = name
self.config = config
@abstractmethod
def check(self) -> CheckResult:
pass

53
checkers/http.py Normal file
View File

@@ -0,0 +1,53 @@
import time
import requests
from .base import BaseChecker, CheckResult
class HttpChecker(BaseChecker):
def check(self) -> CheckResult:
url = self.config["url"]
method = self.config.get("method", "GET").upper()
timeout = self.config.get("timeout", 10)
expected_status = self.config.get("expected_status", 200)
verify_ssl = self.config.get("verify_ssl", True)
headers = self.config.get("headers", {})
start = time.time()
try:
response = requests.request(
method=method,
url=url,
timeout=timeout,
verify=verify_ssl,
headers=headers
)
response_time = (time.time() - start) * 1000 # ms
if response.status_code == expected_status:
return CheckResult(
success=True,
message=f"HTTP {response.status_code}",
response_time=response_time,
details={"status_code": response.status_code}
)
else:
return CheckResult(
success=False,
message=f"Unexpected status: {response.status_code} (expected {expected_status})",
response_time=response_time,
details={"status_code": response.status_code}
)
except requests.Timeout:
return CheckResult(
success=False,
message="HTTP timeout",
response_time=None
)
except requests.RequestException as e:
return CheckResult(
success=False,
message=f"HTTP error: {e}",
response_time=None
)

53
checkers/ping.py Normal file
View File

@@ -0,0 +1,53 @@
import subprocess
import time
import platform
from .base import BaseChecker, CheckResult
class PingChecker(BaseChecker):
def check(self) -> CheckResult:
host = self.config["host"]
count = self.config.get("count", 1)
timeout = self.config.get("timeout", 5)
# Adapt ping command for OS
if platform.system().lower() == "windows":
cmd = ["ping", "-n", str(count), "-w", str(timeout * 1000), host]
else:
cmd = ["ping", "-c", str(count), "-W", str(timeout), host]
start = time.time()
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=timeout + 5
)
response_time = (time.time() - start) * 1000 # ms
if result.returncode == 0:
return CheckResult(
success=True,
message="Host is reachable",
response_time=response_time
)
else:
return CheckResult(
success=False,
message="Host is unreachable",
response_time=None
)
except subprocess.TimeoutExpired:
return CheckResult(
success=False,
message="Ping timeout",
response_time=None
)
except Exception as e:
return CheckResult(
success=False,
message=f"Ping error: {e}",
response_time=None
)

62
checkers/snmp.py Normal file
View File

@@ -0,0 +1,62 @@
import time
from pysnmp.hlapi import (
SnmpEngine,
CommunityData,
UdpTransportTarget,
ContextData,
ObjectType,
ObjectIdentity,
getCmd,
)
from .base import BaseChecker, CheckResult
class SnmpChecker(BaseChecker):
def check(self) -> CheckResult:
host = self.config["host"]
port = self.config.get("port", 161)
community = self.config.get("community", "public")
oid = self.config.get("oid", "1.3.6.1.2.1.1.1.0") # sysDescr
timeout_val = self.config.get("timeout", 5)
start = time.time()
try:
iterator = getCmd(
SnmpEngine(),
CommunityData(community),
UdpTransportTarget((host, port), timeout=timeout_val, retries=1),
ContextData(),
ObjectType(ObjectIdentity(oid))
)
error_indication, error_status, error_index, var_binds = next(iterator)
response_time = (time.time() - start) * 1000 # ms
if error_indication:
return CheckResult(
success=False,
message=f"SNMP error: {error_indication}",
response_time=None
)
elif error_status:
return CheckResult(
success=False,
message=f"SNMP error: {error_status.prettyPrint()}",
response_time=None
)
else:
values = {str(oid): str(val) for oid, val in var_binds}
return CheckResult(
success=True,
message="SNMP response OK",
response_time=response_time,
details=values
)
except Exception as e:
return CheckResult(
success=False,
message=f"SNMP error: {e}",
response_time=None
)