Files
signalk-plugin/plugin/index.cjs
2026-03-11 15:25:03 +01:00

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;
};