15static constexpr uint16_t COMMAND_REG = 0x9C41;
16static constexpr uint16_t STATE_REG = 0x9CB9;
17static constexpr uint16_t BROADCAST_REG = 0x9D31;
18static constexpr float CLOSE_POSITION_THRESHOLD = 0.05f;
19static constexpr float OPEN_POSITION_THRESHOLD = 0.95f;
22static constexpr HoermannHcpCommand COMMAND_CLOSE{
"close", 0x0120};
23static constexpr HoermannHcpCommand COMMAND_IMPULSE{
"impulse", 0x0140};
24static constexpr HoermannHcpCommand COMMAND_VENT{
"vent", 0x0100, 0x4000};
25static constexpr HoermannHcpCommand COMMAND_HALF_OPEN{
"half open", 0x0100, 0x0400};
27static constexpr HoermannHcpCommand COMMAND_LIGHT_ON{
"light on", 0x0880};
28static constexpr HoermannHcpCommand COMMAND_LIGHT_OFF{
"light off", 0x0800, 0x0100};
30static constexpr HoermannHcpCommand COMMAND_STOP{
"stop", 0x0140};
34struct DoorStateMapping {
38static constexpr DoorStateMapping DOOR_STATE_MAPPINGS[] = {
47 for (uint16_t i = 0; i <
count; i++)
48 registers.push_back(0x0000);
64static bool at_destination(
const HoermannHcpCommand &command,
DoorState state) {
65 if (&command == &COMMAND_OPEN)
67 if (&command == &COMMAND_CLOSE)
69 if (&command == &COMMAND_VENT)
71 if (&command == &COMMAND_HALF_OPEN)
77static constexpr uint8_t STATUS_COMMAND = 0x03;
79static constexpr uint32_t STOP_LOCK_MS = 500;
81#ifdef USE_HOERMANN_HCP_IDENTITY
84static constexpr uint8_t ANSWER_REQUEST = 0x22;
85static constexpr uint8_t REQUEST_SERIAL = 0x05;
86static constexpr uint8_t REQUEST_FIRMWARE = 0x06;
88static constexpr uint8_t IDENTITY_MAX_ATTEMPTS = 3;
89static constexpr uint32_t IDENTITY_RETRY_MS = 30000;
92static constexpr uint8_t TRANSFER_COMMAND = 0x04;
93static constexpr uint8_t TRANSFER_SUB_SERIAL = 0x0C;
94static constexpr uint8_t TRANSFER_SUB_FIRMWARE = 0x0D;
95static constexpr uint8_t TRANSFER_ACK = 0xFD;
96static constexpr size_t TRANSFER_PAYLOAD_REG = 2;
98static constexpr uint8_t COUNTER_FIRST_HALF = 0x80;
101static constexpr size_t SERIAL_SINGLE_FRAME_REGS = 6;
102static_assert(SERIAL_SINGLE_FRAME_REGS == SERIAL_SECOND_HALF_REGS,
"one copy path serves both serial frames");
106 for (
size_t i = 0; i <
count; i++) {
107 const uint16_t value = registers[TRANSFER_PAYLOAD_REG + i];
108 out[2 * i] =
static_cast<char>(value >> 8);
109 out[2 * i + 1] =
static_cast<char>(value);
114static size_t text_length(
const char *text,
size_t len) {
116 while (at <
len && text[at] >= 0x20 && text[at] <= 0x7E)
118 while (at > 0 && text[at - 1] ==
' ')
123static void terminate_text(
char *text,
size_t len) { text[text_length(text,
len)] =
'\0'; }
126static void log_identity_value(text_sensor::TextSensor *sensor) {
127 if (sensor !=
nullptr) {
128 ESP_LOGCONFIG(TAG,
" Value: %s",
129 sensor->has_state() ? sensor->get_state().c_str() : LOG_STR_LITERAL(
"not received"));
144 ESP_LOGW(TAG,
"Bus controller did not fetch '%s' command, dropping it", this->
next_command_->
name);
151 ESP_LOGW(TAG,
"Door did not start moving towards the requested position, dropping it");
156 this->starting_ =
false;
158 ESP_LOGW(TAG,
"Door did not report moving, dropping the stop");
161 ESP_LOGD(TAG,
"Door did not start after the command");
170 ESP_LOGW(TAG,
"Door did not report the lamp changing, giving up");
172 ESP_LOGW(TAG,
"Bus controller did not fetch the lamp command, dropping it");
176#ifdef USE_HOERMANN_HCP_IDENTITY
185void HoermannHcp::dump_config() {
187 "Hoermann HCP bridge:\n"
188 " Modbus server address: 0x%02X",
190#ifdef USE_HOERMANN_HCP_IDENTITY
191 LOG_TEXT_SENSOR(
" ",
"Serial Number", this->serial_number_text_sensor_);
192 log_identity_value(this->serial_number_text_sensor_);
193 LOG_TEXT_SENSOR(
" ",
"Firmware Version", this->version_text_sensor_);
194 log_identity_value(this->version_text_sensor_);
198modbus::ResponseStatus HoermannHcp::on_read_holding_registers(uint16_t start_address, uint16_t number_of_registers,
200 if (start_address != STATE_REG) {
201 ESP_LOGW(TAG,
"Unknown read address 0x%04X", start_address);
208#ifdef USE_HOERMANN_HCP_IDENTITY
219 const uint16_t command =
static_cast<uint16_t
>((this->
command_reg_value_ & 0x00FF) << 8);
221 switch (number_of_registers) {
224 registers.push_back(counter);
225 registers.push_back(
static_cast<uint16_t
>(0x0001 | command));
226 if (status_poll &&
static_cast<uint8_t
>(this->
command_reg_value_) == STATUS_COMMAND) {
229 push_zeros(registers, 2);
231 push_zeros(registers, 4);
232#ifdef USE_HOERMANN_HCP_IDENTITY
238 registers.push_back(
static_cast<uint16_t
>(0x0004 | counter));
239 registers.push_back(command);
243 ESP_LOGD(TAG,
"Bus scan received from bus controller");
244 registers.push_back(counter);
245 registers.push_back(
static_cast<uint16_t
>(0x0005 | command));
246 registers.push_back(0x0430);
247 registers.push_back(0x10FF);
248 registers.push_back(0xA845);
251 ESP_LOGW(TAG,
"Unknown read request (read %u registers)", number_of_registers);
252 push_zeros(registers, number_of_registers);
261 if (start_address == COMMAND_REG) {
267#ifdef USE_HOERMANN_HCP_IDENTITY
273 if (start_address != BROADCAST_REG) {
275 ESP_LOGV(TAG,
"Ignoring write to address 0x%04X", start_address);
283 if (registers.size() > 2)
285 if (registers.size() > 1)
287 if (registers.size() > 6) {
295 ESP_LOGD(TAG,
"Broadcast of %u registers carries no lamp state",
static_cast<unsigned>(registers.size()));
302 if (command ==
nullptr)
304 if (command ==
nullptr) {
305 push_zeros(registers, 2);
308 ESP_LOGI(TAG,
"Sending '%s' command to door", command->
name);
315 if (command ==
nullptr)
319 if (command == &COMMAND_STOP && this->starting_ && !moving)
326 ESP_LOGD(TAG,
"Door is already moving that way, dropping '%s'", command->
name);
329 if (command != &COMMAND_STOP) {
330 ESP_LOGD(TAG,
"Door is moving, stopping it instead of '%s'", command->
name);
334 return &COMMAND_STOP;
336 if (command == &COMMAND_STOP) {
337 ESP_LOGD(TAG,
"Door came to rest before the stop was fetched, dropping it");
342 this->starting_ =
true;
356 return this->
light_target_ ? &COMMAND_LIGHT_ON : &COMMAND_LIGHT_OFF;
359#ifdef USE_HOERMANN_HCP_IDENTITY
365 if (this->serial_number_text_sensor_ !=
nullptr || this->version_text_sensor_ !=
nullptr)
372 registers[1] =
static_cast<uint16_t
>(ANSWER_REQUEST | command);
390 if (request == REQUEST_SERIAL) {
406 static_cast<uint8_t
>(registers[0]) != TRANSFER_COMMAND)
407 return NO_TRANSFER_ANSWER;
408 const uint8_t counter =
static_cast<uint8_t
>(registers[0] >> 8);
409 const uint8_t sub_code =
static_cast<uint8_t
>(registers[1] >> 8);
410 if (sub_code != TRANSFER_SUB_SERIAL && sub_code != TRANSFER_SUB_FIRMWARE)
411 return NO_TRANSFER_ANSWER;
413 const uint8_t answer = counter & ~COUNTER_FIRST_HALF;
415 const uint8_t request = sub_code == TRANSFER_SUB_SERIAL ? REQUEST_SERIAL : REQUEST_FIRMWARE;
419 const size_t payload_regs = registers.
size() - TRANSFER_PAYLOAD_REG;
420 if (request == REQUEST_FIRMWARE) {
421 const size_t regs = std::min(payload_regs, FIRMWARE_REGS);
423 if (regs == FIRMWARE_REGS && text_length(this->
firmware_version_, 2 * regs) != 0) {
434 if (regs == FIRMWARE_REGS)
438 if ((counter & COUNTER_FIRST_HALF) != 0) {
442 if (payload_regs >= SERIAL_FIRST_HALF_REGS) {
443 copy_payload(registers, SERIAL_FIRST_HALF_REGS, this->
serial_number_);
453 copy_payload(registers, SERIAL_SINGLE_FRAME_REGS, this->
serial_number_ + at);
454 terminate_text(this->
serial_number_, at + 2 * SERIAL_SINGLE_FRAME_REGS);
464 for (uint16_t i = 0; i < number_of_registers; i++)
465 registers.
push_back(i < 2 ? answer[i] : 0x0000);
471 if (this->serial_number_text_sensor_ !=
nullptr && this->
identity_request_() != REQUEST_SERIAL &&
473 this->serial_number_text_sensor_->publish_state(this->
serial_number_);
478 ESP_LOGW(TAG,
"Unreadable serial number");
485 if (len == 2 * FIRMWARE_REGS &&
487 ESP_LOGD(TAG,
"Motor does not report its firmware version");
490 ESP_LOGW(TAG,
"Unreadable firmware version (%u bytes): %s",
len,
495 if (this->version_text_sensor_ !=
nullptr && this->
firmware_version_[0] !=
'\0') {
500 ESP_LOGW(TAG,
"No usable %s received",
502 : LOG_STR_LITERAL(
"firmware version"));
510 const uint8_t
position =
static_cast<uint8_t
>(value);
532 if (previous == value)
535 const uint8_t
state = value >> 8;
541 for (
const auto &mapping : DOOR_STATE_MAPPINGS) {
542 if (mapping.code ==
state) {
548 if (
state != (previous >> 8)) {
549 ESP_LOGW(TAG,
"Unknown door state 0x%02X",
state);
563 ESP_LOGW(TAG,
"Not connected to the bus controller, dropping '%s' command", command.
name);
570 ESP_LOGW(TAG,
"Previous command not yet fetched by the bus controller");
586 return this->stop_door();
590bool HoermannHcp::open_door() {
return this->
command_door_(COMMAND_OPEN); }
591bool HoermannHcp::close_door() {
return this->
command_door_(COMMAND_CLOSE); }
592bool HoermannHcp::impulse_door() {
return this->
command_door_(COMMAND_IMPULSE); }
593bool HoermannHcp::vent_door() {
return this->
command_door_(COMMAND_VENT); }
594bool HoermannHcp::half_open_door() {
return this->
command_door_(COMMAND_HALF_OPEN); }
595bool HoermannHcp::stop_door() {
599 ESP_LOGD(TAG,
"Stop cancels the unfetched '%s' command", this->
next_command_->
name);
605 ESP_LOGW(TAG,
"Not connected to the bus controller, dropping 'stop' command");
611 ESP_LOGD(TAG,
"Door already stopping, ignoring stop");
619bool HoermannHcp::set_position(
float position) {
621 if (
position <= CLOSE_POSITION_THRESHOLD)
622 return this->close_door();
623 if (
position >= OPEN_POSITION_THRESHOLD)
624 return this->open_door();
627 return this->stop_door();
631 if (!this->
queue_command_(opening ? COMMAND_OPEN : COMMAND_CLOSE))
645 if (this->
valid_ == valid)
650 ESP_LOGI(TAG,
"Bus controller connected");
653 ESP_LOGW(TAG,
"Bus controller connection lost (no request for %" PRIu32
"ms)",
millis() - this->
last_response_);
656 this->starting_ =
false;
678 if (this->starting_ && (is_moving(
state) || at_destination(*this->
start_command_, state))) {
682 this->starting_ =
false;
688 if (!is_moving(
state)) {
706 float position =
static_cast<float>(this->
position_raw_) / 200.0f;
712 if (this->current_position_ !=
position) {
729 ESP_LOGD(TAG,
"Lamp %s at the door", ONOFF(on));
741bool HoermannHcp::set_light(
bool on) {
Minimal static vector - saves memory by avoiding std::vector overhead.
void push_back(const T &value)
bool command_door_(const HoermannHcpCommand &command)
bool firmware_unreadable_
void on_light_reg_(uint16_t value)
void set_door_state_(DoorState state)
uint8_t identity_attempts_
bool take_identity_request_()
uint8_t take_identity_transfer_(const modbus::RegisterValues ®isters)
void on_state_reg_(uint16_t value)
uint8_t transfer_answer_counter_
void set_valid_(bool valid)
uint32_t identity_asked_at_
CallbackManager< void()> state_callback_
void set_light_seen_(bool seen)
void update_current_position_()
const HoermannHcpCommand * take_command_()
void set_light_on_(bool on)
void arm_identity_request_(IdentityPhase phase)
uint16_t command_reg_value_
uint16_t connection_timeout_ms_
const HoermannHcpCommand * next_command_
void push_command_registers_(modbus::RegisterValues ®isters)
bool status_poll_pending_
uint8_t identity_unanswered_
void add_identity_request_(modbus::RegisterValues ®isters, uint16_t command)
const HoermannHcpCommand * start_command_
bool is_moving_or_starting_() const
uint16_t start_window_ms_
char firmware_version_[2 *FIRMWARE_REGS+1]
void push_transfer_answer_(modbus::RegisterValues ®isters, uint16_t number_of_registers)
uint32_t start_fetched_at_
const HoermannHcpCommand * take_light_command_()
bool short_broadcast_logged_
IdentityPhase identity_phase_
uint32_t target_queued_at_
uint8_t firmware_unreadable_len_
DoorState target_direction_
bool queue_command_(const HoermannHcpCommand &command)
void on_position_reg_(uint16_t value)
uint8_t identity_request_() const
char serial_number_[2 *(SERIAL_FIRST_HALF_REGS+SERIAL_SECOND_HALF_REGS)+1]
uint32_t command_queued_at_
void clear_light_request_()
uint8_t get_address() const
std::vector< uint8_t > & out
ESPHOME_LOG_TAG(TAG, "hoermann_hcp.binary_sensor")
@ IDENTITY_PHASE_SERIAL_SECOND_HALF
@ IDENTITY_PHASE_FIRMWARE
@ IDENTITY_PHASE_SERIAL_SPLIT
StaticVector< uint16_t, MAX_NUM_OF_REGISTERS_TO_READ > RegisterValues
std::optional< ExceptionCode > ResponseStatus
constexpr size_t format_hex_size(size_t byte_count)
Calculate buffer size needed for format_hex_to: "XXXXXXXX...\0" = bytes * 2 + 1.
constexpr uint16_t encode_uint16(uint8_t msb, uint8_t lsb)
Encode a 16-bit value given the most and least significant byte.
uint32_t IRAM_ATTR HOT millis()
char * format_hex_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length)
Format byte array as lowercase hex to buffer (base implementation).