diff --git a/kvmd/apps/kvmd/api/export.py b/kvmd/apps/kvmd/api/export.py index fd672f7b..1c964b8a 100644 --- a/kvmd/apps/kvmd/api/export.py +++ b/kvmd/apps/kvmd/api/export.py @@ -57,7 +57,7 @@ class ExportApi: async def __get_prometheus_metrics(self) -> str: (atx_state, info_state, gpio_state) = await asyncio.gather(*[ self.__atx.get_state(), - self.__info_manager.get_state(["hw", "fan"]), + self.__info_manager.get_state(["health", "fan"]), self.__user_gpio.get_state(), ]) rows: list[str] = [] @@ -71,7 +71,7 @@ class ExportApi: for key in ["online", "state"]: 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") return "\n".join(rows) diff --git a/kvmd/apps/kvmd/api/info.py b/kvmd/apps/kvmd/api/info.py index 89d45a84..8116a3fa 100644 --- a/kvmd/apps/kvmd/api/info.py +++ b/kvmd/apps/kvmd/api/info.py @@ -45,7 +45,10 @@ class InfoApi: def __valid_info_fields(self, req: Request) -> list[str]: available = self.__info_manager.get_subs() + available.add("hw") + default = set(available) + default.remove("health") return sorted(valid_info_fields( - arg=req.query.get("fields", ",".join(available)), - variants=available, + arg=req.query.get("fields", ",".join(default)), + variants=(available), ) or available) diff --git a/kvmd/apps/kvmd/info/__init__.py b/kvmd/apps/kvmd/info/__init__.py index 9ede5489..99befdfd 100644 --- a/kvmd/apps/kvmd/info/__init__.py +++ b/kvmd/apps/kvmd/info/__init__.py @@ -31,7 +31,7 @@ from .auth import AuthInfoSubmanager from .system import SystemInfoSubmanager from .meta import MetaInfoSubmanager from .extras import ExtrasInfoSubmanager -from .hw import HwInfoSubmanager +from .health import HealthInfoSubmanager from .fan import FanInfoSubmanager @@ -39,11 +39,11 @@ from .fan import FanInfoSubmanager class InfoManager: def __init__(self, config: Section) -> None: 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), "meta": MetaInfoSubmanager(config.kvmd.info.meta), "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()), } self.__queue: "asyncio.Queue[tuple[str, (dict | None)]]" = asyncio.Queue() @@ -52,12 +52,29 @@ class InfoManager: return set(self.__subs) async def get_state(self, fields: (list[str] | None)=None) -> dict: - fields = (fields or list(self.__subs)) - return dict(zip(fields, await asyncio.gather(*[ + fields = set(fields or list(self.__subs)) + + 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() 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: await asyncio.gather(*[ sub.trigger_state() @@ -70,7 +87,7 @@ class InfoManager: # - auth -- Partial # - meta -- Partial, nullable # - extras -- Partial, nullable - # - hw -- Partial + # - health -- Partial # - fan -- Partial # =========================== diff --git a/kvmd/apps/kvmd/info/hw.py b/kvmd/apps/kvmd/info/health.py similarity index 75% rename from kvmd/apps/kvmd/info/hw.py rename to kvmd/apps/kvmd/info/health.py index 81cd1af6..db38409a 100644 --- a/kvmd/apps/kvmd/info/hw.py +++ b/kvmd/apps/kvmd/info/health.py @@ -20,7 +20,6 @@ # ========================================================================== # -import os import asyncio import copy @@ -45,59 +44,41 @@ _RetvalT = TypeVar("_RetvalT") # ===== -class HwInfoSubmanager(BaseInfoSubmanager): +class HealthInfoSubmanager(BaseInfoSubmanager): def __init__( self, - platform_path: str, vcgencmd_cmd: list[str], ignore_past: bool, state_poll: float, ) -> None: - self.__platform_path = platform_path self.__vcgencmd_cmd = vcgencmd_cmd self.__ignore_past = ignore_past self.__state_poll = state_poll - self.__dt_cache: dict[str, str] = {} - self.__notifier = aiotools.AioNotifier() async def get_state(self) -> dict: ( - base, - serial, - platform, throttling, cpu_percent, cpu_temp, mem, ) = 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_cpu_percent(), self.__get_cpu_temp(), self.__get_mem(), ) return { - "platform": { - "type": "rpi", - "base": base, - "serial": serial, - **platform, # type: ignore + "temp": { + "cpu": cpu_temp, }, - "health": { - "temp": { - "cpu": cpu_temp, - }, - "cpu": { - "percent": cpu_percent, - }, - "mem": mem, - "throttling": throttling, + "cpu": { + "percent": cpu_percent, }, + "mem": mem, + "throttling": throttling, } 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): temp_path = f"{env.SYSFS_PREFIX}/sys/class/thermal/thermal_zone0/temp" try: diff --git a/kvmd/apps/kvmd/info/system.py b/kvmd/apps/kvmd/info/system.py index d4a450de..85b46673 100644 --- a/kvmd/apps/kvmd/info/system.py +++ b/kvmd/apps/kvmd/info/system.py @@ -28,6 +28,7 @@ from typing import AsyncGenerator from ....logging import get_logger +from .... import env from .... import aiotools from .... import aioproc @@ -38,12 +39,30 @@ from .base import 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.__dt_cache: dict[str, str] = {} self.__notifier = aiotools.AioNotifier() 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 return { "kvmd": {"version": __version__}, @@ -52,6 +71,12 @@ class SystemInfoSubmanager(BaseInfoSubmanager): field: getattr(uname_info, field) for field in ["system", "release", "version", "machine"] }, + "platform": { + "type": "rpi", + "base": base, + "serial": serial, + **pl, # type: ignore + }, } 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: version = "" features: dict[str, bool] = {} diff --git a/web/share/js/kvm/session.js b/web/share/js/kvm/session.js index a540a94b..8c31ebf6 100644 --- a/web/share/js/kvm/session.js +++ b/web/share/js/kvm/session.js @@ -58,7 +58,7 @@ export function Session() { var __ocr = new Ocr(__streamer.getGeometry); var __switch = new Switch(); - var __info_hw_state = null; + var __info_health_state = null; var __info_fan_state = null; var __init__ = function() { @@ -71,7 +71,7 @@ export function Session() { for (let key of Object.keys(state)) { switch (key) { 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 "system": __setInfoStateSystem(state.system); break; case "extras": __setInfoStateExtras(state.extras); break; @@ -91,11 +91,10 @@ export function Session() { document.title = "PiKVM Session"; } - if (state.tips && state.tips.left) { - $("kvmd-meta-tips-left").innerText = `${state.tips.left}`; - } - if (state.tips && state.tips.right) { - $("kvmd-meta-tips-right").innerText = `${state.tips.right}`; + for (let place of ["left", "right"]) { + if (state.tips && state.tips[place]) { + $(`kvmd-meta-tips-${place}`).innerText = state.tips[place]; + } } // Don't use this option, it may be removed in any time @@ -105,10 +104,10 @@ export function Session() { } }; - var __setInfoStateHw = function(state) { - if (state.health.throttling !== null) { - let flags = state.health.throttling.parsed_flags; - let ignore_past = state.health.throttling.ignore_past; + var __setInfoStateHealth = function(state) { + if (state.throttling !== null) { + let flags = state.throttling.parsed_flags; + let ignore_past = state.throttling.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)); @@ -118,7 +117,7 @@ export function Session() { tools.hidden.setVisible($("hw-health-message-undervoltage"), undervoltage); tools.hidden.setVisible($("hw-health-message-overheating"), freq_capped); } - __info_hw_state = state; + __info_health_state = state; __renderAboutInfoHardware(); }; @@ -145,37 +144,24 @@ export function Session() { }; var __renderAboutInfoHardware = function() { - let html = ""; - if (__info_hw_state !== null) { - html += ` - Platform: - ${__formatMisc(__info_hw_state)} -