python-casahome 0.2.0__tar.gz → 0.3.0__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.0/PKG-INFO +458 -0
- python_casahome-0.3.0/README.md +438 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/pyproject.toml +7 -1
- {python_casahome-0.2.0 → python_casahome-0.3.0}/src/casahome/__init__.py +3 -1
- python_casahome-0.3.0/src/casahome/_common.py +96 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/src/casahome/api.py +134 -125
- {python_casahome-0.2.0 → python_casahome-0.3.0}/src/casahome/client.py +41 -9
- python_casahome-0.3.0/src/casahome/mail_workflows.py +270 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/src/casahome/models/__init__.py +209 -1
- python_casahome-0.3.0/src/casahome/models/ai.py +139 -0
- python_casahome-0.3.0/src/casahome/models/documents.py +208 -0
- python_casahome-0.3.0/src/casahome/models/entity_publish.py +125 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/src/casahome/models/home.py +2 -2
- python_casahome-0.3.0/src/casahome/models/mail_workflows.py +206 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/src/casahome/models/mailbox.py +6 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/src/casahome/models/membership.py +16 -3
- python_casahome-0.3.0/src/casahome/models/read_todos.py +48 -0
- python_casahome-0.3.0/src/casahome/models/recovered_contracts.py +131 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/src/casahome/models/requests.py +17 -2
- {python_casahome-0.2.0 → python_casahome-0.3.0}/src/casahome/models/todos.py +5 -2
- python_casahome-0.3.0/src/casahome/models/verified_writes.py +163 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/src/casahome/models/visits.py +10 -2
- python_casahome-0.3.0/src/casahome/models/workflows.py +127 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/src/casahome/models/write_contracts.py +9 -6
- python_casahome-0.3.0/src/casahome/recovered.py +133 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/src/casahome/streaming.py +21 -5
- python_casahome-0.3.0/src/casahome/uploads.py +98 -0
- python_casahome-0.3.0/src/casahome/workflows.py +246 -0
- python_casahome-0.3.0/src/casahome/writes.py +715 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/tests/test_api.py +37 -9
- python_casahome-0.3.0/tests/test_catalog_and_room_writes.py +314 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/tests/test_client.py +15 -2
- python_casahome-0.3.0/tests/test_document_structure_response.py +230 -0
- python_casahome-0.3.0/tests/test_mail_workflows.py +1075 -0
- python_casahome-0.3.0/tests/test_observed_contracts.py +456 -0
- python_casahome-0.3.0/tests/test_recovered_contracts.py +352 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/tests/test_static_reads.py +73 -11
- python_casahome-0.3.0/tests/test_upload_security.py +429 -0
- python_casahome-0.3.0/tests/test_verified_writes.py +1166 -0
- python_casahome-0.3.0/tests/test_workflows.py +952 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/tests/test_write_contracts.py +5 -4
- {python_casahome-0.2.0 → python_casahome-0.3.0}/uv.lock +1 -1
- python_casahome-0.2.0/PKG-INFO +0 -272
- python_casahome-0.2.0/README.md +0 -256
- python_casahome-0.2.0/src/casahome/models/ai.py +0 -58
- python_casahome-0.2.0/src/casahome/models/documents.py +0 -127
- python_casahome-0.2.0/src/casahome/models/read_todos.py +0 -22
- {python_casahome-0.2.0 → python_casahome-0.3.0}/.gitignore +0 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/.python-version +0 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/LICENSE +0 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/examples/bootstrap.py +0 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/src/casahome/_base_http.py +0 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/src/casahome/auth.py +0 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/src/casahome/const.py +0 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/src/casahome/exceptions.py +0 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/src/casahome/models/_base.py +0 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/src/casahome/models/assets.py +0 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/src/casahome/models/availability.py +0 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/src/casahome/models/bootstrap.py +0 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/src/casahome/models/calendar.py +0 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/src/casahome/models/catalogs.py +0 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/src/casahome/models/credits.py +0 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/src/casahome/models/enums.py +0 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/src/casahome/models/feed.py +0 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/src/casahome/models/help_center.py +0 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/src/casahome/models/images.py +0 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/src/casahome/models/maintenance.py +0 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/src/casahome/models/paint.py +0 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/src/casahome/models/preferences.py +0 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/src/casahome/models/purchases.py +0 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/src/casahome/models/referral.py +0 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/src/casahome/models/tasks.py +0 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/src/casahome/models/taxes.py +0 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/src/casahome/py.typed +0 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/tests/__init__.py +0 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/tests/conftest.py +0 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/tests/fixtures/bootstrap.json +0 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/tests/fixtures/error.json +0 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/tests/fixtures/token_refresh.json +0 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/tests/test_auth.py +0 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/tests/test_inventory_models.py +0 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/tests/test_models.py +0 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/tests/test_read_coverage.py +0 -0
- {python_casahome-0.2.0 → python_casahome-0.3.0}/tests/test_streaming.py +0 -0
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: python-casahome
|
|
3
|
+
Version: 0.3.0
|
|
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
|
+
Async Python client for the [Casa](https://getcasa.com) home membership API.
|
|
24
|
+
|
|
25
|
+
Unofficial. Not affiliated with or endorsed by Casa.
|
|
26
|
+
|
|
27
|
+
## Status
|
|
28
|
+
|
|
29
|
+
Version 0.3.0, alpha.
|
|
30
|
+
This source tree is local and unpublished. The PyPI package can be older.
|
|
31
|
+
|
|
32
|
+
- Most reads have been confirmed against live traffic.
|
|
33
|
+
- Version 0.3.0 adds writes whose method, path, request body and response were
|
|
34
|
+
confirmed against live app traffic: moving inventory, Casa AI messages and
|
|
35
|
+
ratings, request messages, plain-text to-do creation and deletion, to-do
|
|
36
|
+
questions and hand-off, name-only entity model publishing, Autopilot,
|
|
37
|
+
notification and access settings, mailbox claim, and document upload.
|
|
38
|
+
- A few older writes remain unverified. Treat them as experimental.
|
|
39
|
+
- Endpoint shapes were inferred from the iOS app and confirmed against live
|
|
40
|
+
traffic where marked. Casa can change its private API at any time.
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
Install from PyPI:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
pip install python-casahome
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
For this local source checkout:
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
uv sync --locked
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Requires Python 3.11 or newer.
|
|
57
|
+
|
|
58
|
+
## Authentication
|
|
59
|
+
|
|
60
|
+
Casa signs members in with Firebase phone auth. This library doesn't implement
|
|
61
|
+
phone/SMS sign-in. An independent login flow would need Casa's app-verification
|
|
62
|
+
and callback requirements established first. The supported setup starts from a
|
|
63
|
+
Firebase refresh token and exchanges it for short-lived ID tokens.
|
|
64
|
+
|
|
65
|
+
Casa offers no official way to export that token. You obtain your own refresh
|
|
66
|
+
token from your own Casa app session, for example by inspecting your own app's
|
|
67
|
+
traffic during login and reading `refreshToken` from the Firebase token
|
|
68
|
+
response.
|
|
69
|
+
|
|
70
|
+
Treat the refresh token like a password. Anyone holding it can act as you.
|
|
71
|
+
Keep it in a password manager or secret store, never in source control.
|
|
72
|
+
|
|
73
|
+
The token doesn't expire on a timer, but Firebase can rotate it. Pass
|
|
74
|
+
`on_refresh_token_rotated` to `FirebaseAuth` (sync or async) and persist the new
|
|
75
|
+
value when it fires.
|
|
76
|
+
|
|
77
|
+
## Quick start
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
import asyncio
|
|
81
|
+
|
|
82
|
+
from casahome import CasaApi
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
async def main() -> None:
|
|
86
|
+
async with CasaApi.create("YOUR_REFRESH_TOKEN") as api:
|
|
87
|
+
me = await api.bootstrap()
|
|
88
|
+
for home in me.homes:
|
|
89
|
+
print(home.display_address, home.status)
|
|
90
|
+
weather = await api.weather(home.id)
|
|
91
|
+
print(weather)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
asyncio.run(main())
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
If you already own an `aiohttp.ClientSession` (for example in Home Assistant),
|
|
98
|
+
build the pieces yourself:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from casahome import CasaApi, CasaClient, FirebaseAuth
|
|
102
|
+
|
|
103
|
+
auth = FirebaseAuth(session, refresh_token, on_refresh_token_rotated=save_token)
|
|
104
|
+
api = CasaApi(
|
|
105
|
+
CasaClient(session, auth),
|
|
106
|
+
device_id=stored_device_id,
|
|
107
|
+
timezone="America/Los_Angeles",
|
|
108
|
+
)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`bootstrap` registers a device with Casa. Long-lived callers should persist
|
|
112
|
+
`api.device_id` and pass it back on the next start. Otherwise every restart adds
|
|
113
|
+
a new device to the account.
|
|
114
|
+
|
|
115
|
+
A runnable example lives in `examples/bootstrap.py`:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
CASA_REFRESH_TOKEN=... uv run examples/bootstrap.py
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Coverage
|
|
122
|
+
|
|
123
|
+
Each method's docstring starts with `VERIFIED` or `UNVERIFIED`.
|
|
124
|
+
`casahome.is_unverified(api.some_method)` reports the same thing at runtime.
|
|
125
|
+
|
|
126
|
+
The "Live-confirmed" column means:
|
|
127
|
+
|
|
128
|
+
- **yes**: the path, method, response envelope and list item shapes were seen
|
|
129
|
+
in live traffic.
|
|
130
|
+
- **partial**: the path, method and envelope were seen, but lists came back
|
|
131
|
+
empty or items are kept as opaque JSON. Item models are inferred.
|
|
132
|
+
- **no**: inferred from the app only. Not seen working live.
|
|
133
|
+
|
|
134
|
+
The envelope is the top-level response keys. Item shape is the structure of
|
|
135
|
+
elements inside lists. All live confirmation dates from 2026-10.
|
|
136
|
+
|
|
137
|
+
### Reads
|
|
138
|
+
|
|
139
|
+
| Method | Live-confirmed | Notes |
|
|
140
|
+
|---|---|---|
|
|
141
|
+
| `bootstrap` | yes | Profile, homes, badge counts, app update info. |
|
|
142
|
+
| `onboarding_details` | yes | Raw dict, or `None` on 304. |
|
|
143
|
+
| `onboarding_details_typed` | yes | Typed version of the above. |
|
|
144
|
+
| `onboarding_availability` | yes | |
|
|
145
|
+
| `home_details` | yes | Stories, rooms, inventory entities. |
|
|
146
|
+
| `inventory_item` | yes | One entity. |
|
|
147
|
+
| `inventory_makes` | yes | |
|
|
148
|
+
| `inventory_enums` | yes | Takes a property's `enum_type` key, such as `light_fixture_type`. |
|
|
149
|
+
| `inventory_paint_matches` | yes | |
|
|
150
|
+
| `inventory_service_history` | partial | Room and home targets seen. Entity target untested. |
|
|
151
|
+
| `room_types` | yes | |
|
|
152
|
+
| `room_image_options` | yes | |
|
|
153
|
+
| `room_image_candidates` | yes | |
|
|
154
|
+
| `matterport_images` | yes | |
|
|
155
|
+
| `badge_counts` | yes | |
|
|
156
|
+
| `weather` | yes | |
|
|
157
|
+
| `credits_summary` | yes | |
|
|
158
|
+
| `home_feed` | yes | `props` varies by card template and stays a JSON dict. |
|
|
159
|
+
| `home_members` | yes | |
|
|
160
|
+
| `schedule_preferences` | yes | |
|
|
161
|
+
| `notification_settings` | yes | |
|
|
162
|
+
| `access_preferences` | partial | Option values not seen populated. |
|
|
163
|
+
| `autopilot_plans` | yes | |
|
|
164
|
+
| `paint_brands`, `paint_collections`, `paint_colors`, `paint_search` | yes | |
|
|
165
|
+
| `suggested_todos` | yes | Question rounds and destination stay opaque. |
|
|
166
|
+
| `todo_details` | yes | Question rounds and destination typed. Question items stay JSON. |
|
|
167
|
+
| `document_types` | yes | |
|
|
168
|
+
| `documents` | yes | App sends `limit=30&sort=uploaded`. `q` and `document_type` untested. |
|
|
169
|
+
| `document_details` | yes | |
|
|
170
|
+
| `document_thumbnail` | yes | Signed preview URLs. Never fetched by the library. |
|
|
171
|
+
| `document_file_metadata` | yes | Signed file URL and integer expiry only. No download. |
|
|
172
|
+
| `document_suggestions` | yes | Populated suggestions and citations. `view=all` observed. Other values and filtering semantics unverified. Matches empty, cursor null. |
|
|
173
|
+
| `document_structure_suggestions` | yes | Populated entity/definition IDs, labels, values, response times and citations. |
|
|
174
|
+
| `request_details` | yes | |
|
|
175
|
+
| `request_purchases` | partial | Envelope seen. Item lists empty. |
|
|
176
|
+
| `membership_info` | yes | Active membership. A home without one returns 404. |
|
|
177
|
+
| `help_center_bundle` | yes | Only `member_ios` tested. |
|
|
178
|
+
| `release_notes` | yes | |
|
|
179
|
+
| `release_notes_diff` | yes | |
|
|
180
|
+
| `casa_ai_bootstrap` | partial | Messages not seen populated. |
|
|
181
|
+
| `casa_ai_messages` | partial | |
|
|
182
|
+
| `requests_tab` | yes | Request cards seen populated. Task cards untested. |
|
|
183
|
+
| `completed_requests` | partial | |
|
|
184
|
+
| `calendar` | partial | Bounds sent as epoch seconds. |
|
|
185
|
+
| `calendar_busy` | partial | |
|
|
186
|
+
| `calendar_sync` | partial | |
|
|
187
|
+
| `mailbox`, `mail_items`, `packages` | partial | |
|
|
188
|
+
| `work_document_source` | partial | Only the `todo_id` selector tested. |
|
|
189
|
+
| `vehicles` | partial | |
|
|
190
|
+
| `utilities_insights` | partial | |
|
|
191
|
+
| `purchases` | partial | |
|
|
192
|
+
| `property_taxes` | partial | |
|
|
193
|
+
| `recurring_vendor_categories` | yes | |
|
|
194
|
+
| `recurring_vendors` | partial | |
|
|
195
|
+
| `bookable_handyman_tasks` | partial | Items stay opaque JSON. |
|
|
196
|
+
| `referral_code` | partial | `display` and `referred_homes` stay opaque JSON. |
|
|
197
|
+
| `membership_savings` | no | Returns 404 even with an active membership. |
|
|
198
|
+
| `availability_request_details` | no | |
|
|
199
|
+
| `task_details` | no | |
|
|
200
|
+
| `vendor_visit_details` | no | |
|
|
201
|
+
| `vendor_visit_autopilot_plan` | no | |
|
|
202
|
+
| `vendor_visit_calls` | no | |
|
|
203
|
+
| `vendor_visit_conversation` | no | |
|
|
204
|
+
| `vendor_visit_purchases` | no | |
|
|
205
|
+
| `casa_ai_events` | partial | Opt-in SSE stream. Transport, cursor replay and `message.upsert` updates confirmed. `resync_required` untested. |
|
|
206
|
+
|
|
207
|
+
### Writes
|
|
208
|
+
|
|
209
|
+
"Yes" means the method, path, request body and response were seen in live app
|
|
210
|
+
traffic and the library sends the same body.
|
|
211
|
+
|
|
212
|
+
| Method | Live-confirmed | Notes |
|
|
213
|
+
|---|---|---|
|
|
214
|
+
| `move_entity_to_room` | yes | Replaces the item's room. |
|
|
215
|
+
| `rename_room` | yes | `PATCH` with `room_type_id` and `name`, both always sent. |
|
|
216
|
+
| `save_and_publish_entity`, `update_entity_make_model`, `update_entity_model` | partial | Observed publish subset includes `model.name` and empty `props`. Shared ID/name encoding is statically verified, not traffic verification of every variant. Model-ID traffic remains UNVERIFIED. No clear. |
|
|
217
|
+
| `send_ai_message` | yes | Sends an `Idempotency-Key` and returns it. Reply arrives on the event stream. |
|
|
218
|
+
| `rate_ai_message` | yes | `good` or `bad`. |
|
|
219
|
+
| `answer_todo_question` | yes | Answers seen. The skip body is unverified. |
|
|
220
|
+
| `submit_todo_question_round` | yes | |
|
|
221
|
+
| `respond_to_suggested_todo` | yes | Only `accept` seen. |
|
|
222
|
+
| `materialize_todo`, `send_todo_to_concierge` | yes | Only the `concierge` destination seen. |
|
|
223
|
+
| `create_todo_result`, `create_todo` | yes | Plain text only. Attachments and support-confirmation true unverified. |
|
|
224
|
+
| `delete_todo` | yes | Bodyless DELETE, HTTP 204. |
|
|
225
|
+
| `send_request_message` | yes | Member role and text, HTTP 201 message envelope. Attachments unverified. |
|
|
226
|
+
| `respond_to_document_structure_suggestion` | yes | Only `accepted`. Requires explicit `expected_values`. HTTP 200 with `saved: true`. |
|
|
227
|
+
| `update_autopilot_plan` | yes | |
|
|
228
|
+
| `update_notification_settings` | yes | Only the `push` channel seen. |
|
|
229
|
+
| `update_access_preferences` | yes | All four keys always sent. |
|
|
230
|
+
| `claim_mailbox` | yes | |
|
|
231
|
+
| `create_document_upload_sessions` | yes | Upload step 1. |
|
|
232
|
+
| `upload_document_content` | yes | Upload step 2, to Cloud Storage. |
|
|
233
|
+
| `create_document` | yes | Upload step 3. |
|
|
234
|
+
| `upload_document` | yes | All three steps, after checking the file against `document_types`. |
|
|
235
|
+
| `clear_ai_message_rating` | no | Path from the app only. |
|
|
236
|
+
| `update_entity_properties` | no | |
|
|
237
|
+
|
|
238
|
+
### Offline reconstructed contracts
|
|
239
|
+
|
|
240
|
+
The following SDK methods are **UNVERIFIED against Casa's backend**. Their
|
|
241
|
+
request/response codecs were reconstructed from the compiled app. Synthetic
|
|
242
|
+
loopback tests check serialization and parsing only. No MCP or Home Assistant
|
|
243
|
+
tool is added by these SDK methods.
|
|
244
|
+
|
|
245
|
+
| Method | Route beneath the home prefix | Result |
|
|
246
|
+
|---|---|---|
|
|
247
|
+
| `update_request_detail` | `PATCH requests/{id}` | Direct six-field `UpdateRequestDetailResponse`. Optional title/read_state (`read` or `unread`) are omitted when absent. |
|
|
248
|
+
| `dismiss_document_structure_suggestion` | `POST documents/{id}/structure_suggestions/{id}/respond` | `SavedResponse`. Requires ordered caller-supplied `expected_values`, even when empty. The existing accepted-only method is unchanged. |
|
|
249
|
+
| `update_task`, `delete_task`, `transfer_task_to_concierge` | `PATCH/DELETE tasks/{id}`, `POST tasks/{id}/transfer_to_concierge` | TaskDetails (scalar edits only), BasicMessageResponse, direct TransferToConciergeResponse. DELETE and transfer have no body. |
|
|
250
|
+
| `delete_document` | `DELETE documents/{id}` | `HomeDocumentDeleteResponse.deleted`. False is retained on HTTP success. |
|
|
251
|
+
| `update_room_properties` | `PATCH room/{id}/props` under the **member** home prefix | Required props list of definition_id plus optional string value. No enum or clear input. |
|
|
252
|
+
| `update_story` | `PATCH story/{id}` under the **member** home prefix | Optional name/floor_number, BasicMessageResponse. |
|
|
253
|
+
| `retailer_discounts` | `GET retailer_discounts` | Required `data` array with typed scalar items and preserved unknown fields. Never follows URLs. |
|
|
254
|
+
| `create_support_request` | `POST support_requests` | Text-only, direct request_id. This may contact a person. No attachments. |
|
|
255
|
+
| `mark_vendor_visit_conversation_read` | `PATCH vendor_visits/{id}/conversation/read` | Bodyless, required `ok` Boolean. False isn't treated as success. |
|
|
256
|
+
|
|
257
|
+
Writes make a single attempt without 401 replay. A response parse failure after
|
|
258
|
+
a write is an unknown outcome, not proof the write didn't happen. No implicit
|
|
259
|
+
clearing, conflict retry, dispatch, booking or follow-up action is performed.
|
|
260
|
+
Resource refresh needs raw response bytes rather than the current JSON-only
|
|
261
|
+
client transport, so it isn't exposed. Mail detail, utility statements and
|
|
262
|
+
vendor message sends need further nested response proof. Shopping, billing and
|
|
263
|
+
credential operations remain excluded.
|
|
264
|
+
|
|
265
|
+
## Models
|
|
266
|
+
|
|
267
|
+
Import models from `casahome.models`. Unknown response fields are kept in
|
|
268
|
+
`model_extra` rather than dropped, so new server fields don't break parsing.
|
|
269
|
+
A response that doesn't match the expected shape raises `CasaResponseError`.
|
|
270
|
+
|
|
271
|
+
Identifier arguments are checked before any request. They must be nonempty
|
|
272
|
+
path-safe segments (letters, digits, `.`, `_`, `~`, `-`). IDs aren't trimmed,
|
|
273
|
+
lowercased or forced to UUIDs.
|
|
274
|
+
|
|
275
|
+
Responses can contain personal data: names, contact details, messages,
|
|
276
|
+
documents and purchase info. Avoid logging whole models. URLs in responses are
|
|
277
|
+
returned as data. The library never follows them.
|
|
278
|
+
|
|
279
|
+
## Writes
|
|
280
|
+
|
|
281
|
+
Live-confirmed writes are listed in the coverage table above. The remaining
|
|
282
|
+
writes were inferred from the app and haven't been exercised against the live
|
|
283
|
+
backend. Nothing is sent until you await a write method.
|
|
284
|
+
|
|
285
|
+
- Each write is sent exactly once. There is no retry, including on 401. The ID
|
|
286
|
+
token is refreshed before the request, and a 401 raises `CasaAuthError`.
|
|
287
|
+
Deciding whether to repeat is up to you.
|
|
288
|
+
- If the response fails to parse, the server may still have performed the
|
|
289
|
+
action. Don't assume a parse error means it's safe to repeat.
|
|
290
|
+
- Request bodies are strict Pydantic models. Unknown fields and invalid values
|
|
291
|
+
raise `CasaRequestError` before any I/O. Free-text fields are capped at
|
|
292
|
+
10,000 characters.
|
|
293
|
+
- Optional fields set to `None` are omitted from the JSON. Empty strings and
|
|
294
|
+
empty lists are sent as given.
|
|
295
|
+
- Where the app sends an explicit `null` (for example `other_text` in to-do
|
|
296
|
+
answers, or the access preference keys), the library sends `null` too.
|
|
297
|
+
- `send_ai_message` is the only write with an `Idempotency-Key`. The returned
|
|
298
|
+
key supports tracing, but server deduplication hasn't been tested. A failed
|
|
299
|
+
send may have succeeded. Check the thread before considering another attempt.
|
|
300
|
+
- `send_request_message` only allows the `member` sender role.
|
|
301
|
+
- `create_todo_result` returns the full envelope, including
|
|
302
|
+
`needs_support_confirmation`. `create_todo` is the older helper that returns
|
|
303
|
+
just the todo.
|
|
304
|
+
- `save_and_publish_entity` takes a `SaveAndPublishEntityRequest` with
|
|
305
|
+
optional `make` and `model`, each a `CatalogFieldId(id=...)` or
|
|
306
|
+
`CatalogFieldName(name=...)`, and `props=[]`. At least one side is required.
|
|
307
|
+
`update_entity_make_model(home_id, entity_id, *, make_id=, make_name=,
|
|
308
|
+
model_id=, model_name=)` builds the same body: at most one of id or name per
|
|
309
|
+
side, at least one side. `update_entity_model(home_id, entity_id, model_name)`
|
|
310
|
+
is the model-name-only shortcut. All return `SaveAndPublishEntityResponse`,
|
|
311
|
+
including optional make/model and required identity, entity_id and status.
|
|
312
|
+
Missing or null make/model fields are accepted based on the compiled decoder,
|
|
313
|
+
not a claim of observed server responses. Casa matches names against its catalog
|
|
314
|
+
and may store a different canonical name. An unmatched name raises
|
|
315
|
+
`CasaApiError` with status 400 and code `CATALOG_MODEL_UNVERIFIED`. Names
|
|
316
|
+
are capped at 256 characters and can't be blank. Clearing a make or model
|
|
317
|
+
isn't supported. The compiled clear case encodes explicit null, but its server
|
|
318
|
+
semantics remain UNVERIFIED. SDK `None` omits a side and never clears it.
|
|
319
|
+
Model-ID selection has static codec evidence only, not verified traffic.
|
|
320
|
+
Nonempty props are
|
|
321
|
+
rejected. The older property PATCH is still unverified.
|
|
322
|
+
- `rename_room(home_id, room_id, *, room_type_id, name)` sends both keys. Pass
|
|
323
|
+
the room's current `room_type_id` (from `home_details`) to keep its type.
|
|
324
|
+
Room creation isn't implemented.
|
|
325
|
+
- `CasaApiError` reads `display_title`, `display_message` and `request_id`
|
|
326
|
+
from either camelCase or snake_case error bodies. Both are seen live.
|
|
327
|
+
- `document_suggestions(..., view=None)` omits the query by default. Supplied
|
|
328
|
+
view strings are forwarded unchanged as query values. `view="all"` was
|
|
329
|
+
observed. Other values and filtering semantics aren't verified.
|
|
330
|
+
A null `next_cursor` was observed,
|
|
331
|
+
but no cursor input is added without an observed query contract.
|
|
332
|
+
- `respond_to_document_structure_suggestion(home_id, document_id, suggestion_id,
|
|
333
|
+
expected_values, decision="accepted")` sends only the confirmed response body.
|
|
334
|
+
`expected_values` is required. Pass the suggestion's `current_values`, including
|
|
335
|
+
an explicit empty list when appropriate. There isn't a default race guard or
|
|
336
|
+
automatic refresh. Server mismatch handling remains unverified.
|
|
337
|
+
`"accept"` and dismissal decisions are rejected. `SavedResponse.saved` requires
|
|
338
|
+
a strict Boolean. No document PATCH is added.
|
|
339
|
+
|
|
340
|
+
### Additional reconstructed workflows
|
|
341
|
+
|
|
342
|
+
These methods are implemented with strict request models, typed responses and
|
|
343
|
+
offline tests. Unless explicitly noted, they remain `UNVERIFIED`. Static client
|
|
344
|
+
code doesn't establish current backend acceptance, business effects or safe retries.
|
|
345
|
+
None is automatically executed or exposed as a live MCP action.
|
|
346
|
+
|
|
347
|
+
| Methods | Request distinction and remaining uncertainty |
|
|
348
|
+
|---|---|
|
|
349
|
+
| `update_todo` | Optional title, brief, emoji and attachment deltas omit `None`. Clear behavior unknown. |
|
|
350
|
+
| `reject_suggested_todo` | `reject` with optional `dismissed` or `already_handled` reason. |
|
|
351
|
+
| `confirm_todo_support` | Caller-supplied strict Boolean, no inferred classification. |
|
|
352
|
+
| `update_document` | Date omitted, supplied string, or explicit null. Date format and clearing effects unverified. |
|
|
353
|
+
| `analyze_document`, `organize_document` | Analysis requires an explicit `reanalyze` Boolean. Organization has no JSON body. |
|
|
354
|
+
| `update_schedule_preferences` | Both day-number and time-window-ID arrays required. Numbering and replacement rules unknown. |
|
|
355
|
+
| `update_availability_slots` | Required integer windows. Time units and submission semantics unknown. |
|
|
356
|
+
| `reorder_todo_section` | Required revision and ordered IDs. No conflict refetch/retry. |
|
|
357
|
+
| `retry_todo` | Explicit bodyless preparation action, not transport retry. |
|
|
358
|
+
| `cancel_vendor_visit` | Bodyless PUT. Fees and side effects possible, not established. |
|
|
359
|
+
| `add_mailbox_recipient` | VERIFIED for first/last names. Business-name and partial-name variants unverified. |
|
|
360
|
+
| `update_mailbox_auto_archive` | VERIFIED for `30` and explicit null. Other values and disposal effects unverified. |
|
|
361
|
+
| `remove_mailbox_recipient` | Bodyless DELETE. Recipient restrictions and reversibility unknown. |
|
|
362
|
+
| `mark_mail_item_read`, `mark_mail_item_unread`, `set_mail_item_archived` | Bodyless POSTs. Archiving isn't a shredding claim. |
|
|
363
|
+
| `validate_package_tracking_number`, `add_tracked_package` | Validation uses POST. Package direction is explicit. |
|
|
364
|
+
| `update_package`, `requeue_package_pickup` | Update uses POST. `deleted` may remove a record, not merely cancel pickup. |
|
|
365
|
+
| `create_package_pickup_item` | Requires a matching typed image-upload receipt. No multipart-upload helper or time-slot selection. |
|
|
366
|
+
| `set_package_auto_archive` | Required nullable integer. Allowed ranges unknown. |
|
|
367
|
+
| `create_inventory_entity`, `create_inventory_lighting`, `create_paint_from_library` | Capture constructors. No guessed custom make/model tags or lighting-kind values. |
|
|
368
|
+
|
|
369
|
+
Document-date examples preserve the distinction between unchanged and clearing:
|
|
370
|
+
|
|
371
|
+
```python
|
|
372
|
+
from casahome.models import UpdateDocumentBody
|
|
373
|
+
|
|
374
|
+
rename_only = UpdateDocumentBody(name="Warranty") # No document_date key.
|
|
375
|
+
set_date = UpdateDocumentBody(document_date="2020-01-01") # Format is unverified.
|
|
376
|
+
clear_date = UpdateDocumentBody(document_date=None) # Explicit JSON null.
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
Forwarding, shredding, check deposit, reservation submission and document-suggestion
|
|
380
|
+
dismissal aren't invented from endpoint names or nearby UI text. More evidence is needed.
|
|
381
|
+
|
|
382
|
+
### Document upload
|
|
383
|
+
|
|
384
|
+
```python
|
|
385
|
+
result = await api.upload_document(home_id, Path("inspection.pdf"), "inspection.pdf")
|
|
386
|
+
print(result.document.id, result.already_exists)
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
`upload_document` reads `document_types` first and rejects files whose MIME
|
|
390
|
+
type isn't allowed or whose size exceeds `max_file_size_bytes`. It then
|
|
391
|
+
reserves an upload session, sends the bytes to the signed Cloud Storage URL
|
|
392
|
+
Casa returns, and registers the document. The bytes go out in one request, so
|
|
393
|
+
the file is held in memory. The storage request carries no Casa credentials and
|
|
394
|
+
is only sent to `https://storage.googleapis.com`. Signed URLs never appear in
|
|
395
|
+
exception messages.
|
|
396
|
+
|
|
397
|
+
Local paths are opened once and must refer to nonempty regular files. Symlinks
|
|
398
|
+
to regular files are allowed. FIFOs, devices and directories are rejected.
|
|
399
|
+
The actual read is capped at `max_file_size_bytes` plus one detection byte,
|
|
400
|
+
including when the file grows. The client's configured total timeout bounds
|
|
401
|
+
waiting for a file read. Cancellation can't interrupt a filesystem call already
|
|
402
|
+
running in the worker thread, which closes its descriptor when it finishes.
|
|
403
|
+
|
|
404
|
+
Storage uploads use a separate session with no caller Authorization, BasicAuth,
|
|
405
|
+
cookies, custom headers or middleware. Environment proxy/netrc settings aren't
|
|
406
|
+
used. Normal TLS verification stays enabled. The storage session closes on
|
|
407
|
+
success, failure or cancellation. The caller's Casa session stays open.
|
|
408
|
+
|
|
409
|
+
## CasaAI event stream
|
|
410
|
+
|
|
411
|
+
`api.casa_ai_events(home_id, after=None)` is an opt-in async generator over the
|
|
412
|
+
CasaAI server-sent event stream. It doesn't reconnect on its own. Close it with
|
|
413
|
+
`contextlib.aclosing` if you stop early:
|
|
414
|
+
|
|
415
|
+
```python
|
|
416
|
+
from contextlib import aclosing
|
|
417
|
+
|
|
418
|
+
from casahome.models import CasaAIMessageUpsertChange
|
|
419
|
+
from casahome.streaming import CasaAIResyncRequiredEvent, CasaAIThreadUpdateEvent
|
|
420
|
+
|
|
421
|
+
async with aclosing(api.casa_ai_events(home_id, after=saved_cursor)) as events:
|
|
422
|
+
async for event in events:
|
|
423
|
+
saved_cursor = event.id
|
|
424
|
+
if isinstance(event, CasaAIResyncRequiredEvent):
|
|
425
|
+
break # Reload state, then subscribe again.
|
|
426
|
+
if isinstance(event, CasaAIThreadUpdateEvent):
|
|
427
|
+
for change in event.update().changes:
|
|
428
|
+
if isinstance(change, CasaAIMessageUpsertChange):
|
|
429
|
+
print(change.message.sender_role, change.message.content)
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
`event.update()` parses the payload. `message.upsert` changes are typed, and
|
|
433
|
+
any other change type parses as `CasaAIUnknownChange` with its fields kept.
|
|
434
|
+
`event.payload` stays available as raw JSON.
|
|
435
|
+
|
|
436
|
+
Cursors (`event.id`, and `event_cursor` from `casa_ai_bootstrap`) are opaque.
|
|
437
|
+
Store them and pass them back unchanged. Resuming from an earlier cursor
|
|
438
|
+
replays the updates since that point.
|
|
439
|
+
|
|
440
|
+
Lines are capped at 64 KiB and events at 256 KiB. A 401 refreshes the token and
|
|
441
|
+
retries the GET once.
|
|
442
|
+
|
|
443
|
+
## Development
|
|
444
|
+
|
|
445
|
+
```
|
|
446
|
+
uv sync
|
|
447
|
+
uv run --locked pytest -q
|
|
448
|
+
uv run --locked ruff check .
|
|
449
|
+
uv run --locked ruff format --check .
|
|
450
|
+
uv run --locked mypy src tests
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
Tests use synthetic data and a loopback test server. They make no live service
|
|
454
|
+
calls and need no credentials.
|
|
455
|
+
|
|
456
|
+
## License
|
|
457
|
+
|
|
458
|
+
MIT. See `LICENSE`.
|