# -*- python -*-
# This example adjusts the the colours to make it easier to go to bed
# around a scheduled time, for each weekday.
# Copyright © 2014, 2015, 2016, 2017 Mattias Andrée (maandree@kth.se)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import time
import datetime
# Geographical coodinates.
# ("Kristall, vertikal accent i glas och stål" (Crystal, vertical accent
# in glass and steal) in this example. A glass obelisk, lit from the inside
# with adjustable colours and a default colour of 5600 K, in the middle
# of a hyperelliptic roundabout.)
latitude, longitude = 59.3326, 18.0652
# The time for each weekday you go to bed. The first value is the
# time to start preparing the for sleep and the second value is the
# time the monitors should be fully adjusted for sleep.
time_sleep_monday = ('21:00', '24:00')
time_sleep_tuesday = ('21:00', '24:00')
time_sleep_wednesday = ('21:00', '24:00')
time_sleep_thursday = ('21:00', '24:00')
time_sleep_friday = ('21:00', '24:00')
time_sleep_saturday = ('23:00', '26:00')
time_sleep_sunday = ('23:00', '26:00')
# It is allowed to have values above and including 24:00, these
# values are interprets as that time (minus 24 hours) the next day.
# The time for each weekday you wake up. The first value is the time
# to start adjusting the colours back to normal node, and the second
# value is the time the adjustment should be back to fully normal.
time_wakeup_monday = ('06:00', '07:00')
time_wakeup_tuesday = ('06:00', '07:00')
time_wakeup_wednesday = ('06:00', '07:00')
time_wakeup_thursday = ('06:00', '07:00')
time_wakeup_friday = ('06:00', '07:00')
time_wakeup_saturday = ('13:00', '14:00')
time_wakeup_sunday = ('13:00', '14:00')
# The (zero-based) indices of the monitors (CRTC:s) to apply
# settings to. An empty list means that all monitors are used,
# but all monitors will have the same settings.
monitors = []
# Gamma correction for the red, green and blue components,
# respectively, for each monitor,
gamma_red = [1]
gamma_green = [1]
gamma_blue = [1]
# The colour temperature during the day, night, during
# sleep and the default, respectively.
temperature_day = 5500
temperature_night = 3500
temperature_sleep = 1000
temperature_default = 6500
# The brightness during the day, night, during sleep
# and the default respectively.
brightness_day = 1
brightness_night = 1
brightness_sleep = 0.2
brightness_default = 1
# Method for applying colour curves.
apply_curves = randr
#apply_curves = vidmode
if ttymode:
apply_curves = drm
# Method used to get the degree to which it is day.
get_dayness = lambda : sun(latitude, longitude)
wait_period = 1
'''
:float The number of seconds to wait before invoking `periodically` again
'''
fadein_time = 20
'''
:float? The number of seconds used to fade in on start, `None` for no fading
'''
fadeout_time = 10
'''
:float? The number of seconds used to fade out on exit, `None` for no fading
'''
fadein_steps = 20 * fadein_time if fadein_time is not None else None
'''
:int The number of steps in the fade in phase, if any
'''
fadeout_steps = 20 * fadeout_time if fadeout_time is not None else None
'''
:int The number of steps in the fade out phase, if any
'''
# Time constants.
ONE_DAY = 24 * 60 * 60
ONE_WEEK = 7 * ONE_DAY
# Combine the time points into a matrix.
times = (time_sleep_monday + time_wakeup_tuesday,
time_sleep_tuesday + time_wakeup_wednesday,
time_sleep_wednesday + time_wakeup_thursday,
time_sleep_thursday + time_wakeup_friday,
time_sleep_friday + time_wakeup_saturday,
time_sleep_saturday + time_wakeup_sunday,
time_sleep_sunday + time_wakeup_monday)
def interpret_time(t):
'''
Convert a text representation of a time point to a float
point value of the number of seconds
@param t:str The time as text
@return :float The time as floating point
'''
t = [float(t_) for t_ in t.split(':')]
while len(t) > 3:
t.append(0)
return sum([v * 60 ** (2 - i) for i, v in enumerate(t)])
def monotonic_time(ts):
'''
Ensure that each time points in a sequence is at least
as late as the previous time
@param ts:list<float> The time point sequence
@return :list<float> The time point sequence as an increasing sequence
'''
rc = [ts[0]]
for t in ts[1:]:
if t < rc[-1]:
t += rc[-1] - (rc[-1] % ONE_DAY)
if t < rc[-1]:
t += ONE_DAY
rc.append(t)
return rc
times = [monotonic_time([interpret_time(t) for t in ts]) for ts in times]
# Convert time point matrix to a vector.
timepoints = []
for weekday in range(len(times)):
weekday_ = weekday * ONE_DAY
ts = times[weekday]
for ti in range(len(ts)):
t = (ts[ti] + weekday_) % ONE_WEEK
timepoints.append((t, ti))
timepoints.sort(key = lambda x : x[0])
timepoints.insert(0, (timepoints[-1][0] - ONE_WEEK, timepoints[-1][1]))
timepoints.append((timepoints[1][0] + ONE_WEEK, timepoints[1][1]))
def get_bedness(time):
'''
Calculate to what degree the adjustments should be tuned to bedtime mode
@param time:float The number of seconds in the time modulo the a week
@return :float To what degree the adjustments should be tuned to bedtime mode
'''
for i in range(len(timepoints) - 1):
if timepoints[i][0] <= time <= timepoints[i + 1][0]:
break
(a, p), (b, _) = timepoints[i], timepoints[i + 1]
weight = (time - a) / (b - a)
if p == 0: return weight
if p == 1: return 1
if p == 2: return 1 - weight
return 0
last_dayness, last_bedness = -1, -1
def periodically(year, month, day, hour, minute, second, weekday, fade):
'''
Invoked periodically
If you want to control at what to invoke this function next time
you can set the value of the global variable `wait_period` to the
number of seconds to wait before invoking this function again.
The value does not need to be an integer.
@param year:int The year
@param month:int The month, 1 = January, 12 = December
@param day:int The day, minimum value is 1, probable maximum value is 31 (*)
@param hour:int The hour, minimum value is 0, maximum value is 23
@param minute:int The minute, minimum value is 0, maximum value is 59
@param second:int The second, minimum value is 0, probable maximum value is 60 (**)
@param weekday:int The weekday, 1 = Monday, 7 = Sunday
@param fade:float? Blueshift can use this function to fade into a state when it start
or exits. `fade` can either be negative, zero or positive or `None`,
but the magnitude of value cannot exceed 1. When Blueshift starts,
this function will be invoked multiple with the time parameters
of the time it is invoked and each time `fade` will increase towards
1, starting at 0, when the value is 1, the settings should be applied
to 100 %. After this this function will be invoked once again with
`fade` being `None`. When Blueshift exits the same behaviour is used
except, `fade` decrease towards -1 but start slightly below 0, when
-1 is reached all settings should be normal. Then Blueshift will NOT
invoke this function with `fade` being `None`, instead it will by
itself revert all settings and quit.
(*) Can be exceeded if the calendar system is changed, like in 1712-(02)Feb-30
(**) See https://en.wikipedia.org/wiki/Leap_second
'''
global last_dayness, last_bedness
tzoff = (datetime.datetime.now().hour - datetime.datetime.utcnow().hour) * 60 * 60
tzoff += (datetime.datetime.now().minute - datetime.datetime.utcnow().minute) * 60
now = time.time() + tzoff
h = int((now / (60 * 60)) % 24)
if h < hour:
weekday += 1
dayness = get_dayness()
bedness = get_bedness((weekday - 1) * ONE_DAY + (now % ONE_DAY))
# Do not apply new adjustments if nothing has changed.
if (fade is None) and (dayness == last_dayness) and (bedness == last_bedness):
return
last_dayness, last_bedness = dayness, bedness
# Calculate temperature and brightness.
temperature_ = temperature_day * dayness + temperature_night * (1 - dayness)
brightness_ = brightness_day * dayness + brightness_night * (1 - dayness)
temperature_ = temperature_sleep * bedness + temperature_ * (1 - bedness)
brightness_ = brightness_sleep * bedness + brightness_ * (1 - bedness)
if fade is not None:
alpha = abs(fade)
temperature_ = temperature_ * alpha + temperature_default * (1 - alpha)
brightness_ = brightness_ * alpha + brightness_default * (1 - alpha)
# Remove settings from last run.
start_over()
# Apply colour temperature using raw CIE 1964 10 degree CMF data with interpolation.
temperature(temperature_, lambda t : clip_whitepoint(divide_by_maximum(cmf_10deg(t))))
# Apply colour brightness using the CIE xyY colour space.
cie_brightness(brightness_)
# Store calculates so that they can be reused for each monitor
stored = store()
for m in range(max(1, len(monitors))):
gamma_red_ = gamma_red [m % len(gamma_red)]
gamma_green_ = gamma_green[m % len(gamma_green)]
gamma_blue_ = gamma_blue [m % len(gamma_blue)]
# Reuse stored calculations.
restore(stored)
# Apply gamma correction to monitor.
gamma(gamma_red_, gamma_green_, gamma_blue_)
# Flush settings to monitor.
if len(monitors) == 0:
apply_curves()
else:
apply_curves(monitors[m % len(monitors)])
def reset():
'''
Invoked to reset the displays
'''
for m in range(max(1, len(monitors))):
gamma_red_ = gamma_red [m % len(gamma_red)]
gamma_green_ = gamma_green[m % len(gamma_green)]
gamma_blue_ = gamma_blue [m % len(gamma_blue)]
# Remove settings from last run.
start_over()
# Apply gamma correction to monitor.
gamma(gamma_red_, gamma_green_, gamma_blue_)
# Flush settings to monitor.
if len(monitors) == 0:
apply_curves()
else:
apply_curves(monitors[m % len(monitors)])