Skip to content
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,4 @@ site/

# Build artifacts
dist/
reports/
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# COMcheck API — Python Package

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`.
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`.

**Requirements:**
- Python: `>=3.12`
Expand Down
54 changes: 49 additions & 5 deletions comcheck_api/ai/skill/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ Triggers:
`get_simulation_result`. See `comcheck_api.types.SimulationStatus`
for known lifecycle values (the catalog isn't exhaustive — only
the terminal pair is guaranteed stable).
- **U-values are calculated server-side**: `update_uvalues(project)`
fetches the proposed/effective u-values for the envelope and writes
them back onto the matching `agWall`, `bgWall`, `roof`, and `floor`
assemblies (only these get calculated u-values; `effectiveUFactor`
is `agWall`-only). `start_run_simulation` calls this automatically,
so you rarely call it directly — use it only when you need refreshed
u-values on a project outside the simulation flow.

## Quick start

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

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

### Checking compliance/requirements and generating a report

These are synchronous (no polling). All three take a `ComBuilding`
directly.

```python
# Per-category compliance status
compliance = client.check_UA_compliance(project)
if compliance["mandatoryRequirementsMet"]:
...

# Applicable requirements
requirements = client.check_requirements(project)

# PDF report — returns {url, expires, fileName}. The url is a short-lived
# presigned S3 URL (expires within minutes); don't cache it.
report = client.generate_report(project)

# Download the PDF (saves using the server fileName; default dir is ~/Downloads)
report = client.generate_report(project, download=True)
report = client.generate_report(project, download=True, download_dir="./out")

# Toggle report sections (all default to True)
report = client.generate_report(
project, envelope=True, intlighting=True, extlighting=False, mechanical=True
)
```

`generate_report` deliberately does **not** open a browser — it's a
library, so it returns metadata and lets the caller decide
(`webbrowser.open(report["url"])`).

## Gotchas

- **Field names are lowercase camelCase**, not PascalCase.
Expand Down Expand Up @@ -281,5 +323,7 @@ else:
- For Pydantic model field-level details → read `reference/types.md`.
- For the simulation start/poll/fetch flow → read
`reference/simulation.md`.
- For compliance checks, requirements, and PDF report generation →
read `reference/compliance.md`.
- To validate generated code against a mocked client → run
`scripts/validate_code.py`.
86 changes: 86 additions & 0 deletions comcheck_api/ai/skill/reference/compliance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Compliance, Requirements & Reports Reference

Three client methods evaluate a project without running the full
async simulation flow. All three accept a `ComBuilding` and serialize
it for you — pass the project model directly.

```python
from comcheck_api import COMcheckClient
client = COMcheckClient(api_key="...")
```

| Method | Returns | Purpose |
|---|---|---|
| `client.check_UA_compliance(project)` | `dict` | Per-category compliance status for the project. |
| `client.check_requirements(project)` | `dict` | The applicable requirements for the project. |
| `client.generate_report(project, ...)` | `dict` | Generate a PDF report; returns `{url, expires, fileName}`. |

## check_UA_compliance

Returns a dict with a top-level `mandatoryRequirementsMet` flag and a
status object for each category:

- `envelopeStatus`
- `interiorLightingStatus`
- `exteriorLightingStatus`
- `renewableStatus`
- `energyCreditStatus`

```python
compliance = client.check_UA_compliance(project)
if compliance["mandatoryRequirementsMet"]:
...
```

## check_requirements

Returns the applicable requirements payload for the project. Like
`check_UA_compliance`, it's a single synchronous call — no polling.

```python
requirements = client.check_requirements(project)
```

## generate_report

Builds a PDF report. The PDF is stored in S3; the API returns a
**short-lived presigned URL** (expires within a few minutes) plus the
file name:

```python
report = client.generate_report(project)
# {"url": "...", "expires": "in 5 minutes", "fileName": "report...pdf"}
```

### Signature

```python
client.generate_report(
project,
envelope=True,
extlighting=True,
intlighting=True,
mechanical=True,
download=False,
download_dir=None,
)
```

- The four section flags toggle which sections appear in the report
(all default to `True`).
- `download=True` fetches the PDF from the presigned URL and saves it
using the server-provided `fileName`. `download_dir` defaults to the
user's `~/Downloads` folder.

```python
report = client.generate_report(project, download=True) # ~/Downloads
report = client.generate_report(project, download=True, download_dir="./out")
```

## Don't

