Skip to content
Open
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 @@ -2,6 +2,22 @@

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.update_renewable_area` and `COMCheckApiService.area_for_renewable` placeholder methods (raise `NotImplementedError` until endpoint is available)
- `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
56 changes: 51 additions & 5 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 @@ -153,8 +154,8 @@ 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`)
Expand All @@ -164,9 +165,9 @@ print(result["performanceRating"])
methods (`calculate_interior_space_allowed_wattage`,
`calculate_interior_spaces_allowed_wattage`). 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 +380,51 @@ 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)
```

## 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
29 changes: 29 additions & 0 deletions comcheck_api/api/api_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,35 @@ def get_simulation_result(self, session_id: str) -> SimulationResultResponse:
except Exception as error:
self._handle_api_error(error)

def area_for_renewable(
self, project_data: Dict[str, Any], energy_code: str
) -> Dict[str, Any]:
"""Calculate renewable area metrics (largestThreeFloorArea, roofAreaForRenewable).

.. warning::
Not yet implemented — the ``area-for-renewable`` endpoint is pending
in the external ``comcheck-cdk`` repository.

Args:
project_data: The project data (or subset) to send in the request body.
energy_code: The energy code for the API endpoint path.

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

Raises:
NotImplementedError: Always — the endpoint is not yet available.
"""
# TODO: Implement when the area-for-renewable endpoint is available in comcheck-cdk.
# The results should be written to project.renewable.largestThreeFloorArea
# and project.renewable.roofAreaForRenewable.
raise NotImplementedError(
"The area-for-renewable endpoint is pending implementation in comcheck-cdk. "
"Results will be written to project.renewable.largestThreeFloorArea and "
"project.renewable.roofAreaForRenewable once available."
)

def close(self) -> None:
"""Close the HTTP client connection."""
if self._client is not None:
Expand Down
31 changes: 31 additions & 0 deletions comcheck_api/client/comcheck_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,37 @@ def update_uvalues(self, project: ComBuilding) -> ComBuilding:

return project

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

.. warning::
Not yet implemented — the ``area-for-renewable`` endpoint is pending
in the external ``comcheck-cdk`` repository.

Sends the project (or the needed subset) 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.

Raises:
NotImplementedError: Always — the endpoint is not yet available.
"""
# TODO: Implement when the area-for-renewable endpoint is available in comcheck-cdk.
# This should mirror the pattern of update_uvalues: POST project data to
# /{energy_code}/renewable/area (or similar path), extract the results, and
# write them back to project.renewable.largestThreeFloorArea and
# project.renewable.roofAreaForRenewable.
raise NotImplementedError(
"The area-for-renewable endpoint is pending implementation in comcheck-cdk. "
"This method will calculate largestThreeFloorArea and roofAreaForRenewable "
"from the project envelope/geometry once the endpoint is available."
)

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,
)
)
26 changes: 26 additions & 0 deletions comcheck_api/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
from comcheck_api.constants.exterior_lighting_constants import (
DEFAULT_EXTERIOR_AREA,
)
from comcheck_api.constants.renewable_constants import (
DEFAULT_RENEWABLE_SYSTEM,
DEFAULT_OFFSITE_RENEWABLE_PROCUREMENT,
)
from comcheck_api.constants.envelope_constants import (
DEFAULT_AG_WALL,
DEFAULT_BG_WALL,
Expand Down Expand Up @@ -208,3 +212,25 @@ def get_default_fixture_schedule_template():
# TODO: Implement when DEFAULT_FIXTURE_SCHEDULE constant is available
# return {**DEFAULT_FIXTURE_SCHEDULE, "scheduleFixtureKey": str(uuid4())}
raise NotImplementedError("DEFAULT_FIXTURE_SCHEDULE constant not yet implemented")


def get_default_renewable_system_template():
"""Return a deep copy of the default renewable system template.

Defaults to a Solar PV system with 0.0 capacity in Watts.

Returns:
A new :class:`~comcheck_api.types.core_types.RenewableSystem` instance.
"""
return copy.deepcopy(DEFAULT_RENEWABLE_SYSTEM)


def get_default_offsite_renewable_procurement_template():
"""Return a deep copy of the default offsite renewable procurement template.

Defaults to an unspecified procurement type with 0.0 kWh annual energy.

Returns:
A new :class:`~comcheck_api.types.core_types.OffsiteRenewableProcurement` instance.
"""
return copy.deepcopy(DEFAULT_OFFSITE_RENEWABLE_PROCUREMENT)
2 changes: 2 additions & 0 deletions comcheck_api/introspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@
project_envelope_operations,
project_exterior_lighting_operations,
project_interior_lighting_operations,
project_renewable_operations,
)

_OP_MODULES = {
"building_area": project_building_area_operations,
"envelope": project_envelope_operations,
"interior_lighting": project_interior_lighting_operations,
"exterior_lighting": project_exterior_lighting_operations,
"renewable": project_renewable_operations,
}


Expand Down
10 changes: 7 additions & 3 deletions comcheck_api/managers/data_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,9 +325,7 @@ def modify_one(self, id_value: Any, updates: T | dict[str, Any]) -> T:
# Validate partial updates by attempting to construct with merged data
# This will raise Pydantic ValidationError if types don't align
# Get only the model fields to avoid serializing dynamically added methods
original_dict = original.model_dump(
mode="python", by_alias=False
)
original_dict = original.model_dump(mode="python", by_alias=False)
merged = {**original_dict, **updates_dict}
try:
updated_item = type(original).model_validate(merged)
Expand Down Expand Up @@ -367,6 +365,8 @@ def get_model_info(model_class: Type[BaseModel]) -> IdInfo | None:
Skylight,
ThermalBridge,
WholeBldgUse,
RenewableSystem,
OffsiteRenewableProcurement,
)

MODEL_TO_ID_INFO = {
Expand All @@ -381,5 +381,9 @@ def get_model_info(model_class: Type[BaseModel]) -> IdInfo | None:
Skylight: IdInfo(identifier="assemblyType", id_prefix="Skylight:Skylight"),
ThermalBridge: IdInfo(identifier="id", id_prefix=None),
WholeBldgUse: IdInfo(identifier="key", id_prefix=None),
RenewableSystem: IdInfo(identifier="description", id_prefix="Renewable System"),
OffsiteRenewableProcurement: IdInfo(
identifier="description", id_prefix="Offsite Procurement"
),
}
return MODEL_TO_ID_INFO.get(model_class)
2 changes: 2 additions & 0 deletions comcheck_api/project_operations/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
project_envelope_operations,
project_exterior_lighting_operations,
project_interior_lighting_operations,
project_renewable_operations,
)

__all__ = [
"project_building_area_operations",
"project_envelope_operations",
"project_exterior_lighting_operations",
"project_interior_lighting_operations",
"project_renewable_operations",
]
Loading
Loading