MicroPython vs. The Cloud: Controlling My Smart AC with an ESP32-C3 SuperMini

Ever since our decades-old AC unit kicked the bucket, and our landlord was kind enough to install a shiny, new, smart AC, I’ve been wanting to study how it works, and thought maybe I could build a touchscreen dashboard powered by some microcontroller for controlling it, with a nice 3D printed case or something, seemed like a neat project.

I waited, and waited, days flew by, and now - time’s up!
Why? because WE’RE MOVING IN A WEEK, and I didn’t get to make that cool-ass gadget.
However, I still wanted to do this project in some capacity, maybe with a simple web UI instead of a touchscreen, since the cool part for this project would be reverse engineering the API.

An ESP32 was the obvious choice - all I need is basic Wi-Fi connectivity really, and since time is so tight and I’m feeling hella rusty in the low-level programming department, and I ditched the touchscreen idea,
I decided to try my hand at MicroPython (for the first time!).


Before I could build anything, I needed to understand how the Electra (an Israeli AC brand) app communicates with my AC unit.
I needed a way to intercept the traffic, and I figured it’s probably through a REST API.


I used Genymotion, an Android emulator, to run the Electra app in a controlled environment. The advantage of Genymotion over a physical phone is that it’s much easier to inject system certificates and intercept HTTPS traffic, plus I could try different phones with different Android versions quite rapidly.

For the actual interception, I used HTTP Toolkit.
It’s a fantastic tool that acts as a man-in-the-middle proxy, allowing you to see all the HTTP/HTTPS requests an app makes.

So the basic setup was:

  1. Install Genymotion and create an Android virtual device
  2. Install HTTP Toolkit & intercept HTTPS traffic
  3. Install the Electra app on the emulator
  4. ???
  5. Profit.

After logging into the app and playing around with the AC controls, I captured several interesting API calls. The Electra API lives at app.ecpiot.co.il and is… janky to say the least.

  • All calls use the same API endpoint - /mobile/mobilecommand
  • The desired operation is specified in the json body of the request, in the cmd field
  • The data field provides data for the operation
  • The (apparently not so secret…) token and IMEI(!) of the phone are sent within the request’s body(?!)
  • All calls use POST for some reason

The first thing the app does is check for updates (to the app, I assume?)

HTTP Toolkit showing a CHECK_VERSION API request to app.ecpiot.co.il with JSON body containing version 2.0.24, OS android, and osver showing VirtualBox debug info. Response returns status 0 indicating success.

Interestingly, Electra can see I’m running Android on a virtual machine (VirtualBox through Genymotion) with debug capabilities, but don’t seem to care about it.

Then, the app validates the token against the remote server, getting a session ID (sid), which will be used in all subsequent calls.
The (apparently not so secret…) token and IMEI(!) of the phone are sent within the request’s body(?!)

HTTP Toolkit showing a VALIDATE_TOKEN API request with redacted token and IMEI fields in the JSON body. The response returns a session ID (sid) that will be used for subsequent API calls.

This returns a session ID (sid) that’s used for all subsequent requests (Horrible auth but ok).

Next, it checks which devices are associated with the account:

HTTP Toolkit showing a GET_DEVICES API request with the session ID and an empty data object in the JSON body.

HTTP Toolkit showing the GET_DEVICES response containing device details including providerName Electra, deviceTypeName A/C, a misspelled manufactor field, device name Bedroom, and various redacted fields like serial number, MAC address, and device token. The device ID 217515 is visible at the bottom.

("Manufactor"...? REALLY?)


Now, if you look at the end of the response, there's the ID of our AC.

Then, every few seconds, it gets the current AC state (for the ID we just got), with the GET_LAST_TELEMETRY command:

HTTP Toolkit showing a GET_LAST_TELEMETRY request and response. The request includes the device ID and commandName. The response contains the current AC state as a JSON string with settings like TURBO OFF, AC_MODE HEAT, SPT 30 (set point temperature), FANSPD HIGH, and various other operational parameters.

And to control the AC, it sends the entire desired state in a SEND_COMMAND:

HTTP Toolkit showing a SEND_COMMAND API request containing the device ID and a commandJson field with the entire desired AC state as a JSON string, including TURN_ON_OFF ON, AC_MODE HEAT, SPT 30, and FANSPD HIGH. The response returns a simple success status.

The commandJson field is interesting - it’s a JSON string embedded within JSON. A bit awkward, but workable.


With the API figured out, I needed something to run the web server on. I chose the ESP32-C3 SuperMini for a few reasons:

  • Tiny form factor (about the size of my thumb)
  • Built-in WiFi
  • Runs MicroPython
  • Costs about $3

The ESP32-C3 has around 400KB of RAM, which sounds like a lot until you start loading SSL libraries, a web framework, and JSON parsers. Memory management would become a recurring theme in this project.


