const { config } = require("./config.js"); const registerRoutes = require("./routes"); const { linkBotToApp } = require("./telegram/telegram.core.js"); const { getForecast, getSeaConditions } = require("./api_models/openmeteo.js"); const { publish } = require("./tools/publisher.js"); const realtime = require("./realtime/core.js"); const dataHub = require("./tools/dataHub.js"); const CONFIG = { forecast_current_frequency: 300000, // 5 min default in ms forecast_hourly_frequency: 3600000, // 1 hour default }; const state = { openMeteoTimer: null, app: null, startTime: null, }; const clearIntervalSafe = (timerId) => { if (timerId) clearInterval(timerId); return null; }; module.exports = function (app) { state.app = app; let lastHourlyUpdate = 0; const fetchAndPublishWeather = async (forceHourly = false) => { try { const pos = app.getSelfPath('navigation.position')?.value; if (!pos?.latitude || !pos?.longitude) { console.debug('[MEB] Posizione non disponibile per meteo'); return; } const now = Date.now(); // Richiedi 'hourly' se forzato, o se e' passata piu' di 1 ora const shouldFetchHourly = forceHourly || (now - lastHourlyUpdate > CONFIG.forecast_hourly_frequency); const mode = shouldFetchHourly ? 'both' : 'current'; if (shouldFetchHourly) console.log('[MEB] Scaricamento previsioni complete (hourly + current)...'); else console.debug('[MEB] Aggiornamento meteo (current)...'); const [forecast, sea] = await Promise.all([ getForecast(pos, { mode }), getSeaConditions(pos, { mode }) ]); if (forecast) publish(app, forecast, {}); if (sea) publish(app, sea, {}); if (shouldFetchHourly) { lastHourlyUpdate = now; } if (forecast || sea) { // Aggiorna cache centralizzata per Telegram on-demand dataHub.updateWeatherData(forecast, sea); // Invia al server SOLO quando è hourly (contiene previsioni 7gg) // I dati current-only non vengono inviati — sono già disponibili localmente if (shouldFetchHourly) { realtime.sendWeatherPayload({ forecast, sea }); } } } catch (error) { console.error('[MEB] Errore ciclo meteo:', error.message); } }; const plugin = { id: "meb", name: "MEB Plugin", start: async (settings) => { const randomVal = (min, max) => parseFloat((Math.random() * (max - min) + min).toFixed(2)); // Dati di test — i path SignalK DEVONO corrispondere alle sensor-references rules // Le regole definiscono main_path e subPath, quindi i dati devono seguire esattamente quei path publish(app, { // engine (main_path: propulsion.0, subPath: revolutions) "propulsion.0.revolutions": randomVal(1000, 5000), // navigation (main_path: navigation) "navigation.courseOverGroundTrue": randomVal(0, 360), "navigation.speedOverGround": randomVal(0, 30), "navigation.headingTrue": randomVal(0, 360), // position (lat/lon sotto navigation.position come oggetto) "navigation.position.latitude": randomVal(40, 45), "navigation.position.longitude": randomVal(9, 14), // service battery (main_path: electrical.batteries.service) — NB: spelling "electrical" "electrical.batteries.service.current": randomVal(-50, 50), "electrical.batteries.service.Voltage": randomVal(0, 500), "electrical.batteries.service.stateOfCharge": randomVal(0.1, 1), // traction battery (main_path: electrical.batteries.traction) "electrical.batteries.traction.current": randomVal(-100, 100), "electrical.batteries.traction.power": randomVal(0, 5000), "electrical.batteries.traction.stateOfCharge": randomVal(0.1, 1), "electrical.batteries.traction.temperature": randomVal(20, 45), "electrical.batteries.traction.Voltage": randomVal(48, 58), // temperatura (main_path: meb.temperature, single field) "meb.temperature": randomVal(15, 35), // waves (main_path: meb.waves) "meb.waves.direction": randomVal(0, 360), "meb.waves.height": randomVal(0, 4), "meb.waves.period": randomVal(1, 12), // wind (main_path: meb.wind) "meb.wind.direction": randomVal(0, 360), "meb.wind.speed": randomVal(0, 40), // system uptime (main_path: system.uptime, single field) "system.uptime": Math.floor(process.uptime()) }) try { // Aggiorna CONFIG dai settings di SignalK if (settings && settings.forecast_current_frequency) { CONFIG.forecast_current_frequency = settings.forecast_current_frequency * 1000; } if (settings && settings.forecast_hourly_frequency) { CONFIG.forecast_hourly_frequency = settings.forecast_hourly_frequency * 1000; } state.startTime = Date.now(); // Inizializza realtime (async: carica sensor refs dal server) await realtime.init(app, settings.sensor_code); // Telegram Bot if (config.telegramBotToken) { try { await linkBotToApp(app); console.log('[MEB] Telegram bot started'); } catch (error) { console.error('[MEB] Error starting Telegram bot:', error); } } else { console.warn('[MEB] Telegram bot disabled: TELEGRAM_BOT_TOKEN not set'); } // Map & API routes try { registerRoutes(app, settings); console.log('[MEB] Routes registered'); } catch (error) { console.error('[MEB] Error registering routes:', error); } // Avvio ciclo meteo: Prima esecuzione immediata (con hourly) fetchAndPublishWeather(true); // Timer ricorrente state.openMeteoTimer = setInterval(() => { fetchAndPublishWeather(false); }, CONFIG.forecast_current_frequency); console.log(`[MEB] Meteo polling avviato ogni ${CONFIG.forecast_current_frequency / 1000}s`); // Shutdown hooks (register once) const shutdown = async (reason = 'signal') => { try { console.log(`[MEB] Received ${reason}. Stopping plugin...`); await plugin.stop(); process.exit(0); } catch (err) { console.error('[MEB] Error during shutdown:', err); process.exit(1); } }; if (!process.__meb_shutdown_hooks_installed) { process.__meb_shutdown_hooks_installed = true; process.on('SIGINT', () => shutdown('SIGINT')); process.on('SIGTERM', () => shutdown('SIGTERM')); process.on('uncaughtException', (err) => { console.error('[MEB] uncaughtException:', err); shutdown('uncaughtException'); }); process.on('unhandledRejection', (reason) => { console.error('[MEB] unhandledRejection:', reason); shutdown('unhandledRejection'); }); } } catch (error) { console.error('[MEB] Error during plugin startup:', error); throw error; } }, stop: async () => { try { state.openMeteoTimer = clearIntervalSafe(state.openMeteoTimer); realtime.stop(); console.log('[MEB] Plugin stopped'); } catch (error) { console.error('[MEB] Error during plugin stop:', error); } }, schema: () => ({}), // Aggiorna la configurazione (da Telegram o API) setConfig: (key, value) => { if (key === 'forecast_current_frequency') { const ms = value * 1000; CONFIG.forecast_current_frequency = ms; // Riavvia il timer con la nuova frequenza state.openMeteoTimer = clearIntervalSafe(state.openMeteoTimer); state.openMeteoTimer = setInterval(() => { fetchAndPublishWeather(false); }, ms); console.log(`[MEB] Intervallo current aggiornato a ${value} s`); return true; } if (key === 'forecast_hourly_frequency') { CONFIG.forecast_hourly_frequency = value * 1000; console.log(`[MEB] Intervallo Hourly aggiornato a ${value} s`); return true; } return false; }, // Gestione Polling Meteo (Start/Stop/Force) startPolling: () => { if (state.openMeteoTimer) { console.log('[MEB] Polling già attivo.'); return false; } fetchAndPublishWeather(false); state.openMeteoTimer = setInterval(() => { fetchAndPublishWeather(false); }, CONFIG.forecast_current_frequency); console.log(`[MEB] Meteo AVVIATO (freq: ${CONFIG.forecast_current_frequency / 1000}s)`); return true; }, stopPolling: () => { if (!state.openMeteoTimer) { console.log('[MEB] Polling già fermo.'); return false; } state.openMeteoTimer = clearIntervalSafe(state.openMeteoTimer); console.log('[MEB] Meteo polling FERMATO.'); return true; }, isPollingActive: () => !!state.openMeteoTimer, forceUpdate: async () => { console.log('[MEB] Aggiornamento Meteo Forzato da Utente.'); await fetchAndPublishWeather(false); return true; }, getOpenApi: () => ({ openapi: "3.0.0", info: { title: "MEB Plugin API", version: "2.0.0" }, servers: [{ url: "/plugins/meb" }], paths: {} }), }; app.mebPlugin = plugin; return plugin; };