82 lines
2.9 KiB
TypeScript
82 lines
2.9 KiB
TypeScript
import { Injectable, Logger } from '@nestjs/common';
|
|
import { Cron } from '@nestjs/schedule';
|
|
import { BROADCAST_ADDR } from '../mesh/codec.js';
|
|
import { MeshService } from '../mesh/mesh.service.js';
|
|
|
|
/** Open-Meteo `current` block fields we use (free API, no key). */
|
|
export interface CurrentWeather {
|
|
temperature_2m: number;
|
|
apparent_temperature: number;
|
|
relative_humidity_2m: number;
|
|
wind_speed_10m: number;
|
|
weather_code: number;
|
|
}
|
|
|
|
// WMO weather interpretation codes (the common ones); unknown → `code <n>`
|
|
const WMO_CODES: Record<number, string> = {
|
|
0: 'clear',
|
|
1: 'mainly clear',
|
|
2: 'partly cloudy',
|
|
3: 'overcast',
|
|
45: 'fog',
|
|
48: 'freezing fog',
|
|
51: 'light drizzle',
|
|
53: 'drizzle',
|
|
55: 'heavy drizzle',
|
|
61: 'light rain',
|
|
63: 'rain',
|
|
65: 'heavy rain',
|
|
71: 'light snow',
|
|
73: 'snow',
|
|
75: 'heavy snow',
|
|
80: 'light rain showers',
|
|
81: 'rain showers',
|
|
82: 'violent rain showers',
|
|
95: 'thunderstorm',
|
|
};
|
|
|
|
@Injectable()
|
|
export class WeatherService {
|
|
private readonly logger = new Logger(WeatherService.name);
|
|
// read at construction, like MeshService's env config
|
|
private readonly lat = parseFloat(process.env.WEATHER_LAT ?? '51.2465');
|
|
private readonly lon = parseFloat(process.env.WEATHER_LON ?? '22.5684');
|
|
private readonly city = process.env.WEATHER_CITY ?? 'Lublin';
|
|
|
|
constructor(private readonly mesh: MeshService) {}
|
|
|
|
/** Fetch current conditions from Open-Meteo; throws on HTTP/parse failure. */
|
|
async fetchWeather(): Promise<CurrentWeather> {
|
|
const url =
|
|
`https://api.open-meteo.com/v1/forecast?latitude=${this.lat}` +
|
|
`&longitude=${this.lon}` +
|
|
`¤t=temperature_2m,apparent_temperature,relative_humidity_2m,wind_speed_10m,weather_code`;
|
|
const res = await fetch(url);
|
|
if (!res.ok) throw new Error(`open-meteo HTTP ${res.status}`);
|
|
const json = (await res.json()) as { current: CurrentWeather };
|
|
return json.current;
|
|
}
|
|
|
|
/** One short line, e.g. `Lublin weather: 18°C (feels 17), partly cloudy, humidity 65%, wind 12 km/h`. */
|
|
formatReport(current: CurrentWeather): string {
|
|
const desc =
|
|
WMO_CODES[current.weather_code] ?? `code ${current.weather_code}`;
|
|
return `${this.city} weather: ${Math.round(current.temperature_2m)}°C (feels ${Math.round(
|
|
current.apparent_temperature,
|
|
)}), ${desc}, humidity ${Math.round(current.relative_humidity_2m)}%, wind ${Math.round(
|
|
current.wind_speed_10m,
|
|
)} km/h`;
|
|
}
|
|
|
|
/** Hourly cron: fetch → format → broadcast on the pinned send channel. Never throws — a failed report must not crash the app. */
|
|
@Cron(process.env.WEATHER_CRON || '0 * * * *')
|
|
async report(): Promise<void> {
|
|
try {
|
|
const current = await this.fetchWeather();
|
|
await this.mesh.sendText(BROADCAST_ADDR, this.formatReport(current));
|
|
} catch (err) {
|
|
this.logger.warn(`[weather] report failed: ${(err as Error).message}`);
|
|
}
|
|
}
|
|
}
|