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:
84
services/temps.py
Normal file
84
services/temps.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""Per-enclosure temperature aggregation.
|
||||
|
||||
Builds a compact snapshot of enclosure temperatures suitable for Home
|
||||
Assistant: each named SES temperature sensor plus a derived per-enclosure
|
||||
hotspot (the hottest reading across SES sensors *and* the drives housed in
|
||||
that enclosure — drive temps are usually the real hotspot signal).
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import socket
|
||||
|
||||
from services.enclosure import discover_enclosures, get_enclosure_status, list_slots
|
||||
from services.smart import get_smart_data
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_node_id() -> str:
|
||||
"""Stable identifier for this monitor instance (defaults to hostname)."""
|
||||
import os
|
||||
|
||||
return os.environ.get("MQTT_NODE_ID") or socket.gethostname() or "jbod-monitor"
|
||||
|
||||
|
||||
async def build_temp_snapshot() -> dict:
|
||||
"""Collect per-enclosure temperature metrics for all enclosures."""
|
||||
enclosures = discover_enclosures()
|
||||
|
||||
async def _health(enc):
|
||||
if enc.get("sg_device"):
|
||||
return await get_enclosure_status(enc["sg_device"])
|
||||
return None
|
||||
|
||||
health_results = await asyncio.gather(
|
||||
*[_health(enc) for enc in enclosures], return_exceptions=True
|
||||
)
|
||||
|
||||
out_enclosures: list[dict] = []
|
||||
for enc, health in zip(enclosures, health_results):
|
||||
if isinstance(health, Exception):
|
||||
logger.warning("SES health failed for %s: %s", enc["id"], health)
|
||||
health = None
|
||||
|
||||
sensors: list[dict] = []
|
||||
sensor_temps: list[float] = []
|
||||
if isinstance(health, dict):
|
||||
for t in health.get("temps", []):
|
||||
temp = t.get("temperature_c")
|
||||
sensors.append({
|
||||
"index": t["index"],
|
||||
"name": t.get("name"),
|
||||
"status": t.get("status", "Unknown"),
|
||||
"temperature_c": temp,
|
||||
})
|
||||
if isinstance(temp, (int, float)) and temp > 0:
|
||||
sensor_temps.append(float(temp))
|
||||
|
||||
# Drive temperatures for drives housed in this enclosure.
|
||||
drive_temps: list[float] = []
|
||||
devices = [s["device"] for s in list_slots(enc["id"]) if s.get("device")]
|
||||
if devices:
|
||||
smart_results = await asyncio.gather(
|
||||
*[get_smart_data(d) for d in devices], return_exceptions=True
|
||||
)
|
||||
for res in smart_results:
|
||||
if isinstance(res, Exception):
|
||||
continue
|
||||
data, _hit = res
|
||||
temp = data.get("temperature_c")
|
||||
if isinstance(temp, (int, float)) and temp > 0:
|
||||
drive_temps.append(float(temp))
|
||||
|
||||
all_temps = sensor_temps + drive_temps
|
||||
out_enclosures.append({
|
||||
"id": enc["id"],
|
||||
"vendor": enc.get("vendor", ""),
|
||||
"model": enc.get("model", ""),
|
||||
"hotspot_c": max(all_temps) if all_temps else None,
|
||||
"drive_max_c": max(drive_temps) if drive_temps else None,
|
||||
"drive_temp_count": len(drive_temps),
|
||||
"sensors": sensors,
|
||||
})
|
||||
|
||||
return {"node": get_node_id(), "enclosures": out_enclosures}
|
||||
Reference in New Issue
Block a user