mirror of
https://github.com/esphome/esphome.git
synced 2024-11-24 16:08:10 +01: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:
parent
6424f831e2
commit
391eff8fd5
6 changed files with 374 additions and 0 deletions
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue