health event instead of hw

This commit is contained in:
Maxim Devaev 2025-02-07 01:10:57 +02:00
parent 54f6d93f63
commit 84ec99b332
6 changed files with 133 additions and 117 deletions

View File

@ -57,7 +57,7 @@ class ExportApi:
async def __get_prometheus_metrics(self) -> str: async def __get_prometheus_metrics(self) -> str:
(atx_state, info_state, gpio_state) = await asyncio.gather(*[ (atx_state, info_state, gpio_state) = await asyncio.gather(*[
self.__atx.get_state(), self.__atx.get_state(),
self.__info_manager.get_state(["hw", "fan"]), self.__info_manager.get_state(["health", "fan"]),
self.__user_gpio.get_state(), self.__user_gpio.get_state(),
]) ])
rows: list[str] = [] rows: list[str] = []
@ -71,7 +71,7 @@ class ExportApi:
for key in ["online", "state"]: for key in ["online", "state"]:
self.__append_prometheus_rows(rows, ch_state["state"], f"pikvm_gpio_{mode}_{key}_{channel}") self.__append_prometheus_rows(rows, ch_state["state"], f"pikvm_gpio_{mode}_{key}_{channel}")
self.__append_prometheus_rows(rows, info_state["hw"]["health"], "pikvm_hw") # type: ignore self.__append_prometheus_rows(rows, info_state["health"], "pikvm_hw") # type: ignore
self.__append_prometheus_rows(rows, info_state["fan"], "pikvm_fan") self.__append_prometheus_rows(rows, info_state["fan"], "pikvm_fan")
return "\n".join(rows) return "\n".join(rows)

View File

@ -45,7 +45,10 @@ class InfoApi:
def __valid_info_fields(self, req: Request) -> list[str]: def __valid_info_fields(self, req: Request) -> list[str]:
available = self.__info_manager.get_subs() available = self.__info_manager.get_subs()
available.add("hw")
default = set(available)
default.remove("health")
return sorted(valid_info_fields( return sorted(valid_info_fields(
arg=req.query.get("fields", ",".join(available)), arg=req.query.get("fields", ",".join(default)),
variants=available, variants=(available),
) or available) ) or available)

View File

@ -31,7 +31,7 @@ from .auth import AuthInfoSubmanager
from .system import SystemInfoSubmanager from .system import SystemInfoSubmanager
from .meta import MetaInfoSubmanager from .meta import MetaInfoSubmanager
from .extras import ExtrasInfoSubmanager from .extras import ExtrasInfoSubmanager
from .hw import HwInfoSubmanager from .health import HealthInfoSubmanager
from .fan import FanInfoSubmanager from .fan import FanInfoSubmanager
@ -39,11 +39,11 @@ from .fan import FanInfoSubmanager
class InfoManager: class InfoManager:
def __init__(self, config: Section) -> None: def __init__(self, config: Section) -> None:
self.__subs: dict[str, BaseInfoSubmanager] = { self.__subs: dict[str, BaseInfoSubmanager] = {
"system": SystemInfoSubmanager(config.kvmd.streamer.cmd), "system": SystemInfoSubmanager(config.kvmd.info.hw.platform, config.kvmd.streamer.cmd),
"auth": AuthInfoSubmanager(config.kvmd.auth.enabled), "auth": AuthInfoSubmanager(config.kvmd.auth.enabled),
"meta": MetaInfoSubmanager(config.kvmd.info.meta), "meta": MetaInfoSubmanager(config.kvmd.info.meta),
"extras": ExtrasInfoSubmanager(config), "extras": ExtrasInfoSubmanager(config),
"hw": HwInfoSubmanager(**config.kvmd.info.hw._unpack()), "health": HealthInfoSubmanager(**config.kvmd.info.hw._unpack(ignore="platform")),
"fan": FanInfoSubmanager(**config.kvmd.info.fan._unpack()), "fan": FanInfoSubmanager(**config.kvmd.info.fan._unpack()),
} }
self.__queue: "asyncio.Queue[tuple[str, (dict | None)]]" = asyncio.Queue() self.__queue: "asyncio.Queue[tuple[str, (dict | None)]]" = asyncio.Queue()
@ -52,12 +52,29 @@ class InfoManager:
return set(self.__subs) return set(self.__subs)
async def get_state(self, fields: (list[str] | None)=None) -> dict: async def get_state(self, fields: (list[str] | None)=None) -> dict:
fields = (fields or list(self.__subs)) fields = set(fields or list(self.__subs))
return dict(zip(fields, await asyncio.gather(*[
hw = ("hw" in fields) # Old for compatible
system = ("system" in fields)
if hw:
fields.remove("hw")
fields.add("health")
fields.add("system")
state = dict(zip(fields, await asyncio.gather(*[
self.__subs[field].get_state() self.__subs[field].get_state()
for field in fields for field in fields
]))) ])))
if hw:
state["hw"] = {
"health": state.pop("health"),
"platform": state["system"].pop("platform"),
}
if not system:
state.pop("system")
return state
async def trigger_state(self) -> None: async def trigger_state(self) -> None:
await asyncio.gather(*[ await asyncio.gather(*[
sub.trigger_state() sub.trigger_state()
@ -70,7 +87,7 @@ class InfoManager:
# - auth -- Partial # - auth -- Partial
# - meta -- Partial, nullable # - meta -- Partial, nullable
# - extras -- Partial, nullable # - extras -- Partial, nullable
# - hw -- Partial # - health -- Partial
# - fan -- Partial # - fan -- Partial
# =========================== # ===========================

View File

@ -20,7 +20,6 @@
# ========================================================================== # # ========================================================================== #
import os
import asyncio import asyncio
import copy import copy
@ -45,59 +44,41 @@ _RetvalT = TypeVar("_RetvalT")
# ===== # =====
class HwInfoSubmanager(BaseInfoSubmanager): class HealthInfoSubmanager(BaseInfoSubmanager):
def __init__( def __init__(
self, self,
platform_path: str,
vcgencmd_cmd: list[str], vcgencmd_cmd: list[str],
ignore_past: bool, ignore_past: bool,
state_poll: float, state_poll: float,
) -> None: ) -> None:
self.__platform_path = platform_path
self.__vcgencmd_cmd = vcgencmd_cmd self.__vcgencmd_cmd = vcgencmd_cmd
self.__ignore_past = ignore_past self.__ignore_past = ignore_past
self.__state_poll = state_poll self.__state_poll = state_poll
self.__dt_cache: dict[str, str] = {}
self.__notifier = aiotools.AioNotifier() self.__notifier = aiotools.AioNotifier()
async def get_state(self) -> dict: async def get_state(self) -> dict:
( (
base,
serial,
platform,
throttling, throttling,
cpu_percent, cpu_percent,
cpu_temp, cpu_temp,
mem, mem,
) = await asyncio.gather( ) = await asyncio.gather(
self.__read_dt_file("model", upper=False),
self.__read_dt_file("serial-number", upper=True),
self.__read_platform_file(),
self.__get_throttling(), self.__get_throttling(),
self.__get_cpu_percent(), self.__get_cpu_percent(),
self.__get_cpu_temp(), self.__get_cpu_temp(),
self.__get_mem(), self.__get_mem(),
) )
return { return {
"platform": { "temp": {
"type": "rpi", "cpu": cpu_temp,
"base": base,
"serial": serial,
**platform, # type: ignore
}, },
"health": { "cpu": {
"temp": { "percent": cpu_percent,
"cpu": cpu_temp,
},
"cpu": {
"percent": cpu_percent,
},
"mem": mem,
"throttling": throttling,
}, },
"mem": mem,
"throttling": throttling,
} }
async def trigger_state(self) -> None: async def trigger_state(self) -> None:
@ -115,35 +96,6 @@ class HwInfoSubmanager(BaseInfoSubmanager):
# ===== # =====
async def __read_dt_file(self, name: str, upper: bool) -> (str | None):
if name not in self.__dt_cache:
path = os.path.join(f"{env.PROCFS_PREFIX}/proc/device-tree", name)
try:
value = (await aiotools.read_file(path)).strip(" \t\r\n\0")
self.__dt_cache[name] = (value.upper() if upper else value)
except Exception as ex:
get_logger(0).error("Can't read DT %s from %s: %s", name, path, ex)
return None
return self.__dt_cache[name]
async def __read_platform_file(self) -> dict:
try:
text = await aiotools.read_file(self.__platform_path)
parsed: dict[str, str] = {}
for row in text.split("\n"):
row = row.strip()
if row:
(key, value) = row.split("=", 1)
parsed[key.strip()] = value.strip()
return {
"model": parsed["PIKVM_MODEL"],
"video": parsed["PIKVM_VIDEO"],
"board": parsed["PIKVM_BOARD"],
}
except Exception:
get_logger(0).exception("Can't read device model")
return {"model": None, "video": None, "board": None}
async def __get_cpu_temp(self) -> (float | None): async def __get_cpu_temp(self) -> (float | None):
temp_path = f"{env.SYSFS_PREFIX}/sys/class/thermal/thermal_zone0/temp" temp_path = f"{env.SYSFS_PREFIX}/sys/class/thermal/thermal_zone0/temp"
try: try:

View File

@ -28,6 +28,7 @@ from typing import AsyncGenerator
from ....logging import get_logger from ....logging import get_logger
from .... import env
from .... import aiotools from .... import aiotools
from .... import aioproc from .... import aioproc
@ -38,12 +39,30 @@ from .base import BaseInfoSubmanager
# ===== # =====
class SystemInfoSubmanager(BaseInfoSubmanager): class SystemInfoSubmanager(BaseInfoSubmanager):
def __init__(self, streamer_cmd: list[str]) -> None: def __init__(
self,
platform_path: str,
streamer_cmd: list[str],
) -> None:
self.__platform_path = platform_path
self.__streamer_cmd = streamer_cmd self.__streamer_cmd = streamer_cmd
self.__dt_cache: dict[str, str] = {}
self.__notifier = aiotools.AioNotifier() self.__notifier = aiotools.AioNotifier()
async def get_state(self) -> dict: async def get_state(self) -> dict:
streamer_info = await self.__get_streamer_info() (
base,
serial,
pl,
streamer_info,
) = await asyncio.gather(
self.__read_dt_file("model", upper=False),
self.__read_dt_file("serial-number", upper=True),
self.__read_platform_file(),
self.__get_streamer_info(),
)
uname_info = platform.uname() # Uname using the internal cache uname_info = platform.uname() # Uname using the internal cache
return { return {
"kvmd": {"version": __version__}, "kvmd": {"version": __version__},
@ -52,6 +71,12 @@ class SystemInfoSubmanager(BaseInfoSubmanager):
field: getattr(uname_info, field) field: getattr(uname_info, field)
for field in ["system", "release", "version", "machine"] for field in ["system", "release", "version", "machine"]
}, },
"platform": {
"type": "rpi",
"base": base,
"serial": serial,
**pl, # type: ignore
},
} }
async def trigger_state(self) -> None: async def trigger_state(self) -> None:
@ -64,6 +89,35 @@ class SystemInfoSubmanager(BaseInfoSubmanager):
# ===== # =====
async def __read_dt_file(self, name: str, upper: bool) -> (str | None):
if name not in self.__dt_cache:
path = os.path.join(f"{env.PROCFS_PREFIX}/proc/device-tree", name)
try:
value = (await aiotools.read_file(path)).strip(" \t\r\n\0")
self.__dt_cache[name] = (value.upper() if upper else value)
except Exception as ex:
get_logger(0).error("Can't read DT %s from %s: %s", name, path, ex)
return None
return self.__dt_cache[name]
async def __read_platform_file(self) -> dict:
try:
text = await aiotools.read_file(self.__platform_path)
parsed: dict[str, str] = {}
for row in text.split("\n"):
row = row.strip()
if row:
(key, value) = row.split("=", 1)
parsed[key.strip()] = value.strip()
return {
"model": parsed["PIKVM_MODEL"],
"video": parsed["PIKVM_VIDEO"],
"board": parsed["PIKVM_BOARD"],
}
except Exception:
get_logger(0).exception("Can't read device model")
return {"model": None, "video": None, "board": None}
async def __get_streamer_info(self) -> dict: async def __get_streamer_info(self) -> dict:
version = "" version = ""
features: dict[str, bool] = {} features: dict[str, bool] = {}

View File

@ -58,7 +58,7 @@ export function Session() {
var __ocr = new Ocr(__streamer.getGeometry); var __ocr = new Ocr(__streamer.getGeometry);
var __switch = new Switch(); var __switch = new Switch();
var __info_hw_state = null; var __info_health_state = null;
var __info_fan_state = null; var __info_fan_state = null;
var __init__ = function() { var __init__ = function() {
@ -71,7 +71,7 @@ export function Session() {
for (let key of Object.keys(state)) { for (let key of Object.keys(state)) {
switch (key) { switch (key) {
case "meta": __setInfoStateMeta(state.meta); break; case "meta": __setInfoStateMeta(state.meta); break;
case "hw": __setInfoStateHw(state.hw); break; case "health": __setInfoStateHealth(state.health); break;
case "fan": __setInfoStateFan(state.fan); break; case "fan": __setInfoStateFan(state.fan); break;
case "system": __setInfoStateSystem(state.system); break; case "system": __setInfoStateSystem(state.system); break;
case "extras": __setInfoStateExtras(state.extras); break; case "extras": __setInfoStateExtras(state.extras); break;
@ -91,11 +91,10 @@ export function Session() {
document.title = "PiKVM Session"; document.title = "PiKVM Session";
} }
if (state.tips && state.tips.left) { for (let place of ["left", "right"]) {
$("kvmd-meta-tips-left").innerText = `${state.tips.left}`; if (state.tips && state.tips[place]) {
} $(`kvmd-meta-tips-${place}`).innerText = state.tips[place];
if (state.tips && state.tips.right) { }
$("kvmd-meta-tips-right").innerText = `${state.tips.right}`;
} }
// Don't use this option, it may be removed in any time // Don't use this option, it may be removed in any time
@ -105,10 +104,10 @@ export function Session() {
} }
}; };
var __setInfoStateHw = function(state) { var __setInfoStateHealth = function(state) {
if (state.health.throttling !== null) { if (state.throttling !== null) {
let flags = state.health.throttling.parsed_flags; let flags = state.throttling.parsed_flags;
let ignore_past = state.health.throttling.ignore_past; let ignore_past = state.throttling.ignore_past;
let undervoltage = (flags.undervoltage.now || (flags.undervoltage.past && !ignore_past)); let undervoltage = (flags.undervoltage.now || (flags.undervoltage.past && !ignore_past));
let freq_capped = (flags.freq_capped.now || (flags.freq_capped.past && !ignore_past)); let freq_capped = (flags.freq_capped.now || (flags.freq_capped.past && !ignore_past));
@ -118,7 +117,7 @@ export function Session() {
tools.hidden.setVisible($("hw-health-message-undervoltage"), undervoltage); tools.hidden.setVisible($("hw-health-message-undervoltage"), undervoltage);
tools.hidden.setVisible($("hw-health-message-overheating"), freq_capped); tools.hidden.setVisible($("hw-health-message-overheating"), freq_capped);
} }
__info_hw_state = state; __info_health_state = state;
__renderAboutInfoHardware(); __renderAboutInfoHardware();
}; };
@ -145,37 +144,24 @@ export function Session() {
}; };
var __renderAboutInfoHardware = function() { var __renderAboutInfoHardware = function() {
let html = ""; let parts = [];
if (__info_hw_state !== null) { if (__info_health_state !== null) {
html += ` parts = [
Platform: "Resources:" + __formatMisc(__info_health_state),
${__formatMisc(__info_hw_state)} "Temperature:" + __formatTemp(__info_health_state.temp),
<hr> "Throttling:" + __formatThrottling(__info_health_state.throttling),
Temperature: ];
${__formatTemp(__info_hw_state.health.temp)}
<hr>
Throttling:
${__formatThrottling(__info_hw_state.health.throttling)}
`;
} }
if (__info_fan_state !== null) { if (__info_fan_state !== null) {
if (html.length > 0) { parts.push("Fan:" + __formatFan(__info_fan_state));
html += "<hr>";
}
html += `
Fan:
${__formatFan(__info_fan_state)}
`;
} }
$("about-hardware").innerHTML = html; $("about-hardware").innerHTML = parts.join("<hr>");
}; };
var __formatMisc = function(state) { var __formatMisc = function(state) {
return __formatUl([ return __formatUl([
["Base", state.platform.base], ["CPU", `${state.cpu.percent}%`],
["Serial", state.platform.serial], ["MEM", `${state.mem.percent}%`],
["CPU", `${state.health.cpu.percent}%`],
["MEM", `${state.health.mem.percent}%`],
]); ]);
}; };
@ -183,16 +169,16 @@ export function Session() {
if (!state.monitored) { if (!state.monitored) {
return __formatUl([["Status", "Not monitored"]]); return __formatUl([["Status", "Not monitored"]]);
} else if (state.state === null) { } else if (state.state === null) {
return __formatUl([["Status", __colored("red", "Not available")]]); return __formatUl([["Status", __red("Not available")]]);
} else { } else {
state = state.state; state = state.state;
let pairs = [ let pairs = [
["Status", (state.fan.ok ? __colored("green", "Ok") : __colored("red", "Failed"))], ["Status", (state.fan.ok ? __green("Ok") : __red("Failed"))],
["Desired speed", `${state.fan.speed}%`], ["Desired speed", `${state.fan.speed}%`],
["PWM", `${state.fan.pwm}`], ["PWM", `${state.fan.pwm}`],
]; ];
if (state.hall.available) { if (state.hall.available) {
pairs.push(["RPM", __colored((state.fan.ok ? "green" : "red"), state.hall.rpm)]); pairs.push(["RPM", __colored(state.fan.ok, state.hall.rpm)]);
} }
return __formatUl(pairs); return __formatUl(pairs);
} }
@ -212,9 +198,9 @@ export function Session() {
for (let field of Object.keys(throttling.parsed_flags).sort()) { for (let field of Object.keys(throttling.parsed_flags).sort()) {
let flags = throttling.parsed_flags[field]; let flags = throttling.parsed_flags[field];
let key = tools.upperFirst(field).replace("_", " "); let key = tools.upperFirst(field).replace("_", " ");
let value = (flags["now"] ? __colored("red", "RIGHT NOW") : __colored("green", "No")); let value = (flags["now"] ? __red("RIGHT NOW") : __green("No"));
if (!throttling.ignore_past) { if (!throttling.ignore_past) {
value += "; " + (flags["past"] ? __colored("red", "In the past") : __colored("green", "Never")); value += "; " + (flags["past"] ? __red("In the past") : __green("Never"));
} }
pairs.push([key, value]); pairs.push([key, value]);
} }
@ -224,18 +210,17 @@ export function Session() {
} }
}; };
var __colored = function(color, html) {
return `<font color="${color}">${html}</font>`;
};
var __setInfoStateSystem = function(state) { var __setInfoStateSystem = function(state) {
$("about-version").innerHTML = ` $("about-version").innerHTML = `
KVMD: <span class="code-comment">${state.kvmd.version}</span><br> Base: ${__commented(state.platform.base)}<br>
Serial: ${__commented(state.platform.serial)}<br>
<hr> <hr>
Streamer: <span class="code-comment">${state.streamer.version} (${state.streamer.app})</span> KVMD: ${__commented(state.kvmd.version)}<br>
${__formatStreamerFeatures(state.streamer.features)}
<hr> <hr>
${state.kernel.system} kernel: Streamer: ${__commented(state.streamer.version + " (" + state.streamer.app + ")")}<br>
${__formatStreamerFeatures(state.streamer.features)}<br>
<hr>
${state.kernel.system} kernel:<br>
${__formatUname(state.kernel)} ${__formatUname(state.kernel)}
`; `;
$("kvmd-version-kvmd").innerText = state.kvmd.version; $("kvmd-version-kvmd").innerText = state.kvmd.version;
@ -263,11 +248,16 @@ export function Session() {
var __formatUl = function(pairs) { var __formatUl = function(pairs) {
let html = ""; let html = "";
for (let pair of pairs) { for (let pair of pairs) {
html += `<li>${pair[0]}: <span class="code-comment">${pair[1]}</span></li>`; html += `<li>${pair[0]}: ${__commented(pair[1])}</li>`;
} }
return `<ul>${html}</ul>`; return `<ul>${html}</ul>`;
}; };
var __green = (html) => __colored(true, html);
var __red = (html) => __colored(false, html);
var __colored = (ok, html) => `<font color="${ok ? "green" : "red"}">${html}</font>`;
var __commented = (html) => `<span class="code-comment">${html}</span>`;
var __setInfoStateExtras = function(state) { var __setInfoStateExtras = function(state) {
let show_hook = null; let show_hook = null;
let close_hook = null; let close_hook = null;