Light fader with curves: linear, exponential and smooth

Hi everyone,
I’m new to Home Assistant, but I am experienced in programming. So to learn python scripting in HA, I had to start somewhere.

I found the script “Light Fader by Transition Time” by mrsnyds to implement a fade effect. It bugged me the fade is linear. In lighting, it is common to use a curved correction, as a low light in a darkened room is perceived brighter. But I couldn’t find that, and I just went from there.

I implemented these curves, but you can easily add your own.

curves

I could really need some help on how to make this async. I understand I require AppDeamon, but my learning-curve (pun intended) is not there yet.

Cheers, Ingrid

Edit: large update, version 2.0

  • input checks and debugger. Who know how to throw an exception? I would rather quit the script than put the whole thing in an else statement, which I did now.
  • when changing a state, it takes about a sec before the new state is available for readout, but the script can change them very rapidly. To be able to check if the lamp is manually changed, to break the script, I implemented allowed lag time. The shorter duration of the fade, the more changes per timeframe, the more lag steps are allowed. With a duration of 10sec, lag time is about 2 sec, you won’t be using quick fades, then lag time can be decreased.
  • When light is off, and temperature_start or temperature_end is not defined, the min/max mireds average will be default.
    2.0.1
  • small change, added a round t_cur = round ((states.attributes.get('min_mireds') + states.attributes.get('max_mireds')) / 2)
#   smooth_fader.py
#   Version 2.0.1
#   By Ingrid Bakker studioilb.nl
#
#--------------------------------------
#
#  HOW TO CALL
#   service: python_script.smooth_fader
#   data:
#     entity_id: light.entity ; required
#     duration: '00:00:00' ; required, time under 10 seconds is not advised
#     brightness_start: 0-255 ; default: current
#     brightness_end: 0-255 ; default: current
#     brightness_curve: 'linear' or 'exp2' or 'exp5' or 'smooth' ; default: exp5
#     temperature_start: 154-370 ; default: current 
#     temperature_end: 154-370 ; default: current
#     temperature_curve: 'linear' or 'exp2' or 'exp5' or 'smooth' ; default: exp2
#
#  When light is off, and brighness_start or brighness_end is not defined, the light 
#  will remain off.
#  When light is off, and temperature_start or temperature_end is not defined, the
#  min/max mireds average will be default.
#
#  NOTES
#   adapted from https://community.home-assistant.io/t/light-fader-by-duration-time/99600
#   adapted from https://stackoverflow.com/questions/978599/equation-to-calculate-different-speeds-for-smooth-animation
#   FUNCTION: linear
#   round(start+direction*((x-1)*(difference/steps)+1))
#   50% effect a 50% of time
#   FUNCTION: exp2
#   n = 2, round(abs((direction*start)+(((difference^(1/n)-1)/(steps-1))*(x-1-1)+1)^n))
#   50% effect at 69% time
#   FUNCTION: exp5
#   n = 5, round(abs((direction*start)+(((difference^(1/n)-1)/(steps-1))*(x-1-1)+1)^n))
#   50% effect at 81% time
#   FUNCTION: slow
#   round(abs(direction*start+difference^(x/steps)))
#   50% effect at 88% time

# FOR debugging
debug_report = 0 # 0 = errors only; 1 = change events only; 2 = full
system_lag_time = 2 # Estimation how quick the a new state readout will be available.

# INPUT
entity_id = data.get ('entity_id')
duration = data.get ('duration')
b_start = int (data.get ('brightness_start', -1))
b_end = int (data.get ('brightness_end', -1))
b_curve = data.get ('brightness_curve', 'exp5')
t_start = int (data.get ('temperature_start', -1))
t_end = int (data.get ('temperature_end', -1))
t_curve = data.get ('temperature_curve', 'exp2')
if (debug_report > 1) : logger.info ("Curved fader: entity_id: %s, duration: %s, b_start: %s, b_end: %s, b_curve: %s, t_start: %s, t_end: %s, t_curve: %s", entity_id, duration, b_start, b_end, b_curve, t_start, t_end, t_curve)

# INPUT REQUIREMENTS
if ((entity_id is None) or (duration is None) or (max (b_end, t_end)==-1)) :
  logger.error ("Curved fader: Entity and duration are required, einther brightness_end or temperature_end should be defined")
else :
  duration = int (duration[:2]) * 3600 + int (duration[3:5]) * 60 + int (duration[-2:])
  # GET CURRENT STATE
  states = hass.states.get (entity_id)
  b_cur = b_initial = states.attributes.get ('brightness') or 0
  t_cur = t_initial = states.attributes.get ('color_temp') or 0
  if (b_start < 1 and b_end < 1) :
    logger.error ("Curved fader: When light is off or brighness_start and brighness_end is not defined or 0, the light will remain off, please define brightness")
  if (b_cur == 0 and t_start == -1) :
    logger.warning ("Curved fader: When light is off, and temperature_start is not defined, the min/max mireds average will be default, please define temperature_start.")
    t_cur = round ((states.attributes.get('min_mireds') + states.attributes.get('max_mireds')) / 2)

  if (b_start == -1) : b_start = b_cur
  if (b_end == -1) : b_end = b_start
  if (t_start == -1) : t_start = t_cur
  if (t_end == -1) : t_end = t_start
  if (debug_report > 1) : logger.info ("Curved fader: brightness current: %s, temperature current: %s", b_cur, t_cur)
  # MATH FOR CURVE
  curve = {}
  curve['linear'] = lambda x, steps, start, end, dif, dir : round (start + dir * ((x - 1) * (dif / steps) + 1))
  curve['exp2'] = lambda x, steps, start, end, dif, dir : round (abs ((dir * start) + (((dif ** (1 / 2) - 1) / (steps - 1)) * (x - 1 - 1) + 1) ** 2))
  curve['exp5'] = lambda x, steps, start, end, dif, dir : round (abs ((dir * start) + (((dif ** (1 / 5) - 1) / (steps - 1)) * (x - 1 - 1) + 1) ** 5))
  curve['smooth'] = lambda x, steps, start, end, dif, dir : round (abs (dir * start + dif ** (x / steps)))
  b_dif = abs (b_end - b_start)
  t_dif = abs (t_end - t_start)
  b_dir = t_dir = 1
  if (b_end < b_start) : b_dir = -1
  if (t_end < t_start) : t_dir = -1
  x = 0
  steps = max (1, b_dif, t_dif) # when no change is detected steps = 0, prevent error devide by zero
  step_time = duration / steps
  lag_steps_allowed = round (system_lag_time / step_time)

  if (debug_report > 1) : logger.info ("Curved fader: steps: %s, step_time: %s, lag_steps_allowed: %s", steps, step_time, lag_steps_allowed)
  if (debug_report > 1) : logger.info ("Curved fader: b_curve: %s, b_start: %s, b_end: %s, b_dif %s, b_dir %s, t_curve: %s, t_start: %s, t_end: %s, t_dif %s, t_dir %s", b_curve, b_start, b_end, b_dif, b_dir, t_curve, t_start, t_end, t_dif, t_dir)
  b_curve = curve[b_curve]
  t_curve = curve[t_curve]
  b_new = b_last = b_lag = b_start
  t_new = t_last = b_lag = t_start

  data = { "entity_id" : entity_id, "brightness" : b_new, "color_temp" : t_new }
  hass.services.call('light', 'turn_on', data)
  lag = {}
  lag[x] = {"b": b_new, "t": t_new}

  while (b_new != b_end) or (t_new != t_end) :
    x = x + 1
    xlag = max(0, x - lag_steps_allowed)
    if (x > 400) : # runtime protector
      logger.critical ('Curved fader: Break, runtime exceeded.')
      break
    if (b_dif > 0) : b_new = round(b_curve(x, steps, b_start, b_end, b_dif, b_dir))
    if (t_dif > 0) : t_new = round(t_curve(x, steps, t_start, t_end, t_dif, t_dir))
    lag[x] = {"b": b_new, "t": t_new}

    states = hass.states.get(entity_id)
    b_cur = states.attributes.get('brightness') or 0
    t_cur = states.attributes.get('color_temp') or 0

    #  Because the script runs synchronous and there is no button to stop a running
    #  python script, we need a break.
    #  For some reasone brightness under 25 is not registerd as an state, but it is
    #  visible in my light. So I do use it, but exclude it from this break. If you
    #  want to break the script, elevate the brightness above 25.
    #  In fast transitions the readout of the new state is to slow to check against.
    #  So when step_time < system_lag_time, break when current is not between lag
    # and last. Don't break when initial value is not changed yet.

    if (debug_report > 1) : logger.info("x: %s, b_cur: %s, b_last: %s, b_lag: %s, b_new: %s, t_cur: %s, t_last: %s, t_lag: %s, t_new: %s", x, b_cur, b_last, lag[xlag]["b"], b_new, t_cur, t_last, lag[xlag]["t"], t_new)
    if (x > 0 and b_cur > 24 and ((b_cur - lag[xlag]["b"]) * b_dir < 0 or (b_cur - b_last) * b_dir * -1 < 0 or (t_cur - lag[xlag]["t"]) * t_dir < 0 or (t_cur - t_last) * t_dir * -1 < 0) and b_cur != b_initial and t_cur != t_initial) :
      if ((b_cur - lag[xlag]["b"]) * b_dir < 0 or (t_cur - lag[xlag]["t"]) * t_dir < 0) :          
        logger.error ("Curved fader: Break because system_lag_time is set to low, please increase value. The lagere the difference between cur and lag, the larger the increment.")
        logger.error ("Lag_steps_allowed: %s, x: %s, b_cur: %s, b_lag: %s, t_cur: %s, t_lag: %s", lag_steps_allowed, x, b_cur, lag[xlag]["b"], t_cur, lag[xlag]["t"])
      else :
        logger.info ("Curved fader: Break by external change.")
      break

    if ((b_new != b_last) or (t_new != t_last)):
      data = { "entity_id" : entity_id, "brightness" : b_new, "color_temp" : t_new }
      hass.services.call('light', 'turn_on', data)
      if (debug_report > 0) : logger.info("Setting %s: brightness from %s to %s and color from %s to %s", entity_id, b_last, b_new, t_last, t_new)
      b_last = b_new
      t_last = t_new
    time.sleep(step_time)

