dotfiles/scripts/sunset.py
Michael Bradley 52b7668c72
Reduce hyprsunset impact
Don't change temperature while on Steam and don't send updates unnecessarily
2025-06-05 00:12:28 -04:00

92 lines
3.2 KiB
Python
Executable file

#!/bin/env python
"""Sets the monitor temperature based on the current time using Hyprsunset"""
from datetime import datetime
from os import environ
from pathlib import Path
from socket import AF_UNIX, SOCK_STREAM, socket
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) -> 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())
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) -> 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) and not on_steam():
set_temperature(hyprsunset, temperature)
if __name__ == "__main__":
main(
(day_elapsed(5), day_elapsed(7)),
(day_elapsed(21), day_elapsed(23)),
(2500.0, 6000.0)
)