mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +00:00 
			
		
		
		
	Add Chamberlain/HomEntry HE60R garage door opener (#5834)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
		| @@ -124,6 +124,7 @@ esphome/components/haier/* @paveldn | |||||||
| esphome/components/havells_solar/* @sourabhjaiswal | esphome/components/havells_solar/* @sourabhjaiswal | ||||||
| esphome/components/hbridge/fan/* @WeekendWarrior | esphome/components/hbridge/fan/* @WeekendWarrior | ||||||
| esphome/components/hbridge/light/* @DotNetDann | esphome/components/hbridge/light/* @DotNetDann | ||||||
|  | esphome/components/he60r/* @clydebarrow | ||||||
| esphome/components/heatpumpir/* @rob-deutsch | esphome/components/heatpumpir/* @rob-deutsch | ||||||
| esphome/components/hitachi_ac424/* @sourabhjaiswal | esphome/components/hitachi_ac424/* @sourabhjaiswal | ||||||
| esphome/components/hm3301/* @freekode | esphome/components/hm3301/* @freekode | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								esphome/components/he60r/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								esphome/components/he60r/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | CODEOWNERS = ["@clydebarrow"] | ||||||
							
								
								
									
										47
									
								
								esphome/components/he60r/cover.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								esphome/components/he60r/cover.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | |||||||
|  | import esphome.codegen as cg | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome.components import cover, uart | ||||||
|  | from esphome.const import ( | ||||||
|  |     CONF_CLOSE_DURATION, | ||||||
|  |     CONF_ID, | ||||||
|  |     CONF_OPEN_DURATION, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | he60r_ns = cg.esphome_ns.namespace("he60r") | ||||||
|  | HE60rCover = he60r_ns.class_("HE60rCover", cover.Cover, cg.Component) | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = ( | ||||||
|  |     cover.COVER_SCHEMA.extend(uart.UART_DEVICE_SCHEMA) | ||||||
|  |     .extend(cv.COMPONENT_SCHEMA) | ||||||
|  |     .extend( | ||||||
|  |         { | ||||||
|  |             cv.GenerateID(): cv.declare_id(HE60rCover), | ||||||
|  |             cv.Optional( | ||||||
|  |                 CONF_OPEN_DURATION, default="15s" | ||||||
|  |             ): cv.positive_time_period_milliseconds, | ||||||
|  |             cv.Optional( | ||||||
|  |                 CONF_CLOSE_DURATION, default="15s" | ||||||
|  |             ): cv.positive_time_period_milliseconds, | ||||||
|  |         } | ||||||
|  |     ) | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( | ||||||
|  |     "he60r", | ||||||
|  |     baud_rate=1200, | ||||||
|  |     require_tx=True, | ||||||
|  |     require_rx=True, | ||||||
|  |     data_bits=8, | ||||||
|  |     parity="EVEN", | ||||||
|  |     stop_bits=1, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def to_code(config): | ||||||
|  |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|  |     await cg.register_component(var, config) | ||||||
|  |     await cover.register_cover(var, config) | ||||||
|  |     await uart.register_uart_device(var, config) | ||||||
|  |  | ||||||
|  |     cg.add(var.set_close_duration(config[CONF_CLOSE_DURATION])) | ||||||
|  |     cg.add(var.set_open_duration(config[CONF_OPEN_DURATION])) | ||||||
							
								
								
									
										265
									
								
								esphome/components/he60r/he60r.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										265
									
								
								esphome/components/he60r/he60r.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,265 @@ | |||||||
|  | #include "he60r.h" | ||||||
|  | #include "esphome/core/hal.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace he60r { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "he60r.cover"; | ||||||
|  | static const uint8_t QUERY_BYTE = 0x38; | ||||||
|  | static const uint8_t TOGGLE_BYTE = 0x30; | ||||||
|  |  | ||||||
|  | using namespace esphome::cover; | ||||||
|  |  | ||||||
|  | void HE60rCover::setup() { | ||||||
|  |   auto restore = this->restore_state_(); | ||||||
|  |  | ||||||
|  |   if (restore.has_value()) { | ||||||
|  |     restore->apply(this); | ||||||
|  |     this->publish_state(false); | ||||||
|  |   } else { | ||||||
|  |     // if no other information, assume half open | ||||||
|  |     this->position = 0.5f; | ||||||
|  |   } | ||||||
|  |   this->current_operation = COVER_OPERATION_IDLE; | ||||||
|  |   this->last_recompute_time_ = this->start_dir_time_ = millis(); | ||||||
|  |   this->set_interval(300, [this]() { this->update_(); }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | CoverTraits HE60rCover::get_traits() { | ||||||
|  |   auto traits = CoverTraits(); | ||||||
|  |   traits.set_supports_stop(true); | ||||||
|  |   traits.set_supports_position(true); | ||||||
|  |   traits.set_supports_toggle(true); | ||||||
|  |   traits.set_is_assumed_state(false); | ||||||
|  |   return traits; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void HE60rCover::dump_config() { | ||||||
|  |   LOG_COVER("", "HE60R Cover", this); | ||||||
|  |   this->check_uart_settings(1200, 1, uart::UART_CONFIG_PARITY_EVEN, 8); | ||||||
|  |   ESP_LOGCONFIG(TAG, "  Open Duration: %.1fs", this->open_duration_ / 1e3f); | ||||||
|  |   ESP_LOGCONFIG(TAG, "  Close Duration: %.1fs", this->close_duration_ / 1e3f); | ||||||
|  |   auto restore = this->restore_state_(); | ||||||
|  |   if (restore.has_value()) | ||||||
|  |     ESP_LOGCONFIG(TAG, "  Saved position %d%%", (int) (restore->position * 100.f)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void HE60rCover::endstop_reached_(CoverOperation operation) { | ||||||
|  |   const uint32_t now = millis(); | ||||||
|  |  | ||||||
|  |   this->set_current_operation_(COVER_OPERATION_IDLE); | ||||||
|  |   auto new_position = operation == COVER_OPERATION_OPENING ? COVER_OPEN : COVER_CLOSED; | ||||||
|  |   if (new_position != this->position || this->current_operation != COVER_OPERATION_IDLE) { | ||||||
|  |     this->position = new_position; | ||||||
|  |     this->current_operation = COVER_OPERATION_IDLE; | ||||||
|  |     if (this->last_command_ == operation) { | ||||||
|  |       float dur = (now - this->start_dir_time_) / 1e3f; | ||||||
|  |       ESP_LOGD(TAG, "'%s' - %s endstop reached. Took %.1fs.", this->name_.c_str(), | ||||||
|  |                operation == COVER_OPERATION_OPENING ? "Open" : "Close", dur); | ||||||
|  |     } | ||||||
|  |     this->publish_state(); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void HE60rCover::set_current_operation_(cover::CoverOperation operation) { | ||||||
|  |   if (this->current_operation != operation) { | ||||||
|  |     this->current_operation = operation; | ||||||
|  |     if (operation != COVER_OPERATION_IDLE) | ||||||
|  |       this->last_recompute_time_ = millis(); | ||||||
|  |     this->publish_state(); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void HE60rCover::process_rx_(uint8_t data) { | ||||||
|  |   ESP_LOGV(TAG, "Process RX data %X", data); | ||||||
|  |   if (!this->query_seen_) { | ||||||
|  |     this->query_seen_ = data == QUERY_BYTE; | ||||||
|  |     if (!this->query_seen_) | ||||||
|  |       ESP_LOGD(TAG, "RX Byte %02X", data); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   switch (data) { | ||||||
|  |     case 0xB5:  // at closed endstop, jammed? | ||||||
|  |     case 0xF5:  // at closed endstop, jammed? | ||||||
|  |     case 0x55:  // at closed endstop | ||||||
|  |       this->next_direction_ = COVER_OPERATION_OPENING; | ||||||
|  |       this->endstop_reached_(COVER_OPERATION_CLOSING); | ||||||
|  |       break; | ||||||
|  |  | ||||||
|  |     case 0x52:  // at opened endstop | ||||||
|  |       this->next_direction_ = COVER_OPERATION_CLOSING; | ||||||
|  |       this->endstop_reached_(COVER_OPERATION_OPENING); | ||||||
|  |       break; | ||||||
|  |  | ||||||
|  |     case 0x51:  // travelling up after encountering obstacle | ||||||
|  |     case 0x01:  // travelling up | ||||||
|  |     case 0x11:  // travelling up, triggered by remote | ||||||
|  |       this->set_current_operation_(COVER_OPERATION_OPENING); | ||||||
|  |       this->next_direction_ = COVER_OPERATION_IDLE; | ||||||
|  |       break; | ||||||
|  |  | ||||||
|  |     case 0x44:  // travelling down | ||||||
|  |     case 0x14:  // travelling down, triggered by remote | ||||||
|  |       this->next_direction_ = COVER_OPERATION_IDLE; | ||||||
|  |       this->set_current_operation_(COVER_OPERATION_CLOSING); | ||||||
|  |       break; | ||||||
|  |  | ||||||
|  |     case 0x86:  //  Stopped, jammed? | ||||||
|  |     case 0x16:  // stopped midway while opening, by remote | ||||||
|  |     case 0x06:  // stopped midway while opening | ||||||
|  |       this->next_direction_ = COVER_OPERATION_CLOSING; | ||||||
|  |       this->set_current_operation_(COVER_OPERATION_IDLE); | ||||||
|  |       break; | ||||||
|  |  | ||||||
|  |     case 0x10:  // stopped midway while closing, by remote | ||||||
|  |     case 0x00:  // stopped midway while closing | ||||||
|  |       this->next_direction_ = COVER_OPERATION_OPENING; | ||||||
|  |       this->set_current_operation_(COVER_OPERATION_IDLE); | ||||||
|  |       break; | ||||||
|  |  | ||||||
|  |     default: | ||||||
|  |       break; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void HE60rCover::update_() { | ||||||
|  |   if (toggles_needed_ != 0) { | ||||||
|  |     if ((this->counter_++ & 0x3) == 0) { | ||||||
|  |       toggles_needed_--; | ||||||
|  |       ESP_LOGD(TAG, "Writing byte 0x30, still needed=%d", toggles_needed_); | ||||||
|  |       this->write_byte(TOGGLE_BYTE); | ||||||
|  |     } else { | ||||||
|  |       this->write_byte(QUERY_BYTE); | ||||||
|  |     } | ||||||
|  |   } else { | ||||||
|  |     this->write_byte(QUERY_BYTE); | ||||||
|  |     this->counter_ = 0; | ||||||
|  |   } | ||||||
|  |   if (this->current_operation != COVER_OPERATION_IDLE) { | ||||||
|  |     this->recompute_position_(); | ||||||
|  |  | ||||||
|  |     // if we initiated the move, check if we reached the target position | ||||||
|  |     if (this->last_command_ != COVER_OPERATION_IDLE) { | ||||||
|  |       if (this->is_at_target_()) { | ||||||
|  |         this->start_direction_(COVER_OPERATION_IDLE); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void HE60rCover::loop() { | ||||||
|  |   uint8_t data; | ||||||
|  |  | ||||||
|  |   while (this->available() > 0) { | ||||||
|  |     if (this->read_byte(&data)) { | ||||||
|  |       this->process_rx_(data); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void HE60rCover::control(const CoverCall &call) { | ||||||
|  |   if (call.get_stop()) { | ||||||
|  |     this->start_direction_(COVER_OPERATION_IDLE); | ||||||
|  |   } else if (call.get_toggle().has_value()) { | ||||||
|  |     // toggle action logic: OPEN - STOP - CLOSE | ||||||
|  |     if (this->last_command_ != COVER_OPERATION_IDLE) { | ||||||
|  |       this->start_direction_(COVER_OPERATION_IDLE); | ||||||
|  |     } else { | ||||||
|  |       this->toggles_needed_++; | ||||||
|  |     } | ||||||
|  |   } else if (call.get_position().has_value()) { | ||||||
|  |     // go to position action | ||||||
|  |     auto pos = *call.get_position(); | ||||||
|  |     // are we at the target? | ||||||
|  |     if (pos == this->position) { | ||||||
|  |       this->start_direction_(COVER_OPERATION_IDLE); | ||||||
|  |     } else { | ||||||
|  |       this->target_position_ = pos; | ||||||
|  |       this->start_direction_(pos < this->position ? COVER_OPERATION_CLOSING : COVER_OPERATION_OPENING); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Check if the cover has reached or passed the target position. This is used only | ||||||
|  |  * for partial open/close requests - endstops are used for full open/close. | ||||||
|  |  * @return True if the cover has reached or passed its target position. For full open/close target always return false. | ||||||
|  |  */ | ||||||
|  | bool HE60rCover::is_at_target_() const { | ||||||
|  |   // equality of floats is fraught with peril - this is reliable since the values are 0.0 or 1.0 which are | ||||||
|  |   // exactly representable. | ||||||
|  |   if (this->target_position_ == COVER_OPEN || this->target_position_ == COVER_CLOSED) | ||||||
|  |     return false; | ||||||
|  |   // aiming for an intermediate position - exact comparison here will not work and we need to allow for overshoot | ||||||
|  |   switch (this->last_command_) { | ||||||
|  |     case COVER_OPERATION_OPENING: | ||||||
|  |       return this->position >= this->target_position_; | ||||||
|  |     case COVER_OPERATION_CLOSING: | ||||||
|  |       return this->position <= this->target_position_; | ||||||
|  |     case COVER_OPERATION_IDLE: | ||||||
|  |       return this->current_operation == COVER_OPERATION_IDLE; | ||||||
|  |     default: | ||||||
|  |       return true; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | void HE60rCover::start_direction_(CoverOperation dir) { | ||||||
|  |   this->last_command_ = dir; | ||||||
|  |   if (this->current_operation == dir) | ||||||
|  |     return; | ||||||
|  |   ESP_LOGD(TAG, "'%s' - Direction '%s' requested.", this->name_.c_str(), | ||||||
|  |            dir == COVER_OPERATION_OPENING   ? "OPEN" | ||||||
|  |            : dir == COVER_OPERATION_CLOSING ? "CLOSE" | ||||||
|  |                                             : "STOP"); | ||||||
|  |  | ||||||
|  |   if (dir == this->next_direction_) { | ||||||
|  |     // either moving and needs to stop, or stopped and will move correctly on one trigger | ||||||
|  |     this->toggles_needed_ = 1; | ||||||
|  |   } else { | ||||||
|  |     if (this->current_operation == COVER_OPERATION_IDLE) { | ||||||
|  |       // if stopped, but will go the wrong way, need 3 triggers. | ||||||
|  |       this->toggles_needed_ = 3; | ||||||
|  |     } else { | ||||||
|  |       // just stop and reverse | ||||||
|  |       this->toggles_needed_ = 2; | ||||||
|  |     } | ||||||
|  |     ESP_LOGD(TAG, "'%s' - Reversing direction.", this->name_.c_str()); | ||||||
|  |   } | ||||||
|  |   this->start_dir_time_ = millis(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void HE60rCover::recompute_position_() { | ||||||
|  |   if (this->current_operation == COVER_OPERATION_IDLE) | ||||||
|  |     return; | ||||||
|  |  | ||||||
|  |   const uint32_t now = millis(); | ||||||
|  |   float dir; | ||||||
|  |   float action_dur; | ||||||
|  |  | ||||||
|  |   switch (this->current_operation) { | ||||||
|  |     case COVER_OPERATION_OPENING: | ||||||
|  |       dir = 1.0f; | ||||||
|  |       action_dur = this->open_duration_; | ||||||
|  |       break; | ||||||
|  |     case COVER_OPERATION_CLOSING: | ||||||
|  |       dir = -1.0f; | ||||||
|  |       action_dur = this->close_duration_; | ||||||
|  |       break; | ||||||
|  |     default: | ||||||
|  |       return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (now > this->last_recompute_time_) { | ||||||
|  |     auto diff = now - last_recompute_time_; | ||||||
|  |     auto delta = dir * diff / action_dur; | ||||||
|  |     // make sure our guesstimate never reaches full open or close. | ||||||
|  |     this->position = clamp(delta + this->position, COVER_CLOSED + 0.01f, COVER_OPEN - 0.01f); | ||||||
|  |     ESP_LOGD(TAG, "Recompute %dms, dir=%f, action_dur=%f, delta=%f, pos=%f", (int) diff, dir, action_dur, delta, | ||||||
|  |              this->position); | ||||||
|  |     this->last_recompute_time_ = now; | ||||||
|  |     this->publish_state(); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace he60r | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										47
									
								
								esphome/components/he60r/he60r.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								esphome/components/he60r/he60r.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/core/component.h" | ||||||
|  | #include "esphome/core/automation.h" | ||||||
|  | #include "esphome/components/uart/uart.h" | ||||||
|  | #include "esphome/components/cover/cover.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace he60r { | ||||||
|  |  | ||||||
|  | class HE60rCover : public cover::Cover, public Component, public uart::UARTDevice { | ||||||
|  |  public: | ||||||
|  |   void setup() override; | ||||||
|  |   void loop() override; | ||||||
|  |   void dump_config() override; | ||||||
|  |   float get_setup_priority() const override { return setup_priority::DATA; }; | ||||||
|  |  | ||||||
|  |   void set_open_duration(uint32_t duration) { this->open_duration_ = duration; } | ||||||
|  |   void set_close_duration(uint32_t duration) { this->close_duration_ = duration; } | ||||||
|  |  | ||||||
|  |   cover::CoverTraits get_traits() override; | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   void update_(); | ||||||
|  |   void control(const cover::CoverCall &call) override; | ||||||
|  |   bool is_at_target_() const; | ||||||
|  |   void start_direction_(cover::CoverOperation dir); | ||||||
|  |   void update_operation_(cover::CoverOperation dir); | ||||||
|  |   void endstop_reached_(cover::CoverOperation operation); | ||||||
|  |   void recompute_position_(); | ||||||
|  |   void set_current_operation_(cover::CoverOperation operation); | ||||||
|  |   void process_rx_(uint8_t data); | ||||||
|  |  | ||||||
|  |   uint32_t open_duration_{0}; | ||||||
|  |   uint32_t close_duration_{0}; | ||||||
|  |   uint32_t toggles_needed_{0}; | ||||||
|  |   cover::CoverOperation next_direction_{cover::COVER_OPERATION_IDLE}; | ||||||
|  |   cover::CoverOperation last_command_{cover::COVER_OPERATION_IDLE}; | ||||||
|  |   uint32_t last_recompute_time_{0}; | ||||||
|  |   uint32_t start_dir_time_{0}; | ||||||
|  |   float target_position_{0}; | ||||||
|  |   bool query_seen_{}; | ||||||
|  |   uint8_t counter_{}; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace he60r | ||||||
|  | }  // namespace esphome | ||||||
| @@ -58,6 +58,12 @@ uart: | |||||||
|     tx_pin: GPIO22 |     tx_pin: GPIO22 | ||||||
|     rx_pin: GPIO23 |     rx_pin: GPIO23 | ||||||
|     baud_rate: 9600 |     baud_rate: 9600 | ||||||
|  |   - id: uart_he60r | ||||||
|  |     tx_pin: 22 | ||||||
|  |     rx_pin: 23 | ||||||
|  |     baud_rate: 1200 | ||||||
|  |     parity: EVEN | ||||||
|  |  | ||||||
|  |  | ||||||
| ota: | ota: | ||||||
|   safe_mode: true |   safe_mode: true | ||||||
| @@ -528,6 +534,13 @@ cover: | |||||||
|   - platform: copy |   - platform: copy | ||||||
|     source_id: tuya_cover |     source_id: tuya_cover | ||||||
|     name: Tuya Cover copy |     name: Tuya Cover copy | ||||||
|  |   - platform: he60r | ||||||
|  |     uart_id: uart_he60r | ||||||
|  |     id: garage_door | ||||||
|  |     name: Garage Door | ||||||
|  |     open_duration: 14s | ||||||
|  |     close_duration: 14s | ||||||
|  |  | ||||||
|  |  | ||||||
| display: | display: | ||||||
|   - platform: addressable_light |   - platform: addressable_light | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user