Skip to content

feat: Home Assistant MQTT Auto-Discovery Integration#88

Merged
matteius merged 8 commits intomainfrom
home-assistant
Feb 16, 2026
Merged

feat: Home Assistant MQTT Auto-Discovery Integration#88
matteius merged 8 commits intomainfrom
home-assistant

Conversation

@matteius
Copy link
Contributor

@matteius matteius commented Feb 15, 2026

Summary

Adds native Home Assistant integration via MQTT auto-discovery. When enabled, LightNVR cameras and sensors automatically appear in Home Assistant without any custom Python code, HACS integration, or manual YAML configuration. LightNVR publishes retained discovery messages following HA's native MQTT discovery protocol.

New Configuration Options

Three new settings in the [mqtt] section of lightnvr.ini (also exposed in the web UI):

Setting Default Description
ha_discovery false Enable/disable HA MQTT auto-discovery
ha_discovery_prefix homeassistant Discovery topic prefix (must match HA's MQTT config)
ha_snapshot_interval 30 Seconds between JPEG snapshot publishes (0 = disabled, max 300)

Discovered Entities (per stream)

For each enabled stream, three Home Assistant entities are auto-discovered:

Entity Type HA Domain Description
Camera camera Live JPEG snapshots published via MQTT
Motion Sensor binary_sensor ON/OFF motion detection with 30s debounce
Detection Count sensor Number of currently detected objects

All entities are grouped under a single LightNVR device in HA with version info and a configuration URL pointing to the web UI.

MQTT Topic Layout

{topic_prefix}/availability              → online/offline (LWT)
{topic_prefix}/cameras/{stream}/snapshot  → JPEG image data
{topic_prefix}/cameras/{stream}/motion    → ON / OFF
{topic_prefix}/cameras/{stream}/detection_count → integer
{topic_prefix}/cameras/{stream}/{label}   → per-class count (e.g. person, car)

{ha_prefix}/camera/lightnvr/{stream}/config           → discovery payload
{ha_prefix}/binary_sensor/lightnvr/{stream}_motion/config
{ha_prefix}/sensor/lightnvr/{stream}_detection_count/config

Key Implementation Details

  • Last Will and Testament (LWT): The MQTT broker automatically publishes offline on unexpected disconnect, so HA shows correct availability status.
  • Motion debounce: Publishes ON immediately on first detection, then OFF after 30 seconds with no new detections — prevents rapid toggling in HA automations.
  • Per-object-class counting: Individual label topics (e.g., lightnvr/cameras/front_door/person) enable fine-grained HA automations.
  • Background threads: Snapshot publishing and motion timeout checking each run in dedicated threads with clean shutdown via ha_services_running flag.
  • Retained discovery messages: All discovery config payloads are retained so HA picks them up on restart.
  • Thread-safe motion state: Per-stream motion tracking protected by pthread mutex.
  • Hot-reload from web UI: Changing any MQTT or HA discovery setting via the Settings page triggers an async mqtt_reinit() cycle (cleanup → init → connect → re-publish discovery) in a detached background thread — no restart required.

Files Changed

File Change
config/lightnvr.ini Added HA discovery config documentation and defaults
include/core/config.h Added mqtt_ha_discovery, mqtt_ha_discovery_prefix, mqtt_ha_snapshot_interval fields
include/core/mqtt_client.h Added 6 new public APIs: mqtt_publish_binary, mqtt_publish_ha_discovery, mqtt_set_motion_state, mqtt_start_ha_services, mqtt_stop_ha_services, mqtt_reinit (plus no-op stubs)
src/core/config.c Parse + save new config fields; added full MQTT section to save_config()
src/core/main.c Call HA discovery + start services after MQTT connect
src/core/mqtt_client.c Core implementation (~620 lines): discovery publishing, binary publish, motion state machine, snapshot thread, motion timeout thread, LWT setup, mqtt_reinit()
src/video/api_detection.c Hook mqtt_set_motion_state() into both detection code paths
src/web/api_handlers_detection_results.c Hook mqtt_set_motion_state() into web API detection storage
src/web/api_handlers_settings.c GET/POST handlers for 3 new HA settings + async MQTT reinit on change
web/js/components/preact/SettingsView.jsx UI controls for HA discovery toggle, prefix, and snapshot interval

Testing Notes

  • Requires an MQTT broker shared with Home Assistant (e.g., Mosquitto).
  • Enable MQTT in LightNVR settings, point to the same broker, then toggle Enable HA Discovery.
  • Entities should appear under Settings → Devices → LightNVR in Home Assistant within seconds.
  • Verify availability shows "online" and transitions to "offline" when LightNVR stops.
  • Trigger object detection to confirm motion binary sensor and detection count update in HA.
  • Change settings via the web UI and confirm hot-reload works without restarting LightNVR.

Implement native MQTT auto-discovery so that LightNVR cameras and sensors
automatically appear in Home Assistant without any custom Python code or
HACS integration. When enabled, LightNVR publishes retained discovery
messages following HA's native MQTT discovery protocol.

## New Configuration Options (lightnvr.ini [mqtt] section)

- ha_discovery = true|false    Enable/disable HA MQTT auto-discovery
- ha_discovery_prefix = ...    Discovery topic prefix (default: homeassistant)
- ha_snapshot_interval = N     Snapshot publish interval in seconds (default: 30)

## Discovery Entities (per stream)

For each configured stream, three HA entities are auto-discovered:
- Camera entity         — live JPEG snapshots via MQTT
- Binary sensor (motion) — ON/OFF motion detection state
- Sensor (detection count) — number of detected objects

Discovery topics follow the pattern:
  {prefix}/camera/lightnvr/{stream}/config
  {prefix}/binary_sensor/lightnvr/{stream}_motion/config
  {prefix}/sensor/lightnvr/{stream}_detection_count/config

All entities are grouped under a single LightNVR device in HA with
version info and a configuration URL pointing to the web UI.

## Runtime MQTT Topics

- lightnvr/availability           — online/offline (LWT)
- lightnvr/cameras/{stream}/snapshot       — JPEG image data
- lightnvr/cameras/{stream}/motion         — ON/OFF with 30s debounce
- lightnvr/cameras/{stream}/detection_count — object count
- lightnvr/cameras/{stream}/{label}        — per-class object count

## Key Implementation Details

- Last Will and Testament (LWT) for availability tracking: broker
  publishes 'offline' automatically on unexpected disconnect
- Motion detection uses 30-second debounce: publishes ON immediately
  on first detection, OFF after 30s with no new detections
- Per-object-class counting (e.g., person=2, car=1) published to
  individual label topics for use in HA automations
- Snapshot publishing runs in a background thread, fetching JPEG
  frames from go2rtc's snapshot API
- Motion timeout checking runs in a separate background thread
- All discovery messages are retained so HA picks them up on restart
- Thread-safe motion state tracking with pthread mutex

## Files Changed

- config/lightnvr.ini: Added HA discovery config documentation
- include/core/config.h: Added mqtt_ha_discovery, prefix, interval fields
- include/core/mqtt_client.h: Added 5 new public API functions
- src/core/config.c: Parse/save new config fields, added MQTT save section
- src/core/main.c: Initialize HA discovery after MQTT connection
- src/core/mqtt_client.c: Core implementation (~570 lines added)
- src/video/api_detection.c: Hook motion state into detection pipeline
- src/web/api_handlers_detection_results.c: Hook motion state into web API
Add the three Home Assistant MQTT auto-discovery settings to the
Settings page so they can be configured without editing lightnvr.ini.

Web UI changes (MQTT Event Streaming section):
- Enable HA Discovery checkbox
- Discovery Prefix text input (shown when enabled)
- Snapshot Interval number input (shown when enabled, 0-300s)

API backend changes (api_handlers_settings.c):
- GET /api/settings now returns mqtt_ha_discovery, mqtt_ha_discovery_prefix,
  mqtt_ha_snapshot_interval
- POST /api/settings now parses and validates all three fields with
  bounds checking (interval clamped to 0-300)

Files changed:
- src/web/api_handlers_settings.c: GET + POST handler additions
- web/js/components/preact/SettingsView.jsx: state, mappings, UI controls
Add mqtt_reinit() function that performs a full cleanup → reinit → connect →
HA discovery/services cycle. When any MQTT setting is changed via the web UI
POST /api/settings endpoint, a detached background thread is spawned to handle
the reinit asynchronously (same pattern as go2rtc hot-reload).

Changes:
- src/core/mqtt_client.c: Add mqtt_reinit() that resets the shutting_down flag
  after cleanup so callbacks work again, then re-initializes with updated config
- include/core/mqtt_client.h: Add mqtt_reinit() declaration and no-op stub for
  non-MQTT builds
- src/web/api_handlers_settings.c:
  - Add mqtt_settings_task_t struct and mqtt_settings_worker() background thread
  - Snapshot all 14 MQTT settings before parsing POST body
  - Compare old vs new values to detect changes
  - Spawn detached thread for async reinit when any MQTT setting changes

Supported hot-reload scenarios:
- Toggle MQTT enabled/disabled
- Change broker host, port, credentials, client ID
- Change topic prefix, TLS, keepalive, QoS, retain
- Toggle HA discovery on/off
- Change HA discovery prefix or snapshot interval
@matteius matteius marked this pull request as ready for review February 16, 2026 11:07
@matteius matteius changed the title Home assistant Home assistant--WIP Feb 16, 2026
@matteius matteius changed the title Home assistant--WIP feat: Home Assistant MQTT Auto-Discovery Integration Feb 16, 2026
…itized topics

- Add encoding: '' to camera discovery so HA handles raw JPEG binary
- Add origin block (name, sw_version, url) to all discovery payloads
- Publish initial OFF/0 states after discovery to avoid 'Unknown'
- Subscribe to HA birth topic and re-publish discovery on HA restart
- Remove hardcoded localhost from configuration_url
- Use sanitized stream names consistently in all MQTT state topics
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds native Home Assistant integration via MQTT auto-discovery to LightNVR. When enabled, cameras and sensors automatically appear in Home Assistant without manual YAML configuration or custom integrations. The implementation follows Home Assistant's MQTT discovery protocol and includes snapshot publishing, motion detection with debouncing, and per-object-class detection counting.

Changes:

  • Added three new MQTT configuration options: ha_discovery, ha_discovery_prefix, and ha_snapshot_interval
  • Implemented MQTT auto-discovery publishing for cameras, binary sensors (motion), and sensors (detection count)
  • Added two background threads: one for periodic snapshot publishing and one for motion timeout management
  • Integrated motion state tracking with existing detection code paths
  • Added hot-reload capability via mqtt_reinit() for settings changes without restart

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
web/js/version.js Version bump to 0.23.2
web/js/components/preact/SettingsView.jsx Added UI controls for HA discovery settings with conditional visibility
src/web/api_handlers_settings.c Added GET/POST handlers for HA settings and async MQTT reinit worker
src/web/api_handlers_detection_results.c Integrated motion state tracking hook
src/video/api_detection.c Integrated motion state tracking hooks in both detection paths
src/core/mqtt_client.c Core implementation: discovery, motion tracking, background threads, LWT, reinit (~620 new lines)
src/core/main.c Startup integration for HA discovery and services
src/core/config.c Config parsing, validation, and saving for new HA fields
include/core/version.h Version update to 0.23.2
include/core/mqtt_client.h API declarations for 6 new functions plus stubs
include/core/config.h Added 3 new config fields for HA discovery
config/lightnvr.ini Documented new HA discovery configuration options
CMakeLists.txt Version update to 0.23.2

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 592 to 593
cJSON_AddItemToObject(payload, "device", build_ha_device_block());
cJSON_AddItemToObject(payload, "origin", build_ha_origin_block());
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Memory leak if build_ha_device_block() or build_ha_origin_block() fails to allocate memory. If either function returns NULL, the payload cJSON object is deleted but the successfully allocated block (if any) is not freed before continuing to the next iteration.

For example, at line 592-593, if build_ha_device_block() succeeds but build_ha_origin_block() returns NULL, the device block is added to payload and then payload is deleted, which should free it. However, if build_ha_device_block() returns NULL, it's added to the payload as NULL, which may cause issues.

Consider checking return values before adding:

cJSON *device = build_ha_device_block();
cJSON *origin = build_ha_origin_block();
if (!device || !origin) {
    cJSON_Delete(device);
    cJSON_Delete(origin);
    cJSON_Delete(payload);
    continue;
}
cJSON_AddItemToObject(payload, "device", device);
cJSON_AddItemToObject(payload, "origin", origin);
Suggested change
cJSON_AddItemToObject(payload, "device", build_ha_device_block());
cJSON_AddItemToObject(payload, "origin", build_ha_origin_block());
cJSON *device = build_ha_device_block();
cJSON *origin = build_ha_origin_block();
if (!device || !origin) {
cJSON_Delete(device);
cJSON_Delete(origin);
cJSON_Delete(payload);
continue;
}
cJSON_AddItemToObject(payload, "device", device);
cJSON_AddItemToObject(payload, "origin", origin);

Copilot uses AI. Check for mistakes.
Comment on lines 641 to 643
cJSON_AddItemToObject(payload, "device", build_ha_device_block());
cJSON_AddItemToObject(payload, "origin", build_ha_origin_block());

Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Memory leak if build_ha_device_block() or build_ha_origin_block() fails to allocate memory. Same issue as with the camera entity above - NULL return values are not checked before adding to the payload. See comment on lines 592-593 for the suggested fix.

Suggested change
cJSON_AddItemToObject(payload, "device", build_ha_device_block());
cJSON_AddItemToObject(payload, "origin", build_ha_origin_block());
cJSON *device = build_ha_device_block();
if (!device) {
cJSON_Delete(payload);
continue;
}
cJSON_AddItemToObject(payload, "device", device);
cJSON *origin = build_ha_origin_block();
if (!origin) {
cJSON_Delete(payload);
continue;
}
cJSON_AddItemToObject(payload, "origin", origin);

Copilot uses AI. Check for mistakes.
Comment on lines 688 to 689
cJSON_AddItemToObject(payload, "device", build_ha_device_block());
cJSON_AddItemToObject(payload, "origin", build_ha_origin_block());
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Memory leak if build_ha_device_block() or build_ha_origin_block() fails to allocate memory. Same issue as with the camera entity above - NULL return values are not checked before adding to the payload. See comment on lines 592-593 for the suggested fix.

Suggested change
cJSON_AddItemToObject(payload, "device", build_ha_device_block());
cJSON_AddItemToObject(payload, "origin", build_ha_origin_block());
cJSON *device = build_ha_device_block();
if (device) {
cJSON_AddItemToObject(payload, "device", device);
}
cJSON *origin = build_ha_origin_block();
if (origin) {
cJSON_AddItemToObject(payload, "origin", origin);
}

Copilot uses AI. Check for mistakes.
Comment on lines 986 to 988
if (mqtt_config && mqtt_config->mqtt_ha_snapshot_interval > 0) {
pthread_join(ha_snapshot_thread, NULL);
}
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential issue with thread cleanup logic. When mqtt_ha_snapshot_interval is 0 (snapshots disabled), the snapshot thread is never created, but mqtt_stop_ha_services() will attempt to join it if mqtt_config->mqtt_ha_snapshot_interval > 0 at cleanup time. This creates a race condition:

  1. User enables HA discovery with snapshot interval 0 → no snapshot thread created
  2. User changes snapshot interval to 30 via web UI → mqtt_reinit() is called
  3. mqtt_reinit() calls mqtt_cleanup() → mqtt_stop_ha_services()
  4. At this point mqtt_config still has the OLD value (0), so snapshot thread is not joined
  5. But if the config was reloaded BEFORE cleanup, mqtt_config might have the NEW value (30), causing pthread_join on an uninitialized thread handle

The check should be based on whether the snapshot thread was actually started, not the current config value. Consider tracking whether each thread was started with a flag (e.g., snapshot_thread_started), or always join the motion thread unconditionally and only conditionally join the snapshot thread based on the flag.

Copilot uses AI. Check for mistakes.
Comment on lines 965 to 966
log_error("MQTT HA: Failed to create motion timeout thread");
ha_services_running = false;
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thread creation error handling is incomplete. If the snapshot thread is created successfully but the motion thread creation fails, ha_services_running is set to false, but the snapshot thread is left running without being cleaned up. This will cause a resource leak and the snapshot thread will run indefinitely.

Consider adding cleanup logic: if the motion thread creation fails, set ha_services_running = false first (so the snapshot thread will exit its loop), then join the snapshot thread before returning the error.

Suggested change
log_error("MQTT HA: Failed to create motion timeout thread");
ha_services_running = false;
log_error("MQTT HA: Failed to create motion timeout thread");
// Signal any already-started HA service threads to stop
ha_services_running = false;
// If the snapshot thread was started, wait for it to exit
if (mqtt_config && mqtt_config->mqtt_ha_snapshot_interval > 0) {
pthread_join(ha_snapshot_thread, NULL);
}

Copilot uses AI. Check for mistakes.
pthread_join(ha_snapshot_thread, NULL);
}
pthread_join(ha_motion_thread, NULL);

Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Motion state is not reset during MQTT cleanup/reinit. When mqtt_cleanup() is called (either during shutdown or during mqtt_reinit()), the motion states array retains old data. This could cause stale motion states to be used if:

  1. HA discovery is enabled with some streams
  2. Motion is detected and states are populated
  3. User disables then re-enables HA discovery (triggering mqtt_reinit)
  4. Old motion states persist

Consider resetting the motion state tracking when HA services stop. Add to mqtt_stop_ha_services():

pthread_mutex_lock(&motion_mutex);
num_motion_states = 0;
memset(motion_states, 0, sizeof(motion_states));
pthread_mutex_unlock(&motion_mutex);
Suggested change
// Reset motion state tracking when HA services stop to avoid stale states
pthread_mutex_lock(&motion_mutex);
num_motion_states = 0;
memset(motion_states, 0, sizeof(motion_states));
pthread_mutex_unlock(&motion_mutex);

Copilot uses AI. Check for mistakes.
- Add ha_snapshot_thread_started flag to track actual thread creation
- Use flag instead of config value for safe pthread_join in stop
- Clean up snapshot thread if motion thread creation fails
- Check NULL returns from build_ha_device_block/build_ha_origin_block
  in all 3 discovery payload locations to prevent memory leaks
@matteius matteius merged commit ed676cc into main Feb 16, 2026
1 of 5 checks passed
@matteius matteius deleted the home-assistant branch February 16, 2026 11:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

Comments