Skip to content

Monitoring

OpenLatch exposes two monitoring endpoints for external monitoring systems. Both are unauthenticated so monitoring agents can scrape them without credentials.

Endpoints

Nagios check: GET /api/v1/monitoring/nuki-battery

Returns plain text in Nagios plugin format with performance data.

Query parameters (all optional):

  • warn — WARNING threshold in percent (default: 30)
  • crit — CRITICAL threshold in percent (default: 15)
  • stale_hours — hours before heartbeat data is considered stale (default: 24)

Response examples:

OK - NUKI battery 87% (3min ago) | battery=87%;30;15;0;100
WARNING - NUKI battery 25% (8min ago) | battery=25%;30;15;0;100
CRITICAL - NUKI battery 10% (2min ago) | battery=10%;30;15;0;100
CRITICAL - NUKI not paired with ESP32
UNKNOWN - No heartbeat data received
UNKNOWN - Last heartbeat 1500min ago (stale > 24h)

The NUKI's own battery_critical flag also triggers CRITICAL, regardless of the percent threshold.

Prometheus metrics: GET /api/v1/monitoring/metrics

Returns metrics in Prometheus exposition format (text/plain; version=0.0.4).

Example response:

# HELP openlatch_heartbeat_up Whether a heartbeat has been received (1=yes, 0=no)
# TYPE openlatch_heartbeat_up gauge
openlatch_heartbeat_up 1
# HELP openlatch_heartbeat_age_seconds Seconds since last device heartbeat
# TYPE openlatch_heartbeat_age_seconds gauge
openlatch_heartbeat_age_seconds 42
# HELP openlatch_device_uptime_seconds ESP32 uptime in seconds
# TYPE openlatch_device_uptime_seconds gauge
openlatch_device_uptime_seconds 86400
# HELP openlatch_device_free_heap_bytes ESP32 free heap memory in bytes
# TYPE openlatch_device_free_heap_bytes gauge
openlatch_device_free_heap_bytes 120000
# HELP openlatch_device_wifi_rssi_dbm WiFi signal strength in dBm
# TYPE openlatch_device_wifi_rssi_dbm gauge
openlatch_device_wifi_rssi_dbm -55
# HELP openlatch_nuki_battery_percent NUKI battery level (0-100)
# TYPE openlatch_nuki_battery_percent gauge
openlatch_nuki_battery_percent 87
# HELP openlatch_nuki_battery_critical NUKI critical battery flag (1=critical, 0=ok)
# TYPE openlatch_nuki_battery_critical gauge
openlatch_nuki_battery_critical 0
# HELP openlatch_nuki_paired NUKI pairing status (1=paired, 0=not paired)
# TYPE openlatch_nuki_paired gauge
openlatch_nuki_paired 1

Additionally, if the device sends nuki_daily_report events (3am daily summary), the following metrics are included:

# HELP openlatch_nuki_daily_unlatch_count NUKI unlatch actions in last daily report
# TYPE openlatch_nuki_daily_unlatch_count gauge
openlatch_nuki_daily_unlatch_count 12
# HELP openlatch_nuki_daily_lock_count NUKI lock actions in last daily report
# TYPE openlatch_nuki_daily_lock_count gauge
openlatch_nuki_daily_lock_count 8
# HELP openlatch_nuki_daily_total_actions Total NUKI actions in last daily report
# TYPE openlatch_nuki_daily_total_actions gauge
openlatch_nuki_daily_total_actions 20

Metrics are only included when the device reports them. If no NUKI is configured, NUKI metrics are absent. If no heartbeat has been received yet, only openlatch_heartbeat_up 0 is returned.


Check plugin script

All Nagios-compatible systems (Nagios, Icinga2, Checkmk, Zabbix external checks) use this plugin. Save as /usr/lib/nagios/plugins/check_openlatch_nuki (or your PluginDir):

#!/bin/bash
# check_openlatch_nuki — Nagios/Icinga plugin for OpenLatch NUKI battery
# Usage: check_openlatch_nuki -H <host> [-w <warn>] [-c <crit>] [-s <stale_hours>]

HOST=""
WARN=30
CRIT=15
STALE=24

while getopts "H:w:c:s:" opt; do
  case $opt in
    H) HOST="$OPTARG" ;;
    w) WARN="$OPTARG" ;;
    c) CRIT="$OPTARG" ;;
    s) STALE="$OPTARG" ;;
    *) echo "UNKNOWN - Invalid arguments"; exit 3 ;;
  esac
done

if [ -z "$HOST" ]; then
  echo "UNKNOWN - Missing -H <host>"
  exit 3
fi

URL="$HOST/api/v1/monitoring/nuki-battery?warn=$WARN&crit=$CRIT&stale_hours=$STALE"
OUTPUT=$(curl -sf --max-time 10 "$URL" 2>/dev/null)

if [ $? -ne 0 ]; then
  echo "UNKNOWN - Failed to reach $HOST"
  exit 3
fi

echo "$OUTPUT"

case "$OUTPUT" in
  OK*)       exit 0 ;;
  WARNING*)  exit 1 ;;
  CRITICAL*) exit 2 ;;
  *)         exit 3 ;;
