Weather Station / ECOWITT / DNT
0

Configure Feed

Select the types of activity you want to include in your feed.

glitch minute/hour/day

+86 -26
+85 -14
src/app/api/data/main/route.ts
··· 62 62 fileLabel = parquetFiles.map((p) => path.basename(p)).join(","); 63 63 } 64 64 65 - // For daily resolution, use properly aggregated daily data 65 + // For daily resolution, build proper aggregation with max/avg for all columns 66 66 if (resolution === "day") { 67 - const dailyRows = await queryDailyAggregatesInRange(parquetFiles, start, end); 68 - const header = ["time", "Temperatur Aussen(°C)", "Taupunkt(°C)", "Gefühlte Temperatur", "Niederschlag Tag(mm)", "Windgeschwindigkeit(km/h)", "Böengeschwindigkeit(km/h)"]; 69 - const rows = dailyRows.map((r: any) => ({ 70 - key: r.day, 71 - time: r.day, 72 - "Temperatur Aussen(°C)": r.tmax, 73 - "Taupunkt(°C)": r.tmin, // Using tmin for dewpoint approximation 74 - "Gefühlte Temperatur": r.tfmax, 75 - "Niederschlag Tag(mm)": r.rain_day, 76 - "Windgeschwindigkeit(km/h)": r.wind_max, 77 - "Böengeschwindigkeit(km/h)": r.gust_max, 78 - })); 79 - return NextResponse.json({ file: fileLabel, header, rows }, { status: 200 }); 67 + const parquetPaths = parquetFiles.map((p) => p.replace(/\\/g, "/")); 68 + const colsHints = await discoverMainColumns(parquetPaths); 69 + const conn = await getDuckConn(); 70 + const arr = '[' + parquetPaths.map((p) => `'${p}'`).join(',') + ']'; 71 + const describeSql = `DESCRIBE SELECT * FROM read_parquet(${arr}, union_by_name=true)`; 72 + const descReader = await conn.runAndReadAll(describeSql); 73 + const cols: any[] = descReader.getRowObjects(); 74 + const allNames = cols.map((r: any) => String(r.column_name || r.ColumnName || r.column || "")); 75 + const typedNumericCols = cols 76 + .filter((r: any) => { 77 + const t = String(r.column_type || r.Type || r.type || "").toUpperCase(); 78 + return t && !t.includes("VARCHAR") && !t.includes("BOOLEAN") && t !== ""; 79 + }) 80 + .map((r: any) => String(r.column_name || r.ColumnName || r.column || "")) 81 + .filter((c) => c && c !== "ts" && c !== "Time" && c !== "Zeit"); 82 + 83 + const seen = new Set<string>(); 84 + const orderedCols: string[] = []; 85 + const pushCol = (name?: string | null) => { 86 + if (!name) return; 87 + if (!allNames.includes(name)) return; 88 + if (seen.has(name)) return; 89 + seen.add(name); 90 + orderedCols.push(name); 91 + }; 92 + 93 + pushCol(colsHints.temp); 94 + for (const c of typedNumericCols) pushCol(c); 95 + const candidateGroups: (string | null)[] = [ 96 + colsHints.rainDay, 97 + ...colsHints.dailyRainCandidates, 98 + ...colsHints.hourlyRainCandidates, 99 + ...colsHints.genericRainCandidates, 100 + colsHints.temp, 101 + ...colsHints.tempCandidates, 102 + colsHints.dew, 103 + ...colsHints.dewCandidates, 104 + colsHints.feelsLike, 105 + ...colsHints.feelsLikeCandidates, 106 + colsHints.wind, 107 + ...colsHints.windCandidates, 108 + colsHints.gust, 109 + ...colsHints.gustCandidates, 110 + ]; 111 + for (const c of candidateGroups) pushCol(c); 112 + 113 + // Use max for temperatures and wind/gust, avg for others 114 + const aggList = orderedCols.map((c) => { 115 + const escaped = c.replace(/"/g, '""'); 116 + const isTemp = colsHints.temp === c || colsHints.tempCandidates.includes(c) || c.toLowerCase().includes("temperatur"); 117 + const isDew = colsHints.dew === c || colsHints.dewCandidates.includes(c) || c.toLowerCase().includes("taupunkt"); 118 + const isFeels = colsHints.feelsLike === c || colsHints.feelsLikeCandidates.includes(c) || c.toLowerCase().includes("gefühl"); 119 + const isWind = colsHints.wind === c || colsHints.windCandidates.includes(c); 120 + const isGust = colsHints.gust === c || colsHints.gustCandidates.includes(c); 121 + const isRain = c.toLowerCase().includes("niederschlag") || c.toLowerCase().includes("rain"); 122 + 123 + const numericExpr = isWind || isGust ? speedExprFor(c) : sqlNum('"' + escaped + '"'); 124 + const aggFunc = (isTemp || isDew || isFeels || isWind || isGust) ? "max" : "avg"; 125 + return `${aggFunc}(${numericExpr}) AS "${escaped}"`; 126 + }).join(",\n "); 127 + 128 + const whereStart = start ? `(ts >= strptime('${start.toISOString().slice(0, 16).replace("T", " ")}', ['%Y-%m-%d %H:%M']))` : "1=1"; 129 + const whereEnd = end ? `(ts <= strptime('${end.toISOString().slice(0, 16).replace("T", " ")}', ['%Y-%m-%d %H:%M']))` : "1=1"; 130 + const unionSources = parquetPaths.map((p) => `SELECT * FROM read_parquet('${p}')`).join("\nUNION ALL\n"); 131 + 132 + const sql = ` 133 + WITH src AS ( 134 + ${unionSources} 135 + ), 136 + filt AS ( 137 + SELECT * FROM src WHERE ts IS NOT NULL AND ${whereStart} AND ${whereEnd} 138 + ) 139 + SELECT 140 + strftime(date_trunc('day', ts) + INTERVAL '12 hours', '%Y-%m-%d %H:%M:%S') AS time 141 + ${aggList ? ",\n " + aggList : ""} 142 + FROM filt 143 + GROUP BY date_trunc('day', ts) 144 + ORDER BY 1 145 + `; 146 + const reader = await conn.runAndReadAll(sql); 147 + let outRows: any[] = reader.getRowObjects(); 148 + outRows = outRows.map((r: any) => ({ key: r.time, ...r })); 149 + const header = ["time", ...orderedCols]; 150 + return NextResponse.json({ file: fileLabel, header, rows: outRows }, { status: 200 }); 80 151 } 81 152 82 153 const parquetPaths = parquetFiles.map((p) => p.replace(/\\/g, "/"));
-5
src/components/Statistics.tsx
··· 140 140 {t("statistics.maxDay", "Max day")} : {fmtNum(p.maxDay)} mm<br /> 141 141 <span className="text-xs text-gray-600">({fmtDate(p.maxDayDate)})</span> 142 142 </div> 143 - <div> 144 - {t("statistics.minDay", "Min day")} : {fmtNum(p.minDay)} mm<br /> 145 - <span className="text-xs text-gray-600">({fmtDate(p.minDayDate)})</span> 146 - </div> 147 143 </div> 148 144 ) : ( 149 145 <div className="grid grid-cols-2 sm:grid-cols-4 gap-2 text-sm"> 150 146 <div>{t("dashboard.total")} : {fmtNum(p.total)} mm</div> 151 147 <div>{t("statistics.maxDay", "Max day")} : {fmtNum(p.maxDay)} mm ({fmtDate(p.maxDayDate)})</div> 152 - <div>{t("statistics.minDay", "Min day")} : {fmtNum(p.minDay)} mm ({fmtDate(p.minDayDate)})</div> 153 148 </div> 154 149 )} 155 150 <div className="mt-2">
+1 -5
src/lib/statistics.ts
··· 111 111 FROM casted 112 112 GROUP BY 1 113 113 ) 114 - SELECT strftime(d, '%Y-%m-%d %H:%M:%S') AS day, 114 + SELECT strftime(d, '%Y-%m-%d') AS day, 115 115 tmax, tmin, tavg, 116 116 tfmax, tfmin, 117 117 COALESCE(rdaily, rhourly, rgeneric) AS rain_day, ··· 251 251 const under10: { date: string; value: number }[] = []; 252 252 253 253 let rainTotal = 0; let rainCnt = 0; let rainMax = Number.NEGATIVE_INFINITY; let rainMaxDate: string | null = null; 254 - let rainMin = Number.POSITIVE_INFINITY; let rainMinDate: string | null = null; 255 254 const rainOver20: { date: string; value: number }[] = []; 256 255 const rainOver30: { date: string; value: number }[] = []; 257 256 let rainDays = 0; ··· 287 286 if (rd > 0) rainDays++; 288 287 rainTotal += rd; 289 288 if (rd > rainMax) { rainMax = rd; rainMaxDate = d; } 290 - if (rd < rainMin) { rainMin = rd; rainMinDate = d; } 291 289 if (rd >= 20) rainOver20.push({ date: d, value: rd }); 292 290 if (rd >= 30) rainOver30.push({ date: d, value: rd }); 293 291 } ··· 322 320 total: rainCnt > 0 && Number.isFinite(rainTotal) ? rainTotal : null, 323 321 maxDay: rainCnt > 0 && Number.isFinite(rainMax) ? rainMax : null, 324 322 maxDayDate: rainCnt > 0 ? rainMaxDate : null, 325 - minDay: rainCnt > 0 && Number.isFinite(rainMin) ? rainMin : null, 326 - minDayDate: rainCnt > 0 ? rainMinDate : null, 327 323 over20mm: { count: rainOver20.length, items: rainOver20 }, 328 324 over30mm: { count: rainOver30.length, items: rainOver30 }, 329 325 } as YearStats["precipitation"];
-2
src/types/statistics.ts
··· 25 25 total: number | null; // sum of daily totals over the period (mm) 26 26 maxDay: number | null; // maximum daily total in the period (mm) 27 27 maxDayDate: string | null; // YYYY-MM-DD 28 - minDay: number | null; // minimum daily total in the period (mm) 29 - minDayDate: string | null; // YYYY-MM-DD 30 28 over20mm: ThresholdList; // days with daily total >= 20 mm 31 29 over30mm: ThresholdList; // days with daily total >= 30 mm 32 30 }