homeio
PublicForked from doctor-io/homeio
useCurrentWeather.ts
"use client";
import { useQuery } from "@tanstack/react-query";
import type { WeatherSnapshot } from "@/lib/shared/contracts/weather";
import { queryKeys } from "@/lib/shared/query-keys";
import { useUserLocation } from "@/modules/system/hooks/useUserLocation";
type Coordinates = {
latitude: number;
longitude: number;
};
type OpenMeteoResponse = {
current?: {
temperature_2m?: number;
apparent_temperature?: number;
relative_humidity_2m?: number;
wind_speed_10m?: number;
weather_code?: number;
};
daily?: {
time?: string[];
weather_code?: number[];
temperature_2m_max?: number[];
temperature_2m_min?: number[];
};
};
function toNumber(value: unknown) {
return typeof value === "number" && Number.isFinite(value) ? value : null;
}
function toRounded(value: number | null, precision = 1) {
if (value === null) return null;
return Number(value.toFixed(precision));
}
function mapWeatherCodeToCondition(code: number | null) {
if (code === null) return "Unknown";
if (code === 0) return "Clear sky";
if (code === 1) return "Mainly clear";
if (code === 2) return "Partly cloudy";
if (code === 3) return "Overcast";
if (code === 45 || code === 48) return "Fog";
if (code >= 51 && code <= 57) return "Drizzle";
if (code >= 61 && code <= 67) return "Rain";
if (code >= 71 && code <= 77) return "Snow";
if (code >= 80 && code <= 82) return "Rain showers";
if (code === 85 || code === 86) return "Snow showers";
if (code === 95) return "Thunderstorm";
if (code === 96 || code === 99) return "Thunderstorm with hail";
return "Unknown";
}
function buildDailyForecast(daily: OpenMeteoResponse["daily"]) {
const dates = daily?.time ?? [];
const weatherCodes = daily?.weather_code ?? [];
const maxTemps = daily?.temperature_2m_max ?? [];
const minTemps = daily?.temperature_2m_min ?? [];
const entriesCount = Math.min(
dates.length,
weatherCodes.length,
maxTemps.length,
minTemps.length,
);
if (entriesCount <= 0) return [] as WeatherSnapshot["dailyForecast"];
const startIndex = entriesCount > 5 ? 1 : 0;
const endExclusive = Math.min(startIndex + 5, entriesCount);
const forecast: WeatherSnapshot["dailyForecast"] = [];
for (let index = startIndex; index < endExclusive; index += 1) {
const weatherCode = toNumber(weatherCodes[index]);
forecast.push({
date: dates[index] ?? "",
weatherCode,
condition: mapWeatherCodeToCondition(weatherCode),
tempMaxC: toRounded(toNumber(maxTemps[index]), 1),
tempMinC: toRounded(toNumber(minTemps[index]), 1),
});
}
return forecast;
}
function formatLocationLabel(coords: { city?: string; country?: string }) {
if (coords.city && coords.country) return `${coords.city}, ${coords.country}`;
return coords.city ?? coords.country ?? "Current location";
}
async function fetchCurrentWeather(
coords: Coordinates & { city?: string; country?: string },
) {
const params = new URLSearchParams({
latitude: coords.latitude.toString(),
longitude: coords.longitude.toString(),
current:
"temperature_2m,apparent_temperature,relative_humidity_2m,wind_speed_10m,weather_code",
daily: "weather_code,temperature_2m_max,temperature_2m_min",
timezone: "auto",
});
const endpoint = `https://api.open-meteo.com/v1/forecast?${params.toString()}`;
const response = await fetch(endpoint, {
cache: "no-store",
});
if (!response.ok) {
throw new Error(`Failed to fetch weather (${response.status})`);
}
const json = (await response.json()) as OpenMeteoResponse;
const weatherCode = toNumber(json.current?.weather_code);
const locationLabel = formatLocationLabel(coords);
return {
timestamp: new Date().toISOString(),
source: "navigator",
location: {
label: locationLabel,
latitude: toRounded(coords.latitude, 4),
longitude: toRounded(coords.longitude, 4),
},
current: {
temperatureC: toRounded(toNumber(json.current?.temperature_2m), 1),
feelsLikeC: toRounded(toNumber(json.current?.apparent_temperature), 1),
humidityPercent: toRounded(
toNumber(json.current?.relative_humidity_2m),
0,
),
windSpeedKph: toRounded(toNumber(json.current?.wind_speed_10m), 1),
weatherCode,
condition: mapWeatherCodeToCondition(weatherCode),
},
dailyForecast: buildDailyForecast(json.daily),
} satisfies WeatherSnapshot;
}
export function useCurrentWeather() {
const { location } = useUserLocation();
const coords = location;
return useQuery({
queryKey: queryKeys.currentWeather(
coords?.latitude ?? null,
coords?.longitude ?? null,
),
queryFn: () => {
if (!coords) throw new Error("Missing coordinates");
return fetchCurrentWeather(coords);
},
enabled: Boolean(coords),
staleTime: 300_000,
refetchInterval: 600_000,
});
}