- Don't try to open the report in a browser from library code — this
method intentionally returns metadata only. If a browser is wanted,
the caller does `webbrowser.open(report["url"])`.
- Don't cache the presigned `url` — it expires within minutes.
Re-call `generate_report` to get a fresh one.
21 changes: 21 additions & 0 deletions comcheck_api/ai/skill/reference/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,24 @@ roof.cavityRValue = 38.0
roof.orientation = OrientationOptions.UNSPECIFIED_ORIENTATION
project = env_ops.add_roof_to_project(project, area_key, roof)
```

## U-value calculation requires a construction type

When `update_uvalues` (or `start_run_simulation`) recalculates assembly
u-values, the engine needs a valid construction-type field to classify
each assembly — `roofType` for roofs, `wallType` for walls, etc. If it's
missing or null, the engine falls back to an `"Other"` classification,
returns a `propUValue` of `0.0`, and the response comes back with
`assemblyType: "Other"` instead of the value you sent. Because the client
matches results back by `assemblyType`, an `"Other"` result won't match
your assembly and its u-value is silently left unchanged.

The default templates set these fields (e.g. `roofType=ABOVE_DECK_ROOF`),
so this only bites when you build an assembly by hand or clear the type.
Keep the construction-type field populated:

```python
from comcheck_api.types import RoofTypeOptions

roof.roofType = RoofTypeOptions.ABOVE_DECK_ROOF # don't leave this null
```
3 changes: 2 additions & 1 deletion comcheck_api/ai/skill/reference/simulation.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ client = COMcheckClient(api_key="...")

| Method | Returns | Purpose |
|---|---|---|
| `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. |
| `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. |
| `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`. |
| `client.get_simulation_status(session_id)` | `dict` | Current status. Fields: `sessionId`, `status` (see the Status values table above), optional `message`. |
| `client.get_simulation_result(session_id)` | `dict` | Final result. Fields: `sessionId`, `performanceRating`, `energyCreditPerformanceRating`, `proposedBpf`, `baselineBpf`. |

Expand Down
96 changes: 96 additions & 0 deletions comcheck_api/api/api_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,102 @@ def update_project(
except Exception as error:
self._handle_api_error(error)

def assemblies_uvalue(
self, envelope_data: Dict[str, Any], energy_code
) -> Dict[str, Any]:
"""get assemblies u values

Args:
envelope_data: The envelope data to send in the request body
energy_code: The energy code for the api end point path

Returns:
RunSimulationResponse with session information

Raises:
COMCheckHTTPError: If the API returns an error status
COMCheckConnectionError: If the request fails
"""
try:
client = self._get_client()
response = client.post(
f"/{energy_code}/assemblies/uvalues", json=envelope_data
)
response.raise_for_status()
# may need validation here.
return response.json()
except Exception as error:
self._handle_api_error(error)

def check_UA_compliance(self, project_data: Dict[str, Any]) -> Dict[str, Any]:
"""Check UA path compliance for a project.

Args:
project_data: The project data to send in the request body

Returns:
API response data as dictionary

Raises:
COMCheckHTTPError: If the API returns an error status
COMCheckConnectionError: If the request fails
"""
try:
client = self._get_client()
response = client.post("/compliance", json=project_data)
response.raise_for_status()
return response.json()
except Exception as error:
self._handle_api_error(error)

def check_requirements(self, project_data: Dict[str, Any]) -> Dict[str, Any]:
"""Check requirements for a project.

Args:
project_data: The project data to send in the request body

Returns:
API response data as dictionary

Raises:
COMCheckHTTPError: If the API returns an error status
COMCheckConnectionError: If the request fails
"""
try:
client = self._get_client()
response = client.post("/requirements", json=project_data)
response.raise_for_status()
return response.json()
except Exception as error:
self._handle_api_error(error)

def generate_report(self, report_data: Dict[str, Any]) -> Dict[str, Any]:
"""Generate a PDF report for a project.

The API stores the generated PDF in S3 and returns a presigned URL
to download it.

Args:
report_data: The report request body, containing ``building`` (the
project data) and the ``envelope``, ``extlighting``,
``intlighting``, and ``mechanical`` section flags.

Returns:
API response as a dictionary with ``url`` (the presigned S3 URL),
``expires``, and ``fileName``

Raises:
COMCheckHTTPError: If the API returns an error status
COMCheckConnectionError: If the request fails
"""
try:
client = self._get_client()
response = client.post("/report", json=report_data)
response.raise_for_status()
return response.json()
except Exception as error:
self._handle_api_error(error)

def start_run_simulation(
self, project_data: Dict[str, Any]
) -> RunSimulationResponse:
Expand Down
Loading
Loading