main 375485796a70 cached
13 files
28.8 KB
8.2k tokens
8 symbols
1 requests
Download .txt
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: '^<ext/.*\.h>'
    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)              | <img src="https://user-images.githubusercontent.com/9998984/107797805-a3a6c800-6d5b-11eb-863a-56ae0343995c.png" width="300" /> |

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 <string>

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<uint8_t> 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 <esp_gattc_api.h>

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"
Download .txt
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
Download .txt
SYMBOL INDEX (8 symbols across 4 files)

FILE: components/idasen_desk_controller/__init__.py
  function to_code (line 24) | async def to_code(config):

FILE: components/idasen_desk_controller/cover.py
  function to_code (line 12) | def to_code(config):

FILE: components/idasen_desk_controller/idasen_desk_controller.cpp
  type esphome (line 6) | namespace esphome {
    type idasen_desk_controller (line 7) | namespace idasen_desk_controller {
      function transform_height_to_position (line 13) | static float transform_height_to_position(float height) { return hei...
      function transform_position_to_height (line 14) | static float transform_position_to_height(float position) { return p...
      function uuid128_from_string (line 301) | espbt::ESPBTUUID uuid128_from_string(std::string value) {

FILE: components/idasen_desk_controller/idasen_desk_controller.h
  function namespace (line 12) | namespace esphome {
Condensed preview — 13 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (31K chars).
[
  {
    "path": ".clang-format",
    "chars": 3577,
    "preview": "Language: Cpp\nAccessModifierOffset: -1\nAlignAfterOpenBracket: Align\nAlignConsecutiveAssignments: false\nAlignConsecutiveD"
  },
  {
    "path": ".editorconfig",
    "chars": 345,
    "preview": "root = true\n\n# general\n[*]\nend_of_line = lf\ninsert_final_newline = true\ncharset = utf-8\n\n# python\n[*.{py}]\nindent_style "
  },
  {
    "path": ".github/workflows/ci.yaml",
    "chars": 1881,
    "preview": "---\nname: ESPHome Idasen Desk Controller CI\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n  schedule:\n    - cr"
  },
  {
    "path": ".github/workflows/matchers/gcc.json",
    "chars": 449,
    "preview": "{\n    \"problemMatcher\": [\n        {\n            \"owner\": \"gcc\",\n            \"severity\": \"error\",\n            \"pattern\": "
  },
  {
    "path": ".github/workflows/matchers/python.json",
    "chars": 447,
    "preview": "{\n    \"problemMatcher\": [\n        {\n            \"owner\": \"python\",\n            \"pattern\": [\n                {\n          "
  },
  {
    "path": ".gitignore",
    "chars": 28,
    "preview": "__pycache__\n.python-version\n"
  },
  {
    "path": "LICENSE",
    "chars": 1072,
    "preview": "MIT License\n\nCopyright (c) 2021 Julien Gobillot\n\nPermission is hereby granted, free of charge, to any person obtaining a"
  },
  {
    "path": "README.md",
    "chars": 6614,
    "preview": "This project is archived as Idasen Desk is now compatbile with Home Assistant and ESPHome Bluetooth Proxy (https://www.h"
  },
  {
    "path": "components/idasen_desk_controller/__init__.py",
    "chars": 1026,
    "preview": "import esphome.codegen as cg\nimport esphome.config_validation as cv\nfrom esphome.components import cover, ble_client\nfro"
  },
  {
    "path": "components/idasen_desk_controller/cover.py",
    "chars": 514,
    "preview": "import esphome.codegen as cg\nimport esphome.config_validation as cv\nfrom esphome.components import cover\nfrom . import I"
  },
  {
    "path": "components/idasen_desk_controller/idasen_desk_controller.cpp",
    "chars": 10619,
    "preview": "#include \"idasen_desk_controller.h\"\n#include \"esphome/core/log.h\"\n#include \"esphome/core/helpers.h\"\n#include <string>\n\nn"
  },
  {
    "path": "components/idasen_desk_controller/idasen_desk_controller.h",
    "chars": 2479,
    "preview": "#pragma once\n\n#include \"esphome/core/component.h\"\n#include \"esphome/components/cover/cover.h\"\n#include \"esphome/componen"
  },
  {
    "path": "test.yaml",
    "chars": 424,
    "preview": "---\nesphome:\n  name: test\n  platform: ESP32\n  board: esp32dev\n\nwifi:\n  ssid: wifi_ssid\n  password: wifi_password\n  fast_"
  }
]

About this extraction

This page contains the full source code of the j5lien/esphome-idasen-desk-controller GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 13 files (28.8 KB), approximately 8.2k tokens, and a symbol index with 8 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!