From 9d6fa7f4e4a406c1547b243c5189d1cb7db81bac Mon Sep 17 00:00:00 2001 From: Artem Sheremet Date: Mon, 10 Jun 2024 22:19:33 +0200 Subject: [PATCH] Sync time to the lock automatically --- basementdoorlock.yaml | 6 +++ components/nuki_lock/lock.py | 15 +++++++- components/nuki_lock/nuki_lock.cpp | 59 ++++++++++++++++++++++++++++++ components/nuki_lock/nuki_lock.h | 16 +++++++- frontdoorlock.yaml | 6 +++ 5 files changed, 99 insertions(+), 3 deletions(-) diff --git a/basementdoorlock.yaml b/basementdoorlock.yaml index 41836a9..567c119 100644 --- a/basementdoorlock.yaml +++ b/basementdoorlock.yaml @@ -22,6 +22,10 @@ button: - platform: factory_reset name: Restart with Factory Default Settings +time: + - platform: sntp + id: sntp_time + lock: - platform: nuki_lock name: None @@ -44,3 +48,5 @@ lock: name: "Last action trigger" last_action_completion_status: name: "Last action completion status" + time_source: sntp_time + pin: !secret lock_pin diff --git a/components/nuki_lock/lock.py b/components/nuki_lock/lock.py index 0d3db08..7c1a185 100644 --- a/components/nuki_lock/lock.py +++ b/components/nuki_lock/lock.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv import esphome.const as c -from esphome.components import lock, binary_sensor, text_sensor, sensor, button +from esphome.components import lock, binary_sensor, text_sensor, sensor, button, time AUTO_LOAD = ["binary_sensor", "text_sensor", "sensor", "button"] @@ -32,6 +32,7 @@ CONF_REQUEST_BATTERY_REPORTS = "request_battery_reports" CONF_RESTART_AFTER_BEACON_LATENCY = "restart_after_beacon_latency" CONF_REQUEST_STATE = "request_state" CONF_UNPAIR = "unpair" +CONF_TIME_SOURCE = "time_source" nuki_lock_ns = cg.esphome_ns.namespace("nuki_lock") NukiLockComponent = nuki_lock_ns.class_("NukiLockComponent", lock.Lock, cg.PollingComponent) @@ -184,6 +185,7 @@ CONFIG_SCHEMA = lock.LOCK_SCHEMA.extend({ unit_of_measurement=c.UNIT_AMPERE, device_class=c.DEVICE_CLASS_CURRENT, accuracy_decimals=3, + # DEFAULT DISABLE. A few others, too. ), # Configuration. @@ -203,8 +205,11 @@ CONFIG_SCHEMA = lock.LOCK_SCHEMA.extend({ entity_category=c.ENTITY_CATEGORY_CONFIG, icon="mdi:close", ), -}).extend(cv.polling_component_schema("30min")) + # Time autoupdate. + cv.Optional(c.CONF_PIN): cv.positive_int, + cv.Optional(CONF_TIME_SOURCE): cv.use_id(time.RealTimeClock), +}).extend(cv.polling_component_schema("30min")) async def to_code(config): var = cg.new_Pvariable(config[c.CONF_ID]) @@ -269,3 +274,9 @@ async def to_code(config): if CONF_RESTART_AFTER_BEACON_LATENCY in config: cg.add(var.set_restart_after_beacon_latency(config[CONF_RESTART_AFTER_BEACON_LATENCY])) + + if c.CONF_PIN in config: + cg.add(var.set_pin(config[c.CONF_PIN])) + + if CONF_TIME_SOURCE in config: + cg.add(var.set_time_source(await cg.get_variable(config[CONF_TIME_SOURCE]))) diff --git a/components/nuki_lock/nuki_lock.cpp b/components/nuki_lock/nuki_lock.cpp index 8949ac0..5a4ee56 100644 --- a/components/nuki_lock/nuki_lock.cpp +++ b/components/nuki_lock/nuki_lock.cpp @@ -231,6 +231,49 @@ bool NukiLockComponent::update_key_turner_state_() { return true; } +bool NukiLockComponent::update_lock_time_() { + if (time_source_ == nullptr) { + ESP_LOGE(TAG, "updating lock time failed: time source not set"); + return false; + } + + Nuki::CmdResult result = nuki_lock_->verifySecurityPin(); + if (result != Nuki::CmdResult::Success) { + ESP_LOGE(TAG, "verifying security pin for lock time update failed: %s", + Adapt::cmd_result_to_string(result).c_str()); + error_text_sensor_->publish_state( + Adapt::error_to_string(nuki_lock_->getLastError())); + return false; + } + + ESPTime now = time_source_->utcnow(); + if (!now.is_valid()) { + ESP_LOGE(TAG, "updating lock time failed: time source is invalid"); + return false; + } + + Nuki::TimeValue tv = { + .year = now.year, + .month = now.month, + .day = now.day_of_month, + .hour = now.hour, + .minute = now.minute, + .second = now.second, + }; + + ESP_LOGI(TAG, "Setting lock time: %.4d-%.2d-%.2d %.2d:%.2d:%.2d", tv.year, + tv.month, tv.day, tv.hour, tv.minute, tv.second); + result = nuki_lock_->updateTime(tv); + if (result != Nuki::CmdResult::Success) { + ESP_LOGE(TAG, "updating lock time failed: %s", + Adapt::cmd_result_to_string(result).c_str()); + error_text_sensor_->publish_state( + Adapt::error_to_string(nuki_lock_->getLastError())); + return false; + } + return true; +} + bool NukiLockComponent::update_battery_report_() { NukiLock::BatteryReport batteryReport; Nuki::CmdResult result = @@ -304,6 +347,10 @@ void NukiLockComponent::setup() { nuki_lock_->initialize(); nuki_lock_->setEventHandler(this); + if (pin_set_) { + nuki_lock_->saveSecurityPincode(pin_); + } + bool paired = nuki_lock_->isPairedWithLock(); paired_binary_sensor_->publish_initial_state(paired); error_text_sensor_->publish_state("No error"); @@ -387,6 +434,10 @@ void NukiLockComponent::ble_loop_() { void NukiLockComponent::update() { bool paired = nuki_lock_->isPairedWithLock(); if (paired) { + if (time_source_ != nullptr) { + // Update time first, then request the state so we get updated time. + update_lock_time_(); + } update_key_turner_state_(); if (request_battery_reports_) { update_battery_report_(); @@ -477,6 +528,14 @@ void NukiLockComponent::dump_config() { ESP_LOGCONFIG(TAG, " Restart after beacon latency: %dms", restart_after_beacon_latency_); + if (time_source_ != nullptr) { + ESP_LOGCONFIG(TAG, " Time source: set"); + } + if (pin_set_) { + ESP_LOGCONFIG( + TAG, " Pin: " ESPHOME_LOG_SECRET_BEGIN "%d" ESPHOME_LOG_SECRET_END, + pin_); + } } } // namespace nuki_lock diff --git a/components/nuki_lock/nuki_lock.h b/components/nuki_lock/nuki_lock.h index 4d70bd2..e6425c1 100644 --- a/components/nuki_lock/nuki_lock.h +++ b/components/nuki_lock/nuki_lock.h @@ -8,7 +8,9 @@ #include "esphome/components/lock/lock.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/text_sensor/text_sensor.h" +#include "esphome/components/time/real_time_clock.h" #include "esphome/core/component.h" +#include "esphome/core/time.h" namespace esphome { namespace nuki_lock { @@ -22,7 +24,8 @@ class NukiLockComponent : public lock::Lock, last_passive_update_millis_(0), restart_after_beacon_latency_(0), update_scheduled_(false), - request_battery_reports_(false){}; + request_battery_reports_(false), + pin_set_(false){}; // Component. void setup() override; @@ -81,6 +84,13 @@ class NukiLockComponent : public lock::Lock, restart_after_beacon_latency_ = value; } + void set_pin(unsigned long value) { + pin_ = value; + pin_set_ = true; + } + + void set_time_source(time::RealTimeClock *value) { time_source_ = value; } + // RequestStateButton. void schedule_update(); @@ -95,11 +105,15 @@ class NukiLockComponent : public lock::Lock, private: bool update_key_turner_state_(); bool update_battery_report_(); + bool update_lock_time_(); void ble_loop_(); void send_action_(NukiLock::LockAction action); NukiLock::NukiLock *nuki_lock_; BleScanner::Scanner scanner_; + time::RealTimeClock *time_source_{nullptr}; + uint16_t pin_; + bool pin_set_; unsigned long last_passive_update_millis_; unsigned long restart_after_beacon_latency_; diff --git a/frontdoorlock.yaml b/frontdoorlock.yaml index 677245a..557dab3 100644 --- a/frontdoorlock.yaml +++ b/frontdoorlock.yaml @@ -22,6 +22,10 @@ button: - platform: factory_reset name: Restart with Factory Default Settings +time: + - platform: sntp + id: sntp_time + lock: - platform: nuki_lock name: None @@ -44,3 +48,5 @@ lock: name: "Last action trigger" last_action_completion_status: name: "Last action completion status" + time_source: sntp_time + pin: !secret lock_pin