Created
July 16, 2025 19:07
-
-
Save Murdo-C/185462ea9fe4475f079f6fd9075e6361 to your computer and use it in GitHub Desktop.
TRMNL Plugin - Home Status with Time
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!-- Highcharts + Chartkick --> | |
| <script src="https://code.highcharts.com/highcharts.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/chartkick@5.0.1/dist/chartkick.min.js"></script> | |
| <script src="https://code.highcharts.com/modules/pattern-fill.js"></script> | |
| <script src="https://code.highcharts.com/modules/timezone.js"></script> | |
| <!-- Layout --> | |
| <div class="layout layout--col gap--space-between"> | |
| <div class="grid grid--cols-3"> | |
| <div class="item"> | |
| <div class="meta"></div> | |
| <div class="content"> | |
| <span id="latest-aq" class="value value--tnums"></span> | |
| <span class="label">Living AQ (ppm)</span> | |
| </div> | |
| </div> | |
| <div class="item"> | |
| <div class="meta"></div> | |
| <div class="content"> | |
| <span id="latest-living-temp" class="value value--tnums"></span> | |
| <span class="label">Living Temp (°C)</span> | |
| </div> | |
| </div> | |
| <div class="item"> | |
| <div class="meta"></div> | |
| <div class="content"> | |
| <span id="latest-elena-temp" class="value value--tnums"></span> | |
| <span class="label">Elena Temp (°C)</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="google-sheet-chart-demo" class="w-full"></div> | |
| </div> | |
| <div class="title_bar"> | |
| <img class="image" src="https://www.svgrepo.com/show/22031/home-icon-silhouette.svg" /> | |
| <span class="title">Home Status</span> | |
| <span class="instance">Last updated: <span id="last-updated"></span></span> | |
| </div> | |
| <script type="text/javascript"> | |
| var timeZone = "{{ trmnl.user.time_zone_iana || 'America/Toronto' }}"; | |
| console.log("Device-reported timeZone:", timeZone); | |
| var rawCSV = `{% for row in data offset:1 %}{{ row[0] }},{{ row[1] }},{{ row[2] }},{{ row[3] }},{{ row[4] }},{{ row[5] }}{% unless forloop.last %}\n{% endunless %}{% endfor %}`; | |
| var rows = rawCSV.trim().split("\n"); | |
| var now = new Date(); | |
| var cutoff = now.getTime() - 24 * 60 * 60 * 1000; | |
| var chart_data = { | |
| "Living Temp": [], | |
| "Elena Temp": [], | |
| "Living Humidity": [], | |
| "Elena Humidity": [] | |
| }; | |
| var latestRow = null; | |
| var lastTimestamp = null; | |
| rows.forEach(function(line, idx) { | |
| var [timestampStr, ltStr, lhStr, aqStr, etStr, ehStr] = line.split(","); | |
| var date = new Date(timestampStr); | |
| var ts = date.getTime(); | |
| if (ts < cutoff) return; | |
| var lt = parseFloat(ltStr); | |
| var lh = parseFloat(lhStr); | |
| var et = parseFloat(etStr); | |
| var eh = parseFloat(ehStr); | |
| var aq = parseFloat(aqStr); | |
| if ([lt, lh, et, eh, aq].some(v => isNaN(v))) { | |
| console.warn("Skipping invalid row:", idx, line); | |
| return; | |
| } | |
| chart_data["Living Temp"].push([ts, lt]); | |
| chart_data["Elena Temp"].push([ts, et]); | |
| chart_data["Living Humidity"].push([ts, lh]); | |
| chart_data["Elena Humidity"].push([ts, eh]); | |
| latestRow = { ts: timestampStr, lt, et, aq }; | |
| lastTimestamp = ts; | |
| }); | |
| console.log("Chart Data Sample (Living Temp):", chart_data["Living Temp"].slice(-3)); | |
| if (latestRow) { | |
| document.getElementById("latest-aq").innerText = latestRow.aq.toFixed(0); | |
| document.getElementById("latest-living-temp").innerText = latestRow.lt.toFixed(1); | |
| document.getElementById("latest-elena-temp").innerText = latestRow.et.toFixed(1); | |
| var date = new Date(latestRow.ts); | |
| var formatter = new Intl.DateTimeFormat("en-CA", { | |
| timeZone: timeZone, | |
| dateStyle: "medium", | |
| timeStyle: "short" | |
| }); | |
| document.getElementById("last-updated").innerText = formatter.format(date); | |
| } else { | |
| document.getElementById("last-updated").innerText = "No recent data"; | |
| } | |
| Highcharts.setOptions({ | |
| time: { | |
| useUTC: true, | |
| timezone: timeZone | |
| } | |
| }); | |
| var series = [ | |
| { | |
| name: "Living Temp", | |
| data: chart_data["Living Temp"], | |
| yAxis: 0, | |
| lineWidth: 3, | |
| zIndex: 3, | |
| color: "#000000" | |
| }, | |
| { | |
| name: "Elena Temp", | |
| data: chart_data["Elena Temp"], | |
| yAxis: 0, | |
| lineWidth: 3, | |
| zIndex: 2, | |
| color: { | |
| pattern: { | |
| image: "https://usetrmnl.com/images/grayscale/gray-4.png", | |
| width: 12, | |
| height: 12 | |
| } | |
| } | |
| }, | |
| { | |
| name: "Living Humidity", | |
| data: chart_data["Living Humidity"], | |
| yAxis: 1, | |
| lineWidth: 3, | |
| zIndex: 1, | |
| color: { | |
| pattern: { | |
| image: "https://usetrmnl.com/images/grayscale/gray-5.png", | |
| width: 12, | |
| height: 12 | |
| } | |
| } | |
| }, | |
| { | |
| name: "Elena Humidity", | |
| data: chart_data["Elena Humidity"], | |
| yAxis: 1, | |
| lineWidth: 3, | |
| zIndex: 0, | |
| color: { | |
| pattern: { | |
| image: "https://usetrmnl.com/images/grayscale/gray-6.png", | |
| width: 12, | |
| height: 12 | |
| } | |
| } | |
| } | |
| ]; | |
| var createChart = function () { | |
| new Chartkick.LineChart("google-sheet-chart-demo", series, { | |
| adapter: "highcharts", | |
| curve: true, | |
| points: false, | |
| colors: ["#000000", "#333333", "#666666", "#999999"], | |
| library: { | |
| chart: { height: 320, zoomType: "x" }, | |
| yAxis: [ | |
| { | |
| title: { text: "Temp (°C)" }, | |
| labels: { style: { fontSize: "16px", color: "#000000" } }, | |
| gridLineDashStyle: "shortdot", | |
| gridLineWidth: 1, | |
| tickAmount: 5 | |
| }, | |
| { | |
| title: { text: "Humidity (%)" }, | |
| opposite: true, | |
| labels: { style: { fontSize: "16px", color: "#000000" } }, | |
| gridLineWidth: 0, | |
| tickAmount: 5, | |
| max: 100, | |
| min: 0 | |
| } | |
| ], | |
| xAxis: { | |
| labels: { | |
| style: { fontSize: "16px", color: "#000000" }, | |
| formatter: function () { | |
| var hoursAgo = Math.round((lastTimestamp - this.value) / 3600000); | |
| var labelTime = new Date(lastTimestamp - hoursAgo * 3600000); | |
| return labelTime.toLocaleTimeString("en-CA", { | |
| timeZone: timeZone, | |
| hour: "numeric", | |
| minute: "2-digit" | |
| }); | |
| } | |
| }, | |
| type: "datetime", | |
| gridLineDashStyle: "dot", | |
| gridLineWidth: 1 | |
| }, | |
| plotOptions: { | |
| series: { | |
| lineWidth: 3, | |
| animation: false | |
| } | |
| } | |
| } | |
| }); | |
| }; | |
| if ("Chartkick" in window) { | |
| createChart(); | |
| } else { | |
| window.addEventListener("chartkick:load", createChart, true); | |
| } | |
| </script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment