diff --git a/modules/home/programs/hyprland/default.nix b/modules/home/programs/hyprland/default.nix index dedc1b1..b4d4c6c 100644 --- a/modules/home/programs/hyprland/default.nix +++ b/modules/home/programs/hyprland/default.nix @@ -225,7 +225,7 @@ in halign = "center"; valign = "center"; } - #weather + # weather { monitor = cfg.primaryDisplay; text = "cmd[update:30000] waybar-weather --hyprlock"; @@ -236,6 +236,17 @@ in halign = "right"; valign = "bottom"; } + # media + { + monitor = cfg.primaryDisplay; + text = "cmd[update:1000] waybar-media"; + color = "#eceff4"; + font_size = "15"; + font_family = "JetBrainsMono NFM"; + position = "100, 100"; + halign = "left"; + valign = "bottom"; + } ]; # user box shape = [ @@ -267,6 +278,19 @@ in shadow_passes = 2; } ]; + image = [ + { + monitor = cfg.primaryDisplay; + # path = "/tmp/hyprlock-art"; + reload_cmd = "waybar-media-art"; + reload_time = 3; + size = 150; + rounding = 0; + position = "100, 150"; + halign = "left"; + valign = "bottom"; + } + ]; }; }; }; diff --git a/modules/home/programs/waybar/default.nix b/modules/home/programs/waybar/default.nix index b719b62..4522b4c 100755 --- a/modules/home/programs/waybar/default.nix +++ b/modules/home/programs/waybar/default.nix @@ -328,6 +328,7 @@ in ./options.nix ./scripts/audio-control.nix ./scripts/hass.nix + ./scripts/media.nix ./scripts/notifications.nix ./scripts/weather.nix ]; diff --git a/modules/home/programs/waybar/scripts/media.nix b/modules/home/programs/waybar/scripts/media.nix index e021286..93e54f8 100644 --- a/modules/home/programs/waybar/scripts/media.nix +++ b/modules/home/programs/waybar/scripts/media.nix @@ -21,7 +21,7 @@ let if [[ -n "$artist" && -n "$title" ]]; then echo "♪ $artist - $title" elif [[ -n "$title" ]]; then - echo "♪ $title" + echo "♪ ''\${title//&/&}" else echo "♪ Music Playing" fi @@ -43,15 +43,9 @@ let art=$(playerctl metadata mpris:artUrl 2>/dev/null) if [[ -n "$art" ]]; then - echo "♪ $artist - $title" - else - echo "♪ Music Playing" + echo ''\${art#file://} fi - else - echo "" fi - else - echo "" fi ''; in diff --git a/modules/home/programs/waybar/scripts/weather.nix b/modules/home/programs/waybar/scripts/weather.nix index c205358..c2c426f 100644 --- a/modules/home/programs/waybar/scripts/weather.nix +++ b/modules/home/programs/waybar/scripts/weather.nix @@ -17,6 +17,7 @@ let import shutil from datetime import datetime, timedelta import argparse + import math import requests @@ -25,6 +26,8 @@ let parser.add_argument('--hyprlock', action='store_true') args = parser.parse_args() + # --- MAPPINGS --- + WWO_CODE = { "113": "Sunny", "116": "PartlyCloudy", @@ -76,6 +79,38 @@ let "395": "HeavySnowShowers", } + # Maps WMO codes (OpenMeteo) to WWO codes (wttr.in) + WMO_TO_WWO = { + 0: "113", # Clear sky -> Sunny + 1: "113", # Mainly clear -> Sunny + 2: "116", # Partly cloudy + 3: "122", # Overcast -> VeryCloudy + 45: "143", # Fog + 48: "248", # Depositing rime fog + 51: "266", # Drizzle: Light + 53: "266", # Drizzle: Moderate (mapped to LightRain) + 55: "296", # Drizzle: Dense intensity (LightRain usually suits better than heavy) + 56: "281", # Freezing Drizzle: Light + 57: "284", # Freezing Drizzle: Dense + 61: "296", # Rain: Slight + 63: "302", # Rain: Moderate + 65: "308", # Rain: Heavy + 66: "311", # Freezing Rain: Light + 67: "314", # Freezing Rain: Heavy + 71: "326", # Snow fall: Slight + 73: "332", # Snow fall: Moderate + 75: "338", # Snow fall: Heavy + 77: "350", # Snow grains + 80: "353", # Rain showers: Slight + 81: "356", # Rain showers: Moderate + 82: "359", # Rain showers: Violent + 85: "368", # Snow showers: Slight + 86: "371", # Snow showers: Heavy + 95: "386", # Thunderstorm: Slight or moderate + 96: "389", # Thunderstorm with slight hail + 99: "395", # Thunderstorm with heavy hail + } + WEATHER_SYMBOL = { "Unknown": "", "Cloudy": "", @@ -115,10 +150,6 @@ let "SSE": "↘", } - MOON_PHASES = ( - "󰽤", "󰽧", "󰽡", "󰽨", "󰽢", "󰽦", "󰽣", "󰽥" - ) - WEATHER_SYMBOL_WI_DAY = { "Unknown": "", "Cloudy": "", @@ -297,21 +328,15 @@ let data["text"] = "" def format_time(time): - """get the time formatted""" return datetime.strptime(format_24_time(time), "%H").strftime("%I %p") def format_24_time(time): - """get the time formatted""" return time.replace("00", "").zfill(2) - def format_temp(temp): - """get the temp formatted""" - return (temp + "°").ljust(3) - + return (str(temp) + "°").ljust(3) def format_chances(hour): - """get the chances formatted""" chances = { "chanceoffog": "Fog", "chanceoffrost": "Frost", @@ -322,62 +347,67 @@ let "chanceofthunder": "Thunder", "chanceofwindy": "Wind", } - conditions = [] for chance, event in chances.items(): - if int(hour[chance]) > 0: - conditions.append(event + " " + hour[chance] + "%") + if int(hour.get(chance, 0)) > 0: + conditions.append(event + " " + str(hour[chance]) + "%") return ", ".join(conditions) + def deg_to_compass(num): + val = int((num / 22.5) + 0.5) + arr = ["N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"] + return arr[(val % 16)] + def build_text(current_condition): - """build the text string""" feels_like_f = current_condition["FeelsLikeF"] weather_code = current_condition["weatherCode"] + # Check if we have a mapped format; if not, fallback to Unknown + if weather_code not in WEATHER_CODES: + weather_code = "113" # Fallback to sunny/default to prevent crash - tempint = int(feels_like_f) + tempint = int(float(feels_like_f)) # float cast just in case extrachar = "" if 0 < tempint < 10: extrachar = "+" - current_weather = f"{WEATHER_CODES[weather_code]} {extrachar} {feels_like_f}°F" - + current_weather = f"{WEATHER_CODES[weather_code]} {extrachar} {int(feels_like_f)}°F" return current_weather def build_tooltip(current_condition, astronomy, moon_icon): - """build the tooltip text""" weather_description = current_condition['weatherDesc'][0]['value'] feels_like_f = current_condition["FeelsLikeF"] temp_f = current_condition['temp_F'] humidity = current_condition['humidity'] wind_speed = current_condition['windspeedMiles'] wind_dir = current_condition['winddir16Point'] - moon_phase = astronomy['moon_phase'] - wego = WEATHER_CODES_WEGO[current_condition['weatherCode']] + moon_phase = astronomy.get('moon_phase', 'Unknown') + + weather_code = current_condition['weatherCode'] + if weather_code not in WEATHER_CODES_WEGO: + weather_code = "113" + + wego = WEATHER_CODES_WEGO[weather_code] current = f"{wego[0]}{weather_description} {temp_f}°\n" feels = f"{wego[1]}Feels like: {feels_like_f}°\n" - wind = f"{wego[2]}Wind: {wind_speed}mph {WIND_DIRECTION[wind_dir]}\n" + wind = f"{wego[2]}Wind: {wind_speed}mph {WIND_DIRECTION.get(wind_dir, ''\'')}\n" # Safe get for direction humidityl = f"{wego[3]}Humidity: {humidity}%\n" moon = f"{wego[4]}Moon phase: {moon_phase} " + moon_icon + "\n" tooltip = current + feels + wind + humidityl + moon - return tooltip def build_forecast(weather): - """build a 3 day forecast""" tooltip = "\n" - for i, day in enumerate(weather): - # determine day if i == 0: tooltip += "Today, " if i == 1: tooltip += "Tomorrow, " - # format the date + date = datetime.strptime(day['date'], "%Y-%m-%d").strftime("%a %b %d %Y") tooltip += f"{date}\n" - # set the high and low + max_temp = day['maxtempF'] min_temp = day['mintempF'] tooltip += f" {max_temp}°F  {min_temp}°F" @@ -389,29 +419,34 @@ let tooltip += build_hourly_forecast(i, day['hourly'], sunrise, sunset) return tooltip - def build_hourly_forecast(day_num, hourly, sunrise, sunset): - """build an hourly forecast""" - sunrise_hour = datetime.strptime(sunrise, "%I:%M %p").hour - sunset_hour = datetime.strptime(sunset, "%I:%M %p").hour + try: + sunrise_hour = datetime.strptime(sunrise, "%I:%M %p").hour + sunset_hour = datetime.strptime(sunset, "%I:%M %p").hour + except ValueError: + # Fallback if time format is different (OpenMeteo might send 24h) + sunrise_hour = int(sunrise.split(':')[0]) + sunset_hour = int(sunset.split(':')[0]) + current_hour = datetime.now().hour tooltip = "" for hour in hourly: time_24_hr = int(format_24_time(hour["time"])) - if day_num == 0: if time_24_hr < current_hour - 2: continue - # determine which code to use if is_night_hour(time_24_hr, sunrise_hour, sunset_hour): codes = WEATHER_CODES_WI_NIGHT else: codes = WEATHER_CODES_WI_DAY current_time = format_time(hour['time']) - current_weather_code = codes[hour['weatherCode']] + wcode = hour['weatherCode'] + if wcode not in codes: wcode = "113" # Fallback + + current_weather_code = codes[wcode] feels_like = format_temp(hour['FeelsLikeF']) weather_desc = hour['weatherDesc'][0]['value'] current_chances = format_chances(hour) @@ -422,69 +457,160 @@ let return tooltip def is_night_hour(time_24_hr, sunrise_hour, sunset_hour): - """returns true if the hour is night""" before_sunrise = time_24_hr < sunrise_hour after_sunset = time_24_hr > sunset_hour return after_sunset or before_sunrise def load_cache(path, ttl): - """Load cached file if it is not too old.""" try: if not os.path.exists(path): return None - mtime = datetime.fromtimestamp(os.path.getmtime(path)) if datetime.now() - mtime > ttl: return None - with open(path, "r") as f: if path.endswith(".json"): return json.load(f) return f.read().strip() - except Exception: return None - def save_cache(path, data): - """Write cache file safely.""" os.makedirs(os.path.dirname(path), exist_ok=True) tmp = path + ".tmp" - try: with open(tmp, "w") as f: if isinstance(data, dict): json.dump(data, f) else: f.write(str(data)) - shutil.move(tmp, path) except Exception: pass + # --- OPEN-METEO INTEGRATION HELPER FUNCTIONS --- + + def get_lat_lon(): + """Attempt to get location via IP if using OpenMeteo""" + try: + resp = requests.get("http://ip-api.com/json/", timeout=5).json() + return resp.get('lat'), resp.get('lon') + except: + # Default to a generic location if IP fetch fails (NYC) + return 40.71, -74.00 + + def fetch_open_meteo(): + """Fetch and Transform OpenMeteo data to match Wttr.in JSON structure""" + lat, lon = get_lat_lon() + url = "https://api.open-meteo.com/v1/forecast" + params = { + "latitude": lat, + "longitude": lon, + "current": "temperature_2m,apparent_temperature,precipitation,weather_code,wind_speed_10m,wind_direction_10m,relative_humidity_2m", + "daily": "weather_code,temperature_2m_max,temperature_2m_min,sunrise,sunset,precipitation_probability_max", + "hourly": "temperature_2m,apparent_temperature,precipitation_probability,weather_code", + "temperature_unit": "fahrenheit", + "wind_speed_unit": "mph", + "precipitation_unit": "inch", + "timezone": "auto" + } + + response = requests.get(url, params=params, timeout=10) + om_data = response.json() + + # Transform Current Condition + current = om_data["current"] + wmo_code = current["weather_code"] + wwo_code = WMO_TO_WWO.get(wmo_code, "113") + + wttr_current = { + "temp_F": str(int(current["temperature_2m"])), + "FeelsLikeF": str(int(current["apparent_temperature"])), + "weatherCode": wwo_code, + "weatherDesc": [{"value": WWO_CODE.get(wwo_code, "Unknown")}], + "humidity": str(current["relative_humidity_2m"]), + "windspeedMiles": str(int(current["wind_speed_10m"])), + "winddir16Point": deg_to_compass(current["wind_direction_10m"]), + } + + # Transform Daily Forecast (OpenMeteo gives 7 days, we need 3) + wttr_weather = [] + daily = om_data["daily"] + hourly = om_data["hourly"] + + for i in range(3): + date_str = daily["time"][i] + + # Build Hourly for this day (wttr uses 3-hour intervals: 0, 300, 600...) + # OpenMeteo gives 0, 1, 2... + wttr_hourly = [] + for h in range(0, 24, 3): # Step by 3 hours to mimic wttr + idx = (i * 24) + h + h_code = hourly["weather_code"][idx] + h_wwo = WMO_TO_WWO.get(h_code, "113") + + wttr_hourly.append({ + "time": str(h * 100), # 0, 300, 600 + "weatherCode": h_wwo, + "weatherDesc": [{"value": WWO_CODE.get(h_wwo, "Unknown")}], + "FeelsLikeF": str(int(hourly["apparent_temperature"][idx])), + "chanceofrain": str(hourly["precipitation_probability"][idx]), + # Fill other chances with 0 as API doesn't provide them easily + "chanceoffog": "0", "chanceofsnow": "0", "chanceofthunder": "0", + "chanceofwindy": "0", "chanceofsunshine": "0" + }) + + # Astronomy + sunrise = datetime.fromisoformat(daily["sunrise"][i]).strftime("%I:%M %p") + sunset = datetime.fromisoformat(daily["sunset"][i]).strftime("%I:%M %p") + + wttr_weather.append({ + "date": date_str, + "maxtempF": str(int(daily["temperature_2m_max"][i])), + "mintempF": str(int(daily["temperature_2m_min"][i])), + "astronomy": [{"sunrise": sunrise, "sunset": sunset, "moon_phase": "Unknown"}], + "hourly": wttr_hourly + }) + + return { + "current_condition": [wttr_current], + "weather": wttr_weather + } def get_wttr_json(hyprlock=False): - """get the weather json""" - # Try loading cached JSON cached = load_cache(CACHE_FILE, CACHE_TTL) cached_moon = load_cache(CACHE_MOON_FILE, CACHE_TTL) cached_moon_icon = load_cache(CACHE_MOON_ICON_FILE, CACHE_TTL) if cached and cached_moon and cached_moon_icon: - current_condition = cached + weather = cached + current_condition = weather["current_condition"][0] astronomy = cached_moon moon_icon = cached_moon_icon else: - weather = requests.get("https://wttr.in/?u&format=j1", timeout=30).json() - moon = requests.get("https://wttr.in/?format=%m", timeout=30) - moon_icon = moon.text + try: + # Primary Source: wttr.in + weather = requests.get("https://wttr.in/?u&format=j1", timeout=5).json() + moon = requests.get("https://wttr.in/?format=%m", timeout=5) + moon_icon = moon.text + + current_condition = weather["current_condition"][0] + astronomy = weather["weather"][0]['astronomy'][0] + except Exception: + # Fallback Source: Open-Meteo + try: + print("open_mateo fallback") + weather = fetch_open_meteo() + current_condition = weather["current_condition"][0] + astronomy = weather["weather"][0]['astronomy'][0] + moon_icon = "" # Generic moon icon for fallback + except Exception as e: + # If both fail + raise e - current_condition = weather["current_condition"][0] - astronomy = weather["weather"][0]['astronomy'][0] - - # Save cache - save_cache(CACHE_FILE, current_condition) + # Save cache (works for both sources since we transformed OM data) + save_cache(CACHE_FILE, weather) save_cache(CACHE_MOON_FILE, astronomy) save_cache(CACHE_MOON_ICON_FILE, moon_icon) @@ -492,30 +618,27 @@ let return build_tooltip(current_condition, astronomy, moon_icon) else: text = build_text(current_condition) - tooltip = build_tooltip(current_condition, astronomy, moon_icon) + build_forecast(weather["weather"]) - data["text"] = text data["tooltip"] = tooltip - return json.dumps(data) def main(): - """main""" if args.hyprlock: try: print(get_wttr_json(hyprlock=True)) except Exception as e: print("error") - print(e) + # print(e) # Uncomment for debug else: try: print(get_wttr_json()) except Exception as e: - print("error") - print(e) + print(json.dumps({"text": "Err", "tooltip": str(e)})) + + if __name__ == "__main__": + main() - main() ''; in {