- 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
85 lines
3.0 KiB
Python
85 lines
3.0 KiB
Python
"""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}
|