A reverse-engineered async Python library for the CatGenie AI smart litter box by PetNovations. Provides full authentication, device status, and cleaning cycle control — designed as the foundation for a Home Assistant integration.
- Phone + SMS authentication — full login flow using the same OTP mechanism as the official app
- Automatic token refresh — access tokens (~30 min) are transparently refreshed using the long-lived refresh token (~10 years)
- Device status & control — start/stop cleaning cycles, read configuration, operation state, and sani-solution level
- Fully typed Pydantic v2 models — every API response is validated into typed Python objects; no raw dicts
- PEP 561 compliant — ships
py.typed, zero mypy errors with strict settings - Home Assistant ready — async context managers, shared session injection, and a clean model hierarchy that maps directly to HA entities
The CatGenie app communicates with iot.petnovations.com (CloudFront-fronted AWS API Gateway). Every request requires three custom headers derived from an AES-CBC encrypted timestamp and HMAC-SHA256 signatures. The phone number itself is AES-encrypted in the auth request body.
There is one significant non-obvious requirement: PetNovations' edge silently discards requests from Python's standard SSL stack, returning 200 OK with an empty body instead of sending the SMS or returning data. This library uses curl_cffi to impersonate a Chrome Android TLS fingerprint, which is the only way to reliably trigger the SMS and receive API responses.
pip install catgenieOr for development:
git clone https://github.com/kclif9/catgenieapi
cd catgenie-api
pip install -e ".[dev]"import asyncio
from catgenie import CatGenieAuth, CatGenieClient
async def main():
auth = CatGenieAuth()
# Step 1 — trigger SMS to the phone number
await auth.request_login_code(country_code=61, phone="499999999")
# Step 2 — exchange the SMS code for tokens
code = input("Enter SMS code: ")
credentials = await auth.login(country_code=61, phone="499999999", code=code)
await auth.async_close()
# Step 3 — use the client
client = CatGenieClient(credentials)
try:
devices = await client.get_devices()
for device in devices:
print(f"{device.name}: {'online' if device.is_online else 'offline'}")
print(f" Sani-solution remaining: {device.remaining_sani_solution}%")
print(f" Lifetime cycles: {device.configuration.total_cycles}")
finally:
await client.async_close()
asyncio.run(main())client = CatGenieClient(credentials)
try:
devices = await client.get_devices()
device = devices[0]
# Start a cleaning cycle
await client.start_cleaning(device.manufacturer_id)
# Stop a cleaning cycle
await client.stop_cleaning(device.manufacturer_id)
finally:
await client.async_close()import json, dataclasses
from catgenie import Credentials
# Save after login
with open("credentials.json", "w") as f:
json.dump(dataclasses.asdict(credentials), f)
# Restore on next run
with open("credentials.json") as f:
credentials = Credentials(**json.load(f))from curl_cffi.requests import AsyncSession, Response
from catgenie import CatGenieAuth, CatGenieClient
from catgenie.const import TLS_IMPERSONATE
session = AsyncSession(impersonate=TLS_IMPERSONATE)
try:
auth = CatGenieAuth(session=session)
credentials = await auth.login(...)
client = CatGenieClient(credentials, session=session)
devices = await client.get_devices()
finally:
await session.close()| Method | Description |
|---|---|
get_base_url(country_code, phone) |
Preflight config call (mirrors app behaviour) |
request_login_code(country_code, phone) |
Triggers SMS OTP to the phone number |
login(country_code, phone, code) |
Exchanges OTP for Credentials |
refresh() |
Refreshes the access token using the refresh token |
| Method | Returns | Description |
|---|---|---|
get_devices() |
list[Device] |
All devices on the account |
get_device_status(device_id) |
dict |
Raw operation status |
start_cleaning(device_id) |
dict |
Start a cleaning cycle |
stop_cleaning(device_id) |
dict |
Stop a cleaning cycle |
get_account() |
dict |
User profile |
get_pets() |
list[dict] |
Pet profiles |
get_pet_statistics() |
dict |
Per-pet usage statistics |
get_notifications() |
NotificationList |
Push notification history |
get_notification_settings() |
dict |
Push notification preferences |
get_firmware_info(manufacturer_id) |
dict |
Available firmware update |
get_mainboard(manufacturer_id) |
dict |
Mainboard hardware info |
| Model | Notable properties |
|---|---|
Device |
unique_id, name, is_online, is_cleaning, remaining_sani_solution, last_clean, fw_version |
DeviceConfiguration |
total_cycles, mode (CleaningMode), cat_delay, schedule, binary_elements |
OperationStatus |
is_cleaning, clean_progress_pct (0–100 while running, None when idle) |
ActivationInfo |
count (lifetime cleaning cycles), date (first activation) |
Notification / NotificationData |
parsed_data property auto-parses the nested JSON payload |
Credentials |
access_token, refresh_token, is_token_expired |
src/catgenie/
├── __init__.py # Public API exports
├── py.typed # PEP 561 marker
├── auth.py # Phone + SMS authentication (CatGenieAuth, Credentials)
├── client.py # High-level async client (CatGenieClient)
├── const.py # Constants and all endpoint paths
├── models.py # Pydantic v2 models for every API response
└── signing.py # AES-CBC encryption + HMAC-SHA256 request signing
- Rukongai/CatDjinni — reverse engineering research: signing algorithm, Frida scripts
- appkins/hass-catgenie — earlier HA integration (requires manual refresh token)
- PrimeAutomation/petnovations — earlier HA integration (same limitation)
MIT