X-Sense Link+ (SC07-W) Integration via rtl_433 and MQTT

I recently managed to integrate my seven interconnected X-Sense SC07-W smoke/CO detectors into Home Assistant. This required a generic RTL-SDR and a few hours of reverse-engineering the RF protocol.

These sensors operate on the 915MHz band and use GFSK modulation. Below are the configurations to get these working as a binary sensor in HA.

1. rtl_433 Configuration

/config/rtl_433/xsense.conf.template:

Note on Matching: The match line below is “broad” to help you find your specific Network ID. Once you identify your 24-bit house code (the 6 hex digits following 5898), you can update the match to {104} and append your hex code to ignore neighboring systems.

# Disable all built-in decoders to save CPU
protocol -all

# Frequency and Sample Rate
# https://fcc.report/FCC-ID/2AU4DDBM/
frequency 915.275M
sample_rate 250k

# X-Sense SC07-W Protocol Decoder
# ---------------------------------------------------------
# Physical Layer: GFSK, 915.275 MHz, 10.0 kbps (100 us pulse width)
#
# Packet Anatomy (Total ~137 bits):
# [Preamble]  0x5555555555555554 - alternating-bit preamble + 0100 start frame delimiter
# [Bytes 0-1] Protocol header: 0x5898 - X-Sense Link+ manufacturer signature
# [Bytes 2-4] Network ID: 24-bit house code, programmed at pairing time
# [Byte 5]    Flags. Bits 7-1 are constant (0xbe base). LSB: 0=normal, 1=button active
# [Byte 6]    Message type:
#   0x00 (0)   = Decode artifact - anomalous flag; discard
#   0x05 (5)   = Standby - network resting state after a real smoke event clears
#   0x0d (13)  = Standby - network resting state after a test button event clears
#   0x17 (23)  = Alarm relay - peer sensor echoing an alarm trigger
#   0x19 (25)  = Alarm relay - secondary relay variant
#   0x1f (31)  = Alarm test source - test button long-press
#   0x23 (35)  = Pair request - sensor requesting to join the network
#   0x25 (37)  = Pair accept - network acknowledging a new sensor
#   0x82 (130) = Alarm smoke source - REAL smoke detection
#   0xc0 (192) = Button active - NOT mapped Safe; appears during active alarms
#   0x?? (??)  = CO alarm - not yet captured; failsafe ON covers any unmapped type
# [Byte 7]    CRC-8 over bytes 0-6: poly=0x07, init=0xb2
# [Tail]      Padding bits
decoder {
    name=XSense,
    modulation=FSK_PCM,
    short=100,
    long=100,
    reset=5000,
    match={80}0x55555555555555545898,
    get=net_id:@80:{24},
    get=flag:@104:{8},
    get=msg_type:@112:{8},
    get=event:@112:{8}:[0:Decode_Error 5:Standby 13:Standby 23:Alarm_Relay 25:Alarm_Relay 31:Alarm_Test 35:Pair_Request 37:Pair_Accept 130:Alarm_Smoke 192:Button_Active],
    get=alarm_logic:@112:{8}:[0:Safe 5:Safe 13:Safe 35:Safe 37:Safe]
}

output mqtt://${host}:${port},user=${username},pass=${password},retain=${retain}
report_meta time:iso:usec:tz
output kv

2. Home Assistant MQTT Configuration

Add the following to your mqtt.yaml (ensure you have mqtt: !include mqtt.yaml in your configuration.yaml).

This setup creates a single “Network” device in HA. It uses a Failsafe approach: any message type not explicitly mapped to “Safe” (like a real smoke event 130 or an uncaptured CO event) will trip the binary sensor to ON.

binary_sensor:
  - name: "X-Sense House Alarm"
    unique_id: "xsense_house_alarm_binary"
    state_topic: "rtl_433/+/devices/XSense/rows/0/alarm_logic"
    device_class: smoke
    off_delay: 60
    device: &xsense_dev
      identifiers: ["xsense_network"]
      name: "X-Sense Smoke Alarm Network"
      manufacturer: "X-Sense"
      model: "SC07-W / Link+"
    value_template: >-
      {% if value == 'Safe' %}
        {# If HA just rebooted (unknown), use the standby to set it to OFF #}
        {# Otherwise, IGNORE standby events so they don't overwrite an active alarm #}
        {% if this is not defined or this.state in ['unknown', 'unavailable'] %}
          OFF
        {% else %}
          IGNORE
        {% endif %}
      {% else %}
        {# FAILSAFE: Anything else is Real Smoke, Test, or Relay #}
        ON
      {% endif %}
sensor:
  - name: "X-Sense Network Status"
    unique_id: "xsense_house_status_text"
    state_topic: "rtl_433/+/devices/XSense/rows/0/event"
    icon: mdi:smoke-detector-variant
    device: *xsense_dev
    entity_category: diagnostic
    value_template: >-
      {% set event = value | string | replace('_', ' ') %}
      {% if event.isdigit() %}
        Unknown ({{ event }})
      {% else %}
        {{ event }}
      {% endif %}
  - name: "X-Sense Last Communication"
    unique_id: "xsense_house_last_comms"
    state_topic: "rtl_433/+/devices/XSense/time"
    device_class: timestamp
    entity_category: diagnostic
    device: *xsense_dev
    value_template: "{{ value }}"
  - name: "X-Sense Flag"
    unique_id: "xsense_house_flag"
    state_topic: "rtl_433/+/devices/XSense/rows/0/flag"
    device: *xsense_dev
    entity_category: diagnostic
    value_template: "{{ '0x%02x' % (value | int) }}"
  - name: "X-Sense Raw Message Type"
    unique_id: "xsense_house_msg_raw"
    state_topic: "rtl_433/+/devices/XSense/rows/0/msg_type"
    device: *xsense_dev
    entity_category: diagnostic
    value_template: "{{ value }}"

How to customize for your specific Network ID

To avoid picking up neighbors, first run the config above. Long-press the test button on one of your units and check the rtl_433 logs to find your 24-bit ID.

Once found, update your decoder match line:

  • Original: match={80}0x55555555555555545898,
  • Locked: match={104}0x55555555555555545898FFFFFF, (Replace FFFFFF with your hex code).

Automation to remind you to test the sensors

alias: "Safety: X-Sense 6-Month Manual Test Reminder"
description: >-
  Reminds you to press the test button on the X-Sense smoke alarms if  it has
  been over 180 days since the last RF packet was received.
triggers:
  - trigger: time
    at: "09:00:00"
  - trigger: homeassistant
    event: start
conditions:
  - condition: template
    value_template: >-
      {% set last_comms =
      states('sensor.x_sense_smoke_alarm_network_x_sense_last_communication') %}
      {% if last_comms not in ['unknown', 'unavailable', 'none'] %}
        {{ now() - (last_comms | as_datetime) > timedelta(days=180) }}
      {% else %}
        False
      {% endif %}
actions:
  - action: persistent_notification.create
    data:
      title: ⚠️ Smoke Alarm Test Required
      message: >
        It has been over 6 months since Home Assistant received a signal from
        your X-Sense smoke alarm network. 

        Please press the Test button on any unit. This will verify the mesh
        network is working, confirm the RTL-SDR is still receiving RF data, and
        automatically reset this 6-month countdown timer.
      notification_id: smoke_alarm_test_required
mode: single