[
  {
    "path": ".clang-format",
    "content": "Language: Cpp\nAccessModifierOffset: -1\nAlignAfterOpenBracket: Align\nAlignConsecutiveAssignments: false\nAlignConsecutiveDeclarations: false\nAlignEscapedNewlines: DontAlign\nAlignOperands: true\nAlignTrailingComments: true\nAllowAllParametersOfDeclarationOnNextLine: true\nAllowShortBlocksOnASingleLine: false\nAllowShortCaseLabelsOnASingleLine: false\nAllowShortFunctionsOnASingleLine: All\nAllowShortIfStatementsOnASingleLine: false\nAllowShortLoopsOnASingleLine: false\nAlwaysBreakAfterReturnType: None\nAlwaysBreakBeforeMultilineStrings: false\nAlwaysBreakTemplateDeclarations: MultiLine\nBinPackArguments: true\nBinPackParameters: true\nBraceWrapping:\n  AfterClass: false\n  AfterControlStatement: false\n  AfterEnum: false\n  AfterFunction: false\n  AfterNamespace: false\n  AfterObjCDeclaration: false\n  AfterStruct: false\n  AfterUnion: false\n  AfterExternBlock: false\n  BeforeCatch: false\n  BeforeElse: false\n  IndentBraces: false\n  SplitEmptyFunction: true\n  SplitEmptyRecord: true\n  SplitEmptyNamespace: true\nBreakBeforeBinaryOperators: None\nBreakBeforeBraces: Attach\nBreakBeforeInheritanceComma: false\nBreakInheritanceList: BeforeColon\nBreakBeforeTernaryOperators: true\nBreakConstructorInitializersBeforeComma: false\nBreakConstructorInitializers: BeforeColon\nBreakAfterJavaFieldAnnotations: false\nBreakStringLiterals: true\nColumnLimit: 120\nCommentPragmas: \"^ IWYU pragma:\"\nCompactNamespaces: false\nConstructorInitializerAllOnOneLineOrOnePerLine: true\nConstructorInitializerIndentWidth: 4\nContinuationIndentWidth: 4\nCpp11BracedListStyle: true\nDerivePointerAlignment: true\nDisableFormat: false\nExperimentalAutoDetectBinPacking: false\nFixNamespaceComments: true\nForEachMacros:\n  - foreach\n  - Q_FOREACH\n  - BOOST_FOREACH\nIncludeBlocks: Preserve\nIncludeCategories:\n  - Regex: '^<ext/.*\\.h>'\n    Priority: 2\n  - Regex: '^<.*\\.h>'\n    Priority: 1\n  - Regex: \"^<.*\"\n    Priority: 2\n  - Regex: \".*\"\n    Priority: 3\nIncludeIsMainRegex: \"([-_](test|unittest))?$\"\nIndentCaseLabels: true\nIndentPPDirectives: None\nIndentWidth: 2\nIndentWrappedFunctionNames: false\nKeepEmptyLinesAtTheStartOfBlocks: false\nMacroBlockBegin: \"\"\nMacroBlockEnd: \"\"\nMaxEmptyLinesToKeep: 1\nNamespaceIndentation: None\nPenaltyBreakAssignment: 2\nPenaltyBreakBeforeFirstCallParameter: 1\nPenaltyBreakComment: 300\nPenaltyBreakFirstLessLess: 120\nPenaltyBreakString: 1000\nPenaltyBreakTemplateDeclaration: 10\nPenaltyExcessCharacter: 1000000\nPenaltyReturnTypeOnItsOwnLine: 2000\nPointerAlignment: Right\nRawStringFormats:\n  - Language: Cpp\n    Delimiters:\n      - cc\n      - CC\n      - cpp\n      - Cpp\n      - CPP\n      - \"c++\"\n      - \"C++\"\n    CanonicalDelimiter: \"\"\n    BasedOnStyle: google\n  - Language: TextProto\n    Delimiters:\n      - pb\n      - PB\n      - proto\n      - PROTO\n    EnclosingFunctions:\n      - EqualsProto\n      - EquivToProto\n      - PARSE_PARTIAL_TEXT_PROTO\n      - PARSE_TEST_PROTO\n      - PARSE_TEXT_PROTO\n      - ParseTextOrDie\n      - ParseTextProtoOrDie\n    CanonicalDelimiter: \"\"\n    BasedOnStyle: google\nReflowComments: true\nSortIncludes: false\nSortUsingDeclarations: false\nSpaceAfterCStyleCast: true\nSpaceAfterTemplateKeyword: false\nSpaceBeforeAssignmentOperators: true\nSpaceBeforeCpp11BracedList: false\nSpaceBeforeCtorInitializerColon: true\nSpaceBeforeInheritanceColon: true\nSpaceBeforeParens: ControlStatements\nSpaceBeforeRangeBasedForLoopColon: true\nSpaceInEmptyParentheses: false\nSpacesBeforeTrailingComments: 2\nSpacesInAngles: false\nSpacesInContainerLiterals: false\nSpacesInCStyleCastParentheses: false\nSpacesInParentheses: false\nSpacesInSquareBrackets: false\nStandard: Auto\nTabWidth: 2\nUseTab: Never\n"
  },
  {
    "path": ".editorconfig",
    "content": "root = true\n\n# general\n[*]\nend_of_line = lf\ninsert_final_newline = true\ncharset = utf-8\n\n# python\n[*.{py}]\nindent_style = space\nindent_size = 4\n\n# C++\n[*.{cpp,h,tcc}]\nindent_style = space\nindent_size = 2\n\n# Web\n[*.{js,html,css}]\nindent_style = space\nindent_size = 2\n\n# YAML\n[*.{yaml,yml}]\nindent_style = space\nindent_size = 2\nquote_type = single"
  },
  {
    "path": ".github/workflows/ci.yaml",
    "content": "---\nname: ESPHome Idasen Desk Controller CI\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n  schedule:\n    - cron: 0 12 * * *\n\njobs:\n  esphome-config:\n    runs-on: ubuntu-latest\n    steps:\n      - name: ⤵️ Check out configuration from GitHub\n        uses: actions/checkout@v2\n      - name: Setup Python 3.7\n        uses: actions/setup-python@v1\n        with:\n          python-version: 3.7\n      - name: Install dependencies\n        run: |\n          python -m pip install --upgrade pip setuptools wheel\n          pip install esphome\n          pip list\n          esphome version\n      - name: 🚀 Run esphome config on test file\n        run: |\n          esphome config test.yaml\n\n  esphome-compile:\n    runs-on: ubuntu-latest\n    needs: [esphome-config]\n    steps:\n      - name: ⤵️ Check out configuration from GitHub\n        uses: actions/checkout@v2\n      - name: Cache .esphome\n        uses: actions/cache@v2\n        with:\n          path: .esphome\n          key: esphome-compile-esphome-${{ hashFiles('*.yaml') }}\n          restore-keys: esphome-compile-esphome-\n      - name: Cache .pioenvs\n        uses: actions/cache@v2\n        with:\n          path: .pioenvs\n          key: esphome-compile-pioenvs-${{ hashFiles('*.yaml') }}\n          restore-keys: esphome-compile-pioenvs-\n      - name: Set up Python 3.7\n        uses: actions/setup-python@v1\n        with:\n          python-version: 3.7\n      - name: Install dependencies\n        run: |\n          python -m pip install --upgrade pip setuptools wheel\n          pip install esphome\n          pip list\n          esphome version\n      - name: Register problem matchers\n        run: |\n          echo \"::add-matcher::.github/workflows/matchers/gcc.json\"\n          echo \"::add-matcher::.github/workflows/matchers/python.json\"\n      - name: 🚀 Run esphome compile on test file\n        run: |\n          esphome compile test.yaml\n"
  },
  {
    "path": ".github/workflows/matchers/gcc.json",
    "content": "{\n    \"problemMatcher\": [\n        {\n            \"owner\": \"gcc\",\n            \"severity\": \"error\",\n            \"pattern\": [\n                {\n                    \"regexp\": \"^(.*):(\\\\d+):(\\\\d+):\\\\s+(?:fatal\\\\s+)?(warning|error):\\\\s+(.*)$\",\n                    \"file\": 1,\n                    \"line\": 2,\n                    \"column\": 3,\n                    \"severity\": 4,\n                    \"message\": 5\n                }\n            ]\n        }\n    ]\n}"
  },
  {
    "path": ".github/workflows/matchers/python.json",
    "content": "{\n    \"problemMatcher\": [\n        {\n            \"owner\": \"python\",\n            \"pattern\": [\n                {\n                    \"regexp\": \"^\\\\s*File\\\\s\\\\\\\"(.*)\\\\\\\",\\\\sline\\\\s(\\\\d+),\\\\sin\\\\s(.*)$\",\n                    \"file\": 1,\n                    \"line\": 2\n                },\n                {\n                    \"regexp\": \"^\\\\s*raise\\\\s(.*)\\\\(\\\\'(.*)\\\\'\\\\)$\",\n                    \"message\": 2\n                }\n            ]\n        }\n    ]\n}"
  },
  {
    "path": ".gitignore",
    "content": "__pycache__\n.python-version\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021 Julien Gobillot\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "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/).\n-------------\n\nThis 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).\n\n| [Cover integration](https://www.home-assistant.io/integrations/cover/) | [Linak Desk Card](https://github.com/IhorSyerkov/linak-desk-card)                                                              |\n| ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |\n| ![Home Assistant Desk Controller](ha-desk-controller.png)              | <img src=\"https://user-images.githubusercontent.com/9998984/107797805-a3a6c800-6d5b-11eb-863a-56ae0343995c.png\" width=\"300\" /> |\n\nThe 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.\n\n## Dependencies\n\n* This component requires an [ESP32 device](https://esphome.io/devices/esp32.html).\n* [ESPHome 2021.10.0 or higher](https://github.com/esphome/esphome/releases).\n\n## Installation\n\nYou can install this component with [ESPHome external components feature](https://esphome.io/components/external_components.html) like this:\n```\nexternal_components:\n  - source: github://j5lien/esphome-idasen-desk-controller@v4.0.0\n```\n\nFor the first connection you will need to press the pairing button on the desk.\n\n## Configuration\n\n### BLE Client\n\nYou need first to configure [ESPHome BLE Client](https://esphome.io/components/ble_client.html) (check the documentation for more information):\n\n```yaml\nesp32_ble_tracker:\n\nble_client:\n  - mac_address: \"00:00:00:00:00:00\" # Replace with the desk bluetooth mac address\n    id: idasen_desk\n```\n\n> 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:\n`system_profiler SPBluetoothDataType`\n\n### Idasen Desk Controller\n\nThen you need to enable this component with the id of the ble_client component:\n\n```yaml\nidasen_desk_controller:\n    # Reference to the ble client component id\n    # -----------\n    # Required\n    ble_client_id: idasen_desk\n    # Fallback to use only up and down commands (less precise)\n    # -----------\n    # Optional\n    only_up_down_command: false\n```\n\n### Cover\n\nNow you can add the cover component that will allow you to control your desk:\n\n```yaml\ncover:\n  - platform: idasen_desk_controller\n    name: \"Desk\"\n```\n\n### Extra Desk informations\n\nUsing [ESPHome BLE Client Sensor](https://esphome.io/components/sensor/ble_client.html), you can expose more informations that doesn't require this custom component.\n\nThis is an example that generates sensors that were available in previous versions:\n\n```yaml\nesp32_ble_tracker:\n\nglobals:\n  # To store the Desk Connection Status\n  - id: ble_client_connected\n    type: bool\n    initial_value: 'false'\n\nble_client:\n  - mac_address: \"00:00:00:00:00:00\"\n    id: idasen_desk\n    on_connect:\n      then:\n        # Update the Desk Connection Status\n        - lambda: |-\n            id(ble_client_connected) = true;\n        - delay: 5s\n        # Update desk height and speed sensors after bluetooth is connected\n        - lambda: |-\n            id(desk_height).update();\n            id(desk_speed).update();\n    on_disconnect:\n      then:\n        # Update the Desk Connection Status\n        - lambda: |-\n            id(ble_client_connected) = false;\n\nsensor:\n  # Desk Height Sensor\n  - platform: ble_client\n    type: characteristic\n    ble_client_id: idasen_desk\n    id: desk_height\n    name: 'Desk Height'\n    service_uuid: '99fa0020-338a-1024-8a49-009c0215f78a'\n    characteristic_uuid: '99fa0021-338a-1024-8a49-009c0215f78a'\n    icon: 'mdi:arrow-up-down'\n    unit_of_measurement: 'cm'\n    accuracy_decimals: 1\n    update_interval: never\n    notify: true\n    lambda: |-\n      uint16_t raw_height = ((uint16_t)x[1] << 8) | x[0];\n      unsigned short height_mm = raw_height / 10;\n\n      return (float) height_mm / 10;\n\n  # Desk Speed Sensor\n  - platform: ble_client\n    type: characteristic\n    ble_client_id: idasen_desk\n    id: desk_speed\n    name: 'Desk Speed'\n    service_uuid: '99fa0020-338a-1024-8a49-009c0215f78a'\n    characteristic_uuid: '99fa0021-338a-1024-8a49-009c0215f78a'\n    icon: 'mdi:speedometer'\n    unit_of_measurement: 'cm/min' # I'm not sure this unit is correct\n    accuracy_decimals: 0\n    update_interval: never\n    notify: true\n    lambda: |-\n      uint16_t raw_speed = ((uint16_t)x[3] << 8) | x[2];\n      return raw_speed / 100;\n\nbinary_sensor:\n  # Desk Bluetooth Connection Status\n  - platform: template\n    name: 'Desk Connection'\n    id: desk_connection\n    lambda: 'return id(ble_client_connected);'\n\n  # Desk Moving Status\n  - platform: template\n    name: 'Desk Moving'\n    id: desk_moving\n    lambda: 'return id(desk_speed).state > 0;'\n```\n\n## Troubleshooting\n\n### ESPHome lower than 1.19.0\n\nCheck the version [v1.2.4](https://github.com/j5lien/esphome-idasen-desk-controller/releases/tag/v1.2.4) of this component\n\n### Not moving using cover component\n\nIf 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.\n\n```yaml\nidasen_desk_controller:\n    ble_client_id: idasen_desk\n    only_up_down_command: true\n```\n\n### Wifi deconnexion\n\nIf you experience Wifi deconnexion, try to activate the [wifi fast connect](https://esphome.io/components/wifi.html) option.\n```yaml\nwifi:\n  ssid: ...\n  password: ...\n  fast_connect: true\n```\n\nYou can also try to set the [power save mode](https://esphome.io/components/wifi.html?highlight=wifi#power-save-mode) option to `none`.\n```yaml\nwifi:\n  ssid: ...\n  password: ...\n  fast_connect: true\n  power_save_mode: none\n```\n\n## References\n\n* https://github.com/TheRealMazur/LinakDeskEsp32Controller\n* https://esphome.io/components/ble_client.html\n* https://esphome.io/components/sensor/ble_client.html\n"
  },
  {
    "path": "components/idasen_desk_controller/__init__.py",
    "content": "import esphome.codegen as cg\nimport esphome.config_validation as cv\nfrom esphome.components import cover, ble_client\nfrom esphome.const import CONF_ID\n\nDEPENDENCIES = ['esp32', 'ble_client']\n\nAUTO_LOAD = ['cover']\nMULTI_CONF = True\n\nCONF_IDASEN_DESK_CONTROLLER_ID = 'idasen_desk_controller_id'\nCONF_ONLY_UP_DOWN_COMMAND = 'only_up_down_command'\n\nidasen_desk_controller_ns = cg.esphome_ns.namespace('idasen_desk_controller')\n\nIdasenDeskControllerComponent = idasen_desk_controller_ns.class_(\n    'IdasenDeskControllerComponent', cg.Component, cover.Cover, ble_client.BLEClientNode)\n\nCONFIG_SCHEMA = cv.Schema({\n    cv.GenerateID(): cv.declare_id(IdasenDeskControllerComponent),\n    cv.Optional(CONF_ONLY_UP_DOWN_COMMAND, False): cv.boolean,\n}).extend(ble_client.BLE_CLIENT_SCHEMA)\n\nasync def to_code(config):\n    var = cg.new_Pvariable(config[CONF_ID])\n    await cg.register_component(var, config)\n    await ble_client.register_ble_node(var, config)\n\n    cg.add(var.use_only_up_down_command(config[CONF_ONLY_UP_DOWN_COMMAND]))\n"
  },
  {
    "path": "components/idasen_desk_controller/cover.py",
    "content": "import esphome.codegen as cg\nimport esphome.config_validation as cv\nfrom esphome.components import cover\nfrom . import IdasenDeskControllerComponent, CONF_IDASEN_DESK_CONTROLLER_ID\n\nDEPENDENCIES = ['esp32', 'idasen_desk_controller']\n\nCONFIG_SCHEMA = cover.COVER_SCHEMA.extend(({\n    cv.GenerateID(CONF_IDASEN_DESK_CONTROLLER_ID): cv.use_id(IdasenDeskControllerComponent),\n}))\n\ndef to_code(config):\n    hub = yield cg.get_variable(config[CONF_IDASEN_DESK_CONTROLLER_ID])\n    yield cover.register_cover(hub, config)\n"
  },
  {
    "path": "components/idasen_desk_controller/idasen_desk_controller.cpp",
    "content": "#include \"idasen_desk_controller.h\"\n#include \"esphome/core/log.h\"\n#include \"esphome/core/helpers.h\"\n#include <string>\n\nnamespace esphome {\nnamespace idasen_desk_controller {\n\nstatic const char *TAG = \"idasen_desk_controller\";\n\nstatic const float DESK_MAX_HEIGHT = 6500;\n\nstatic float transform_height_to_position(float height) { return height / DESK_MAX_HEIGHT; }\nstatic float transform_position_to_height(float position) { return position * DESK_MAX_HEIGHT; }\n\nvoid IdasenDeskControllerComponent::loop() {}\n\nvoid IdasenDeskControllerComponent::setup() {\n  ESP_LOGCONFIG(TAG, \"Setting up Idasen Desk Controller...\");\n  this->set_interval(\"update_desk\", 200, [this]() { this->move_desk_(); });\n}\n\nvoid IdasenDeskControllerComponent::dump_config() {\n  ESP_LOGCONFIG(TAG, \"Idasen Desk Controller:\");\n  ESP_LOGCONFIG(TAG, \"  MAC address        : %s\", this->parent()->address_str().c_str());\n  ESP_LOGCONFIG(TAG, \"  Notifications      : %s\", this->notify_disable_ ? \"disable\" : \"enable\");\n  LOG_COVER(\"  \", \"Desk\", this);\n}\n\nvoid IdasenDeskControllerComponent::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,\n                                                        esp_ble_gattc_cb_param_t *param) {\n  switch (event) {\n    case ESP_GATTC_WRITE_CHAR_EVT: {\n      if (param->write.status != ESP_GATT_OK) {\n        ESP_LOGW(TAG, \"Error writing char at handle %d, status=%d\", param->write.handle, param->write.status);\n      }\n      break;\n    }\n\n    case ESP_GATTC_OPEN_EVT: {\n      if (param->open.status == ESP_GATT_OK) {\n        ESP_LOGI(TAG, \"[%s] Connected successfully!\", this->get_name().c_str());\n        break;\n      }\n      break;\n    }\n\n    case ESP_GATTC_DISCONNECT_EVT: {\n      ESP_LOGW(TAG, \"[%s] Disconnected!\", this->get_name().c_str());\n      this->status_set_warning();\n      break;\n    }\n\n    case ESP_GATTC_SEARCH_CMPL_EVT: {\n      // Look for output handle\n      this->output_handle_ = 0;\n      auto chr_output = this->parent()->get_characteristic(this->output_service_uuid_, this->output_char_uuid_);\n      if (chr_output == nullptr) {\n        this->status_set_warning();\n        ESP_LOGW(TAG, \"No characteristic found at service %s char %s\", this->output_service_uuid_.to_string().c_str(),\n                 this->output_char_uuid_.to_string().c_str());\n        break;\n      }\n      this->output_handle_ = chr_output->handle;\n\n      // Register for notification\n      auto status_notify =\n          esp_ble_gattc_register_for_notify(this->parent()->get_gattc_if(), this->parent()->get_remote_bda(), this->output_handle_);\n      if (status_notify) {\n        ESP_LOGW(TAG, \"esp_ble_gattc_register_for_notify failed, status=%d\", status_notify);\n      }\n\n      // Look for input handle\n      this->input_handle_ = 0;\n      auto chr_input = this->parent()->get_characteristic(this->input_service_uuid_, this->input_char_uuid_);\n      if (chr_input == nullptr) {\n        this->status_set_warning();\n        ESP_LOGW(TAG, \"No characteristic found at service %s char %s\", this->input_service_uuid_.to_string().c_str(),\n                 this->input_char_uuid_.to_string().c_str());\n        break;\n      }\n      this->input_handle_ = chr_input->handle;\n\n      // Look for control handle\n      this->control_handle_ = 0;\n      auto chr_control = this->parent()->get_characteristic(this->control_service_uuid_, this->control_char_uuid_);\n      if (chr_control == nullptr) {\n        this->status_set_warning();\n        ESP_LOGW(TAG, \"No characteristic found at service %s char %s\", this->control_service_uuid_.to_string().c_str(),\n                 this->control_char_uuid_.to_string().c_str());\n        break;\n      }\n      this->control_handle_ = chr_control->handle;\n\n      this->set_timeout(\"desk_init\", 5000, [this]() { this->read_value_(this->output_handle_); });\n\n      break;\n    }\n\n    case ESP_GATTC_READ_CHAR_EVT: {\n      if (param->read.conn_id != this->parent()->get_conn_id())\n        break;\n      if (param->read.status != ESP_GATT_OK) {\n        ESP_LOGW(TAG, \"Error reading char at handle %d, status=%d\", param->read.handle, param->read.status);\n        break;\n      }\n      if (param->read.handle == this->output_handle_) {\n        this->status_clear_warning();\n        this->publish_cover_state_(param->read.value, param->read.value_len);\n      }\n      break;\n    }\n\n    case ESP_GATTC_NOTIFY_EVT: {\n      if (param->notify.conn_id != this->parent()->get_conn_id() || param->notify.handle != this->output_handle_)\n        break;\n      ESP_LOGV(TAG, \"[%s] ESP_GATTC_NOTIFY_EVT: handle=0x%x, value=0x%x\", this->get_name().c_str(),\n               param->notify.handle, param->notify.value[0]);\n      this->publish_cover_state_(param->notify.value, param->notify.value_len);\n      break;\n    }\n\n    case ESP_GATTC_REG_FOR_NOTIFY_EVT: {\n      this->node_state = espbt::ClientState::ESTABLISHED;\n      if (param->reg_for_notify.status == ESP_GATT_OK) {\n        this->notify_disable_ = false;\n      }\n      break;\n    }\n\n    default:\n      break;\n  }\n}\n\nvoid IdasenDeskControllerComponent::write_value_(uint16_t handle, unsigned short value) {\n  uint8_t data[2];\n  data[0] = value;\n  data[1] = value >> 8;\n\n  esp_err_t status = ::esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), handle, 2, data,\n                                                ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);\n\n  if (status != ESP_OK) {\n    this->status_set_warning();\n    ESP_LOGW(TAG, \"[%s] Error sending write request for cover, status=%d\", this->get_name().c_str(), status);\n  }\n}\n\nvoid IdasenDeskControllerComponent::read_value_(uint16_t handle) {\n  auto status_read =\n      esp_ble_gattc_read_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), handle, ESP_GATT_AUTH_REQ_NONE);\n  if (status_read) {\n    this->status_set_warning();\n    ESP_LOGW(TAG, \"[%s] Error sending read request for cover, status=%d\", this->get_name().c_str(), status_read);\n  }\n}\n\ncover::CoverTraits IdasenDeskControllerComponent::get_traits() {\n  auto traits = cover::CoverTraits();\n  traits.set_is_assumed_state(false);\n  traits.set_supports_position(true);\n  traits.set_supports_tilt(false);\n  return traits;\n}\n\nvoid IdasenDeskControllerComponent::publish_cover_state_(uint8_t *value, uint16_t value_len) {\n  std::vector<uint8_t> x(value, value + value_len);\n\n  uint16_t height = ((uint16_t) x[1] << 8) | x[0];\n  uint16_t speed = ((uint16_t) x[3] << 8) | x[2];\n\n  float position = transform_height_to_position((float) height);\n\n  if (speed == 0) {\n    this->current_operation = cover::COVER_OPERATION_IDLE;\n  } else if (this->position < position) {\n    this->current_operation = cover::COVER_OPERATION_OPENING;\n  } else if (this->position > position) {\n    this->current_operation = cover::COVER_OPERATION_CLOSING;\n  }\n\n  this->position = position;\n  this->publish_state(false);\n}\n\nvoid IdasenDeskControllerComponent::move_desk_() {\n  if (this->notify_disable_) {\n    if (this->controlled_ || this->current_operation != cover::COVER_OPERATION_IDLE) {\n      this->read_value_(this->output_handle_);\n    }\n  }\n\n  if (!this->controlled_) {\n    return;\n  }\n\n  // Check if target has been reached\n  if (this->is_at_target_()) {\n    ESP_LOGD(TAG, \"Update Desk - target reached\");\n    this->stop_move_();\n    return;\n  }\n\n  if (this->notify_disable_) {\n    if (this->current_operation == cover::COVER_OPERATION_IDLE) {\n      this->not_moving_loop_++;\n      if (this->not_moving_loop_ > 4) {\n        ESP_LOGD(TAG, \"Update Desk - desk not moving\");\n        this->stop_move_();\n      }\n    } else {\n      this->not_moving_loop_ = 0;\n    }\n  }\n\n  ESP_LOGD(TAG, \"Update Desk - Move from %.0f to %.0f\", this->position * 100, this->position_target_ * 100);\n  this->move_torwards_();\n}\n\nvoid IdasenDeskControllerComponent::control(const cover::CoverCall &call) {\n  if (this->notify_disable_) {\n    this->read_value_(this->output_handle_);\n  }\n\n  if (call.get_position().has_value()) {\n    if (this->current_operation != cover::COVER_OPERATION_IDLE) {\n      this->stop_move_();\n    }\n\n    this->position_target_ = *call.get_position();\n\n    if (this->position == this->position_target_) {\n      return;\n    }\n\n    if (this->position_target_ > this->position) {\n      this->current_operation = cover::COVER_OPERATION_OPENING;\n    } else {\n      this->current_operation = cover::COVER_OPERATION_CLOSING;\n    }\n\n    this->start_move_torwards_();\n    return;\n  }\n\n  if (call.get_stop()) {\n    ESP_LOGD(TAG, \"Cover control - STOP\");\n    this->stop_move_();\n  }\n}\n\nvoid IdasenDeskControllerComponent::start_move_torwards_() {\n  this->controlled_ = true;\n  if (this->notify_disable_) {\n    this->not_moving_loop_ = 0;\n  }\n  if (false == this->use_only_up_down_command_) {\n    this->write_value_(this->control_handle_, 0xFE);\n    this->write_value_(this->control_handle_, 0xFF);\n  }\n}\n\nvoid IdasenDeskControllerComponent::move_torwards_() {\n  if (this->use_only_up_down_command_) {\n    if (this->current_operation == cover::COVER_OPERATION_OPENING) {\n      this->write_value_(this->control_handle_, 0x47);\n    } else if (this->current_operation == cover::COVER_OPERATION_CLOSING) {\n      this->write_value_(this->control_handle_, 0x46);\n    }\n  } else {\n    this->write_value_(this->input_handle_, transform_position_to_height(this->position_target_));\n  }\n}\n\nvoid IdasenDeskControllerComponent::stop_move_() {\n  this->write_value_(this->control_handle_, 0xFF);\n  if (false == this->use_only_up_down_command_) {\n    this->write_value_(this->input_handle_, 0x8001);\n  }\n\n  this->current_operation = cover::COVER_OPERATION_IDLE;\n  this->controlled_ = false;\n}\n\nbool IdasenDeskControllerComponent::is_at_target_() const {\n  switch (this->current_operation) {\n    case cover::COVER_OPERATION_OPENING:\n      return this->position >= this->position_target_;\n    case cover::COVER_OPERATION_CLOSING:\n      return this->position <= this->position_target_;\n    case cover::COVER_OPERATION_IDLE:\n      if (this->notify_disable_) {\n        return !this->controlled_;\n      }\n    default:\n      return true;\n  }\n}\n\nespbt::ESPBTUUID uuid128_from_string(std::string value) {\n  esp_bt_uuid_t m_uuid;\n  m_uuid.len = ESP_UUID_LEN_128;\n  int n = 0;\n  for (int i = 0; i < value.length();) {\n    if (value.c_str()[i] == '-') {\n      i++;\n    }\n    uint8_t MSB = value.c_str()[i];\n    uint8_t LSB = value.c_str()[i + 1];\n    if (MSB > '9') {\n      MSB -= 7;\n    }\n    if (LSB > '9') {\n      LSB -= 7;\n    }\n    m_uuid.uuid.uuid128[15 - n++] = ((MSB & 0x0F) << 4) | (LSB & 0x0F);\n    i += 2;\n  }\n\n  return espbt::ESPBTUUID::from_uuid(m_uuid);\n}\n\n}  // namespace idasen_desk_controller\n}  // namespace esphome"
  },
  {
    "path": "components/idasen_desk_controller/idasen_desk_controller.h",
    "content": "#pragma once\n\n#include \"esphome/core/component.h\"\n#include \"esphome/components/cover/cover.h\"\n#include \"esphome/components/ble_client/ble_client.h\"\n#include \"esphome/components/esp32_ble_tracker/esp32_ble_tracker.h\"\n\n#ifdef ARDUINO_ARCH_ESP32\n// https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/bluetooth/esp_gattc.html\n#include <esp_gattc_api.h>\n\nnamespace esphome {\nnamespace idasen_desk_controller {\n\nnamespace espbt = esphome::esp32_ble_tracker;\n\nusing namespace ble_client;\n\nespbt::ESPBTUUID uuid128_from_string(std::string value);\n\nclass IdasenDeskControllerComponent : public Component, public cover::Cover, public BLEClientNode {\n public:\n  IdasenDeskControllerComponent() : Component(){};\n\n  float get_setup_priority() const override { return setup_priority::LATE; }\n  void setup() override;\n  void dump_config() override;\n\n  void loop() override;\n\n  void use_only_up_down_command(bool use_only_up_down_command) { use_only_up_down_command_ = use_only_up_down_command; }\n\n  void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,\n                           esp_ble_gattc_cb_param_t *param) override;\n\n  cover::CoverTraits get_traits() override;\n  void control(const cover::CoverCall &call) override;\n\n private:\n  bool use_only_up_down_command_ = false;\n\n  espbt::ESPBTUUID output_service_uuid_ = uuid128_from_string(\"99fa0020-338a-1024-8a49-009c0215f78a\");\n  espbt::ESPBTUUID output_char_uuid_ = uuid128_from_string(\"99fa0021-338a-1024-8a49-009c0215f78a\");\n  uint16_t output_handle_;\n\n  espbt::ESPBTUUID input_service_uuid_ = uuid128_from_string(\"99fa0030-338a-1024-8a49-009c0215f78a\");\n  espbt::ESPBTUUID input_char_uuid_ = uuid128_from_string(\"99fa0031-338a-1024-8a49-009c0215f78a\");\n  uint16_t input_handle_;\n\n  espbt::ESPBTUUID control_service_uuid_ = uuid128_from_string(\"99fa0001-338a-1024-8a49-009c0215f78a\");\n  espbt::ESPBTUUID control_char_uuid_ = uuid128_from_string(\"99fa0002-338a-1024-8a49-009c0215f78a\");\n  uint16_t control_handle_;\n\n  bool controlled_ = false;\n  float position_target_;\n\n  bool notify_disable_ = true;\n  int not_moving_loop_ = 0;\n\n  void write_value_(uint16_t handle, unsigned short value);\n  void read_value_(uint16_t handle);\n  void publish_cover_state_(uint8_t *value, uint16_t value_len);\n  void move_desk_();\n  bool is_at_target_() const;\n  void start_move_torwards_();\n  void move_torwards_();\n  void stop_move_();\n};\n}  // namespace idasen_desk_controller\n}  // namespace esphome\n\n#endif"
  },
  {
    "path": "test.yaml",
    "content": "---\nesphome:\n  name: test\n  platform: ESP32\n  board: esp32dev\n\nwifi:\n  ssid: wifi_ssid\n  password: wifi_password\n  fast_connect: true\n\nlogger:\n\nexternal_components:\n  - source: components\n\nesp32_ble_tracker:\n\nble_client:\n  - mac_address: \"00:00:00:00:00:00\"\n    id: idasen_desk\n\nidasen_desk_controller:\n  ble_client_id: idasen_desk\n  only_up_down_command: false\n\ncover:\n  - platform: idasen_desk_controller\n    name: \"Desk\"\n"
  }
]