logger.info ("Curved fader: finished.")
2 Likes

Hi. I am giving this a test, but getting an error. Are my test settings incorrect?

error:

testing in developer:

Remove the words “string( )”, its already one.
And you should give it any direction to go. Either a brightness_end or a temperature_end, otherwise, nothing will change.

ok. I have removed string, but still getting same erorr.


service: python_script.curved_fader
   data:
     entity_id: light.computer_room
     duration: ('00:05:00')

ok. hold on. You edited your post. will give the brightness_end a try.

Remove parenthesis, it should look like ‘00:00:00’. And add either brightness_end or temperature_end.
I will change my original post to make this more clear.

Sorry, can you give me a sample of what I need to add for a fade out scenario? This is what I have tried so far and I get the same error message.

service: python_script.curved_fader
   data:
     entity_id: light.computer_room
     brightness_start: 255
     brightness_end: 0
     brightness_curve: exp5
     duration: '00:05:00'

The input is correct. But when I look at your error message, it points to line 44:
states = hass.states.get(entity_id)

This means your entity is not properly defined. Is ‘light.computer_room’ the right entity?

yes, I have a light.computer_room

I tested it with multiple lights in my house, but did not encounter this problem. More testing will be done.

I also tried with 3 other light entities around the house which are different model and getting same error message.

Found it! It’s an indentation error.
“service” and “data” should be on the same level.

service: python_script.curved_fader
data:
  entity_id: light.bedlampjes
  brightness_start: 255
  brightness_end: 0
  brightness_curve: exp5
  duration: '00:00:10'

ok. Moved the indentation and now this error.

Error executing script: value must be at least 1 for dictionary value @ data['color_temp']
   service: python_script.curved_fader
   data:
     entity_id: light.computer_room
     brightness_start: 255
     brightness_end: 0
     brightness_curve: exp5
     duration: '00:05:00'

Found that one too, already edited the original post to add two lines.

if(b_start<1):b_start=1
if(t_start<1):t_start=1

Above the b_new = b_last = b_start.

Thank you for helping my debug! Your kinda my betatester…

No problem.

Found that one too, already edited the original post to add two lines.

