fan monitoring

This commit is contained in:
Maxim Devaev 2022-03-25 21:19:28 +03:00
parent 67180e244f
commit ed23fef512
9 changed files with 258 additions and 38 deletions

View File

@ -200,6 +200,7 @@ for _variant in "${_variants[@]}"; do
if [ -f configs/kvmd/fan/$_platform.ini ]; then if [ -f configs/kvmd/fan/$_platform.ini ]; then
backup=(\"\${backup[@]}\" etc/kvmd/fan.ini) backup=(\"\${backup[@]}\" etc/kvmd/fan.ini)
depends=(\"\${depends[@]}\" \"kvmd-fan>=0.18\")
install -DTm444 configs/kvmd/fan/$_platform.ini \"\$pkgdir/etc/kvmd/fan.ini\" install -DTm444 configs/kvmd/fan/$_platform.ini \"\$pkgdir/etc/kvmd/fan.ini\"
fi fi

View File

@ -12,6 +12,10 @@ kvmd:
auth: !include auth.yaml auth: !include auth.yaml
info:
fan:
unix: /run/kvmd/fan.sock
hid: hid:
type: otg type: otg
keyboard: keyboard:

View File

@ -375,6 +375,11 @@ def _get_config_scheme() -> Dict:
"vcgencmd_cmd": Option(["/opt/vc/bin/vcgencmd"], type=valid_command), "vcgencmd_cmd": Option(["/opt/vc/bin/vcgencmd"], type=valid_command),
"state_poll": Option(10.0, type=valid_float_f01), "state_poll": Option(10.0, type=valid_float_f01),
}, },
"fan": {
"unix": Option("", type=valid_abs_path, if_empty="", unpack_as="unix_path"),
"timeout": Option(5.0, type=valid_float_f01),
"state_poll": Option(5.0, type=valid_float_f01),
},
}, },
"hid": { "hid": {

View File

@ -29,6 +29,7 @@ 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 .hw import HwInfoSubmanager
from .fan import FanInfoSubmanager
# ===== # =====
@ -39,6 +40,7 @@ class InfoManager:
"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()), "hw": HwInfoSubmanager(**config.kvmd.info.hw._unpack()),
"fan": FanInfoSubmanager(**config.kvmd.info.fan._unpack()),
} }
def get_subs(self) -> Set[str]: def get_subs(self) -> Set[str]:

View File

@ -0,0 +1,98 @@
# ========================================================================== #
# #
# KVMD - The main PiKVM daemon. #
# #
# Copyright (C) 2018-2022 Maxim Devaev <mdevaev@gmail.com> #
# #
# 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 copy
import asyncio
from typing import Dict
from typing import AsyncGenerator
from typing import Optional
import aiohttp
from ....logging import get_logger
from .... import aiotools
from .... import htclient
from .base import BaseInfoSubmanager
# =====
class FanInfoSubmanager(BaseInfoSubmanager):
def __init__(
self,
unix_path: str,
timeout: float,
state_poll: float,
) -> None:
self.__unix_path = unix_path
self.__timeout = timeout
self.__state_poll = state_poll
async def get_state(self) -> Dict:
return {
"monitored": bool(self.__unix_path),
"state": ((await self.__get_fan_state() if self.__unix_path else None)),
}
async def poll_state(self) -> AsyncGenerator[Dict, None]:
prev_state: Dict = {}
while True:
if self.__unix_path:
pure = state = await self.get_state()
if pure["state"] is not None:
try:
pure = copy.deepcopy(state)
pure["state"]["service"]["now_ts"] = 0
except Exception:
pass
if pure != prev_state:
yield state
prev_state = pure
await asyncio.sleep(self.__state_poll)
else:
yield (await self.get_state())
await aiotools.wait_infinite()
# =====
async def __get_fan_state(self) -> Optional[Dict]:
try:
async with self.__make_http_session() as session:
async with session.get("http://localhost/state") as response:
htclient.raise_not_200(response)
return (await response.json())["result"]
except Exception as err:
get_logger(0).error("Can't read fan state: %s", err)
return None
def __make_http_session(self) -> aiohttp.ClientSession:
kwargs: Dict = {
"headers": {
"User-Agent": htclient.make_user_agent("KVMD"),
},
"timeout": aiohttp.ClientTimeout(total=self.__timeout),
"connector": aiohttp.UnixConnector(path=self.__unix_path)
}
return aiohttp.ClientSession(**kwargs)

