

{"id":6387,"date":"2025-08-29T02:28:35","date_gmt":"2025-08-28T18:28:35","guid":{"rendered":"https:\/\/meatball3c.com\/?page_id=6387"},"modified":"2025-08-29T04:07:14","modified_gmt":"2025-08-28T20:07:14","slug":"iot-dashboard","status":"publish","type":"page","link":"https:\/\/meatball3c.com\/en\/iot-dashboard\/","title":{"rendered":"IOT Dashboard"},"content":{"rendered":"    <div class=\"iot-dashboard\"\r\n         data-device=\"pump1\"\r\n         data-limit=\"1000\"\r\n         data-refresh=\"20\"\r\n         data-chart1=\"temperature\"\r\n         data-chart2=\"current\">\r\n\r\n        <div style=\"margin-bottom:12px;\">\r\n            <label>Device:\r\n                <select id=\"iot-device\"><\/select>\r\n            <\/label>\r\n            <label>Start:\r\n                <input id=\"iot-start\" type=\"text\" style=\"width:130px;\">\r\n            <\/label>\r\n            <label>End:\r\n                <input id=\"iot-end\" type=\"text\" style=\"width:130px;\">\r\n            <\/label>\r\n            <button id=\"iot-refresh\">Refresh<\/button>\r\n            <button id=\"iot-csv\">Download CSV<\/button>\r\n            <span id=\"iot-status\" style=\"margin-left:8px; opacity:.7;\"><\/span>\r\n        <\/div>\r\n\r\n        <h4>Chart: temperature<\/h4>\r\n        <canvas id=\"iot-chart1\" height=\"100\"><\/canvas>\r\n\r\n        <h4 style=\"margin-top:20px;\">Chart: current<\/h4>\r\n        <canvas id=\"iot-chart2\" height=\"100\"><\/canvas>\r\n\r\n        <h4 style=\"margin-top:20px;\">Latest Readings<\/h4>\r\n        <table id=\"iot-table\" style=\"width:100%; border-collapse:collapse;\">\r\n            <thead>\r\n                <tr>\r\n                    <th>Timestamp<\/th>\r\n                    <th>Device<\/th>\r\n                    <th>Sensor<\/th>\r\n                    <th>Value<\/th>\r\n                    <th>Unit<\/th>\r\n                <\/tr>\r\n            <\/thead>\r\n            <tbody><\/tbody>\r\n        <\/table>\r\n    <\/div>\r\n\r\n    <script>\r\n    (function(){\r\n        const root = document.currentScript.previousElementSibling;\r\n        const API = 'https:\/\/meatball3c.com\/en\/wp-json\/iot\/v1';\r\n        const deviceDefault = root.dataset.device;\r\n        const chart1Type = root.dataset.chart1;\r\n        const chart2Type = root.dataset.chart2;\r\n        const limit = parseInt(root.dataset.limit,10);\r\n        const refreshMs = parseInt(root.dataset.refresh,10) * 1000;\r\n\r\n        const selDev = root.querySelector('#iot-device');\r\n        const inpStart = root.querySelector('#iot-start');\r\n        const inpEnd   = root.querySelector('#iot-end');\r\n        const btnRef   = root.querySelector('#iot-refresh');\r\n        const btnCSV   = root.querySelector('#iot-csv');\r\n        const status   = root.querySelector('#iot-status');\r\n        const tableB   = root.querySelector('#iot-table tbody');\r\n        const ctx1     = root.querySelector('#iot-chart1').getContext('2d');\r\n        const ctx2     = root.querySelector('#iot-chart2').getContext('2d');\r\n\r\n        let chart1, chart2, timer;\r\n\r\n        flatpickr(inpStart, { enableTime:true, dateFormat:\"Y-m-d H:i\" });\r\n        flatpickr(inpEnd, { enableTime:true, dateFormat:\"Y-m-d H:i\" });\r\n\r\n        function setStatus(msg){ status.textContent = msg; }\r\n        function fmtTime(ts){ return ts.replace('T',' ').replace('Z',''); }\r\n\r\n        async function fetchMeta() {\r\n            const r = await fetch(`${API}\/meta`);\r\n            const j = await r.json();\r\n            selDev.innerHTML = '<option value=\"\">(all)<\/option>' +\r\n              j.devices.map(d => `<option>${d}<\/option>`).join('');\r\n            if (deviceDefault) selDev.value = deviceDefault;\r\n        }\r\n\r\n        async function fetchData() {\r\n            const dev = selDev.value;\r\n            const q = new URLSearchParams({limit});\r\n            if (dev) q.append('device_id', dev);\r\n            if (inpStart.value) q.append('start', inpStart.value);\r\n            if (inpEnd.value) q.append('end', inpEnd.value);\r\n\r\n            setStatus('Loading...');\r\n            const r = await fetch(`${API}\/list?`+q.toString());\r\n            const j = await r.json();\r\n            const rows = (j.data||[]).slice().reverse();\r\n            setStatus(`Loaded ${rows.length}`);\r\n\r\n            \/\/ Split by sensor type\r\n            const rows1 = rows.filter(r => r.sensor_type===chart1Type);\r\n            const rows2 = rows.filter(r => r.sensor_type===chart2Type);\r\n\r\n            \/\/ Chart 1\r\n            const labels1 = rows1.map(x=>fmtTime(x.timestamp));\r\n            const vals1 = rows1.map(x=>Number(x.value));\r\n            if (!chart1){\r\n                chart1 = new Chart(ctx1, { type:'line',\r\n                  data:{ labels:labels1, datasets:[{label:chart1Type, data:vals1}] },\r\n                  options:{ responsive:true, tension:0.2 }\r\n                });\r\n            } else {\r\n                chart1.data.labels=labels1; chart1.data.datasets[0].data=vals1; chart1.update();\r\n            }\r\n\r\n            \/\/ Chart 2\r\n            const labels2 = rows2.map(x=>fmtTime(x.timestamp));\r\n            const vals2 = rows2.map(x=>Number(x.value));\r\n            if (!chart2){\r\n                chart2 = new Chart(ctx2, { type:'line',\r\n                  data:{ labels:labels2, datasets:[{label:chart2Type, data:vals2, borderColor:'red'}] },\r\n                  options:{ responsive:true, tension:0.2 }\r\n                });\r\n            } else {\r\n                chart2.data.labels=labels2; chart2.data.datasets[0].data=vals2; chart2.update();\r\n            }\r\n\r\n            \/\/ Update table\r\n            tableB.innerHTML = rows.slice().reverse().map(r=>`\r\n              <tr>\r\n                <td>${fmtTime(r.timestamp)}<\/td>\r\n                <td>${r.device_id}<\/td>\r\n                <td>${r.sensor_type}<\/td>\r\n                <td>${r.value}<\/td>\r\n                <td>${r.unit||''}<\/td>\r\n              <\/tr>`).join('');\r\n        }\r\n\r\n        function exportCSV() {\r\n            let csv = \"Timestamp,Device,Sensor,Value,Unit\\n\";\r\n            tableB.querySelectorAll('tr').forEach(tr=>{\r\n                const cols=[...tr.querySelectorAll('td')].map(td=>td.innerText);\r\n                csv += cols.join(',') + \"\\\\n\";\r\n            });\r\n            const blob = new Blob([csv], {type:'text\/csv'});\r\n            const url = URL.createObjectURL(blob);\r\n            const a = document.createElement('a');\r\n            a.href=url; a.download=\"iot_data.csv\"; a.click();\r\n            URL.revokeObjectURL(url);\r\n        }\r\n\r\n        btnRef.addEventListener('click', fetchData);\r\n        btnCSV.addEventListener('click', exportCSV);\r\n\r\n        (async function init(){\r\n            await fetchMeta();\r\n            await fetchData();\r\n            if (refreshMs>=2000) timer=setInterval(fetchData, refreshMs);\r\n        })();\r\n    })();\r\n    <\/script>\r\n    \n","protected":false},"excerpt":{"rendered":"","protected":false},"author":31,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-6387","page","type-page","status-publish","hentry"],"blocksy_meta":[],"_links":{"self":[{"href":"https:\/\/meatball3c.com\/en\/wp-json\/wp\/v2\/pages\/6387","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/meatball3c.com\/en\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/meatball3c.com\/en\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/meatball3c.com\/en\/wp-json\/wp\/v2\/users\/31"}],"replies":[{"embeddable":true,"href":"https:\/\/meatball3c.com\/en\/wp-json\/wp\/v2\/comments?post=6387"}],"version-history":[{"count":6,"href":"https:\/\/meatball3c.com\/en\/wp-json\/wp\/v2\/pages\/6387\/revisions"}],"predecessor-version":[{"id":6398,"href":"https:\/\/meatball3c.com\/en\/wp-json\/wp\/v2\/pages\/6387\/revisions\/6398"}],"wp:attachment":[{"href":"https:\/\/meatball3c.com\/en\/wp-json\/wp\/v2\/media?parent=6387"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}