├── screenshots
├── 01.jpg
└── 02.jpg
├── root
└── usr
│ └── share
│ ├── rpcd
│ ├── acl.d
│ │ └── luci-app-temp-status.json
│ └── ucode
│ │ └── luci.temp-status
│ └── luci
│ └── menu.d
│ └── luci-app-temp-status.json
├── Makefile
├── po
├── templates
│ └── temp-status.pot
├── zh-cn
│ └── temp-status.po
├── es
│ └── temp-status.po
└── ru
│ └── temp-status.po
├── htdocs
└── luci-static
│ └── resources
│ ├── svg
│ └── temperature.svg
│ └── view
│ └── status
│ ├── include
│ └── 27_temperature.js
│ └── temperature.js
├── LICENSE
└── README.md
/screenshots/01.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gSpotx2f/luci-app-temp-status/HEAD/screenshots/01.jpg
--------------------------------------------------------------------------------
/screenshots/02.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gSpotx2f/luci-app-temp-status/HEAD/screenshots/02.jpg
--------------------------------------------------------------------------------
/root/usr/share/rpcd/acl.d/luci-app-temp-status.json:
--------------------------------------------------------------------------------
1 | {
2 | "luci-app-temp-status": {
3 | "description": "Grant access to temp-status procedures",
4 | "read": {
5 | "ubus": {
6 | "luci.temp-status": [ "getSensors", "getTempData", "getTempStatus" ]
7 | }
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/root/usr/share/luci/menu.d/luci-app-temp-status.json:
--------------------------------------------------------------------------------
1 | {
2 | "admin/status/realtime/temperature": {
3 | "title": "Temperature",
4 | "order": 10,
5 | "action": {
6 | "type": "view",
7 | "path": "status/temperature"
8 | },
9 | "depends": {
10 | "acl": [ "luci-app-temp-status" ]
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (C) 2025 gSpot (https://github.com/gSpotx2f/luci-app-temp-status)
3 | #
4 | # This is free software, licensed under the MIT License.
5 | #
6 |
7 | include $(TOPDIR)/rules.mk
8 |
9 | PKG_NAME:=luci-app-temp-status
10 | PKG_VERSION:=0.7.1
11 | PKG_RELEASE:=2
12 | LUCI_TITLE:=Temperature sensors data for the LuCI status page
13 | LUCI_DEPENDS:=+ucode +ucode-mod-fs
14 | LUCI_PKGARCH:=all
15 | PKG_LICENSE:=MIT
16 |
17 | #include ../../luci.mk
18 | include $(TOPDIR)/feeds/luci/luci.mk
19 |
20 | # call BuildPackage - OpenWrt buildroot signature
21 |
--------------------------------------------------------------------------------
/po/templates/temp-status.pot:
--------------------------------------------------------------------------------
1 | msgid ""
2 | msgstr "Content-Type: text/plain; charset=UTF-8"
3 |
4 | msgid "(%d minute window, %d second interval)"
5 | msgstr ""
6 |
7 | msgid "Average:"
8 | msgstr ""
9 |
10 | msgid "Hide"
11 | msgstr ""
12 |
13 | msgid "Hot:"
14 | msgstr ""
15 |
16 | msgid "Minimum:"
17 | msgstr ""
18 |
19 | msgid "No temperature sensors available"
20 | msgstr ""
21 |
22 | msgid "Overheat:"
23 | msgstr ""
24 |
25 | msgid "Peak:"
26 | msgstr ""
27 |
28 | msgid "Sensor"
29 | msgstr ""
30 |
31 | msgid "Show hidden sensors"
32 | msgstr ""
33 |
34 | msgid "Temperature"
35 | msgstr ""
36 |
37 | msgid "This page displays the temperature sensors data."
38 | msgstr ""
39 |
40 |
--------------------------------------------------------------------------------
/htdocs/luci-static/resources/svg/temperature.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
19 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 gSpotx2f
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/po/zh-cn/temp-status.po:
--------------------------------------------------------------------------------
1 | msgid ""
2 | msgstr ""
3 | "Content-Type: text/plain; charset=UTF-8\n"
4 | "Project-Id-Version: \n"
5 | "POT-Creation-Date: \n"
6 | "PO-Revision-Date: \n"
7 | "Language-Team: \n"
8 | "MIME-Version: 1.0\n"
9 | "Content-Transfer-Encoding: 8bit\n"
10 | "X-Generator: Poedit 2.3\n"
11 | "Last-Translator: \n"
12 | "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"
13 | "Language: zh-cn\n"
14 |
15 | msgid "(%d minute window, %d second interval)"
16 | msgstr "(最近 %d 分钟信息,每 %d 秒刷新)"
17 |
18 | msgid "Average:"
19 | msgstr "平均:"
20 |
21 | msgid "Hide"
22 | msgstr "隐藏"
23 |
24 | msgid "Hot:"
25 | msgstr "最热:"
26 |
27 | msgid "Minimum:"
28 | msgstr "最低:"
29 |
30 | msgid "No temperature sensors available"
31 | msgstr "温度传感器不可用"
32 |
33 | msgid "Overheat:"
34 | msgstr "过热:"
35 |
36 | msgid "Peak:"
37 | msgstr "峰值:"
38 |
39 | msgid "Sensor"
40 | msgstr "传感器"
41 |
42 | msgid "Show hidden sensors"
43 | msgstr "显示已隐藏的传感器"
44 |
45 | msgid "Temperature"
46 | msgstr "温度"
47 |
48 | msgid "This page displays the temperature sensors data."
49 | msgstr "此页面显示温度传感器的信息。"
50 |
--------------------------------------------------------------------------------
/po/es/temp-status.po:
--------------------------------------------------------------------------------
1 | msgid ""
2 | msgstr ""
3 | "Project-Id-Version: \n"
4 | "POT-Creation-Date: \n"
5 | "PO-Revision-Date: \n"
6 | "Last-Translator: DeciBelioS\n"
7 | "Language-Team: \n"
8 | "Language: es\n"
9 | "MIME-Version: 1.0\n"
10 | "Content-Type: text/plain; charset=UTF-8\n"
11 | "Content-Transfer-Encoding: 8bit\n"
12 | "Plural-Forms: nplurals=2; plural=(n != 1);\n"
13 | "X-Generator: Poedit 3.5.1\n"
14 | "X-Poedit-SourceCharset: UTF-8\n"
15 |
16 | msgid "(%d minute window, %d second interval)"
17 | msgstr "(ventana de %d minutos, intervalo de %d segundos)"
18 |
19 | msgid "Average:"
20 | msgstr "Promedio:"
21 |
22 | msgid "Critical:"
23 | msgstr "Crítico:"
24 |
25 | msgid "Hide"
26 | msgstr "Ocultar"
27 |
28 | msgid "Hot:"
29 | msgstr "Hot:"
30 |
31 | msgid "Minimum:"
32 | msgstr "Mínimo:"
33 |
34 | msgid "No temperature sensors available"
35 | msgstr "No hay sensores de temperatura disponibles"
36 |
37 | msgid "Peak:"
38 | msgstr "Máximo:"
39 |
40 | msgid "Sensor"
41 | msgstr "Sensor"
42 |
43 | msgid "Show hidden sensors"
44 | msgstr "Mostrar sensores ocultos"
45 |
46 | msgid "Temperature"
47 | msgstr "Temperatura"
48 |
49 | msgid "This page displays the temperature sensors data."
50 | msgstr "Esta página muestra los datos de los sensores de temperatura."
51 |
--------------------------------------------------------------------------------
/po/ru/temp-status.po:
--------------------------------------------------------------------------------
1 | msgid ""
2 | msgstr ""
3 | "Content-Type: text/plain; charset=UTF-8\n"
4 | "Project-Id-Version: \n"
5 | "POT-Creation-Date: \n"
6 | "PO-Revision-Date: \n"
7 | "Language-Team: \n"
8 | "MIME-Version: 1.0\n"
9 | "Content-Transfer-Encoding: 8bit\n"
10 | "X-Generator: Poedit 2.3\n"
11 | "Last-Translator: \n"
12 | "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"
13 | "Language: ru\n"
14 |
15 | msgid "(%d minute window, %d second interval)"
16 | msgstr "(%d минутное окно, %d секундный интервал)"
17 |
18 | msgid "Average:"
19 | msgstr "Средняя:"
20 |
21 | msgid "Hide"
22 | msgstr "Скрыть"
23 |
24 | msgid "Hot:"
25 | msgstr "Горячая:"
26 |
27 | msgid "Minimum:"
28 | msgstr "Минимальная:"
29 |
30 | msgid "No temperature sensors available"
31 | msgstr "Нет доступных датчиков температуры"
32 |
33 | msgid "Overheat:"
34 | msgstr "Перегрев:"
35 |
36 | msgid "Peak:"
37 | msgstr "Пиковая:"
38 |
39 | msgid "Sensor"
40 | msgstr "Датчик"
41 |
42 | msgid "Show hidden sensors"
43 | msgstr "Показать скрытые датчики"
44 |
45 | msgid "Temperature"
46 | msgstr "Температура"
47 |
48 | msgid "This page displays the temperature sensors data."
49 | msgstr "На этой странице отображаются данные датчиков температуры."
50 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # luci-app-temp-status
2 | Temperature sensors data for the LuCI status page (OpenWrt webUI).
3 |
4 | OpenWrt >= 22.03.
5 |
6 | **Dependences:** ucode, ucode-mod-fs.
7 |
8 | ## Installation notes
9 |
10 | **OpenWrt >= 25.12:**
11 |
12 | wget --no-check-certificate -O /tmp/luci-app-temp-status-0.7.1-r2.apk https://github.com/gSpotx2f/packages-openwrt/raw/master/25.12/luci-app-temp-status-0.7.1-r2.apk
13 | apk --allow-untrusted add /tmp/luci-app-temp-status-0.7.1-r2.apk
14 | rm /tmp/luci-app-temp-status-0.7.1-r2.apk
15 | service rpcd restart
16 |
17 | i18n-ru:
18 |
19 | wget --no-check-certificate -O /tmp/luci-i18n-temp-status-ru-0.7.1-r2.apk https://github.com/gSpotx2f/packages-openwrt/raw/master/25.12/luci-i18n-temp-status-ru-0.7.1-r2.apk
20 | apk --allow-untrusted add /tmp/luci-i18n-temp-status-ru-0.7.1-r2.apk
21 | rm /tmp/luci-i18n-temp-status-ru-0.7.1-r2.apk
22 |
23 | **OpenWrt <= 24.10:**
24 |
25 | wget --no-check-certificate -O /tmp/luci-app-temp-status_0.7.1-r2_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/24.10/luci-app-temp-status_0.7.1-r2_all.ipk
26 | opkg install /tmp/luci-app-temp-status_0.7.1-r2_all.ipk
27 | rm /tmp/luci-app-temp-status_0.7.1-r2_all.ipk
28 | service rpcd restart
29 |
30 | i18n-ru:
31 |
32 | wget --no-check-certificate -O /tmp/luci-i18n-temp-status-ru_0.7.1-r2_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/24.10/luci-i18n-temp-status-ru_0.7.1-r2_all.ipk
33 | opkg install /tmp/luci-i18n-temp-status-ru_0.7.1-r2_all.ipk
34 | rm /tmp/luci-i18n-temp-status-ru_0.7.1-r2_all.ipk
35 |
36 | ## Screenshots:
37 |
38 | 
39 |
--------------------------------------------------------------------------------
/root/usr/share/rpcd/ucode/luci.temp-status:
--------------------------------------------------------------------------------
1 |
2 | 'use strict';
3 |
4 | import { chdir, lsdir, readfile, readlink, realpath, stat } from 'fs';
5 |
6 | const sysHwmon = '/sys/class/hwmon';
7 | const sysThermal = '/sys/class/thermal';
8 |
9 | function readFile(path) {
10 | let r = readfile(path);
11 | return r && trim(r);
12 | }
13 |
14 | function findTPoints(thermalDirPath) {
15 | let tPoints = {};
16 | if(stat(thermalDirPath)?.type == 'directory') {
17 | for(let tItem in lsdir(thermalDirPath)) {
18 | if(match(tItem, /^trip_point_[0-9]+_type$/)) {
19 | let pTypePath = sprintf('%s/%s', thermalDirPath, tItem);
20 | let m = match(tItem, /[0-9]+/);
21 | let pNumber = m && m[0];
22 | let pType = readFile(pTypePath);
23 | let pTemp = readFile(replace(pTypePath, /_type$/, '_temp'));
24 | if(pNumber && pType && pTemp) {
25 | tPoints[pNumber] = { type: pType, temp: int(pTemp) };
26 | }
27 | }
28 | }
29 | }
30 | return (length(tPoints) > 0) ? tPoints : null;
31 | }
32 |
33 | function getHwmonData(tDevPath, tempData) {
34 | let hwmon = [];
35 | if(stat(sysHwmon)?.type == 'directory') {
36 | for(let item in lsdir(sysHwmon)) {
37 | if(match(item, /^hwmon[0-9]+$/)) {
38 | let hwmonDirPath = sprintf('%s/%s', sysHwmon, item);
39 | let deviceDirPath = readlink(sprintf('%s/device', hwmonDirPath));
40 | if(deviceDirPath) {
41 | chdir(hwmonDirPath);
42 | let path = realpath(deviceDirPath);
43 | if(path) {
44 | tDevPath[path] = true;
45 | }
46 | }
47 | if(stat(hwmonDirPath)?.type == 'directory') {
48 | let m = match(item, /[0-9]+/);
49 | let dNumber = m && m[0];
50 | let title = readFile(hwmonDirPath + '/name');
51 | let sources = [];
52 | for(let source in lsdir(hwmonDirPath)) {
53 | if(match(source, /^temp[0-9]+_input$/)) {
54 | let tPoints = {};
55 | let m = match(source, /[0-9]+/);
56 | let sNumber = m && m[0];
57 | let sourceFilePath = sprintf('%s/%s', hwmonDirPath, source);
58 | let temp = readFile(sourceFilePath);
59 | if(sNumber && temp) {
60 | tempData[sourceFilePath] = int(temp);
61 | let label = readFile(sprintf(
62 | "%s/%s", hwmonDirPath, replace(source, /_input$/, '_label')));
63 | let tPointEmer = readFile(sprintf(
64 | "%s/%s", hwmonDirPath, replace(source, /_input$/, '_emergency')));
65 | if(tPointEmer) {
66 | tPoints['0'] = { type: 'emergency', temp: int(tPointEmer) };
67 | }
68 | let tPointCrit = readFile(sprintf(
69 | "%s/%s", hwmonDirPath, replace(source, /_input$/, '_crit')));
70 | if(tPointCrit) {
71 | tPoints['1'] = { type: 'critical', temp: int(tPointCrit) };
72 | }
73 | let tPointMax = readFile(sprintf(
74 | "%s/%s", hwmonDirPath, replace(source, /_input$/, '_max')));
75 | if(tPointMax) {
76 | tPoints['2'] = { type: 'max', temp: int(tPointMax) };
77 | }
78 | if(deviceDirPath && length(tPoints) == 0) {
79 | tPoints = (findTPoints(deviceDirPath) || tPoints);
80 | }
81 | let sDict = {
82 | number: int(sNumber),
83 | item : source,
84 | path : sourceFilePath,
85 | temp : int(temp),
86 | };
87 | if(label) {
88 | sDict['label'] = label;
89 | };
90 | if(length(tPoints) > 0) {
91 | sDict['tpoints'] = tPoints;
92 | }
93 | push(sources, sDict);
94 | }
95 | }
96 | }
97 | if(dNumber && length(sources) > 0) {
98 | let d = {
99 | number : int(dNumber),
100 | item : item,
101 | sources: sources,
102 | };
103 | if(title) {
104 | d['title'] = title;
105 | }
106 | push(hwmon, d);
107 | }
108 | }
109 | }
110 | }
111 | }
112 | return hwmon;
113 | }
114 |
115 | function getThermalData(tDevPath, tempData) {
116 | let thermal = [];
117 | if(stat(sysThermal)?.type == 'directory') {
118 | chdir(sysThermal);
119 | for(let item in lsdir(sysThermal)) {
120 | if(match(item, /^thermal_zone[0-9]+$/)) {
121 | let thermalDirPath = sprintf('%s/%s', sysThermal, item);
122 | let deviceDirPath = readlink(thermalDirPath);
123 | if(deviceDirPath && exists(tDevPath, realpath(deviceDirPath))) {
124 | continue;
125 | }
126 | let m = match(item, /[0-9]+/);
127 | let number = m && m[0];
128 | let tempFilePath = thermalDirPath + '/temp';
129 | let temp = readFile(tempFilePath);
130 | if(number && temp) {
131 | tempData[tempFilePath] = int(temp);
132 | let title = readFile(thermalDirPath + '/type');
133 | let tPoints = findTPoints(thermalDirPath);
134 | let sDict = {
135 | number: 0,
136 | path : tempFilePath,
137 | temp : int(temp),
138 | };
139 | if(tPoints && length(tPoints) > 0) {
140 | sDict['tpoints'] = tPoints;
141 | }
142 | let tDict = {
143 | number : int(number),
144 | item : item,
145 | sources: [ sDict ],
146 | };
147 | if(title) {
148 | tDict['title'] = title;
149 | }
150 | push(thermal, tDict);
151 | }
152 | }
153 | }
154 | }
155 | return thermal;
156 | }
157 |
158 | function getSensors() {
159 | const tDevPath = {};
160 | const tempData = {};
161 | let sensors = {};
162 | let hwmon = getHwmonData(tDevPath, tempData);
163 | if(length(hwmon) > 0) {
164 | sensors['0'] = hwmon;
165 | }
166 | let thermal = getThermalData(tDevPath, tempData);
167 | if(length(thermal) > 0) {
168 | sensors['1'] = thermal;
169 | }
170 | return { sensors: sensors, temp: tempData };
171 | }
172 |
173 | const methods = {
174 | getSensors: {
175 | call: getSensors
176 | },
177 |
178 | getTempData: {
179 | args: { tpaths: [] },
180 | call: function(request) {
181 | const tpaths = request.args?.tpaths;
182 | let tData = {};
183 | if(tpaths && length(tpaths) > 0) {
184 | for(let i in tpaths) {
185 | let t = readFile(i);
186 | tData[i] = (t && int(t));
187 | }
188 | }
189 | return { temp: tData };
190 | }
191 | },
192 |
193 | /*
194 | * For compatibility with <= v0.6.
195 | * Will be removed in the future.
196 | */
197 | getTempStatus: {
198 | call: function() { return getSensors().sensors }
199 | }
200 | };
201 |
202 | return { 'luci.temp-status': methods };
203 |
--------------------------------------------------------------------------------
/htdocs/luci-static/resources/view/status/include/27_temperature.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | 'require baseclass';
3 | 'require rpc';
4 |
5 | document.head.append(E('style', {'type': 'text/css'},
6 | `
7 | :root {
8 | --app-temp-status-font-color: #2e2e2e;
9 | --app-temp-status-hot-color: #fff7e2;
10 | --app-temp-status-overheat-color: #ffe9e8;
11 | }
12 | :root[data-darkmode="true"] {
13 | --app-temp-status-font-color: #fff;
14 | --app-temp-status-hot-color: #8d7000;
15 | --app-temp-status-overheat-color: #a93734;
16 | }
17 | .temp-status-hot {
18 | background-color: var(--app-temp-status-hot-color) !important;
19 | color: var(--app-temp-status-font-color) !important;
20 | }
21 | .temp-status-hot .td {
22 | color: var(--app-temp-status-font-color) !important;
23 | }
24 | .temp-status-hot td {
25 | color: var(--app-temp-status-font-color) !important;
26 | }
27 | .temp-status-overheat {
28 | background-color: var(--app-temp-status-overheat-color) !important;
29 | color: var(--app-temp-status-font-color) !important;
30 | }
31 | .temp-status-overheat .td {
32 | color: var(--app-temp-status-font-color) !important;
33 | }
34 | .temp-status-overheat td {
35 | color: var(--app-temp-status-font-color) !important;
36 | }
37 | .temp-status-unhide-all {
38 | display: inline-block;
39 | cursor: pointer;
40 | margin: 2px !important;
41 | padding: 2px 4px;
42 | border: 1px dotted;
43 | -webkit-border-radius: 4px;
44 | -moz-border-radius: 4px;
45 | border-radius: 4px;
46 | opacity: 0.7;
47 | }
48 | .temp-status-unhide-all:hover {
49 | opacity: 0.9;
50 | }
51 | .temp-status-hide-item {
52 | display: inline-block;
53 | cursor: pointer;
54 | margin: 0 0.5em 0 0 !important;
55 | padding: 0 4px;
56 | border: 1px dotted;
57 | -webkit-border-radius: 4px;
58 | -moz-border-radius: 4px;
59 | border-radius: 4px;
60 | opacity: 0.7;
61 | }
62 | .temp-status-hide-item:hover {
63 | opacity: 1.0;
64 | }
65 | `));
66 |
67 | return baseclass.extend({
68 | title : _('Temperature'),
69 |
70 | viewName : 'temp-status',
71 |
72 | tempHot : 95,
73 |
74 | tempOverheat: 105,
75 |
76 | sensorsData : null,
77 |
78 | tempData : null,
79 |
80 | sensorsPath : [],
81 |
82 | hiddenItems : new Set(),
83 |
84 | tempTable : E('table', { 'class': 'table' }),
85 |
86 | callSensors : rpc.declare({
87 | object: 'luci.temp-status',
88 | method: 'getSensors',
89 | expect: { '': {} },
90 | }),
91 |
92 | callTempData: rpc.declare({
93 | object: 'luci.temp-status',
94 | method: 'getTempData',
95 | params: [ 'tpaths' ],
96 | expect: { '': {} },
97 | }),
98 |
99 | formatTemp(mc) {
100 | return Number((mc / 1000).toFixed(1));
101 | },
102 |
103 | sortFunc(a, b) {
104 | return (a.number > b.number) ? 1 : (a.number < b.number) ? -1 : 0;
105 | },
106 |
107 | restoreSettingsFromLocalStorage() {
108 | let hiddenItems = localStorage.getItem(`luci-app-${this.viewName}-hiddenItems`);
109 | if(hiddenItems) {
110 | this.hiddenItems = new Set(hiddenItems.split(','));
111 | };
112 | },
113 |
114 | saveSettingsToLocalStorage() {
115 | localStorage.setItem(
116 | `luci-app-${this.viewName}-hiddenItems`, Array.from(this.hiddenItems).join(','));
117 | },
118 |
119 | makeTempTableContent() {
120 | this.tempTable.innerHTML = '';
121 | this.tempTable.append(
122 | E('tr', { 'class': 'tr table-titles' }, [
123 | E('th', { 'class': 'th left', 'width': '33%' }, _('Sensor')),
124 | E('th', { 'class': 'th left' }, _('Temperature')),
125 | E('th', { 'class': 'th right', 'width': '1%' }, ' '),
126 | ])
127 | );
128 |
129 | if(this.sensorsData && this.tempData) {
130 | for(let [k, v] of Object.entries(this.sensorsData)) {
131 | v.sort(this.sortFunc);
132 |
133 | for(let i of Object.values(v)) {
134 | let sensor = i.title || i.item;
135 |
136 | if(i.sources === undefined) {
137 | continue;
138 | };
139 |
140 | i.sources.sort(this.sortFunc);
141 |
142 | for(let j of i.sources) {
143 | if(this.hiddenItems.has(j.path)) {
144 | continue;
145 | };
146 |
147 | let temp = this.tempData[j.path];
148 | let name = (j.label !== undefined) ? sensor + " / " + j.label :
149 | (j.item !== undefined) ? sensor + " / " + j.item.replace(/_input$/, "") : sensor
150 |
151 | if(temp !== undefined && temp !== null) {
152 | temp = this.formatTemp(temp);
153 | };
154 |
155 | let tempHot = NaN;
156 | let tempOverheat = NaN;
157 | let tpoints = j.tpoints;
158 | let tpointsString = '';
159 |
160 | if(tpoints) {
161 | for(let i of Object.values(tpoints)) {
162 | let t = this.formatTemp(i.temp);
163 | tpointsString += `
${i.type}: ${t} °C`;
164 |
165 | if(i.type == 'max' || i.type == 'critical' || i.type == 'emergency') {
166 | if(!(tempOverheat <= t)) {
167 | tempOverheat = t;
168 | };
169 | }
170 | else if(i.type == 'hot') {
171 | tempHot = t;
172 | };
173 | };
174 | };
175 |
176 | if(isNaN(tempHot) && isNaN(tempOverheat)) {
177 | tempHot = this.tempHot;
178 | tempOverheat = this.tempOverheat;
179 | };
180 |
181 | let rowStyle = (temp >= tempOverheat) ? ' temp-status-overheat':
182 | (temp >= tempHot) ? ' temp-status-hot' : '';
183 |
184 | this.tempTable.append(
185 | E('tr', {
186 | 'class' : 'tr' + rowStyle,
187 | 'data-path': j.path ,
188 | }, [
189 | E('td', {
190 | 'class' : 'td left',
191 | 'data-title': _('Sensor')
192 | },
193 | (tpointsString.length > 0) ?
194 | `${name}` :
195 | name
196 | ),
197 | E('td', {
198 | 'class' : 'td left',
199 | 'data-title': _('Temperature')
200 | },
201 | (temp === undefined || temp === null) ? '-' : temp + ' °C'
202 | ),
203 | E('td', {
204 | 'class' : 'td right',
205 | 'data-title': _('Hide'),
206 | 'title' : _('Hide'),
207 | },
208 | E('span', {
209 | 'class': 'temp-status-hide-item',
210 | 'title': _('Hide'),
211 | 'click': () => this.hideItem(j.path),
212 | }, 'Χ'),
213 | ),
214 | ])
215 | );
216 | };
217 | };
218 | };
219 | };
220 |
221 | if(this.tempTable.childNodes.length == 1) {
222 | this.tempTable.append(
223 | E('tr', { 'class': 'tr placeholder' },
224 | E('td', { 'class': 'td' },
225 | E('em', {}, _('No temperature sensors available'))
226 | )
227 | )
228 | );
229 | };
230 | },
231 |
232 | hideItem(path) {
233 | this.hiddenItems.add(path);
234 | this.saveSettingsToLocalStorage();
235 | this.makeTempTableContent();
236 | document.getElementById('temp-status-hnum').textContent = this.hiddenItems.size;
237 | },
238 |
239 | unhideAllItems() {
240 | this.hiddenItems.clear();
241 | this.saveSettingsToLocalStorage();
242 | this.makeTempTableContent();
243 | document.getElementById('temp-status-hnum').textContent = this.hiddenItems.size;
244 | },
245 |
246 | load() {
247 | this.restoreSettingsFromLocalStorage();
248 | if(this.sensorsData) {
249 | return (this.sensorsPath.length > 0) ?
250 | L.resolveDefault(this.callTempData(this.sensorsPath), null) :
251 | Promise.resolve(null);
252 | } else {
253 | return L.resolveDefault(this.callSensors(), null);
254 | };
255 | },
256 |
257 | render(data) {
258 | if(data) {
259 | if(!this.sensorsData) {
260 | this.sensorsData = data.sensors;
261 | this.sensorsPath = data.temp && new Array(...Object.keys(data.temp));
262 | };
263 | this.tempData = data.temp;
264 | };
265 |
266 | if(!this.sensorsData || !this.tempData) {
267 | return;
268 | };
269 |
270 | this.makeTempTableContent();
271 |
272 | return E('div', { 'class': 'cbi-section' }, [
273 | E('div',
274 | { 'style': 'margin-bottom:1em; padding:0 4px;' },
275 | E('span', {
276 | 'class': 'temp-status-unhide-all',
277 | 'href' : 'javascript:void(0)',
278 | 'click': () => this.unhideAllItems(),
279 | }, [
280 | _('Show hidden sensors'),
281 | ' (',
282 | E('span', { 'id': 'temp-status-hnum' }, this.hiddenItems.size),
283 | ')',
284 | ])
285 | ),
286 | this.tempTable,
287 | ]);
288 | },
289 | });
290 |
--------------------------------------------------------------------------------
/htdocs/luci-static/resources/view/status/temperature.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | 'require dom';
3 | 'require poll';
4 | 'require request';
5 | 'require rpc';
6 | 'require view';
7 | 'require ui';
8 |
9 | document.head.append(E('style', {'type': 'text/css'},
10 | `
11 | :root {
12 | --app-temp-status-temp: #147aff;
13 | --app-temp-status-hot: orange;
14 | --app-temp-status-overheat: red;
15 | }
16 | .svg_background {
17 | width: 100%;
18 | height: 300px;
19 | border: 1px solid #000;
20 | background: #fff';
21 | }
22 | [data-darkmode="true"] .svg_background {
23 | background-color: var(--background-color-high) !important;
24 | }
25 | .graph_legend {
26 | border-bottom: 2px solid;
27 | }
28 | .temp {
29 | border-color: var(--app-temp-status-temp);
30 | }
31 | .hot {
32 | border-color: var(--app-temp-status-hot);
33 | }
34 | .overheat {
35 | border-color: var(--app-temp-status-overheat);
36 | }
37 | svg line.grid {
38 | stroke: black;
39 | stroke-width: 0.1;
40 | }
41 | [data-darkmode="true"] svg line.grid {
42 | stroke: #fff !important;
43 | }
44 | svg text {
45 | fill: #eee;
46 | font-size: 9pt;
47 | font-family: sans-serif;
48 | text-shadow: 1px 1px 1px #000;
49 | }
50 | svg #temp_line {
51 | fill: var(--app-temp-status-temp);
52 | fill-opacity: 0.4;
53 | stroke: var(--app-temp-status-temp);
54 | stroke-width: 1;
55 | }
56 | svg #hot_line {
57 | stroke: var(--app-temp-status-hot);
58 | stroke-width: 1;
59 | }
60 | svg #overheat_line {
61 | stroke: var(--app-temp-status-overheat);
62 | stroke-width: 1;
63 | }
64 | `));
65 |
66 | Math.log2 = Math.log2 || (x => Math.log(x) * Math.LOG2E);
67 |
68 | return view.extend({
69 | tempHot : 95,
70 |
71 | tempOverheat : 105,
72 |
73 | pollInterval : 3,
74 |
75 | tempBufferSize: 4,
76 |
77 | sensorsData : null,
78 |
79 | sensorsPath : [],
80 |
81 | tempSources : {},
82 |
83 | graphPolls : [],
84 |
85 | callSensors : rpc.declare({
86 | object: 'luci.temp-status',
87 | method: 'getSensors',
88 | expect: { '': {} },
89 | }),
90 |
91 | callTempData : rpc.declare({
92 | object: 'luci.temp-status',
93 | method: 'getTempData',
94 | params: [ 'tpaths' ],
95 | expect: { '': {} },
96 | }),
97 |
98 | formatTemp(mc) {
99 | return Number((mc / 1000).toFixed(1));
100 | },
101 |
102 | sortFunc(a, b) {
103 | return (a.number > b.number) ? 1 : (a.number < b.number) ? -1 : 0;
104 | },
105 |
106 | getSensorsData() {
107 | return this.callSensors().then(data => {
108 | if(data) {
109 | this.sensorsData = data.sensors;
110 | this.sensorsPath = new Array(...Object.keys(data.temp));
111 | let tempData = data.temp;
112 | if(this.sensorsData && tempData) {
113 | for(let e of Object.values(this.sensorsData)) {
114 | e.sort(this.sortFunc);
115 |
116 | for(let i of Object.values(e)) {
117 | let sensor = i.title || i.item;
118 |
119 | if(i.sources === undefined) {
120 | continue;
121 | };
122 |
123 | i.sources.sort(this.sortFunc);
124 |
125 | for(let j of i.sources) {
126 | let path = j.path;
127 | let temp = tempData[path];
128 | let name = (j.label !== undefined) ? sensor + " / " + j.label :
129 | (j.item !== undefined) ? sensor + " / " + j.item.replace(/_input$/, "") : sensor
130 |
131 | if(temp !== undefined && temp !== null) {
132 | temp = this.formatTemp(temp);
133 | };
134 |
135 | let temp_hot = NaN;
136 | let temp_overheat = NaN;
137 | let tpoints = j.tpoints;
138 |
139 | if(tpoints) {
140 | for(let i of Object.values(tpoints)) {
141 | let t = this.formatTemp(i.temp);
142 | if(i.type === 'max' || i.type === 'critical' || i.type === 'emergency') {
143 | if(!(temp_overheat <= t)) {
144 | temp_overheat = t;
145 | };
146 | }
147 | else if(i.type === 'hot') {
148 | temp_hot = t;
149 | };
150 | };
151 | };
152 |
153 | if(isNaN(temp_hot) && isNaN(temp_overheat)) {
154 | temp_hot = this.tempHot;
155 | temp_overheat = this.tempOverheat;
156 | };
157 |
158 | if(!(path in this.tempSources)) {
159 | this.tempSources[path] = {
160 | name,
161 | path,
162 | temp: [ [ new Date().getTime(), temp || 0 ] ],
163 | temp_hot,
164 | temp_overheat,
165 | tpoints,
166 | };
167 | };
168 | };
169 | };
170 | };
171 | };
172 | };
173 | return this.tempSources;
174 | });
175 | },
176 |
177 | getTempData() {
178 | return this.callTempData(this.sensorsPath).then(data => {
179 | if(data) {
180 | let tempData = data.temp;
181 | if(this.sensorsData && tempData) {
182 | for(let [path, temp] of Object.entries(tempData)) {
183 | if(path in this.tempSources) {
184 | if(temp !== undefined && temp !== null) {
185 | temp = this.formatTemp(temp);
186 | };
187 | let temp_array = this.tempSources[path].temp;
188 | temp_array.push([ new Date().getTime(), temp || 0 ]);
189 | if(temp_array.length > this.tempBufferSize) {
190 | temp_array.shift();
191 | };
192 | };
193 | };
194 | };
195 | };
196 | return this.tempSources;
197 | });
198 | },
199 |
200 | loadSVG(src) {
201 | return request.get(src).then(response => {
202 | if(!response.ok) {
203 | throw new Error(response.statusText);
204 | };
205 |
206 | return E('div', {
207 | 'class': 'svg_background',
208 | }, E(response.text()));
209 | });
210 | },
211 |
212 | updateGraph(tpath, svg, y_peaks, lines, cb) {
213 | let G = svg.firstElementChild;
214 | let view = document.querySelector('#view');
215 | let width = view.offsetWidth - 2;
216 | let height = 300 - 2;
217 | let base_step = 5;
218 | let time_interval = 60;
219 | let time_interval_min = time_interval / 60
220 | let step = base_step * this.pollInterval;
221 | let data_wanted = Math.ceil(width / step);
222 | let timeline_offset = width % step;
223 | let data_values = [];
224 | let line_elements = [];
225 |
226 | for(let i = 0; i < lines.length; i++) {
227 | if(lines[i] != null) {
228 | data_values.push([]);
229 | };
230 | };
231 |
232 | let info = {
233 | line_current : [],
234 | line_average : [],
235 | line_peak : [],
236 | line_min : [],
237 | hot_line : svg.firstElementChild.getElementById('hot_line'),
238 | overheat_line: svg.firstElementChild.getElementById('overheat_line'),
239 | };
240 |
241 | /* prefill datasets */
242 | for(let i = 0; i < data_values.length; i++) {
243 | for(let j = 0; j < data_wanted; j++) {
244 | data_values[i][j] = NaN;
245 | };
246 | };
247 |
248 | /* plot horizontal time interval lines */
249 | for(let i = width % (base_step * time_interval); i < width; i += base_step * time_interval) {
250 | let x = i - (timeline_offset);
251 | let line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
252 | line.setAttribute('x1', x);
253 | line.setAttribute('y1', 0);
254 | line.setAttribute('x2', x);
255 | line.setAttribute('y2', '100%');
256 | line.setAttribute('class', 'grid');
257 |
258 | let text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
259 | text.setAttribute('x', x + 5);
260 | text.setAttribute('y', 15);
261 | text.append(document.createTextNode(String((width - i) / base_step / time_interval) + 'm'));
262 |
263 | G.append(line);
264 | G.append(text);
265 | };
266 |
267 | info.interval = this.pollInterval;
268 | info.timeframe = Math.floor(data_wanted / time_interval * this.pollInterval);
269 |
270 | this.graphPolls.push({
271 | tpath,
272 | svg,
273 | y_peaks,
274 | lines,
275 | cb,
276 | info,
277 | width,
278 | height,
279 | step,
280 | data_wanted,
281 | values : data_values,
282 | timestamp : 0,
283 | fill : 1,
284 | });
285 | },
286 |
287 | pollData() {
288 | poll.add(L.bind(function() {
289 | return this.getTempData().then(L.bind(function(datasets) {
290 | for(let gi = 0; gi < this.graphPolls.length; gi++) {
291 | let ctx = this.graphPolls[gi];
292 |
293 | if(!datasets[ctx.tpath]) {
294 | continue;
295 | };
296 |
297 | let data = datasets[ctx.tpath].temp;
298 |
299 | if(!data) {
300 | continue;
301 | };
302 |
303 | let values = ctx.values;
304 | let lines = ctx.lines;
305 | let info = ctx.info;
306 | let y_peaks = ctx.y_peaks;
307 | let temp_hot = datasets[ctx.tpath].temp_hot;
308 | let temp_overheat = datasets[ctx.tpath].temp_overheat;
309 | let data_scale = 0;
310 | let data_wanted = ctx.data_wanted;
311 | let last_timestamp = NaN;
312 |
313 | for(let i = 0, di = 0; di < lines.length; di++) {
314 | if(lines[di] == null) {
315 | continue;
316 | };
317 |
318 | for(let j = ctx.timestamp ? 0 : 1; j < data.length; j++) {
319 |
320 | /* skip overlapping and empty entries */
321 | if(data[j][0] <= ctx.timestamp) {
322 | continue;
323 | };
324 |
325 | if(i == 0) {
326 | ctx.fill++;
327 | last_timestamp = data[j][0];
328 | };
329 |
330 | info.line_current[i] = data[j][di + 1];
331 | values[i].push(info.line_current[i]);
332 | };
333 |
334 | i++;
335 | };
336 |
337 | /* cut off outdated entries */
338 | ctx.fill = Math.min(ctx.fill, data_wanted);
339 |
340 | for(let i = 0; i < values.length; i++) {
341 | let len = values[i].length;
342 | values[i] = values[i].slice(len - data_wanted, len);
343 |
344 | /* find peaks, averages */
345 | info.line_peak[i] = NaN;
346 | info.line_average[i] = 0;
347 | info.line_min[i] = NaN;
348 |
349 | let nonempty = 0;
350 | for(let j = 0; j < values[i].length; j++) {
351 | info.line_peak[i] = isNaN(info.line_peak[i]) ? values[i][j] : Math.max(info.line_peak[i], values[i][j]);
352 | info.line_peak[i] = Number(info.line_peak[i].toFixed(1));
353 | info.line_min[i] = isNaN(info.line_min[i]) ? values[i][j] : Math.min(info.line_min[i], values[i][j]);
354 | info.line_min[i] = Number(info.line_min[i].toFixed(1));
355 |
356 | if(!isNaN(values[i][j])) {
357 | nonempty++;
358 | info.line_average[i] += values[i][j];
359 | };
360 | };
361 |
362 | info.line_average[i] = info.line_average[i] / nonempty;
363 | info.line_average[i] = Number(info.line_average[i].toFixed(1));
364 | };
365 |
366 | info.peak = Math.max.apply(Math, info.line_peak);
367 |
368 | /* remember current timestamp, calculate horizontal scale */
369 | if(!isNaN(last_timestamp)) {
370 | ctx.timestamp = last_timestamp;
371 | };
372 |
373 | let size = Math.floor(Math.log2(info.peak));
374 | let div = Math.pow(2, size - (size % 10));
375 |
376 | if(y_peaks) {
377 | info.peak = (info.peak > y_peaks.t2) ? y_peaks.t2 + y_peaks.incr :
378 | ((info.peak > y_peaks.t1) ? y_peaks.t1 + y_peaks.incr : y_peaks.t1);
379 | } else {
380 | let mult = info.peak / div;
381 | mult = (mult < 5) ? 2 : ((mult < 50) ? 10 : ((mult < 500) ? 100 : 1000));
382 | info.peak = info.peak + (mult * div) - (info.peak % (mult * div));
383 | };
384 |
385 | data_scale = ctx.height / info.peak;
386 |
387 | /* plot data */
388 | for(let i = 0, di = 0; di < lines.length; di++) {
389 | if(lines[di] == null) {
390 | continue;
391 | };
392 |
393 | let el = ctx.svg.firstElementChild.getElementById(lines[di].line);
394 | let pt = '0,' + ctx.height;
395 | let y = 0;
396 |
397 | if(!el) {
398 | continue;
399 | };
400 |
401 | for(let j = 0; j < values[i].length; j++) {
402 | let x = j * ctx.step;
403 |
404 | y = ctx.height - Math.floor(values[i][j] * data_scale);
405 | y = isNaN(y) ? ctx.height + 1 : y;
406 | pt += ` ${x},${y}`;
407 | };
408 |
409 | pt += ` ${ctx.width},${y} ${ctx.width},${ctx.height}`;
410 | el.setAttribute('points', pt);
411 |
412 | i++;
413 | };
414 |
415 | /* hot line y */
416 | if(!isNaN(temp_hot)) {
417 | info.hot_line.setAttribute(
418 | 'y1', ctx.height - Math.floor(temp_hot * data_scale));
419 | info.hot_line.setAttribute(
420 | 'y2', ctx.height - Math.floor(temp_hot * data_scale));
421 | };
422 |
423 | /* overheat line y */
424 | if(!isNaN(temp_overheat)) {
425 | info.overheat_line.setAttribute(
426 | 'y1', ctx.height - Math.floor(temp_overheat * data_scale));
427 | info.overheat_line.setAttribute(
428 | 'y2', ctx.height - Math.floor(temp_overheat * data_scale));
429 | };
430 |
431 | info.label_25 = 0.25 * info.peak;
432 | info.label_50 = 0.50 * info.peak;
433 | info.label_75 = 0.75 * info.peak;
434 |
435 | if(typeof(ctx.cb) == 'function') {
436 | ctx.cb(ctx.svg, info);
437 | };
438 | };
439 | }, this));
440 | }, this), this.pollInterval);
441 | },
442 |
443 | load() {
444 | return Promise.all([
445 | this.loadSVG(L.resource('svg/temperature.svg')),
446 | this.getSensorsData(),
447 | ]);
448 | },
449 |
450 | render(data) {
451 | let svg = data[0];
452 | let tsources = data[1];
453 | let map = E('div', { 'class': 'cbi-map', 'id': 'map' });
454 |
455 | if(!tsources || Object.keys(tsources).length == 0) {
456 | map.append(E('div', { 'class': 'cbi-section' },
457 | E('div', { 'class': 'cbi-section-node' },
458 | E('div', { 'class': 'cbi-value' },
459 | E('em', {}, _('No temperature sensors available'))
460 | )
461 | )
462 | ));
463 | } else {
464 | let tabs = E('div');
465 | map.append(tabs);
466 |
467 | for(let i of Object.values(tsources)) {
468 | let tsource_name = i.name;
469 | let tsource_path = i.path;
470 | let tsource_hot = i.temp_hot;
471 | let tsource_overheat = i.temp_overheat;
472 | let tsource_tpoints = i.tpoints;
473 |
474 | if(!tsource_name || !tsource_path) {
475 | continue;
476 | };
477 |
478 | let csvg = svg.cloneNode(true);
479 | let tpoints_section = null;
480 |
481 | if(tsource_tpoints) {
482 | tpoints_section = E('div', { 'class': 'cbi-section-node' })
483 | let tpoints_table = E('table', { 'class': 'table' });
484 | tpoints_section.append(tpoints_table);
485 |
486 | for(let i of Object.values(tsource_tpoints)) {
487 | tpoints_table.append(
488 | E('tr', { 'class': 'tr' }, [
489 | E('td', { 'class': 'td left' }, i.type),
490 | E('td', { 'class': 'td left' }, this.formatTemp(i.temp) + ' °C' ),
491 | ])
492 | );
493 | };
494 | };
495 |
496 | tabs.append(E('div', { 'class': 'cbi-section', 'data-tab': tsource_path, 'data-tab-title': tsource_name }, [
497 | csvg,
498 | E('div', { 'class': 'right' }, E('small', { 'data-graph': 'scale' }, '-')),
499 | E('br'),
500 | E('table', { 'class': 'table', 'style': 'width:100%;table-layout:fixed' }, [
501 | E('tr', { 'class': 'tr' }, [
502 | E('td', { 'class': 'td right top' }, E('strong', { 'class': 'graph_legend temp' }, _('Temperature') + ':')),
503 | E('td', { 'class': 'td', 'data-graph': 'temp_cur' }, '-'),
504 |
505 | E('td', { 'class': 'td right top' }, E('strong', {}, _('Minimum:'))),
506 | E('td', { 'class': 'td', 'data-graph': 'temp_min' }, '-'),
507 |
508 | E('td', { 'class': 'td right top' }, E('strong', {}, _('Average:'))),
509 | E('td', { 'class': 'td', 'data-graph': 'temp_avg' }, '-'),
510 |
511 | E('td', { 'class': 'td right top' }, E('strong', {}, _('Peak:'))),
512 | E('td', { 'class': 'td', 'data-graph': 'temp_peak' }, '-'),
513 | ]),
514 | (!isNaN(tsource_hot) ?
515 | E('tr', { 'class': 'tr' }, [
516 | E('td', { 'class': 'td right top' }, E('strong', { 'class': 'graph_legend hot' }, _('Hot:'))),
517 | E('td', { 'class': 'td', 'data-graph': 'temp_hot' }, tsource_hot + ' °C'),
518 |
519 | E('td', { 'class': 'td right top' }),
520 | E('td', { 'class': 'td right top' }),
521 | E('td', { 'class': 'td right top' }),
522 | E('td', { 'class': 'td right top' }),
523 | E('td', { 'class': 'td right top' }),
524 | E('td', { 'class': 'td right top' }),
525 | ]) : ''
526 | ),
527 | (!isNaN(tsource_overheat) ?
528 | E('tr', { 'class': 'tr' }, [
529 | E('td', { 'class': 'td right top' }, E('strong', { 'class': 'graph_legend overheat' }, _('Overheat:'))),
530 | E('td', { 'class': 'td', 'data-graph': 'temp_overheat' }, tsource_overheat + ' °C'),
531 |
532 | E('td', { 'class': 'td right top' }),
533 | E('td', { 'class': 'td right top' }),
534 | E('td', { 'class': 'td right top' }),
535 | E('td', { 'class': 'td right top' }),
536 | E('td', { 'class': 'td right top' }),
537 | E('td', { 'class': 'td right top' }),
538 | ]) : ''
539 | ),
540 | ]),
541 | E('br'),
542 | tpoints_section || '',
543 | E('br'),
544 | ]));
545 |
546 | this.updateGraph(
547 | tsource_path,
548 | csvg,
549 | { 't1': 120, 't2': 180, 'incr': 60 },
550 | [ { 'line': 'temp_line' } ],
551 | (svg, info) => {
552 | let G = svg.firstElementChild, tab = svg.parentNode;
553 |
554 | G.getElementById('label_25').firstChild.data = '%d °C'.format(info.label_25);
555 | G.getElementById('label_50').firstChild.data = '%d °C'.format(info.label_50);
556 | G.getElementById('label_75').firstChild.data = '%d °C'.format(info.label_75);
557 |
558 | tab.querySelector('[data-graph="scale"]').firstChild.data = _('(%d minute window, %d second interval)').format(info.timeframe, info.interval);
559 |
560 | dom.content(tab.querySelector('[data-graph="temp_cur"]'), info.line_current[0] + ' °C');
561 | dom.content(tab.querySelector('[data-graph="temp_min"]'), info.line_min[0] + ' °C');
562 | dom.content(tab.querySelector('[data-graph="temp_avg"]'), info.line_average[0] + ' °C');
563 | dom.content(tab.querySelector('[data-graph="temp_peak"]'), info.line_peak[0] + ' °C');
564 | }
565 | );
566 | };
567 |
568 | ui.tabs.initTabGroup(tabs.childNodes);
569 | this.pollData();
570 | };
571 |
572 | return E([], [
573 | E('h2', _('Temperature')),
574 | E('div', {'class': 'cbi-map-descr'}, _('This page displays the temperature sensors data.')),
575 | map,
576 | ]);
577 | },
578 |
579 | handleSaveApply: null,
580 | handleSave : null,
581 | handleReset : null,
582 | });
583 |
--------------------------------------------------------------------------------