The Midea app's scheduler has been broken for months.
Set the AC to turn off at midnight — it stays on. Set it to turn on at 7 AM — nothing happens. I'd wake up freezing at 3 AM because the scheduler forgot to run, stumble to my phone, open the app, wait for the cloud handshake to time out, retry, and finally turn the damn thing off manually.
The AC unit sits on my LAN. It speaks a local protocol. The cloud is just middleware — and broken middleware at that. So I cut it out.
Phase 1: Find the AC
The unit is a Midea split system, model 00000Q17. I scanned the subnet and found it listening on port 6444 — the standard Midea V3 protocol endpoint. MAC address on file in case DHCP ever reassigns the IP.
Midea's V3 protocol uses encrypted local communication after an initial cloud handshake. You authenticate once via the cloud, get a token and key, and then you can talk to the AC directly over the LAN. No ongoing cloud dependency.
The open-source midea-local Python library handles the protocol. I installed it, ran through the one-time token acquisition using a shared test account (no personal Midea credentials), and cached the auth material in a JSON config file. The token lasts months, and if it expires, the library can refresh it.
I wrote a quick CLI script — ac_control.py — to test the basics:
$ python3 ac_control.py status
{'power': True, 'mode': 'cool', 'target_temp': 24, 'indoor_temp': 26.5}
$ python3 ac_control.py off
AC turned off
$ python3 ac_control.py temp 22
Temperature set to 22°C
$ python3 ac_control.py mode heat
Mode changed to heat
All working. The AC was mine.
Phase 2: The Oversimplification
Here's where the agent screwed up. I told my homelab AI "I want to lock the AC's IP and set up night scheduling." The agent created a task called midea-ac-dhcp-reservation — a single-phase manual-commands task that was just "here are the OpenWrt DHCP reservation instructions." It collapsed the entire reverse engineering project into a router config note.
I killed that task. Told the agent this was a multi-phase project: discovery, reverse engineering, app development, Docker deployment. Four phases minimum. The agent created midea-ac-control-system with a proper PLAN.md and got to work.
This is a recurring pattern with AI agents — they oversimplify on the first pass, collapsing complex engineering into a single trivial step. You have to be willing to say "no, that's wrong, start over with proper scope." The agent won't push back. It'll just do it.
Phase 3: The Client Wrapper
midea-local works, but its API is verbose. Every interaction requires device construction, open(), polling for availability, attribute reads, and close(). I wanted a clean Python interface.
I wrote ac_client.py — 120 lines wrapping midea-local with a context manager:
class ACConnection:
"""Use as `with ACConnection() as ac:` for safe open/close."""
def __enter__(self):
self.client = ACClient()
if not self.client.connect():
self.client.close()
raise ConnectionError("Could not connect to AC device")
return self.client
def __exit__(self, *args):
self.client.close()
The ACClient class exposes clean methods: set_power(), set_temperature(), set_mode(), set_fan_speed(), set_swing(), set_eco(), set_sleep(), set_turbo(). Each maps directly to a V3 protocol attribute. The status() method returns a flat dict — power state, mode, target temp, indoor temp, fan speed, swing positions, eco/sleep/turbo flags.
Connection timeout is 10 seconds. If the AC doesn't respond, it raises. No silent failures.
Phase 4: The Flask App
With a clean client, the web app wrote itself. Flask + SQLite + Bootstrap 5 + APScheduler.
The UI is a single-page dashboard. Big power button in the center — tap it, the AC turns on or off. Temperature slider with a real-time display. Mode buttons (auto/cool/dry/fan/heat) that highlight the active mode. Fan speed selector. Swing controls. Eco and sleep toggles. Status refreshes every 30 seconds via the REST API.
The REST API:
GET /api/ac/status → {power, mode, target_temp, indoor_temp, fan_speed, swing, eco, sleep}
POST /api/ac/power → {"state": "on"|"off"}
POST /api/ac/temp → {"temp": 24}
POST /api/ac/mode → {"mode": "cool"}
POST /api/ac/fan → {"speed": "auto"|"low"|"medium"|"high"}
POST /api/ac/swing → {"vertical": true, "horizontal": false}
POST /api/ac/eco → {"state": true|false}
POST /api/ac/sleep → {"state": true|false}
Every POST endpoint opens a fresh connection to the AC, sends the command, waits 1.5 seconds for the state to settle, and returns the new status. The 1.5-second delay is empirical — the AC takes about a second to process a command and update its internal state. Anything less and you get stale reads.
The scheduler runs on APScheduler in the same process. It checks every 30 seconds whether any schedule entry matches the current time. Schedules are stored in SQLite:
CREATE TABLE schedules (
id INTEGER PRIMARY KEY AUTOINCREMENT,
time TEXT NOT NULL, -- "22:00"
action TEXT NOT NULL, -- "off", "on", "temp"
temp REAL, -- for "temp" action
mode TEXT, -- for "temp" action
days TEXT DEFAULT 'daily', -- "daily", "weekday", "weekend", "0,1,2,3,4"
enabled INTEGER DEFAULT 1
);
I set it to turn off at midnight and on at 7 AM on weekdays. It hasn't missed a beat.
Phase 5: Docker + Proxy
The app runs as a Docker container on the bridge host. The compose file is 12 lines:
services:
midea-ac:
build: .
container_name: midea-ac
restart: unless-stopped
network_mode: host
volumes:
- midea_ac_data:/app/data
network_mode: host is necessary. The AC client needs raw socket access to the AC unit on the physical LAN, and bridge networking was adding latency and occasional connection drops. Host mode puts the container directly on the host's network stack — same subnet, same ARP table, same route to the AC.
The container has been up for 4 days without a restart. It's stable.
Nginx Proxy Manager handles the public face. I created a proxy host at ac.local.curci.cc pointing to the app on port 8127, SSL forced with the wildcard cert. It's internal-only — the DNS record only resolves inside the LAN.
Why Not Home Assistant?
I know about Home Assistant's Midea integration. I could have added the AC to HA in 10 minutes and been done.
But I didn't want a dashboard. I wanted an API.
My homelab agent — the same one that monitors Proxmox, audits SEO, and writes these blog posts — can now control the AC programmatically. If a temperature sensor in the server rack goes above 35°C on a summer day, the agent can turn on the AC in dry mode. If I'm away and the indoor temp drops below 16°C in winter, it can switch to heat. These are automations I can write as Python scripts that hit a REST endpoint, not YAML configurations buried in a Home Assistant instance.
The API-first approach is dumber than HA. Deliberately dumber. It does exactly what I tell it and nothing else. No auto-discovery, no entity registry, no Lovelace UI — just curl -X POST /api/ac/power -d '{"state":"off"}' and the AC turns off. That's the feature.
What I'd Do Differently
The oversimplification cost time. If I'd started with a proper multi-phase plan — discovery → reverse engineering → app → deploy — the whole thing would have taken a day instead of two.
The network_mode: host compromise bothers me. It's not clean Docker practice. But midea-local opens a persistent TCP socket to the AC, and Docker's bridge networking was dropping it after periods of inactivity. Host mode fixed it. Sometimes the clean pattern loses to the working pattern.
The scheduling could be more sophisticated — cron-style expressions instead of "daily" vs "weekday" dropdowns, temperature curves instead of single setpoints. But those are features for a problem I don't have yet. The AC turns off at midnight and on at 7 AM. That was the whole point.