The MQTT publisher now tracks connection state, publish counters, discovery and availability timestamps, and the last error, exposed at /api/mqtt and on the dashboard as an MQTT card and a Home Assistant section. Connection failures, disconnects, and dropped publishes are logged; paho's on_connect_fail callback was not registered before, so failed attempts were silent. MQTT connect, disconnect, and unreachable events go to the timeline. When a connection attempt fails the service runs a probe from inside the container: system resolver, each nameserver from resolv.conf queried directly, /etc/hosts, and a TCP connect to every address found. The result is shown as a summary and raw JSON on the dashboard and can be rerun via POST /api/mqtt/probe. The image base moves from Alpine to Debian slim. musl queries all nameservers in parallel and accepts the first reply, so a fast public NXDOMAIN beats a slower local server that knows the name. glibc asks the nameservers in order. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
103 lines
2.8 KiB
Python
103 lines
2.8 KiB
Python
import logging
|
|
import time
|
|
|
|
from flask import Flask, jsonify, render_template, request
|
|
|
|
from config import Config
|
|
from monitors import State, build_monitors
|
|
from storage import Storage
|
|
|
|
cfg = Config()
|
|
logging.basicConfig(level=cfg.log_level, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
|
|
log = logging.getLogger("app")
|
|
|
|
storage = Storage(cfg.db_path)
|
|
state = State()
|
|
publisher = None
|
|
app = Flask(__name__)
|
|
|
|
WIFI_KEYS = ("wifi_interface", "wifi_interfaces", "wifi_status", "wifi_error",
|
|
"wifi_link", "wifi_link_checked_at", "wifi_link_error",
|
|
"wifi_scan", "wifi_scan_error")
|
|
|
|
|
|
@app.route('/alive', methods=['GET'])
|
|
def get_healthcheck():
|
|
response = {
|
|
"alive": True
|
|
}
|
|
return jsonify(response)
|
|
|
|
|
|
@app.route('/')
|
|
def index():
|
|
return render_template("index.html")
|
|
|
|
|
|
@app.route('/api/status')
|
|
def api_status():
|
|
snapshot = state.snapshot()
|
|
snapshot.pop("wifi_scan", None)
|
|
snapshot["now"] = time.time()
|
|
snapshot["config"] = cfg.public()
|
|
return jsonify(snapshot)
|
|
|
|
|
|
@app.route('/api/wifi')
|
|
def api_wifi():
|
|
snapshot = state.snapshot()
|
|
result = {key: snapshot.get(key) for key in WIFI_KEYS}
|
|
result["now"] = time.time()
|
|
result["scan_interval"] = cfg.wifi_scan_interval
|
|
return jsonify(result)
|
|
|
|
|
|
@app.route('/api/mqtt')
|
|
def api_mqtt():
|
|
result = publisher.diagnostics() if publisher else {"enabled": False}
|
|
result["now"] = time.time()
|
|
return jsonify(result)
|
|
|
|
|
|
@app.route('/api/mqtt/probe', methods=['POST'])
|
|
def api_mqtt_probe():
|
|
if not publisher:
|
|
return jsonify({"error": "MQTT is not configured"}), 404
|
|
return jsonify(publisher.run_probe())
|
|
|
|
|
|
@app.route('/api/events')
|
|
def api_events():
|
|
limit = min(max(request.args.get("limit", 200, type=int), 1), 1000)
|
|
return jsonify(storage.events(limit))
|
|
|
|
|
|
@app.route('/api/samples')
|
|
def api_samples():
|
|
prefix = request.args.get("prefix", "")
|
|
span = min(max(request.args.get("range", 3600, type=int), 60), cfg.sample_retention_days * 86400)
|
|
bucket = max(10, span // 400)
|
|
since = time.time() - span
|
|
return jsonify({"since": since, "bucket": bucket, "series": storage.samples_by_prefix(prefix, since, bucket)})
|
|
|
|
|
|
def start_background():
|
|
global publisher
|
|
storage.add_event("service_started", "Health check service started")
|
|
storage.prune_samples(cfg.sample_retention_days)
|
|
|
|
if cfg.mqtt_host:
|
|
from hamqtt import HomeAssistantPublisher
|
|
publisher = HomeAssistantPublisher(cfg, storage)
|
|
state.on_change(publisher.publish_state)
|
|
publisher.start()
|
|
|
|
for monitor in build_monitors(cfg, state, storage):
|
|
monitor.start()
|
|
log.info("started monitor %s every %ss", monitor.name, monitor.interval)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
start_background()
|
|
app.run(host="0.0.0.0", port=cfg.port)
|