Turn your Desktronic standing desk into a smart desk!

Thank you for the great project base! :palms_up_together:

I have a similar odd desk.


I am embarrassed to be still very confused about the wiring to the ESP dongle between desk and the controller, and how the UART wires should be.

The cable itself has 10 pins, which i counted after ordering the expanders from Aliexpress with 8 pins :confused: But i discarded the outermost ones that control the 12 Volts, and it almost works, it fits to the base nicely and i can use the inner 8.

But here are the problems:

  1. When inserting WHITE to TX on ESP32, i can get the binary sensors (pressed button up, button down, etc)
  2. When inserting WHITE to RX on ESP32, i can get the table to move with buttons
    1. Modified your cpp code to expose more public functions for testing, so i can move it up or down.
    void move_to_from(const float height_in_cm, const float current_height);
    void move_to(const float height_in_cm);
    void stop();
    void move_up();
    void move_down();
    void move_to_memory_1();
    void move_to_memory_2();
  1. But never have I got the desk height :confused:
  2. Not have I got the dongle to work together with the physical controller. The display just says 888. and buttons dont work.

So about wiring:

  • should I keep the existing connection between table and controller, and add my wires for ESP just in the middle?
  • or remove the existing WHITE and BLACK to just go through my ESP32?
  • i have 2 uart buses configured, where do the wires go? exactly? :flushed:
  - id: remote_bus
    tx_pin: 1 # when WHITE - allows to control the desk
    rx_pin: 3 # when WHITE - reads sensor data from controller
   # together they break

  - id: desk_bus
    rx_pin: 17 # no combo does anything
    tx_pin: 16 # no combo does anything

I’m ashamed to have spent too much time on this, and now I just want to finish it out of spite.

Tried the uart_mitm component for no avail.

So I almost hacked out the height from desk, logging and reading the hex data, parsing it to numbers (without proper wiring):

    debug:
      direction: BOTH 
      dummy_receiver: true
      after:
        delimiter: "\xA5"  
      sequence:
        - lambda: |-
            UARTDebug::log_hex(direction , bytes, ',');  //Still log the data

            auto uart_direction = "IDK";

            if (direction = uart::UART_DIRECTION_TX ){
              uart_direction = "TX: ";
            }              
            else if (direction = uart::UART_DIRECTION_RX ){
              uart_direction = "RX_ ";
            }
              auto bytes_size = bytes.size();
                std::string log_message = uart_direction;
                for(int i = 0; i < bytes_size; ++i){
                  char buffer[4]; 
                  sprintf(buffer, "%02x ", bytes[i + bytes_size - 1 ]); 
                  log_message += buffer; 
                }
              if ( bytes_size >= 5){
                ESP_LOGW("desk_bus", "%s", log_message.c_str());
              }
              else {
                ESP_LOGI("desk_bus", "%s", log_message.c_str());
              }         

even got this far as to read the height value out of the uart buffer, somewhat semi-consistently:

script:
  - id: get_debug_height
    mode: single
    then: 
      - lambda: |- 
          // provided map gives error on "4" // data[2] is data2 = EDGE CASE if contain 4 (74, 84, 94, 104, 114, 124)
          // static const int SEGMENT_MAP[10] = {0x3f, 0x06, 0x5b, 0x4f, 0x67, 0x6d, 0x7d, 0x07, 0x7f, 0x6f};
                                              //  0     1     2     3     4     5     6     7     8     9     
          static const int SEGMENT_MAP[10] = {0x3f, 0x06, 0x5b, 0x4f, 0x67, 0x6d, 0x7d, 0x07, 0x7f, 0x6f};
                                                                    //0x66
          uint8_t data[5];
          while (id(desk_uart).available() >= 5) {

            id(desk_uart).read_array(data, 5);
            if (data[0] != 0x5A) {
              ESP_LOGE("seisuk", "[0] %02x must be 0x5A, [3] is %02x", data[1], data[3]);
              //break;
              continue;
            }
            if ((data[1] | data[2] | data[3]) == 0x00) {
              ESP_LOGD("seisuk", "null height");
              //break;
              continue;
            }
            int data0 = -1, data1 = -1, data2 = -1;
            for (int i = 0; i < 10; i++) {
              if (data[1] == SEGMENT_MAP[i]) data0 = i;
              if (data[2] == SEGMENT_MAP[i]) data1 = i;
              if (data[3] == SEGMENT_MAP[i]) data2 = i;
            }

            float got_height = 0.0;

            if (data0 < 0 || data1 < 0 || data2 < 0) {
              if (data0 < 0 ) { 
                ESP_LOGE("data", "data0: %02x", data[1] );  // if ( data[1] != 0x00 ) { }               
              }
              if (data1 < 0 ) { 
                // true for values above 100, false for below 100
                ESP_LOGW("data", "data1: %02x", data[2] );  // if ( data[2] != 0x00 ) { }
              }
              if (data2 < 0 ) {
                ESP_LOGW("seisuk", "data2:  %02x", data[3] );
              }
              //break;
              continue;
            }

            // get decimal
            int decimal = ((~data[4] + 256) % 256) / 10;

            // sum if up            
            got_height = data0 * 100 + data1 * 10 + data2 + decimal * 0.1;

            add decimal 
            //if (got_height >= 100.0){
            //  got_height += decimal * 0.1;
            //}

            // If belo 3 digis, e.g 100
            // INVERT HEIGHT FOR BELOW ZERO

            if (data[2] & 0x80 && data1 < 0) { 

              got_height = got_height / 10 + 10;

              float under_99 = ( ( ~data[4] + 256 ) % 256 ); 

              if (under_99 > 100 ){
                //under_99 /= 10;
              }

              float inverted = ( ( data[4] + 256 ) % 256); // + 100;

              if (inverted > 100 ){
                //inverted /= 10;
              }

              float reinverted = ( ( data[4] ) % 256) / 2; // + 100;

              if (reinverted < 10 ){
                //reinverted *= 10;
              }

              if (under_99 < 101 && under_99 > 65  ){ // && under_99 != 68.00
                ESP_LOGI("under_99", "%.2f", under_99 );               
              }
              if (inverted < 101 && inverted > 65 ){
                ESP_LOGW("inverted", "%.2f", inverted );               
              }
              if (reinverted < 101 && reinverted > 65  ){ // && reinverted != 100.00
                ESP_LOGE("reinverted", "%.2f", reinverted );       
              }

              if (data1 < 0 ) { 
                // ESP_LOGE("data1", "%02x", data[2] );  // if ( data[2] != 0x00 ) { }
              }

              if (under_99 <= 100 && under_99 > 66 ){
                //got_height = under_99;
                //ESP_LOGI("got_height", "%.2f", under_99);
              }
              
              if (got_height <= 100 ){
                ESP_LOGI("lt100", " %.1f", got_height );                                   
              }
            }
            
            // IF BELOW 100, divide by 10, get decimal place

            ESP_LOGW("height", "data[2]: %02x, data1: %d", data[2], data1);              

            // IF ALL IS CORRECT, PUBLISH VALUES, DO STUFF

            if (got_height <= id(max_height) && got_height >= id(min_height) ){
              ESP_LOGI("height", "%.1f ", got_height );
              id(uart_height).publish_state(got_height);
              id(height) = got_height;
            }

            if (got_height >= id(max_height) ||  got_height <= id(min_height) ){
              id(seisuk).stop();
            }
          }
