mirror of
https://github.com/caiwx86/small-packages.git
synced 2026-11-01 04:43:56 +08:00
update 2026-05-10 09:48:30
This commit is contained in:
1 parent
95ac96413e
commit
0b362674f1
6244 files changed
+2992248
No files matched your search
@@ -0,0 +1,18 @@
|
||||
<?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>
|
||||
|
After Width: | Height: | Size: 740 B |
+470
@@ -0,0 +1,470 @@
|
||||
'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 += ` ${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),
|
||||
}, 'Χ'),
|
||||
),
|
||||
])
|
||||
);
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
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 += ` ${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),
|
||||
}, 'Χ'),
|
||||
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,
|
||||
]);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,582 @@
|
||||
'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,
|
||||
});
|
||||
Reference in new issue
Block a user