View File

@ -106,6 +106,38 @@
</div> </div>
</li> </li>
</div> </div>
<div class="hidden" id="fan-health-dropdown">
<li class="left"><a class="menu-button" href="#"><img class="hidden" data-dont-hide-menu id="fan-health-led" src="/share/svg/led-fan.svg"></a>
<div class="menu" data-dont-hide-menu>
<div class="text">
<table>
<tr>
<td rowspan="2"><img class="sign " src="/share/svg/warning.svg"></td>
<td style="line-height:1.5"><b>Raspberry Pi's health is at risk</b></td>
</tr>
<tr>
<td><sup style="line-height:1">This is not a drill! A red icon indicates a current issue,<br>
a yellow one that was observed in the past</sup></td>
</tr>
</table>
</div>
<div id="fan-health-message-fail">
<hr>
<div class="text">
<table>
<tr>
<td rowspan="2"><img class="sign led-gray" src="/share/svg/led-fan.svg"></td>
<td style="line-height:1.5"><b>Fan failed</b></td>
</tr>
<tr>
<td><sup style="line-height:1">A fan error occured, please check the log</sup></td>
</tr>
</table>
</div>
</div>
</div>
</li>
</div>
<li class="right"><a class="menu-button" href="#"><img class="led-gray" data-dont-hide-menu id="link-led" src="/share/svg/led-link.svg"><img class="led-gray" data-dont-hide-menu id="stream-led" src="/share/svg/led-stream.svg"><img class="led-gray" data-dont-hide-menu id="hid-keyboard-led" src="/share/svg/led-hid-keyboard.svg"><img class="led-gray" data-dont-hide-menu id="hid-mouse-led" src="/share/svg/led-hid-mouse.svg">System</a> <li class="right"><a class="menu-button" href="#"><img class="led-gray" data-dont-hide-menu id="link-led" src="/share/svg/led-link.svg"><img class="led-gray" data-dont-hide-menu id="stream-led" src="/share/svg/led-stream.svg"><img class="led-gray" data-dont-hide-menu id="hid-keyboard-led" src="/share/svg/led-hid-keyboard.svg"><img class="led-gray" data-dont-hide-menu id="hid-mouse-led" src="/share/svg/led-hid-mouse.svg">System</a>
<div class="menu" data-dont-hide-menu> <div class="menu" data-dont-hide-menu>
<table class="kv" style="width: calc(100% - 20px)"> <table class="kv" style="width: calc(100% - 20px)">
@ -1539,10 +1571,10 @@
<div class="code" id="about-meta"><span class="code-comment">No data</span> <div class="code" id="about-meta"><span class="code-comment">No data</span>
</div> </div>
</div> </div>
<input type="radio" name="about-tab-button" id="about-tab-hw-button"> <input type="radio" name="about-tab-button" id="about-tab-hardware-button">
<label for="about-tab-hw-button">Hardware</label> <label for="about-tab-hardware-button">Hardware</label>
<div class="tab"> <div class="tab">
<div class="code" id="about-hw"><span class="code-comment">No data</span> <div class="code" id="about-hardware"><span class="code-comment">No data</span>
</div> </div>
</div> </div>
<input type="radio" name="about-tab-button" id="about-tab-version-button"> <input type="radio" name="about-tab-button" id="about-tab-version-button">

View File

