From d8764acf0016bc8317e03a21b566ef19b0218b64 Mon Sep 17 00:00:00 2001 From: Michael Bradley Date: Sat, 29 Mar 2025 16:50:51 -0400 Subject: [PATCH 1/3] Port sunset script to Python for maintainability --- scripts/sunset.py | 88 ++++++++++++++++++++++++++++++++++++++++++ scripts/sunset.sh | 65 ------------------------------- systemd/sunset.service | 2 +- 3 files changed, 89 insertions(+), 66 deletions(-) create mode 100644 scripts/sunset.py delete mode 100755 scripts/sunset.sh diff --git a/scripts/sunset.py b/scripts/sunset.py new file mode 100644 index 0000000..26e72d3 --- /dev/null +++ b/scripts/sunset.py @@ -0,0 +1,88 @@ +#!/bin/env python +"""Sets the monitor temperature based on the current time using Hyprsunset""" +from datetime import datetime +from os import environ +from socket import AF_UNIX, SOCK_STREAM, socket + + +def find_hyprsunset_socket() -> str: + """Gets the socket file location""" + xdg_runtime_directory = environ["XDG_RUNTIME_DIR"] + hyprland_instance_signature = environ["HYPRLAND_INSTANCE_SIGNATURE"] + return f"{xdg_runtime_directory}/hypr/{hyprland_instance_signature}/.hyprsunset.sock" + + +def apply_temperature(temperature: float) -> None: + """Uses IPC to tell Hyprsunset to update the monitor temperature""" + hyprsunset = socket(AF_UNIX, SOCK_STREAM) + hyprsunset.connect(find_hyprsunset_socket()) + message = f"temperature {temperature:.0f}".encode() + sent = 0 + while sent < len(message): + sent += hyprsunset.send(message[sent:]) + + +type Range[T] = tuple[T, T] +type LerpRange = Range[float] +type HourRange = Range[int] +type ElapsedRange = Range[float] +type TemperatureRange = Range[float] + + +def lerped_amount(x: float, edges: LerpRange) -> float: + """How far `x` is between `values`""" + return (x - edges[0]) / (edges[1] - edges[0]) + + +def smoothstep(x: float, edges: LerpRange) -> 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 calculate_temperature(elapsed: float, sunrise: ElapsedRange, sunset: ElapsedRange, temperature: TemperatureRange) -> float: + """Determines which temperature to set the monitors to""" + if elapsed <= sunrise[0]: + return temperature[0] + elif elapsed < sunrise[1]: + return smoothstep(lerped_amount(elapsed, sunrise), temperature) + elif elapsed <= sunset[0]: + return temperature[1] + elif elapsed < sunset[1]: + return smoothstep(lerped_amount(elapsed, sunrise), temperature[::-1]) + else: + return temperature[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""" + TOTAL_SECONDS = 24 * 60 * 60 + elapsed = (((hours * 60) + minutes) * 60) + seconds + return elapsed / TOTAL_SECONDS + + +def current_elapsed() -> float: + """Time through the day represented in [0, 1]""" + current = datetime.now() + return day_elapsed(current.hour, current.minute, current.second) + + +def to_elapsed(hours: HourRange) -> ElapsedRange: + """Converts a range of hours into a range representing how far through the day is is""" + return day_elapsed(hours[0]), day_elapsed(hours[1]) + + +def main(sunrise: HourRange, sunset: HourRange, temperature: TemperatureRange) -> None: + """Adjusts the monitor temperature based on the current time""" + apply_temperature( + calculate_temperature( + current_elapsed(), + to_elapsed(sunrise), + to_elapsed(sunset), + temperature + ) + ) + + +if __name__ == "__main__": + main((5, 7), (21, 23), (2500.0, 6000.0)) diff --git a/scripts/sunset.sh b/scripts/sunset.sh deleted file mode 100755 index fb25f57..0000000 --- a/scripts/sunset.sh +++ /dev/null @@ -1,65 +0,0 @@ -#!/bin/env sh - -SUNRISE_START="$((5 * 60 * 60))" # When to start turning up temperature -SUNRISE_END="$((7 * 60 * 60))" # When to reach max temperature - -SUNSET_START="$((21 * 60 * 60))" # When to start turning down temperature -SUNSET_END="$((23 * 60 * 60))" # When to reach min temperature - -DAY_TEMP="6000" # Display temperature (K) to use in full daylight -NIGHT_TEMP="2400" # Display temperature (K) to use at night - -CURRENT="$(((($(date '+%-H') * 60) + $(date '+%-M')) * 60 + $(date '+%-S')))" # Time in seconds since start of day - -# Performs a calculation using an argument containing an input string -calc() { - echo "scale=4; $1" | bc -} - -# Evaluates a boolean expression on an argument containing an input string -bool() { - echo "scale=1; $1" | bc -} - -# GLSL Smoothstep, takes a single number as an argument -smoothstep() { - if [ "$(bool "$1 <= 0")" = "1" ]; then - echo 0 - elif [ "$(bool "$1 >= 1")" = "1" ]; then - echo 1 - else - calc "$1 * $1 * (3 - 2 * $1)" - fi -} - -# Interpolates between the 4th and 5th arguments based on the value of the 2nd in relation to the 1st and 3rd -interpolate() { - LOWER_IN="$1" - VALUE="$2" - UPPER_IN="$3" - LOWER_OUT="$4" - UPPER_OUT="$5" - - if [ "$((VALUE <= LOWER_IN))" = "1" ]; then - echo "$LOWER_OUT" - elif [ "$((VALUE < UPPER_IN))" = "1" ]; then - calc "$LOWER_OUT + (($UPPER_OUT - $LOWER_OUT) * $(smoothstep "$(calc "($VALUE - $LOWER_IN) / ($UPPER_IN - $LOWER_IN)")"))" - else - echo "$UPPER_OUT" - fi -} - -if [ "$((CURRENT <= SUNRISE_START))" = "1" ]; then - TEMP="$NIGHT_TEMP" -elif [ "$((CURRENT < SUNRISE_END))" = "1" ]; then - TEMP="$(interpolate "$SUNRISE_START" "$CURRENT" "$SUNRISE_END" "$NIGHT_TEMP" "$DAY_TEMP")" -elif [ "$((CURRENT <= SUNSET_START))" = "1" ]; then - TEMP="$DAY_TEMP" -elif [ "$((CURRENT < SUNSET_END))" = "1" ]; then - TEMP="$(interpolate "$SUNSET_START" "$CURRENT" "$SUNSET_END" "$DAY_TEMP" "$NIGHT_TEMP")" -else - TEMP="$NIGHT_TEMP" -fi - -# TODO: Figure out a nice way to wait until Hyprland has properly started up before running this -hyprctl hyprsunset temperature "$TEMP" diff --git a/systemd/sunset.service b/systemd/sunset.service index 4fa5f55..c6d5b2f 100644 --- a/systemd/sunset.service +++ b/systemd/sunset.service @@ -3,4 +3,4 @@ Description=Sets the monitor temperature based on the time of day [Service] Type=oneshot -ExecStart=/home/mbradley/scripts/sunset.sh +ExecStart=/home/mbradley/scripts/sunset.py From 08ecda72eeb3e8d60d7005243abbf8e69513672b Mon Sep 17 00:00:00 2001 From: Michael Bradley Date: Sat, 29 Mar 2025 17:08:16 -0400 Subject: [PATCH 2/3] Optimize Python sunset script --- scripts/sunset.py | 91 ++++++++++++++---------------------------- systemd/sunset.service | 2 +- 2 files changed, 30 insertions(+), 63 deletions(-) mode change 100644 => 100755 scripts/sunset.py diff --git a/scripts/sunset.py b/scripts/sunset.py old mode 100644 new mode 100755 index 26e72d3..ce0426d --- a/scripts/sunset.py +++ b/scripts/sunset.py @@ -5,84 +5,51 @@ from os import environ from socket import AF_UNIX, SOCK_STREAM, socket -def find_hyprsunset_socket() -> str: - """Gets the socket file location""" - xdg_runtime_directory = environ["XDG_RUNTIME_DIR"] - hyprland_instance_signature = environ["HYPRLAND_INSTANCE_SIGNATURE"] - return f"{xdg_runtime_directory}/hypr/{hyprland_instance_signature}/.hyprsunset.sock" +type Range = tuple[float, float] -def apply_temperature(temperature: float) -> None: - """Uses IPC to tell Hyprsunset to update the monitor temperature""" - hyprsunset = socket(AF_UNIX, SOCK_STREAM) - hyprsunset.connect(find_hyprsunset_socket()) - message = f"temperature {temperature:.0f}".encode() - sent = 0 - while sent < len(message): - sent += hyprsunset.send(message[sent:]) - - -type Range[T] = tuple[T, T] -type LerpRange = Range[float] -type HourRange = Range[int] -type ElapsedRange = Range[float] -type TemperatureRange = Range[float] - - -def lerped_amount(x: float, edges: LerpRange) -> 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: LerpRange) -> float: +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 calculate_temperature(elapsed: float, sunrise: ElapsedRange, sunset: ElapsedRange, temperature: TemperatureRange) -> float: - """Determines which temperature to set the monitors to""" - if elapsed <= sunrise[0]: - return temperature[0] - elif elapsed < sunrise[1]: - return smoothstep(lerped_amount(elapsed, sunrise), temperature) - elif elapsed <= sunset[0]: - return temperature[1] - elif elapsed < sunset[1]: - return smoothstep(lerped_amount(elapsed, sunrise), temperature[::-1]) - else: - return temperature[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""" - TOTAL_SECONDS = 24 * 60 * 60 - elapsed = (((hours * 60) + minutes) * 60) + seconds - return elapsed / TOTAL_SECONDS + return ((((hours * 60) + minutes) * 60) + seconds) / 86400 -def current_elapsed() -> float: - """Time through the day represented in [0, 1]""" - current = datetime.now() - return day_elapsed(current.hour, current.minute, current.second) - - -def to_elapsed(hours: HourRange) -> ElapsedRange: - """Converts a range of hours into a range representing how far through the day is is""" - return day_elapsed(hours[0]), day_elapsed(hours[1]) - - -def main(sunrise: HourRange, sunset: HourRange, temperature: TemperatureRange) -> None: +def main(sunrise: Range, sunset: Range, temp_range: Range) -> None: """Adjusts the monitor temperature based on the current time""" - apply_temperature( - calculate_temperature( - current_elapsed(), - to_elapsed(sunrise), - to_elapsed(sunset), - temperature - ) - ) + now = datetime.now() + elapsed = day_elapsed(now.hour, now.minute, now.second) + + if elapsed <= sunrise[0]: + temperature = temp_range[0] + elif elapsed < sunrise[1]: + temperature = smoothstep(lerped_amount(elapsed, sunrise), temp_range) + elif elapsed <= sunset[0]: + temperature = temp_range[1] + elif elapsed < sunset[1]: + temperature = smoothstep(lerped_amount(elapsed, sunset), (temp_range[1], temp_range[0])) + else: + temperature = temp_range[0] + + 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"/run/user/1000/hypr/{environ["HYPRLAND_INSTANCE_SIGNATURE"]}/.hyprsunset.sock") + # In theory the message might not send in one go, but in practice it does do I won't bother handling it + _ = hyprsunset.send(f"temperature {temperature:.1f}".encode()) if __name__ == "__main__": - main((5, 7), (21, 23), (2500.0, 6000.0)) + main( + (day_elapsed(5), day_elapsed(7)), + (day_elapsed(21), day_elapsed(23)), + (2500.0, 6000.0) + ) diff --git a/systemd/sunset.service b/systemd/sunset.service index c6d5b2f..30686f3 100644 --- a/systemd/sunset.service +++ b/systemd/sunset.service @@ -3,4 +3,4 @@ Description=Sets the monitor temperature based on the time of day [Service] Type=oneshot -ExecStart=/home/mbradley/scripts/sunset.py +ExecStart=/bin/env python -O /home/mbradley/scripts/sunset.py From b8b20cf817fba8f40781113f151d43b0c2f16004 Mon Sep 17 00:00:00 2001 From: Michael Bradley Date: Sat, 29 Mar 2025 17:11:47 -0400 Subject: [PATCH 3/3] Reduce frequency of sunset script runs --- systemd/sunset.timer | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/systemd/sunset.timer b/systemd/sunset.timer index ac60c6b..bc257aa 100644 --- a/systemd/sunset.timer +++ b/systemd/sunset.timer @@ -4,7 +4,7 @@ After=graphical-session.target After=hyprsunset.service [Timer] -OnCalendar=*-*-* *:*:00 +OnCalendar=*:0/5 Persistent=true AccuracySec=5s