Skip to content

Commit 5cf5b0c

Browse files
Merge pull request #20 from pnnl/main
main -> develop
2 parents a83dd58 + 8fa04e2 commit 5cf5b0c

14 files changed

Lines changed: 702 additions & 8 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,4 @@ site/
6161

6262
# Build artifacts
6363
dist/
64+
reports/

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# COMcheck API — Python Package
22

3-
A type-safe Python package for the [COMcheck Web](https://comcheck.energycode.pnl.gov) — PNNL's hosted service for commercial building energy-code (ASHRAE 90.1 / IECC) compliance. Use it to build COMcheck projects, run compliance simulations, and read results from Python over the live API. Requires a free Personal Access Token (see below). The package is maintained with `uv`.
3+
A type-safe Python package for the [COMcheck Web](https://comcheck.energycode.pnl.gov) — PNNL's hosted service for commercial building energy-code (ASHRAE 90.1 / IECC) compliance. Use it to build COMcheck projects, run compliance simulations, check compliance and requirements, generate PDF reports, and read results from Python over the live API. Requires a free Personal Access Token (see below). The package is maintained with `uv`.
44

55
**Requirements:**
66
- Python: `>=3.12`

comcheck_api/ai/skill/SKILL.md

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,13 @@ Triggers:
5757
`get_simulation_result`. See `comcheck_api.types.SimulationStatus`
5858
for known lifecycle values (the catalog isn't exhaustive — only
5959
the terminal pair is guaranteed stable).
60+
- **U-values are calculated server-side**: `update_uvalues(project)`
61+
fetches the proposed/effective u-values for the envelope and writes
62+
them back onto the matching `agWall`, `bgWall`, `roof`, and `floor`
63+
assemblies (only these get calculated u-values; `effectiveUFactor`
64+
is `agWall`-only). `start_run_simulation` calls this automatically,
65+
so you rarely call it directly — use it only when you need refreshed
66+
u-values on a project outside the simulation flow.
6067

6168
## Quick start
6269

@@ -148,11 +155,14 @@ print(result["performanceRating"])
148155
editable through `project_building_area_operations`; the per-
149156
activity lighting nested under `activityUse[]` is not. The
150157
`COMcheckClient` user methods (`list_projects`, `get_project`,
151-
`update_project`, `start_run_simulation`, `get_simulation_status`,
152-
`get_simulation_result`, `set_api_key`) are fully supported and
153-
fine to use. If asked for an unsupported mutation area, tell the
154-
user it's not implemented and offer building-area / envelope /
155-
simulation instead. Confirm operation scope with
158+
`update_project`, `update_uvalues`, `start_run_simulation`,
159+
`get_simulation_status`, `get_simulation_result`, `set_api_key`)
160+
are fully supported and
161+
fine to use. The compliance/report client methods
162+
(`check_UA_compliance`, `check_requirements`, `generate_report`) are
163+
also fully supported. If asked for an unsupported mutation area,
164+
tell the user it's not implemented and offer building-area /
165+
envelope / simulation instead. Confirm operation scope with
156166
`comcheck_api.list_operations()` (only `building_area` and
157167
`envelope` groups exist).
158168

@@ -230,6 +240,38 @@ else:
230240
raise TimeoutError(f"Simulation {session_id} did not complete in 5 min")
231241
```
232242

243+
### Checking compliance/requirements and generating a report
244+
245+
These are synchronous (no polling). All three take a `ComBuilding`
246+
directly.
247+
248+
```python
249+
# Per-category compliance status
250+
compliance = client.check_UA_compliance(project)
251+
if compliance["mandatoryRequirementsMet"]:
252+
...
253+
254+
# Applicable requirements
255+
requirements = client.check_requirements(project)
256+
257+
# PDF report — returns {url, expires, fileName}. The url is a short-lived
258+
# presigned S3 URL (expires within minutes); don't cache it.
259+
report = client.generate_report(project)
260+
261+
# Download the PDF (saves using the server fileName; default dir is ~/Downloads)
262+
report = client.generate_report(project, download=True)
263+
report = client.generate_report(project, download=True, download_dir="./out")
264+
265+
# Toggle report sections (all default to True)
266+
report = client.generate_report(
267+
project, envelope=True, intlighting=True, extlighting=False, mechanical=True
268+
)
269+
```
270+
271+
`generate_report` deliberately does **not** open a browser — it's a
272+
library, so it returns metadata and lets the caller decide
273+
(`webbrowser.open(report["url"])`).
274+
233275
## Gotchas
234276

235277
- **Field names are lowercase camelCase**, not PascalCase.
@@ -281,5 +323,7 @@ else:
281323
- For Pydantic model field-level details → read `reference/types.md`.
282324
- For the simulation start/poll/fetch flow → read
283325
`reference/simulation.md`.
326+
- For compliance checks, requirements, and PDF report generation →
327+
read `reference/compliance.md`.
284328
- To validate generated code against a mocked client → run
285329
`scripts/validate_code.py`.
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Compliance, Requirements & Reports Reference
2+
3+
Three client methods evaluate a project without running the full
4+
async simulation flow. All three accept a `ComBuilding` and serialize
5+
it for you — pass the project model directly.
6+
7+
```python
8+
from comcheck_api import COMcheckClient
9+
client = COMcheckClient(api_key="...")
10+
```
11+
12+
| Method | Returns | Purpose |
13+
|---|---|---|
14+
| `client.check_UA_compliance(project)` | `dict` | Per-category compliance status for the project. |
15+
| `client.check_requirements(project)` | `dict` | The applicable requirements for the project. |
16+
| `client.generate_report(project, ...)` | `dict` | Generate a PDF report; returns `{url, expires, fileName}`. |
17+
18+
## check_UA_compliance
19+
20+
Returns a dict with a top-level `mandatoryRequirementsMet` flag and a
21+
status object for each category:
22+
23+
- `envelopeStatus`
24+
- `interiorLightingStatus`
25+
- `exteriorLightingStatus`
26+
- `renewableStatus`
27+
- `energyCreditStatus`
28+
29+
```python
30+
compliance = client.check_UA_compliance(project)
31+
if compliance["mandatoryRequirementsMet"]:
32+
...
33+
```
34+
35+
## check_requirements
36+
37+
Returns the applicable requirements payload for the project. Like
38+
`check_UA_compliance`, it's a single synchronous call — no polling.
39+
40+
```python
41+
requirements = client.check_requirements(project)
42+
```
43+
44+
## generate_report
45+
46+
Builds a PDF report. The PDF is stored in S3; the API returns a
47+
**short-lived presigned URL** (expires within a few minutes) plus the
48+
file name:
49+
50+
```python
51+
report = client.generate_report(project)
52+
# {"url": "...", "expires": "in 5 minutes", "fileName": "report...pdf"}
53+
```
54+
55+
### Signature
56+
57+
```python
58+
client.generate_report(
59+
project,
60+
envelope=True,
61+
extlighting=True,
62+
intlighting=True,
63+
mechanical=True,
64+
download=False,
65+
download_dir=None,
66+
)
67+
```
68+
69+
- The four section flags toggle which sections appear in the report
70+
(all default to `True`).
71+
- `download=True` fetches the PDF from the presigned URL and saves it
72+
using the server-provided `fileName`. `download_dir` defaults to the
73+
user's `~/Downloads` folder.
74+
75+
```python
76+
report = client.generate_report(project, download=True) # ~/Downloads
77+
report = client.generate_report(project, download=True, download_dir="./out")
78+
```
79+
80+
## Don't
81+
82+
- Don't try to open the report in a browser from library code — this
83+
method intentionally returns metadata only. If a browser is wanted,
84+
the caller does `webbrowser.open(report["url"])`.
85+
- Don't cache the presigned `url` — it expires within minutes.
86+
Re-call `generate_report` to get a fresh one.

comcheck_api/ai/skill/reference/operations.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,24 @@ roof.cavityRValue = 38.0
9999
roof.orientation = OrientationOptions.UNSPECIFIED_ORIENTATION
100100
project = env_ops.add_roof_to_project(project, area_key, roof)
101101
```
102+
103+
## U-value calculation requires a construction type
104+
105+
When `update_uvalues` (or `start_run_simulation`) recalculates assembly
106+
u-values, the engine needs a valid construction-type field to classify
107+
each assembly — `roofType` for roofs, `wallType` for walls, etc. If it's
108+
missing or null, the engine falls back to an `"Other"` classification,
109+
returns a `propUValue` of `0.0`, and the response comes back with
110+
`assemblyType: "Other"` instead of the value you sent. Because the client
111+
matches results back by `assemblyType`, an `"Other"` result won't match
112+
your assembly and its u-value is silently left unchanged.
113+
114+
The default templates set these fields (e.g. `roofType=ABOVE_DECK_ROOF`),
115+
so this only bites when you build an assembly by hand or clear the type.
116+
Keep the construction-type field populated:
117+
118+
```python
119+
from comcheck_api.types import RoofTypeOptions
120+
121+
roof.roofType = RoofTypeOptions.ABOVE_DECK_ROOF # don't leave this null
122+
```

comcheck_api/ai/skill/reference/simulation.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ client = COMcheckClient(api_key="...")
4343

4444
| Method | Returns | Purpose |
4545
|---|---|---|
46-
| `client.start_run_simulation(project, project_id=None)` | `str` (session ID) | Kick off the simulation. If `project_id` is provided, the project is also saved/updated. |
46+
| `client.start_run_simulation(project, project_id=None)` | `str` (session ID) | Kick off the simulation. Always refreshes the envelope u-values first (via `update_uvalues`). If `project_id` is provided, the project is also saved/updated. |
47+
| `client.update_uvalues(project)` | `ComBuilding` | Calculate assembly u-values and write them back onto the project's `agWall`, `bgWall`, `roof`, and `floor` assemblies (matched by `assemblyType`). Mutates and returns the same project. Called automatically by `start_run_simulation`. |
4748
| `client.get_simulation_status(session_id)` | `dict` | Current status. Fields: `sessionId`, `status` (see the Status values table above), optional `message`. |
4849
| `client.get_simulation_result(session_id)` | `dict` | Final result. Fields: `sessionId`, `performanceRating`, `energyCreditPerformanceRating`, `proposedBpf`, `baselineBpf`. |
4950

comcheck_api/api/api_services.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,102 @@ def update_project(
187187
except Exception as error:
188188
self._handle_api_error(error)
189189

190+
def assemblies_uvalue(
191+
self, envelope_data: Dict[str, Any], energy_code
192+
) -> Dict[str, Any]:
193+
"""get assemblies u values
194+
195+
Args:
196+
envelope_data: The envelope data to send in the request body
197+
energy_code: The energy code for the api end point path
198+
199+
Returns:
200+
RunSimulationResponse with session information
201+
202+
Raises:
203+
COMCheckHTTPError: If the API returns an error status
204+
COMCheckConnectionError: If the request fails
205+
"""
206+
try:
207+
client = self._get_client()
208+
response = client.post(
209+
f"/{energy_code}/assemblies/uvalues", json=envelope_data
210+
)
211+
response.raise_for_status()
212+
# may need validation here.
213+
return response.json()
214+
except Exception as error:
215+
self._handle_api_error(error)
216+
217+
def check_UA_compliance(self, project_data: Dict[str, Any]) -> Dict[str, Any]:
218+
"""Check UA path compliance for a project.
219+
220+
Args:
221+
project_data: The project data to send in the request body
222+
223+
Returns:
224+
API response data as dictionary
225+
226+
Raises:
227+
COMCheckHTTPError: If the API returns an error status
228+
COMCheckConnectionError: If the request fails
229+
"""
230+
try:
231+
client = self._get_client()
232+
response = client.post("/compliance", json=project_data)
233+
response.raise_for_status()
234+
return response.json()
235+
except Exception as error:
236+
self._handle_api_error(error)
237+
238+
def check_requirements(self, project_data: Dict[str, Any]) -> Dict[str, Any]:
239+
"""Check requirements for a project.
240+
241+
Args:
242+
project_data: The project data to send in the request body
243+
244+
Returns:
245+
API response data as dictionary
246+
247+
Raises:
248+
COMCheckHTTPError: If the API returns an error status
249+
COMCheckConnectionError: If the request fails
250+
"""
251+
try:
252+
client = self._get_client()
253+
response = client.post("/requirements", json=project_data)
254+
response.raise_for_status()
255+
return response.json()
256+
except Exception as error:
257+
self._handle_api_error(error)
258+
259+
def generate_report(self, report_data: Dict[str, Any]) -> Dict[str, Any]:
260+
"""Generate a PDF report for a project.
261+
262+
The API stores the generated PDF in S3 and returns a presigned URL
263+
to download it.
264+
265+
Args:
266+
report_data: The report request body, containing ``building`` (the
267+
project data) and the ``envelope``, ``extlighting``,
268+
``intlighting``, and ``mechanical`` section flags.
269+
270+
Returns:
271+
API response as a dictionary with ``url`` (the presigned S3 URL),
272+
``expires``, and ``fileName``
273+
274+
Raises:
275+
COMCheckHTTPError: If the API returns an error status
276+
COMCheckConnectionError: If the request fails
277+
"""
278+
try:
279+
client = self._get_client()
280+
response = client.post("/report", json=report_data)
281+
response.raise_for_status()
282+
return response.json()
283+
except Exception as error:
284+
self._handle_api_error(error)
285+
190286
def start_run_simulation(
191287
self, project_data: Dict[str, Any]
192288
) -> RunSimulationResponse:

0 commit comments

Comments
 (0)