The Charger, the Car, and the API That Wasn't There
My Ohme wall charger has lived its whole life on my wife's phone. Every time I wanted to know if the car was charging, or wanted to flip it to max charge before a long trip, the answer was "ask her phone". Tonight I fixed that, and accidentally fell down one of the deeper rabbit holes I've hit in a while.
A menu bar app in an evening
The plan was simple. The Home Assistant crowd has a well-documented unofficial Ohme integration, and underneath it sits a tidy little Python library called ohmepy that lays out the whole API: log in through Ohme's Firebase identity endpoint with plain email and password, then talk to api.ohme.io with the resulting token. Pause, resume, max charge, charge slots, live power. All there.
So I built OhmeBar: a native SwiftUI menu bar app, in the same mould as the UniFi camera viewer I keep in my status bar. Bolt icon that changes with charger state, popover with live kW, amps, volts, session energy, charge slots, and the controls. Credentials in the Keychain, tokens auto-refreshing. An evening's work, tests passing, charging away at 7 kW. Lovely.
Then I looked at the battery readout. 22%. The car was at more like double that.
The number that lied
Pulling the raw API response showed two battery readings: one marked USER (the 22% someone typed into the Ohme app at plug-in, hours earlier) and one marked EXTRAPOLATION (Ohme's running estimate, 46%). I'd copied the library's preference for the car-reported value, which sounds right until you realise that for my car there is no car-reported value any more.
That's when the real story surfaced. My BMW used to feed its state of charge straight into Ohme. In late 2025 BMW slammed the door on third-party access to its ConnectedDrive API - rate limits in August, captchas and hard blocks in September. The Home Assistant BMW integration died. And Ohme quietly removed BMW from its supported brands, which is why the pairing option had vanished from the app. Nobody tells you; the sign-in screen just isn't there any more.
CarData, eventually
BMW's official replacement is CarData: a customer-facing API where you create your own OAuth client in the BMW portal and approve it with a device code. So OhmeBar grew a second integration: fetch the real state of charge from BMW, push it into Ohme at plug-in, and the charge maths is honest again.
Getting the pairing to work was the hard part. Every approval came back access_denied - the user has declined authorization, despite very much not declining. Watching the raw token endpoint from a terminal showed the truth: at the exact moment of approval, BMW's backend threw an internal 500, then recorded the request as declined. The fix, buried in the issue trackers of two Home Assistant integrations, is gloriously stupid: BMW sometimes issues client IDs that simply never propagate to its own auth servers. Delete the client, create a new one, wait a few minutes. The new client paired in 31 seconds.
Other traps for anyone following: the approval page caches stale codes from previous attempts (pass the code in the URL), the consent check wants both the API and streaming scopes even if you never touch the stream, and the telematics endpoint silently requires you to register a "container" of data descriptors first. All of it is in the README now, so the next person spends five minutes on this instead of ninety.
Shipping it
Since nothing like this seemed to exist - a native Mac app for Ohme, let alone one that patches the BMW hole - it's now open source: signed, notarized, MIT licensed, with CI and a setup guide written in the scars of the evening.
It works with any Ohme charger and any EV; the BMW part is an optional extra for those of us caught in the API shutdown. The notarization step had its own mini-saga involving an expired developer agreement and Apple's Digital Services Act questionnaire, but that's a yak best left unshaved in print.
The thing I keep thinking about: the car knows its battery level, the charger needs it, and for months the two sat three metres apart unable to speak - because a manufacturer decided an open API was a liability. It took a hobby project and two reverse-engineered integrations to reconnect them. The EU's Data Act forced BMW to open CarData at all, and on tonight's evidence, that legislation is doing more for my garage than any firmware update ever has.