ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
hoermann_hcp.h
Go to the documentation of this file.
1#pragma once
2
3#include <utility>
4
9#ifdef USE_HOERMANN_HCP_IDENTITY
11#endif
12
13namespace esphome::hoermann_hcp {
14
15// Door state as reported by the Hoermann bus controller.
16enum class DoorState : uint8_t {
17 OPEN,
18 OPENING,
19 CLOSED,
20 CLOSING,
23 VENT,
25 STOPPED,
26};
27
28#ifdef USE_HOERMANN_HCP_IDENTITY
29// Payload registers of each value, two bytes each.
30static constexpr size_t SERIAL_FIRST_HALF_REGS = 7;
31static constexpr size_t SERIAL_SECOND_HALF_REGS = 6;
32static constexpr size_t FIRMWARE_REGS = 6;
33// Counter of the transfer the read half of the frame acknowledges. Real counters have the 0x80 half marker
34// stripped, so this value never occurs.
35static constexpr uint8_t NO_TRANSFER_ANSWER = 0xFF;
36
37// Where the identity exchange stands. The low nibble is the code the motor is asked with, 0 while nothing is
38// outstanding. Like Hoermann's own bus accessory, the first status answer stays ordinary. Older motors (index B1
39// seen) send the whole serial number in one frame, without the half marker.
40enum class IdentityPhase : uint8_t {
43 IDENTITY_PHASE_SERIAL = 0x05, // A plain frame is the whole serial number.
44 IDENTITY_PHASE_SERIAL_SPLIT = 0x15, // A half marker was seen, so a plain frame can only be the second half.
45 IDENTITY_PHASE_SERIAL_SECOND_HALF = 0x25, // The first half is in.
47};
48#endif
49
50// Sent once, in a single status answer, as Hoermann's own bus accessory does.
52 const char *name;
53 uint16_t value;
54 uint16_t value_2{0x0000};
55};
56
58#ifdef USE_HOERMANN_HCP_IDENTITY
59 // The motor is asked for these only when one of them is configured.
60 SUB_TEXT_SENSOR(serial_number)
61 SUB_TEXT_SENSOR(version)
62#endif
63
64 public:
65 void update() override;
66 void dump_config() override;
67
68 // Registered by child entities to be notified when the door state changes.
69 template<typename F> void add_on_state_callback(F &&callback) {
70 this->state_callback_.add(std::forward<F>(callback));
71 }
72
73 // Modbus server callbacks. The bus controller pushes commands and polls state with 0x17 (the hub runs the write
74 // half first, storing the command register that the read half echoes back) and broadcasts status with 0x10.
75 modbus::ResponseStatus on_write_registers(uint16_t start_address, const modbus::RegisterValues &registers) override;
76 modbus::ResponseStatus on_read_holding_registers(uint16_t start_address, uint16_t number_of_registers,
77 modbus::RegisterValues &registers) override;
78
79 // Positions follow the cover convention: 0.0 is fully closed, 1.0 fully open. These return false when the bus
80 // controller cannot be asked right now, so the caller can react.
81 bool open_door();
82 bool close_door();
83 bool impulse_door();
84 // The door drives to these intermediate positions on its own, so neither takes a target to be stopped at.
85 bool vent_door();
86 bool half_open_door();
87 bool stop_door();
88 bool set_position(float position);
89 // False while the door has not reported the lamp.
90 bool set_light(bool on);
91
92 DoorState get_door_state() const { return this->door_state_; }
93 // False until a broadcast has carried a state the door is known to report. Bus traffic alone makes the
94 // connection valid, so get_door_state() would still be its default.
95 bool is_door_state_known() const { return this->door_state_seen_; }
96 float get_current_position() const { return this->current_position_; }
97 bool is_valid() const { return this->valid_; }
98 bool is_light_on() const { return this->light_on_; }
99 // False until a broadcast has actually carried the lamp register. Bus traffic alone makes the connection
100 // valid without saying anything about the lamp, so is_light_on() would still be its default.
101 bool is_light_known() const { return this->light_seen_; }
102 // The requested state while switching, else the reported one.
103 bool is_light_heading_on() const { return this->light_requested_ ? this->light_target_ : this->light_on_; }
104
105 protected:
106 void record_response_();
107 bool command_door_(const HoermannHcpCommand &command);
108 // Returns false when the bus controller has not fetched the previous command yet.
109 bool queue_command_(const HoermannHcpCommand &command);
110 void drop_command_();
113 // Decide at the fetch what goes into a status answer, against the door as it stands then.
116 void on_position_reg_(uint16_t value);
117 void on_state_reg_(uint16_t value);
118 void on_light_reg_(uint16_t value);
119#ifdef USE_HOERMANN_HCP_IDENTITY
120 // Puts a due request into a status answer.
121 void add_identity_request_(modbus::RegisterValues &registers, uint16_t command);
123 uint8_t identity_request_() const { return static_cast<uint8_t>(this->identity_phase_) & 0x0F; }
124 // True when the request is due, counting the attempt. Gives up after the last one.
126 // Takes a value the motor hands over as a payload transfer. Returns the counter to acknowledge it with, kept or
127 // not, or NO_TRANSFER_ANSWER when the frame was something else.
128 uint8_t take_identity_transfer_(const modbus::RegisterValues &registers);
129 // Acknowledges the transfer taken by the write half of the same frame.
130 void push_transfer_answer_(modbus::RegisterValues &registers, uint16_t number_of_registers);
131 // Runs from update(), outside the bus callbacks.
132 void publish_identity_();
133#endif
134
135 void set_valid_(bool valid);
137 // Recomputes the reported position from position_raw_ and the current door state.
139 bool has_target_() const { return this->target_position_ != 0.0f; }
140 bool is_moving_or_starting_() const;
141 void clear_target_();
142 void set_light_on_(bool on);
143 void set_light_seen_(bool seen);
144
146
147 float current_position_{0.0f};
148 // Position the door was told to travel to; 0.0 means no target is armed.
149 float target_position_{0.0f};
150
154 // Separate from command_queued_at_ so an unrelated command cannot extend the target's start deadline.
157 // Start of the wait for the fetch, then for the report.
161 bool stop_sent_{false};
162 // A door command was fetched and the door has not answered it by moving or reaching its destination yet.
163 bool starting_{false};
164
165 // Drop the "connected" flag if the bus controller has not polled us for this long.
167 // A chosen margin for a door to report moving after a fetched command.
168 uint16_t start_window_ms_{5000};
169 // The state starts on a value the bus controller never reports, so the first broadcast is decoded even when
170 // it reads 0x0000.
171 uint16_t prev_state_reg_{0xFFFF};
172 // 0x17 write half: command register last written to COMMAND_REG. The read half echoes its high-byte message
173 // counter and low-byte command back from STATE_REG.
175
177 // Direction the door was started in for the current target, judged only once the door reports moving that way.
179 // Position as reported by the bus controller, 0..200 across the full travel.
180 uint8_t position_raw_{0};
181 bool target_started_{false};
182 bool valid_{false};
183 bool changed_{false};
184 bool light_on_{false};
185 bool light_seen_{false};
186 bool light_requested_{false};
188 bool light_target_{false};
189 bool door_state_seen_{false};
191 // Only the read half right after a 0x17 write carries a command, so a second read without a new write does not.
193
194#ifdef USE_HOERMANN_HCP_IDENTITY
198 // The request given up on, for update() to report.
200 uint8_t transfer_answer_counter_{NO_TRANSFER_ANSWER};
201 // Length of an unreadable firmware version left in firmware_version_.
203 // A serial number arrived without text, for update() to log.
206 char serial_number_[2 * (SERIAL_FIRST_HALF_REGS + SERIAL_SECOND_HALF_REGS) + 1]{};
207 char firmware_version_[2 * FIRMWARE_REGS + 1]{};
208#endif
209};
210
211} // namespace esphome::hoermann_hcp
This class simplifies creating components that periodically check a state.
Definition component.h:514
virtual void update()=0
Minimal static vector - saves memory by avoiding std::vector overhead.
Definition helpers.h:286
bool command_door_(const HoermannHcpCommand &command)
void set_door_state_(DoorState state)
uint8_t take_identity_transfer_(const modbus::RegisterValues &registers)
CallbackManager< void()> state_callback_
const HoermannHcpCommand * take_command_()
void arm_identity_request_(IdentityPhase phase)
const HoermannHcpCommand * next_command_
void push_command_registers_(modbus::RegisterValues &registers)
void add_identity_request_(modbus::RegisterValues &registers, uint16_t command)
const HoermannHcpCommand * start_command_
char firmware_version_[2 *FIRMWARE_REGS+1]
void push_transfer_answer_(modbus::RegisterValues &registers, uint16_t number_of_registers)
const HoermannHcpCommand * take_light_command_()
bool queue_command_(const HoermannHcpCommand &command)
void on_position_reg_(uint16_t value)
char serial_number_[2 *(SERIAL_FIRST_HALF_REGS+SERIAL_SECOND_HALF_REGS)+1]
float position
Definition cover.h:0
bool state
Definition fan.h:2
std::optional< ExceptionCode > ResponseStatus
Definition modbus.h:307
bool valid
static void uint32_t
char serial_number[10]
Definition sun_gtil2.cpp:15