#!/usr/bin/env python3
import re
import os
import sys
import subprocess
import json
import urllib.request
import datetime
import math
import random
def decode_icon(code, is_day):
icons = {
"sun": " ",
"moon": " ",
"cloud": "",
"cloud-bolt": "",
"snowflake": " ",
"wind": "",
"tornado": "",
"temperature-low": "",
"temperature-high": "",
"smog": "",
"cloud-sun-rain": "",
"cloud-sun": "",
"cloud-showers-water": "",
"cloud-showers-heavy": "",
"cloud-rain": "",
"cloud-moon-rain": "",
"cloud-moon": " ",
"default": " "
}
match code:
case 200:
icon_status = "cloud-bolt"
case 201:
icon_status = "cloud-bolt"
case 202:
icon_status = "cloud-bolt"
case 210:
icon_status = "cloud-bolt"
case 211:
icon_status = "cloud-bolt"
case 212:
icon_status = "cloud-bolt"
case 221:
icon_status = "cloud-bolt"
case 230:
icon_status = "cloud-bolt"
case 231:
icon_status = "cloud-bolt"
case 232:
icon_status = "cloud-bolt"
case 500:
icon_status = "cloud-sun-rain" if is_day else "cloud-moon-rain"
case 501:
icon_status = "cloud-sun-rain" if is_day else "cloud-moon-rain"
case 502:
icon_status = "cloud-sun-rain" if is_day else "cloud-moon-rain"
case 503:
icon_status = "cloud-sun-rain" if is_day else "cloud-moon-rain"
case 504:
icon_status = "cloud-sun-rain" if is_day else "cloud-moon-rain"
case 511:
icon_status = "snowflake"
case 520:
icon_status = "cloud-rain"
case 521:
icon_status = "cloud-rain"
case 522:
icon_status = "cloud-showers-heavy"
case 531:
icon_status = "cloud-rain"
case 600:
icon_status = "snowflake"
case 601:
icon_status = "snowflake"
case 602:
icon_status = "snowflake"
case 611:
icon_status = "snowflake"
case 612:
icon_status = "snowflake"
case 613:
icon_status = "snowflake"
case 615:
icon_status = "snowflake"
case 616:
icon_status = "snowflake"
case 620:
icon_status = "snowflake"
case 621:
icon_status = "snowflake"
case 622:
icon_status = "snowflake"
case 800:
icon_status = "sun" if is_day else "moon"
case 801:
icon_status = "cloud-sun" if is_day else "cloud-moon"
case 802:
icon_status = "cloud"
case 803:
icon_status = "cloud"
case 804:
icon_status = "cloud"
case 10001:
icon_status = "temperature-low"
case 10002:
icon_status = "temperature-high"
case _:
icon_status = "default"
icon = (
icons[icon_status]
if icon_status in icons
else icons["default"]
)
return icon
def is_daytime(dt, resp):
events = []
for day in range(8):
events.append(resp['daily'][day]['sunrise'])
events.append(resp['daily'][day]['sunset'])
events.append(dt)
events.sort()
return events.index(dt) % 2 == 1
def format_temp(temp):
rounded_temp = round(temp)
return f"{rounded_temp: >3}°"
def render_minutely_precip_chart(resp):
chart = ""
icons = [" ", "▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"]
for minute in range(60):
precip = resp['minutely'][minute]['precipitation']
precip = math.ceil(precip)
precip = 8 if precip > 8 else precip
# precip = random.randint(0, 8)
chart = chart + icons[precip]
return chart
def format_precip_chart_string(chart, resp):
total_precip = 0
for minute in range(60):
total_precip = total_precip + resp['minutely'][minute]['precipitation']
# total_precip = 1
if total_precip == 0:
return ""
else:
first_minute = resp['minutely'][minute]['dt']
first_minute = datetime.datetime.fromtimestamp(first_minute).minute
seq = ["15", "30", "45", " 0"]
first_target = seq[int(first_minute / 15)]
init_spaces = int(first_target) - first_minute
timelabel = " "
for i in range(init_spaces):
timelabel = timelabel + " "
for i in range(4):
timelabel = timelabel + seq[(int(first_minute / 15) + i) % 4]
if i < 3:
timelabel = timelabel + " "
chart_string = f" {chart} \n"
chart_string = chart_string + f"{timelabel}\n\n"
return chart_string
def get_hourly_hours(hours, resp):
hour_string = ""
for hour in range(hours):
dt = resp['hourly'][hour]['dt']
dt = datetime.datetime.fromtimestamp(dt).hour
hour_string = hour_string + " " + f"{dt:2}" + " "
return hour_string
def get_hourly_icons(hours, resp):
icon_string = ""
for hour in range(hours):
status_code = resp['hourly'][hour]['weather'][0]['id']
dt = resp['hourly'][hour]['dt']
hour_is_daytime = is_daytime(dt, resp)
icon_string = icon_string + " " + decode_icon(status_code, hour_is_daytime) + " "
return icon_string
def get_hourly_temps(hours, resp):
temp_string = ""
for hour in range(hours):
temp_string = temp_string + format_temp(resp['hourly'][hour]['temp'])
return temp_string
def compute_daily_minmax(days, resp):
dmin = 100
dmax = -100
for day in range(days):
day_min = resp['daily'][day]['temp']['min']
day_max = resp['daily'][day]['temp']['max']
if day_min < dmin: dmin = day_min
if day_max > dmax: dmax = day_max
return dmin, dmax
def format_percentage(num):
num = str(int(100 * num))
for i in range(3 - len(num)):
num = " " + num
num = num + "%"
return num
def get_daily(days, resp, dlow, dhigh):
delta = dhigh - dlow
steps = 20
incr = delta / steps
if days == 0:
return ""
daily_string = "\n"
for day in range(days):
dt = resp['daily'][day]['dt']
dt = datetime.datetime.fromtimestamp(dt)
dt = dt.strftime('%a %b %e')
code = resp['daily'][day]['weather'][0]['id']
icon = decode_icon(code, True)
day_min = resp['daily'][day]['temp']['min']
day_max = resp['daily'][day]['temp']['max']
lt = format_temp(day_min)
ht = format_temp(day_max)
pop = format_percentage(resp['daily'][day]['pop'])
daily_string = daily_string + "\n" + dt \
+ " " + icon + " " + pop + " " + lt + " "
day_tempc_startc = int((day_min - dlow) / incr)
day_tempc_stopc = int((day_max - dlow) / incr)
for character in range(steps):
if character < day_tempc_startc:
daily_string = daily_string + " "
elif character > day_tempc_stopc:
daily_string = daily_string + " "
else:
daily_string = daily_string + "─"
daily_string = daily_string + ht + ""
return daily_string
def validate_latitude(lat):
try:
lat = float(lat)
return -90 <= lat <= 90
except ValueError:
return False
def validate_longitude(lon):
try:
lon = float(lon)
return -180 <= lon <= 180
except ValueError:
return False
def validate_units(units):
valid_units = ['metric', 'standard', 'imperial']
return units.lower() in valid_units
URL = "https://api.openweathermap.org/data/3.0/onecall?"
try:
config_file = os.path.expanduser("~/.config/waybar/weather.conf")
with open(config_file, "r") as file:
for line in file:
key, value = line.strip().replace(" ", "").split("=")
if key == "lat":
if not validate_latitude(value):
print("Error: Invalid latitude")
sys.exit(1)
elif key == "lon":
if not validate_longitude(value):
print("Error: Invalid longitude")
sys.exit(1)
elif key == "units":
if not validate_units(value):
print("Error: Invalid units")
sys.exit(1)
elif key == "appid":
pass
else:
print("Error: Unknown key '{}'".format(key))
sys.exit(1)
URL = URL + "&" + key + "=" + value
except FileNotFoundError:
print("Error: File '{}' not found".format(config_file))
sys.exit(1)
with urllib.request.urlopen(URL) as url:
resp = json.load(url)
current_status_code = resp['current']['weather'][0]['id']
current_temp = round(resp['current']['temp'])
current_desc = resp['current']['weather'][0]['description']
current_dt = resp['current']['dt']
current_is_daytime = is_daytime(current_dt, resp)
current_icon = decode_icon(current_status_code, current_is_daytime)
precipitation_chart = render_minutely_precip_chart(resp)
precip_chart_string = format_precip_chart_string(precipitation_chart, resp)
hours_to_show = 16
hourly_hours = get_hourly_hours(hours_to_show, resp)
hourly_icons = get_hourly_icons(hours_to_show, resp)
hourly_temps = get_hourly_temps(hours_to_show, resp)
days_to_show = 8
dlow, dhigh = compute_daily_minmax(days_to_show, resp)
daily_forecast = get_daily(days_to_show, resp, dlow, dhigh)
tooltip_text = f"{current_icon} {current_desc}\n\n" \
+ precip_chart_string \
+ f"{hourly_hours}\n" \
+ f"{hourly_icons}\n" \
+ f"{hourly_temps}" \
+ daily_forecast
out_data = {
"text": f"{current_icon} {current_temp} °F",
"class": current_status_code,
"alt": current_desc,
"tooltip": tooltip_text
}
print(json.dumps(out_data))