#!/usr/bin/env bash # Configuration settings defaultCity="Raleigh, NC" defaultLat="35.78" defaultLon="-78.64" tempUnit="F" updateInterval=30 configDir="${XDG_CONFIG_HOME:-$HOME/.config}/stormux/I38" configFile="$configDir/weather.conf" mkdir -p "$configDir" # Initialize variables cityName="Detecting..." latitude=0.0 longitude=0.0 currentTemp="--" currentHumidity="--" currentWindSpeed="--" currentWindSpeedMph="--" currentConditions="Unknown" weatherLastUpdate=0 severeWeatherAlerted=0 declare -a forecastDates=("--" "--" "--") declare -a forecastFormattedDates=("--" "--" "--") declare -a forecastMinTemps=("--" "--" "--") declare -a forecastMaxTemps=("--" "--" "--") declare -a forecastConditions=("Unknown" "Unknown" "Unknown") declare -A weatherCodes weatherCodes[0]="Clear sky" weatherCodes[1]="Mainly clear" weatherCodes[2]="Partly cloudy" weatherCodes[3]="Overcast" weatherCodes[45]="Fog" weatherCodes[48]="Rime fog" weatherCodes[51]="Light drizzle" weatherCodes[53]="Moderate drizzle" weatherCodes[55]="Dense drizzle" weatherCodes[56]="Light freezing drizzle" weatherCodes[57]="Dense freezing drizzle" weatherCodes[61]="Slight rain" weatherCodes[63]="Moderate rain" weatherCodes[65]="Heavy rain" weatherCodes[66]="Light freezing rain" weatherCodes[67]="Heavy freezing rain" weatherCodes[71]="Slight snow fall" weatherCodes[73]="Moderate snow fall" weatherCodes[75]="Heavy snow fall" weatherCodes[77]="Snow flurries" weatherCodes[80]="Slight rain showers" weatherCodes[81]="Moderate rain showers" weatherCodes[82]="Heavy rain showers" weatherCodes[85]="Slight snow showers" weatherCodes[86]="Heavy snow showers" weatherCodes[95]="Thunderstorm" weatherCodes[96]="Thunderstorm with slight hail" weatherCodes[99]="Thunderstorm with heavy hail" declare -a severeWeatherCodes=(65 67 75 82 86 95 96 99) # Button return codes refreshBtn=0 quitBtn=1 settingsBtn=2 trap "pkill -P $$" EXIT INT TERM # Load configuration if available if [ -f "$configFile" ]; then source "$configFile" # Convert lastWeatherUpdate string to integer if it exists [[ -n "$lastWeatherUpdate" ]] && weatherLastUpdate=$lastWeatherUpdate || weatherLastUpdate=0 if [[ -n "$city" ]]; then cityName="$city" latitude="$latitude" longitude="$longitude" fi # Try to reload saved weather data if [[ "$weatherLastUpdate" -gt 0 && "$currentTemp" == "--" ]]; then [[ -n "$savedCurrentTemp" ]] && currentTemp="$savedCurrentTemp" [[ -n "$savedCurrentHumidity" ]] && currentHumidity="$savedCurrentHumidity" [[ -n "$savedCurrentConditions" ]] && currentConditions="$savedCurrentConditions" [[ -n "$savedCurrentWindSpeed" ]] && currentWindSpeedMph="$savedCurrentWindSpeed" for i in {0..2}; do varDate="savedForecastDate_$i" varMin="savedForecastMin_$i" varMax="savedForecastMax_$i" varCond="savedForecastCond_$i" [[ -n "${!varDate}" ]] && forecastFormattedDates[$i]="${!varDate}" [[ -n "${!varMin}" ]] && forecastMinTemps[$i]="${!varMin}" [[ -n "${!varMax}" ]] && forecastMaxTemps[$i]="${!varMax}" [[ -n "${!varCond}" ]] && forecastConditions[$i]="${!varCond}" done fi fi # Helper functions time_diff() { local timestamp="$1" local now=$(date +%s) local diff=$((now - timestamp)) if [ $diff -lt 60 ]; then echo "just now" elif [ $diff -lt 3600 ]; then local minutes=$((diff / 60)) echo "$minutes minute$([ $minutes -ne 1 ] && echo "s") ago" elif [ $diff -lt 86400 ]; then local hours=$((diff / 3600)) echo "$hours hour$([ $hours -ne 1 ] && echo "s") ago" else local days=$((diff / 86400)) echo "$days day$([ $days -ne 1 ] && echo "s") ago" fi } celsius_to_fahrenheit() { local celsius="$1" [[ -z "$celsius" || "$celsius" == "--" ]] && echo "--" && return [[ ! "$celsius" =~ ^-?[0-9]+(\.[0-9]+)?$ ]] && echo "--" && return local fahrenheit fahrenheit=$(echo "scale=1; ($celsius * 9/5) + 32" | bc -l) echo "$fahrenheit" } kmh_to_mph() { local kmh="$1" [[ -z "$kmh" || "$kmh" == "--" ]] && echo "--" && return [[ ! "$kmh" =~ ^-?[0-9]+(\.[0-9]+)?$ ]] && echo "--" && return local mph mph=$(echo "scale=1; $kmh * 0.621371" | bc -l) echo "$mph" } format_date() { local isoDate="$1" [[ -z "$isoDate" || "$isoDate" == "--" ]] && echo "--" && return date -d "$isoDate" "+%A, %B %d" 2>/dev/null || echo "$isoDate" } # Save configuration save_config() { cat > "$configFile" << EOF city="$cityName" latitude="$latitude" longitude="$longitude" tempUnit=$tempUnit updateInterval=$updateInterval lastWeatherUpdate=$weatherLastUpdate savedCurrentTemp="$currentTemp" savedCurrentHumidity="$currentHumidity" savedCurrentConditions="$currentConditions" savedCurrentWindSpeed="$currentWindSpeedMph" savedForecastDate_0="${forecastFormattedDates[0]}" savedForecastMin_0="${forecastMinTemps[0]}" savedForecastMax_0="${forecastMaxTemps[0]}" savedForecastCond_0="${forecastConditions[0]}" savedForecastDate_1="${forecastFormattedDates[1]}" savedForecastMin_1="${forecastMinTemps[1]}" savedForecastMax_1="${forecastMaxTemps[1]}" savedForecastCond_1="${forecastConditions[1]}" savedForecastDate_2="${forecastFormattedDates[2]}" savedForecastMin_2="${forecastMinTemps[2]}" savedForecastMax_2="${forecastMaxTemps[2]}" savedForecastCond_2="${forecastConditions[2]}" EOF } # Play severe weather alert sound using Sox play_severe_weather_alert() { if command -v play &>/dev/null; then # Generate alert sound pattern using sox play -nqV0 synth 2 sine 853 sine 960 remix - norm -15 & fi # Also display notification if available if command -v notify-send &>/dev/null; then notify-send "Severe Weather Alert" "Severe weather conditions detected for $cityName: $currentConditions" -u critical fi } # Function to check if a value is in array in_array() { local value="$1" shift local array=("$@") for item in "${array[@]}"; do if [[ "$item" == "$value" ]]; then return 0 # True, found in array fi done return 1 # False, not found } # Function to detect location get_location() { # Only try location detection if we don't already have a city name if [[ "$cityName" == "Detecting..." ]]; then echo "Attempting to detect location via ipinfo.io..." # Try to fetch location data local locationData locationData=$(curl -s --connect-timeout 5 "https://ipinfo.io/json" 2>/dev/null) if [[ $? -eq 0 && -n "$locationData" && $(echo "$locationData" | jq -e '.city') ]]; then echo "Location data received successfully" cityName=$(echo "$locationData" | jq -r '.city // "Unknown"') local region=$(echo "$locationData" | jq -r '.region // ""') # Add region/state to city name if available [[ -n "$region" ]] && cityName="$cityName, $region" # Extract coordinates directly from the "loc" field local loc=$(echo "$locationData" | jq -r '.loc // "0,0"') latitude=$(echo "$loc" | cut -d',' -f1) longitude=$(echo "$loc" | cut -d',' -f2) save_config else cityName="$defaultCity" latitude="$defaultLat" longitude="$defaultLon" save_config fi fi } # Function to fetch weather data fetch_weather_data() { local now=$(date +%s) local elapsedMinutes=$(( (now - weatherLastUpdate) / 60 )) # Only fetch if needed if [[ $weatherLastUpdate -eq 0 || $elapsedMinutes -ge $updateInterval ]]; then local url="https://api.open-meteo.com/v1/forecast?latitude=$latitude&longitude=$longitude¤t=temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m&daily=weather_code,temperature_2m_max,temperature_2m_min&timezone=auto" local response=$(curl -s --connect-timeout 10 "$url" 2>/dev/null) if [[ $? -eq 0 && -n "$response" && $(echo "$response" | jq -e '.current' 2>/dev/null) ]]; then # Update current weather data local tempCelsius=$(echo "$response" | jq -r '.current.temperature_2m // "--"' 2>/dev/null) [[ "$tempCelsius" != "--" && "$tempCelsius" != "null" ]] && currentTemp=$(celsius_to_fahrenheit "$tempCelsius") || currentTemp="--" currentHumidity=$(echo "$response" | jq -r '.current.relative_humidity_2m // "--"' 2>/dev/null) [[ "$currentHumidity" == "null" ]] && currentHumidity="--" currentWindSpeed=$(echo "$response" | jq -r '.current.wind_speed_10m // "--"' 2>/dev/null) if [[ "$currentWindSpeed" != "--" && "$currentWindSpeed" != "null" ]]; then currentWindSpeedMph=$(kmh_to_mph "$currentWindSpeed") else currentWindSpeed="--" currentWindSpeedMph="--" fi local weatherCode=$(echo "$response" | jq -r '.current.weather_code // 0' 2>/dev/null) [[ "$weatherCode" == "null" ]] && weatherCode=0 currentConditions="${weatherCodes[$weatherCode]:-Unknown}" # Check for severe weather and play alert if needed if in_array "$weatherCode" "${severeWeatherCodes[@]}"; then if [ "$severeWeatherAlerted" -eq 0 ]; then play_severe_weather_alert severeWeatherAlerted=1 fi else # Reset alert flag if weather is no longer severe severeWeatherAlerted=0 fi # Process forecast data (limited to 3 days) if [[ $(echo "$response" | jq -e '.daily' 2>/dev/null) ]]; then for i in {0..2}; do # Process forecast data forecastDates[$i]=$(echo "$response" | jq -r ".daily.time[$i] // \"--\"" 2>/dev/null) [[ "${forecastDates[$i]}" != "--" && "${forecastDates[$i]}" != "null" ]] && \ forecastFormattedDates[$i]=$(format_date "${forecastDates[$i]}") || forecastFormattedDates[$i]="--" local minTempC=$(echo "$response" | jq -r ".daily.temperature_2m_min[$i] // \"--\"" 2>/dev/null) [[ "$minTempC" != "--" && "$minTempC" != "null" ]] && \ forecastMinTemps[$i]=$(celsius_to_fahrenheit "$minTempC") || forecastMinTemps[$i]="--" local maxTempC=$(echo "$response" | jq -r ".daily.temperature_2m_max[$i] // \"--\"" 2>/dev/null) [[ "$maxTempC" != "--" && "$maxTempC" != "null" ]] && \ forecastMaxTemps[$i]=$(celsius_to_fahrenheit "$maxTempC") || forecastMaxTemps[$i]="--" local code=$(echo "$response" | jq -r ".daily.weather_code[$i] // 0" 2>/dev/null) [[ "$code" == "null" ]] && code=0 forecastConditions[$i]="${weatherCodes[$code]:-Unknown}" done fi # Update timestamp weatherLastUpdate=$(date +%s) save_config else echo "Failed to fetch weather data. Response code: $?" if [[ -n "$response" ]]; then echo "First 100 chars of response: ${response:0:100}" fi fi fi } # Function to change location (for settings) change_location() { local newLocation="$1" if [[ -n "$newLocation" && "$newLocation" != "$cityName" ]]; then # Try to parse the location using curl to a geocoding service local result=$(curl -s --connect-timeout 10 "https://nominatim.openstreetmap.org/search?q=$newLocation&format=json" 2>/dev/null) if [[ -n "$result" && $(echo "$result" | jq -e '.[0]') ]]; then cityName="$newLocation" latitude=$(echo "$result" | jq -r '.[0].lat // "0.0"') longitude=$(echo "$result" | jq -r '.[0].lon // "0.0"') # Force weather update weatherLastUpdate=0 save_config return 0 else yad --title "Location Error" --text="Could not find location: $newLocation" --button=gtk-ok return 1 fi fi return 1 } # Display weather information in a text-info dialog display_weather() { local lastUpdateText="Never updated" [[ "$weatherLastUpdate" -gt 0 ]] && lastUpdateText="Last updated: $(time_diff "$weatherLastUpdate")" # Create the weather information text with proper line breaks weatherInfoText="Weather for $cityName $lastUpdateText Current Conditions Temperature: ${currentTemp}° F Conditions: $currentConditions Humidity: ${currentHumidity}% Wind Speed: ${currentWindSpeedMph} mph 3-Day Forecast ──────────────────────────────────── ${forecastFormattedDates[0]} Temp: ${forecastMinTemps[0]}° to ${forecastMaxTemps[0]}° F Conditions: ${forecastConditions[0]} ──────────────────────────────────── ${forecastFormattedDates[1]} Temp: ${forecastMinTemps[1]}° to ${forecastMaxTemps[1]}° F Conditions: ${forecastConditions[1]} ──────────────────────────────────── ${forecastFormattedDates[2]} Temp: ${forecastMinTemps[2]}° to ${forecastMaxTemps[2]}° F Conditions: ${forecastConditions[2]} End of text. Press Control+Home to return to the beginning." # Display in text-info dialog for screen reader accessibility echo "$weatherInfoText" | yad --pname=I38Weather \ --title="I38 Weather Monitor" \ --text-info \ --show-cursor \ --width=500 \ --height=600 \ --center \ --button="Settings:$settingsBtn" \ --button="Refresh:$refreshBtn" \ --button="Close:$quitBtn" return $? } # Function to display settings dialog display_settings() { local ret=$(yad --pname=I38WeatherSettings \ --title="I38 Weather Settings" \ --form \ --width=400 \ --center \ --field="Location:":TEXT "$cityName" \ --field="Current Coordinates:":LBL "Lat: $latitude, Lon: $longitude" \ --field="Temperature Unit:":CB "F!C" \ --field="Update Interval (minutes):":NUM "$updateInterval!5..120!5" \ --button="Cancel:1" \ --button="Save:0") local saveResult=$? if [[ $saveResult -eq 0 && -n "$ret" ]]; then local newLocation=$(echo "$ret" | cut -d"|" -f1) local newUnit=$(echo "$ret" | cut -d"|" -f3) local newInterval=$(echo "$ret" | cut -d"|" -f4) # Apply any changes [[ -n "$newLocation" && "$newLocation" != "$cityName" ]] && change_location "$newLocation" [[ -n "$newUnit" && "$newUnit" != "$tempUnit" ]] && tempUnit="$newUnit" && save_config [[ -n "$newInterval" && "$newInterval" != "$updateInterval" ]] && updateInterval="$newInterval" && save_config fi } # Main loop while : ; do get_location fetch_weather_data # Display weather using the text-info widget display_weather ret=$? # Handle button actions case $ret in $refreshBtn) # Force a weather update weatherLastUpdate=0 continue ;; $settingsBtn) # Display settings dialog display_settings continue ;; $quitBtn|252) # Quit button or window closed break ;; esac done exit 0