#!/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) class Arguments(Namespace): check_steam: bool = False instant: bool = False if __name__ == "__main__": 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 )