Repository: j5lien/esphome-idasen-desk-controller Branch: main Commit: 375485796a70 Files: 13 Total size: 28.8 KB Directory structure: gitextract_29xte4fo/ ├── .clang-format ├── .editorconfig ├── .github/ │ └── workflows/ │ ├── ci.yaml │ └── matchers/ │ ├── gcc.json │ └── python.json ├── .gitignore ├── LICENSE ├── README.md ├── components/ │ └── idasen_desk_controller/ │ ├── __init__.py │ ├── cover.py │ ├── idasen_desk_controller.cpp │ └── idasen_desk_controller.h └── test.yaml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .clang-format ================================================ Language: Cpp AccessModifierOffset: -1 AlignAfterOpenBracket: Align AlignConsecutiveAssignments: false AlignConsecutiveDeclarations: false AlignEscapedNewlines: DontAlign AlignOperands: true AlignTrailingComments: true AllowAllParametersOfDeclarationOnNextLine: true AllowShortBlocksOnASingleLine: false AllowShortCaseLabelsOnASingleLine: false AllowShortFunctionsOnASingleLine: All AllowShortIfStatementsOnASingleLine: false AllowShortLoopsOnASingleLine: false AlwaysBreakAfterReturnType: None AlwaysBreakBeforeMultilineStrings: false AlwaysBreakTemplateDeclarations: MultiLine BinPackArguments: true BinPackParameters: true BraceWrapping: AfterClass: false AfterControlStatement: false AfterEnum: false AfterFunction: false AfterNamespace: false AfterObjCDeclaration: false AfterStruct: false AfterUnion: false AfterExternBlock: false BeforeCatch: false BeforeElse: false IndentBraces: false SplitEmptyFunction: true SplitEmptyRecord: true SplitEmptyNamespace: true BreakBeforeBinaryOperators: None BreakBeforeBraces: Attach BreakBeforeInheritanceComma: false BreakInheritanceList: BeforeColon BreakBeforeTernaryOperators: true BreakConstructorInitializersBeforeComma: false BreakConstructorInitializers: BeforeColon BreakAfterJavaFieldAnnotations: false BreakStringLiterals: true ColumnLimit: 120 CommentPragmas: "^ IWYU pragma:" CompactNamespaces: false ConstructorInitializerAllOnOneLineOrOnePerLine: true ConstructorInitializerIndentWidth: 4 ContinuationIndentWidth: 4 Cpp11BracedListStyle: true DerivePointerAlignment: true DisableFormat: false ExperimentalAutoDetectBinPacking: false FixNamespaceComments: true ForEachMacros: - foreach - Q_FOREACH - BOOST_FOREACH IncludeBlocks: Preserve IncludeCategories: - Regex: '^' Priority: 2 - Regex: '^<.*\.h>' Priority: 1 - Regex: "^<.*" Priority: 2 - Regex: ".*" Priority: 3 IncludeIsMainRegex: "([-_](test|unittest))?$" IndentCaseLabels: true IndentPPDirectives: None IndentWidth: 2 IndentWrappedFunctionNames: false KeepEmptyLinesAtTheStartOfBlocks: false MacroBlockBegin: "" MacroBlockEnd: "" MaxEmptyLinesToKeep: 1 NamespaceIndentation: None PenaltyBreakAssignment: 2 PenaltyBreakBeforeFirstCallParameter: 1 PenaltyBreakComment: 300 PenaltyBreakFirstLessLess: 120 PenaltyBreakString: 1000 PenaltyBreakTemplateDeclaration: 10 PenaltyExcessCharacter: 1000000 PenaltyReturnTypeOnItsOwnLine: 2000 PointerAlignment: Right RawStringFormats: - Language: Cpp Delimiters: - cc - CC - cpp - Cpp - CPP - "c++" - "C++" CanonicalDelimiter: "" BasedOnStyle: google - Language: TextProto Delimiters: - pb - PB - proto - PROTO EnclosingFunctions: - EqualsProto - EquivToProto - PARSE_PARTIAL_TEXT_PROTO - PARSE_TEST_PROTO - PARSE_TEXT_PROTO - ParseTextOrDie - ParseTextProtoOrDie CanonicalDelimiter: "" BasedOnStyle: google ReflowComments: true SortIncludes: false SortUsingDeclarations: false SpaceAfterCStyleCast: true SpaceAfterTemplateKeyword: false SpaceBeforeAssignmentOperators: true SpaceBeforeCpp11BracedList: false SpaceBeforeCtorInitializerColon: true SpaceBeforeInheritanceColon: true SpaceBeforeParens: ControlStatements SpaceBeforeRangeBasedForLoopColon: true SpaceInEmptyParentheses: false SpacesBeforeTrailingComments: 2 SpacesInAngles: false SpacesInContainerLiterals: false SpacesInCStyleCastParentheses: false SpacesInParentheses: false SpacesInSquareBrackets: false Standard: Auto TabWidth: 2 UseTab: Never ================================================ FILE: .editorconfig ================================================ root = true # general [*] end_of_line = lf insert_final_newline = true charset = utf-8 # python [*.{py}] indent_style = space indent_size = 4 # C++ [*.{cpp,h,tcc}] indent_style = space indent_size = 2 # Web [*.{js,html,css}] indent_style = space indent_size = 2 # YAML [*.{yaml,yml}] indent_style = space indent_size = 2 quote_type = single ================================================ FILE: .github/workflows/ci.yaml ================================================ --- name: ESPHome Idasen Desk Controller CI on: push: branches: - main pull_request: schedule: - cron: 0 12 * * * jobs: esphome-config: runs-on: ubuntu-latest steps: - name: ⤵️ Check out configuration from GitHub uses: actions/checkout@v2 - name: Setup Python 3.7 uses: actions/setup-python@v1 with: python-version: 3.7 - name: Install dependencies run: | python -m pip install --upgrade pip setuptools wheel pip install esphome pip list esphome version - name: 🚀 Run esphome config on test file run: | esphome config test.yaml esphome-compile: runs-on: ubuntu-latest needs: [esphome-config] steps: - name: ⤵️ Check out configuration from GitHub uses: actions/checkout@v2 - name: Cache .esphome uses: actions/cache@v2 with: path: .esphome key: esphome-compile-esphome-${{ hashFiles('*.yaml') }} restore-keys: esphome-compile-esphome- - name: Cache .pioenvs uses: actions/cache@v2 with: path: .pioenvs key: esphome-compile-pioenvs-${{ hashFiles('*.yaml') }} restore-keys: esphome-compile-pioenvs- - name: Set up Python 3.7 uses: actions/setup-python@v1 with: python-version: 3.7 - name: Install dependencies run: | python -m pip install --upgrade pip setuptools wheel pip install esphome pip list esphome version - name: Register problem matchers run: | echo "::add-matcher::.github/workflows/matchers/gcc.json" echo "::add-matcher::.github/workflows/matchers/python.json" - name: 🚀 Run esphome compile on test file run: | esphome compile test.yaml ================================================ FILE: .github/workflows/matchers/gcc.json ================================================ { "problemMatcher": [ { "owner": "gcc", "severity": "error", "pattern": [ { "regexp": "^(.*):(\\d+):(\\d+):\\s+(?:fatal\\s+)?(warning|error):\\s+(.*)$", "file": 1, "line": 2, "column": 3, "severity": 4, "message": 5 } ] } ] } ================================================ FILE: .github/workflows/matchers/python.json ================================================ { "problemMatcher": [ { "owner": "python", "pattern": [ { "regexp": "^\\s*File\\s\\\"(.*)\\\",\\sline\\s(\\d+),\\sin\\s(.*)$", "file": 1, "line": 2 }, { "regexp": "^\\s*raise\\s(.*)\\(\\'(.*)\\'\\)$", "message": 2 } ] } ] } ================================================ FILE: .gitignore ================================================ __pycache__ .python-version ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2021 Julien Gobillot Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ This project is archived as Idasen Desk is now compatbile with Home Assistant and ESPHome Bluetooth Proxy (https://www.home-assistant.io/integrations/idasen_desk/). ------------- This component creates a bluetooth bridge for an [Ikea Idasen](https://www.ikea.com/gb/en/p/idasen-desk-sit-stand-brown-dark-grey-s19280958/) desk that uses a Linak controller with [ESPHome](https://esphome.io) and an [ESP32 device](https://esphome.io/devices/esp32.html). | [Cover integration](https://www.home-assistant.io/integrations/cover/) | [Linak Desk Card](https://github.com/IhorSyerkov/linak-desk-card) | | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | ![Home Assistant Desk Controller](ha-desk-controller.png) | | The desk is controlled using the [cover integration](https://www.home-assistant.io/integrations/cover/) or [Linak Desk Card](https://github.com/IhorSyerkov/linak-desk-card) which is available in [HACS](https://hacs.xyz) in Home assistant. ## Dependencies * This component requires an [ESP32 device](https://esphome.io/devices/esp32.html). * [ESPHome 2021.10.0 or higher](https://github.com/esphome/esphome/releases). ## Installation You can install this component with [ESPHome external components feature](https://esphome.io/components/external_components.html) like this: ``` external_components: - source: github://j5lien/esphome-idasen-desk-controller@v4.0.0 ``` For the first connection you will need to press the pairing button on the desk. ## Configuration ### BLE Client You need first to configure [ESPHome BLE Client](https://esphome.io/components/ble_client.html) (check the documentation for more information): ```yaml esp32_ble_tracker: ble_client: - mac_address: "00:00:00:00:00:00" # Replace with the desk bluetooth mac address id: idasen_desk ``` > On OSX, you can find the mac address of the desk by first connecting to it using a supported app (like [Idasen Controller](https://github.com/DWilliames/idasen-controller-mac) or [Desk Remote Control](https://apps.apple.com/us/app/desk-remote-control/id1509037746)), and then running this command in terminal: `system_profiler SPBluetoothDataType` ### Idasen Desk Controller Then you need to enable this component with the id of the ble_client component: ```yaml idasen_desk_controller: # Reference to the ble client component id # ----------- # Required ble_client_id: idasen_desk # Fallback to use only up and down commands (less precise) # ----------- # Optional only_up_down_command: false ``` ### Cover Now you can add the cover component that will allow you to control your desk: ```yaml cover: - platform: idasen_desk_controller name: "Desk" ``` ### Extra Desk informations Using [ESPHome BLE Client Sensor](https://esphome.io/components/sensor/ble_client.html), you can expose more informations that doesn't require this custom component. This is an example that generates sensors that were available in previous versions: ```yaml esp32_ble_tracker: globals: # To store the Desk Connection Status - id: ble_client_connected type: bool initial_value: 'false' ble_client: - mac_address: "00:00:00:00:00:00" id: idasen_desk on_connect: then: # Update the Desk Connection Status - lambda: |- id(ble_client_connected) = true; - delay: 5s # Update desk height and speed sensors after bluetooth is connected - lambda: |- id(desk_height).update(); id(desk_speed).update(); on_disconnect: then: # Update the Desk Connection Status - lambda: |- id(ble_client_connected) = false; sensor: # Desk Height Sensor - platform: ble_client type: characteristic ble_client_id: idasen_desk id: desk_height name: 'Desk Height' service_uuid: '99fa0020-338a-1024-8a49-009c0215f78a' characteristic_uuid: '99fa0021-338a-1024-8a49-009c0215f78a' icon: 'mdi:arrow-up-down' unit_of_measurement: 'cm' accuracy_decimals: 1 update_interval: never notify: true lambda: |- uint16_t raw_height = ((uint16_t)x[1] << 8) | x[0]; unsigned short height_mm = raw_height / 10; return (float) height_mm / 10; # Desk Speed Sensor - platform: ble_client type: characteristic ble_client_id: idasen_desk id: desk_speed name: 'Desk Speed' service_uuid: '99fa0020-338a-1024-8a49-009c0215f78a' characteristic_uuid: '99fa0021-338a-1024-8a49-009c0215f78a' icon: 'mdi:speedometer' unit_of_measurement: 'cm/min' # I'm not sure this unit is correct accuracy_decimals: 0 update_interval: never notify: true lambda: |- uint16_t raw_speed = ((uint16_t)x[3] << 8) | x[2]; return raw_speed / 100; binary_sensor: # Desk Bluetooth Connection Status - platform: template name: 'Desk Connection' id: desk_connection lambda: 'return id(ble_client_connected);' # Desk Moving Status - platform: template name: 'Desk Moving' id: desk_moving lambda: 'return id(desk_speed).state > 0;' ``` ## Troubleshooting ### ESPHome lower than 1.19.0 Check the version [v1.2.4](https://github.com/j5lien/esphome-idasen-desk-controller/releases/tag/v1.2.4) of this component ### Not moving using cover component If the desk is not moving using the cover component you can try to activate a fallback option `only_up_down_command`. It will only use up and down commands to control the desk height, it is less precise when you specify a target position. ```yaml idasen_desk_controller: ble_client_id: idasen_desk only_up_down_command: true ``` ### Wifi deconnexion If you experience Wifi deconnexion, try to activate the [wifi fast connect](https://esphome.io/components/wifi.html) option. ```yaml wifi: ssid: ... password: ... fast_connect: true ``` You can also try to set the [power save mode](https://esphome.io/components/wifi.html?highlight=wifi#power-save-mode) option to `none`. ```yaml wifi: ssid: ... password: ... fast_connect: true power_save_mode: none ``` ## References * https://github.com/TheRealMazur/LinakDeskEsp32Controller * https://esphome.io/components/ble_client.html * https://esphome.io/components/sensor/ble_client.html ================================================ FILE: components/idasen_desk_controller/__init__.py ================================================ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import cover, ble_client from esphome.const import CONF_ID DEPENDENCIES = ['esp32', 'ble_client'] AUTO_LOAD = ['cover'] MULTI_CONF = True CONF_IDASEN_DESK_CONTROLLER_ID = 'idasen_desk_controller_id' CONF_ONLY_UP_DOWN_COMMAND = 'only_up_down_command' idasen_desk_controller_ns = cg.esphome_ns.namespace('idasen_desk_controller') IdasenDeskControllerComponent = idasen_desk_controller_ns.class_( 'IdasenDeskControllerComponent', cg.Component, cover.Cover, ble_client.BLEClientNode) CONFIG_SCHEMA = cv.Schema({ cv.GenerateID(): cv.declare_id(IdasenDeskControllerComponent), cv.Optional(CONF_ONLY_UP_DOWN_COMMAND, False): cv.boolean, }).extend(ble_client.BLE_CLIENT_SCHEMA) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await ble_client.register_ble_node(var, config) cg.add(var.use_only_up_down_command(config[CONF_ONLY_UP_DOWN_COMMAND])) ================================================ FILE: components/idasen_desk_controller/cover.py ================================================ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import cover from . import IdasenDeskControllerComponent, CONF_IDASEN_DESK_CONTROLLER_ID DEPENDENCIES = ['esp32', 'idasen_desk_controller'] CONFIG_SCHEMA = cover.COVER_SCHEMA.extend(({ cv.GenerateID(CONF_IDASEN_DESK_CONTROLLER_ID): cv.use_id(IdasenDeskControllerComponent), })) def to_code(config): hub = yield cg.get_variable(config[CONF_IDASEN_DESK_CONTROLLER_ID]) yield cover.register_cover(hub, config) ================================================ FILE: components/idasen_desk_controller/idasen_desk_controller.cpp ================================================ #include "idasen_desk_controller.h" #include "esphome/core/log.h" #include "esphome/core/helpers.h" #include namespace esphome { namespace idasen_desk_controller { static const char *TAG = "idasen_desk_controller"; static const float DESK_MAX_HEIGHT = 6500; static float transform_height_to_position(float height) { return height / DESK_MAX_HEIGHT; } static float transform_position_to_height(float position) { return position * DESK_MAX_HEIGHT; } void IdasenDeskControllerComponent::loop() {} void IdasenDeskControllerComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up Idasen Desk Controller..."); this->set_interval("update_desk", 200, [this]() { this->move_desk_(); }); } void IdasenDeskControllerComponent::dump_config() { ESP_LOGCONFIG(TAG, "Idasen Desk Controller:"); ESP_LOGCONFIG(TAG, " MAC address : %s", this->parent()->address_str().c_str()); ESP_LOGCONFIG(TAG, " Notifications : %s", this->notify_disable_ ? "disable" : "enable"); LOG_COVER(" ", "Desk", this); } void IdasenDeskControllerComponent::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { switch (event) { case ESP_GATTC_WRITE_CHAR_EVT: { if (param->write.status != ESP_GATT_OK) { ESP_LOGW(TAG, "Error writing char at handle %d, status=%d", param->write.handle, param->write.status); } break; } case ESP_GATTC_OPEN_EVT: { if (param->open.status == ESP_GATT_OK) { ESP_LOGI(TAG, "[%s] Connected successfully!", this->get_name().c_str()); break; } break; } case ESP_GATTC_DISCONNECT_EVT: { ESP_LOGW(TAG, "[%s] Disconnected!", this->get_name().c_str()); this->status_set_warning(); break; } case ESP_GATTC_SEARCH_CMPL_EVT: { // Look for output handle this->output_handle_ = 0; auto chr_output = this->parent()->get_characteristic(this->output_service_uuid_, this->output_char_uuid_); if (chr_output == nullptr) { this->status_set_warning(); ESP_LOGW(TAG, "No characteristic found at service %s char %s", this->output_service_uuid_.to_string().c_str(), this->output_char_uuid_.to_string().c_str()); break; } this->output_handle_ = chr_output->handle; // Register for notification auto status_notify = esp_ble_gattc_register_for_notify(this->parent()->get_gattc_if(), this->parent()->get_remote_bda(), this->output_handle_); if (status_notify) { ESP_LOGW(TAG, "esp_ble_gattc_register_for_notify failed, status=%d", status_notify); } // Look for input handle this->input_handle_ = 0; auto chr_input = this->parent()->get_characteristic(this->input_service_uuid_, this->input_char_uuid_); if (chr_input == nullptr) { this->status_set_warning(); ESP_LOGW(TAG, "No characteristic found at service %s char %s", this->input_service_uuid_.to_string().c_str(), this->input_char_uuid_.to_string().c_str()); break; } this->input_handle_ = chr_input->handle; // Look for control handle this->control_handle_ = 0; auto chr_control = this->parent()->get_characteristic(this->control_service_uuid_, this->control_char_uuid_); if (chr_control == nullptr) { this->status_set_warning(); ESP_LOGW(TAG, "No characteristic found at service %s char %s", this->control_service_uuid_.to_string().c_str(), this->control_char_uuid_.to_string().c_str()); break; } this->control_handle_ = chr_control->handle; this->set_timeout("desk_init", 5000, [this]() { this->read_value_(this->output_handle_); }); break; } case ESP_GATTC_READ_CHAR_EVT: { if (param->read.conn_id != this->parent()->get_conn_id()) break; if (param->read.status != ESP_GATT_OK) { ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status); break; } if (param->read.handle == this->output_handle_) { this->status_clear_warning(); this->publish_cover_state_(param->read.value, param->read.value_len); } break; } case ESP_GATTC_NOTIFY_EVT: { if (param->notify.conn_id != this->parent()->get_conn_id() || param->notify.handle != this->output_handle_) break; ESP_LOGV(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: handle=0x%x, value=0x%x", this->get_name().c_str(), param->notify.handle, param->notify.value[0]); this->publish_cover_state_(param->notify.value, param->notify.value_len); break; } case ESP_GATTC_REG_FOR_NOTIFY_EVT: { this->node_state = espbt::ClientState::ESTABLISHED; if (param->reg_for_notify.status == ESP_GATT_OK) { this->notify_disable_ = false; } break; } default: break; } } void IdasenDeskControllerComponent::write_value_(uint16_t handle, unsigned short value) { uint8_t data[2]; data[0] = value; data[1] = value >> 8; esp_err_t status = ::esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), handle, 2, data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); if (status != ESP_OK) { this->status_set_warning(); ESP_LOGW(TAG, "[%s] Error sending write request for cover, status=%d", this->get_name().c_str(), status); } } void IdasenDeskControllerComponent::read_value_(uint16_t handle) { auto status_read = esp_ble_gattc_read_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), handle, ESP_GATT_AUTH_REQ_NONE); if (status_read) { this->status_set_warning(); ESP_LOGW(TAG, "[%s] Error sending read request for cover, status=%d", this->get_name().c_str(), status_read); } } cover::CoverTraits IdasenDeskControllerComponent::get_traits() { auto traits = cover::CoverTraits(); traits.set_is_assumed_state(false); traits.set_supports_position(true); traits.set_supports_tilt(false); return traits; } void IdasenDeskControllerComponent::publish_cover_state_(uint8_t *value, uint16_t value_len) { std::vector x(value, value + value_len); uint16_t height = ((uint16_t) x[1] << 8) | x[0]; uint16_t speed = ((uint16_t) x[3] << 8) | x[2]; float position = transform_height_to_position((float) height); if (speed == 0) { this->current_operation = cover::COVER_OPERATION_IDLE; } else if (this->position < position) { this->current_operation = cover::COVER_OPERATION_OPENING; } else if (this->position > position) { this->current_operation = cover::COVER_OPERATION_CLOSING; } this->position = position; this->publish_state(false); } void IdasenDeskControllerComponent::move_desk_() { if (this->notify_disable_) { if (this->controlled_ || this->current_operation != cover::COVER_OPERATION_IDLE) { this->read_value_(this->output_handle_); } } if (!this->controlled_) { return; } // Check if target has been reached if (this->is_at_target_()) { ESP_LOGD(TAG, "Update Desk - target reached"); this->stop_move_(); return; } if (this->notify_disable_) { if (this->current_operation == cover::COVER_OPERATION_IDLE) { this->not_moving_loop_++; if (this->not_moving_loop_ > 4) { ESP_LOGD(TAG, "Update Desk - desk not moving"); this->stop_move_(); } } else { this->not_moving_loop_ = 0; } } ESP_LOGD(TAG, "Update Desk - Move from %.0f to %.0f", this->position * 100, this->position_target_ * 100); this->move_torwards_(); } void IdasenDeskControllerComponent::control(const cover::CoverCall &call) { if (this->notify_disable_) { this->read_value_(this->output_handle_); } if (call.get_position().has_value()) { if (this->current_operation != cover::COVER_OPERATION_IDLE) { this->stop_move_(); } this->position_target_ = *call.get_position(); if (this->position == this->position_target_) { return; } if (this->position_target_ > this->position) { this->current_operation = cover::COVER_OPERATION_OPENING; } else { this->current_operation = cover::COVER_OPERATION_CLOSING; } this->start_move_torwards_(); return; } if (call.get_stop()) { ESP_LOGD(TAG, "Cover control - STOP"); this->stop_move_(); } } void IdasenDeskControllerComponent::start_move_torwards_() { this->controlled_ = true; if (this->notify_disable_) { this->not_moving_loop_ = 0; } if (false == this->use_only_up_down_command_) { this->write_value_(this->control_handle_, 0xFE); this->write_value_(this->control_handle_, 0xFF); } } void IdasenDeskControllerComponent::move_torwards_() { if (this->use_only_up_down_command_) { if (this->current_operation == cover::COVER_OPERATION_OPENING) { this->write_value_(this->control_handle_, 0x47); } else if (this->current_operation == cover::COVER_OPERATION_CLOSING) { this->write_value_(this->control_handle_, 0x46); } } else { this->write_value_(this->input_handle_, transform_position_to_height(this->position_target_)); } } void IdasenDeskControllerComponent::stop_move_() { this->write_value_(this->control_handle_, 0xFF); if (false == this->use_only_up_down_command_) { this->write_value_(this->input_handle_, 0x8001); } this->current_operation = cover::COVER_OPERATION_IDLE; this->controlled_ = false; } bool IdasenDeskControllerComponent::is_at_target_() const { switch (this->current_operation) { case cover::COVER_OPERATION_OPENING: return this->position >= this->position_target_; case cover::COVER_OPERATION_CLOSING: return this->position <= this->position_target_; case cover::COVER_OPERATION_IDLE: if (this->notify_disable_) { return !this->controlled_; } default: return true; } } espbt::ESPBTUUID uuid128_from_string(std::string value) { esp_bt_uuid_t m_uuid; m_uuid.len = ESP_UUID_LEN_128; int n = 0; for (int i = 0; i < value.length();) { if (value.c_str()[i] == '-') { i++; } uint8_t MSB = value.c_str()[i]; uint8_t LSB = value.c_str()[i + 1]; if (MSB > '9') { MSB -= 7; } if (LSB > '9') { LSB -= 7; } m_uuid.uuid.uuid128[15 - n++] = ((MSB & 0x0F) << 4) | (LSB & 0x0F); i += 2; } return espbt::ESPBTUUID::from_uuid(m_uuid); } } // namespace idasen_desk_controller } // namespace esphome ================================================ FILE: components/idasen_desk_controller/idasen_desk_controller.h ================================================ #pragma once #include "esphome/core/component.h" #include "esphome/components/cover/cover.h" #include "esphome/components/ble_client/ble_client.h" #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #ifdef ARDUINO_ARCH_ESP32 // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/bluetooth/esp_gattc.html #include namespace esphome { namespace idasen_desk_controller { namespace espbt = esphome::esp32_ble_tracker; using namespace ble_client; espbt::ESPBTUUID uuid128_from_string(std::string value); class IdasenDeskControllerComponent : public Component, public cover::Cover, public BLEClientNode { public: IdasenDeskControllerComponent() : Component(){}; float get_setup_priority() const override { return setup_priority::LATE; } void setup() override; void dump_config() override; void loop() override; void use_only_up_down_command(bool use_only_up_down_command) { use_only_up_down_command_ = use_only_up_down_command; } void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override; cover::CoverTraits get_traits() override; void control(const cover::CoverCall &call) override; private: bool use_only_up_down_command_ = false; espbt::ESPBTUUID output_service_uuid_ = uuid128_from_string("99fa0020-338a-1024-8a49-009c0215f78a"); espbt::ESPBTUUID output_char_uuid_ = uuid128_from_string("99fa0021-338a-1024-8a49-009c0215f78a"); uint16_t output_handle_; espbt::ESPBTUUID input_service_uuid_ = uuid128_from_string("99fa0030-338a-1024-8a49-009c0215f78a"); espbt::ESPBTUUID input_char_uuid_ = uuid128_from_string("99fa0031-338a-1024-8a49-009c0215f78a"); uint16_t input_handle_; espbt::ESPBTUUID control_service_uuid_ = uuid128_from_string("99fa0001-338a-1024-8a49-009c0215f78a"); espbt::ESPBTUUID control_char_uuid_ = uuid128_from_string("99fa0002-338a-1024-8a49-009c0215f78a"); uint16_t control_handle_; bool controlled_ = false; float position_target_; bool notify_disable_ = true; int not_moving_loop_ = 0; void write_value_(uint16_t handle, unsigned short value); void read_value_(uint16_t handle); void publish_cover_state_(uint8_t *value, uint16_t value_len); void move_desk_(); bool is_at_target_() const; void start_move_torwards_(); void move_torwards_(); void stop_move_(); }; } // namespace idasen_desk_controller } // namespace esphome #endif ================================================ FILE: test.yaml ================================================ --- esphome: name: test platform: ESP32 board: esp32dev wifi: ssid: wifi_ssid password: wifi_password fast_connect: true logger: external_components: - source: components esp32_ble_tracker: ble_client: - mac_address: "00:00:00:00:00:00" id: idasen_desk idasen_desk_controller: ble_client_id: idasen_desk only_up_down_command: false cover: - platform: idasen_desk_controller name: "Desk"