Files
2026-06-19 07:18:00 +02:00

255 lines
9.5 KiB
JavaScript

'use strict';
const axios = require('axios');
// ─── Cache ─────────────────────────────────────────────────────────────────────
let cache = { data: null, ts: null };
const CACHE_MS = () => (parseInt(process.env.CACHE_MINUTES, 10) || 10) * 60 * 1000;
// ─── Appel API principal ────────────────────────────────────────────────────────
async function fetchWeatherData() {
const now = Date.now();
if (cache.data && cache.ts && now - cache.ts < CACHE_MS()) {
if (process.env.DEBUG_METEO === 'true') console.log('[cache] Données météo en cache');
return cache.data;
}
const lat = process.env.LAT;
const lon = process.env.LON;
if (!lat || !lon) throw new Error('LAT et LON manquants dans .env');
const hourlyVars = [
'temperature_2m', 'apparent_temperature', 'relative_humidity_2m',
'wind_speed_10m', 'wind_gusts_10m', 'wind_direction_10m',
'precipitation', 'precipitation_probability', 'rain', 'snowfall', 'showers',
'pressure_msl', 'cloud_cover', 'cloud_cover_low', 'cloud_cover_mid', 'cloud_cover_high',
'visibility', 'uv_index', 'weather_code',
].join(',');
const dailyVars = [
'temperature_2m_max', 'temperature_2m_min',
'apparent_temperature_max', 'apparent_temperature_min',
'precipitation_sum', 'precipitation_probability_max',
'wind_speed_10m_max', 'wind_gusts_10m_max',
'uv_index_max', 'sunrise', 'sunset', 'weather_code',
].join(',');
const currentVars = [
'temperature_2m', 'apparent_temperature', 'relative_humidity_2m',
'wind_speed_10m', 'wind_gusts_10m', 'wind_direction_10m',
'precipitation', 'rain', 'snowfall', 'weather_code',
'pressure_msl', 'cloud_cover', 'visibility', 'uv_index', 'is_day',
].join(',');
const baseParams = {
latitude: lat,
longitude: lon,
timezone: 'Europe/Paris',
wind_speed_unit: 'kmh',
forecast_days: 2,
past_days: 1,
};
// Appels parallèles : météo principale + marine
const [weatherRes, marineRes] = await Promise.allSettled([
axios.get('https://api.open-meteo.com/v1/forecast', {
params: { ...baseParams, current: currentVars, hourly: hourlyVars, daily: dailyVars },
timeout: 10000,
}),
axios.get('https://marine-api.open-meteo.com/v1/marine', {
params: {
...baseParams,
hourly: 'wave_height,wave_direction,wave_period,swell_wave_height,swell_wave_direction',
daily: 'wave_height_max,swell_wave_height_max',
},
timeout: 8000,
}),
]);
if (weatherRes.status === 'rejected') {
throw new Error(`Open-Meteo inaccessible : ${weatherRes.reason?.message}`);
}
const weather = weatherRes.value.data;
const marine = marineRes.status === 'fulfilled' ? marineRes.value.data : null;
if (process.env.DEBUG_METEO === 'true') {
console.log('[debug] current:', JSON.stringify(weather.current, null, 2));
if (marine) console.log('[debug] marine disponible');
}
const result = { weather, marine, fetchedAt: new Date() };
cache = { data: result, ts: now };
return result;
}
// ─── Invalidation manuelle du cache ───────────────────────────────────────────
function invalidateCache() {
cache = { data: null, ts: null };
}
// ─── Index de l'heure courante dans les données horaires ──────────────────────
function getCurrentHourIndex(hourlyTimes) {
// Open-Meteo renvoie les heures en heure locale Paris (ex: "2024-06-10T14:00")
// On génère la même chaîne à partir de l'heure Paris actuelle
const nowParis = new Date().toLocaleString('sv', { timeZone: 'Europe/Paris' });
// format: "YYYY-MM-DD HH:MM:SS"
const parisHourPrefix = nowParis.substring(0, 13).replace(' ', 'T'); // "YYYY-MM-DDTHH"
let best = 0;
for (let i = 0; i < hourlyTimes.length; i++) {
if (hourlyTimes[i].startsWith(parisHourPrefix)) return i;
if (hourlyTimes[i] < `${parisHourPrefix}:59`) best = i;
}
return best;
}
// ─── Fenêtre des prochaines N heures depuis maintenant ────────────────────────
function getNextHoursWindow(hourlyTimes, count = 24) {
const start = getCurrentHourIndex(hourlyTimes);
const end = Math.min(start + count, hourlyTimes.length);
return { start, end };
}
// ─── Index du jour courant dans les tableaux daily ────────────────────────────
function getTodayDailyIndex(dailyTimes) {
// avec past_days:1, index 0 = hier, 1 = aujourd'hui, 2 = demain
const todayParis = new Date().toLocaleString('sv', { timeZone: 'Europe/Paris' }).substring(0, 10);
const idx = dailyTimes.findIndex(d => d === todayParis);
return idx >= 0 ? idx : 1; // fallback à 1 (aujourd'hui)
}
// ─── Helpers ──────────────────────────────────────────────────────────────────
function celsiusToKelvin(c) {
return (c + 273.15).toFixed(2);
}
function getWindDirection(deg) {
if (deg == null) return 'N/A';
const dirs = ['N','NNE','NE','ENE','E','ESE','SE','SSE','S','SSO','SO','OSO','O','ONO','NO','NNO'];
return dirs[Math.round(((deg % 360) + 360) % 360 / 22.5) % 16];
}
function getWeatherDescription(code) {
const map = {
0: '☀️ Ciel dégagé',
1: '🌤️ Peu nuageux',
2: '⛅ Partiellement nuageux',
3: '☁️ Couvert',
45: '🌫️ Brouillard',
48: '🌫️ Brouillard givrant',
51: '🌦️ Bruine légère',
53: '🌦️ Bruine modérée',
55: '🌧️ Bruine dense',
61: '🌧️ Pluie légère',
63: '🌧️ Pluie modérée',
65: '🌧️ Pluie forte',
71: '🌨️ Neige légère',
73: '🌨️ Neige modérée',
75: '❄️ Neige forte',
77: '🌨️ Grésil',
80: '🌦️ Averses légères',
81: '🌧️ Averses modérées',
82: '⛈️ Averses violentes',
85: '🌨️ Averses de neige',
86: '❄️ Averses de neige fortes',
95: '⛈️ Orage',
96: '⛈️ Orage avec grêle',
99: '⛈️ Orage avec grêle forte',
};
return map[code] ?? `Code météo ${code}`;
}
function getEmbedColor(code) {
if (code === 0) return 0xFFD700;
if (code <= 2) return 0x87CEEB;
if (code <= 3) return 0x708090;
if (code <= 48) return 0x9E9E9E;
if (code <= 55) return 0x90CAF9;
if (code <= 65) return 0x2196F3;
if (code <= 77) return 0xB0C4DE;
if (code <= 82) return 0x42A5F5;
return 0x7B1FA2; // orages
}
function getPressureTendency(hourlyPressure, currentIdx) {
if (currentIdx < 3) return '→ Stable';
const diff = hourlyPressure[currentIdx] - hourlyPressure[currentIdx - 3];
if (diff > 2) return '↑ En hausse';
if (diff < -2) return '↓ En baisse';
return '→ Stable';
}
function getVisibilityLabel(vis) {
if (vis == null) return 'N/A';
const km = vis / 1000;
if (km >= 20) return '✨ Excellente';
if (km >= 10) return '👍 Bonne';
if (km >= 5) return '😶‍🌫️ Correcte';
if (km >= 2) return '⚠️ Réduite';
if (km >= 1) return '🟠 Mauvaise';
return '🔴 Brouillard';
}
function knotsFromKmh(kmh) {
if (kmh == null) return 'N/A';
return (kmh / 1.852).toFixed(1);
}
function getUvLabel(uv) {
if (uv == null) return 'N/A';
if (uv <= 2) return '🟢 Faible';
if (uv <= 5) return '🟡 Modéré';
if (uv <= 7) return '🟠 Élevé';
if (uv <= 10) return '🔴 Très élevé';
return '🟣 Extrême';
}
// ─── Détection d'alertes météo extrêmes ───────────────────────────────────────
function checkAlerts(current, todayDaily) {
const alerts = [];
const wind = current.wind_speed_10m;
const gusts = current.wind_gusts_10m;
const rain = current.precipitation;
const code = current.weather_code;
const vis = current.visibility;
if (wind >= 90) alerts.push({ lvl: '🔴', msg: `Vent violent : **${wind} km/h**` });
else if (wind >= 65) alerts.push({ lvl: '🟠', msg: `Vent fort : **${wind} km/h**` });
else if (wind >= 50) alerts.push({ lvl: '🟡', msg: `Vent modéré-fort : **${wind} km/h**` });
if (gusts >= 110) alerts.push({ lvl: '🔴', msg: `Rafales extrêmes : **${gusts} km/h**` });
else if (gusts >= 80) alerts.push({ lvl: '🟠', msg: `Rafales fortes : **${gusts} km/h**` });
if (rain >= 15) alerts.push({ lvl: '🔴', msg: `Pluie torrentielle : **${rain} mm/h**` });
else if (rain >= 7) alerts.push({ lvl: '🟠', msg: `Fortes précipitations : **${rain} mm/h**` });
if (code >= 95) alerts.push({ lvl: '🔴', msg: `Orage en cours ⛈️` });
const snowDaily = todayDaily?.precipitation_sum;
if (current.snowfall >= 1) alerts.push({ lvl: '🟡', msg: `Chutes de neige en cours ❄️` });
if (vis != null && vis < 500) alerts.push({ lvl: '🔴', msg: `Visibilité quasi nulle : **${(vis/1000).toFixed(1)} km**` });
else if (vis != null && vis < 1500) alerts.push({ lvl: '🟠', msg: `Brouillard dense : **${(vis/1000).toFixed(1)} km**` });
return alerts;
}
module.exports = {
fetchWeatherData,
invalidateCache,
getCurrentHourIndex,
getNextHoursWindow,
getTodayDailyIndex,
celsiusToKelvin,
getWindDirection,
getWeatherDescription,
getEmbedColor,
getPressureTendency,
getVisibilityLabel,
getUvLabel,
knotsFromKmh,
checkAlerts,
};