AC wall switch: parts, wiring, program

Servo-actuated rocker flipper · ESP32 + Cloudflare Worker · compiled 11 Sep 2026 from the Taobao cart (11 Aug 2026), the firmware and the project README

A servo mounted beside the AC's wall rocker switch presses it over the internet. Nothing touches mains wiring: the servo pushes the existing switch the way a finger would. A temperature sensor on the same board lets the cloud side run it as a thermostat.

Parts used in the switch

Listing titles copied exactly from the Taobao cart

PartExact Taobao listingShopVariantQty
ESP32 dev boardesp32开发板模块 CP2102 Type-C USB接口WIFI蓝牙无线模块泽杰旗舰店Type-C, 30-pin, headers soldered · ¥19.981
ServoMG996R MG946R MG995 金属标准舵机180度360度数码机器人20KG舵机深圳市轩特佳电子MG996R, 180° (not 360°), all-metal gears · ¥11.301
BME280 sensorBMP180 280 BME280 388AHT20高精度大气压强气压传感器模块高度计欣薇电子企业店BME280-3.3V模块 (I2C)1
Dupont wires F-F杜邦线 母对母 公对母 公对公40P彩色排线连接线10/15/30/20/40CM欣薇电子企业店母对母, 20 cm, 40-pin strip1
Dupont wires M-F彩色40p杜邦线排线公对公母对母公对母实验端子连接线10/20/30CM深圳新芯得电子商城公对母, 20 cm, 40-pin strip1
5 V USB charger (board)Already owned. Powers the ESP32 through its USB-C port.
5 V supply (servo)Already owned. A separate supply just for the servo, so its current spikes stay off the board.
USB-C data cableAlready owned. Several cables on hand were charge-only (marked UL2464 22AWG). A data cable is marked like 28AWG/1P + 24AWG/2C.
⚠ The ESP32 listing says CP2102, but the board that arrived enumerates as an FT232R USB-serial bridge (chip ESP32-D0WD-V3 rev 3.1). It makes no difference to the build: any classic 30-pin ESP32 DevKit works, as long as it is not an S3 or C3.
⚠ The servo listing bundles MG996R, MG946R and MG995. The variant picked was the MG996R 180°.

Wiring schematic

Two 5 V supplies: one charger powers the board over USB-C, and a separate one powers the servo. Their grounds are joined at the board.

AC wall rocker I O horn presses 69° on · 10° off MG996R servo signal GND +5V yellow brown red 5 V supply servo only common GND ESP32 DevKit 30-pin D13 GND D23 D22 D21 D18 GND USB-C BME280 I2C @ 0x76 CSB SCL SDA VCC GND SDO held HIGH GPIO power USB-C data cable 5 V USB charger board only power ground signal / data

Servo

Servo wireESP32 pinNote
yellow (signal)D1350 Hz PWM, 500–2400 µs pulse
red (+5V)servo supply +Not connected to the board
brown (GND)GND
servo supply −GND (a second pin)Common ground. Without it the signal on D13 has no reference and the servo ignores it.

BME280

Sensor pinESP32 pinWhy
VCCD18A GPIO, not 3V3. The firmware powers the sensor only after CSB is already high (see below).
GNDGND
SDAD21I2C data
SCLD22I2C clock
CSBD23Driven HIGH, which selects I2C mode
SDOGNDSets I2C address 0x76

How the program works

Roughly. Firmware is Arduino C++ on the ESP32. The cloud side is one Cloudflare Worker plus one Durable Object.

phone UI / Siri shortcut
        │ HTTPS  (key-gated)
        ▼
Cloudflare Worker ──► Durable Object "switch"   ← thermostat + run log live here
                            ▲        │
          board dials OUT   │        │ pushes "on" / "off" / "set 69 10"
          one WebSocket     │        ▼
                         ESP32 ──PWM D13──► servo ──► rocker switch
                            ▲
                     I2C D21/D22
                            │
                         BME280

The board sits on campus WiFi behind NAT, so nothing on the internet can reach it directly. It opens one outbound WebSocket to the Worker and keeps it open, and commands come back down that socket. No laptop or home server sits in the path.

Firmware loop

  1. Boot: power up the sensor in the right order (CSB high, then VCC), join WiFi with modem sleep off, open the WebSocket and send a ping every 20 s so the NAT keeps the connection alive. Switch state starts as unknown, and the servo does not move on boot.
  2. Every 15 s read temperature, humidity and pressure. A reading that is NaN or outside −50…90 °C is thrown away.
  3. Every 20 s send env 26.10 58.2 up the socket.
  4. On a command: attach the servo and turn it to 69° (on) or 10° (off). If the arm is already resting at that angle, it first backs off to 40° so the move still presses the switch. It waits 600 ms, then detaches PWM, so the servo doesn't buzz, heat up or keep pushing on the switch.
  5. Whenever its state changes, the board sends on / off back, so the cloud always knows what the board believes.
loop():
  every 15s:  read BME280
  every 20s:  ws.send("env <temp> <hum>")
  on ws msg "on"/"off":
      if arm already at target: servo.write(40); wait 350ms
      servo.write(target ? 69 : 10); wait 600ms; servo.detach()
  if state changed: ws.send(state)

Cloud side

  1. Thermostat ("cruise") runs on every reading the board sends, not on a timer, so it never acts on stale data. It keeps a band, 25–27.5 °C by default. The remote's set temperature is the middle of the band, and swing is how far the room may drift either way.
    • AC on and room ≤ low edge → send off
    • AC off and room ≥ high edge → send on
    • After a reboot, state is unknown → drive the switch to whichever side the temperature calls for
  2. Remote UI: an LCD panel over / power / +. Power toggles the thermostat, and ± moves the set temperature in 0.5° steps. Siri and Control Centre shortcuts call the same /config URL.
  3. Offline board: commands are refused (503), not queued. A queued command once fired hours late and switched the AC on with nobody there.
  4. Run log: one row per on/off transition, plus one sensor row a minute kept for 7 days. These feed the usage totals, the charts and a "not cooling" check. If the room hasn't dropped 0.3 °C after 45 minutes of running (average of the first 6 minutes vs the last 6), the UI shows a warning.
  5. Security: anything that moves the switch needs the key, and a client IP gets 8 failed attempts per 15 minutes. Reading the temperature and state is public.

Socket messages

DirectionMessageMeaning
board → cloudon · off · unknownWhat the board last did. Sent on every change and on every reconnect.
board → cloudenv 26.10 58.2Temperature °C, humidity %
cloud → boardon · off · toggleFlip the switch
cloud → boardset 69 10Calibration angles (on, off), pushed live on every connect
cloud → boardtestWiggle ±20° without pressing the switch

Numbers

SettingValue
On / off / neutral angle69° / 10° / 40°, set by trial against the real switch
Servo pulse500–2400 µs at 50 Hz, PWM released after 600 ms
Sensor read / report15 s / 20 s
Tap → switch confirmed~0.5 s
Idle costPings are answered by Cloudflare without waking the Durable Object, and storage writes stay well inside the free tier

Software

Lessons that cost the most time