Add new Lock core component (#2958)

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
Keilin Bickar 2022-02-03 13:24:31 -05:00 committed by GitHub
parent 62b366a5ec
commit 21803607e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 1558 additions and 3 deletions

View File

@ -89,6 +89,7 @@ esphome/components/json/* @OttoWinter
esphome/components/kalman_combinator/* @Cat-Ion
esphome/components/ledc/* @OttoWinter
esphome/components/light/* @esphome/core
esphome/components/lock/* @esphome/core
esphome/components/logger/* @esphome/core
esphome/components/ltr390/* @sjtrny
esphome/components/max7219digit/* @rspaargaren

View File

@ -41,6 +41,7 @@ service APIConnection {
rpc number_command (NumberCommandRequest) returns (void) {}
rpc select_command (SelectCommandRequest) returns (void) {}
rpc button_command (ButtonCommandRequest) returns (void) {}
rpc lock_command (LockCommandRequest) returns (void) {}
}
@ -956,6 +957,63 @@ message SelectCommandRequest {
string state = 2;
}
// ==================== LOCK ====================
enum LockState {
LOCK_STATE_NONE = 0;
LOCK_STATE_LOCKED = 1;
LOCK_STATE_UNLOCKED = 2;
LOCK_STATE_JAMMED = 3;
LOCK_STATE_LOCKING = 4;
LOCK_STATE_UNLOCKING = 5;
}
enum LockCommand {
LOCK_UNLOCK = 0;
LOCK_LOCK = 1;
LOCK_OPEN = 2;
}
message ListEntitiesLockResponse {
option (id) = 58;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_LOCK";
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
string icon = 5;
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
bool assumed_state = 8;
bool supports_open = 9;
bool requires_code = 10;
# Not yet implemented:
string code_format = 11;
}
message LockStateResponse {
option (id) = 59;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_LOCK";
option (no_delay) = true;
fixed32 key = 1;
LockState state = 2;
}
message LockCommandRequest {
option (id) = 60;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_LOCK";
option (no_delay) = true;
fixed32 key = 1;
LockCommand command = 2;
# Not yet implemented:
bool has_code = 3;
string code = 4;
}
// ==================== BUTTON ====================
message ListEntitiesButtonResponse {
option (id) = 61;
@ -980,3 +1038,4 @@ message ButtonCommandRequest {
fixed32 key = 1;
}

View File

@ -700,6 +700,49 @@ void APIConnection::button_command(const ButtonCommandRequest &msg) {
}
#endif
#ifdef USE_LOCK
bool APIConnection::send_lock_state(lock::Lock *a_lock, lock::LockState state) {
if (!this->state_subscription_)
return false;
LockStateResponse resp{};
resp.key = a_lock->get_object_id_hash();
resp.state = static_cast<enums::LockState>(state);
return this->send_lock_state_response(resp);
}
bool APIConnection::send_lock_info(lock::Lock *a_lock) {
ListEntitiesLockResponse msg;
msg.key = a_lock->get_object_id_hash();
msg.object_id = a_lock->get_object_id();
msg.name = a_lock->get_name();
msg.unique_id = get_default_unique_id("lock", a_lock);
msg.icon = a_lock->get_icon();
msg.assumed_state = a_lock->traits.get_assumed_state();
msg.disabled_by_default = a_lock->is_disabled_by_default();
msg.entity_category = static_cast<enums::EntityCategory>(a_lock->get_entity_category());
msg.supports_open = a_lock->traits.get_supports_open();
msg.requires_code = a_lock->traits.get_requires_code();
return this->send_list_entities_lock_response(msg);
}
void APIConnection::lock_command(const LockCommandRequest &msg) {
lock::Lock *a_lock = App.get_lock_by_key(msg.key);
if (a_lock == nullptr)
return;
switch (msg.command) {
case enums::LOCK_UNLOCK:
a_lock->unlock();
break;
case enums::LOCK_LOCK:
a_lock->lock();
break;
case enums::LOCK_OPEN:
a_lock->open();
break;
}
}
#endif
#ifdef USE_ESP32_CAMERA
void APIConnection::send_camera_state(std::shared_ptr<esp32_camera::CameraImage> image) {
if (!this->state_subscription_)

View File

@ -77,6 +77,11 @@ class APIConnection : public APIServerConnection {
#ifdef USE_BUTTON
bool send_button_info(button::Button *button);
void button_command(const ButtonCommandRequest &msg) override;
#endif
#ifdef USE_LOCK
bool send_lock_state(lock::Lock *a_lock, lock::LockState state);
bool send_lock_info(lock::Lock *a_lock);
void lock_command(const LockCommandRequest &msg) override;
#endif
bool send_log_message(int level, const char *tag, const char *line);
void send_homeassistant_service_call(const HomeassistantServiceResponse &call) {

View File

@ -278,6 +278,36 @@ template<> const char *proto_enum_to_string<enums::NumberMode>(enums::NumberMode
return "UNKNOWN";
}
}
template<> const char *proto_enum_to_string<enums::LockState>(enums::LockState value) {
switch (value) {
case enums::LOCK_STATE_NONE:
return "LOCK_STATE_NONE";
case enums::LOCK_STATE_LOCKED:
return "LOCK_STATE_LOCKED";
case enums::LOCK_STATE_UNLOCKED:
return "LOCK_STATE_UNLOCKED";
case enums::LOCK_STATE_JAMMED:
return "LOCK_STATE_JAMMED";
case enums::LOCK_STATE_LOCKING:
return "LOCK_STATE_LOCKING";
case enums::LOCK_STATE_UNLOCKING:
return "LOCK_STATE_UNLOCKING";
default:
return "UNKNOWN";
}
}
template<> const char *proto_enum_to_string<enums::LockCommand>(enums::LockCommand value) {
switch (value) {
case enums::LOCK_UNLOCK:
return "LOCK_UNLOCK";
case enums::LOCK_LOCK:
return "LOCK_LOCK";
case enums::LOCK_OPEN:
return "LOCK_OPEN";
default:
return "UNKNOWN";
}
}
bool HelloRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 1: {
@ -4186,6 +4216,234 @@ void SelectCommandRequest::dump_to(std::string &out) const {
out.append("}");
}
#endif
bool ListEntitiesLockResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 6: {
this->disabled_by_default = value.as_bool();
return true;
}
case 7: {
this->entity_category = value.as_enum<enums::EntityCategory>();
return true;
}
case 8: {
this->assumed_state = value.as_bool();
return true;
}
case 9: {
this->supports_open = value.as_bool();
return true;
}
case 10: {
this->requires_code = value.as_bool();
return true;
}
default:
return false;
}
}
bool ListEntitiesLockResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 1: {
this->object_id = value.as_string();
return true;
}
case 3: {
this->name = value.as_string();
return true;
}
case 4: {
this->unique_id = value.as_string();
return true;
}
case 5: {
this->icon = value.as_string();
return true;
}
case 11: {
this->code_format = value.as_string();
return true;
}
default:
return false;
}
}
bool ListEntitiesLockResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
switch (field_id) {
case 2: {
this->key = value.as_fixed32();
return true;
}
default:
return false;
}
}
void ListEntitiesLockResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(1, this->object_id);
buffer.encode_fixed32(2, this->key);
buffer.encode_string(3, this->name);
buffer.encode_string(4, this->unique_id);
buffer.encode_string(5, this->icon);
buffer.encode_bool(6, this->disabled_by_default);
buffer.encode_enum<enums::EntityCategory>(7, this->entity_category);
buffer.encode_bool(8, this->assumed_state);
buffer.encode_bool(9, this->supports_open);
buffer.encode_bool(10, this->requires_code);
buffer.encode_string(11, this->code_format);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void ListEntitiesLockResponse::dump_to(std::string &out) const {
__attribute__((unused)) char buffer[64];
out.append("ListEntitiesLockResponse {\n");
out.append(" object_id: ");
out.append("'").append(this->object_id).append("'");
out.append("\n");
out.append(" key: ");
sprintf(buffer, "%u", this->key);
out.append(buffer);
out.append("\n");
out.append(" name: ");
out.append("'").append(this->name).append("'");
out.append("\n");
out.append(" unique_id: ");
out.append("'").append(this->unique_id).append("'");
out.append("\n");
out.append(" icon: ");
out.append("'").append(this->icon).append("'");
out.append("\n");
out.append(" disabled_by_default: ");
out.append(YESNO(this->disabled_by_default));
out.append("\n");
out.append(" entity_category: ");
out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category));
out.append("\n");
out.append(" assumed_state: ");
out.append(YESNO(this->assumed_state));
out.append("\n");
out.append(" supports_open: ");
out.append(YESNO(this->supports_open));
out.append("\n");
out.append(" requires_code: ");
out.append(YESNO(this->requires_code));
out.append("\n");
out.append(" code_format: ");
out.append("'").append(this->code_format).append("'");
out.append("\n");
out.append("}");
}
#endif
bool LockStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 2: {
this->state = value.as_enum<enums::LockState>();
return true;
}
default:
return false;
}
}
bool LockStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
switch (field_id) {
case 1: {
this->key = value.as_fixed32();
return true;
}
default:
return false;
}
}
void LockStateResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_fixed32(1, this->key);
buffer.encode_enum<enums::LockState>(2, this->state);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void LockStateResponse::dump_to(std::string &out) const {
__attribute__((unused)) char buffer[64];
out.append("LockStateResponse {\n");
out.append(" key: ");
sprintf(buffer, "%u", this->key);
out.append(buffer);
out.append("\n");
out.append(" state: ");
out.append(proto_enum_to_string<enums::LockState>(this->state));
out.append("\n");
out.append("}");
}
#endif
bool LockCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 2: {
this->command = value.as_enum<enums::LockCommand>();
return true;
}
case 3: {
this->has_code = value.as_bool();
return true;
}
default:
return false;
}
}
bool LockCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 4: {
this->code = value.as_string();
return true;
}
default:
return false;
}
}
bool LockCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
switch (field_id) {
case 1: {
this->key = value.as_fixed32();
return true;
}
default:
return false;
}
}
void LockCommandRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_fixed32(1, this->key);
buffer.encode_enum<enums::LockCommand>(2, this->command);
buffer.encode_bool(3, this->has_code);
buffer.encode_string(4, this->code);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void LockCommandRequest::dump_to(std::string &out) const {
__attribute__((unused)) char buffer[64];
out.append("LockCommandRequest {\n");
out.append(" key: ");
sprintf(buffer, "%u", this->key);
out.append(buffer);
out.append("\n");
out.append(" command: ");
out.append(proto_enum_to_string<enums::LockCommand>(this->command));
out.append("\n");
out.append(" has_code: ");
out.append(YESNO(this->has_code));
out.append("\n");
out.append(" code: ");
out.append("'").append(this->code).append("'");
out.append("\n");
out.append("}");
}
#endif
bool ListEntitiesButtonResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 6: {
@ -4248,7 +4506,7 @@ void ListEntitiesButtonResponse::encode(ProtoWriteBuffer buffer) const {
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void ListEntitiesButtonResponse::dump_to(std::string &out) const {
char buffer[64];
__attribute__((unused)) char buffer[64];
out.append("ListEntitiesButtonResponse {\n");
out.append(" object_id: ");
out.append("'").append(this->object_id).append("'");
@ -4298,7 +4556,7 @@ bool ButtonCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
void ButtonCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); }
#ifdef HAS_PROTO_MESSAGE_DUMP
void ButtonCommandRequest::dump_to(std::string &out) const {
char buffer[64];
__attribute__((unused)) char buffer[64];
out.append("ButtonCommandRequest {\n");
out.append(" key: ");
sprintf(buffer, "%u", this->key);

View File

@ -128,6 +128,19 @@ enum NumberMode : uint32_t {
NUMBER_MODE_BOX = 1,
NUMBER_MODE_SLIDER = 2,
};
enum LockState : uint32_t {
LOCK_STATE_NONE = 0,
LOCK_STATE_LOCKED = 1,
LOCK_STATE_UNLOCKED = 2,
LOCK_STATE_JAMMED = 3,
LOCK_STATE_LOCKING = 4,
LOCK_STATE_UNLOCKING = 5,
};
enum LockCommand : uint32_t {
LOCK_UNLOCK = 0,
LOCK_LOCK = 1,
LOCK_OPEN = 2,
};
} // namespace enums
@ -1049,6 +1062,58 @@ class SelectCommandRequest : public ProtoMessage {
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
};
class ListEntitiesLockResponse : public ProtoMessage {
public:
std::string object_id{};
uint32_t key{0};
std::string name{};
std::string unique_id{};
std::string icon{};
bool disabled_by_default{false};
enums::EntityCategory entity_category{};
bool assumed_state{false};
bool supports_open{false};
bool requires_code{false};
std::string code_format{};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class LockStateResponse : public ProtoMessage {
public:
uint32_t key{0};
enums::LockState state{};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class LockCommandRequest : public ProtoMessage {
public:
uint32_t key{0};
enums::LockCommand command{};
bool has_code{false};
std::string code{};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class ListEntitiesButtonResponse : public ProtoMessage {
public:
std::string object_id{};

View File

@ -282,6 +282,24 @@ bool APIServerConnectionBase::send_select_state_response(const SelectStateRespon
#endif
#ifdef USE_SELECT
#endif
#ifdef USE_LOCK
bool APIServerConnectionBase::send_list_entities_lock_response(const ListEntitiesLockResponse &msg) {
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "send_list_entities_lock_response: %s", msg.dump().c_str());
#endif
return this->send_message_<ListEntitiesLockResponse>(msg, 58);
}
#endif
#ifdef USE_LOCK
bool APIServerConnectionBase::send_lock_state_response(const LockStateResponse &msg) {
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "send_lock_state_response: %s", msg.dump().c_str());
#endif
return this->send_message_<LockStateResponse>(msg, 59);
}
#endif
#ifdef USE_LOCK
#endif
#ifdef USE_BUTTON
bool APIServerConnectionBase::send_list_entities_button_response(const ListEntitiesButtonResponse &msg) {
#ifdef HAS_PROTO_MESSAGE_DUMP
@ -523,6 +541,17 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
ESP_LOGVV(TAG, "on_select_command_request: %s", msg.dump().c_str());
#endif
this->on_select_command_request(msg);
#endif
break;
}
case 60: {
#ifdef USE_LOCK
LockCommandRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_lock_command_request: %s", msg.dump().c_str());
#endif
this->on_lock_command_request(msg);
#endif
break;
}
@ -771,6 +800,19 @@ void APIServerConnection::on_button_command_request(const ButtonCommandRequest &
this->button_command(msg);
}
#endif
#ifdef USE_LOCK
void APIServerConnection::on_lock_command_request(const LockCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->lock_command(msg);
}
#endif
} // namespace api
} // namespace esphome

View File

@ -130,6 +130,15 @@ class APIServerConnectionBase : public ProtoService {
#ifdef USE_SELECT
virtual void on_select_command_request(const SelectCommandRequest &value){};
#endif
#ifdef USE_LOCK
bool send_list_entities_lock_response(const ListEntitiesLockResponse &msg);
#endif
#ifdef USE_LOCK
bool send_lock_state_response(const LockStateResponse &msg);
#endif
#ifdef USE_LOCK
virtual void on_lock_command_request(const LockCommandRequest &value){};
#endif
#ifdef USE_BUTTON
bool send_list_entities_button_response(const ListEntitiesButtonResponse &msg);
#endif
@ -180,6 +189,9 @@ class APIServerConnection : public APIServerConnectionBase {
#endif
#ifdef USE_BUTTON
virtual void button_command(const ButtonCommandRequest &msg) = 0;
#endif
#ifdef USE_LOCK
virtual void lock_command(const LockCommandRequest &msg) = 0;
#endif
protected:
void on_hello_request(const HelloRequest &msg) override;
@ -221,6 +233,9 @@ class APIServerConnection : public APIServerConnectionBase {
#ifdef USE_BUTTON
void on_button_command_request(const ButtonCommandRequest &msg) override;
#endif
#ifdef USE_LOCK
void on_lock_command_request(const LockCommandRequest &msg) override;
#endif
};
} // namespace api

View File

@ -263,6 +263,15 @@ void APIServer::on_select_update(select::Select *obj, const std::string &state)
}
#endif
#ifdef USE_LOCK
void APIServer::on_lock_update(lock::Lock *obj) {
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_lock_state(obj, obj->state);
}
#endif
float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; }
void APIServer::set_port(uint16_t port) { this->port_ = port; }
APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)

View File

@ -66,6 +66,9 @@ class APIServer : public Component, public Controller {
#endif
#ifdef USE_SELECT
void on_select_update(select::Select *obj, const std::string &state) override;
#endif
#ifdef USE_LOCK
void on_lock_update(lock::Lock *obj) override;
#endif
void send_homeassistant_service_call(const HomeassistantServiceResponse &call);
void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }

View File

@ -35,6 +35,9 @@ bool ListEntitiesIterator::on_text_sensor(text_sensor::TextSensor *text_sensor)
return this->client_->send_text_sensor_info(text_sensor);
}
#endif
#ifdef USE_LOCK
bool ListEntitiesIterator::on_lock(lock::Lock *a_lock) { return this->client_->send_lock_info(a_lock); }
#endif
bool ListEntitiesIterator::on_end() { return this->client_->send_list_info_done(); }
ListEntitiesIterator::ListEntitiesIterator(APIServer *server, APIConnection *client)

View File

@ -48,6 +48,9 @@ class ListEntitiesIterator : public ComponentIterator {
#endif
#ifdef USE_SELECT
bool on_select(select::Select *select) override;
#endif
#ifdef USE_LOCK
bool on_lock(lock::Lock *a_lock) override;
#endif
bool on_end() override;

View File

@ -47,6 +47,9 @@ bool InitialStateIterator::on_select(select::Select *select) {
return this->client_->send_select_state(select, select->state);
}
#endif
#ifdef USE_LOCK
bool InitialStateIterator::on_lock(lock::Lock *a_lock) { return this->client_->send_lock_state(a_lock, a_lock->state); }
#endif
InitialStateIterator::InitialStateIterator(APIServer *server, APIConnection *client)
: ComponentIterator(server), client_(client) {}

View File

@ -45,6 +45,9 @@ class InitialStateIterator : public ComponentIterator {
#endif
#ifdef USE_SELECT
bool on_select(select::Select *select) override;
#endif
#ifdef USE_LOCK
bool on_lock(lock::Lock *a_lock) override;
#endif
protected:
APIConnection *client_;

View File

@ -212,6 +212,21 @@ void ComponentIterator::advance() {
}
}
break;
#endif
#ifdef USE_LOCK
case IteratorState::LOCK:
if (this->at_ >= App.get_locks().size()) {
advance_platform = true;
} else {
auto *a_lock = App.get_locks()[this->at_];
if (a_lock->is_internal()) {
success = true;
break;
} else {
success = this->on_lock(a_lock);
}
}
break;
#endif
case IteratorState::MAX:
if (this->on_end()) {

View File

@ -56,6 +56,9 @@ class ComponentIterator {
#endif
#ifdef USE_SELECT
virtual bool on_select(select::Select *select) = 0;
#endif
#ifdef USE_LOCK
virtual bool on_lock(lock::Lock *a_lock) = 0;
#endif
virtual bool on_end();
@ -99,6 +102,9 @@ class ComponentIterator {
#endif
#ifdef USE_SELECT
SELECT,
#endif
#ifdef USE_LOCK
LOCK,
#endif
MAX,
} state_{IteratorState::NONE};

View File

@ -7,6 +7,7 @@ from esphome.const import (
CONF_ID,
CONF_DEVICE_CLASS,
CONF_STATE,
CONF_ON_OPEN,
CONF_POSITION,
CONF_POSITION_COMMAND_TOPIC,
CONF_POSITION_STATE_TOPIC,
@ -74,7 +75,6 @@ CoverClosedTrigger = cover_ns.class_(
"CoverClosedTrigger", automation.Trigger.template()
)
CONF_ON_OPEN = "on_open"
CONF_ON_CLOSED = "on_closed"
COVER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend(

View File

@ -0,0 +1,102 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import automation
from esphome.automation import Condition, maybe_simple_id
from esphome.components import mqtt
from esphome.const import (
CONF_ID,
CONF_ON_LOCK,
CONF_ON_UNLOCK,
CONF_TRIGGER_ID,
CONF_MQTT_ID,
)
from esphome.core import CORE, coroutine_with_priority
from esphome.cpp_helpers import setup_entity
CODEOWNERS = ["@esphome/core"]
IS_PLATFORM_COMPONENT = True
lock_ns = cg.esphome_ns.namespace("lock")
Lock = lock_ns.class_("Lock", cg.EntityBase)
LockPtr = Lock.operator("ptr")
LockCall = lock_ns.class_("LockCall")
UnlockAction = lock_ns.class_("UnlockAction", automation.Action)
LockAction = lock_ns.class_("LockAction", automation.Action)
OpenAction = lock_ns.class_("OpenAction", automation.Action)
LockPublishAction = lock_ns.class_("LockPublishAction", automation.Action)
LockCondition = lock_ns.class_("LockCondition", Condition)
LockLockTrigger = lock_ns.class_("LockLockTrigger", automation.Trigger.template())
LockUnlockTrigger = lock_ns.class_("LockUnlockTrigger", automation.Trigger.template())
LOCK_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend(
{
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTLockComponent),
cv.Optional(CONF_ON_LOCK): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LockLockTrigger),
}
),
cv.Optional(CONF_ON_UNLOCK): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LockUnlockTrigger),
}
),
}
)
async def setup_lock_core_(var, config):
await setup_entity(var, config)
for conf in config.get(CONF_ON_LOCK, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_UNLOCK, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
if CONF_MQTT_ID in config:
mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var)
await mqtt.register_mqtt_component(mqtt_, config)
async def register_lock(var, config):
if not CORE.has_id(config[CONF_ID]):
var = cg.Pvariable(config[CONF_ID], var)
cg.add(cg.App.register_lock(var))
await setup_lock_core_(var, config)
LOCK_ACTION_SCHEMA = maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(Lock),
}
)
@automation.register_action("lock.unlock", UnlockAction, LOCK_ACTION_SCHEMA)
@automation.register_action("lock.lock", LockAction, LOCK_ACTION_SCHEMA)
@automation.register_action("lock.open", OpenAction, LOCK_ACTION_SCHEMA)
async def lock_action_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_condition("lock.is_locked", LockCondition, LOCK_ACTION_SCHEMA)
async def lock_is_on_to_code(config, condition_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(condition_id, template_arg, paren, True)
@automation.register_condition("lock.is_unlocked", LockCondition, LOCK_ACTION_SCHEMA)
async def lock_is_off_to_code(config, condition_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(condition_id, template_arg, paren, False)
@coroutine_with_priority(100.0)
async def to_code(config):
cg.add_global(lock_ns.using)
cg.add_define("USE_LOCK")

View File

@ -0,0 +1,87 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "esphome/components/lock/lock.h"
namespace esphome {
namespace lock {
template<typename... Ts> class LockAction : public Action<Ts...> {
public:
explicit LockAction(Lock *a_lock) : lock_(a_lock) {}
void play(Ts... x) override { this->lock_->lock(); }
protected:
Lock *lock_;
};
template<typename... Ts> class UnlockAction : public Action<Ts...> {
public:
explicit UnlockAction(Lock *a_lock) : lock_(a_lock) {}
void play(Ts... x) override { this->lock_->unlock(); }
protected:
Lock *lock_;
};
template<typename... Ts> class OpenAction : public Action<Ts...> {
public:
explicit OpenAction(Lock *a_lock) : lock_(a_lock) {}
void play(Ts... x) override { this->lock_->open(); }
protected:
Lock *lock_;
};
template<typename... Ts> class LockCondition : public Condition<Ts...> {
public:
LockCondition(Lock *parent, bool state) : parent_(parent), state_(state) {}
bool check(Ts... x) override {
auto check_state = this->state_ ? LockState::LOCK_STATE_LOCKED : LockState::LOCK_STATE_UNLOCKED;
return this->parent_->state == check_state;
}
protected:
Lock *parent_;
bool state_;
};
class LockLockTrigger : public Trigger<> {
public:
LockLockTrigger(Lock *a_lock) {
a_lock->add_on_state_callback([this, a_lock]() {
if (a_lock->state == LockState::LOCK_STATE_LOCKED) {
this->trigger();
}
});
}
};
class LockUnlockTrigger : public Trigger<> {
public:
LockUnlockTrigger(Lock *a_lock) {
a_lock->add_on_state_callback([this, a_lock]() {
if (a_lock->state == LockState::LOCK_STATE_UNLOCKED) {
this->trigger();
}
});
}
};
template<typename... Ts> class LockPublishAction : public Action<Ts...> {
public:
LockPublishAction(Lock *a_lock) : lock_(a_lock) {}
TEMPLATABLE_VALUE(LockState, state)
void play(Ts... x) override { this->lock_->publish_state(this->state_.value(x...)); }
protected:
Lock *lock_;
};
} // namespace lock
} // namespace esphome

View File

@ -0,0 +1,109 @@
#include "lock.h"
#include "esphome/core/log.h"
namespace esphome {
namespace lock {
static const char *const TAG = "lock";
const char *lock_state_to_string(LockState state) {
switch (state) {
case LOCK_STATE_LOCKED:
return "LOCKED";
case LOCK_STATE_UNLOCKED:
return "UNLOCKED";
case LOCK_STATE_JAMMED:
return "JAMMED";
case LOCK_STATE_LOCKING:
return "LOCKING";
case LOCK_STATE_UNLOCKING:
return "UNLOCKING";
case LOCK_STATE_NONE:
default:
return "UNKNOWN";
}
}
Lock::Lock(const std::string &name) : EntityBase(name), state(LOCK_STATE_NONE) {}
Lock::Lock() : Lock("") {}
LockCall Lock::make_call() { return LockCall(this); }
void Lock::lock() {
auto call = this->make_call();
call.set_state(LOCK_STATE_LOCKED);
this->control(call);
}
void Lock::unlock() {
auto call = this->make_call();
call.set_state(LOCK_STATE_UNLOCKED);
this->control(call);
}
void Lock::open() {
if (traits.get_supports_open()) {
ESP_LOGD(TAG, "'%s' Opening.", this->get_name().c_str());
this->open_latch();
} else {
ESP_LOGW(TAG, "'%s' Does not support Open.", this->get_name().c_str());
}
}
void Lock::publish_state(LockState state) {
if (!this->publish_dedup_.next(state))
return;
this->state = state;
this->rtc_.save(&this->state);
ESP_LOGD(TAG, "'%s': Sending state %s", this->name_.c_str(), lock_state_to_string(state));
this->state_callback_.call();
}
void Lock::add_on_state_callback(std::function<void()> &&callback) { this->state_callback_.add(std::move(callback)); }
uint32_t Lock::hash_base() { return 856245656UL; }
void LockCall::perform() {
ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str());
this->validate_();
if (this->state_.has_value()) {
const char *state_s = lock_state_to_string(*this->state_);
ESP_LOGD(TAG, " State: %s", state_s);
}
this->parent_->control(*this);
}
void LockCall::validate_() {
if (this->state_.has_value()) {
auto state = *this->state_;
if (!this->parent_->traits.supports_state(state)) {
ESP_LOGW(TAG, " State %s is not supported by this device!", lock_state_to_string(*this->state_));
this->state_.reset();
}
}
}
LockCall &LockCall::set_state(LockState state) {
this->state_ = state;
return *this;
}
LockCall &LockCall::set_state(optional<LockState> state) {
this->state_ = state;
return *this;
}
LockCall &LockCall::set_state(const std::string &state) {
if (str_equals_case_insensitive(state, "LOCKED")) {
this->set_state(LOCK_STATE_LOCKED);
} else if (str_equals_case_insensitive(state, "UNLOCKED")) {
this->set_state(LOCK_STATE_UNLOCKED);
} else if (str_equals_case_insensitive(state, "JAMMED")) {
this->set_state(LOCK_STATE_JAMMED);
} else if (str_equals_case_insensitive(state, "LOCKING")) {
this->set_state(LOCK_STATE_LOCKING);
} else if (str_equals_case_insensitive(state, "UNLOCKING")) {
this->set_state(LOCK_STATE_UNLOCKING);
} else if (str_equals_case_insensitive(state, "NONE")) {
this->set_state(LOCK_STATE_NONE);
} else {
ESP_LOGW(TAG, "'%s' - Unrecognized state %s", this->parent_->get_name().c_str(), state.c_str());
}
return *this;
}
const optional<LockState> &LockCall::get_state() const { return this->state_; }
} // namespace lock
} // namespace esphome

View File

@ -0,0 +1,178 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/entity_base.h"
#include "esphome/core/preferences.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <set>
namespace esphome {
namespace lock {
class Lock;
#define LOG_LOCK(prefix, type, obj) \
if ((obj) != nullptr) { \
ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \
if (!(obj)->get_icon().empty()) { \
ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \
} \
if ((obj)->traits.get_assumed_state()) { \
ESP_LOGCONFIG(TAG, "%s Assumed State: YES", prefix); \
} \
}
/// Enum for all states a lock can be in.
enum LockState : uint8_t {
LOCK_STATE_NONE = 0,
LOCK_STATE_LOCKED = 1,
LOCK_STATE_UNLOCKED = 2,
LOCK_STATE_JAMMED = 3,
LOCK_STATE_LOCKING = 4,
LOCK_STATE_UNLOCKING = 5
};
const char *lock_state_to_string(LockState state);
class LockTraits {
public:
LockTraits() = default;
bool get_supports_open() const { return this->supports_open_; }
void set_supports_open(bool supports_open) { this->supports_open_ = supports_open; }
bool get_requires_code() const { return this->requires_code_; }
void set_requires_code(bool requires_code) { this->requires_code_ = requires_code; }
bool get_assumed_state() const { return this->assumed_state_; }
void set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; }
bool supports_state(LockState state) const { return supported_states_.count(state); }
std::set<LockState> get_supported_states() const { return supported_states_; }
void set_supported_states(std::set<LockState> states) { supported_states_ = std::move(states); }
void add_supported_state(LockState state) { supported_states_.insert(state); }
protected:
bool supports_open_{false};
bool requires_code_{false};
bool assumed_state_{false};
std::set<LockState> supported_states_ = {LOCK_STATE_NONE, LOCK_STATE_LOCKED, LOCK_STATE_UNLOCKED};
};
/** This class is used to encode all control actions on a lock device.
*
* It is supposed to be used by all code that wishes to control a lock device (mqtt, api, lambda etc).
* Create an instance of this class by calling `id(lock_device).make_call();`. Then set all attributes
* with the `set_x` methods. Finally, to apply the changes call `.perform();`.
*
* The integration that implements the lock device receives this instance with the `control` method.
* It should check all the properties it implements and apply them as needed. It should do so by
* getting all properties it controls with the getter methods in this class. If the optional value is
* set (check with `.has_value()`) that means the user wants to control this property. Get the value
* of the optional with the star operator (`*call.get_state()`) and apply it.
*/
class LockCall {
public:
LockCall(Lock *parent) : parent_(parent) {}
/// Set the state of the lock device.
LockCall &set_state(LockState state);
/// Set the state of the lock device.
LockCall &set_state(optional<LockState> state);
/// Set the state of the lock device based on a string.
LockCall &set_state(const std::string &state);
void perform();
const optional<LockState> &get_state() const;
protected:
void validate_();
Lock *const parent_;
optional<LockState> state_;
};
/** Base class for all locks.
*
* A lock is basically a switch with a combination of a binary sensor (for reporting lock values)
* and a write_state method that writes a state to the hardware. Locks can also have an "open"
* method to unlatch.
*
* For integrations: Integrations must implement the method control().
* Control will be called with the arguments supplied by the user and should be used
* to control all values of the lock.
*/
class Lock : public EntityBase {
public:
explicit Lock();
explicit Lock(const std::string &name);
/** Make a lock device control call, this is used to control the lock device, see the LockCall description
* for more info.
* @return A new LockCall instance targeting this lock device.
*/
LockCall make_call();
/** Publish a state to the front-end from the back-end.
*
* Then the internal value member is set and finally the callbacks are called.
*
* @param state The new state.
*/
void publish_state(LockState state);
/// The current reported state of the lock.
LockState state{LOCK_STATE_NONE};
LockTraits traits;
/** Turn this lock on. This is called by the front-end.
*
* For implementing locks, please override control.
*/
void lock();
/** Turn this lock off. This is called by the front-end.
*
* For implementing locks, please override control.
*/
void unlock();
/** Open (unlatch) this lock. This is called by the front-end.
*
* For implementing locks, please override control.
*/
void open();
/** Set callback for state changes.
*
* @param callback The void(bool) callback.
*/
void add_on_state_callback(std::function<void()> &&callback);
protected:
friend LockCall;
/** Perform the open latch action with hardware. This method is optional to implement
* when creating a new lock.
*
* In the implementation of this method, it is recommended you also call
* publish_state with "unlock" to acknowledge that the state was written to the hardware.
*/
virtual void open_latch() { unlock(); };
/** Control the lock device, this is a virtual method that each lock integration must implement.
*
* See more info in LockCall. The integration should check all of its values in this method and
* set them accordingly. At the end of the call, the integration must call `publish_state()` to
* notify the frontend of a changed state.
*
* @param call The LockCall instance encoding all attribute changes.
*/
virtual void control(const LockCall &call) = 0;
uint32_t hash_base() override;
CallbackManager<void()> state_callback_{};
Deduplicator<LockState> publish_dedup_;
ESPPreferenceObject rtc_;
};
} // namespace lock
} // namespace esphome

