diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..c83521e --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,32 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/python +{ + "name": "Python 3", + "image": "mcr.microsoft.com/devcontainers/python:1-3.11-bookworm", + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "pip3 install --user -r requirements.txt && pre-commit install", + + // Configure tool-specific properties. + "customizations": { + "vscode": { + "extensions": [ + "GitHub.copilot-chat", + "ESPHome.esphome-vscode", + "redhat.vscode-yaml", + "ms-python.python", + "ms-vscode.cpptools", + "ms-vscode.cpptools-extension-pack" + ] + } + } + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..9033f3e --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @iMicknl diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..a760482 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +version: 2 +updates: + - package-ecosystem: "devcontainers" + directory: "/" + schedule: + interval: daily + time: "08:00" + + - package-ecosystem: pip + directory: "/" + schedule: + interval: "daily" + time: "08:00" diff --git a/.github/workflows/build-and-publish.yaml b/.github/workflows/build-and-publish.yaml new file mode 100644 index 0000000..6ee8977 --- /dev/null +++ b/.github/workflows/build-and-publish.yaml @@ -0,0 +1,3 @@ +# uses: esphome/build-action@v1 +# with: +# yaml_file: my_configuration.yaml diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 0000000..854b748 --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,29 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + +jobs: + pre-commit: + name: "Linting and formatting" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: "3.11" + cache: "pip" + + - name: "Install Python dependencies" + run: | + python -m pip install --upgrade pip + python -m pip install -r requirements.txt + + - uses: pre-commit/action@v3.0.1 + + # - uses: pre-commit-ci/lite-action@v1.0.2 + # if: always() diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..da446e7 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,37 @@ +# Test ESPHome configuration files using local components +name: Compile and validate YAML configuration + +on: + pull_request: + push: + branches: [main] + +jobs: + tests: + name: "${{ matrix.version }}" + runs-on: "ubuntu-latest" + + strategy: + fail-fast: false + matrix: + version: ["esp32", "esp8266", "esp32-passthrough", "esp8266-passthrough"] + + steps: + - uses: "actions/checkout@v4" + + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: "3.11" + cache: "pip" + + - name: "Install Python dependencies" + run: | + python -m pip install --upgrade pip + python -m pip install -r requirements.txt + + - name: "Validate ${{ matrix.version }}" + run: esphome config tests/office-desk-${{ matrix.version }}.yaml + + - name: "Compile ${{ matrix.version }}" + run: esphome compile tests/office-desk-${{ matrix.version }}.yaml diff --git a/.github/workflows/validate-packages.yaml b/.github/workflows/validate-packages.yaml new file mode 100644 index 0000000..b9db4f0 --- /dev/null +++ b/.github/workflows/validate-packages.yaml @@ -0,0 +1,37 @@ +# Validate ESPHome packages using latest published components +name: Validate ESPHome packages + +on: + pull_request: + push: + branches: [main] + +jobs: + tests: + name: "${{ matrix.version }}" + runs-on: "ubuntu-latest" + + strategy: + fail-fast: false + matrix: + version: ["esp32"] + + steps: + - uses: "actions/checkout@v4" + + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: "3.11" + cache: "pip" + + - name: "Install Python dependencies" + run: | + python -m pip install --upgrade pip + python -m pip install -r requirements.txt + + - name: "Validate ${{ matrix.version }}" + run: esphome config tests/office-desk-${{ matrix.version }}.yaml + + - name: "Compile ${{ matrix.version }}" + run: esphome compile tests/office-desk-${{ matrix.version }}.yaml diff --git a/.gitignore b/.gitignore index 1e59088..8475e9c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ venv -.esphome \ No newline at end of file +.esphome + +*.pyc +__pycache__/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..8c732e9 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,21 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: check-yaml + args: ['--unsafe'] # required for !secret + - id: trailing-whitespace + - id: end-of-file-fixer + + # - repo: https://github.com/pocc/pre-commit-hooks + # rev: v1.3.5 + # hooks: + # - id: clang-format + # - id: clang-tidy + # - id: cppcheck + # - id: cpplint + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v18.1.6 + hooks: + - id: clang-format + types_or: [c++, c, cuda] diff --git a/README.md b/README.md index 050acbb..ed0b79f 100644 --- a/README.md +++ b/README.md @@ -14,11 +14,11 @@ This repository will help you to connect your desk to the internet via the seria > Use the information in this repository at your own risk and with caution. Tinkering with electronics always has risks. -| Name | Description | -| ------------------------------------- | -------------------------------------------------------------------------- | -| [Arduino](packages/arduino) | Custom code to control your desk via an ESP32/ESP8266 module via MQTT. | -| [ESPHome](packages/esphome) | Control your desk via an ESP32/ESP8266 module connected to Home Assistant. | -| [Raspberry Pi](packages/raspberry-pi) | Custom code to control your desk via a Raspberry Pi via Python. | +| Name | Description | +| ------------------------------------ | -------------------------------------------------------------------------------------------------------------------- | +| [ESPHome](packages/office-desk.yaml) | Control your desk via an ESP32 module connected to Home Assistant. Can be adapted to ESP8266 or other ESP32 variant. | + +The V1 packages, including the Arduino and Raspberry Pi ones, can be found in the `archive` directory. For more packaged solutions, see [similar projects](#similar-projects--research). Pull requests are welcome. @@ -30,10 +30,15 @@ If you are interested in the internals of the LoctecMotion desk system, have a l At the time of writing, LoctekMotion sells [11 different control panels](https://www.loctekmotion.com/product/control-panel/). The features can differ per model, but it looks like the serial interface is pretty similar for the more advanced models. -The tables below will show a mapping of the RJ45 pinout to the pinout used by the control panel. Please note that all RJ45 pins are described in the following way; +The tables below will show a mapping of the RJ45 pinout to the pinout used by the control panel. Please note that all RJ45 pins are described in the following way: ![RJ-45 connector layout](images/RJ-45_connector.jpg) +The most common [color convention](https://www.showmecables.com/blog/post/rj45-pinout) +for wiring RJ45 for network cables is: + +![RJ45 T568B colors](images/RJ45-Pinout-T568B.jpg) + In order to connect the control box to a Raspberry Pi and ESP32/ESP8266 chip I used a [RJ45 to RS232 adapter](https://www.allekabels.nl/rs232-kabel/4568/1041186/rj45-naar-rs232.html) with DuPont cables (jump wires), but you simply can cut and split an ethernet cable as well. #### Supported Control Panels @@ -49,70 +54,73 @@ In order to connect the control box to a Raspberry Pi and ESP32/ESP8266 chip I u -If your control panel is missing, feel free to [create an issue](https://github.com/iMicknl/LoctekMotion_IoT/issues/new) to discuss the possibilities or create a PR to add your research to this overview. +If your control panel is missing, feel free to [create an issue](https://github.com/iMicknl/LoctekMotion_IoT/issues/new) to discuss the possibilities or create a PR to add your research to this overview. #### HS13B-1 - **Desk model**: Flexispot E7 - **Tested with control box**: CB38M2J(IB)-1 - **Source**: Printed on the PCB of the control box. - + | RJ45 pin | Name | Original Cable Color | Ethernet cable color (T568B) | | -------- | ---------- | -------------------- | ---------------------------- | -| 8 | RESET | Brown | White-Orange | -| 7 | SWIM | White | Orange | -| 6 | EMPTY | Purple | White-Green | -| 5 | PIN 20 | Red | Blue | -| 4 | RX | Green | White-Blue | -| 3 | TX | Black | Green | -| 2 | GND | Blue | White-Brown | -| 1 | +5V (VDD) | Yellow | Brown | - -Note that RX and TX is defined like this on receiver (control panel) side. So RX can be used to receive data, TX to send data. +| 1 | RESET | Brown | White-Orange | +| 2 | SWIM | White | Orange | +| 3 | EMPTY | Purple | White-Green | +| 4 | PIN 20 | Red | Blue | +| 5 | RX | Green | White-Blue | +| 6 | TX | Black | Green | +| 7 | GND | Blue | White-Brown | +| 8 | +5V (VDD) | Yellow | Brown | + +Note that RX and TX is defined like this on receiver (control panel) side. +So the custom controller also uses RX to receive data and TX to send data. #### HS13A-1 - **Desk model**: Flexispot EK5 - **Tested with control box**: CB38M2B(IB)-1 - **Source**: Printed on the PCB of the control box. - + | RJ45 pin | Name | Original Cable Color | Ethernet cable color (T568B) | | -------- | ---------- | -------------------- | ---------------------------- | -| 8 | RESET SWIM | Brown | White-Orange | -| 7 | PIN 20 | White | Orange | -| 6 | RX | Purple | White-Green | -| 5 | TX | Red | Blue | -| 4 | GND1 | Green | White-Blue | -| 3 | +5V (VDD) | Black | Green | -| 2 | 29V+ | Blue | White-Brown | -| 1 | 29V- | Yellow | Brown | - -Note that RX and TX is defined like this on receiver (control panel) side. So RX can be used to receive data, TX to send data. +| 1 | RESET SWIM | Brown | White-Orange | +| 2 | PIN 20 | White | Orange | +| 3 | RX | Purple | White-Green | +| 4 | TX | Red | Blue | +| 5 | GND1 | Green | White-Blue | +| 6 | +5V (VDD) | Black | Green | +| 7 | 29V+ | Blue | White-Brown | +| 8 | 29V- | Yellow | Brown | + +Note that RX and TX is defined like this on receiver (control panel) side. +So the custom controller also uses RX to receive data and TX to send data. #### HS01B-1 - **Desk model**: Flexispot E5B - **Tested with control box**: CB38M2A-1 - **Source**: [nv1t/standing-desk-interceptor](https://github.com/nv1t/standing-desk-interceptor) - -| RJ45 pin | Name | Original Cable Color | Ethernet cable color (T568B) | -| -------- | --------- | --------------------- | ---------------------------- | -| 8 | +5V (VDD) | Yellow | Brown | -| 7 | GND | Blue | White-Brown | -| 6 | TX | Black | Green | -| 5 | RX | Green | White-Blue | -| 4 | PIN 20 | Red | Blue | -| 3 | (unknown) | Purple | White-Green | -| 2 | SWIM | White | Orange | -| 1 | RES | Brown | White-Orange | - -Note that RX and TX is defined like this on receiver (control panel) side. So RX can be used to receive data, TX to send data. + +| RJ45 pin | Name | +| -------- | --------- | +| 8 | +5V (VDD) | +| 7 | GND | +| 6 | TX | +| 5 | RX | +| 4 | PIN 20 | +| 3 | (unknown) | +| 2 | SWIM | +| 1 | RES | + +Note that RX and TX is defined like this on receiver (control panel) side. +So the custom controller also uses RX to receive data and TX to send data. Other control panels / control boxes could be supported in the same way, but you would need to figure the RJ45 pinout mapping. Most control boxes have an extra RJ45 port for serial communication, but otherwise you would need to place your device in between the control panel and the control box. ### Retrieve current height -Based upon the great work of [minifloat](https://www.mikrocontroller.net/topic/493524), it became clear that the control panel utilises a [7-segment display](https://en.wikipedia.org/wiki/Seven-segment_display). Fortunately, this is very common in such devices and thus there is a lot of [documentation](https://lastminuteengineers.com/seven-segment-arduino-tutorial/) on this topic. +Based upon the great work of [minifloat](https://www.mikrocontroller.net/topic/493524), it became clear that the control panel utilises a [7-segment display](https://en.wikipedia.org/wiki/Seven-segment_display). Fortunately, this is very common in such devices and thus there is a lot of [documentation](https://lastminuteengineers.com/seven-segment-arduino-tutorial/) on this topic. The control box sends the height as 4-bit hexadecimal, which is decoded in the control panel to drive the 7-segment display. The second number on the display also supports an optional decimal point ("8 segment"). @@ -122,6 +130,11 @@ Make sure you set the baud rate to 9600. For most LoctekMotion desks, the contro source: [alselectro](https://alselectro.wordpress.com/2015/03/03/8051-tutorials-3-interfacing-7-segment-display/) + +### Known issues +- Number entity may overshoot. For more accurate positioning, use the provided presets. + + ### Execute a command The control box only accepts commands when the 'screen is active'. To activate the screen, `PIN 20` needs to be set to HIGH for about 1 second. The screen gets disabled automatically again after some amount of time receiving no commands. @@ -146,7 +159,7 @@ All bytes combined will become the command to send to the control box. See the [ While working on this project, I found out that I am not the only one with this idea. There are a few repositories on GitHub with great research which helped me kickstart this project. ❤️ - [grssmnn / ha-flexispot-standing-desk](https://github.com/grssmnn/ha-flexispot-standing-desk) - Home Assistant integration via MQTT (micropython) -- [Dude88 / loctek_IOT_box](https://github.com/Dude88/loctek_IOT_box) - Arduino code to control via Alexa and MQTT +- [Dude88 / loctek_IOT_box](https://github.com/Dude88/loctek_IOT_box) - Arduino code to control via Alexa and MQTT - [nv1t / standing-desk-interceptor](https://github.com/nv1t/standing-desk-interceptor) - Research on intercepting commands from Flexispot desks - [VinzSpring / LoctekReverseengineering](https://github.com/VinzSpring/LoctekReverseengineering#assumptions) - Research and Python samples diff --git a/packages/arduino/README.md b/archive/arduino/README.md similarity index 100% rename from packages/arduino/README.md rename to archive/arduino/README.md diff --git a/packages/arduino/flexispot_e8.ino b/archive/arduino/flexispot_e8.ino similarity index 69% rename from packages/arduino/flexispot_e8.ino rename to archive/arduino/flexispot_e8.ino index 5c37aeb..38c1f9a 100644 --- a/packages/arduino/flexispot_e8.ino +++ b/archive/arduino/flexispot_e8.ino @@ -5,26 +5,29 @@ #include #define displayPin20 4 // D2 GPIO4 -#define rxPin 12 // D5 GPIO12 -#define txPin 14 // D6 GPIO14 +#define rxPin 12 // D5 GPIO12 +#define txPin 14 // D6 GPIO14 SoftwareSerial sSerial(rxPin, txPin); // RX, TX byte history[2]; // Supported Commands -const byte wakeup[] = { 0x9b, 0x06, 0x02, 0x00, 0x00, 0x6c, 0xa1, 0x9d }; -const byte command_up[] = { 0x9b, 0x06, 0x02, 0x01, 0x00, 0xfc, 0xa0, 0x9d }; -const byte command_down[] = { 0x9b, 0x06, 0x02, 0x02, 0x00, 0x0c, 0xa0, 0x9d }; -const byte command_m[] = {0x9b, 0x06, 0x02, 0x20, 0x00, 0xac, 0xb8, 0x9d }; -const byte command_preset_1[] = { 0x9b, 0x06, 0x02, 0x04, 0x00, 0xac, 0xa3, 0x9d }; -const byte command_preset_2[] = { 0x9b, 0x06, 0x02, 0x08, 0x00, 0xac, 0xa6, 0x9d }; -const byte command_preset_3[] = { 0x9b, 0x06, 0x02, 0x10, 0x00, 0xac, 0xac, 0x9d }; -const byte command_preset_4[] = { 0x9b, 0x06, 0x02, 0x00, 0x01, 0xac, 0x60, 0x9d }; - +const byte wakeup[] = {0x9b, 0x06, 0x02, 0x00, 0x00, 0x6c, 0xa1, 0x9d}; +const byte command_up[] = {0x9b, 0x06, 0x02, 0x01, 0x00, 0xfc, 0xa0, 0x9d}; +const byte command_down[] = {0x9b, 0x06, 0x02, 0x02, 0x00, 0x0c, 0xa0, 0x9d}; +const byte command_m[] = {0x9b, 0x06, 0x02, 0x20, 0x00, 0xac, 0xb8, 0x9d}; +const byte command_preset_1[] = {0x9b, 0x06, 0x02, 0x04, + 0x00, 0xac, 0xa3, 0x9d}; +const byte command_preset_2[] = {0x9b, 0x06, 0x02, 0x08, + 0x00, 0xac, 0xa6, 0x9d}; +const byte command_preset_3[] = {0x9b, 0x06, 0x02, 0x10, + 0x00, 0xac, 0xac, 0x9d}; +const byte command_preset_4[] = {0x9b, 0x06, 0x02, 0x00, + 0x01, 0xac, 0x60, 0x9d}; void setup() { - Serial.begin(115200); // Debug serial - sSerial.begin(9600); // Flexispot E8 + Serial.begin(115200); // Debug serial + sSerial.begin(9600); // Flexispot E8 pinMode(displayPin20, OUTPUT); digitalWrite(displayPin20, LOW); @@ -33,7 +36,6 @@ void setup() { demo(); } - void demo() { // Calls sit-preset and waits 20 seconds @@ -49,7 +51,6 @@ void demo() { wake(); } - void turnon() { // Turn desk in operating mode by setting controller pin20 to HIGH Serial.println("sending turn on command"); @@ -58,7 +59,6 @@ void turnon() { digitalWrite(displayPin20, LOW); } - void wake() { turnon(); @@ -70,7 +70,6 @@ void wake() { sSerial.enableTx(false); } - void sit() { turnon(); @@ -82,7 +81,6 @@ void sit() { sSerial.enableTx(false); } - void stand() { turnon(); @@ -94,10 +92,8 @@ void stand() { sSerial.enableTx(false); } - void loop() { - while (sSerial.available()) - { + while (sSerial.available()) { unsigned long in = sSerial.read(); // Start of packet @@ -121,4 +117,4 @@ void loop() { Serial.print(in, HEX); Serial.print(" "); } -} \ No newline at end of file +} diff --git a/archive/arduino/flexispot_ek5.ino b/archive/arduino/flexispot_ek5.ino new file mode 100644 index 0000000..a59d9d5 --- /dev/null +++ b/archive/arduino/flexispot_ek5.ino @@ -0,0 +1,67 @@ +#include + +#define displayPin20 4 // D2 GPIO4 +#define rxPin 12 // D5 GPIO12 +#define txPin 14 // D6 GPIO14 + +SoftwareSerial sSerial(rxPin, txPin); // RX, TX +byte history[2]; + +// Supported Commands +unsigned long wakeup[] = {0x9b, 0x06, 0x02, 0x00, 0x00, 0x6c, 0xa1, 0x9d}; +unsigned long command_up[] = {0x9b, 0x06, 0x02, 0x01, 0x00, 0xfc, 0xa0, 0x9d}; +unsigned long command_down[] = {0x9b, 0x06, 0x02, 0x02, 0x00, 0x0c, 0xa0, 0x9d}; +unsigned long command_m[] = {0x9b, 0x06, 0x02, 0x20, 0x00, 0xac, 0xb8, 0x9d}; +unsigned long command_preset_1[] = {0x9b, 0x06, 0x02, 0x04, + 0x00, 0xac, 0xa3, 0x9d}; +unsigned long command_preset_2[] = {0x9b, 0x06, 0x02, 0x08, + 0x00, 0xac, 0xa6, 0x9d}; +unsigned long command_preset_3[] = {0x9b, 0x06, 0x02, 0x10, + 0x00, 0xac, 0xac, 0x9d}; +unsigned long command_preset_4[] = {0x9b, 0x06, 0x02, 0x00, + 0x01, 0xac, 0x60, 0x9d}; + +void setup() { + Serial.begin(115200); // Debug serial + sSerial.begin(9600); // Flexispot EK5 + + // Turn desk in operating mode by setting controller pin20 to HIGH + // This will allow us to send commands and to receive the current height + Serial.println("Turn Operation Mode on"); + pinMode(displayPin20, OUTPUT); + digitalWrite(displayPin20, LOW); + + // Run command + Serial.println("Send command"); + sSerial.write(command_down, sizeof(command_down)); + + // Wake up to get height + sSerial.write(wakeup, sizeof(wakeup)); +} + +void loop() { + while (sSerial.available()) { + unsigned long in = sSerial.read(); + + // Start of packet + if (in == 0x9b) { + Serial.println(); + } + + // Second byte defines the message length + if (history[0] == 0x9b) { + int msg_len = (int)in; + Serial.print("(LENGTH:"); + Serial.print(in); + Serial.print(")"); + } + + // Get package length (second byte) + history[1] = history[0]; + history[0] = in; + + // Print hex for debug + Serial.print(in, HEX); + Serial.print(" "); + } +} diff --git a/packages/arduino/height_demo b/archive/arduino/height_demo similarity index 97% rename from packages/arduino/height_demo rename to archive/arduino/height_demo index 3c96a6b..e37ec70 100644 --- a/packages/arduino/height_demo +++ b/archive/arduino/height_demo @@ -6,7 +6,7 @@ #define displayPin20 4 // D2 GPIO4 #define rxPin 12 // D5 GPIO12 -#define txPin 14 // D6 GPIO14 +#define txPin 14 // D6 GPIO14 SoftwareSerial sSerial(rxPin, txPin); // RX, TX int packet_pos = 0; //position in packet @@ -107,11 +107,11 @@ void DecodeHeight() if(height != h) { height = h; - + Serial.print("Height: "); Serial.print(height); Serial.println(); - } + } } int DecodeNumber(byte in) { @@ -188,7 +188,7 @@ void loop() { while (sSerial.available()) { - unsigned long in = sSerial.read(); + unsigned long in = sSerial.read(); // Start of packet if(in == 0x9B) //start of packet @@ -211,6 +211,6 @@ void loop() else if(packet_pos == 2) //position 2 = type of packet packet_type = (int)in; else if(packet_type == 18 && packet_pos >= 3 && packet_pos <= 5) //if packet type is height (0x12) and position is 3-5 - message_height[packet_pos - 3] = in; + message_height[packet_pos - 3] = in; } } diff --git a/packages/esphome/Flexispot_E7_MQTT_control.yaml b/archive/esphome/Flexispot_E7_MQTT_control.yaml similarity index 97% rename from packages/esphome/Flexispot_E7_MQTT_control.yaml rename to archive/esphome/Flexispot_E7_MQTT_control.yaml index de98be3..eb80ce4 100644 --- a/packages/esphome/Flexispot_E7_MQTT_control.yaml +++ b/archive/esphome/Flexispot_E7_MQTT_control.yaml @@ -23,7 +23,7 @@ wifi: ssid: "${device_name} Fallback Hotspot" password: !secret ap_fallback_password -# Enable MQTT communication. ReadOnly Variables are published immediatly +# Enable MQTT communication. ReadOnly Variables are published immediatly mqtt: topic_prefix: Flexispot @@ -32,17 +32,17 @@ mqtt: port: 1883 username: !secret mqtt_username password: !secret mqtt_password - + # OnMessage is used for MQTT control - # Only a short - + # Only a short + on_message: - topic: Flexispot/Sit/command then: - switch.turn_on: switch_sit - topic: Flexispot/Stand/command then: - - switch.turn_on: switch_stand + - switch.turn_on: switch_stand - topic: Flexispot/Preset1/command then: - switch.turn_on: switch_preset1 @@ -183,14 +183,14 @@ cover: - logger.log: "Executing up command" - switch.turn_on: switch_up - delay: 10ms - + # Move desk down close_action: - while: condition: sensor.in_range: id: desk_height - above: ${min_height} + above: ${min_height} then: - logger.log: "Executing down command" - switch.turn_on: switch_down diff --git a/packages/esphome/README.md b/archive/esphome/README.md similarity index 100% rename from packages/esphome/README.md rename to archive/esphome/README.md diff --git a/packages/esphome/desk_height_sensor.h b/archive/esphome/desk_height_sensor.h similarity index 54% rename from packages/esphome/desk_height_sensor.h rename to archive/esphome/desk_height_sensor.h index 7efcdd2..a80bbfc 100644 --- a/packages/esphome/desk_height_sensor.h +++ b/archive/esphome/desk_height_sensor.h @@ -1,8 +1,7 @@ #include "esphome.h" #include -class DeskHeightSensor : public Component, public UARTDevice, public Sensor -{ +class DeskHeightSensor : public Component, public UARTDevice, public Sensor { public: DeskHeightSensor(UARTComponent *parent) : UARTDevice(parent) {} @@ -14,157 +13,124 @@ class DeskHeightSensor : public Component, public UARTDevice, public Sensor unsigned long msg_type; bool valid = false; - float get_setup_priority() const override { return esphome::setup_priority::DATA; } + float get_setup_priority() const override { + return esphome::setup_priority::DATA; + } - int hex_to_int(byte s) - { + int hex_to_int(byte s) { std::bitset<8> b(s); - if (b[0] && b[1] && b[2] && b[3] && b[4] && b[5] && !b[6]) - { + if (b[0] && b[1] && b[2] && b[3] && b[4] && b[5] && !b[6]) { return 0; } - if (not b[0] && b[1] && b[2] && !b[3] && !b[4] && !b[5] && !b[6]) - { + if (not b[0] && b[1] && b[2] && !b[3] && !b[4] && !b[5] && !b[6]) { return 1; } - if (b[0] && b[1] && !b[2] && b[3] && b[4] && !b[5] && b[6]) - { + if (b[0] && b[1] && !b[2] && b[3] && b[4] && !b[5] && b[6]) { return 2; } - if (b[0] && b[1] && b[2] && b[3] && !b[4] && !b[5] && b[6]) - { + if (b[0] && b[1] && b[2] && b[3] && !b[4] && !b[5] && b[6]) { return 3; } - if (not b[0] && b[1] && b[2] && !b[3] && !b[4] && b[5] && b[6]) - { + if (not b[0] && b[1] && b[2] && !b[3] && !b[4] && b[5] && b[6]) { return 4; } - if (b[0] && !b[1] && b[2] && b[3] && !b[4] && b[5] && b[6]) - { + if (b[0] && !b[1] && b[2] && b[3] && !b[4] && b[5] && b[6]) { return 5; } - if (b[0] && !b[1] && b[2] && b[3] && b[4] && b[5] && b[6]) - { + if (b[0] && !b[1] && b[2] && b[3] && b[4] && b[5] && b[6]) { return 6; } - if (b[0] && b[1] && b[2] && !b[3] && !b[4] && !b[5] && !b[6]) - { + if (b[0] && b[1] && b[2] && !b[3] && !b[4] && !b[5] && !b[6]) { return 7; } - if (b[0] && b[1] && b[2] && b[3] && b[4] && b[5] && b[6]) - { + if (b[0] && b[1] && b[2] && b[3] && b[4] && b[5] && b[6]) { return 8; } - if (b[0] && b[1] && b[2] && b[3] && !b[4] && b[5] && b[6]) - { + if (b[0] && b[1] && b[2] && b[3] && !b[4] && b[5] && b[6]) { return 9; } - if (!b[0] && !b[1] && !b[2] && !b[3] && !b[4] && !b[5] && b[6]) - { + if (!b[0] && !b[1] && !b[2] && !b[3] && !b[4] && !b[5] && b[6]) { return 10; } return 0; } - bool is_decimal(byte b) - { - return (b & 0x80) == 0x80; - } + bool is_decimal(byte b) { return (b & 0x80) == 0x80; } - void setup() override - { + void setup() override { // nothing to do here } - void loop() override - { - while (available() > 0) - { + void loop() override { + while (available() > 0) { byte incomingByte = read(); // ESP_LOGD("DEBUG", "Incoming byte is: %08x", incomingByte); - + // First byte, start of a packet - if (incomingByte == 0x9b) - { + if (incomingByte == 0x9b) { // Reset message length msg_len = 0; valid = false; } // Second byte defines the message length - if (history[0] == 0x9b) - { + if (history[0] == 0x9b) { msg_len = (int)incomingByte; } // Third byte is message type - if (history[1] == 0x9b) - { + if (history[1] == 0x9b) { msg_type = incomingByte; } // Fourth byte is first height digit, if msg type 0x12 & msg len 7 - if (history[2] == 0x9b) - { + if (history[2] == 0x9b) { - if (msg_type == 0x12 && (msg_len == 7 || msg_len == 10)) - { + if (msg_type == 0x12 && (msg_len == 7 || msg_len == 10)) { // Empty height - if (incomingByte == 0) - { - //ESP_LOGD("DEBUG", "Height 1 is EMPTY -> 0x%02x", incomingByte); - //deskSerial.write(command_wakeup, sizeof(command_wakeup)); - } - else if (hex_to_int(incomingByte) == 0) - { - //ESP_LOGD("DEBUG", "Invalid height 1 -> 0x%02x", incomingByte); - //deskSerial.write(command_wakeup, sizeof(command_wakeup)); - } - else - { + if (incomingByte == 0) { + // ESP_LOGD("DEBUG", "Height 1 is EMPTY -> 0x%02x", incomingByte); + // deskSerial.write(command_wakeup, sizeof(command_wakeup)); + } else if (hex_to_int(incomingByte) == 0) { + // ESP_LOGD("DEBUG", "Invalid height 1 -> 0x%02x", incomingByte); + // deskSerial.write(command_wakeup, sizeof(command_wakeup)); + } else { valid = true; - // ESP_LOGD("DEBUG", "Height 1 is: 0x%02x", incomingByte); + // ESP_LOGD("DEBUG", "Height 1 is: 0x%02x", incomingByte); } } } // Fifth byte is second height digit - if (history[3] == 0x9b) - { - if (valid == true) - { - //ESP_LOGD("DEBUG", "Height 2 is: 0x%02x", incomingByte); + if (history[3] == 0x9b) { + if (valid == true) { + // ESP_LOGD("DEBUG", "Height 2 is: 0x%02x", incomingByte); } } // Sixth byte is third height digit - if (history[4] == 0x9b) - { - if (valid == true) - { + if (history[4] == 0x9b) { + if (valid == true) { int height1 = hex_to_int(history[1]) * 100; int height2 = hex_to_int(history[0]) * 10; int height3 = hex_to_int(incomingByte); - if (height2 == 100) // check if 'number' is a hyphen, return value 10 multiplied by 10 - { - - } - else + if (height2 == 100) // check if 'number' is a hyphen, return value 10 + // multiplied by 10 { + + } else { float finalHeight = height1 + height2 + height3; - if (is_decimal(history[0])) - { + if (is_decimal(history[0])) { finalHeight = finalHeight / 10; } value = finalHeight; - // ESP_LOGD("DeskHeightSensor", "Current height is: %f", finalHeight); + // ESP_LOGD("DeskHeightSensor", "Current height is: %f", + // finalHeight); } } } - - // Save byte buffer to history arrary history[4] = history[3]; history[3] = history[2]; @@ -172,15 +138,13 @@ class DeskHeightSensor : public Component, public UARTDevice, public Sensor history[1] = history[0]; history[0] = incomingByte; - // End byte - if (incomingByte == 0x9d) - { - if (value && value != lastPublished) - { + // End byte + if (incomingByte == 0x9d) { + if (value && value != lastPublished) { publish_state(value); - lastPublished = value; - } + lastPublished = value; + } } - } + } } }; diff --git a/archive/esphome/desk_keypad.h b/archive/esphome/desk_keypad.h new file mode 100644 index 0000000..86302b0 --- /dev/null +++ b/archive/esphome/desk_keypad.h @@ -0,0 +1,98 @@ +#include "esphome.h" +#include + +class DeskKeypad : public Component, public UARTDevice, public Sensor { + +public: + DeskKeypad(UARTComponent *parent) : UARTDevice(parent) {} + + enum Command { + Up = 1, + Down = 2, + Preset1 = 3, + Preset2 = 4, + Preset3 = 5, + M = 6, + Alarm = 7, + Empty = 8 + }; + Command mReturnCommand; + Command lastPublished = Command::Empty; + unsigned long history[3]; + + int msg_len = 0; + unsigned long msg_type; + bool valid = false; + + float get_setup_priority() const override { + return esphome::setup_priority::DATA; + } + + void setup() override { + // nothing to do here + } + + void loop() override { + while (available() > 0) { + byte incomingByte = read(); + // ESP_LOGD("DEBUG", "Incoming byte is: %08x", incomingByte); + // First byte, start of a packet + if (incomingByte == 0x9b) { + // Reset message length + msg_len = 0; + valid = false; + } + + // Second byte defines the message length + if (history[0] == 0x9b) { + msg_len = (int)incomingByte; + } + + // Third byte is message type + if (history[1] == 0x9b) { + msg_type = incomingByte; + } + + // Fourth byte is first height digit, if msg type 0x12 & msg len 7 + if (history[2] == 0x9b) { + switch (incomingByte) { + case 0x00: + mReturnCommand = Command::Empty; + break; + case 0x01: + mReturnCommand = Command::Up; + break; + case 0x02: + mReturnCommand = Command::Down; + break; + case 0x04: + mReturnCommand = Command::Preset1; + break; + case 0x08: + mReturnCommand = Command::Preset2; + break; + case 0x10: + mReturnCommand = Command::Preset3; + break; + case 0x20: + mReturnCommand = Command::M; + break; + case 0x40: + mReturnCommand = Command::Alarm; + break; + } + } + + if (incomingByte == 0x9d && msg_type == 0x02 && msg_len == 6 && + mReturnCommand && mReturnCommand != lastPublished) { + publish_state(mReturnCommand); + lastPublished = mReturnCommand; + } + + // Save byte buffer to history arrary + history[2] = history[1]; + history[1] = history[0]; + history[0] = incomingByte; + } + } +}; diff --git a/packages/esphome/flexispot_e5b.yaml b/archive/esphome/flexispot_e5b.yaml similarity index 98% rename from packages/esphome/flexispot_e5b.yaml rename to archive/esphome/flexispot_e5b.yaml index 3e1225f..0b2f865 100644 --- a/packages/esphome/flexispot_e5b.yaml +++ b/archive/esphome/flexispot_e5b.yaml @@ -3,7 +3,7 @@ substitutions: name: flexispot_e5b min_height: "73.6" # Min height + 0.1 max_height: "122.9" # Max height - 0.1 - + esphome: name: ${name} comment: ${device_name} @@ -139,16 +139,16 @@ cover: - logger.log: "Executing up command" - switch.turn_on: switch_up - delay: 10ms - + # Move desk down close_action: - while: condition: sensor.in_range: id: desk_height - above: ${min_height} + above: ${min_height} then: - logger.log: "Executing down command" - switch.turn_on: switch_down - delay: 10ms - optimistic: true \ No newline at end of file + optimistic: true diff --git a/packages/esphome/flexispot_e5b_esp32.yaml b/archive/esphome/flexispot_e5b_esp32.yaml similarity index 99% rename from packages/esphome/flexispot_e5b_esp32.yaml rename to archive/esphome/flexispot_e5b_esp32.yaml index 8a63c9f..bfde352 100644 --- a/packages/esphome/flexispot_e5b_esp32.yaml +++ b/archive/esphome/flexispot_e5b_esp32.yaml @@ -4,7 +4,7 @@ substitutions: min_height: "71.0" # Min height + 0.1 #min_height: "62.1" # Min height + 0.1 (original one) max_height: "125.1" # Max height - 0.1 - + esphome: name: ${name} comment: ${device_name} @@ -64,7 +64,7 @@ script: - delay: 480ms else: - script.execute: screen_timer - + uart: - id: desk_uart baud_rate: 9600 @@ -291,9 +291,9 @@ cover: # current_operation: OPENING - switch.turn_on: switch_up - delay: 108ms - - - + + + # Move desk down close_action: - script.execute: script_start_command @@ -302,7 +302,7 @@ cover: condition: sensor.in_range: id: desk_height - above: ${min_height} + above: ${min_height} then: - logger.log: "Executing down command" # - cover.template.publish: @@ -311,5 +311,5 @@ cover: - switch.turn_on: switch_down - delay: 108ms - + optimistic: true diff --git a/packages/esphome/flexispot_e6.yaml b/archive/esphome/flexispot_e6.yaml similarity index 98% rename from packages/esphome/flexispot_e6.yaml rename to archive/esphome/flexispot_e6.yaml index 5a3eb90..9ce0ba2 100644 --- a/packages/esphome/flexispot_e6.yaml +++ b/archive/esphome/flexispot_e6.yaml @@ -3,7 +3,7 @@ substitutions: name: flexispot_e6 min_height: "60.1" # Min height + 0.1 max_height: "122.9" # Max height - 0.1 - + esphome: name: ${name} comment: ${device_name} @@ -139,16 +139,16 @@ cover: - logger.log: "Executing up command" - switch.turn_on: switch_up - delay: 10ms - + # Move desk down close_action: - while: condition: sensor.in_range: id: desk_height - above: ${min_height} + above: ${min_height} then: - logger.log: "Executing down command" - switch.turn_on: switch_down - delay: 10ms - optimistic: true \ No newline at end of file + optimistic: true diff --git a/packages/esphome/flexispot_ek5.yaml b/archive/esphome/flexispot_ek5.yaml similarity index 98% rename from packages/esphome/flexispot_ek5.yaml rename to archive/esphome/flexispot_ek5.yaml index 7727caa..20aa91e 100644 --- a/packages/esphome/flexispot_ek5.yaml +++ b/archive/esphome/flexispot_ek5.yaml @@ -147,16 +147,16 @@ cover: - logger.log: "Executing up command" - switch.turn_on: switch_up - delay: 10ms - + # Move desk down close_action: - while: condition: sensor.in_range: id: desk_height - above: ${min_height} + above: ${min_height} then: - logger.log: "Executing down command" - switch.turn_on: switch_down - delay: 10ms - optimistic: true \ No newline at end of file + optimistic: true diff --git a/packages/esphome/secrets.yaml b/archive/esphome/secrets.yaml similarity index 79% rename from packages/esphome/secrets.yaml rename to archive/esphome/secrets.yaml index 8d20754..d17dc1a 100644 --- a/packages/esphome/secrets.yaml +++ b/archive/esphome/secrets.yaml @@ -1,4 +1,4 @@ wifi_ssid: "Abraham Linksys" wifi_password: "PASSWORD42" ap_fallback_password: "VGhp4HGdsorM" -ha_api_password: "1234" \ No newline at end of file +ha_api_password: "1234" diff --git a/packages/raspberry-pi/README.md b/archive/raspberry-pi/README.md similarity index 100% rename from packages/raspberry-pi/README.md rename to archive/raspberry-pi/README.md diff --git a/packages/raspberry-pi/flexispot.py b/archive/raspberry-pi/flexispot.py similarity index 100% rename from packages/raspberry-pi/flexispot.py rename to archive/raspberry-pi/flexispot.py diff --git a/components/loctekmotion_desk_height/__init__.py b/components/loctekmotion_desk_height/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/components/loctekmotion_desk_height/desk_height_sensor.cpp b/components/loctekmotion_desk_height/desk_height_sensor.cpp new file mode 100644 index 0000000..2d1caf5 --- /dev/null +++ b/components/loctekmotion_desk_height/desk_height_sensor.cpp @@ -0,0 +1,147 @@ +#include "desk_height_sensor.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include + +namespace esphome { +namespace loctekmotion_desk_height { + +static const char *const TAG = "loctekmotion_desk_height.sensor"; + +// ========== UTILITY METHODS ========== +int hex_to_int(uint8_t s) { + std::bitset<8> b(s); + + if (b[0] && b[1] && b[2] && b[3] && b[4] && b[5] && !b[6]) { + return 0; + } + if (not b[0] && b[1] && b[2] && !b[3] && !b[4] && !b[5] && !b[6]) { + return 1; + } + if (b[0] && b[1] && !b[2] && b[3] && b[4] && !b[5] && b[6]) { + return 2; + } + if (b[0] && b[1] && b[2] && b[3] && !b[4] && !b[5] && b[6]) { + return 3; + } + if (not b[0] && b[1] && b[2] && !b[3] && !b[4] && b[5] && b[6]) { + return 4; + } + if (b[0] && !b[1] && b[2] && b[3] && !b[4] && b[5] && b[6]) { + return 5; + } + if (b[0] && !b[1] && b[2] && b[3] && b[4] && b[5] && b[6]) { + return 6; + } + if (b[0] && b[1] && b[2] && !b[3] && !b[4] && !b[5] && !b[6]) { + return 7; + } + if (b[0] && b[1] && b[2] && b[3] && b[4] && b[5] && b[6]) { + return 8; + } + if (b[0] && b[1] && b[2] && b[3] && !b[4] && b[5] && b[6]) { + return 9; + } + if (!b[0] && !b[1] && !b[2] && !b[3] && !b[4] && !b[5] && b[6]) { + return 10; + } + return 0; +} + +bool is_decimal(uint8_t b) { return (b & 0x80) == 0x80; } + +// ========== INTERNAL METHODS ========== +void DeskHeightSensor::loop() { + uint8_t incomingByte; + while (this->available() > 0) { + if (this->read_byte(&incomingByte)) { + // ESP_LOGD("DEBUG", "Incoming byte is: %08x", incomingByte); + + // First byte, start of a packet + if (incomingByte == 0x9b) { + // Reset message length + this->msg_len = 0; + this->valid = false; + } + + // Second byte defines the message length + if (this->history[0] == 0x9b) { + this->msg_len = incomingByte; + } + + // Third byte is message type + if (this->history[1] == 0x9b) { + this->msg_type = incomingByte; + } + + // Fourth byte is first height digit, if msg type 0x12 & msg len 7 + if (this->history[2] == 0x9b) { + + if (this->msg_type == 0x12 && + (this->msg_len == 7 || this->msg_len == 10)) { + // Empty height + if (incomingByte == 0) { + // ESP_LOGD("DEBUG", "Height 1 is EMPTY -> 0x%02x", incomingByte); + // deskSerial.write(command_wakeup, sizeof(command_wakeup)); + } else if (hex_to_int(incomingByte) == 0) { + // ESP_LOGD("DEBUG", "Invalid height 1 -> 0x%02x", incomingByte); + // deskSerial.write(command_wakeup, sizeof(command_wakeup)); + } else { + this->valid = true; + // ESP_LOGD("DEBUG", "Height 1 is: 0x%02x", incomingByte); + } + } + } + + // Fifth byte is second height digit + if (this->history[3] == 0x9b) { + if (this->valid == true) { + // ESP_LOGD("DEBUG", "Height 2 is: 0x%02x", incomingByte); + } + } + + // Sixth byte is third height digit + if (this->history[4] == 0x9b) { + if (this->valid == true) { + int height1 = hex_to_int(this->history[1]) * 100; + int height2 = hex_to_int(this->history[0]) * 10; + int height3 = hex_to_int(incomingByte); + if (height2 == 100) // check if 'number' is a hyphen, return value 10 + // multiplied by 10 + { + } else { + float finalHeight = height1 + height2 + height3; + if (is_decimal(this->history[0])) { + finalHeight = finalHeight / 10; + } + this->value = finalHeight; + // ESP_LOGD("DeskHeightSensor", "Current height is: %f", + // finalHeight); + } + } + } + + // Save byte buffer to history arrary + this->history[4] = this->history[3]; + this->history[3] = this->history[2]; + this->history[2] = this->history[1]; + this->history[1] = this->history[0]; + this->history[0] = incomingByte; + + // End byte + if (incomingByte == 0x9d) { + if (this->value && this->value != this->lastPublished) { + this->publish_state(this->value); + this->lastPublished = this->value; + } + } + } + } +} + +void DeskHeightSensor::dump_config() { + LOG_SENSOR("", "LoctekMotion Desk Height Sensor", this); +} + +} // namespace loctekmotion_desk_height +} // namespace esphome diff --git a/components/loctekmotion_desk_height/desk_height_sensor.h b/components/loctekmotion_desk_height/desk_height_sensor.h new file mode 100644 index 0000000..90bc20f --- /dev/null +++ b/components/loctekmotion_desk_height/desk_height_sensor.h @@ -0,0 +1,33 @@ +#pragma once + +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace loctekmotion_desk_height { + +class DeskHeightSensor : public sensor::Sensor, + public Component, + public uart::UARTDevice { +public: + float get_setup_priority() const override { + return esphome::setup_priority::DATA; + } + + // ========== INTERNAL METHODS ========== + void loop() override; + void dump_config() override; + +protected: + float value = 0; + float lastPublished = -1; + uint8_t history[5]; + + uint8_t msg_len = 0; + uint8_t msg_type; + bool valid = false; +}; + +} // namespace loctekmotion_desk_height +} // namespace esphome diff --git a/components/loctekmotion_desk_height/sensor.py b/components/loctekmotion_desk_height/sensor.py new file mode 100644 index 0000000..f7695f0 --- /dev/null +++ b/components/loctekmotion_desk_height/sensor.py @@ -0,0 +1,41 @@ +import esphome.codegen as cg +from esphome.components import sensor, uart +from esphome.const import ( + STATE_CLASS_MEASUREMENT, + UNIT_CENTIMETER, + ICON_ARROW_EXPAND_VERTICAL, + DEVICE_CLASS_DISTANCE, +) + +CODEOWNERS = ["@iMicknl"] +DEPENDENCIES = ["uart"] + +loctekmotion_ns = cg.esphome_ns.namespace("loctekmotion_desk_height") +DeskHeightSensor = loctekmotion_ns.class_( + "DeskHeightSensor", sensor.Sensor, cg.Component, uart.UARTDevice +) + +CONFIG_SCHEMA = sensor.sensor_schema( + DeskHeightSensor, + unit_of_measurement=UNIT_CENTIMETER, + icon=ICON_ARROW_EXPAND_VERTICAL, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_DISTANCE, +).extend(uart.UART_DEVICE_SCHEMA) + +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "loctekmotion_desk_height", + baud_rate=9600, + require_tx=False, + require_rx=True, + data_bits=8, + parity=None, + stop_bits=1, +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) diff --git a/images/RJ45-Pinout-T568B.jpg b/images/RJ45-Pinout-T568B.jpg new file mode 100644 index 0000000..bf606ec Binary files /dev/null and b/images/RJ45-Pinout-T568B.jpg differ diff --git a/packages/arduino/flexispot_ek5.ino b/packages/arduino/flexispot_ek5.ino deleted file mode 100644 index 6fca971..0000000 --- a/packages/arduino/flexispot_ek5.ino +++ /dev/null @@ -1,65 +0,0 @@ -#include - -#define displayPin20 4 // D2 GPIO4 -#define rxPin 12 // D5 GPIO12 -#define txPin 14 // D6 GPIO14 - -SoftwareSerial sSerial(rxPin, txPin); // RX, TX -byte history[2]; - -// Supported Commands -unsigned long wakeup[] = { 0x9b, 0x06, 0x02, 0x00, 0x00, 0x6c, 0xa1, 0x9d }; -unsigned long command_up[] = { 0x9b, 0x06, 0x02, 0x01, 0x00, 0xfc, 0xa0, 0x9d }; -unsigned long command_down[] = { 0x9b, 0x06, 0x02, 0x02, 0x00, 0x0c, 0xa0, 0x9d }; -unsigned long command_m[] = {0x9b, 0x06, 0x02, 0x20, 0x00, 0xac, 0xb8, 0x9d }; -unsigned long command_preset_1[] = { 0x9b, 0x06, 0x02, 0x04, 0x00, 0xac, 0xa3, 0x9d }; -unsigned long command_preset_2[] = { 0x9b, 0x06, 0x02, 0x08, 0x00, 0xac, 0xa6, 0x9d }; -unsigned long command_preset_3[] = { 0x9b, 0x06, 0x02, 0x10, 0x00, 0xac, 0xac, 0x9d }; -unsigned long command_preset_4[] = { 0x9b, 0x06, 0x02, 0x00, 0x01, 0xac, 0x60, 0x9d }; - - -void setup() { - Serial.begin(115200); // Debug serial - sSerial.begin(9600); // Flexispot EK5 - - // Turn desk in operating mode by setting controller pin20 to HIGH - // This will allow us to send commands and to receive the current height - Serial.println("Turn Operation Mode on"); - pinMode(displayPin20, OUTPUT); - digitalWrite(displayPin20, LOW); - - // Run command - Serial.println("Send command"); - sSerial.write(command_down, sizeof(command_down)); - - // Wake up to get height - sSerial.write(wakeup, sizeof(wakeup)); -} - -void loop() { - while (sSerial.available()) - { - unsigned long in = sSerial.read(); - - // Start of packet - if(in == 0x9b) { - Serial.println(); - } - - // Second byte defines the message length - if(history[0] == 0x9b) { - int msg_len = (int)in; - Serial.print("(LENGTH:"); - Serial.print(in); - Serial.print(")"); - } - - // Get package length (second byte) - history[1] = history[0]; - history[0] = in; - - // Print hex for debug - Serial.print(in, HEX); - Serial.print(" "); - } -} diff --git a/packages/esphome/desk_keypad.h b/packages/esphome/desk_keypad.h deleted file mode 100644 index a9da991..0000000 --- a/packages/esphome/desk_keypad.h +++ /dev/null @@ -1,81 +0,0 @@ -#include "esphome.h" -#include "esphome.h" -#include - -class DeskKeypad : public Component, public UARTDevice, public Sensor -{ - -public: - DeskKeypad(UARTComponent *parent) : UARTDevice(parent) {} - - enum Command { Up = 1, Down = 2, Preset1 = 3 , Preset2 = 4 , Preset3 = 5, M = 6, Alarm = 7, Empty = 8}; - Command mReturnCommand; - Command lastPublished = Command::Empty; - unsigned long history[3]; - - int msg_len = 0; - unsigned long msg_type; - bool valid = false; - - float get_setup_priority() const override { return esphome::setup_priority::DATA; } - - void setup() override - { - // nothing to do here - } - - void loop() override - { - while (available() > 0) - { - byte incomingByte = read(); - //ESP_LOGD("DEBUG", "Incoming byte is: %08x", incomingByte); - // First byte, start of a packet - if (incomingByte == 0x9b) - { - // Reset message length - msg_len = 0; - valid = false; - } - - // Second byte defines the message length - if (history[0] == 0x9b) - { - msg_len = (int)incomingByte; - } - - // Third byte is message type - if (history[1] == 0x9b) - { - msg_type = incomingByte; - } - - // Fourth byte is first height digit, if msg type 0x12 & msg len 7 - if (history[2] == 0x9b) - { - switch(incomingByte) - { - case 0x00: mReturnCommand = Command::Empty; break; - case 0x01: mReturnCommand = Command::Up; break; - case 0x02: mReturnCommand = Command::Down; break; - case 0x04: mReturnCommand = Command::Preset1; break; - case 0x08: mReturnCommand = Command::Preset2; break; - case 0x10: mReturnCommand = Command::Preset3; break; - case 0x20: mReturnCommand = Command::M; break; - case 0x40: mReturnCommand = Command::Alarm; break; - } - } - - if (incomingByte == 0x9d && msg_type == 0x02 && msg_len == 6 && mReturnCommand && mReturnCommand != lastPublished) - { - publish_state(mReturnCommand); - lastPublished = mReturnCommand; - } - - // Save byte buffer to history arrary - history[2] = history[1]; - history[1] = history[0]; - history[0] = incomingByte; - } - } -}; \ No newline at end of file diff --git a/packages/office-desk-esp32.yaml b/packages/office-desk-esp32.yaml new file mode 100644 index 0000000..afda2a1 --- /dev/null +++ b/packages/office-desk-esp32.yaml @@ -0,0 +1,273 @@ +substitutions: + device_name: Flexispot EK5 + name: office-desk-flexispot-ek5 + min_height: "73.5" # cm + max_height: "123" # cm + ssid: "your_wifi_ssid" + wifi_password: "your_wifi_password" + ap_fallback_password: "MnANX95MWad1" + tx_pin: GPIO17 # TXD 2 + rx_pin: GPIO16 # RXD 2 + screen_pin: GPIO23 + encryption_key: "iOZqtvw31Yy6sasRl5h2DElG2VDlqW2WjJEKObVN8bg=" + +external_components: + source: github://iMicknl/LoctekMotion_IoT@v2 + components: [ loctekmotion_desk_height ] + +esp32: + board: esp32dev + framework: + type: arduino + +esphome: + name: ${name} + friendly_name: ${device_name} + comment: Used to control your ${device_name} standing desk via Home Assistant. + + # Wake Desk by sending the "M" command + # This will pull the current height after boot + on_boot: + priority: -10 + then: + - button.press: button_m + + +# Enable logging +logger: + +# Enable Home Assistant API +api: + encryption: + key: ${encryption_key} + +ota: + +wifi: + ssid: ${ssid} + password: ${wifi_password} + + # Enable fallback hotspot (captive portal) in case wifi connection fails + ap: + ssid: ${device_name} Fallback Hotspot + password: ${ap_fallback_password} + +captive_portal: + +uart: + - id: desk_uart + baud_rate: 9600 + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + +sensor: + - platform: wifi_signal + name: "WiFi Signal" + update_interval: 60s + + - platform: uptime + name: Uptime + + - platform: loctekmotion_desk_height + id: "desk_height" + name: Desk Height + on_value_range: + - below: ${min_height} + then: + - switch.turn_off: switch_down + - above: ${max_height} + then: + - switch.turn_off: switch_up + on_value: + then: + - cover.template.publish: + id: desk_cover + position: !lambda |- + // The sensor outputs values from min_height (cm) to max_height (cm) + // We need to translate this to 0 - 1 scale. + float position = (float(x) - float(${min_height})) / (float(${max_height}) - float(${min_height})); + return position; + - component.update: set_desk_height + +switch: + - platform: gpio + name: "Virtual Screen" # PIN20 + pin: + number: ${screen_pin} + mode: OUTPUT + restore_mode: ALWAYS_ON + entity_category: "config" + internal: true + + - platform: uart + name: "Up" + id: switch_up + icon: mdi:arrow-up-bold + data: [0x9b, 0x06, 0x02, 0x01, 0x00, 0xfc, 0xa0, 0x9d] + uart_id: desk_uart + send_every: 108ms + internal: true + + - platform: uart + name: "Down" + id: switch_down + icon: mdi:arrow-down-bold + data: [0x9b, 0x06, 0x02, 0x02, 0x00, 0x0c, 0xa0, 0x9d] + uart_id: desk_uart + send_every: 108ms + internal: true + + - platform: uart + name: "Alarm off" + id: switch_alarm + icon: mdi:alarm + data: [0x9b, 0x06, 0x02, 0x40, 0x00, 0xAC, 0x90, 0x9d] + uart_id: desk_uart + send_every: 108ms + on_turn_on: + - delay: 3000ms + - switch.turn_off: switch_alarm + entity_category: "config" + + - platform: uart + name: "Child Lock" + id: switch_child_lock + icon: mdi:account-lock + data: [0x9b, 0x06, 0x02, 0x20, 0x00, 0xac, 0xb8, 0x9d] + uart_id: desk_uart + send_every: 108ms + on_turn_on: + - delay: 5000ms + - switch.turn_off: switch_child_lock + entity_category: "config" + +button: + - platform: template + name: "Preset 1" + icon: mdi:numeric-1-box + on_press: + - uart.write: + id: desk_uart + data: [0x9b, 0x06, 0x02, 0x04, 0x00, 0xac, 0xa3, 0x9d] + + - platform: template + name: "Preset 2" + icon: mdi:numeric-2-box + on_press: + - uart.write: + id: desk_uart + data: [0x9b, 0x06, 0x02, 0x08, 0x00, 0xac, 0xa6, 0x9d] + + - platform: template + name: "Sit" # Preset 3 on some control panels + icon: mdi:chair-rolling + on_press: + - uart.write: + id: desk_uart + data: [0x9b, 0x06, 0x02, 0x00, 0x01, 0xac, 0x60, 0x9d] + + - platform: template + name: "Stand" # Preset 4 on some control panels + icon: mdi:human-handsup + on_press: + - uart.write: + id: desk_uart + data: [0x9b, 0x06, 0x02, 0x10, 0x00, 0xac, 0xac, 0x9d] + + - platform: template + name: "Memory" + id: button_m + icon: mdi:alpha-m-box + entity_category: "config" + on_press: + - uart.write: + id: desk_uart + data: [0x9b, 0x06, 0x02, 0x20, 0x00, 0xac, 0xb8, 0x9d] + + - platform: template + name: "Wake Screen" + id: button_wake_screen + icon: mdi:gesture-tap-button + entity_category: "config" + on_press: + - uart.write: + id: desk_uart + data: [0x9b, 0x06, 0x02, 0x00, 0x00, 0x6c, 0xa1, 0x9d] + + - platform: template + name: "Alarm" + id: button_alarm + icon: mdi:alarm + on_press: + - uart.write: + id: desk_uart + data: [0x9b, 0x06, 0x02, 0x40, 0x00, 0xAC, 0x90, 0x9d] + + - platform: restart + name: "Restart" + entity_category: "config" + +cover: + - platform: template + id: "desk_cover" + icon: mdi:desk # or mdi:human-male-height-variant + name: "Desk" + device_class: blind # makes it easier to integrate with Google/Alexa + has_position: true + position_action: + - if: + condition: + - lambda: !lambda |- + return pos > id(desk_cover).position; + then: + - cover.open: desk_cover + - wait_until: + lambda: |- + return id(desk_cover).position >= pos; + - cover.stop: desk_cover + else: + - cover.close: desk_cover + - wait_until: + lambda: |- + return id(desk_cover).position <= pos; + - cover.stop: desk_cover + stop_action: + - switch.turn_off: switch_up + - switch.turn_off: switch_down + open_action: + - switch.turn_off: switch_down + - switch.turn_on: switch_up + close_action: + - switch.turn_off: switch_up + - switch.turn_on: switch_down + optimistic: false + +number: + - platform: template + name: "Desk Height" + id: set_desk_height + min_value: ${min_height} + max_value: ${max_height} + icon: "mdi:counter" + unit_of_measurement: "cm" + device_class: "distance" + step: 0.1 + lambda: !lambda |- + return id(desk_height).state; + set_action: + - if: + condition: + - lambda: !lambda |- + return x > id(desk_height).state; + then: + - cover.open: desk_cover + - wait_until: + lambda: |- + return id(desk_height).state >= x; + - cover.stop: desk_cover + else: + - cover.close: desk_cover + - wait_until: + lambda: |- + return id(desk_height).state <= x; + - cover.stop: desk_cover diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..11038dd --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +esphome==2024.5.5 +pre-commit diff --git a/tests/office-desk-esp32-passthrough.yaml b/tests/office-desk-esp32-passthrough.yaml new file mode 100644 index 0000000..19cff0f --- /dev/null +++ b/tests/office-desk-esp32-passthrough.yaml @@ -0,0 +1,30 @@ +substitutions: + device_name: Flexispot EK5 + name: office-desk-flexispot-ek5 + min_height: "73.5" # cm + max_height: "123" # cm + ssid: "your_wifi_ssid" + wifi_password: "your_wifi_password" + ap_fallback_password: "your_ap_fallback_password" + tx_pin: GPIO17 # TXD 2 + rx_pin: GPIO16 # RXD 2 + screen_pin: GPIO23 + encryption_key: "iOZqtvw31Yy6sasRl5h2DElG2VDlqW2WjJEKObVN8bg=" + +external_components: + - source: + type: local + path: ../components + +esp32: + board: esp32dev + framework: + type: arduino + +esphome: + name: ${name} + friendly_name: ${device_name} + comment: Used to control your ${device_name} standing desk via Home Assistant. + + +# TODO diff --git a/tests/office-desk-esp32.yaml b/tests/office-desk-esp32.yaml new file mode 100644 index 0000000..55c3b62 --- /dev/null +++ b/tests/office-desk-esp32.yaml @@ -0,0 +1,274 @@ +substitutions: + device_name: Flexispot EK5 + name: office-desk-flexispot-ek5 + min_height: "73.5" # cm + max_height: "123" # cm + ssid: "your_wifi_ssid" + wifi_password: "your_wifi_password" + ap_fallback_password: "your_ap_fallback_password" + tx_pin: GPIO17 # TXD 2 + rx_pin: GPIO16 # RXD 2 + screen_pin: GPIO23 + encryption_key: "iOZqtvw31Yy6sasRl5h2DElG2VDlqW2WjJEKObVN8bg=" + +external_components: + - source: + type: local + path: ../components + +esp32: + board: esp32dev + framework: + type: arduino + +esphome: + name: ${name} + friendly_name: ${device_name} + comment: Used to control your ${device_name} standing desk via Home Assistant. + + # Wake Desk by sending the "M" command + # This will pull the current height after boot + on_boot: + priority: -10 + then: + - button.press: button_m + + +# Enable logging +logger: + +# Enable Home Assistant API +api: + encryption: + key: ${encryption_key} + +ota: + +wifi: + ssid: ${ssid} + password: ${wifi_password} + + # Enable fallback hotspot (captive portal) in case wifi connection fails + ap: + ssid: ${device_name} Fallback Hotspot + password: ${ap_fallback_password} + +captive_portal: + +uart: + - id: desk_uart + baud_rate: 9600 + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + +sensor: + - platform: wifi_signal + name: "WiFi Signal" + update_interval: 60s + + - platform: uptime + name: Uptime + + - platform: loctekmotion_desk_height + id: "desk_height" + name: Desk Height + on_value_range: + - below: ${min_height} + then: + - switch.turn_off: switch_down + - above: ${max_height} + then: + - switch.turn_off: switch_up + on_value: + then: + - cover.template.publish: + id: desk_cover + position: !lambda |- + // The sensor outputs values from min_height (cm) to max_height (cm) + // We need to translate this to 0 - 1 scale. + float position = (float(x) - float(${min_height})) / (float(${max_height}) - float(${min_height})); + return position; + - component.update: set_desk_height + +switch: + - platform: gpio + name: "Virtual Screen" # PIN20 + pin: + number: ${screen_pin} + mode: OUTPUT + restore_mode: ALWAYS_ON + entity_category: "config" + internal: true + + - platform: uart + name: "Up" + id: switch_up + icon: mdi:arrow-up-bold + data: [0x9b, 0x06, 0x02, 0x01, 0x00, 0xfc, 0xa0, 0x9d] + uart_id: desk_uart + send_every: 108ms + internal: true + + - platform: uart + name: "Down" + id: switch_down + icon: mdi:arrow-down-bold + data: [0x9b, 0x06, 0x02, 0x02, 0x00, 0x0c, 0xa0, 0x9d] + uart_id: desk_uart + send_every: 108ms + internal: true + + - platform: uart + name: "Alarm off" + id: switch_alarm + icon: mdi:alarm + data: [0x9b, 0x06, 0x02, 0x40, 0x00, 0xAC, 0x90, 0x9d] + uart_id: desk_uart + send_every: 108ms + on_turn_on: + - delay: 3000ms + - switch.turn_off: switch_alarm + entity_category: "config" + + - platform: uart + name: "Child Lock" + id: switch_child_lock + icon: mdi:account-lock + data: [0x9b, 0x06, 0x02, 0x20, 0x00, 0xac, 0xb8, 0x9d] + uart_id: desk_uart + send_every: 108ms + on_turn_on: + - delay: 5000ms + - switch.turn_off: switch_child_lock + entity_category: "config" + +button: + - platform: template + name: "Preset 1" + icon: mdi:numeric-1-box + on_press: + - uart.write: + id: desk_uart + data: [0x9b, 0x06, 0x02, 0x04, 0x00, 0xac, 0xa3, 0x9d] + + - platform: template + name: "Preset 2" + icon: mdi:numeric-2-box + on_press: + - uart.write: + id: desk_uart + data: [0x9b, 0x06, 0x02, 0x08, 0x00, 0xac, 0xa6, 0x9d] + + - platform: template + name: "Sit" # Preset 3 on some control panels + icon: mdi:chair-rolling + on_press: + - uart.write: + id: desk_uart + data: [0x9b, 0x06, 0x02, 0x00, 0x01, 0xac, 0x60, 0x9d] + + - platform: template + name: "Stand" # Preset 4 on some control panels + icon: mdi:human-handsup + on_press: + - uart.write: + id: desk_uart + data: [0x9b, 0x06, 0x02, 0x10, 0x00, 0xac, 0xac, 0x9d] + + - platform: template + name: "Memory" + id: button_m + icon: mdi:alpha-m-box + entity_category: "config" + on_press: + - uart.write: + id: desk_uart + data: [0x9b, 0x06, 0x02, 0x20, 0x00, 0xac, 0xb8, 0x9d] + + - platform: template + name: "Wake Screen" + id: button_wake_screen + icon: mdi:gesture-tap-button + entity_category: "config" + on_press: + - uart.write: + id: desk_uart + data: [0x9b, 0x06, 0x02, 0x00, 0x00, 0x6c, 0xa1, 0x9d] + + - platform: template + name: "Alarm" + id: button_alarm + icon: mdi:alarm + on_press: + - uart.write: + id: desk_uart + data: [0x9b, 0x06, 0x02, 0x40, 0x00, 0xAC, 0x90, 0x9d] + + - platform: restart + name: "Restart" + entity_category: "config" + +cover: + - platform: template + id: "desk_cover" + icon: mdi:desk # or mdi:human-male-height-variant + name: "Desk" + device_class: blind # makes it easier to integrate with Google/Alexa + has_position: true + position_action: + - if: + condition: + - lambda: !lambda |- + return pos > id(desk_cover).position; + then: + - cover.open: desk_cover + - wait_until: + lambda: |- + return id(desk_cover).position >= pos; + - cover.stop: desk_cover + else: + - cover.close: desk_cover + - wait_until: + lambda: |- + return id(desk_cover).position <= pos; + - cover.stop: desk_cover + stop_action: + - switch.turn_off: switch_up + - switch.turn_off: switch_down + open_action: + - switch.turn_off: switch_down + - switch.turn_on: switch_up + close_action: + - switch.turn_off: switch_up + - switch.turn_on: switch_down + optimistic: false + +number: + - platform: template + name: "Desk Height" + id: set_desk_height + min_value: ${min_height} + max_value: ${max_height} + icon: "mdi:counter" + unit_of_measurement: "cm" + device_class: "distance" + step: 0.1 + lambda: !lambda |- + return id(desk_height).state; + set_action: + - if: + condition: + - lambda: !lambda |- + return x > id(desk_height).state; + then: + - cover.open: desk_cover + - wait_until: + lambda: |- + return id(desk_height).state >= x; + - cover.stop: desk_cover + else: + - cover.close: desk_cover + - wait_until: + lambda: |- + return id(desk_height).state <= x; + - cover.stop: desk_cover diff --git a/tests/office-desk-esp8266-passthrough.yaml b/tests/office-desk-esp8266-passthrough.yaml new file mode 100644 index 0000000..db87b28 --- /dev/null +++ b/tests/office-desk-esp8266-passthrough.yaml @@ -0,0 +1,30 @@ +substitutions: + device_name: Flexispot EK5 + name: office-desk-flexispot-ek5 + min_height: "73.5" # cm + max_height: "123" # cm + ssid: "your_wifi_ssid" + wifi_password: "your_wifi_password" + ap_fallback_password: "your_ap_fallback_password" + tx_pin: D5 # =GPIO14 + rx_pin: D6 # =GPIO12 + screen_pin: D2 # =GPIO4 + encryption_key: "iOZqtvw31Yy6sasRl5h2DElG2VDlqW2WjJEKObVN8bg=" + +external_components: + - source: + type: local + path: ../components + +esp8266: + board: d1_mini + framework: + version: recommended + +esphome: + name: ${name} + friendly_name: ${device_name} + comment: Used to control your ${device_name} standing desk via Home Assistant. + + +# TODO diff --git a/tests/office-desk-esp8266.yaml b/tests/office-desk-esp8266.yaml new file mode 100644 index 0000000..361babd --- /dev/null +++ b/tests/office-desk-esp8266.yaml @@ -0,0 +1,274 @@ +substitutions: + device_name: Flexispot EK5 + name: office-desk-flexispot-ek5 + min_height: "73.5" # cm + max_height: "123" # cm + ssid: "your_wifi_ssid" + wifi_password: "your_wifi_password" + ap_fallback_password: "your_ap_fallback_password" + tx_pin: D5 # =GPIO14 + rx_pin: D6 # =GPIO12 + screen_pin: D2 # =GPIO4 + encryption_key: "iOZqtvw31Yy6sasRl5h2DElG2VDlqW2WjJEKObVN8bg=" + +external_components: + - source: + type: local + path: ../components + +esp8266: + board: d1_mini + framework: + version: recommended + +esphome: + name: ${name} + friendly_name: ${device_name} + comment: Used to control your ${device_name} standing desk via Home Assistant. + + # Wake Desk by sending the "M" command + # This will pull the current height after boot + on_boot: + priority: -10 + then: + - button.press: button_m + + +# Enable logging +logger: + +# Enable Home Assistant API +api: + encryption: + key: ${encryption_key} + +ota: + +wifi: + ssid: ${ssid} + password: ${wifi_password} + + # Enable fallback hotspot (captive portal) in case wifi connection fails + ap: + ssid: ${device_name} Fallback Hotspot + password: ${ap_fallback_password} + +captive_portal: + +uart: + - id: desk_uart + baud_rate: 9600 + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + +sensor: + - platform: wifi_signal + name: "WiFi Signal" + update_interval: 60s + + - platform: uptime + name: Uptime + + - platform: loctekmotion_desk_height + id: "desk_height" + name: Desk Height + on_value_range: + - below: ${min_height} + then: + - switch.turn_off: switch_down + - above: ${max_height} + then: + - switch.turn_off: switch_up + on_value: + then: + - cover.template.publish: + id: desk_cover + position: !lambda |- + // The sensor outputs values from min_height (cm) to max_height (cm) + // We need to translate this to 0 - 1 scale. + float position = (float(x) - float(${min_height})) / (float(${max_height}) - float(${min_height})); + return position; + - component.update: set_desk_height + +switch: + - platform: gpio + name: "Virtual Screen" # PIN20 + pin: + number: ${screen_pin} + mode: OUTPUT + restore_mode: ALWAYS_ON + entity_category: "config" + internal: true + + - platform: uart + name: "Up" + id: switch_up + icon: mdi:arrow-up-bold + data: [0x9b, 0x06, 0x02, 0x01, 0x00, 0xfc, 0xa0, 0x9d] + uart_id: desk_uart + send_every: 108ms + internal: true + + - platform: uart + name: "Down" + id: switch_down + icon: mdi:arrow-down-bold + data: [0x9b, 0x06, 0x02, 0x02, 0x00, 0x0c, 0xa0, 0x9d] + uart_id: desk_uart + send_every: 108ms + internal: true + + - platform: uart + name: "Alarm off" + id: switch_alarm + icon: mdi:alarm + data: [0x9b, 0x06, 0x02, 0x40, 0x00, 0xAC, 0x90, 0x9d] + uart_id: desk_uart + send_every: 108ms + on_turn_on: + - delay: 3000ms + - switch.turn_off: switch_alarm + entity_category: "config" + + - platform: uart + name: "Child Lock" + id: switch_child_lock + icon: mdi:account-lock + data: [0x9b, 0x06, 0x02, 0x20, 0x00, 0xac, 0xb8, 0x9d] + uart_id: desk_uart + send_every: 108ms + on_turn_on: + - delay: 5000ms + - switch.turn_off: switch_child_lock + entity_category: "config" + +button: + - platform: template + name: "Preset 1" + icon: mdi:numeric-1-box + on_press: + - uart.write: + id: desk_uart + data: [0x9b, 0x06, 0x02, 0x04, 0x00, 0xac, 0xa3, 0x9d] + + - platform: template + name: "Preset 2" + icon: mdi:numeric-2-box + on_press: + - uart.write: + id: desk_uart + data: [0x9b, 0x06, 0x02, 0x08, 0x00, 0xac, 0xa6, 0x9d] + + - platform: template + name: "Sit" # Preset 3 on some control panels + icon: mdi:chair-rolling + on_press: + - uart.write: + id: desk_uart + data: [0x9b, 0x06, 0x02, 0x00, 0x01, 0xac, 0x60, 0x9d] + + - platform: template + name: "Stand" # Preset 4 on some control panels + icon: mdi:human-handsup + on_press: + - uart.write: + id: desk_uart + data: [0x9b, 0x06, 0x02, 0x10, 0x00, 0xac, 0xac, 0x9d] + + - platform: template + name: "Memory" + id: button_m + icon: mdi:alpha-m-box + entity_category: "config" + on_press: + - uart.write: + id: desk_uart + data: [0x9b, 0x06, 0x02, 0x20, 0x00, 0xac, 0xb8, 0x9d] + + - platform: template + name: "Wake Screen" + id: button_wake_screen + icon: mdi:gesture-tap-button + entity_category: "config" + on_press: + - uart.write: + id: desk_uart + data: [0x9b, 0x06, 0x02, 0x00, 0x00, 0x6c, 0xa1, 0x9d] + + - platform: template + name: "Alarm" + id: button_alarm + icon: mdi:alarm + on_press: + - uart.write: + id: desk_uart + data: [0x9b, 0x06, 0x02, 0x40, 0x00, 0xAC, 0x90, 0x9d] + + - platform: restart + name: "Restart" + entity_category: "config" + +cover: + - platform: template + id: "desk_cover" + icon: mdi:desk # or mdi:human-male-height-variant + name: "Desk" + device_class: blind # makes it easier to integrate with Google/Alexa + has_position: true + position_action: + - if: + condition: + - lambda: !lambda |- + return pos > id(desk_cover).position; + then: + - cover.open: desk_cover + - wait_until: + lambda: |- + return id(desk_cover).position >= pos; + - cover.stop: desk_cover + else: + - cover.close: desk_cover + - wait_until: + lambda: |- + return id(desk_cover).position <= pos; + - cover.stop: desk_cover + stop_action: + - switch.turn_off: switch_up + - switch.turn_off: switch_down + open_action: + - switch.turn_off: switch_down + - switch.turn_on: switch_up + close_action: + - switch.turn_off: switch_up + - switch.turn_on: switch_down + optimistic: false + +number: + - platform: template + name: "Desk Height" + id: set_desk_height + min_value: ${min_height} + max_value: ${max_height} + icon: "mdi:counter" + unit_of_measurement: "cm" + device_class: "distance" + step: 0.1 + lambda: !lambda |- + return id(desk_height).state; + set_action: + - if: + condition: + - lambda: !lambda |- + return x > id(desk_height).state; + then: + - cover.open: desk_cover + - wait_until: + lambda: |- + return id(desk_height).state >= x; + - cover.stop: desk_cover + else: + - cover.close: desk_cover + - wait_until: + lambda: |- + return id(desk_height).state <= x; + - cover.stop: desk_cover