2023-08-06 16:42:56 -07:00
""" Simple web app to visualize brightness over time. """
2023-08-08 17:34:35 -07:00
import datetime as dt
from contextlib import suppress
from pathlib import Path
from typing import Any
2023-08-06 16:42:56 -07:00
import matplotlib . pyplot as plt
import numpy as np
2023-08-08 17:34:35 -07:00
import shinyswatch
fix: replace deprecated `get_astral_location` with `get_astral_observer` (#1482)
* fix: replace deprecated get_astral_location with get_astral_observer (#1481)
HA 2026.7 deprecates homeassistant.helpers.sun.get_astral_location
(removal planned for 2027.7) in favor of get_astral_observer, causing a
deprecation warning in the HA logs.
- Switch SunEvents/SunLightSettings from astral.location.Location to
astral.Observer, using the astral.sun module functions (which return
UTC times by default, matching the previous local=False calls).
- Use get_astral_observer in switch.py, with a fallback for HA < 2026.7
that constructs the Observer directly from the HA config.
- Update tests and the webapp simulator accordingly.
* ci: handle removal of requirements_test_all.txt in HA 2026.8 dev
HA core removed requirements_test_all.txt (home-assistant/core#171530),
which made test_dependencies.py crash with FileNotFoundError and broke
the dev pytest job and the Docker builds. Fall back to
requirements_all.txt, which carries the same per-integration
'# homeassistant.components.x' annotations. Also extend the
aiohasupervisor pin lookup in scripts/setup-dependencies accordingly.
* test: support modern template light config for HA 2026.6+
HA 2026.6 removed the legacy `light: platform: template` YAML format
(home-assistant/core#169615), so setup_lights found no template platform
on HA dev and every test using it failed with IndexError. Detect legacy
support at runtime (PLATFORM_SCHEMA presence) and fall back to the
modern `template:` config format. The group platform is set up before
the template integration in the modern path, because setting up
`template` also sets up the `light` domain, which would make a later
async_setup_component(hass, LIGHT_DOMAIN, ...) a no-op.
2026-07-01 23:00:35 -07:00
from astral import Observer
2023-08-10 17:00:16 -07:00
from homeassistant_util_color import color_temperature_to_rgb
2023-08-08 17:34:35 -07:00
from shiny import App , render , ui
2023-08-06 16:42:56 -07:00
2023-08-08 17:34:35 -07:00
def date_range ( tzinfo : dt . tzinfo ) - > list [ dt . datetime ] :
""" Return a list of datetimes for the current day. """
2023-08-08 14:31:17 -07:00
start_of_day = dt . datetime . now ( tzinfo ) . replace (
2023-08-08 17:34:35 -07:00
hour = 0 ,
minute = 0 ,
second = 0 ,
microsecond = 0 ,
2023-08-08 14:31:17 -07:00
)
# one second before the next day
end_of_day = start_of_day + dt . timedelta ( days = 1 ) - dt . timedelta ( seconds = 1 )
hours_range = [ start_of_day ]
while hours_range [ - 1 ] < end_of_day :
2023-08-08 17:34:35 -07:00
hours_range . append ( hours_range [ - 1 ] + dt . timedelta ( minutes = 1 ) )
2023-08-08 14:31:17 -07:00
return hours_range [ : - 1 ]
2023-08-08 17:34:35 -07:00
def copy_color_and_brightness_module ( ) - > None :
""" Copy the color_and_brightness module to the webapp folder. """
2023-08-08 14:31:17 -07:00
with suppress ( Exception ) :
webapp_folder = Path ( __file__ ) . parent . absolute ( )
module = (
webapp_folder . parent
/ " custom_components "
/ " adaptive_lighting "
/ " color_and_brightness.py "
)
new_module = webapp_folder / module . name
with module . open ( ) as f :
lines = [
line . replace ( " homeassistant.util.color " , " homeassistant_util_color " )
for line in f . readlines ( )
]
with new_module . open ( " r " ) as f :
existing_lines = f . readlines ( )
if existing_lines != lines :
with new_module . open ( " w " ) as f :
f . writelines ( lines )
2023-08-06 16:42:56 -07:00
2023-08-08 14:31:17 -07:00
copy_color_and_brightness_module ( )
2023-08-06 16:42:56 -07:00
2023-08-08 17:34:35 -07:00
from color_and_brightness import SunLightSettings # noqa: E402
2023-08-06 16:42:56 -07:00
2023-08-08 17:34:35 -07:00
def plot_brightness ( inputs : dict [ str , Any ] , sleep_mode : bool ) :
""" Plot the brightness over time for different modes. """
2023-08-06 16:42:56 -07:00
# Define the time range for our simulation
2023-08-08 17:34:35 -07:00
sun_linear = SunLightSettings ( * * inputs , brightness_mode = " linear " )
sun_tanh = SunLightSettings ( * * inputs , brightness_mode = " tanh " )
sun = SunLightSettings ( * * inputs , brightness_mode = " default " )
2023-08-08 14:31:17 -07:00
# Calculate the brightness for each time in the time range for all modes
dt_range = date_range ( sun . timezone )
time_range = [ time_to_float ( dt ) for dt in dt_range ]
2023-08-06 16:42:56 -07:00
brightness_linear_values = [
2023-08-08 14:31:17 -07:00
sun_linear . brightness_pct ( dt , sleep_mode ) for dt in dt_range
2023-08-06 16:42:56 -07:00
]
brightness_tanh_values = [
2023-08-08 14:31:17 -07:00
sun_tanh . brightness_pct ( dt , sleep_mode ) for dt in dt_range
2023-08-06 16:42:56 -07:00
]
2023-08-08 14:31:17 -07:00
brightness_default_values = [ sun . brightness_pct ( dt , sleep_mode ) for dt in dt_range ]
2023-08-06 16:42:56 -07:00
# Plot the brightness over time for both modes
2023-08-08 14:31:17 -07:00
fig , ax = plt . subplots ( figsize = ( 10 , 6 ) )
ax . plot ( time_range , brightness_linear_values , label = " Linear Mode " )
ax . plot ( time_range , brightness_tanh_values , label = " Tanh Mode " )
2023-08-08 17:34:35 -07:00
ax . plot ( time_range , brightness_default_values , label = " Default Mode " , c = " C5 " )
2023-08-08 14:31:17 -07:00
sunrise_time = sun . sun . sunrise ( dt . date . today ( ) )
sunset_time = sun . sun . sunset ( dt . date . today ( ) )
ax . vlines (
time_to_float ( sunrise_time ) ,
0 ,
100 ,
color = " C2 " ,
label = " Sunrise " ,
linestyles = " dashed " ,
)
ax . vlines (
time_to_float ( sunset_time ) ,
0 ,
100 ,
color = " C3 " ,
label = " Sunset " ,
linestyles = " dashed " ,
)
ax . set_xlim ( 0 , 24 )
ax . set_xticks ( np . arange ( 0 , 25 , 1 ) )
yticks = np . arange ( 0 , 105 , 5 )
ytick_labels = [ f " { label : .0f } % " for label in yticks ]
ax . set_yticks ( yticks , ytick_labels )
ax . set_xlabel ( " Time (hours) " )
ax . set_ylabel ( " Brightness " )
ax . set_title ( " Brightness over Time for Different Modes " )
2023-08-06 16:42:56 -07:00
# Add text box
textstr = " \n " . join (
(
2023-08-08 14:31:17 -07:00
f " Sunrise Time = { sunrise_time . time ( ) } " ,
f " Sunset Time = { sunset_time . time ( ) } " ,
f " Max Brightness = { sun . max_brightness : .0f } % " ,
f " Min Brightness = { sun . min_brightness : .0f } % " ,
f " Time Light = { sun . brightness_mode_time_light } " ,
f " Time Dark = { sun . brightness_mode_time_dark } " ,
2023-08-06 16:42:56 -07:00
) ,
)
2023-08-08 14:31:17 -07:00
ax . legend ( )
2023-08-06 16:42:56 -07:00
2023-08-08 14:31:17 -07:00
ax . text (
2023-08-06 16:42:56 -07:00
0.4 ,
0.55 ,
textstr ,
2023-08-08 14:31:17 -07:00
transform = ax . transAxes ,
2023-08-06 16:42:56 -07:00
fontsize = 10 ,
verticalalignment = " center " ,
2023-08-08 14:31:17 -07:00
bbox = { " boxstyle " : " round " , " facecolor " : " wheat " , " alpha " : 0.5 } ,
2023-08-06 16:42:56 -07:00
)
2023-08-08 17:34:35 -07:00
ax . grid ( visible = True )
2023-08-06 16:42:56 -07:00
2023-08-08 14:31:17 -07:00
return fig
2023-08-08 17:34:35 -07:00
def plot_color_temp ( inputs : dict [ str , Any ] , sleep_mode : bool ) - > plt . Figure :
""" Plot the color temperature over time for different modes. """
sun = SunLightSettings ( * * inputs , brightness_mode = " default " )
2023-08-08 14:31:17 -07:00
dt_range = date_range ( tzinfo = sun . timezone )
time_range = [ time_to_float ( dt ) for dt in dt_range ]
settings = [ sun . brightness_and_color ( dt , sleep_mode ) for dt in dt_range ]
2023-08-10 17:00:16 -07:00
if sleep_mode and sun . sleep_rgb_or_color_temp == " color_temp " :
colors = [
color_temperature_to_rgb ( setting [ " color_temp_kelvin " ] )
for setting in settings
]
else :
colors = [ setting [ " rgb_color " ] for setting in settings ]
color_temp_values = np . array ( [ ( * col , 255 ) for col in colors ] ) / 255
2023-08-08 14:31:17 -07:00
color_temp_values = color_temp_values . reshape ( - 1 , 1 , 4 )
sun_position = [ setting [ " sun_position " ] for setting in settings ]
fig , ax = plt . subplots ( figsize = ( 10 , 6 ) )
# Display as a horizontal bar
ax . imshow (
np . rot90 ( color_temp_values ) [ : , : : 1 ] ,
aspect = " auto " ,
extent = [ 0 , 24 , - 1 , 1 ] ,
origin = " upper " ,
)
# Plot a curve on top of the imshow
ax . plot ( time_range , sun_position , color = " k " , label = " Sun Position " )
sunrise_time = sun . sun . sunrise ( dt . date . today ( ) )
sunset_time = sun . sun . sunset ( dt . date . today ( ) )
ax . vlines (
time_to_float ( sunrise_time ) ,
- 1 ,
1 ,
color = " C2 " ,
label = " Sunrise " ,
linestyles = " dashed " ,
)
ax . vlines (
time_to_float ( sunset_time ) ,
- 1 ,
1 ,
color = " C3 " ,
label = " Sunset " ,
linestyles = " dashed " ,
)
ax . set_xlim ( 0 , 24 )
ax . set_xticks ( np . arange ( 0 , 25 , 1 ) )
yticks = np . arange ( - 1 , 1.1 , 0.1 )
ax . set_yticks ( yticks , [ f " { label * 100 : .0f } % " for label in yticks ] )
ax . set_xlabel ( " Time (hours) " )
ax . legend ( )
ax . set_ylabel ( " Sun position ( % ) " )
ax . set_title ( " RGB Color Intensity over Time " )
2023-08-08 17:34:35 -07:00
ax . grid ( visible = False )
2023-08-08 14:31:17 -07:00
return fig
2023-08-06 16:42:56 -07:00
2023-08-06 17:20:58 -07:00
SEC_PER_HR = 60 * 60
2023-08-08 17:34:35 -07:00
desc_top = """
2023-08-06 17:20:58 -07:00
* * Experience the Dynamics of [ Adaptive Lighting ] ( https : / / github . com / basnijholt / adaptive - lighting ) in Real - Time . * *
Have you ever wondered how the intricate settings of [ Adaptive Lighting ] ( https : / / github . com / basnijholt / adaptive - lighting ) impact your home ambiance ? The Adaptive Lighting Simulator WebApp is here to demystify just that .
2023-08-08 17:34:35 -07:00
( More text below the plots )
"""
desc_bottom = """
2023-08-06 17:20:58 -07:00
Harnessing the technology of the popular Adaptive Lighting integration for Home Assistant , this webapp provides a hands - on , visual platform to explore , tweak , and understand the myriad of parameters that dictate the behavior of your smart lights . Whether you ' re aiming for a subtle morning glow or a cozy evening warmth, observe firsthand how each tweak changes the ambiance.
* * Why Use the Simulator ? * *
- * * Interactive Exploration * * : No more guesswork . See in real - time how changes to settings influence the lighting dynamics .
- * * Circadian Cycle Preview * * : Understand how Adaptive Lighting adjusts throughout the day based on specific parameters , ensuring your lighting aligns with your circadian rhythms .
- * * Tailored Testing * * : Play with parameters and find the perfect combination that suits your personal or family ' s needs.
- * * Educational Experience * * : For both newbies and experts , delve deep into the intricacies of Adaptive Lighting ' s logic and potential.
Dive into the simulator , experiment with different settings , and fine - tune the behavior of Adaptive Lighting to perfection . Whether you ' re setting it up for the first time or optimizing an existing setup, this tool ensures you get the most out of your smart lighting experience.
"""
# Shiny UI
app_ui = ui . page_fluid (
ui . panel_title ( " 🌞 Adaptive Lighting Simulator WebApp 🌛 " ) ,
ui . layout_sidebar (
2024-12-05 14:35:19 -08:00
ui . sidebar (
2023-08-08 17:34:35 -07:00
ui . input_switch ( " adapt_until_sleep " , " adapt_until_sleep " , value = False ) ,
ui . input_switch ( " sleep_mode " , " sleep_mode " , value = False ) ,
2023-08-08 14:31:17 -07:00
ui . input_slider ( " min_brightness " , " min_brightness " , 1 , 100 , 30 , post = " % " ) ,
ui . input_slider ( " max_brightness " , " max_brightness " , 1 , 100 , 100 , post = " % " ) ,
ui . input_numeric ( " min_color_temp " , " min_color_temp " , 2000 ) ,
ui . input_numeric ( " max_color_temp " , " max_color_temp " , 6666 ) ,
ui . input_slider (
2023-08-08 17:34:35 -07:00
" sleep_brightness " ,
" sleep_brightness " ,
1 ,
100 ,
1 ,
post = " % " ,
2023-08-08 14:31:17 -07:00
) ,
ui . input_radio_buttons (
" sleep_rgb_or_color_temp " ,
" sleep_rgb_or_color_temp " ,
[ " rgb_color " , " color_temp " ] ,
) ,
ui . input_numeric ( " sleep_color_temp " , " sleep_color_temp " , 2000 ) ,
ui . input_text ( " sleep_rgb_color " , " sleep_rgb_color " , " 255,0,0 " ) ,
2023-08-06 17:20:58 -07:00
ui . input_slider (
" brightness_mode_time_dark " ,
2023-08-08 14:31:17 -07:00
" brightness_mode_time_dark " ,
1 ,
2023-08-06 17:20:58 -07:00
5 * SEC_PER_HR ,
3 * SEC_PER_HR ,
post = " sec " ,
) ,
ui . input_slider (
" brightness_mode_time_light " ,
2023-08-08 14:31:17 -07:00
" brightness_mode_time_light " ,
1 ,
2023-08-06 17:20:58 -07:00
5 * SEC_PER_HR ,
0.5 * SEC_PER_HR ,
post = " sec " ,
) ,
ui . input_slider (
" sunrise_time " ,
" sunrise_time " ,
0 ,
24 ,
6 ,
step = 0.5 ,
post = " hr " ,
) ,
ui . input_slider (
" sunset_time " ,
" sunset_time " ,
0 ,
24 ,
18 ,
step = 0.5 ,
post = " hr " ,
) ,
) ,
2024-12-05 14:35:19 -08:00
ui . markdown ( desc_top ) ,
ui . output_plot ( id = " brightness_plot " ) ,
ui . output_plot ( id = " color_temp_plot " ) ,
ui . markdown ( desc_bottom ) ,
2023-08-06 17:20:58 -07:00
) ,
2024-12-05 14:35:19 -08:00
theme = shinyswatch . theme . sandstone ,
2023-08-06 17:20:58 -07:00
)
2023-08-08 14:31:17 -07:00
def float_to_time ( value : float ) - > dt . time :
2023-08-08 17:34:35 -07:00
""" Convert a float to a time object. """
2023-08-08 14:31:17 -07:00
hours = int ( value )
minutes = int ( ( value - hours ) * 60 )
2023-08-08 17:34:35 -07:00
return dt . time ( hours , minutes )
2023-08-08 14:31:17 -07:00
def time_to_float ( time : dt . time | dt . datetime ) - > float :
2023-08-08 17:34:35 -07:00
""" Convert a time object to a float. """
2023-08-08 14:31:17 -07:00
return time . hour + time . minute / 60
def _kw ( input ) :
2023-08-08 17:34:35 -07:00
return {
" name " : " Adaptive Lighting Simulator " ,
" adapt_until_sleep " : input . adapt_until_sleep ( ) ,
" max_brightness " : input . max_brightness ( ) ,
" min_brightness " : input . min_brightness ( ) ,
" min_color_temp " : input . min_color_temp ( ) ,
" max_color_temp " : input . max_color_temp ( ) ,
" sleep_brightness " : input . sleep_brightness ( ) ,
" sleep_rgb_or_color_temp " : input . sleep_rgb_or_color_temp ( ) ,
" sleep_color_temp " : input . sleep_color_temp ( ) ,
" sleep_rgb_color " : [ int ( x ) for x in input . sleep_rgb_color ( ) . split ( " , " ) ] ,
" sunrise_time " : float_to_time ( input . sunrise_time ( ) ) ,
" sunset_time " : float_to_time ( input . sunset_time ( ) ) ,
" brightness_mode_time_dark " : dt . timedelta (
seconds = input . brightness_mode_time_dark ( ) ,
2023-08-08 14:31:17 -07:00
) ,
2023-08-08 17:34:35 -07:00
" brightness_mode_time_light " : dt . timedelta (
seconds = input . brightness_mode_time_light ( ) ,
2023-08-08 14:31:17 -07:00
) ,
2023-08-08 17:34:35 -07:00
" sunrise_offset " : dt . timedelta ( 0 ) ,
" sunset_offset " : dt . timedelta ( 0 ) ,
" min_sunrise_time " : None ,
" max_sunrise_time " : None ,
" min_sunset_time " : None ,
" max_sunset_time " : None ,
fix: replace deprecated `get_astral_location` with `get_astral_observer` (#1482)
* fix: replace deprecated get_astral_location with get_astral_observer (#1481)
HA 2026.7 deprecates homeassistant.helpers.sun.get_astral_location
(removal planned for 2027.7) in favor of get_astral_observer, causing a
deprecation warning in the HA logs.
- Switch SunEvents/SunLightSettings from astral.location.Location to
astral.Observer, using the astral.sun module functions (which return
UTC times by default, matching the previous local=False calls).
- Use get_astral_observer in switch.py, with a fallback for HA < 2026.7
that constructs the Observer directly from the HA config.
- Update tests and the webapp simulator accordingly.
* ci: handle removal of requirements_test_all.txt in HA 2026.8 dev
HA core removed requirements_test_all.txt (home-assistant/core#171530),
which made test_dependencies.py crash with FileNotFoundError and broke
the dev pytest job and the Docker builds. Fall back to
requirements_all.txt, which carries the same per-integration
'# homeassistant.components.x' annotations. Also extend the
aiohasupervisor pin lookup in scripts/setup-dependencies accordingly.
* test: support modern template light config for HA 2026.6+
HA 2026.6 removed the legacy `light: platform: template` YAML format
(home-assistant/core#169615), so setup_lights found no template platform
on HA dev and every test using it failed with IndexError. Detect legacy
support at runtime (PLATFORM_SCHEMA presence) and fall back to the
modern `template:` config format. The group platform is set up before
the template integration in the modern path, because setting up
`template` also sets up the `light` domain, which would make a later
async_setup_component(hass, LIGHT_DOMAIN, ...) a no-op.
2026-07-01 23:00:35 -07:00
" astral_observer " : Observer ( ) ,
" timezone " : dt . timezone . utc ,
2023-08-08 17:34:35 -07:00
}
2023-08-08 14:31:17 -07:00
2023-08-08 17:34:35 -07:00
def server ( input , output , session ) : # noqa: ARG001
""" Shiny server. """
2023-08-08 14:31:17 -07:00
2023-08-06 17:20:58 -07:00
@output
@render.plot
def brightness_plot ( ) :
2023-08-08 14:31:17 -07:00
return plot_brightness ( _kw ( input ) , sleep_mode = input . sleep_mode ( ) )
@output
@render.plot
def color_temp_plot ( ) :
return plot_color_temp ( _kw ( input ) , sleep_mode = input . sleep_mode ( ) )
2023-08-06 17:20:58 -07:00
2023-08-06 16:42:56 -07:00
app = App ( app_ui , server )