View File

@ -97,6 +97,7 @@ MQTTTextSensor = mqtt_ns.class_("MQTTTextSensor", MQTTComponent)
MQTTNumberComponent = mqtt_ns.class_("MQTTNumberComponent", MQTTComponent)
MQTTSelectComponent = mqtt_ns.class_("MQTTSelectComponent", MQTTComponent)
MQTTButtonComponent = mqtt_ns.class_("MQTTButtonComponent", MQTTComponent)
MQTTLockComponent = mqtt_ns.class_("MQTTLockComponent", MQTTComponent)
MQTTDiscoveryUniqueIdGenerator = mqtt_ns.enum("MQTTDiscoveryUniqueIdGenerator")
MQTT_DISCOVERY_UNIQUE_ID_GENERATOR_OPTIONS = {

View File

@ -0,0 +1,55 @@
#include "mqtt_lock.h"
#include "esphome/core/log.h"
#include "mqtt_const.h"
#ifdef USE_MQTT
#ifdef USE_LOCK
namespace esphome {
namespace mqtt {
static const char *const TAG = "mqtt.lock";
using namespace esphome::lock;
MQTTLockComponent::MQTTLockComponent(lock::Lock *a_lock) : lock_(a_lock) {}
void MQTTLockComponent::setup() {
this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) {
if (strcasecmp(payload.c_str(), "LOCK") == 0) {
this->lock_->lock();
} else if (strcasecmp(payload.c_str(), "UNLOCK") == 0) {
this->lock_->unlock();
} else if (strcasecmp(payload.c_str(), "OPEN") == 0) {
this->lock_->open();
} else {
ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name().c_str(), payload.c_str());
this->status_momentary_warning("state", 5000);
}
});
this->lock_->add_on_state_callback([this]() { this->defer("send", [this]() { this->publish_state(); }); });
}
void MQTTLockComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT Lock '%s': ", this->lock_->get_name().c_str());
LOG_MQTT_COMPONENT(true, true);
}
std::string MQTTLockComponent::component_type() const { return "lock"; }
const EntityBase *MQTTLockComponent::get_entity() const { return this->lock_; }
void MQTTLockComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
if (this->lock_->traits.get_assumed_state())
root[MQTT_OPTIMISTIC] = true;
}
bool MQTTLockComponent::send_initial_state() { return this->publish_state(); }
bool MQTTLockComponent::publish_state() {
std::string payload = lock_state_to_string(this->lock_->state);
return this->publish(this->get_state_topic_(), payload);
}
} // namespace mqtt
} // namespace esphome
#endif
#endif // USE_MQTT

View File

@ -0,0 +1,41 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_MQTT
#ifdef USE_LOCK
#include "esphome/components/lock/lock.h"
#include "mqtt_component.h"
namespace esphome {
namespace mqtt {
class MQTTLockComponent : public mqtt::MQTTComponent {
public:
explicit MQTTLockComponent(lock::Lock *a_lock);
// ========== INTERNAL METHODS ==========
// (In most use cases you won't need these)
void setup() override;
void dump_config() override;
void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override;
bool send_initial_state() override;
bool publish_state();
protected:
/// "lock" component type.
std::string component_type() const override;
const EntityBase *get_entity() const override;
lock::Lock *lock_;
};
} // namespace mqtt
} // namespace esphome
#endif
#endif // USE_MQTT

View File

@ -0,0 +1,23 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import output, lock
from esphome.const import CONF_ID, CONF_OUTPUT
from .. import output_ns
OutputLock = output_ns.class_("OutputLock", lock.Lock, cg.Component)
CONFIG_SCHEMA = lock.LOCK_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(OutputLock),
cv.Required(CONF_OUTPUT): cv.use_id(output.BinaryOutput),
}
).extend(cv.COMPONENT_SCHEMA)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await lock.register_lock(var, config)
output_ = await cg.get_variable(config[CONF_OUTPUT])
cg.add(var.set_output(output_))

View File

@ -0,0 +1,22 @@
#include "output_lock.h"
#include "esphome/core/log.h"
namespace esphome {
namespace output {
static const char *const TAG = "output.lock";
void OutputLock::dump_config() { LOG_LOCK("", "Output Lock", this); }
void OutputLock::control(const lock::LockCall &call) {
auto state = *call.get_state();
if (state == lock::LOCK_STATE_LOCKED) {
this->output_->turn_on();
} else if (state == lock::LOCK_STATE_UNLOCKED) {
this->output_->turn_off();
}
this->publish_state(state);
}
} // namespace output
} // namespace esphome

View File

@ -0,0 +1,24 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/lock/lock.h"
#include "esphome/components/output/binary_output.h"
namespace esphome {
namespace output {
class OutputLock : public lock::Lock, public Component {
public:
void set_output(BinaryOutput *output) { output_ = output; }
float get_setup_priority() const override { return setup_priority::HARDWARE - 1.0f; }
void dump_config() override;
protected:
void control(const lock::LockCall &call) override;
output::BinaryOutput *output_;
};
} // namespace output
} // namespace esphome

View File

@ -45,6 +45,12 @@ void PrometheusHandler::handleRequest(AsyncWebServerRequest *req) {
this->switch_row_(stream, obj);
#endif
#ifdef USE_LOCK
this->lock_type_(stream);
for (auto *obj : App.get_locks())
this->lock_row_(stream, obj);
#endif
req->send(stream);
}
@ -310,6 +316,30 @@ void PrometheusHandler::switch_row_(AsyncResponseStream *stream, switch_::Switch
}
#endif
#ifdef USE_LOCK
void PrometheusHandler::lock_type_(AsyncResponseStream *stream) {
stream->print(F("#TYPE esphome_lock_value GAUGE\n"));
stream->print(F("#TYPE esphome_lock_failed GAUGE\n"));
}
void PrometheusHandler::lock_row_(AsyncResponseStream *stream, lock::Lock *obj) {
if (obj->is_internal())
return;
stream->print(F("esphome_lock_failed{id=\""));
stream->print(obj->get_object_id().c_str());
stream->print(F("\",name=\""));
stream->print(obj->get_name().c_str());
stream->print(F("\"} 0\n"));
// Data itself
stream->print(F("esphome_lock_value{id=\""));
stream->print(obj->get_object_id().c_str());
stream->print(F("\",name=\""));
stream->print(obj->get_name().c_str());
stream->print(F("\"} "));
stream->print(obj->state);
stream->print('\n');
}
#endif
} // namespace prometheus
} // namespace esphome

