I38/scripts/weather.sh

440 lines
16 KiB
Bash
Executable File

#!/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&current=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