|
@@ -11,16 +11,14 @@
|
|
|
*/
|
|
*/
|
|
|
|
|
|
|
|
import { NextResponse } from 'next/server';
|
|
import { NextResponse } from 'next/server';
|
|
|
-import type { HKOCurrentTemp, HKOStation } from '@/lib/types';
|
|
|
|
|
|
|
+import type { HKOCurrentTemp } from '@/lib/types';
|
|
|
import { getObservedDailyHigh, persistObservedTemp } from '@/lib/queries';
|
|
import { getObservedDailyHigh, persistObservedTemp } from '@/lib/queries';
|
|
|
|
|
+import { fetchHKOCurrent, REFERENCE_STATION_JSON } from '@/lib/hko';
|
|
|
|
|
|
|
|
export const dynamic = 'force-dynamic';
|
|
export const dynamic = 'force-dynamic';
|
|
|
|
|
|
|
|
-const CSV_URL = 'https://data.weather.gov.hk/weatherAPI/hko_data/regional-weather/latest_1min_temperature.csv';
|
|
|
|
|
const JSON_URL = 'https://data.weather.gov.hk/weatherAPI/opendata/weather.php?dataType=rhrread&lang=en';
|
|
const JSON_URL = 'https://data.weather.gov.hk/weatherAPI/opendata/weather.php?dataType=rhrread&lang=en';
|
|
|
const FLW_URL = 'https://data.weather.gov.hk/weatherAPI/opendata/weather.php?dataType=flw&lang=en';
|
|
const FLW_URL = 'https://data.weather.gov.hk/weatherAPI/opendata/weather.php?dataType=flw&lang=en';
|
|
|
-const REFERENCE_STATION_CSV = 'HK Observatory';
|
|
|
|
|
-const REFERENCE_STATION_JSON = 'Hong Kong Observatory';
|
|
|
|
|
|
|
|
|
|
// In-memory cache (1-min TTL — CSV updates every minute)
|
|
// In-memory cache (1-min TTL — CSV updates every minute)
|
|
|
let cache: { data: HKOCurrentTemp; expiresAt: number } | null = null;
|
|
let cache: { data: HKOCurrentTemp; expiresAt: number } | null = null;
|
|
@@ -78,113 +76,6 @@ async function fetchWeatherInfo(): Promise<{ temp: number | null; icon: number |
|
|
|
return { temp: dailyHigh, icon, forecastDesc };
|
|
return { temp: dailyHigh, icon, forecastDesc };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/**
|
|
|
|
|
- * Parse the HKO 1-minute temperature CSV.
|
|
|
|
|
- * Format: "Date time,Automatic Weather Station,Air Temperature(degree Celsius)"
|
|
|
|
|
- * Example: "202603191730,HK Observatory,26.4"
|
|
|
|
|
- */
|
|
|
|
|
-function parseTemperatureCSV(csv: string): { stations: HKOStation[]; recordTime: string } | null {
|
|
|
|
|
- const lines = csv.trim().split('\n');
|
|
|
|
|
- if (lines.length < 2) return null;
|
|
|
|
|
-
|
|
|
|
|
- const stations: HKOStation[] = [];
|
|
|
|
|
- let recordTime = '';
|
|
|
|
|
-
|
|
|
|
|
- for (let i = 1; i < lines.length; i++) {
|
|
|
|
|
- const parts = lines[i].split(',');
|
|
|
|
|
- if (parts.length < 3) continue;
|
|
|
|
|
-
|
|
|
|
|
- const [datetime, place, tempStr] = parts;
|
|
|
|
|
- if (!datetime || !place || !tempStr || tempStr.trim() === 'N/A') continue;
|
|
|
|
|
-
|
|
|
|
|
- const temp = parseFloat(tempStr);
|
|
|
|
|
- if (isNaN(temp)) continue;
|
|
|
|
|
-
|
|
|
|
|
- stations.push({ place: place.trim(), value: temp, unit: 'C' });
|
|
|
|
|
-
|
|
|
|
|
- if (!recordTime && datetime) {
|
|
|
|
|
- // Convert "202603191730" to ISO format
|
|
|
|
|
- const y = datetime.slice(0, 4);
|
|
|
|
|
- const m = datetime.slice(4, 6);
|
|
|
|
|
- const d = datetime.slice(6, 8);
|
|
|
|
|
- const h = datetime.slice(8, 10);
|
|
|
|
|
- const min = datetime.slice(10, 12);
|
|
|
|
|
- recordTime = `${y}-${m}-${d}T${h}:${min}:00+08:00`;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return stations.length > 0 ? { stations, recordTime } : null;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-async function fetchFromCSV(): Promise<HKOCurrentTemp | null> {
|
|
|
|
|
- const res = await fetch(CSV_URL, {
|
|
|
|
|
- signal: AbortSignal.timeout(10_000),
|
|
|
|
|
- cache: 'no-store',
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- if (!res.ok) return null;
|
|
|
|
|
-
|
|
|
|
|
- const text = await res.text();
|
|
|
|
|
- const parsed = parseTemperatureCSV(text);
|
|
|
|
|
- if (!parsed) return null;
|
|
|
|
|
-
|
|
|
|
|
- const refStation = parsed.stations.find((s) => s.place === REFERENCE_STATION_CSV);
|
|
|
|
|
-
|
|
|
|
|
- return {
|
|
|
|
|
- current: {
|
|
|
|
|
- temp: refStation?.value ?? parsed.stations[0]?.value ?? 0,
|
|
|
|
|
- unit: 'C',
|
|
|
|
|
- recordTime: parsed.recordTime,
|
|
|
|
|
- },
|
|
|
|
|
- stations: parsed.stations,
|
|
|
|
|
- };
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-async function fetchFromJSON(): Promise<HKOCurrentTemp | null> {
|
|
|
|
|
- const res = await fetch(JSON_URL, {
|
|
|
|
|
- headers: { Accept: 'application/json' },
|
|
|
|
|
- signal: AbortSignal.timeout(10_000),
|
|
|
|
|
- cache: 'no-store',
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- if (!res.ok) return null;
|
|
|
|
|
-
|
|
|
|
|
- const json = await res.json();
|
|
|
|
|
- const stations: HKOStation[] = (json.temperature?.data ?? []).map(
|
|
|
|
|
- (s: { place: string; value: number; unit: string }) => ({
|
|
|
|
|
- place: s.place,
|
|
|
|
|
- value: s.value,
|
|
|
|
|
- unit: s.unit,
|
|
|
|
|
- }),
|
|
|
|
|
- );
|
|
|
|
|
-
|
|
|
|
|
- const refStation = stations.find((s) => s.place === REFERENCE_STATION_JSON);
|
|
|
|
|
-
|
|
|
|
|
- const result: HKOCurrentTemp = {
|
|
|
|
|
- current: {
|
|
|
|
|
- temp: refStation?.value ?? stations[0]?.value ?? 0,
|
|
|
|
|
- unit: refStation?.unit ?? 'C',
|
|
|
|
|
- recordTime: json.temperature?.recordTime ?? new Date().toISOString(),
|
|
|
|
|
- },
|
|
|
|
|
- stations,
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- // Add humidity if available
|
|
|
|
|
- if (json.humidity?.data?.length) {
|
|
|
|
|
- const hkoHumidity = json.humidity.data.find(
|
|
|
|
|
- (h: { place: string; value: number }) => h.place === REFERENCE_STATION_JSON,
|
|
|
|
|
- );
|
|
|
|
|
- if (hkoHumidity) {
|
|
|
|
|
- result.humidity = {
|
|
|
|
|
- value: hkoHumidity.value,
|
|
|
|
|
- recordTime: json.humidity.recordTime,
|
|
|
|
|
- };
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return result;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
export async function GET() {
|
|
export async function GET() {
|
|
|
try {
|
|
try {
|
|
|
// Return cached if fresh
|
|
// Return cached if fresh
|
|
@@ -192,26 +83,34 @@ export async function GET() {
|
|
|
return NextResponse.json(cache.data);
|
|
return NextResponse.json(cache.data);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Try CSV first (0.1°C precision), fallback to JSON (integer)
|
|
|
|
|
- let result = await fetchFromCSV().catch(() => null);
|
|
|
|
|
|
|
+ const reading = await fetchHKOCurrent();
|
|
|
|
|
|
|
|
- if (!result) {
|
|
|
|
|
- result = await fetchFromJSON().catch(() => null);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (!result) {
|
|
|
|
|
|
|
+ if (!reading) {
|
|
|
return NextResponse.json(
|
|
return NextResponse.json(
|
|
|
{ error: 'Failed to fetch HKO data from all sources' },
|
|
{ error: 'Failed to fetch HKO data from all sources' },
|
|
|
{ status: 502 },
|
|
{ status: 502 },
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ const result: HKOCurrentTemp = {
|
|
|
|
|
+ current: {
|
|
|
|
|
+ temp: reading.temp,
|
|
|
|
|
+ unit: reading.unit,
|
|
|
|
|
+ recordTime: reading.recordTime,
|
|
|
|
|
+ },
|
|
|
|
|
+ stations: reading.stations,
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ if (reading.humidity) {
|
|
|
|
|
+ result.humidity = reading.humidity;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// Persist current reading to DB (fire-and-forget)
|
|
// Persist current reading to DB (fire-and-forget)
|
|
|
persistObservedTemp({
|
|
persistObservedTemp({
|
|
|
city: 'hong_kong',
|
|
city: 'hong_kong',
|
|
|
- temp: result.current.temp,
|
|
|
|
|
|
|
+ temp: reading.temp,
|
|
|
unit: 'C',
|
|
unit: 'C',
|
|
|
- recordTime: new Date(result.current.recordTime),
|
|
|
|
|
|
|
+ recordTime: new Date(reading.recordTime),
|
|
|
source: 'hko',
|
|
source: 'hko',
|
|
|
}).catch(() => {});
|
|
}).catch(() => {});
|
|
|
|
|
|