View File

@ -76,6 +76,13 @@ class PrometheusHandler : public AsyncWebHandler, public Component {
void switch_row_(AsyncResponseStream *stream, switch_::Switch *obj);
#endif
#ifdef USE_LOCK
/// Return the type for prometheus
void lock_type_(AsyncResponseStream *stream);
/// Return the lock Values state as prometheus data point
void lock_row_(AsyncResponseStream *stream, lock::Lock *obj);
#endif
web_server_base::WebServerBase *base_;
};

View File

@ -0,0 +1,103 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import automation
from esphome.components import lock
from esphome.const import (
CONF_ASSUMED_STATE,
CONF_ID,
CONF_LAMBDA,
CONF_LOCK_ACTION,
CONF_OPEN_ACTION,
CONF_OPTIMISTIC,
CONF_STATE,
CONF_UNLOCK_ACTION,
)
from .. import template_ns
TemplateLock = template_ns.class_("TemplateLock", lock.Lock, cg.Component)
LockState = lock.lock_ns.enum("LockState")
LOCK_STATES = {
"LOCKED": LockState.LOCK_STATE_LOCKED,
"UNLOCKED": LockState.LOCK_STATE_UNLOCKED,
"JAMMED": LockState.LOCK_STATE_JAMMED,
"LOCKING": LockState.LOCK_STATE_LOCKING,
"UNLOCKING": LockState.LOCK_STATE_UNLOCKING,
}
validate_lock_state = cv.enum(LOCK_STATES, upper=True)
def validate(config):
if not config[CONF_OPTIMISTIC] and (
CONF_LOCK_ACTION not in config or CONF_UNLOCK_ACTION not in config
):
raise cv.Invalid(
"Either optimistic mode must be enabled, or lock_action and unlock_action must be set, "
"to handle the lock being changed."
)
return config
CONFIG_SCHEMA = cv.All(
lock.LOCK_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(TemplateLock),
cv.Optional(CONF_LAMBDA): cv.returning_lambda,
cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean,
cv.Optional(CONF_ASSUMED_STATE, default=False): cv.boolean,
cv.Optional(CONF_UNLOCK_ACTION): automation.validate_automation(
single=True
),
cv.Optional(CONF_LOCK_ACTION): automation.validate_automation(single=True),
cv.Optional(CONF_OPEN_ACTION): automation.validate_automation(single=True),
}
).extend(cv.COMPONENT_SCHEMA),
validate,
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await lock.register_lock(var, config)
if CONF_LAMBDA in config:
template_ = await cg.process_lambda(
config[CONF_LAMBDA], [], return_type=cg.optional.template(LockState)
)
cg.add(var.set_state_lambda(template_))
if CONF_UNLOCK_ACTION in config:
await automation.build_automation(
var.get_unlock_trigger(), [], config[CONF_UNLOCK_ACTION]
)
if CONF_LOCK_ACTION in config:
await automation.build_automation(
var.get_lock_trigger(), [], config[CONF_LOCK_ACTION]
)
if CONF_OPEN_ACTION in config:
await automation.build_automation(
var.get_open_trigger(), [], config[CONF_OPEN_ACTION]
)
cg.add(var.traits.set_supports_open(CONF_OPEN_ACTION in config))
cg.add(var.traits.set_assumed_state(config[CONF_ASSUMED_STATE]))
cg.add(var.set_optimistic(config[CONF_OPTIMISTIC]))
@automation.register_action(
"lock.template.publish",
lock.LockPublishAction,
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(lock.Lock),
cv.Required(CONF_STATE): cv.templatable(validate_lock_state),
}
),
)
async def lock_template_publish_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_STATE], args, LockState)
cg.add(var.set_state(template_))
return var

