ESPHomeHome Assistant

No-solder standing desk controller

An ESP32 on the desk handset port reads height and exposes sit and stand presets to Home Assistant. A sitting timer presses stand while the desk is occupied, or sends a nudge instead. Nothing is soldered to the desk.

Difficulty · IntermediateSetup · 60–120 minutesHA · current stable (native API)ESPHome · current stable
Get the pack · from $19Have us set it up

Some hardware links on this page are affiliate links. As an Amazon Associate I earn from qualifying purchases. We may earn a commission if you buy through them, at no extra cost to you. See the affiliate disclosure.

A sit/stand desk’s handset already speaks a small serial protocol over the RJ45 port on the control box. This pack puts an ESP32 on that port, reads the height the box is painting for the handset display, and presses Up or Down the way the handset would. Home Assistant gets the height, a sit preset, a stand preset, and a stop. A sitting timer either presses stand, when a presence sensor says the office is occupied, or sends a nudge and leaves the desk alone.

Wi-Fi credentials and the API encryption key stay in secrets.yaml. Do not publish a desk height you treat as private, and do not publish which room this is beyond a generic name.

Safety and compatibility

This moves a motorized desk. The only obstruction behavior is whatever the control box already does. Keep chairs, cables, and pets out from under the top. The automation in this pack presses Stand only while the presence sensor is on. Use the nudge path until that sensor exists.

Only this handset protocol is supported. Open the handset (a few screws) and check the board before you wire anything.

Part Marking this pack supports
Control box MXKZH06
Handset MXSK05_A (Up, Down, presets, a 3-digit display)
Handset chip TM1650

A different chip uses different addresses. This YAML will not move that desk correctly, and guessing key codes is how a desk gets a factory reset. The reset combo on this box is Up and Down together. The pack refuses to send both. Do not remove that refusal.

The original handset still works. It is unplugged while the ESP32 cable is in the control box. Unplug the ESP32 cable and plug the handset back in to return to the buttons. Do not put both on the port.

What problem it solves

You want the desk in Home Assistant, with a height you can read and a reminder to stand, without soldering the control box or giving up the handset.

How it works

The ESP32 is a stand-in for the handset. It watches the clock and data lines, decodes the height from the display bytes, and holds Up or Down until the height is close to the target.

  1. Three wires — RJ45 pin 2 is clock, to GPIO18. Pin 4 is data, to GPIO19. Pin 3 is ground, to GND. Pin 1 is 3.3 V from the control box. Leave it disconnected. Power the ESP32 from USB. Pins 5–8 stay disconnected. The breakout is labeled by pin number. Use those numbers. A T568B patch cable is orange on pin 2, green/white on pin 3, and blue on pin 4, and a handset cable is often a different color code. The pin number wins.
  2. Height — the sensor reports inches, from the digits the box is already sending. Readings outside 18–60 inches are ignored. At the bottom of travel the box can show a code that is not a height. That reading is dropped.
  3. Sit and stand — two number entities (pack defaults 27 in and 42 in, range 22–52). The buttons drive to those heights. The box keeps moving a little after the key is released, so a “stop early” number (pack default 0.3 in) lets go before the target. Past the target, raise it. Short of the target, lower it.
  4. The box gives up on a long hold — a single keypress runs for about 15 seconds and then the box stops. The pack presses again on a long move. It also presses again if the desk has not started within a few seconds (the box was asleep), and it gives up if motion stops early (end of travel, or something in the way) or if the whole move runs too long. Stop, or a nudge, cancels a move.
  5. Sitting timer — the ESP32 counts minutes since the height last went to the sitting side of the midpoint between the two presets. The automation compares that count to a limit (pack default 45 minutes). If presence is on, it presses Stand. If you picked nudge, or presence is off, it sends a notification and does not move the desk. A reboot of the ESP32 clears the minute count. The desk does not stand itself just because the chip restarted.

Hardware notes

  • Board. An original ESP32-WROOM-32 dev board. The linked board is the HiLetgo ESP-WROOM-32. In ESPHome use board: esp32dev for that devkit, or board: nodemcu-32s if your board is a NodeMCU-32S. GPIO18 and GPIO19 are the same on both. An ESP32-C3 or C6 is a different pin map. Do not use one here.
  • Headers. Nothing is soldered to the desk, the handset, or the control box. The patch cable plugs into the breakout, and jumpers go from the screws to the ESP32. Use a board whose pin headers are already soldered. Loose header strips are a soldering job on the ESP32, not on the desk. Skip a breakout that needs jumper wires soldered between the jack and the screws.
  • Straps. Keep the bus on GPIO18 and GPIO19. GPIO0, GPIO2, GPIO12, and GPIO15 are strap pins on this chip. A wire holding one of them during reset will not boot.
  • Presence. Any binary sensor that is on while someone is at the desk. The move path requires it. The nudge path does not.
  • Power. USB, from the micro-USB cable. Not from RJ45 pin 1.

