Skip to content

Commit

Permalink
Merge pull request #1 from AzonInc/dev
Browse files Browse the repository at this point in the history
Merge Dev
  • Loading branch information
AzonInc authored Mar 26, 2024
2 parents 730f356 + 262d851 commit 73f1662
Show file tree
Hide file tree
Showing 5 changed files with 968 additions and 300 deletions.
45 changes: 35 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
# Nuki Lock for ESPHome (ESP32)
[![Build Component](https://github.com/uriyacovy/ESPHome_nuki_lock/actions/workflows/build.yaml/badge.svg)](https://github.com/uriyacovy/ESPHome_nuki_lock/actions/workflows/build.yaml)
[![Build Component](https://github.com/AzonInc/ESPHome_nuki_lock/actions/workflows/build.yaml/badge.svg)](https://github.com/AzonInc/ESPHome_nuki_lock/actions/workflows/build.yaml)

This module builds an ESPHome lock platform for Nuki Smartlock (nuki_lock) that creates 6 new entities in Home Assistant:
This module builds an ESPHome lock platform for Nuki Smartlock (nuki_lock) that creates 9 new entities in Home Assistant:
- Lock
- Binary Sensor: Is Paired
- Binary Sensor: Is Connected
- Binary Sensor: Critical Battery
- Sensor: Battery Level
- Binary Sensor: Door Sensor
- Text Sensor: Door Sensor State
- Switch: Pairing Mode
- Button: Unpair

The lock entity is updated whenever the look changes state (via Nuki App, HA, or manually) using Nuki BT advertisement mechanism.

![screenshot](https://user-images.githubusercontent.com/1754967/183266065-d1a6e9fe-d7f7-4295-9c0d-4bf9235bf4cd.png)
![dashboard](./docs/nuki_dashboard.png)

## How to use
Add the following to the ESPHome yaml file:
Expand All @@ -22,10 +24,10 @@ esphome:
libraries:
- Preferences
- https://github.com/vinmenn/Crc16.git
- https://github.com/uriyacovy/NukiBleEsp32
- https://github.com/AzonInc/NukiBleEsp32

external_components:
- source: github://uriyacovy/ESPHome_nuki_lock
- source: github://AzonInc/ESPHome_nuki_lock

esp32:
board: "esp32dev" # Or whatever other board you're using
Expand All @@ -41,7 +43,8 @@ lock:
is_connected:
name: "Nuki Connected"
is_paired:
name: "Nuki Paired"
name: "Nuki Paired"

# Optional:
battery_critical:
name: "Nuki Battery Critical"
Expand All @@ -51,6 +54,18 @@ lock:
name: "Nuki Door Sensor"
door_sensor_state:
name: "Nuki Door Sensor State"
unpair:
name: "Nuki Unpair"
pairing_mode:
name: "Nuki Pairing Mode"

# Optional: Callbacks
on_pairing_mode_on_action:
- lambda: ESP_LOGI("nuki_lock", "Pairing mode turned on");
on_pairing_mode_off_action:
- lambda: ESP_LOGI("nuki_lock", "Pairing mode turned off");
on_paired_action:
- lambda: ESP_LOGI("nuki_lock", "Paired sucessfuly");
```
After running ESPHome (esphome run <yamlfile.yaml>), the module will actively try to pair to Nuki.
Expand All @@ -74,14 +89,24 @@ service: esphome.<NODE_NAME>_lock_n_go
data: {}
```
## Unparing Nuki
To unpair Nuki, add the following to ESPHome yaml file below `platform: nuki_lock` section and run ESPHome again:
## Supported Automation Actions ##
### Pairing Mode ###
You can use automations to turn on/off the pairing mode:
```yaml
on_...:
- nuki_lock.set_pairing_mode:
```
### Unpair
You can useautomations to unpair your Nuki:
```yaml
unpair: true
on_...:
- nuki_lock.unpair:
```
## Dependencies
The module depends on the work done by [I-Connect](https://github.com/I-Connect), https://github.com/I-Connect/NukiBleEsp32
The module is a Fork of [ESPHome_nuki_lock](https://github.com/uriyacovy/ESPHome_nuki_lock) and depends on the work done by [I-Connect](https://github.com/I-Connect) with [NukiBleEsp32](https://github.com/I-Connect/NukiBleEsp32).
## Tested Hardware
- ESP32 wroom
Expand Down
147 changes: 128 additions & 19 deletions components/nuki_lock/lock.py
Original file line number Diff line number Diff line change
@@ -1,41 +1,102 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import lock, binary_sensor, text_sensor, sensor, switch
from esphome.const import CONF_ID, CONF_BATTERY_LEVEL, DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_BATTERY, DEVICE_CLASS_DOOR, UNIT_PERCENT, ENTITY_CATEGORY_CONFIG
from esphome import automation
from esphome.components import lock, binary_sensor, text_sensor, sensor, switch, button
from esphome.const import (
CONF_ID,
CONF_BATTERY_LEVEL,
DEVICE_CLASS_CONNECTIVITY,
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_DOOR,
UNIT_PERCENT,
ENTITY_CATEGORY_CONFIG,
ENTITY_CATEGORY_DIAGNOSTIC,
CONF_TRIGGER_ID
)

AUTO_LOAD = ["binary_sensor", "text_sensor", "sensor", "switch"]
AUTO_LOAD = ["binary_sensor", "text_sensor", "sensor", "switch", "button"]

CONF_IS_CONNECTED = "is_connected"
CONF_IS_PAIRED = "is_paired"
CONF_UNPAIR = "unpair"
CONF_UNPAIR_BUTTON = "unpair"
CONF_PAIRING_MODE_SWITCH = "pairing_mode"
CONF_BATTERY_CRITICAL = "battery_critical"
CONF_BATTERY_LEVEL = "battery_level"
CONF_DOOR_SENSOR = "door_sensor"
CONF_DOOR_SENSOR_STATE = "door_sensor_state"

CONF_SET_PAIRING_MODE = "pairing_mode"
CONF_PAIRING_TIMEOUT = "pairing_timeout"

CONF_ON_PAIRING_MODE_ON = "on_pairing_mode_on_action"
CONF_ON_PAIRING_MODE_OFF = "on_pairing_mode_off_action"
CONF_ON_PAIRED = "on_paired_action"

nuki_lock_ns = cg.esphome_ns.namespace('nuki_lock')
NukiLock = nuki_lock_ns.class_('NukiLockComponent', lock.Lock, switch.Switch, cg.Component)

NukiLockUnpairButton = nuki_lock_ns.class_("NukiLockUnpairButton", button.Button, cg.Component)
NukiLockPairingModeSwitch = nuki_lock_ns.class_("NukiLockPairingModeSwitch", switch.Switch, cg.Component)

NukiLockUnpairAction = nuki_lock_ns.class_(
"NukiLockUnpairAction", automation.Action
)

NukiLockPairingModeAction = nuki_lock_ns.class_(
"NukiLockPairingModeAction", automation.Action
)

PairingModeOnTrigger = nuki_lock_ns.class_("PairingModeOnTrigger", automation.Trigger.template())
PairingModeOffTrigger = nuki_lock_ns.class_("PairingModeOffTrigger", automation.Trigger.template())
PairedTrigger = nuki_lock_ns.class_("PairedTrigger", automation.Trigger.template())

CONFIG_SCHEMA = lock.LOCK_SCHEMA.extend({
cv.GenerateID(): cv.declare_id(NukiLock),
cv.Required(CONF_IS_CONNECTED): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_CONNECTIVITY,
),
device_class=DEVICE_CLASS_CONNECTIVITY,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
cv.Required(CONF_IS_PAIRED): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_CONNECTIVITY,
),
device_class=DEVICE_CLASS_CONNECTIVITY,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
cv.Optional(CONF_BATTERY_CRITICAL): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_BATTERY,
),
device_class=DEVICE_CLASS_BATTERY,
),
cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(
device_class=DEVICE_CLASS_BATTERY,
unit_of_measurement=UNIT_PERCENT,
),
device_class=DEVICE_CLASS_BATTERY,
unit_of_measurement=UNIT_PERCENT,
),
cv.Optional(CONF_DOOR_SENSOR): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_DOOR,
),
device_class=DEVICE_CLASS_DOOR,
),
cv.Optional(CONF_DOOR_SENSOR_STATE): text_sensor.text_sensor_schema(),
cv.Optional(CONF_UNPAIR, default=False): cv.boolean,
cv.Optional(CONF_UNPAIR_BUTTON): button.button_schema(
NukiLockUnpairButton,
entity_category=ENTITY_CATEGORY_CONFIG,
icon="mdi:link-off",
),
cv.Optional(CONF_PAIRING_MODE_SWITCH): switch.switch_schema(
NukiLockPairingModeSwitch,
entity_category=ENTITY_CATEGORY_CONFIG,
icon="mdi:bluetooth-connect",
),
cv.Optional(CONF_PAIRING_TIMEOUT, default="300s"): cv.positive_time_period_seconds,
cv.Optional(CONF_ON_PAIRING_MODE_ON): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PairingModeOnTrigger),
}
),
cv.Optional(CONF_ON_PAIRING_MODE_OFF): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PairingModeOffTrigger),
}
),
cv.Optional(CONF_ON_PAIRED): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PairedTrigger),
}
),
}).extend(cv.polling_component_schema("500ms"))


Expand Down Expand Up @@ -68,6 +129,54 @@ async def to_code(config):
sens = await text_sensor.new_text_sensor(config[CONF_DOOR_SENSOR_STATE])
cg.add(var.set_door_sensor_state(sens))

if CONF_UNPAIR in config:
cg.add(var.set_unpair(config[CONF_UNPAIR]))

if CONF_UNPAIR_BUTTON in config:
btn = await button.new_button(config[CONF_UNPAIR_BUTTON])
await cg.register_parented(btn, config[CONF_ID])
cg.add(var.set_unpair_button(btn))

if CONF_PAIRING_MODE_SWITCH in config:
sw = await switch.new_switch(config[CONF_PAIRING_MODE_SWITCH])
await cg.register_parented(sw, config[CONF_ID])
cg.add(var.set_pairing_mode_switch(sw))

cg.add(var.set_pairing_timeout(config[CONF_PAIRING_TIMEOUT]))

for conf in config.get(CONF_ON_PAIRING_MODE_ON, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)

for conf in config.get(CONF_ON_PAIRING_MODE_OFF, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)

for conf in config.get(CONF_ON_PAIRED, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)

@automation.register_action(
"nuki_lock.unpair",
NukiLockUnpairAction,
automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(NukiLock)
}
),
)
async def nuki_lock_unpair_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_action(
"nuki_lock.set_pairing_mode",
NukiLockPairingModeAction,
automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(NukiLock),
cv.Required(CONF_SET_PAIRING_MODE): cv.templatable(cv.boolean)
}
),
)
async def nuki_lock_pairing_mode_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)
Loading

0 comments on commit 73f1662

Please sign in to comment.