From 0559c830eff21c7e99f366a9c57d0377701d4776 Mon Sep 17 00:00:00 2001 From: Vincent Bernat Date: Tue, 10 Aug 2021 13:08:15 +0200 Subject: [PATCH] polybar: display current weather --- bin/polybar-weather | 168 +++++++++++++++++++++++++++++ dotfiles/polybar.conf | 10 +- dotfiles/systemd/i3-session.target | 2 + dotfiles/systemd/weather.service | 11 ++ dotfiles/systemd/weather.timer | 7 ++ 5 files changed, 197 insertions(+), 1 deletion(-) create mode 100755 bin/polybar-weather create mode 100644 dotfiles/systemd/weather.service create mode 100644 dotfiles/systemd/weather.timer diff --git a/bin/polybar-weather b/bin/polybar-weather new file mode 100755 index 0000000..0e5bc95 --- /dev/null +++ b/bin/polybar-weather @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 + +"""Get current weather condition and forecast for Polybar.""" + +import requests +import logging +import argparse +import sys +import os +import subprocess +from systemd import journal + +logger = logging.getLogger("polybar-weather") + + +def get_location(): + """Return current location as latitude/longitude tuple.""" + logger.debug("query ip-api.com for location") + r = requests.get("http://ip-api.com/json") + r.raise_for_status() + data = r.json() + logger.debug("current location data: %s", data) + logger.info(f'current location is {data["city"]}, {data["country"]}') + return (data["lat"], data["lon"]) + + +def get_weather(apikey, latitude, longitude, endpoint): + """Return current weather data from openweathermap.""" + logger.debug("query openweathermap for %s, %s", latitude, longitude) + r = requests.get( + f"https://api.openweathermap.org/data/2.5/{endpoint}", + params={ + "appid": apikey, + "lat": latitude, + "lon": longitude, + "units": "metric", + "cnt": 3, + }, + ) + r.raise_for_status() + data = r.json() + logger.debug("%s data: %s", endpoint, data) + return data + + +def format_weather(data): + """Translate OWM icon to WeatherIcons.""" + icon = data["weather"][0]["icon"] + temperature = data["main"]["temp"] + icon = { + "01d": "\uf00d", # Clear sky - day + "01n": "\uf02e", # Clear sky - night + "02d": "\uf002", # Few clouds (11-25%) - day + "02n": "\uf086", # Few clouds (11-25%) - night + "03d": "\uf041", # Scattered clouds (25-50%) - day/night + "03n": "\uf041", # Scattered clouds (25-50%) - day/night + "04d": "\uf013", # Broken / Overcast clouds (51-84% / 85-100%) - day/night + "04n": "\uf013", # Broken / Overcast clouds (51-84% / 85-100%) - day/night + "09d": "\uf018", # Shower rain - day + "09n": "\uf018", # Shower rain - night + "10d": "\uf008", # Moderate / heavy rain - day + "10n": "\uf036", # Moderate / heavy rain - night + "11d": "\uf005", # Thunderstorm - day + "11n": "\uf025", # Thunderstorm - night + "13d": "\uf00a", # Snow - day + "13n": "\uf02a", # Snow - night + "50d": "\uf003", # Fog - day + "50n": "\uf04a", # Fog - night + }.get(icon, "\uf075") + return "".join( + [ + "%{Tx}", + icon, + "%{T-} ", + str(int(round(temperature))), + "°C", + ] + ) + + +def update_status(status, output): + """Update current status.""" + # Write it to file + with open(output, "w") as f: + f.write(status) + + # Send it to polybar + subprocess.run(["polybar-msg", "action", f"#weather.send.{status}"], check=True) + + +if __name__ == "__main__": + # Parse + description = sys.modules[__name__].__doc__ + parser = argparse.ArgumentParser(description=description) + parser.add_argument( + "--debug", + "-d", + action="store_true", + default=False, + help="enable debugging", + ) + parser.add_argument( + "--owm-api-key", + default=os.environ.get("OWM_API_KEY"), + help="OpenWeatherMap API key", + ) + parser.add_argument("--font", default=4, type=int, help="Weather Icons 1-index") + parser.add_argument( + "--output", + default=f"{os.environ['XDG_RUNTIME_DIR']}/i3/weather.txt", + help="Output destination", + ) + parser.add_argument( + "--timeout", + default=30, + type=int, + help="Wait up to TIMEOUT minutes to be online", + ) + options = parser.parse_args() + + # Logging + root = logging.getLogger("") + root.setLevel(logging.WARNING) + logger.setLevel(options.debug and logging.DEBUG or logging.INFO) + if sys.stderr.isatty(): + ch = logging.StreamHandler() + ch.setFormatter(logging.Formatter("%(levelname)s: %(message)s")) + root.addHandler(ch) + else: + root.addHandler(journal.JournalHandler(SYSLOG_IDENTIFIER=logger.name)) + + try: + # Wait to be online + logger.debug("check if we are online") + if subprocess.run(["nm-online", "-s", "-q", "-t", "5"]).returncode != 0: + logger.info("not online, waiting") + update_status("", options.output) + if ( + subprocess.run( + ["nm-online", "-s", "-q", "-t", str(options.timeout * 60)] + ).returncode + != 0 + ): + logger.warning("not online, exiting") + sys.exit(1) + + # Grab information. We only use forecast weather, otherwise, + # we may get something "late". + location = get_location() + forecast_weather = get_weather(options.owm_api_key, *location, "forecast") + description = forecast_weather["list"][0]["weather"][0]["description"] + logger.info(f"current weather: {description}") + + # Format output + output = "{} {} {}".format( + format_weather(forecast_weather["list"][0]), + format_weather(forecast_weather["list"][1]), + format_weather(forecast_weather["list"][2]), + ) + output = output.replace("%{Tx}", "%%{T%d}" % options.font) + logger.debug("output: %s", output) + + update_status(output, options.output) + + except Exception as e: + logger.exception("%s", e) + sys.exit(1) + sys.exit(0) diff --git a/dotfiles/polybar.conf b/dotfiles/polybar.conf index 6f1583b..a7e7254 100644 --- a/dotfiles/polybar.conf +++ b/dotfiles/polybar.conf @@ -29,9 +29,10 @@ foreground = ${colors.foreground} font-0 = Iosevka:style=Regular:size=10;2 font-1 = Font Awesome 6 Pro:style=Solid:size=10;2 font-2 = Font Awesome 6 Brands:style=Regular:size=10;2 +font-3 = Weather Icons:style=Regular:size=10;2 modules-left = i3 -modules-center = date +modules-center = date weather [bar/alone] inherit = bar/common @@ -43,6 +44,7 @@ modules-right = cpu memory brightness battery bluetooth network disk dunst pulse [bar/secondary] inherit = bar/common +modules-center = date modules-right = pulseaudio [module/i3] @@ -106,6 +108,12 @@ type = custom/ipc hook-0 = cat $XDG_RUNTIME_DIR/i3/bluetooth.txt 2> /dev/null initial = 1 +[module/weather] +type = custom/ipc +hook-0 = cat $XDG_RUNTIME_DIR/i3/weather.txt 2> /dev/null +initial = 1 +click-left = xdg-open https://www.meteoblue.com/en/weather/week + [module/dunst] type = custom/ipc hook-0 = cat $XDG_RUNTIME_DIR/i3/dunst.txt 2> /dev/null diff --git a/dotfiles/systemd/i3-session.target b/dotfiles/systemd/i3-session.target index 4448688..d2724ba 100644 --- a/dotfiles/systemd/i3-session.target +++ b/dotfiles/systemd/i3-session.target @@ -3,6 +3,8 @@ Description=i3 session BindsTo=graphical-session.target Wants=wallpaper.service Wants=wallpaper.timer +Wants=weather.service +Wants=weather.timer Wants=polybar.service Wants=i3-companion.service Wants=misc-x.service diff --git a/dotfiles/systemd/weather.service b/dotfiles/systemd/weather.service new file mode 100644 index 0000000..ae775c8 --- /dev/null +++ b/dotfiles/systemd/weather.service @@ -0,0 +1,11 @@ +[Unit] +Description=Build and display wallpaper +PartOf=graphical-session.target +After=polybar.service + +[Service] +Environment=OWM_API_KEY=81687c78e0376836871a6cb9fc347249 +ExecStart=/usr/bin/mkdir -p %h/.cache/i3 +ExecStart=%h/.config/i3/bin/polybar-weather +Type=oneshot +RemainAfterExit=false diff --git a/dotfiles/systemd/weather.timer b/dotfiles/systemd/weather.timer new file mode 100644 index 0000000..1721c51 --- /dev/null +++ b/dotfiles/systemd/weather.timer @@ -0,0 +1,7 @@ +[Unit] +Description=Weather update +PartOf=graphical-session.target + +[Timer] +OnUnitActiveSec=1h +OnClockChange=true