QNAP’s Notification Center can send alerts by email, SMS, app push, or Qmiix, but I’ve never been happy with any of those for my setup. Email parsing is flaky, Qmiix routes everything through QNAP’s cloud, and none of them fit how I actually consume notifications: as events inside Home Assistant, alongside everything else my homelab reports on. So I built a small relay that listens for QNAP’s own syslog output and republishes it to MQTT, which HA already speaks fluently. This post walks through the whole thing end to end: the QNAP config, the relay script, the systemd service, the debugging session that nearly derailed me, and the Home Assistant side that turns raw events into something actually useful.
Why not just use email or Qmiix
I tried the QNAP-native options first. Email integration into HA has always been the weak link in my setup, and I didn’t want to add IMAP polling just for NAS alerts. Qmiix looked promising as a generic “if this then that” layer for QNAP, and it’s free to use, but it’s cloud-routed: your NAS needs internet access and notifications bounce through QNAP’s cloud infrastructure before reaching a webhook. That’s a dependency I don’t want for something as basic as “tell me when an app updates.”
What I actually wanted was for the NAS to talk directly to my local network, the same way my Shelly sensors, GoodWe inverter, and Frigate NVR already do.
QNAP already speaks syslog
Buried in Control Panel → QuLog Center → Log Sender is a “Send to Syslog Server” tab. It’s easy to miss the neighboring “Send to QuLog Center” tab, which looks similar but uses QNAP’s own proprietary protocol over TLS instead — I initially configured the wrong one and spent a while wondering why nothing arrived. The Syslog Server tab lets you point QNAP at any UDP destination, RFC-3164 formatted, no QNAP cloud involved. The destination I configured:
- Destination IP: my MQTT broker LXC (192.168.1.9)
- Port: 1514 (kept off the privileged 514 so the relay doesn’t need root)
- Transfer protocol: UDP
- Format: RFC-3164
- Log type: Event Log (Access Log wasn’t something I needed for this)
A tiny relay on my MQTT container
I already run a dedicated LXC on Proxmox for MQTT and NTP (mqttbroker-ntp, 192.168.1.9), so it made sense to add a small Python relay there rather than stand up new infrastructure. The relay is a plain UDP listener using Python’s socketserver, paired with paho-mqtt. Here’s the full script:
#!/usr/bin/env python3
"""
QNAP syslog -> MQTT relay
Listens for QNAP QuLog Center syslog messages (UDP) and republishes
parsed fields to MQTT topics for Home Assistant to consume.
"""
import json
import logging
import re
import socketserver
import paho.mqtt.client as mqtt
# --- Configuration ---
UDP_HOST = "0.0.0.0"
UDP_PORT = 1514
MQTT_HOST = "localhost"
MQTT_PORT = 1883
MQTT_USER = None
MQTT_PASS = None
MQTT_TOPIC_BASE = "qnap/notifications"
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("qnap-syslog-mqtt")
LOG_PATTERN = re.compile(r'(conn log|event log): (.+)')
mqtt_client = mqtt.Client(
callback_api_version=mqtt.CallbackAPIVersion.VERSION2,
client_id="qnap-syslog-relay",
)
if MQTT_USER:
mqtt_client.username_pw_set(MQTT_USER, MQTT_PASS)
mqtt_client.connect(MQTT_HOST, MQTT_PORT, keepalive=60)
mqtt_client.loop_start()
def parse_qnap_line(raw: str) -> dict:
"""Parse a QNAP qlogd syslog line into a flat dict of fields."""
match = LOG_PATTERN.search(raw)
if not match:
return {}
log_type, fields = match.groups()
parsed = {"log_type": log_type.strip()}
for pair in fields.split(", "):
if ": " in pair:
key, _, value = pair.partition(": ")
parsed[key.strip().lower().replace(" ", "_")] = value.strip()
return parsed
class SyslogUDPHandler(socketserver.BaseRequestHandler):
def handle(self):
data = bytes.decode(self.request[0].strip(), errors="replace")
log.info("RAW: %s", data)
parsed = parse_qnap_line(data)
if not parsed:
log.debug("Unparsed line, skipping: %s", data)
return
category = parsed.get("category", parsed.get("log_type", "misc"))
topic = f"{MQTT_TOPIC_BASE}/{category.lower().replace(' ', '_')}"
payload = json.dumps(parsed)
mqtt_client.publish(topic, payload, qos=0, retain=False)
mqtt_client.publish(f"{MQTT_TOPIC_BASE}/last", payload, qos=0, retain=True)
log.info("Published to %s: %s", topic, payload)
# Separate retained topic for firmware-related events
is_config_change = parsed.get("application", "").lower() == "notification center"
haystack = " ".join([
parsed.get("category", ""),
parsed.get("application", ""),
parsed.get("content", ""),
]).lower()
firmware_keywords = ["firmware update", "new firmware", "qts update", "system update available"]
matched = any(kw in haystack for kw in firmware_keywords)
if matched and not is_config_change:
fw_topic = f"{MQTT_TOPIC_BASE}/firmware_update"
mqtt_client.publish(fw_topic, payload, qos=0, retain=True)
log.info("Published firmware event to %s: %s", fw_topic, payload)
if __name__ == "__main__":
log.info(
"Starting QNAP syslog relay on %s:%s -> MQTT %s:%s",
UDP_HOST, UDP_PORT, MQTT_HOST, MQTT_PORT,
)
with socketserver.UDPServer((UDP_HOST, UDP_PORT), SyslogUDPHandler) as server:
server.serve_forever()
A couple of details worth calling out: the topic name is derived straight from QNAP’s own category field, so new categories automatically get their own topic without any code changes on my end. And the firmware-detection block specifically excludes anything where the application field is “Notification Center” — without that exclusion, simply editing or creating a notification rule about firmware would falsely trigger the firmware topic, since the word “firmware” appears in the rule’s own name.
Running it as a proper service
The relay runs under systemd on the LXC as a dedicated non-root user:
[Unit]
Description=QNAP Syslog to MQTT Relay
After=network.target mosquitto.service
[Service]
Type=simple
WorkingDirectory=/opt/qnap-syslog-relay
ExecStart=/opt/qnap-syslog-relay/venv/bin/python3 /opt/qnap-syslog-relay/syslog_mqtt_relay.py
Restart=on-failure
RestartSec=5
User=qnaprelay
[Install]
WantedBy=multi-user.target
Since everything on this LXC is managed from the Proxmox host via pct, I deployed both files with pct push straight from the host shell rather than setting up separate SSH access into the container. The full deployment, run entirely from the Proxmox host:
pct exec 109 -- mkdir -p /opt/qnap-syslog-relay
pct push 109 /tmp/syslog_mqtt_relay.py /opt/qnap-syslog-relay/syslog_mqtt_relay.py
pct push 109 /tmp/qnap-syslog-relay.service /etc/systemd/system/qnap-syslog-relay.service
pct exec 109 -- bash -c "apt update && apt install -y python3-pip python3-venv"
pct exec 109 -- bash -c "cd /opt/qnap-syslog-relay && python3 -m venv venv && ./venv/bin/pip install paho-mqtt"
pct exec 109 -- bash -c "id -u qnaprelay &>/dev/null || useradd -r -s /usr/sbin/nologin qnaprelay"
pct exec 109 -- chown -R qnaprelay:qnaprelay /opt/qnap-syslog-relay
pct exec 109 -- systemctl daemon-reload
pct exec 109 -- systemctl enable --now qnap-syslog-relay
pct exec 109 -- systemctl status qnap-syslog-relay --no-pager
Proving the pipe works before trusting the NAS
Before relying on a real QNAP event, I confirmed the relay itself worked by injecting a fake syslog line straight from the Proxmox host with nc:
echo "<14>Aug 1 12:00:00 testhost qlogd[1]: event log: Users: test, Source IP: 1.2.3.4, Application: Test, Category: Test, Content: manual test" | nc -u -w1 192.168.1.9 1514
Watching the live log confirmed it end to end:
pct exec 109 -- journalctl -u qnap-syslog-relay -f
That test also proved something important: the relay, MQTT broker, and network path were all fine well before the real NAS traffic ever worked, which narrowed the eventual bug down to just the QNAP-side config.
What the NAS actually sends
QNAP’s qlogd daemon emits lines like this over syslog:
qlogd[14049]: event log: Users: rutger2, Source IP: 192.168.1.29, Computer name: localhost, Application: App Center, Category: App Installation, Content: [App Center] Uninstalled Text Editor 1.1.5.
That comma-separated Key: Value structure is trivial to parse into a dictionary, which then maps cleanly onto an MQTT JSON payload — category becomes part of the topic, and the rest becomes payload fields HA can read as attributes:
{
"log_type": "event log",
"users": "rutger2",
"source_ip": "192.168.1.29",
"computer_name": "localhost",
"application": "App Center",
"category": "App Installation",
"content": "[App Center] Uninstalled Text Editor 1.1.5."
}
The one bug that cost me an hour
Everything looked right — service running, no errors, MQTT broker reachable — but nothing arrived from the NAS. I worked through the usual suspects: wrong QuLog tab, wrong transport protocol, a nonexistent Proxmox firewall rule (there was no iptables even installed in the container, so that was ruled out fast). The manual nc injection proved the relay itself worked perfectly end to end. The actual problem turned out to be embarrassingly simple: the destination IP in QNAP’s syslog config read 192168.1.9 instead of 192.168.1.9 — a missing dot, silently swallowed because malformed IPs just don’t route anywhere rather than throwing a visible error. Once fixed, real events started flowing immediately.
Cleaning up the MQTT client warning
The first version threw a DeprecationWarning about the paho-mqtt callback API on every start. Since the script only calls .connect() and .publish() directly and doesn’t define any callbacks, the fix was a one-line change to opt into the v2 callback API:
mqtt_client = mqtt.Client(
callback_api_version=mqtt.CallbackAPIVersion.VERSION2,
client_id="qnap-syslog-relay",
)
Chasing down firmware update notifications
One category I specifically wanted was firmware update availability. That turned out to be its own small investigation. Clicking “Check for Updates” manually in the QNAP UI doesn’t generate a loggable event at all — I confirmed this by checking QuLog Center’s own Event Log immediately after clicking it, and nothing showed up. Only the scheduled automatic check, paired with an active Notification Center rule set to Info and Warning severity, actually produces something the syslog pipeline can catch:
- Control Panel → System → Firmware Update → Update Settings: “Notify me, do not automatically update”, scheduled Daily at 01:00
- Notification Center → Alert Notification Rules: an active rule covering Firmware Update events, with Info and Warning severities enabled — QNAP’s own settings page explicitly notes that without both of those severities checked, a routine “update available” notification gets filtered out before it’s ever logged
Since my TS-253D is already on relatively recent firmware, I haven’t yet seen a real “update available” event land, so the exact keyword matching in the relay is a reasonable best guess rather than a confirmed match. The pipeline and the daily scheduled check are both proven and running, so whenever new firmware actually ships for this model, it’ll get picked up automatically.
Routing events in Home Assistant, not just logging them
With events landing reliably, the temptation is to fire a mobile notification on every single one — but a NAS generates a lot of routine noise (app installs, rule edits, scheduled checkups). Instead I built one routing automation that subscribes to the wildcard topic qnap/notifications/+, always writes the event into my existing MySQL logbook, and only escalates to a persistent notification and mobile push for a curated set of categories.
A simple sensor for dashboard visibility, tracking the most recent event regardless of category:
mqtt:
sensor:
- name: "QNAP Last Event"
unique_id: qnap_last_event
state_topic: "qnap/notifications/last"
value_template: "{{ value_json.content }}"
json_attributes_topic: "qnap/notifications/last"
icon: mdi:nas
- name: "QNAP Firmware Update"
unique_id: qnap_firmware_update
state_topic: "qnap/notifications/firmware_update"
value_template: "{{ value_json.content }}"
json_attributes_topic: "qnap/notifications/firmware_update"
icon: mdi:chip
And the routing automation itself:
automation:
- alias: "QNAP Notification Router"
trigger:
- platform: mqtt
topic: "qnap/notifications/+"
condition:
- condition: template
value_template: "{{ trigger.topic.split('/')[-1] != 'last' }}"
variables:
category: "{{ trigger.payload_json.category | default('Unknown') }}"
application: "{{ trigger.payload_json.application | default('Unknown') }}"
content: "{{ trigger.payload_json.content | default('') }}"
users: "{{ trigger.payload_json.users | default('---') }}"
push_worthy: >
{{ category in ['Security Center', 'System', 'Firmware Update',
'Malware Remover', 'VPN', 'Antivirus']
or 'missed' in content | lower
or 'failed' in content | lower }}
action:
- service: rest_command.logbook_write
data:
date: "{{ now().strftime('%Y-%m-%d %H:%M:%S') }}"
category: "QNAP - {{ category }}"
entry: "{{ content }}"
- if:
- condition: template
value_template: "{{ push_worthy }}"
then:
- service: persistent_notification.create
data:
title: "QNAP: {{ category }}"
message: "{{ content }}"
- service: notify.mobile_app_my_phone
data:
title: "QNAP Alert"
message: "{{ content }}"
Everything still lands in the logbook for full historical record, but my phone only buzzes for things that actually deserve attention — security events, system-level issues, malware/antivirus activity, or anything whose content mentions being “missed” or “failed”. The category allowlist and keyword list in push_worthy are the two places I’ll tune over time as I see what’s genuinely worth a push versus what’s just routine housekeeping noise.
Verifying it live
MQTT Explorer made it easy to watch events land in real time under the qnap/notifications tree, and from the command line a simple wildcard subscription does the same job:
mosquitto_sub -h 192.168.1.9 -t 'qnap/notifications/#' -v
Where this leaves things
The relay has been running cleanly since I deployed it, catching everything from app installs to Notification Center rule changes in real time, with zero dependency on QNAP’s cloud services or my flaky email integration. It’s a small piece of infrastructure, but it fits the pattern I’ve built everywhere else in this homelab: local-first, MQTT as the common language, and Home Assistant as the single place I actually look for the state of my house and my hardware.
Related Posts
May 30, 2026
Monitor QNAP Updates in Home Assistant
May 28, 2026
Monitor WordPress Updates in Home Assistant
October 20, 2025