View File

@ -0,0 +1,59 @@
#include "template_lock.h"
#include "esphome/core/log.h"
namespace esphome {
namespace template_ {
using namespace esphome::lock;
static const char *const TAG = "template.lock";
TemplateLock::TemplateLock()
: lock_trigger_(new Trigger<>()), unlock_trigger_(new Trigger<>()), open_trigger_(new Trigger<>()) {}
void TemplateLock::loop() {
if (!this->f_.has_value())
return;
auto val = (*this->f_)();
if (!val.has_value())
return;
this->publish_state(*val);
}
void TemplateLock::control(const lock::LockCall &call) {
if (this->prev_trigger_ != nullptr) {
this->prev_trigger_->stop_action();
}
auto state = *call.get_state();
if (state == LOCK_STATE_LOCKED) {
this->prev_trigger_ = this->lock_trigger_;
this->lock_trigger_->trigger();
} else if (state == LOCK_STATE_UNLOCKED) {
this->prev_trigger_ = this->unlock_trigger_;
this->unlock_trigger_->trigger();
}
if (this->optimistic_)
this->publish_state(state);
}
void TemplateLock::open_latch() {
if (this->prev_trigger_ != nullptr) {
this->prev_trigger_->stop_action();
}
this->prev_trigger_ = this->open_trigger_;
this->open_trigger_->trigger();
}
void TemplateLock::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
void TemplateLock::set_state_lambda(std::function<optional<lock::LockState>()> &&f) { this->f_ = f; }
float TemplateLock::get_setup_priority() const { return setup_priority::HARDWARE; }
Trigger<> *TemplateLock::get_lock_trigger() const { return this->lock_trigger_; }
Trigger<> *TemplateLock::get_unlock_trigger() const { return this->unlock_trigger_; }
Trigger<> *TemplateLock::get_open_trigger() const { return this->open_trigger_; }
void TemplateLock::dump_config() {
LOG_LOCK("", "Template Lock", this);
ESP_LOGCONFIG(TAG, " Optimistic: %s", YESNO(this->optimistic_));
}
} // namespace template_
} // namespace esphome

