Add per-enclosure temperature metrics + Home Assistant MQTT publisher

- enclosure: fetch SES element descriptor page (0x07) and label temp/fan/
  psu/voltage elements with human-readable names
- temps service + /api/temps: per-enclosure hotspot (max across SES sensors
  and housed drive temps) plus named SES sensor list
- mqtt_publisher: paho-mqtt with HA discovery (device per enclosure, hotspot
  + per-sensor entities), LWT availability, opt-in via MQTT_HOST
- secrets: OpenBao KV v2 reader; MQTT creds sourced from secret/home_assistant
  with env fallback
- compose/requirements/README updated
This commit is contained in:
2026-06-16 04:45:32 +00:00
parent 50dd2a631e
commit d41e3838d5
11 changed files with 591 additions and 10 deletions

30
main.py
View File

@@ -10,9 +10,10 @@ from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from models.schemas import HealthCheck
from routers import drives, enclosures, leds, overview
from routers import drives, enclosures, leds, overview, temps
from services.cache import cache_set, close_cache, init_cache, redis_available
from services.enclosure import discover_enclosures, list_slots
from services.mqtt_publisher import MqttPublisher, _PAHO_AVAILABLE, mqtt_enabled
from services.smart import SMART_CACHE_TTL, _run_smartctl, sg_ses_available, smartctl_available
from services.zfs import get_zfs_pool_map
@@ -26,6 +27,8 @@ SMART_POLL_INTERVAL = int(os.environ.get("SMART_POLL_INTERVAL", "90"))
_tool_status: dict[str, bool] = {}
_poll_task: asyncio.Task | None = None
_mqtt_task: asyncio.Task | None = None
_mqtt_publisher: MqttPublisher | None = None
async def smart_poll_loop():
@@ -77,7 +80,7 @@ async def smart_poll_loop():
@asynccontextmanager
async def lifespan(app: FastAPI):
global _poll_task
global _poll_task, _mqtt_task, _mqtt_publisher
# Startup
_tool_status["smartctl"] = smartctl_available()
_tool_status["sg_ses"] = sg_ses_available()
@@ -95,9 +98,31 @@ async def lifespan(app: FastAPI):
if redis_available():
_poll_task = asyncio.create_task(smart_poll_loop())
# Optional MQTT publisher for Home Assistant (opt-in via MQTT_HOST).
_tool_status["mqtt"] = False
if mqtt_enabled():
if not _PAHO_AVAILABLE:
logger.warning("MQTT_HOST set but paho-mqtt not installed — MQTT disabled")
else:
try:
_mqtt_publisher = MqttPublisher()
_mqtt_publisher.start()
_mqtt_task = asyncio.create_task(_mqtt_publisher.run())
_tool_status["mqtt"] = True
except Exception as e:
logger.error("Failed to start MQTT publisher: %s", e)
yield
# Shutdown
if _mqtt_task is not None:
_mqtt_task.cancel()
try:
await _mqtt_task
except asyncio.CancelledError:
pass
if _mqtt_publisher is not None:
_mqtt_publisher.stop()
if _poll_task is not None:
_poll_task.cancel()
try:
@@ -125,6 +150,7 @@ app.include_router(enclosures.router)
app.include_router(drives.router)
app.include_router(leds.router)
app.include_router(overview.router)
app.include_router(temps.router)
@app.get("/api/health", response_model=HealthCheck, tags=["health"])