mirror of
https://github.com/esphome/esphome.git
synced 2025-10-09 13:23:47 +01:00
Add new Lock core component (#2958)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
102
esphome/components/lock/__init__.py
Normal file
102
esphome/components/lock/__init__.py
Normal 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")
|
87
esphome/components/lock/automation.h
Normal file
87
esphome/components/lock/automation.h
Normal 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
|
109
esphome/components/lock/lock.cpp
Normal file
109
esphome/components/lock/lock.cpp
Normal 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
|
178
esphome/components/lock/lock.h
Normal file
178
esphome/components/lock/lock.h
Normal 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
|
Reference in New Issue
Block a user