472 lines
13 KiB
Bash
Executable File
472 lines
13 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# openweater - OpenWeatherMap Weather Fetcher for Waybar/Status Bars
|
|
#
|
|
# Copyright (C) 2025 Johannes Kamprad
|
|
#
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
|
|
# openweater - OpenWeatherMap Weather Fetcher for Waybar/Status Bars
|
|
# Secure API key handling with configurable locations
|
|
# including setup script and help
|
|
# Options:
|
|
# -h, --help Show this help message
|
|
# -c, --city-id ID Override city ID (required if not in config)
|
|
# -k, --api-key KEY Override API key (not recommended, use config file)
|
|
# -u, --units UNITS Units: metric, imperial, kelvin (default: $DEFAULT_UNITS)
|
|
# -f, --force-refresh Force refresh (ignore cache)
|
|
# --setup Interactive setup wizard
|
|
# --show-config Show current configuration
|
|
|
|
set -euo pipefail
|
|
|
|
# Set C locale to avoid German decimal formatting issues
|
|
export LC_NUMERIC=C
|
|
export LC_ALL=C
|
|
|
|
# Colors for output
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
NC='\033[0m' # No Color
|
|
|
|
# Default configuration (no default location)
|
|
DEFAULT_UNITS="metric"
|
|
CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}"
|
|
CONFIG_FILE="$CONFIG_DIR/openweather/config"
|
|
CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/openweather"
|
|
CACHE_FILE="$CACHE_DIR/weather_data"
|
|
CACHE_DURATION=600 # 10 minutes
|
|
|
|
# Logging functions
|
|
log_error() {
|
|
echo -e "${RED}[ERROR]${NC} $*" >&2
|
|
}
|
|
|
|
log_warn() {
|
|
echo -e "${YELLOW}[WARN]${NC} $*" >&2
|
|
}
|
|
|
|
# Show help
|
|
show_help() {
|
|
cat << EOF
|
|
Usage: $(basename "$0") [OPTIONS]
|
|
|
|
Secure OpenWeatherMap weather fetcher with configurable locations.
|
|
|
|
Options:
|
|
-h, --help Show this help message
|
|
-c, --city-id ID Override city ID (required if not in config)
|
|
-k, --api-key KEY Override API key (not recommended, use config file)
|
|
-u, --units UNITS Units: metric, imperial, kelvin (default: $DEFAULT_UNITS)
|
|
-f, --force-refresh Force refresh (ignore cache)
|
|
--setup Interactive setup wizard
|
|
--show-config Show current configuration
|
|
|
|
Configuration:
|
|
Config file: $CONFIG_FILE
|
|
|
|
Create config file with:
|
|
OPENWEATHER_API_KEY="your_api_key_here"
|
|
OPENWEATHER_CITY_ID="your_city_id" # Required
|
|
OPENWEATHER_UNITS="metric" # Optional
|
|
|
|
Examples:
|
|
$(basename "$0") # Use config file settings
|
|
$(basename "$0") --city-id 5128581 # New York
|
|
$(basename "$0") --units imperial # Fahrenheit
|
|
$(basename "$0") --setup # Run setup wizard
|
|
|
|
Get your free API key at: https://openweathermap.org/api
|
|
Find city IDs at: https://openweathermap.org/find
|
|
|
|
EOF
|
|
}
|
|
|
|
# Check dependencies
|
|
check_dependencies() {
|
|
local missing_deps=()
|
|
|
|
for cmd in jq curl; do
|
|
if ! command -v "$cmd" >/dev/null 2>&1; then
|
|
missing_deps+=("$cmd")
|
|
fi
|
|
done
|
|
|
|
if [[ ${#missing_deps[@]} -gt 0 ]]; then
|
|
log_error "Missing required dependencies: ${missing_deps[*]}"
|
|
echo "Install with: sudo pacman -S ${missing_deps[*]}"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# Load configuration
|
|
load_config() {
|
|
# Set defaults (no default city ID)
|
|
OPENWEATHER_CITY_ID=""
|
|
OPENWEATHER_UNITS="$DEFAULT_UNITS"
|
|
|
|
# Load from config file if it exists
|
|
if [[ -f "$CONFIG_FILE" ]]; then
|
|
source "$CONFIG_FILE"
|
|
fi
|
|
|
|
# Override with environment variables if set
|
|
OPENWEATHER_API_KEY="${OPENWEATHER_API_KEY:-}"
|
|
OPENWEATHER_CITY_ID="${OPENWEATHER_CITY_ID:-}"
|
|
OPENWEATHER_UNITS="${OPENWEATHER_UNITS:-$DEFAULT_UNITS}"
|
|
}
|
|
|
|
# Validate API key
|
|
validate_api_key() {
|
|
if [[ -z "$OPENWEATHER_API_KEY" ]]; then
|
|
log_error "No API key found!"
|
|
echo
|
|
echo "To fix this:"
|
|
echo "1. Get free API key: https://openweathermap.org/api"
|
|
echo "2. Run setup wizard: $0 --setup"
|
|
echo "3. Or set environment variable: export OPENWEATHER_API_KEY=your_key"
|
|
echo "4. Or create config file: $CONFIG_FILE"
|
|
exit 1
|
|
fi
|
|
|
|
# Basic validation (OpenWeatherMap keys are typically 32 chars)
|
|
if [[ ${#OPENWEATHER_API_KEY} -ne 32 ]]; then
|
|
log_warn "API key length seems incorrect (expected 32 characters, got ${#OPENWEATHER_API_KEY})"
|
|
fi
|
|
}
|
|
|
|
# Validate city ID
|
|
validate_city_id() {
|
|
if [[ -z "$OPENWEATHER_CITY_ID" ]]; then
|
|
log_error "No city ID found!"
|
|
echo
|
|
echo "To fix this:"
|
|
echo "1. Run setup wizard: $0 --setup"
|
|
echo "2. Or set environment variable: export OPENWEATHER_CITY_ID=your_city_id"
|
|
echo "3. Or add OPENWEATHER_CITY_ID=\"your_city_id\" to: $CONFIG_FILE"
|
|
echo "4. Or provide city ID via command line: $0 --city-id your_city_id"
|
|
echo
|
|
echo "Find city IDs at: https://openweathermap.org/find"
|
|
exit 1
|
|
fi
|
|
|
|
# Basic validation (city IDs are typically numeric)
|
|
if [[ ! "$OPENWEATHER_CITY_ID" =~ ^[0-9]+$ ]]; then
|
|
log_warn "City ID format seems incorrect (expected numeric, got: $OPENWEATHER_CITY_ID)"
|
|
fi
|
|
}
|
|
|
|
# Setup wizard
|
|
run_setup() {
|
|
echo -e "${GREEN}=== OpenWeatherMap Setup Wizard ===${NC}"
|
|
echo
|
|
|
|
# Create config directory
|
|
mkdir -p "$(dirname "$CONFIG_FILE")"
|
|
|
|
# Get API key
|
|
echo "Get your free API key at: https://openweathermap.org/api"
|
|
echo
|
|
echo -n "Enter your OpenWeatherMap API key: "
|
|
read -r api_key
|
|
|
|
if [[ -z "$api_key" ]]; then
|
|
log_error "API key cannot be empty"
|
|
exit 1
|
|
fi
|
|
|
|
# Get city ID (required)
|
|
echo
|
|
echo "Find your city ID at: https://openweathermap.org/find"
|
|
echo -n "Enter city ID: "
|
|
read -r city_id
|
|
|
|
if [[ -z "$city_id" ]]; then
|
|
log_error "City ID cannot be empty"
|
|
exit 1
|
|
fi
|
|
|
|
# Get units (optional)
|
|
echo
|
|
echo "Available units: metric (°C), imperial (°F), kelvin (K)"
|
|
echo -n "Enter units [metric]: "
|
|
read -r units
|
|
units="${units:-metric}"
|
|
|
|
# Write config file
|
|
cat > "$CONFIG_FILE" << EOF
|
|
# OpenWeatherMap Configuration
|
|
# Generated by $(basename "$0") setup wizard on $(date)
|
|
|
|
# Your API key from https://openweathermap.org/api
|
|
OPENWEATHER_API_KEY="$api_key"
|
|
|
|
# City ID from https://openweathermap.org/find
|
|
OPENWEATHER_CITY_ID="$city_id"
|
|
|
|
# Units: metric, imperial, kelvin
|
|
OPENWEATHER_UNITS="$units"
|
|
EOF
|
|
|
|
# Set secure permissions
|
|
chmod 600 "$CONFIG_FILE"
|
|
|
|
echo
|
|
echo -e "${GREEN}✅ Configuration saved to: $CONFIG_FILE${NC}"
|
|
echo -e "${GREEN}✅ File permissions set to 600 (user read/write only)${NC}"
|
|
echo
|
|
echo "Testing configuration..."
|
|
|
|
# Test the configuration
|
|
OPENWEATHER_API_KEY="$api_key"
|
|
OPENWEATHER_CITY_ID="$city_id"
|
|
OPENWEATHER_UNITS="$units"
|
|
|
|
if fetch_weather_data; then
|
|
echo -e "${GREEN}✅ Configuration test successful!${NC}"
|
|
else
|
|
log_error "Configuration test failed. Please check your API key and city ID."
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# Show current configuration
|
|
show_config() {
|
|
echo "=== Current Configuration ==="
|
|
echo "Config file: $CONFIG_FILE"
|
|
echo "Cache file: $CACHE_FILE"
|
|
echo "Cache duration: ${CACHE_DURATION}s"
|
|
echo
|
|
|
|
if [[ -f "$CONFIG_FILE" ]]; then
|
|
echo "Configuration:"
|
|
echo " API Key: ${OPENWEATHER_API_KEY:0:8}... (${#OPENWEATHER_API_KEY} chars)"
|
|
echo " City ID: $OPENWEATHER_CITY_ID"
|
|
echo " Units: $OPENWEATHER_UNITS"
|
|
else
|
|
echo "❌ No configuration file found at: $CONFIG_FILE"
|
|
echo "Run: $0 --setup"
|
|
fi
|
|
}
|
|
|
|
# Check cache validity
|
|
is_cache_valid() {
|
|
[[ -f "$CACHE_FILE" ]] && \
|
|
[[ $(($(date +%s) - $(stat -c %Y "$CACHE_FILE" 2>/dev/null || echo 0))) -lt $CACHE_DURATION ]]
|
|
}
|
|
|
|
# Fetch weather data from API
|
|
fetch_weather_data() {
|
|
local url="https://api.openweathermap.org/data/2.5/weather?id=${OPENWEATHER_CITY_ID}&units=${OPENWEATHER_UNITS}&appid=${OPENWEATHER_API_KEY}"
|
|
|
|
local response
|
|
if ! response=$(curl -sf "$url" 2>/dev/null); then
|
|
log_error "Failed to fetch weather data from API"
|
|
return 1
|
|
fi
|
|
|
|
# Validate JSON response
|
|
if ! echo "$response" | jq . >/dev/null 2>&1; then
|
|
log_error "Invalid JSON response from API"
|
|
return 1
|
|
fi
|
|
|
|
# Check for API error
|
|
local api_code
|
|
api_code=$(echo "$response" | jq -r '.cod // empty')
|
|
if [[ "$api_code" != "200" ]]; then
|
|
local api_message
|
|
api_message=$(echo "$response" | jq -r '.message // "Unknown API error"')
|
|
log_error "API Error ($api_code): $api_message"
|
|
return 1
|
|
fi
|
|
|
|
# Cache the response
|
|
mkdir -p "$CACHE_DIR"
|
|
echo "$response" > "$CACHE_FILE"
|
|
|
|
return 0
|
|
}
|
|
|
|
# Get weather data (from cache or API)
|
|
get_weather_data() {
|
|
local force_refresh="${1:-false}"
|
|
|
|
if [[ "$force_refresh" != "true" ]] && is_cache_valid; then
|
|
cat "$CACHE_FILE"
|
|
else
|
|
if fetch_weather_data; then
|
|
cat "$CACHE_FILE"
|
|
else
|
|
# Fallback to cache if available
|
|
if [[ -f "$CACHE_FILE" ]]; then
|
|
log_warn "Using stale cache data due to API failure"
|
|
cat "$CACHE_FILE"
|
|
else
|
|
return 1
|
|
fi
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# Calculate wind direction
|
|
get_wind_direction() {
|
|
local degrees="$1"
|
|
local directions=(N NNE NE ENE E ESE SE SSE S SSW SW WSW W WNW NW NNW)
|
|
local index
|
|
index=$(awk "BEGIN {print int(($degrees % 360) / 22.5)}")
|
|
echo "${directions[$index]}"
|
|
}
|
|
|
|
# Get weather icon
|
|
get_weather_icon() {
|
|
local condition="$1"
|
|
case "$condition" in
|
|
'Clear') echo "☀" ;; # Clear sky
|
|
'Clouds') echo "☁" ;; # Cloudy
|
|
'Rain'|'Drizzle') echo "🌧" ;; # Rain
|
|
'Snow') echo "❄" ;; # Snow
|
|
'Thunderstorm') echo "⛈" ;; # Thunder
|
|
'Mist'|'Fog') echo "🌫" ;; # Fog
|
|
*) echo "🌤" ;; # Default
|
|
esac
|
|
}
|
|
|
|
# Safe number formatting (handles locale issues)
|
|
safe_printf() {
|
|
local format="$1"
|
|
local number="$2"
|
|
|
|
# Validate number is actually numeric
|
|
if [[ ! "$number" =~ ^-?[0-9]+(\.[0-9]+)?$ ]]; then
|
|
echo "0.0"
|
|
return 1
|
|
fi
|
|
|
|
# Use awk for reliable formatting regardless of locale
|
|
awk "BEGIN {printf \"$format\", $number}"
|
|
}
|
|
|
|
# Format weather output
|
|
format_weather() {
|
|
local weather_data="$1"
|
|
|
|
# Parse weather data with error checking
|
|
local condition temp wind_speed_ms wind_deg
|
|
condition=$(echo "$weather_data" | jq -r '.weather[0].main // "Unknown"')
|
|
temp=$(echo "$weather_data" | jq -r '.main.temp // "0"')
|
|
wind_speed_ms=$(echo "$weather_data" | jq -r '.wind.speed // "0"')
|
|
wind_deg=$(echo "$weather_data" | jq -r '.wind.deg // "0"')
|
|
|
|
# Validate parsed data
|
|
[[ "$condition" == "null" ]] && condition="Unknown"
|
|
[[ "$temp" == "null" ]] && temp="0"
|
|
[[ "$wind_speed_ms" == "null" ]] && wind_speed_ms="0"
|
|
[[ "$wind_deg" == "null" ]] && wind_deg="0"
|
|
|
|
# Format temperature with safe formatting
|
|
local temp_formatted
|
|
temp_formatted=$(safe_printf "%.1f" "$temp")
|
|
|
|
# Convert wind speed to km/h with safe formatting
|
|
local wind_speed_kmh
|
|
wind_speed_kmh=$(awk "BEGIN {printf \"%.1f\", ($wind_speed_ms + 0) * 3.6}")
|
|
|
|
# Get wind direction
|
|
local wind_dir
|
|
wind_dir=$(get_wind_direction "$wind_deg")
|
|
|
|
# Get weather icon
|
|
local icon
|
|
icon=$(get_weather_icon "$condition")
|
|
|
|
# Format unit symbol
|
|
local unit_symbol
|
|
case "$OPENWEATHER_UNITS" in
|
|
"imperial") unit_symbol="°F" ;;
|
|
"kelvin") unit_symbol="K" ;;
|
|
*) unit_symbol="°C" ;;
|
|
esac
|
|
|
|
# Output formatted weather
|
|
echo "${icon} ${temp_formatted}${unit_symbol}, ${wind_speed_kmh} km/h ${wind_dir}"
|
|
}
|
|
|
|
# Main function
|
|
main() {
|
|
local city_id_override=""
|
|
local api_key_override=""
|
|
local units_override=""
|
|
local force_refresh="false"
|
|
|
|
# Parse command line arguments
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
-h|--help)
|
|
show_help
|
|
exit 0
|
|
;;
|
|
-c|--city-id)
|
|
city_id_override="$2"
|
|
shift 2
|
|
;;
|
|
-k|--api-key)
|
|
api_key_override="$2"
|
|
shift 2
|
|
;;
|
|
-u|--units)
|
|
units_override="$2"
|
|
shift 2
|
|
;;
|
|
-f|--force-refresh)
|
|
force_refresh="true"
|
|
shift
|
|
;;
|
|
--setup)
|
|
check_dependencies
|
|
run_setup
|
|
exit 0
|
|
;;
|
|
--show-config)
|
|
load_config
|
|
show_config
|
|
exit 0
|
|
;;
|
|
*)
|
|
log_error "Unknown option: $1"
|
|
show_help
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# Check dependencies
|
|
check_dependencies
|
|
|
|
# Load configuration
|
|
load_config
|
|
|
|
# Apply overrides
|
|
[[ -n "$city_id_override" ]] && OPENWEATHER_CITY_ID="$city_id_override"
|
|
[[ -n "$api_key_override" ]] && OPENWEATHER_API_KEY="$api_key_override"
|
|
[[ -n "$units_override" ]] && OPENWEATHER_UNITS="$units_override"
|
|
|
|
# Validate configuration
|
|
validate_api_key
|
|
validate_city_id
|
|
|
|
# Get and format weather data
|
|
local weather_data
|
|
if weather_data=$(get_weather_data "$force_refresh"); then
|
|
format_weather "$weather_data"
|
|
else
|
|
echo "⚠️ Weather data unavailable"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# Run main function
|
|
main "$@"
|