#!/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) )