Community project

Physical Light Switch Control

ESP32
Photo of Physical Light Switch Control
Generated with AI

Trevor Hinkle

Published August 30, 2026

This project turns physical pushbuttons into wireless controls for Philips Hue smart lights. An ESP32 microcontroller connects to a Hue Bridge over WiFi, letting two momentary tactile buttons toggle individual lights on and off with a simple press.

The guide provides a complete wiring diagram, parts list, and step-by-step assembly instructions for connecting the buttons to the ESP32. Firmware handles debouncing, WiFi configuration through a setup portal, and communication with the Hue system, so builders can have physical light control up and running without writing code.

Wiring diagram

Wiring diagram for Physical Light Switch Control

Gather all the parts

QtyComponent
1

Momentary tactile pushbutton

Momentary, normally open

A small pushbutton that tells the controller to turn the first Hue light on or off.

1

Momentary tactile pushbutton

Momentary, normally open

A small pushbutton that tells the controller to turn the second Hue light on or off.

Assemble it in 4 steps

1. Lay out the controller parts

Place the ESP32 DevKit V1 on a breadboard so its two rows of pins sit on opposite sides of the center gap. Put two small momentary pushbuttons nearby. A button only makes contact while you are pressing it; it does not switch mains electricity or connect to the Hue lamps directly.

  • If you use four-leg tactile buttons, place each button so it straddles the breadboard’s center gap. The two legs on each same side are already connected together inside the button.
  • Do not connect any household mains wires, lamp wires, or the Hue Bridge power supply to this project. The ESP32 only sends wireless commands through your existing Hue Bridge.

2. Wire the first light button

Run one jumper wire from one side of the first button to the ESP32 pin labelled GPIO32. Run a second jumper from the opposite side of that same button to any ESP32 pin labelled GND. This lets the ESP32 notice a press for your first light.

  • GPIO32 → button A (signal); button B → GND (ground). There is no direction or positive/negative side on a plain pushbutton.
  • Use two different wire colors so you can trace them later.
  • Do not use the two legs on the same side of a four-leg tactile button; they are already joined and the button will appear permanently pressed.

3. Wire the second light button

Run one jumper wire from one side of the second button to the ESP32 pin labelled GPIO33. Run another jumper from its opposite side to any ESP32 GND pin. Both buttons may share the same GND pin because they are only signals to the controller.

  • GPIO33 → button A (signal); button B → GND (ground).
  • The ESP32 turns on a small built-in helper inside each input, so no separate resistor is needed for either button.
  • Keep GPIO32 and GPIO33 on separate button sides. If they touch each other, pressing one button can trigger the wrong light.

4. Power and set up the controller

Connect the ESP32 to a normal USB phone charger or a USB port using its USB connector. After the firmware is deployed, connect your phone to the temporary Wi-Fi network named “Hue Buttons Setup” and open 192.168.4.1. Enter your normal Wi-Fi name and password, the Hue Bridge local IP address, its application key, and the two Hue light IDs. Save the page; the controller then joins your normal Wi-Fi.

  • Label the two physical buttons “Light 1” and “Light 2” after you confirm which Hue light each ID controls.
  • To start over, hold both buttons down together for five seconds; this erases the saved setup details.
  • Only enter the Hue Bridge application key on this controller’s private setup page. Treat it like a password because it permits control of your Hue devices.
  • Make sure the ESP32 and Hue Bridge are on the same home network; a guest Wi-Fi network often prevents them from talking to each other.

Review all connections

1. Connections between "button_light_1" and "ESP32"

Functionbutton_light_1ESP32
digitalAGPIO 32
groundBGND

2. Connections between "button_light_2" and "ESP32"

Functionbutton_light_2ESP32
digitalAGPIO 33
groundBGND

Deploy the firmware

#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>
#include <HTTPClient.h>
#include <WiFiClientSecure.h>
#include <Preferences.h>

struct ButtonState {
  int pin;
  bool stableLevel;
  bool lastReading;
  unsigned long lastChangeMs;
};

String pageHeader(const String &title);
void startSetupPortal();
bool loadConfiguration();
bool connectWiFi();
bool readLightState(const String &lightId, bool &isOn);

constexpr int BUTTON_1_PIN = 32;
constexpr int BUTTON_2_PIN = 33;
constexpr unsigned long DEBOUNCE_MS = 40;
constexpr unsigned long RESET_HOLD_MS = 5000;

Preferences preferences;
WebServer server(80);
String wifiSsid;
String wifiPassword;
String hueBridgeIp;
String hueAppKey;
String light1Id;
String light2Id;
bool setupMode = false;

ButtonState button1 = {BUTTON_1_PIN, HIGH, HIGH, 0};
ButtonState button2 = {BUTTON_2_PIN, HIGH, HIGH, 0};
unsigned long bothPressedSinceMs = 0;

String pageHeader(const String &title) {
  return "<!doctype html><html><head><meta name='viewport' content='width=device-width,initial-scale=1'>"
         "<style>body{font-family:sans-serif;max-width:42rem;margin:2rem auto;padding:0 1rem}input{width:100%;box-sizing:border-box;padding:.65rem;margin:.3rem 0 1rem}button{padding:.7rem 1rem}</style>"
         "<title>" + title + "</title></head><body><h1>" + title + "</h1>";
}

