Nuki Card with Callback support (supports both Lock & Opener, it replaces the official integration)

IMPORTANT UPDATE:
this integration/automation has been superseded and ported to a custom component: Nuki NG by @kvj, so Nuki Card won’t be updated/maintained anymore. I strongly advice to switch to the custom component version which provides all functionalities of Nuki Card, and more. You can use this thread or the Nuki Card discord server for support. Installation instructions of the component are on the GitHub repository.

IMPORTANT UPDATE 2:
I migrated from Nuki NG to Nuki Hub. It’s an ESP32 based project, that basically replaces the bridge by communicating via bluetooth with the lock (like the bridge does) but it leverages MQTT and HA MQTT discovery. It overcomes a lot of issues with Nuki due to the bad design of Nuki Bridge firmware. Nuki, after a couple of years of complaints, has decided to implement MQTT too, but the fw is still in beta and only for SL 3.0. Once the fw will be ready also for Nuki Bridge I will test it, but for now, Nuki Hub is working perfectly and it’s the best solution for my use case.

If you have a Nuki Lock system, and you are not satisfied by the official integration, here’s a way to overcome all the current limitations and monitor/operate the Nuki in a better way.

I know devs are working on the official integration to implement some of the things you’ll find in this integration, but since I wanted to learn HA better, I decided to start creating this for the Nuki solution.

Main Features of this card:

  • doesn’t require the official integration
  • supports both the Smart Lock and the Opener
  • lock device support through bridge callback (fast updates)
  • door sensor support through bridge callback (fast updates)
  • ALL Nuki info mapped to sensors (polling via 2 REST sensors)
  • Dynamic state icons (battery state and type, lock, door, etc.)
  • Easy configuration - it only requires 4 items in secrets.yaml
  • Automatic Bridge Callback Configuration and Enforcement (add/delete duplicate entries) with HA persistent notification of the actions
  • Automatic language support (strings shown in language selected in HA profile)

@HITMIK, @Joerg, @Friedrieck, @mahikeulbody @123 @spokin: thanks for your help and contributions of ideas and tests.

There’s also a Nuki Card server on Discord for support.


-= Nuki Card v11 =-

Changelog (click to expand)

