Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,25 @@

All notable changes to this project are documented in this file.

## [Unreleased]

### Added
- Renewable energy operations module (`project_renewable_operations`) for managing on-site renewable systems and off-site procurement
- `set_renewable_exception_in_project` — set the renewable exception on a project
- `update_renewable_in_project` — update scalar fields on the renewable singleton
- `add_renewable_system_to_project` / `update_renewable_system_in_project` / `remove_renewable_system_from_project` — manage on-site renewable systems
- `get_renewable_system_keys_from_project` — list all renewable systems in a project
- `add_offsite_renewable_procurement_to_project` / `update_offsite_renewable_procurement_in_project` / `remove_offsite_renewable_procurement_from_project` — manage offsite renewable procurement
- `get_offsite_renewable_procurement_keys_from_project` — list all offsite renewable procurements
- `get_default_renewable_system_template` and `get_default_offsite_renewable_procurement_template` default templates
- `COMcheckClient.area_for_renewable` — calculate `largestThreeFloorArea` and `roofAreaForRenewable` via `POST /renewable/area-for-renewable` and write them onto `project.renewable`
- `COMcheckClient.renewable_capacity` — calculate required/proposed renewable capacity and off-site renewable energy via `POST /renewable/renewable-capacity` and write them onto `project.renewable`
- `COMCheckApiService.area_for_renewable` and `COMCheckApiService.renewable_capacity` service methods
- `examples/client/renewable.py` example
- `RenewableSystem` and `OffsiteRenewableProcurement` added to `MODEL_TO_ID_INFO` with auto-generated descriptions



## [2.1.0] - 2026-10-16

### Added
Expand Down
2 changes: 2 additions & 0 deletions comcheck_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
project_envelope_operations,
project_exterior_lighting_operations,
project_interior_lighting_operations,
project_renewable_operations,
)

# Introspection helpers
Expand Down Expand Up @@ -91,6 +92,7 @@
"project_envelope_operations",
"project_exterior_lighting_operations",
"project_interior_lighting_operations",
"project_renewable_operations",
# Introspection
"list_operations",
"lookup_type",
Expand Down
71 changes: 65 additions & 6 deletions comcheck_api/ai/skill/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Triggers:
`envelope`, `lighting` (which contains `wholeBldgUse[]` — the
building areas), `hvac`, `renewable`, and `control` (energy code).
No `Project`/`Control` PascalCase aliases exist.
The fields `hvac`, `renewable`, and the shared `fixtureSchedule[]`
The fields `hvac` and the shared `fixtureSchedule[]`
exist on the model but have **no operation functions** — leave them
at template defaults. All other mutable areas have dedicated modules.
- **Operation modules (functional)**: all mutations go through free
Expand All @@ -39,6 +39,7 @@ Triggers:
- `project_envelope_operations` — roofs, walls, floors, windows, doors, skylights, thermal bridges
- `project_interior_lighting_operations` — `ActivityUse` items, aliased as `InteriorSpace` (interior lighting spaces + fixtures)
- `project_exterior_lighting_operations` — `ExteriorUse` items, aliased as `ExteriorArea`, + zone type
- `project_renewable_operations` — renewable exception, on-site systems, off-site procurement
- **Envelope items attach to a building-area key**: every
`add_*_to_project` envelope function takes
`(project, building_area_key, new_component)`. Look up the key
Expand Down Expand Up @@ -70,6 +71,14 @@ Triggers:
method mutates the input or writes to `allowedWattage`; both return the
raw calculation payload (`{"spaceAllowedWattage": ...}` for the single
form, `{areaDescription: wattage, ...}` for the list form).
- **Renewable areas and capacity are calculated server-side**:
`area_for_renewable(project)` writes `largestThreeFloorArea` and
`roofAreaForRenewable` onto `project.renewable`;
`renewable_capacity(project)` writes `requiredCapacity`,
`proposedCapacity`, `requiredOffsiteRenewableEnergy`, and
`proposedOffsiteRenewableEnergy`. Both mutate and return the same
project, like `update_uvalues`. Both send the full project; the energy
code comes from `project.control.code`.

## Quick start

