145 lines
4.1 KiB
TypeScript
145 lines
4.1 KiB
TypeScript
|
|
/**
|
|||
|
|
* The one shape the UI understands.
|
|||
|
|
*
|
|||
|
|
* Every provider returns wildly different JSON. Each adapter in ./providers
|
|||
|
|
* translates its provider's response into these types, so the UI never has
|
|||
|
|
* to know which provider it is talking to.
|
|||
|
|
*
|
|||
|
|
* Rules every adapter follows:
|
|||
|
|
* - Units are metric: °C, km/h, hPa, km, mm.
|
|||
|
|
* - Times are ISO-8601 instants in UTC ("2026-10-21T23:00:00.000Z").
|
|||
|
|
* The UI formats them in the location's time zone.
|
|||
|
|
* - A reading the provider doesn't offer is `null`, and the UI hides it.
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
export type ProviderId = "openweather" | "weatherapi" | "tomorrow" | "openmeteo";
|
|||
|
|
|
|||
|
|
/** A deliberately small set of conditions — one per pixel icon. */
|
|||
|
|
export type Condition = "clear" | "clouds" | "rain" | "storm" | "snow" | "fog";
|
|||
|
|
|
|||
|
|
export interface Place {
|
|||
|
|
name: string;
|
|||
|
|
region?: string;
|
|||
|
|
country?: string;
|
|||
|
|
lat: number;
|
|||
|
|
lon: number;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export interface CurrentWeather {
|
|||
|
|
time: string;
|
|||
|
|
tempC: number;
|
|||
|
|
feelsLikeC: number;
|
|||
|
|
condition: Condition;
|
|||
|
|
description: string; // human words, e.g. "light rain"
|
|||
|
|
isDay: boolean;
|
|||
|
|
humidity: number; // %
|
|||
|
|
windKph: number;
|
|||
|
|
gustKph: number | null;
|
|||
|
|
windDeg: number | null; // direction the wind blows FROM, 0 = north
|
|||
|
|
pressureHpa: number | null;
|
|||
|
|
dewPointC: number | null;
|
|||
|
|
visibilityKm: number | null;
|
|||
|
|
uvIndex: number | null;
|
|||
|
|
cloudCover: number | null; // %
|
|||
|
|
precipMm: number | null; // in the last hour
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export interface HourlyForecast {
|
|||
|
|
time: string; // start of the hour (or 3-hour block)
|
|||
|
|
tempC: number;
|
|||
|
|
feelsLikeC: number | null;
|
|||
|
|
condition: Condition;
|
|||
|
|
isDay: boolean;
|
|||
|
|
precipChance: number | null; // %
|
|||
|
|
precipMm: number | null;
|
|||
|
|
windKph: number;
|
|||
|
|
gustKph: number | null;
|
|||
|
|
windDeg: number | null;
|
|||
|
|
humidity: number | null;
|
|||
|
|
uvIndex: number | null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export interface DailyForecast {
|
|||
|
|
time: string; // any instant inside that local day (midday where possible)
|
|||
|
|
highC: number;
|
|||
|
|
lowC: number;
|
|||
|
|
condition: Condition;
|
|||
|
|
summary: string | null; // provider's own one-liner, if it has one
|
|||
|
|
precipChance: number | null;
|
|||
|
|
precipMm: number | null;
|
|||
|
|
windKph: number | null; // daily max
|
|||
|
|
gustKph: number | null;
|
|||
|
|
windDeg: number | null;
|
|||
|
|
uvIndexMax: number | null;
|
|||
|
|
humidity: number | null; // daily average
|
|||
|
|
sunrise: string | null;
|
|||
|
|
sunset: string | null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export type AlertSeverity = "extreme" | "severe" | "moderate" | "minor" | "unknown";
|
|||
|
|
|
|||
|
|
export interface WeatherAlert {
|
|||
|
|
title: string;
|
|||
|
|
severity: AlertSeverity;
|
|||
|
|
source: string; // who issued it, e.g. "NWS Twin Cities/Chanhassen MN"
|
|||
|
|
starts: string | null;
|
|||
|
|
ends: string | null;
|
|||
|
|
description: string;
|
|||
|
|
instruction: string | null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export interface AirQuality {
|
|||
|
|
usAqi: number; // US EPA scale, 0–500
|
|||
|
|
pm25: number | null; // µg/m³
|
|||
|
|
pm10: number | null;
|
|||
|
|
ozone: number | null;
|
|||
|
|
no2: number | null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** What one provider adapter returns. */
|
|||
|
|
export interface ProviderForecast {
|
|||
|
|
current: CurrentWeather;
|
|||
|
|
hourly: HourlyForecast[];
|
|||
|
|
daily: DailyForecast[];
|
|||
|
|
hourlyStepHours: 1 | 3;
|
|||
|
|
timeZone: string | null; // IANA name, if the provider tells us
|
|||
|
|
alerts: WeatherAlert[];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** What /api/weather returns: the provider's forecast plus extras from other sources. */
|
|||
|
|
export interface WeatherReport extends ProviderForecast {
|
|||
|
|
provider: ProviderId;
|
|||
|
|
timeZone: string | null;
|
|||
|
|
airQuality: AirQuality | null;
|
|||
|
|
alertSource: string | null;
|
|||
|
|
fetchedAt: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** The contract each provider adapter fulfils. */
|
|||
|
|
export interface WeatherProvider {
|
|||
|
|
id: ProviderId;
|
|||
|
|
label: string;
|
|||
|
|
/** Name of the env var holding its key, or null if no key is needed. */
|
|||
|
|
envKey: string | null;
|
|||
|
|
signupUrl: string;
|
|||
|
|
/** What this provider can deliver on the plan configured in .env.local. */
|
|||
|
|
limits(): { forecastDays: number; hourlyStepHours: 1 | 3 };
|
|||
|
|
getForecast(lat: number, lon: number): Promise<ProviderForecast>;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** Safe-to-share provider info for the browser (never includes keys). */
|
|||
|
|
export interface ProviderInfo {
|
|||
|
|
id: ProviderId;
|
|||
|
|
label: string;
|
|||
|
|
ready: boolean;
|
|||
|
|
signupUrl: string;
|
|||
|
|
forecastDays: number;
|
|||
|
|
hourlyStepHours: 1 | 3;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** Map overlays the radar panel can show. */
|
|||
|
|
export interface RadarFrame {
|
|||
|
|
time: string;
|
|||
|
|
url: string; // Leaflet tile template with {z}/{x}/{y}
|
|||
|
|
}
|