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) |
| ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|  | <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"
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
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.