Expand Down Expand Up @@ -153,20 +162,21 @@ print(result["performanceRating"])
bypass the validation logic in the operation modules. Always go
through `project_envelope_operations` and
`project_building_area_operations` instead.
- Don't add, update, or remove `fixtureSchedule[]`, HVAC/mechanical, or
renewable-energy components — no operations exist for them yet.
- Don't add, update, or remove `fixtureSchedule[]` or HVAC/mechanical
components — no operations exist for them yet.
The `COMcheckClient` user methods (`list_projects`, `get_project`,
`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, as are the allowed-wattage
methods (`calculate_interior_space_allowed_wattage`,
`calculate_interior_spaces_allowed_wattage`). If asked for an
`calculate_interior_spaces_allowed_wattage`) and the renewable methods
(`area_for_renewable`, `renewable_capacity`). If asked for an
unsupported mutation area, tell the user it's not implemented and
offer building-area / envelope / lighting / simulation instead.
offer building-area / envelope / lighting / renewable / simulation instead.
`comcheck_api.list_operations()` enumerates the `building_area`,
`envelope`, `interior_lighting`, and `exterior_lighting` groups.
`envelope`, `interior_lighting`, `exterior_lighting`, and `renewable` groups.

## Common patterns

Expand Down Expand Up @@ -379,6 +389,55 @@ report = client.generate_report(project, use_backstop_compliance=True)
library, so it returns metadata and lets the caller decide
(`webbrowser.open(report["url"])`).

### Adding renewable energy systems

Renewable requirements only apply under some energy codes; the operations
don't check `control.code`, so ask the user whether the project needs
renewables rather than deciding from the code. Exception
and on-site systems coexist — setting an exception doesn't preclude adding
renewable systems. **Confirm with the user before adding off-site
procurement** (not always present on projects).

```python
from comcheck_api import project_renewable_operations as rn_ops
from comcheck_api.defaults import (
get_default_renewable_system_template,
get_default_offsite_renewable_procurement_template,
)
from comcheck_api.types import (
RenewableExceptionOptions,
RenewableSystemTypeOptions,
RenewableSystemCapacityUnitOptions,
OffsiteRenewableProcurementType,
)

# 1. Set a renewable exception (coexists with systems)
project = rn_ops.set_renewable_exception_in_project(
project, RenewableExceptionOptions.RENEWABLE_ONSITE_EXCEPTION_NONE
)

# 2. Add an on-site renewable system
renewable_system = get_default_renewable_system_template()
renewable_system.description = "Rooftop Solar"
renewable_system.renewableSystemType = RenewableSystemTypeOptions.RENEWABLE_SYSTEM_TYPE_SOLAR
renewable_system.renewableSystemCapacity = 50000.0
renewable_system.renewableSystemCapacityUnit = RenewableSystemCapacityUnitOptions.RENEWABLE_SYSTEM_UNIT_WATT
project = rn_ops.add_renewable_system_to_project(project, renewable_system)

# 3. Only add off-site procurement if confirmed with the user
# offsite_procurement = get_default_offsite_renewable_procurement_template()
# offsite_procurement.description = "Wind PPA"
# offsite_procurement.offsiteRenewableProcurementType = (
# OffsiteRenewableProcurementType.OFFSITE_RENEWABLE_PROCUREMENT_PHYSICAL_AGREEMENT
# )
# offsite_procurement.offsiteRenewableEnergy = 100000.0 # kWh
# project = rn_ops.add_offsite_renewable_procurement_to_project(project, offsite_procurement)

# 4. Calculate renewable areas and capacity (written onto project.renewable)
project = client.area_for_renewable(project)
project = client.renewable_capacity(project)
```

## Gotchas

- **Field names are lowercase camelCase**, not PascalCase.
Expand Down
34 changes: 33 additions & 1 deletion comcheck_api/ai/skill/reference/operations.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
# Project Operations Reference

Operation functions are free functions in four modules:
Operation functions are free functions in five modules:

- `comcheck_api.project_operations.project_building_area_operations`
- `comcheck_api.project_operations.project_envelope_operations`
- `comcheck_api.project_operations.project_interior_lighting_operations`
- `comcheck_api.project_operations.project_exterior_lighting_operations`
- `comcheck_api.project_operations.project_renewable_operations`

Each function takes a `ComBuilding` and a payload, and returns a new
`ComBuilding`. Treat them as immutable transformations.
Expand Down Expand Up @@ -174,6 +175,37 @@ Use `get_default_exterior_area_template()` as a starting point.
`areaDescription` is the identifier — it is unique within `exteriorUse[]`
and is auto-generated if missing.

## Renewable energy operations

```python
from comcheck_api import project_renewable_operations as rn_ops
```

Renewable energy is managed at the project level through the
`ComBuilding.renewable` singleton. Renewable requirements only apply under
some energy codes; the operations don't check `control.code` — ask the user
whether the project needs renewables. Exception and on-site systems coexist — setting an
exception does not preclude adding renewable systems. **Confirm with the user
before adding off-site procurement** (not always present on projects).

| Function | Purpose |
|---|---|
| `set_renewable_exception_in_project(project, renewable_exception)` | Set `renewable.renewableException`. Raises `ValueError` for `NoneType_None`, `TypeError` for non-enum values. |
| `update_renewable_in_project(project, updates)` | Update scalar fields on the renewable singleton. Raises `ValueError` for engine-calculated, API-calculated, or list fields. |
| `add_renewable_system_to_project(project, new_renewable_system)` | Add an on-site `RenewableSystem`. |
| `update_renewable_system_in_project(project, renewable_system_description, updates)` | Update a `RenewableSystem` by its `description`. |
| `remove_renewable_system_from_project(project, renewable_system_description)` | Remove a `RenewableSystem` by its `description`. |
| `get_renewable_system_keys_from_project(project)` | List `[{description, renewableSystemType, renewableSystemCapacity, renewableSystemCapacityUnit}, …]` for the project. Works for any code. |
| `add_offsite_renewable_procurement_to_project(project, new_procurement)` | Add an `OffsiteRenewableProcurement`. **Confirm with user first.** |
| `update_offsite_renewable_procurement_in_project(project, procurement_description, updates)` | Update an `OffsiteRenewableProcurement` by its `description`. |
| `remove_offsite_renewable_procurement_from_project(project, procurement_description)` | Remove an `OffsiteRenewableProcurement` by its `description`. |
| `get_offsite_renewable_procurement_keys_from_project(project)` | List `[{description, offsiteRenewableProcurementType, offsiteRenewableEnergy}, …]` for the project. Works for any code. |

Use `get_default_renewable_system_template()` and
`get_default_offsite_renewable_procurement_template()` as starting points.
`description` is the identifier — it is unique within each list and is
auto-generated if missing.

## U-value calculation requires a construction type

When `update_uvalues` (or `start_run_simulation`) recalculates assembly
Expand Down
8 changes: 4 additions & 4 deletions comcheck_api/ai/skill/scripts/validate_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ def _read_input(arg: str) -> str:
return Path(arg).read_text()


UNSUPPORTED_PROJECT_ATTRS = {"hvac", "renewable"}
UNSUPPORTED_PROJECT_ATTRS = {"hvac"}
# `wholeBldgUse` (building areas + interior lighting), `activityUse` (interior
# lighting spaces), `exteriorUse` (exterior lighting), and
# `exteriorLightingZoneType` all have operation modules. Only `fixtureSchedule`
# has no operations yet.
# lighting spaces), `exteriorUse` (exterior lighting), `exteriorLightingZoneType`,
# and `renewable` (renewable energy systems and procurement) all have operation
# modules. Only `hvac` and `fixtureSchedule` have no operations yet.
UNSUPPORTED_LIGHTING_ATTRS = {
"fixtureSchedule",
}
Expand Down
47 changes: 47 additions & 0 deletions comcheck_api/api/api_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,53 @@ def get_simulation_result(self, session_id: str) -> SimulationResultResponse:
except Exception as error:
self._handle_api_error(error)

def renewable_capacity(self, project_data: Dict[str, Any]) -> Dict[str, Any]:
"""Calculate required and proposed renewable capacity for a project.

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

Returns:
API response data as dictionary. ``data`` holds
``requiredRenewableCapacity``, ``proposedRenewableCapacity``,
``requiredOffsiteRenewableProcurementEnergy``,
``proposedOffsiteRenewableProcurementEnergy``, and
``renewableComplianceStatus``.

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

def area_for_renewable(self, project_data: Dict[str, Any]) -> Dict[str, Any]:
"""Calculate the building areas used for renewable requirements.

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

Returns:
API response data as dictionary. ``data`` holds
``largestThreeFloorArea`` and ``roofAreaForRenewable``.

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

def close(self) -> None:
"""Close the HTTP client connection."""
if self._client is not None:
Expand Down
70 changes: 70 additions & 0 deletions comcheck_api/client/comcheck_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@
Mode = Literal["python", "json"]


def _optional_float(value: Any) -> Optional[float]:
"""Convert an API numeric value to ``float``, passing ``None`` through."""
return None if value is None else float(value)


class COMcheckClient:
"""High-level client for the COMcheck Web API.

Expand Down Expand Up @@ -288,6 +293,71 @@ def update_uvalues(self, project: ComBuilding) -> ComBuilding:

return project

def area_for_renewable(self, project: ComBuilding) -> ComBuilding:
"""Calculate renewable area metrics and update the project in place.

Sends the project to the area-for-renewable endpoint and writes the
returned ``largestThreeFloorArea`` and ``roofAreaForRenewable`` values
back onto ``project.renewable``.

Args:
project: The project whose renewable area metrics should be refreshed.

Returns:
The same ``project`` instance, with renewable area metrics updated.
"""
project_data = project.model_dump(mode="json")
areas = self._service.area_for_renewable(project_data).get("data") or {}

renewable = project.renewable
renewable.largestThreeFloorArea = _optional_float(
areas.get("largestThreeFloorArea")
)
renewable.roofAreaForRenewable = _optional_float(
areas.get("roofAreaForRenewable")
)
return project

def renewable_capacity(self, project: ComBuilding) -> ComBuilding:
"""Calculate renewable capacity and update the project in place.

Sends the project to the renewable-capacity endpoint and writes the
returned values back onto ``project.renewable``:

- ``requiredRenewableCapacity`` → ``requiredCapacity``
- ``proposedRenewableCapacity`` → ``proposedCapacity``
- ``requiredOffsiteRenewableProcurementEnergy`` →
``requiredOffsiteRenewableEnergy``
- ``proposedOffsiteRenewableProcurementEnergy`` →
``proposedOffsiteRenewableEnergy``

The endpoint's ``renewableComplianceStatus`` has no field on the
project model and is not written back.

Args:
project: The project whose renewable capacity should be refreshed.

Returns:
The same ``project`` instance, with renewable capacity updated.
"""
project_data = project.model_dump(mode="json")
capacity = self._service.renewable_capacity(project_data).get("data") or {}

renewable = project.renewable
renewable.requiredCapacity = _optional_float(
capacity.get("requiredRenewableCapacity")
)
renewable.proposedCapacity = _optional_float(
capacity.get("proposedRenewableCapacity")
)
renewable.requiredOffsiteRenewableEnergy = _optional_float(
capacity.get("requiredOffsiteRenewableProcurementEnergy")
)
renewable.proposedOffsiteRenewableEnergy = _optional_float(
capacity.get("proposedOffsiteRenewableProcurementEnergy")
)
return project

def calculate_interior_space_allowed_wattage(
self, interior_space: InteriorSpace, energy_code: str
) -> Any:
Expand Down
24 changes: 24 additions & 0 deletions comcheck_api/constants/renewable_constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""Renewable energy constants for COMcheck projects."""

from comcheck_api.types.core_types import (
OffsiteRenewableProcurement,
OffsiteRenewableProcurementType,
RenewableSystem,
RenewableSystemTypeOptions,
RenewableSystemCapacityUnitOptions,
)

DEFAULT_RENEWABLE_SYSTEM: RenewableSystem = RenewableSystem(
description="Solar PV",
renewableSystemType=RenewableSystemTypeOptions.RENEWABLE_SYSTEM_TYPE_SOLAR,
renewableSystemCapacity=0.0,
renewableSystemCapacityUnit=RenewableSystemCapacityUnitOptions.RENEWABLE_SYSTEM_UNIT_WATT,
)

DEFAULT_OFFSITE_RENEWABLE_PROCUREMENT: OffsiteRenewableProcurement = (
OffsiteRenewableProcurement(
description="Offsite Procurement",
offsiteRenewableProcurementType=OffsiteRenewableProcurementType.OFFSITE_RENEWABLE_PROCUREMENT_UNSPECIFIED,
offsiteRenewableEnergy=0.0,
)
)
Loading
Loading