mirror of
https://github.com/lucaspalomodevelop/core.git
synced 2026-03-16 09:34:39 +00:00
dashboard: add thermal sensors widget
This commit is contained in:
parent
10f7043769
commit
2d73903529
@ -132,7 +132,12 @@ class DashboardController extends ApiControllerBase
|
||||
'rtt' => gettext('RTT'),
|
||||
'rttd' => gettext('RTTd'),
|
||||
'loss' => gettext('Loss'),
|
||||
]
|
||||
],
|
||||
'thermalsensors' => [
|
||||
'title' => gettext('Thermal Sensors'),
|
||||
'help' => gettext('CPU thermal sensors often measure the same temperature for each core. If this is the case, only the first core is shown.'),
|
||||
'unconfigured' => gettext('Thermal sensors not available or not configured.')
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@ -332,4 +332,24 @@ class SystemController extends ApiControllerBase
|
||||
{
|
||||
return json_decode((new Backend())->configdRun('system show swapinfo'), true);
|
||||
}
|
||||
|
||||
public function systemTemperatureAction()
|
||||
{
|
||||
$result = [];
|
||||
|
||||
foreach (explode("\n", (new Backend())->configdRun('system temp')) as $sysctl) {
|
||||
$parts = explode('=', $);
|
||||
if (count($parts) >= 2) {
|
||||
$tempItem = array();
|
||||
$tempItem['device'] = $parts[0];
|
||||
$tempItem['device_seq'] = filter_var($tempItem['device'], FILTER_SANITIZE_NUMBER_INT);
|
||||
$tempItem['temperature'] = trim(str_replace('C', '', $parts[1]));
|
||||
$tempItem['type'] = strpos($tempItem['device'], 'hw.acpi') !== false ? "zone" : "core";
|
||||
$tempItem['type_translated'] = $tempItem['type'] == "zone" ? gettext("Zone") : gettext("Core");
|
||||
$result[] = $tempItem;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
@ -501,7 +501,7 @@ class WidgetManager {
|
||||
let $header = $(`
|
||||
<div class="widget-header">
|
||||
<div></div>
|
||||
<div><b>${title}</b></div>
|
||||
<div id="${identifier}-title"><b>${title}</b></div>
|
||||
<div id="close-handle-${identifier}" class="close-handle">
|
||||
<i class="fa fa-times fa-xs"></i>
|
||||
</div>
|
||||
|
||||
234
src/opnsense/www/js/widgets/ThermalSensors.js
Normal file
234
src/opnsense/www/js/widgets/ThermalSensors.js
Normal file
@ -0,0 +1,234 @@
|
||||
// endpoint:/api/core/system/systemTemperature
|
||||
|
||||
/*
|
||||
* Copyright (C) 2024 Deciso B.V.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
|
||||
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
import BaseWidget from "./BaseWidget.js";
|
||||
|
||||
export default class ThermalSensors extends BaseWidget {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.chart = null;
|
||||
this.width = null;
|
||||
this.height = null;
|
||||
this.colors = [];
|
||||
}
|
||||
|
||||
getMarkup() {
|
||||
return $(`
|
||||
<div class="${this.id}-chart-container" style="margin-left: 10px; margin-right: 10px;">
|
||||
<div class="canvas-container" style="position: relative;">
|
||||
<canvas id="${this.id}-chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
async onMarkupRendered() {
|
||||
let context = document.getElementById(`${this.id}-chart`).getContext("2d");
|
||||
|
||||
const data = {
|
||||
datasets: [
|
||||
{
|
||||
data: [],
|
||||
metadata: [],
|
||||
backgroundColor: (context) => {
|
||||
const {chartArea} = context.chart;
|
||||
|
||||
if (!chartArea || this.colors.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let dataIndex = context.dataIndex;
|
||||
let value = parseInt(context.raw);
|
||||
if (value >= 80) {
|
||||
this.colors[dataIndex] = '#dc3545';
|
||||
} else if (value >= 70) {
|
||||
this.colors[dataIndex] = '#ffc107';
|
||||
} else {
|
||||
this.colors[dataIndex] = '#28a745';
|
||||
}
|
||||
return this.colors;
|
||||
},
|
||||
barPercentage: 0.8,
|
||||
borderWidth: 1,
|
||||
borderSkipped: false,
|
||||
borderRadius: 20,
|
||||
barThickness: 20
|
||||
},
|
||||
{
|
||||
data: [],
|
||||
backgroundColor: ['#E5E5E5'],
|
||||
borderRadius: 20,
|
||||
barPercentage: 0.5,
|
||||
borderWidth: 1,
|
||||
borderSkipped: true,
|
||||
barThickness: 10,
|
||||
pointHitRadius: 0
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const lines = {
|
||||
id: 'lines',
|
||||
afterDatasetsDraw: (chart, args, plugins) => {
|
||||
const {ctx, data, chartArea} = chart;
|
||||
if (data.datasets[0].data.length === 0) {
|
||||
return;
|
||||
}
|
||||
let count = data.datasets[0].data.length;
|
||||
ctx.save();
|
||||
for (let i = 0; i < count; i++) {
|
||||
const meta = chart.getDatasetMeta(0);
|
||||
const xPos = meta.data[i].x;
|
||||
const yPos = meta.data[i].y;
|
||||
const barHeight = meta.data[i].height;
|
||||
|
||||
ctx.font = 'semibold 12px sans-serif';
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillText(`${data.datasets[0].data[i]}°C`, xPos - 50, yPos + barHeight / 4);
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
const config = {
|
||||
type: 'bar',
|
||||
data: data,
|
||||
options: {
|
||||
responsive: true,
|
||||
indexAxis: 'y',
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
filter: function(tooltipItem) {
|
||||
return tooltipItem.datasetIndex === 0;
|
||||
},
|
||||
callbacks: {
|
||||
label: (tooltipItem) => {
|
||||
let idx = tooltipItem.dataIndex;
|
||||
if (!tooltipItem.dataset.metadata) {
|
||||
return;
|
||||
}
|
||||
let meta = tooltipItem.dataset.metadata[idx];
|
||||
return `${meta.device}: ${meta.temperature}°C / ${meta.temperature_fahrenheit}°F`;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
stacked: true,
|
||||
beginAtZero: true,
|
||||
grid: {
|
||||
display: false,
|
||||
drawBorder: false,
|
||||
},
|
||||
ticks: {
|
||||
display: false,
|
||||
}
|
||||
},
|
||||
y: {
|
||||
autoSkip: false,
|
||||
stacked: true,
|
||||
grid: {
|
||||
display: false,
|
||||
drawBorder: false,
|
||||
},
|
||||
ticks: {
|
||||
autoSkip: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
lines
|
||||
]
|
||||
}
|
||||
|
||||
this.chart = new Chart(context, config);
|
||||
|
||||
$(`#${this.id}-title`).append(` <i class="fa fa-question-circle" data-toggle="tooltip" title="${this.translations.help}"></i>`);
|
||||
$('[data-toggle="tooltip"]').tooltip({container: 'body', triger: 'hover'});
|
||||
}
|
||||
|
||||
async onWidgetTick() {
|
||||
ajaxGet('/api/core/system/systemTemperature', { }, (data, status) => {
|
||||
if (!data || !data.length) {
|
||||
$(`.${this.id}-chart-container`).html(`
|
||||
<a href="/system_advanced_misc.php">${this.translations.unconfigured}</a>
|
||||
`).css('margin', '2em auto')
|
||||
return;
|
||||
}
|
||||
let parsed = this._parseSensors(data);
|
||||
this._update(parsed);
|
||||
});
|
||||
}
|
||||
|
||||
_update(data = []) {
|
||||
if (!this.chart || data.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.colors = new Array(data.length).fill(0);
|
||||
|
||||
data.forEach((value, index) => {
|
||||
this.chart.data.labels[index] = `${value.type_translated} ${value.device_seq}`;
|
||||
this.chart.data.datasets[0].data[index] = Math.max(1, Math.min(100, value.temperature));
|
||||
this.chart.data.datasets[0].metadata[index] = value;
|
||||
this.chart.data.datasets[1].data[index] = 100 - value.temperature;
|
||||
});
|
||||
this.chart.canvas.parentNode.style.height = `${30 + (data.length * 30)}px`;
|
||||
this.chart.update();
|
||||
}
|
||||
|
||||
_parseSensors(data) {
|
||||
const toFahrenheit = (celsius) => (celsius * 9 / 5) + 32;
|
||||
data.forEach(item => {
|
||||
item.temperature_fahrenheit = toFahrenheit(parseFloat(item.temperature)).toFixed(1);
|
||||
});
|
||||
|
||||
// Find cores with differing temperatures
|
||||
const coreTemperatures = data.filter(item => item.type === 'core').map(item => parseFloat(item.temperature));
|
||||
const uniqueTemperatures = new Set(coreTemperatures);
|
||||
|
||||
let result = [];
|
||||
if (uniqueTemperatures.size === 1) {
|
||||
// If all temperatures are the same, include only the first core
|
||||
result.push(data.find(item => item.type === 'core'));
|
||||
} else {
|
||||
// Include all cores with differing temperatures
|
||||
result = data.filter(item => item.type !== 'core' || coreTemperatures.filter(temp => temp !== parseFloat(item.temperature)).length > 0);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user