home | add weather to waybar
This commit is contained in:
parent
daa8270aeb
commit
e8760c8250
3 changed files with 344 additions and 2 deletions
4
home/gui/files/sway/weather.conf
Normal file
4
home/gui/files/sway/weather.conf
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
lat=29.749
|
||||
lon=-95.773
|
||||
units=imperial
|
||||
appid=0ea01c37da47559728248e3be2872b20
|
||||
329
home/gui/files/sway/weather.py
Executable file
329
home/gui/files/sway/weather.py
Executable 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))
|
||||
|
|
@ -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; }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue