home | add weather to waybar

This commit is contained in:
Don Harper 2024-07-06 23:15:51 -05:00
parent daa8270aeb
commit e8760c8250
3 changed files with 344 additions and 2 deletions

View file

@ -0,0 +1,4 @@
lat=29.749
lon=-95.773
units=imperial
appid=0ea01c37da47559728248e3be2872b20

329
home/gui/files/sway/weather.py Executable file
View file

@ -0,0 +1,329 @@
#!/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"<span font_family=\"Fantasque Sans Mono\"> {chart} </span>\n"
chart_string = chart_string + f"<span font_family=\"Fantasque Sans Mono\">{timelabel}</span>\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<span font_family=\"Fantasque Sans Mono\" size=\"large\">" + 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 + "</span>"
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"<span font_family=\"Fantasque Sans Mono\" size=\"xx-large\">{current_icon} {current_desc}</span>\n\n" \
+ precip_chart_string \
+ f"<span font_family=\"Fantasque Sans Mono\">{hourly_hours}</span>\n" \
+ f"<span font_family=\"Fantasque Sans Mono\">{hourly_icons}</span>\n" \
+ f"<span font_family=\"Fantasque Sans Mono\">{hourly_temps}</span>" \
+ daily_forecast
out_data = {
"text": f"{current_icon} {current_temp} °C",
"class": current_status_code,
"alt": current_desc,
"tooltip": tooltip_text
}
print(json.dumps(out_data))

View file

@ -7,6 +7,8 @@
...
}: {
xdg.configFile."sway/config".source = ./files/sway/${osConfig.networking.hostName};
xdg.configFile."waybar/weather.py".source = ./files/sway/weather.py;
xdg.configFile."waybar/weather.conf".source = ./files/sway/weather.conf;
programs = {
swaylock = {
@ -30,7 +32,8 @@
spacing = 0;
modules-left = ["sway/workspaces" "sway/mode" "wlr/workspaces" "custom/mycal"];
modules-center = [];
modules-right = ["idle_inhibitor" "custom/mytimew" "pulseaudio" "custom/mymusic" "bluetooth" "network" "backlight" "battery" "battery#bat2" "tray" "custom/mycal" "clock"];
# modules-right = ["idle_inhibitor" "custom/mytimew" "pulseaudio" "custom/mymusic" "bluetooth" "network" "backlight" "battery" "battery#bat2" "tray" "custom/mycal" "clock"];
modules-right = ["idle_inhibitor" "custom/weather" "pulseaudio" "custom/mymusic" "bluetooth" "network" "backlight" "battery" "battery#bat2" "tray" "custom/mycal" "clock"];
bluetooth = {
format-alt = "bt: {status}";
format-on = "bt";
@ -47,6 +50,11 @@
on-scroll-down = "${pkgs.light}/bin/light -A 1";
on-scroll-up = "${pkgs.light}/bin/lightlight -U 1";
};
"custom/weather" = {
exec = "~/.config/waybar/weather.py";
interval = 300;
return-type = "json";
};
"custom/mytimew" = {
exec = "~/bin/timebar";
interval = 30;
@ -157,7 +165,7 @@
#workspaces button.focused { background: #64727D; border-bottom: 3px solid #ffffff; }
#workspaces button.urgent { background-color: #eb4d4b; }
#mode { background: #64727D; border-bottom: 3px solid #ffffff; }
#clock, #battery, #cpu, #memory, #temperature, #backlight, #network, #pulseaudio, #custom-mymusic, #tray, #mode, #idle_inhibitor, #bluetooth {
#clock, #battery, #cpu, #memory, #temperature, #backlight, #network, #pulseaudio, #custom-weather, #custom-mymusic, #tray, #mode, #idle_inhibitor, #bluetooth {
padding: 0 5px;
margin: 0 0px;
}
@ -182,6 +190,7 @@
#network.disconnected { background: #ff5555; }
#pulseaudio { background: #ffb86c; color: #000000; }
#pulseaudio.muted { background: #90b1b1; color: #2a5c45; }
#custom-weather { background: #bd93f9; color: #000000; }
#custom-mytimew { background: #bd93f9; color: #000000; }
#custom-mymusic { background: #8be9fd; color: #000000; }
#custom-mycal { background: #cccc99; color: #2a5c45; }