diff options
author | Devaev Maxim <[email protected]> | 2020-07-09 05:04:21 +0300 |
---|---|---|
committer | Devaev Maxim <[email protected]> | 2020-07-09 05:13:03 +0300 |
commit | 53eb74670d040c9e8745433a2641f18796da365c (patch) | |
tree | b321d856280e6fe4bb2b410b582c046ae4e7ee20 /kvmd | |
parent | 69f77ce48b279c614f1aaca809c1d74e411f1fd1 (diff) |
hw monitoring
Diffstat (limited to 'kvmd')
-rw-r--r-- | kvmd/apps/__init__.py | 7 | ||||
-rw-r--r-- | kvmd/apps/kvmd/__init__.py | 2 | ||||
-rw-r--r-- | kvmd/apps/kvmd/api/hw.py | 41 | ||||
-rw-r--r-- | kvmd/apps/kvmd/hw.py | 150 | ||||
-rw-r--r-- | kvmd/apps/kvmd/server.py | 5 |
5 files changed, 205 insertions, 0 deletions
diff --git a/kvmd/apps/__init__.py b/kvmd/apps/__init__.py index a5ad6f25..4b61a65e 100644 --- a/kvmd/apps/__init__.py +++ b/kvmd/apps/__init__.py @@ -221,6 +221,13 @@ def _get_config_scheme() -> Dict: "extras": Option("/usr/share/kvmd/extras", type=valid_abs_dir), }, + "hw": { + "vcgencmd_cmd": Option(["/opt/vc/bin/vcgencmd"], type=valid_command), + "procfs_prefix": Option("", type=(lambda arg: str(arg).strip())), + "sysfs_prefix": Option("", type=(lambda arg: str(arg).strip())), + "state_poll": Option(10.0, type=valid_float_f01), + }, + "wol": { "ip": Option("255.255.255.255", type=(lambda arg: valid_ip(arg, v6=False))), "port": Option(9, type=valid_port), diff --git a/kvmd/apps/kvmd/__init__.py b/kvmd/apps/kvmd/__init__.py index 1684339f..dad0da42 100644 --- a/kvmd/apps/kvmd/__init__.py +++ b/kvmd/apps/kvmd/__init__.py @@ -35,6 +35,7 @@ from .. import init from .auth import AuthManager from .info import InfoManager +from .hw import HwManager from .logreader import LogReader from .wol import WakeOnLan from .streamer import Streamer @@ -77,6 +78,7 @@ def main(argv: Optional[List[str]]=None) -> None: enabled=config.auth.enabled, ), info_manager=InfoManager(global_config), + hw_manager=HwManager(**config.hw._unpack()), log_reader=LogReader(), wol=WakeOnLan(**config.wol._unpack()), diff --git a/kvmd/apps/kvmd/api/hw.py b/kvmd/apps/kvmd/api/hw.py new file mode 100644 index 00000000..ff097c4f --- /dev/null +++ b/kvmd/apps/kvmd/api/hw.py @@ -0,0 +1,41 @@ +# ========================================================================== # +# # +# KVMD - The main Pi-KVM daemon. # +# # +# Copyright (C) 2018 Maxim Devaev <[email protected]> # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see <https://www.gnu.org/licenses/>. # +# # +# ========================================================================== # + + +from aiohttp.web import Request +from aiohttp.web import Response + +from ..hw import HwManager + +from ..http import exposed_http +from ..http import make_json_response + + +# ===== +class HwApi: + def __init__(self, hw_manager: HwManager) -> None: + self.__hw_manager = hw_manager + + # ===== + + @exposed_http("GET", "/hw") + async def __state_handler(self, _: Request) -> Response: + return make_json_response(await self.__hw_manager.get_state()) diff --git a/kvmd/apps/kvmd/hw.py b/kvmd/apps/kvmd/hw.py new file mode 100644 index 00000000..13f7e9de --- /dev/null +++ b/kvmd/apps/kvmd/hw.py @@ -0,0 +1,150 @@ +# ========================================================================== # +# # +# KVMD - The main Pi-KVM daemon. # +# # +# Copyright (C) 2018 Maxim Devaev <[email protected]> # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see <https://www.gnu.org/licenses/>. # +# # +# ========================================================================== # + + +import asyncio + +from typing import List +from typing import Dict +from typing import Callable +from typing import AsyncGenerator +from typing import TypeVar +from typing import Optional + +import aiofiles + +from ...logging import get_logger + +from ... import aioproc + + +# ===== +_RetvalT = TypeVar("_RetvalT") + + +# ===== +class HwManager: + def __init__( + self, + vcgencmd_cmd: List[str], + procfs_prefix: str, + sysfs_prefix: str, + state_poll: float, + ) -> None: + + self.__vcgencmd_cmd = vcgencmd_cmd + self.__sysfs_prefix = sysfs_prefix + self.__procfs_prefix = procfs_prefix + self.__state_poll = state_poll + + async def get_state(self) -> Dict: + (model, cpu_temp, gpu_temp, throttling) = await asyncio.gather( + self.__get_dt_model(), + self.__get_cpu_temp(), + self.__get_gpu_temp(), + self.__get_throttling(), + ) + return { + "platform": { + "type": "rpi", + "base": model, + }, + "state": { + "temp": { + "cpu": cpu_temp, + "gpu": gpu_temp, + }, + "throttling": throttling, + }, + } + + async def poll_state(self) -> AsyncGenerator[Dict, None]: + prev_state: Dict = {} + while True: + state = await self.get_state() + if state != prev_state: + yield state + prev_state = state + await asyncio.sleep(self.__state_poll) + + # ===== + + async def __get_dt_model(self) -> Optional[str]: + model_path = f"{self.__procfs_prefix}/proc/device-tree/model" + try: + async with aiofiles.open(model_path) as model_file: + return (await model_file.read()).strip(" \t\r\n\0") + except Exception as err: + get_logger(0).error("Can't read DT model from %s: %s", model_path, err) + return None + + async def __get_cpu_temp(self) -> Optional[float]: + temp_path = f"{self.__sysfs_prefix}/sys/class/thermal/thermal_zone0/temp" + try: + async with aiofiles.open(temp_path) as temp_file: + return int((await temp_file.read()).strip()) / 1000 + except Exception as err: + get_logger(0).error("Can't read CPU temp from %s: %s", temp_path, err) + return None + + async def __get_throttling(self) -> Optional[Dict]: + # https://www.raspberrypi.org/forums/viewtopic.php?f=63&t=147781&start=50#p972790 + if (flags := await self.__parse_vcgencmd( + arg="get_throttled", + parser=(lambda text: int(text.split("=")[-1].strip(), 16)), + )) is not None: + return { + "raw_flags": flags, + "parsed_flags": { + "undervoltage": { + "now": bool(flags & (1 << 0)), + "past": bool(flags & (1 << 16)), + }, + "freq_capped": { + "now": bool(flags & (1 << 1)), + "past": bool(flags & (1 << 17)), + }, + "throttled": { + "now": bool(flags & (1 << 2)), + "past": bool(flags & (1 << 18)), + }, + }, + } + return None + + async def __get_gpu_temp(self) -> Optional[float]: + return (await self.__parse_vcgencmd( + arg="measure_temp", + parser=(lambda text: float(text.split("=")[1].split("'")[0])), + )) + + async def __parse_vcgencmd(self, arg: str, parser: Callable[[str], _RetvalT]) -> Optional[_RetvalT]: + cmd = [*self.__vcgencmd_cmd, arg] + try: + text = (await aioproc.read_process(cmd, err_to_null=True))[1] + except Exception: + get_logger(0).exception("Error while executing %s", cmd) + return None + try: + return parser(text) + except Exception as err: + get_logger(0).error("Can't parse %s output: %r: %s", cmd, text, err) + return None diff --git a/kvmd/apps/kvmd/server.py b/kvmd/apps/kvmd/server.py index 31fb8b88..2e5975c6 100644 --- a/kvmd/apps/kvmd/server.py +++ b/kvmd/apps/kvmd/server.py @@ -62,6 +62,7 @@ from ... import aioproc from .auth import AuthManager from .info import InfoManager +from .hw import HwManager from .logreader import LogReader from .wol import WakeOnLan from .streamer import Streamer @@ -81,6 +82,7 @@ from .api.auth import AuthApi from .api.auth import check_request_auth from .api.info import InfoApi +from .api.hw import HwApi from .api.log import LogApi from .api.wol import WolApi from .api.hid import HidApi @@ -123,6 +125,7 @@ class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-ins self, auth_manager: AuthManager, info_manager: InfoManager, + hw_manager: HwManager, log_reader: LogReader, wol: WakeOnLan, @@ -148,6 +151,7 @@ class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-ins self.__components = [ _Component("Auth manager", "", auth_manager), _Component("Info manager", "info_state", info_manager), + _Component("HW manager", "hw_state", hw_manager), _Component("Wake-on-LAN", "wol_state", wol), _Component("HID", "hid_state", hid), _Component("ATX", "atx_state", atx), @@ -159,6 +163,7 @@ class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-ins self, AuthApi(auth_manager), InfoApi(info_manager), + HwApi(hw_manager), LogApi(log_reader), WolApi(wol), HidApi(hid, keymap_path), |