Async Python client for the CatGenie AI smart litter box API by PetNovations
Project description
catgenie-api
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.
Features
- 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
How It Works
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.
Installation
pip install catgenie
Or for development:
git clone https://github.com/kclif9/catgenieapi
cd catgenie-api
pip install -e ".[dev]"
Usage
Authentication
import asyncio
from catgenie import CatGenieAuth, CatGenieClient
async def main():
async with CatGenieAuth() as auth:
# 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)
# Step 3 — use the client
async with CatGenieClient(credentials) as client:
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}")
asyncio.run(main())
Controlling a Device
async with CatGenieClient(credentials) as client:
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)
Token Persistence
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))
Shared Session (Home Assistant pattern)
from curl_cffi.requests import AsyncSession, Response
from catgenie import CatGenieAuth, CatGenieClient
from catgenie.const import TLS_IMPERSONATE
async with AsyncSession(impersonate=TLS_IMPERSONATE) as session:
async with CatGenieAuth(session=session) as auth:
credentials = await auth.login(...)
async with CatGenieClient(credentials, session=session) as client:
devices = await client.get_devices()
API Reference
CatGenieAuth
| 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 |
CatGenieClient
| 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 |
Key Models
| 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 |
Project Structure
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
Prior Art & Credits
- 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)
License
MIT
Project details
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 catgenie-0.1.0.tar.gz.
File metadata
- Download URL: catgenie-0.1.0.tar.gz
- Upload date:
- Size: 28.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
be6929a644d65f194ed1d5cc19dc4235214ff87c268ba8bd2863be6a2f00dfeb
|
|
| MD5 |
e07cefe7df260b63cf77544f03074b2d
|
|
| BLAKE2b-256 |
0f7d0b4562f9eaa5283d89318991ebbe3e63acaf5fbaeb91f5217ff3160ac8e4
|
Provenance
The following attestation bundles were made for catgenie-0.1.0.tar.gz:
Publisher:
publish.yml on kclif9/catgenie-api
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
catgenie-0.1.0.tar.gz -
Subject digest:
be6929a644d65f194ed1d5cc19dc4235214ff87c268ba8bd2863be6a2f00dfeb - Sigstore transparency entry: 1399382052
- Sigstore integration time:
-
Permalink:
kclif9/catgenie-api@3e5131a7c396114859dcae4f2dd11dac70f2741a -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/kclif9
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@3e5131a7c396114859dcae4f2dd11dac70f2741a -
Trigger Event:
release
-
Statement type:
File details
Details for the file catgenie-0.1.0-py3-none-any.whl.
File metadata
- Download URL: catgenie-0.1.0-py3-none-any.whl
- Upload date:
- Size: 18.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 |
6a4b496dc2bc0c2f8f37792d455a50e10b48cb1df56307895b615eca59748b22
|
|
| MD5 |
9b164bf67026e46cbf1d19f9e7c05165
|
|
| BLAKE2b-256 |
4663a18a195953394682eeffadd421d4f81360be3e11de94c10bd7037bdd3d34
|
Provenance
The following attestation bundles were made for catgenie-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on kclif9/catgenie-api
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
catgenie-0.1.0-py3-none-any.whl -
Subject digest:
6a4b496dc2bc0c2f8f37792d455a50e10b48cb1df56307895b615eca59748b22 - Sigstore transparency entry: 1399382070
- Sigstore integration time:
-
Permalink:
kclif9/catgenie-api@3e5131a7c396114859dcae4f2dd11dac70f2741a -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/kclif9
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@3e5131a7c396114859dcae4f2dd11dac70f2741a -
Trigger Event:
release
-
Statement type: