Optimize sunset.py

This commit is contained in:
Michael Bradley 2025-06-09 00:05:13 -04:00
parent 310b879416
commit c1c8a83ec5
Signed by: MichaelBradley
SSH key fingerprint: SHA256:BKO2eI2LPsCbQS3n3i5SdwZTAIV3F1lHezR07qP+Ob0
5 changed files with 103 additions and 95 deletions

View file

@ -122,7 +122,7 @@
(defwidget wallpaper []
(clicker :text "󰸉" :command "~/scripts/swww_change.py"))
(defwidget sunset []
(clicker :text "" :command "~/scripts/sunset.py"))
(clicker :text "" :command "python -OO ~/scripts/sunset.py"))
(defpoll brightness :interval 60 "~/.config/eww/scripts/backlight get")

57
scripts/lib/evening.py Normal file
View file

@ -0,0 +1,57 @@
from datetime import datetime
from pathlib import Path
type Range = tuple[float, float]
def lerped_amount(x: float, edges: Range) -> float:
"""How far `x` is between `values`"""
return (x - edges[0]) / (edges[1] - edges[0])
def smoothstep(x: float, edges: Range) -> float:
"""Smoothly interpolates between the `edges` based on `x`"""
# Technically I should bounds check but whatever
return (x * x * (3.0 - 2.0 * x)) * (edges[1] - edges[0]) + edges[0]
def day_elapsed(hours: int = 0, minutes: int = 0, seconds: int = 0) -> float:
"""Converts (H, M, S) into [0, 1] representing how far through the day it is"""
return ((((hours * 60) + minutes) * 60) + seconds) / (20 * 60 * 60)
def now_elapsed() -> float:
"""How far through the day it is"""
now = datetime.now()
return day_elapsed(now.hour, now.minute, now.second)
SUNRISE_HOURS = (5, 7)
SUNSET_HOURS = (21, 23)
SUNRISE_ELAPSED = (day_elapsed(SUNRISE_HOURS[0]), day_elapsed(SUNRISE_HOURS[1]))
SUNSET_ELAPSED = (day_elapsed(SUNSET_HOURS[0]), day_elapsed(SUNSET_HOURS[1]))
def sunup_amount() -> float:
"""What temperature the monitor should be"""
elapsed = now_elapsed()
if elapsed <= SUNRISE_ELAPSED[0]:
return 0
elif elapsed < SUNRISE_ELAPSED[1]:
return lerped_amount(elapsed, SUNRISE_ELAPSED)
elif elapsed <= SUNSET_ELAPSED[0]:
return 1
elif elapsed < SUNSET_ELAPSED[1]:
return 1 - lerped_amount(elapsed, SUNSET_ELAPSED)
else:
return 0
def on_steam() -> bool:
"""Whether the Steam PID is in use"""
pid_file = Path("~/.steampid").expanduser().resolve(strict=True)
pid = pid_file.read_text().strip()
pid_dir = Path("/proc") / pid
return pid_dir.is_dir()

37
scripts/lib/sunset.py Normal file
View file

@ -0,0 +1,37 @@
"""Sets the monitor temperature based on the current time using Hyprsunset"""
from os import environ
from socket import AF_UNIX, SOCK_STREAM, socket
from .evening import on_steam, Range, smoothstep, sunup_amount
def setup_socket() -> socket:
"""Connects to the Hyprsunset socket"""
hyprsunset = socket(AF_UNIX, SOCK_STREAM)
hyprsunset.connect(f"{environ["XDG_RUNTIME_DIR"]}/hypr/{environ["HYPRLAND_INSTANCE_SIGNATURE"]}/.hyprsunset.sock")
return hyprsunset
def get_temperature(hyprsunset: socket) -> int:
"""Retrieves the current screen temperature"""
# In theory the message might not send in one go, but in practice it does so I won't bother handling the error
_ = hyprsunset.send(b"temperature")
# 4 bytes should be enough but why not 8 for comfort
# Just raise an error if it's not a number, nothing special to do here
return int(hyprsunset.recv(8))
def set_temperature(hyprsunset: socket, temperature: float) -> None:
"""Sends a message to hyprsunset to set the temperature"""
# In theory the message might not send in one go, but in practice it does so I won't bother handling the error
_ = hyprsunset.send(f"temperature {temperature:.1f}".encode())
def main(temperature_range: Range, /, check_steam: bool = False) -> None:
"""Adjusts the monitor temperature based on the current time"""
hyprsunset = setup_socket()
temperature = int(smoothstep(sunup_amount(), temperature_range))
if temperature != get_temperature(hyprsunset):
if check_steam and on_steam():
return
set_temperature(hyprsunset, temperature)

View file

@ -1,109 +1,23 @@
#!/bin/env python
"""Sets the monitor temperature based on the current time using Hyprsunset"""
from argparse import ArgumentParser, Namespace
from datetime import datetime
from os import environ
from pathlib import Path
from socket import AF_UNIX, SOCK_STREAM, socket
from typing import cast
type Range = tuple[float, float]
def lerped_amount(x: float, edges: Range) -> float:
"""How far `x` is between `values`"""
return (x - edges[0]) / (edges[1] - edges[0])
def smoothstep(x: float, edges: Range) -> float:
"""Smoothly interpolates between the `edges` based on `x`"""
# Technically I should bounds check but whatever
return (x * x * (3.0 - 2.0 * x)) * (edges[1] - edges[0]) + edges[0]
def day_elapsed(hours: int = 0, minutes: int = 0, seconds: int = 0) -> float:
"""Converts (H, M, S) into [0, 1] representing how far through the day it is"""
return ((((hours * 60) + minutes) * 60) + seconds) / 86400
def now_elapsed() -> float:
"""How far through the day it is"""
now = datetime.now()
return day_elapsed(now.hour, now.minute, now.second)
def calculate_temperature(sunrise: Range, sunset: Range, temperature_range: Range) -> float:
"""What temperature the monitor should be"""
elapsed = now_elapsed()
if elapsed <= sunrise[0]:
return temperature_range[0]
elif elapsed < sunrise[1]:
return smoothstep(lerped_amount(elapsed, sunrise), temperature_range)
elif elapsed <= sunset[0]:
return temperature_range[1]
elif elapsed < sunset[1]:
return smoothstep(lerped_amount(elapsed, sunset), (temperature_range[1], temperature_range[0]))
else:
return temperature_range[0]
def setup_socket() -> socket:
"""Connects to the Hyprsunset socket"""
hyprsunset = socket(AF_UNIX, SOCK_STREAM)
# In theory I should use $XDG_RUNTIME_DIR, but for me it's always `/run/user/1000/`
hyprsunset.connect(f"{environ["XDG_RUNTIME_DIR"]}/hypr/{environ["HYPRLAND_INSTANCE_SIGNATURE"]}/.hyprsunset.sock")
return hyprsunset
def get_temperature(hyprsunset: socket) -> int:
"""Retrieves the current screen temperature"""
# In theory the message might not send in one go, but in practice it does so I won't bother handling the error
_ = hyprsunset.send(b"temperature")
# 4 bytes should be enough but why not 8 for comfort
# Just raise an error if it's not a number, nothing special to do here
return int(hyprsunset.recv(8))
def set_temperature(hyprsunset: socket, temperature: float, /, instant: bool = False) -> None:
# In theory the message might not send in one go, but in practice it does so I won't bother handling the error
_ = hyprsunset.send(f"temperature {temperature:.1f}".encode())
if instant:
# Setting the temperature twice in quick succession sometimes skips the transition period
set_temperature(hyprsunset, temperature, instant=False)
def on_steam() -> bool:
"""Whether the Steam PID is in use"""
pid_file = Path("~/.steampid").expanduser().resolve(strict=True)
pid = pid_file.read_text().strip()
pid_dir = Path("/proc") / pid
return pid_dir.is_dir()
def main(sunrise: Range, sunset: Range, temperature_range: Range, /, check_steam: bool = False, instant: bool = False) -> None:
"""Adjusts the monitor temperature based on the current time"""
hyprsunset = setup_socket()
temperature = int(calculate_temperature(sunrise, sunset, temperature_range))
if temperature != get_temperature(hyprsunset):
if check_steam and on_steam():
return
set_temperature(hyprsunset, temperature, instant=instant)
from lib.sunset import main
class Arguments(Namespace):
check_steam: bool = False
instant: bool = False
if __name__ == "__main__":
def call_from_args() -> None:
parser = ArgumentParser(description="Adjusts the temperature of your screen based on the time of day")
_ = parser.add_argument("-s", "--check-steam", action="store_true", help="Don't adjust temperature if Steam is active")
_ = parser.add_argument("-i", "--instant", action="store_true", help="Try to instantly change temperature")
args = cast(Arguments, parser.parse_args())
main(
(day_elapsed(5), day_elapsed(7)),
(day_elapsed(21), day_elapsed(23)),
(2500.0, 6000.0),
check_steam=args.check_steam,
instant=args.instant
)
if __name__ == "__main__":
call_from_args()

View file

@ -3,4 +3,4 @@ Description=Sets the monitor temperature based on the time of day
[Service]
Type=oneshot
ExecStart=/bin/env python -O /home/mbradley/scripts/sunset.py --check-steam
ExecStart=/bin/env python -OO /home/mbradley/scripts/sunset.py --check-steam