1 Like

I haven’t been following this in detail, but based on my experiences with another project, here is how I believe uart wiring needs to be for two controlling devices to be able to issue commands…

  1. If you only need to “read signals on a wire” (listen to them, let them “pass through”, but not inject your own commands), then you can just “Hop” the wire. See yellow wire.

  2. If you need to both read signals on the wire and inject your own (listen for keypad presses and insert your own from the ESP), then the wire must only pass through the ESP via a “uart forward”. This is where I got stuck for a long time with my project. I believe this is key to a “Man-In-The-Middle” (MITM) set-up. See green wire. If you try to hop a signal you need to both read and write from, it won’t work when both devices are connected.

In my diagram, you can pretend the convertors aren’t there (wires go straight through). Pendant is control panel. Robot is desk control box.

I’m not 100% if this applies to this project, but it took me a while to figure this out so sharing in case it is of use.

I also found for my project that the MITM may introduce a small lag as it recieves and forwards uart messages. This may or may not create issues if communication is two way (there is a request that waits for a response and may timeout).

1 Like

As wire colors are different, I only have black and white to play with, and red for control takeover. I use green/purple to power the ESP. Yellow and Brown are ignored because I messed up buying the 8-pin over 10-pin cable expander :no_mouth: This means I cant get USB power from the control panel, but that’s okay for now…

I can do without knowing the binary sensors for if controller button is pressed, i just would like to get the desk height, so i can move it to set height. Currently using my modified code i can move it up and down with switches, but since i dont know the height, i cant set the position…

P → Key1 # RED = P (Panel?)
R → D-TX # BLACK = Rx
T → D-RX # WHITE = TX
5 → +5V # GREEN = 5g
G → G # PURPLE = GND
YELLOW = DC + 12v
BROWN = DC_G - 12v

1 Like

Are the height messages the same protocol as the OP? I assume they are on the black wire?

1 Like

Good find, they are not exactly the same. As when using the original external component, I didn’t get anything as height, but with my custom lambda script at least something did come out…

 // provided map gives error on "4" // data[2] is data2 = EDGE CASE if contain 4 (74, 84, 94, 104, 114, 124)
 // static const int SEGMENT_MAP[10] = {0x3f, 0x06, 0x5b, 0x4f, 0x67, 0x6d, 0x7d, 0x07, 0x7f, 0x6f};
                                     //  0     1     2     3     4     5     6     7     8     9     
 static const int SEGMENT_MAP[10] = {0x3f, 0x06, 0x5b, 0x4f, 0x67, 0x6d, 0x7d, 0x07, 0x7f, 0x6f};

using the original map i logged some weird jumps, and missing numbers (the component provided sensor didn’t work still):

5A,07,FF,7F,85 -> 138.0
5A,07,FF,7F,85 -> 138.0
5A,07,FF,6F,75 -> 136.0
5A,07,FF,6F,75 -> 136.0
5A,07,EF,3F,35 -> 113.0
5A,07,EF,3F,35 -> 113.0
5A,07,EF,06,FC -> 110.0
5A,07,EF,06,FC -> 110.0
5A,07,EF,5B,51 -> 112.0
5A,07,EF,4F,45 -> 113.0
5A,07,EF,4F,45 -> 113.0
5A,07,EF,6D,63 -> 114.0
5A,07,EF,6D,63 -> 114.0
5A,07,EF,07,FD -> 110.0
5A,07,EF,07,FD -> 110.0
5A,07,EF,6F,65 -> 115.0
5A,07,EF,6F,65 -> 115.0
5A,7D,FD,4F,C9 -> 64,9
5A,7D,FD,5B,D5 -> 62,5
5A,7D,FD,3F,B9 -> 60,9
5A,7D,ED,6F,D9 -> 79,9
5A,7D,ED,07,71 -> 77,1
5A,7D,ED,6D,D7 -> 75,7
5A,7D,ED,66,D0 -> 74,0
5A,7D,ED,4F,B9 -> 73,9
5A,7D,ED,5B,C5 -> 72,5
5A,7D,ED,06,70 -> 71,0
5A,7D,ED,3F,A9 -> 70,9
5A,7D,FD,6D,E7 -> 65,4
5A,7D,FD,07,81 -> 67,1
5A,7D,FD,7F,F9 -> 68,9
5A,7D,FD,6F,E9 -> 69,9
5A,7D,FD,4F,C9 
5A,7D,FD,5B,D5 -> 62,5 
5A,7D,FD,3F,B9 -> 60,9 
5A,7D,ED,6F,D9 -> 79,9 
5A,7D,ED,07,71 -> 77,1 
5A,7D,ED,6D,D7 -> 75,7 
5A,7D,ED,66,D0 -> 74,0 
5A,7D,ED,4F,B9 -> 73,9 
5A,7D,ED,5B,C5 -> 72,5 
5A,7D,ED,06,70 -> 71,0 
5A,7D,ED,3F,A9 -> 70,9 
5A,7D,FD,6D,E7 -> 65,4 
5A,7D,FD,07,81 -> 67,1 
5A,7D,FD,7F,F9 -> 68,9 
5A,7D,FD,6F,E9 -> 69,9  - bottom target reached

Got it working through some ugly hacks :slight_smile: Forgot to post, hope this will help someone else. Buttons on keypad and the screen is not working though, maybe i’ll deal with it in the future. Maybe even add some fun things, since its possible to write random stuff on the 7-segment-display · GitHub Topics · GitHub

substitutions:
  friendly_name: 'Seisuk'
  device_name: 'seisuk'
  node_name: 'seisuk'
  device_description: 'Seisuk AOKE WP-H01 WP-CB01-001 Desktronic/JSDRIVE DCU_G-PRT5G'
  project_base: 'AOKE' # https://aoke-europe.com/troubleshooting
  project_name: 'WP-H01' # panel
  project_version: 'WP-CB01-001' # control box # https://aoke-europe.com/mwdownloads/download/link/id/88/

    # https://profeqprofessional.nl/media/productattach//e/r/error_codes_wp-cb01-001_2.pdf
    # https://motionwise-products.com/wp-content/uploads/2018/11/MotionWise_sf_Manual_081318_E_Rev-USB.pdf
    # https://profeqprofessional.nl/media/productattach//e/r/error_codes_wp-cb01-001_2.pdf

  min_height: "65.5" # real 65  # Min height + 0.5
  max_height: "129.5" # real 130  # Max height - 0.5
  initial_value: "90"

globals:
  - id: height
    type: float
    restore_value: yes
    initial_value: '100.0'

  - id: min_height
    type: float
    restore_value: yes
    initial_value: '65.5'

  - id: max_height
    type: float
    restore_value: yes
    initial_value: '129.5'

esphome:
  name: ${device_name}
  friendly_name: ${friendly_name}
  name_add_mac_suffix: false
  comment: ${device_description}
  project:
    name: ${project_base}.${project_name}
    version: $project_version
  on_boot:
    - priority: 600.0 
      then:
        - button.press: stop
    - priority: -200.0 
      then:
        - lambda: |-
            id(uart_height).publish_state( id(height) );

esp8266:
  board: nodemcu
  early_pin_init: false
  restore_from_flash: true

packages:
  device_base: !include common/device.base.yaml

ota:
  on_begin:
    then:
      - button.press: stop

logger:
  esp8266_store_log_strings_in_flash: false  
  level: DEBUG
  baud_rate: 0

uart:
  - id: remote_bus
    tx_pin: 1 # WHITE (2nd pin)
    rx_pin: 3 # N/A (3rd pin)
    baud_rate: 9600

  - id: desk_bus
    tx_pin: 4 # N/A (D2)
    rx_pin: 5 # BLACK (D1) 
    baud_rate: 9600
    rx_buffer_size: 256

external_components:
  - source: components
    components: [ desktronic ]  

desktronic:
  id: seisuk
  desk_uart: desk_bus
  remote_uart: remote_bus
  height:
    name: Wnotworkign desktronic Height
    id: desk_height_sensor
    icon: mdi:human-male-height
    device_class: distance
    unit_of_measurement: cm
    accuracy_decimals: 1    
    internal: true
  move_pin:
    number: 14 # (D5)

