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 ` const WMO_CODES: Record = { 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 { 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 { 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}`); } } }