Drift in hue and saturation of Ikea Tradfiri bulb

I’ve written the following automation that generally works well for using a remote control with four buttons to control the hue and saturation of my Ikea Tradfiri color LED bulb. However, there seems to be some issue with round-trip conversion because when I repeatedly press the right/left buttons to change the hue, the saturation slowly decreases (e.g., it should remain at 100, but it slowly drops from 100 to 98.8 to 97.6 and so on each time the hue changes until the saturation gets close to 0). After doing some research, I believe this has to do with some rounding errors. Home Assistant maps hue and saturation to a narrower range of values than Zigbee’s color cluster. While this is not a show-stopper, it’s a bit annoying as I would like to be able to cycle through all the colors using the remote without the saturation dropping. Any thoughts on how I can fix this?

automation:
  - id: "light_color_cycle"
    alias: "Reading light color"
    trigger:
      - platform: device
        device_id: 1f5b19e8dcb1889edffcadd2ab8b558c
        domain: zha
        type: remote_button_short_press
        subtype: left
        id: hue_down
      - platform: device
        device_id: 1f5b19e8dcb1889edffcadd2ab8b558c
        domain: zha
        type: remote_button_short_press
        subtype: right
        id: hue_up
      - platform: device
        device_id: 1f5b19e8dcb1889edffcadd2ab8b558c
        domain: zha
        type: remote_button_short_press
        subtype: turn_on
        id: sat_up
      - platform: device
        device_id: 1f5b19e8dcb1889edffcadd2ab8b558c
        domain: zha
        type: remote_button_short_press
        subtype: turn_off
        id: sat_down
      - platform: device
        device_id: 1f5b19e8dcb1889edffcadd2ab8b558c
        domain: zha
        type: remote_button_long_press
        subtype: dim_up
        id: sat_max
      - platform: device
        device_id: 1f5b19e8dcb1889edffcadd2ab8b558c
        domain: zha
        type: remote_button_long_press
        subtype: dim_down
        id: sat_min
    action:
      service: light.turn_on
      entity_id: light.reading_nook
      data:
        hs_color: >-
          {% set hue = int(state_attr('light.reading_nook', 'hs_color')[0]) %}
          {% set sat = int(state_attr('light.reading_nook', 'hs_color')[1]) %}
          {% if trigger.id == 'hue_down' %}
            {% set hue = (hue - 9) % 360 %}
          {% elif trigger.id == 'hue_up' %}
            {% set hue = (hue + 9) % 360 %}
          {% elif trigger.id == 'sat_up' %}
            {% set sat = min(sat + 5, 100) %}
          {% elif trigger.id == 'sat_down' %}
            {% set sat = max(sat - 5, 0) %}
          {% elif trigger.id == 'sat_max' %}
            {% set sat = 100 %}
          {% elif trigger.id == 'sat_min' %}
            {% set sat = 0 %}
          {% endif %}
          [{{ hue|int }}, {{ sat|int }}]

How about when a button is pressed for the hue, first before changing the hue, just put the saturation into a variable that you then use to adjust the saturation after the hue gets changed?

What is the native color space of the bulb? I don’t have any Trådfri color bulbs (only bulbs which can only do temperature and/or brightness), but all my Philips Hue bulbs use XY color for their native space.

Either do as above, store your expected saturation in a number helper, and always base your changes on that rather than the actual value on the bulb. Or do your own rounding, to the nearest multiple of 5. If the saturation is 98.8, round it back up to 100 when changing the hue.

this would be supported on an Ikea light

min_color_temp_kelvin=2000, max_color_temp_kelvin=6535, min_mireds=153, max_mireds=500, supported_color_modes=[<ColorMode.COLOR_TEMP: 'color_temp'>, <ColorMode.HS: 'hs'>], color_mode=hs, brightness=73.95, color_temp_kelvin=None, color_temp=None, hs_color=(8, 65), rgb_color=(255, 111, 89), xy_color=(0.583, 0.326)

so yeah do as @KruseLuds suggests, copy the relevant value from the Hue light and sync it to the Ikea lights upon change

Thanks everyone! I created two variables to save the current hue/saturation of the Ikea light. I manipulate those variables directly and then, at the end of the automation, set the light color to the variables. Here’s my updated code. It’s a little complicated because I want to manipulate two different Ikea Tradfiri bulbs – they are controlled by a regular on/off switch, so only one of them may be actually on at any given time (hence the parallel execution).

input_number:
  ikea_light_hue:
    name: Ikea light hue
    min: 0
    max: 360

  ikea_light_saturation:
    name: Ikea light saturation
    min: 0
    max: 100

