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>
This commit is contained in:
Chris Nussbaum 2023-02-16 13:31:58 -06:00 committed by GitHub
parent 9998ad39db
commit 5820355a7f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 1833 additions and 0 deletions

View File

View File

@ -0,0 +1,128 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import cover, output, sensor, binary_sensor, rtttl
from esphome.const import (
CONF_ID,
CONF_OPEN_DURATION,
CONF_CLOSE_DURATION,
UNIT_MILLISECOND,
DEVICE_CLASS_DURATION,
STATE_CLASS_MEASUREMENT,
ICON_TIMER,
ENTITY_CATEGORY_DIAGNOSTIC,
)
AUTO_LOAD = ["lock"]
CONF_CONTROL_OUTPUT = "control_output"
CONF_BUTTON_SENSOR = "button_sensor"
CONF_CLOSED_SENSOR = "closed_sensor"
CONF_OPEN_SENSOR = "open_sensor"
CONF_REMOTE_SENSOR = "remote_sensor"
CONF_REMOTE_LIGHT_SENSOR = "remote_light_sensor"
CONF_WARNING_RTTTL = "warning_rtttl"
CONF_CLOSE_WARNING_TONES = "close_warning_tones"
DEFAULT_CLOSE_WARNING_TONES = "Imperial:d=4, o=5, b=100:e, e, e, 8c, 16p, 16g, e, 8c, 16p, 16g, e, p, b, b, b, 8c6, 16p, 16g, d#, 8c, 16p, 16g, e, 8p"
CONF_CONTROL_ACTIVE_DURATION = "control_active_duration"
CONF_CONTROL_INACTIVE_DURATION = "control_inactive_duration"
CONF_REVERSES_ON_STOP_OPENING = "reverses_on_stop_opening"
CONF_REVERSES_ON_STOP_CLOSING = "reverses_on_stop_closing"
CONF_LAST_OPEN_TIME_SENSOR = "last_open_time_sensor"
CONF_LAST_CLOSE_TIME_SENSOR = "last_close_time_sensor"
DEVICE_CLASS = "garage"
cover_ns = cg.esphome_ns.namespace("cover")
GarageDoor = cover_ns.class_("GarageDoor", cover.Cover, cg.PollingComponent)
CONFIG_SCHEMA = (
cover.COVER_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(GarageDoor),
cv.Required(CONF_OPEN_DURATION): cv.positive_time_period_milliseconds,
cv.Required(CONF_CLOSE_DURATION): cv.positive_time_period_milliseconds,
cv.Required(CONF_CONTROL_OUTPUT): cv.use_id(output.BinaryOutput),
cv.Required(CONF_BUTTON_SENSOR): cv.use_id(sensor.Sensor),
cv.Required(CONF_CLOSED_SENSOR): cv.use_id(binary_sensor.BinarySensor),
cv.Required(CONF_OPEN_SENSOR): cv.use_id(binary_sensor.BinarySensor),
cv.Required(CONF_REMOTE_SENSOR): cv.use_id(binary_sensor.BinarySensor),
cv.Required(CONF_REMOTE_LIGHT_SENSOR): cv.use_id(binary_sensor.BinarySensor),
cv.Required(CONF_WARNING_RTTTL): cv.use_id(rtttl.Rtttl),
cv.Optional(CONF_CLOSE_WARNING_TONES, default=DEFAULT_CLOSE_WARNING_TONES): cv.string,
cv.Optional(CONF_CONTROL_ACTIVE_DURATION, default="200ms"): cv.positive_time_period_milliseconds,
cv.Optional(CONF_CONTROL_INACTIVE_DURATION, default="200ms"): cv.positive_time_period_milliseconds,
cv.Optional(CONF_REVERSES_ON_STOP_OPENING, default="false"): cv.boolean,
cv.Optional(CONF_REVERSES_ON_STOP_CLOSING, default="false"): cv.boolean,
cv.Optional(CONF_LAST_OPEN_TIME_SENSOR): sensor.sensor_schema(
unit_of_measurement=UNIT_MILLISECOND,
device_class=DEVICE_CLASS_DURATION,
state_class=STATE_CLASS_MEASUREMENT,
icon=ICON_TIMER,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
cv.Optional(CONF_LAST_CLOSE_TIME_SENSOR): sensor.sensor_schema(
unit_of_measurement=UNIT_MILLISECOND,
device_class=DEVICE_CLASS_DURATION,
state_class=STATE_CLASS_MEASUREMENT,
icon=ICON_TIMER,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
}
)
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await cover.register_cover(var, config)
cg.add(var.set_device_class(DEVICE_CLASS))
cg.add(var.set_open_duration(config[CONF_OPEN_DURATION]))
cg.add(var.set_close_duration(config[CONF_CLOSE_DURATION]))
control_output = await cg.get_variable(config[CONF_CONTROL_OUTPUT])
cg.add(var.set_control_output(control_output))
button_sensor = await cg.get_variable(config[CONF_BUTTON_SENSOR])
cg.add(var.set_button_sensor(button_sensor))
closed_sensor = await cg.get_variable(config[CONF_CLOSED_SENSOR])
cg.add(var.set_closed_sensor(closed_sensor))
open_sensor = await cg.get_variable(config[CONF_OPEN_SENSOR])
cg.add(var.set_open_sensor(open_sensor))
remote_sensor = await cg.get_variable(config[CONF_REMOTE_SENSOR])
cg.add(var.set_remote_sensor(remote_sensor))
remote_light_sensor = await cg.get_variable(config[CONF_REMOTE_LIGHT_SENSOR])
cg.add(var.set_remote_light_sensor(remote_light_sensor))
warning_rtttl = await cg.get_variable(config[CONF_WARNING_RTTTL])
cg.add(var.set_warning_rtttl(warning_rtttl))
close_warning_tones = config[CONF_CLOSE_WARNING_TONES]
cg.add(var.set_close_warning_tones(close_warning_tones))
control_active_duration = config[CONF_CONTROL_ACTIVE_DURATION]
cg.add(var.set_control_active_duration(control_active_duration))
control_inactive_duration = config[CONF_CONTROL_INACTIVE_DURATION]
cg.add(var.set_control_inactive_duration(control_inactive_duration))
reverses_on_stop_opening = config[CONF_REVERSES_ON_STOP_OPENING]
cg.add(var.set_reverses_on_stop_opening(reverses_on_stop_opening))
reverses_on_stop_closing = config[CONF_REVERSES_ON_STOP_CLOSING]
cg.add(var.set_reverses_on_stop_closing(reverses_on_stop_closing))
if CONF_LAST_OPEN_TIME_SENSOR in config:
last_open_time_config = config[CONF_LAST_OPEN_TIME_SENSOR]
last_open_time_sensor = await sensor.new_sensor(last_open_time_config)
cg.add(var.set_last_open_time_sensor(last_open_time_sensor))
if CONF_LAST_CLOSE_TIME_SENSOR in config:
last_close_time_config = config[CONF_LAST_CLOSE_TIME_SENSOR]
last_close_time_sensor = await sensor.new_sensor(last_close_time_config)
cg.add(var.set_last_close_time_sensor(last_close_time_sensor))

View File

@ -0,0 +1,557 @@
#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

View File

@ -0,0 +1,156 @@
#pragma once
#include <stdio.h>
#include "esphome/core/application.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/components/api/custom_api_device.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
#include "esphome/components/cover/cover.h"
#include "esphome/components/lock/lock.h"
#include "esphome/components/output/binary_output.h"
#include "esphome/components/rtttl/rtttl.h"
#include "esphome/components/sensor/sensor.h"
namespace esphome {
namespace cover {
#define PHYSICAL_STATES \
P(UNKNOWN) \
P(MOVING) \
P(CLOSED) \
P(OPENING) \
P(STOPPED_OPENING) \
P(OPEN) \
P(CLOSING) \
P(STOPPED_CLOSING)
#define P(x) PHYSICAL_STATE_##x,
enum PhysicalState { PHYSICAL_STATES P };
#undef P
#define P(x) #x,
const char * const physical_state_names[] = { PHYSICAL_STATES };
#define INTERNAL_STATES \
I(UNKNOWN) \
I(MOVING) \
I(LOCKED) \
I(CLOSED) \
I(OPENING) \
I(STOPPED) \
I(OPEN) \
I(CLOSE_WARNING) \
I(CLOSING)
#define I(x) INTERNAL_STATE_##x,
enum InternalState { INTERNAL_STATES I };
#undef I
#define I(x) #x,
const char * const internal_state_names[] = { INTERNAL_STATES };
enum TargetState {
TARGET_STATE_NONE,
TARGET_STATE_LOCKED,
TARGET_STATE_CLOSED,
TARGET_STATE_OPEN,
TARGET_STATE_STOPPED,
TARGET_STATE_POSITION,
};
enum LocalButton {
LOCAL_BUTTON_DISCONNECTED, // The button is not connected resulting in 0 volts at pin A0
LOCAL_BUTTON_NONE,
LOCAL_BUTTON_DOOR,
LOCAL_BUTTON_LOCK,
LOCAL_BUTTON_LIGHT
};
class GarageDoor;
class GarageDoorLock : public lock::Lock, public Component
{
public:
GarageDoorLock(GarageDoor *garage_door) : garage_door_(garage_door) {}
float get_setup_priority() const override { return setup_priority::DATA; }
void setup() override;
void loop() override {}
protected:
void control(const lock::LockCall &call) override;
GarageDoor *garage_door_{nullptr};
};
class GarageDoor : public cover::Cover, public Component, public api::CustomAPIDevice {
public:
GarageDoor();
void set_name(const std::string &name) { Cover::set_name(name); this->lock_comp_->set_name(name); }
void set_open_duration(uint32_t open_duration) { this->open_duration_ = open_duration; }
void set_close_duration(uint32_t close_duration) { this->close_duration_ = close_duration; }
void set_control_output(output::BinaryOutput *control_output) { this->control_output_ = control_output; }
void set_button_sensor(sensor::Sensor *button_sensor);
void set_closed_sensor(binary_sensor::BinarySensor *closed_sensor);
void set_open_sensor(binary_sensor::BinarySensor *open_sensor);
void set_remote_sensor(binary_sensor::BinarySensor *remote_sensor);
void set_remote_light_sensor(binary_sensor::BinarySensor *remote_light_sensor);
void set_warning_rtttl(rtttl::Rtttl *warning_rtttl);
void set_close_warning_tones(const std::string &close_warning_tones) { this->close_warning_tones_ = close_warning_tones; }
void set_control_active_duration(uint32_t control_active_duration) { this->control_active_duration_ = control_active_duration; }
void set_control_inactive_duration(uint32_t control_inactive_duration) { this->control_inactive_duration_ = control_inactive_duration; }
void set_reverses_on_stop_opening(bool reverses_on_stop_opening) { this->reverses_on_stop_opening_ = reverses_on_stop_opening; }
void set_reverses_on_stop_closing(bool reverses_on_stop_closing) { this->reverses_on_stop_closing_ = reverses_on_stop_closing; }
void set_last_open_time_sensor(sensor::Sensor *last_open_time_sensor) { this->last_open_time_sensor_ = last_open_time_sensor; }
void set_last_close_time_sensor(sensor::Sensor *last_close_time_sensor) { this->last_close_time_sensor_ = last_close_time_sensor; }
float get_setup_priority() const override { return setup_priority::DATA; }
cover::CoverTraits get_traits() override;
void setup() override;
void loop() override;
protected:
friend GarageDoorLock;
GarageDoorLock *lock_comp_;
uint32_t open_duration_;
uint32_t close_duration_;
output::BinaryOutput *control_output_;
sensor::Sensor *button_sensor_;
binary_sensor::BinarySensor *closed_sensor_;
binary_sensor::BinarySensor *open_sensor_;
binary_sensor::BinarySensor *remote_sensor_;
binary_sensor::BinarySensor *remote_light_sensor_;
rtttl::Rtttl *warning_rtttl_;
std::string close_warning_tones_;
uint32_t control_active_duration_;
uint32_t control_inactive_duration_;
bool reverses_on_stop_opening_{false};
bool reverses_on_stop_closing_{false};
sensor::Sensor *last_open_time_sensor_{nullptr};
sensor::Sensor *last_close_time_sensor_{nullptr};
PhysicalState previous_physical_state_{PHYSICAL_STATE_UNKNOWN};
PhysicalState physical_state_{PHYSICAL_STATE_UNKNOWN};
uint32_t last_physical_state_change_time_{0};
InternalState internal_state_{INTERNAL_STATE_UNKNOWN};
TargetState target_state_{TARGET_STATE_NONE};
float target_position_{0};
LocalButton last_local_button_{LOCAL_BUTTON_NONE};
uint32_t last_recompute_time_{0};
bool control_output_state_{false};
uint32_t control_output_state_change_time_{0};
uint32_t last_publish_time_{0};
void control(const cover::CoverCall &call) override;
void lock_();
void unlock_();
void recompute_position_();
void ensure_target_state_();
bool is_at_target_position_() { return (this->target_position_ - 0.05f) <= this->position && this->position <= (this->target_position_ + 0.05f); }
void activate_control_output_();
void handle_button_press_(bool is_local);
void set_state_(PhysicalState physical_state, InternalState internal_state, bool is_initial_state = false);
std::string get_event_(std::string event_type);
};
} // namespace cover
} // namespace esphome

885
custom/GarageDoor.h Normal file
View File

@ -0,0 +1,885 @@
#include "esphome.h"
using namespace esphome;
static const char* TAG = "NuttyGarageDoor";
const bool HAS_INTERNAL_CLOSED_SENSOR = true;
const bool HAS_INTERNAL_OPEN_SENSOR = true;
// Open/Close durations used to determine position while opening/closing
const uint32_t NORMAL_OPEN_DURATION = 13000;
const uint32_t NORMAL_CLOSE_DURATION = 12000;
// Minimum number of milliseconds to keep the control pin active/inactive when changing states
const uint32_t CONTROL_PIN_ACTIVE_DURATION = 200;
const uint32_t CONTROL_PIN_INACTIVE_DURATION = 200;
// Number of milliseconds between publishing the state while the door is opening or closing
const uint32_t DOOR_MOVING_PUBLISH_INTERVAL = 750;
// Number of milliseconds between reads of the ADC to get local button state, this is needed to prevent wifi issues from reading to frequently
const uint32_t LOCAL_BUTTON_READ_INTERVAL = 75;
// Number of milliseconds to wait before checking the open/close sensors after starting to open/close the door to prevent false "failed" triggers
const uint32_t SENSOR_READ_DELAY = 1000;
// Close warning
const std::string CLOSE_WARNING_RTTTL = "Imperial:d=4, o=5, b=100:e, e, e, 8c, 16p, 16g, e, 8c, 16p, 16g, e, p, b, b, b, 8c6, 16p, 16g, d#, 8c, 16p, 16g, e, 8p";
const uint8_t PIN_CONTROL_RELAY = D1;
const uint8_t PIN_CLOSED_SENSOR = D2;
const uint8_t PIN_OPEN_SENSOR = D7;
const uint8_t PIN_REMOTE_BUTTON = D5;
const uint8_t PIN_REMOTE_LIGHT_BUTTON = D6;
const uint8_t PIN_STATUS_LED = D4;
const uint8_t PIN_CLOSE_WARNING_BUZZER = D8;
// 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";
// Lock Binary sensor state aliases
const bool LOCK_STATE_LOCKED = false;
const bool LOCK_STATE_UNLOCKED = true;
enum StateChangeType : uint8_t {
STATE_CHANGE_INTERNAL = 0,
STATE_CHANGE_BUTTON,
STATE_CHANGE_CLOSE_SENSOR,
STATE_CHANGE_OPEN_SENSOR,
STATE_CHANGE_CANCEL_WARNING
};
enum InternalState : uint8_t {
// On startup the state is unknown
STATE_UNKNOWN = 0,
// The door is moving from an unknown state so which direction it is moving is unknown
STATE_MOVING,
// The door is "locked" which is the same as closed but will not open for remotes (but local buttons and Home Assistant can open or unlock it)
STATE_LOCKED,
// The door is closed
STATE_CLOSED,
// The door is opening
STATE_OPENING,
// The door was stopped while it was opening
STATE_STOPPED_OPENING,
// The door is open
STATE_OPEN,
// The door is currently at least partially open but was requested closed by Home Assistant so we are waiting to close after an alert (beeper)
STATE_CLOSE_WARNING,
// The door is closing
STATE_CLOSING,
// The door was stopped while it was closing
STATE_STOPPED_CLOSING,
// Special states that the door will never actually be in but are used as target states
// Stopped where ever it currently is at
STATE_STOPPED,
// Stopped at a specific position
STATE_POSITION,
// No currently requested state
STATE_NONE
};
enum LocalButton : uint8_t {
LOCAL_BUTTON_DISCONNECTED = 0, // The button is not connected resulting in 0 volts at pin A0
LOCAL_BUTTON_NONE,
LOCAL_BUTTON_DOOR,
LOCAL_BUTTON_LOCK,
LOCAL_BUTTON_LIGHT
};
class GarageDoorLock : public binary_sensor::BinarySensor, public Component
{
public:
GarageDoorLock() {}
float get_setup_priority() const override { return setup_priority::DATA; }
void setup() override {}
void loop() override {}
};
class GarageDoorCover : public cover::Cover, public Component, public api::CustomAPIDevice
{
public:
GarageDoorCover();
void set_rtttl_buzzer(rtttl::Rtttl *buzzer) { this->buzzer_ = buzzer; }
float get_setup_priority() const override { return setup_priority::DATA; }
void setup() override;
cover::CoverTraits get_traits() override;
void control(const cover::CoverCall &call) override;
void loop() override;
GarageDoorLock *get_lock_sensor() { return this->lock_sensor_; }
protected:
rtttl::Rtttl *buzzer_;
esphome::esp8266::ESP8266GPIOPin *control_pin_;
esphome::esp8266::ESP8266GPIOPin *closed_pin_;
esphome::esp8266::ESP8266GPIOPin *open_pin_;
esphome::esp8266::ESP8266GPIOPin *remote_pin_;
esphome::esp8266::ESP8266GPIOPin *remote_light_pin_;
GarageDoorLock *lock_sensor_;
InternalState internal_state_{STATE_UNKNOWN};
InternalState target_state_{STATE_NONE};
float target_position_{0};
uint32_t control_pin_active_time_{0};
uint32_t control_pin_inactive_time_{0};
uint32_t last_state_change_time_{0};
uint32_t last_position_time_{0};
uint32_t last_publish_time_{0};
uint32_t last_local_button_read_time_{0};
LocalButton last_local_button_{LOCAL_BUTTON_NONE};
bool last_remote_state_{false};
bool last_remote_light_state_{false};
void lock_();
void unlock_();
bool check_control_pin_();
bool check_for_closed_position_();
bool check_for_open_position_();
bool check_for_position_update_();
bool ensure_target_state_();
bool check_local_buttons_();
bool check_remote_buttons_();
void change_to_next_state_(StateChangeType change_type = STATE_CHANGE_INTERNAL);
void set_internal_state_(InternalState state, bool is_initial_state = false);
InternalState get_internal_state_() { return this->internal_state_; }
std::string get_event_(std::string event_type);
const char *internal_state_to_str_(InternalState state);
};
GarageDoorCover::GarageDoorCover()
{
this->set_device_class("garage");
this->control_pin_ = new esphome::esp8266::ESP8266GPIOPin();
this->control_pin_->set_pin(PIN_CONTROL_RELAY);
this->control_pin_->set_inverted(false);
this->control_pin_->set_flags(gpio::Flags::FLAG_OUTPUT);
this->closed_pin_ = new esphome::esp8266::ESP8266GPIOPin();
this->closed_pin_->set_pin(PIN_CLOSED_SENSOR);
this->closed_pin_->set_inverted(true);
this->closed_pin_->set_flags(gpio::Flags::FLAG_PULLUP);
this->open_pin_ = new esphome::esp8266::ESP8266GPIOPin();
this->open_pin_->set_pin(PIN_OPEN_SENSOR);
this->open_pin_->set_inverted(true);
this->open_pin_->set_flags(gpio::Flags::FLAG_PULLUP);
this->remote_pin_ = new esphome::esp8266::ESP8266GPIOPin();
this->remote_pin_->set_pin(PIN_REMOTE_BUTTON);
this->remote_pin_->set_inverted(true);
this->remote_pin_->set_flags(gpio::Flags::FLAG_PULLUP);
this->remote_light_pin_ = new esphome::esp8266::ESP8266GPIOPin();
this->remote_light_pin_->set_pin(PIN_REMOTE_LIGHT_BUTTON);
this->remote_light_pin_->set_inverted(true);
this->remote_light_pin_->set_flags(gpio::Flags::FLAG_PULLUP);
this->lock_sensor_ = new GarageDoorLock();
}
void GarageDoorCover::setup()
{
this->control_pin_->setup();
this->closed_pin_->setup();
this->open_pin_->setup();
this->remote_pin_->setup();
this->remote_light_pin_->setup();
this->lock_sensor_->publish_initial_state(LOCK_STATE_UNLOCKED);
if (this->closed_pin_->digital_read())
{
this->set_internal_state_(STATE_CLOSED, true);
}
else if (this->open_pin_->digital_read())
{
this->set_internal_state_(STATE_OPEN, true);
}
else
{
this->set_internal_state_(STATE_UNKNOWN, true);
}
this->register_service(&GarageDoorCover::lock_, "lock");
this->register_service(&GarageDoorCover::unlock_, "unlock");
}
cover::CoverTraits GarageDoorCover::get_traits() {
auto traits = CoverTraits();
traits.set_is_assumed_state(false);
traits.set_supports_position(true);
traits.set_supports_tilt(false);
return traits;
}
void GarageDoorCover::control(const cover::CoverCall &call)
{
this->change_to_next_state_(STATE_CHANGE_CANCEL_WARNING);
if (call.get_stop())
{
this->target_state_ = STATE_STOPPED;
}
else if (call.get_position().has_value())
{
this->target_position_ = *call.get_position();
if (this->target_position_ == this->position && this->current_operation == COVER_OPERATION_IDLE && this->get_internal_state_() != STATE_UNKNOWN)
{
return;
}
else if (this->target_position_ == COVER_CLOSED)
{
this->target_state_ = this->get_internal_state_() == STATE_LOCKED ? STATE_LOCKED : STATE_CLOSED;
}
else if (this->target_position_ == COVER_OPEN)
{
this->target_state_ = STATE_OPEN;
}
else
{
this->target_state_ = STATE_POSITION;
}
}
}
void GarageDoorCover::loop()
{
if (this->check_control_pin_())
{
return;
}
if (this->check_for_closed_position_())
{
return;
}
if (this->check_for_open_position_())
{
return;
}
if (this->check_for_position_update_())
{
return;
}
if (this->ensure_target_state_())
{
return;
}
if (this->check_local_buttons_())
{
return;
}
if (this->check_remote_buttons_())
{
return;
}
}
void GarageDoorCover::lock_()
{
if (this->get_internal_state_() != STATE_LOCKED)
{
this->target_state_ = STATE_LOCKED;
}
}
void GarageDoorCover::unlock_()
{
if (this->get_internal_state_() == STATE_LOCKED)
{
this->target_state_ = STATE_CLOSED;
}
}
bool GarageDoorCover::check_control_pin_()
{
if (this->control_pin_active_time_ > 0)
{
const uint32_t now = millis();
if (now - this->control_pin_active_time_ >= CONTROL_PIN_ACTIVE_DURATION)
{
this->control_pin_->digital_write(false);
this->control_pin_active_time_ = 0;
this->control_pin_inactive_time_ = now;
}
return true;
}
else
{
return false;
}
}
bool GarageDoorCover::check_for_closed_position_()
{
if (this->current_operation != COVER_OPERATION_IDLE && (millis() - this->last_state_change_time_) >= SENSOR_READ_DELAY && this->closed_pin_->digital_read())
{
ESP_LOGD(TAG, "Door closed sensor is active");
if (this->get_internal_state_() == STATE_OPENING)
{
this->target_state_ = STATE_NONE;
this->fire_homeassistant_event(this->get_event_(EVENT_FAILED_OPEN));
}
this->change_to_next_state_(STATE_CHANGE_CLOSE_SENSOR);
return true;
}
else
{
return false;
}
}
bool GarageDoorCover::check_for_open_position_()
{
if (this->current_operation != COVER_OPERATION_IDLE && (millis() - this->last_state_change_time_) >= SENSOR_READ_DELAY && this->open_pin_->digital_read())
{
ESP_LOGD(TAG, "Door open sensor is active");
if (this->get_internal_state_() == STATE_CLOSING)
{
this->target_state_ = STATE_NONE;
this->fire_homeassistant_event(this->get_event_(EVENT_FAILED_CLOSE));
}
this->change_to_next_state_(STATE_CHANGE_OPEN_SENSOR);
return true;
}
else
{
return false;
}
}
bool GarageDoorCover::check_for_position_update_()
{
float direction;
uint32_t normal_duration;
switch (this->get_internal_state_())
{
case STATE_OPENING:
direction = 1.0f;
normal_duration = NORMAL_OPEN_DURATION;
break;
case STATE_CLOSING:
direction = -1.0f;
normal_duration = NORMAL_CLOSE_DURATION;
break;
default:
return false;
}
const uint32_t now = millis();
const uint32_t current_duration = now - this->last_state_change_time_;
if (current_duration > normal_duration * 2)
{
// This should never happen but if it does we go into an unknown state
this->set_internal_state_(STATE_UNKNOWN);
return true;
}
else
{
this->position = clamp(this->position + (direction * (now - this->last_position_time_) / normal_duration), 0.01f, 0.99f);
this->last_position_time_ = now;
if (this->target_state_ == STATE_POSITION && this->position == this->target_position_)
{
return false;
}
else if (now - this->last_publish_time_ >= DOOR_MOVING_PUBLISH_INTERVAL)
{
this->publish_state(false);
this->last_publish_time_ = now;
return true;
}
else
{
return false;
}
}
}
bool GarageDoorCover::ensure_target_state_()
{
if (!this->buzzer_->is_playing() && this->target_state_ != STATE_NONE)
{
InternalState current_state = this->get_internal_state_();
switch (this->target_state_)
{
case STATE_LOCKED:
if (current_state == STATE_LOCKED)
{
this->target_state_ = STATE_NONE;
return false;
}
if (current_state == STATE_MOVING || current_state == STATE_CLOSING)
{
return false;
}
else if (current_state == STATE_CLOSED)
{
this->set_internal_state_(STATE_LOCKED);
return true;
}
else
{
this->change_to_next_state_();
return true;
}
case STATE_CLOSED:
if (current_state == STATE_CLOSED)
{
this->target_state_ = STATE_NONE;
return false;
}
if (current_state == STATE_MOVING || current_state == STATE_CLOSING)
{
return false;
}
else if (current_state == STATE_LOCKED)
{
this->set_internal_state_(STATE_CLOSED);
this->target_state_ = STATE_NONE;
return true;
}
else
{
this->change_to_next_state_();
return true;
}
case STATE_OPEN:
if (current_state == STATE_OPEN)
{
this->target_state_ = STATE_NONE;
return false;
}
if (current_state == STATE_MOVING || current_state == STATE_OPENING)
{
return false;
}
else
{
this->change_to_next_state_();
return true;
}
case STATE_POSITION:
if (current_state == STATE_MOVING)
{
return false;
}
else if (this->position < this->target_position_ && current_state != STATE_OPENING)
{
this->change_to_next_state_();
return true;
}
else if (this->position > this->target_position_ && current_state != STATE_CLOSING)
{
this->change_to_next_state_();
return true;
}
else if (this->position == this->target_position_ && (current_state == STATE_OPENING || current_state == STATE_CLOSING))
{
this->change_to_next_state_();
this->target_state_ = STATE_NONE;
return true;
}
else if (this->position == this->target_position_)
{
this->target_state_ = STATE_NONE;
return true;
}
else
{
return false;
}
case STATE_STOPPED:
if (this->current_operation != COVER_OPERATION_IDLE)
{
this->change_to_next_state_();
this->target_state_ = STATE_NONE;
return true;
}
else
{
this->target_state_ = STATE_NONE;
return false;
}
default:
return false;
}
}
else
{
return false;
}
}
bool GarageDoorCover::check_local_buttons_()
{
const uint32_t now = millis();
if (now - this->last_local_button_read_time_ > LOCAL_BUTTON_READ_INTERVAL)
{
this->last_local_button_read_time_ = now;
int buttonValue = analogRead(A0);
// Expected values:
// 0 = Disconnected
// 133 = None
// 434 = Light
// 714 = Lock
// 993 = Door
LocalButton currentButton;
if (buttonValue < 50)
{
currentButton = LOCAL_BUTTON_DISCONNECTED;
}
else if (buttonValue < 284)
{
currentButton = LOCAL_BUTTON_NONE;
}
else if (buttonValue < 574)
{
currentButton = LOCAL_BUTTON_LIGHT;
}
else if (buttonValue < 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", buttonValue);
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", buttonValue);
break;
case LOCAL_BUTTON_LIGHT:
ESP_LOGD(TAG, "Local light button was pressed");
ESP_LOGD(TAG, " Read Value: %u", buttonValue);
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", buttonValue);
if (this->get_internal_state_() == 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", buttonValue);
this->change_to_next_state_(STATE_CHANGE_BUTTON);
break;
default:
break;
}
return true;
}
}
return false;
}
bool GarageDoorCover::check_remote_buttons_()
{
bool currentRemoteState = this->remote_pin_->digital_read();
if (currentRemoteState != this->last_remote_state_)
{
last_remote_state_ = currentRemoteState;
if (currentRemoteState && this->get_internal_state_() != STATE_LOCKED)
{
ESP_LOGD(TAG, "Remote door button was pressed");
this->change_to_next_state_(STATE_CHANGE_BUTTON);
return true;
}
}
bool currentRemoteLightState = this->remote_light_pin_->digital_read();
if (currentRemoteLightState != this->last_remote_light_state_)
{
last_remote_light_state_ = currentRemoteLightState;
if (currentRemoteLightState)
{
ESP_LOGD(TAG, "Remote light button was pressed");
this->fire_homeassistant_event(this->get_event_(EVENT_REMOTE_LIGHT));
return true;
}
}
return false;
}
void GarageDoorCover::change_to_next_state_(StateChangeType change_type)
{
if (millis() < this->control_pin_inactive_time_ + CONTROL_PIN_INACTIVE_DURATION)
{
return;
}
switch (change_type)
{
case STATE_CHANGE_CLOSE_SENSOR:
this->set_internal_state_(STATE_CLOSED);
if (HAS_INTERNAL_CLOSED_SENSOR)
{
return;
}
break;
case STATE_CHANGE_OPEN_SENSOR:
this->set_internal_state_(STATE_OPEN);
if (HAS_INTERNAL_OPEN_SENSOR)
{
return;
}
break;
case STATE_CHANGE_CANCEL_WARNING:
if (this->get_internal_state_() == STATE_CLOSE_WARNING)
{
this->buzzer_->stop();
this->set_internal_state_(this->position == COVER_OPEN ? STATE_OPEN : STATE_STOPPED_OPENING);
this->target_state_ = STATE_NONE;
}
return;
default:
if (change_type == STATE_CHANGE_BUTTON)
{
this->target_state_ = STATE_NONE;
}
switch (this->get_internal_state_())
{
case STATE_UNKNOWN:
this->set_internal_state_(STATE_MOVING);
break;
case STATE_MOVING:
this->set_internal_state_(STATE_UNKNOWN);
break;
case STATE_LOCKED:
this->set_internal_state_(STATE_OPENING);
break;
case STATE_CLOSED:
this->set_internal_state_(STATE_OPENING);
break;
case STATE_OPENING:
this->set_internal_state_(STATE_STOPPED_OPENING);
break;
case STATE_STOPPED_OPENING:
if (change_type == STATE_CHANGE_BUTTON || this->position > this->target_position_)
{
this->set_internal_state_(STATE_CLOSING);
}
else
{
this->set_internal_state_(STATE_CLOSE_WARNING);
}
break;
case STATE_OPEN:
this->set_internal_state_(change_type == STATE_CHANGE_BUTTON ? STATE_CLOSING : STATE_CLOSE_WARNING);
break;
case STATE_CLOSE_WARNING:
if (change_type == STATE_CHANGE_BUTTON)
{
this->buzzer_->stop();
this->set_internal_state_(this->position == COVER_OPEN ? STATE_OPEN : STATE_STOPPED_OPENING);
return;
}
else
{
this->set_internal_state_(STATE_CLOSING);
}
break;
case STATE_CLOSING:
this->set_internal_state_(STATE_STOPPED_CLOSING);
break;
case STATE_STOPPED_CLOSING:
this->set_internal_state_(STATE_OPENING);
break;
default:
break;
}
if (this->get_internal_state_() == STATE_CLOSE_WARNING)
{
this->buzzer_->play(CLOSE_WARNING_RTTTL);
}
else
{
// "Press" the control "button"
this->control_pin_->digital_write(true);
this->control_pin_active_time_ = millis();
}
break;
}
}
void GarageDoorCover::set_internal_state_(InternalState state, bool is_initial_state)
{
if (is_initial_state)
{
ESP_LOGD(TAG, "Setting Initial Internal State: '%s'", this->internal_state_to_str_(state));
}
else if (this->internal_state_ != state)
{
ESP_LOGD(TAG, "Setting Internal State:");
ESP_LOGD(TAG, " Current State: '%s'", this->internal_state_to_str_(this->get_internal_state_()));
ESP_LOGD(TAG, " New State: '%s'", this->internal_state_to_str_(state));
}
else
{
return;
}
this->internal_state_ = state;
if (state == STATE_UNKNOWN)
{
this->position = 0.5f;
this->current_operation = COVER_OPERATION_IDLE;
}
else if (state == STATE_MOVING)
{
this->position = 0.5f;
this->current_operation = COVER_OPERATION_CLOSING;
}
else if (state == STATE_LOCKED || state == STATE_CLOSED)
{
this->position = COVER_CLOSED;
this->current_operation = COVER_OPERATION_IDLE;
}
else if (state == STATE_OPENING)
{
this->current_operation = COVER_OPERATION_OPENING;
}
else if (state == STATE_STOPPED_OPENING || state == STATE_CLOSE_WARNING || state == STATE_STOPPED_CLOSING)
{
this->current_operation = COVER_OPERATION_IDLE;
}
else if (state == STATE_OPEN)
{
this->position = COVER_OPEN;
this->current_operation = COVER_OPERATION_IDLE;
}
else if (state == STATE_CLOSING)
{
this->current_operation = COVER_OPERATION_CLOSING;
}
this->publish_state(false);
if (is_initial_state)
{
this->lock_sensor_->publish_initial_state(state == STATE_LOCKED ? LOCK_STATE_LOCKED : LOCK_STATE_UNLOCKED);
}
else
{
this->lock_sensor_->publish_state(state == STATE_LOCKED ? LOCK_STATE_LOCKED : LOCK_STATE_UNLOCKED);
}
const uint32_t now = millis();
this->last_state_change_time_ = now;
this->last_position_time_ = now;
}
std::string GarageDoorCover::get_event_(std::string event_type)
{
std::string event = "esphome.";
event.append(this->get_object_id());
event.append(".");
event.append(event_type);
return event;
}
const char *GarageDoorCover::internal_state_to_str_(InternalState state)
{
switch (state)
{
case STATE_UNKNOWN:
return "UNKNOWN";
case STATE_MOVING:
return "MOVING";
case STATE_LOCKED:
return "LOCKED";
case STATE_CLOSED:
return "CLOSED";
case STATE_OPENING:
return "OPENING";
case STATE_STOPPED_OPENING:
return "STOPPED_OPENING";
case STATE_OPEN:
return "OPEN";
case STATE_CLOSE_WARNING:
return "CLOSE_WARNING";
case STATE_CLOSING:
return "CLOSING";
case STATE_STOPPED_CLOSING:
return "STOPPED_CLOSING";
default:
return "INTERNAL_STATE_UNKNOWN";
}
}
GarageDoorCover *GarageDoor = new GarageDoorCover();

View File

@ -0,0 +1,107 @@
substitutions:
device_id: second-garage-door
device_name: Second Garage Door
board: d1_mini
ip_address: !secret second-garage-door-ip
ota_pwd: !secret second-garage-door-ota-pwd
api_pwd: !secret second-garage-door-api-pwd
ap_wifi_pwd: !secret second-garage-door-ap-pwd
log_level: debug
packages:
device_base: !include ../packages/device_base_esp8266.yaml
external_components:
- source:
type: local
path: ../components
components: [ garage_door ]
binary_sensor:
- platform: gpio
name: ${device_name} Closed Sensor
id: closed_in
entity_category: diagnostic
pin:
number: D2
inverted: true
mode:
input: true
pullup: true
filters:
- delayed_on: 50ms
- delayed_off: 50ms
- platform: gpio
name: ${device_name} Open Sensor
id: open_in
entity_category: diagnostic
pin:
number: D7
inverted: true
mode:
input: true
pullup: true
filters:
- delayed_on: 50ms
- delayed_off: 50ms
- platform: gpio
id: remote_in
pin:
number: D5
inverted: true
mode:
input: true
pullup: true
- platform: gpio
id: remote_light_in
pin:
number: D6
inverted: true
mode:
input: true
pullup: true
cover:
- platform: garage_door
name: ${device_name}
open_duration: 12947ms
close_duration: 12875ms
control_output: control_out
button_sensor: button_in
closed_sensor: closed_in
open_sensor: open_in
remote_sensor: remote_in
remote_light_sensor: remote_light_in
warning_rtttl: warning_rtttl
last_open_time_sensor:
name: ${device_name} Last Open Time
last_close_time_sensor:
name: ${device_name} Last Close Time
logger:
logs:
sensor: NONE
output:
- platform: gpio
id: control_out
pin: D1
- platform: esp8266_pwm
id: rtttl_out
pin: D8
rtttl:
id: warning_rtttl
output: rtttl_out
sensor:
- platform: adc
id: button_in
pin: A0
raw: true
update_interval: 75ms
status_led:
pin:
number: D4
inverted: false

Binary file not shown.