View File

@ -0,0 +1,38 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "esphome/components/lock/lock.h"
namespace esphome {
namespace template_ {
class TemplateLock : public lock::Lock, public Component {
public:
TemplateLock();
void dump_config() override;
void set_state_lambda(std::function<optional<lock::LockState>()> &&f);
Trigger<> *get_lock_trigger() const;
Trigger<> *get_unlock_trigger() const;
Trigger<> *get_open_trigger() const;
void set_optimistic(bool optimistic);
void loop() override;
float get_setup_priority() const override;
protected:
void control(const lock::LockCall &call) override;
void open_latch() override;
optional<std::function<optional<lock::LockState>()>> f_;
bool optimistic_{false};
Trigger<> *lock_trigger_;
Trigger<> *unlock_trigger_;
Trigger<> *open_trigger_;
Trigger<> *prev_trigger_{nullptr};
};
} // namespace template_
} // namespace esphome

View File

@ -152,6 +152,13 @@ void WebServer::setup() {
client->send(this->select_json(obj, obj->state).c_str(), "state");
}
#endif
#ifdef USE_LOCK
for (auto *obj : App.get_locks()) {
if (this->include_internal_ || !obj->is_internal())
client->send(this->lock_json(obj, obj->state).c_str(), "state");
}
#endif
});
#ifdef USE_LOGGER
@ -287,6 +294,20 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) {
}
#endif
#ifdef USE_LOCK
for (auto *obj : App.get_locks()) {
if (this->include_internal_ || !obj->is_internal()) {
write_row(stream, obj, "lock", "", [](AsyncResponseStream &stream, EntityBase *obj) {
lock::Lock *lock = (lock::Lock *) obj;
stream.print("<button>Lock</button><button>Unlock</button>");
if (lock->traits.get_supports_open()) {
stream.print("<button>Open</button>");
}
});
}
}
#endif
stream->print(F("</tbody></table><p>See <a href=\"https://esphome.io/web-api/index.html\">ESPHome Web API</a> for "
"REST API documentation.</p>"));
if (this->allow_ota_) {
@ -763,6 +784,43 @@ std::string WebServer::select_json(select::Select *obj, const std::string &value
}
#endif
#ifdef USE_LOCK
void WebServer::on_lock_update(lock::Lock *obj) {
this->events_.send(this->lock_json(obj, obj->state).c_str(), "state");
}
std::string WebServer::lock_json(lock::Lock *obj, lock::LockState value) {
return json::build_json([obj, value](JsonObject root) {
root["id"] = "lock-" + obj->get_object_id();
root["state"] = lock::lock_state_to_string(value);
root["value"] = value;
});
}
void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (lock::Lock *obj : App.get_locks()) {
if (obj->get_object_id() != match.id)
continue;
if (request->method() == HTTP_GET) {
std::string data = this->lock_json(obj, obj->state);
request->send(200, "text/json", data.c_str());
} else if (match.method == "lock") {
this->defer([obj]() { obj->lock(); });
request->send(200);
} else if (match.method == "unlock") {
this->defer([obj]() { obj->unlock(); });
request->send(200);
} else if (match.method == "open") {
this->defer([obj]() { obj->open(); });
request->send(200);
} else {
request->send(404);
}
return;
}
request->send(404);
}
#endif
bool WebServer::canHandle(AsyncWebServerRequest *request) {
if (request->url() == "/")
return true;
@ -830,6 +888,11 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) {
return true;
#endif
#ifdef USE_LOCK
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "lock")
return true;
#endif
return false;
}
void WebServer::handleRequest(AsyncWebServerRequest *request) {
@ -922,6 +985,13 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) {
return;
}
#endif
#ifdef USE_LOCK
if (match.domain == "lock") {
this->handle_lock_request(request, match);
return;
}
#endif
}
bool WebServer::isRequestHandlerTrivial() { return false; }

