From 21803607e79ecb799171c59081c53979c1bfc84a Mon Sep 17 00:00:00 2001 From: Keilin Bickar Date: Thu, 3 Feb 2022 13:24:31 -0500 Subject: [PATCH] Add new Lock core component (#2958) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/api/api.proto | 59 ++++ esphome/components/api/api_connection.cpp | 43 +++ esphome/components/api/api_connection.h | 5 + esphome/components/api/api_pb2.cpp | 262 +++++++++++++++++- esphome/components/api/api_pb2.h | 65 +++++ esphome/components/api/api_pb2_service.cpp | 42 +++ esphome/components/api/api_pb2_service.h | 15 + esphome/components/api/api_server.cpp | 9 + esphome/components/api/api_server.h | 3 + esphome/components/api/list_entities.cpp | 3 + esphome/components/api/list_entities.h | 3 + esphome/components/api/subscribe_state.cpp | 3 + esphome/components/api/subscribe_state.h | 3 + esphome/components/api/util.cpp | 15 + esphome/components/api/util.h | 6 + esphome/components/cover/__init__.py | 2 +- esphome/components/lock/__init__.py | 102 +++++++ esphome/components/lock/automation.h | 87 ++++++ esphome/components/lock/lock.cpp | 109 ++++++++ esphome/components/lock/lock.h | 178 ++++++++++++ esphome/components/mqtt/__init__.py | 1 + esphome/components/mqtt/mqtt_lock.cpp | 55 ++++ esphome/components/mqtt/mqtt_lock.h | 41 +++ esphome/components/output/lock/__init__.py | 23 ++ .../components/output/lock/output_lock.cpp | 22 ++ esphome/components/output/lock/output_lock.h | 24 ++ .../prometheus/prometheus_handler.cpp | 30 ++ .../prometheus/prometheus_handler.h | 7 + esphome/components/template/lock/__init__.py | 103 +++++++ .../template/lock/template_lock.cpp | 59 ++++ .../components/template/lock/template_lock.h | 38 +++ esphome/components/web_server/web_server.cpp | 70 +++++ esphome/components/web_server/web_server.h | 10 + esphome/const.py | 5 + esphome/core/application.h | 19 ++ esphome/core/controller.cpp | 6 + esphome/core/controller.h | 6 + esphome/core/defines.h | 1 + script/ci-custom.py | 1 + tests/test1.yaml | 25 ++ 41 files changed, 1558 insertions(+), 3 deletions(-) create mode 100644 esphome/components/lock/__init__.py create mode 100644 esphome/components/lock/automation.h create mode 100644 esphome/components/lock/lock.cpp create mode 100644 esphome/components/lock/lock.h create mode 100644 esphome/components/mqtt/mqtt_lock.cpp create mode 100644 esphome/components/mqtt/mqtt_lock.h create mode 100644 esphome/components/output/lock/__init__.py create mode 100644 esphome/components/output/lock/output_lock.cpp create mode 100644 esphome/components/output/lock/output_lock.h create mode 100644 esphome/components/template/lock/__init__.py create mode 100644 esphome/components/template/lock/template_lock.cpp create mode 100644 esphome/components/template/lock/template_lock.h diff --git a/CODEOWNERS b/CODEOWNERS index f075cc8649..5fa3090aaf 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -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 diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 9d43e22497..3ab426979e 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -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; } + diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 8a106dc39c..21388b547e 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -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(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(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 image) { if (!this->state_subscription_) diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index c1f520c83b..10f0becc54 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -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) { diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 4ecd727f29..e7e0476afc 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -278,6 +278,36 @@ template<> const char *proto_enum_to_string(enums::NumberMode return "UNKNOWN"; } } +template<> const char *proto_enum_to_string(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 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(); + 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(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(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(); + 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(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(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(); + 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(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(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); diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 48ecb5f682..4c9a0e9c0f 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -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{}; diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index 567fbf02c9..d981a3bf4e 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -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_(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_(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 diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 50b08d3ec4..5aaf831c91 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -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 diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 93c9209716..4521cc5bfc 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -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) diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 23b01df375..3214da5b3d 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -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); } diff --git a/esphome/components/api/list_entities.cpp b/esphome/components/api/list_entities.cpp index 35a590a828..fb0dfa3d05 100644 --- a/esphome/components/api/list_entities.cpp +++ b/esphome/components/api/list_entities.cpp @@ -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) diff --git a/esphome/components/api/list_entities.h b/esphome/components/api/list_entities.h index 81b814676a..bfceb39ebf 100644 --- a/esphome/components/api/list_entities.h +++ b/esphome/components/api/list_entities.h @@ -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; diff --git a/esphome/components/api/subscribe_state.cpp b/esphome/components/api/subscribe_state.cpp index 07b3913ff7..10416ecc5c 100644 --- a/esphome/components/api/subscribe_state.cpp +++ b/esphome/components/api/subscribe_state.cpp @@ -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) {} diff --git a/esphome/components/api/subscribe_state.h b/esphome/components/api/subscribe_state.h index 4b83b5e793..caea013f84 100644 --- a/esphome/components/api/subscribe_state.h +++ b/esphome/components/api/subscribe_state.h @@ -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_; diff --git a/esphome/components/api/util.cpp b/esphome/components/api/util.cpp index f5fd752101..fd55f89f9b 100644 --- a/esphome/components/api/util.cpp +++ b/esphome/components/api/util.cpp @@ -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()) { diff --git a/esphome/components/api/util.h b/esphome/components/api/util.h index f329867a4e..9204b0829e 100644 --- a/esphome/components/api/util.h +++ b/esphome/components/api/util.h @@ -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}; diff --git a/esphome/components/cover/__init__.py b/esphome/components/cover/__init__.py index 0fd27f3f27..d2421f07d9 100644 --- a/esphome/components/cover/__init__.py +++ b/esphome/components/cover/__init__.py @@ -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( diff --git a/esphome/components/lock/__init__.py b/esphome/components/lock/__init__.py new file mode 100644 index 0000000000..f659c48a6e --- /dev/null +++ b/esphome/components/lock/__init__.py @@ -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") diff --git a/esphome/components/lock/automation.h b/esphome/components/lock/automation.h new file mode 100644 index 0000000000..74cfbe2ef6 --- /dev/null +++ b/esphome/components/lock/automation.h @@ -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 class LockAction : public Action { + public: + explicit LockAction(Lock *a_lock) : lock_(a_lock) {} + + void play(Ts... x) override { this->lock_->lock(); } + + protected: + Lock *lock_; +}; + +template class UnlockAction : public Action { + public: + explicit UnlockAction(Lock *a_lock) : lock_(a_lock) {} + + void play(Ts... x) override { this->lock_->unlock(); } + + protected: + Lock *lock_; +}; + +template class OpenAction : public Action { + public: + explicit OpenAction(Lock *a_lock) : lock_(a_lock) {} + + void play(Ts... x) override { this->lock_->open(); } + + protected: + Lock *lock_; +}; + +template class LockCondition : public Condition { + 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 class LockPublishAction : public Action { + 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 diff --git a/esphome/components/lock/lock.cpp b/esphome/components/lock/lock.cpp new file mode 100644 index 0000000000..e32ab6d0a6 --- /dev/null +++ b/esphome/components/lock/lock.cpp @@ -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 &&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 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 &LockCall::get_state() const { return this->state_; } + +} // namespace lock +} // namespace esphome diff --git a/esphome/components/lock/lock.h b/esphome/components/lock/lock.h new file mode 100644 index 0000000000..f11035c03e --- /dev/null +++ b/esphome/components/lock/lock.h @@ -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 + +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 get_supported_states() const { return supported_states_; } + void set_supported_states(std::set 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 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 state); + /// Set the state of the lock device based on a string. + LockCall &set_state(const std::string &state); + + void perform(); + + const optional &get_state() const; + + protected: + void validate_(); + + Lock *const parent_; + optional 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 &&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 state_callback_{}; + Deduplicator publish_dedup_; + ESPPreferenceObject rtc_; +}; + +} // namespace lock +} // namespace esphome diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index a7e14bd4a6..88e5d43509 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -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 = { diff --git a/esphome/components/mqtt/mqtt_lock.cpp b/esphome/components/mqtt/mqtt_lock.cpp new file mode 100644 index 0000000000..197d0c32d4 --- /dev/null +++ b/esphome/components/mqtt/mqtt_lock.cpp @@ -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 diff --git a/esphome/components/mqtt/mqtt_lock.h b/esphome/components/mqtt/mqtt_lock.h new file mode 100644 index 0000000000..789f74c795 --- /dev/null +++ b/esphome/components/mqtt/mqtt_lock.h @@ -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 diff --git a/esphome/components/output/lock/__init__.py b/esphome/components/output/lock/__init__.py new file mode 100644 index 0000000000..3be2cb09aa --- /dev/null +++ b/esphome/components/output/lock/__init__.py @@ -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_)) diff --git a/esphome/components/output/lock/output_lock.cpp b/esphome/components/output/lock/output_lock.cpp new file mode 100644 index 0000000000..2545f62481 --- /dev/null +++ b/esphome/components/output/lock/output_lock.cpp @@ -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 diff --git a/esphome/components/output/lock/output_lock.h b/esphome/components/output/lock/output_lock.h new file mode 100644 index 0000000000..c183c3a3ea --- /dev/null +++ b/esphome/components/output/lock/output_lock.h @@ -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 diff --git a/esphome/components/prometheus/prometheus_handler.cpp b/esphome/components/prometheus/prometheus_handler.cpp index e65729b184..e4dd6b9043 100644 --- a/esphome/components/prometheus/prometheus_handler.cpp +++ b/esphome/components/prometheus/prometheus_handler.cpp @@ -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 diff --git a/esphome/components/prometheus/prometheus_handler.h b/esphome/components/prometheus/prometheus_handler.h index 82e3fe28e0..5c8d51c60f 100644 --- a/esphome/components/prometheus/prometheus_handler.h +++ b/esphome/components/prometheus/prometheus_handler.h @@ -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_; }; diff --git a/esphome/components/template/lock/__init__.py b/esphome/components/template/lock/__init__.py new file mode 100644 index 0000000000..24709ff4f2 --- /dev/null +++ b/esphome/components/template/lock/__init__.py @@ -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 diff --git a/esphome/components/template/lock/template_lock.cpp b/esphome/components/template/lock/template_lock.cpp new file mode 100644 index 0000000000..87ba1046eb --- /dev/null +++ b/esphome/components/template/lock/template_lock.cpp @@ -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()> &&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 diff --git a/esphome/components/template/lock/template_lock.h b/esphome/components/template/lock/template_lock.h new file mode 100644 index 0000000000..4f798eca81 --- /dev/null +++ b/esphome/components/template/lock/template_lock.h @@ -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()> &&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()>> f_; + bool optimistic_{false}; + Trigger<> *lock_trigger_; + Trigger<> *unlock_trigger_; + Trigger<> *open_trigger_; + Trigger<> *prev_trigger_{nullptr}; +}; + +} // namespace template_ +} // namespace esphome diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 83b6ca1e2f..44d044750e 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -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(""); + if (lock->traits.get_supports_open()) { + stream.print(""); + } + }); + } + } +#endif + stream->print(F("

See ESPHome Web API for " "REST API documentation.

")); 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; } diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index afd4f1d4b5..3dd5c93f59 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -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//'. + 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. diff --git a/esphome/const.py b/esphome/const.py index 2d2f5f1da0..a2dd20269a 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -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" diff --git a/esphome/core/application.h b/esphome/core/application.h index 2598a2f4a4..cec6e7baa9 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -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 C *register_component(C *c) { static_assert(std::is_base_of::value, "Only Component subclasses can be registered"); @@ -257,6 +264,15 @@ class Application { return nullptr; } #endif +#ifdef USE_LOCK + const std::vector &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 selects_{}; #endif +#ifdef USE_LOCK + std::vector locks_{}; +#endif std::string name_; std::string compilation_time_; diff --git a/esphome/core/controller.cpp b/esphome/core/controller.cpp index 6d3a76a292..dfcef5e4c1 100644 --- a/esphome/core/controller.cpp +++ b/esphome/core/controller.cpp @@ -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 diff --git a/esphome/core/controller.h b/esphome/core/controller.h index 49750d1cc4..0be854828b 100644 --- a/esphome/core/controller.h +++ b/esphome/core/controller.h @@ -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 diff --git a/esphome/core/defines.h b/esphome/core/defines.h index acdc5df815..574a8dcafe 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -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 diff --git a/script/ci-custom.py b/script/ci-custom.py index 956716e5fa..7bbaaf1c79 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -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", diff --git a/tests/test1.yaml b/tests/test1.yaml index 40cd0d4827..a0c9d03f14 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -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