esac
chmod +x /usr/lib/nagios/plugins/check_openlatch_nuki

Icinga2

// /etc/icinga2/conf.d/openlatch.conf

object CheckCommand "openlatch_nuki" {
  command = [ PluginDir + "/check_openlatch_nuki" ]
  arguments = {
    "-H" = "$openlatch_address$"
    "-w" = "$openlatch_nuki_warn$"
    "-c" = "$openlatch_nuki_crit$"
    "-s" = "$openlatch_nuki_stale$"
  }
  vars.openlatch_nuki_warn = 30
  vars.openlatch_nuki_crit = 15
  vars.openlatch_nuki_stale = 24
}

object Host "openlatch" {
  import "generic-host"
  address = "openlatch.example.com"
  vars.openlatch_address = "https://openlatch.example.com"
}

object Service "nuki-battery" {
  import "generic-service"
  host_name = "openlatch"
  check_command = "openlatch_nuki"
  check_interval = 15m
  retry_interval = 5m
  // Override thresholds per-service if needed:
  // vars.openlatch_nuki_warn = 25
  // vars.openlatch_nuki_crit = 10
}

Restart Icinga2:

systemctl restart icinga2

Nagios

# /etc/nagios/objects/openlatch.cfg

define command {
  command_name  check_openlatch_nuki
  command_line  $USER1$/check_openlatch_nuki -H $ARG1$ -w $ARG2$ -c $ARG3$
}

define host {
  use             linux-server
  host_name       openlatch
  alias           OpenLatch Door Controller
  address         openlatch.example.com
}

define service {
  use                 generic-service
  host_name           openlatch
  service_description NUKI Battery
  check_command       check_openlatch_nuki!https://openlatch.example.com!30!15
  check_interval      15
  retry_interval      5
}

Add to your nagios.cfg:

cfg_file=/etc/nagios/objects/openlatch.cfg

Restart Nagios:

systemctl restart nagios

Checkmk

Checkmk supports Nagios-compatible plugins via "local checks" or MRPE.

Copy the check plugin to the Checkmk agent's local checks directory on the OpenLatch server:

cp check_openlatch_nuki /usr/lib/check_mk_agent/local/
chmod +x /usr/lib/check_mk_agent/local/check_openlatch_nuki

Create a wrapper script that Checkmk's agent will execute:

cat > /usr/lib/check_mk_agent/local/openlatch_nuki_battery <<'EOF'
#!/bin/bash
# Checkmk local check — outputs Checkmk local check format
URL="http://localhost:8000/api/v1/monitoring/nuki-battery?warn=30&crit=15"
OUTPUT=$(curl -sf --max-time 10 "$URL" 2>/dev/null)

if [ $? -ne 0 ]; then
  echo "3 openlatch_nuki_battery - UNKNOWN - Failed to reach OpenLatch"
  exit 0
fi

# Extract perfdata (after |) and status
PERFDATA=$(echo "$OUTPUT" | sed 's/.*| //')
STATUS_TEXT=$(echo "$OUTPUT" | sed 's/ |.*//')

case "$OUTPUT" in
  OK*)       STATE=0 ;;
  WARNING*)  STATE=1 ;;
  CRITICAL*) STATE=2 ;;
  *)         STATE=3 ;;
esac

echo "$STATE openlatch_nuki_battery $PERFDATA $STATUS_TEXT"
EOF
chmod +x /usr/lib/check_mk_agent/local/openlatch_nuki_battery

In Checkmk WATO, run a service discovery on the host. The openlatch_nuki_battery service will appear automatically.

Option B: MRPE (if agent runs on a different host)

Add to /etc/check_mk/mrpe.cfg:

OpenLatch_NUKI_Battery /usr/lib/nagios/plugins/check_openlatch_nuki -H https://openlatch.example.com -w 30 -c 15

Zabbix

In Zabbix web UI:

  1. Go to Configuration → Hosts → (your host) → Items → Create item
  2. Set:

    Field Value
    Name OpenLatch NUKI Battery
    Type HTTP agent
    URL https://openlatch.example.com/api/v1/monitoring/nuki-battery
    Request type GET
    Type of information Text
    Update interval 5m
  3. Create a trigger:

    Field Value
    Name NUKI battery warning
    Severity Warning
    Expression find(/host/openlatch.nuki.battery,,"regexp","^WARNING")=1
    Field Value
    Name NUKI battery critical
    Severity High
    Expression find(/host/openlatch.nuki.battery,,"regexp","^CRITICAL")=1

Option B: External check (uses the plugin script)

Copy the check plugin to /usr/lib/zabbix/externalscripts/:

cp check_openlatch_nuki /usr/lib/zabbix/externalscripts/
chmod +x /usr/lib/zabbix/externalscripts/check_openlatch_nuki

Create an item with type External check:

check_openlatch_nuki[-H,https://openlatch.example.com,-w,30,-c,15]

Option C: Prometheus integration

If you already have Zabbix 6.0+ with Prometheus preprocessing, create an HTTP agent item pointing at /api/v1/monitoring/metrics and use Prometheus pattern preprocessing to extract individual metrics. See the Prometheus section below for the available metric names.


Prometheus + Grafana

This is the recommended setup for battery and usage graphs.

Prometheus configuration

Add to prometheus.yml:

scrape_configs:
  - job_name: openlatch
    scrape_interval: 60s
    metrics_path: /api/v1/monitoring/metrics
    static_configs:
      - targets:
          - openlatch.example.com
    scheme: https

Reload Prometheus:

systemctl reload prometheus
# or: kill -HUP $(pidof prometheus)

Verify scraping works:

curl -s https://openlatch.example.com/api/v1/monitoring/metrics

Available metrics

Metric Type Description
openlatch_heartbeat_up gauge 1 if heartbeat received, 0 if not
openlatch_heartbeat_age_seconds gauge Seconds since last heartbeat
openlatch_device_uptime_seconds gauge ESP32 uptime
openlatch_device_free_heap_bytes gauge ESP32 free heap memory
openlatch_device_wifi_rssi_dbm gauge WiFi signal strength
openlatch_nuki_battery_percent gauge Battery level 0-100
openlatch_nuki_battery_critical gauge 1 if NUKI reports critical battery
openlatch_nuki_paired gauge 1 if paired, 0 if not
openlatch_nuki_daily_unlatch_count gauge Unlatch actions in last daily report
openlatch_nuki_daily_lock_count gauge Lock actions in last daily report
openlatch_nuki_daily_total_actions gauge Total actions in last daily report

Prometheus alerting rules

Save as /etc/prometheus/rules/openlatch.yml:

groups:
  - name: openlatch
    rules:
      - alert: NukiBatteryLow
        expr: openlatch_nuki_battery_percent < 30
        for: 1h
        labels:
          severity: warning
        annotations:
          summary: "NUKI battery low ({{ $value }}%)"
          description: "Battery has been below 30% for over 1 hour."

      - alert: NukiBatteryCritical
        expr: openlatch_nuki_battery_percent < 15 or openlatch_nuki_battery_critical == 1
        for: 10m
        labels:
          severity: critical
        annotations:
          summary: "NUKI battery critical ({{ $value }}%)"
          description: "Replace batteries immediately."

      - alert: OpenLatchHeartbeatMissing
        expr: openlatch_heartbeat_up == 0 or openlatch_heartbeat_age_seconds > 900
        for: 15m
        labels:
          severity: warning
        annotations:
          summary: "No heartbeat from OpenLatch for >15 minutes"
          description: "ESP32 may be offline or network connectivity lost."

      - alert: NukiNotPaired
        expr: openlatch_nuki_paired == 0
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "NUKI not paired with ESP32"
          description: "Lock cannot be controlled. Re-pair required."

Grafana dashboard

Import the following dashboard JSON or create panels manually.

Panel 1: NUKI Battery Level

  • Type: Time series (line chart)
  • Query: openlatch_nuki_battery_percent
  • Unit: Percent (0-100)
  • Thresholds: 30 (yellow), 15 (red)

PromQL:

openlatch_nuki_battery_percent{job="openlatch"}

Panel 2: Device Health

  • Type: Stat panel (4 values)
  • Queries:
openlatch_nuki_battery_percent{job="openlatch"}
openlatch_device_uptime_seconds{job="openlatch"} / 3600
openlatch_device_wifi_rssi_dbm{job="openlatch"}
openlatch_device_free_heap_bytes{job="openlatch"} / 1024

Panel 3: Heartbeat Freshness

  • Type: Gauge
  • Query: openlatch_heartbeat_age_seconds{job="openlatch"}
  • Unit: Seconds
  • Thresholds: 300 (green), 600 (yellow), 900 (red)

Panel 4: Battery + Daily Usage over Time

  • Type: Time series with dual Y-axes
  • Left Y-axis: Battery % (line)
  • Right Y-axis: Daily action count (bar)
  • X-axis: Time

PromQL queries:

# Battery level over time
openlatch_nuki_battery_percent{job="openlatch"}

# Daily usage (total actions from 3am daily report)
openlatch_nuki_daily_total_actions{job="openlatch"}

For more detail, split by action type:

openlatch_nuki_daily_unlatch_count{job="openlatch"}
openlatch_nuki_daily_lock_count{job="openlatch"}

Alternatively, query the audit log directly via Grafana's PostgreSQL data source for historical daily reports:

SELECT
  details->>'battery_percent' AS battery,
  details->>'total_actions' AS actions,
  created_at
FROM audit_log
WHERE event_type = 'nuki_daily_report'
ORDER BY created_at;

Uptime Kuma

  1. Add a new monitor: HTTP(s) - Keyword
  2. Set:

    Field Value
    URL https://openlatch.example.com/api/v1/monitoring/nuki-battery
    Keyword OK
    Heartbeat interval 300 (5 min)

This marks the service as DOWN when the response does not contain "OK" (i.e., WARNING, CRITICAL, or UNKNOWN).

For more granularity, create separate monitors:

  • NUKI Battery Critical: keyword CRITICAL, invert match, expected status UP when keyword is absent
  • NUKI Heartbeat: URL https://openlatch.example.com/api/v1/health, keyword ok