View File

@ -185,6 +185,16 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
std::string select_json(select::Select *obj, const std::string &value);
#endif
#ifdef USE_LOCK
void on_lock_update(lock::Lock *obj) override;
/// Handle a lock request under '/lock/<id>/</lock/unlock/open>'.
void handle_lock_request(AsyncWebServerRequest *request, const UrlMatch &match);
/// Dump the lock state with its value as a JSON string.
std::string lock_json(lock::Lock *obj, lock::LockState value);
#endif
/// Override the web handler's canHandle method.
bool canHandle(AsyncWebServerRequest *request) override;
/// Override the web handler's handleRequest method.

View File

@ -334,6 +334,7 @@ CONF_LINE_THICKNESS = "line_thickness"
CONF_LINE_TYPE = "line_type"
CONF_LOADED_INTEGRATIONS = "loaded_integrations"
CONF_LOCAL = "local"
CONF_LOCK_ACTION = "lock_action"
CONF_LOG_TOPIC = "log_topic"
CONF_LOGGER = "logger"
CONF_LOGS = "logs"
@ -426,9 +427,11 @@ CONF_ON_ENROLLMENT_SCAN = "on_enrollment_scan"
CONF_ON_FINGER_SCAN_MATCHED = "on_finger_scan_matched"
CONF_ON_FINGER_SCAN_UNMATCHED = "on_finger_scan_unmatched"
CONF_ON_JSON_MESSAGE = "on_json_message"
CONF_ON_LOCK = "on_lock"
CONF_ON_LOOP = "on_loop"
CONF_ON_MESSAGE = "on_message"
CONF_ON_MULTI_CLICK = "on_multi_click"
CONF_ON_OPEN = "on_open"
CONF_ON_PRESS = "on_press"
CONF_ON_RAW_VALUE = "on_raw_value"
CONF_ON_RELEASE = "on_release"
@ -442,6 +445,7 @@ CONF_ON_TIME_SYNC = "on_time_sync"
CONF_ON_TOUCH = "on_touch"
CONF_ON_TURN_OFF = "on_turn_off"
CONF_ON_TURN_ON = "on_turn_on"
CONF_ON_UNLOCK = "on_unlock"
CONF_ON_VALUE = "on_value"
CONF_ON_VALUE_RANGE = "on_value_range"
CONF_ONE = "one"
@ -709,6 +713,7 @@ CONF_UART_ID = "uart_id"
CONF_UID = "uid"
CONF_UNIQUE = "unique"
CONF_UNIT_OF_MEASUREMENT = "unit_of_measurement"
CONF_UNLOCK_ACTION = "unlock_action"
CONF_UPDATE_INTERVAL = "update_interval"
CONF_UPDATE_ON_BOOT = "update_on_boot"
CONF_URL = "url"

View File

@ -42,6 +42,9 @@
#ifdef USE_SELECT
#include "esphome/components/select/select.h"
#endif
#ifdef USE_LOCK
#include "esphome/components/lock/lock.h"
#endif
namespace esphome {
@ -104,6 +107,10 @@ class Application {
void register_select(select::Select *select) { this->selects_.push_back(select); }
#endif
#ifdef USE_LOCK
void register_lock(lock::Lock *a_lock) { this->locks_.push_back(a_lock); }
#endif
/// Register the component in this Application instance.
template<class C> C *register_component(C *c) {
static_assert(std::is_base_of<Component, C>::value, "Only Component subclasses can be registered");
@ -257,6 +264,15 @@ class Application {
return nullptr;
}
#endif
#ifdef USE_LOCK
const std::vector<lock::Lock *> &get_locks() { return this->locks_; }
lock::Lock *get_lock_by_key(uint32_t key, bool include_internal = false) {
for (auto *obj : this->locks_)
if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
return obj;
return nullptr;
}
#endif
Scheduler scheduler;
@ -305,6 +321,9 @@ class Application {
#ifdef USE_SELECT
std::vector<select::Select *> selects_{};
#endif
#ifdef USE_LOCK
std::vector<lock::Lock *> locks_{};
#endif
std::string name_;
std::string compilation_time_;

View File

@ -65,6 +65,12 @@ void Controller::setup_controller(bool include_internal) {
obj->add_on_state_callback([this, obj](const std::string &state) { this->on_select_update(obj, state); });
}
#endif
#ifdef USE_LOCK
for (auto *obj : App.get_locks()) {
if (include_internal || !obj->is_internal())
obj->add_on_state_callback([this, obj]() { this->on_lock_update(obj); });
}
#endif
}
} // namespace esphome

View File

@ -34,6 +34,9 @@
#ifdef USE_SELECT
#include "esphome/components/select/select.h"
#endif
#ifdef USE_LOCK
#include "esphome/components/lock/lock.h"
#endif
namespace esphome {
@ -70,6 +73,9 @@ class Controller {
#ifdef USE_SELECT
virtual void on_select_update(select::Select *obj, const std::string &state){};
#endif
#ifdef USE_LOCK
virtual void on_lock_update(lock::Lock *obj){};
#endif
};
} // namespace esphome

View File

@ -26,6 +26,7 @@
#define USE_GRAPH
#define USE_HOMEASSISTANT_TIME
#define USE_LIGHT
#define USE_LOCK
#define USE_LOGGER
#define USE_MDNS
#define USE_NUMBER

View File

@ -598,6 +598,7 @@ def lint_inclusive_language(fname, match):
"esphome/components/display/display_buffer.h",
"esphome/components/fan/fan.h",
"esphome/components/i2c/i2c.h",
"esphome/components/lock/lock.h",
"esphome/components/mqtt/mqtt_component.h",
"esphome/components/number/number.h",
"esphome/components/output/binary_output.h",

View File

@ -2582,3 +2582,28 @@ select:
qr_code:
- id: homepage_qr
value: https://esphome.io/index.html
lock:
- platform: template
id: test_lock1
name: "Template Switch"
lambda: |-
if (id(binary_sensor1).state) {
return LOCK_STATE_LOCKED;
}else{
return LOCK_STATE_UNLOCKED;
}
optimistic: true
assumed_state: no
on_unlock:
- lock.template.publish:
id: test_lock1
state: !lambda "return LOCK_STATE_UNLOCKED;"
on_lock:
- lock.template.publish:
id: test_lock1
state: !lambda "return LOCK_STATE_LOCKED;"
- platform: output
name: "Generic Output Lock"
id: test_lock2
output: pca_6