Files
apcupsd-client/app/apc_cli.py
T
2026-04-04 13:53:57 -04:00

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