feat: Implement rulesets and layout management for kiosk plugin

- Added rulesets manager to handle various data types and updates via HTTP and WebSocket.
- Introduced layout store for managing kiosk layouts with caching and server synchronization.
- Enhanced dashboard and data routes to support new layout and ruleset features.
- Updated kiosk HTML and JavaScript to utilize new layout rendering and data binding.
- Removed obsolete map route and integrated map functionality into the new tile renderer.
- Improved Telegram commands to reflect changes in data structure and logging.
- Refactored weather fetching intervals to prevent multiple instances.
- Added SSE stream for real-time layout updates in the kiosk.
This commit is contained in:
Giuseppe Raffa
2026-05-12 10:17:54 +02:00
parent bb8d267cd4
commit c2c1598226
27 changed files with 1061 additions and 326 deletions

View File

@@ -1,41 +1,28 @@
const skFlow = require('../config/skFlow');
const realtimeCore = require('./realtime/core');
const {
FORECAST_CURRENT,
FORECAST_HOURLY,
MARINE_CURRENT,
MARINE_HOURLY
} = require('../rules');
const rulesets = require('./rulesets');
/**
* Helper: legge i codici openmeteo + il path SK destinato per i 4 tipi forecast/marine,
* leggendoli dal ruleset attivo. Fall back ai default in rules.js se il ruleset
* non e' ancora stato caricato.
*/
function paramsAndMap(type) {
const items = rulesets.getEnabledItems(type);
if (!items.length) return { codes: [], map: {} };
const codes = items.map(it => it.path).filter(Boolean);
const map = {};
for (const it of items) {
if (it.meta?.sk_path) map[it.path] = it.meta.sk_path;
}
return { codes, map };
}
const FETCH_TIMEOUT = 10000;
const FORECAST_API = 'https://api.open-meteo.com/v1/forecast';
const MARINE_API = 'https://marine-api.open-meteo.com/v1/marine';
/**
* Mapping da parametri API Open-Meteo a path Signal K.
* Questi path vengono pubblicati sul databrowser e letti dai log.
*/
const FORECAST_PATH_MAP = {
'temperature_2m': 'meb.forecasts.temperature',
'wind_speed_10m': 'meb.forecast.wind.speed',
'wind_direction_10m': 'meb.forecast.wind.direction',
'wind_gusts_10m': 'meb.forecast.wind.gusts',
'precipitation': 'meb.forecast.precipitation',
'rain': 'meb.forecast.rain',
'relative_humidity_2m': 'meb.forecast.humidity',
'pressure_msl': 'meb.forecast.pressure',
'precipitation_probability':'meb.forecast.precipitationProbability',
'cloud_cover': 'meb.forecast.cloudCover',
};
const MARINE_PATH_MAP = {
'wave_height': 'meb.waves.height',
'wave_direction': 'meb.waves.direction',
'wave_period': 'meb.waves.period',
'wave_peak_period': 'meb.waves.peakPeriod',
'ocean_current_velocity': 'meb.waves.currentVelocity',
'ocean_current_direction': 'meb.waves.currentDirection',
};
// I path map sono ora ottenuti dal ruleset (rulesets.getPathMap('forecast_*'/'marine_*'))
/**
* Fetch JSON con timeout
@@ -59,19 +46,20 @@ async function fetchJSON(url) {
*/
function publishCurrentToSignalK(forecastData, marineData) {
const skData = {};
const fMap = paramsAndMap('forecast_current').map;
const mMap = paramsAndMap('marine_current').map;
if (forecastData?.current) {
for (const [key, value] of Object.entries(forecastData.current)) {
if (key === 'time' || key === 'interval') continue;
const skPath = FORECAST_PATH_MAP[key];
const skPath = fMap[key];
if (skPath && value != null) skData[skPath] = value;
}
}
if (marineData?.current) {
for (const [key, value] of Object.entries(marineData.current)) {
if (key === 'time' || key === 'interval') continue;
const skPath = MARINE_PATH_MAP[key];
const skPath = mMap[key];
if (skPath && value != null) skData[skPath] = value;
}
}
@@ -79,7 +67,7 @@ function publishCurrentToSignalK(forecastData, marineData) {
if (Object.keys(skData).length > 0) {
skData['meb.weather.timestamp'] = Date.now();
skFlow.publish(skData);
console.log(`[OPENMETEO] Pubblicati ${Object.keys(skData).length} valori su Signal K`);
console.log(`[OPENMETEO] pubblicati ${Object.keys(skData).length} valori su Signal K`);
}
}
@@ -89,19 +77,20 @@ function publishCurrentToSignalK(forecastData, marineData) {
*/
function sendCurrentToRealtime(forecastData, marineData) {
const fields = {};
const fMap = paramsAndMap('forecast_current').map;
const mMap = paramsAndMap('marine_current').map;
if (forecastData?.current) {
for (const [key, value] of Object.entries(forecastData.current)) {
if (key === 'time' || key === 'interval') continue;
const skPath = FORECAST_PATH_MAP[key];
const skPath = fMap[key];
if (skPath && value != null) fields[skPath] = value;
}
}
if (marineData?.current) {
for (const [key, value] of Object.entries(marineData.current)) {
if (key === 'time' || key === 'interval') continue;
const skPath = MARINE_PATH_MAP[key];
const skPath = mMap[key];
if (skPath && value != null) fields[skPath] = value;
}
}
@@ -123,6 +112,8 @@ function sendForecastBatchToRealtime(forecastData, marineData) {
const times = forecastHourly?.time || marineHourly?.time;
const points = [];
const fMap = paramsAndMap('forecast_hourly').map;
const mMap = paramsAndMap('marine_hourly').map;
for (let i = 0; i < times.length; i++) {
const ts = new Date(times[i]).getTime();
@@ -131,15 +122,14 @@ function sendForecastBatchToRealtime(forecastData, marineData) {
if (forecastHourly) {
for (const [key, values] of Object.entries(forecastHourly)) {
if (key === 'time') continue;
const skPath = FORECAST_PATH_MAP[key];
const skPath = fMap[key];
if (skPath && values?.[i] != null) fields[skPath] = values[i];
}
}
if (marineHourly) {
for (const [key, values] of Object.entries(marineHourly)) {
if (key === 'time') continue;
const skPath = MARINE_PATH_MAP[key];
const skPath = mMap[key];
if (skPath && values?.[i] != null) fields[skPath] = values[i];
}
}
@@ -166,27 +156,30 @@ async function fetchCurrentWeather(location) {
return;
}
if (FORECAST_CURRENT.length === 0 && MARINE_CURRENT.length === 0) {
const forecastCurrent = paramsAndMap('forecast_current').codes;
const marineCurrent = paramsAndMap('marine_current').codes;
if (forecastCurrent.length === 0 && marineCurrent.length === 0) {
console.warn('[OPENMETEO] Nessun parametro current configurato');
return;
}
console.log(`[OPENMETEO] Fetch current — forecast: ${FORECAST_CURRENT.length} params, marine: ${MARINE_CURRENT.length} params`);
console.log(`[OPENMETEO] Fetch current — forecast:${forecastCurrent.length} marine:${marineCurrent.length}`);
let forecastData = null, marineData = null;
try {
const promises = [];
if (FORECAST_CURRENT.length > 0) {
const url = `${FORECAST_API}?latitude=${location.latitude}&longitude=${location.longitude}&current=${FORECAST_CURRENT.join(',')}`;
if (forecastCurrent.length > 0) {
const url = `${FORECAST_API}?latitude=${location.latitude}&longitude=${location.longitude}&current=${forecastCurrent.join(',')}`;
promises.push(fetchJSON(url).then(d => { forecastData = d; }).catch(e => {
console.error(`[OPENMETEO] Errore forecast current: ${e.message}`);
}));
}
if (MARINE_CURRENT.length > 0) {
const url = `${MARINE_API}?latitude=${location.latitude}&longitude=${location.longitude}&current=${MARINE_CURRENT.join(',')}&models=ecmwf_wam`;
if (marineCurrent.length > 0) {
const url = `${MARINE_API}?latitude=${location.latitude}&longitude=${location.longitude}&current=${marineCurrent.join(',')}&models=ecmwf_wam`;
promises.push(fetchJSON(url).then(d => { marineData = d; }).catch(e => {
console.error(`[OPENMETEO] Errore marine current: ${e.message}`);
}));
@@ -211,27 +204,30 @@ async function fetchHourlyForecasts(location) {
return;
}
if (FORECAST_HOURLY.length === 0 && MARINE_HOURLY.length === 0) {
const forecastHourly = paramsAndMap('forecast_hourly').codes;
const marineHourly = paramsAndMap('marine_hourly').codes;
if (forecastHourly.length === 0 && marineHourly.length === 0) {
console.warn('[OPENMETEO] Nessun parametro hourly configurato');
return;
}
console.log(`[OPENMETEO] Fetch hourly 7gg — forecast: ${FORECAST_HOURLY.length} params, marine: ${MARINE_HOURLY.length} params`);
console.log(`[OPENMETEO] Fetch hourly 7gg — forecast:${forecastHourly.length} marine:${marineHourly.length}`);
let forecastData = null, marineData = null;
try {
const promises = [];
if (FORECAST_HOURLY.length > 0) {
const url = `${FORECAST_API}?latitude=${location.latitude}&longitude=${location.longitude}&hourly=${FORECAST_HOURLY.join(',')}&forecast_days=7`;
if (forecastHourly.length > 0) {
const url = `${FORECAST_API}?latitude=${location.latitude}&longitude=${location.longitude}&hourly=${forecastHourly.join(',')}&forecast_days=7`;
promises.push(fetchJSON(url).then(d => { forecastData = d; }).catch(e => {
console.error(`[OPENMETEO] Errore forecast hourly: ${e.message}`);
}));
}
if (MARINE_HOURLY.length > 0) {
const url = `${MARINE_API}?latitude=${location.latitude}&longitude=${location.longitude}&hourly=${MARINE_HOURLY.join(',')}&forecast_days=7&models=ecmwf_wam`;
if (marineHourly.length > 0) {
const url = `${MARINE_API}?latitude=${location.latitude}&longitude=${location.longitude}&hourly=${marineHourly.join(',')}&forecast_days=7&models=ecmwf_wam`;
promises.push(fetchJSON(url).then(d => { marineData = d; }).catch(e => {
console.error(`[OPENMETEO] Errore marine hourly: ${e.message}`);
}));