binary_sensor:
  - id: moving
    name: Moving
    platform: template
    device_class: moving
    icon: mdi:hand-back-right-off
    lambda: return id(seisuk).current_operation != desktronic::DESKTRONIC_OPERATION_IDLE;
    on_state:
      - while:
          condition:
            binary_sensor.is_on: moving
          then:
            - script.execute: get_debug_height
            - delay: 50ms

  - id: going_up
    name: Moving up
    platform: template
    device_class: moving
    icon: mdi:transfer-up
    entity_category: diagnostic
    lambda: return (id(seisuk).current_operation == desktronic::DESKTRONIC_OPERATION_RAISING) || (id(seisuk).current_operation == desktronic::DESKTRONIC_OPERATION_UP);

  - id: going_down
    name: Moving down
    platform: template
    device_class: moving
    icon: mdi:transfer-down
    entity_category: diagnostic
    lambda: return (id(seisuk).current_operation == desktronic::DESKTRONIC_OPERATION_LOWERING) || (id(seisuk).current_operation == desktronic::DESKTRONIC_OPERATION_DOWN);

sensor:
  - id: uart_height
    platform: template
    name: Height
    icon: mdi:human-male-height
    device_class: distance
    unit_of_measurement: cm
    accuracy_decimals: 1
    update_interval: never
    lambda: |-
      float current = id(height);
      return current;

number:
  - platform: template
    id: desk_height
    name: Height
    icon: mdi:human-male-height-variant
    min_value: $min_height
    max_value: $max_height
    lambda: |-
      return id(height);
    device_class: distance
    unit_of_measurement: cm
    update_interval: never
    mode: box
    step: 0.1
    set_action:
      then:
        - while:
            condition: 
              - lambda: |-
                  float to = std::round(x * 10) / 10;
                  float from = std::round(id(height) * 10) / 10;
                  if ( to != from ) {
                    return true;
                  }
                  else{
                    return false;
                  }
            then:
              - script.execute: 
                  id: move_it
                  to: !lambda 'return x;'
              - delay: 150ms

cover:
  - platform: template
    name: None
    optimistic: true
    device_class: damper
    icon: mdi:desk
    assumed_state: true
    has_position: true    
    lambda: |-
      auto value = (int)id(height);
      float percentage = (float)(value - id(min_height)) / (id(max_height) - id(min_height));
      return percentage;
    open_action:
      - number.set:
          id: desk_height
          value: $max_height
    close_action:
      - number.set:
          id: desk_height
          value: $min_height
    stop_action:
      - button.press: stop
    position_action:
      - number.set:
          id: desk_height
          value: !lambda |-
            int value = id(min_height) + (int)( ( pos ) * (id(max_height) - id(min_height)) );
            return value;

button:
  - platform: template
    name: Stop
    icon: mdi:stop
    entity_category: config
    id: stop
    on_press:
      then:
        - switch.turn_off: takeover
        - script.stop: move_it
        - switch.turn_off: switch_up
        - switch.turn_off: switch_down
        - lambda: |-
            id(seisuk).stop();
        - number.set: 
            id: desk_height
            value: !lambda |-
              float x = id(height);
              return x;
        - component.update: desk_height

switch:
 - platform: gpio
   name: Takeover
   icon: mdi:gesture-tap
   id: takeover
   pin:
     number: 14 # (D5)
     mode: OUTPUT 
   restore_mode:  ALWAYS_OFF   
   entity_category: diagnostic
   disabled_by_default: true
   on_turn_on:
    then:
      - delay: 50ms
      - switch.turn_off: takeover

 - platform: template
   optimistic: true
   name: Move Up
   entity_category: config
   restore_mode: ALWAYS_OFF
   icon: mdi:arrow-up-bold-box
   id: switch_up
   on_turn_on:
     then:
       - switch.turn_on: takeover
       - switch.turn_off: switch_down
       - lambda: |-
           id(seisuk).move_up();
   on_turn_off:
     then:
       button.press: stop
 
 - platform: template
   name: Move Down
   entity_category: config
   restore_mode: ALWAYS_OFF
   optimistic: true
   icon: mdi:arrow-down-bold-box
   id: switch_down
   on_turn_on:
     then:
       - switch.turn_on: takeover
       - switch.turn_off: switch_up #  interlock: [up]
       - lambda: |-
           id(seisuk).move_down();
   on_turn_off:
     then:
       button.press: stop