automation:
  - id: "light_color_cycle"
    alias: "Reading light color"
    mode: restart
    trigger:
      - platform: device
        device_id: 1f5b19e8dcb1889edffcadd2ab8b558c
        domain: zha
        type: remote_button_short_press
        subtype: left
        id: hue_down
      - platform: device
        device_id: 1f5b19e8dcb1889edffcadd2ab8b558c
        domain: zha
        type: remote_button_short_press
        subtype: right
        id: hue_up
      - platform: device
        device_id: 1f5b19e8dcb1889edffcadd2ab8b558c
        domain: zha
        type: remote_button_short_press
        subtype: turn_on
        id: sat_up
      - platform: device
        device_id: 1f5b19e8dcb1889edffcadd2ab8b558c
        domain: zha
        type: remote_button_short_press
        subtype: turn_off
        id: sat_down
      - platform: device
        device_id: 1f5b19e8dcb1889edffcadd2ab8b558c
        domain: zha
        type: remote_button_long_press
        subtype: dim_up
        id: sat_max
      - platform: device
        device_id: 1f5b19e8dcb1889edffcadd2ab8b558c
        domain: zha
        type: remote_button_long_press
        subtype: dim_down
        id: sat_min
      - platform: device
        device_id: 1f5b19e8dcb1889edffcadd2ab8b558c
        domain: zha
        type: remote_button_long_press
        subtype: left
        id: colorloop
      - platform: device
        device_id: 1f5b19e8dcb1889edffcadd2ab8b558c
        domain: zha
        type: remote_button_long_press
        subtype: right
        id: colorloop
    action: 
      - choose:
          - conditions:
             - condition: template
               value_template: "{{ trigger.id == 'hue_down' }}"
            sequence:
              - service: input_number.set_value
                target:
                  entity_id: input_number.ikea_light_hue
                data: 
                  value: "{{ (states('input_number.ikea_light_hue')|int - 9) % 360 }}"
          - conditions:
             - condition: template
               value_template: "{{ trigger.id == 'hue_up' }}"
            sequence:
              - service: input_number.set_value
                target:
                  entity_id: input_number.ikea_light_hue
                data: 
                  value: "{{ (states('input_number.ikea_light_hue')|int + 9) % 360 }}"
          - conditions:
             - condition: template
               value_template: "{{ trigger.id == 'hue_down' }}"
            sequence:
              - service: input_number.set_value
                target:
                  entity_id: input_number.ikea_light_hue
                data: 
                  value: "{{ (states('input_number.ikea_light_hue')|int - 9) % 360 }}"
          - conditions:
             - condition: template
               value_template: "{{ trigger.id == 'hue_up' }}"
            sequence:
              - service: input_number.set_value
                target:
                  entity_id: input_number.ikea_light_hue
                data: 
                  value: "{{ (states('input_number.ikea_light_hue')|int + 9) % 360 }}"
          - conditions:
             - condition: template
               value_template: "{{ trigger.id == 'sat_down' }}"
            sequence:
              - service: input_number.set_value
                target:
                  entity_id: input_number.ikea_light_saturation
                data:
                  value: "{{ max(states('input_number.ikea_light_saturation')|int - 5, 0) }}"
          - conditions:
             - condition: template
               value_template: "{{ trigger.id == 'sat_up' }}"
            sequence:
              - service: input_number.set_value
                target:
                  entity_id: input_number.ikea_light_saturation
                data:
                  value: "{{ min(states('input_number.ikea_light_saturation')|int + 5, 100) }}"
          - conditions:
             - condition: template
               value_template: "{{ trigger.id == 'sat_min' }}"
            sequence:
              - service: input_number.set_value
                target:
                  entity_id: input_number.ikea_light_saturation
                data:
                  value: 0
          - conditions:
             - condition: template
               value_template: "{{ trigger.id == 'sat_max' }}"
            sequence:
              - service: input_number.set_value
                target:
                  entity_id: input_number.ikea_light_saturation
                data:
                  value: 100
          - conditions:
             - condition: template
               value_template: "{{ trigger.id == 'colorloop' }}"
            sequence:
              - parallel:
                - service: light.turn_on
                  target:
                    entity_id: light.reading_nook
                  data:
                    effect: colorloop
                - service: light.turn_on
                  target:
                    entity_id: light.attic_floor
                  data:
                    effect: colorloop
      - if:
          - condition: template
            value_template: "{{ trigger.id != 'colorloop' }}"
        then:
          - parallel:
            - service: light.turn_on
              target:
                entity_id: light.reading_nook
              data:
                hs_color: >-
                  [ {{ states('input_number.ikea_light_hue') }}, {{ states('input_number.ikea_light_saturation') }} ]

you are aware you have the first 2 conditions in the chosen listed twice?

also, why are you using the final condition in an if structure? and the use of the parallel seems moot, as there is nothing else you are doing at that time?

I wonder why/how you came to your main automation setup