1
0
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:
Keilin Bickar
2022-02-03 13:24:31 -05:00
committed by GitHub
parent 62b366a5ec
commit 21803607e7
41 changed files with 1558 additions and 3 deletions

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