void startSetupPortal() {
  setupMode = true;
  WiFi.mode(WIFI_AP);
  WiFi.softAP("Hue Buttons Setup");

  server.on("/", HTTP_GET, []() {
    String form = pageHeader("Hue Buttons Setup");
    form += "<p>Enter your home Wi-Fi details and Hue Bridge details, then save.</p>"
            "<p>Press the round button on top of the Bridge before creating a Hue application key. Enter the Bridge's local IP address, that key, and the two light IDs.</p>"
            "<form method='post' action='/save'>"
            "Wi-Fi name<input name='ssid' required>"
            "Wi-Fi password<input name='pass' type='password'>"
            "Hue Bridge local IP address<input name='bridge' placeholder='192.168.1.50' required>"
            "Hue application key<input name='key' required>"
            "First light ID<input name='light1' required>"
            "Second light ID<input name='light2' required>"
            "<button type='submit'>Save and restart</button></form></body></html>";
    server.send(200, "text/html", form);
  });

  server.on("/save", HTTP_POST, []() {
    preferences.putString("ssid", server.arg("ssid"));
    preferences.putString("pass", server.arg("pass"));
    preferences.putString("bridge", server.arg("bridge"));
    preferences.putString("key", server.arg("key"));
    preferences.putString("light1", server.arg("light1"));
    preferences.putString("light2", server.arg("light2"));
    server.send(200, "text/html", pageHeader("Saved") + "<p>Saved. The controller will restart and join your Wi-Fi in a moment.</p></body></html>");
    delay(1500);
    ESP.restart();
  });
  server.begin();
}

bool loadConfiguration() {
  wifiSsid = preferences.getString("ssid", "");
  wifiPassword = preferences.getString("pass", "");
  hueBridgeIp = preferences.getString("bridge", "");
  hueAppKey = preferences.getString("key", "");
  light1Id = preferences.getString("light1", "");
  light2Id = preferences.getString("light2", "");
  return wifiSsid.length() && hueBridgeIp.length() && hueAppKey.length() && light1Id.length() && light2Id.length();
}

bool connectWiFi() {
  WiFi.mode(WIFI_STA);
  WiFi.begin(wifiSsid.c_str(), wifiPassword.c_str());
  unsigned long started = millis();
  while (WiFi.status() != WL_CONNECTED && millis() - started < 20000) {
    delay(250);
  }
  return WiFi.status() == WL_CONNECTED;
}

bool readLightState(const String &lightId, bool &isOn) {
  WiFiClientSecure client;
  client.setInsecure();
  HTTPClient http;
  String url = "https://" + hueBridgeIp + "/clip/v2/resource/light/" + lightId;
  http.begin(client, url);
  http.addHeader("hue-application-key", hueAppKey);
  int status = http.GET();
  if (status != 200) {
    Serial.printf("Hue state request failed: %d\n", status);
    http.end();
    return false;
  }
  String response = http.getString();
  http.end();
  int onObject = response.indexOf("\"on\":{");
  int trueValue = response.indexOf("\"on\":true", onObject);
  int falseValue = response.indexOf("\"on\":false", onObject);
  if (onObject < 0 || (trueValue < 0 && falseValue < 0)) {
    return false;
  }
  isOn = trueValue >= 0 && (falseValue < 0 || trueValue < falseValue);
  return true;
}

void toggleLight(const String &lightId) {
  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("Not connected to Wi-Fi; cannot reach the Hue Bridge.");
    return;
  }
  bool currentlyOn = false;
  if (!readLightState(lightId, currentlyOn)) {
    return;
  }
  WiFiClientSecure client;
  client.setInsecure();
  HTTPClient http;
  String url = "https://" + hueBridgeIp + "/clip/v2/resource/light/" + lightId;
  http.begin(client, url);
  http.addHeader("hue-application-key", hueAppKey);
  http.addHeader("Content-Type", "application/json");
  String body = currentlyOn ? "{\"on\":{\"on\":false}}" : "{\"on\":{\"on\":true}}";
  int status = http.PUT(body);
  Serial.printf("Hue toggle request returned %d\n", status);
  http.end();
}

bool pressed(ButtonState &button) {
  bool reading = digitalRead(button.pin);
  unsigned long now = millis();
  if (reading != button.lastReading) {
    button.lastChangeMs = now;
    button.lastReading = reading;
  }
  if (now - button.lastChangeMs >= DEBOUNCE_MS && reading != button.stableLevel) {
    button.stableLevel = reading;
    return button.stableLevel == LOW;
  }
  return false;
}

void checkFactoryReset() {
  bool bothHeld = digitalRead(BUTTON_1_PIN) == LOW && digitalRead(BUTTON_2_PIN) == LOW;
  if (bothHeld) {
    if (bothPressedSinceMs == 0) {
      bothPressedSinceMs = millis();
    } else if (millis() - bothPressedSinceMs >= RESET_HOLD_MS) {
      preferences.clear();
      delay(300);
      ESP.restart();
    }
  } else {
    bothPressedSinceMs = 0;
  }
}

void setup() {
  Serial.begin(115200);
  pinMode(BUTTON_1_PIN, INPUT_PULLUP);
  pinMode(BUTTON_2_PIN, INPUT_PULLUP);
  preferences.begin("huebuttons", false);
  if (!loadConfiguration() || !connectWiFi()) {
    startSetupPortal();
  }
}

void loop() {
  if (setupMode) {
    server.handleClient();
    return;
  }
  checkFactoryReset();
  if (pressed(button1)) {
    toggleLight(light1Id);
  }
  if (pressed(button2)) {
    toggleLight(light2Id);
  }
  delay(2);
}

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik