Add front door and basement nuki locks

This commit is contained in:
2024-05-18 14:01:09 +02:00
parent 04ea807105
commit a06d6f74c1
6 changed files with 977 additions and 0 deletions
View File
+271
View File
@@ -0,0 +1,271 @@
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
AUTO_LOAD = ["binary_sensor", "text_sensor", "sensor", "button"]
CONF_PAIRED = "paired"
CONF_ERROR = "error"
CONF_BATTERY_CRITICAL = "battery_critical"
CONF_DOOR_CONTACT = "door_contact"
CONF_DOOR_CONTACT_SENSOR = "door_contact_sensor"
CONF_BEACON_RSSI = "beacon_rssi"
CONF_BEACON_LATENCY = "beacon_latency"
CONF_HEARTBEAT_LATENCY = "heartbeat_latency"
CONF_BEACON_BLE_ADDRESS = "beacon_ble_address"
CONF_LOCK_CURRENT_DATETIME = "lock_current_datetime"
CONF_TRIGGER = "trigger"
CONF_CONFIG_UPDATE_COUNT = "config_update_count"
CONF_LAST_ACTION = "last_action"
CONF_LAST_ACTION_TRIGGER = "last_action_trigger"
CONF_LAST_ACTION_COMPLETION_STATUS = "last_action_completion_status"
CONF_NIGHT_MODE_ACTIVE = "night_mode_active"
CONF_BATTERY_RESISTANCE = "battery_resistance"
CONF_BATTERY_LOWEST_VOLTAGE = "battery_lowest_voltage"
CONF_LAST_ACTION_START_VOLTAGE = "last_action_start_voltage"
CONF_LAST_ACTION_LOCK_DISTANCE = "last_action_lock_distance"
CONF_LAST_ACTION_START_TEMPERATURE = "last_action_start_temperature"
CONF_LAST_ACTION_BATTERY_DRAIN = "last_action_battery_drain"
CONF_LAST_ACTION_MAX_TURN_CURRENT = "last_action_max_turn_current"
CONF_REQUEST_BATTERY_REPORTS = "request_battery_reports"
CONF_RESTART_AFTER_BEACON_LATENCY = "restart_after_beacon_latency"
CONF_REQUEST_STATE = "request_state"
CONF_UNPAIR = "unpair"
nuki_lock_ns = cg.esphome_ns.namespace("nuki_lock")
NukiLockComponent = nuki_lock_ns.class_("NukiLockComponent", lock.Lock, cg.PollingComponent)
RequestStateButton = nuki_lock_ns.class_("RequestStateButton", button.Button)
UnpairButton = nuki_lock_ns.class_("UnpairButton", button.Button)
CONFIG_SCHEMA = lock.LOCK_SCHEMA.extend({
cv.GenerateID(): cv.declare_id(NukiLockComponent),
# Required sensors (if unconfigured, defaults are used).
cv.Optional(CONF_PAIRED, default={
"name": "Paired",
}): binary_sensor.binary_sensor_schema(
device_class=c.DEVICE_CLASS_CONNECTIVITY,
),
cv.Optional(CONF_ERROR, default={
"name": "Error",
}): text_sensor.text_sensor_schema(
icon="mdi:alert",
),
cv.Optional(CONF_BATTERY_CRITICAL, default={
"name": "Battery critical",
}): binary_sensor.binary_sensor_schema(
device_class=c.DEVICE_CLASS_BATTERY,
),
cv.Optional(c.CONF_BATTERY_LEVEL, default={
"name": "Battery",
}): sensor.sensor_schema(
device_class=c.DEVICE_CLASS_BATTERY,
unit_of_measurement=c.UNIT_PERCENT,
),
cv.Optional(CONF_DOOR_CONTACT, default={
"name": "Door contact",
}): binary_sensor.binary_sensor_schema(
device_class=c.DEVICE_CLASS_DOOR,
),
cv.Optional(CONF_DOOR_CONTACT_SENSOR, default={
"name": "Door contact sensor",
}): text_sensor.text_sensor_schema(),
cv.Optional(CONF_TRIGGER, default={
"name": "Trigger",
}): text_sensor.text_sensor_schema(),
cv.Optional(CONF_NIGHT_MODE_ACTIVE, default={
"name": "Night mode active",
}): binary_sensor.binary_sensor_schema(
icon="mdi:weather-night",
),
# Optional sensors.
cv.Optional(CONF_BEACON_RSSI): sensor.sensor_schema(
entity_category=c.ENTITY_CATEGORY_DIAGNOSTIC,
state_class=c.STATE_CLASS_MEASUREMENT,
device_class=c.DEVICE_CLASS_SIGNAL_STRENGTH,
unit_of_measurement=c.UNIT_DECIBEL_MILLIWATT,
),
cv.Optional(CONF_BEACON_LATENCY): sensor.sensor_schema(
entity_category=c.ENTITY_CATEGORY_DIAGNOSTIC,
state_class=c.STATE_CLASS_MEASUREMENT,
unit_of_measurement=c.UNIT_MILLISECOND,
),
cv.Optional(CONF_HEARTBEAT_LATENCY): sensor.sensor_schema(
entity_category=c.ENTITY_CATEGORY_DIAGNOSTIC,
state_class=c.STATE_CLASS_MEASUREMENT,
unit_of_measurement=c.UNIT_MILLISECOND,
),
cv.Optional(CONF_BEACON_BLE_ADDRESS): text_sensor.text_sensor_schema(
entity_category=c.ENTITY_CATEGORY_DIAGNOSTIC,
icon="mdi:bluetooth",
),
cv.Optional(CONF_LOCK_CURRENT_DATETIME): text_sensor.text_sensor_schema(
entity_category=c.ENTITY_CATEGORY_DIAGNOSTIC,
device_class=c.DEVICE_CLASS_TIMESTAMP,
),
cv.Optional(CONF_CONFIG_UPDATE_COUNT): sensor.sensor_schema(
entity_category=c.ENTITY_CATEGORY_DIAGNOSTIC,
state_class=c.STATE_CLASS_TOTAL,
),
cv.Optional(CONF_LAST_ACTION): text_sensor.text_sensor_schema(),
cv.Optional(CONF_LAST_ACTION_TRIGGER): text_sensor.text_sensor_schema(),
cv.Optional(CONF_LAST_ACTION_COMPLETION_STATUS): text_sensor.text_sensor_schema(),
# Optional sensors - battery report.
cv.Optional(CONF_REQUEST_BATTERY_REPORTS, default=False): cv.boolean,
cv.Optional(c.CONF_BATTERY_VOLTAGE, default={
"name": "Battery voltage",
}): sensor.sensor_schema(
state_class=c.STATE_CLASS_MEASUREMENT,
entity_category=c.ENTITY_CATEGORY_DIAGNOSTIC,
device_class=c.DEVICE_CLASS_VOLTAGE,
unit_of_measurement=c.UNIT_VOLT,
accuracy_decimals=3,
),
cv.Optional(CONF_BATTERY_RESISTANCE, default={
"name": "Battery resistance",
}): sensor.sensor_schema(
state_class=c.STATE_CLASS_MEASUREMENT,
entity_category=c.ENTITY_CATEGORY_DIAGNOSTIC,
unit_of_measurement=c.UNIT_OHM,
accuracy_decimals=3,
icon="mdi:resistor",
),
cv.Optional(CONF_BATTERY_LOWEST_VOLTAGE, default={
"name": "Battery lowest voltage",
}): sensor.sensor_schema(
state_class=c.STATE_CLASS_MEASUREMENT,
entity_category=c.ENTITY_CATEGORY_DIAGNOSTIC,
device_class=c.DEVICE_CLASS_VOLTAGE,
unit_of_measurement=c.UNIT_VOLT,
accuracy_decimals=3,
),
cv.Optional(CONF_LAST_ACTION_START_VOLTAGE, default={
"name": "Last action start voltage",
}): sensor.sensor_schema(
state_class=c.STATE_CLASS_MEASUREMENT,
entity_category=c.ENTITY_CATEGORY_DIAGNOSTIC,
device_class=c.DEVICE_CLASS_VOLTAGE,
unit_of_measurement=c.UNIT_VOLT,
accuracy_decimals=3,
),
cv.Optional(CONF_LAST_ACTION_LOCK_DISTANCE, default={
"name": "Last action lock distance",
}): sensor.sensor_schema(
state_class=c.STATE_CLASS_MEASUREMENT,
entity_category=c.ENTITY_CATEGORY_DIAGNOSTIC,
unit_of_measurement=c.UNIT_DEGREES,
icon="mdi:rotate-360",
),
cv.Optional(CONF_LAST_ACTION_START_TEMPERATURE, default={
"name": "Last action start temperature",
}): sensor.sensor_schema(
state_class=c.STATE_CLASS_MEASUREMENT,
entity_category=c.ENTITY_CATEGORY_DIAGNOSTIC,
unit_of_measurement=c.UNIT_DEGREES,
device_class=c.DEVICE_CLASS_TEMPERATURE,
),
cv.Optional(CONF_LAST_ACTION_BATTERY_DRAIN, default={
"name": "Last action battery drain",
}): sensor.sensor_schema(
state_class=c.STATE_CLASS_MEASUREMENT,
entity_category=c.ENTITY_CATEGORY_DIAGNOSTIC,
unit_of_measurement=c.UNIT_WATT_HOURS,
device_class=c.DEVICE_CLASS_ENERGY,
accuracy_decimals=3,
),
cv.Optional(CONF_LAST_ACTION_MAX_TURN_CURRENT, default={
"name": "Last action max turn current",
}): sensor.sensor_schema(
state_class=c.STATE_CLASS_MEASUREMENT,
entity_category=c.ENTITY_CATEGORY_DIAGNOSTIC,
unit_of_measurement=c.UNIT_AMPERE,
device_class=c.DEVICE_CLASS_CURRENT,
accuracy_decimals=3,
),
# Configuration.
cv.Optional(CONF_RESTART_AFTER_BEACON_LATENCY, default="10min"): cv.positive_time_period_milliseconds,
# Actions.
cv.Optional(CONF_REQUEST_STATE, default={
"name": "Request state",
}): button.button_schema(RequestStateButton,
entity_category=c.ENTITY_CATEGORY_CONFIG,
icon="mdi:refresh",
),
cv.Optional(CONF_UNPAIR, default={
"name": "Unpair",
}): button.button_schema(
UnpairButton,
entity_category=c.ENTITY_CATEGORY_CONFIG,
icon="mdi:close",
),
}).extend(cv.polling_component_schema("30min"))
async def to_code(config):
var = cg.new_Pvariable(config[c.CONF_ID])
await cg.register_component(var, config)
await lock.register_lock(var, config)
request_state_button = await button.new_button(config[CONF_REQUEST_STATE])
await cg.register_parented(request_state_button, config[c.CONF_ID])
cg.add(var.set_request_state_button(request_state_button))
unpair_button = await button.new_button(config[CONF_UNPAIR])
await cg.register_parented(unpair_button, config[c.CONF_ID])
cg.add(var.set_request_state_button(unpair_button))
cg.add(var.set_paired_binary_sensor(await binary_sensor.new_binary_sensor(config[CONF_PAIRED])))
cg.add(var.set_error_text_sensor(await text_sensor.new_text_sensor(config[CONF_ERROR])))
cg.add(var.set_battery_critical_binary_sensor(await binary_sensor.new_binary_sensor(config[CONF_BATTERY_CRITICAL])))
cg.add(var.set_battery_level_sensor(await sensor.new_sensor(config[c.CONF_BATTERY_LEVEL])))
cg.add(var.set_door_contact_binary_sensor(await binary_sensor.new_binary_sensor(config[CONF_DOOR_CONTACT])))
cg.add(var.set_door_contact_sensor_text_sensor(await text_sensor.new_text_sensor(config[CONF_DOOR_CONTACT_SENSOR])))
cg.add(var.set_trigger_text_sensor(await text_sensor.new_text_sensor(config[CONF_TRIGGER])))
cg.add(var.set_night_mode_active_binary_sensor(await binary_sensor.new_binary_sensor(config[CONF_NIGHT_MODE_ACTIVE])))
if CONF_BEACON_RSSI in config:
cg.add(var.set_beacon_rssi_sensor( await sensor.new_sensor(config[CONF_BEACON_RSSI])))
if CONF_BEACON_LATENCY in config:
cg.add(var.set_beacon_latency_sensor(await sensor.new_sensor(config[CONF_BEACON_LATENCY])))
if CONF_HEARTBEAT_LATENCY in config:
cg.add(var.set_heartbeat_latency_sensor(await sensor.new_sensor(config[CONF_HEARTBEAT_LATENCY])))
if CONF_BEACON_BLE_ADDRESS in config:
cg.add(var.set_beacon_ble_address_text_sensor(await text_sensor.new_text_sensor(config[CONF_BEACON_BLE_ADDRESS])))
if CONF_LOCK_CURRENT_DATETIME in config:
sens = await text_sensor.new_text_sensor(config[CONF_LOCK_CURRENT_DATETIME])
cg.add(var.set_lock_current_datetime_text_sensor(sens))
if CONF_CONFIG_UPDATE_COUNT in config:
cg.add(var.set_config_update_count_sensor(await sensor.new_sensor(config[CONF_CONFIG_UPDATE_COUNT])))
if CONF_LAST_ACTION in config:
cg.add(var.set_last_action_text_sensor(await text_sensor.new_text_sensor(config[CONF_LAST_ACTION])))
if CONF_LAST_ACTION_TRIGGER in config:
cg.add(var.set_last_action_trigger_text_sensor(await text_sensor.new_text_sensor(config[CONF_LAST_ACTION_TRIGGER])))
if CONF_LAST_ACTION_COMPLETION_STATUS in config:
cg.add(var.set_last_action_completion_status_text_sensor(await text_sensor.new_text_sensor(config[CONF_LAST_ACTION_COMPLETION_STATUS])))
if config.get(CONF_REQUEST_BATTERY_REPORTS):
cg.add(var.set_request_battery_reports(True))
cg.add(var.set_battery_voltage_sensor(await sensor.new_sensor(config[c.CONF_BATTERY_VOLTAGE])))
cg.add(var.set_battery_resistance_sensor(await sensor.new_sensor(config[CONF_BATTERY_RESISTANCE])))
cg.add(var.set_battery_lowest_voltage_sensor(await sensor.new_sensor(config[CONF_BATTERY_LOWEST_VOLTAGE])))
cg.add(var.set_last_action_start_voltage_sensor(await sensor.new_sensor(config[CONF_LAST_ACTION_START_VOLTAGE])))
cg.add(var.set_last_action_lock_distance_sensor(await sensor.new_sensor(config[CONF_LAST_ACTION_LOCK_DISTANCE])))
cg.add(var.set_last_action_start_temperature_sensor(await sensor.new_sensor(config[CONF_LAST_ACTION_START_TEMPERATURE])))
cg.add(var.set_last_action_battery_drain_sensor(await sensor.new_sensor(config[CONF_LAST_ACTION_BATTERY_DRAIN])))
cg.add(var.set_last_action_max_turn_current_sensor(await sensor.new_sensor(config[CONF_LAST_ACTION_MAX_TURN_CURRENT])))
if CONF_RESTART_AFTER_BEACON_LATENCY in config:
cg.add(var.set_restart_after_beacon_latency(config[CONF_RESTART_AFTER_BEACON_LATENCY]))
+483
View File
@@ -0,0 +1,483 @@
#include "nuki_lock.h"
#include "esphome/core/application.h"
#include "esphome/core/log.h"
namespace {
using namespace esphome;
const char *TAG = "nuki_lock";
class Adapt {
public:
static esphome::lock::LockState lock_state_to_esphome(
NukiLock::LockState state) {
switch (state) {
case NukiLock::LockState::Locked:
return esphome::lock::LOCK_STATE_LOCKED;
case NukiLock::LockState::Unlocked:
return esphome::lock::LOCK_STATE_UNLOCKED;
case NukiLock::LockState::MotorBlocked:
return esphome::lock::LOCK_STATE_JAMMED;
case NukiLock::LockState::Locking:
return esphome::lock::LOCK_STATE_LOCKING;
case NukiLock::LockState::Unlocking:
return esphome::lock::LOCK_STATE_UNLOCKING;
default:
return esphome::lock::LOCK_STATE_NONE;
}
}
static std::string lock_action_to_string(NukiLock::LockAction action) {
char result[256];
NukiLock::lockactionToString(action, result);
return result;
}
static bool door_sensor_state_to_is_door_open(Nuki::DoorSensorState state) {
switch (state) {
case Nuki::DoorSensorState::DoorClosed:
return false;
default:
return true;
}
}
static std::string door_sensor_state_to_string(Nuki::DoorSensorState state) {
char result[256];
NukiLock::doorSensorStateToString(state, result);
return result;
}
static std::string key_turner_datetime_iso(
const NukiLock::KeyTurnerState &state) {
uint8_t tzOffsetHours = abs(state.timeZoneOffset / 60);
uint8_t tzOffsetMinutes = abs(state.timeZoneOffset % 60);
char ts[256];
sprintf(ts, "%d-%.2d-%.2dT%.2d:%.2d:%.2d%c%.2d:%.2d", state.currentTimeYear,
state.currentTimeMonth, state.currentTimeDay, state.currentTimeHour,
state.currentTimeMinute, state.currentTimeSecond,
state.timeZoneOffset >= 0 ? '+' : '-', tzOffsetHours,
tzOffsetMinutes);
return ts;
}
static std::string trigger_to_string(NukiLock::Trigger trigger) {
char result[256];
NukiLock::triggerToString(trigger, result);
return result;
}
static std::string completion_status_to_string(
NukiLock::CompletionStatus status) {
char result[256];
NukiLock::completionStatusToString(status, result);
return result;
}
static std::string cmd_result_to_string(Nuki::CmdResult result) {
char resultStr[256];
NukiLock::cmdResultToString(result, resultStr);
return resultStr;
}
static std::string error_to_string(NukiLock::ErrorCode error) {
switch (error) {
case NukiLock::ErrorCode::ERROR_BAD_CRC:
return "ERROR_BAD_CRC";
case NukiLock::ErrorCode::ERROR_BAD_LENGTH:
return "ERROR_BAD_LENGTH";
case NukiLock::ErrorCode::ERROR_UNKNOWN:
return "ERROR_UNKNOWN";
case NukiLock::ErrorCode::P_ERROR_NOT_PAIRING:
return "P_ERROR_NOT_PAIRING";
case NukiLock::ErrorCode::P_ERROR_BAD_AUTHENTICATOR:
return "P_ERROR_BAD_AUTHENTICATOR";
case NukiLock::ErrorCode::P_ERROR_BAD_PARAMETER:
return "P_ERROR_BAD_PARAMETER";
case NukiLock::ErrorCode::P_ERROR_MAX_USER:
return "P_ERROR_MAX_USER";
case NukiLock::ErrorCode::K_ERROR_NOT_AUTHORIZED:
return "K_ERROR_NOT_AUTHORIZED";
case NukiLock::ErrorCode::K_ERROR_BAD_PIN:
return "K_ERROR_BAD_PIN";
case NukiLock::ErrorCode::K_ERROR_BAD_NONCE:
return "K_ERROR_BAD_NONCE";
case NukiLock::ErrorCode::K_ERROR_BAD_PARAMETER:
return "K_ERROR_BAD_PARAMETER";
case NukiLock::ErrorCode::K_ERROR_INVALID_AUTH_ID:
return "K_ERROR_INVALID_AUTH_ID";
case NukiLock::ErrorCode::K_ERROR_DISABLED:
return "K_ERROR_DISABLED";
case NukiLock::ErrorCode::K_ERROR_REMOTE_NOT_ALLOWED:
return "K_ERROR_REMOTE_NOT_ALLOWED";
case NukiLock::ErrorCode::K_ERROR_TIME_NOT_ALLOWED:
return "K_ERROR_TIME_NOT_ALLOWED";
case NukiLock::ErrorCode::K_ERROR_TOO_MANY_PIN_ATTEMPTS:
return "K_ERROR_TOO_MANY_PIN_ATTEMPTS";
case NukiLock::ErrorCode::K_ERROR_TOO_MANY_ENTRIES:
return "K_ERROR_TOO_MANY_ENTRIES";
case NukiLock::ErrorCode::K_ERROR_CODE_ALREADY_EXISTS:
return "K_ERROR_CODE_ALREADY_EXISTS";
case NukiLock::ErrorCode::K_ERROR_CODE_INVALID:
return "K_ERROR_CODE_INVALID";
case NukiLock::ErrorCode::K_ERROR_CODE_INVALID_TIMEOUT_1:
return "K_ERROR_CODE_INVALID_TIMEOUT_1";
case NukiLock::ErrorCode::K_ERROR_CODE_INVALID_TIMEOUT_2:
return "K_ERROR_CODE_INVALID_TIMEOUT_2";
case NukiLock::ErrorCode::K_ERROR_CODE_INVALID_TIMEOUT_3:
return "K_ERROR_CODE_INVALID_TIMEOUT_3";
case NukiLock::ErrorCode::K_ERROR_AUTO_UNLOCK_TOO_RECENT:
return "K_ERROR_AUTO_UNLOCK_TOO_RECENT";
case NukiLock::ErrorCode::K_ERROR_POSITION_UNKNOWN:
return "K_ERROR_POSITION_UNKNOWN";
case NukiLock::ErrorCode::K_ERROR_MOTOR_BLOCKED:
return "K_ERROR_MOTOR_BLOCKED";
case NukiLock::ErrorCode::K_ERROR_CLUTCH_FAILURE:
return "K_ERROR_CLUTCH_FAILURE";
case NukiLock::ErrorCode::K_ERROR_MOTOR_TIMEOUT:
return "K_ERROR_MOTOR_TIMEOUT";
case NukiLock::ErrorCode::K_ERROR_BUSY:
return "K_ERROR_BUSY";
case NukiLock::ErrorCode::K_ERROR_CANCELED:
return "K_ERROR_CANCELED";
case NukiLock::ErrorCode::K_ERROR_NOT_CALIBRATED:
return "K_ERROR_NOT_CALIBRATED";
case NukiLock::ErrorCode::K_ERROR_MOTOR_POSITION_LIMIT:
return "K_ERROR_MOTOR_POSITION_LIMIT";
case NukiLock::ErrorCode::K_ERROR_MOTOR_LOW_VOLTAGE:
return "K_ERROR_MOTOR_LOW_VOLTAGE";
case NukiLock::ErrorCode::K_ERROR_MOTOR_POWER_FAILURE:
return "K_ERROR_MOTOR_POWER_FAILURE";
case NukiLock::ErrorCode::K_ERROR_CLUTCH_POWER_FAILURE:
return "K_ERROR_CLUTCH_POWER_FAILURE";
case NukiLock::ErrorCode::K_ERROR_VOLTAGE_TOO_LOW:
return "K_ERROR_VOLTAGE_TOO_LOW";
case NukiLock::ErrorCode::K_ERROR_FIRMWARE_UPDATE_NEEDED:
return "K_ERROR_FIRMWARE_UPDATE_NEEDED";
default:
return std::string("Unknown #") +
std::to_string(static_cast<uint8_t>(error));
}
}
};
} // namespace
namespace esphome {
namespace nuki_lock {
bool NukiLockComponent::update_key_turner_state_() {
NukiLock::KeyTurnerState retrievedKeyTurnerState;
Nuki::CmdResult result =
nuki_lock_->requestKeyTurnerState(&retrievedKeyTurnerState);
if (result != Nuki::CmdResult::Success) {
ESP_LOGE(TAG, "request for key turner state failed: %s",
Adapt::cmd_result_to_string(result).c_str());
publish_state(lock::LOCK_STATE_NONE);
error_text_sensor_->publish_state(
Adapt::error_to_string(nuki_lock_->getLastError()));
// TODO(https://github.com/esphome/feature-requests/issues/1568): publish
// unavailable for all related sensors.
schedule_update();
return false;
}
publish_state(
Adapt::lock_state_to_esphome(retrievedKeyTurnerState.lockState));
door_contact_binary_sensor_->publish_state(
Adapt::door_sensor_state_to_is_door_open(
retrievedKeyTurnerState.doorSensorState));
door_contact_sensor_text_sensor_->publish_state(
Adapt::door_sensor_state_to_string(
retrievedKeyTurnerState.doorSensorState));
battery_level_sensor_->publish_state(nuki_lock_->getBatteryPerc());
battery_critical_binary_sensor_->publish_state(
nuki_lock_->isBatteryCritical());
trigger_text_sensor_->publish_state(
Adapt::trigger_to_string(retrievedKeyTurnerState.trigger));
night_mode_active_binary_sensor_->publish_state(
retrievedKeyTurnerState.nightModeActive > 0);
if (lock_current_datetime_text_sensor_ != nullptr) {
lock_current_datetime_text_sensor_->publish_state(
Adapt::key_turner_datetime_iso(retrievedKeyTurnerState));
}
if (config_update_count_sensor_ != nullptr) {
config_update_count_sensor_->publish_state(
retrievedKeyTurnerState.configUpdateCount);
}
if (last_action_text_sensor_ != nullptr) {
last_action_text_sensor_->publish_state(
Adapt::lock_action_to_string(retrievedKeyTurnerState.lastLockAction));
}
if (last_action_trigger_text_sensor_ != nullptr) {
last_action_trigger_text_sensor_->publish_state(Adapt::trigger_to_string(
retrievedKeyTurnerState.lastLockActionTrigger));
}
if (last_action_completion_status_text_sensor_ != nullptr) {
last_action_completion_status_text_sensor_->publish_state(
Adapt::completion_status_to_string(
retrievedKeyTurnerState.lastLockActionCompletionStatus));
}
return true;
}
bool NukiLockComponent::update_battery_report_() {
NukiLock::BatteryReport batteryReport;
Nuki::CmdResult result =
this->nuki_lock_->requestBatteryReport(&batteryReport);
if (result != Nuki::CmdResult::Success) {
ESP_LOGE(TAG, "request for battery report failed: %s",
Adapt::cmd_result_to_string(result).c_str());
// TODO(https://github.com/esphome/feature-requests/issues/1568): publish
// unavailable for all related sensors.
error_text_sensor_->publish_state(
Adapt::error_to_string(nuki_lock_->getLastError()));
return false;
}
battery_voltage_sensor_->publish_state(batteryReport.batteryVoltage /
1000.0f);
battery_resistance_sensor_->publish_state(batteryReport.batteryResistance /
1000.0f);
battery_lowest_voltage_sensor_->publish_state(batteryReport.lowestVoltage /
1000.0f);
last_action_start_voltage_sensor_->publish_state(batteryReport.startVoltage /
1000.0f);
last_action_lock_distance_sensor_->publish_state(batteryReport.lockDistance);
last_action_start_temperature_sensor_->publish_state(
batteryReport.startTemperature);
last_action_battery_drain_sensor_->publish_state(batteryReport.batteryDrain /
1000.0f);
last_action_max_turn_current_sensor_->publish_state(
batteryReport.maxTurnCurrent / 1000.0f);
return true;
}
void NukiLockComponent::notify(Nuki::EventType event) {
ESP_LOGI(TAG, "received Nuki event - updating state");
// Have to execute later as this runs in BLE scanner context and we can't do
// any BLE in this context.
schedule_update();
}
void NukiLockComponent::schedule_update() {
if (!update_scheduled_) {
update_scheduled_ = true;
defer([this]() {
update();
update_scheduled_ = false;
});
}
}
void NukiLockComponent::setup() {
traits.set_supports_open(true);
traits.set_supported_states(std::set<lock::LockState>{
lock::LOCK_STATE_NONE, lock::LOCK_STATE_LOCKED, lock::LOCK_STATE_UNLOCKED,
lock::LOCK_STATE_JAMMED, lock::LOCK_STATE_LOCKING,
lock::LOCK_STATE_UNLOCKING});
publish_state(lock::LOCK_STATE_NONE);
uint8_t mac[6];
get_mac_address_raw(mac);
our_device_id_ = mac[0] + (mac[1] << 8) + (mac[2] << 16) + (mac[3] << 24);
// name will be used as part of the Preferences key to save credentials.
// There's a limit of 15 characters on key length, keep it predictable.
our_device_name_ = "nuki" + std::to_string(get_object_id_hash());
nuki_lock_ = new NukiLock::NukiLock(our_device_name_, our_device_id_);
scanner_.initialize();
nuki_lock_->registerBleScanner(&scanner_);
nuki_lock_->initialize();
nuki_lock_->setEventHandler(this);
bool paired = nuki_lock_->isPairedWithLock();
paired_binary_sensor_->publish_initial_state(paired);
error_text_sensor_->publish_state("No error");
set_interval("ble_loop", 320 /* ms*/, [this]() { ble_loop_(); });
schedule_update();
}
void NukiLockComponent::unpair() {
ESP_LOGI(TAG, "unpairing Nuki lock");
nuki_lock_->unPairNuki();
// Force BLE update on the next loop.
last_passive_update_millis_ = 0;
// And a regular fetch.
schedule_update();
}
void NukiLockComponent::ble_loop_() {
scanner_.update();
unsigned long ts = millis();
bool should_publish_update = last_passive_update_millis_ == 0 ||
last_passive_update_millis_ + 60000 < ts;
bool paired = nuki_lock_->isPairedWithLock();
if (should_publish_update) {
last_passive_update_millis_ = ts;
paired_binary_sensor_->publish_state(paired);
}
if (!paired) {
if (nuki_lock_->pairNuki() == Nuki::PairingResult::Success) {
ESP_LOGI(TAG, "Nuki lock successfully paired!");
// Force BLE update on the next loop.
last_passive_update_millis_ = 0;
// And a regular fetch.
schedule_update();
} else if (should_publish_update) {
ESP_LOGW(TAG,
"Nuki lock is not yet paired. Make sure Bluetooth pairing "
"is enabled in lock settings, then press and hold the button "
"on the lock for a few seconds until LED glows constantly");
}
return;
}
nuki_lock_->updateConnectionState();
// Update timestamp as we might have gotten a refresh packet on BLE.
ts = millis();
unsigned long beaconLatency = ts - nuki_lock_->getLastReceivedBeaconTs(),
heartbeatLatency = ts - nuki_lock_->getLastHeartbeat();
if (should_publish_update) {
if (beacon_rssi_sensor_ != nullptr) {
int rssi = nuki_lock_->getRssi();
if (rssi != 0) {
beacon_rssi_sensor_->publish_state(rssi);
}
}
if (beacon_latency_sensor_ != nullptr) {
beacon_latency_sensor_->publish_state(beaconLatency);
}
if (heartbeat_latency_sensor_ != nullptr) {
heartbeat_latency_sensor_->publish_state(heartbeatLatency);
}
if (beacon_ble_address_text_sensor_ != nullptr) {
beacon_ble_address_text_sensor_->publish_state(
nuki_lock_->getBleAddress().toString());
}
}
if (restart_after_beacon_latency_ != 0 &&
beaconLatency > restart_after_beacon_latency_) {
ESP_LOGE(TAG,
"Beacon latency %d higher than configured maximum %d. Restarting "
"the device");
App.safe_reboot();
}
}
void NukiLockComponent::update() {
bool paired = nuki_lock_->isPairedWithLock();
if (paired) {
update_key_turner_state_();
if (request_battery_reports_) {
update_battery_report_();
}
}
}
void NukiLockComponent::send_action_(NukiLock::LockAction action) {
if (!this->nuki_lock_->isPairedWithLock()) {
ESP_LOGE(TAG, "lock or unlock action %d called before a lock is paired",
action);
return;
}
Nuki::CmdResult result = this->nuki_lock_->lockAction(action);
if (result != Nuki::CmdResult::Success) {
ESP_LOGE(TAG, "setting state %d failed: %s", action,
Adapt::cmd_result_to_string(result).c_str());
publish_state(lock::LOCK_STATE_NONE);
error_text_sensor_->publish_state(
Adapt::error_to_string(nuki_lock_->getLastError()));
schedule_update();
}
}
void NukiLockComponent::control(const lock::LockCall &call) {
auto action = *call.get_state();
switch (action) {
case lock::LOCK_STATE_LOCKED:
publish_state(lock::LOCK_STATE_LOCKING);
send_action_(NukiLock::LockAction::Lock);
break;
case lock::LOCK_STATE_UNLOCKED:
publish_state(lock::LOCK_STATE_UNLOCKING);
send_action_(NukiLock::LockAction::Unlock);
break;
default:
ESP_LOGE(TAG, "unsupported lock state requested: %d", action);
return;
}
// Final lock state will be published once Nuki informs us of an update.
}
void NukiLockComponent::open_latch() {
publish_state(lock::LOCK_STATE_UNLOCKING);
send_action_(NukiLock::LockAction::Unlatch);
}
void NukiLockComponent::dump_config() {
char device_id[256];
sprintf(device_id, "%s (0x%.8x)", our_device_name_.c_str(), our_device_id_);
LOG_LOCK("", device_id, this);
// Required.
LOG_BINARY_SENSOR(" ", "", paired_binary_sensor_);
LOG_TEXT_SENSOR(" ", "", error_text_sensor_);
LOG_BINARY_SENSOR(" ", "", battery_critical_binary_sensor_);
LOG_SENSOR(" ", "", battery_level_sensor_);
LOG_BINARY_SENSOR(" ", "", door_contact_binary_sensor_);
LOG_TEXT_SENSOR(" ", "", door_contact_sensor_text_sensor_);
LOG_TEXT_SENSOR(" ", "", trigger_text_sensor_);
LOG_BINARY_SENSOR(" ", "", night_mode_active_binary_sensor_);
// Optional.
LOG_SENSOR(" ", "", beacon_rssi_sensor_);
LOG_SENSOR(" ", "", beacon_latency_sensor_);
LOG_SENSOR(" ", "", heartbeat_latency_sensor_);
LOG_TEXT_SENSOR(" ", "", beacon_ble_address_text_sensor_);
LOG_TEXT_SENSOR(" ", "", lock_current_datetime_text_sensor_);
LOG_SENSOR(" ", "", config_update_count_sensor_);
LOG_TEXT_SENSOR(" ", "", last_action_text_sensor_);
LOG_TEXT_SENSOR(" ", "", last_action_trigger_text_sensor_);
LOG_TEXT_SENSOR(" ", "", last_action_completion_status_text_sensor_);
if (request_battery_reports_) {
// Optional sensors - battery report.
LOG_SENSOR(" ", "", battery_voltage_sensor_);
LOG_SENSOR(" ", "", battery_resistance_sensor_);
LOG_SENSOR(" ", "", battery_lowest_voltage_sensor_);
LOG_SENSOR(" ", "", last_action_start_voltage_sensor_);
LOG_SENSOR(" ", "", last_action_lock_distance_sensor_);
LOG_SENSOR(" ", "", last_action_start_temperature_sensor_);
LOG_SENSOR(" ", "", last_action_battery_drain_sensor_);
LOG_SENSOR(" ", "", last_action_max_turn_current_sensor_);
}
ESP_LOGCONFIG(TAG, " Restart after beacon latency: %dms",
restart_after_beacon_latency_);
}
} // namespace nuki_lock
} // namespace esphome
+131
View File
@@ -0,0 +1,131 @@
#pragma once
#include "BleScanner.h"
#include "NukiConstants.h"
#include "NukiLock.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
#include "esphome/components/button/button.h"
#include "esphome/components/lock/lock.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/text_sensor/text_sensor.h"
#include "esphome/core/component.h"
namespace esphome {
namespace nuki_lock {
class NukiLockComponent : public lock::Lock,
public PollingComponent,
public Nuki::SmartlockEventHandler {
public:
explicit NukiLockComponent()
: Lock(),
last_passive_update_millis_(0),
restart_after_beacon_latency_(0),
update_scheduled_(false),
request_battery_reports_(false){};
// Component.
void setup() override;
float get_setup_priority() const override {
return setup_priority::HARDWARE - 1.0f;
}
void dump_config() override;
// PollingComponent.
void update() override;
// Nuki::SmartlockEventHandler.
void notify(Nuki::EventType event);
// Passive updates (predefined update frequency).
SUB_BINARY_SENSOR(paired)
SUB_SENSOR(beacon_rssi)
SUB_SENSOR(beacon_latency)
SUB_SENSOR(heartbeat_latency)
SUB_TEXT_SENSOR(beacon_ble_address)
// Active updates (configured update_interval frequency).
// Key turner state.
SUB_BINARY_SENSOR(battery_critical)
SUB_SENSOR(battery_level)
SUB_BINARY_SENSOR(door_contact)
SUB_TEXT_SENSOR(door_contact_sensor)
SUB_TEXT_SENSOR(lock_current_datetime)
SUB_TEXT_SENSOR(error)
SUB_TEXT_SENSOR(trigger)
SUB_SENSOR(config_update_count)
SUB_TEXT_SENSOR(last_action)
SUB_TEXT_SENSOR(last_action_trigger)
SUB_TEXT_SENSOR(last_action_completion_status)
SUB_BINARY_SENSOR(night_mode_active)
// Battery report.
SUB_SENSOR(battery_voltage)
SUB_SENSOR(battery_resistance)
SUB_SENSOR(battery_lowest_voltage)
SUB_SENSOR(last_action_start_voltage)
SUB_SENSOR(last_action_lock_distance)
SUB_SENSOR(last_action_start_temperature)
SUB_SENSOR(last_action_battery_drain)
SUB_SENSOR(last_action_max_turn_current)
// Actions.
SUB_BUTTON(lock_n_go)
SUB_BUTTON(unpair)
SUB_BUTTON(request_state)
void set_request_battery_reports(bool value) {
request_battery_reports_ = value;
}
void set_restart_after_beacon_latency(unsigned long value) {
restart_after_beacon_latency_ = value;
}
// RequestStateButton.
void schedule_update();
// UnpairButton.
void unpair();
protected:
// lock::Lock.
void control(const lock::LockCall &call) override;
void open_latch() override;
private:
bool update_key_turner_state_();
bool update_battery_report_();
void ble_loop_();
void send_action_(NukiLock::LockAction action);
NukiLock::NukiLock *nuki_lock_;
BleScanner::Scanner scanner_;
unsigned long last_passive_update_millis_;
unsigned long restart_after_beacon_latency_;
bool update_scheduled_;
bool request_battery_reports_;
uint32_t our_device_id_;
std::string our_device_name_;
};
class RequestStateButton : public button::Button,
public Parented<NukiLockComponent> {
public:
RequestStateButton() = default;
protected:
void press_action() override { get_parent()->schedule_update(); }
};
class UnpairButton : public button::Button, public Parented<NukiLockComponent> {
public:
UnpairButton() = default;
protected:
void press_action() override { get_parent()->unpair(); }
};
} // namespace nuki_lock
} // namespace esphome