Hacking a Lamp

Using ESP Zerocode to add Matter connectivity to a “dumb” lamp

May 22, 2024

The Problem

Living in Portland means I don’t get to see the sun very often. To try and combat this a bit, I got a full-spectrum “sunlight lamp” to try and get some more light into my room during the winter.
The issue was that it used touch controls on the base, and was really designed to be a desk lamp. It didn’t fit on my desk, but I thought it’d be nice to use as a replacement for the ancient florescent room light on the ceiling.
Because of this, it would be nice to be able to control it remotely (kinda a requirement in some way if it’s on the ceiling) - and it’d be perfect if I could integrate it into my smart home setup. Additionally, this would let me use it alongside my other lights as a sort of alarm clock light, integrating it into my smart home routines to help me wake up as if the sun was rising. Rather than buying new hardware, I decided to try to retrofit the lamp with an esp32 to turn it into a matter-controlled smart home device.

Signaling

The first step was figuring out how the lamp brightness was controlled. Opening it revealed that the capacitive buttons on the outside were connected to a small daugther board, and that was connected with three wires to the main lamp power supply. This meant I couldn’t just wire in a digital potentiometer as a dimmer, but did mean that it would hopefully be easy to reverse engineer.
I used a handheld oscillocsope to probe the control lines going to the main board. Luckily enough, the control signal was just a PWM waveform. This made the rest of the project much simpler.
I initially spent a bit of time building a voltage divider to level-shift the PWM signal, Turns out that the lamp had one built into the driver circuit all along! I wasted a couple hours there, but I was happy the fix was easy.
notion image

Testing

Before committing to making it a smart home device, I wanted to test everything with an Arduino. This let me verify that everything was working, and figure out exactly what the lamp needed to power on.
You might be able to see from the oscilloscope picture that the PWM frequency of the signal was 1225 Hz, which is higher than the Arduino’s default frequency. This meant I needed to drive the timer directly, since the Arduino’s PWM timer runs at 490hz by default. This meant writing to the timer registers directly, so a standard AnalogWrite wouldn’t work. This means my peak value would be 16 MHZ / 1225 = 13,061, in theory.
Getting the frequency right took some back-and-forth. When you configure Timer1 directly, the frequency is determined by a "top" value (ICR1) that the counter counts up to before resetting. The higher the top value, the lower the frequency, according to:
ICR1 = f_clk / (2 × N × f_target) (link)
ICR1 = 16,000,000 / (2 × 1225) - 1 ~= 6508
The 2 in the denominator accounts for the timer counting up and back down. The formula suggested an ICR1 value of 6508, but that produced the wrong frequency on the actual hardware. Doubling it to 13016 gave the correct 1225 Hz output. I'm not entirely sure why, but I verified that the Arduino was outputting a 1225 Hz signal and moved on, since this was just a test.
With a bit of trial and error, I found that a range from 230 to 13016 worked for a lower/upper limit, with 230 being just enough for the LED driver to turn on the lamp from cold, and the lamp not getting brighter about 13016. This is the code snippet the mapped the encoder value to an output.
void LED(int pwm_request) { OCR1A = map(pwm_request, PWMMIN, PWMMAX, LEDMIN, LEDMAX); // Set duty cycle for LED pin (9) }
pwm_request
pwm request based on the rotary encoder
ROTARYMIN,
0
ROTARYMAX
255 (encoder input range)
LEDMIN
230 - the minimum value that I found was enough for the lamp to turn on
ICR1
the max pwm frequency, which I ended up setting to 13016
PWM_REQUEST was the value read from the rotary encoder. The function is called as LED(tracked encoder value)

It works!

notion image

ESP32 & Espressif ZeroCode

Now that brightness control worked using the Arduino, it was safe to move on to the smart home device config.
For this, I chose Espressif ESP Zerocode, which is a platform for creating matter-compatible smart home devices with simple configuration, rather than code. It allows you to create and flash firmware online, right from the configurator.
I used a Seeed Studio esp32-c3 board for this. I’d used Seeed’s other products in the past and liked them, and this one also had the benefit of an an external antenna that could be stuck to the inside of the housing. The external antenna was important, as the wifi signal in my room of the house is not very strong.
 
notion image
In the ZeroCode config, I set the PWM output to pin 8 at 1225 Hz to match the frequency I'd already validated. ZeroCode natively handles scaling brightness percentages to PWM outputs at the correct frequency, so I ended up setting the minimum brightness (white_min) to 10, rather than the 230 (out of 13016) I'd used with the Arduino. A bit of trial and error got it to a point where the lamp would reliably turn on from cold without flickering.
 
Once the device was working in Home Assistant, I ran into a weird issue with transition times. Setting different transition times in automations (or manually) had no effect - every transition took roughly the same time, and a weird one at that:
Requested transition time
Actual time
1s
38s
2s
38s….
5s
…. also 38s?
I was a bit confused at this point. It seemed that the transition time wasn’t being respected by Home Assistant. After digging into it, it turns out that there was a known bug between Matter’s transition time and Home Assistant’s implementation, and that this was common for both custom and first-party devices. Telling the lamp to take time between transitions seemed to make it take time to transition between every step commanded to it, instead of the whole transition. Or something like that. Eventually, I decided to just not use it. I could easily work around it with automations, and the lamp worked well outside of that! was happy with that tradeoff for something that would just be turning on slowly as an “alarm clock” in the morning.

Working Lamp!

notion image
 

Case

I don’t have a picture of the finished product, but I also made a case for the electronics to live in using Fusion360. it has a hole for the light wires to come out, as well as a spot for the ESP’s usb port in case I need to re-program it in the future.
notion image
I printed it out of ABS to withstand the mild head from the power supply, and mounted it to the back of the lamp housing.

Takeaways

Overall, this was a fun, quick project that I completed in a few days. I learned a lot about Matter/ESP-based smart home devices, as well as dealing with non-standard PWM frequencies in Arduino. The lamp lived on my ceiling until a moved to an apartment with more natural light, but I’ve kept it around if I ever need it again.