Async Python client for the Ratio EV Charging cloud API
Project description
aioratio
Async Python client for the Ratio EV Charging cloud API.
What this is
A standalone, async, dependency-light Python client for the cloud API behind the official Ratio EV Charging mobile app. Authenticates via AWS Cognito (USER_SRP_AUTH + DEVICE_SRP_AUTH), persists device and refresh tokens, exposes typed dataclass models for chargers, sessions, settings, and vehicles. Designed to be embedded in Home Assistant integrations or used directly from scripts.
An optional [ble] extra adds a local Inspiro IPC BLE client
(aioratio.BleClient) for charger control without going through the cloud.
This is an unofficial library. Not affiliated with Ratio.
Install
pip install aioratio
Requires Python 3.11+. Only runtime dep is aiohttp>=3.9.
Quick start
import asyncio
import aiohttp
from aioratio import RatioClient, JsonFileTokenStore
async def main() -> None:
async with aiohttp.ClientSession() as session:
client = RatioClient(
email="you@example.com",
password="...",
token_store=JsonFileTokenStore("ratio-tokens.json"),
session=session,
)
async with client:
chargers = await client.chargers_overview()
for c in chargers:
print(c.serial_number, c.cloud_connection_state)
await client.start_charge(chargers[0].serial_number,
vehicle_id="<your-vehicle-id>")
asyncio.run(main())
The token store persists access/refresh tokens and Cognito device metadata (DeviceKey, DeviceGroupKey, DevicePassword) so subsequent runs go through the DEVICE_SRP_AUTH fast path without re-prompting.
Public API
RatioClient is the entry point. Construct with email + password (and
optionally a TokenStore), use as an async context manager.
| Method | Returns | Notes |
|---|---|---|
login() |
None |
Force a fresh login. Idempotent. |
user_id() |
str |
Cognito sub claim from the IdToken. |
chargers() |
list[Charger] |
Bare charger registry. |
chargers_overview() |
list[ChargerOverview] |
Aggregate status of all chargers (single call). |
charger_overview(serial) |
ChargerOverview |
Single-charger full state. |
start_charge(serial, vehicle_id=None) |
None |
vehicle_id is optional but recommended; if omitted, the client sends an empty startCommandParameters object. |
stop_charge(serial) |
None |
|
user_settings(serial) |
UserSettings |
|
set_user_settings(serial, settings) |
None |
Accepts UserSettings dataclass (recommended) or a pre-formed camelCase dict. |
charge_schedule(serial) |
ChargeSchedule |
|
set_charge_schedule(serial, schedule) |
None |
|
solar_settings(serial) |
SolarSettings |
|
set_solar_settings(serial, settings) |
None |
Accepts SolarSettings dataclass (recommended) or a pre-formed camelCase dict. |
grant_upgrade_permission(serial, firmware_update_job_ids) |
None |
Approve queued firmware update jobs by id. Raises ValueError if the list is empty. |
diagnostics(serial) |
ChargerDiagnostics |
Read-only system info: hardware/firmware versions, network status (WiFi/ethernet), backend connectivity, OCPP status. |
ocpp_settings(serial) |
InstallerOcppSettings |
Read installer OCPP settings including is_change_allowed metadata per field. |
set_ocpp_settings(serial, settings) |
None |
Write OCPP settings (enabled, cpms, charge_point_identifier). Accepts InstallerOcppSettings or a flat dict. |
cpms_options(serial) |
list[CpmsConfig] |
List operator-provided CPMS choices. Returns [] on 403/error (operator may not expose this). |
session_history(...) |
SessionHistoryPage |
Paginated; pass next_token to continue. |
vehicles() |
list[Vehicle] |
|
add_vehicle(vehicle) |
Vehicle |
|
remove_vehicle(vehicle_id) |
None |
Errors:
RatioAuthError-- credentials invalid, refresh expired, unsupported Cognito challenge.RatioApiError-- non-2xx response from the REST API. Also raised when calling methods on a closed client.RatioRateLimitError-- HTTP 429.RatioConnectionError-- network/timeout failure.RatioError-- common base.
All public methods raise RatioApiError("client is closed") after close() has been called. User-supplied path segments (serial numbers, user IDs, vehicle IDs) are percent-encoded to prevent path traversal.
Token storage:
JsonFileTokenStore(path)— atomic writes, mode 0o600.MemoryTokenStore()— for tests / ephemeral CLI use.TokenStore(ABC) — implementload()/save()/clear()to plug into other backends (e.g. HA'sStorehelper).
Optional BLE support (aioratio[ble])
The charger speaks Inspiro IPC (null-byte-delimited JSON) over a single BLE
GATT service. aioratio.BleClient is the optional local-control counterpart
to the cloud RatioClient.
pip install aioratio[ble]
Cloud-only installs do not pull in bleak; the BLE subpackage is
lazy-imported. Touching aioratio.BleClient without the [ble] extras
raises RuntimeError with an install hint.
Quick start
import asyncio
from aioratio import BleClient
from bleak import BleakScanner
async def main() -> None:
device = await BleakScanner.find_device_by_address("AA:BB:CC:DD:EE:FF")
async with BleClient(device) as client:
info = await client.get_product_information()
status = await client.get_charger_status()
print(info.connectivity_controller.serial_number, status.indicators)
asyncio.run(main())
Public BLE API
BleClient is an async context manager (async with BleClient(device) as c:).
Construction options:
BleClient(device)— takes ableak.backends.device.BLEDevice. Prefer this whenever you already have aBLEDevicein hand (e.g. any caller routing through a BLE proxy).BleClient.from_address(address)— convenience for scripts; scans for the device first.BleClient.from_service_info(info)— accepts anything carrying aBLEDeviceon.device(duck-typed). Convenient when the caller is handed a discovery-record wrapper rather than the rawBLEDevice.
| Method | Returns | Notes |
|---|---|---|
connect() / disconnect() |
None |
Manual lifecycle; prefer async with. |
get_product_information() |
ProductInformationResponse |
Serial numbers, firmware/hardware versions. |
get_charger_status() |
ChargerStatusResponse |
Cloud-connection state + charging indicators. |
get_charger_sensor_values() |
ChargerSensorValuesResponse |
Live per-phase V/I. Voltages are deciV; use voltage_phase_{1,2,3}_volts / current_phase_{1,2,3}_amps. |
charge_control(control) |
ChargeControlResponse |
ChargeControl.START / STOP / PAUSE / RESUME. |
get_user_settings() / set_user_settings(update) |
… | Get returns SettableValue-wrapped fields ({value, isChangeAllowed, allowedValues?, lowerLimit?, upperLimit?}); set takes a flat UserSettingsUpdate. |
get_solar_settings() / set_solar_settings(update) |
… | Same shape asymmetry. |
get_time_settings() / set_time_settings(update) |
… | Same shape asymmetry. |
get_network_status() |
NetworkStatusResponse |
Wi-Fi / ethernet / IPv4 detail. SSID is base64 on the wire, decoded on .ssid. |
get_ocpp_status() / get_backend_status() |
… | central_system / url are base64 on the wire, decoded on the property. |
wifi_scan() |
list[WifiAccessPoint] |
Triggers a scan and follows up with one WifiAccessPointRequest per AP. |
wifi_connect(ssid, password) |
WifiConnectResponse |
SSID is base64-encoded for you. Password wire format is unverified — see warning. |
Each command is gated against the charger's reported Inspiro IPC protocol
version (read from the Version characteristic on connect); calls below the
minimum raise RatioBleUnsupportedCommandError before any wire write.
Discovery
Chargers advertise with manufacturer ID 0x0BFF (3071) and a local name
prefixed RATIO_. parse_advertisement accepts the same shape as
bleak.backends.scanner.AdvertisementData and returns None for non-Ratio
adverts:
from aioratio.ble import parse_advertisement
adv = parse_advertisement(adv_data.local_name, adv_data.manufacturer_data)
if adv is not None:
print(adv.local_name, hex(adv.manufacturer_byte))
Home Assistant's BluetoothServiceInfoBleak exposes .name (not
.local_name) and .manufacturer_data — use parse_service_info to skip
the field re-mapping in async_step_bluetooth:
from aioratio.ble import parse_service_info
adv = parse_service_info(discovery_info)
Note: the advertised manufacturer byte is not the IPC protocol version.
One charger advertises 0x03 but reports 0x06 (BASELINE_4_0_0) on the
Version characteristic — read the characteristic for the authoritative value.
Bonding and errors
The charger requires an SMP-bonded link before any GATT operation; the
Version characteristic read returns Insufficient Authentication until the
bond exists. Pairing uses a per-device PIN printed in the charger
documentation, so BleClient does not auto-bond — the caller is responsible
for bonding before connect() succeeds (BlueZ pairing agent on Linux, OS
pairing dialog on Windows / macOS).
When the GATT op fails for lack of a bond, BleClient.connect() raises
RatioBleNotBondedError (a RatioBleError subclass), distinct from the
generic RatioBleConnectionError, so callers can distinguish "needs pairing"
from "lost the link":
RatioBleError— common base for the optional BLE client.RatioBleConnectionError— transport-level failure (scan, connect, GATT).RatioBleNotBondedError— peer rejected GATT for lack of a bond.RatioBleProtocolError— Inspiro IPC framing or response error.RatioBleUnsupportedCommandError— command below the charger's protocol version.
scripts/ble_smoke.py walks the priority reads against a real charger
(excluded from the wheel).
Architecture
+-------------------+ +-----------------+ +------------+
| RatioClient | --> | _CloudTransport | --> | aiohttp |
| (public façade) | | (private) | +------------+
+-------------------+ +-----------------+
| |
| v 401 retry once via auth
v
+-------------------+ +-----------------+
| CognitoSrpAuth | --> | TokenStore |
| USER_SRP + | | (Memory/JSON) |
| DEVICE_SRP + | +-----------------+
| REFRESH_TOKEN |
+-------------------+
|
v
+-------------------+
| srp.py |
| Cognito SRP-6a |
+-------------------+
Files under src/aioratio/:
client.py— publicRatioClientasync context manager._transport.py-- private aiohttp transport. 401 -> invalidate_access_token -> retry once.auth.py--CognitoSrpAuthdriver: USER_SRP first-login, ConfirmDevice + UpdateDeviceStatus, REFRESH_TOKEN_AUTH (with rotation handling), DEVICE_SRP_AUTH second-login. Refresh serialised viaasyncio.Lock. Accepts atimeoutparameter (default 30s) for Cognito HTTP calls. Exposesinvalidate_access_token()for callers to force a refresh on the next access.srp.py— pure-Python Cognito SRP-6a (3072-bit MODP,Caldera Derived KeyHKDF, Java-style timestamp). Uses JavaBigInteger.toByteArray()(padHex) semantics throughout — variable length with0x00sign byte when high bit set. BothUserSrpandDeviceSrpvariants.token_store.py—TokenBundledataclass +TokenStoreABC +MemoryTokenStore+JsonFileTokenStore(atomic write).models/— dataclasses derived from APK DTOs:Charger,ChargerOverview,ChargerStatus,UserSettings,ChargeSchedule,SolarSettings,InstallerOcppSettings,CpmsConfig,ChargerDiagnostics,Session,SessionHistoryPage,Vehicle, plus nested types. All have afrom_dict()classmethod tolerant of unknown fields.InstallerOcppSettings.to_dict()emits the flat PUT shape;ChargerDiagnosticsis read-only (noto_dict).exceptions.py,const.py.
Cognito specifics (notes for maintainers and LLMs)
The Ratio user pool uses USER_SRP_AUTH with no client secret. Pool eu-west-1_mH4sFjLoF, client 78cs05mc0hc5ibqv1tui22n962, region eu-west-1, API base https://8q4y72fwo3.execute-api.eu-west-1.amazonaws.com/prod — all centralised in const.py.
Confirmed-live behaviours:
- First login flow:
InitiateAuth(USER_SRP_AUTH)→RespondToAuthChallenge(PASSWORD_VERIFIER)→ tokens +NewDeviceMetadata→ConfirmDevice(with our generated device verifier) →UpdateDeviceStatus(remembered). - Subsequent login on a remembered device: PASSWORD_VERIFIER →
DEVICE_SRP_AUTH→DEVICE_PASSWORD_VERIFIER→ tokens. REFRESH_TOKEN_AUTHdoes not rotate the refresh token (server keeps the existing one; library handles either case).compute_xuses the formatpoolName + username + ":" + password(no colon between pool and user). Allsalt,verifier,A,B,u,Suse JavaBigInteger.toByteArraybyte semantics, not fixed-length padding.start-chargerequiresstartCommandParametersto always be present in the body (empty object is accepted; missing object is not).
Development
git clone https://github.com/aaearon/aioratio
cd aioratio
uv venv --python 3.11 # or python3.11 -m venv .venv
.venv/bin/pip install -e '.[dev]'
.venv/bin/pytest -q
Tests are run with pytest. Live smoke against a real account
lives in scripts/smoke.py (reads creds from ../.env).
The SRP vector tests are intentionally self-consistent and do not catch
encoding bugs against Cognito — see commit 0283fb5 for two real bugs that
slipped past them. Live smoke is the source of truth.
Status
Early. Used in production by home-assistant-ratio against the Ratio Solar charger. Field nullability across some models is best-effort against the decompiled APK; flag mismatches as issues.
set_solar_settingsHTTP 502 (#9): Fixed. The cloud PUT endpoint expects flat nullable integers ("sunOffDelayMinutes": 5), not the nested value objects returned by GET ({"value": 5, "isChangeAllowed": true, ...}).SolarSettings.to_dict()now emits the correct PUT shape. Smoke-tested against the live API.ScheduleSlotandChargeSchedulenow have explicitto_dict()methods for controlled serialisation.UpperLowerLimitSetting.to_dict()now echoes back the full raw GET shape (used byUserSettings.to_dict()).
License
MIT.
Disclaimer
Unofficial. Reverse-engineered from the public Android APK and observed Cognito traffic. Use at your own risk; the API is not contractually stable. No affiliation with Ratio.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file aioratio-0.11.0.tar.gz.
File metadata
- Download URL: aioratio-0.11.0.tar.gz
- Upload date:
- Size: 96.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ec064134356fda99d43be2f33b71ee77ea7fea347b5d27e67cc3b580709c4011
|
|
| MD5 |
ce2429a1b23f1e1384bf1d53e3664596
|
|
| BLAKE2b-256 |
a9fd0fec9aaba5d1ad9ffb06c8c57f758fbfdd271fe8687457d488bf03aa7a08
|
Provenance
The following attestation bundles were made for aioratio-0.11.0.tar.gz:
Publisher:
publish.yml on aaearon/aioratio
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
aioratio-0.11.0.tar.gz -
Subject digest:
ec064134356fda99d43be2f33b71ee77ea7fea347b5d27e67cc3b580709c4011 - Sigstore transparency entry: 1549077730
- Sigstore integration time:
-
Permalink:
aaearon/aioratio@f2a9671a10db0fe034040a093511793b690256cc -
Branch / Tag:
refs/tags/v0.11.0 - Owner: https://github.com/aaearon
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@f2a9671a10db0fe034040a093511793b690256cc -
Trigger Event:
push
-
Statement type:
File details
Details for the file aioratio-0.11.0-py3-none-any.whl.
File metadata
- Download URL: aioratio-0.11.0-py3-none-any.whl
- Upload date:
- Size: 65.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2956fc8b026c15c12b03da1ac6e33e9c4ff25674a4ce7b4e0707af017e8ea15f
|
|
| MD5 |
8adf472224b43bdcc7f2733fca306024
|
|
| BLAKE2b-256 |
8e33823240b59414cd42bf4cc178d3ab335dc911fe30edb3a7231af48ff89932
|
Provenance
The following attestation bundles were made for aioratio-0.11.0-py3-none-any.whl:
Publisher:
publish.yml on aaearon/aioratio
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
aioratio-0.11.0-py3-none-any.whl -
Subject digest:
2956fc8b026c15c12b03da1ac6e33e9c4ff25674a4ce7b4e0707af017e8ea15f - Sigstore transparency entry: 1549077822
- Sigstore integration time:
-
Permalink:
aaearon/aioratio@f2a9671a10db0fe034040a093511793b690256cc -
Branch / Tag:
refs/tags/v0.11.0 - Owner: https://github.com/aaearon
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@f2a9671a10db0fe034040a093511793b690256cc -
Trigger Event:
push
-
Statement type: