aboutsummaryrefslogtreecommitdiffstats
path: root/config/hypr/UserScripts
diff options
context:
space:
mode:
Diffstat (limited to 'config/hypr/UserScripts')
-rwxr-xr-xconfig/hypr/UserScripts/WallpaperEffects.sh58
-rwxr-xr-xconfig/hypr/UserScripts/WallpaperSelect.sh22
-rwxr-xr-xconfig/hypr/UserScripts/Weather.py591
3 files changed, 52 insertions, 619 deletions
diff --git a/config/hypr/UserScripts/WallpaperEffects.sh b/config/hypr/UserScripts/WallpaperEffects.sh
index 2ba58d0c..ac8fc0e8 100755
--- a/config/hypr/UserScripts/WallpaperEffects.sh
+++ b/config/hypr/UserScripts/WallpaperEffects.sh
@@ -108,31 +108,41 @@ main
sleep 1
if [[ -n "$choice" ]]; then
- sddm_simple="/usr/share/sddm/themes/simple_sddm_2"
- if [ -d "$sddm_simple" ]; then
-
- # Check if yad is running to avoid multiple yad notification
- if pidof yad > /dev/null; then
- killall yad
- fi
-
- if yad --info --text="Set current wallpaper as SDDM background?\n\nNOTE: This only applies to SIMPLE SDDM v2 Theme" \
- --text-align=left \
- --title="SDDM Background" \
- --timeout=5 \
- --timeout-indicator=right \
- --button="yad-yes:0" \
- --button="yad-no:1" \
- ; then
+ # Resolve SDDM themes directory (standard and NixOS path)
+ sddm_themes_dir=""
+ if [ -d "/usr/share/sddm/themes" ]; then
+ sddm_themes_dir="/usr/share/sddm/themes"
+ elif [ -d "/run/current-system/sw/share/sddm/themes" ]; then
+ sddm_themes_dir="/run/current-system/sw/share/sddm/themes"
+ fi
- # Check if terminal exists
- if ! command -v "$terminal" &>/dev/null; then
- notify-send -i "$iDIR/ja.png" "Missing $terminal" "Install $terminal to enable setting of wallpaper background"
- exit 1
- fi
+ if [ -n "$sddm_themes_dir" ]; then
+ sddm_simple="$sddm_themes_dir/simple_sddm_2"
+
+ # Only prompt if theme exists and its Backgrounds directory is writable
+ if [ -d "$sddm_simple" ] && [ -w "$sddm_simple/Backgrounds" ]; then
+ # Check if yad is running to avoid multiple yad notification
+ if pidof yad > /dev/null; then
+ killall yad
+ fi
- exec $SCRIPTSDIR/sddm_wallpaper.sh --effects
-
+ if yad --info --text="Set current wallpaper as SDDM background?\n\nNOTE: This only applies to SIMPLE SDDM v2 Theme" \
+ --text-align=left \
+ --title="SDDM Background" \
+ --timeout=5 \
+ --timeout-indicator=right \
+ --button="yad-yes:0" \
+ --button="yad-no:1" \
+ ; then
+
+ # Check if terminal exists
+ if ! command -v "$terminal" &>/dev/null; then
+ notify-send -i "$iDIR/ja.png" "Missing $terminal" "Install $terminal to enable setting of wallpaper background"
+ exit 1
+ fi
+
+ exec "$SCRIPTSDIR/sddm_wallpaper.sh" --effects
+ fi
fi
fi
-fi \ No newline at end of file
+fi
diff --git a/config/hypr/UserScripts/WallpaperSelect.sh b/config/hypr/UserScripts/WallpaperSelect.sh
index a08b53ce..466832ba 100755
--- a/config/hypr/UserScripts/WallpaperSelect.sh
+++ b/config/hypr/UserScripts/WallpaperSelect.sh
@@ -101,9 +101,21 @@ menu() {
# Offer SDDM Simple Wallpaper Option (only for non-video wallpapers)
set_sddm_wallpaper() {
sleep 1
- sddm_simple="/usr/share/sddm/themes/simple_sddm_2"
- if [ -d "$sddm_simple" ]; then
+ # Resolve SDDM themes directory (standard and NixOS path)
+ local sddm_themes_dir=""
+ if [ -d "/usr/share/sddm/themes" ]; then
+ sddm_themes_dir="/usr/share/sddm/themes"
+ elif [ -d "/run/current-system/sw/share/sddm/themes" ]; then
+ sddm_themes_dir="/run/current-system/sw/share/sddm/themes"
+ fi
+
+ [ -z "$sddm_themes_dir" ] && return 0
+
+ local sddm_simple="$sddm_themes_dir/simple_sddm_2"
+
+ # Only prompt if theme exists and its Backgrounds directory is writable
+ if [ -d "$sddm_simple" ] && [ -w "$sddm_simple/Backgrounds" ]; then
# Check if yad is running to avoid multiple notifications
if pidof yad >/dev/null; then
@@ -123,9 +135,9 @@ set_sddm_wallpaper() {
notify-send -i "$iDIR/error.png" "Missing $terminal" "Install $terminal to enable setting of wallpaper background"
exit 1
fi
-
- exec $SCRIPTSDIR/sddm_wallpaper.sh --normal
-
+
+ exec "$SCRIPTSDIR/sddm_wallpaper.sh" --normal
+
fi
fi
}
diff --git a/config/hypr/UserScripts/Weather.py b/config/hypr/UserScripts/Weather.py
index a71fe8ca..ca1d5281 100755
--- a/config/hypr/UserScripts/Weather.py
+++ b/config/hypr/UserScripts/Weather.py
@@ -9,7 +9,6 @@ import sys
import time
import html
from typing import Any, Dict, List, Optional, Tuple
-from datetime import datetime
import requests
@@ -18,7 +17,7 @@ import requests
# Examples (zsh):
# # One-off run
# # WEATHER_UNITS can be "metric" or "imperial"
-# WEATHER_UNITS=imperial WEATHER_PLACE="Concord, NH" python3 ~/.config/hypr/UserScripts/Weather.py
+# WEATHER_UNITS=imperial WEATHER_PLACE="Concord, NH" python3 /home/dwilliams/Projects/Weather.py
#
# # Persist in current shell session
# export WEATHER_UNITS=imperial
@@ -277,594 +276,6 @@ def fetch_open_meteo(lat: float, lon: float) -> Dict[str, Any]:
"latitude": lat,
"longitude": lon,
"current": "temperature_2m,apparent_temperature,relative_humidity_2m,wind_speed_10m,wind_direction_10m,weather_code,visibility,precipitation,pressure_msl,is_day",
- "hourly": "precipitation_probability,weather_code",
- "daily": "temperature_2m_max,temperature_2m_min",
- "timezone": "auto",
- }
- params.update(units_params(UNITS))
- resp = SESSION.get(base, params=params, timeout=TIMEOUT)
- resp.raise_for_status()
- return resp.json()
-
-
-def fetch_aqi(lat: float, lon: float) -> Optional[Dict[str, Any]]:
- try:
- base = "https://air-quality-api.open-meteo.com/v1/air-quality"
- params = {
- "latitude": lat,
- "longitude": lon,
- "current": "european_aqi",
- "timezone": "auto",
- }
- resp = SESSION.get(base, params=params, timeout=TIMEOUT)
- resp.raise_for_status()
- return resp.json()
- except Exception as e:
- print(f"AQI fetch failed: {e}", file=sys.stderr)
- return None
-
-
-def fetch_place(lat: float, lon: float) -> Optional[str]:
- """Reverse geocode lat/lon to an approximate place. Tries Nominatim first, then Open-Meteo."""
- lang = os.getenv("WEATHER_LANG", "en")
-
- # 1) Nominatim (OpenStreetMap)
- try:
- base = "https://nominatim.openstreetmap.org/reverse"
- params = {
- "lat": lat,
- "lon": lon,
- "format": "jsonv2",
- "accept-language": lang,
- }
- headers = {"User-Agent": UA + " Weather.py/1.0"}
- resp = SESSION.get(base, params=params, headers=headers, timeout=TIMEOUT)
- resp.raise_for_status()
- data = resp.json()
- address = data.get("address", {})
- name = data.get("name") or address.get("city") or address.get("town") or address.get("village") or address.get("hamlet")
- admin1 = address.get("state")
- country = address.get("country")
- parts = [part for part in [name, admin1, country] if part]
- if parts:
- return ", ".join(parts)
- except Exception as e:
- log_debug(f"Reverse geocoding (Nominatim) failed: {e}")
-
- # 2) Open-Meteo reverse (fallback)
- try:
- base = "https://geocoding-api.open-meteo.com/v1/reverse"
- params = {
- "latitude": lat,
- "longitude": lon,
- "language": lang,
- "format": "json",
- }
- resp = SESSION.get(base, params=params, timeout=TIMEOUT)
- resp.raise_for_status()
- data = resp.json()
- results = data.get("results") or []
- if results:
- p = results[0]
- name = p.get("name")
- admin1 = p.get("admin1")
- country = p.get("country")
- parts = [part for part in [name, admin1, country] if part]
- if parts:
- return ", ".join(parts)
- except Exception as e:
- log_debug(f"Reverse geocoding (Open-Meteo) failed: {e}")
-
- return None
-
-
-# =============== Build Output ===============
-
-def safe_get(dct: Dict[str, Any], *keys, default=None):
- cur: Any = dct
- for k in keys:
- if isinstance(cur, dict):
- if k not in cur:
- return default
- cur = cur[k]
- elif isinstance(cur, list):
- try:
- cur = cur[k] # type: ignore[index]
- except Exception:
- return default
- else:
- return default
- return cur
-
-
-def build_hourly_precip(forecast: Dict[str, Any]) -> str:
- try:
- times: List[str] = safe_get(forecast, "hourly", "time", default=[]) or []
- probs: List[Optional[float]] = safe_get(
- forecast, "hourly", "precipitation_probability", default=[]
- ) or []
- cur_time: Optional[str] = safe_get(forecast, "current", "time")
- idx = times.index(cur_time) if cur_time in times else 0
- window = probs[idx : idx + 6]
- if not window:
- return ""
- parts = [f"{int(p)}%" if p is not None else "-" for p in window]
- return " (next 6h) " + " ".join(parts)
- except Exception:
- return ""
-
-
-def build_output(lat: float, lon: float, forecast: Dict[str, Any], aqi: Optional[Dict[str, Any]], place: Optional[str] = None) -> Tuple[Dict[str, Any], str]:
- cur = forecast.get("current", {})
- cur_units = forecast.get("current_units", {})
- daily = forecast.get("daily", {})
- daily_units = forecast.get("daily_units", {})
-
- temp_val = cur.get("temperature_2m")
- temp_unit = cur_units.get("temperature_2m", "")
- temp_str = f"{int(round(temp_val))}{temp_unit}" if isinstance(temp_val, (int, float)) else "N/A"
-
- feels_val = cur.get("apparent_temperature")
- feels_unit = cur_units.get("apparent_temperature", "")
- feels_str = f"Feels like {int(round(feels_val))}{feels_unit}" if isinstance(feels_val, (int, float)) else ""
-
- is_day = int(cur.get("is_day", 1) or 1)
- code = int(cur.get("weather_code", -1) or -1)
-
- unavailable = False
- if code == -1:
- try:
- times: List[str] = safe_get(forecast, "hourly", "time", default=[]) or []
- codes: List[Optional[int]] = safe_get(forecast, "hourly", "weather_code", default=[]) or []
- cur_time: Optional[str] = safe_get(forecast, "current", "time")
-
- idx = 0
- if cur_time and times:
- try:
- ct = datetime.fromisoformat(cur_time)
- diffs = []
- for t in times:
- try:
- diffs.append(abs((datetime.fromisoformat(t) - ct).total_seconds()))
- except Exception:
- diffs.append(float("inf"))
- idx = min(range(len(diffs)), key=lambda i: diffs[i]) if diffs else 0
- except Exception:
- idx = times.index(cur_time) if cur_time in times else 0
-
- hcode = None
- if isinstance(codes, list) and codes:
- if idx < len(codes) and isinstance(codes[idx], (int, float)):
- hcode = int(codes[idx])
- else:
- for c in codes:
- if isinstance(c, (int, float)):
- hcode = int(c)
- break
- if isinstance(hcode, int):
- code = hcode
- log_debug(f"Fallback hourly weather_code used: code={code} idx={idx} cur_time={cur_time}")
- except Exception as e:
- log_debug(f"Hourly code fallback failed: {e}")
-
- if not isinstance(code, int) or code < 0:
- unavailable = True
- log_debug("Weather code invalid; setting status to 'Condition Unavailable'")
-
- if unavailable:
- icon = WEATHER_ICONS["default"]
- status = "Condition Unavailable"
- code_for_class = "unavailable"
- else:
- icon = wmo_to_icon(code, is_day)
- status = wmo_to_status(code)
- code_for_class = f"wmo-{code} {'day' if is_day else 'night'}"
-
- # min/max today (index 0)
- tmin_val = safe_get(daily, "temperature_2m_min", 0)
- tmax_val = safe_get(daily, "temperature_2m_max", 0)
- dtemp_unit = daily_units.get("temperature_2m_min", temp_unit)
- tmin_str = f"{int(round(tmin_val))}{dtemp_unit}" if isinstance(tmin_val, (int, float)) else ""
- tmax_str = f"{int(round(tmax_val))}{dtemp_unit}" if isinstance(tmax_val, (int, float)) else ""
- min_max = f" {tmin_str}\t\t {tmax_str}" if tmin_str and tmax_str else ""
-
- wind_val = cur.get("wind_speed_10m")
- wind_unit = cur_units.get("wind_speed_10m", "")
- wind_text = f" {int(round(wind_val))}{wind_unit}" if isinstance(wind_val, (int, float)) else ""
-
- hum_val = cur.get("relative_humidity_2m")
- humidity_text = f" {int(hum_val)}%" if isinstance(hum_val, (int, float)) else ""
-
- vis_val = cur.get("visibility")
- visibility_text = f" {format_visibility(vis_val)}" if isinstance(vis_val, (int, float)) else ""
-
- aqi_val = safe_get(aqi or {}, "current", "european_aqi")
- aqi_text = f"AQI {int(aqi_val)}" if isinstance(aqi_val, (int, float)) else "AQI N/A"
-
- hourly_precip = build_hourly_precip(forecast)
- prediction = f"\n\n{hourly_precip}" if hourly_precip else ""
-
- # Build place string (priority: MANUAL_PLACE > ENV_PLACE > reverse geocode > lat,lon)
- place_str = (MANUAL_PLACE or ENV_PLACE or place or f"{lat:.3f}, {lon:.3f}")
- location_text = f"{LOC_ICON} {place_str}"
-
- # Build tooltip (markup or plain)
- if TOOLTIP_MARKUP:
- # Escape dynamic text to avoid breaking Pango markup
- tooltip_text = str.format(
- "\t\t{}\t\t\n{}\n{}\n{}\n{}\n\n{}\n{}\n{}{}",
- f'<span size="xx-large">{esc(temp_str)}</span>',
- f"<big> {icon}</big>",
- f"<b>{esc(status)}</b>",
- esc(location_text),
- f"<small>{esc(feels_str)}</small>" if feels_str else "",
- f"<b>{esc(min_max)}</b>" if min_max else "",
- f"{esc(wind_text)}\t{esc(humidity_text)}",
- f"{esc(visibility_text)}\t{esc(aqi_text)}",
- f"<i> {esc(prediction)}</i>" if prediction else "",
- )
- else:
- lines = [
- f"{icon} {temp_str}",
- status,
- location_text,
- ]
- if feels_str:
- lines.append(feels_str)
- if min_max:
- lines.append(min_max)
- lines.append(f"{wind_text} {humidity_text}".strip())
- lines.append(f"{visibility_text} {aqi_text}".strip())
- if prediction:
- lines.append(hourly_precip)
- tooltip_text = "\n".join([ln for ln in lines if ln])
-
- out_data = {
- "text": f"{icon} {temp_str}",
- "alt": status,
- "tooltip": tooltip_text,
- "class": code_for_class,
- }
-
- simple_weather = (
- f"{icon} {status}\n"
- + f" {temp_str} ({feels_str})\n"
- + (f"{wind_text} \n" if wind_text else "")
- + (f"{humidity_text} \n" if humidity_text else "")
- + f"{visibility_text} {aqi_text}\n"
- )
-
- return out_data, simple_weather
-
-
-def main() -> None:
- lat, lon = get_coords()
-
- # Try cache first
- cached = read_api_cache()
- if cached and isinstance(cached, dict):
- forecast = cached.get("forecast")
- aqi = cached.get("aqi")
- cached_place = cached.get("place") if isinstance(cached.get("place"), str) else None
- place_effective = MANUAL_PLACE or ENV_PLACE or cached_place
- try:
- out, simple = build_output(lat, lon, forecast, aqi, place_effective)
- print(json.dumps(out, ensure_ascii=False))
- write_simple_text_cache(simple)
- return
- except Exception as e:
- print(f"Cached data build failed, refetching: {e}", file=sys.stderr)
-
- # Fetch fresh
- try:
- forecast = fetch_open_meteo(lat, lon)
- aqi = fetch_aqi(lat, lon)
- # Use manual/env place if provided; otherwise reverse geocode
- place_effective = MANUAL_PLACE or ENV_PLACE or fetch_place(lat, lon)
- write_api_cache({"forecast": forecast, "aqi": aqi, "place": place_effective})
- out, simple = build_output(lat, lon, forecast, aqi, place_effective)
- print(json.dumps(out, ensure_ascii=False))
- write_simple_text_cache(simple)
- except Exception as e:
- print(f"Open-Meteo fetch failed: {e}", file=sys.stderr)
- # Last resort: try stale cache without TTL
- try:
- if os.path.exists(API_CACHE_PATH):
- with open(API_CACHE_PATH, "r", encoding="utf-8") as f:
- stale = json.load(f)
- out, simple = build_output(lat, lon, stale.get("forecast", {}), stale.get("aqi"), stale.get("place") if isinstance(stale.get("place"), str) else None)
- print(json.dumps(out, ensure_ascii=False))
- write_simple_text_cache(simple)
- return
- except Exception as e2:
- print(f"Failed to use stale cache: {e2}", file=sys.stderr)
- # Fallback minimal output
- fallback = {
- "text": f"{WEATHER_ICONS['default']} N/A",
- "alt": "Unavailable",
- "tooltip": "Weather unavailable",
- "class": "unavailable",
- }
- print(json.dumps(fallback, ensure_ascii=False))
-
-
-if __name__ == "__main__":
- main()
-status = html_data("div[data-testid='wxPhrase']").text()
-status = f"{status[:16]}.." if len(status) > 17 else status
-
-# status code
-status_code = html_data("#regionHeader").attr("class").split(" ")[2].split("-")[2]
-
-# status icon
-icon = (
- weather_icons[status_code]
- if status_code in weather_icons
- else weather_icons["default"]
-)
-
-# temperature feels like
-temp_feel = html_data(
- "div[data-testid='FeelsLikeSection'] > span > span[data-testid='TemperatureValue']"
-).text()
-temp_feel_text = f"Feels like {temp_feel}c"
-
-# min-max temperature
-temp_min = (
- html_data("div[data-testid='wxData'] > span[data-testid='TemperatureValue']")
- .eq(1)
- .text()
-)
-temp_max = (
- html_data("div[data-testid='wxData'] > span[data-testid='TemperatureValue']")
- .eq(0)
- .text()
-)
-temp_min_max = f" {temp_min}\t\t {temp_max}"
-
-# wind speed
-wind_speed = html_data("span[data-testid='Wind']").text().split("\n")[1]
-wind_text = f" {wind_speed}"
-
-# humidity
-humidity = html_data("span[data-testid='PercentageValue']").text()
-humidity_text = f" {humidity}"
-
-# visibility
-visibility = html_data("span[data-testid='VisibilityValue']").text()
-visibility_text = f" {visibility}"
-
-# air quality index
-air_quality_index = html_data("text[data-testid='DonutChartValue']").text()
-
-# hourly rain prediction
-prediction = html_data("section[aria-label='Hourly Forecast']")(
- "div[data-testid='SegmentPrecipPercentage'] > span"
-).text()
-prediction = prediction.replace("Chance of Rain", "")
-prediction = f"\n\n (hourly) {prediction}" if len(prediction) > 0 else prediction
-
-# tooltip text
-tooltip_text = str.format(
- "\t\t{}\t\t\n{}\n{}\n{}\n\n{}\n{}\n{}{}",
- f'<span size="xx-large">{temp}</span>',
- f"<big> {icon}</big>",
- f"<b>{status}</b>",
- f"<small>{temp_feel_text}</small>",
- f"<b>{temp_min_max}</b>",
- f"{wind_text}\t{humidity_text}",
- f"{visibility_text}\tAQI {air_quality_index}",
- f"<i> {prediction}</i>",
-)
-
-# print waybar module data
-out_data = {
- "text": f"{icon} {temp}",
- "alt": status,
- "tooltip": tooltip_text,
- "class": status_code,
-=======
-WMO_STATUS = {
- 0: "Clear sky",
- 1: "Mainly clear",
- 2: "Partly cloudy",
- 3: "Overcast",
- 45: "Fog",
- 48: "Depositing rime fog",
- 51: "Light drizzle",
- 53: "Moderate drizzle",
- 55: "Dense drizzle",
- 56: "Freezing drizzle",
- 57: "Freezing drizzle",
- 61: "Light rain",
- 63: "Moderate rain",
- 65: "Heavy rain",
- 66: "Freezing rain",
- 67: "Freezing rain",
- 71: "Slight snow",
- 73: "Moderate snow",
- 75: "Heavy snow",
- 77: "Snow grains",
- 80: "Rain showers",
- 81: "Rain showers",
- 82: "Violent rain showers",
- 85: "Snow showers",
- 86: "Heavy snow showers",
- 95: "Thunderstorm",
- 96: "Thunderstorm w/ hail",
- 99: "Thunderstorm w/ hail",
->>>>>>> 2a5a7c5 (Weather.py: switch to Open-Meteo; add caching, reverse geocoding, robust geolocation, and config options)
-}
-
-
-def wmo_to_icon(code: int, is_day: int) -> str:
- day = bool(is_day)
- if code == 0:
- return WEATHER_ICONS["sunnyDay" if day else "clearNight"]
- if code in (1, 2, 3, 45, 48):
- return WEATHER_ICONS["cloudyFoggyDay" if day else "cloudyFoggyNight"]
- if code in (51, 53, 55, 61, 63, 65, 80, 81, 82):
- return WEATHER_ICONS["rainyDay" if day else "rainyNight"]
- if code in (56, 57, 66, 67, 71, 73, 75, 77, 85, 86):
- return WEATHER_ICONS["snowyIcyDay" if day else "snowyIcyNight"]
- if code in (95, 96, 99):
- return WEATHER_ICONS["severe"]
- return WEATHER_ICONS["default"]
-
-
-def wmo_to_status(code: int) -> str:
- return WMO_STATUS.get(code, "Unknown")
-
-
-# =============== Utilities ===============
-
-def esc(s: Optional[str]) -> str:
- return html.escape(s, quote=False) if s else ""
-
-def log_debug(msg: str) -> None:
- if DEBUG:
- print(msg, file=sys.stderr)
-
-def ensure_cache_dir() -> None:
- try:
- os.makedirs(CACHE_DIR, exist_ok=True)
- except Exception as e:
- print(f"Error creating cache dir: {e}", file=sys.stderr)
-
-
-def read_api_cache() -> Optional[Dict[str, Any]]:
- try:
- if not os.path.exists(API_CACHE_PATH):
- return None
- with open(API_CACHE_PATH, "r", encoding="utf-8") as f:
- data = json.load(f)
- if (time.time() - data.get("timestamp", 0)) <= CACHE_TTL_SECONDS:
- return data
- return None
- except Exception as e:
- print(f"Error reading cache: {e}", file=sys.stderr)
- return None
-
-
-def write_api_cache(payload: Dict[str, Any]) -> None:
- try:
- ensure_cache_dir()
- payload["timestamp"] = time.time()
- with open(API_CACHE_PATH, "w", encoding="utf-8") as f:
- json.dump(payload, f)
- except Exception as e:
- print(f"Error writing API cache: {e}", file=sys.stderr)
-
-
-def write_simple_text_cache(text: str) -> None:
- try:
- ensure_cache_dir()
- with open(SIMPLE_TEXT_CACHE_PATH, "w", encoding="utf-8") as f:
- f.write(text)
- except Exception as e:
- print(f"Error writing simple cache: {e}", file=sys.stderr)
-
-
-def get_coords() -> Tuple[float, float]:
- # 1) Explicit env
- if ENV_LAT and ENV_LON:
- try:
- return float(ENV_LAT), float(ENV_LON)
- except ValueError:
- print("Invalid WEATHER_LAT/WEATHER_LON; falling back to IP geolocation", file=sys.stderr)
-
- # 2) Try cached coordinates from last successful forecast
- try:
- cached = read_api_cache()
- if cached and isinstance(cached, dict):
- fc = cached.get("forecast") or {}
- lat = fc.get("latitude")
- lon = fc.get("longitude")
- if isinstance(lat, (int, float)) and isinstance(lon, (int, float)):
- return float(lat), float(lon)
- except Exception as e:
- print(f"Reading cached coords failed: {e}", file=sys.stderr)
-
- # 3) IP-based geolocation with multiple providers (prefer ipwho.is, ipapi.co; ipinfo.io as fallback)
- # ipwho.is
- try:
- resp = SESSION.get("https://ipwho.is/", timeout=TIMEOUT)
- resp.raise_for_status()
- data = resp.json()
- if data.get("success"):
- lat = data.get("latitude")
- lon = data.get("longitude")
- if isinstance(lat, (int, float)) and isinstance(lon, (int, float)):
- return float(lat), float(lon)
- except Exception as e:
- print(f"ipwho.is failed: {e}", file=sys.stderr)
-
- # ipapi.co
- try:
- resp = SESSION.get("https://ipapi.co/json", timeout=TIMEOUT)
- resp.raise_for_status()
- data = resp.json()
- lat = data.get("latitude")
- lon = data.get("longitude")
- if isinstance(lat, (int, float)) and isinstance(lon, (int, float)):
- return float(lat), float(lon)
- except Exception as e:
- print(f"ipapi.co failed: {e}", file=sys.stderr)
-
- # ipinfo.io (fallback)
- try:
- resp = SESSION.get("https://ipinfo.io/json", timeout=TIMEOUT)
- resp.raise_for_status()
- data = resp.json()
- loc = data.get("loc")
- if loc and "," in loc:
- lat_s, lon_s = loc.split(",", 1)
- return float(lat_s), float(lon_s)
- except Exception as e:
- print(f"ipinfo.io failed: {e}", file=sys.stderr)
-
- # 4) Last resort
- print("IP geolocation failed: no providers succeeded", file=sys.stderr)
- return 0.0, 0.0
-
-
-def units_params(units: str) -> Dict[str, str]:
- if units == "imperial":
- return {
- "temperature_unit": "fahrenheit",
- "wind_speed_unit": "mph",
- "precipitation_unit": "inch",
- }
- # default metric
- return {
- "temperature_unit": "celsius",
- "wind_speed_unit": "kmh",
- "precipitation_unit": "mm",
- }
-
-
-def format_visibility(meters: Optional[float]) -> str:
- if meters is None:
- return ""
- try:
- if UNITS == "imperial":
- miles = meters / 1609.344
- return f"{miles:.1f} mi"
- else:
- km = meters / 1000.0
- return f"{km:.1f} km"
- except Exception:
- return ""
-
-
-# =============== API Fetching ===============
-
-def fetch_open_meteo(lat: float, lon: float) -> Dict[str, Any]:
- base = "https://api.open-meteo.com/v1/forecast"
- params = {
- "latitude": lat,
- "longitude": lon,
- "current": "temperature_2m,apparent_temperature,relative_humidity_2m,wind_speed_10m,wind_direction_10m,weather_code,visibility,precipitation,pressure_msl,is_day",
"hourly": "precipitation_probability",
"daily": "temperature_2m_max,temperature_2m_min",
"timezone": "auto",
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage