commit 12444cb5376df9156c9f3acd9b36bba195c78673
Author: Rocka84 <f.dillmeier@gmail.com>
Date:   Mon Jan 8 22:30:27 2024 +0100

    initial commit

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..59ee0a5
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+**/__pycache__/**
diff --git a/.vimrc b/.vimrc
new file mode 100644
index 0000000..00f4114
--- /dev/null
+++ b/.vimrc
@@ -0,0 +1,4 @@
+if SpmSetProject("esphome_components") == 0
+    nmap <leader>h yy'fpwd2f:f)lC;<esc>==mf^w
+    nmap <leader>s 02w"aye2b02w"byej/::a
Vf{%jd/::b
f{%jpzz^wj
+endif
diff --git a/components/jiecang_desk_controller/README.md b/components/jiecang_desk_controller/README.md
new file mode 100644
index 0000000..c5a2ccb
--- /dev/null
+++ b/components/jiecang_desk_controller/README.md
@@ -0,0 +1,120 @@
+# ESPHome Jiecang Desk Controller
+
+[ESPHome](https://esphome.io/) component for controlling Jiecang desk controllers via their serial protocol.
+
+Attention: I only have one [RJ12 model](https://www.jiecang.com/product/jcb35m11c.html) to test this
+but it's likely that other Jiecang controllers are supported as their serial protocol should be compatible.
+
+## Usage
+
+### What you need
+
+* ESPHome compatible microcontroller
+* depending on your model of desk controller
+    * cable with RJ12 connector (phone cable, RJ11 may also work)
+    * cable with RJ45 connector (network cable)
+
+### Wiring
+
+#### RJ12
+
+Please double check this for your specific model!
+
+pin | function
+----|---------
+ 1  | NC (pulled up)
+ 2  | GND
+ 3  | TX
+ 4  | VCC
+ 5  | GND
+ 6  | NC (pulled up)
+
+#### RJ45
+
+Untested and only for reference!
+
+pin | function
+----|---------
+ 1  | HS3 [^1]
+ 2  | TX
+ 3  | GND
+ 4  | RX
+ 5  | VCC
+ 6  | HS2 [^1]
+ 7  | HS1 [^1]
+ 8  | HS0 [^1]
+
+[^1]: not used here
+
+#### microcontroller
+
+ESP | desk
+----|-----
+GND | GND
+5V  | VCC
+RX  | TX
+TX  | RX
+
+## Usage
+
+```yaml
+uart:
+  id: uart_bus
+  tx_pin: TX
+  rx_pin: RX
+  baud_rate: 9600
+
+external_components:
+  - source:
+      type: git
+      url: https://github.com/Rocka84/esphome_components/
+    components: [ jiecang_desk_controller ]
+
+jiecang_desk_controller:
+  id: my_desk
+  sensors:
+    height:
+      name: "Height"
+  buttons:
+    raise:
+      name: "Raise"
+    position1:
+      name: "Position 1"
+
+button:
+  - platform: template
+    name: "Lower"
+    on_press:
+      lambda: "id(my_desk).lower();"
+```
+
+See also [example_jiecang_desk_controller.yaml](../../example_jiecang_desk_controller.yaml).
+
+### Available sensors
+
+sensor     | description
+-----------|----------------------------
+height     | current height of the desk
+height_pct | height in percent
+height_min | minimal height
+height_max | maximal height
+position1  | height of 1st stored height
+position2  | height of 2nd stored height
+position3  | height of 3rd stored height
+position4  | height of 4th stored height
+
+### Available buttons and methods
+
+button     | lambda method                  | description
+-----------|--------------------------------|---------------------------
+raise      | `id(my_desk).raise()`          | raise desk by one step (~14mm)
+lower      | `id(my_desk).raise()`          | lower desk by one step (~14mm)
+position1  | `id(my_desk).goto_position(1)` | move to 1st stored height
+position2  | `id(my_desk).goto_position(2)` | move to 2nd stored height
+position3  | `id(my_desk).goto_position(3)` | move to 3rd stored height
+position4  | `id(my_desk).goto_position(4)` | move to 4th stored height
+
+## Sources
+
+Thanks to [phord/Jarvis](https://github.com/phord/Jarvis) for reverse engineering the UART interface and most control messages
+
diff --git a/components/jiecang_desk_controller/__init__.py b/components/jiecang_desk_controller/__init__.py
new file mode 100644
index 0000000..0cbef8e
--- /dev/null
+++ b/components/jiecang_desk_controller/__init__.py
@@ -0,0 +1,128 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome import pins
+from esphome.components import uart, sensor, button
+from esphome.const import CONF_ID, CONF_HEIGHT, UNIT_CENTIMETER, UNIT_PERCENT
+
+DEPENDENCIES = ['uart']
+AUTO_LOAD = ['sensor', 'button']
+
+jiecang_desk_controller_ns = cg.esphome_ns.namespace('jiecang_desk_controller')
+
+JiecangDeskController = jiecang_desk_controller_ns.class_('JiecangDeskController', cg.Component, uart.UARTDevice)
+JiecangDeskButton = jiecang_desk_controller_ns.class_('JiecangDeskButton', button.Button, cg.Component)
+
+
+CONF_SENSORS = "sensors"
+CONF_BUTTONS = "buttons"
+CONF_UNIT = "unit"
+CONF_HEIGHT_MIN = "height_min"
+CONF_HEIGHT_MAX = "height_max"
+CONF_HEIGHT_PCT = "height_pct"
+CONF_POSITION1 = "position1"
+CONF_POSITION2 = "position2"
+CONF_POSITION3 = "position3"
+CONF_POSITION4 = "position4"
+CONF_RAISE = "raise"
+CONF_LOWER = "lower"
+
+
+button_constants = {}
+button_constants[CONF_RAISE] = 0
+button_constants[CONF_LOWER] = 1
+button_constants[CONF_POSITION1] = 2
+button_constants[CONF_POSITION2] = 3
+button_constants[CONF_POSITION3] = 4
+button_constants[CONF_POSITION4] = 5
+
+CONFIG_SCHEMA = cv.COMPONENT_SCHEMA.extend({
+    cv.GenerateID(): cv.declare_id(JiecangDeskController),
+    cv.Optional(CONF_SENSORS): cv.Schema({
+        cv.Optional(CONF_HEIGHT): sensor.sensor_schema(
+            accuracy_decimals = 1,
+            unit_of_measurement = UNIT_CENTIMETER
+        ),
+        cv.Optional(CONF_UNIT): sensor.sensor_schema(
+            accuracy_decimals = 0
+        ),
+        cv.Optional(CONF_HEIGHT_PCT): sensor.sensor_schema(
+            accuracy_decimals = 1,
+            unit_of_measurement = UNIT_PERCENT
+        ),
+        cv.Optional(CONF_HEIGHT_MIN): sensor.sensor_schema(
+            accuracy_decimals = 1,
+            unit_of_measurement = UNIT_CENTIMETER
+        ),
+        cv.Optional(CONF_HEIGHT_MAX): sensor.sensor_schema(
+            accuracy_decimals = 1,
+            unit_of_measurement = UNIT_CENTIMETER
+        ),
+        cv.Optional(CONF_POSITION1): sensor.sensor_schema(
+            accuracy_decimals = 1,
+            unit_of_measurement = UNIT_CENTIMETER
+        ),
+        cv.Optional(CONF_POSITION2): sensor.sensor_schema(
+            accuracy_decimals = 1,
+            unit_of_measurement = UNIT_CENTIMETER
+        ),
+        cv.Optional(CONF_POSITION3): sensor.sensor_schema(
+            accuracy_decimals = 1,
+            unit_of_measurement = UNIT_CENTIMETER
+        ),
+        cv.Optional(CONF_POSITION4): sensor.sensor_schema(
+            accuracy_decimals = 1,
+            unit_of_measurement = UNIT_CENTIMETER
+        ),
+    }),
+    cv.Optional(CONF_BUTTONS): cv.Schema({
+        cv.Optional(CONF_RAISE): button.BUTTON_SCHEMA.extend({cv.GenerateID(): cv.declare_id(JiecangDeskButton)}),
+        cv.Optional(CONF_LOWER): button.BUTTON_SCHEMA.extend({cv.GenerateID(): cv.declare_id(JiecangDeskButton)}),
+        cv.Optional(CONF_POSITION1): button.BUTTON_SCHEMA.extend({cv.GenerateID(): cv.declare_id(JiecangDeskButton)}),
+        cv.Optional(CONF_POSITION2): button.BUTTON_SCHEMA.extend({cv.GenerateID(): cv.declare_id(JiecangDeskButton)}),
+        cv.Optional(CONF_POSITION3): button.BUTTON_SCHEMA.extend({cv.GenerateID(): cv.declare_id(JiecangDeskButton)}),
+        cv.Optional(CONF_POSITION4): button.BUTTON_SCHEMA.extend({cv.GenerateID(): cv.declare_id(JiecangDeskButton)}),
+    }),
+}).extend(uart.UART_DEVICE_SCHEMA)
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
+    await uart.register_uart_device(var, config)
+
+    if CONF_SENSORS in config:
+        sensors = config[CONF_SENSORS]
+
+        if CONF_HEIGHT in sensors:
+            sens = await sensor.new_sensor(sensors[CONF_HEIGHT])
+            cg.add(var.set_sensor_height(sens))
+        if CONF_UNIT in sensors:
+            sens = await sensor.new_sensor(sensors[CONF_UNIT])
+            cg.add(var.set_sensor_unit(sens))
+        if CONF_HEIGHT_MIN in sensors:
+            sens = await sensor.new_sensor(sensors[CONF_HEIGHT_MIN])
+            cg.add(var.set_sensor_height_min(sens))
+        if CONF_HEIGHT_MAX in sensors:
+            sens = await sensor.new_sensor(sensors[CONF_HEIGHT_MAX])
+            cg.add(var.set_sensor_height_max(sens))
+        if CONF_HEIGHT_PCT in sensors:
+            sens = await sensor.new_sensor(sensors[CONF_HEIGHT_PCT])
+            cg.add(var.set_sensor_height_pct(sens))
+        if CONF_POSITION1 in sensors:
+            sens = await sensor.new_sensor(sensors[CONF_POSITION1])
+            cg.add(var.set_sensor_position1(sens))
+        if CONF_POSITION2 in sensors:
+            sens = await sensor.new_sensor(sensors[CONF_POSITION2])
+            cg.add(var.set_sensor_position2(sens))
+        if CONF_POSITION3 in sensors:
+            sens = await sensor.new_sensor(sensors[CONF_POSITION3])
+            cg.add(var.set_sensor_position3(sens))
+        if CONF_POSITION4 in sensors:
+            sens = await sensor.new_sensor(sensors[CONF_POSITION4])
+            cg.add(var.set_sensor_position4(sens))
+
+    if CONF_BUTTONS in config:
+        buttons = config[CONF_BUTTONS]
+        for button_type in buttons.keys():
+            btn = await button.new_button(buttons[button_type])
+            cg.add(var.add_button(btn, button_constants[button_type]))
+
diff --git a/components/jiecang_desk_controller/jiecang_desk_controller.cpp b/components/jiecang_desk_controller/jiecang_desk_controller.cpp
new file mode 100644
index 0000000..5bcf60a
--- /dev/null
+++ b/components/jiecang_desk_controller/jiecang_desk_controller.cpp
@@ -0,0 +1,203 @@
+#include "jiecang_desk_controller.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+    namespace jiecang_desk_controller {
+
+        static const char *const TAG = "jiecang_desk_controller";
+
+        float JiecangDeskController::byte2float(int high, int low) {
+            return static_cast<float>((high<<8) + low)/10;
+        }
+
+        bool JiecangDeskController::bufferMessage(int data, unsigned int *buffer, int len) {
+            // This is a very simple method of receiving messages from the desk.
+            // It will fail on messages that contain the value 0x7E in their payload.
+            // But it is super simple and works for the messages we care about.
+
+            // format: 0xF2 0xF2 [command] [param_count] [[param] ...] [checksum] 0x7E
+            // checksum: sum of [command], [param_count] and all [param]s
+
+            static int cmd_incoming = 0; // 0: wait for F2, 1: wait for 2nd F2, 2: buffer data
+            static int pos = 0;
+
+            if (cmd_incoming < 2 && data == 0xF2) { // start of message, must appear twice
+                cmd_incoming++;
+                pos = 0;
+
+            } else if (cmd_incoming == 1) { // no second F2 received
+                cmd_incoming = 0;
+
+            } else if (cmd_incoming == 2) {
+                if (data == 0x7E) { // end of message
+                    cmd_incoming = 0;
+                    for (;pos<len-1;pos++) { // fill rest of buffer with zeros
+                        buffer[pos]=0;
+                    }
+                    return true;
+
+                } else if (pos >= len) { // message too long, drop it
+                    cmd_incoming = 0;
+
+                } else {
+                    buffer[pos++] = data; // buffer data
+                }
+
+            } // else: received garbage
+
+            return false;
+        }
+
+        void JiecangDeskController::handleMessage(unsigned int *message) {
+            // ESP_LOGV("jiecang_desk_controller", "message %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X", message[0], message[1], message[2], message[3], message[4], message[5], message[6], message[7], message[8], message[9]);
+
+            switch (message[0]) {
+                case 0x01:
+                    ESP_LOGV("jiecang_desk_controller", "height 0x%0X%0X", message[2], message[3]);
+                    if (height != nullptr) height->publish_state(byte2float(message[2], message[3]));
+
+                    if (height_pct != nullptr)
+                        height_pct->publish_state((height->state - height_min->state) / (height_max->state - height_min->state) * 100);
+                    break;
+
+                case 0x0e:
+                    ESP_LOGV("jiecang_desk_controller", "unit 0x%0X", message[2]);
+                    if (unit != nullptr) unit->publish_state(message[2]);
+                    break;
+
+                case 0x20:
+                    ESP_LOGV("jiecang_desk_controller", "limits 0x%0X  max %i min %i", message[2], (message[2] & 1), (message[2]>>4));
+
+                    if (height_min != nullptr && (message[2] & 1) == 0) { // low nibble 0 -> no max limit, use physical_max
+                        height_max->publish_state(physical_max);
+                    }
+                    if (height_max != nullptr && (message[2]>>4) == 0) { // high nibble 0 -> no min limit, use physical_min
+                        height_min->publish_state(physical_min);
+                    }
+                    break;
+
+                case 0x07:
+                    ESP_LOGV("jiecang_desk_controller", "physical limits 0x%02X%02X 0x%02X%02X", message[2], message[3], message[4], message[5]);
+                    physical_max = byte2float(message[2], message[3]);
+                    physical_min = byte2float(message[4], message[5]);
+                    break;
+
+                case 0x21:
+                    ESP_LOGV("jiecang_desk_controller", "height_max 0x%02X%02X", message[2], message[3]);
+                    if (height_max != nullptr) height_max->publish_state(byte2float(message[2], message[3]));
+                    break;
+
+                case 0x22:
+                    ESP_LOGV("jiecang_desk_controller", "height_min 0x%02X%02X", message[2], message[3]);
+                    if (height_min != nullptr) height_min->publish_state(byte2float(message[2], message[3]));
+                    break;
+
+                case 0x25:
+                    ESP_LOGV("jiecang_desk_controller", "position1 0x%02X%02X", message[2], message[3]);
+                    if (position1 != nullptr) position1->publish_state(byte2float(message[2], message[3]));
+                    break;
+
+                case 0x26:
+                    ESP_LOGV("jiecang_desk_controller", "position2 0x%02X%02X", message[2], message[3]);
+                    if (position2 != nullptr) position2->publish_state(byte2float(message[2], message[3]));
+                    break;
+
+                case 0x27:
+                    ESP_LOGV("jiecang_desk_controller", "position3 0x%02X%02X", message[2], message[3]);
+                    if (position3 != nullptr) position3->publish_state(byte2float(message[2], message[3]));
+                    break;
+
+                case 0x28:
+                    ESP_LOGV("jiecang_desk_controller", "position4 0x%02X%02X", message[2], message[3]);
+                    if (position4 != nullptr) position4->publish_state(byte2float(message[2], message[3]));
+                    break;
+
+                default:
+                    ESP_LOGV("jiecang_desk_controller", "unknown message %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X", message[0], message[1], message[2], message[3], message[4], message[5], message[6], message[7], message[8], message[9]);
+            }
+        }
+
+        void JiecangDeskController::update() {
+            const int max_length = 10;
+            static unsigned int buffer[max_length];
+            while (available()) {
+                if(bufferMessage(read(), buffer, max_length)) {
+                    handleMessage(buffer);
+                }
+            }
+        }
+
+        void JiecangDeskController::send_simple_command(unsigned char cmd) {
+            write_array({ 0xF1, 0xF1, cmd, 0x00, cmd, 0x7E });
+        }
+
+        void JiecangDeskController::add_button(button::Button *btn, int action) {
+            btn->add_on_press_callback([this, action]() { this->button_press_action(action); });
+        }
+
+        void JiecangDeskController::raise() {
+            send_simple_command(0x01);
+        }
+
+        void JiecangDeskController::lower() {
+            send_simple_command(0x02);
+        }
+
+        void JiecangDeskController::goto_position(int pos) {
+            switch (pos) {
+                case 1:
+                    send_simple_command(0x05);
+                    break;
+                case 2:
+                    send_simple_command(0x06);
+                    break;
+                case 3:
+                    send_simple_command(0x27);
+                    break;
+                case 4:
+                    send_simple_command(0x28);
+                    break;
+            }
+        }
+
+        void JiecangDeskController::request_physical_limits() {
+            send_simple_command(0x0C);
+        }
+
+        void JiecangDeskController::request_limits() {
+            send_simple_command(0x20);
+        }
+
+        void JiecangDeskController::request_settings() {
+            send_simple_command(0x07);
+        }
+
+        void JiecangDeskController::button_press_action(int action) {
+            ESP_LOGV("JiecangDeskController", "button_press_action %i", action);
+            switch (action) {
+                case BUTTON_RAISE:
+                    raise();
+                    break;
+                case BUTTON_LOWER:
+                    lower();
+                    break;
+                case BUTTON_POSITION1:
+                    goto_position(1);
+                    break;
+                case BUTTON_POSITION2:
+                    goto_position(2);
+                    break;
+                case BUTTON_POSITION3:
+                    goto_position(3);
+                    break;
+                case BUTTON_POSITION4:
+                    goto_position(4);
+                    break;
+            }
+        }
+
+        void JiecangDeskButton::press_action() {}
+
+    } //namespace jiecang_desk_controller
+} //namespace esphome
+
diff --git a/components/jiecang_desk_controller/jiecang_desk_controller.h b/components/jiecang_desk_controller/jiecang_desk_controller.h
new file mode 100644
index 0000000..f16197e
--- /dev/null
+++ b/components/jiecang_desk_controller/jiecang_desk_controller.h
@@ -0,0 +1,76 @@
+#pragma once
+
+#include <future>
+#include "esphome/core/component.h"
+#include "esphome/components/sensor/sensor.h"
+#include "esphome/components/button/button.h"
+#include "esphome/components/uart/uart.h"
+#include "esphome/core/hal.h"
+#include "esphome/core/log.h"
+
+#define BUTTON_RAISE 0
+#define BUTTON_LOWER 1
+#define BUTTON_POSITION1 2
+#define BUTTON_POSITION2 3
+#define BUTTON_POSITION3 4
+#define BUTTON_POSITION4 5
+
+namespace esphome {
+    namespace jiecang_desk_controller {
+
+        class JiecangDeskController : public PollingComponent, public sensor::Sensor, public uart::UARTDevice {
+            private:
+                float physical_min = 0;
+                float physical_max = 0;
+
+                float byte2float(int high, int low);
+                bool bufferMessage(int data, unsigned int *buffer, int len);
+                void handleMessage(unsigned int *message);
+
+            public:
+                void update() override;
+
+                void set_sensor_height(sensor::Sensor *sensor) { this->height = sensor; }
+                void set_sensor_unit(sensor::Sensor *sensor) { this->unit = sensor; }
+                void set_sensor_height_min(sensor::Sensor *sensor) { this->height_min = sensor; }
+                void set_sensor_height_max(sensor::Sensor *sensor) { this->height_max = sensor; }
+                void set_sensor_height_pct(sensor::Sensor *sensor) { this->height_pct = sensor; }
+
+                void set_sensor_position1(sensor::Sensor *sensor) { this->position1 = sensor; }
+                void set_sensor_position2(sensor::Sensor *sensor) { this->position2 = sensor; }
+                void set_sensor_position3(sensor::Sensor *sensor) { this->position3 = sensor; }
+                void set_sensor_position4(sensor::Sensor *sensor) { this->position4 = sensor; }
+
+                void send_simple_command(unsigned char cmd);
+                void add_button(button::Button *btn, int action);
+
+                void raise();
+                void lower();
+                void goto_position(int pos);
+
+                void request_physical_limits();
+                void request_limits();
+                void request_settings();
+
+            protected:
+                Sensor *height{nullptr};
+                Sensor *unit{nullptr};
+                Sensor *height_min{nullptr};
+                Sensor *height_max{nullptr};
+                Sensor *height_pct{nullptr};
+
+                Sensor *position1{nullptr};
+                Sensor *position2{nullptr};
+                Sensor *position3{nullptr};
+                Sensor *position4{nullptr};
+
+                void button_press_action(int type);
+        };
+
+        class JiecangDeskButton : public Component, public button::Button {
+            protected:
+                void press_action() override;
+        };
+
+    } //namespace jiecang_desk_controller
+} //namespace esphome
diff --git a/example_jiecang_desk_controller.yaml b/example_jiecang_desk_controller.yaml
new file mode 100644
index 0000000..30e264b
--- /dev/null
+++ b/example_jiecang_desk_controller.yaml
@@ -0,0 +1,108 @@
+esphome:
+  name: jiecang-desk-controller
+  friendly_name: Jiecang Desk Controller
+  on_boot:
+    priority: 0 # when mostly everything else is done
+    then:
+      - lambda: "id(my_desk).request_physical_limits();"
+      - delay: 0.1s
+      - lambda: "id(my_desk).request_limits();"
+      - delay: 0.1s
+      - lambda: "id(my_desk).request_settings();"
+
+esp8266:
+  board: esp01_1m
+
+logger:
+  level: WARN
+  baud_rate: 0 # disable logging over uart, required when using the RX/TX pins for the controller
+
+api:
+
+ota:
+
+wifi:
+  ssid: !secret wifi_ssid
+  password: !secret wifi_password
+
+  # Enable fallback hotspot (captive portal) in case wifi connection fails
+  ap:
+    ssid: "esphome-desk"
+    password: "9ebc6eac0b4e0e26b8d3b955ec660557"
+
+captive_portal:
+
+
+
+uart:
+  id: uart_bus
+  tx_pin: TX
+  rx_pin: RX
+  baud_rate: 9600
+
+external_components:
+  - source:
+      type: git
+      url: https://github.com/Rocka84/esphome_components/
+    components: [ jiecang_desk_controller ]
+
+jiecang_desk_controller:
+  id: my_desk
+  sensors:
+    height:
+      name: "Height"
+    height_min:
+      name: "Height Min"
+    height_max:
+      name: "Height Max"
+    height_pct:
+      name: "Height Percent"
+    position1:
+      name: "Position 1"
+    position2:
+      name: "Position 2"
+    position3:
+      name: "Position 3"
+    position4:
+      name: "Position 4"
+  buttons:
+    raise:
+      name: "Raise"
+    lower:
+      name: "Lower"
+    position1:
+      name: "Position 1"
+    position2:
+      name: "Position 2"
+    position3:
+      name: "Position 3"
+    position4:
+      name: "Position 4"
+
+## lambda usage
+# button:
+#   - platform: template
+#     name: "Raise"
+#     on_press:
+#       lambda: "id(my_desk).raise();"
+#   - platform: template
+#     name: "Lower"
+#     on_press:
+#       lambda: "id(my_desk).lower();"
+#   - platform: template
+#     name: "Position 1"
+#     on_press:
+#       lambda: "id(my_desk).goto_position(1);"
+#   - platform: template
+#     name: "Position 2"
+#     on_press:
+#       lambda: "id(my_desk).goto_position(2);"
+#   - platform: template
+#     name: "Position 3"
+#     on_press:
+#       lambda: "id(my_desk).goto_position(3);"
+#   - platform: template
+#     name: "Position 4"
+#     on_press:
+#       lambda: "id(my_desk).goto_position(4);"
+
