converted weather to openMeteo

This commit is contained in:
Drew Peifer 2026-02-14 00:45:59 -05:00
parent d3eeaa7f40
commit 659a7951ca
6 changed files with 593 additions and 83 deletions

View file

@ -8,6 +8,9 @@ REACT_APP_DATA_SOURCES=https://www.reddit.com/r/technology.json,https://hacker-n
# Location for weather (supports "city, state, country" or zip code)
REACT_APP_LOCATION=harrisburg, pa, usa
# Optional: Temperature unit for weather ('F' for Fahrenheit, 'C' for Celsius, default: F)
REACT_APP_TEMPERATURE_UNIT=F
# Optional: Custom CORS proxy (default: allorigins.win)
REACT_APP_CORS_PROXY=https://api.allorigins.win/raw?url=
@ -26,3 +29,6 @@ REACT_APP_CLAUDE_INSTRUCTIONS=
# Optional: Debug mode (shows raw data, enables dummy data)
REACT_APP_DEBUG_MODE=false
# Optional: Date format for charts (MM/DD/YYYY, DD/MM/YYYY, MM/DD, DD/MM)
REACT_APP_CHART_DATE_FORMAT=MM/DD/YYYY

View file

@ -213,7 +213,7 @@ function Dashboard({ themeToggle, isDark }) {
<Box className="widget-grid">
<Box className="widget-row">
<BriefingWidget briefing={briefing} />
<BriefingWidget briefing={briefing} styles={{maxWidth:'50%'}}/>
<WeatherWidget
data={sourceData.weather}
/>

View file

@ -1,80 +1,309 @@
import { Box, Chip, Stack } from '@mui/material';
import { Cloud, WbSunny, Thunderstorm, AcUnit } from '@mui/icons-material';
import { Box, Chip, Stack, Paper, Typography, Divider, useTheme } from '@mui/material';
import {
Cloud,
WbSunny,
Thunderstorm,
AcUnit,
Opacity,
Air,
WbCloudy,
Grain,
Foggy
} from '@mui/icons-material';
import { LineChart } from '@mui/x-charts/LineChart';
import GenericWidget from './GenericWidget';
function WeatherWidget({ data, sx }) {
const getWeatherIcon = (description) => {
const desc = description?.toLowerCase() || '';
if (desc.includes('sun') || desc.includes('clear')) return <WbSunny />;
if (desc.includes('cloud') || desc.includes('overcast')) return <Cloud />;
if (desc.includes('rain') || desc.includes('storm')) return <Thunderstorm />;
if (desc.includes('snow') || desc.includes('ice')) return <AcUnit />;
return <Cloud />;
const theme = useTheme();
// Weather icon mapping based on Open-Meteo icon types
const getWeatherIcon = (iconType) => {
const iconMap = {
'sunny': <WbSunny sx={{ fontSize: 40, color: theme.palette.warning.main }} />,
'partly_cloudy': <WbCloudy sx={{ fontSize: 40, color: theme.palette.grey[600] }} />,
'cloudy': <Cloud sx={{ fontSize: 40, color: theme.palette.grey[500] }} />,
'foggy': <Foggy sx={{ fontSize: 40, color: theme.palette.grey[400] }} />,
'rainy': <Grain sx={{ fontSize: 40, color: theme.palette.info.main }} />,
'snowy': <AcUnit sx={{ fontSize: 40, color: theme.palette.info.light }} />,
'stormy': <Thunderstorm sx={{ fontSize: 40, color: theme.palette.error.main }} />
};
return iconMap[iconType] || <Cloud sx={{ fontSize: 40 }} />;
};
// Small icon for forecast
const getForecastIcon = (iconType) => {
const iconMap = {
'sunny': <WbSunny sx={{ fontSize: 24, color: theme.palette.warning.main }} />,
'partly_cloudy': <WbCloudy sx={{ fontSize: 24, color: theme.palette.grey[600] }} />,
'cloudy': <Cloud sx={{ fontSize: 24, color: theme.palette.grey[500] }} />,
'foggy': <Foggy sx={{ fontSize: 24, color: theme.palette.grey[400] }} />,
'rainy': <Grain sx={{ fontSize: 24, color: theme.palette.info.main }} />,
'snowy': <AcUnit sx={{ fontSize: 24, color: theme.palette.info.light }} />,
'stormy': <Thunderstorm sx={{ fontSize: 24, color: theme.palette.error.main }} />
};
return iconMap[iconType] || <Cloud sx={{ fontSize: 24 }} />;
};
// Format date for display
const formatDate = (dateStr) => {
const date = new Date(dateStr);
return date.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' });
};
// Format short date for charts based on user preference
const formatChartDate = (dateStr) => {
const date = new Date(dateStr);
const format = import.meta.env.REACT_APP_CHART_DATE_FORMAT || 'MM/DD/YYYY';
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const year = date.getFullYear();
switch(format) {
case 'MM/DD/YYYY':
return `${month}/${day}/${year}`;
case 'DD/MM/YYYY':
return `${day}/${month}/${year}`;
case 'MM/DD':
return `${month}/${day}`;
case 'DD/MM':
return `${day}/${month}`;
default:
return `${month}/${day}/${year}`; // Default to MM/DD/YYYY
}
};
if (!data || data.error) {
return (
<GenericWidget title="Weather" sx={{ opacity: 0.5, ...sx }}>
<p className="widget-error">
{data?.message || 'Weather data unavailable'}
</p>
<GenericWidget title="Weather" sx={{ minWidth: '50%', opacity: 0.7, ...sx }}>
<Box sx={{ textAlign: 'center', py: 3 }}>
<Cloud sx={{ fontSize: 48, color: theme.palette.grey[400], mb: 2 }} />
<Typography variant="body1" color="text.secondary">
{data?.message || 'Weather data unavailable'}
</Typography>
{data?.location && (
<Typography variant="caption" color="text.secondary">
Location: {data.location}
</Typography>
)}
</Box>
</GenericWidget>
);
}
const current = data.current || {};
const location = data.nearestArea?.areaName?.[0]?.value || data.location || 'Unknown';
const temp = current.temp_F || current.temp_C || '?';
const tempUnit = current.temp_F ? '°F' : '°C';
const description = current.weatherDesc?.[0]?.value || 'No description';
const humidity = current.humidity || '?';
const windSpeed = current.windspeedMiles || current.windspeedKmph || '?';
const windUnit = current.windspeedMiles ? 'mph' : 'km/h';
const { current, forecast, historical, units, location } = data;
// Prepare chart data
const tempChartData = historical?.dates ? historical.dates.map((date, idx) => ({
date: formatChartDate(date),
high: historical.maxTemps[idx],
low: historical.minTemps[idx]
})) : [];
const windChartData = historical?.dates ? historical.dates.map((date, idx) => ({
date: formatChartDate(date),
max: historical.maxWindSpeeds[idx],
avg: historical.avgWindSpeeds[idx]
})) : [];
return (
<GenericWidget title={`Weather - ${location}`} sx={{ ...sx }}>
<Box sx={{ mt: 1 }}>
<GenericWidget title={`Weather - ${location}`} sx={{ minWidth: '50%', ...sx }}>
{/* Current Weather Section */}
<Box sx={{ mb: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<Box sx={{ mr: 2 }}>
{getWeatherIcon(description)}
<Box sx={{ mr: 3 }}>
{getWeatherIcon(current?.icon)}
</Box>
<Box>
<span className="weather-temp">{temp}</span>
<span className="weather-unit">{tempUnit}</span>
<Box sx={{ flexGrow: 1 }}>
<Typography variant="h3" component="span" sx={{ fontWeight: 300 }}>
{Math.round(current?.temp || 0)}
</Typography>
<Typography variant="h5" component="span" sx={{ ml: 0.5 }}>
{units?.temperature}
</Typography>
<Typography variant="body1" color="text.secondary" sx={{ mt: 1 }}>
{current?.description}
</Typography>
</Box>
</Box>
<p className="weather-description">{description}</p>
<Stack direction="row" spacing={1}>
<Stack direction="row" spacing={1} sx={{ flexWrap: 'wrap', gap: 1 }}>
<Chip
label={`Humidity: ${humidity}%`}
icon={<Opacity />}
label={`${current?.humidity}%`}
size="small"
variant="outlined"
sx={{ borderColor: theme.palette.info.main }}
/>
<Chip
label={`Wind: ${windSpeed} ${windUnit}`}
icon={<Air />}
label={`${Math.round(current?.windSpeed || 0)} ${units?.windSpeed}`}
size="small"
variant="outlined"
sx={{ borderColor: theme.palette.success.main }}
/>
<Chip
label={`Feels like ${Math.round(current?.apparentTemp || 0)}${units?.temperature}`}
size="small"
variant="outlined"
/>
</Stack>
{data.forecast && data.forecast.length > 0 && (
<Box sx={{ mt: 2 }}>
<div className="forecast-label">Forecast</div>
<Stack direction="row" spacing={1}>
{data.forecast.slice(0, 3).map((day, index) => (
<Box key={index} sx={{ textAlign: 'center' }}>
<div className="forecast-day">Day {index + 1}</div>
<div className="forecast-temp">
{day.maxtempF || day.maxtempC}° / {day.mintempF || day.mintempC}°
</div>
</Box>
))}
</Stack>
</Box>
)}
</Box>
<Divider sx={{ my: 2 }} />
{/* 5-Day Forecast */}
{forecast && forecast.length > 0 && (
<Box sx={{ mb: 3 }}>
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
5-Day Forecast
</Typography>
<Box sx={{ display: 'flex', gap: 1, overflowX: 'auto' }}>
{forecast.map((day, index) => (
<Paper
key={index}
elevation={1}
sx={{
p: 1.5,
minWidth: 90,
textAlign: 'center',
bgcolor: theme.palette.mode === 'dark'
? 'rgba(255,255,255,0.05)'
: 'rgba(0,0,0,0.02)',
borderRadius: 2
}}
>
<Typography variant="caption" display="block" sx={{ mb: 1 }}>
{index === 0 ? 'Today' : formatDate(day.date)}
</Typography>
{getForecastIcon(day.icon)}
<Typography variant="body2" sx={{ mt: 1, fontWeight: 500 }}>
{Math.round(day.maxTemp)}°
</Typography>
<Typography variant="caption" color="text.secondary">
{Math.round(day.minTemp)}°
</Typography>
</Paper>
))}
</Box>
</Box>
)}
<Divider sx={{ my: 2 }} />
{/* Historical Temperature Chart */}
{tempChartData.length > 0 && (
<Box sx={{ mb: 3 }}>
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
7-Day Temperature History
</Typography>
<LineChart
height={200}
series={[
{
data: tempChartData.map(d => d.high),
label: 'High',
color: theme.palette.error.main,
showMark: false
},
{
data: tempChartData.map(d => d.low),
label: 'Low',
color: theme.palette.info.main,
showMark: false
}
]}
xAxis={[{
scaleType: 'point',
data: tempChartData.map(d => d.date),
tickLabelStyle: {
angle: 45,
textAnchor: 'start',
fontSize: 10
},
tickNumber: tempChartData.length
}]}
yAxis={[{
label: `Temperature (${units?.temperature})`,
labelStyle: { fontSize: 12 }
}]}
margin={{ top: 20, bottom: 80, left: 60, right: 30 }}
sx={{
'.MuiLineElement-root': {
strokeWidth: 2
},
'.MuiMarkElement-root': {
scale: '0.6'
}
}}
slotProps={{
legend: {
direction: 'row',
position: { vertical: 'top', horizontal: 'middle' },
padding: 0
}
}}
/>
</Box>
)}
<Divider sx={{ my: 2 }} />
{/* Historical Wind Speed Chart */}
{windChartData.length > 0 && (
<Box>
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
7-Day Wind Speed History
</Typography>
<LineChart
height={200}
series={[
{
data: windChartData.map(d => d.max),
label: 'Max Wind',
color: theme.palette.warning.main,
showMark: false
},
{
data: windChartData.map(d => d.avg),
label: 'Avg Wind',
color: theme.palette.success.main,
showMark: false
}
]}
xAxis={[{
scaleType: 'point',
data: windChartData.map(d => d.date),
tickLabelStyle: {
angle: 45,
textAnchor: 'start',
fontSize: 10
},
tickNumber: windChartData.length
}]}
yAxis={[{
label: `Wind Speed (${units?.windSpeed})`,
labelStyle: { fontSize: 12 }
}]}
margin={{ top: 20, bottom: 80, left: 60, right: 30 }}
sx={{
'.MuiLineElement-root': {
strokeWidth: 2
},
'.MuiMarkElement-root': {
scale: '0.6'
}
}}
slotProps={{
legend: {
direction: 'row',
position: { vertical: 'top', horizontal: 'middle' },
padding: 0
}
}}
/>
</Box>
)}
</GenericWidget>
);
}

View file

@ -1,10 +1,12 @@
import { parseSource, standardizeSourceData } from './sourceParser';
import { getCachedData, cacheSourceData } from './storageService';
import { fetchWithCors } from '../utils/corsProxy';
import { fetchOpenMeteoWeather } from './openMeteoService';
import {
DEFAULT_DATA_SOURCES,
WEATHER_API_BASE,
DEFAULT_REFRESH_INTERVAL
DEFAULT_REFRESH_INTERVAL,
DEFAULT_TEMPERATURE_UNIT
} from '../utils/constants';
/**
@ -64,8 +66,8 @@ export async function fetchSingleSource(url) {
}
/**
* Fetch weather data from wttr.in
* @param {string} location - Location string (city, zip, etc.)
* Fetch weather data using Open-Meteo API
* @param {string|number|Object} location - Location as string, zip code, or {latitude, longitude}
* @returns {Promise<Object>} Weather data
*/
export async function fetchWeather(location) {
@ -74,41 +76,27 @@ export async function fetchWeather(location) {
return null;
}
const cacheKey = `weather_${location.replace(/[^a-zA-Z0-9]/g, '_')}`;
// Check cache first
const cached = getCachedData(cacheKey);
if (cached) {
console.log(`Using cached weather data for ${location}`);
return cached;
// Parse location - check if it's a numeric string (zip code)
let parsedLocation = location;
if (typeof location === 'string' && /^\d+$/.test(location)) {
parsedLocation = parseInt(location, 10);
}
// Get temperature unit from environment or use default
const tempUnit = import.meta.env.REACT_APP_TEMPERATURE_UNIT || DEFAULT_TEMPERATURE_UNIT;
try {
// Format location for wttr.in
const formattedLocation = encodeURIComponent(location);
const weatherUrl = `${WEATHER_API_BASE}/${formattedLocation}?format=j1`;
console.log(`Fetching weather for ${location}`);
const data = await fetchWithCors(weatherUrl);
// Standardize weather data
const weatherData = {
type: 'weather',
location: location,
current: data.current_condition?.[0] || {},
forecast: data.weather || [],
nearestArea: data.nearest_area?.[0] || {},
fetchedAt: Date.now()
};
// Cache the result
const ttl = parseInt(import.meta.env.REACT_APP_REFRESH_INTERVAL) || DEFAULT_REFRESH_INTERVAL;
cacheSourceData(cacheKey, weatherData, ttl);
// Use the new Open-Meteo service (now supports numbers and objects)
const weatherData = await fetchOpenMeteoWeather(parsedLocation, tempUnit);
return weatherData;
} catch (error) {
console.error(`Error fetching weather for ${location}:`, error);
return null;
console.error(`Error fetching weather for ${parsedLocation}:`, error);
return {
type: 'weather',
error: true,
message: error.message || 'Failed to fetch weather data',
location: location
};
}
}

View file

@ -0,0 +1,286 @@
import { getCachedData, cacheSourceData } from './storageService';
import { fetchWithCors } from '../utils/corsProxy';
// Weather code descriptions mapping (WMO Weather interpretation codes)
const WEATHER_CODE_MAP = {
0: { description: 'Clear sky', icon: 'sunny' },
1: { description: 'Mainly clear', icon: 'partly_cloudy' },
2: { description: 'Partly cloudy', icon: 'partly_cloudy' },
3: { description: 'Overcast', icon: 'cloudy' },
45: { description: 'Foggy', icon: 'foggy' },
48: { description: 'Depositing rime fog', icon: 'foggy' },
51: { description: 'Light drizzle', icon: 'rainy' },
53: { description: 'Moderate drizzle', icon: 'rainy' },
55: { description: 'Dense drizzle', icon: 'rainy' },
56: { description: 'Light freezing drizzle', icon: 'rainy' },
57: { description: 'Dense freezing drizzle', icon: 'rainy' },
61: { description: 'Slight rain', icon: 'rainy' },
63: { description: 'Moderate rain', icon: 'rainy' },
65: { description: 'Heavy rain', icon: 'rainy' },
66: { description: 'Light freezing rain', icon: 'rainy' },
67: { description: 'Heavy freezing rain', icon: 'rainy' },
71: { description: 'Slight snow fall', icon: 'snowy' },
73: { description: 'Moderate snow fall', icon: 'snowy' },
75: { description: 'Heavy snow fall', icon: 'snowy' },
77: { description: 'Snow grains', icon: 'snowy' },
80: { description: 'Slight rain showers', icon: 'rainy' },
81: { description: 'Moderate rain showers', icon: 'rainy' },
82: { description: 'Violent rain showers', icon: 'stormy' },
85: { description: 'Slight snow showers', icon: 'snowy' },
86: { description: 'Heavy snow showers', icon: 'snowy' },
95: { description: 'Thunderstorm', icon: 'stormy' },
96: { description: 'Thunderstorm with slight hail', icon: 'stormy' },
99: { description: 'Thunderstorm with heavy hail', icon: 'stormy' }
};
/**
* Get weather description and icon from weather code
* @param {number} code - WMO weather interpretation code
* @returns {Object} Description and icon type
*/
function getWeatherInfo(code) {
return WEATHER_CODE_MAP[code] || { description: 'Unknown', icon: 'cloudy' };
}
/**
* Format date for API calls
* @param {Date} date - Date object
* @returns {string} Formatted date string YYYY-MM-DD
*/
function formatDate(date) {
return date.toISOString().split('T')[0];
}
/**
* Geocode location to coordinates using Open-Meteo
* @param {string|number|Object} location - Location as string, zip code (number), or {latitude, longitude} object
* @returns {Promise<Object>} Geocoded coordinates
*/
async function geocodeLocation(location) {
// If location is already a coordinates object, return it
if (typeof location === 'object' && location.latitude && location.longitude) {
return {
latitude: location.latitude,
longitude: location.longitude,
displayName: location.displayName || `${location.latitude}, ${location.longitude}`,
name: location.name || 'Custom Location',
country: location.country || '',
admin1: location.admin1 || ''
};
}
// Convert zip code (number) to string for geocoding
const locationStr = typeof location === 'number' ? location.toString() : location;
const cacheKey = `geocode_${locationStr.replace(/[^a-zA-Z0-9]/g, '_')}`;
// Check cache first (geocoding results are stable)
const cached = getCachedData(cacheKey);
if (cached) {
console.log(`Using cached geocoding for ${locationStr}`);
return cached;
}
try {
const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(locationStr)}&count=1&language=en&format=json`;
console.log(`Geocoding location: ${locationStr}`);
const data = await fetchWithCors(url);
if (data.results && data.results.length > 0) {
const result = data.results[0];
const geocodeData = {
latitude: result.latitude,
longitude: result.longitude,
name: result.name,
country: result.country,
admin1: result.admin1, // State/Province
displayName: `${result.name}${result.admin1 ? ', ' + result.admin1 : ''}${result.country ? ', ' + result.country : ''}`
};
// Cache for 7 days (geocoding doesn't change often)
cacheSourceData(cacheKey, geocodeData, 60 * 24 * 7);
return geocodeData;
} else {
throw new Error(`Location not found: ${location}`);
}
} catch (error) {
console.error(`Geocoding error for ${location}:`, error);
throw error;
}
}
/**
* Fetch current weather and forecast from Open-Meteo
* @param {number} latitude - Latitude
* @param {number} longitude - Longitude
* @param {string} tempUnit - Temperature unit ('F' or 'C')
* @returns {Promise<Object>} Current weather and forecast data
*/
async function fetchCurrentAndForecast(latitude, longitude, tempUnit = 'F') {
const tempUnitParam = tempUnit === 'F' ? 'fahrenheit' : 'celsius';
const windUnitParam = tempUnit === 'F' ? 'mph' : 'kmh';
const url = `https://api.open-meteo.com/v1/forecast?` +
`latitude=${latitude}&longitude=${longitude}` +
`&current=temperature_2m,relative_humidity_2m,apparent_temperature,weather_code,wind_speed_10m,wind_direction_10m` +
`&daily=weather_code,temperature_2m_max,temperature_2m_min,precipitation_sum,wind_speed_10m_max` +
`&temperature_unit=${tempUnitParam}` +
`&wind_speed_unit=${windUnitParam}` +
`&timezone=auto` +
`&forecast_days=5`;
console.log(`Fetching current weather and forecast for ${latitude}, ${longitude}`);
const data = await fetchWithCors(url);
return data;
}
/**
* Fetch historical weather data from Open-Meteo Archive API
* @param {number} latitude - Latitude
* @param {number} longitude - Longitude
* @param {string} tempUnit - Temperature unit ('F' or 'C')
* @returns {Promise<Object>} Historical weather data
*/
async function fetchHistorical(latitude, longitude, tempUnit = 'F') {
const tempUnitParam = tempUnit === 'F' ? 'fahrenheit' : 'celsius';
const windUnitParam = tempUnit === 'F' ? 'mph' : 'kmh';
// Calculate date range (past 7 days)
const endDate = new Date();
endDate.setDate(endDate.getDate() - 1); // Yesterday
const startDate = new Date();
startDate.setDate(startDate.getDate() - 7); // 7 days ago
const url = `https://archive-api.open-meteo.com/v1/archive?` +
`latitude=${latitude}&longitude=${longitude}` +
`&start_date=${formatDate(startDate)}` +
`&end_date=${formatDate(endDate)}` +
`&daily=temperature_2m_max,temperature_2m_min,wind_speed_10m_max,wind_speed_10m_mean,weather_code` +
`&temperature_unit=${tempUnitParam}` +
`&wind_speed_unit=${windUnitParam}` +
`&timezone=auto`;
console.log(`Fetching historical weather for ${latitude}, ${longitude}`);
const data = await fetchWithCors(url);
return data;
}
/**
* Fetch complete weather data from Open-Meteo
* @param {string|number|Object} location - Location as string, zip code, or {latitude, longitude}
* @param {string} tempUnit - Temperature unit ('F' or 'C')
* @returns {Promise<Object>} Complete weather data
*/
export async function fetchOpenMeteoWeather(location, tempUnit = 'F') {
if (!location) {
throw new Error('No location provided');
}
// Generate cache key based on location type
let cacheKeyLocation;
if (typeof location === 'object' && location.latitude && location.longitude) {
cacheKeyLocation = `${location.latitude}_${location.longitude}`;
} else {
cacheKeyLocation = String(location).replace(/[^a-zA-Z0-9]/g, '_');
}
const cacheKey = `openmeteo_${cacheKeyLocation}_${tempUnit}`;
// Check cache first
const cached = getCachedData(cacheKey);
if (cached) {
console.log(`Using cached Open-Meteo data for ${location}`);
return cached;
}
try {
// Step 1: Geocode the location
const geocoded = await geocodeLocation(location);
// Step 2: Fetch current weather and forecast
const currentForecast = await fetchCurrentAndForecast(
geocoded.latitude,
geocoded.longitude,
tempUnit
);
// Step 3: Fetch historical data
const historical = await fetchHistorical(
geocoded.latitude,
geocoded.longitude,
tempUnit
);
// Parse current weather
const currentWeatherInfo = getWeatherInfo(currentForecast.current.weather_code);
// Parse forecast data
const forecast = currentForecast.daily.time.map((date, index) => {
const weatherInfo = getWeatherInfo(currentForecast.daily.weather_code[index]);
return {
date: date,
maxTemp: currentForecast.daily.temperature_2m_max[index],
minTemp: currentForecast.daily.temperature_2m_min[index],
precipitation: currentForecast.daily.precipitation_sum[index],
windSpeed: currentForecast.daily.wind_speed_10m_max[index],
weatherCode: currentForecast.daily.weather_code[index],
description: weatherInfo.description,
icon: weatherInfo.icon
};
});
// Parse historical data
const historicalData = {
dates: historical.daily.time,
maxTemps: historical.daily.temperature_2m_max,
minTemps: historical.daily.temperature_2m_min,
maxWindSpeeds: historical.daily.wind_speed_10m_max,
avgWindSpeeds: historical.daily.wind_speed_10m_mean,
weatherCodes: historical.daily.weather_code
};
// Build complete weather data structure
const weatherData = {
type: 'weather',
location: geocoded.displayName,
coordinates: {
latitude: geocoded.latitude,
longitude: geocoded.longitude
},
current: {
temp: currentForecast.current.temperature_2m,
apparentTemp: currentForecast.current.apparent_temperature,
humidity: currentForecast.current.relative_humidity_2m,
windSpeed: currentForecast.current.wind_speed_10m,
windDirection: currentForecast.current.wind_direction_10m,
weatherCode: currentForecast.current.weather_code,
description: currentWeatherInfo.description,
icon: currentWeatherInfo.icon,
time: currentForecast.current.time
},
forecast: forecast,
historical: historicalData,
units: {
temperature: tempUnit === 'F' ? '°F' : '°C',
windSpeed: tempUnit === 'F' ? 'mph' : 'km/h',
precipitation: 'in'
},
fetchedAt: Date.now()
};
// Cache for 15 minutes
cacheSourceData(cacheKey, weatherData, 15);
return weatherData;
} catch (error) {
console.error(`Error fetching Open-Meteo weather for ${location}:`, error);
// Return error object that WeatherWidget can handle
return {
type: 'weather',
error: true,
message: error.message || 'Failed to fetch weather data',
location: location
};
}
}

View file

@ -5,6 +5,7 @@ export const DEFAULT_REFRESH_INTERVAL = 15; // minutes
export const DEFAULT_HISTORY_DAYS = 7;
export const DEFAULT_CLAUDE_MODEL = 'claude-sonnet-4-5-20250929';
export const DEFAULT_CORS_PROXY = 'https://api.allorigins.win/raw?url=';
export const DEFAULT_TEMPERATURE_UNIT = 'F'; // 'F' for Fahrenheit, 'C' for Celsius
// Cache TTL in minutes
export const CACHE_TTL = 15;