Files
ESPHome-Devices/components/garage_door/garage_door.cpp
Chris Nussbaum 5820355a7f F/garage door (#45)
* Start work on the garage door controller

* More work on the Garage Door

* Another update

* Get garage door to compilable state

* Core functionality is mostly complete

* More work on the garage door

* Garage door (mostly) ready for testing

* Begin debugging

* More debugging work

* Closer to a working state

* Hoping this is close to the final version

* Couple more tweaks

* Still have some significant bugs

* So close!!

* Add sensor read delays

* Properly handle stopping when endstops are reached

* Adjust some timings

* Cleanup, more logging, and timing adjustments

* Fix typo

* Major refactor of the state management and fix the repeated disconnects

* More adjustments

* Add schematic

* Mostly complete

* Make device more configurable

---------

Co-authored-by: Chris Nussbaum <chris.nussbaum@protolabs.com>
2023-02-16 13:31:58 -06:00

558 lines
18 KiB
C++

#include "garage_door.h"
namespace esphome {
namespace cover {
using namespace esphome::lock;
static const char* TAG = "GarageDoor";
// Number of milliseconds between publishing the state while the door is opening or closing
const uint32_t DOOR_MOVING_PUBLISH_INTERVAL = 1000;
// Events
const std::string EVENT_LOCAL_LIGHT = "local_light_button";
const std::string EVENT_REMOTE_LIGHT = "remote_light_button";
const std::string EVENT_FAILED_OPEN = "open_failed";
const std::string EVENT_FAILED_CLOSE = "close_failed";
const std::string EVENT_BUTTON_DISCONNECTED = "button_disconnected";
void GarageDoorLock::setup() {
LockState restored_state{};
this->rtc_.load(&restored_state);
if (restored_state == LOCK_STATE_LOCKED) {
this->garage_door_->lock_();
}
}
void GarageDoorLock::control(const lock::LockCall &call) {
auto state = *call.get_state();
if (state == lock::LOCK_STATE_LOCKED) {
this->publish_state(lock::LOCK_STATE_LOCKING);
this->garage_door_->lock_();
} else if (state == lock::LOCK_STATE_UNLOCKED) {
this->garage_door_->unlock_();
}
}
GarageDoor::GarageDoor() {
this->lock_comp_ = new GarageDoorLock(this);
App.register_component(this->lock_comp_);
App.register_lock(this->lock_comp_);
}
void GarageDoor::set_button_sensor(sensor::Sensor *button_sensor) {
this->button_sensor_ = button_sensor;
this->button_sensor_->add_on_state_callback([this](float state) {
// Expected values:
// 0 = Disconnected
// 133 = None
// 434 = Light
// 714 = Lock
// 993 = Door
LocalButton currentButton;
if (state < 50) {
currentButton = LOCAL_BUTTON_DISCONNECTED;
} else if (state < 284) {
currentButton = LOCAL_BUTTON_NONE;
} else if (state < 574) {
currentButton = LOCAL_BUTTON_LIGHT;
} else if (state < 854) {
currentButton = LOCAL_BUTTON_LOCK;
} else {
currentButton = LOCAL_BUTTON_DOOR;
}
if (currentButton != this->last_local_button_) {
this->last_local_button_ = currentButton;
switch (currentButton) {
case LOCAL_BUTTON_DISCONNECTED:
ESP_LOGD(TAG, "Local button is disconnected");
ESP_LOGD(TAG, " Read Value: %u", state);
this->fire_homeassistant_event(this->get_event_(EVENT_BUTTON_DISCONNECTED));
break;
case LOCAL_BUTTON_NONE:
ESP_LOGD(TAG, "No local buttons currently pressed");
ESP_LOGD(TAG, " Read Value: %u", state);
break;
case LOCAL_BUTTON_LIGHT:
ESP_LOGD(TAG, "Local light button was pressed");
ESP_LOGD(TAG, " Read Value: %u", state);
this->fire_homeassistant_event(this->get_event_(EVENT_LOCAL_LIGHT));
break;
case LOCAL_BUTTON_LOCK:
ESP_LOGD(TAG, "Local lock button was pressed");
ESP_LOGD(TAG, " Read Value: %u", state);
if (this->internal_state_ == INTERNAL_STATE_LOCKED) {
this->unlock_();
} else {
this->lock_();
}
break;
case LOCAL_BUTTON_DOOR:
ESP_LOGD(TAG, "Local door button was pressed");
ESP_LOGD(TAG, " Read Value: %u", state);
this->handle_button_press_(true);
break;
default:
break;
}
}
});
}
void GarageDoor::set_closed_sensor(binary_sensor::BinarySensor *closed_sensor) {
this->closed_sensor_ = closed_sensor;
this->closed_sensor_->add_on_state_callback([this](bool state) {
if (state) {
ESP_LOGD(TAG, "Closed sensor is active");
this->set_state_(PHYSICAL_STATE_CLOSED, INTERNAL_STATE_CLOSED);
} else {
ESP_LOGD(TAG, "Closed sensor is inactive");
}
});
}
void GarageDoor::set_open_sensor(binary_sensor::BinarySensor *open_sensor) {
this->open_sensor_ = open_sensor;
this->open_sensor_->add_on_state_callback([this](bool state) {
if (state) {
ESP_LOGD(TAG, "Open sensor is active");
this->set_state_(PHYSICAL_STATE_OPEN, INTERNAL_STATE_OPEN);
} else {
ESP_LOGD(TAG, "Open sensor is inactive");
}
});
}
void GarageDoor::set_remote_sensor(binary_sensor::BinarySensor *remote_sensor) {
this->remote_sensor_ = remote_sensor;
this->remote_sensor_->add_on_state_callback([this](bool state) {
ESP_LOGD(TAG, "Remote door button was pressed");
if (state && this->internal_state_ != INTERNAL_STATE_LOCKED) {
this->handle_button_press_(false);
}
});
}
void GarageDoor::set_remote_light_sensor(binary_sensor::BinarySensor *remote_light_sensor) {
this->remote_light_sensor_ = remote_light_sensor;
this->remote_light_sensor_->add_on_state_callback([this](bool state) {
ESP_LOGD(TAG, "Remote light button was pressed");
this->fire_homeassistant_event(this->get_event_(EVENT_REMOTE_LIGHT));
});
}
void GarageDoor::set_warning_rtttl(rtttl::Rtttl *warning_rtttl) {
this->warning_rtttl_ = warning_rtttl;
this->warning_rtttl_->add_on_finished_playback_callback([this]() {
this->activate_control_output_();
});
}
cover::CoverTraits GarageDoor::get_traits() {
auto traits = CoverTraits();
traits.set_supports_position(true);
traits.set_supports_tilt(false);
traits.set_is_assumed_state(false);
return traits;
}
void GarageDoor::setup() {
if (this->closed_sensor_->state) {
this->set_state_(PHYSICAL_STATE_CLOSED, INTERNAL_STATE_CLOSED, true);
} else if (this->open_sensor_->state) {
this->set_state_(PHYSICAL_STATE_OPEN, INTERNAL_STATE_OPEN, true);
} else {
this->set_state_(PHYSICAL_STATE_UNKNOWN, INTERNAL_STATE_UNKNOWN, true);
}
}
void GarageDoor::loop() {
this->recompute_position_();
this->ensure_target_state_();
const uint32_t now = millis();
if (this->current_operation != COVER_OPERATION_IDLE && now - this->last_publish_time_ > DOOR_MOVING_PUBLISH_INTERVAL) {
this->publish_state(false);
this->last_publish_time_ = now;
}
}
void GarageDoor::control(const cover::CoverCall &call) {
if (call.get_stop()) {
this->target_state_ = TARGET_STATE_STOPPED;
} else if (call.get_position().has_value()) {
this->target_position_ = *call.get_position();
if (this->target_position_ == COVER_CLOSED) {
this->target_state_ = this->internal_state_ == INTERNAL_STATE_LOCKED ? TARGET_STATE_LOCKED : TARGET_STATE_CLOSED;
} else if (this->target_position_ == COVER_OPEN) {
this->target_state_ = TARGET_STATE_OPEN;
} else {
this->target_state_ = TARGET_STATE_POSITION;
}
if (!this->is_at_target_position_() && this->target_position_ < this->position && this->current_operation != COVER_OPERATION_CLOSING) {
this->warning_rtttl_->play(this->close_warning_tones_);
this->set_state_(this->physical_state_, INTERNAL_STATE_CLOSE_WARNING);
}
}
}
void GarageDoor::lock_() {
this->target_state_ = TARGET_STATE_LOCKED;
}
void GarageDoor::unlock_() {
if (this->internal_state_ == INTERNAL_STATE_LOCKED) {
this->target_state_ = TARGET_STATE_CLOSED;
}
}
void GarageDoor::recompute_position_() {
float direction;
float normal_duration;
switch (this->internal_state_) {
case INTERNAL_STATE_OPENING:
direction = 1.0f;
normal_duration = this->open_duration_;
break;
case INTERNAL_STATE_CLOSING:
direction = -1.0f;
normal_duration = this->close_duration_;
break;
case INTERNAL_STATE_UNKNOWN:
if (this->closed_sensor_->state) {
this->set_state_(PHYSICAL_STATE_CLOSED, INTERNAL_STATE_CLOSED);
} else if (this->open_sensor_->state) {
this->set_state_(PHYSICAL_STATE_OPEN, INTERNAL_STATE_OPEN);
}
return;
default:
return;
}
const uint32_t now = millis();
this->position += direction * (now - this->last_recompute_time_) / normal_duration;
this->position = clamp(this->position, 0.01f, .99f);
this->last_recompute_time_ = now;
}
void GarageDoor::ensure_target_state_() {
const uint32_t now = millis();
if (this->control_output_state_) {
if (now - this->control_output_state_change_time_ >= this->control_active_duration_) {
this->control_output_->turn_off();
this->control_output_state_ = false;
this->control_output_state_change_time_ = now;
}
return;
} else if (now - this->control_output_state_change_time_ < this->control_inactive_duration_) {
return;
}
switch (this->target_state_) {
case TARGET_STATE_LOCKED:
switch (this->internal_state_) {
case INTERNAL_STATE_LOCKED:
this->target_state_ = TARGET_STATE_NONE;
break;
case INTERNAL_STATE_CLOSED:
this->set_state_(PHYSICAL_STATE_CLOSED, INTERNAL_STATE_LOCKED);
this->target_state_ = TARGET_STATE_NONE;
break;
case INTERNAL_STATE_UNKNOWN:
case INTERNAL_STATE_OPENING:
case INTERNAL_STATE_STOPPED:
case INTERNAL_STATE_OPEN:
this->activate_control_output_();
break;
case INTERNAL_STATE_MOVING:
case INTERNAL_STATE_CLOSE_WARNING:
case INTERNAL_STATE_CLOSING:
default:
break;
}
break;
case TARGET_STATE_CLOSED:
switch (this->internal_state_) {
case INTERNAL_STATE_CLOSED:
this->target_state_ = TARGET_STATE_NONE;
break;
case INTERNAL_STATE_LOCKED:
this->set_state_(PHYSICAL_STATE_CLOSED, INTERNAL_STATE_CLOSED);
this->target_state_ = TARGET_STATE_NONE;
break;
case INTERNAL_STATE_UNKNOWN:
case INTERNAL_STATE_OPENING:
case INTERNAL_STATE_STOPPED:
case INTERNAL_STATE_OPEN:
this->activate_control_output_();
break;
case INTERNAL_STATE_MOVING:
case INTERNAL_STATE_CLOSE_WARNING:
case INTERNAL_STATE_CLOSING:
default:
break;
}
break;
case TARGET_STATE_OPEN:
switch (this->internal_state_) {
case INTERNAL_STATE_OPEN:
this->target_state_ = TARGET_STATE_NONE;
break;
case INTERNAL_STATE_UNKNOWN:
case INTERNAL_STATE_LOCKED:
case INTERNAL_STATE_CLOSED:
case INTERNAL_STATE_STOPPED:
case INTERNAL_STATE_CLOSING:
this->activate_control_output_();
break;
case INTERNAL_STATE_CLOSE_WARNING:
this->warning_rtttl_->stop();
if (this->physical_state_ == PHYSICAL_STATE_OPEN) {
this->set_state_(PHYSICAL_STATE_OPEN, INTERNAL_STATE_OPEN);
this->target_state_ = TARGET_STATE_NONE;
} else {
this->activate_control_output_();
}
break;
case INTERNAL_STATE_MOVING:
case INTERNAL_STATE_OPENING:
default:
break;
}
break;
case TARGET_STATE_STOPPED:
switch (this->internal_state_) {
case INTERNAL_STATE_MOVING:
case INTERNAL_STATE_OPENING:
case INTERNAL_STATE_CLOSING:
this->activate_control_output_();
break;
case INTERNAL_STATE_CLOSE_WARNING:
this->warning_rtttl_->stop();
if (this->physical_state_ == PHYSICAL_STATE_OPEN) {
this->set_state_(PHYSICAL_STATE_OPEN, INTERNAL_STATE_OPEN);
} else {
this->set_state_(this->physical_state_, INTERNAL_STATE_STOPPED);
}
this->target_state_ = TARGET_STATE_NONE;
break;
case INTERNAL_STATE_UNKNOWN:
case INTERNAL_STATE_LOCKED:
case INTERNAL_STATE_CLOSED:
case INTERNAL_STATE_STOPPED:
case INTERNAL_STATE_OPEN:
this->target_state_ = TARGET_STATE_NONE;
break;
default:
break;
}
break;
case TARGET_STATE_POSITION:
switch (this->internal_state_) {
case INTERNAL_STATE_UNKNOWN:
case INTERNAL_STATE_LOCKED:
case INTERNAL_STATE_CLOSED:
case INTERNAL_STATE_OPEN:
this->activate_control_output_();
break;
case INTERNAL_STATE_OPENING:
if (this->is_at_target_position_() || this->position > this->target_position_) {
this->activate_control_output_();
}
break;
case INTERNAL_STATE_STOPPED:
if (!this->is_at_target_position_()) {
this->activate_control_output_();
} else {
this->target_state_ = TARGET_STATE_NONE;
}
break;
case INTERNAL_STATE_CLOSE_WARNING:
if (this->is_at_target_position_()) {
this->warning_rtttl_->stop();
this->set_state_(this->physical_state_, INTERNAL_STATE_STOPPED);
this->target_state_ = TARGET_STATE_NONE;
} else if (this->position < this->target_position_) {
this->warning_rtttl_->stop();
this->activate_control_output_();
}
break;
case INTERNAL_STATE_CLOSING:
if (this->is_at_target_position_() || this->position < this->target_position_) {
this->activate_control_output_();
}
break;
case INTERNAL_STATE_MOVING:
default:
break;
}
break;
default:
break;
}
}
void GarageDoor::activate_control_output_() {
const uint32_t now = millis();
this->control_output_->turn_on();
this->control_output_state_ = true;
this->control_output_state_change_time_ = now;
this->last_recompute_time_ = now;
switch (this->physical_state_) {
case PHYSICAL_STATE_UNKNOWN:
this->set_state_(PHYSICAL_STATE_MOVING, INTERNAL_STATE_MOVING);
break;
case PHYSICAL_STATE_MOVING:
this->set_state_(PHYSICAL_STATE_UNKNOWN, INTERNAL_STATE_UNKNOWN);
break;
case PHYSICAL_STATE_CLOSED:
this->set_state_(PHYSICAL_STATE_OPENING, INTERNAL_STATE_OPENING);
break;
case PHYSICAL_STATE_OPENING:
if (this->reverses_on_stop_opening_) {
this->set_state_(PHYSICAL_STATE_CLOSING, INTERNAL_STATE_CLOSING);
} else {
this->set_state_(PHYSICAL_STATE_STOPPED_OPENING, INTERNAL_STATE_STOPPED);
}
break;
case PHYSICAL_STATE_STOPPED_OPENING:
this->set_state_(PHYSICAL_STATE_CLOSING, INTERNAL_STATE_CLOSING);
break;
case PHYSICAL_STATE_OPEN:
this->set_state_(PHYSICAL_STATE_CLOSING, INTERNAL_STATE_CLOSING);
break;
case PHYSICAL_STATE_CLOSING:
if (this->reverses_on_stop_closing_) {
this->set_state_(PHYSICAL_STATE_OPENING, INTERNAL_STATE_OPENING);
} else {
this->set_state_(PHYSICAL_STATE_STOPPED_CLOSING, INTERNAL_STATE_STOPPED);
}
break;
case PHYSICAL_STATE_STOPPED_CLOSING:
this->set_state_(PHYSICAL_STATE_OPENING, INTERNAL_STATE_OPENING);
break;
default:
break;
}
}
void GarageDoor::handle_button_press_(bool is_local) {
switch (this->internal_state_) {
case INTERNAL_STATE_UNKNOWN:
case INTERNAL_STATE_OPEN:
this->target_state_ = TARGET_STATE_CLOSED;
break;
case INTERNAL_STATE_MOVING:
case INTERNAL_STATE_OPENING:
case INTERNAL_STATE_CLOSE_WARNING:
case INTERNAL_STATE_CLOSING:
this->target_state_ = TARGET_STATE_STOPPED;
break;
case INTERNAL_STATE_LOCKED:
if (is_local) {
this->target_state_ = TARGET_STATE_OPEN;
}
break;
case INTERNAL_STATE_CLOSED:
this->target_state_ = TARGET_STATE_OPEN;
break;
case INTERNAL_STATE_STOPPED:
if (this->physical_state_ == PHYSICAL_STATE_STOPPED_OPENING) {
this->target_state_ = TARGET_STATE_CLOSED;
} else if (this->physical_state_ == PHYSICAL_STATE_STOPPED_CLOSING) {
this->target_state_ = TARGET_STATE_OPEN;
}
break;
default:
break;
}
}
void GarageDoor::set_state_(PhysicalState physical_state, InternalState internal_state, bool is_initial_state) {
if (is_initial_state) {
ESP_LOGD(TAG, "Setting Initial Physical State: '%s'", physical_state_names[physical_state]);
ESP_LOGD(TAG, "Setting Initial Internal State: '%s'", internal_state_names[internal_state]);
} else if (this->physical_state_ != physical_state || this->internal_state_ != internal_state) {
if (this->physical_state_ != physical_state) {
ESP_LOGD(TAG, "Setting Physical State:");
ESP_LOGD(TAG, " Current State: '%s'", physical_state_names[this->physical_state_]);
ESP_LOGD(TAG, " New State: '%s'", physical_state_names[physical_state]);
}
if (this->internal_state_ != internal_state) {
ESP_LOGD(TAG, "Setting Internal State:");
ESP_LOGD(TAG, " Current State: '%s'", internal_state_names[this->internal_state_]);
ESP_LOGD(TAG, " New State: '%s'", internal_state_names[internal_state]);
}
} else {
return;
}
if (this->physical_state_ != physical_state) {
const uint32_t now = millis();
if (this->last_open_time_sensor_ != nullptr && this->previous_physical_state_ == PHYSICAL_STATE_CLOSED && physical_state == PHYSICAL_STATE_OPEN) {
uint32_t open_time = now - this->last_physical_state_change_time_;
this->last_open_time_sensor_->publish_state(open_time);
} else if (this->last_close_time_sensor_ != nullptr && this->previous_physical_state_ == PHYSICAL_STATE_OPEN && physical_state == PHYSICAL_STATE_CLOSED) {
uint32_t close_time = now - this->last_physical_state_change_time_;
this->last_close_time_sensor_->publish_state(close_time);
}
this->previous_physical_state_ = this->physical_state_;
this->last_physical_state_change_time_ = now;
this->physical_state_ = physical_state;
}
this->internal_state_ = internal_state;
switch (internal_state) {
case INTERNAL_STATE_UNKNOWN:
this->position = 0.5f;
this->current_operation = COVER_OPERATION_IDLE;
break;
case INTERNAL_STATE_MOVING:
this->position = 0.5f;
this->current_operation = COVER_OPERATION_CLOSING;
break;
case INTERNAL_STATE_LOCKED:
case INTERNAL_STATE_CLOSED:
this->position = COVER_CLOSED;
this->current_operation = COVER_OPERATION_IDLE;
break;
case INTERNAL_STATE_OPENING:
this->current_operation = COVER_OPERATION_OPENING;
break;
case INTERNAL_STATE_STOPPED:
this->current_operation = COVER_OPERATION_IDLE;
break;
case INTERNAL_STATE_OPEN:
this->position = COVER_OPEN;
this->current_operation = COVER_OPERATION_IDLE;
break;
case INTERNAL_STATE_CLOSE_WARNING:
case INTERNAL_STATE_CLOSING:
this->current_operation = COVER_OPERATION_CLOSING;
break;
default:
break;
}
this->publish_state(false);
this->lock_comp_->publish_state(internal_state == INTERNAL_STATE_LOCKED ? LOCK_STATE_LOCKED : LOCK_STATE_UNLOCKED);
}
std::string GarageDoor::get_event_(std::string event_type)
{
std::string event = "esphome.";
event.append(this->get_object_id());
event.append(".");
event.append(event_type);
return event;
}
} // namespace cover
} // namespace esphome