138 lines
8.2 KiB
JavaScript
138 lines
8.2 KiB
JavaScript
// Weather location data access and form normalization.
|
|
|
|
const { fetchPagedRows, validateMaxLength } = require('./utils');
|
|
const { fetchAppSettings } = require('./app-settings');
|
|
|
|
const NAME_MAX_LENGTH = 255;
|
|
const LOCATION_MAX_LENGTH = 255;
|
|
const TIMEZONE_MAX_LENGTH = 128;
|
|
const PROVIDERS = ['open-meteo', 'pirate-weather'];
|
|
const TEMPERATURE_UNITS = ['celsius', 'fahrenheit'];
|
|
const WIND_UNITS = ['kmh', 'mph', 'ms'];
|
|
const PRECIPITATION_UNITS = ['mm', 'inch'];
|
|
|
|
async function fetchWeatherLocationSuggestions(query) {
|
|
const search = validateMaxLength(query, LOCATION_MAX_LENGTH, 'Location search');
|
|
if (!search) {
|
|
return [];
|
|
}
|
|
|
|
const response = await fetch('https://geocoding-api.open-meteo.com/v1/search?name=' + encodeURIComponent(search) + '&count=8&language=en&format=json', {
|
|
headers: { Accept: 'application/json', 'User-Agent': 'Pulse Signage weather location lookup' }
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error('Weather location lookup failed.');
|
|
}
|
|
const data = await response.json();
|
|
return (Array.isArray(data.results) ? data.results : []).map(function (result) {
|
|
return {
|
|
label: [result.name, result.admin1, result.country].filter(Boolean).join(', '),
|
|
latitude: Number(result.latitude),
|
|
longitude: Number(result.longitude),
|
|
timezone: String(result.timezone || '')
|
|
};
|
|
}).filter(function (result) {
|
|
return result.label && Number.isFinite(result.latitude) && Number.isFinite(result.longitude) && result.timezone;
|
|
});
|
|
}
|
|
|
|
function normalizeChoice(value, choices, fallback) {
|
|
const normalized = String(value || '').trim().toLowerCase();
|
|
return choices.includes(normalized) ? normalized : fallback;
|
|
}
|
|
|
|
async function fetchWeatherLocationsPage(pool, page, pageSize, searchTerm, sortKey, sortDirection) {
|
|
const paged = await fetchPagedRows(pool, {
|
|
selectSql: 'SELECT id, name, location_label, latitude, longitude, timezone, provider, temperature_unit, wind_unit, precipitation_unit, enabled, update_interval_value, update_interval_unit, last_pulled_at, last_pull_error, created_at, modified_at FROM i_weather_locations ORDER BY modified_at DESC, id DESC',
|
|
countSql: 'SELECT COUNT(*) AS count FROM i_weather_locations',
|
|
searchColumns: ['name', 'location_label', 'timezone', 'provider'],
|
|
searchTerm: searchTerm,
|
|
sortColumns: {
|
|
name: 'name',
|
|
location: 'location_label',
|
|
provider: 'provider',
|
|
interval: ['update_interval_value', 'update_interval_unit'],
|
|
last_pulled: 'last_pulled_at',
|
|
modified: 'modified_at'
|
|
},
|
|
sortKey: sortKey,
|
|
sortDirection: sortDirection,
|
|
page: page,
|
|
pageSize: pageSize
|
|
});
|
|
return Object.assign({ weatherLocations: paged.rows }, paged);
|
|
}
|
|
|
|
async function fetchWeatherLocationsData(pool) {
|
|
const [rows] = await pool.query('SELECT id, name, location_label, latitude, longitude, timezone, provider, temperature_unit, wind_unit, precipitation_unit, enabled, update_interval_value, update_interval_unit, last_pulled_at, last_pull_error, last_response_json, created_at, modified_at FROM i_weather_locations ORDER BY modified_at DESC, id DESC');
|
|
return { weatherLocations: rows };
|
|
}
|
|
|
|
async function fetchWeatherLocationById(pool, id) {
|
|
const [rows] = await pool.query('SELECT id, name, location_label, latitude, longitude, timezone, provider, temperature_unit, wind_unit, precipitation_unit, enabled, update_interval_value, update_interval_unit, last_pulled_at, last_pull_error, last_response_json, created_at, modified_at, created_by, modified_by FROM i_weather_locations WHERE id = ?', [id]);
|
|
return rows[0] || null;
|
|
}
|
|
|
|
async function fetchWeatherLocationForecast(pool, location) {
|
|
const source = location || {};
|
|
const settings = await fetchAppSettings(pool);
|
|
const latitude = Number(source.latitude);
|
|
const longitude = Number(source.longitude);
|
|
const temperatureUnit = source.temperature_unit === 'fahrenheit' ? 'fahrenheit' : 'celsius';
|
|
const windUnit = source.wind_unit === 'mph' ? 'mph' : source.wind_unit === 'ms' ? 'ms' : 'kmh';
|
|
const precipitationUnit = source.precipitation_unit === 'inch' ? 'inch' : 'mm';
|
|
let url;
|
|
let headers = { Accept: 'application/json', 'User-Agent': 'Pulse Signage weather reader' };
|
|
|
|
if (source.provider === 'pirate-weather') {
|
|
const apiKey = String(settings['weather.pirate_weather_api_key'] || '').trim();
|
|
if (!apiKey) throw new Error('Pirate Weather API key is not configured.');
|
|
url = 'https://api.pirateweather.net/forecast/' + encodeURIComponent(apiKey) + '/' + latitude + ',' + longitude + '?units=' + (temperatureUnit === 'fahrenheit' ? 'us' : 'si');
|
|
} else {
|
|
const params = new URLSearchParams({ latitude: String(latitude), longitude: String(longitude), timezone: String(source.timezone || 'auto'), forecast_days: '7', forecast_hours: '24', current: 'temperature_2m,relative_humidity_2m,apparent_temperature,is_day,precipitation,rain,weather_code,wind_speed_10m,wind_direction_10m,uv_index,cloud_cover', hourly: 'temperature_2m,precipitation_probability,precipitation,weather_code,wind_speed_10m,uv_index,cloud_cover', daily: 'weather_code,temperature_2m_max,temperature_2m_min,sunrise,sunset,precipitation_probability_max,precipitation_sum,wind_speed_10m_max,uv_index_max,cloud_cover_mean', temperature_unit: temperatureUnit, wind_speed_unit: windUnit, precipitation_unit: precipitationUnit });
|
|
const apiKey = String(settings['weather.open_meteo_api_key'] || '').trim();
|
|
if (apiKey) params.set('apikey', apiKey);
|
|
url = 'https://api.open-meteo.com/v1/forecast?' + params.toString();
|
|
}
|
|
|
|
const response = await fetch(url, { headers: headers });
|
|
if (!response.ok) throw new Error('Weather provider returned HTTP ' + response.status + '.');
|
|
const snapshot = await response.json();
|
|
return { snapshot: snapshot, responseJson: JSON.stringify(snapshot), fetchedAt: new Date() };
|
|
}
|
|
|
|
function buildWeatherLocationPayload(req, existingLocation) {
|
|
const body = req && req.body ? req.body : {};
|
|
const fallback = existingLocation || {};
|
|
const name = validateMaxLength(body.name || fallback.name || '', NAME_MAX_LENGTH, 'Weather location name');
|
|
const locationLabel = validateMaxLength(body.location_label || fallback.location_label || '', LOCATION_MAX_LENGTH, 'Location label');
|
|
const latitude = Number(body.latitude !== undefined ? body.latitude : fallback.latitude);
|
|
const longitude = Number(body.longitude !== undefined ? body.longitude : fallback.longitude);
|
|
const timezone = validateMaxLength(body.timezone || fallback.timezone || '', TIMEZONE_MAX_LENGTH, 'Timezone');
|
|
const provider = normalizeChoice(body.provider || fallback.provider, PROVIDERS, 'open-meteo');
|
|
const temperatureUnit = normalizeChoice(body.temperature_unit || fallback.temperature_unit, TEMPERATURE_UNITS, 'celsius');
|
|
const windUnit = normalizeChoice(body.wind_unit || fallback.wind_unit, WIND_UNITS, 'kmh');
|
|
const precipitationUnit = normalizeChoice(body.precipitation_unit || fallback.precipitation_unit, PRECIPITATION_UNITS, 'mm');
|
|
const updateIntervalValue = Number(body.update_interval_value || fallback.update_interval_value || 30);
|
|
const updateIntervalUnit = normalizeChoice(body.update_interval_unit || fallback.update_interval_unit, ['minutes', 'hours'], 'minutes');
|
|
|
|
if (!name || !locationLabel || !timezone) {
|
|
const error = new Error('Name, location label, and timezone are required.');
|
|
error.statusCode = 400;
|
|
throw error;
|
|
}
|
|
if (!Number.isFinite(latitude) || latitude < -90 || latitude > 90 || !Number.isFinite(longitude) || longitude < -180 || longitude > 180) {
|
|
const error = new Error('Latitude must be between -90 and 90, and longitude must be between -180 and 180.');
|
|
error.statusCode = 400;
|
|
throw error;
|
|
}
|
|
if (!Number.isInteger(updateIntervalValue) || updateIntervalValue < 1 || updateIntervalValue > 1440) {
|
|
const error = new Error('Update interval must be a whole number between 1 and 1440.');
|
|
error.statusCode = 400;
|
|
throw error;
|
|
}
|
|
|
|
return { name, locationLabel, latitude, longitude, timezone, provider, temperatureUnit, windUnit, precipitationUnit, updateIntervalValue, updateIntervalUnit };
|
|
}
|
|
|
|
module.exports = { fetchWeatherLocationsData, fetchWeatherLocationsPage, fetchWeatherLocationById, fetchWeatherLocationSuggestions, fetchWeatherLocationForecast, buildWeatherLocationPayload, PROVIDERS, TEMPERATURE_UNITS, WIND_UNITS, PRECIPITATION_UNITS }; |