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
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,22 @@ All notable changes to this project are documented in this file.
- `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
- Energy credit operations module (`project_credit_operations`) for managing energy credits (`efficiencyPackages`) per building area
- `add_energy_credit_to_project` / `update_energy_credit_in_project` / `remove_energy_credit_from_project` — manage a building area's energy credits
- `get_energy_credit_keys_from_project` — list the energy credits in a project
- `set_energy_credit_multiplier_exception_in_project` and `set_core_and_shell_credit_in_project` — set the project-level credit fields
- `COMcheckClient.calculate_energy_credits` — calculate proposed and required energy credits via `POST /update-energy-credits`, returning an `EnergyCreditResult`
- `COMCheckApiService.update_energy_credits` service method
- `EnergyCreditResult`, `BuildingAreaCredit`, and `EnergyCreditMessage` response models
- `examples/client/credits.py` example
- Schema: Vermont 2024 credit types (`VT2024EnergyCreditTypeOptions`), all eight `EnergyCreditMultiplierException` values, and the `EfficiencyPackage` inputs read by the IECC 2024 / Vermont 2024 credits (`onSiteRenewableCapacity`, `airInfiltrationMeasurementType`, `demandResponseSignal`, `demandResponseRequired`, `ventilationShiftIncluded`, `sumOfGuestRoomFloorAreaWithKitchens`, `sumOfTotalGuestRoomFloorArea`, `tsprProposed`, `tsprTarget`, `tsprReference`, `tsprMpf`)

### Changed
- `ComBuilding.efficiencyPackages` is now typed `list[EfficiencyPackage]` (was `list[Any]`); the schema used `item` instead of `items`, so entries were never validated
- `remove_building_area_from_project` also removes the building area's energy credits

### Deprecated
- `EfficiencyPackage.renewableCapacity` — use `onSiteRenewableCapacity`. The API still accepts it and copies it to `onSiteRenewableCapacity`.



Expand Down
2 changes: 2 additions & 0 deletions comcheck_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
# Project Operations
from .project_operations import (
project_building_area_operations,
project_credit_operations,
project_envelope_operations,
project_exterior_lighting_operations,
project_interior_lighting_operations,
Expand Down Expand Up @@ -89,6 +90,7 @@
"defaults",
# Project Operations
"project_building_area_operations",
"project_credit_operations",
"project_envelope_operations",
"project_exterior_lighting_operations",
"project_interior_lighting_operations",
Expand Down
50 changes: 45 additions & 5 deletions comcheck_api/ai/skill/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ Triggers:
- **Project shape**: a project is a `ComBuilding` Pydantic model with
lowercase top-level fields: `project` (metadata), `location`,
`envelope`, `lighting` (which contains `wholeBldgUse[]` — the
building areas), `hvac`, `renewable`, and `control` (energy code).
building areas), `hvac`, `renewable`, `efficiencyPackages` (energy
credits), and `control` (energy code).
No `Project`/`Control` PascalCase aliases exist.
The fields `hvac` and the shared `fixtureSchedule[]`
exist on the model but have **no operation functions** — leave them
Expand All @@ -40,6 +41,7 @@ Triggers:
- `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
- `project_credit_operations` — energy credits (`EfficiencyPackage` items) per building area, multiplier exception, core and shell credit
- **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 @@ -79,6 +81,12 @@ Triggers:
`proposedOffsiteRenewableEnergy`. Both mutate and return the same
project, like `update_uvalues`. Both send the full project; the energy
code comes from `project.control.code`.
- **Energy credits are calculated server-side**:
`calculate_energy_credits(project)` returns an `EnergyCreditResult`
(`energyCredits[]` per building area and credit type, `proposed`,
`required`, renewable and load management totals, `warnings`,
`userAlerts`, and a derived `passes`). Unlike the renewable methods it
does **not** modify the project.

## Quick start

Expand Down Expand Up @@ -171,12 +179,14 @@ print(result["performanceRating"])
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`) and the renewable methods
(`area_for_renewable`, `renewable_capacity`). If asked for an
`calculate_interior_spaces_allowed_wattage`), the renewable methods
(`area_for_renewable`, `renewable_capacity`), and
`calculate_energy_credits`. If asked for an
unsupported mutation area, tell the user it's not implemented and
offer building-area / envelope / lighting / renewable / simulation instead.
offer building-area / envelope / lighting / renewable / credits / simulation instead.
`comcheck_api.list_operations()` enumerates the `building_area`,
`envelope`, `interior_lighting`, `exterior_lighting`, and `renewable` groups.
`envelope`, `interior_lighting`, `exterior_lighting`, `renewable`, and
`credits` groups.

## Common patterns

Expand Down Expand Up @@ -438,6 +448,36 @@ project = client.area_for_renewable(project)
project = client.renewable_capacity(project)
```

