From 9987442c7cd7c17548e71e0af49af49478c38701 Mon Sep 17 00:00:00 2001 From: accius Date: Sat, 31 Jan 2026 17:03:01 -0500 Subject: [PATCH] More N3DD Requests Local weather and 12/24 time --- README.md | 2 +- public/index.html | 143 +++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 135 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 1d53ae7..02c72ee 100644 --- a/README.md +++ b/README.md @@ -285,6 +285,6 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file **73 de K0CJH and the OpenHamClock contributors!** -*The original HamClock will cease to function in June 2026. OpenHamClock carries forward Elwood's legacy with modern technology and open-source community development.* +*"The original HamClock will cease to function in June 2026. OpenHamClock carries forward Elwood's legacy with modern technology and open-source community development."* diff --git a/public/index.html b/public/index.html index c0abb4b..d7fcce9 100644 --- a/public/index.html +++ b/public/index.html @@ -813,6 +813,86 @@ return { data, loading }; }; + // ============================================ + // LOCAL WEATHER HOOK - Using Open-Meteo API (free, no API key) + // ============================================ + const useLocalWeather = (location) => { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + if (!location || !location.lat || !location.lon) { + setLoading(false); + return; + } + + const fetchWeather = async () => { + try { + // Open-Meteo free API - no key required + const url = `https://api.open-meteo.com/v1/forecast?latitude=${location.lat}&longitude=${location.lon}¤t=temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m,wind_direction_10m&temperature_unit=fahrenheit&wind_speed_unit=mph&timezone=auto`; + + const response = await fetch(url); + if (response.ok) { + const result = await response.json(); + const current = result.current; + + // Weather code to description mapping + const weatherCodes = { + 0: { desc: 'Clear', icon: '☀️' }, + 1: { desc: 'Mostly Clear', icon: '🌤️' }, + 2: { desc: 'Partly Cloudy', icon: '⛅' }, + 3: { desc: 'Overcast', icon: '☁️' }, + 45: { desc: 'Foggy', icon: '🌫️' }, + 48: { desc: 'Rime Fog', icon: '🌫️' }, + 51: { desc: 'Light Drizzle', icon: '🌧️' }, + 53: { desc: 'Drizzle', icon: '🌧️' }, + 55: { desc: 'Heavy Drizzle', icon: '🌧️' }, + 61: { desc: 'Light Rain', icon: '🌧️' }, + 63: { desc: 'Rain', icon: '🌧️' }, + 65: { desc: 'Heavy Rain', icon: '🌧️' }, + 71: { desc: 'Light Snow', icon: '🌨️' }, + 73: { desc: 'Snow', icon: '🌨️' }, + 75: { desc: 'Heavy Snow', icon: '🌨️' }, + 77: { desc: 'Snow Grains', icon: '🌨️' }, + 80: { desc: 'Light Showers', icon: '🌦️' }, + 81: { desc: 'Showers', icon: '🌦️' }, + 82: { desc: 'Heavy Showers', icon: '🌦️' }, + 85: { desc: 'Snow Showers', icon: '🌨️' }, + 86: { desc: 'Heavy Snow Showers', icon: '🌨️' }, + 95: { desc: 'Thunderstorm', icon: '⛈️' }, + 96: { desc: 'Thunderstorm w/ Hail', icon: '⛈️' }, + 99: { desc: 'Severe Thunderstorm', icon: '⛈️' } + }; + + const weather = weatherCodes[current.weather_code] || { desc: 'Unknown', icon: '❓' }; + + setData({ + temp: Math.round(current.temperature_2m), + humidity: current.relative_humidity_2m, + windSpeed: Math.round(current.wind_speed_10m), + windDir: current.wind_direction_10m, + description: weather.desc, + icon: weather.icon + }); + console.log('[Weather] Loaded:', current.temperature_2m + '°F', weather.desc); + } + } catch (err) { + console.error('Weather fetch error:', err); + setData(null); + } finally { + setLoading(false); + } + }; + + fetchWeather(); + // Refresh every 15 minutes + const interval = setInterval(fetchWeather, 900000); + return () => clearInterval(interval); + }, [location.lat, location.lon]); + + return { data, loading }; + }; + // ============================================ // CONTEST CALENDAR HOOK // ============================================ @@ -2230,6 +2310,7 @@ config, currentTime, utcTime, utcDate, localTime, localDate, deGrid, dxGrid, deSunTimes, dxSunTimes, dxLocation, onDXChange, spaceWeather, bandConditions, potaSpots, dxCluster, dxPaths, contests, propagation, mySpots, satellites, + localWeather, use12Hour, onTimeFormatToggle, onSettingsClick, onFullscreenToggle, isFullscreen }) => { const bearing = calculateBearing(config.location.lat, config.location.lon, dxLocation.lat, dxLocation.lon); @@ -2274,7 +2355,7 @@ padding: '2px', overflow: 'hidden' }}> - {/* TOP LEFT - Callsign & Time */} + {/* TOP LEFT - Callsign & Weather */}
{config.callsign}
-
- SFI {spaceWeather.data?.solarFlux || '--'} • K{spaceWeather.data?.kIndex || '-'} • SSN {spaceWeather.data?.sunspotNumber || '--'} -
+ {localWeather.data ? ( +
+ {localWeather.data.icon} {localWeather.data.temp}°F • {localWeather.data.description} +
+ ) : ( +
+ SFI {spaceWeather.data?.solarFlux || '--'} • K{spaceWeather.data?.kIndex || '-'} +
+ )}
{/* TOP CENTER - Large Clock */} @@ -2296,7 +2383,11 @@
-
{localTime}
+
{localTime}
{localDate} Local
@@ -2864,6 +2955,26 @@ const [dxLocation, setDxLocation] = useState(config.defaultDX); const [showSettings, setShowSettings] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false); + + // 12/24 hour format preference with localStorage persistence + const [use12Hour, setUse12Hour] = useState(() => { + try { + const saved = localStorage.getItem('openhamclock_use12Hour'); + return saved === 'true'; + } catch (e) { return false; } + }); + + // Save 12/24 hour preference when changed + useEffect(() => { + try { + localStorage.setItem('openhamclock_use12Hour', use12Hour.toString()); + } catch (e) { console.error('Failed to save time format:', e); } + }, [use12Hour]); + + // Toggle time format handler + const handleTimeFormatToggle = useCallback(() => { + setUse12Hour(prev => !prev); + }, []); // Fullscreen toggle handler const handleFullscreenToggle = useCallback(() => { @@ -2920,6 +3031,7 @@ const propagation = usePropagation(config.location, dxLocation); const mySpots = useMySpots(config.callsign); const satellites = useSatellites(config.location); + const localWeather = useLocalWeather(config.location); const deGrid = useMemo(() => calculateGridSquare(config.location.lat, config.location.lon), [config.location]); const dxGrid = useMemo(() => calculateGridSquare(dxLocation.lat, dxLocation.lon), [dxLocation]); @@ -2943,7 +3055,7 @@ }, []); const utcTime = currentTime.toISOString().substr(11, 8); - const localTime = currentTime.toLocaleTimeString('en-US', { hour12: false }); + const localTime = currentTime.toLocaleTimeString('en-US', { hour12: use12Hour }); const utcDate = currentTime.toISOString().substr(0, 10); const localDate = currentTime.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' }); @@ -2973,6 +3085,9 @@ propagation={propagation} mySpots={mySpots} satellites={satellites} + localWeather={localWeather} + use12Hour={use12Hour} + onTimeFormatToggle={handleTimeFormatToggle} onSettingsClick={() => setShowSettings(true)} onFullscreenToggle={handleFullscreenToggle} isFullscreen={isFullscreen} @@ -3065,15 +3180,25 @@ {utcDate} - {/* Local Clock */} -
+ {/* Local Clock - Clickable to toggle 12/24 hour format */} +
LOCAL {localTime} {localDate}
- {/* Solar Quick Stats */} + {/* Weather & Solar Stats */}
+ {localWeather.data && ( +
+ {localWeather.data.icon} + {localWeather.data.temp}°F +
+ )}
SFI {spaceWeather.data?.solarFlux || '--'}
K = 4 ? 'var(--accent-red)' : 'var(--accent-green)', fontWeight: '600' }}>{spaceWeather.data?.kIndex ?? '--'}
SSN {spaceWeather.data?.sunspotNumber || '--'}