A few updates, cleanup updated weather module.
This commit is contained in:
@@ -1,73 +1,440 @@
|
||||
#!/usr/bin/env bash
|
||||
[ -f functions.sh ] && source functions.sh
|
||||
|
||||
# Dependencies required by this module
|
||||
dependencies=("curl" "jq" "date")
|
||||
|
||||
if ! check_dependencies "${dependencies[@]}"; then
|
||||
msg "$2" "$1: This module requires: ${dependencies[*]}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -f
|
||||
|
||||
name="$1"
|
||||
channelName="$2"
|
||||
shift
|
||||
shift
|
||||
shift 2
|
||||
|
||||
if [[ $# -eq 0 ]]; then
|
||||
msg "$channelName" "$name: Please provide a location or postcode."
|
||||
# Database file for user locations and cached geocoding
|
||||
weatherDb="data/weather.db"
|
||||
mkdir -p "$(dirname "$weatherDb")"
|
||||
touch "$weatherDb"
|
||||
|
||||
# Weather code mapping (Open-Meteo codes to descriptions)
|
||||
declare -A weatherCodes=(
|
||||
[0]="Clear sky"
|
||||
[1]="Mainly clear"
|
||||
[2]="Partly cloudy"
|
||||
[3]="Overcast"
|
||||
[45]="Fog"
|
||||
[48]="Rime fog"
|
||||
[51]="Light drizzle"
|
||||
[53]="Moderate drizzle"
|
||||
[55]="Dense drizzle"
|
||||
[56]="Light freezing drizzle"
|
||||
[57]="Dense freezing drizzle"
|
||||
[61]="Slight rain"
|
||||
[63]="Moderate rain"
|
||||
[65]="Heavy rain"
|
||||
[66]="Light freezing rain"
|
||||
[67]="Heavy freezing rain"
|
||||
[71]="Slight snow fall"
|
||||
[73]="Moderate snow fall"
|
||||
[75]="Heavy snow fall"
|
||||
[77]="Snow flurries"
|
||||
[80]="Slight rain showers"
|
||||
[81]="Moderate rain showers"
|
||||
[82]="Heavy rain showers"
|
||||
[85]="Slight snow showers"
|
||||
[86]="Heavy snow showers"
|
||||
[95]="Thunderstorm"
|
||||
[96]="Thunderstorm with slight hail"
|
||||
[99]="Thunderstorm with heavy hail"
|
||||
)
|
||||
|
||||
# Function to get cached geocode result
|
||||
get_cached_location() {
|
||||
local query="$1"
|
||||
local queryLower="${query,,}"
|
||||
grep -i "^CACHE|${queryLower}|" "$weatherDb" 2>/dev/null | head -n1
|
||||
}
|
||||
|
||||
# Function to get user's saved location
|
||||
get_user_location() {
|
||||
local user="$1"
|
||||
grep "^USER|${user}|" "$weatherDb" 2>/dev/null | head -n1
|
||||
}
|
||||
|
||||
# Function to save user location
|
||||
save_user_location() {
|
||||
local user="$1"
|
||||
local location="$2"
|
||||
local lat="$3"
|
||||
local lon="$4"
|
||||
local formatted="$5"
|
||||
|
||||
# Remove old entry if exists
|
||||
if [[ -f "$weatherDb" ]]; then
|
||||
grep -v "^USER|${user}|" "$weatherDb" > "${weatherDb}.tmp"
|
||||
mv "${weatherDb}.tmp" "$weatherDb"
|
||||
fi
|
||||
|
||||
# Add new entry
|
||||
echo "USER|${user}|${location}|${lat}|${lon}|${formatted}" >> "$weatherDb"
|
||||
}
|
||||
|
||||
# Function to delete user location
|
||||
delete_user_location() {
|
||||
local user="$1"
|
||||
|
||||
if [[ -f "$weatherDb" ]]; then
|
||||
grep -v "^USER|${user}|" "$weatherDb" > "${weatherDb}.tmp"
|
||||
mv "${weatherDb}.tmp" "$weatherDb"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to cache geocode result
|
||||
cache_location() {
|
||||
local query="$1"
|
||||
local location="$2"
|
||||
local lat="$3"
|
||||
local lon="$4"
|
||||
local formatted="$5"
|
||||
local queryLower="${query,,}"
|
||||
|
||||
# Check if already cached
|
||||
local cached
|
||||
cached=$(get_cached_location "$query")
|
||||
if [[ -z "$cached" ]]; then
|
||||
echo "CACHE|${queryLower}|${location}|${lat}|${lon}|${formatted}" >> "$weatherDb"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to format location (City, State or City, Country)
|
||||
format_location() {
|
||||
local address="$1"
|
||||
|
||||
# Use jq to parse the address components
|
||||
local locality
|
||||
local state
|
||||
local country
|
||||
local countryCode
|
||||
|
||||
# Try various location types in order of specificity
|
||||
locality=$(echo "$address" | jq -r '.city // .town // .village // .hamlet // .municipality // .suburb // empty' 2>/dev/null)
|
||||
state=$(echo "$address" | jq -r '.state // empty' 2>/dev/null)
|
||||
country=$(echo "$address" | jq -r '.country // empty' 2>/dev/null)
|
||||
countryCode=$(echo "$address" | jq -r '.country_code // empty' 2>/dev/null)
|
||||
|
||||
# For US addresses, always show state if available
|
||||
if [[ "$countryCode" == "us" && -n "$locality" && -n "$state" ]]; then
|
||||
echo "${locality}, ${state}"
|
||||
elif [[ "$countryCode" == "us" && -n "$state" ]]; then
|
||||
# No locality, just show state
|
||||
echo "${state}, United States"
|
||||
elif [[ -n "$locality" && -n "$country" ]]; then
|
||||
# International: show locality and country
|
||||
echo "${locality}, ${country}"
|
||||
elif [[ -n "$country" ]]; then
|
||||
# Just country
|
||||
echo "$country"
|
||||
else
|
||||
# Fallback to display_name if parsing fails
|
||||
echo "$address" | jq -r '.display_name // "Unknown location"' 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to geocode a location
|
||||
geocode_location() {
|
||||
local query="$1"
|
||||
|
||||
# Check cache first
|
||||
local cached
|
||||
local location
|
||||
local lat
|
||||
local lon
|
||||
local formatted
|
||||
cached=$(get_cached_location "$query")
|
||||
if [[ -n "$cached" ]]; then
|
||||
# Format: CACHE|query|location|lat|lon|formatted
|
||||
location=$(echo "$cached" | cut -d'|' -f3)
|
||||
lat=$(echo "$cached" | cut -d'|' -f4)
|
||||
lon=$(echo "$cached" | cut -d'|' -f5)
|
||||
formatted=$(echo "$cached" | cut -d'|' -f6)
|
||||
|
||||
# If no formatted location in cache (old format), use location
|
||||
if [[ -z "$formatted" ]]; then
|
||||
formatted="$location"
|
||||
fi
|
||||
|
||||
echo "${location}|${lat}|${lon}|${formatted}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Query Nominatim API
|
||||
local url="https://nominatim.openstreetmap.org/search"
|
||||
local userAgent="stormbot-weather/1.0"
|
||||
local response
|
||||
|
||||
# URL encode the query (replace spaces with +)
|
||||
local encodedQuery="${query// /+}"
|
||||
|
||||
response=$(curl -s --connect-timeout 5 --max-time 10 \
|
||||
-H "User-Agent: ${userAgent}" \
|
||||
"${url}?q=${encodedQuery}&format=json&limit=1&addressdetails=1")
|
||||
|
||||
if [[ -z "$response" || "$response" == "[]" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Parse response
|
||||
local fullAddress
|
||||
local addressDetails
|
||||
local formattedLocation
|
||||
lat=$(echo "$response" | jq -r '.[0].lat // empty' 2>/dev/null)
|
||||
lon=$(echo "$response" | jq -r '.[0].lon // empty' 2>/dev/null)
|
||||
fullAddress=$(echo "$response" | jq -r '.[0].display_name // empty' 2>/dev/null)
|
||||
addressDetails=$(echo "$response" | jq -r '.[0].address // empty' 2>/dev/null)
|
||||
|
||||
if [[ -z "$lat" || -z "$lon" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Format the location nicely
|
||||
formattedLocation=$(format_location "$addressDetails")
|
||||
|
||||
# Cache the result
|
||||
cache_location "$query" "$fullAddress" "$lat" "$lon" "$formattedLocation"
|
||||
|
||||
echo "${fullAddress}|${lat}|${lon}|${formattedLocation}"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Function to get weather data
|
||||
get_weather() {
|
||||
local lat="$1"
|
||||
local lon="$2"
|
||||
|
||||
local url="https://api.open-meteo.com/v1/forecast"
|
||||
local params="latitude=${lat}&longitude=${lon}"
|
||||
params+="¤t=temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m"
|
||||
params+="&daily=weather_code,temperature_2m_max,temperature_2m_min"
|
||||
params+="&timezone=auto&forecast_days=3&temperature_unit=fahrenheit&wind_speed_unit=mph"
|
||||
|
||||
local response
|
||||
response=$(curl -s --connect-timeout 5 --max-time 10 "${url}?${params}")
|
||||
|
||||
if [[ -z "$response" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "$response"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Function to format weather output
|
||||
format_weather() {
|
||||
local weatherData="$1"
|
||||
local locationName="$2"
|
||||
|
||||
# Parse current weather
|
||||
local temp
|
||||
local humidity
|
||||
local windSpeed
|
||||
local weatherCode
|
||||
temp=$(echo "$weatherData" | jq -r '.current.temperature_2m // "N/A"')
|
||||
humidity=$(echo "$weatherData" | jq -r '.current.relative_humidity_2m // "N/A"')
|
||||
windSpeed=$(echo "$weatherData" | jq -r '.current.wind_speed_10m // "N/A"')
|
||||
weatherCode=$(echo "$weatherData" | jq -r '.current.weather_code // 0')
|
||||
local conditions="${weatherCodes[$weatherCode]:-Unknown}"
|
||||
|
||||
# Build message
|
||||
local message="Weather for ${locationName}: ${temp}°F, ${conditions}"
|
||||
|
||||
if [[ "$humidity" != "N/A" ]]; then
|
||||
message+=", Humidity: ${humidity}%"
|
||||
fi
|
||||
|
||||
if [[ "$windSpeed" != "N/A" ]]; then
|
||||
message+=", Wind: ${windSpeed} mph"
|
||||
fi
|
||||
|
||||
echo "$message"
|
||||
}
|
||||
|
||||
# Function to format forecast output
|
||||
format_forecast() {
|
||||
local weatherData="$1"
|
||||
|
||||
# Parse forecast data
|
||||
local dates
|
||||
dates=$(echo "$weatherData" | jq -r '.daily.time[]' 2>/dev/null)
|
||||
|
||||
if [[ -z "$dates" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
local forecastParts=()
|
||||
local i=0
|
||||
|
||||
while IFS= read -r dateStr; do
|
||||
local minTemp
|
||||
local maxTemp
|
||||
local code
|
||||
minTemp=$(echo "$weatherData" | jq -r ".daily.temperature_2m_min[$i] // \"N/A\"")
|
||||
maxTemp=$(echo "$weatherData" | jq -r ".daily.temperature_2m_max[$i] // \"N/A\"")
|
||||
code=$(echo "$weatherData" | jq -r ".daily.weather_code[$i] // 0")
|
||||
|
||||
# Format date to day of week
|
||||
local dayName
|
||||
if date --version &>/dev/null; then
|
||||
# GNU date
|
||||
dayName=$(date -d "$dateStr" '+%A' 2>/dev/null)
|
||||
else
|
||||
# BSD date (macOS)
|
||||
dayName=$(date -j -f '%Y-%m-%d' "$dateStr" '+%A' 2>/dev/null)
|
||||
fi
|
||||
|
||||
if [[ -z "$dayName" ]]; then
|
||||
dayName="$dateStr"
|
||||
fi
|
||||
|
||||
local forecastConditions="${weatherCodes[$code]:-Unknown}"
|
||||
|
||||
if [[ "$minTemp" != "N/A" && "$maxTemp" != "N/A" ]]; then
|
||||
forecastParts+=("${dayName}: ${minTemp}°F to ${maxTemp}°F, ${forecastConditions}")
|
||||
fi
|
||||
|
||||
((i++))
|
||||
|
||||
# Only show 3 days
|
||||
if [[ $i -ge 3 ]]; then
|
||||
break
|
||||
fi
|
||||
done <<< "$dates"
|
||||
|
||||
if [[ ${#forecastParts[@]} -gt 0 ]]; then
|
||||
local IFS=" | "
|
||||
echo "Forecast: ${forecastParts[*]}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Main command logic
|
||||
subcommand="${1:-}"
|
||||
|
||||
# If subcommand is the module name itself (w or weather), treat as no argument
|
||||
if [[ "$subcommand" == "w" || "$subcommand" == "weather" ]]; then
|
||||
subcommand=""
|
||||
fi
|
||||
|
||||
location="${*}"
|
||||
case "$subcommand" in
|
||||
set)
|
||||
# Set user's location
|
||||
shift
|
||||
if [[ $# -eq 0 ]]; then
|
||||
msg "$channelName" "$name: Usage: weather set <location>"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Convert spaces to +.
|
||||
location="${location//[[:space:]]/+}"
|
||||
location="$*"
|
||||
|
||||
# Validate location length
|
||||
if [[ ${#location} -gt 100 ]]; then
|
||||
msg "$channelName" "$name: Location is too long (max 100 characters)."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# c Weather condition,
|
||||
# C Weather condition textual name,
|
||||
# h Humidity,
|
||||
# t Temperature (Actual),
|
||||
# f Temperature (Feels Like),
|
||||
# w Wind,
|
||||
# l Location,
|
||||
# m Moonphase ðð,
|
||||
# M Moonday,
|
||||
# p precipitation (mm),
|
||||
# o Probability of Precipitation,
|
||||
# P pressure (hPa),
|
||||
|
||||
# D Dawn*,
|
||||
# S Sunrise*,
|
||||
# z Zenith*,
|
||||
# s Sunset*,
|
||||
# d Dusk*.
|
||||
# Geocode the location
|
||||
geocodeResult=$(geocode_location "$location")
|
||||
if [[ $? -ne 0 || -z "$geocodeResult" ]]; then
|
||||
msg "$channelName" "$name: Could not find that location. Please try a different search term."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
format="%c|%C|%h|%t|%f|%w|%l|%m|%M|%p|%o|%P|%D|%S|%z|%s|%d"
|
||||
argList="${format//%/}"
|
||||
argList="${argList//|/ }"
|
||||
weatherString="$(curl -s https://wttr.in/${location}?format="${format}")"
|
||||
# Parse result: fullAddress|lat|lon|formattedLocation
|
||||
fullAddress=$(echo "$geocodeResult" | cut -d'|' -f1)
|
||||
lat=$(echo "$geocodeResult" | cut -d'|' -f2)
|
||||
lon=$(echo "$geocodeResult" | cut -d'|' -f3)
|
||||
formattedLocation=$(echo "$geocodeResult" | cut -d'|' -f4)
|
||||
|
||||
i=1
|
||||
declare -A weatherInfo
|
||||
for j in $argList ; do
|
||||
weatherInfo[$j]="$(echo "${weatherString}" | cut -d '|' -f $i)"
|
||||
((i++))
|
||||
done
|
||||
# Save user location
|
||||
save_user_location "$name" "$fullAddress" "$lat" "$lon" "$formattedLocation"
|
||||
|
||||
# Format times to 12 hour format.
|
||||
for i in S D z s d ; do
|
||||
weatherInfo[$i]="$(date '+%r' --date="${weatherInfo[$i]}")"
|
||||
done
|
||||
msg "$channelName" "$name: Your location has been set to: ${formattedLocation}"
|
||||
;;
|
||||
|
||||
message="${weatherInfo[l]}: ${weatherInfo[t]} and ${weatherInfo[C]}"
|
||||
if [[ "${weatherInfo[t]}" == "${weatherInfo[f]}" ]]; then
|
||||
message+=". "
|
||||
else
|
||||
message+=" with a real feel of ${weatherInfo[f]}. "
|
||||
fi
|
||||
message+="Wind: ${weatherInfo[w]} "
|
||||
if [[ "${weatherInfo[p]}" != "0.0mm" ]]; then
|
||||
message+=" Precipitation: ${weatherInfo[p]} "
|
||||
fi
|
||||
if [[ -n "${weatherInfo[o]}" ]]; then
|
||||
message+="Chance of precipitation ${weatherInfo[o]} "
|
||||
fi
|
||||
message+="Humidity: ${weatherInfo[h]}. "
|
||||
message+="Sunrise: ${weatherInfo[S]}, Sunset: ${weatherInfo[s]}."
|
||||
del|delete)
|
||||
# Delete user's location
|
||||
delete_user_location "$name"
|
||||
msg "$channelName" "$name: Your location has been deleted."
|
||||
;;
|
||||
|
||||
msg "$channelName" "$name: ${message//+/ }"
|
||||
*)
|
||||
# Show weather (default action)
|
||||
if [[ -n "$subcommand" ]]; then
|
||||
# Weather for specified location
|
||||
location="$*"
|
||||
|
||||
# Validate location length
|
||||
if [[ ${#location} -gt 100 ]]; then
|
||||
msg "$channelName" "$name: Location is too long (max 100 characters)."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Geocode the location
|
||||
geocodeResult=$(geocode_location "$location")
|
||||
if [[ $? -ne 0 || -z "$geocodeResult" ]]; then
|
||||
msg "$channelName" "$name: Could not find that location. Please try a different search term."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Parse result
|
||||
fullAddress=$(echo "$geocodeResult" | cut -d'|' -f1)
|
||||
lat=$(echo "$geocodeResult" | cut -d'|' -f2)
|
||||
lon=$(echo "$geocodeResult" | cut -d'|' -f3)
|
||||
formattedLocation=$(echo "$geocodeResult" | cut -d'|' -f4)
|
||||
|
||||
locationDisplay="$formattedLocation"
|
||||
else
|
||||
# Weather for user's saved location
|
||||
userLocation=$(get_user_location "$name")
|
||||
if [[ -z "$userLocation" ]]; then
|
||||
msg "$channelName" "$name: You have not set a location. Use 'weather set <location>' to save your location, or 'weather <location>' to check weather for a specific location."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Parse user location: USER|name|location|lat|lon|formatted
|
||||
fullAddress=$(echo "$userLocation" | cut -d'|' -f3)
|
||||
lat=$(echo "$userLocation" | cut -d'|' -f4)
|
||||
lon=$(echo "$userLocation" | cut -d'|' -f5)
|
||||
formattedLocation=$(echo "$userLocation" | cut -d'|' -f6)
|
||||
|
||||
# If no formatted location stored (old format), use full address
|
||||
if [[ -z "$formattedLocation" ]]; then
|
||||
formattedLocation="$fullAddress"
|
||||
fi
|
||||
|
||||
locationDisplay="$formattedLocation"
|
||||
fi
|
||||
|
||||
# Get weather data
|
||||
weatherData=$(get_weather "$lat" "$lon")
|
||||
if [[ $? -ne 0 || -z "$weatherData" ]]; then
|
||||
msg "$channelName" "$name: Could not retrieve weather data. Please try again later."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Format and send current weather
|
||||
weatherMessage=$(format_weather "$weatherData" "$locationDisplay")
|
||||
msg "$channelName" "$name: ${weatherMessage}"
|
||||
|
||||
# Format and send forecast
|
||||
forecastMessage=$(format_forecast "$weatherData")
|
||||
if [[ $? -eq 0 && -n "$forecastMessage" ]]; then
|
||||
msg "$channelName" "$name: ${forecastMessage}"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
Reference in New Issue
Block a user