@ -17,3 +17,16 @@ div(id="hw-health-dropdown" class="hidden")
+menu_message("led-overheating", "Overheating detected", "led-gray") +menu_message("led-overheating", "Overheating detected", "led-gray")
| Frequency capping due to overheating,#[br] | Frequency capping due to overheating,#[br]
| improve cooling of the Raspberry Pi | improve cooling of the Raspberry Pi
div(id="fan-health-dropdown" class="hidden")
li(class="left")
a(class="menu-button" href="#")
+navbar_led("fan-health-led", "led-fan", "hidden")
div(data-dont-hide-menu class="menu")
+menu_message("warning", "Raspberry Pi's health is at risk")
| This is not a drill! A red icon indicates a current issue,#[br]
| a yellow one that was observed in the past
div(id="fan-health-message-fail")
hr
+menu_message("led-fan", "Fan failed", "led-gray")
| A fan error occured, please check the log

View File

@ -28,7 +28,7 @@ div(id="about-window" class="window")
br br
div(class="tabs-box") div(class="tabs-box")
+about_tab("meta", "Meta", true) +about_tab("meta", "Meta", true)
+about_tab("hw", "Hardware") +about_tab("hardware", "Hardware")
+about_tab("version", "Version") +about_tab("version", "Version")
+about_tab("thanks", "Thanks") +about_tab("thanks", "Thanks")

View File

