64 lines
1.8 KiB
Python
64 lines
1.8 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import logging
|
|
import shutil
|
|
from typing import Any
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
APCACCESS_BIN = shutil.which('apcaccess') or 'apcaccess'
|
|
APCACCESS_TIMEOUT_SECONDS = 10.0
|
|
|
|
|
|
class APCStatusError(Exception):
|
|
pass
|
|
|
|
|
|
async def fetch_status(host: str, port: int) -> dict[str, Any]:
|
|
"""Invoke apcaccess CLI and parse key:value lines into a dict.
|
|
|
|
Raises APCStatusError if binary fails, times out, or returns no data.
|
|
"""
|
|
proc = await asyncio.create_subprocess_exec(
|
|
APCACCESS_BIN,
|
|
'-h', f'{host}:{port}',
|
|
'status',
|
|
stdout=asyncio.subprocess.PIPE,
|
|
stderr=asyncio.subprocess.PIPE,
|
|
)
|
|
try:
|
|
stdout, stderr = await asyncio.wait_for(
|
|
proc.communicate(), timeout=APCACCESS_TIMEOUT_SECONDS
|
|
)
|
|
except TimeoutError:
|
|
try:
|
|
proc.kill()
|
|
await proc.wait()
|
|
except ProcessLookupError:
|
|
pass
|
|
raise APCStatusError(
|
|
f"apcaccess timed out after {APCACCESS_TIMEOUT_SECONDS}s for {host}:{port}"
|
|
)
|
|
if proc.returncode != 0:
|
|
err_txt = stderr.decode(errors="replace").strip()
|
|
raise APCStatusError(
|
|
f'apcaccess exit {proc.returncode}: {err_txt}'
|
|
)
|
|
text = stdout.decode(errors='replace')
|
|
data: dict[str, Any] = {}
|
|
for line in text.splitlines():
|
|
if ':' not in line:
|
|
continue
|
|
k, v = line.split(':', 1)
|
|
key = k.strip()
|
|
val = v.strip()
|
|
data[key] = val
|
|
if not data:
|
|
raise APCStatusError("apcaccess returned no data")
|
|
if 'UPSNAME' not in data and 'NAME' in data:
|
|
data['UPSNAME'] = data['NAME']
|
|
if 'MODEL' in data:
|
|
data['MODEL_NAME'] = data['MODEL']
|
|
return data
|