import axios, { AxiosResponse } from 'axios';
import type {
OneCallResponse,
WeatherOptions,
LocationInput,
} from '../types/weather.js';
import { GeocodingService } from './geocoding.js';
import { LocationService, DetectedLocation } from './location.js';
export class WeatherService {
private readonly apiKey: string;
private readonly geocoding: GeocodingService;
private readonly locationService: LocationService;
private readonly baseUrl = 'https://api.openweathermap.org/data/3.0/onecall';
constructor(apiKey: string) {
this.apiKey = apiKey;
this.geocoding = new GeocodingService(apiKey);
this.locationService = new LocationService();
}
async getCurrentWeather(
location: LocationInput | undefined,
options: WeatherOptions = {}
): Promise<{
weather: OneCallResponse;
detectedLocation?: DetectedLocation;
}> {
let coordinates: { lat: number; lon: number };
let detectedLocation: DetectedLocation | undefined;
if (this.isEmptyLocation(location)) {
// No location provided, detect from IP
detectedLocation = await this.locationService.detectLocationFromIP();
coordinates = { lat: detectedLocation.lat, lon: detectedLocation.lon };
} else {
// Use provided location
coordinates = await this.geocoding.resolveLocation(location!);
}
const weather = await this.getWeatherByCoordinates(
coordinates.lat,
coordinates.lon,
{
units: 'metric',
...options,
}
);
return { weather, detectedLocation };
}
private isEmptyLocation(location: LocationInput | undefined): boolean {
if (!location) return true;
return (
!location.lat && !location.lon && !location.city && !location.zipCode
);
}
async getWeatherByCoordinates(
lat: number,
lon: number,
options: WeatherOptions = {}
): Promise<OneCallResponse> {
try {
const params: Record<string, string | number> = {
lat,
lon,
appid: this.apiKey,
};
// Default to metric units if not specified
params.units = options.units || 'metric';
if (options.lang) {
params.lang = options.lang;
}
if (options.exclude && options.exclude.length > 0) {
params.exclude = options.exclude.join(',');
}
const response: AxiosResponse<OneCallResponse> = await axios.get(
this.baseUrl,
{ params }
);
return response.data;
} catch (error) {
if (axios.isAxiosError(error)) {
if (error.response?.status === 401) {
throw new Error(
'Invalid API key or insufficient permissions for One Call API 3.0'
);
}
if (error.response?.status === 400) {
throw new Error(`Invalid coordinates: lat=${lat}, lon=${lon}`);
}
}
throw new Error(
`Failed to fetch weather data: ${
error instanceof Error ? error.message : 'Unknown error'
}`
);
}
}
async getHourlyForecast(
location: LocationInput | undefined,
hours = 24,
options: WeatherOptions = {}
): Promise<{
forecast: OneCallResponse['hourly'];
detectedLocation?: DetectedLocation;
}> {
const result = await this.getCurrentWeather(location, {
...options,
exclude: ['current', 'daily', 'alerts'],
});
return {
forecast: result.weather.hourly.slice(0, hours),
detectedLocation: result.detectedLocation,
};
}
async getDailyForecast(
location: LocationInput | undefined,
days = 7,
options: WeatherOptions = {}
): Promise<{
forecast: OneCallResponse['daily'];
detectedLocation?: DetectedLocation;
}> {
const result = await this.getCurrentWeather(location, {
...options,
exclude: ['current', 'hourly', 'alerts'],
});
return {
forecast: result.weather.daily.slice(0, days),
detectedLocation: result.detectedLocation,
};
}
async getWeatherAlerts(
location: LocationInput | undefined,
options: WeatherOptions = {}
): Promise<{
alerts: OneCallResponse['alerts'];
detectedLocation?: DetectedLocation;
}> {
const result = await this.getCurrentWeather(location, {
...options,
exclude: ['current', 'hourly', 'daily'],
});
return {
alerts: result.weather.alerts || [],
detectedLocation: result.detectedLocation,
};
}
}