Wiring

RJ45 pin Signal ESP32 Leave alone when
1 3.3 V from the box do not connect always
2 CLK GPIO18
3 GND GND
4 DAT GPIO19
5–8 unused do not connect always

Unplug the handset first. Plug the patch cable into the control box.

Snippet

Pins and the two heights are substitutions. The bus reader that decodes the display and times the keypresses ships in the pack next to this YAML.

substitutions:
  device_name: standing_desk
  clk_pin: GPIO18
  dat_pin: GPIO19
  sit_inches: "27"
  stand_inches: "42"
  stop_early_inches: "0.3"

esp32:
  board: esp32dev
  framework:
    type: esp-idf

binary_sensor:
  - platform: template
    name: "Control box connected"
    id: bus_ok
    device_class: connectivity
    lambda: return desk::connected();

sensor:
  - platform: template
    name: "Height"
    unit_of_measurement: "in"
    accuracy_decimals: 1
    update_interval: 1s
    lambda: |-
      float h = desk::known_height();
      if (std::isnan(h)) return {};
      return h;

button:
  - platform: template
    name: "Sit"
    on_press:
      - lambda: desk::go_to(${sit_inches}, ${stop_early_inches});
  - platform: template
    name: "Stand"
    on_press:
      - lambda: desk::go_to(${stand_inches}, ${stop_early_inches});
  - platform: template
    name: "Stop"
    on_press:
      - lambda: desk::stop();

The automation the pack imports, with inputs for the entities:

trigger:
  - trigger: time_pattern
    minutes: "/1"
condition:
  - condition: numeric_state
    entity_id: !input sitting_minutes
    above: !input sit_limit
action:
  - if:
      - condition: state
        entity_id: !input presence
        state: "on"
      - condition: state
        entity_id: !input action_mode
        state: "move"
    then:
      - action: button.press
        target:
          entity_id: !input stand_button
    else:
      - action: !input notify_service
        data:
          title: "Time to stand"
          message: "The desk has been at sitting height past the limit."

action_mode is an input_select with move and nudge. The minute trigger still fires after a Home Assistant restart, because a for: state trigger would not. The ESP32 minute count itself still resets if that board reboots.

Setup outline

  1. Confirm the control box and the handset chip against the table above. If the chip is not a TM1650, stop. This pack is the wrong protocol.
  2. Wire pins 2, 3, and 4 only. Leave pin 1 open. Power the ESP32 from USB. Flash the YAML.
  3. Control box connected should be on. If it is off, clock and ground are swapped or the handset is still the thing plugged into the box. Press Nudge Up once. The first press may only wake the box. The second should move it, and Height should change. Press Stop.
  4. Set sit and stand to heights you have already used from the handset. Press Sit, then Stand, from the device page while you are at the desk. Adjust stop-early until it lands on the number.
  5. Import the automation. Point presence at a binary sensor that stays off, and leave the mode on nudge, until that sensor matches the room. Then stand at the desk, confirm presence is on, switch the mode to move, and wait out a short limit you set for the test. The desk should rise. Walk away, or turn presence off, and confirm the next time it notifies and does not move.

Troubleshooting

  • Control box connected stays off. Pin 2 and pin 3, or a patch cable that is not seated. Pin 1 must stay empty. Plugging pin 1 into an ESP32 pin can fight the two 3.3 V rails.
  • Height is blank or stuck. The chip is not the TM1650 this reader expects, or data (pin 4) is on the wrong screw. Recheck the silkscreen in the handset before changing the YAML.
  • It moves the wrong way, or a short distance and stops. Stop-early is larger than the gap, or the box hit its own timeout and the retry has not pressed again yet. Wait one cycle. If it never retries, the bus reader is not the pack file.
  • The desk resets its memory. Up and Down were sent together. That is the factory reset combo on this box. Restore presets from the handset after you plug it back in, and keep the pack’s refusal in place.
  • It stood up with nobody there. Presence was on while the room was empty, or the move mode was used with the wrong binary sensor. Use nudge until presence matches the room. The automation will not press Stand while that sensor is off.
  • Sitting minutes jumped back to zero. The ESP32 rebooted. The count is on the chip, not in Home Assistant. The timer starts over. That is safer than standing the desk because of a restart.
  • The handset does nothing now. It was left unplugged. Unplug the ESP32 cable from the control box and plug the handset in. The handset is unchanged.

Credits

Based on esp32-smartdesk (ESPHome on an ESP32-WROOM-32, no solder on the desk, height and presets in Home Assistant). The handset protocol comes from desk-gateway. This write-up is an original overview of that build: the RJ45 breakout wiring, the sit and stand presets, the refusal to press Up and Down together, and a sitting timer that either stands the desk while presence is on or sends a nudge.

Changelog

v1.0 · Oct 28, 2026: First publish.