440 lines
16 KiB
Bash
Executable File
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¤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
|