When I set up the LED strip around my living room window, I thought the little IR remote it came with was fine. Point, click, color changes. Simple.
Then I lost the remote in the couch cushions for the third time and decided this was now an engineering problem.
The LED strip itself is a 120V outdoor-rated, waterproofed unit. Cracking into the controller to directly hack the hardware was off the table, I like my apartments un-scorched and my fingers un-electrocuted. But the IR remote? That’s just blasting NEC-encoded signals into the void. Nothing stopping me from doing the same thing with an ESP8266 and a cheap IR LED.
So that’s exactly what I did. I built a small networked device that sits near the LED strip controller, listens for HTTP requests over Wi-Fi, and fires off the corresponding IR commands. No cloud service required. No Alexa integration (yet). Just a clean REST API that lets me (or anything on my network) control the lights.
Hardware
The hardware side of this project is refreshingly simple compared to some of my other hacks.
- ESP8266 (NodeMCU) as the usual suspect for Wi-Fi-enabled microcontroller projects
- IR LED on pin D2 with a 10-ohm series resistor
- Status LED on pin D6 with a 1k-ohm resistor to ground
That’s basically it. The status LED blinks every 500ms to let me know the device is alive and connected. No MOSFETs, no signal extraction, no stuffing a custom PCB into an existing enclosure. Just point the IR LED at the LED strip’s receiver and go.
The boot pin configuration (D3, D4, D8) follows the standard ESP8266 pull-up/pull-down setup. Nothing fancy but worth documenting so future-me doesn’t spend an hour wondering why the board won’t flash.
IR Protocol
The LED strip’s remote uses NEC encoding, which is one of the more common IR protocols out there. Each command is a 32-bit transmission with a fixed address byte (0x00) and a variable command byte.
I mapped out every button on the remote to its corresponding hex code:
| Function | Command | HEX |
| Power On | ON | 0x07 |
| Power Off | OFF | 0x06 |
| Brightness Up | UP | 0x05 |
| Brightness Down | DOWN | 0x04 |
| White | COLOR | 0x08 |
| Red | COLOR | 0x09 |
| Green | COLOR | 0x0C |
| Blue | COLOR | 0x10 |
| Flash | FUNCTION | 0x0F |
| Strobe | FUNCTION | 0x17 |
| Fade | FUNCTION | 0x13 |
| Smooth | FUNCTION | 0x1B |
There are 16 colors total (including some fun ones like “pea green” and “dark orchid”) and four special functions. The IRremoteESP8266 library handles all the heavy lifting for encoding and transmission.
void led_send_value(uint8_t value) {
irsend.sendNEC(irsend.encodeNEC(LED_ADDRESS, value));
}
One line. Beautiful.
REST API
Here’s where the project gets interesting. Rather than just replicating the remote’s buttons, I built a proper REST API that maps each control surface to its own endpoint. The ESP8266 runs a lightweight HTTP server on port 80 with the following routes:
- GET / : Returns API documentation and available routes
- POST /power?power=on|off : Power control
- POST /color?color=<name> : Set color (16 options)
- POST /brightness?brightness=up|down : Adjust brightness (9 steps)
- POST /function?function=flash|strobe|fade|smooth : Special effects
- POST /raw?raw=<byte> : Send raw NEC command (for debugging)
- GET /cached-state : Returns last commanded values
Every endpoint validates its input and returns a plain-text status message. All endpoints support both GET and POST and have a documentation=true parameter that returns usage information. I wanted this to be something I could poke at with curl from a terminal without needing to remember anything.
curl -X POST "http://window-leds.local/color?color=blue"
That’s it. Blue window. Done.
State Caching
The service maintains a cache of the last commanded values for power, color, brightness, and function mode. You can query it at /cached-state to see what the device *thinks* the LED strip is doing.
The key word there is “thinks.” IR communication is one-way. The LED strip has no way to report its actual state back to the ESP8266. If someone walks up and hits a button on the physical remote (assuming they found it in the couch cushions; thank you roommate), the cache goes stale and the service has no idea.
This is an acknowledged limitation and honestly, I’m fine with it. The physical remote still works alongside the network control. I didn’t want to break that compatibility; it’s one of the reasons I went with IR replication instead of hacking the controller directly.
Network Setup
For Wi-Fi, I used the WiFiManager library. On first boot, the ESP8266 creates an access point called “Window_LEDs_AP” with a captive portal. Connect to it, enter your Wi-Fi credentials, and you’re done. It remembers the network and auto-connects on subsequent boots.
I also enabled mDNS so the device registers itself as window-leds.local on the network. This means I don’t need to remember or look up the IP address every time my router decides to shuffle things around. Just http://window-leds.local and I’m in.
void station_start() {
wifiManager.autoConnect(AP_NAME, AP_PASS);
if (MDNS_ENABLED) {
MDNS.begin(MDNS_HOSTNAME);
}
rest_service_start();
}
There’s also some connection monitoring logic. If the Wi-Fi drops, the station module detects it, tears down the REST service, flips back to AP mode for reconfiguration, and restarts everything when connectivity returns. Nothing groundbreaking but it means I don’t have to physically reset the device when the router hiccups.
Architecture
The codebase is organized into five modules, each with a clear responsibility:
- LED-Service.ino : Entry point and main loop
- HardwareConfig.h : GPIO pin definitions and hardware initialization
- LEDStrip.h/cpp : IR protocol layer (NEC encoding, command mapping)
- RestService.h/cpp : HTTP server and API endpoint handlers
- Station.h/cpp : Wi-Fi management, mDNS, service lifecycle
- NetworkConfig.h : Port, hostname, and AP configuration
I’m pretty happy with how cleanly the concerns separated out here. The REST service doesn’t know anything about NEC encoding. The IR module doesn’t know anything about HTTP. The station module just manages connectivity and delegates everything else.
Quirks and Findings
A few things I discovered along the way that are worth noting:
Brightness is context-dependent. When the strip is in a static color mode, the brightness up/down commands adjust LED intensity across 9 steps. But when you’re in one of the special function modes (flash, strobe, fade, smooth), the same brightness commands adjust the *transition speed* instead. The API handles both cases transparently.
Special functions have hidden behaviors. Pressing the same function button twice does something different than pressing it once:
- Flash twice = Strobe effect
- Strobe twice = Smooth fade through brightness
- Fade twice = RGB single-color cycling
- Smooth twice = RGB single-color flashing
I documented these in the code comments but didn’t expose them as separate API endpoints. The /function endpoint just fires the command — if you want the double-press behavior, hit it twice.
Final Thoughts
This project slots into my broader home automation setup as one of those “just works” pieces. No cloud dependency, no monthly subscription, no app to install. Any device on my local network can control the window LEDs with a simple HTTP request. A curl command from my laptop or a quick shortcut on my phone does the job.
It was one of those rare projects where the hardware was trivial and the software was straightforward and the whole thing just… worked. No teardown photos of a destroyed enclosure. No signal extraction detective work. No MOSFET trickery. Just an IR LED, an ESP8266, and a REST API.
Sometimes the best projects are the ones where the problem is simple and the solution matches. I lost a remote, so I replaced it with an entire web server. As one does.
You can find the source code on GitHub.
Thanks for reading. Stay tuned and keep building.