Changelog:

  • v3.0 added the Door sensor and Nuki Lock switch based on callbacks.
  • v3.1 changed the lock entity from Template Switch to Template Lock
  • v4.0 changed all legacy state sensors to the new Template State Sensors and recoded the Door Sensor to be ready for multiple triggers (not yet completed, it will allow to overcome the problem mentioned in the following note.
  • v4.1 fixed some bugs in templates.yaml (if you configured v4.0, you just need to reconfigure templates.yaml)
  • v4.2 fixed other minor things in templates.yaml (if you configured v4.1, you just need to reconfigure templates.yaml)
  • v4.3 fixed some code in the trigger door sensor (if you configured v4.2, you just need to reconfigure templates.yaml)
  • v4.4 fixed some code in several sensors (if you configured v4.3, you just need to reconfigure templates.yaml) that finally solved the problem of the initialization after a restart. thx to @Friedrieck and @123 for the advices and the debugging
  • v4.5 fixed some nasty bugs introduced in 4.4 and updated icons for door/lock polled sensors (if you configured v4.4, you just need to reconfigure templates.yaml)
  • v4.6 finally solved the HA restart issue, using a state trigger to link the door triggered sensor to the polled state sensor. (if you configured v4.5, you just need to reconfigure templates.yaml)
  • v4.7 removed homeassistant/start event from the door triggered sensor, added content_type to the rest_command, modified icon of device name sensor, moved door/lock polled sensors to attributes of the door triggered sensor - updated files: templates.yaml rest_commands.yaml
  • v5.0 reworked all sensors to avoid log errors at startup while REST sensors were not initialized yet - updated files: templates.yaml
  • v5.1 modified rest_command (removed content_type) and keypad sensor to avoid log errors at startup for users without the keypad - updated files: templates.yaml and rest_commands.yaml
  • v6.0 Automation version in package (supereasy installation)
  • v7.0 automated bridge callback configuration and enforcement (with persistent notification) (super-super-easy installation), optimized some code, renamed some sensors, unified nuki secrets url variables
  • v8.0 (major release): rewrote many parts of the package, optimized the code, and fixed 2 bugs regarding the automatic bridge configuration. Now the reconfiguration of the bridge should be much quicker (1m max.).
  • v8.1 last minute minor changes
  • v8.2 couple of bugs fixed, automation settings changed (thanks @spokin)
  • v8.3 optimized automation settings and added checks everywhere a sensor was referenced in the code to avoid as much as possible errors in HA log, mainly in init/restart phase.
  • v8.4 reconfigured automation mode and rest sensors polling times to avoid concurrent calls to the bridge
  • v8.5 changed lock actions to Simple Lock Actions (thanks @kvj), device configuration driven, so if you configured the lock with a knob (instead of a handle) it will unlatch instead of simply unlocking. Min. bridge fw required: v2.5.3.
  • v8.6 further optimized the code to avoid concurrent http requests to the bridge, that is poorly designed, supporting only 1 request at a time
  • v8.7 localization support by @spokin (just use input_select.nuki_language to configure it). Removed the duplicated door/lock polling sensors. Relaxed the REST sensors polling periods even further. Lot of optimizations to the code. Please re-edit the lovelace card, as sensor names have changed.
  • v8.8 typo in json file corrected, reference to an old binary_sensor corrected in the code.
  • v9.0 eliminated localization support in code, leveraging HA device_class so translation of status strings is automatically managed by HA (please delete the json file). Introduced two new input_select to configure the lock and unlock actions: you can decide what actions are executed when you press the lock/unlock button. Sensor names have changed, please update the lovecard code.
  • v10.0 breaking change release: introduced serialization of http calls to the bridge, finally those errors in the log due to the bridge limitations should disappear. Had to redesign the way to communicate with the bridge, and am very happy with the result. Hope you’ll notice the difference. Just upgrade the code and restart HA.
  • v10.1 forgot to configure the script mode, sorry about that.
  • v10.2 sometimes script.nuki_bridge_polling_queue wasn’t called on startup. enforced it in the trigger.
  • v11.0 Major release: finally added support for the Opener, highly requested feature. Improved refresh times for some sensors. Fixed some bugs.

Screenshots (click to expand)

Compact card:

image

Full cards of the Smart Lock and the Opener

Notification of automatic Bridge Configuration:

image


Now let’s go to the configuration of the card. :slight_smile:

NOTES:

  • the code is based on Nuki Bridge API v1.12, make sure the bridge fw is updated to last available version before starting the configuration of this integration. min. version supported: v2.5.3
  • Nuki Bridge doesn’t support https, so if you have HA secured with https-only, you need to allow http access locally (LAN/WLAN access, you can leave https from the internet). The NGINX HA addon allows this kind of configuration.
  • Bridge API has to be active and you have to know the token, you can do this via Nuki mobile app: go to Burger menu > Manage my devices > Bridge and follow the described steps.
  • Home Assistant has to be updated to one of the latest version (2021.8.x), the card will use the latest features introduced in latest versions for template triggers, etc. so it needs the latest HA version possible.

  • 1st step is the creation of a long-lived token in HA profile’s page, used for the callback’s webhook. Create it and take note of it.

NOTE: Please don’t confuse the Nuki access token with the HA long-term token for the webhook, the first is needed by HA to communicate with the Nuki Bridge API, the second is needed by the Nuki Bridge to communicate with HA


  • in secrets.yaml you need to configure 4 items: Nuki Bridge full url path, Bridge API token you create through Nuki App or the web console, long-lived token (webhook) you created in the first step, HA full url path.

NOTE: use hostnames ONLY if you have a properly configured DNS, and use reserved/fixed IP addresses for HA and the Bridge. If you don’t use a DNS but only rely on the provider’s router, I strongly advice to use IP addresses instead of hostnames for the URLs in the secrets.

nuki_bridge_url: "http://nuki-bridge.axel.dom:8080"
nuki_bridge_token: "yyxxzz"
nuki_bridge_webhook: "hdCI6MTYyMTcyODg0MSwiZXhwIjoxOTM3MDg4ODQxfQ.-ECrJx_lbPLRRJwRTfJsoW-RfIaIioKVtcyfEvJiHZE"
nuki_ha_internal_url: "http://hass.axel.dom:8123"

  • in previous versions (up to 6.1), you had to configure the callback on the bridge manually through curl or browser. this is no longer the case, v7.0 introduced automatic callback configuration/enforcement of the bridge. It does this through a script and a couple of new sensors, one of which (sensor.nuki_bridge_callback_list) is used to check the bridge callback config. Another sensor (binary_sensor.nuki_bridge_callback) checks if the bridge config contains the corrent token/webhook url, and if not, it calls script.nuki_bridge_check_callback, generating a persistent notification so you can be alerted of the change (see screenshots above).

  • create a folder called packages in the main CONFIG folder, and in that folder create a file named nuki_card_callback.yaml, you can download them here or copy the code from it, whatever way you prefer: Nuki Card v11.0 · GitHub

  • in configuration.yaml, under the homeassistant: section, reference the directory containing all packages:
    image

  • and finally the code for the lovelace cards, I included all sensors created, customize the list as you prefer:

Minimal Version (click to expand)
- type: entities
entities:
  - entity: lock.nuki_lock_action
    secondary_info: last-updated
  - entity: binary_sensor.nuki_door_sensor_state
    secondary_info: last-updated
  - entity: binary_sensor.nuki_bridge_callback
    secondary_info: last-updated
  - entity: sensor.nuki_last_activity
state_color: true
title: Nuki Card
Full version for the Lock and Bridge, with all entities (click to expand)
type: vertical-stack
cards:
  - type: entities
    entities:
      - entity: sensor.nuki_lock_friendly_name
      - entity: lock.nuki_lock_action
        secondary_info: last-updated
      - entity: binary_sensor.nuki_door_state
        secondary_info: last-updated
      - entity: binary_sensor.nuki_bridge_callback
        secondary_info: last-updated
      - entity: sensor.nuki_lock_last_update
      - entity: input_select.nuki_choose_lock_action
      - entity: input_select.nuki_choose_unlock_action
      - entity: sensor.nuki_bridge_fw_version
      - entity: sensor.nuki_bridge_wifi_fw_version
      - entity: binary_sensor.nuki_bridge_wifi_connected
      - entity: binary_sensor.nuki_bridge_cloud_connected
      - entity: sensor.nuki_lock_id
      - entity: sensor.nuki_lock_device_name
      - entity: binary_sensor.nuki_lock_bridge_bt_state
      - entity: sensor.nuki_lock_bridge_bt_rssi
      - entity: sensor.nuki_lock_battery_level
      - entity: sensor.nuki_lock_fw_version
      - entity: binary_sensor.nuki_lock_battery_critical_state
      - entity: binary_sensor.nuki_keypad_battery_critical_state
    state_color: true
    title: Nuki Card v11 (Lock/Bridge)
Full version for the Opener, with all entities (click to expand)
type: vertical-stack
cards:
  - type: entities
    entities:
      - entity: sensor.nuki_opener_friendly_name
      - entity: lock.nuki_opener_action
      - entity: sensor.nuki_opener_state
      - entity: sensor.nuki_opener_last_update
      - entity: input_select.nuki_choose_opener_lock_action
      - entity: input_select.nuki_choose_opener_unlock_action
      - entity: sensor.nuki_opener_mode
      - entity: binary_sensor.nuki_opener_ring_action_state
      - entity: sensor.nuki_opener_id
      - entity: sensor.nuki_opener_device_name
      - entity: binary_sensor.nuki_opener_bridge_bt_state
      - entity: sensor.nuki_opener_bridge_bt_rssi
      - entity: sensor.nuki_opener_fw_version
      - entity: binary_sensor.nuki_opener_battery_critical_state
    state_color: true
    title: Nuki Card v11 (Opener)

That’s about it, enjoy. :slightly_smiling_face:

Alessandro

19 Likes

@123 @Friedrieck here’s the state of the two sensors after a restart or a template reload:

image image

I’m testing without the availability section for binary_sensor.nuki_door_state

@mahikeulbody @Joerg please let me know if the instructions work, unfortunately many things changed, so you have to redo all the configuration from scratch. sorry for that. :slight_smile:

That seems ok here. Good job.
I will re-try tomorrow removing Nuki integration.

Strange. If there is no availability section, as it is a binary_sensor, and the state does not evaluate to true, then it should be false, not unavailable. I think it could happen only during the actual reload of the sensor, but this is short.
Probably no impact, but I would have written the condition like this:
{{ is_state('sensor.nuki_door_sensor_state', 'open') or is_state('sensor.nuki_door_sensor_state', 'closed') }}
I think you could also test the sensor’s own state:
{{ is_state('binary_sensor.nuki_door_state', 'on') or is_state('binary_sensor.nuki_door_state', 'off') }}

Also to correct, I think:

        lock_battery: >
          {% if trigger.platform == 'webhook' %}
            {{ trigger.json.batteryChargeState }}%
          {% else %}
            {{ sensor.nuki_id }}

More importantly, there is a couple of this:

        lock_battery_critical: >
          {% if trigger.platform == 'webhook' %}
            {{ is_state('trigger.json.batteryCritical', 'True') }}

I don’t think you can use is_state() with the trigger object. I would assume it only is for entity objects. It could be the cause of the invalidation of the sensor, but maybe I’m wrong. Have you checked the logs?

You’re right. I forgot to optimize those.

I thought about that, but then I thought: what happens the first time? Does it work? :slight_smile:

Cut&paste errors, fixed both of them:

        lock_battery_critical: >
          {% if trigger.platform == 'webhook' %}
            {{ trigger.json.batteryCritical }}
          {% else %}
            {{ sensor.nuki_lock_battery_level }}
          {% endif %}
        keypad_battery_critical: >
          {% if trigger.platform == 'webhook' %}
            {{ trigger.json.keypadbatteryCritical }}
          {% else %}
            {{ sensor.nuki_keypad_battery_critical_state }}
          {% endif %}

I’ll recheck the logs and test with these modifications…

Thanks a lot.

v4.2 released, fixed some bugs thanks to @Friedrieck. Just copy&paste the updated templates.yaml and you’re done. Sorry.

@Friedrieck tested with the fixes, same thing, it’s unavailable after template reload. But I noticed this now in the logs:

And also this now (I reloaded templates):

Logger: homeassistant.helpers.template
Source: helpers/template.py:1377
First occurred: 1:08:45 (2 occurrences)
Last logged: 1:10:07

Template variable error: 'sensor' is undefined when rendering '{% if trigger.platform == 'webhook' %} {% set my_state = {1: 'deactivated', 2: 'closed', 3: 'open', 4: 'unknown', 5: 'calibrating'} %} {% set trigdoor = my_state[trigger.json.doorsensorState] %} {% set my_state = {0: 'uncalibrated', 1: 'locked', 2:'unlocking', 3: 'unlocked', 4: 'locking', 5: 'unlatched', 6: "unlocked (lock ‘n’ go)", 7: 'unlatching', 254: 'motor blocked', 255: 'undefined'} %} {% set triglock = my_state[trigger.json.state] %} {% else %} {% set trigdoor = sensor.nuki_door_sensor_state %} {% set triglock = sensor.nuki_lock_sensor_state %} {% endif %} {% if (trigdoor == 'open') %} mdi:door-open {% elif trigdoor == 'closed' and triglock == 'locked' %} mdi:door-closed-lock {% elif trigdoor == 'closed' and triglock == 'unlocked' %} mdi:door-closed {% else %} mdi:alert-box-outline {% endif %}'
1 Like

Hi Alex,
works here as well, I had the Nuki integration removed already the other day.
Great job, thank you for this.

IMO there’s a glitch in the documentation:
Now in rest_commands.yaml file, paste this code, it is required for the lock/unlock action of the previously created lock sensor.
The file should be named rest_command.yaml.

1 Like

I have in config.yaml
rest_command: !include rest_commands.yaml

You can choice any name of file, I think.

1 Like

:roll_eyes:
Sure. I shouldn’t do something like this before having at least the third coffee.
I used
rest_commands: !include rest_commands.yaml
and got an error that this integration doesn’t exist. :rofl:

I just copy&paste the updated templates.yaml and it is not ok (all sensors are unavailable). May be you changed also another file ?

Anyway, to be sure, I copy&paste all v4.2 files and now it is ok. So v4.2 is ok but may be not the description of the diff with v4.1 ?

PS. I finally removed Nuki integration.

I suggest a very small cosmetic modification:

name: "Nuki Friendly Name"
icon: mdi:alpha

instead of mdi:numeric

Is the availability template into locks.yaml file correct ?
(I am not very good in this yaml syntax)

So, the logs are instructive, and tell where to look at: there is a problem with the icon part.
And indeed, I think this may cause troubles:

[...]
        {% else %}
          {% set trigdoor = sensor.nuki_door_sensor_state %}
          {% set triglock = sensor.nuki_lock_sensor_state %}
        {% endif %}
        {% if (trigdoor == 'open') %}
          mdi:door-open
        {% elif trigdoor == 'closed' and triglock == 'locked' %}
          mdi:door-closed-lock
        {% elif trigdoor == 'closed' and triglock == 'unlocked' %}
[...]

The two first lines should probably be corrected like this:

          {% set trigdoor = states(sensor.nuki_door_sensor_state) %}
          {% set triglock = states(sensor.nuki_lock_sensor_state) %}

And the same goes for your corrections in your post here: Nuki Card with callbacks (official integration not needed) - #6 by alexdelprete

1 Like

I’m at the first coffee, so I better not answer anything now…:smiley:

I removed all unique_id from templates.yaml. So in order to start clean, in case you have orphan entities, you need to remove everything and reconfigure. Sorry for that. But I touched only templates.yaml.

ok, no problem.

Thanks for spotting all those, corrected here:

- trigger:
    - platform: event
      event_type: event_template_reloaded
    - platform: homeassistant
      event: start
    - platform: webhook
      webhook_id: !secret nuki_bridge_webhook
  binary_sensor:
    - name: "Nuki Door State"
      device_class: door
      state: >
        {% if trigger.platform == 'webhook' %}
          {{ trigger.json.doorsensorState == 3 }}
        {% else %}
          {{ is_state('sensor.nuki_door_sensor_state', 'open') }}
        {% endif %}
      availability: >
        {% if trigger.platform == 'webhook' %}
          {{ trigger.json.doorsensorState != None }}
        {% else %}
          {{ (is_state('sensor.nuki_door_sensor_state', 'open') or is_state('sensor.nuki_door_sensor_state', 'closed')) }}
        {% endif %}
      icon: >
        {% if trigger.platform == 'webhook' %}
          {% set my_state = {1: 'deactivated', 2: 'closed', 3: 'open', 4: 'unknown', 5: 'calibrating'} %}
          {% set trigdoor = my_state[trigger.json.doorsensorState] %}
          {% set my_state = {0: 'uncalibrated', 1: 'locked', 2:'unlocking', 3: 'unlocked', 4: 'locking', 5: 'unlatched', 6: "unlocked (lock ‘n’ go)", 7: 'unlatching', 254: 'motor blocked', 255: 'undefined'} %}
          {% set triglock = my_state[trigger.json.state] %}
        {% else %}
          {% set trigdoor = states(sensor.nuki_door_sensor_state) %}
          {% set triglock = states(sensor.nuki_lock_sensor_state) %}
        {% endif %}
        {% if (trigdoor == 'open') %}
          mdi:door-open
        {% elif trigdoor == 'closed' and triglock == 'locked' %}
          mdi:door-closed-lock
        {% elif trigdoor == 'closed' and triglock == 'unlocked' %}
          mdi:door-closed
        {% else %}
          mdi:alert-box-outline
        {% endif %}
      attributes:
        nuki_id: >
          {% if trigger.platform == 'webhook' %}
            {{ trigger.json.nukiId }}
          {% else %}
            {{ states(sensor.nuki_id) }}
          {% endif %}
        door_state: >
          {% set my_state = {1: 'deactivated', 2: 'closed', 3: 'open', 4: 'unknown', 5: 'calibrating'} %}
          {% if trigger.platform == 'webhook' %}
            {{ my_state[trigger.json.doorsensorState] }}
          {% else %}
            {{ states(sensor.nuki_door_sensor_state) }}
          {% endif %}
        lock_state: >
          {% set my_state = {0: 'uncalibrated', 1: 'locked', 2:'unlocking', 3: 'unlocked', 4: 'locking', 5: 'unlatched', 6: "unlocked (lock ‘n’ go)", 7: 'unlatching', 254: 'motor blocked', 255: 'undefined'} %}          
          {% if trigger.platform == 'webhook' %}
            {{ my_state[trigger.json.state] }}
          {% else %}
            {{ states(sensor.nuki_lock_sensor_state) }}
          {% endif %}
        lock_battery: >
          {% if trigger.platform == 'webhook' %}
            {{ trigger.json.batteryChargeState }}%
          {% else %}
            {{ states(sensor.nuki_lock_battery_level) }}
          {% endif %}
        lock_battery_critical: >
          {% if trigger.platform == 'webhook' %}
            {{ trigger.json.batteryCritical }}
          {% else %}
            {{ states(sensor.nuki_lock_battery_critical_state) }}
          {% endif %}
        keypad_battery_critical: >
          {% if trigger.platform == 'webhook' %}
            {{ trigger.json.keypadBatteryCritical }}
          {% else %}
            {{ states(sensor.nuki_keypad_battery_critical_state) }}
          {% endif %}
        last_update: "{{ strptime(as_timestamp(now()) | timestamp_local, '%Y%m%d %H:%M:%S') }}"
        trigger: '{{ trigger.platform }}'

I just did a template reload, and same result, unfortunately.

image

Here’s what’s in the logs after the reload:

Logger: homeassistant.components.template.binary_sensor
Source: components/template/trigger_entity.py:141
Integration: Template (documentation, issues)
First occurred: 1:59:04 (2 occurrences)
Last logged: 12:00:12

Error rendering icon template for binary_sensor.nuki_door_state: UndefinedError: ‘sensor’ is undefined

Logger: homeassistant.helpers.template
Source: helpers/template.py:1377
First occurred: 1:59:04 (2 occurrences)
Last logged: 12:00:12

Template variable error: ‘sensor’ is undefined when rendering ‘{% if trigger.platform == ‘webhook’ %} {% set my_state = {1: ‘deactivated’, 2: ‘closed’, 3: ‘open’, 4: ‘unknown’, 5: ‘calibrating’} %} {% set trigdoor = my_state[trigger.json.doorsensorState] %} {% set my_state = {0: ‘uncalibrated’, 1: ‘locked’, 2:‘unlocking’, 3: ‘unlocked’, 4: ‘locking’, 5: ‘unlatched’, 6: “unlocked (lock ‘n’ go)”, 7: ‘unlatching’, 254: ‘motor blocked’, 255: ‘undefined’} %} {% set triglock = my_state[trigger.json.state] %} {% else %} {% set trigdoor = sensor.nuki_door_sensor_state %} {% set triglock = sensor.nuki_lock_sensor_state %} {% endif %} {% if (trigdoor == ‘open’) %} mdi:door-open {% elif trigdoor == ‘closed’ and triglock == ‘locked’ %} mdi:door-closed-lock {% elif trigdoor == ‘closed’ and triglock == ‘unlocked’ %} mdi:door-closed {% else %} mdi:alert-box-outline {% endif %}’
Template variable error: ‘sensor’ is undefined when rendering ‘{% if trigger.platform == ‘webhook’ %} {% set my_state = {1: ‘deactivated’, 2: ‘closed’, 3: ‘open’, 4: ‘unknown’, 5: ‘calibrating’} %} {% set trigdoor = my_state[trigger.json.doorsensorState] %} {% set my_state = {0: ‘uncalibrated’, 1: ‘locked’, 2:‘unlocking’, 3: ‘unlocked’, 4: ‘locking’, 5: ‘unlatched’, 6: “unlocked (lock ‘n’ go)”, 7: ‘unlatching’, 254: ‘motor blocked’, 255: ‘undefined’} %} {% set triglock = my_state[trigger.json.state] %} {% else %} {% set trigdoor = states(sensor.nuki_door_sensor_state) %} {% set triglock = states(sensor.nuki_lock_sensor_state) %} {% endif %} {% if (trigdoor == ‘open’) %} mdi:door-open {% elif trigdoor == ‘closed’ and triglock == ‘locked’ %} mdi:door-closed-lock {% elif trigdoor == ‘closed’ and triglock == ‘unlocked’ %} mdi:door-closed {% else %} mdi:alert-box-outline {% endif %}’

Sorry, my bad (even if at my third coffee already, but not Italian ones!).
I forgot the ' around the sensor object:

          {% set trigdoor = states('sensor.nuki_door_sensor_state') %}
          {% set triglock = states('sensor.nuki_lock_sensor_state') %}

Also to correct in several of the attributes.
Don’t worry, there is some light at the end of the tunnel…