@ -53,6 +53,9 @@ export function Session() {
var __gpio = new Gpio(__recorder); var __gpio = new Gpio(__recorder);
var __ocr = new Ocr(__streamer.getGeometry); var __ocr = new Ocr(__streamer.getGeometry);
var __info_hw_state = null;
var __info_fan_state = null;
var __init__ = function() { var __init__ = function() {
__startSession(); __startSession();
}; };
@ -86,16 +89,6 @@ export function Session() {
}; };
var __setAboutInfoHw = function(state) { var __setAboutInfoHw = function(state) {
$("about-hw").innerHTML = `
Platform base: <span class="code-comment">${state.platform.base}</span><br>
<hr>
Temperature:
${__formatTemp(state.health.temp)}
<hr>
Throttling:
${__formatThrottling(state.health.throttling)}
`;
if (state.health.throttling !== null) { if (state.health.throttling !== null) {
let flags = state.health.throttling.parsed_flags; let flags = state.health.throttling.parsed_flags;
let undervoltage = (flags.undervoltage.now || flags.undervoltage.past); let undervoltage = (flags.undervoltage.now || flags.undervoltage.past);
@ -107,31 +100,74 @@ 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;
__renderAboutInfoHardware();
}; };
var __setExtras = function(state) { var __setAboutInfoFan = function(state) {
let show_hook = null; let failed = false;
let close_hook = null; let failed_past = false;
let has_webterm = (state.webterm && (state.webterm.enabled || state.webterm.started)); if (state.monitored) {
if (has_webterm) { if (state.state === null) {
let path = "/" + state.webterm.path; failed = true;
show_hook = function() { } else {
tools.info("Terminal opened: ", path); if (!state.state.fan.ok) {
$("webterm-iframe").src = path; failed = true;
}; } else if (state.state.fan.last_fail_ts >= 0) {
close_hook = function() { failed = true;
tools.info("Terminal closed"); failed_past = true;
$("webterm-iframe").src = "";
};
} }
tools.feature.setEnabled($("webterm"), has_webterm); }
$("webterm-window").show_hook = show_hook; }
$("webterm-window").close_hook = close_hook; tools.hidden.setVisible($("fan-health-dropdown"), failed);
$("fan-health-led").className = (failed ? (failed_past ? "led-yellow" : "led-red") : "hidden");
__streamer.setJanusEnabled( __info_fan_state = state;
(state.janus && (state.janus.enabled || state.janus.started)) __renderAboutInfoHardware();
|| (state.janus_static && (state.janus_static.enabled || state.janus_static.started)) };
);
var __renderAboutInfoHardware = function() {
let html = "";
if (__info_hw_state !== null) {
html += `
Platform base: <span class="code-comment">${__info_hw_state.platform.base}</span><br>
<hr>
Temperature:
${__formatTemp(__info_hw_state.health.temp)}
<hr>
Throttling:
${__formatThrottling(__info_hw_state.health.throttling)}
`;
}
if (__info_fan_state !== null) {
if (html.length > 0) {
html += "<hr>";
}
html += `
Fan:
${__formatFan(__info_fan_state)}
`;
}
$("about-hardware").innerHTML = html;
};
var __formatFan = function(state) {
if (!state.monitored) {
return __formatUl([["Status", "Not monitored"]]);
} else if (state.state === null) {
return __formatUl([["Status", __colored("red", "Not available")]]);
} else {
state = state.state;
let pairs = [
["Status", (state.fan.ok ? __colored("green", "Ok") : __colored("red", "Failed"))],
["Desired speed", `${state.fan.speed}%`],
["PWM", `${state.fan.pwm}`],
];
if (state.hall.available) {
pairs.push(["RPM", __colored((state.fan.ok ? "green" : "red"), state.hall.rpm)]);
}
return __formatUl(pairs);
}
}; };
var __formatTemp = function(temp) { var __formatTemp = function(temp) {
@ -158,13 +194,16 @@ export function Session() {
}; };
var __formatThrottleError = function(flags) { var __formatThrottleError = function(flags) {
let colored = ((color, text) => `<font color="${color}">${text}</font>`);
return ` return `
${flags["now"] ? colored("red", "RIGHT NOW") : colored("green", "No")}; ${flags["now"] ? __colored("red", "RIGHT NOW") : __colored("green", "No")};
${flags["past"] ? colored("red", "In the past") : colored("green", "Never")} ${flags["past"] ? __colored("red", "In the past") : __colored("green", "Never")}
`; `;
}; };
var __colored = function(color, text) {
return `<font color="${color}">${text}</font>`;
};
var __setAboutInfoSystem = function(state) { var __setAboutInfoSystem = function(state) {
$("about-version").innerHTML = ` $("about-version").innerHTML = `
KVMD: <span class="code-comment">${state.kvmd.version}</span><br> KVMD: <span class="code-comment">${state.kvmd.version}</span><br>
@ -203,6 +242,31 @@ export function Session() {
return text + "</ul>"; return text + "</ul>";
}; };
var __setExtras = function(state) {
let show_hook = null;
let close_hook = null;
let has_webterm = (state.webterm && (state.webterm.enabled || state.webterm.started));
if (has_webterm) {
let path = "/" + state.webterm.path;
show_hook = function() {
tools.info("Terminal opened: ", path);
$("webterm-iframe").src = path;
};
close_hook = function() {
tools.info("Terminal closed");
$("webterm-iframe").src = "";
};
}
tools.feature.setEnabled($("webterm"), has_webterm);
$("webterm-window").show_hook = show_hook;
$("webterm-window").close_hook = close_hook;
__streamer.setJanusEnabled(
(state.janus && (state.janus.enabled || state.janus.started))
|| (state.janus_static && (state.janus_static.enabled || state.janus_static.started))
);
};
var __startSession = function() { var __startSession = function() {
$("link-led").className = "led-yellow"; $("link-led").className = "led-yellow";
$("link-led").title = "Connecting..."; $("link-led").title = "Connecting...";
@ -244,6 +308,7 @@ export function Session() {
case "pong": __missed_heartbeats = 0; break; case "pong": __missed_heartbeats = 0; break;
case "info_meta_state": __setAboutInfoMeta(data.event); break; case "info_meta_state": __setAboutInfoMeta(data.event); break;
case "info_hw_state": __setAboutInfoHw(data.event); break; case "info_hw_state": __setAboutInfoHw(data.event); break;
case "info_fan_state": __setAboutInfoFan(data.event); break;
case "info_system_state": __setAboutInfoSystem(data.event); break; case "info_system_state": __setAboutInfoSystem(data.event); break;
case "info_extras_state": __setExtras(data.event); break; case "info_extras_state": __setExtras(data.event); break;
case "gpio_model_state": __gpio.setModel(data.event); break; case "gpio_model_state": __gpio.setModel(data.event); break;