#!/usr/bin/env node "use strict"; // 2026-09-12 - Hyperling /* Prototype mostly generated by Ask Brave from prompt: * node.js script which has 8 location variables including name, lat, lon, and elevation. * loops over the locations sending them to open-meteo requesting temperature min and max for 7 days, no hourly. * uses units for USA such as fahrenheit, inches, mph. * prints the location name and elevation with the average high and low for the week. * */ // Maybe AI is actually getting better than I thought. ;D /* AI Updates: * - highlight highs between 60 and 75, highlight lows between 40 and 55 * - order locations by elevation ascending * */ /* User Updates * - fix spacing in table * - move temps to be right aligned * - send elevation to API * - new highlighting function with many other colors * - added apparent (feels like) * - added args, basically took over the program without any incremental commits * */ /* TODO * - Add way to manage a DB such as ~/.confg/nomadic-weather/locations.json * - -c argument for creating locations * - Should act like "read" to intake data for Season, Nickname, Lat, Long, and Elevation. * - -d argument for deleting locations * - Displays the list and asks which index# to delete. * - -m argument for modifying locations * - Displays a list and asks which index to open, then goes through create-like process. * - Tie locations to seasons, as noted for the GUI in the Design, and sort on that rather than elevation. * */ /* * * Libraries * * */ const yargs = require('yargs/yargs'); const { hideBin } = require('yargs/helpers'); /* * * Variables * * */ const version = '0.1.2'; const days_fore_max = 16; const days_fore_default = 7; const days_past_default = 7; const ft_to_m = 0.3048; const RED = "\x1b[31m"; const GRN = "\x1b[32m"; const YLW = "\x1b[33m"; const BLU = "\x1b[34m"; const MGN = "\x1b[35m"; const CYN = "\x1b[36m"; const WHT = "\x1b[37m"; const RESET = "\x1b[0m"; const pref_high_upper = 75; const pref_high_lower = 60; const pref_low_upper = 55; const pref_low_lower = 40; const tolerance_light = 10; const tolerance_heavy = 20; const locations = [ //{ name: "Flagstaff, AZ", lat: 35.1983, lon: -111.6513, elevation: 6903 }, //{ name: "Flagstaff: NG", lat: 35.1983, lon: -111.6513, elevation: 6931 }, { name: "Flagstaff: ++", lat: 35.1983, lon: -111.6513, elevation: 8200 }, //{ name: "Tucson, AZ", lat: 32.2226, lon: -110.9747, elevation: 2490 }, //{ name: "Tucson: NG", lat: 32.2226, lon: -110.9747, elevation: 2483 }, //{ name: "Tucson: GV", lat: 32.2226, lon: -110.9747, elevation: 2371 }, { name: "Tucson: ++", lat: 32.2226, lon: -110.9747, elevation: 2150 }, //{ name: "Prescott, AZ", lat: 34.5400, lon: -112.4685, elevation: 5374 }, //{ name: "Prescott: NG", lat: 34.5400, lon: -112.4685, elevation: 5496 }, { name: "Prescott: ++", lat: 34.5400, lon: -112.4685, elevation: 6400 }, //{ name: "Frisco, CO", lat: 39.5744, lon: -106.0975, elevation: 9075 }, //{ name: "Frisco: NG", lat: 39.5744, lon: -106.0975, elevation: 9067 }, { name: "Frisco: ++", lat: 39.5744, lon: -106.0975, elevation: 10200 }, //{ name: "Greer, AZ", lat: 34.0100, lon: -109.4587, elevation: 8356 }, { name: "Greer: ++", lat: 34.0100, lon: -109.4587, elevation: 9000 }, //{ name: "Sedona, AZ", lat: 34.8697, lon: -111.7610, elevation: 4350 }, { name: "Sedona: ++", lat: 34.8697, lon: -111.7610, elevation: 4500 }, { name: "Cottonwood: ++", lat: 34.7392, lon: -112.0099, elevation: 3600 }, { name: "Cherry: ++", lat: 34.5781, lon: -112.0677, elevation: 5300 }, //{ name: "Sedona: NG", lat: 34.8697, lon: -111.7610, elevation: 4420 }, //{ name: "Ehrenberg, AZ", lat: 33.9897, lon: -114.3550, elevation: 333 }, { name: "Ehrenberg: ++", lat: 33.9897, lon: -114.3550, elevation: 300 }, { name: "Quartzsite: ++", lat: 33.6763, lon: -114.2104, elevation: 900 }, { name: "Kanab: BF", lat: 37.1412, lon: -112.5439, elevation: 5500 }, //{ name: "Kanab: ++", lat: 37.1399, lon: -112.5926, elevation: 5700 }, { name: "Pahrump, NV", lat: 36.1925, lon: -115.9758, elevation: 2700 }, ]; locations.sort((a, b) => a.elevation - b.elevation); /* * * Functions * * */ function highlight(value, min, max) { const n = parseFloat(value); return (n >= min && n <= max) ? `${GRN}${value}${RESET}` : value; } function pad(str, width) { const visible = str.replace(/\x1b\[\d+m/g, '').length; return ' '.repeat(Math.max(0, width - visible)) + str; } function check (value, min, max) { const n = parseFloat(value); var CLR = ""; if (n >= max + tolerance_heavy) CLR = MGN; else if (n >= max + tolerance_light) CLR = RED; else if (n >= max) CLR = YLW; else if (n <= max && n >= min) CLR = GRN; else if (n <= min - tolerance_heavy) CLR = WHT else if (n <= min - tolerance_light) CLR = CYN else if (n <= min) CLR = BLU; else CLR = RESET; return `${CLR}${value}${RESET}`; //return (n >= min && n <= max) ? `${GRN}${value}${RESET}` : value; } function max (value, upper) { return (value > upper) ? upper : value; } /* * * Parameters * * */ const argv = yargs(hideBin(process.argv)) .option('days_fore', { alias: 'f', describe: 'Future / forecast days, such as "-f 10". Maximum of 16.', type: 'number', requiresArg: true, }) .option('days_past', { alias: 'p', describe: 'Past days, such as "-p 3"', type: 'number', requiresArg: true, }) .option('calc_elevation', { alias: 'e', describe: 'Set to calculate elevation at lat/long rather than use supplied.', type: 'boolean', }) .check((argv) => { if (argv.f !== undefined && (isNaN(argv.f) || argv.f < 0)) { throw new Error('Forecast days must be a non-negative number'); } if (argv.p !== undefined && (isNaN(argv.p) || argv.p < 0)) { throw new Error('Past days must be a non-negative number'); } return true; }) //.demandOption(['timeout', 'debug']) // Optional: require these arguments .strict() .help() .version(version) .argv ; /* * * Processing * * */ const days_fore = max(argv.hasOwnProperty("days_fore") ? argv.days_fore : days_fore_default, days_fore_max); const days_past = argv.hasOwnProperty("days_past") ? argv.days_past : days_past_default; const calc_elevation = argv.calc_elevation ? true : false; const days = days_fore + days_past; console.log(`Averaging for ${days} days: ${days_fore} forecasted, ${days_past} past.`); if (calc_elevation) console.log("Calculating elevation rather than using provided values."); else console.log("Using user-provided elevations when they exist."); /* * * Main * * */ async function getForecast(loc) { const url = new URL("https://api.open-meteo.com/v1/forecast"); url.searchParams.set("latitude", loc.lat); url.searchParams.set("longitude", loc.lon); if (!calc_elevation && loc.elevation) url.searchParams.set("elevation", loc.elevation * ft_to_m); url.searchParams.set("daily", "temperature_2m_max,temperature_2m_min,apparent_temperature_max,apparent_temperature_min"); url.searchParams.set("temperature_unit", "fahrenheit"); url.searchParams.set("precipitation_unit", "inch"); url.searchParams.set("wind_speed_unit", "mph"); url.searchParams.set("timezone", "auto"); url.searchParams.set("forecast_days", days_fore); url.searchParams.set("past_days", days_past); const res = await fetch(url); const data = await res.json(); const highs = data.daily.temperature_2m_max; const lows = data.daily.temperature_2m_min; const avgHigh = (highs.reduce((a, b) => a + b, 0) / highs.length).toFixed(1); const avgLow = (lows.reduce((a, b) => a + b, 0) / lows.length).toFixed(1); const highs2 = data.daily.apparent_temperature_max; const lows2 = data.daily.apparent_temperature_min; const avgHigh2 = (highs2.reduce((a, b) => a + b, 0) / highs2.length).toFixed(1); const avgLow2 = (lows2.reduce((a, b) => a + b, 0) / lows2.length).toFixed(1); const elev = (!calc_elevation && loc.elevation) ? loc.elevation : (data.elevation / ft_to_m).toFixed(0); return { name: loc.name, elevation: elev, avgHigh, avgLow, avgHigh2, avgLow2 }; } (async () => { console.log("Location | Elev (ft) | High (°F) | Low (°F) | FeelH (°F) | FeelL (°F)"); console.log("----------------------+-----------+-----------+----------+------------+------------"); for (const loc of locations) { try { const r = await getForecast(loc); const hi = check(r.avgHigh, pref_high_lower, pref_high_upper); const lo = check(r.avgLow, pref_low_lower, pref_low_upper); const hi2 = check(r.avgHigh2, pref_high_lower, pref_high_upper); const lo2 = check(r.avgLow2, pref_low_lower, pref_low_upper); console.log( `${pad(r.name, 21)} | ${pad(String(r.elevation), 9)} | ${pad(hi, 9)} | ${pad(lo, 8)} | ${pad(hi2, 10)} | ${pad(lo2, 10)}` ); } catch (e) { console.log(`${loc.name}: error – ${e.message}`); } } })(); /* * * Finish * * */