### Adding energy credits

Energy credits apply to IECC 2021+, ASHRAE 90.1-2022, and state codes
based on them; each code family has its own credit type enum
(`EfficiencyPackageTypeOptions`, `IECC2024EnergyCreditTypeOptions`,
`VT2024EnergyCreditTypeOptions`, ...). Pick credit types matching
`project.control.code`. A building area holds at most one credit per type.

```python
from comcheck_api import project_credit_operations as cr_ops
from comcheck_api.types.core_types import (
EfficiencyPackage,
IECC2024EnergyCreditTypeOptions as Credit,
)

area_key = ba_ops.get_building_area_keys_from_project(project)[0]["key"]
project = cr_ops.add_energy_credit_to_project(
project,
area_key,
EfficiencyPackage(
type=Credit.EFF_PACKAGE_ENERGY_CREDIT_IECC2024_REDUCE_AIR_INFILTRATION,
airInfiltration=0.25,
airInfiltrationMeasurementType="AIR_INFILTRATION_MEASUREMENT_75PA",
),
)

result = client.calculate_energy_credits(project) # project is not modified
print(result.required, result.proposed, result.passes)
```

## Gotchas

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

Operation functions are free functions in five modules:
Operation functions are free functions in six 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`
- `comcheck_api.project_operations.project_credit_operations`

Each function takes a `ComBuilding` and a payload, and returns a new
`ComBuilding`. Treat them as immutable transformations.
Expand Down Expand Up @@ -206,6 +207,31 @@ Use `get_default_renewable_system_template()` and
`description` is the identifier — it is unique within each list and is
auto-generated if missing.

## Energy credit operations

```python
from comcheck_api import project_credit_operations as cr_ops
```

Energy credits live in the flat `project.efficiencyPackages` list. Each
`EfficiencyPackage` belongs to a building area (`bldgUseKey` =
`WholeBldgUse.key`) and is identified by its `type`; a building area holds
at most one credit per type. The operations don't check `control.code` —
pick credit types from the enum for the project's code family.

| Function | Notes |
|---|---|
| `add_energy_credit_to_project(project, building_area_key, new_energy_credit)` | Sets `bldgUseKey`. Raises `ValueError` if the building area is missing or already has that credit type. |
| `update_energy_credit_in_project(project, building_area_key, credit_type, updates)` | Revalidates the merged entry. Raises `ValueError` for `type` / `bldgUseKey` updates (remove + add instead). |
| `remove_energy_credit_from_project(project, building_area_key, credit_type)` | Remove one credit. |
| `get_energy_credit_keys_from_project(project, building_area_key=None)` | List `[{bldgUseKey, type}, …]`, optionally for one building area. |
| `set_energy_credit_multiplier_exception_in_project(project, multiplier_exception)` | Set `energyCreditMultiplierException` (IECC 2024 Appendix C/F). Raises `TypeError` for non-enum values. |
| `set_core_and_shell_credit_in_project(project, core_and_shell_credit)` | Set `projectCoreAndShellCredit`; `None` clears it. Raises `ValueError` for negative values. |

`remove_building_area_from_project` also removes the building area's
energy credits. Use `client.calculate_energy_credits(project)` for the
proposed and required credits.

## U-value calculation requires a construction type

When `update_uvalues` (or `start_run_simulation`) recalculates assembly
Expand Down
9 changes: 5 additions & 4 deletions comcheck_api/ai/skill/scripts/validate_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ def _read_input(arg: str) -> str:
UNSUPPORTED_PROJECT_ATTRS = {"hvac"}
# `wholeBldgUse` (building areas + interior lighting), `activityUse` (interior
# 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.
# `renewable` (renewable energy systems and procurement), and `efficiencyPackages`
# (energy credits) all have operation modules. Only `hvac` and `fixtureSchedule`
# have no operations yet.
UNSUPPORTED_LIGHTING_ATTRS = {
"fixtureSchedule",
}
Expand Down Expand Up @@ -80,8 +81,8 @@ def validate(code: str) -> dict:
1. Syntax check via :func:`compile`.
2. Import check on every imported module name.
3. Scope check that the code only uses operations actually exposed
by the SDK and does not mutate the unsupported `hvac`,
`renewable`, or `lighting.fixtureSchedule` subtrees.
by the SDK and does not mutate the unsupported `hvac` or
`lighting.fixtureSchedule` subtrees.
"""
errors: list[dict] = []

Expand Down
23 changes: 23 additions & 0 deletions comcheck_api/api/api_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,29 @@ def area_for_renewable(self, project_data: Dict[str, Any]) -> Dict[str, Any]:
except Exception as error:
self._handle_api_error(error)

def update_energy_credits(self, project_data: Dict[str, Any]) -> Dict[str, Any]:
"""Calculate the proposed and required energy credits for a project.

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

Returns:
API response data as dictionary. ``data`` holds ``energyCredits``,
``proposed``, ``required``, ``proposedRenewableAndLoadMgmt``,
``requiredRenewableAndLoadMgmt``, ``warnings``, and ``userAlerts``.

Raises:
COMCheckHTTPError: If the API returns an error status
COMCheckConnectionError: If the request fails
"""
try:
client = self._get_client()
response = client.post("/update-energy-credits", 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
19 changes: 19 additions & 0 deletions comcheck_api/client/comcheck_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
COMCheckProjectNotFoundError,
COMCheckSimulationError,
)
from comcheck_api.types.api_types import EnergyCreditResult
from comcheck_api.types.common_types import InteriorSpace
from comcheck_api.types.core_types import ComBuilding

Expand Down Expand Up @@ -358,6 +359,24 @@ def renewable_capacity(self, project: ComBuilding) -> ComBuilding:
)
return project

def calculate_energy_credits(self, project: ComBuilding) -> EnergyCreditResult:
"""Calculate the proposed and required energy credits of a project.

Sends the project to the update-energy-credits endpoint. Nothing is
written back to the project — the project model has no fields for
calculated credits.

Args:
project: The project whose energy credits should be calculated.

Returns:
The credits achieved per building area and credit type, the proposed
and required totals, and any warnings and user alerts.
"""
project_data = project.model_dump(mode="json")
credits = self._service.update_energy_credits(project_data).get("data") or {}
return EnergyCreditResult.model_validate(credits)

def calculate_interior_space_allowed_wattage(
self, interior_space: InteriorSpace, energy_code: str
) -> Any:
Expand Down
2 changes: 2 additions & 0 deletions comcheck_api/introspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

from comcheck_api import (
project_building_area_operations,
project_credit_operations,
project_envelope_operations,
project_exterior_lighting_operations,
project_interior_lighting_operations,
Expand All @@ -34,6 +35,7 @@
"interior_lighting": project_interior_lighting_operations,
"exterior_lighting": project_exterior_lighting_operations,
"renewable": project_renewable_operations,
"credits": project_credit_operations,
}


Expand Down
2 changes: 2 additions & 0 deletions comcheck_api/project_operations/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from . import (
project_building_area_operations,
project_credit_operations,
project_envelope_operations,
project_exterior_lighting_operations,
project_interior_lighting_operations,
Expand All @@ -10,6 +11,7 @@

__all__ = [
"project_building_area_operations",
"project_credit_operations",
"project_envelope_operations",
"project_exterior_lighting_operations",
"project_interior_lighting_operations",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Project Building Area Operations."""

from typing import Any
from pydantic.experimental.missing_sentinel import MISSING

from comcheck_api.constants.building_area_constants import DEFAULT_BUILDING_AREA
from comcheck_api.types.core_types import ComBuilding, WholeBldgUse
Expand Down Expand Up @@ -91,6 +92,8 @@ def remove_building_area_from_project(
) -> ComBuilding:
"""Remove an existing building area in the project.

Energy credits (``efficiencyPackages``) of the building area are removed too.

Args:
project: The project object to modify
building_area_key: The key of the building area to update
Expand All @@ -106,6 +109,12 @@ def remove_building_area_from_project(
updated_project.lighting.remove_from_subcomponent_list(
subcomponent_id=building_area_key, subcomponent_name="wholeBldgUse"
)
if updated_project.efficiencyPackages is not MISSING:
updated_project.efficiencyPackages = [
efficiency_package
for efficiency_package in updated_project.efficiencyPackages
if efficiency_package.bldgUseKey != building_area_key
]

return updated_project

Expand Down
Loading
Loading