mirror of
https://github.com/caiwx86/small-packages.git
synced 2026-07-30 10:51:32 +08:00
583 lines
16 KiB
JavaScript
583 lines
16 KiB
JavaScript
'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,
|
|
});
|