Files
i3-dot/i3/scripts/openweather
T
2026-06-15 02:07:53 +07:00

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 "$@"