script:
  - id: move_it
    parameters:
      to: float
    mode: restart
    then:
      - if:
          condition:
              - binary_sensor.is_off: moving
          then:
            - lambda: |-
                float target = std::round(to * 10) / 10;
                float from = std::round(id(height) * 10) / 10;
      
                ESP_LOGI("seisuk", "target %.1f from %.1f", target, from);
      
                if ( target != from ) {  
                  if ( target < from ) { 
                    id(switch_down).turn_on();
                  }
                  else if ( target > from ) {
                    id(switch_up).turn_on();
                  }
                  else if ( target == from ) {
                    id(stop).press();
                    ESP_LOGI("seisuk", "target == from");
                  }          
                  else {
                    id(stop).press();
                    ESP_LOGI("seisuk", "else height same");
                  }
                }
                else{
                  id(stop).press();
                  ESP_LOGI("seisuk", "height not not same");
                }
          else:
          - if:
              condition:
                  - binary_sensor.is_on: going_up 
              then:
              - lambda: |-
                  float target = std::round(to * 10) / 10;
                  float from = std::round(id(uart_height).state * 10) / 10;

                  ESP_LOGI("seisuk", "moving to %.1f from %.1f", target, from);

                  if ( target <= from ) { 
                    id(stop).press();
                    ESP_LOGE("seisuk", "height more");

                    auto call = id(desk_height).make_call();
                    call.set_value(id(uart_height).state);
                    call.perform();                  
                  }
          - if:
              condition:
                  - binary_sensor.is_on: going_down
              then:
              - lambda: |-
                  float target = std::round(to * 10) / 10;
                  float from = std::round(id(uart_height).state * 10) / 10;

                  ESP_LOGI("seisuk", "moving to %.1f from %.1f", target, from);

                  if ( target >= from ) { 
                    id(stop).press();
                    ESP_LOGE("seisuk", "height less");

                    auto call = id(desk_height).make_call();
                    call.set_value(id(uart_height).state);
                    call.perform();                  
                  }
      - delay: 100ms

  - id: get_debug_height
    mode: single
    then: 
      - lambda: |- 

          // before continuing tell its not really moving

          // id(moving).publish_state(false);
          // id(going_up).publish_state(false);
          // id(going_down).publish_state(false);

          uint8_t data[5];
          while (id(desk_bus).available() >= 5) {

            id(desk_bus).read_array(data, 5);
            if (data[0] != 0x5A) {
              break;
            }
            if ((data[1] | data[2] | data[3]) == 0x00) {
              break;
            }

            enum Segment : uint8_t
            {
                SEGMENT_INVALID = 0x00,
                SEGMENT_0 = 0x3f,
                SEGMENT_1 = 0x06,
                SEGMENT_2 = 0x5b,
                SEGMENT_3 = 0x4f,
                SEGMENT_4 = 0x67,
                SEGMENT_5 = 0x6d,
                SEGMENT_6 = 0x7d,
                SEGMENT_7 = 0x07,
                SEGMENT_8 = 0x7f,
                SEGMENT_9 = 0x6f,
            };
          
            auto segment_to_number = [](const uint8_t segment) {
                switch (segment & 0x7f)
                {
                case SEGMENT_0:
                    return 0;
                case SEGMENT_1:
                    return 1;
                case SEGMENT_2:
                    return 2;
                case SEGMENT_3:
                    return 3;
                case SEGMENT_4:
                    return 4;
                case SEGMENT_5:
                    return 5;
                case SEGMENT_6:
                    return 6;
                case SEGMENT_7:
                    return 7;
                case SEGMENT_8:
                    return 8;
                case SEGMENT_9:
                    return 9;
                default:
                    ESP_LOGD("desktronic", "idk");
                }
                return -1;
            };            

            int data0 = segment_to_number(data[1]);
            int data1 = segment_to_number(data[2]);
            int data2 = segment_to_number(data[3]);

            float got_height = 0.0;

            if (data0 < 0x00 || data1 < 0x00 || data2 < 0x00) {
              break;
            }

            got_height = data0 * 100 + data1 * 10 + data2; // + decimal * 0.1;

            // flip height for below 100
            if (data[2] & 0x80 ) { 
              got_height /= 10.0;
            }
            
            // If all correct, publish values
            if (got_height <= id(max_height) - 0.5 && got_height >= id(min_height) + 0.5 ){
              id(height) = got_height;
              id(uart_height).publish_state(got_height);              
            }

            if (got_height >= id(max_height) || got_height <= id(min_height) ){
              // id(stop).press();
              ESP_LOGE("seisuk","stopped, out of bounds");
            }

            // now its moving
            // id(moving).publish_state(true);
            // id(going_up).publish_state(true);
            // id(going_down).publish_state(true);            

          }
      - delay: 50ms


Converting it to template cover is really nice, and allows exposing to to Google Home for voice control

Thanks again for the code @MhouneyLH!

1 Like

Hey @veli, the interface for controlling the desk in HomeAssistant looks pretty good! I will have a look at it, as this was my first project using HomeAssistant :slight_smile:

I haven’t been working on the desk project for the last few months, so I haven’t checked the forum at all. Sorry about that. But I’m glad to see it’s working for you now. It took me a few weeks to figure out how to modify the desk. You should not feel bad about the time you invested. The result (and of course the adventure) is what counts :slight_smile:

I am currently working on an app that will allow you to control the desk with your smartphone. For the app I will have to update the component a bit. I will try to keep this thread updated. I don’t know if anyone would use it, but theoretically you could test the app on your desk.

1 Like

An app separate from HA/ESPHome? Happy to test it, but personally I use HA to get rid of extra apps :grinning_face_with_smiling_eyes:

I had a small business plan (too lazy to execute it, feel free to steal or collaborate with me) of creating the dongles to sell to our local standing desk manufacturer. An app would for sure be a necessity for that since their customers woult not use HA or like to connect to the ESP AP to control it…

I rewrote bits of the component, hence using it as this - public functions, more binary sensors, etc:

  - source: components
    components: [ desktronic ]  
1 Like

A friend of mine and I also had this business plan. But at the moment (when the app is ready) it is in the first place just a fun gadget to use. Maybe the business plan will be executed in the future, but currently I’m too lazy too.

