mirror of
https://github.com/vincentbernat/i3wm-configuration.git
synced 2025-06-20 17:15:41 +02:00
198 lines
6.6 KiB
Python
Executable file
198 lines
6.6 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
|
|
"""Get current weather condition and forecast for Polybar."""
|
|
|
|
import requests
|
|
import logging
|
|
import argparse
|
|
import sys
|
|
import os
|
|
import subprocess
|
|
import time
|
|
from systemd import journal
|
|
|
|
logger = logging.getLogger("weather")
|
|
|
|
|
|
def get_location():
|
|
"""Return current location as latitude/longitude tuple."""
|
|
logger.debug("query MaxMind for location")
|
|
r = requests.get(
|
|
"https://geoip.maxmind.com/geoip/v2.1/city/me",
|
|
headers={"referer": "https://www.maxmind.com/en/locate-my-ip-address"},
|
|
timeout=10,
|
|
)
|
|
r.raise_for_status()
|
|
data = r.json()
|
|
logger.debug("current location data: %s", data)
|
|
location = (data.get("city") or data["country"])["names"]["en"]
|
|
logger.info(f"current location: {location}")
|
|
return ((data["location"]["latitude"], data["location"]["longitude"]), location)
|
|
|
|
|
|
def get_weather(latitude, longitude):
|
|
"""Return data from met.no."""
|
|
logger.debug("query met.no for %s, %s", latitude, longitude)
|
|
r = requests.get(
|
|
"https://api.met.no/weatherapi/locationforecast/2.0/complete.json",
|
|
params={
|
|
"lat": f"{latitude:.4f}",
|
|
"lon": f"{longitude:.4f}",
|
|
},
|
|
headers={
|
|
"user-agent": "WeatherWidget https://github.com/vincentbernat/i3wm-configuration"
|
|
},
|
|
timeout=10,
|
|
)
|
|
r.raise_for_status()
|
|
data = r.json()
|
|
logger.debug("weather data: %s", data)
|
|
return data
|
|
|
|
|
|
def format_icon(symbol_code):
|
|
"""Translate met.no icon to Font Awesome."""
|
|
# See https://github.com/metno/weathericons/blob/main/weather/legend.csv
|
|
symbol_code = symbol_code.removeprefix("light")
|
|
symbol_code = symbol_code.removeprefix("heavy")
|
|
if symbol_code.startswith("ss"):
|
|
symbol_code = symbol_code[1:]
|
|
icon = {
|
|
"clearsky_day": "\uf00d",
|
|
"clearsky_night": "\uf02e",
|
|
"cloudy": "\uf013",
|
|
"fair_day": "\uf002",
|
|
"fair_night": "\uf086",
|
|
"fog": "\uf014",
|
|
"partlycloudy_day": "\uf002",
|
|
"partlycloudy_night": "\uf086",
|
|
"rain": "\uf019",
|
|
"rainandthunder": "\uf01e",
|
|
"rainshowers_day": "\uf009",
|
|
"rainshowers_night": "\uf037",
|
|
"rainshowersandthunder_day": "\uf010",
|
|
"rainshowersandthunder_night": "\uf03b",
|
|
"sleet": "\uf0b5",
|
|
"sleetandthunder": "\uf01d",
|
|
"sleetshowers_day": "\uf0b2",
|
|
"sleetshowers_night": "\uf0b3",
|
|
"sleetshowersandthunder_day": "\uf068",
|
|
"sleetshowersandthunder_night": "\uf069",
|
|
"snow": "\uf01b",
|
|
"snowandthunder": "\uf06b",
|
|
"snowshowers_day": "\uf009",
|
|
"snowshowers_night": "\uf038",
|
|
"snowshowersandthunder_day": "\uf06b",
|
|
"snowshowersandthunder_night": "\uf06c",
|
|
}.get(symbol_code, "?")
|
|
logger.debug("symbol %s translated to %s (\\u%04x)", symbol_code, icon, ord(icon))
|
|
output = ["%{Tx}", icon, "%{T-}"]
|
|
return "".join(output)
|
|
|
|
|
|
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}"])
|
|
|
|
|
|
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(
|
|
"--font-index", default=4, type=int, help="Polybar font 1-index"
|
|
)
|
|
parser.add_argument(
|
|
"--output",
|
|
default=f"{os.environ['XDG_RUNTIME_DIR']}/i3/weather.txt",
|
|
help="Output destination",
|
|
)
|
|
parser.add_argument(
|
|
"--online-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:
|
|
# Get location
|
|
while True:
|
|
try:
|
|
location, city = get_location()
|
|
break
|
|
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout):
|
|
# Wait to be online
|
|
logger.info("not online, waiting")
|
|
update_status("", options.output)
|
|
time.sleep(5)
|
|
process = subprocess.run(
|
|
["nm-online", "-q", "-t", str(options.online_timeout * 60)]
|
|
)
|
|
if process.returncode != 0:
|
|
logger.warning("not online, exiting")
|
|
sys.exit(1)
|
|
|
|
# Grab current weather and daily forecast
|
|
weather = get_weather(*location)
|
|
weather = weather["properties"]["timeseries"]
|
|
|
|
# Compute min/max temperatures for the forecast. We use the next 24
|
|
# hours. So we use 18 entries.
|
|
mintemp = min(
|
|
d["data"]["next_6_hours"]["details"]["air_temperature_min"]
|
|
for d in weather[:18]
|
|
)
|
|
maxtemp = max(
|
|
d["data"]["next_6_hours"]["details"]["air_temperature_max"]
|
|
for d in weather[:18]
|
|
)
|
|
|
|
# Format output
|
|
conditions = [
|
|
# Current conditions: use the symbol for the next hour and the
|
|
# instant temperature
|
|
format_icon(weather[0]["data"]["next_1_hours"]["summary"]["symbol_code"]),
|
|
"{}°C".format(
|
|
round(weather[0]["data"]["instant"]["details"]["air_temperature"])
|
|
),
|
|
# Forecast: use the symbol for the next 6 hours and the period after
|
|
format_icon(weather[0]["data"]["next_6_hours"]["summary"]["symbol_code"])
|
|
+ format_icon(weather[6]["data"]["next_6_hours"]["summary"]["symbol_code"]),
|
|
# And the temperature range computed for the next 24 hours
|
|
"{}—{}°C".format(round(mintemp), round(maxtemp)),
|
|
]
|
|
city = city.replace("%", "%%")
|
|
conditions.insert(0, f"%{{F#bbb}}{city}%{{F-}}")
|
|
output = " ".join(conditions).replace("%{Tx}", "%%{T%d}" % options.font_index)
|
|
logger.debug("output: %s", output)
|
|
|
|
update_status(output, options.output)
|
|
|
|
except Exception as e:
|
|
logger.exception("%s", e)
|
|
sys.exit(1)
|
|
sys.exit(0)
|