How to sync 2 Climate entities? Infinite loop problem

I have this Tuya device (Moes S16Pro) which is used to control AC units. I flashed it with ESPHome via Cloud cutter (more info here). The way it works is that it has a main Climate device as a TuyaClimate entity, and a secondary IR Climate entity to relay the changes from the primary entity as IR commands to the AC. It works pretty well. However, there is a functionaluty present on the original Tuya firmware which I’m strugling to make it work: The device could read IR commands from the original IR controller and stay in sync with it, so you could use both controll methods in parallel. Here is the current yaml I have:

esphome:
  name: termostato-criancas
  friendly_name: Termostato Crianças

bk72xx:
  board: generic-bk7231n-qfn32-tuya

# Enable logging
logger:
  # level: INFO

# Enable Home Assistant API
api:
  encryption:
    key: ""

ota:
  - platform: esphome
    password: ""

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Termostato-Criancas Hotspot"
    password: ""

captive_portal:
    
########################
### IR BLASTER SETUP ###
########################
remote_transmitter:
  id: ir_t
  pin: P7
  carrier_duty_percent: 50%

remote_receiver:
  id: ir_r
  pin: 
    number: P8
    inverted: true
  dump: 
    #- lg
    #- raw
  tolerance: 55%
  

######################
### TUYA MCU SETUP ###
######################
uart:
  rx_pin: RX1
  tx_pin: TX1
  baud_rate: 9600

tuya:

###############
### SENSORS ###
###############
sensor:
  - platform: tuya
    name: "Umidade"
    sensor_datapoint: 12
    unit_of_measurement: "%"
  
  - platform: tuya
    name: "Temperatura"
    sensor_datapoint: 2
    unit_of_measurement: "°C"
    accuracy_decimals: 1
    filters:
      - multiply: 0.1


#######################
### CLIMATE DEVICES ###
#######################
climate:
# THIS IS THE TUYA CLIMATE ENTITY
# IT RELAYS STATE CHANGES TO THE SECONDARY CLIMATE ENTITY WHICH PROCESSES THEM AS IR COMMANDS
  - platform: tuya
    name: "AC"
    id: climate_primary
    visual:
      min_temperature: 17
      max_temperature: 30
      temperature_step: 1
    switch_datapoint: 1
    target_temperature_datapoint: 3
    target_temperature_multiplier: 1
    current_temperature_datapoint: 2
    current_temperature_multiplier: 0.1
    supports_heat: true
    supports_cool: true
    active_state:
      datapoint: 4
      cooling_value: 0
      heating_value: 1
      fanonly_value: 3
      drying_value: 4
    fan_mode:
      datapoint: 5
      auto_value: 0
      low_value: 1
      medium_value: 2
      high_value: 3
    
    
    # AUTOMATION THAT WHENEVER THE DATAPOINTS UPDATE THOSE STATUSES SHOULD ALSO BE UPDATED ON THE MAIN CLIMATE ENTITY
    on_state:
      
      - lambda: |-
          if (id(climate_primary).mode != (id(climate_secondary).mode)) {
            auto call = id(climate_secondary).make_call();
            call.set_mode(id(climate_primary).mode);
            call.perform();
          }
          else if (id(climate_primary).fan_mode != (id(climate_secondary).fan_mode)) {
            auto call = id(climate_secondary).make_call();
            call.set_fan_mode(id(climate_primary).fan_mode);
            call.perform();
          }
          else if (id(climate_primary).target_temperature != (id(climate_secondary).target_temperature)) {
            auto call = id(climate_secondary).make_call();
            call.set_target_temperature(id(climate_primary).target_temperature);
            call.perform();
          }
      # - lambda: |-
      #     if ((id(climate_secondary).swing_mode) != CLIMATE_SWING_BOTH) {
      #       auto call = id(climate_secondary).make_call();
      #       call.set_swing_mode(CLIMATE_SWING_BOTH);
      #       call.perform();
      #     }

  # THIS THE THE SECONDARY HIDDEN CLIMATE DEVICE THAT PROCESSES THE COMMANDS FROM THE PRIMARY ENTITY AS IR COMMANDS
  
  - platform: climate_ir_lg  
    name: "IR"
    id: climate_secondary
    header_high: 3265us # AC Units from LG in Brazil use these timings
    header_low: 9856us
    
    receiver_id: ir_r

    on_state:
      - lambda: |-
          # Code to sync back states received from IR controller would go here

This code is working right now, the syncroniztion work in one way (primary > secondary). The primary climate entity is the one exposed in HA. any commands entered via HA or via the touch panel of the device itself are propagated to the secondary climate entity and transmited as IR commands.

In the secondary entity I’m using the receiver_id property to map the IR receiver, and I can see via logs that the commands from the controller are interpreted correcly and states on the secondary climate entity are updated accordingly. So the only missing part is making it propagate back to primary climate. I tried doing it via on_state attriburte in the secondary, but I think this causes an infinite loop which makes the device unresponsive. On my first attempt testing a sync back, I tought I had bricked the device because I coudn’t OTA it again, but thanks to safe mode I was able to recover. Now I’m afraid to test any other code :disappointed_relieved:

Anyway, I need help figuring out how I could keep both entities in sync without entering on a infinite loop again. Also, is there an alternative way to test this code without risking to render the device unresponsive again?

What did you use for the lambdas? If you used the same if/else != conditions, I don’t see the loop. When the thermostats are synced the conditions should fail and no update called.

I know you don’t want to have it go unresponsive but it may be able to tell you what exactly is happening. If you set the log level to VERY_VERBOSE and leave the log window open when trying to sync, it will output everything it does before becoming unresponsive.

Yes, I used the same if/else != conditions. The last 2 times I tried using that code the device became so irresponsive that it lost connection, I coudn’t get any network response, even power cycling it. After several times turing it on and off it finally was able to connect to my network and I could revert the firmware. So now I’m kind of afraid of trying again :sweat_smile: