Add front door and basement nuki locks
This commit is contained in:
@@ -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]))
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user