Local control for a Midea air conditioner. No cloud, no vendor app. HTTP API, web dashboard, continuous logging, and a native Apple HomeKit bridge. Once the device is discovered, it never talks to the internet again.
Built on msmart-ng and HAP-python. Runs on any always-on box on your LAN (Raspberry Pi, Mac mini, NUC), reachable from anywhere over Tailscale.
Named after Aeolus (Αἴολος), keeper of the winds in the Odyssey.
This started during a French heatwave. I had just bought a Midea split AC, and the only way to turn it on remotely was the vendor app, which needs the cloud and only works when your phone is on the home WiFi. What I actually wanted was simple: turn the AC on from my phone on the way home, so the flat is already cool when I walk in.
So I went local instead. The AC speaks a proprietary protocol on the LAN, and msmart-ng implements it. From there it grew: a small HTTP API, then logging (how much does it actually cost? how does it behave?), then a dashboard, then HomeKit so the rest of the household could use it too. Everything runs on a box on the home LAN and is reachable from anywhere over Tailscale, no cloud involved.
- Full control: power, mode, target temperature, fan, swing, and the extras (iECO, turbo, sleep, outdoor-silent, purifier, freeze protection).
- Continuous SQLite logging every 60s: indoor/outdoor temp, target, power draw, mode.
- Web dashboard at
/chart: temperature and power charts, drag to zoom, a readable power-vs-setpoint curve, and a command journal that records who changed what and when, including changes made from the physical remote or the Midea app. - HomeKit: shows up in the Home app, works with Siri, shareable with the household.
- Resilient: survives the AC being unplugged, the router rebooting, or the IP changing (auto re-discovery). Never records phantom samples.
- The temperature chart shades the background by the sun's height (computed, no API) and can extend the outdoor curve with a weather forecast (Open-Meteo, AROME by default), so you can see the heat coming.
- Autopilot (optional, toggle on the dashboard): treats the AC's setpoint as a ceiling, turns the unit fully off when the room is cool enough (silent, ~0 W, still measuring), and starts cooling ahead of forecast heat. Lead time comes from the room's own cooling rate, measured per fan speed from the logged history, and from the AROME forecast de-biased against the outdoor temperature it actually measured. A live indicator shows what it is doing and when it will act next.
The hysteresis view aggregates power by distance from setpoint, so it stays readable over any span. On an inverter that modulates, the cooling and warming branches separate by how hard it is working:
The command journal shows every change with its source, including ones made from the physical remote or the Midea app (detected by watching the state, not our API):
- A Midea AC (or an msmart-compatible rebrand: Comfee, Inventor, Pioneer, some Toshiba/Carrier units) already on your WiFi via the vendor app.
- An always-on machine on the same LAN. Python 3.11+.
- Optional: Tailscale on that machine for remote access without exposing anything.
git clone https://github.com/<you>/clim-midea.git
cd clim-midea
python3 -m venv .venv
source .venv/bin/activate
pip install "msmart-ng" fastapi "uvicorn[standard]" python-dotenv "HAP-python[QRCode]" pypngThis is the one step that uses the Midea cloud, once. It gives you the IP, device id, and (on "V3" devices) a token/key pair that encrypts the local session.
.venv/bin/msmart-ng discoverOutput looks like:
{'ip': '192.168.1.42', 'id': 123456789012345, 'supported': True,
'name': 'net_ac_XXXX', 'key': 'YOUR_KEY', 'token': 'YOUR_TOKEN'}
Keep the token and key somewhere safe. They are what makes the setup independent:
regenerating them needs the Midea cloud to answer. If discover finds nothing, check
that the machine and the AC are on the same WiFi subnet (not a guest network). Some
devices need --account / --password (your Midea app login), see
msmart-ng discover --help.
cp .env.example .env
# then fill AC_IP, AC_ID, AC_TOKEN, AC_KEY from the discover output.venv/bin/uvicorn ac_server:app --host 127.0.0.1 --port 8787Open http://127.0.0.1:8787/chart, or curl http://127.0.0.1:8787/status.
To keep it running: macOS, edit clim.plist.example (paths, bind host) and load it
with launchd. Linux, a systemd unit does the same job.
For remote access, bind to your Tailscale IP instead of 0.0.0.0. The service then
answers on your tailnet only, never the LAN or internet. There is no app-level auth,
so the tailnet is the security boundary. Do not bind to 0.0.0.0.
GET /status current state, read back from the AC
GET /capabilities what this unit supports
GET /health diagnostics, never touches the AC
GET /chart web dashboard
GET /say state as plain text (for Siri / Shortcuts)
GET /history?hours=24 samples (or ?start=<epoch>&end=<epoch>)
GET /hysteresis?hours=24
GET /commands?limit=50 command journal
GET /pair HomeKit pairing page (QR + code)
POST /set {"power":true,"mode":"COOL","target":23,"fan":"SILENT"}
POST /on?temp=23&mode=COOL&fan=SILENT
POST /off
POST /toggle?temp=23&mode=COOL
POST /temp?value=22.5
POST /mode?value=HEAT
POST /fan?value=AUTO
POST /display
POST /autopilot?on=true toggle autopilot at runtime
/set validates against the device's real capabilities and returns the state read
back after applying. Every command silences the beep.
The bridge serves its own pairing page at /pair: open it in any browser and it
shows a generated QR code plus the pairing code, so you don't have to hunt for
either. Then in the Home app: Add Accessory, scan the QR (or "More options" and pick
the accessory), enter the code.
Change AC_HOMEKIT_PIN before deploying. The phone must be on the same WiFi to pair.
Remote HomeKit access needs an Apple home hub (HomePod or Apple TV).
Pairing a DIY HomeKit accessory can be finicky. If it does not show up or says it is "already in another home", the fixes that worked here: make sure the phone is on the same LAN with any VPN off, and if a previous attempt got stuck, remove the phantom accessory (or delete the home) to clear Apple's iCloud cache. The accessory uses a unique serial per identity to avoid that trap.
- The AC accepts only one LAN session at a time. Logger, API and HomeKit share one, behind a lock.
msmart.refresh()does not raise on a network error, it keeps default values. The logger checksdevice.onlineso it never records phantom samples.- Some settings are device-specific:
ecomay really beieco, and iSense/Follow-Me can only be set from the remote. The code validates and refuses rather than lie.
MIT.


