Modified to work with maidsite

This commit is contained in:
ElVit 2024-07-23 15:59:21 +02:00
parent 5533a35801
commit b851b63f2c
24 changed files with 1039 additions and 923 deletions

2
.gitignore vendored
View File

@ -1 +1 @@
**/__pycache__/**
**/__pycache__/**

4
.vimrc
View File

@ -1,4 +0,0 @@
if SpmSetProject("esphome_components") == 0
nmap <leader>h yy'fpwd2f:f)lC;<esc>==mf^w
nmap <leader>s 02w"aye2b02w"byej/::a Vf{%jd/::b f{%jpzz^wj
endif

View File

@ -1,6 +1,6 @@
# Rocka84's ESPHome Components
## Available Components
* [Jiecang Desk Controller](components/jiecang_desk_controller/)
# ElVit's ESPHome Components
## Available Components
* [Maidsite Desk Controller](components/maidsite_desk_controller/)

View File

@ -1,184 +0,0 @@
# ESPHome Jiecang Desk Controller
[ESPHome](https://esphome.io/) component for controlling Jiecang desk controllers via their serial protocol.
![](https://github.com/Rocka84/esphome_components/assets/2353088/b04d1fd7-d0ed-443d-87b7-87454757c0f0)
Attention: I only have one [RJ12 model](https://www.jiecang.com/product/jcb35m11c.html) to test this
but it's likely that other Jiecang controllers are supported as their serial protocol should be compatible.
## Usage
### What you need
* ESPHome compatible microcontroller
* depending on your model of desk controller
* cable with RJ12 connector (phone cable, RJ11 may also work)
* cable with RJ45 connector (network cable)
### Wiring
#### RJ12
Please double check this for your specific model!
pin | function
----|---------
1 | NC (pulled up)
2 | GND
3 | TX
4 | VCC
5 | RX
6 | NC (pulled up)
#### RJ45
**Untested** and only for reference!
pin | function
----|---------
1 | HS3 [^1]
2 | TX
3 | GND
4 | RX
5 | VCC
6 | HS2 [^1]
7 | HS1 [^1]
8 | HS0 [^1]
[^1]: not used here
#### microcontroller
ESP | desk
----|-----
GND | GND
5V | VCC
RX | TX
TX | RX
![](https://github.com/Rocka84/esphome_components/assets/2353088/39e08774-a215-4e7e-8345-a2acafce28a2)
## Usage
```yaml
esphome:
...
on_boot:
# This script is required to initialize the following sensors:
# height_pct, height_min, height_max, position1 - position4
# You can skip this if you don't use those.
priority: 0 # when mostly everything else is done
then:
- lambda: "id(my_desk).request_physical_limits();"
- delay: 0.1s
- lambda: "id(my_desk).request_limits();"
- delay: 0.1s
- lambda: "id(my_desk).request_settings();"
external_components:
- source:
type: git
url: https://github.com/Rocka84/esphome_components/
components: [ jiecang_desk_controller ]
uart:
id: uart_bus
tx_pin: TX
rx_pin: RX
baud_rate: 9600
logger:
baud_rate: 0 # disable logging over uart, required when using the RX/TX pins for the controller
jiecang_desk_controller:
id: my_desk
sensors:
height:
name: "Height"
numbers:
height:
name: "Height"
buttons:
move_up:
name: "Move up"
move_down:
name: "Move down"
stop:
name: "Stop"
position1:
name: "Position 1"
button:
- platform: template
name: "Step down"
on_press:
lambda: "id(my_desk).goto_height(84.0);"
```
### Examples
* basic, sufficient for most people: [example_basic.yaml](example_basic.yaml)
* full with all features in use: [example_full.yaml](example_full.yaml)
### Features
#### sensors
sensor | description
-----------|----------------------------
height | current height of the desk
height_pct | height in percent
height_min | minimal height
height_max | maximal height
position1 | 1st stored height
position2 | 2nd stored height
position3 | 3rd stored height
position4 | 4th stored height
#### number entities
Hold current values read from the desk and set values to desk when changed.
sensor | description
-----------|----------------------------
height | current height of the desk
#### buttons
button | description
--------------|---------------------------
step_up | move up desk by one step (~14mm)
step_down | move down desk by one step (~14mm)
move_up | move up until stopped
move_down | move down until stopped
stop | stop movement of desk
position1 | move to 1st stored height
position2 | move to 2nd stored height
position3 | move to 3rd stored height
position4 | move to 4th stored height
save_position | press position button afterwards to store current height to that position
#### lambda methods
lambda method | description
----------------------------------|---------------------------
`id(my_desk).step_up()` | step_up desk by one step (~14mm)
`id(my_desk).step_down()` | step_down desk by one step (~14mm)
`id(my_desk).move_up()` | move up
`id(my_desk).move_down()` | move down
`id(my_desk).stop()` | stop movement of desk
`id(my_desk).goto_position(pos)` | move to stored height `pos`
`id(my_desk).save_position(pos)` | save current height to position `pos`
`id(my_desk).goto_height(height)` | move to specified height
## Case
You can find a 3D-printable case in the [stl folder](stl/) or on [onshape](https://cad.onshape.com/documents/9619b6e4e11b26a3e9d82630/w/ca8259951d8b6bb3513992f7/e/2aa1144a31e5b4c252a13681?renderMode=0&uiState=665987de1c1f4a6c5d2d97a4).
## Sources
Thanks to [phord/Jarvis](https://github.com/phord/Jarvis) for reverse engineering the UART interface and most control messages!
Thanks to [OkhammahkO](https://github.com/OkhammahkO) for collecting the scattered information at [pimp-my-desk/desk-control](https://gitlab.com/pimp-my-desk/desk-control) and for his work in the home assistant community!

View File

@ -1,174 +0,0 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import pins
from esphome.components import uart, sensor, button, number
from esphome.const import CONF_ID, CONF_HEIGHT, CONF_UNIT_OF_MEASUREMENT, CONF_ACCURACY_DECIMALS, UNIT_CENTIMETER, UNIT_PERCENT
DEPENDENCIES = ['uart']
AUTO_LOAD = ['sensor', 'button', 'number']
jiecang_desk_controller_ns = cg.esphome_ns.namespace('jiecang_desk_controller')
JiecangDeskController = jiecang_desk_controller_ns.class_('JiecangDeskController', cg.Component, uart.UARTDevice)
JiecangDeskButton = jiecang_desk_controller_ns.class_('JiecangDeskButton', button.Button, cg.Component)
JiecangDeskNumber = jiecang_desk_controller_ns.class_('JiecangDeskNumber', number.Number, cg.Component)
CONF_SENSORS = "sensors"
CONF_BUTTONS = "buttons"
CONF_NUMBERS = "numbers"
CONF_UNIT = "unit"
CONF_STEP_UP = "step_up"
CONF_STEP_DOWN = "step_down"
CONF_STOP = "stop"
CONF_HEIGHT_MIN = "height_min"
CONF_HEIGHT_MAX = "height_max"
CONF_HEIGHT_PCT = "height_pct"
CONF_POSITION1 = "position1"
CONF_POSITION2 = "position2"
CONF_POSITION3 = "position3"
CONF_POSITION4 = "position4"
CONF_SAVE_POSITION = "save_position"
CONF_MOVE_UP = "move_up"
CONF_MOVE_DOWN = "move_down"
button_constants = {}
button_constants[CONF_STEP_UP] = 0
button_constants[CONF_STEP_DOWN] = 1
button_constants[CONF_STOP] = 2
button_constants[CONF_POSITION1] = 3
button_constants[CONF_POSITION2] = 4
button_constants[CONF_POSITION3] = 5
button_constants[CONF_POSITION4] = 6
button_constants[CONF_SAVE_POSITION] = 7
button_constants[CONF_MOVE_UP] = 8
button_constants[CONF_MOVE_DOWN] = 9
# Backward compatibility
CONF_RAISE = "raise"
CONF_LOWER = "lower"
button_constants[CONF_RAISE] = button_constants[CONF_STEP_UP]
button_constants[CONF_LOWER] = button_constants[CONF_STEP_DOWN]
number_constants = {}
number_constants[CONF_HEIGHT] = 0
number_constants[CONF_HEIGHT_PCT] = 1
CONFIG_SCHEMA = cv.COMPONENT_SCHEMA.extend({
cv.GenerateID(): cv.declare_id(JiecangDeskController),
cv.Optional(CONF_SENSORS): cv.Schema({
cv.Optional(CONF_HEIGHT): sensor.sensor_schema(
accuracy_decimals = 1,
unit_of_measurement = UNIT_CENTIMETER
),
cv.Optional(CONF_UNIT): sensor.sensor_schema(
accuracy_decimals = 0
),
cv.Optional(CONF_HEIGHT_PCT): sensor.sensor_schema(
accuracy_decimals = 1,
unit_of_measurement = UNIT_PERCENT
),
cv.Optional(CONF_HEIGHT_MIN): sensor.sensor_schema(
accuracy_decimals = 1,
unit_of_measurement = UNIT_CENTIMETER
),
cv.Optional(CONF_HEIGHT_MAX): sensor.sensor_schema(
accuracy_decimals = 1,
unit_of_measurement = UNIT_CENTIMETER
),
cv.Optional(CONF_POSITION1): sensor.sensor_schema(
accuracy_decimals = 1,
unit_of_measurement = UNIT_CENTIMETER
),
cv.Optional(CONF_POSITION2): sensor.sensor_schema(
accuracy_decimals = 1,
unit_of_measurement = UNIT_CENTIMETER
),
cv.Optional(CONF_POSITION3): sensor.sensor_schema(
accuracy_decimals = 1,
unit_of_measurement = UNIT_CENTIMETER
),
cv.Optional(CONF_POSITION4): sensor.sensor_schema(
accuracy_decimals = 1,
unit_of_measurement = UNIT_CENTIMETER
),
}),
cv.Optional(CONF_NUMBERS): cv.Schema({
cv.Optional(CONF_HEIGHT): number.NUMBER_SCHEMA.extend({
cv.GenerateID(): cv.declare_id(JiecangDeskNumber),
cv.Optional(CONF_ACCURACY_DECIMALS, default=1): cv.int_,
cv.Optional(CONF_UNIT_OF_MEASUREMENT, default=UNIT_CENTIMETER): cv.string,
}),
cv.Optional(CONF_HEIGHT_PCT): number.NUMBER_SCHEMA.extend({
cv.GenerateID(): cv.declare_id(JiecangDeskNumber),
cv.Optional(CONF_ACCURACY_DECIMALS, default=1): cv.int_,
cv.Optional(CONF_UNIT_OF_MEASUREMENT, default=UNIT_PERCENT): cv.string,
}),
}),
cv.Optional(CONF_BUTTONS): cv.Schema({
cv.Optional(CONF_STEP_UP): button.BUTTON_SCHEMA.extend({cv.GenerateID(): cv.declare_id(JiecangDeskButton)}),
cv.Optional(CONF_STEP_DOWN): button.BUTTON_SCHEMA.extend({cv.GenerateID(): cv.declare_id(JiecangDeskButton)}),
cv.Optional(CONF_STOP): button.BUTTON_SCHEMA.extend({cv.GenerateID(): cv.declare_id(JiecangDeskButton)}),
cv.Optional(CONF_POSITION1): button.BUTTON_SCHEMA.extend({cv.GenerateID(): cv.declare_id(JiecangDeskButton)}),
cv.Optional(CONF_POSITION2): button.BUTTON_SCHEMA.extend({cv.GenerateID(): cv.declare_id(JiecangDeskButton)}),
cv.Optional(CONF_POSITION3): button.BUTTON_SCHEMA.extend({cv.GenerateID(): cv.declare_id(JiecangDeskButton)}),
cv.Optional(CONF_POSITION4): button.BUTTON_SCHEMA.extend({cv.GenerateID(): cv.declare_id(JiecangDeskButton)}),
cv.Optional(CONF_SAVE_POSITION): button.BUTTON_SCHEMA.extend({cv.GenerateID(): cv.declare_id(JiecangDeskButton)}),
cv.Optional(CONF_MOVE_UP): button.BUTTON_SCHEMA.extend({cv.GenerateID(): cv.declare_id(JiecangDeskButton)}),
cv.Optional(CONF_MOVE_DOWN): button.BUTTON_SCHEMA.extend({cv.GenerateID(): cv.declare_id(JiecangDeskButton)}),
# Backward compatibility
cv.Optional(CONF_RAISE): button.BUTTON_SCHEMA.extend({cv.GenerateID(): cv.declare_id(JiecangDeskButton)}),
cv.Optional(CONF_LOWER): button.BUTTON_SCHEMA.extend({cv.GenerateID(): cv.declare_id(JiecangDeskButton)}),
}),
}).extend(uart.UART_DEVICE_SCHEMA)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
if CONF_SENSORS in config:
sensors = config[CONF_SENSORS]
if CONF_HEIGHT in sensors:
sens = await sensor.new_sensor(sensors[CONF_HEIGHT])
cg.add(var.set_sensor_height(sens))
if CONF_UNIT in sensors:
sens = await sensor.new_sensor(sensors[CONF_UNIT])
cg.add(var.set_sensor_unit(sens))
if CONF_HEIGHT_MIN in sensors:
sens = await sensor.new_sensor(sensors[CONF_HEIGHT_MIN])
cg.add(var.set_sensor_height_min(sens))
if CONF_HEIGHT_MAX in sensors:
sens = await sensor.new_sensor(sensors[CONF_HEIGHT_MAX])
cg.add(var.set_sensor_height_max(sens))
if CONF_HEIGHT_PCT in sensors:
sens = await sensor.new_sensor(sensors[CONF_HEIGHT_PCT])
cg.add(var.set_sensor_height_pct(sens))
if CONF_POSITION1 in sensors:
sens = await sensor.new_sensor(sensors[CONF_POSITION1])
cg.add(var.set_sensor_position1(sens))
if CONF_POSITION2 in sensors:
sens = await sensor.new_sensor(sensors[CONF_POSITION2])
cg.add(var.set_sensor_position2(sens))
if CONF_POSITION3 in sensors:
sens = await sensor.new_sensor(sensors[CONF_POSITION3])
cg.add(var.set_sensor_position3(sens))
if CONF_POSITION4 in sensors:
sens = await sensor.new_sensor(sensors[CONF_POSITION4])
cg.add(var.set_sensor_position4(sens))
if CONF_BUTTONS in config:
buttons = config[CONF_BUTTONS]
for button_type in buttons.keys():
btn = await button.new_button(buttons[button_type])
cg.add(var.add_button(btn, button_constants[button_type]))
if CONF_NUMBERS in config:
numbers = config[CONF_NUMBERS]
for number_type in numbers.keys():
num = await number.new_number(numbers[number_type], min_value=0, max_value=100, step=.1)
cg.add(var.add_number(num, number_constants[number_type]))

View File

@ -1,314 +0,0 @@
#include "jiecang_desk_controller.h"
#include "esphome/core/log.h"
namespace esphome {
namespace jiecang_desk_controller {
static const char *const TAG = "jiecang_desk_controller";
float JiecangDeskController::byte2float(int high, int low) {
return static_cast<float>((high<<8) + low)/10;
}
bool JiecangDeskController::bufferMessage(int data, unsigned int *buffer, int len) {
// This is a very simple method of receiving messages from the desk.
// It will fail on messages that contain the value 0x7E in their payload.
// But it is super simple and works for the messages we care about.
// format: 0xF2 0xF2 [command] [param_count] [[param] ...] [checksum] 0x7E
// checksum: sum of [command], [param_count] and all [param]s
static int cmd_incoming = 0; // 0: wait for F2, 1: wait for 2nd F2, 2: buffer data
static int pos = 0;
if (cmd_incoming < 2 && data == 0xF2) { // start of message, must appear twice
cmd_incoming++;
pos = 0;
} else if (cmd_incoming == 1) { // no second F2 received
cmd_incoming = 0;
} else if (cmd_incoming == 2) {
if (data == 0x7E) { // end of message
cmd_incoming = 0;
for (;pos<len-1;pos++) { // fill rest of buffer with zeros
buffer[pos]=0;
}
return true;
} else if (pos >= len) { // message too long, drop it
cmd_incoming = 0;
} else {
buffer[pos++] = data; // buffer data
}
} // else: received garbage
return false;
}
void JiecangDeskController::handleMessage(unsigned int *message) {
// ESP_LOGV("jiecang_desk_controller", "message %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X", message[0], message[1], message[2], message[3], message[4], message[5], message[6], message[7], message[8], message[9]);
switch (message[0]) {
case 0x01:
ESP_LOGV("jiecang_desk_controller", "height 0x%0X%0X", message[2], message[3]);
float new_height;
new_height = byte2float(message[2], message[3]);
if (new_height == current_height) return;
current_height = new_height;
if (height != nullptr) height->publish_state(current_height);
if (number_height != nullptr) number_height->publish_state(current_height);
if (height_pct != nullptr && limit_max != 0)
height_pct->publish_state((current_height - limit_min) / (limit_max - limit_min) * 100);
if (number_height_pct != nullptr && limit_max != 0)
number_height_pct->publish_state(roundf((current_height - limit_min) / (limit_max - limit_min) * 1000)/10);
break;
case 0x20:
ESP_LOGV("jiecang_desk_controller", "limits 0x%0X max %i min %i", message[2], (message[2] & 1), (message[2]>>4));
if ((message[2] & 1) == 0) { // low nibble 0 -> no max limit, use physical_max
limit_max = physical_max;
if (height_max != nullptr) height_max->publish_state(limit_max);
if (number_height != nullptr) number_height->set_max_value(limit_max);
}
if ((message[2]>>4) == 0) { // high nibble 0 -> no min limit, use physical_min
limit_min = physical_min;
if (height_min != nullptr) height_min->publish_state(limit_min);
if (number_height != nullptr) number_height->set_min_value(limit_min);
}
break;
case 0x07:
ESP_LOGV("jiecang_desk_controller", "physical limits 0x%02X%02X 0x%02X%02X", message[2], message[3], message[4], message[5]);
physical_max = byte2float(message[2], message[3]);
physical_min = byte2float(message[4], message[5]);
break;
case 0x21:
ESP_LOGV("jiecang_desk_controller", "height_max 0x%02X%02X", message[2], message[3]);
limit_max = byte2float(message[2], message[3]);
if (height_max != nullptr) height_max->publish_state(limit_max);
if (number_height != nullptr) number_height->set_max_value(limit_max);
break;
case 0x22:
ESP_LOGV("jiecang_desk_controller", "height_min 0x%02X%02X", message[2], message[3]);
limit_min = byte2float(message[2], message[3]);
if (height_min != nullptr) height_min->publish_state(limit_min);
if (number_height != nullptr) number_height->set_min_value(limit_min);
break;
case 0x25:
ESP_LOGV("jiecang_desk_controller", "position1 0x%02X%02X", message[2], message[3]);
if (position1 != nullptr) position1->publish_state(byte2float(message[2], message[3]));
break;
case 0x26:
ESP_LOGV("jiecang_desk_controller", "position2 0x%02X%02X", message[2], message[3]);
if (position2 != nullptr) position2->publish_state(byte2float(message[2], message[3]));
break;
case 0x27:
ESP_LOGV("jiecang_desk_controller", "position3 0x%02X%02X", message[2], message[3]);
if (position3 != nullptr) position3->publish_state(byte2float(message[2], message[3]));
break;
case 0x28:
ESP_LOGV("jiecang_desk_controller", "position4 0x%02X%02X", message[2], message[3]);
if (position4 != nullptr) position4->publish_state(byte2float(message[2], message[3]));
break;
default:
ESP_LOGV("jiecang_desk_controller", "unknown message %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X", message[0], message[1], message[2], message[3], message[4], message[5], message[6], message[7], message[8], message[9]);
}
}
void JiecangDeskController::update() {
const int max_length = 10;
static unsigned int buffer[max_length];
while (available()) {
if(bufferMessage(read(), buffer, max_length)) {
handleMessage(buffer);
}
}
}
void JiecangDeskController::send_simple_command(unsigned char cmd) {
write_array({ 0xF1, 0xF1, cmd, 0x00, cmd, 0x7E });
}
void JiecangDeskController::add_button(button::Button *btn, int action) {
btn->add_on_press_callback([this, action]() { this->button_press_action(action); });
}
void JiecangDeskController::step_up() {
send_simple_command(0x01);
}
void JiecangDeskController::step_down() {
send_simple_command(0x02);
}
void JiecangDeskController::stop() {
send_simple_command(0x2B);
}
void JiecangDeskController::goto_position(int pos) {
switch (pos) {
case 1:
send_simple_command(0x05);
break;
case 2:
send_simple_command(0x06);
break;
case 3:
send_simple_command(0x27);
break;
case 4:
send_simple_command(0x28);
break;
}
}
void JiecangDeskController::save_position(int pos) {
switch (pos) {
case 1:
send_simple_command(0x03);
break;
case 2:
send_simple_command(0x04);
break;
case 3:
send_simple_command(0x25);
break;
case 4:
send_simple_command(0x26);
break;
}
}
void JiecangDeskController::goto_height(float height) {
unsigned char cmd = 0x1B;
unsigned char high_byte = ((int) height * 10) >> 8;
unsigned char low_byte = ((int) height * 10) & 0xFF;
unsigned char checksum = cmd + 2 + high_byte + low_byte;
write_array({ 0xF1, 0xF1, cmd, 0x02, high_byte, low_byte, checksum, 0x7E });
}
void JiecangDeskController::move_up() {
goto_height(limit_max);
}
void JiecangDeskController::move_down() {
goto_height(limit_min);
}
void JiecangDeskController::request_physical_limits() {
send_simple_command(0x0C);
}
void JiecangDeskController::request_limits() {
send_simple_command(0x20);
}
void JiecangDeskController::request_settings() {
send_simple_command(0x07);
}
void JiecangDeskController::button_press_action(int action) {
ESP_LOGV("JiecangDeskController", "button_press_action %i", action);
switch (action) {
case BUTTON_STEP_UP:
step_up();
break;
case BUTTON_STEP_DOWN:
step_down();
break;
case BUTTON_STOP:
stop();
save_position_mode = false;
break;
case BUTTON_MOVE_UP:
move_up();
break;
case BUTTON_MOVE_DOWN:
move_down();
break;
case BUTTON_POSITION1:
if (save_position_mode) {
save_position_mode = false;
save_position(1);
return;
}
goto_position(1);
break;
case BUTTON_POSITION2:
if (save_position_mode) {
save_position_mode = false;
save_position(2);
return;
}
goto_position(2);
break;
case BUTTON_POSITION3:
if (save_position_mode) {
save_position_mode = false;
save_position(3);
return;
}
goto_position(3);
break;
case BUTTON_POSITION4:
if (save_position_mode) {
save_position_mode = false;
save_position(4);
return;
}
goto_position(4);
break;
case BUTTON_SAVE_POSITION:
save_position_mode = true;
break;
}
}
void JiecangDeskController::add_number(JiecangDeskNumber *number, int type) {
switch (type) {
case NUMBER_HEIGHT:
number_height = number;
break;
case NUMBER_HEIGHT_PCT:
number_height_pct = number;
number_height_pct->set_min_value(0);
number_height_pct->set_max_value(100);
break;
default:
return;
}
number->set_type(type);
number->set_parent(this);
}
void JiecangDeskController::number_control(int type, float value) {
ESP_LOGV("JiecangDeskController", "number_control %i", type);
switch (type) {
case NUMBER_HEIGHT:
goto_height(value);
break;
case NUMBER_HEIGHT_PCT:
if (limit_max != 0) goto_height((limit_max - limit_min) * value / 100 + limit_min);
break;
}
}
void JiecangDeskButton::press_action() {}
} //namespace jiecang_desk_controller
} //namespace esphome

View File

@ -1,98 +0,0 @@
#pragma once
#include <future>
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/button/button.h"
#include "esphome/components/uart/uart.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "jiecang_desk_number.h"
#define BUTTON_STEP_UP 0
#define BUTTON_STEP_DOWN 1
#define BUTTON_STOP 2
#define BUTTON_POSITION1 3
#define BUTTON_POSITION2 4
#define BUTTON_POSITION3 5
#define BUTTON_POSITION4 6
#define BUTTON_SAVE_POSITION 7
#define BUTTON_MOVE_UP 8
#define BUTTON_MOVE_DOWN 9
#define NUMBER_HEIGHT 0
#define NUMBER_HEIGHT_PCT 1
namespace esphome {
namespace jiecang_desk_controller {
class JiecangDeskController : public PollingComponent, public sensor::Sensor, public uart::UARTDevice {
private:
float current_height = 0;
float limit_min = 0;
float limit_max = 0;
float physical_min = 0;
float physical_max = 0;
bool save_position_mode = false;
float byte2float(int high, int low);
bool bufferMessage(int data, unsigned int *buffer, int len);
void handleMessage(unsigned int *message);
public:
void update() override;
void set_sensor_height(sensor::Sensor *sensor) { this->height = sensor; }
void set_sensor_height_min(sensor::Sensor *sensor) { this->height_min = sensor; }
void set_sensor_height_max(sensor::Sensor *sensor) { this->height_max = sensor; }
void set_sensor_height_pct(sensor::Sensor *sensor) { this->height_pct = sensor; }
void set_sensor_position1(sensor::Sensor *sensor) { this->position1 = sensor; }
void set_sensor_position2(sensor::Sensor *sensor) { this->position2 = sensor; }
void set_sensor_position3(sensor::Sensor *sensor) { this->position3 = sensor; }
void set_sensor_position4(sensor::Sensor *sensor) { this->position4 = sensor; }
void send_simple_command(unsigned char cmd);
void add_button(button::Button *btn, int action);
void add_number(JiecangDeskNumber *number, int type);
void step_up();
void step_down();
void stop();
void goto_position(int pos);
void save_position(int pos);
void goto_height(float height);
void move_up();
void move_down();
void request_physical_limits();
void request_limits();
void request_settings();
void number_control(int type, float value);
protected:
Sensor *height{nullptr};
Sensor *height_min{nullptr};
Sensor *height_max{nullptr};
Sensor *height_pct{nullptr};
Sensor *position1{nullptr};
Sensor *position2{nullptr};
Sensor *position3{nullptr};
Sensor *position4{nullptr};
JiecangDeskNumber *number_height{nullptr};
JiecangDeskNumber *number_height_pct{nullptr};
void button_press_action(int type);
};
class JiecangDeskButton : public Component, public button::Button {
protected:
void press_action() override;
};
} //namespace jiecang_desk_controller
} //namespace esphome

View File

@ -1,32 +0,0 @@
#include "jiecang_desk_number.h"
#include "jiecang_desk_controller.h"
#include "esphome/core/log.h"
namespace esphome {
namespace jiecang_desk_controller {
static const char *const TAG = "jiecang_desk.number";
void JiecangDeskNumber::setup() {}
void JiecangDeskNumber::dump_config() {
LOG_NUMBER("", "JiecangDeskNumber", this);
ESP_LOGCONFIG(TAG, " type %i", type);
}
void JiecangDeskNumber::control(float value) {
this->publish_state(value);
parent->number_control(this->type, value);
}
void JiecangDeskNumber::set_min_value(float value) {
traits.set_min_value(value);
}
void JiecangDeskNumber::set_max_value(float value) {
traits.set_max_value(value);
}
} // namespace jiecang_desk_controller
} // namespace esphome

View File

@ -1,33 +0,0 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/number/number.h"
#include "esphome/core/log.h"
namespace esphome {
namespace jiecang_desk_controller {
class JiecangDeskController;
class JiecangDeskNumber : public number::Number, public Component {
private:
int type;
JiecangDeskController *parent;
public:
void setup() override;
void dump_config() override;
void set_parent(JiecangDeskController *parent) { this->parent = parent; }
void set_type(int type) { this->type = type; }
void set_min_value(float value);
void set_max_value(float value);
protected:
void control(float value) override;
};
} // namespace jiecang_desk_controller
} // namespace esphome

View File

@ -0,0 +1,198 @@
# ESPHome Maidsite Desk Controller
[ESPHome](https://esphome.io/) component for controlling Maidsite desk controllers via their serial protocol.
![](https://github.com/Rocka84/esphome_components/assets/2353088/b04d1fd7-d0ed-443d-87b7-87454757c0f0)
Attention: I only have one [RJ12 model](https://www.maidsite.com/product/jcb35m11c.html) to test this
but it's likely that other Maidsite controllers are supported as their serial protocol should be compatible.
## Usage
### What you need
* ESPHome compatible microcontroller
* depending on your model of desk controller
* cable with RJ12 connector (phone cable, RJ11 may also work)
* cable with RJ45 connector (network cable)
### Wiring
#### RJ12
Please double check this for your specific model!
pin | function
----|---------
1 | NC (pulled up)
2 | GND
3 | TX
4 | VCC
5 | RX
6 | NC (pulled up)
#### RJ45
**Untested** and only for reference!
pin | function
----|---------
1 | HS3 [^1]
2 | TX
3 | GND
4 | RX
5 | VCC
6 | HS2 [^1]
7 | HS1 [^1]
8 | HS0 [^1]
[^1]: not used here
#### microcontroller
ESP | desk
----|-----
GND | GND
5V | VCC
RX | TX
TX | RX
![](https://github.com/Rocka84/esphome_components/assets/2353088/39e08774-a215-4e7e-8345-a2acafce28a2)
## Usage
```yaml
esphome:
...
on_boot:
# This script is required to initialize the following sensors:
# height_pct, height_min, height_max, position_m1 - position_m4
# You can skip this if you don't use those.
priority: 0 # when mostly everything else is done
then:
- lambda: "id(my_desk).request_physical_limits();"
- delay: 0.1s
- lambda: "id(my_desk).request_limits();"
- delay: 0.1s
- lambda: "id(my_desk).request_settings();"
external_components:
- source:
type: git
url: https://github.com/ElVit/esphome_maidsite_desk/
components: [ maidsite_desk_controller ]
uart:
- id: uart_bus
tx_pin: $pin_tx
rx_pin: $pin_rx
baud_rate: 9600
logger:
baud_rate: 0 # disable logging over uart, required when using the RX/TX pins for the controller
maidsite_desk_controller:
id: my_desk
sensors:
height_abs:
name: "Height"
numbers:
height_abs:
name: "Height"
mode: SLIDER
buttons:
stop:
name: "Stop"
step_up:
name: "Step up"
step_down:
name: "Step down"
goto_m1:
name: "Move to M1"
goto_m2:
name: "Move to M2"
goto_m3:
name: "Move to M3"
goto_m4:
name: "Move to M4"
button:
- platform: template
name: "Step down"
on_press:
lambda: "id(my_desk).goto_height(84.0);"
```
### Examples
* basic, sufficient for most people: [example_basic.yaml](example_basic.yaml)
* full with all features in use: [example_full.yaml](example_full.yaml)
### Features
#### sensors
sensor | description
-------------|----------------------------
height_abs | current height of the desk
height_pct | height in percent
height_min | minimal height
height_max | maximal height
position_m1 | 1st stored height
position_m2 | 2nd stored height
position_m3 | 3rd stored height
position_m4 | 4th stored height
#### number entities
Hold current values read from the desk and set values to desk when changed.
sensor | description
------------|----------------------------
height_abs | current height of the desk
height_pct | height in percent
#### buttons
button | description
-----------|---------------------------
step_up | move up desk by one step (~14mm)
step_down | move down desk by one step (~14mm)
goto_max | move up until stopped
goto_min | move down until stopped
stop | stop movement of desk
goto_m1 | move to 1st stored height
goto_m2 | move to 2nd stored height
goto_m3 | move to 3rd stored height
goto_m4 | move to 4th stored height
save_m1 | save current height to 1th position
save_m2 | save current height to 2th position
save_m3 | save current height to 3th position
save_m4 | save current height to 4th position
#### lambda methods
lambda method | description
--------------------------------------|---------------------------
`id(my_desk).step_up()` | step_up desk by one step (~14mm)
`id(my_desk).step_down()` | step_down desk by one step (~14mm)
`id(my_desk).goto_max_position()` | move up
`id(my_desk).goto_min_position()` | move down
`id(my_desk).stop()` | stop movement of desk
`id(my_desk).goto_mem_position(pos)` | move to stored height `pos`
`id(my_desk).save_mem_position(pos)` | save current height to position `pos`
`id(my_desk).goto_height(height)` | move to specified height
## Case
You can find a 3D-printable case in the [stl folder](stl/) or on [onshape](https://cad.onshape.com/documents/9619b6e4e11b26a3e9d82630/w/ca8259951d8b6bb3513992f7/e/2aa1144a31e5b4c252a13681?renderMode=0&uiState=665987de1c1f4a6c5d2d97a4).
## Sources
Thanks to [Rocka84](https://github.com/Rocka84/esphome_components)
Thanks to [phord/Jarvis](https://github.com/phord/Jarvis) for reverse engineering the UART interface and most control messages!
Thanks to [OkhammahkO](https://github.com/OkhammahkO) for collecting the scattered information at [pimp-my-desk/desk-control](https://gitlab.com/pimp-my-desk/desk-control) and for his work in the home assistant community!

View File

@ -0,0 +1,184 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import pins
from esphome.components import uart, sensor, button, number
from esphome.const import CONF_ID, CONF_UNIT_OF_MEASUREMENT, CONF_STEP, CONF_ACCURACY_DECIMALS, CONF_ENTITY_CATEGORY, UNIT_CENTIMETER, UNIT_PERCENT, ENTITY_CATEGORY_CONFIG
DEPENDENCIES = ['uart']
AUTO_LOAD = ['sensor', 'button', 'number']
maidsite_desk_controller_ns = cg.esphome_ns.namespace('maidsite_desk_controller')
MaidsiteDeskController = maidsite_desk_controller_ns.class_('MaidsiteDeskController', cg.Component, uart.UARTDevice)
MaidsiteDeskButton = maidsite_desk_controller_ns.class_('MaidsiteDeskButton', button.Button, cg.Component)
MaidsiteDeskNumber = maidsite_desk_controller_ns.class_('MaidsiteDeskNumber', number.Number, cg.Component)
CONF_SENSORS = "sensors"
CONF_BUTTONS = "buttons"
CONF_NUMBERS = "numbers"
CONF_UNIT = "unit"
CONF_STEP_UP = "step_up"
CONF_STEP_DOWN = "step_down"
CONF_STOP = "stop"
CONF_GOTO_MAX = "goto_max"
CONF_GOTO_MIN = "goto_min"
CONF_HEIGHT_MIN = "height_min"
CONF_HEIGHT_MAX = "height_max"
CONF_HEIGHT_ABS = "height_abs"
CONF_HEIGHT_PCT = "height_pct"
CONF_POSITION_M1 = "position_m1"
CONF_POSITION_M2 = "position_m2"
CONF_POSITION_M3 = "position_m3"
CONF_POSITION_M4 = "position_m4"
CONF_GOTO_M1 = "goto_m1"
CONF_GOTO_M2 = "goto_m2"
CONF_GOTO_M3 = "goto_m3"
CONF_GOTO_M4 = "goto_m4"
CONF_SAVE_M1 = "save_m1"
CONF_SAVE_M2 = "save_m2"
CONF_SAVE_M3 = "save_m3"
CONF_SAVE_M4 = "save_m4"
button_constants = {}
button_constants[CONF_STEP_UP] = 0
button_constants[CONF_STEP_DOWN] = 1
button_constants[CONF_STOP] = 2
button_constants[CONF_GOTO_MAX] = 3
button_constants[CONF_GOTO_MIN] = 4
button_constants[CONF_GOTO_M1] = 5
button_constants[CONF_GOTO_M2] = 6
button_constants[CONF_GOTO_M3] = 7
button_constants[CONF_GOTO_M4] = 8
button_constants[CONF_SAVE_M1] = 9
button_constants[CONF_SAVE_M2] = 10
button_constants[CONF_SAVE_M3] = 11
button_constants[CONF_SAVE_M4] = 12
number_constants = {}
number_constants[CONF_HEIGHT_ABS] = 0
number_constants[CONF_HEIGHT_PCT] = 1
CONFIG_SCHEMA = cv.COMPONENT_SCHEMA.extend({
cv.GenerateID(): cv.declare_id(MaidsiteDeskController),
cv.Optional(CONF_SENSORS): cv.Schema({
cv.Optional(CONF_UNIT): sensor.sensor_schema(accuracy_decimals = 0),
cv.Optional(CONF_HEIGHT_ABS): sensor.sensor_schema(accuracy_decimals = 1, unit_of_measurement = UNIT_CENTIMETER),
cv.Optional(CONF_HEIGHT_PCT): sensor.sensor_schema(accuracy_decimals = 1, unit_of_measurement = UNIT_PERCENT),
cv.Optional(CONF_HEIGHT_MIN): sensor.sensor_schema(accuracy_decimals = 1, unit_of_measurement = UNIT_CENTIMETER),
cv.Optional(CONF_HEIGHT_MAX): sensor.sensor_schema(accuracy_decimals = 1, unit_of_measurement = UNIT_CENTIMETER),
cv.Optional(CONF_POSITION_M1): sensor.sensor_schema(accuracy_decimals = 1, unit_of_measurement = UNIT_CENTIMETER),
cv.Optional(CONF_POSITION_M2): sensor.sensor_schema(accuracy_decimals = 1, unit_of_measurement = UNIT_CENTIMETER),
cv.Optional(CONF_POSITION_M3): sensor.sensor_schema(accuracy_decimals = 1, unit_of_measurement = UNIT_CENTIMETER),
cv.Optional(CONF_POSITION_M4): sensor.sensor_schema(accuracy_decimals = 1, unit_of_measurement = UNIT_CENTIMETER),
}),
cv.Optional(CONF_NUMBERS): cv.Schema({
cv.Optional(CONF_HEIGHT_ABS): number.NUMBER_SCHEMA.extend({
cv.GenerateID(): cv.declare_id(MaidsiteDeskNumber),
cv.Optional(CONF_ACCURACY_DECIMALS, default=1): cv.int_,
cv.Optional(CONF_UNIT_OF_MEASUREMENT, default=UNIT_CENTIMETER): cv.string,
cv.Optional(CONF_STEP, default=1): cv.float_,
}),
cv.Optional(CONF_HEIGHT_PCT): number.NUMBER_SCHEMA.extend({
cv.GenerateID(): cv.declare_id(MaidsiteDeskNumber),
cv.Optional(CONF_ACCURACY_DECIMALS, default=1): cv.int_,
cv.Optional(CONF_UNIT_OF_MEASUREMENT, default=UNIT_PERCENT): cv.string,
cv.Optional(CONF_STEP, default=.1): cv.float_,
}),
}),
cv.Optional(CONF_BUTTONS): cv.Schema({
cv.Optional(CONF_STEP_UP): button.BUTTON_SCHEMA.extend({
cv.GenerateID(): cv.declare_id(MaidsiteDeskButton),
}),
cv.Optional(CONF_STEP_DOWN): button.BUTTON_SCHEMA.extend({
cv.GenerateID(): cv.declare_id(MaidsiteDeskButton),
}),
cv.Optional(CONF_STOP): button.BUTTON_SCHEMA.extend({
cv.GenerateID(): cv.declare_id(MaidsiteDeskButton),
}),
cv.Optional(CONF_GOTO_MAX): button.BUTTON_SCHEMA.extend({
cv.GenerateID(): cv.declare_id(MaidsiteDeskButton),
}),
cv.Optional(CONF_GOTO_MIN): button.BUTTON_SCHEMA.extend({
cv.GenerateID(): cv.declare_id(MaidsiteDeskButton),
}),
cv.Optional(CONF_GOTO_M1): button.BUTTON_SCHEMA.extend({
cv.GenerateID(): cv.declare_id(MaidsiteDeskButton),
}),
cv.Optional(CONF_GOTO_M2): button.BUTTON_SCHEMA.extend({
cv.GenerateID(): cv.declare_id(MaidsiteDeskButton),
}),
cv.Optional(CONF_GOTO_M3): button.BUTTON_SCHEMA.extend({
cv.GenerateID(): cv.declare_id(MaidsiteDeskButton),
}),
cv.Optional(CONF_GOTO_M4): button.BUTTON_SCHEMA.extend({
cv.GenerateID(): cv.declare_id(MaidsiteDeskButton),
}),
cv.Optional(CONF_SAVE_M1): button.BUTTON_SCHEMA.extend({
cv.GenerateID(): cv.declare_id(MaidsiteDeskButton),
cv.Optional(CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_CONFIG): cv.entity_category,
}),
cv.Optional(CONF_SAVE_M2): button.BUTTON_SCHEMA.extend({
cv.GenerateID(): cv.declare_id(MaidsiteDeskButton),
cv.Optional(CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_CONFIG): cv.entity_category,
}),
cv.Optional(CONF_SAVE_M3): button.BUTTON_SCHEMA.extend({
cv.GenerateID(): cv.declare_id(MaidsiteDeskButton),
cv.Optional(CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_CONFIG): cv.entity_category,
}),
cv.Optional(CONF_SAVE_M4): button.BUTTON_SCHEMA.extend({
cv.GenerateID(): cv.declare_id(MaidsiteDeskButton),
cv.Optional(CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_CONFIG): cv.entity_category,
}),
}),
}).extend(uart.UART_DEVICE_SCHEMA)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
if CONF_SENSORS in config:
sensors = config[CONF_SENSORS]
if CONF_UNIT in sensors:
sens = await sensor.new_sensor(sensors[CONF_UNIT])
cg.add(var.set_sensor_unit(sens))
if CONF_HEIGHT_MIN in sensors:
sens = await sensor.new_sensor(sensors[CONF_HEIGHT_MIN])
cg.add(var.set_sensor_height_min(sens))
if CONF_HEIGHT_MAX in sensors:
sens = await sensor.new_sensor(sensors[CONF_HEIGHT_MAX])
cg.add(var.set_sensor_height_max(sens))
if CONF_HEIGHT_ABS in sensors:
sens = await sensor.new_sensor(sensors[CONF_HEIGHT_ABS])
cg.add(var.set_sensor_height(sens))
if CONF_HEIGHT_PCT in sensors:
sens = await sensor.new_sensor(sensors[CONF_HEIGHT_PCT])
cg.add(var.set_sensor_height_pct(sens))
if CONF_POSITION_M1 in sensors:
sens = await sensor.new_sensor(sensors[CONF_POSITION_M1])
cg.add(var.set_sensor_position_m1(sens))
if CONF_POSITION_M2 in sensors:
sens = await sensor.new_sensor(sensors[CONF_POSITION_M2])
cg.add(var.set_sensor_position_m2(sens))
if CONF_POSITION_M3 in sensors:
sens = await sensor.new_sensor(sensors[CONF_POSITION_M3])
cg.add(var.set_sensor_position_m3(sens))
if CONF_POSITION_M4 in sensors:
sens = await sensor.new_sensor(sensors[CONF_POSITION_M4])
cg.add(var.set_sensor_position_m4(sens))
if CONF_BUTTONS in config:
buttons = config[CONF_BUTTONS]
for button_type in buttons.keys():
btn = await button.new_button(buttons[button_type])
cg.add(var.add_button(btn, button_constants[button_type]))
if CONF_NUMBERS in config:
numbers = config[CONF_NUMBERS]
for number_type in numbers.keys():
num = await number.new_number(numbers[number_type], min_value=0, max_value=100, step=.1)
cg.add(var.add_number(num, number_constants[number_type]))

View File

@ -1,6 +1,6 @@
esphome:
name: jiecang-desk-controller # change this!
friendly_name: Jiecang Desk Controller # change this!
name: maidsite-desk-controller # change this!
friendly_name: Maidsite Desk Controller # change this!
on_boot:
# don't touch if you don't know what you're doing!
priority: 0 # when mostly everything else is done
@ -14,43 +14,47 @@ esphome:
external_components:
- source:
type: git
url: https://github.com/Rocka84/esphome_components/
components: [ jiecang_desk_controller ]
url: https://github.com/ElVit/esphome_maidsite_desk/
components: [ maidsite_desk_controller ]
uart:
id: uart_bus
tx_pin: TX
rx_pin: RX
baud_rate: 9600
- id: uart_bus
tx_pin: $pin_tx
rx_pin: $pin_rx
baud_rate: 9600
logger:
baud_rate: 0 # disable logging over uart, required when using the RX/TX pins for the controller
# see full example for more options: https://github.com/Rocka84/esphome_components/blob/master/components/jiecang_desk_controller/example_full.yaml
jiecang_desk_controller:
# see full example for more options: https://github.com/Rocka84/esphome_components/blob/master/components/maidsite_desk_controller/example_full.yaml
maidsite_desk_controller:
id: my_desk
sensors:
height_abs:
name: "Height"
buttons:
move_up:
name: "Move up"
move_down:
name: "Move down"
stop:
name: "Stop"
step_up:
name: "Step up"
step_down:
name: "Step down"
position1:
name: "Position 1"
position2:
name: "Position 2"
position3:
name: "Position 3"
position4:
name: "Position 4"
goto_max:
name: "Move to MAX"
goto_min:
name: "Move to MIN"
goto_m1:
name: "Move to M1"
goto_m2:
name: "Move to M2"
goto_m3:
name: "Move to M3"
goto_m4:
name: "Move to M4"
numbers:
height:
height_abs:
name: "Height"
mode: SLIDER
# the usual stuff
@ -70,3 +74,4 @@ api:
key: !secret encryption_key
ota:
logger:

View File

@ -1,9 +1,9 @@
esphome:
name: jiecang-desk-controller
friendly_name: Jiecang Desk Controller
name: maidsite-desk-controller
friendly_name: Maidsite Desk Controller
on_boot:
# This script is required to initialize the following sensors:
# height_pct, height_min, height_max, position1 - position4
# height_pct, height_min, height_max, position_m1 - position_m4
# You can skip this if you don't use those.
priority: 0 # when mostly everything else is done
then:
@ -16,69 +16,79 @@ esphome:
external_components:
- source:
type: local
path: rocka84_esphome_components/components/
path: elvit_esphome_maidsite_desk/components/
# type: git
# url: https://github.com/Rocka84/esphome_components/
components: [ jiecang_desk_controller ]
# url: https://github.com/ElVit/esphome_maidsite_desk/
components: [ maidsite_desk_controller ]
uart:
id: uart_bus
tx_pin: TX
rx_pin: RX
baud_rate: 9600
- id: uart_bus
tx_pin: $pin_tx
rx_pin: $pin_rx
baud_rate: 9600
# debug:
# direction: BOTH
# after:
# bytes: 9
# sequence:
# - lambda: UARTDebug::log_hex(direction, bytes, ',');
logger:
baud_rate: 0 # disable logging over uart, required when using the RX/TX pins for the controller
jiecang_desk_controller:
maidsite_desk_controller:
id: my_desk
sensors:
height:
name: "Height"
height_abs:
name: "Height absolut"
height_pct:
name: "Height percent"
height_min:
name: "Height Min"
height_max:
name: "Height Max"
height_pct:
name: "Height Percent"
position1:
name: "Position 1"
position2:
name: "Position 2"
position3:
name: "Position 3"
position4:
name: "Position 4"
position_m1:
name: "Position M1"
position_m2:
name: "Position M2"
position_m3:
name: "Position M3"
position_m4:
name: "Position M4"
buttons:
move_up:
name: "Move up"
move_down:
name: "Move down"
stop:
name: "Stop"
step_up:
name: "Step up"
step_down:
name: "Step down"
position1:
name: "Position 1"
position2:
name: "Position 2"
position3:
name: "Position 3"
position4:
name: "Position 4"
save_position:
name: "Save Position"
move_up:
name: "Move up"
move_down:
name: "Move down"
goto_max:
name: "Move to MAX"
goto_min:
name: "Move to MIN"
goto_m1:
name: "Move to M1"
goto_m2:
name: "Move to M2"
goto_m3:
name: "Move to M3"
goto_m4:
name: "Move to M4"
save_m1:
name: "Save M1"
save_m2:
name: "Save M2"
save_m3:
name: "Save M3"
save_m4:
name: "Save M4"
numbers:
height:
name: "Height"
height_abs:
name: "Height absolut"
mode: SLIDER
height_pct:
name: "Height Percent"
name: "Height percent"
mode: SLIDER
## lambda usage
# button:
@ -97,17 +107,16 @@ jiecang_desk_controller:
# - platform: template
# name: "Position 2"
# on_press:
# lambda: "id(my_desk).goto_position(2);"
# lambda: "id(my_desk).goto_mem_position(2);"
# - platform: template
# name: "Save Position 4"
# on_press:
# lambda: "id(my_desk).save_position(4);"
# lambda: "id(my_desk).save_mem_position(4);"
# - platform: template
# name: "Go to 100cm"
# on_press:
# lambda: "id(my_desk).goto_height(100);"
# the usual stuff
esp8266:
@ -126,3 +135,4 @@ api:
key: !secret encryption_key
ota:
logger:

View File

@ -0,0 +1,381 @@
#include "maidsite_desk_controller.h"
#include "esphome/core/log.h"
namespace esphome
{
namespace maidsite_desk_controller
{
static const char *const TAG = "maidsite_desk_controller";
float MaidsiteDeskController::byte2float(int high, int low)
{
return static_cast<float>((high << 8) + low) / 10;
}
bool MaidsiteDeskController::bufferMessage(int data, unsigned int *buffer, int len)
{
// This is a very simple method of receiving messages from the desk.
// It will fail on messages that contain the value 0x7E in their payload.
// But it is super simple and works for the messages we care about.
// Read message:
// format: 0xF2 0xF2 [command] [param_count] [[param] ...] [checksum] 0x7E
// checksum: sum of [command], [param_count] and all [param]s
static int cmd_incoming = 0; // 0: wait for F2, 1: wait for 2nd F2, 2: buffer data
static int pos = 0;
if (cmd_incoming < 2 && data == 0xF2)
{ // start of message, must appear twice
cmd_incoming++;
pos = 0;
}
else if (cmd_incoming == 1)
{ // no second F2 received
cmd_incoming = 0;
}
else if (cmd_incoming == 2)
{
if (data == 0x7E)
{ // end of message
cmd_incoming = 0;
for (; pos < len - 1; pos++)
{ // fill rest of buffer with zeros
buffer[pos] = 0;
}
return true;
}
else if (pos >= len)
{ // message too long, drop it
cmd_incoming = 0;
}
else
{
buffer[pos++] = data; // buffer data
}
} // else: received garbage
return false;
}
void MaidsiteDeskController::handleMessage(unsigned int *message)
{
// ESP_LOGV("maidsite_desk_controller", "message %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X", message[0], message[1], message[2], message[3], message[4], message[5], message[6], message[7], message[8], message[9]);
switch (message[0])
{
case 0x01:
ESP_LOGV("maidsite_desk_controller", "sensor_height 0x%0X%0X", message[2], message[3]);
float new_height;
new_height = byte2float(message[2], message[3]);
if (new_height == current_height)
return;
current_height = new_height;
if (sensor_height != nullptr)
sensor_height->publish_state(current_height);
if (number_height_abs != nullptr)
number_height_abs->publish_state(current_height);
if (sensor_height_pct != nullptr && limit_max != 0)
sensor_height_pct->publish_state((current_height - limit_min) / (limit_max - limit_min) * 100);
if (number_height_pct != nullptr && limit_max != 0)
number_height_pct->publish_state(roundf((current_height - limit_min) / (limit_max - limit_min) * 1000) / 10);
break;
case 0x20:
ESP_LOGV("maidsite_desk_controller", "sensor_limits 0x%0X max %i min %i", message[2], (message[2] & 1), (message[2] >> 4));
if ((message[2] & 1) == 0)
{ // low nibble 0 -> no max limit, use physical_max
limit_max = physical_max;
if (sensor_height_max != nullptr)
sensor_height_max->publish_state(limit_max);
if (number_height_abs != nullptr)
number_height_abs->set_max_value(limit_max);
}
if ((message[2] >> 4) == 0)
{ // high nibble 0 -> no min limit, use physical_min
limit_min = physical_min;
if (sensor_height_min != nullptr)
sensor_height_min->publish_state(limit_min);
if (number_height_abs != nullptr)
number_height_abs->set_min_value(limit_min);
}
break;
case 0x07:
ESP_LOGV("maidsite_desk_controller", "sensor_physical_limits 0x%02X%02X 0x%02X%02X", message[2], message[3], message[4], message[5]);
physical_max = byte2float(message[2], message[3]);
physical_min = byte2float(message[4], message[5]);
break;
case 0x21:
ESP_LOGV("maidsite_desk_controller", "sensor_height_max 0x%02X%02X", message[2], message[3]);
limit_max = byte2float(message[2], message[3]);
if (sensor_height_max != nullptr)
sensor_height_max->publish_state(limit_max);
if (number_height_abs != nullptr)
number_height_abs->set_max_value(limit_max);
break;
case 0x22:
ESP_LOGV("maidsite_desk_controller", "sensor_height_min 0x%02X%02X", message[2], message[3]);
limit_min = byte2float(message[2], message[3]);
if (sensor_height_min != nullptr)
sensor_height_min->publish_state(limit_min);
if (number_height_abs != nullptr)
number_height_abs->set_min_value(limit_min);
break;
case 0x25:
ESP_LOGV("maidsite_desk_controller", "sensor_position_m1 0x%02X%02X", message[2], message[3]);
if (sensor_position_m1 != nullptr)
sensor_position_m1->publish_state(byte2float(message[2], message[3]));
break;
case 0x26:
ESP_LOGV("maidsite_desk_controller", "sensor_position_m2 0x%02X%02X", message[2], message[3]);
if (sensor_position_m2 != nullptr)
sensor_position_m2->publish_state(byte2float(message[2], message[3]));
break;
case 0x27:
ESP_LOGV("maidsite_desk_controller", "sensor_position_m3 0x%02X%02X", message[2], message[3]);
if (sensor_position_m3 != nullptr)
sensor_position_m3->publish_state(byte2float(message[2], message[3]));
break;
case 0x28:
ESP_LOGV("maidsite_desk_controller", "sensor_position_m4 0x%02X%02X", message[2], message[3]);
if (sensor_position_m4 != nullptr)
sensor_position_m4->publish_state(byte2float(message[2], message[3]));
break;
// case 0x0E:
// ESP_LOGV("maidsite_desk_controller", "units 0x%02X", message[2]);
// if (units != nullptr)
// units->publish_state(byte2float(message[2], message[3]));
// break;
default:
ESP_LOGV("maidsite_desk_controller", "unknown message %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X", message[0], message[1], message[2], message[3], message[4], message[5], message[6], message[7], message[8], message[9]);
}
}
void MaidsiteDeskController::update()
{
const int max_length = 10;
static unsigned int buffer[max_length];
while (available())
{
if (bufferMessage(read(), buffer, max_length))
{
handleMessage(buffer);
}
}
}
void MaidsiteDeskController::add_button(button::Button *btn, int action)
{
btn->add_on_press_callback([this, action]() { this->button_press_action(action); });
}
// Write message:
// format: 0xF1 0xF1 [command] [param_count] [[param] ...] [checksum] 0x7E
// checksum: sum of [command], [param_count] and all [param]s
void MaidsiteDeskController::send_simple_command(unsigned char cmd)
{
write_array({0xF1, 0xF1, cmd, 0x00, cmd, 0x7E});
}
void MaidsiteDeskController::send_2byte_command(unsigned char cmd, unsigned char high_byte, unsigned char low_byte)
{
unsigned char checksum = cmd + 2 + high_byte + low_byte;
write_array({ 0xF1, 0xF1, cmd, 0x02, high_byte, low_byte, checksum, 0x7E });
}
void MaidsiteDeskController::step_up()
{
send_simple_command(0x01);
}
void MaidsiteDeskController::step_down()
{
send_simple_command(0x02);
}
void MaidsiteDeskController::stop()
{
send_simple_command(0x2B);
}
void MaidsiteDeskController::goto_mem_position(int pos)
{
switch (pos)
{
case 1:
send_simple_command(0x05);
break;
case 2:
send_simple_command(0x06);
break;
case 3:
send_simple_command(0x27);
break;
case 4:
send_simple_command(0x28);
break;
}
}
void MaidsiteDeskController::save_mem_position(int pos)
{
switch (pos)
{
case 1:
send_simple_command(0x03);
break;
case 2:
send_simple_command(0x04);
break;
case 3:
send_simple_command(0x25);
break;
case 4:
send_simple_command(0x26);
break;
}
}
void MaidsiteDeskController::request_physical_limits()
{
send_simple_command(0x0C);
}
void MaidsiteDeskController::request_limits()
{
send_simple_command(0x20);
}
void MaidsiteDeskController::request_settings()
{
send_simple_command(0x07);
}
void MaidsiteDeskController::request_move_to()
{
send_simple_command(0x1B);
}
void MaidsiteDeskController::goto_height(float height)
{
unsigned char high_byte = ((int)height * 10) >> 8;
unsigned char low_byte = ((int)height * 10) & 0xFF;
send_2byte_command(0x80, high_byte, low_byte);
request_move_to();
}
void MaidsiteDeskController::goto_max_position()
{
goto_height(limit_max);
}
void MaidsiteDeskController::goto_min_position()
{
goto_height(limit_min);
}
void MaidsiteDeskController::button_press_action(int action)
{
ESP_LOGV("MaidsiteDeskController", "button_press_action %i", action);
switch (action)
{
case BUTTON_STEP_UP:
step_up();
break;
case BUTTON_STEP_DOWN:
step_down();
break;
case BUTTON_STOP:
stop();
break;
case BUTTON_GOTO_MAX:
goto_max_position();
break;
case BUTTON_GOTO_MIN:
goto_min_position();
break;
case BUTTON_GOTO_M1:
goto_mem_position(1);
break;
case BUTTON_GOTO_M2:
goto_mem_position(2);
break;
case BUTTON_GOTO_M3:
goto_mem_position(3);
break;
case BUTTON_GOTO_M4:
goto_mem_position(4);
break;
case BUTTON_SAVE_M1:
save_mem_position(1);
delay(100);
request_settings();
break;
case BUTTON_SAVE_M2:
save_mem_position(2);
delay(100);
request_settings();
break;
case BUTTON_SAVE_M3:
save_mem_position(3);
delay(100);
request_settings();
break;
case BUTTON_SAVE_M4:
save_mem_position(4);
delay(100);
request_settings();
break;
}
}
void MaidsiteDeskController::add_number(MaidsiteDeskNumber *number, int type)
{
switch (type)
{
case NUMBER_HEIGHT:
number_height_abs = number;
break;
case NUMBER_HEIGHT_PCT:
number_height_pct = number;
number_height_pct->set_min_value(0);
number_height_pct->set_max_value(100);
break;
default:
return;
}
number->set_type(type);
number->set_parent(this);
}
void MaidsiteDeskController::number_control(int type, float value)
{
ESP_LOGV("MaidsiteDeskController", "number_control %i", type);
switch (type)
{
case NUMBER_HEIGHT:
goto_height(value);
break;
case NUMBER_HEIGHT_PCT:
if (limit_max > 0 && limit_max > limit_min)
goto_height((limit_max - limit_min) * value / 100 + limit_min);
break;
}
}
void MaidsiteDeskButton::press_action() {}
} // namespace maidsite_desk_controller
} // namespace esphome

View File

@ -0,0 +1,105 @@
#pragma once
#include <future>
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/button/button.h"
#include "esphome/components/uart/uart.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "maidsite_desk_number.h"
#define BUTTON_STEP_UP 0
#define BUTTON_STEP_DOWN 1
#define BUTTON_STOP 2
#define BUTTON_GOTO_MAX 3
#define BUTTON_GOTO_MIN 4
#define BUTTON_GOTO_M1 5
#define BUTTON_GOTO_M2 6
#define BUTTON_GOTO_M3 7
#define BUTTON_GOTO_M4 8
#define BUTTON_SAVE_M1 9
#define BUTTON_SAVE_M2 10
#define BUTTON_SAVE_M3 11
#define BUTTON_SAVE_M4 12
#define NUMBER_HEIGHT 0
#define NUMBER_HEIGHT_PCT 1
namespace esphome
{
namespace maidsite_desk_controller
{
class MaidsiteDeskController : public PollingComponent, public sensor::Sensor, public uart::UARTDevice
{
private:
float current_height = 0;
float limit_min = 0;
float limit_max = 0;
float physical_min = 0;
float physical_max = 0;
float byte2float(int high, int low);
bool bufferMessage(int data, unsigned int *buffer, int len);
void handleMessage(unsigned int *message);
public:
void update() override;
void set_sensor_height(sensor::Sensor *sensor) { this->sensor_height = sensor; }
void set_sensor_height_min(sensor::Sensor *sensor) { this->sensor_height_min = sensor; }
void set_sensor_height_max(sensor::Sensor *sensor) { this->sensor_height_max = sensor; }
void set_sensor_height_pct(sensor::Sensor *sensor) { this->sensor_height_pct = sensor; }
void set_sensor_position_m1(sensor::Sensor *sensor) { this->sensor_position_m1 = sensor; }
void set_sensor_position_m2(sensor::Sensor *sensor) { this->sensor_position_m2 = sensor; }
void set_sensor_position_m3(sensor::Sensor *sensor) { this->sensor_position_m3 = sensor; }
void set_sensor_position_m4(sensor::Sensor *sensor) { this->sensor_position_m4 = sensor; }
void add_button(button::Button *btn, int action);
void add_number(MaidsiteDeskNumber *number, int type);
void send_simple_command(unsigned char cmd);
void send_2byte_command(unsigned char cmd, unsigned char high_byte, unsigned char low_byte);
void step_up();
void step_down();
void stop();
void goto_mem_position(int pos);
void save_mem_position(int pos);
void goto_height(float height);
void goto_max_position();
void goto_min_position();
void request_physical_limits();
void request_limits();
void request_settings();
void request_move_to();
void number_control(int type, float value);
protected:
Sensor *sensor_height{nullptr};
Sensor *sensor_height_min{nullptr};
Sensor *sensor_height_max{nullptr};
Sensor *sensor_height_pct{nullptr};
Sensor *sensor_position_m1{nullptr};
Sensor *sensor_position_m2{nullptr};
Sensor *sensor_position_m3{nullptr};
Sensor *sensor_position_m4{nullptr};
MaidsiteDeskNumber *number_height_abs{nullptr};
MaidsiteDeskNumber *number_height_pct{nullptr};
void button_press_action(int type);
};
class MaidsiteDeskButton : public Component, public button::Button
{
protected:
void press_action() override;
};
} // namespace maidsite_desk_controller
} // namespace esphome

View File

@ -0,0 +1,37 @@
#include "maidsite_desk_number.h"
#include "maidsite_desk_controller.h"
#include "esphome/core/log.h"
namespace esphome
{
namespace maidsite_desk_controller
{
static const char *const TAG = "maidsite_desk.number";
void MaidsiteDeskNumber::setup() {}
void MaidsiteDeskNumber::dump_config()
{
LOG_NUMBER("", "MaidsiteDeskNumber", this);
ESP_LOGCONFIG(TAG, " type %i", type);
}
void MaidsiteDeskNumber::control(float value)
{
this->publish_state(value);
parent->number_control(this->type, value);
}
void MaidsiteDeskNumber::set_min_value(float value)
{
traits.set_min_value(value);
}
void MaidsiteDeskNumber::set_max_value(float value)
{
traits.set_max_value(value);
}
} // namespace maidsite_desk_controller
} // namespace esphome

View File

@ -0,0 +1,35 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/number/number.h"
#include "esphome/core/log.h"
namespace esphome
{
namespace maidsite_desk_controller
{
class MaidsiteDeskController;
class MaidsiteDeskNumber : public number::Number, public Component
{
private:
int type;
MaidsiteDeskController *parent;
public:
void setup() override;
void dump_config() override;
void set_parent(MaidsiteDeskController *parent) { this->parent = parent; }
void set_type(int type) { this->type = type; }
void set_min_value(float value);
void set_max_value(float value);
protected:
void control(float value) override;
};
} // namespace maidsite_desk_controller
} // namespace esphome

View File

@ -1 +0,0 @@
components/jiecang_desk_controller/example_basic.yaml

View File

@ -0,0 +1 @@
components/maidsite_desk_controller/example_basic.yaml