I have edited the new code and reload the script.

Test with these settings. Lights turn full on, but does not fade out. Stays at 255 brightness. There are no more errors in log.


   service: python_script.curved_fader
   data:
     entity_id: light.computer_room
     brightness_start: 255
     brightness_end: 0
     brightness_curve: exp5
     duration: '00:05:00'

The error lies somewhere within either defined startpoits or not defined, and with current light on or off.
Will revisit tomorrow.

ok.

Here is an error with the lights on while running the script.

   service: python_script.curved_fader
   data:
     entity_id: light.computer_room
    brightness_end: 0
     brightness_start: 255
     brightness_curve: exp5
     duration: '00:05:00'

Updated my original post, version 2.

Trying V2 and it’s fading out. Seems to be working with some issues.

fading out: does not turn off completely after the duration expires.

service: python_script.smooth_fader
data:
  entity_id: light.computer_room
  duration: '00:05:00'
  brightness_start: 255     
  brightness_end: 0

fading in: Seems to work only if the light is on when script starts. If the light is off and script starts, it stays at the 0 position. The slider may have move slightly up. No more.
Also, I noticed with the lights in off and the script starts, it takes a few seconds before it dims down to 0.

service: python_script.smooth_fader
data:
  entity_id: light.computer_room
  duration: '00:05:00'
  brightness_start: 0     
  brightness_end: 255

First one, fading out: start with light on or off? I cannot reproduce your error. Maybe your light doesn’t turn off when brightness is set to zero? Possibly I need to add a “state off” statement?

Second one is perhaps same thing? If your light doesn’t respond to brightness 1 when state is still off, nothing will happen?

Third point: I guess this is fading out again. This is because of the curve, standard exp5, so 50% effect 81% of the time. Maybe choose linear of exp2 for now, I will implement other curves later on.

Please set the debug to 1 and add your log.

-Ingrid

Test again with log set to 2 now. Script seems to work if the light is initial on. The lights I am testing are Ikea GU10 track light.

Fading out: initial light on [seems to work, but doesn’t turn off completely.]

error:
Logger: homeassistant.core
Source: components/hue/bridge.py:138
First occurred: 6:58:46 AM (1 occurrences)
Last logged: 6:58:46 AM