Of course, people who use HomeAssistant, probably won’t use the app the as you explained. :slight_smile:

1 Like

Technically, JS already has a BT adapter on the market: Standing Desk Bluetooth from China manufacturer - JS Technology

No WiFi adapter yet.

I have the same jsm-2.1-0.3w controller in my desk, however the control panel pinout seems to be quite different (as well as the colours for the pins).

The RJ50 end has colours in order of brown, black, white, orange, red, blue, white, red, green and yellow.

The controller on the other hand, uses blue, green, white, black, red, brown and yellow (with a not connected pin between red and brown).

The labels are, same order as the colours: “G”, “5”, “T”, “R”, “P”, NC, “U_G”, “DC”

The cabling actually seems to line up with @MhouneyLH’s from the first post, with the main difference limited to a colour change - purple, aka “G” is blue on mine, purple on theirs.

I’ve ordered an RJ50 male to female short cable (30cm) that I’m going to cannibalize and embed an ESP32 in a small through-package.

Oh and I also managed to coax out a bit of documentation from the seller of my desk, EYOJYA - they’ve sent over the documentation for a BLUETOOTH adapter they don’t even have on sale! Nonetheless here’s the PDF, it seems to use the same, or at the very least, incredibly similar command structure, even if it’s kinda full of typos.

@veli would you mind sharing your changes to the desktronic component?

Oh, and also, JS also has some quite much fancier versions of the controllers, with (apparently controllable?) RGB lighting, motorised knob, and a proper OLED display:

1 Like

In the desktronic.h, just moved things from private to public so i can lambda call id(desk).move_up() and down. Will check for any not needed test comments in files, fork the OP code on github and perhaps we can make it work without hacks :slight_smile:

#pragma once

#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/uart/uart.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
#include "esphome/components/number/number.h"

namespace esphome
{
namespace desktronic
{

enum DesktronicOperation : uint8_t
{
    DESKTRONIC_OPERATION_IDLE = 0U,
    DESKTRONIC_OPERATION_RAISING,
    DESKTRONIC_OPERATION_LOWERING,
    DESKTRONIC_OPERATION_UP,
    DESKTRONIC_OPERATION_DOWN,
    DESKTRONIC_OPERATION_MEMORY_1,
    DESKTRONIC_OPERATION_MEMORY_2,    
};

enum Segment : uint8_t
{
    SEGMENT_INVALID = 0x00,
    SEGMENT_0 = 0x3f,
    SEGMENT_1 = 0x06,
    SEGMENT_2 = 0x5b,
    SEGMENT_3 = 0x4f,
    SEGMENT_4 = 0x67,
    SEGMENT_5 = 0x6d,
    SEGMENT_6 = 0x7d,
    SEGMENT_7 = 0x07,
    SEGMENT_8 = 0x7f,
    SEGMENT_9 = 0x6f,
};

enum MovingIdentifier : uint8_t
{
    MOVING_IDENTIFIER_UP = 0x20,
    MOVING_IDENTIFIER_DOWN = 0x40,
    MOVING_IDENTIFIER_MEMORY_1 = 0x02,
    MOVING_IDENTIFIER_MEMORY_2 = 0x04,
    MOVING_IDENTIFIER_MEMORY_3 = 0x08,
    MOVING_IDENTIFIER_MEMORY_4 = 0x10,    
};

static const char* desktronic_operation_to_string(const DesktronicOperation operation);
static int segment_to_number(const uint8_t segment);

class Desktronic : public Component
{
public:
    float get_setup_priority() const override { return setup_priority::LATE; }
    void setup() override;
    void loop() override;
    void dump_config() override;

    void set_remote_uart(uart::UARTComponent* uart) { remote_uart_ = uart; }
    void set_desk_uart(uart::UARTComponent* uart) { desk_uart_ = uart; }
    void set_move_pin(GPIOPin* pin) { move_pin_ = pin; }
    void set_height_sensor(sensor::Sensor* sensor) { height_sensor_ = sensor; }
    void set_up_bsensor(binary_sensor::BinarySensor* sensor) { up_bsensor_ = sensor; }
    void set_down_bsensor(binary_sensor::BinarySensor* sensor) { down_bsensor_ = sensor; }
    void set_memory1_bsensor(binary_sensor::BinarySensor* sensor) { memory1_bsensor_ = sensor; }
    void set_memory2_bsensor(binary_sensor::BinarySensor* sensor) { memory2_bsensor_ = sensor; }
    void set_memory3_bsensor(binary_sensor::BinarySensor* sensor) { memory3_bsensor_ = sensor; }

    void move_to_from(const float height_in_cm, const float current_height);
    void move_to(const float height_in_cm);
    void stop();
    void move_up();
    void move_down();
    void move_to_memory_1();
    void move_to_memory_2();

public:
    DesktronicOperation current_operation{DesktronicOperation::DESKTRONIC_OPERATION_IDLE};

private:
    void read_remote_uart();
    void read_desk_uart();
    void publish_remote_states(const uint8_t data);
    void reset_remote_buffer();
    void reset_desk_buffer();

    bool must_move_up(const float height_in_cm) const;
    void move_to_target_height();

    bool isCurrentHeightValid() const;
    bool isCurrentHeightInTargetBoundaries() const;

protected:
    uart::UARTComponent* remote_uart_{nullptr};
    uart::UARTComponent* desk_uart_{nullptr};
    GPIOPin* move_pin_{nullptr};
    sensor::Sensor* height_sensor_{nullptr};
    binary_sensor::BinarySensor* up_bsensor_{nullptr};
    binary_sensor::BinarySensor* down_bsensor_{nullptr};
    binary_sensor::BinarySensor* memory1_bsensor_{nullptr};
    binary_sensor::BinarySensor* memory2_bsensor_{nullptr};
    binary_sensor::BinarySensor* memory3_bsensor_{nullptr};

    std::vector<uint8_t> remote_buffer_;
    std::vector<uint8_t> desk_buffer_;
    bool is_remote_rx_uart_message_start_found{false};
    bool is_desk_rx_uart_message_start_found{false};
    float current_height_{0.0};
    float target_height_{-1.0};
};
} // namespace desktronic
} // namespace esphome
2 Likes

I’ve done some further investigation.

The BT protocol I was sent? It uses a secondary UART port on the 12V lines - B-RX, B-TX and B-C1 pins on the MC side.

Here’s the FCCID page for the BT dongle

I’ve also grabbed the close-up shots of the board:

With some image editing wizardry the PCB layout should be somewhat salvageable. However the logic is pretty straightforward, it takes the RJ50 incoming port, splits it out to 10 pins, puts 5V, GND, KEY1, KEY2 and the UART pins forward, and breaks out B-RX, B-TX, and B-C1 for the Bluetooth MCU:

The orange path, B-TX, is pretty straightforward, it goes onot an UART pin of the MCU, with a voltage divider in the way.

B-RX on the other hand… It runs into a resistor that has B-C1 on the other end, then these two run into a diode that is hooked up to a separate pin on the MCU, and finally the output goes through another resistor before heading into the MCU’s other UART pin (transmit side of MCU).

Then finally purple… Now that’s a total mystery for me, it doesn’t seem to go anywhere or do anything.

According to the BLE chip’s spec sheet, Orange and Blue pins are an UART port, whereas Yellow and Purple are an I2C port. However I see no I2C component there, or anywhere on the board…

Nonetheless, this puts us a step closer to having a proper, non-injection control solution for these desks, as we have the documentation for the B-bus messages, as well as the pins to use.

Fair warning, one CAN NOT hook up an ESP32 or similar to the B-RX and B-TX pins, as these operate at 12V - there needs to be some level of voltage conversion, via optocouplers or similar!


I’ve also discovered a secret menu on my controller. By pressing the REMINDER/TIMER button first, then the M button, you’ll be presented with a 7-entry menu.

I’m yet to figure out all the menu options, but here’s what i’ve found so far:

1 - boolean, 0 or 1 as values, defaults to 1
2 - numeric, 0 to 8 as possible values, defaults to 3 on my controller
3 - numeric, 0 to 7 as possible values, defaults to 6 on my controller
4 - numeric, 0 to 7 as possible values, defaults to 6 on my controller
5 - numeric, 0 to 9 as possible values, defaults to 5 on my controller
6 - numeric, menu option is not displayed, defaults to 60 on my controller (I believe this is the minimum height of the desk)
7 - boolean, 0 or 1 as values, defaults to 1
1 Like

Oh and one more thing, the DC and U_G pins on my HC seem to be 35V instead of 12V - can anyone else confirm what their desk reads at?

1 Like

@veli this looks nice and almost works for me! Did you change any code in the cpp file? I get messages saying the checksum did not match, and I wondered if you changed that.

1 Like

Indeed, here is the fork. :slight_smile:

1 Like

Hello guys, this is my first time ever in the world of home assistant.

I have a desk that is exactly the same as Veli’s from what i saw.
Using this post i thought i could make my desk also smart (then it would be smarter than me).

I bought a d1 mini v3 (esp01_1m) and I cloned GitHub - velijv/esphome_custom_components: A collection of custom components for esphome. .

I also copied the following config Turn your Desktronic standing desk into a smart desk! - #38 by veli with the only changes being that i changed the board from nodemcu to esp01_1m and also adding “allow_other_uses: true” to all references of pin number 14 (compiler was giving me errors without it)

Then set up the rj45 cable to pins as follows

I Know that i may be doing something wrong (by my side as always) because I have some trouble making it work.
Firstly my d1 mini does not power up when connected with the referred pinout (where as if i give 5v from my arduino to 5v pin in d1 mini OR usb it powers up normally).

Having it as is (without it getting power) the manual control panel works (ups,lowers desk and memory buttons work)

Now lets say that while i had it connected using the previously mentioned pinout and powering it with a usb as soon as it gets voltage it raises the desk to its max height without me pressing anything
So my final pinout is as follows:

P → RED = D5
R → BLACK = Rx
T → WHITE = TX
5 → GREEN = 5V
G → PURPLE = GND

Any tip or guidance is more than welcome ^^

Appreciate all the work all of you mates put to make it possible up until now.
Cheers! :smiley:

Hi @kostaskam!

Sorry for the late reply. Try to set this switch to not internal, and toggle it off - then it should stop. this is the takeover switch for me:

