const { html, render } = lighterhtml; const $container = document.getElementById("sensors-container"); const $filters = document.createElement("div"); $filters.className = "filters"; $container.appendChild($filters); const $sensorsContainer = document.createElement("div"); $sensorsContainer.className = "sensors"; $container.appendChild($sensorsContainer); const $config = document.createElement("div"); $config.className = "config"; $container.appendChild($config); let sensorComponents = []; function load() { fetch("/api/sensors") .then((r) => r.json()) .then((sensors) => { $sensorsContainer.innerHTML = ""; sensorComponents = sensors.map((sensor) => createSensor(sensor)); sensorComponents.forEach((component) => $sensorsContainer.appendChild(component.container) ); }); } function preventPropagation(e) { e.stopPropagation(); } function hideConfig() { renderConfig({ sensor: { config: {} }, shown: false }); } function showConfigOf(sensor, onSave) { renderConfig({ sensor, onSave, shown: true }); } function renderConfig({ sensor, onSave, shown }) { const handleSave = async (e) => { e.preventDefault(); e.stopPropagation(); const data = Object.fromEntries(new FormData(e.target)); Promise.all( Object.entries(data).map(([name, value]) => fetch(`/api/sensors/${sensor.sensor}/config/${name}`, { method: "PUT", headers: { "content-type": "application/json" }, body: JSON.stringify({ value }), }) ) ).then(() => onSave?.()); hideConfig(); }; const config = sensor.config; render( $config, html`
` ); } const CURRENT_FILTERS = loadFilters(); function loadFilters() { const params = new URLSearchParams(location.search.replace(/^\?/, "")); return { interval: params.get("interval") || "week", refresh: "1s", customFrom: params.get("from") || new Date().toISOString(), customTo: params.get("to") || new Date().toISOString(), }; } function saveFilters() { const values = { interval: CURRENT_FILTERS.interval, }; if (CURRENT_FILTERS.interval === "custom") { values.from = CURRENT_FILTERS.customFrom.toISOString(); values.to = CURRENT_FILTERS.customTo.toISOString(); } const url = new URL(location.href); const params = new URLSearchParams(values); url.search = params.toString(); history.replaceState(null, null, url.toString()); } function refreshAll() { sensorComponents.forEach((c) => c.refreshValues()); } let lastIntervalSelected = CURRENT_FILTERS.interval; function splitDateTime(v) { const d = new Date(v); const date = d.getFullYear() + "-" + (d.getMonth() + 1).toString().padStart(2, "0") + "-" + d.getDate().toString().padStart(2, "0"); const time = d.getHours().toString().padStart(2, "0") + ":" + d.getMinutes().toString().padStart(2, "0"); return [date, time]; } function renderFilters() { const handleApply = async (e) => { e.preventDefault(); e.stopPropagation(); const data = Object.fromEntries(new FormData(e.target)); CURRENT_FILTERS.interval = data.interval; if (data.interval !== "custom") { const [from, to] = getDataInterval(CURRENT_FILTERS.interval); CURRENT_FILTERS.customFrom = from.toISOString(); CURRENT_FILTERS.customTo = to.toISOString(); } else { CURRENT_FILTERS.customFrom = new Date( `${data.fromDate} ${data.fromTime}` ); CURRENT_FILTERS.customTo = new Date(`${data.toDate} ${data.toTime}`); } refreshAll(); renderFilters(); saveFilters(); }; const handleIntervalChange = (e) => { lastIntervalSelected = e.target.value; renderFilters(); }; const customFrom = splitDateTime(CURRENT_FILTERS.customFrom); const customTo = splitDateTime(CURRENT_FILTERS.customTo); const isCustomSelected = lastIntervalSelected === "custom"; render( $filters, html`
${isCustomSelected ? html`
` : undefined}
` ); } function getDataInterval() { switch (CURRENT_FILTERS.interval) { case "hour": { return [new Date(Date.now() - 3600 * 1000), new Date()]; } case "day": { return [new Date(Date.now() - 24 * 3600 * 1000), new Date()]; } case "week": { return [new Date(Date.now() - 5 * 24 * 3600 * 1000), new Date()]; } case "month": { return [new Date(Date.now() - 30 * 24 * 3600 * 1000), new Date()]; } case "year": { return [new Date(Date.now() - 356 * 24 * 3600 * 1000), new Date()]; } case "custom": { return [ new Date(CURRENT_FILTERS.customFrom), new Date(CURRENT_FILTERS.customTo), ]; } } } function createSensor(sensor) { const container = document.createElement("div"); container.className = "sensor"; const header = document.createElement("div"); header.className = "header"; const body = document.createElement("div"); body.className = "body"; const showSettings = () => { showConfigOf(sensor, () => refreshSensorConfig()); }; const renderHeader = () => { render( header, html`
${sensor.config?.name ?? sensor.sensor}
` ); }; const renderBody = (range, values) => { const { from, to } = range; const minValue = parseFloat(sensor.config.min); const maxValue = parseFloat(sensor.config.max); const customRange = !isNaN(minValue) && !isNaN(maxValue); Plotly.newPlot( body, [ { ...(sensor.config.graphType === 'line' && { type: 'scatter', mode: 'lines' }), ...(sensor.config.graphType === 'points' && { type: 'scatter', mode: 'markers' }), ...(sensor.config.graphType === 'lineAndPoints' && { type: 'scatter', mode: 'lines+markers' }), ...(sensor.config.graphType === 'bar' && { type: 'bar' }), x: values.map((v) => new Date(v.timestamp * 1000)), y: values.map((v) => v.value), }, ], { xaxis: { range: [from, to], type: "date" }, yaxis: { ...(customRange && { range: [minValue, maxValue] }), ...(sensor.config.unit && { ticksuffix: ` ${sensor.config.unit}` }), }, margin: { l: 70, r: 20, b: 60, t: 20, pad: 5, }, }, { responsive: true, } ); }; const refreshValues = () => { const [from, to] = getDataInterval(CURRENT_FILTERS.interval); fetch( `/api/sensors/${sensor.sensor}/values?from=${Math.round( from.getTime() / 1000 )}&to=${Math.round(to.getTime() / 1000)}` ) .then((r) => r.json()) .then((values) => renderBody({ from, to }, values ?? [])); }; const refreshSensorConfig = () => { fetch(`/api/sensors/${sensor.sensor}/config`) .then((r) => r.json()) .then((c) => (sensor.config = c)) .then(() => { renderHeader(); refreshValues(); }); }; renderHeader(); refreshValues(); container.appendChild(header); container.appendChild(body); return { container, refreshValues, }; } load(); renderFilters();