Sentinel

In September 2023, I started researching into how devices communicate via radio frequencies. Around this time, I learned that a lot of security sensors communicate in 433 MHz frequency to send events like motion detection, sensors opening or closing, and other events. I thought it would be a fun project to try to implement my own system to detect my garage door opening and closing, and through this journey I learned a lot about how this communication works.

I purchased a generic 433 MHz contact sensor from AliExpress in hopes that I could receive the events. Unfortunately, the original listing of this product has been taken down, but here is an image:

screenshot of the purchased sensor

The way these kind of sensors work is fairly simple. The smaller part of the sensor is simply a magnet, while the larger part of the sensor typically contains a reed switch, which can sense whether the magnet is in contact with the sensor or not. When the point of entry is opened or closed, the sensor emits an event of the new state, and a separate gateway device typically handles the event.

Since this device communicates in 433 MHz frequency, I needed a receiver capable of receiving the events from a bit of a distance. I originally purchased a very cheap set of modules, however the range was not great at all and often didn’t detect the events sent by the sensor. I researched about it online, and found out that superheterodyne sensors have a much better detection range than cheaper modules, so I ended up purchasing the following kit on Amazon.

screenshot of the purchased kit

Since I already had an ESP32, which is microcontroller capable of Wi-Fi functionality, I decided to go with it as the main gateway device for handling the events. Once I had all of my sensors, I had a basic idea of the communication down:

  1. The contact sensor sends an event when the garage door is opened or closed.
  2. The ESP32 receives and parses the event from the contact sensor, then sends an API request to a web server containing the event.
  3. The API server sends a notification to my mobile phone containing the event.

Protocol

The events from the contact sensor is easily received by the RCSwitch Arduino library, and the structure of each event is simple:

  • The first 16 bits of each event contains the unique Serial ID of the contact sensor. This is often used to validate that events are coming from the right device, as other devices can and tend to interfere on the same frequency.
  • The next 4 bits are always null. (0)
  • The next 4 bits indicate the type of event. It is likely that each bit represents specific flags, but I have to do further research to confirm this:
1010 - OPEN
1110 - CLOSE
0100 - TAMPER
0110 - LOW BATTERY

Programming

I wrote a program for my ESP32 that automatically connects it to my Wi-Fi network with automatic reconnection, and listens for events from the contact sensor. Below is a function responsible for parsing the buffer received from the sensor:

bool parse_data(uint data) {
    // extract serial and state
    uint serial = (data & 0xffff00) >> 8;
    uint state = data & 0xff;

    // ensure serial matches
    if (serial == 29478) {
        // send a web request containing the event
        send_request("event", "state=" + String(state) + "&count=" + String(bufferIndex));
        return true;
    }

    return false;
}

The “bufferIndex” is a global variable for keeping track of the amount of packets received for debug purposes.

For the web server, I have a route powered by Express that listens for an event and sends a request to Pushover to send a push notification straight to my phone:

const States = {
	0xa: 'opened',
	0xe: 'closed'
};

app.post('/event', (req, res) => {
	// validate address
	const address = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
	if (address != process.env.WHITELIST) {
		return;
	}

	// validate authorization secret
	const auth = req.headers['authorization'] || 'Basic NONE';
	const secret = auth.split(' ')[1];
	if (secret != process.env.SECRET) {
		return;
	}

	// parse state and packet count
	const state = parseInt(req.body.state);
	const count = parseInt(req.body.count);

	// parse state message
	const stateMessage = States[state];
	if (!stateMessage) {
		console.log(`Unknown state: ${state} | ${count} packets received`);
		return res.send('OK');
	}

	// send push notification
	sendNotification({
		message: `Garage door ${stateMessage}`,
		title: 'Sensor Detection',
		priority: 0,
		count
	});

	// acknowledge
	res.send('OK');
});

I ensure that every request is from a whitelisted IP address and contains a secret key to prevent anyone from finding my web server and sending events, just to be safe. I don’t think I would want thousands of push notifications saying my garage is opening and closing!

Installation

The installation was simple, with the contact sensor (in white above the black attachment) being placed along one of the moving parts of the garage door. I then placed the ESP32 with the RF receiver upstairs directly above the garage door so it can optimally receive the events.

picture of the garage door picture of the ESP32 gateway

Usage

As of June 30th, 2025, I still have this system running to this day, and it has worked well! Below is a screenshot of the notifications I receive when my garage door is opened or closed:

screenshot of the sensor notifications

Looking back on it, an improvement I could make to my project is cutting out the web server portion altogether, and instead sending a request to Pushover directly from the ESP32. I’m really happy with how this project turned out, and I look forward to working with more radio frequency devices in the future!