Error executing service: <ServiceCall light.turn_on (c:2166f72589bf1afb7cfbe02eb5695c4d): entity_id=['light.computer_room'], params=brightness=7, color_temp=326>
Traceback (most recent call last):
  File "/usr/src/homeassistant/homeassistant/core.py", line 1511, in catch_exceptions
    await coro_or_task
  File "/usr/src/homeassistant/homeassistant/core.py", line 1530, in _execute_service
    await handler.job.target(service_call)
  File "/usr/src/homeassistant/homeassistant/helpers/entity_component.py", line 213, in handle_service
    await self.hass.helpers.service.entity_service_call(
  File "/usr/src/homeassistant/homeassistant/helpers/service.py", line 667, in entity_service_call
    future.result()  # pop exception if have
  File "/usr/src/homeassistant/homeassistant/helpers/entity.py", line 863, in async_request_call
    await coro
  File "/usr/src/homeassistant/homeassistant/helpers/service.py", line 704, in _handle_entity_call
    await result
  File "/usr/src/homeassistant/homeassistant/components/light/__init__.py", line 496, in async_handle_light_on_service
    await light.async_turn_on(**filter_turn_on_params(light, params))
  File "/usr/src/homeassistant/homeassistant/components/hue/light.py", line 519, in async_turn_on
    await self.bridge.async_request_call(
  File "/usr/src/homeassistant/homeassistant/components/hue/bridge.py", line 138, in async_request_call
    return await task()
  File "/usr/local/lib/python3.9/site-packages/aiohue/groups.py", line 112, in set_action
    await self._request("put", "groups/{}/action".format(self.id), json=data)
  File "/usr/local/lib/python3.9/site-packages/aiohue/bridge.py", line 124, in request
    _raise_on_error(data)
  File "/usr/local/lib/python3.9/site-packages/aiohue/bridge.py", line 245, in _raise_on_error
    raise_error(data["error"])
  File "/usr/local/lib/python3.9/site-packages/aiohue/errors.py", line 25, in raise_error
    raise cls("{}: {}".format(type, error["description"]))
aiohue.errors.AiohueException: 901: Internal error, 404

Fading in: initial light off [doesn’t seem to work. Stays on with no fading.]

Lag_steps_allowed: 2, x: 2, b_cur: 254, b_lag: 255, t_cur: 326, t_lag: 326.5
7:06:09 AM – (ERROR) Python Scripts

Curved fader: Break because system_lag_time is set to low, please increase value. The lagere the difference between cur and lag, the larger the increment.
7:06:09 AM – (ERROR) Python Scripts

Curved fader: When light is off, and temperature_start is not defined, the min/max mireds average will be default, please define temperature_start.
7:06:08 AM – (WARNING) Python Scripts

Fading in: initial light on [Seems to work. Though I see this error which reference the light that I am testing on. Not sure if it is releated to your script.]

Logger: homeassistant.core
Source: components/hue/bridge.py:138
First occurred: 7:21:42 AM (2 occurrences)
Last logged: 7:22:00 AM

Error executing service: <ServiceCall light.turn_on (c:f5bba7ddbf96740e609cef75df3a2479): entity_id=['light.computer_room'], params=brightness=203, color_temp=326>
Error executing service: <ServiceCall light.turn_on (c:85e1fa49bd94158bdaa8a2ed9d1971ad): entity_id=['light.computer_room'], params=brightness=248, color_temp=326>
Traceback (most recent call last):
  File "/usr/src/homeassistant/homeassistant/core.py", line 1511, in catch_exceptions
    await coro_or_task
  File "/usr/src/homeassistant/homeassistant/core.py", line 1530, in _execute_service
    await handler.job.target(service_call)
  File "/usr/src/homeassistant/homeassistant/helpers/entity_component.py", line 213, in handle_service
    await self.hass.helpers.service.entity_service_call(
  File "/usr/src/homeassistant/homeassistant/helpers/service.py", line 667, in entity_service_call
    future.result()  # pop exception if have
  File "/usr/src/homeassistant/homeassistant/helpers/entity.py", line 863, in async_request_call
    await coro
  File "/usr/src/homeassistant/homeassistant/helpers/service.py", line 704, in _handle_entity_call
    await result
  File "/usr/src/homeassistant/homeassistant/components/light/__init__.py", line 496, in async_handle_light_on_service
    await light.async_turn_on(**filter_turn_on_params(light, params))
  File "/usr/src/homeassistant/homeassistant/components/hue/light.py", line 519, in async_turn_on
    await self.bridge.async_request_call(
  File "/usr/src/homeassistant/homeassistant/components/hue/bridge.py", line 138, in async_request_call
    return await task()
  File "/usr/local/lib/python3.9/site-packages/aiohue/groups.py", line 112, in set_action
    await self._request("put", "groups/{}/action".format(self.id), json=data)
  File "/usr/local/lib/python3.9/site-packages/aiohue/bridge.py", line 124, in request
    _raise_on_error(data)
  File "/usr/local/lib/python3.9/site-packages/aiohue/bridge.py", line 245, in _raise_on_error
    raise_error(data["error"])
  File "/usr/local/lib/python3.9/site-packages/aiohue/errors.py", line 25, in raise_error
    raise cls("{}: {}".format(type, error["description"]))
aiohue.errors.AiohueException: 901: Internal error, 404

Fading in: initial light off [when script starts, the lights turn on at 255. Takes a few seconds to drop down to 0 as it should start at 0 to begin with. The dim slider moves about an inch, but doesn’t seem to be fading in to 255. Lights stays at 0 or low. The error suggesting to increase the delay which is set at 2 (default)]

Curved fader: When light is off, and temperature_start is not defined, the min/max mireds average will be default, please define temperature_start.
7:26:46 AM – (WARNING) Python Scripts

Lag_steps_allowed: 2, x: 116, b_cur: 25, b_lag: 24, t_cur: 326, t_lag: 326.5
7:29:02 AM – (ERROR) Python Scripts

Curved fader: Break because system_lag_time is set to low, please increase value. The lagere the difference between cur and lag, the larger the increment.
7:29:02 AM – (ERROR) Python Scripts