Use local time
This commit is contained in:
5
.env
5
.env
@@ -1,4 +1,5 @@
|
|||||||
|
INFLUX_TOKEN=
|
||||||
|
ALTITUDE=
|
||||||
LATITUDE=
|
LATITUDE=
|
||||||
LONGITUDE=
|
LONGITUDE=
|
||||||
ALTITUDE=
|
TZ=
|
||||||
INFLUX_TOKEN=
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import EnvironmentCard from '@/components/EnvironmentCard.vue';
|
import EnvironmentCard from '@/components/EnvironmentCard.vue';
|
||||||
import GraphModal from '@/components/GraphModal.vue';
|
import GraphModal from '@/components/GraphModal.vue';
|
||||||
import HourlyForecast from '@/components/HourlyForecast.vue';
|
import HourlyForecast from '@/components/HourlyForecast.vue';
|
||||||
|
import {BASE} from '@/services/api.ts';
|
||||||
import {formatDate} from '@ztimson/utils';
|
import {formatDate} from '@ztimson/utils';
|
||||||
import {onMounted, onUnmounted, provide, ref} from 'vue';
|
import {onMounted, onUnmounted, provide, ref} from 'vue';
|
||||||
import MapView from '../components/MapView.vue';
|
import MapView from '../components/MapView.vue';
|
||||||
@@ -49,10 +50,6 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
.panel-col {
|
.panel-col {
|
||||||
width: 380px;
|
width: 380px;
|
||||||
flex-shrink: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
border-left: 1px solid var(--border);
|
border-left: 1px solid var(--border);
|
||||||
@@ -154,7 +151,7 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel-col">
|
<div class="panel-col">
|
||||||
<div class="flex-r align-items-center gap-3">
|
<div class="flex-r align-items-center gap-3 mb-3">
|
||||||
<div class="pe-3" style="border-right: 2px solid var(--border)">
|
<div class="pe-3" style="border-right: 2px solid var(--border)">
|
||||||
<h2 class="fs-6 m-0" style="line-height: 1em">{{time}}</h2>
|
<h2 class="fs-6 m-0" style="line-height: 1em">{{time}}</h2>
|
||||||
</div>
|
</div>
|
||||||
@@ -163,11 +160,14 @@ onUnmounted(() => {
|
|||||||
<h2 class="m-0 fs-1 fg-muted" style="line-height: 1em">{{date}}</h2>
|
<h2 class="m-0 fs-1 fg-muted" style="line-height: 1em">{{date}}</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<CurrentWeather />
|
<CurrentWeather class="mb-3" />
|
||||||
<HourlyForecast />
|
<HourlyForecast class="mb-3" />
|
||||||
<ForecastStrip />
|
<ForecastStrip class="mb-3" />
|
||||||
|
|
||||||
<EnvironmentCard />
|
<EnvironmentCard class="mb-3" />
|
||||||
|
<div class="align-x">
|
||||||
|
<a :href="BASE + '/docs'" style="font-size: 0.75em;" target="_blank">OpenAPI Docs</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<GraphModal :metric-key="selectedMetric" @close="selectedMetric = null" />
|
<GraphModal :metric-key="selectedMetric" @close="selectedMetric = null" />
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { createWriteStream, existsSync, mkdirSync } from 'fs'
|
|||||||
import { resolve, dirname } from 'path'
|
import { resolve, dirname } from 'path'
|
||||||
import { fileURLToPath } from 'url'
|
import { fileURLToPath } from 'url'
|
||||||
import { pipeline } from 'stream/promises'
|
import { pipeline } from 'stream/promises'
|
||||||
import { localDateStr } from './config.mjs'
|
import { tzOffsetMinutes } from './config.mjs'
|
||||||
|
|
||||||
const DIR = dirname(fileURLToPath(import.meta.url))
|
const DIR = dirname(fileURLToPath(import.meta.url))
|
||||||
const ICON_DIR = resolve(DIR, 'public', 'icons')
|
const ICON_DIR = resolve(DIR, 'public', 'icons')
|
||||||
@@ -122,10 +122,10 @@ export async function dailyWeather(lat, lon, start, end) {
|
|||||||
const humidity = data.daily.relative_humidity_2m_mean[i]
|
const humidity = data.daily.relative_humidity_2m_mean[i]
|
||||||
const uv = data.daily.uv_index_max[i]
|
const uv = data.daily.uv_index_max[i]
|
||||||
|
|
||||||
// Open-Meteo returns local date strings — parse as local midnight using TZ
|
// "2026-03-01" → local midnight by shifting from UTC midnight by tz offset
|
||||||
// e.g. "2026-03-01" → local midnight, not UTC midnight
|
const utcMidnight = new Date(`${time}T00:00:00Z`)
|
||||||
const tzOffset = (new Date(new Date().toLocaleString('en-US', { timeZone: require('./config.mjs').cfg().TZ })) - new Date(new Date().toLocaleString('en-US', { timeZone: 'UTC' }))) / 60_000
|
const offset = tzOffsetMinutes(utcMidnight)
|
||||||
const localMidnight = new Date(new Date(`${time}T00:00:00Z`).getTime() - tzOffset * 60_000)
|
const localMidnight = new Date(utcMidnight.getTime() - offset * 60_000)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
label: WMO[code] || 'Unknown',
|
label: WMO[code] || 'Unknown',
|
||||||
@@ -175,8 +175,8 @@ export async function hourlyWeather(lat, lon, start, end) {
|
|||||||
const data = await cachedFetch(`hourly_${lat}_${lon}_${startStr}_${endStr}`, url)
|
const data = await cachedFetch(`hourly_${lat}_${lon}_${startStr}_${endStr}`, url)
|
||||||
if (!data?.hourly?.time) return []
|
if (!data?.hourly?.time) return []
|
||||||
|
|
||||||
// Build sunrise/sunset map keyed by local date string from Open-Meteo
|
// Open-Meteo returns local time strings e.g. "2026-03-01T06:00" (no Z)
|
||||||
// Open-Meteo returns these as local time ISO strings e.g. "2026-03-01T06:45"
|
// sunMap keyed by date portion of those local strings
|
||||||
const sunMap = {}
|
const sunMap = {}
|
||||||
if (data.daily?.time) {
|
if (data.daily?.time) {
|
||||||
data.daily.time.forEach((date, i) => {
|
data.daily.time.forEach((date, i) => {
|
||||||
@@ -188,14 +188,14 @@ export async function hourlyWeather(lat, lon, start, end) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Promise.all(data.hourly.time.map(async (time, i) => {
|
return Promise.all(data.hourly.time.map(async (time, i) => {
|
||||||
// Open-Meteo returns local time strings — treat as UTC-equivalent for Date parsing
|
const dateKey = time.slice(0, 10) // "2026-03-01" from local string
|
||||||
// then key into sunMap using the local date portion
|
|
||||||
const slotDate = new Date(time) // parsed as local ISO (no Z suffix) — browser/node both treat naive as local
|
|
||||||
const dateKey = localDateStr(slotDate)
|
|
||||||
const sun = sunMap[dateKey] ?? {}
|
const sun = sunMap[dateKey] ?? {}
|
||||||
const t = slotDate.getTime()
|
// Open-Meteo local strings have no Z — append Z to treat as UTC for ms comparison
|
||||||
|
// This works because Open-Meteo already returns times in the location's local tz
|
||||||
|
// and sunrise/sunset are parsed the same way, so the comparison is consistent
|
||||||
|
const t = new Date(time + 'Z').getTime()
|
||||||
const daytime = sun.sunrise && sun.sunset
|
const daytime = sun.sunrise && sun.sunset
|
||||||
? t >= sun.sunrise.getTime() && t < sun.sunset.getTime()
|
? t >= new Date(sun.sunrise.toISOString().replace('Z', '') + 'Z').getTime() && t < new Date(sun.sunset.toISOString().replace('Z', '') + 'Z').getTime()
|
||||||
: false
|
: false
|
||||||
|
|
||||||
const code = data.hourly.weathercode[i]
|
const code = data.hourly.weathercode[i]
|
||||||
@@ -206,7 +206,7 @@ export async function hourlyWeather(lat, lon, start, end) {
|
|||||||
const clouds = Math.round(data.hourly.cloudcover[i]) / 100
|
const clouds = Math.round(data.hourly.cloudcover[i]) / 100
|
||||||
|
|
||||||
return {
|
return {
|
||||||
time: slotDate,
|
time: new Date(time + 'Z'),
|
||||||
daytime,
|
daytime,
|
||||||
label: WMO[code] || 'Unknown',
|
label: WMO[code] || 'Unknown',
|
||||||
icon,
|
icon,
|
||||||
|
|||||||
@@ -109,31 +109,26 @@ app.get('/api/hourly', async (req, res) => {
|
|||||||
app.get('/api/daily', async (req, res) => {
|
app.get('/api/daily', async (req, res) => {
|
||||||
const { fields } = req.query
|
const { fields } = req.query
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
|
const todayStart = new Date(now)
|
||||||
|
todayStart.setHours(0, 0, 0, 0)
|
||||||
|
const next = new Date(todayStart)
|
||||||
|
next.setDate(todayStart.getDate() + 1)
|
||||||
|
|
||||||
// Use local midnight as "today" boundary
|
const start = req.query.start ? new Date(req.query.start) : todayStart
|
||||||
const offset = tzOffsetMinutes(now)
|
const end = req.query.end ? new Date(req.query.end) : next
|
||||||
const localNow = new Date(now.getTime() + offset * 60_000)
|
|
||||||
localNow.setUTCHours(0, 0, 0, 0)
|
|
||||||
const todayLocal = new Date(localNow.getTime() - offset * 60_000) // back to UTC
|
|
||||||
const nextLocal = new Date(todayLocal.getTime() + 24 * 3600_000)
|
|
||||||
|
|
||||||
const start = req.query.start ? new Date(req.query.start) : todayLocal
|
|
||||||
const end = req.query.end ? new Date(req.query.end) : nextLocal
|
|
||||||
|
|
||||||
const coords = await getCoords()
|
const coords = await getCoords()
|
||||||
|
|
||||||
const todayStr = localDateStr(now)
|
const todayStr = localDateStr(now)
|
||||||
const usePhysics = lastForecast.summary && start <= now && end > now
|
const usePhysics = lastForecast.summary && start <= now && end > now
|
||||||
|
|
||||||
const [sensorResult] = await Promise.allSettled([start < now ? queryDaily(start, now) : Promise.resolve([])])
|
const [sensorResult] = await Promise.allSettled([start < now ? queryDaily(start, now) : Promise.resolve([])])
|
||||||
const history = sensorResult.status === 'fulfilled' ? sensorResult.value : []
|
const history = sensorResult.status === 'fulfilled' ? sensorResult.value : []
|
||||||
|
|
||||||
const meteoFrom = new Date(Math.max(nextLocal.getTime(), start.getTime()))
|
const meteoFrom = new Date(Math.max(next.getTime(), start.getTime()))
|
||||||
const [meteoResult] = await Promise.allSettled([end > nextLocal ? dailyWeather(coords.latitude, coords.longitude, meteoFrom, end) : Promise.resolve([])])
|
const [meteoResult] = await Promise.allSettled([end > next ? dailyWeather(coords.latitude, coords.longitude, meteoFrom, end) : Promise.resolve([])])
|
||||||
const meteo = meteoResult.status === 'fulfilled' ? meteoResult.value : []
|
const meteo = meteoResult.status === 'fulfilled' ? meteoResult.value : []
|
||||||
|
|
||||||
const todaySlot = usePhysics ? [lastForecast.summary] : []
|
const todaySlot = usePhysics ? [lastForecast.summary] : []
|
||||||
// Filter meteo using local date string so we never double-count today
|
|
||||||
const meteoClean = meteo.filter(r => localDateStr(new Date(r.time)) !== todayStr)
|
const meteoClean = meteo.filter(r => localDateStr(new Date(r.time)) !== todayStr)
|
||||||
|
|
||||||
const daily = getCelestialForecast(coords.latitude, coords.longitude, [...history, ...todaySlot, ...meteoClean])
|
const daily = getCelestialForecast(coords.latitude, coords.longitude, [...history, ...todaySlot, ...meteoClean])
|
||||||
|
|||||||
Reference in New Issue
Block a user