switch:
 - platform: gpio
   name: Takeover
   icon: mdi:gesture-tap
   id: takeover
   pin:
     number: 14 # (D5)
     mode: OUTPUT 
   restore_mode:  ALWAYS_OFF   
   entity_category: diagnostic
   disabled_by_default: true
   on_turn_on:
    then:
      - delay: 50ms
      - switch.turn_off: takeover

Depending on the ESP model, you might need to fiddle with:

  • early_pin_init (Optional, boolean): Specifies whether pins should be initialised as early as possible to known values. Recommended value is false where switches are involved, as these will toggle when updating the firmware or when restarting the device. Defaults to true.
  • restore_from_flash (Optional, boolean): Whether to store some persistent preferences in flash memory. Defaults to false.

I firstly would like to say, kudos for doing this, I am also one of them geeks that seems to be automating “everything”. I actually stumbled into this thread due to the fact I have one of these white box desks that I purchased from Amazon during COVID. I started actually playing about to figure out how it ticked and wanted to have it a little more automated aka via ESPHome. Anyhow, this is where I am today. I have a ESP32 (M5 Stack Atom) running fine off the voltage coming from the pins and the code @veli created works (ish).

However, I have a problem, I am not sure why but the code does not fully work. The desk moves a little bit sometimes when requested and also does not move sometimes on the same turn of the page, which is makes the commands very hit and miss. I have tried to identify the differences that everyone has been recommended aka the ESP8266 compared to the ESP32 and the only thing I can see is the CPU clock is 80mhz rather than 240mhz that the ESP32 clock is. So I have down clocked the ESP32 to 80mhz with still no fix. Looking at the console, with debug on, I get logs of checksum mismatches?? which I assume is the start of the issue.

I am a little at a loss on what to do here its almost like it wants to work but just cant do the last thing which is move the desk. Everything else works, aka I can see the height of the desk consistently and when it does move the height changes (Yes it only changes by .1 but I can see it change) and send it to pre-sets but when the motors need to move they are either moving slow, and or lumpy or not at all. Anything anyone can suggest on what to try?

It would seem my fork was not perfect as described here partly:

Happy if someone would collaborate to improve it :slight_smile:

I still haven’t got even the buttons to register as sensors, and latest updates have rendered my code broke, as it wont stop in promised location as previously… might have to swap the nodemcu with something more modern.


seisuk:617 - idk
seisuk:617 - idk
seisuk:617 - idk
seisuk:617 - idk
seisuk:617 - idk
seisuk:617 - idk
seisuk:617 - idk
seisuk:617 - idk
seisuk:617 - idk
seisuk:617 - idk
seisuk:617 - idk
seisuk:617 - idk
seisuk:617 - idk
seisuk:617 - idk
seisuk:617 - idk
seisuk:617 - idk
seisuk:617 - idk
seisuk:617 - idk
seisuk:617 - idk
seisuk:617 - idk
seisuk:617 - idk
seisuk:617 - idk
seisuk:617 - idk
seisuk:617 - idk
seisuk:617 - idk
sensor:093 - 'Seisuk': Sending state 89.50000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.50000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.50000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.50000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.50000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.50000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.50000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.50000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.50000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.50000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.50000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.50000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.60000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.60000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.60000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.60000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.60000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.60000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.60000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.60000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.60000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.70000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.70000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.70000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.70000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.70000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.70000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.80000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.80000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.80000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.80000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.80000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.80000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.90000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.90000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.90000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 90.00000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 90.00000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 90.00000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 90.10000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 90.10000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 90.20000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 90.30000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 90.30000 cm with 1 decimals of accuracy
seisuk:617 - idk
sensor:093 - 'Seisuk': Sending state 90.60000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 90.60000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 90.60000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 90.70000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 90.70000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 90.80000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 91.00000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 91.00000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 91.10000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 91.20000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 91.30000 cm with 1 decimals of accuracy
seisuk:617 - idk
sensor:093 - 'Seisuk': Sending state 91.70000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 91.70000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 91.70000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 91.80000 cm wit

Might be a simple case of fixing the segment map:

// provided map gives error on "4" // data[2] is data2 = EDGE CASE if contain 4 (74, 84, 94, 104, 114, 124)
 // static const int SEGMENT_MAP[10] = {0x3f, 0x06, 0x5b, 0x4f, 0x67, 0x6d, 0x7d, 0x07, 0x7f, 0x6f};
                                     //  0     1     2     3     4     5     6     7     8     9     
 static const int SEGMENT_MAP[10] = {0x3f, 0x06, 0x5b, 0x4f, 0x67, 0x6d, 0x7d, 0x07, 0x7f, 0x6f};

I don’t see why you’re quoting the debug menu, it’s not relevant to the UART proxying.

Also, I had the MC and HC replaced by my retailer as mine died, and on the new controller (slightly different model with push buttons, PCB has no labels at all), the default values changed somewhat:

Menu option Old value New value
1 1 0
2 3 3
3 6 6
4 6 6
5 5 4
6 60 60
7 1 1

I’m guessing field 6 is the minimum height (as it is 60cm on my unit), however changing this does not effect the displayed values, even after a reset.

It’s still unclear what the rest means.

As for your issues with updates, I presume you mean ESPHome updates?