I started simple. MicroPython has urequests for HTTP calls and ujson for JSON handling. My first script just connected to WiFi, authenticated with the API, and printed the session ID to the serial console so I could see authentication succeeded.

import urequests as requests
import ujson as json

# Connect to WiFi...

def get_ac_sid(url):
  body = {
    "sid": None,
    "pvdid": 1,
    "id": 2,
    "cmd": "VALIDATE_TOKEN",
    "data": {
      "token": "my-token",
      "imei": "my-device-id",
      "os": "android",
      "osver": "..."
    }
  }
  headers = {
    "Content-Type": "application/json; charset=utf-8",
    "Accept-Encoding": "gzip",
    "User-Agent": "Electra Client",
    "Connection": "close"
  }
  r = requests.post(f"{url}/mobile/mobilecommand", json=body, headers=headers)
  return r.json()

sid = get_ac_sid(ac_url)["data"]["sid"]
print(f"SID: {sid}")

This worked! I could see the SID printed in the console. But then I tried to fetch the telemetry and got my first memory-related error.


The Electra app requests gzip-compressed responses from the server, so I did too.
However, decompressing gzip requires a 32KB buffer, and combined with the SSL connection overhead, the ESP32 just couldn’t handle it.

The fix was embarrassingly simple: change the header to Accept-Encoding: identity. This tells the server to send uncompressed data. Slightly more bandwidth, but the ESP32 could actually process it.


Once I could reliably communicate with the API, I wanted a web interface. I chose Microdot - a Flask-like web framework designed for MicroPython.

It’s not in the official MicroPython package index, so I had to grab it directly from GitHub:

mpremote mip install github:miguelgrinberg/microdot/src/microdot/microdot.py

My first web server was just a “Hello from ESP32!” page. When that loaded in my browser, I felt unreasonably proud. Then I needed to make a UI, and for that I leveraged Claude Code (I’m not a frontend dev, sue me).


Getting the web server and API calls to coexist was tricky. The ESP32-C3 has limited RAM, and I was constantly fighting memory pressure left and right.

The solution was aggressive garbage collection. Before every API call:

import gc
gc.collect()

I also learned to close response objects immediately after reading them:

r = requests.post(url, json=body)
result = r.json()
r.close()  # Free that memory!

With these optimizations, I had around 117KB of free memory during normal operation - plenty of headroom.


The first version worked, but it was slow. Pressing a button meant:

  1. Browser sends POST to ESP32
  2. ESP32 fetches current AC state from API (1-2 seconds)
  3. ESP32 sends command to API (1-2 seconds)
  4. ESP32 redirects browser
  5. Browser fetches new page
  6. ESP32 fetches state again for display (1-2 seconds)

That’s 5+ seconds of waiting just to change the temperature.

I implemented three optimizations (again, with the help of Claude Code):

1. State Caching - Instead of fetching the AC state before every command, I cache the last known state and use that.

2. Optimistic UI - The JavaScript updates the UI immediately when you press a button, before the server even responds.

3. AJAX Instead of Forms - Using fetch() API calls instead of form submissions means no page reload. (I learned something new!)

The result? The UI is still kind of shit 💀.
mainly because it’s just a little too heavy for this tiny board.
I’d be able to press 1 or 2 buttons before it would completly hang and I’d get connections resetting left and right.
The API though? Chef’s kiss! curling a command to the ESP32 is honestly impressively fast and stable.


The project ended up with this file structure:

boot.py      - WiFi connection, API authentication
ac.py        - Electra API functions
web.py       - Microdot web server and routes
templates.py - HTML/CSS/JavaScript template
main.py      - Just starts the web server really

On power-up:

  1. boot.py runs first - connects to WiFi, gets the session ID
  2. main.py starts the Microdot server on port 80
  3. ???
  4. Profit.

The web interface shows the current state (power, room temperature, set temperature, mode, fan speed) and has buttons to control everything. It auto-refreshes every 10 seconds to stay in sync with the actual AC state.

The ESP32 web interface showing AC Status with Power OFF, Room Temp 25C, Set Temp 30C, Mode HEAT, and Fan HIGH. Below are controls for temperature adjustment with minus and plus buttons, mode selection buttons for Cool, Heat, Fan, and Dry, fan speed buttons for Low, Med, High, and Auto, and a green Turn ON button at the bottom.


Memory is precious on microcontrollers. Every byte counts. Use gc.collect() liberally, close connections immediately, and think twice about the “work cost” of adding a feature.

Reverse engineering APIs is fun. With the right tools (Genymotion + HTTP Toolkit), it’s surprisingly straightforward to see what apps are doing behind the scenes.

MicroPython is great for rapid prototyping. Being able to iterate quickly without compile cycles made this project much more enjoyable.

The ESP32-C3 SuperMini is impressive. For $3, you get WiFi, plenty of GPIO (which, admittedly, I didn’t use), and enough power to run a web server (albeit a really shitty one). What a time to be alive.


You can find all the code on GitLab.

Categories