@@ -22,47 +22,62 @@ Triggers:
2222## Core concepts
2323
2424- ** Single entry point** : ` COMcheckClient ` is the only client class
25- users instantiate. Construct with ` api_key=... ` or rely on the
26- ` COM_API_KEY ` env var.
27- - ** Project shape** : a project is a ` ComBuilding ` Pydantic model. It
28- contains: ` Project ` (metadata), ` Location ` , ` Envelope ` , ` WholeBldgUse[] `
29- (building areas), ` HVAC ` , ` Lighting ` , ` Renewable ` , and ` Control ` .
30- - ** Operation classes (functional)** : building areas and envelope
25+ users instantiate. Construct with ` api_key=... ` . The client does
26+ ** not** auto-read any environment variable — read it yourself and
27+ pass it: ` COMcheckClient(api_key=os.environ["COM_API_KEY"]) ` .
28+ - ** Project shape** : a project is a ` ComBuilding ` Pydantic model with
29+ lowercase top-level fields: ` project ` (metadata), ` location ` ,
30+ ` envelope ` , ` lighting ` (which contains ` wholeBldgUse[] ` — the
31+ building areas), ` hvac ` , ` renewable ` , and ` control ` (energy code).
32+ No ` Project ` /` Control ` PascalCase aliases exist.
33+ - ** Operation modules (functional)** : building areas and envelope
3134 components are added/updated/removed via free functions in
3235 ` project_building_area_operations ` and ` project_envelope_operations ` .
3336 Each function takes a ` ComBuilding ` and returns a new ` ComBuilding ` .
37+ - ** Envelope items attach to a building-area key** : every
38+ ` add_*_to_project ` envelope function takes
39+ ` (project, building_area_key, new_component) ` . Look up the key
40+ with ` ba_ops.get_building_area_keys_from_project(project) ` first.
41+ Default projects have ** no** building areas — add one with
42+ ` ba_ops.add_building_area_to_project(...) ` before any envelope
43+ work.
3444- ** Defaults** : ` comcheck_api.defaults ` has ` get_default_*_template() `
3545 functions that return Pydantic models filled with sensible defaults.
3646 Always start from these.
3747- ** Simulation flow is async** : ` start_run_simulation ` returns a
38- session ID. Poll ` get_simulation_status ` until status is ` complete ` ,
39- then call ` get_simulation_result ` .
48+ session ID. Poll ` get_simulation_status ` until status is
49+ ` "SUCCESS" ` (terminal-ok) or ` "FAILED" ` (terminal-error), then call
50+ ` get_simulation_result ` . See ` comcheck_api.types.SimulationStatus `
51+ for known lifecycle values (the catalog isn't exhaustive — only
52+ the terminal pair is guaranteed stable).
4053
4154## Quick start
4255
4356``` python
57+ import os
58+ import time
59+
4460from comcheck_api import COMcheckClient
4561from comcheck_api.defaults import get_default_project_template
62+ from comcheck_api.types import SimulationStatus
4663
47- client = COMcheckClient(api_key = " your-key" )
64+ # Client does NOT auto-read COM_API_KEY — pass it in.
65+ client = COMcheckClient(api_key = os.environ[" COM_API_KEY" ])
4866
4967# Build a project from a default template
5068project = get_default_project_template()
51- project.Project.title = " 5,000 sqft office in Seattle"
52-
53- # Save it (creates server-side project, returns ID via list)
54- # Update if you have an existing ID:
55- # updated = client.update_project(project_id="123", project_data=project)
69+ project.project.projectTitle = " 5,000 sqft office in Seattle"
5670
5771# Run a simulation
5872session_id = client.start_run_simulation(project)
5973
60- # Poll until complete
61- import time
74+ # Poll until terminal — SUCCESS or FAILED
6275while True :
6376 status = client.get_simulation_status(session_id)
64- if status[" status" ] == " complete " :
77+ if status[" status" ] == SimulationStatus. SUCCESS :
6578 break
79+ if status[" status" ] == SimulationStatus.FAILED :
80+ raise RuntimeError (f " Simulation failed: { status.get(' message' )} " )
6681 time.sleep(5 )
6782
6883result = client.get_simulation_result(session_id)
@@ -73,11 +88,33 @@ print(result["performanceRating"])
7388
7489- Use ` get_default_*_template() ` to start any new component, then
7590 customize. Don't construct ` ComBuilding ` from scratch.
76- - Use the operation classes (e.g.,
77- ` project_envelope_operations.add_ag_wall_to_project(project, wall) ` )
78- to mutate project structure. Don't manipulate nested dicts directly.
79- - Read the API key from ` COM_API_KEY ` env var by default; let users
80- pass ` api_key=... ` to override.
91+ - Use the operation modules (e.g.,
92+ ` env_ops.add_ag_wall_to_project(project, area_key, wall) ` ) to
93+ mutate project structure. Don't manipulate nested dicts directly.
94+ - For envelope items, look up the building-area key first via
95+ ` ba_ops.get_building_area_keys_from_project(project) ` . The key
96+ goes between ` project ` and the new component in every
97+ ` add_*_to_project ` envelope call.
98+ - Set enum-typed fields (` orientation ` , ` wallType ` , ` code ` , etc.)
99+ with members from the matching ` *Options ` enum imported from
100+ ` comcheck_api.types ` — not raw strings, which trigger
101+ Pydantic serialization warnings.
102+ - Pass ` api_key= ` explicitly to ` COMcheckClient(...) ` . The SDK does
103+ ** not** auto-load any env var — read it yourself:
104+
105+ ``` python
106+ from dotenv import load_dotenv
107+ load_dotenv()
108+ client = COMcheckClient(api_key = os.environ[" COM_API_KEY" ])
109+ ```
110+
111+ ` client.set_api_key(api_key) ` is the equivalent post-construction
112+ setter. Users get a Personal Access Token from the COMcheck Web
113+ site (Settings → Developer Setting); see the
114+ [ GitHub README] ( https://github.com/pnnl/comcheckweb-api-python#1-obtain-an-api-key )
115+ or the
116+ [ Getting Started page] ( https://pnnl.github.io/comcheckweb-api-python/getting-started/ )
117+ for the full walkthrough.
81118- Wrap network calls in try/except and catch ` COMCheckHTTPError ` ,
82119 ` COMCheckConnectionError ` , ` COMCheckValidationError ` ,
83120 ` COMCheckSimulationError ` , ` COMCheckProjectNotFoundError ` .
@@ -91,47 +128,104 @@ print(result["performanceRating"])
91128- Don't import private modules (anything starting with ` _ ` ).
92129- Don't poll ` get_simulation_status ` faster than every 5 seconds.
93130- Don't put the API key in source code; use env var or argument.
131+ - Don't use ` comcheck_api.managers.* ` (e.g. ` AgWallListManager ` ,
132+ ` RoofListManager ` ). Those are internal list-mutation helpers; they
133+ bypass the validation logic in the operation modules. Always go
134+ through ` project_envelope_operations ` and
135+ ` project_building_area_operations ` instead.
94136
95137## Common patterns
96138
97- ### Adding an above-grade wall
139+ ### Adding a building area, then an above-grade wall
98140
99- ``` python
100- from comcheck_api import project_envelope_operations as envelope_ops
101- from comcheck_api.defaults import get_default_ag_wall_template
141+ ` get_default_project_template() ` starts with ** zero building
142+ areas** — add one before attaching any envelope component.
102143
144+ ``` python
145+ from comcheck_api import (
146+ project_envelope_operations as env_ops,
147+ project_building_area_operations as ba_ops,
148+ )
149+ from comcheck_api.defaults import (
150+ get_default_building_area_template,
151+ get_default_ag_wall_template,
152+ )
153+ from comcheck_api.types import OrientationOptions
154+
155+ # 1. Add the building area (no areas exist by default).
156+ area = get_default_building_area_template()
157+ area.areaDescription = " Open office"
158+ project = ba_ops.add_building_area_to_project(project, area)
159+
160+ # 2. Look up its key.
161+ area_key = ba_ops.get_building_area_keys_from_project(project)[0 ][" key" ]
162+
163+ # 3. Attach the wall to that area.
103164wall = get_default_ag_wall_template()
104- wall.name = " South wall"
105- wall.area = 4800.0
106- project = envelope_ops.add_ag_wall_to_project(project, wall)
165+ wall.description = " South wall"
166+ wall.orientation = OrientationOptions.SOUTH # use the enum, not "SOUTH"
167+ wall.grossArea = 4800.0 # field is grossArea, not area
168+ project = env_ops.add_ag_wall_to_project(project, area_key, wall)
107169```
108170
109171### Listing the user's projects and updating one
110172
111173``` python
112174projects = client.list_projects()
113175target = next (p for p in projects if p[" title" ] == " My office" )
114- project_obj = client.get_project(target[" id " ])
115- project_obj.Project.title = " My office (revised)"
116- client.update_project(project_id = target[" id " ], project_data = project_obj)
176+ project_obj = client.get_project(target[" _id " ]) # note the underscore
177+ project_obj.project.projectTitle = " My office (revised)"
178+ client.update_project(project_id = target[" _id " ], project_data = project_obj)
117179```
118180
181+ ` get_project(project_id, mode="json") ` returns a raw dict instead of
182+ a ` ComBuilding ` model — handy when you just need the JSON shape.
183+
119184### Polling a simulation with a timeout
120185
121186``` python
122187import time
188+ from comcheck_api.types import SimulationStatus
189+
123190session_id = client.start_run_simulation(project)
124191deadline = time.time() + 300 # 5 min
125192while time.time() < deadline:
126193 status = client.get_simulation_status(session_id)
127- if status[" status" ] == " complete " :
194+ if status[" status" ] == SimulationStatus. SUCCESS :
128195 result = client.get_simulation_result(session_id)
129196 break
197+ if status[" status" ] == SimulationStatus.FAILED :
198+ raise RuntimeError (f " Simulation failed: { status.get(' message' )} " )
130199 time.sleep(5 )
131200else :
132201 raise TimeoutError (f " Simulation { session_id} did not complete in 5 min " )
133202```
134203
204+ ## Gotchas
205+
206+ - ** Field names are lowercase camelCase** , not PascalCase.
207+ ` project.project.projectTitle ` , ` project.control.code ` ,
208+ ` project.lighting.wholeBldgUse[0].areaDescription ` . There is no
209+ ` Project ` , ` Control ` , ` WholeBldgUse ` (top-level), or ` .title ` .
210+ - ** AG/BG wall area field is ` grossArea ` ** , not ` area ` . Same for
211+ roofs/floors/windows/doors. Setting ` .area ` silently does nothing
212+ because Pydantic models reject unknown attributes only in strict
213+ mode.
214+ - ** Use enum members for typed fields** : ` OrientationOptions.NORTH ` ,
215+ ` WallTypeOptions.WOOD_FRAME_16_AG_WALL ` ,
216+ ` EnergyCodeOptions.CEZ_90_1_2022 ` . Setting them to raw strings
217+ works at runtime but emits ` PydanticSerializationUnexpectedValue `
218+ warnings every time the model serializes.
219+ - ** ` COMcheckClient(api_key=...) ` does not auto-read env vars.** No
220+ ` COM_API_KEY ` fallback exists in ` __init__ ` . Pass it explicitly.
221+ - ** ` SimulationStatus ` is a known-values catalog, not an exhaustive
222+ contract.** The server may emit lifecycle values not yet in the
223+ enum (e.g. ` EVALUATING ` was added in a later version). The
224+ ` status ` field comes back as a plain ` str ` so unknown values
225+ don't crash polling. Only ` SUCCESS ` and ` FAILED ` are guaranteed
226+ terminal — break/raise on those, keep polling for everything
227+ else (don't enumerate non-terminals).
228+
135229## When you need more detail
136230
137231- For envelope assemblies (roof, walls, floor, windows, doors,
0 commit comments