python-casahome 0.3.0__tar.gz → 0.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- python_casahome-0.3.1/PKG-INFO +272 -0
- python_casahome-0.3.1/README.md +252 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/pyproject.toml +1 -1
- {python_casahome-0.3.0 → python_casahome-0.3.1}/src/casahome/__init__.py +1 -1
- {python_casahome-0.3.0 → python_casahome-0.3.1}/uv.lock +1 -1
- python_casahome-0.3.0/PKG-INFO +0 -458
- python_casahome-0.3.0/README.md +0 -438
- {python_casahome-0.3.0 → python_casahome-0.3.1}/.gitignore +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/.python-version +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/LICENSE +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/examples/bootstrap.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/src/casahome/_base_http.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/src/casahome/_common.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/src/casahome/api.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/src/casahome/auth.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/src/casahome/client.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/src/casahome/const.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/src/casahome/exceptions.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/src/casahome/mail_workflows.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/src/casahome/models/__init__.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/src/casahome/models/_base.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/src/casahome/models/ai.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/src/casahome/models/assets.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/src/casahome/models/availability.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/src/casahome/models/bootstrap.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/src/casahome/models/calendar.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/src/casahome/models/catalogs.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/src/casahome/models/credits.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/src/casahome/models/documents.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/src/casahome/models/entity_publish.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/src/casahome/models/enums.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/src/casahome/models/feed.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/src/casahome/models/help_center.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/src/casahome/models/home.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/src/casahome/models/images.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/src/casahome/models/mail_workflows.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/src/casahome/models/mailbox.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/src/casahome/models/maintenance.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/src/casahome/models/membership.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/src/casahome/models/paint.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/src/casahome/models/preferences.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/src/casahome/models/purchases.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/src/casahome/models/read_todos.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/src/casahome/models/recovered_contracts.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/src/casahome/models/referral.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/src/casahome/models/requests.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/src/casahome/models/tasks.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/src/casahome/models/taxes.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/src/casahome/models/todos.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/src/casahome/models/verified_writes.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/src/casahome/models/visits.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/src/casahome/models/workflows.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/src/casahome/models/write_contracts.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/src/casahome/py.typed +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/src/casahome/recovered.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/src/casahome/streaming.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/src/casahome/uploads.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/src/casahome/workflows.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/src/casahome/writes.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/tests/__init__.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/tests/conftest.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/tests/fixtures/bootstrap.json +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/tests/fixtures/error.json +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/tests/fixtures/token_refresh.json +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/tests/test_api.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/tests/test_auth.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/tests/test_catalog_and_room_writes.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/tests/test_client.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/tests/test_document_structure_response.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/tests/test_inventory_models.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/tests/test_mail_workflows.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/tests/test_models.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/tests/test_observed_contracts.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/tests/test_read_coverage.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/tests/test_recovered_contracts.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/tests/test_static_reads.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/tests/test_streaming.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/tests/test_upload_security.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/tests/test_verified_writes.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/tests/test_workflows.py +0 -0
- {python_casahome-0.3.0 → python_casahome-0.3.1}/tests/test_write_contracts.py +0 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: python-casahome
|
|
3
|
+
Version: 0.3.1
|
|
4
|
+
Summary: Unofficial async client for the Casa (getcasa.com) member API
|
|
5
|
+
Project-URL: Repository, https://github.com/borski/python-casahome
|
|
6
|
+
Project-URL: Homepage, https://github.com/borski/python-casahome
|
|
7
|
+
Project-URL: Issues, https://github.com/borski/python-casahome/issues
|
|
8
|
+
Author: Borski
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: aiohttp,api,async,casa,home
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Framework :: AsyncIO
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Typing :: Typed
|
|
16
|
+
Requires-Python: >=3.11
|
|
17
|
+
Requires-Dist: aiohttp>=3.9
|
|
18
|
+
Requires-Dist: pydantic>=2.5
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# python-casahome
|
|
22
|
+
|
|
23
|
+
An async Python client for the [Casa](https://getcasa.com) member API. This is
|
|
24
|
+
an unofficial library, unaffiliated with Casa. It uses a private API that Casa
|
|
25
|
+
can change without notice.
|
|
26
|
+
|
|
27
|
+
The current source version is 0.3.1. The package is alpha software. Some calls
|
|
28
|
+
were observed in app traffic, while others were reconstructed from the app and
|
|
29
|
+
have never been checked against Casa's backend. The SDK doesn't cover the full
|
|
30
|
+
app or backend API. See the method docstrings for the evidence and limits of a
|
|
31
|
+
particular call.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
pip install python-casahome
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Python 3.11 or newer is required. To work from a source checkout, use
|
|
40
|
+
`uv sync --locked`.
|
|
41
|
+
|
|
42
|
+
## Authentication
|
|
43
|
+
|
|
44
|
+
Casa uses Firebase phone authentication. This library starts with a Firebase
|
|
45
|
+
refresh token and exchanges it for short lived ID tokens. It doesn't implement
|
|
46
|
+
phone or SMS sign in. There is no official Casa token export. If you inspect
|
|
47
|
+
your own app session to obtain a token, keep it out of logs and source control.
|
|
48
|
+
Treat it like a password.
|
|
49
|
+
|
|
50
|
+
Firebase can rotate refresh tokens. For a long lived client, construct
|
|
51
|
+
`FirebaseAuth` with `on_refresh_token_rotated` and store the new token when the
|
|
52
|
+
callback runs. The callback can be synchronous or asynchronous. Callback
|
|
53
|
+
failures are logged and don't fail the request, so verify that storage
|
|
54
|
+
succeeded.
|
|
55
|
+
|
|
56
|
+
## Quick start
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
import asyncio
|
|
60
|
+
|
|
61
|
+
import aiohttp
|
|
62
|
+
|
|
63
|
+
from casahome import CasaApi, CasaClient, FirebaseAuth
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
async def main() -> None:
|
|
67
|
+
async with aiohttp.ClientSession() as session:
|
|
68
|
+
auth = FirebaseAuth(
|
|
69
|
+
session,
|
|
70
|
+
"YOUR_REFRESH_TOKEN",
|
|
71
|
+
on_refresh_token_rotated=save_token,
|
|
72
|
+
)
|
|
73
|
+
api = CasaApi(
|
|
74
|
+
CasaClient(session, auth),
|
|
75
|
+
device_id="YOUR_PERSISTED_DEVICE_ID",
|
|
76
|
+
)
|
|
77
|
+
me = await api.bootstrap()
|
|
78
|
+
for home in me.homes:
|
|
79
|
+
print(home.status)
|
|
80
|
+
print(await api.weather(home.id))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
asyncio.run(main())
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Define `save_token` to write to your secret store and fail if the write doesn't
|
|
87
|
+
succeed. Replace `YOUR_PERSISTED_DEVICE_ID` with a stable value that you retain
|
|
88
|
+
between runs.
|
|
89
|
+
|
|
90
|
+
`bootstrap` registers a device. Persist `api.device_id` and pass it as
|
|
91
|
+
`device_id` on later starts. Otherwise each start registers a new device.
|
|
92
|
+
`CasaApi.create` is a convenience for short scripts, but it doesn't install a
|
|
93
|
+
token rotation callback. Use the explicit setup above when the token must
|
|
94
|
+
survive restarts.
|
|
95
|
+
|
|
96
|
+
If you own an `aiohttp.ClientSession`, you can reuse it:
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from casahome import CasaApi, CasaClient, FirebaseAuth
|
|
100
|
+
|
|
101
|
+
auth = FirebaseAuth(session, refresh_token, on_refresh_token_rotated=save_token)
|
|
102
|
+
api = CasaApi(CasaClient(session, auth), device_id=stored_device_id)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The caller owns that session and must close it. `CasaApi.create` owns and closes
|
|
106
|
+
its session instead. A runnable bootstrap example is in
|
|
107
|
+
`examples/bootstrap.py`. It doesn't persist a rotated token. Its `--show-token`
|
|
108
|
+
option prints the token to the terminal, so don't use it in captured logs. Set
|
|
109
|
+
`CASA_REFRESH_TOKEN` in your shell to run it:
|
|
110
|
+
|
|
111
|
+
```sh
|
|
112
|
+
uv run examples/bootstrap.py
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## API coverage and evidence
|
|
116
|
+
|
|
117
|
+
Read methods include `home_details`, `inventory_item`, `weather`,
|
|
118
|
+
`credits_summary`, `requests_tab`, `request_details`, `todo_details`,
|
|
119
|
+
`documents`, `document_suggestions`, `autopilot_plans`, `mail_items`,
|
|
120
|
+
`packages` and `calendar`. Some routes were observed with populated items.
|
|
121
|
+
Others were observed only with empty lists, or have untested query variants.
|
|
122
|
+
For example, `document_suggestions` was observed with `view="all"` but other
|
|
123
|
+
filter values aren't confirmed. Several visit and task detail reads remain
|
|
124
|
+
unverified against the backend.
|
|
125
|
+
|
|
126
|
+
Observed write paths include `move_entity_to_room`, `rename_room`,
|
|
127
|
+
`send_ai_message`, `send_request_message`, `create_todo_result`, `delete_todo`,
|
|
128
|
+
`answer_todo_question`, `update_autopilot_plan`,
|
|
129
|
+
`update_notification_settings`, `update_access_preferences` and
|
|
130
|
+
`upload_document`. An observed path doesn't verify every optional input. For
|
|
131
|
+
example, `save_and_publish_entity` was observed with `model.name` and empty
|
|
132
|
+
`props`. The shared compiled codec supports ID and name selections, but
|
|
133
|
+
model ID traffic hasn't been verified. The SDK doesn't expose clearing make or
|
|
134
|
+
model. The app's compiled clear case encodes JSON null, but backend clearing
|
|
135
|
+
behavior is unknown.
|
|
136
|
+
|
|
137
|
+
Methods marked `UNVERIFIED` are based on static app reconstruction and local
|
|
138
|
+
synthetic tests, not a successful backend call. They include
|
|
139
|
+
`update_document`, `update_schedule_preferences`, `update_availability_slots`,
|
|
140
|
+
`cancel_vendor_visit`, `update_entity_properties` and the newer
|
|
141
|
+
`update_request_detail`, `dismiss_document_structure_suggestion`,
|
|
142
|
+
`update_task`, `delete_task`, `transfer_task_to_concierge`, `delete_document`,
|
|
143
|
+
`update_room_properties`, `update_story`, `retailer_discounts`,
|
|
144
|
+
`create_support_request` and `mark_vendor_visit_conversation_read`.
|
|
145
|
+
`update_document` is a PATCH method, with unverified date and clearing effects.
|
|
146
|
+
`create_support_request` may contact a person. A message, `ok` or `deleted`
|
|
147
|
+
field in a response must be read as returned. HTTP success alone doesn't
|
|
148
|
+
establish the action's business effect.
|
|
149
|
+
|
|
150
|
+
Use `casahome.is_unverified(api.update_document)` to check a method's marker.
|
|
151
|
+
Read its docstring for finer distinctions. An unmarked method can still have
|
|
152
|
+
untested variants. Response models live in `casahome.models`. They keep unknown
|
|
153
|
+
fields in `model_extra` and distinguish response envelopes from their nested
|
|
154
|
+
items. A schema mismatch raises `CasaResponseError`.
|
|
155
|
+
|
|
156
|
+
This SDK is separate from any MCP or Home Assistant adapter. A library method
|
|
157
|
+
doesn't imply that an adapter exposes it. Adapters choose their own limited
|
|
158
|
+
tools and action gates.
|
|
159
|
+
|
|
160
|
+
## Write behavior
|
|
161
|
+
|
|
162
|
+
Each ordinary Casa mutation is sent once. The client force refreshes the ID
|
|
163
|
+
token before sending it and doesn't replay the mutation after a 401. Normal GET
|
|
164
|
+
requests retry once after a 401. `bootstrap` is a deliberate exception: it is a
|
|
165
|
+
POST that registers a device and keeps the one time 401 retry. `upload_document`
|
|
166
|
+
is a multi step workflow with a policy GET and three write steps. Each step is
|
|
167
|
+
attempted once, and its Cloud Storage PUT uses no Casa token. A response parsing
|
|
168
|
+
failure, timeout or connection error after a mutation can leave its outcome
|
|
169
|
+
unknown. Check the resulting state before deciding whether to send another
|
|
170
|
+
request. The `Idempotency-Key` on `send_ai_message` helps trace a send, but
|
|
171
|
+
server deduplication hasn't been verified.
|
|
172
|
+
|
|
173
|
+
Write bodies use strict validation. Invalid or unknown input fields raise
|
|
174
|
+
`CasaRequestError` before the write. Optional `None` values generally omit a
|
|
175
|
+
field, while empty strings and lists are sent as supplied. Some contracts send
|
|
176
|
+
explicit null, including the access preference fields. In
|
|
177
|
+
`UpdateDocumentBody`, omitting `document_date` leaves the key out, and
|
|
178
|
+
explicit `document_date=None` sends null. Don't treat those forms as
|
|
179
|
+
interchangeable. The backend effect of document date clearing is unverified.
|
|
180
|
+
|
|
181
|
+
Path identifiers must be nonempty safe segments. They aren't normalized to
|
|
182
|
+
UUIDs. Responses can contain home addresses, coordinates, phone numbers, email
|
|
183
|
+
addresses, account identifiers, payment and membership metadata, private
|
|
184
|
+
messages, documents, purchase details and short lived signed URLs. Avoid
|
|
185
|
+
logging complete models, request paths, event payloads or read exceptions.
|
|
186
|
+
Response URLs aren't followed unless a documented workflow requires it.
|
|
187
|
+
`upload_document` follows only its validated Cloud Storage session URL.
|
|
188
|
+
|
|
189
|
+
## Document upload
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
from pathlib import Path
|
|
193
|
+
|
|
194
|
+
result = await api.upload_document(home_id, Path("inspection.pdf"), "inspection.pdf")
|
|
195
|
+
print(result.document.id, result.already_exists)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
`upload_document` first reads `document_types` to check MIME type and file
|
|
199
|
+
size. It reserves a session, uploads the bytes to the signed Cloud Storage URL
|
|
200
|
+
Casa returns, then registers the document. `find_todos=True` asks Casa to look
|
|
201
|
+
for suggested tasks. Each step is attempted once. An error in a later step
|
|
202
|
+
doesn't undo an earlier one.
|
|
203
|
+
|
|
204
|
+
`source` can be bytes or a local path. The MIME type is guessed from `filename`
|
|
205
|
+
when omitted and must appear in Casa's allowed MIME types. Local paths must
|
|
206
|
+
open as nonempty regular files. Symlinks to regular files are allowed. The read
|
|
207
|
+
is bounded by Casa's reported file size limit, plus one byte for detecting an
|
|
208
|
+
oversized file. The entire file is held in memory for one storage request. The
|
|
209
|
+
client timeout bounds the file read and storage request, but cancellation can't
|
|
210
|
+
stop a filesystem call already running in a worker thread.
|
|
211
|
+
|
|
212
|
+
The filename must contain 1 to 1,024 characters, include a nonwhitespace
|
|
213
|
+
character and contain no controls. The signed URL can contain at most 8,192
|
|
214
|
+
characters, must contain no embedded credentials and must use HTTPS on
|
|
215
|
+
`storage.googleapis.com` at port 443. A separate storage session sends no Casa
|
|
216
|
+
credentials, caller headers, cookies, environment proxy settings or netrc
|
|
217
|
+
configuration. It doesn't follow redirects and accepts only HTTP 200 or 201.
|
|
218
|
+
Keep signed session URLs private. See the `upload_document` and
|
|
219
|
+
`upload_document_content` docstrings for the complete contract.
|
|
220
|
+
|
|
221
|
+
## Casa AI events
|
|
222
|
+
|
|
223
|
+
`casa_ai_events` is an opt in server sent event stream. A live connection and
|
|
224
|
+
some update traffic were observed, but the full event and reconnect behavior
|
|
225
|
+
isn't verified. The generator doesn't reconnect automatically. Close it when
|
|
226
|
+
you stop reading early:
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
from contextlib import aclosing
|
|
230
|
+
|
|
231
|
+
from casahome.models import CasaAIMessageUpsertChange
|
|
232
|
+
from casahome.streaming import CasaAIResyncRequiredEvent, CasaAIThreadUpdateEvent
|
|
233
|
+
|
|
234
|
+
async with aclosing(api.casa_ai_events(home_id, after=saved_cursor)) as events:
|
|
235
|
+
async for event in events:
|
|
236
|
+
if event.id is not None:
|
|
237
|
+
saved_cursor = event.id
|
|
238
|
+
if isinstance(event, CasaAIResyncRequiredEvent):
|
|
239
|
+
break # Reload state before subscribing again.
|
|
240
|
+
if isinstance(event, CasaAIThreadUpdateEvent):
|
|
241
|
+
for change in event.update().changes:
|
|
242
|
+
if isinstance(change, CasaAIMessageUpsertChange):
|
|
243
|
+
print(change.message.sender_role, change.message.content)
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
An `after` cursor is opaque. Store it unchanged. `event.update()` validates the
|
|
247
|
+
decoded thread update. `event.payload` retains the decoded JSON value, while
|
|
248
|
+
`event.data` retains the original event text. Other change types remain
|
|
249
|
+
available as `CasaAIUnknownChange`. The stream limits lines to 64 KiB, events to
|
|
250
|
+
256 KiB and cursors to 2,048 characters without controls. The cursor is sent as
|
|
251
|
+
both `after` and `Last-Event-ID`. Redirects are disabled. A 60 second socket
|
|
252
|
+
read gap raises a connection error. HTTP 204 ends the generator, and incomplete
|
|
253
|
+
event framing at EOF is discarded. `resync_required` is yielded once before
|
|
254
|
+
the response closes. The GET can refresh an expired token and retry once on
|
|
255
|
+
401. That retry behavior doesn't apply to ordinary mutations.
|
|
256
|
+
|
|
257
|
+
## Development
|
|
258
|
+
|
|
259
|
+
```sh
|
|
260
|
+
uv sync
|
|
261
|
+
uv run --locked pytest -q
|
|
262
|
+
uv run --locked ruff check .
|
|
263
|
+
uv run --locked ruff format --check .
|
|
264
|
+
uv run --locked mypy src tests
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Tests use synthetic data and a loopback server. They don't call Casa or need
|
|
268
|
+
credentials.
|
|
269
|
+
|
|
270
|
+
## License
|
|
271
|
+
|
|
272
|
+
MIT. See `LICENSE`.
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# python-casahome
|
|
2
|
+
|
|
3
|
+
An async Python client for the [Casa](https://getcasa.com) member API. This is
|
|
4
|
+
an unofficial library, unaffiliated with Casa. It uses a private API that Casa
|
|
5
|
+
can change without notice.
|
|
6
|
+
|
|
7
|
+
The current source version is 0.3.1. The package is alpha software. Some calls
|
|
8
|
+
were observed in app traffic, while others were reconstructed from the app and
|
|
9
|
+
have never been checked against Casa's backend. The SDK doesn't cover the full
|
|
10
|
+
app or backend API. See the method docstrings for the evidence and limits of a
|
|
11
|
+
particular call.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
pip install python-casahome
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Python 3.11 or newer is required. To work from a source checkout, use
|
|
20
|
+
`uv sync --locked`.
|
|
21
|
+
|
|
22
|
+
## Authentication
|
|
23
|
+
|
|
24
|
+
Casa uses Firebase phone authentication. This library starts with a Firebase
|
|
25
|
+
refresh token and exchanges it for short lived ID tokens. It doesn't implement
|
|
26
|
+
phone or SMS sign in. There is no official Casa token export. If you inspect
|
|
27
|
+
your own app session to obtain a token, keep it out of logs and source control.
|
|
28
|
+
Treat it like a password.
|
|
29
|
+
|
|
30
|
+
Firebase can rotate refresh tokens. For a long lived client, construct
|
|
31
|
+
`FirebaseAuth` with `on_refresh_token_rotated` and store the new token when the
|
|
32
|
+
callback runs. The callback can be synchronous or asynchronous. Callback
|
|
33
|
+
failures are logged and don't fail the request, so verify that storage
|
|
34
|
+
succeeded.
|
|
35
|
+
|
|
36
|
+
## Quick start
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
import asyncio
|
|
40
|
+
|
|
41
|
+
import aiohttp
|
|
42
|
+
|
|
43
|
+
from casahome import CasaApi, CasaClient, FirebaseAuth
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def main() -> None:
|
|
47
|
+
async with aiohttp.ClientSession() as session:
|
|
48
|
+
auth = FirebaseAuth(
|
|
49
|
+
session,
|
|
50
|
+
"YOUR_REFRESH_TOKEN",
|
|
51
|
+
on_refresh_token_rotated=save_token,
|
|
52
|
+
)
|
|
53
|
+
api = CasaApi(
|
|
54
|
+
CasaClient(session, auth),
|
|
55
|
+
device_id="YOUR_PERSISTED_DEVICE_ID",
|
|
56
|
+
)
|
|
57
|
+
me = await api.bootstrap()
|
|
58
|
+
for home in me.homes:
|
|
59
|
+
print(home.status)
|
|
60
|
+
print(await api.weather(home.id))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
asyncio.run(main())
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Define `save_token` to write to your secret store and fail if the write doesn't
|
|
67
|
+
succeed. Replace `YOUR_PERSISTED_DEVICE_ID` with a stable value that you retain
|
|
68
|
+
between runs.
|
|
69
|
+
|
|
70
|
+
`bootstrap` registers a device. Persist `api.device_id` and pass it as
|
|
71
|
+
`device_id` on later starts. Otherwise each start registers a new device.
|
|
72
|
+
`CasaApi.create` is a convenience for short scripts, but it doesn't install a
|
|
73
|
+
token rotation callback. Use the explicit setup above when the token must
|
|
74
|
+
survive restarts.
|
|
75
|
+
|
|
76
|
+
If you own an `aiohttp.ClientSession`, you can reuse it:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from casahome import CasaApi, CasaClient, FirebaseAuth
|
|
80
|
+
|
|
81
|
+
auth = FirebaseAuth(session, refresh_token, on_refresh_token_rotated=save_token)
|
|
82
|
+
api = CasaApi(CasaClient(session, auth), device_id=stored_device_id)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The caller owns that session and must close it. `CasaApi.create` owns and closes
|
|
86
|
+
its session instead. A runnable bootstrap example is in
|
|
87
|
+
`examples/bootstrap.py`. It doesn't persist a rotated token. Its `--show-token`
|
|
88
|
+
option prints the token to the terminal, so don't use it in captured logs. Set
|
|
89
|
+
`CASA_REFRESH_TOKEN` in your shell to run it:
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
uv run examples/bootstrap.py
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## API coverage and evidence
|
|
96
|
+
|
|
97
|
+
Read methods include `home_details`, `inventory_item`, `weather`,
|
|
98
|
+
`credits_summary`, `requests_tab`, `request_details`, `todo_details`,
|
|
99
|
+
`documents`, `document_suggestions`, `autopilot_plans`, `mail_items`,
|
|
100
|
+
`packages` and `calendar`. Some routes were observed with populated items.
|
|
101
|
+
Others were observed only with empty lists, or have untested query variants.
|
|
102
|
+
For example, `document_suggestions` was observed with `view="all"` but other
|
|
103
|
+
filter values aren't confirmed. Several visit and task detail reads remain
|
|
104
|
+
unverified against the backend.
|
|
105
|
+
|
|
106
|
+
Observed write paths include `move_entity_to_room`, `rename_room`,
|
|
107
|
+
`send_ai_message`, `send_request_message`, `create_todo_result`, `delete_todo`,
|
|
108
|
+
`answer_todo_question`, `update_autopilot_plan`,
|
|
109
|
+
`update_notification_settings`, `update_access_preferences` and
|
|
110
|
+
`upload_document`. An observed path doesn't verify every optional input. For
|
|
111
|
+
example, `save_and_publish_entity` was observed with `model.name` and empty
|
|
112
|
+
`props`. The shared compiled codec supports ID and name selections, but
|
|
113
|
+
model ID traffic hasn't been verified. The SDK doesn't expose clearing make or
|
|
114
|
+
model. The app's compiled clear case encodes JSON null, but backend clearing
|
|
115
|
+
behavior is unknown.
|
|
116
|
+
|
|
117
|
+
Methods marked `UNVERIFIED` are based on static app reconstruction and local
|
|
118
|
+
synthetic tests, not a successful backend call. They include
|
|
119
|
+
`update_document`, `update_schedule_preferences`, `update_availability_slots`,
|
|
120
|
+
`cancel_vendor_visit`, `update_entity_properties` and the newer
|
|
121
|
+
`update_request_detail`, `dismiss_document_structure_suggestion`,
|
|
122
|
+
`update_task`, `delete_task`, `transfer_task_to_concierge`, `delete_document`,
|
|
123
|
+
`update_room_properties`, `update_story`, `retailer_discounts`,
|
|
124
|
+
`create_support_request` and `mark_vendor_visit_conversation_read`.
|
|
125
|
+
`update_document` is a PATCH method, with unverified date and clearing effects.
|
|
126
|
+
`create_support_request` may contact a person. A message, `ok` or `deleted`
|
|
127
|
+
field in a response must be read as returned. HTTP success alone doesn't
|
|
128
|
+
establish the action's business effect.
|
|
129
|
+
|
|
130
|
+
Use `casahome.is_unverified(api.update_document)` to check a method's marker.
|
|
131
|
+
Read its docstring for finer distinctions. An unmarked method can still have
|
|
132
|
+
untested variants. Response models live in `casahome.models`. They keep unknown
|
|
133
|
+
fields in `model_extra` and distinguish response envelopes from their nested
|
|
134
|
+
items. A schema mismatch raises `CasaResponseError`.
|
|
135
|
+
|
|
136
|
+
This SDK is separate from any MCP or Home Assistant adapter. A library method
|
|
137
|
+
doesn't imply that an adapter exposes it. Adapters choose their own limited
|
|
138
|
+
tools and action gates.
|
|
139
|
+
|
|
140
|
+
## Write behavior
|
|
141
|
+
|
|
142
|
+
Each ordinary Casa mutation is sent once. The client force refreshes the ID
|
|
143
|
+
token before sending it and doesn't replay the mutation after a 401. Normal GET
|
|
144
|
+
requests retry once after a 401. `bootstrap` is a deliberate exception: it is a
|
|
145
|
+
POST that registers a device and keeps the one time 401 retry. `upload_document`
|
|
146
|
+
is a multi step workflow with a policy GET and three write steps. Each step is
|
|
147
|
+
attempted once, and its Cloud Storage PUT uses no Casa token. A response parsing
|
|
148
|
+
failure, timeout or connection error after a mutation can leave its outcome
|
|
149
|
+
unknown. Check the resulting state before deciding whether to send another
|
|
150
|
+
request. The `Idempotency-Key` on `send_ai_message` helps trace a send, but
|
|
151
|
+
server deduplication hasn't been verified.
|
|
152
|
+
|
|
153
|
+
Write bodies use strict validation. Invalid or unknown input fields raise
|
|
154
|
+
`CasaRequestError` before the write. Optional `None` values generally omit a
|
|
155
|
+
field, while empty strings and lists are sent as supplied. Some contracts send
|
|
156
|
+
explicit null, including the access preference fields. In
|
|
157
|
+
`UpdateDocumentBody`, omitting `document_date` leaves the key out, and
|
|
158
|
+
explicit `document_date=None` sends null. Don't treat those forms as
|
|
159
|
+
interchangeable. The backend effect of document date clearing is unverified.
|
|
160
|
+
|
|
161
|
+
Path identifiers must be nonempty safe segments. They aren't normalized to
|
|
162
|
+
UUIDs. Responses can contain home addresses, coordinates, phone numbers, email
|
|
163
|
+
addresses, account identifiers, payment and membership metadata, private
|
|
164
|
+
messages, documents, purchase details and short lived signed URLs. Avoid
|
|
165
|
+
logging complete models, request paths, event payloads or read exceptions.
|
|
166
|
+
Response URLs aren't followed unless a documented workflow requires it.
|
|
167
|
+
`upload_document` follows only its validated Cloud Storage session URL.
|
|
168
|
+
|
|
169
|
+
## Document upload
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
from pathlib import Path
|
|
173
|
+
|
|
174
|
+
result = await api.upload_document(home_id, Path("inspection.pdf"), "inspection.pdf")
|
|
175
|
+
print(result.document.id, result.already_exists)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
`upload_document` first reads `document_types` to check MIME type and file
|
|
179
|
+
size. It reserves a session, uploads the bytes to the signed Cloud Storage URL
|
|
180
|
+
Casa returns, then registers the document. `find_todos=True` asks Casa to look
|
|
181
|
+
for suggested tasks. Each step is attempted once. An error in a later step
|
|
182
|
+
doesn't undo an earlier one.
|
|
183
|
+
|
|
184
|
+
`source` can be bytes or a local path. The MIME type is guessed from `filename`
|
|
185
|
+
when omitted and must appear in Casa's allowed MIME types. Local paths must
|
|
186
|
+
open as nonempty regular files. Symlinks to regular files are allowed. The read
|
|
187
|
+
is bounded by Casa's reported file size limit, plus one byte for detecting an
|
|
188
|
+
oversized file. The entire file is held in memory for one storage request. The
|
|
189
|
+
client timeout bounds the file read and storage request, but cancellation can't
|
|
190
|
+
stop a filesystem call already running in a worker thread.
|
|
191
|
+
|
|
192
|
+
The filename must contain 1 to 1,024 characters, include a nonwhitespace
|
|
193
|
+
character and contain no controls. The signed URL can contain at most 8,192
|
|
194
|
+
characters, must contain no embedded credentials and must use HTTPS on
|
|
195
|
+
`storage.googleapis.com` at port 443. A separate storage session sends no Casa
|
|
196
|
+
credentials, caller headers, cookies, environment proxy settings or netrc
|
|
197
|
+
configuration. It doesn't follow redirects and accepts only HTTP 200 or 201.
|
|
198
|
+
Keep signed session URLs private. See the `upload_document` and
|
|
199
|
+
`upload_document_content` docstrings for the complete contract.
|
|
200
|
+
|
|
201
|
+
## Casa AI events
|
|
202
|
+
|
|
203
|
+
`casa_ai_events` is an opt in server sent event stream. A live connection and
|
|
204
|
+
some update traffic were observed, but the full event and reconnect behavior
|
|
205
|
+
isn't verified. The generator doesn't reconnect automatically. Close it when
|
|
206
|
+
you stop reading early:
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
from contextlib import aclosing
|
|
210
|
+
|
|
211
|
+
from casahome.models import CasaAIMessageUpsertChange
|
|
212
|
+
from casahome.streaming import CasaAIResyncRequiredEvent, CasaAIThreadUpdateEvent
|
|
213
|
+
|
|
214
|
+
async with aclosing(api.casa_ai_events(home_id, after=saved_cursor)) as events:
|
|
215
|
+
async for event in events:
|
|
216
|
+
if event.id is not None:
|
|
217
|
+
saved_cursor = event.id
|
|
218
|
+
if isinstance(event, CasaAIResyncRequiredEvent):
|
|
219
|
+
break # Reload state before subscribing again.
|
|
220
|
+
if isinstance(event, CasaAIThreadUpdateEvent):
|
|
221
|
+
for change in event.update().changes:
|
|
222
|
+
if isinstance(change, CasaAIMessageUpsertChange):
|
|
223
|
+
print(change.message.sender_role, change.message.content)
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
An `after` cursor is opaque. Store it unchanged. `event.update()` validates the
|
|
227
|
+
decoded thread update. `event.payload` retains the decoded JSON value, while
|
|
228
|
+
`event.data` retains the original event text. Other change types remain
|
|
229
|
+
available as `CasaAIUnknownChange`. The stream limits lines to 64 KiB, events to
|
|
230
|
+
256 KiB and cursors to 2,048 characters without controls. The cursor is sent as
|
|
231
|
+
both `after` and `Last-Event-ID`. Redirects are disabled. A 60 second socket
|
|
232
|
+
read gap raises a connection error. HTTP 204 ends the generator, and incomplete
|
|
233
|
+
event framing at EOF is discarded. `resync_required` is yielded once before
|
|
234
|
+
the response closes. The GET can refresh an expired token and retry once on
|
|
235
|
+
401. That retry behavior doesn't apply to ordinary mutations.
|
|
236
|
+
|
|
237
|
+
## Development
|
|
238
|
+
|
|
239
|
+
```sh
|
|
240
|
+
uv sync
|
|
241
|
+
uv run --locked pytest -q
|
|
242
|
+
uv run --locked ruff check .
|
|
243
|
+
uv run --locked ruff format --check .
|
|
244
|
+
uv run --locked mypy src tests
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Tests use synthetic data and a loopback server. They don't call Casa or need
|
|
248
|
+
credentials.
|
|
249
|
+
|
|
250
|
+
## License
|
|
251
|
+
|
|
252
|
+
MIT. See `LICENSE`.
|