277 lines
11 KiB
JavaScript
277 lines
11 KiB
JavaScript
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;
|
|
};
|