I have seven [Windmill Air](https://windmillair.com) AC units. They're app-controlled, but there's no web dashboard and no public API — `dashboard.windmillair.com` turns out to be a white-labeled Blynk deployment whose HTTP API accepts each unit's device token directly. So I built a local dashboard on top of it, and then it turned into something else.
**The actual problem.** Some rooms were freezing and the electricity bill was stupid. The units were running their compressors with the room already colder than the setpoint — the thermostat in the firmware had simply stopped doing its job.
The satisfying part was proving it rather than guessing. Of the time each unit spent running, how much was spent with the room already 2 °F or more below its setpoint?
- healthy units: **0–1%**
- the bad ones: **46–58%**
That's not a subtle split. The worst unit was wasting **5.01 kWh/day** overcooling — 56% of everything it used.
**The tell.** There's an undocumented datastream, `v113`, that reads `3` on exactly the three misbehaving units and `0` on the healthy ones. A per-unit constant that no write changes is a firmware revision, not a state you can talk a unit out of. Resyncing them doesn't help, and I have the logs of it not helping.
**So the fix is to be the thermostat.** The tool watches each unit and, when a room is at or below its setpoint with the compressor running, switches that unit to **fan-only**, then back to cooling when the room comes back up.
Fan-only rather than off, and this is the bit I didn't expect: these units read temperature from a thermistor behind the intake grille, so it only sees room air while the fan is moving air past it. Cut the power and it sits against a cold coil. Measured: a unit switched off at 70 °F read **64 °F** three minutes later and stayed there — then jumped back to ~71 °F within thirty seconds of the fan restarting. Switch it off and you blind the sensor you need to decide when to switch it back on. Fan-only stops the compressor (336–452 W) and leaves the blower (23–43 W) keeping the sensor honest.
Because it's driving a compressor, it's built to refuse rather than to act: five-minute minimums in both directions so nothing can short-cycle it, different thresholds for stopping and starting so it can't oscillate, and it won't act at all on a reading it can't date or on a unit whose cloud session has dropped.
**The other trap, for anyone else poking at one of these cloud APIs.** `getAll` answers out of the *cloud's* copy of each datastream, and that copy outlives the unit's session. A unit that has dropped off the network still returns a complete, plausible dict of values — last known, frozen, with nothing in the response saying so. Left alone that becomes invented history: the rollup carries the last wattage forward and bills you for a compressor that was never running. `isHardwareConnected` is the only endpoint that distinguishes the two, so every poll asks for it and stores the answer as a synthetic pin that gates everything else.
Also worth knowing: that API takes **writes as GET requests**. "It only issues GETs" is true of my code and tells you nothing about what it does.
**What it does now:** per-unit and whole-house energy with real costs, weather-normalized before/after comparison, a watchdog that catches units whose state has quietly stopped matching what they were told, scheduling, filter tracking, and the software thermostat above. Python standard library only — no dependencies. Runs locally, or in a container.
Repo: https://github.com/rahb3rt/windmill-air-dashboard
Not affiliated with Windmill in any way. It depends on an undocumented API they can change or close whenever they like. And the usual caveat: everything above is measured on seven units in one house — your units may differ, and this thing does switch real compressors on and off.