From fa5bbee56ebd207d92c041847133f2f9099f7fdf Mon Sep 17 00:00:00 2001 From: Kevin Alberts Date: Tue, 6 Jan 2026 09:41:51 +0100 Subject: [PATCH] Estimate sunup/sundown times for earlier wallpaper changes in winter, update for Qtile 0.34.x --- kuro/config/__init__.py | 5 ++ kuro/theme.py | 24 ++++--- kuro/utils/suntime.py | 141 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 161 insertions(+), 9 deletions(-) create mode 100644 kuro/utils/suntime.py diff --git a/kuro/config/__init__.py b/kuro/config/__init__.py index 85c1bff..630827f 100644 --- a/kuro/config/__init__.py +++ b/kuro/config/__init__.py @@ -80,6 +80,11 @@ class Config(BaseConfig): {'title': 'Transaction splits', 'wm_class': 'homebank'}, ] + # Location + loc_latitude = 52.357603 + loc_longitude = 6.663761 + loc_timezone = "Europe/Amsterdam" + # Autostart applications apps_autostart_group = [ {'group': "", 'command': ["firefox"]}, diff --git a/kuro/theme.py b/kuro/theme.py index 3f554a6..aed645e 100644 --- a/kuro/theme.py +++ b/kuro/theme.py @@ -5,11 +5,11 @@ import time import datetime import socket import subprocess +from dateutil import tz from typing import Optional from libqtile.backend.base import Window -from libqtile.backend.wayland.layer import LayerStatic -from libqtile.backend.wayland.xwindow import XWindow as WaylandXWindow, XStatic as WaylandXStatic +from libqtile.backend.wayland.window import Window as WaylandWindow, Static as WaylandStatic from libqtile.backend.x11.window import XWindow as XorgXWindow # Initialize logging from libqtile.log_utils import logger @@ -30,6 +30,7 @@ logger.warning("Importing kuro utils...") import kuro.utils.widgets from kuro.utils import general as utils +from kuro.utils.suntime import Sun logger.warning("Importing variables and other utils...") @@ -643,11 +644,9 @@ class Kuro(BaseTheme): try: w_pid = client.get_pid() except AttributeError: # Some windows might not have this .get_pid method. Try other ways - if isinstance(client, WaylandXWindow) or isinstance(client, WaylandXStatic): - w_pid = client.surface.pid - elif isinstance(client, XorgXWindow): + if isinstance(client, XorgXWindow): w_pid = client.get_net_wm_pid() - elif isinstance(client, LayerStatic): + elif isinstance(client, WaylandStatic) or isinstance(client, WaylandInternal): pass # Wayland background layer 'window' else: logger.error(f"Unknown window type {client.__class__.__name__}") @@ -714,11 +713,18 @@ class Kuro(BaseTheme): wallpapers = [] wallpaper_dir = Config.get("desktop_bg_folder", "") - # Use a wallpaper from the night folder after 9PM and before 6AM + # Get current and sunset/sunrise times using util + sun = Sun(Config.get("loc_latitude", 52.357603), Config.get("loc_longitude", 6.663761)) + tzone = tz.gettz(Config.get("loc_timezone", "Europe/Amsterdam")) + cur_time = datetime.datetime.now(tz=tzone) + sunset = sun.get_sunset_time(time_zone=tzone) + sunrise = sun.get_sunrise_time(time_zone=tzone) + logger.warning("Using current time {}, sunset {}, sunrise {} to determine wallpaper.".format(cur_time, sunset, sunrise)) + + # Use a wallpaper from the night folder after sunset and before sunrise wallpaper_night_dir = Config.get("desktop_bg_night_folder", "") if wallpaper_night_dir and os.path.isdir(wallpaper_night_dir): - cur_time = datetime.datetime.now() - if cur_time.hour > 21 or cur_time.hour < 6: + if cur_time > sunset or cur_time < sunrise: wallpaper_dir = wallpaper_night_dir try: diff --git a/kuro/utils/suntime.py b/kuro/utils/suntime.py new file mode 100644 index 0000000..e4997bb --- /dev/null +++ b/kuro/utils/suntime.py @@ -0,0 +1,141 @@ +import math +import warnings +from datetime import datetime, timedelta, time, timezone + +# Copied from: https://github.com/SatAgro/suntime/blob/master/suntime/suntime.py + +# CONSTANT +TO_RAD = math.pi/180.0 + + +class SunTimeException(Exception): + + def __init__(self, message): + super(SunTimeException, self).__init__(message) + + +class Sun: + """ + Approximated calculation of sunrise and sunset datetimes. Adapted from: + https://stackoverflow.com/questions/19615350/calculate-sunrise-and-sunset-times-for-a-given-gps-coordinate-within-postgresql + """ + def __init__(self, lat, lon): + self._lat = lat + self._lon = lon + + self.lngHour = self._lon / 15 + + def get_sunrise_time(self, at_date=datetime.now(), time_zone=timezone.utc): + """ + :param at_date: Reference date. datetime.now() if not provided. + :param time_zone: pytz object with .tzinfo() or None + :return: sunrise datetime. + :raises: SunTimeException when there is no sunrise and sunset on given location and date. + """ + time_delta = self.get_sun_timedelta(at_date, time_zone=time_zone, is_rise_time=True) + if time_delta is None: + raise SunTimeException('The sun never rises on this location (on the specified date)') + else: + return datetime.combine(at_date, time(tzinfo=time_zone)) + time_delta + + def get_sunset_time(self, at_date=datetime.now(), time_zone=timezone.utc): + """ + Calculate the sunset time for given date. + :param at_date: Reference date. datetime.now() if not provided. + :param time_zone: pytz object with .tzinfo() or None + :return: sunset datetime. + :raises: SunTimeException when there is no sunrise and sunset on given location and date. + """ + time_delta = self.get_sun_timedelta(at_date, time_zone=time_zone, is_rise_time=False) + if time_delta is None: + raise SunTimeException('The sun never rises on this location (on the specified date)') + else: + return datetime.combine(at_date, time(tzinfo=time_zone)) + time_delta + + def get_sun_timedelta(self, at_date, time_zone, is_rise_time=True, zenith=90.8): + """ + Calculate sunrise or sunset date. + :param at_date: Reference date + :param time_zone: pytz object with .tzinfo() or None + :param is_rise_time: True if you want to calculate sunrise time. + :param zenith: Sun reference zenith + :return: timedelta showing hour, minute, and second of sunrise or sunset + """ + + # If not set get local timezone from datetime + if time_zone is None: + time_zone = datetime.now().tzinfo + + # 1. first get the day of the year + N = at_date.timetuple().tm_yday + + # 2. convert the longitude to hour value and calculate an approximate time + if is_rise_time: + t = N + ((6 - self.lngHour) / 24) + else: # sunset + t = N + ((18 - self.lngHour) / 24) + + # 3a. calculate the Sun's mean anomaly + M = (0.9856 * t) - 3.289 + + # 3b. calculate the Sun's true longitude + L = M + (1.916 * math.sin(TO_RAD*M)) + (0.020 * math.sin(TO_RAD * 2 * M)) + 282.634 + L = self._force_range(L, 360) # NOTE: L adjusted into the range [0,360) + + # 4a. calculate the Sun's declination + sinDec = 0.39782 * math.sin(TO_RAD*L) + cosDec = math.cos(math.asin(sinDec)) + + # 4b. calculate the Sun's local hour angle + cosH = (math.cos(TO_RAD*zenith) - (sinDec * math.sin(TO_RAD*self._lat))) / (cosDec * math.cos(TO_RAD*self._lat)) + + if cosH > 1: + return None # The sun never rises on this location (on the specified date) + if cosH < -1: + return None # The sun never sets on this location (on the specified date) + + # 4c. finish calculating H and convert into hours + if is_rise_time: + H = 360 - (1/TO_RAD) * math.acos(cosH) + else: # setting + H = (1/TO_RAD) * math.acos(cosH) + H = H / 15 + + # 5a. calculate the Sun's right ascension + RA = (1/TO_RAD) * math.atan(0.91764 * math.tan(TO_RAD*L)) + RA = self._force_range(RA, 360) # NOTE: RA adjusted into the range [0,360) + + # 5b. right ascension value needs to be in the same quadrant as L + Lquadrant = (math.floor(L/90)) * 90 + RAquadrant = (math.floor(RA/90)) * 90 + RA = RA + (Lquadrant - RAquadrant) + + # 5c. right ascension value needs to be converted into hours + RA = RA / 15 + + # 6. calculate local mean time of rising/setting + T = H + RA - (0.06571 * t) - 6.622 + + # 7a. adjust back to UTC + UT = T - self.lngHour + + if time_zone: + # 7b. adjust back to local time + UT += time_zone.utcoffset(at_date).total_seconds() / 3600 + + # 7c. rounding and impose range bounds + UT = round(UT, 2) + # if is_rise_time: + UT = self._force_range(UT, 24) + + # 8. return timedelta + return timedelta(hours=UT) + + @staticmethod + def _force_range(v, max): + # force v to be >= 0 and < max + if v < 0: + return v + max + elif v >= max: + return v - max + return v