Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
platform_cover.cpp
Go to the documentation of this file.
1/// @file platform_cover.cpp
2/// @brief ESPHome cover entity for IO-Homecontrol devices.
3/// @ingroup hioc_platforms
4
5#include "platform_cover.h"
6#include "hub_internal.h"
7#include "esphome/core/log.h"
8
9namespace esphome {
10namespace home_io_control {
11
12static const char *const TAG = "home_io_control.cover";
13
15 // Covers compute their initial inversion from the explicit YAML override or the device-type
16 // default; the rest of the registration ritual is shared with the other entity types.
17 const bool initial_invert = this->invert_explicit_ ? this->invert_ : default_inverted_for_type(this->device_type_);
19 this, initial_invert, [this](const std::string &id, const IoDevice &dev) { this->on_device_update_(id, dev); });
20}
21
22cover::CoverTraits IOHomeCover::get_traits() {
23 auto traits = cover::CoverTraits();
24 traits.set_supports_position(true); // Slider in HA UI
25 traits.set_supports_stop(true); // Stop button in HA UI
26 traits.set_supports_tilt(this->supports_tilt());
27 traits.set_is_assumed_state(false); // We hopefully get real feedback from the device
28 return traits;
29}
30
34
36 if (this->invert_explicit_)
37 return this->invert_;
38 if (this->parent_ == nullptr)
39 return false;
40 const auto *dev = this->parent_->get_device(this->device_id_);
41 return dev != nullptr && dev->inverted;
42}
43
44cover::CoverOperation IOHomeCover::infer_operation_from_position_delta_(bool invert, float current_io_position) const {
45 if (this->last_io_position_ == UNKNOWN_POSITION || current_io_position == UNKNOWN_POSITION ||
46 current_io_position == this->last_io_position_) {
47 return cover::COVER_OPERATION_IDLE;
48 }
49
50 if (invert) {
51 return (current_io_position > this->last_io_position_) ? cover::COVER_OPERATION_OPENING
52 : cover::COVER_OPERATION_CLOSING;
53 }
54
55 return (current_io_position < this->last_io_position_) ? cover::COVER_OPERATION_OPENING
56 : cover::COVER_OPERATION_CLOSING;
57}
58
59void IOHomeCover::control(const cover::CoverCall &call) {
60 // Optimistic state gives immediate HA UI feedback for the queue-dispatch + TX/response gap;
61 // the queued command's own response (update_device_status_()) remains the source of truth
62 // and will overwrite this. No-op when this device has optimistic_state=false (see
63 // DeviceRegistry::apply_optimistic_target()/clear_optimistic_target()).
64 if (call.get_stop()) {
65 this->parent_->clear_optimistic_target(this->device_id_);
66 this->parent_->queue_device_command(this->device_id_, CoverCommand::STOP);
67 return;
68 }
69
70 const auto &tilt_opt = call.get_tilt();
71 const auto &position_opt = call.get_position();
72
73 // Combined position+tilt in one atomic command when both are present.
74 /// @todo Monitor https://github.com/home-assistant/core/issues/174533 — if HA adds a combined
75 /// cover.set_cover_position_and_tilt action, this branch would be exercised directly from
76 /// a single CoverCall. The queue coalescing in queue_set_device_position/tilt remains a
77 /// useful optimization for the two-separate-calls path regardless.
78 // Position/tilt fraction -> IO percent uses detail::round_percent() (hub_internal.h) — rounds
79 // rather than truncates, since HA quantizes call values to 0-255 before they reach us (its
80 // "50%" is 128/255=0.502, not exactly 0.5) and a truncating cast would compound that into a
81 // consistent ~1% bias (caught on hardware; see PlatformCover.ControlRoundsQuantizedPosition-
82 // InsteadOfTruncating).
83 if (position_opt.has_value() && tilt_opt.has_value() && this->supports_tilt()) {
84 float const ha_pos = *position_opt;
85 const bool invert = this->effective_invert_();
86 uint8_t const io_pos = invert ? detail::round_percent(ha_pos) : detail::round_percent(1.0F - ha_pos);
87 auto const tilt = detail::round_percent(*tilt_opt);
88 this->parent_->apply_optimistic_target(this->device_id_, io_pos);
89 this->parent_->apply_optimistic_tilt(this->device_id_, tilt);
90 this->parent_->queue_set_device_position_and_tilt(this->device_id_, io_pos, tilt);
91 return;
92 }
93
94 if (tilt_opt.has_value()) {
95 auto const tilt = detail::round_percent(*tilt_opt);
96 // Position commands get their optimistic feedback from apply_optimistic_target() above; tilt
97 // needs its own because a tilt command's reply carries no usable slat angle, so without this
98 // the slider snaps back to the pre-command angle until the next status poll.
99 this->parent_->apply_optimistic_tilt(this->device_id_, tilt);
100 this->parent_->queue_set_device_tilt(this->device_id_, tilt);
101 return;
102 }
103
104 if (position_opt.has_value()) {
105 float const ha_pos = *position_opt; // HA: 1.0 = fully open, 0.0 = fully closed
106 const bool invert = this->effective_invert_();
107
108 // Convert HA position (0.0-1.0) to IO position (0-100)
109 // Standard: HA 1.0 (open) → IO 0 (open), HA 0.0 (closed) → IO 100 (closed)
110 // Inverted: HA 1.0 (open) → IO 100, HA 0.0 (closed) → IO 0
111 // (used for devices like horizontal awnings where the IO convention is reversed)
112 const uint8_t io_pos = invert ? detail::round_percent(ha_pos) : detail::round_percent(1.0F - ha_pos);
113
114 this->parent_->apply_optimistic_target(this->device_id_, io_pos);
115 this->parent_->queue_set_device_position(this->device_id_, io_pos);
116 }
117}
118
119void IOHomeCover::on_device_update_(const std::string &id, const IoDevice &dev) {
120 if (id != this->device_id_)
121 return;
122
123 const bool invert = this->effective_invert_();
124 const float previous_io_position = this->last_io_position_;
125
126 if (dev.position != UNKNOWN_POSITION) {
127 // Convert IO position (0-100) back to HA position (0.0-1.0)
128 float ha_pos;
129 if (invert) {
130 ha_pos = dev.position / 100.0F;
131 } else {
132 ha_pos = 1.0F - (dev.position / 100.0F);
133 }
134
135 this->position = ha_pos;
136 }
137
138 if (this->supports_tilt() && dev.tilt != UNKNOWN_POSITION) {
139 this->tilt = dev.tilt / 100.0F;
140 }
141
142 // Determine movement direction for the HA UI animation
143 if (dev.is_stopped) {
144 this->current_operation = cover::COVER_OPERATION_IDLE;
145 } else if (dev.target != UNKNOWN_POSITION && dev.position != UNKNOWN_POSITION) {
146 // Figure out if we're opening or closing based on target vs current position
147 if (invert) {
148 this->current_operation =
149 (dev.target > dev.position) ? cover::COVER_OPERATION_OPENING : cover::COVER_OPERATION_CLOSING;
150 } else {
151 // Standard: lower IO position = more open, so target < current = opening
152 this->current_operation =
153 (dev.target < dev.position) ? cover::COVER_OPERATION_OPENING : cover::COVER_OPERATION_CLOSING;
154 }
155 } else if (dev.position != UNKNOWN_POSITION) {
156 this->current_operation = this->infer_operation_from_position_delta_(invert, dev.position);
157 }
158
159 if (dev.position != UNKNOWN_POSITION) {
160 this->last_io_position_ = dev.position;
161 } else {
162 this->last_io_position_ = previous_io_position;
163 }
164
165 this->publish_state();
166}
167
169 LOG_COVER("", "IO-Homecontrol Cover", this);
170 ESP_LOGCONFIG(TAG, " Device ID: %s", this->device_id_.c_str());
171 ESP_LOGCONFIG(TAG, " Invert Position Override: %s", this->invert_explicit_ ? YESNO(this->invert_) : "AUTO");
172 this->log_poll_interval_config_(TAG);
173 ESP_LOGCONFIG(TAG, " Supports Tilt: %s", YESNO(this->supports_tilt()));
174}
175
176} // namespace home_io_control
177} // namespace esphome
void register_device_binding_(Component *self, bool inverted, std::function< void(const std::string &, const IoDevice &)> on_update)
Perform the shared setup() registration ritual.
void log_poll_interval_config_(const char *tag) const
Emit the shared two-branch poll-interval line for dump_config().
cover::CoverOperation infer_operation_from_position_delta_(bool invert, float current_io_position) const
Infer HA movement direction from successive IO positions when the protocol target is unknown.
bool supports_tilt() const
Query whether this device supports tilt (slat angle) control.
void on_device_update_(const std::string &id, const IoDevice &dev)
Callback invoked when the underlying device state changes.
bool effective_invert_() const
Resolve the current inversion mode.
cover::CoverTraits get_traits() override
Return the traits object describing this cover's capabilities.
void setup() override
Initialize the cover entity (register device, subscribe to updates, schedule initial status poll).
void dump_config() override
Dump configuration to log.
void control(const cover::CoverCall &call) override
Handle cover commands from Home Assistant (open/close/stop/set_position).
Internal helpers shared by the hub implementation .cpp files.
uint8_t round_percent(float fraction)
Convert a 0.0-1.0 HA fraction (position, tilt, or brightness) to a 0-100 IO percent.
static constexpr float UNKNOWN_POSITION
Sentinel value meaning "position is not known yet".
@ UNKNOWN
Unknown/unspecified device.
static constexpr const char * TAG
bool default_inverted_for_type(DeviceType type)
Determine whether a device type has inverted position mapping by default.
bool device_supports_tilt(DeviceType type)
Does this device type support tilt (slat angle) control?
ESPHome cover entity for IO‑Homecontrol devices.
Runtime state of a paired IO‑Homecontrol device.
float target
Target position the device is moving toward.
float tilt
Current tilt: 0=closed, 100=open, or UNKNOWN_POSITION.
float position
Current position: 0=open, 100=closed, or UNKNOWN_POSITION.
bool is_stopped
True if device is not moving.