Placeholder wind/rain measurements
This commit is contained in:
@@ -1,11 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import {computed, onMounted, ref} from 'vue';
|
||||
import {api, BASE, type DataRow} from '../services/api';
|
||||
import {onMounted, ref} from 'vue';
|
||||
import {api, BASE} from '../services/api';
|
||||
|
||||
const data = ref<any>({});
|
||||
const precipitation = '';
|
||||
const humidity = computed(() => data.value.humidity || '—');
|
||||
const label = computed(() => data.value.forecast_weather_label || '');
|
||||
|
||||
function dir(deg: number) {
|
||||
if(deg > 0 && deg <= 22.5 || deg > 337.5) return 'N';
|
||||
if(deg > 22.5 && deg <= 67.5) return 'NE';
|
||||
if(deg > 67.5 && deg <= 112.5) return 'E';
|
||||
if(deg > 112.5 && deg <= 157.5) return 'SE';
|
||||
if(deg > 157.5 && deg <= 202.5) return 'S';
|
||||
if(deg > 202.5 && deg <= 247.5) return 'SW';
|
||||
if(deg > 247.5 && deg <= 292.5) return 'W';
|
||||
if(deg > 292.5 && deg <= 337.5) return 'NW';
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
data.value = await api.current();
|
||||
@@ -120,13 +128,13 @@ onMounted(async () => {
|
||||
<div class="today-label">{{ data.label }}</div>
|
||||
<img v-if="data.icon" :src="BASE + data.icon" class="today-icon" alt="weather" />
|
||||
</div>
|
||||
<div class="flex-r align-items-center gap-3">
|
||||
<div class="flex-r align-items-center gap-2">
|
||||
<div class="fs-6">
|
||||
0°C
|
||||
{{ data.temperature.toFixed(0) }}°C
|
||||
</div>
|
||||
<div class="flex-c fg-muted">
|
||||
<span class="hi">↑ {{ high }}</span>
|
||||
<span class="lo">↓ {{ low }}</span>
|
||||
<span class="hi">↑ {{ 0 }}°</span>
|
||||
<span class="lo">↓ {{ 0 }}°</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -135,12 +143,11 @@ onMounted(async () => {
|
||||
|
||||
<div class="stat-grid">
|
||||
<div class="stat"><span class="stat-label">Precipitation</span><span class="stat-value">{{ precipProb }}</span></div>
|
||||
<div class="stat"><span class="stat-label">Humidity</span><span class="stat-value">{{ humidity }}</span><span v-if="precipHrs" class="stat-sub">over {{ precipHrs }}</span></div>
|
||||
<div class="stat"><span class="stat-label">Wind</span><span class="stat-value">{{ windMax }}</span></div>
|
||||
<div class="stat"><span class="stat-label">Clouds</span><span class="stat-value">{{ windMax }}</span></div>
|
||||
<div class="stat"><span class="stat-label">Air Quality</span><span class="stat-value">{{ windMax }}</span></div>
|
||||
<div class="stat"><span class="stat-label">UV index</span><span class="stat-value">{{ windMax }}</span></div>
|
||||
<div class="stat"><span class="stat-label">Humidity</span><span class="stat-value">{{ data.humidity }}</span><span v-if="precipHrs" class="stat-sub">over {{ precipHrs }}</span></div>
|
||||
<div class="stat"><span class="stat-label">Wind</span><span class="stat-value">{{ data.wind_gusts }} ({{ dir(data.wind_direction) }})</span></div>
|
||||
<div class="stat"><span class="stat-label">Clouds</span><span class="stat-value">{{ data.clouds }}</span></div>
|
||||
<div class="stat"><span class="stat-label">Air Quality</span><span class="stat-value">{{ data.air_quality }}</span></div>
|
||||
<div class="stat"><span class="stat-label">UV index</span><span class="stat-value">{{ data.uv_index }}</span></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -2,15 +2,18 @@ export const BASE = import.meta.env.DEV ? 'http://10.69.5.23' : ''
|
||||
|
||||
export type DataRow = Record<string, number | string | null>
|
||||
|
||||
const cache: any = {};
|
||||
|
||||
async function get<T>(path: string, params: Record<string, string> = {}): Promise<T> {
|
||||
const url = new URL(`${BASE}${path}`, window.location.origin)
|
||||
Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v))
|
||||
const res = await fetch(url.toString())
|
||||
return res.json()
|
||||
if(cache[url.toString()]) return cache[url.toString()];
|
||||
cache[url.toString()] = await fetch(url.toString()).then(resp => resp.json()).finally(() => { delete cache[url.toString()]; });
|
||||
return cache[url.toString()]
|
||||
}
|
||||
|
||||
export const api = {
|
||||
current: (fields?: string) => get<DataRow>('/api/data', fields ? { fields } : {}),
|
||||
current: (fields?: string) => get<DataRow>('/api/current', fields ? { fields } : {}),
|
||||
hourly: (start?: string, end?: string, fields?: string) => get<DataRow[]>('/api/hourly', { ...(start ? { start } : {}), ...(end ? { end } : {}), ...(fields ? { fields } : {}) }),
|
||||
daily: (start?: string, end?: string, fields?: string) => get<DataRow[]>('/api/daily', { ...(start ? { start } : {}), ...(end ? { end } : {}), ...(fields ? { fields } : {}) }),
|
||||
}
|
||||
|
||||
@@ -492,6 +492,29 @@ def solar_elevation(lat, lon):
|
||||
))
|
||||
return round(el, 2)
|
||||
|
||||
# ── Rain ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
def read_rain(c):
|
||||
try:
|
||||
write(c, 'rain', {
|
||||
'precipitation': 0,
|
||||
'raining': False,
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"[Rain] {e}")
|
||||
|
||||
# ── Wind ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
def read_rain(c):
|
||||
try:
|
||||
write(c, 'wind', {
|
||||
'wind_direction': 0,
|
||||
'wind_speed': 0,
|
||||
'wind_gusts': 0,
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"[Wind] {e}")
|
||||
|
||||
# ── System ────────────────────────────────────────────────────────────────────
|
||||
|
||||
def read_system(c):
|
||||
@@ -566,6 +589,8 @@ if __name__ == '__main__':
|
||||
read_ltr(c, ltr_sensor, sol, humidity=last_rh)
|
||||
read_luna(c, last_temp)
|
||||
read_as3935(c)
|
||||
read_rain(c)
|
||||
read_wind(c)
|
||||
read_system(c)
|
||||
|
||||
time.sleep(c['LOOP_INTERVAL'])
|
||||
|
||||
@@ -1,148 +1,150 @@
|
||||
import { InfluxDB } from '@influxdata/influxdb-client'
|
||||
import { cfg } from './config.mjs'
|
||||
import {InfluxDB} from '@influxdata/influxdb-client';
|
||||
import {cfg} from './config.mjs';
|
||||
|
||||
let client, currentUrl, currentToken
|
||||
const MEASUREMENTS = ['accumulation', 'environment', 'light', 'lightning', 'rain', 'seismic', 'wind'];
|
||||
|
||||
let client, currentUrl, currentToken;
|
||||
|
||||
function getClient() {
|
||||
const c = cfg()
|
||||
if (c.INFLUX_URL !== currentUrl || c.INFLUX_TOKEN !== currentToken) {
|
||||
client = new InfluxDB({ url: c.INFLUX_URL, token: c.INFLUX_TOKEN })
|
||||
currentUrl = c.INFLUX_URL
|
||||
currentToken = c.INFLUX_TOKEN
|
||||
}
|
||||
return { client, c }
|
||||
const c = cfg();
|
||||
if(c.INFLUX_URL !== currentUrl || c.INFLUX_TOKEN !== currentToken) {
|
||||
client = new InfluxDB({url: c.INFLUX_URL, token: c.INFLUX_TOKEN});
|
||||
currentUrl = c.INFLUX_URL;
|
||||
currentToken = c.INFLUX_TOKEN;
|
||||
}
|
||||
return {client, c};
|
||||
}
|
||||
|
||||
async function query(flux) {
|
||||
const { client, c } = getClient()
|
||||
const api = client.getQueryApi(c.INFLUX_ORG)
|
||||
const rows = []
|
||||
return new Promise((resolve, reject) => {
|
||||
api.queryRows(flux, {
|
||||
next(row, meta) { rows.push(meta.toObject(row)) },
|
||||
error(err) { reject(err) },
|
||||
complete() { resolve(rows) },
|
||||
})
|
||||
})
|
||||
const {client, c} = getClient();
|
||||
const api = client.getQueryApi(c.INFLUX_ORG);
|
||||
const rows = [];
|
||||
return new Promise((resolve, reject) => {
|
||||
api.queryRows(flux, {
|
||||
next(row, meta) { rows.push(meta.toObject(row)); },
|
||||
error(err) { reject(err); },
|
||||
complete() { resolve(rows); },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function truncateToHour(iso) {
|
||||
const d = new Date(iso)
|
||||
d.setMinutes(0, 0, 0)
|
||||
const pad = n => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}T${pad(d.getHours())}:00`
|
||||
const d = new Date(iso);
|
||||
d.setMinutes(0, 0, 0);
|
||||
const pad = n => String(n).padStart(2, '0');
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:00`;
|
||||
}
|
||||
|
||||
function truncateToDay(iso) {
|
||||
return new Date(iso).toISOString().slice(0, 10)
|
||||
return new Date(iso).toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function extractRow(row) {
|
||||
const out = {}
|
||||
for (const [k, v] of Object.entries(row)) {
|
||||
if (!k.startsWith('_') && k !== 'result' && k !== 'table') out[k] = v
|
||||
}
|
||||
return out
|
||||
const out = {};
|
||||
for(const [k, v] of Object.entries(row)) {
|
||||
if(!k.startsWith('_') && k !== 'result' && k !== 'table') out[k] = v;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export async function queryCurrent() {
|
||||
const { c } = getClient()
|
||||
const flux = `
|
||||
const {c} = getClient();
|
||||
const flux = `
|
||||
from(bucket: "${c.INFLUX_BUCKET}")
|
||||
|> range(start: -1h)
|
||||
|> filter(fn: (r) => r._measurement != "system")
|
||||
|> filter(fn: (r) => ${MEASUREMENTS.map(m => `r._measurement == "${m}"`).join(' or ')})
|
||||
|> last()
|
||||
|> pivot(rowKey: ["_time"], columnKey: ["_field"], valueColumn: "_value")
|
||||
`
|
||||
const rows = await query(flux)
|
||||
const result = {}
|
||||
for (const row of rows) {
|
||||
Object.assign(result, extractRow(row))
|
||||
}
|
||||
return result
|
||||
`;
|
||||
const rows = await query(flux);
|
||||
const result = {};
|
||||
for(const row of rows) {
|
||||
Object.assign(result, extractRow(row));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function queryHourly(start, end) {
|
||||
const { c } = getClient()
|
||||
const flux = `
|
||||
const {c} = getClient();
|
||||
const flux = `
|
||||
from(bucket: "${c.INFLUX_BUCKET}")
|
||||
|> range(start: ${start}, stop: ${end})
|
||||
|> filter(fn: (r) => r._measurement != "system")
|
||||
|> filter(fn: (r) => ${MEASUREMENTS.map(m => `r._measurement == "${m}"`).join(' or ')})
|
||||
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|
||||
|> pivot(rowKey: ["_time"], columnKey: ["_field"], valueColumn: "_value")
|
||||
`
|
||||
const rows = await query(flux)
|
||||
return rows.map(row => ({
|
||||
time: truncateToHour(row._time),
|
||||
...extractRow(row),
|
||||
}))
|
||||
`;
|
||||
const rows = await query(flux);
|
||||
return rows.map(row => ({
|
||||
time: truncateToHour(row._time),
|
||||
...extractRow(row),
|
||||
}));
|
||||
}
|
||||
|
||||
export async function queryDaily(start, end) {
|
||||
const { c } = getClient()
|
||||
const measurements = ['accumulation', 'environment', 'light', 'lightning', 'seismic']
|
||||
const results = {}
|
||||
const {c} = getClient();
|
||||
const measurements = ['accumulation', 'environment', 'light', 'lightning', 'seismic'];
|
||||
const results = {};
|
||||
|
||||
for (const m of measurements) {
|
||||
const [means, mins, maxs] = await Promise.all([
|
||||
query(`
|
||||
for(const m of measurements) {
|
||||
const [means, mins, maxs] = await Promise.all([
|
||||
query(`
|
||||
from(bucket: "${c.INFLUX_BUCKET}")
|
||||
|> range(start: ${start}, stop: ${end})
|
||||
|> filter(fn: (r) => r._measurement == "${m}")
|
||||
|> aggregateWindow(every: 1d, fn: mean, createEmpty: false)
|
||||
|> pivot(rowKey: ["_time"], columnKey: ["_field"], valueColumn: "_value")
|
||||
`),
|
||||
query(`
|
||||
query(`
|
||||
from(bucket: "${c.INFLUX_BUCKET}")
|
||||
|> range(start: ${start}, stop: ${end})
|
||||
|> filter(fn: (r) => r._measurement == "${m}")
|
||||
|> aggregateWindow(every: 1d, fn: min, createEmpty: false)
|
||||
|> pivot(rowKey: ["_time"], columnKey: ["_field"], valueColumn: "_value")
|
||||
`),
|
||||
query(`
|
||||
query(`
|
||||
from(bucket: "${c.INFLUX_BUCKET}")
|
||||
|> range(start: ${start}, stop: ${end})
|
||||
|> filter(fn: (r) => r._measurement == "${m}")
|
||||
|> aggregateWindow(every: 1d, fn: max, createEmpty: false)
|
||||
|> pivot(rowKey: ["_time"], columnKey: ["_field"], valueColumn: "_value")
|
||||
`),
|
||||
])
|
||||
]);
|
||||
|
||||
// Mean values are the base — flat field names, no suffix
|
||||
for (const row of means) {
|
||||
const time = truncateToDay(row._time)
|
||||
if (!results[time]) results[time] = { time }
|
||||
Object.assign(results[time], extractRow(row))
|
||||
}
|
||||
// Mean values are the base — flat field names, no suffix
|
||||
for(const row of means) {
|
||||
const time = truncateToDay(row._time);
|
||||
if(!results[time]) results[time] = {time};
|
||||
Object.assign(results[time], extractRow(row));
|
||||
}
|
||||
|
||||
// Only promote temp min/max to named fields, ignore the rest
|
||||
for (const row of mins) {
|
||||
const time = truncateToDay(row._time)
|
||||
if (!results[time]) results[time] = { time }
|
||||
if (row.temperature != null) results[time].env_temp_min_c = row.temperature
|
||||
}
|
||||
for (const row of maxs) {
|
||||
const time = truncateToDay(row._time)
|
||||
if (!results[time]) results[time] = { time }
|
||||
if (row.temperature != null) results[time].env_temp_max_c = row.temperature
|
||||
}
|
||||
}
|
||||
// Only promote temp min/max to named fields, ignore the rest
|
||||
for(const row of mins) {
|
||||
const time = truncateToDay(row._time);
|
||||
if(!results[time]) results[time] = {time};
|
||||
if(row.temperature != null) results[time].env_temp_min_c = row.temperature;
|
||||
}
|
||||
for(const row of maxs) {
|
||||
const time = truncateToDay(row._time);
|
||||
if(!results[time]) results[time] = {time};
|
||||
if(row.temperature != null) results[time].env_temp_max_c = row.temperature;
|
||||
}
|
||||
}
|
||||
|
||||
return Object.values(results).sort((a, b) => a.time.localeCompare(b.time))
|
||||
return Object.values(results).sort((a, b) => a.time.localeCompare(b.time));
|
||||
}
|
||||
|
||||
export async function getCoords() {
|
||||
const { c } = getClient()
|
||||
const flux = `
|
||||
const {c} = getClient();
|
||||
const flux = `
|
||||
from(bucket: "${c.INFLUX_BUCKET}")
|
||||
|> range(start: -24h)
|
||||
|> filter(fn: (r) => r._measurement == "gps")
|
||||
|> last()
|
||||
|> pivot(rowKey: ["_time"], columnKey: ["_field"], valueColumn: "_value")
|
||||
`
|
||||
const rows = await query(flux)
|
||||
if (rows.length && rows[0].latitude && rows[0].longitude) {
|
||||
return { lat: rows[0].latitude, lon: rows[0].longitude, alt: rows[0].altitude || c.ALTITUDE }
|
||||
}
|
||||
return { latitude: c.LATITUDE, longitude: c.LONGITUDE, altitude: c.ALTITUDE }
|
||||
`;
|
||||
const rows = await query(flux);
|
||||
if(rows.length && rows[0].latitude && rows[0].longitude) {
|
||||
return {lat: rows[0].latitude, lon: rows[0].longitude, alt: rows[0].altitude || c.ALTITUDE};
|
||||
}
|
||||
return {latitude: c.LATITUDE, longitude: c.LONGITUDE, altitude: c.ALTITUDE};
|
||||
}
|
||||
|
||||
@@ -34,16 +34,6 @@ function filterArr(arr, fields) {
|
||||
return arr.map(row => filterFields(row, fields));
|
||||
}
|
||||
|
||||
function mergeRows(arrays) {
|
||||
const map = {};
|
||||
for(const arr of arrays) {
|
||||
for(const row of arr) {
|
||||
map[row.time] = {...map[row.time], ...row};
|
||||
}
|
||||
}
|
||||
return Object.values(map).sort((a, b) => a.time.localeCompare(b.time));
|
||||
}
|
||||
|
||||
// ── Sensor Data ───────────────────────────────────────────────────────────────
|
||||
|
||||
app.get('/api/current', async (req, res) => {
|
||||
|
||||
Reference in New Issue
Block a user