UNPKG

amaran-light-cli

Version:

Command line tool for controlling Aputure Amaran lights via WebSocket to a local Amaran desktop app.

194 lines 7.97 kB
import * as fs from 'node:fs'; import * as path from 'node:path'; const CACHE_FILE = path.join(process.env.HOME || '', '.amaran-cli-weather.json'); const CACHE_TTL_MS = 4 * 60 * 60 * 1000; // 4 hours function hasWeatherDescription(value) { return Array.isArray(value) && value.length > 0 && typeof value[0]?.value === 'string'; } function isWttrResponse(data) { if (!data || typeof data !== 'object') return false; const candidate = data; const current = candidate.current_condition?.[0]; return (Array.isArray(candidate.current_condition) && typeof current?.weatherCode === 'string' && typeof current.temp_C === 'string' && typeof current.localObsDateTime === 'string' && hasWeatherDescription(current.weatherDesc) && Array.isArray(candidate.weather) && candidate.weather.every((day) => typeof day.date === 'string' && Array.isArray(day.hourly) && day.hourly.length > 0 && day.hourly.every((hour) => typeof hour.time === 'string' && typeof hour.weatherCode === 'string' && typeof hour.tempC === 'string' && hasWeatherDescription(hour.weatherDesc)))); } /** * WWO Weather Code Mapping to Cloud Cover and Precipitation * Based on: https://www.worldweatheronline.com/feed/wwo-weather-codes.ashx */ function mapWeatherCode(code) { const c = parseInt(code, 10); // Default values let cloudCover = 0; let precipitation = 'none'; // Precipitation mapping if ([395, 392, 371, 368, 338, 335, 332, 329, 326, 323, 230, 227, 179].includes(c)) { precipitation = 'snow'; } else if ([389, 386, 359, 356, 353, 308, 305, 302, 299, 296, 293, 176].includes(c)) { precipitation = 'rain'; } else if ([284, 281, 266, 263, 185, 182, 143].includes(c)) { precipitation = 'drizzle'; } // Cloud cover mapping (estimated) if ([122, 143, 248, 260].includes(c)) { cloudCover = 1.0; // Overcast / Fog } else if ([119, 200, 308, 338].includes(c)) { cloudCover = 0.8; // Cloudy } else if ([116].includes(c)) { cloudCover = 0.4; // Partly Cloudy } else if ([113].includes(c)) { cloudCover = 0.0; // Clear } else if (precipitation !== 'none') { cloudCover = 0.9; // If raining/snowing, usually very cloudy } return { cloudCover, precipitation }; } function getWeatherForTime(data, date) { // Find the day const dateStr = date.toISOString().split('T')[0]; const day = data.weather.find((w) => w.date === dateStr); if (!day) { const options = mapWeatherCode(data.current_condition[0].weatherCode); return { ...options, temp_C: data.current_condition[0].temp_C, description: data.current_condition[0].weatherDesc[0].value, }; } // Find the hour - hourly is every 3 hours (0, 300, 600...) const targetHour = date.getHours() * 100 + date.getMinutes(); let bestHour = day.hourly[0]; let minDiff = Math.abs(parseInt(bestHour.time, 10) - targetHour); for (const h of day.hourly) { const diff = Math.abs(parseInt(h.time, 10) - targetHour); if (diff < minDiff) { minDiff = diff; bestHour = h; } } const options = mapWeatherCode(bestHour.weatherCode); return { ...options, temp_C: bestHour.tempC, description: bestHour.weatherDesc[0].value, effectiveTime: `Forecast for ${bestHour.time.padStart(4, '0').slice(0, 2)}:00`, dataTimestamp: new Date(`${dateStr}T${bestHour.time.padStart(4, '0').replace(/(..)(..)/, '$1:$2')}:00`).getTime(), }; } export async function getWeatherData(lat, lon, date = new Date(), debug = false) { // Check cache first try { if (fs.existsSync(CACHE_FILE)) { const cacheContent = fs.readFileSync(CACHE_FILE, 'utf8'); const cache = JSON.parse(cacheContent); if (!isWttrResponse(cache.data)) { throw new Error('Invalid weather cache format'); } const now = Date.now(); const isRecent = now - cache.timestamp < CACHE_TTL_MS; // Also check if location is roughly the same (within ~11km/0.1 degree) const isSameLocation = Math.abs(cache.lat - lat) < 0.1 && Math.abs(cache.lon - lon) < 0.1; if (isRecent && isSameLocation) { if (debug) console.log('Using cached weather data'); // If the cache is more than 1 hour old, use hourly forecast for the requested time const ageMs = Math.abs(date.getTime() - cache.timestamp); if (ageMs > 60 * 60 * 1000) { if (debug) console.log(`Cache is ${Math.round(ageMs / 60000)}m old, using hourly forecast`); const forecast = getWeatherForTime(cache.data, date); return { ...forecast, raw: cache.data, source: 'forecast' }; } const options = mapWeatherCode(cache.data.current_condition[0].weatherCode); return { ...options, raw: cache.data, source: 'cache', description: cache.data.current_condition[0].weatherDesc[0].value, effectiveTime: cache.data.current_condition[0].localObsDateTime, dataTimestamp: cache.timestamp, }; } } } catch (err) { if (debug) console.error('Error reading weather cache:', err); } // Try API try { if (debug) console.log(`Fetching weather from wttr.in for ${lat},${lon}...`); const response = await fetch(`https://wttr.in/${lat},${lon}?format=j1`, { headers: { 'User-Agent': 'amaran-cli' }, }); if (!response.ok) { throw new Error(`Weather API returned ${response.status}`); } const data = await response.json(); if (!isWttrResponse(data)) { throw new Error('Weather API returned an unexpected response'); } const cacheObj = { lat, lon, timestamp: Date.now(), data, }; fs.writeFileSync(CACHE_FILE, JSON.stringify(cacheObj, null, 2)); const options = mapWeatherCode(data.current_condition[0].weatherCode); return { ...options, raw: data, source: 'api', description: data.current_condition[0].weatherDesc[0].value, effectiveTime: data.current_condition[0].localObsDateTime, dataTimestamp: Date.now(), }; } catch (err) { if (debug) console.error('Weather API failed:', err); // Last resort: try expired cache if location matches try { if (fs.existsSync(CACHE_FILE)) { const cacheContent = fs.readFileSync(CACHE_FILE, 'utf8'); const cache = JSON.parse(cacheContent); if (!isWttrResponse(cache.data)) { throw new Error('Invalid weather cache format'); } const isSameLocation = Math.abs(cache.lat - lat) < 0.1 && Math.abs(cache.lon - lon) < 0.1; if (isSameLocation) { if (debug) console.log('API failed, using expired cache for same location'); const forecast = getWeatherForTime(cache.data, date); return { ...forecast, raw: cache.data, source: 'forecast' }; } } } catch (_innerErr) { // Ignore inner error } // If all else fails return { cloudCover: 0, precipitation: 'none', source: 'cache' }; } } //# sourceMappingURL=weatherUtil.js.map