update 2026-09-26 11:07:27

This commit is contained in:
action committed 2026-09-26 11:07:27 +08:00
1 parent 7b88b273d0
commit 97ae9f68da
4898 files changed
-1712274

No files matched your search

-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2021 gSpotx2f
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-20
View File
@@ -1,20 +0,0 @@
#
# Copyright (C) 2026 gSpot (https://github.com/gSpotx2f/luci-app-temp-status)
#
# This is free software, licensed under the MIT License.
#
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-temp-status
PKG_VERSION:=0.8.1
PKG_RELEASE:=1
LUCI_TITLE:=Temperature sensors data for the LuCI status page
LUCI_DEPENDS:=+ucode +ucode-mod-fs
LUCI_PKGARCH:=all
PKG_LICENSE:=MIT
#include ../../luci.mk
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature
-38
View File
@@ -1,38 +0,0 @@
# luci-app-temp-status
Temperature sensors data for the LuCI status page (OpenWrt webUI).
OpenWrt >= 22.03.
**Dependences:** ucode, ucode-mod-fs.
## Installation notes
**OpenWrt >= 25.12:**
wget --no-check-certificate -O /tmp/luci-app-temp-status-0.8.1-r1.apk https://github.com/gSpotx2f/packages-openwrt/raw/master/25.12/luci-app-temp-status-0.8.1-r1.apk
apk --allow-untrusted add /tmp/luci-app-temp-status-0.8.1-r1.apk
rm /tmp/luci-app-temp-status-0.8.1-r1.apk
service rpcd restart
i18n-ru:
wget --no-check-certificate -O /tmp/luci-i18n-temp-status-ru-0.8.1-r1.apk https://github.com/gSpotx2f/packages-openwrt/raw/master/25.12/luci-i18n-temp-status-ru-0.8.1-r1.apk
apk --allow-untrusted add /tmp/luci-i18n-temp-status-ru-0.8.1-r1.apk
rm /tmp/luci-i18n-temp-status-ru-0.8.1-r1.apk
**OpenWrt <= 24.10:**
wget --no-check-certificate -O /tmp/luci-app-temp-status_0.8.1-r1_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/24.10/luci-app-temp-status_0.8.1-r1_all.ipk
opkg install /tmp/luci-app-temp-status_0.8.1-r1_all.ipk
rm /tmp/luci-app-temp-status_0.8.1-r1_all.ipk
service rpcd restart
i18n-ru:
wget --no-check-certificate -O /tmp/luci-i18n-temp-status-ru_0.8.1-r1_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/24.10/luci-i18n-temp-status-ru_0.8.1-r1_all.ipk
opkg install /tmp/luci-i18n-temp-status-ru_0.8.1-r1_all.ipk
rm /tmp/luci-i18n-temp-status-ru_0.8.1-r1_all.ipk
## Screenshots:
![](https://github.com/gSpotx2f/luci-app-temp-status/blob/master/screenshots/01.jpg)
@@ -1,18 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" version="1.1" xmlns="http://www.w3.org/2000/svg">
<polyline id="temp_line" class="temp" points="" />
<line class="grid" x1="0" y1="25%" x2="100%" y2="25%" />
<text id="label_75" x="20" y="24%"> </text>
<line class="grid" x1="0" y1="50%" x2="100%" y2="50%" />
<text id="label_50" x="20" y="49%"> </text>
<line class="grid" x1="0" y1="75%" x2="100%" y2="75%" />
<text id="label_25" x="20" y="74%"> </text>
<line id="hot_line" class="hot" x1="0" y1="-1" x2="100%" y2="-1" />
<line id="overheat_line" class="overheat" x1="0" y1="-1" x2="100%" y2="-1" />
</svg>

Before

Width:  |  Height:  |  Size: 740 B

@@ -1,470 +0,0 @@
'use strict';
'require baseclass';
'require rpc';
document.head.append(E('style', {'type': 'text/css'},
`
:root {
--app-temp-status-font-color: #2e2e2e;
--app-temp-status-border-color: var(--border-color-medium, #d4d4d4);
--app-temp-status-hot-color: #fff7e2;
--app-temp-status-overheat-color: #ffe9e8;
}
:root[data-darkmode="true"] {
--app-temp-status-font-color: #fff;
--app-temp-status-border-color: var(--border-color-medium, #444);
--app-temp-status-hot-color: #8d7000;
--app-temp-status-overheat-color: #a93734;
}
.temp-status-hot {
background-color: var(--app-temp-status-hot-color) !important;
color: var(--app-temp-status-font-color) !important;
}
.temp-status-hot .td {
color: var(--app-temp-status-font-color) !important;
}
.temp-status-hot td {
color: var(--app-temp-status-font-color) !important;
}
.temp-status-overheat {
background-color: var(--app-temp-status-overheat-color) !important;
color: var(--app-temp-status-font-color) !important;
}
.temp-status-overheat .td {
color: var(--app-temp-status-font-color) !important;
}
.temp-status-overheat td {
color: var(--app-temp-status-font-color) !important;
}
.temp-status-temp-area {
width: 100%;
padding: 0 0 1em 0;
display: flex;
-webkit-align-items: flex-start;
align-items: flex-start;
-webkit-justify-content: left;
justify-content: left;
-webkit-flex-wrap: wrap;
flex-wrap: wrap;
-webkit-flex-direction: row;
flex-direction: row;
}
.temp-status-list-item {
display: inline-block;
flex-grow: 0;
-webkit-flex-grow 0;
flex-shrink: 1;
-webkit-flex-shrink 1;
width: 29em;
word-wrap: break-word;
margin: 0px 4px 4px 0 !important;
padding: 4px;
border: 1px solid;
border-color: transparent;
border-color: var(--app-temp-status-border-color);
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
}
.temp-status-temp-value {
display: inline-block;
margin-left: 0.5em !important;
width: 4em;
text-align: left;
}
.temp-status-sensor-name {
display: inline-block;
padding-left: 1.5em;
font-size: 95%;
}
#temp-status-buttons-wrapper {
margin-bottom: 1em;
}
.temp-status-button {
display: inline-block;
cursor: pointer;
margin: 2px 4px 2px 0 !important;
padding: 3px 5px;
border: 1px solid;
border-color: transparent;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
opacity: 0.7;
background-color: rgba(100 100 100 / 0.2);
}
.temp-status-button:hover {
opacity: 0.9;
}
.temp-status-button:active {
opacity: 1.0;
}
.temp-status-hide-item {
display: inline-block;
cursor: pointer;
margin: 0 0.5em 0 0 !important;
padding: 1px 5px;
border: 1px dotted;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
opacity: 0.6;
background-color: rgba(100 100 100 / 0.2);
font-weight: bold;
}
.temp-status-hide-item:hover {
opacity: 0.9;
}
.temp-status-hide-item:active {
opacity: 1.0;
}
`));
return baseclass.extend({
title : _('Temperature'),
viewName : 'temp-status',
tempHot : 95,
tempOverheat : 105,
sensorsData : null,
tempData : null,
sensorsPath : [],
hiddenItems : new Set(),
hiddenNum : E('span', {}),
tempTable : E('table', { 'class': 'table' }),
tempArea : E('div', { 'class': 'temp-status-temp-area' }),
tempView : E('div', {}),
viewType : 'table',
callSensors : rpc.declare({
object: 'luci.temp-status',
method: 'getSensors',
expect: { '': {} },
}),
callTempData: rpc.declare({
object: 'luci.temp-status',
method: 'getTempData',
params: [ 'tpaths' ],
expect: { '': {} },
}),
formatTemp(mc) {
return Number((mc / 1000).toFixed(1));
},
sortFunc(a, b) {
return (a.number > b.number) ? 1 : (a.number < b.number) ? -1 : 0;
},
restoreSettingsFromLocalStorage() {
let hiddenItems = localStorage.getItem(`luci-app-${this.viewName}-hiddenItems`);
if(hiddenItems) {
this.hiddenItems = new Set(hiddenItems.split(','));
};
let view = localStorage.getItem(`luci-app-${this.viewName}-view`);
if(view) {
this.viewType = view;
};
},
saveSettingsToLocalStorage() {
localStorage.setItem(
`luci-app-${this.viewName}-hiddenItems`, Array.from(this.hiddenItems).join(','));
localStorage.setItem(
`luci-app-${this.viewName}-view`, this.viewType);
},
makeTempTableContent() {
this.tempTable.innerHTML = '';
this.tempTable.append(
E('tr', { 'class': 'tr table-titles' }, [
E('th', { 'class': 'th left', 'width': '33%' }, _('Sensor')),
E('th', { 'class': 'th left' }, _('Temperature')),
E('th', { 'class': 'th right', 'width': '1%' }, ' '),
])
);
if(this.sensorsData && this.tempData) {
for(let [k, v] of Object.entries(this.sensorsData)) {
v.sort(this.sortFunc);
for(let i of Object.values(v)) {
let sensor = i.title || i.item;
if(i.sources === undefined) {
continue;
};
i.sources.sort(this.sortFunc);
for(let j of i.sources) {
if(this.hiddenItems.has(j.path)) {
continue;
};
let temp = this.tempData[j.path];
let name = (j.label !== undefined) ? sensor + " / " + j.label :
(j.item !== undefined) ? sensor + " / " + j.item.replace(/_input$/, "") : sensor
if(temp !== undefined && temp !== null) {
temp = this.formatTemp(temp);
};
let tempHot = NaN;
let tempOverheat = NaN;
let tpoints = j.tpoints;
let tpointsString = '';
if(tpoints) {
for(let i of Object.values(tpoints)) {
let t = this.formatTemp(i.temp);
tpointsString += `&#10;${i.type}: ${t} °C`;
if(i.type == 'max' || i.type == 'critical' || i.type == 'emergency') {
if(!(tempOverheat <= t)) {
tempOverheat = t;
};
}
else if(i.type == 'hot') {
tempHot = t;
};
};
};
if(isNaN(tempHot) && isNaN(tempOverheat)) {
tempHot = this.tempHot;
tempOverheat = this.tempOverheat;
};
let rowStyle = (temp >= tempOverheat) ? ' temp-status-overheat':
(temp >= tempHot) ? ' temp-status-hot' : '';
this.tempTable.append(
E('tr', {
'class' : 'tr' + rowStyle,
'data-path': j.path ,
}, [
E('td', {
'class' : 'td left',
'data-title': _('Sensor')
},
(tpointsString.length > 0) ?
`<span style="cursor:help; border-bottom:1px dotted" data-tooltip="${tpointsString}">${name}</span>` :
name
),
E('td', {
'class' : 'td left',
'data-title': _('Temperature')
},
(temp === undefined || temp === null) ? '-' : temp + ' °C'
),
E('td', {
'class' : 'td right',
'data-title': _('Hide'),
'title' : _('Hide'),
},
E('span', {
'class': 'temp-status-hide-item',
'title': _('Hide'),
'click': () => this.hideItem(j.path),
}, '&#935;'),
),
])
);
};
};
};
};
if(this.tempTable.childNodes.length == 1) {
this.tempTable.append(
E('tr', { 'class': 'tr placeholder' },
E('td', { 'class': 'td' },
E('em', {}, _('No temperature sensors available'))
)
)
);
};
return this.tempTable;
},
makeTempAreaContent() {
this.tempArea.innerHTML = '';
for(let [k, v] of Object.entries(this.sensorsData)) {
v.sort(this.sortFunc);
for(let i of Object.values(v)) {
let sensor = i.title || i.item;
if(i.sources === undefined) {
continue;
};
i.sources.sort(this.sortFunc);
for(let j of i.sources) {
if(this.hiddenItems.has(j.path)) {
continue;
};
let temp = this.tempData[j.path];
let name = (j.label !== undefined) ? sensor + " / " + j.label :
(j.item !== undefined) ? sensor + " / " + j.item.replace(/_input$/, "") : sensor
if(temp !== undefined && temp !== null) {
temp = this.formatTemp(temp);
};
let tempHot = NaN;
let tempOverheat = NaN;
let tpoints = j.tpoints;
let tpointsString = '';
if(tpoints) {
for(let i of Object.values(tpoints)) {
let t = this.formatTemp(i.temp);
tpointsString += `&#10;${i.type}: ${t} °C`;
if(i.type == 'max' || i.type == 'critical' || i.type == 'emergency') {
if(!(tempOverheat <= t)) {
tempOverheat = t;
};
}
else if(i.type == 'hot') {
tempHot = t;
};
};
};
if(isNaN(tempHot) && isNaN(tempOverheat)) {
tempHot = this.tempHot;
tempOverheat = this.tempOverheat;
};
let itemStyle = (temp >= tempOverheat) ? ' temp-status-overheat':
(temp >= tempHot) ? ' temp-status-hot' : '';
this.tempArea.append(
E('div', { 'class': 'temp-status-list-item' + itemStyle }, [
E('span', {
'class': 'temp-status-hide-item',
'title': _('Hide'),
'click': () => this.hideItem(j.path),
}, '&#935;'),
E('span', {
'class': 'temp-status-temp-value',
}, (temp === undefined || temp === null) ? '-' : temp + ' °C'),
E('span', {
'class': 'temp-status-sensor-name'
}, (tpointsString.length > 0) ?
`<span style="cursor:help; border-bottom:1px dotted" data-tooltip="${tpointsString}">${name}</span>` :
name
),
])
);
};
};
};
if(this.tempArea.childNodes.length == 0) {
this.tempArea.append(
E('em', {}, _('No temperature sensors available'))
);
};
return this.tempArea;
},
makeViewContent() {
this.tempView.innerHTML = '';
this.tempView.append((this.viewType == 'list') ? this.makeTempAreaContent() : this.makeTempTableContent());
this.hiddenNum.textContent = this.hiddenItems.size;
let unhide = document.getElementById('temp-status-unhide-all');
if(unhide) {
unhide.style.display = (this.hiddenItems.size > 0) ? 'inline-block' : 'none';
};
},
hideItem(path) {
this.hiddenItems.add(path);
this.saveSettingsToLocalStorage();
this.makeViewContent();
},
unhideAllItems() {
this.hiddenItems.clear();
this.saveSettingsToLocalStorage();
this.makeViewContent();
},
switchView() {
this.tempView.innerHTML = '';
this.viewType = (this.viewType == 'list') ? 'table' : 'list'
this.saveSettingsToLocalStorage();
this.makeViewContent();
},
load() {
this.restoreSettingsFromLocalStorage();
if(this.sensorsData) {
return (this.sensorsPath.length > 0) ?
L.resolveDefault(this.callTempData(this.sensorsPath), null) :
Promise.resolve(null);
} else {
return L.resolveDefault(this.callSensors(), null);
};
},
render(data) {
if(data) {
if(!this.sensorsData) {
this.sensorsData = data.sensors;
this.sensorsPath = data.temp && new Array(...Object.keys(data.temp));
};
this.tempData = data.temp;
};
if(!this.sensorsData || !this.tempData) {
return;
};
this.makeViewContent();
return E('div', { 'class': 'cbi-section' }, [
E('div', { 'id': 'temp-status-buttons-wrapper' }, [
E('span', {
'class': 'temp-status-button',
'click': () => this.switchView(),
}, _('Switch view')),
E('span', {
'id' : 'temp-status-unhide-all',
'class': 'temp-status-button',
'style': `display:${(this.hiddenItems.size > 0) ? 'inline-block' : 'none'}`,
'click': () => this.unhideAllItems(),
}, [
_('Show hidden sensors'),
' (',
this.hiddenNum,
')',
])
]),
this.tempView,
]);
},
});
@@ -1,582 +0,0 @@
'use strict';
'require dom';
'require poll';
'require request';
'require rpc';
'require view';
'require ui';
document.head.append(E('style', {'type': 'text/css'},
`
:root {
--app-temp-status-temp: #147aff;
--app-temp-status-hot: orange;
--app-temp-status-overheat: red;
}
.svg_background {
width: 100%;
height: 300px;
border: 1px solid #000;
background: #fff';
}
[data-darkmode="true"] .svg_background {
background-color: var(--background-color-high) !important;
}
.graph_legend {
border-bottom: 2px solid;
}
.temp {
border-color: var(--app-temp-status-temp);
}
.hot {
border-color: var(--app-temp-status-hot);
}
.overheat {
border-color: var(--app-temp-status-overheat);
}
svg line.grid {
stroke: black;
stroke-width: 0.1;
}
[data-darkmode="true"] svg line.grid {
stroke: #fff !important;
}
svg text {
fill: #eee;
font-size: 9pt;
font-family: sans-serif;
text-shadow: 1px 1px 1px #000;
}
svg #temp_line {
fill: var(--app-temp-status-temp);
fill-opacity: 0.4;
stroke: var(--app-temp-status-temp);
stroke-width: 1;
}
svg #hot_line {
stroke: var(--app-temp-status-hot);
stroke-width: 1;
}
svg #overheat_line {
stroke: var(--app-temp-status-overheat);
stroke-width: 1;
}
`));
Math.log2 = Math.log2 || (x => Math.log(x) * Math.LOG2E);
return view.extend({
tempHot : 95,
tempOverheat : 105,
pollInterval : 3,
tempBufferSize: 4,
sensorsData : null,
sensorsPath : [],
tempSources : {},
graphPolls : [],
callSensors : rpc.declare({
object: 'luci.temp-status',
method: 'getSensors',
expect: { '': {} },
}),
callTempData : rpc.declare({
object: 'luci.temp-status',
method: 'getTempData',
params: [ 'tpaths' ],
expect: { '': {} },
}),
formatTemp(mc) {
return Number((mc / 1000).toFixed(1));
},
sortFunc(a, b) {
return (a.number > b.number) ? 1 : (a.number < b.number) ? -1 : 0;
},
getSensorsData() {
return this.callSensors().then(data => {
if(data) {
this.sensorsData = data.sensors;
this.sensorsPath = new Array(...Object.keys(data.temp));
let tempData = data.temp;
if(this.sensorsData && tempData) {
for(let e of Object.values(this.sensorsData)) {
e.sort(this.sortFunc);
for(let i of Object.values(e)) {
let sensor = i.title || i.item;
if(i.sources === undefined) {
continue;
};
i.sources.sort(this.sortFunc);
for(let j of i.sources) {
let path = j.path;
let temp = tempData[path];
let name = (j.label !== undefined) ? sensor + " / " + j.label :
(j.item !== undefined) ? sensor + " / " + j.item.replace(/_input$/, "") : sensor
if(temp !== undefined && temp !== null) {
temp = this.formatTemp(temp);
};
let temp_hot = NaN;
let temp_overheat = NaN;
let tpoints = j.tpoints;
if(tpoints) {
for(let i of Object.values(tpoints)) {
let t = this.formatTemp(i.temp);
if(i.type === 'max' || i.type === 'critical' || i.type === 'emergency') {
if(!(temp_overheat <= t)) {
temp_overheat = t;
};
}
else if(i.type === 'hot') {
temp_hot = t;
};
};
};
if(isNaN(temp_hot) && isNaN(temp_overheat)) {
temp_hot = this.tempHot;
temp_overheat = this.tempOverheat;
};
if(!(path in this.tempSources)) {
this.tempSources[path] = {
name,
path,
temp: [ [ new Date().getTime(), temp || 0 ] ],
temp_hot,
temp_overheat,
tpoints,
};
};
};
};
};
};
};
return this.tempSources;
});
},
getTempData() {
return this.callTempData(this.sensorsPath).then(data => {
if(data) {
let tempData = data.temp;
if(this.sensorsData && tempData) {
for(let [path, temp] of Object.entries(tempData)) {
if(path in this.tempSources) {
if(temp !== undefined && temp !== null) {
temp = this.formatTemp(temp);
};
let temp_array = this.tempSources[path].temp;
temp_array.push([ new Date().getTime(), temp || 0 ]);
if(temp_array.length > this.tempBufferSize) {
temp_array.shift();
};
};
};
};
};
return this.tempSources;
});
},
loadSVG(src) {
return request.get(src).then(response => {
if(!response.ok) {
throw new Error(response.statusText);
};
return E('div', {
'class': 'svg_background',
}, E(response.text()));
});
},
updateGraph(tpath, svg, y_peaks, lines, cb) {
let G = svg.firstElementChild;
let view = document.querySelector('#view');
let width = view.offsetWidth - 2;
let height = 300 - 2;
let base_step = 5;
let time_interval = 60;
let time_interval_min = time_interval / 60
let step = base_step * this.pollInterval;
let data_wanted = Math.ceil(width / step);
let timeline_offset = width % step;
let data_values = [];
let line_elements = [];
for(let i = 0; i < lines.length; i++) {
if(lines[i] != null) {
data_values.push([]);
};
};
let info = {
line_current : [],
line_average : [],
line_peak : [],
line_min : [],
hot_line : svg.firstElementChild.getElementById('hot_line'),
overheat_line: svg.firstElementChild.getElementById('overheat_line'),
};
/* prefill datasets */
for(let i = 0; i < data_values.length; i++) {
for(let j = 0; j < data_wanted; j++) {
data_values[i][j] = NaN;
};
};
/* plot horizontal time interval lines */
for(let i = width % (base_step * time_interval); i < width; i += base_step * time_interval) {
let x = i - (timeline_offset);
let line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('x1', x);
line.setAttribute('y1', 0);
line.setAttribute('x2', x);
line.setAttribute('y2', '100%');
line.setAttribute('class', 'grid');
let text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
text.setAttribute('x', x + 5);
text.setAttribute('y', 15);
text.append(document.createTextNode(String((width - i) / base_step / time_interval) + 'm'));
G.append(line);
G.append(text);
};
info.interval = this.pollInterval;
info.timeframe = Math.floor(data_wanted / time_interval * this.pollInterval);
this.graphPolls.push({
tpath,
svg,
y_peaks,
lines,
cb,
info,
width,
height,
step,
data_wanted,
values : data_values,
timestamp : 0,
fill : 1,
});
},
pollData() {
poll.add(L.bind(function() {
return this.getTempData().then(L.bind(function(datasets) {
for(let gi = 0; gi < this.graphPolls.length; gi++) {
let ctx = this.graphPolls[gi];
if(!datasets[ctx.tpath]) {
continue;
};
let data = datasets[ctx.tpath].temp;
if(!data) {
continue;
};
let values = ctx.values;
let lines = ctx.lines;
let info = ctx.info;
let y_peaks = ctx.y_peaks;
let temp_hot = datasets[ctx.tpath].temp_hot;
let temp_overheat = datasets[ctx.tpath].temp_overheat;
let data_scale = 0;
let data_wanted = ctx.data_wanted;
let last_timestamp = NaN;
for(let i = 0, di = 0; di < lines.length; di++) {
if(lines[di] == null) {
continue;
};
for(let j = ctx.timestamp ? 0 : 1; j < data.length; j++) {
/* skip overlapping and empty entries */
if(data[j][0] <= ctx.timestamp) {
continue;
};
if(i == 0) {
ctx.fill++;
last_timestamp = data[j][0];
};
info.line_current[i] = data[j][di + 1];
values[i].push(info.line_current[i]);
};
i++;
};
/* cut off outdated entries */
ctx.fill = Math.min(ctx.fill, data_wanted);
for(let i = 0; i < values.length; i++) {
let len = values[i].length;
values[i] = values[i].slice(len - data_wanted, len);
/* find peaks, averages */
info.line_peak[i] = NaN;
info.line_average[i] = 0;
info.line_min[i] = NaN;
let nonempty = 0;
for(let j = 0; j < values[i].length; j++) {
info.line_peak[i] = isNaN(info.line_peak[i]) ? values[i][j] : Math.max(info.line_peak[i], values[i][j]);
info.line_peak[i] = Number(info.line_peak[i].toFixed(1));
info.line_min[i] = isNaN(info.line_min[i]) ? values[i][j] : Math.min(info.line_min[i], values[i][j]);
info.line_min[i] = Number(info.line_min[i].toFixed(1));
if(!isNaN(values[i][j])) {
nonempty++;
info.line_average[i] += values[i][j];
};
};
info.line_average[i] = info.line_average[i] / nonempty;
info.line_average[i] = Number(info.line_average[i].toFixed(1));
};
info.peak = Math.max.apply(Math, info.line_peak);
/* remember current timestamp, calculate horizontal scale */
if(!isNaN(last_timestamp)) {
ctx.timestamp = last_timestamp;
};
let size = Math.floor(Math.log2(info.peak));
let div = Math.pow(2, size - (size % 10));
if(y_peaks) {
info.peak = (info.peak > y_peaks.t2) ? y_peaks.t2 + y_peaks.incr :
((info.peak > y_peaks.t1) ? y_peaks.t1 + y_peaks.incr : y_peaks.t1);
} else {
let mult = info.peak / div;
mult = (mult < 5) ? 2 : ((mult < 50) ? 10 : ((mult < 500) ? 100 : 1000));
info.peak = info.peak + (mult * div) - (info.peak % (mult * div));
};
data_scale = ctx.height / info.peak;
/* plot data */
for(let i = 0, di = 0; di < lines.length; di++) {
if(lines[di] == null) {
continue;
};
let el = ctx.svg.firstElementChild.getElementById(lines[di].line);
let pt = '0,' + ctx.height;
let y = 0;
if(!el) {
continue;
};
for(let j = 0; j < values[i].length; j++) {
let x = j * ctx.step;
y = ctx.height - Math.floor(values[i][j] * data_scale);
y = isNaN(y) ? ctx.height + 1 : y;
pt += ` ${x},${y}`;
};
pt += ` ${ctx.width},${y} ${ctx.width},${ctx.height}`;
el.setAttribute('points', pt);
i++;
};
/* hot line y */
if(!isNaN(temp_hot)) {
info.hot_line.setAttribute(
'y1', ctx.height - Math.floor(temp_hot * data_scale));
info.hot_line.setAttribute(
'y2', ctx.height - Math.floor(temp_hot * data_scale));
};
/* overheat line y */
if(!isNaN(temp_overheat)) {
info.overheat_line.setAttribute(
'y1', ctx.height - Math.floor(temp_overheat * data_scale));
info.overheat_line.setAttribute(
'y2', ctx.height - Math.floor(temp_overheat * data_scale));
};
info.label_25 = 0.25 * info.peak;
info.label_50 = 0.50 * info.peak;
info.label_75 = 0.75 * info.peak;
if(typeof(ctx.cb) == 'function') {
ctx.cb(ctx.svg, info);
};
};
}, this));
}, this), this.pollInterval);
},
load() {
return Promise.all([
this.loadSVG(L.resource('svg/temperature.svg')),
this.getSensorsData(),
]);
},
render(data) {
let svg = data[0];
let tsources = data[1];
let map = E('div', { 'class': 'cbi-map', 'id': 'map' });
if(!tsources || Object.keys(tsources).length == 0) {
map.append(E('div', { 'class': 'cbi-section' },
E('div', { 'class': 'cbi-section-node' },
E('div', { 'class': 'cbi-value' },
E('em', {}, _('No temperature sensors available'))
)
)
));
} else {
let tabs = E('div');
map.append(tabs);
for(let i of Object.values(tsources)) {
let tsource_name = i.name;
let tsource_path = i.path;
let tsource_hot = i.temp_hot;
let tsource_overheat = i.temp_overheat;
let tsource_tpoints = i.tpoints;
if(!tsource_name || !tsource_path) {
continue;
};
let csvg = svg.cloneNode(true);
let tpoints_section = null;
if(tsource_tpoints) {
tpoints_section = E('div', { 'class': 'cbi-section-node' })
let tpoints_table = E('table', { 'class': 'table' });
tpoints_section.append(tpoints_table);
for(let i of Object.values(tsource_tpoints)) {
tpoints_table.append(
E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td left' }, i.type),
E('td', { 'class': 'td left' }, this.formatTemp(i.temp) + ' °C' ),
])
);
};
};
tabs.append(E('div', { 'class': 'cbi-section', 'data-tab': tsource_path, 'data-tab-title': tsource_name }, [
csvg,
E('div', { 'class': 'right' }, E('small', { 'data-graph': 'scale' }, '-')),
E('br'),
E('table', { 'class': 'table', 'style': 'width:100%;table-layout:fixed' }, [
E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td right top' }, E('strong', { 'class': 'graph_legend temp' }, _('Temperature') + ':')),
E('td', { 'class': 'td', 'data-graph': 'temp_cur' }, '-'),
E('td', { 'class': 'td right top' }, E('strong', {}, _('Minimum:'))),
E('td', { 'class': 'td', 'data-graph': 'temp_min' }, '-'),
E('td', { 'class': 'td right top' }, E('strong', {}, _('Average:'))),
E('td', { 'class': 'td', 'data-graph': 'temp_avg' }, '-'),
E('td', { 'class': 'td right top' }, E('strong', {}, _('Peak:'))),
E('td', { 'class': 'td', 'data-graph': 'temp_peak' }, '-'),
]),
(!isNaN(tsource_hot) ?
E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td right top' }, E('strong', { 'class': 'graph_legend hot' }, _('Hot:'))),
E('td', { 'class': 'td', 'data-graph': 'temp_hot' }, tsource_hot + ' °C'),
E('td', { 'class': 'td right top' }),
E('td', { 'class': 'td right top' }),
E('td', { 'class': 'td right top' }),
E('td', { 'class': 'td right top' }),
E('td', { 'class': 'td right top' }),
E('td', { 'class': 'td right top' }),
]) : ''
),
(!isNaN(tsource_overheat) ?
E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td right top' }, E('strong', { 'class': 'graph_legend overheat' }, _('Overheat:'))),
E('td', { 'class': 'td', 'data-graph': 'temp_overheat' }, tsource_overheat + ' °C'),
E('td', { 'class': 'td right top' }),
E('td', { 'class': 'td right top' }),
E('td', { 'class': 'td right top' }),
E('td', { 'class': 'td right top' }),
E('td', { 'class': 'td right top' }),
E('td', { 'class': 'td right top' }),
]) : ''
),
]),
E('br'),
tpoints_section || '',
E('br'),
]));
this.updateGraph(
tsource_path,
csvg,
{ 't1': 120, 't2': 180, 'incr': 60 },
[ { 'line': 'temp_line' } ],
(svg, info) => {
let G = svg.firstElementChild, tab = svg.parentNode;
G.getElementById('label_25').firstChild.data = '%d °C'.format(info.label_25);
G.getElementById('label_50').firstChild.data = '%d °C'.format(info.label_50);
G.getElementById('label_75').firstChild.data = '%d °C'.format(info.label_75);
tab.querySelector('[data-graph="scale"]').firstChild.data = _('(%d minute window, %d second interval)').format(info.timeframe, info.interval);
dom.content(tab.querySelector('[data-graph="temp_cur"]'), info.line_current[0] + ' °C');
dom.content(tab.querySelector('[data-graph="temp_min"]'), info.line_min[0] + ' °C');
dom.content(tab.querySelector('[data-graph="temp_avg"]'), info.line_average[0] + ' °C');
dom.content(tab.querySelector('[data-graph="temp_peak"]'), info.line_peak[0] + ' °C');
}
);
};
ui.tabs.initTabGroup(tabs.childNodes);
this.pollData();
};
return E([], [
E('h2', _('Temperature')),
E('div', {'class': 'cbi-map-descr'}, _('This page displays the temperature sensors data.')),
map,
]);
},
handleSaveApply: null,
handleSave : null,
handleReset : null,
});
-50
View File
@@ -1,50 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: \n"
"POT-Creation-Date: \n"
"PO-Revision-Date: \n"
"Last-Translator: DeciBelioS\n"
"Language-Team: \n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 3.5.1\n"
"X-Poedit-SourceCharset: UTF-8\n"
msgid "(%d minute window, %d second interval)"
msgstr "(ventana de %d minutos, intervalo de %d segundos)"
msgid "Average:"
msgstr "Promedio:"
msgid "Critical:"
msgstr "Crítico:"
msgid "Hide"
msgstr "Ocultar"
msgid "Hot:"
msgstr "Hot:"
msgid "Minimum:"
msgstr "Mínimo:"
msgid "No temperature sensors available"
msgstr "No hay sensores de temperatura disponibles"
msgid "Peak:"
msgstr "Máximo:"
msgid "Sensor"
msgstr "Sensor"
msgid "Show hidden sensors"
msgstr "Mostrar sensores ocultos"
msgid "Temperature"
msgstr "Temperatura"
msgid "This page displays the temperature sensors data."
msgstr "Esta página muestra los datos de los sensores de temperatura."
-52
View File
@@ -1,52 +0,0 @@
msgid ""
msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Project-Id-Version: \n"
"POT-Creation-Date: \n"
"PO-Revision-Date: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 2.3\n"
"Last-Translator: \n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);\n"
"Language: ru\n"
msgid "(%d minute window, %d second interval)"
msgstr "(%d минутное окно, %d секундный интервал)"
msgid "Average:"
msgstr "Средняя:"
msgid "Hide"
msgstr "Скрыть"
msgid "Hot:"
msgstr "Горячая:"
msgid "Minimum:"
msgstr "Минимальная:"
msgid "No temperature sensors available"
msgstr "Нет доступных датчиков температуры"
msgid "Overheat:"
msgstr "Перегрев:"
msgid "Peak:"
msgstr "Пиковая:"
msgid "Sensor"
msgstr "Датчик"
msgid "Show hidden sensors"
msgstr "Показать скрытые датчики"
msgid "Switch view"
msgstr "Переключить вид"
msgid "Temperature"
msgstr "Температура"
msgid "This page displays the temperature sensors data."
msgstr "На этой странице отображаются данные датчиков температуры."
@@ -1,42 +0,0 @@
msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8"
msgid "(%d minute window, %d second interval)"
msgstr ""
msgid "Average:"
msgstr ""
msgid "Hide"
msgstr ""
msgid "Hot:"
msgstr ""
msgid "Minimum:"
msgstr ""
msgid "No temperature sensors available"
msgstr ""
msgid "Overheat:"
msgstr ""
msgid "Peak:"
msgstr ""
msgid "Sensor"
msgstr ""
msgid "Show hidden sensors"
msgstr ""
msgid "Switch view"
msgstr ""
msgid "Temperature"
msgstr ""
msgid "This page displays the temperature sensors data."
msgstr ""
-52
View File
@@ -1,52 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: \n"
"POT-Creation-Date: \n"
"PO-Revision-Date: \n"
"Last-Translator: egemen <95934830+projectaccc@users.noreply.github.com>\n"
"Language-Team: \n"
"Language: tr\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 3.9\n"
msgid "(%d minute window, %d second interval)"
msgstr "(%d dakikalık aralık, %d saniyelik sıklık)"
msgid "Average:"
msgstr "Ortalama:"
msgid "Hide"
msgstr "Gizle"
msgid "Hot:"
msgstr "Sıcak:"
msgid "Minimum:"
msgstr "En Düşük:"
msgid "No temperature sensors available"
msgstr "Kullanılabilir sıcaklık sensörü yok"
msgid "Overheat:"
msgstr "Aşırı Isınma:"
msgid "Peak:"
msgstr "En Yüksek:"
msgid "Sensor"
msgstr "Sensör"
msgid "Show hidden sensors"
msgstr "Gizlenmiş sensörleri göster"
msgid "Switch view"
msgstr "Görünümü değiştir"
msgid "Temperature"
msgstr "Sıcaklık"
msgid "This page displays the temperature sensors data."
msgstr "Bu sayfada sıcaklık sensörü verileri görüntülenir."
@@ -1,52 +0,0 @@
msgid ""
msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Project-Id-Version: \n"
"POT-Creation-Date: \n"
"PO-Revision-Date: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 2.3\n"
"Last-Translator: \n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);\n"
"Language: zh_Hans\n"
msgid "(%d minute window, %d second interval)"
msgstr "(最近 %d 分钟信息,每 %d 秒刷新)"
msgid "Average:"
msgstr "平均:"
msgid "Hide"
msgstr "隐藏"
msgid "Hot:"
msgstr "最热:"
msgid "Minimum:"
msgstr "最低:"
msgid "No temperature sensors available"
msgstr "温度传感器不可用"
msgid "Overheat:"
msgstr "过热:"
msgid "Peak:"
msgstr "峰值:"
msgid "Sensor"
msgstr "传感器"
msgid "Show hidden sensors"
msgstr "显示已隐藏的传感器"
msgid "Switch view"
msgstr "切换视图"
msgid "Temperature"
msgstr "温度"
msgid "This page displays the temperature sensors data."
msgstr "此页面显示温度传感器的信息。"
@@ -1,13 +0,0 @@
{
"admin/status/realtime/temperature": {
"title": "Temperature",
"order": 10,
"action": {
"type": "view",
"path": "status/temperature"
},
"depends": {
"acl": [ "luci-app-temp-status" ]
}
}
}
@@ -1,10 +0,0 @@
{
"luci-app-temp-status": {
"description": "Grant access to temp-status procedures",
"read": {
"ubus": {
"luci.temp-status": [ "getSensors", "getTempData", "getTempStatus" ]
}
}
}
}
@@ -1,202 +0,0 @@
'use strict';
import { chdir, lsdir, readfile, readlink, realpath, stat } from 'fs';
const sysHwmon = '/sys/class/hwmon';
const sysThermal = '/sys/class/thermal';
function readFile(path) {
let r = readfile(path);
return r && trim(r);
}
function findTPoints(thermalDirPath) {
let tPoints = {};
if(stat(thermalDirPath)?.type == 'directory') {
for(let tItem in lsdir(thermalDirPath)) {
if(match(tItem, /^trip_point_[0-9]+_type$/)) {
let pTypePath = sprintf('%s/%s', thermalDirPath, tItem);
let m = match(tItem, /[0-9]+/);
let pNumber = m && m[0];
let pType = readFile(pTypePath);
let pTemp = readFile(replace(pTypePath, /_type$/, '_temp'));
if(pNumber && pType && pTemp) {
tPoints[pNumber] = { type: pType, temp: int(pTemp) };
}
}
}
}
return (length(tPoints) > 0) ? tPoints : null;
}
function getHwmonData(tDevPath, tempData) {
let hwmon = [];
if(stat(sysHwmon)?.type == 'directory') {
for(let item in lsdir(sysHwmon)) {
if(match(item, /^hwmon[0-9]+$/)) {
let hwmonDirPath = sprintf('%s/%s', sysHwmon, item);
let deviceDirPath = readlink(sprintf('%s/device', hwmonDirPath));
if(deviceDirPath) {
chdir(hwmonDirPath);
let path = realpath(deviceDirPath);
if(path) {
tDevPath[path] = true;
}
}
if(stat(hwmonDirPath)?.type == 'directory') {
let m = match(item, /[0-9]+/);
let dNumber = m && m[0];
let title = readFile(hwmonDirPath + '/name');
let sources = [];
for(let source in lsdir(hwmonDirPath)) {
if(match(source, /^temp[0-9]+_input$/)) {
let tPoints = {};
let m = match(source, /[0-9]+/);
let sNumber = m && m[0];
let sourceFilePath = sprintf('%s/%s', hwmonDirPath, source);
let temp = readFile(sourceFilePath);
if(sNumber && temp) {
tempData[sourceFilePath] = int(temp);
let label = readFile(sprintf(
"%s/%s", hwmonDirPath, replace(source, /_input$/, '_label')));
let tPointEmer = readFile(sprintf(
"%s/%s", hwmonDirPath, replace(source, /_input$/, '_emergency')));
if(tPointEmer) {
tPoints['0'] = { type: 'emergency', temp: int(tPointEmer) };
}
let tPointCrit = readFile(sprintf(
"%s/%s", hwmonDirPath, replace(source, /_input$/, '_crit')));
if(tPointCrit) {
tPoints['1'] = { type: 'critical', temp: int(tPointCrit) };
}
let tPointMax = readFile(sprintf(
"%s/%s", hwmonDirPath, replace(source, /_input$/, '_max')));
if(tPointMax) {
tPoints['2'] = { type: 'max', temp: int(tPointMax) };
}
if(deviceDirPath && length(tPoints) == 0) {
tPoints = (findTPoints(deviceDirPath) || tPoints);
}
let sDict = {
number: int(sNumber),
item : source,
path : sourceFilePath,
temp : int(temp),
};
if(label) {
sDict['label'] = label;
};
if(length(tPoints) > 0) {
sDict['tpoints'] = tPoints;
}
push(sources, sDict);
}
}
}
if(dNumber && length(sources) > 0) {
let d = {
number : int(dNumber),
item : item,
sources: sources,
};
if(title) {
d['title'] = title;
}
push(hwmon, d);
}
}
}
}
}
return hwmon;
}
function getThermalData(tDevPath, tempData) {
let thermal = [];
if(stat(sysThermal)?.type == 'directory') {
chdir(sysThermal);
for(let item in lsdir(sysThermal)) {
if(match(item, /^thermal_zone[0-9]+$/)) {
let thermalDirPath = sprintf('%s/%s', sysThermal, item);
let deviceDirPath = readlink(thermalDirPath);
if(deviceDirPath && exists(tDevPath, realpath(deviceDirPath))) {
continue;
}
let m = match(item, /[0-9]+/);
let number = m && m[0];
let tempFilePath = thermalDirPath + '/temp';
let temp = readFile(tempFilePath);
if(number && temp) {
tempData[tempFilePath] = int(temp);
let title = readFile(thermalDirPath + '/type');
let tPoints = findTPoints(thermalDirPath);
let sDict = {
number: 0,
path : tempFilePath,
temp : int(temp),
};
if(tPoints && length(tPoints) > 0) {
sDict['tpoints'] = tPoints;
}
let tDict = {
number : int(number),
item : item,
sources: [ sDict ],
};
if(title) {
tDict['title'] = title;
}
push(thermal, tDict);
}
}
}
}
return thermal;
}
function getSensors() {
const tDevPath = {};
const tempData = {};
let sensors = {};
let hwmon = getHwmonData(tDevPath, tempData);
if(length(hwmon) > 0) {
sensors['0'] = hwmon;
}
let thermal = getThermalData(tDevPath, tempData);
if(length(thermal) > 0) {
sensors['1'] = thermal;
}
return { sensors: sensors, temp: tempData };
}
const methods = {
getSensors: {
call: getSensors
},
getTempData: {
args: { tpaths: [] },
call: function(request) {
const tpaths = request.args?.tpaths;
let tData = {};
if(tpaths && length(tpaths) > 0) {
for(let i in tpaths) {
let t = readFile(i);
tData[i] = (t && int(t));
}
}
return { temp: tData };
}
},
/*
* For compatibility with <= v0.6.
* Will be removed in the future.
*/
getTempStatus: {
call: function() { return getSensors().sensors }
}
};
return { 'luci.temp-status': methods };
Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB