From c47dd66cdc7a356ebe9795ad2a95fb7402e8a03b Mon Sep 17 00:00:00 2001 From: Florian Hess Date: Thu, 31 Oct 2024 21:35:36 +0100 Subject: [PATCH 1/3] Move security pin to number entity --- .github/test-config.yaml | 3 +- .gitignore | 5 ++ README.md | 9 ++-- components/nuki_lock/lock.py | 26 ++++++++--- components/nuki_lock/nuki_lock.cpp | 74 ++++++++++++++++++++++++------ components/nuki_lock/nuki_lock.h | 33 +++++++++++-- 6 files changed, 122 insertions(+), 28 deletions(-) diff --git a/.github/test-config.yaml b/.github/test-config.yaml index a9b8b5e..799227a 100644 --- a/.github/test-config.yaml +++ b/.github/test-config.yaml @@ -71,6 +71,8 @@ lock: led_brightness: name: "Nuki LED Brightness" + security_pin: + name: "Nuki Security Pin" single_buton_press_action: name: "Nuki Single Button Press Action" @@ -83,7 +85,6 @@ lock: fob_action_3: name: "Nuki Fob Action 3" - security_pin: 1234 pairing_mode_timeout: 300s event: "nuki" diff --git a/.gitignore b/.gitignore index dfcfd56..0083d34 100644 --- a/.gitignore +++ b/.gitignore @@ -348,3 +348,8 @@ MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ + +# ESPHome +__pycache__ +.esphome +/*.yaml \ No newline at end of file diff --git a/README.md b/README.md index 6d54990..20d244b 100644 --- a/README.md +++ b/README.md @@ -87,8 +87,10 @@ lock: unpair: name: "Nuki Unpair Device" + security_pin: + name: "Nuki Security Pin" + # Optional: Settings - security_pin: 1234 pairing_mode_timeout: 300s event: "nuki" @@ -263,15 +265,16 @@ context: - Auto Lock: Immediately - Automatic Updates -**Select:** +**Select Input:** - Single Button Press Action - Double Button Press Action - Fob Action 1 - Fob Action 2 - Fob Action 3 -**Number:** +**Number Input:** - LED Brightness +- Security Pin **Button:** - Unpair Device diff --git a/components/nuki_lock/lock.py b/components/nuki_lock/lock.py index dc2b48f..0badbeb 100644 --- a/components/nuki_lock/lock.py +++ b/components/nuki_lock/lock.py @@ -2,6 +2,7 @@ import esphome.config_validation as cv from esphome import automation from esphome.components import lock, binary_sensor, text_sensor, sensor, switch, button, number, select +from esphome.components.number import NUMBER_MODES from esphome.const import ( CONF_ID, CONF_BATTERY_LEVEL, @@ -12,7 +13,8 @@ UNIT_PERCENT, ENTITY_CATEGORY_CONFIG, ENTITY_CATEGORY_DIAGNOSTIC, - CONF_TRIGGER_ID + CONF_TRIGGER_ID, + CONF_MODE, ) AUTO_LOAD = ["binary_sensor", "text_sensor", "sensor", "switch", "button", "number", "select"] @@ -41,12 +43,15 @@ CONF_AUTO_UNLOCK_DISABLED_SWITCH = "auto_unlock_disabled" CONF_IMMEDIATE_AUTO_LOCK_ENABLED_SWITCH = "immediate_auto_lock_enabled" CONF_AUTO_UPDATE_ENABLED_SWITCH = "auto_update_enabled" + CONF_SINGLE_BUTTON_PRESS_ACTION_SELECT = "single_buton_press_action" CONF_DOUBLE_BUTTON_PRESS_ACTION_SELECT = "double_buton_press_action" CONF_FOB_ACTION_1_SELECT = "fob_action_1" CONF_FOB_ACTION_2_SELECT = "fob_action_2" CONF_FOB_ACTION_3_SELECT = "fob_action_3" + CONF_LED_BRIGHTNESS_NUMBER = "led_brightness" +CONF_SECURITY_PIN_NUMBER = "security_pin" CONF_BUTTON_PRESS_ACTION_SELECT_OPTIONS = [ "No Action", @@ -67,7 +72,6 @@ ] CONF_PAIRING_MODE_TIMEOUT = "pairing_mode_timeout" -CONF_SECURITY_PIN = "security_pin" CONF_EVENT = "event" CONF_SET_PAIRING_MODE = "pairing_mode" @@ -93,6 +97,7 @@ NukiLockImmediateAutoLockEnabledSwitch = nuki_lock_ns.class_("NukiLockImmediateAutoLockEnabledSwitch", switch.Switch, cg.Component) NukiLockAutoUpdateEnabledSwitch = nuki_lock_ns.class_("NukiLockAutoUpdateEnabledSwitch", switch.Switch, cg.Component) NukiLockLedBrightnessNumber = nuki_lock_ns.class_("NukiLockLedBrightnessNumber", number.Number, cg.Component) +NukiLockSecurityPinNumber = nuki_lock_ns.class_("NukiLockSecurityPinNumber", number.Number, cg.Component) NukiLockSingleButtonPressActionSelect = nuki_lock_ns.class_("NukiLockSingleButtonPressActionSelect", select.Select, cg.Component) NukiLockDoubleButtonPressActionSelect = nuki_lock_ns.class_("NukiLockDoubleButtonPressActionSelect", select.Select, cg.Component) NukiLockFobAction1Select = nuki_lock_ns.class_("NukiLockFobAction1Select", select.Select, cg.Component) @@ -232,6 +237,12 @@ icon="mdi:brightness-6", ), + cv.Optional(CONF_SECURITY_PIN_NUMBER): number.number_schema( + NukiLockSecurityPinNumber, + entity_category=ENTITY_CATEGORY_CONFIG, + icon="mdi:shield-key", + ).extend({ cv.Optional(CONF_MODE, default="BOX"): cv.enum(NUMBER_MODES, upper=True), }), + cv.Optional(CONF_SINGLE_BUTTON_PRESS_ACTION_SELECT): select.select_schema( NukiLockSingleButtonPressActionSelect, entity_category=ENTITY_CATEGORY_CONFIG, @@ -258,7 +269,6 @@ icon="mdi:gesture-tap", ), - cv.Optional(CONF_SECURITY_PIN, default=0): cv.uint16_t, cv.Optional(CONF_PAIRING_MODE_TIMEOUT, default="300s"): cv.positive_time_period_seconds, cv.Optional(CONF_EVENT, default="nuki"): cv.string, @@ -286,9 +296,6 @@ async def to_code(config): await lock.register_lock(var, config) # Component Settings - if CONF_SECURITY_PIN in config: - cg.add(var.set_security_pin(config[CONF_SECURITY_PIN])) - if CONF_PAIRING_MODE_TIMEOUT in config: cg.add(var.set_pairing_mode_timeout(config[CONF_PAIRING_MODE_TIMEOUT])) @@ -340,6 +347,13 @@ async def to_code(config): await cg.register_parented(n, config[CONF_ID]) cg.add(var.set_led_brightness_number(n)) + if security_pin := config.get(CONF_SECURITY_PIN_NUMBER): + n = await number.new_number( + security_pin, min_value=0, max_value=65535, step=0 + ) + await cg.register_parented(n, config[CONF_ID]) + cg.add(var.set_security_pin_number(n)) + # Switch if pairing_mode := config.get(CONF_PAIRING_MODE_SWITCH): s = await switch.new_switch(pairing_mode) diff --git a/components/nuki_lock/nuki_lock.cpp b/components/nuki_lock/nuki_lock.cpp index fe70c47..edad670 100644 --- a/components/nuki_lock/nuki_lock.cpp +++ b/components/nuki_lock/nuki_lock.cpp @@ -1,11 +1,18 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" +#include "esphome/core/preferences.h" + +#ifdef USE_API #include "esphome/components/api/custom_api_device.h" +#endif + #include "nuki_lock.h" namespace esphome { namespace nuki_lock { +uint32_t global_nuki_lock_id = 1912044085ULL; + lock::LockState NukiLockComponent::nuki_to_lock_state(NukiLock::LockState nukiLockState) { switch(nukiLockState) { case NukiLock::LockState::Locked: @@ -100,6 +107,18 @@ std::string NukiLockComponent::fob_action_to_string(uint8_t action) return "No Action"; } +void NukiLockComponent::save_settings() +{ + NukiLockSettings settings { + this->security_pin_ + }; + + if (!this->pref_.save(&settings)) + { + ESP_LOGW(TAG, "Failed to save settings"); + } +} + void NukiLockComponent::update_status() { this->status_update_ = false; @@ -482,6 +501,7 @@ void NukiLockComponent::process_log_entries(const std::list& } // Send as Home Assistant Event + #ifdef USE_API if(log.index > this->last_rolling_log_id) { this->last_rolling_log_id = log.index; @@ -490,6 +510,7 @@ void NukiLockComponent::process_log_entries(const std::list& ESP_LOGD(TAG, "Send event to Home Assistant on %s", event_); capi->fire_homeassistant_event(event_, event_data); } + #endif } } @@ -532,6 +553,23 @@ bool NukiLockComponent::execute_lock_action(NukiLock::LockAction lock_action) { } } +void NukiLockComponent::set_security_pin(uint16_t security_pin) +{ + this->security_pin_ = security_pin; + + bool result = this->nuki_lock_.saveSecurityPincode(this->security_pin_); + if (result) { + ESP_LOGI(TAG, "Set pincode done"); + } else { + ESP_LOGE(TAG, "Set pincode failed!"); + } + + #ifdef USE_NUMBER + if (this->security_pin_number_ != nullptr) + this->security_pin_number_->publish_state(this->security_pin_); + #endif +} + void NukiLockComponent::setup() { ESP_LOGI(TAG, "Starting NUKI Lock..."); @@ -539,6 +577,16 @@ void NukiLockComponent::setup() { // Fixes Pairing Crash esp_task_wdt_init(15, false); + // Restore settings from flash + this->pref_ = global_preferences->make_preference(global_nuki_lock_id); + + NukiLockSettings recovered; + if (!this->pref_.load(&recovered)) + { + recovered = {0}; + } + this->set_security_pin(recovered.security_pin); + this->traits.set_supported_states( std::set { lock::LOCK_STATE_NONE, @@ -557,15 +605,6 @@ void NukiLockComponent::setup() { this->nuki_lock_.initialize(); this->nuki_lock_.setConnectTimeout(BLE_CONNECT_TIMEOUT_SEC); this->nuki_lock_.setConnectRetries(BLE_CONNECT_TIMEOUT_RETRIES); - - if(this->security_pin_ > 0) { - bool result = this->nuki_lock_.saveSecurityPincode(this->security_pin_); - if (result) { - ESP_LOGI(TAG, "Set pincode done"); - } else { - ESP_LOGE(TAG, "Set pincode failed!"); - } - } if (this->nuki_lock_.isPairedWithLock()) { this->status_update_ = true; @@ -592,11 +631,13 @@ void NukiLockComponent::setup() { this->publish_state(lock::LOCK_STATE_NONE); - register_service(&NukiLockComponent::lock_n_go, "lock_n_go"); - register_service(&NukiLockComponent::print_keypad_entries, "print_keypad_entries"); - register_service(&NukiLockComponent::add_keypad_entry, "add_keypad_entry", {"name", "code"}); - register_service(&NukiLockComponent::update_keypad_entry, "update_keypad_entry", {"id", "name", "code", "enabled"}); - register_service(&NukiLockComponent::delete_keypad_entry, "delete_keypad_entry", {"id"}); + #ifdef USE_API + this->custom_api_device_.register_service(&NukiLockComponent::lock_n_go, "lock_n_go"); + this->custom_api_device_.register_service(&NukiLockComponent::print_keypad_entries, "print_keypad_entries"); + this->custom_api_device_.register_service(&NukiLockComponent::add_keypad_entry, "add_keypad_entry", {"name", "code"}); + this->custom_api_device_.register_service(&NukiLockComponent::update_keypad_entry, "update_keypad_entry", {"id", "name", "code", "enabled"}); + this->custom_api_device_.register_service(&NukiLockComponent::delete_keypad_entry, "delete_keypad_entry", {"id"}); + #endif } void NukiLockComponent::update() { @@ -1263,6 +1304,11 @@ void NukiLockAutoUpdateEnabledSwitch::write_state(bool state) { void NukiLockLedBrightnessNumber::control(float value) { this->parent_->set_config_number("led_brightness", value); } +void NukiLockSecurityPinNumber::control(float value) { + this->publish_state(value); + this->parent_->set_security_pin(value); + this->parent_->save_settings(); +} #endif // Callbacks diff --git a/components/nuki_lock/nuki_lock.h b/components/nuki_lock/nuki_lock.h index b44059f..751fd1e 100644 --- a/components/nuki_lock/nuki_lock.h +++ b/components/nuki_lock/nuki_lock.h @@ -1,8 +1,12 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/components/api/custom_api_device.h" #include "esphome/components/lock/lock.h" +#include "esphome/core/preferences.h" + +#ifdef USE_API +#include "esphome/components/api/custom_api_device.h" +#endif #ifdef USE_BUTTON #include "esphome/components/button/button.h" @@ -35,7 +39,12 @@ namespace nuki_lock { static const char *TAG = "nuki_lock.lock"; -class NukiLockComponent : public lock::Lock, public PollingComponent, public api::CustomAPIDevice, public Nuki::SmartlockEventHandler { +struct NukiLockSettings +{ + uint16_t security_pin; +}; + +class NukiLockComponent : public lock::Lock, public PollingComponent, public Nuki::SmartlockEventHandler { #ifdef USE_BINARY_SENSOR SUB_BINARY_SENSOR(is_connected) @@ -52,6 +61,7 @@ class NukiLockComponent : public lock::Lock, public PollingComponent, public api #endif #ifdef USE_NUMBER SUB_NUMBER(led_brightness) + SUB_NUMBER(security_pin) #endif #ifdef USE_SELECT SUB_SELECT(single_button_press_action) @@ -108,7 +118,6 @@ class NukiLockComponent : public lock::Lock, public PollingComponent, public api void notify(Nuki::EventType event_type) override; float get_setup_priority() const override { return setup_priority::HARDWARE - 1.0f; } - void set_security_pin(uint16_t security_pin) { this->security_pin_ = security_pin; } void set_pairing_mode_timeout(uint16_t pairing_mode_timeout) { this->pairing_mode_timeout_ = pairing_mode_timeout; } void set_event(const char *event) { this->event_ = event; } @@ -133,6 +142,9 @@ class NukiLockComponent : public lock::Lock, public PollingComponent, public api void unpair(); void set_pairing_mode(bool enabled); + void set_security_pin(uint16_t security_pin); + void save_settings(); + #ifdef USE_NUMBER void set_config_number(std::string config, float value); #endif @@ -161,6 +173,10 @@ class NukiLockComponent : public lock::Lock, public PollingComponent, public api NukiLock::KeyTurnerState retrieved_key_turner_state_; NukiLock::LockAction lock_action_; + #ifdef USE_API + api::CustomAPIDevice custom_api_device_; + #endif + std::map auth_entries_; uint32_t auth_id_ = 0; char auth_name_[33]; @@ -181,13 +197,15 @@ class NukiLockComponent : public lock::Lock, public PollingComponent, public api uint16_t security_pin_ = 0; const char* event_; - uint16_t pairing_mode_timeout_ = 0; + uint16_t pairing_mode_timeout_ = 0; bool pairing_mode_ = false; uint32_t pairing_mode_timer_ = 0; uint32_t last_rolling_log_id = 0; + ESPPreferenceObject pref_; + private: NukiLock::NukiLock nuki_lock_; @@ -399,6 +417,13 @@ class NukiLockLedBrightnessNumber : public number::Number, public Parented { + public: + NukiLockSecurityPinNumber() = default; + + protected: + void control(float value) override; +}; #endif } //namespace nuki_lock From 25cc0467e75bc8ea3600d508f241cfc2954f1cf5 Mon Sep 17 00:00:00 2001 From: Florian Date: Thu, 31 Oct 2024 22:57:52 +0100 Subject: [PATCH 2/3] Fix security pin steps --- components/nuki_lock/lock.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/components/nuki_lock/lock.py b/components/nuki_lock/lock.py index 0badbeb..b1bba26 100644 --- a/components/nuki_lock/lock.py +++ b/components/nuki_lock/lock.py @@ -130,6 +130,7 @@ ), cv.Optional(CONF_BATTERY_CRITICAL): binary_sensor.binary_sensor_schema( device_class=DEVICE_CLASS_BATTERY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, icon="mdi:battery-alert-variant-outline", ), cv.Optional(CONF_DOOR_SENSOR): binary_sensor.binary_sensor_schema( @@ -148,6 +149,7 @@ cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( device_class=DEVICE_CLASS_BATTERY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, unit_of_measurement=UNIT_PERCENT, icon="mdi:battery-50", ), @@ -349,7 +351,7 @@ async def to_code(config): if security_pin := config.get(CONF_SECURITY_PIN_NUMBER): n = await number.new_number( - security_pin, min_value=0, max_value=65535, step=0 + security_pin, min_value=0, max_value=65535, step=1 ) await cg.register_parented(n, config[CONF_ID]) cg.add(var.set_security_pin_number(n)) From 843c3f2746758850506baea30e359b5b10b157c5 Mon Sep 17 00:00:00 2001 From: Florian Date: Fri, 1 Nov 2024 01:41:14 +0100 Subject: [PATCH 3/3] Set last unlock user to manual instead of empty string --- components/nuki_lock/nuki_lock.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/components/nuki_lock/nuki_lock.cpp b/components/nuki_lock/nuki_lock.cpp index edad670..1f22d3b 100644 --- a/components/nuki_lock/nuki_lock.cpp +++ b/components/nuki_lock/nuki_lock.cpp @@ -386,6 +386,12 @@ void NukiLockComponent::process_log_entries(const std::list& auth_name[sizeName] = '\0'; } + if (std::string(auth_name) == "") + { + memset(auth_name, 0, sizeof(auth_name)); + memcpy(auth_name, "Manual", strlen("Manual")); + } + if(log.index > auth_index) { auth_index = log.index; @@ -407,7 +413,7 @@ void NukiLockComponent::process_log_entries(const std::list& std::map event_data; event_data["index"] = std::to_string(log.index); event_data["authorizationId"] = std::to_string(log.authId); - event_data["authorizationName"] = auth_name; + event_data["authorizationName"] = this->auth_name_; if(this->auth_entries_.count(log.authId) > 0) {