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:
@@ -152,29 +152,88 @@ def _parse_slot_number(entry: Path) -> int | None:
|
||||
return None
|
||||
|
||||
|
||||
async def get_enclosure_status(sg_device: str) -> dict | None:
|
||||
"""Run sg_ses --page=0x02 and parse enclosure health data."""
|
||||
async def _run_sg_ses(sg_device: str, page: str) -> str | None:
|
||||
"""Run sg_ses for a given page, returning decoded stdout or None."""
|
||||
try:
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
"sg_ses", "--page=0x02", sg_device,
|
||||
"sg_ses", f"--page={page}", sg_device,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
stdout, stderr = await proc.communicate()
|
||||
if proc.returncode != 0:
|
||||
logger.warning("sg_ses failed for %s: %s", sg_device, stderr.decode().strip())
|
||||
logger.warning(
|
||||
"sg_ses page %s failed for %s: %s",
|
||||
page, sg_device, stderr.decode().strip(),
|
||||
)
|
||||
return None
|
||||
return _parse_ses_page02(stdout.decode(errors="replace"))
|
||||
return stdout.decode(errors="replace")
|
||||
except FileNotFoundError:
|
||||
logger.warning("sg_ses not found")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.warning("sg_ses error for %s: %s", sg_device, e)
|
||||
logger.warning("sg_ses page %s error for %s: %s", page, sg_device, e)
|
||||
return None
|
||||
|
||||
|
||||
def _parse_ses_page02(text: str) -> dict:
|
||||
"""Parse sg_ses --page=0x02 text output into structured health data."""
|
||||
async def get_enclosure_status(sg_device: str) -> dict | None:
|
||||
"""Run sg_ses status (0x02) + element descriptor (0x07) pages and parse.
|
||||
|
||||
The element descriptor page provides human-readable names for each
|
||||
element (e.g. "Temp Inlet", "Fan 1"); these are merged into the status
|
||||
elements so callers get labelled sensors. Descriptor lookup is
|
||||
best-effort — if page 0x07 is unsupported, elements fall back to bare
|
||||
indices.
|
||||
"""
|
||||
status_text, desc_text = await asyncio.gather(
|
||||
_run_sg_ses(sg_device, "0x02"),
|
||||
_run_sg_ses(sg_device, "0x07"),
|
||||
)
|
||||
if status_text is None:
|
||||
return None
|
||||
names = _parse_element_descriptors(desc_text) if desc_text else {}
|
||||
return _parse_ses_page02(status_text, names)
|
||||
|
||||
|
||||
def _norm_type(element_type: str) -> str:
|
||||
"""Normalize an SES element type line to a stable key.
|
||||
|
||||
sg_ses appends qualifiers like ", subenclosure id: 0 [ti=2]" to the
|
||||
type line; strip those so the same element type matches across the
|
||||
status (0x02) and element descriptor (0x07) pages.
|
||||
"""
|
||||
return element_type.split(",")[0].strip().lower()
|
||||
|
||||
|
||||
def _parse_element_descriptors(text: str) -> dict[tuple[str, int], str]:
|
||||
"""Parse sg_ses --page=0x07 into {(element_type, index): name}."""
|
||||
names: dict[tuple[str, int], str] = {}
|
||||
sections = re.split(r"(?=\s*Element type:)", text)
|
||||
for section in sections:
|
||||
type_match = re.match(r"\s*Element type:\s*(.+)", section)
|
||||
if not type_match:
|
||||
continue
|
||||
etype = _norm_type(type_match.group(1).strip().rstrip(","))
|
||||
for line in section.splitlines():
|
||||
m = re.match(r"\s*Element (\d+) descriptor:\s*(.*)", line)
|
||||
if not m:
|
||||
continue
|
||||
idx = int(m.group(1))
|
||||
name = m.group(2).strip()
|
||||
if name:
|
||||
names[(etype, idx)] = name
|
||||
return names
|
||||
|
||||
|
||||
def _parse_ses_page02(
|
||||
text: str, names: dict[tuple[str, int], str] | None = None
|
||||
) -> dict:
|
||||
"""Parse sg_ses --page=0x02 text output into structured health data.
|
||||
|
||||
``names`` maps (normalized element type, index) -> descriptor name from
|
||||
the element descriptor page (0x07); when present it labels each element.
|
||||
"""
|
||||
names = names or {}
|
||||
result = {
|
||||
"overall_status": "OK",
|
||||
"psus": [],
|
||||
@@ -192,6 +251,7 @@ def _parse_ses_page02(text: str) -> dict:
|
||||
if not type_match:
|
||||
continue
|
||||
element_type = type_match.group(1).strip().rstrip(",").lower()
|
||||
etype_key = _norm_type(element_type)
|
||||
|
||||
# Find individual element blocks (skip "Overall descriptor")
|
||||
elements = re.split(r"(?=\s*Element \d+ descriptor:)", section)
|
||||
@@ -209,12 +269,15 @@ def _parse_ses_page02(text: str) -> dict:
|
||||
if status.lower() == "not installed":
|
||||
continue
|
||||
|
||||
name = names.get((etype_key, idx))
|
||||
|
||||
if "power supply" in element_type:
|
||||
fail = "Fail=1" in elem_text
|
||||
ac_fail = "AC fail=1" in elem_text
|
||||
dc_fail = "DC fail=1" in elem_text
|
||||
result["psus"].append({
|
||||
"index": idx,
|
||||
"name": name,
|
||||
"status": status,
|
||||
"fail": fail,
|
||||
"ac_fail": ac_fail,
|
||||
@@ -227,6 +290,7 @@ def _parse_ses_page02(text: str) -> dict:
|
||||
rpm = int(rpm_match.group(1)) if rpm_match else None
|
||||
result["fans"].append({
|
||||
"index": idx,
|
||||
"name": name,
|
||||
"status": status,
|
||||
"rpm": rpm,
|
||||
"fail": fail,
|
||||
@@ -243,6 +307,7 @@ def _parse_ses_page02(text: str) -> dict:
|
||||
continue
|
||||
result["temps"].append({
|
||||
"index": idx,
|
||||
"name": name,
|
||||
"status": status,
|
||||
"temperature_c": temp,
|
||||
})
|
||||
@@ -254,6 +319,7 @@ def _parse_ses_page02(text: str) -> dict:
|
||||
voltage = float(volt_match.group(1)) if volt_match else None
|
||||
result["voltages"].append({
|
||||
"index": idx,
|
||||
"name": name,
|
||||
"status": status,
|
||||
"voltage": voltage,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user