├── 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 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 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 | ![](https://github.com/gSpotx2f/luci-app-temp-status/blob/master/screenshots/01.jpg) 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 | --------------------------------------------------------------------------------