mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2025-12-12 09:10:30 +08:00
fan monitoring
This commit is contained in:
parent
67180e244f
commit
ed23fef512
1
PKGBUILD
1
PKGBUILD
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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": {
|
||||||
|
|||||||
@ -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]:
|
||||||
|
|||||||
98
kvmd/apps/kvmd/info/fan.py
Normal file
98
kvmd/apps/kvmd/info/fan.py
Normal 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)
|
||||||
@ -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">
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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")
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user