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
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,26 @@ 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
- Mechanical operations module (`project_mechanical_operations`) for managing HVAC systems, fan systems, HVAC plants, and water heaters
- `add_hvac_system_to_project` / `update_hvac_system_in_project` / `remove_hvac_system_from_project` — manage HVAC systems
- `assign_fan_system_to_hvac_system` — assign a fan system to an HVAC system by description (copies the UUID into `fanSystemKey`)
- `get_hvac_system_keys_from_project` — list all HVAC systems with their fan system links
- `add_fan_system_to_project` / `update_fan_system_in_project` / `remove_fan_system_from_project` — manage fan systems
- `get_fan_system_keys_from_project` — list all fan systems
- `add_fan_to_fan_system` / `update_fan_in_fan_system` / `remove_fan_from_fan_system` — manage fans within fan systems
- `add_pressure_drop_credit_to_fan_system` / `update_pressure_drop_credit_in_fan_system` / `remove_pressure_drop_credit_from_fan_system` — manage a fan system's pressure drop credits by `type`
- `add_hvac_plant_to_project` / `update_hvac_plant_in_project` / `remove_hvac_plant_from_project` — manage HVAC plants
- `get_hvac_plant_keys_from_project` — list all HVAC plants
- `add_water_heater_to_project` / `update_water_heater_in_project` / `remove_water_heater_from_project` — manage water heaters
- `get_water_heater_keys_from_project` — list all water heaters
- Automatic `fanSystemKey` UUID generation and validation for fan systems
- `HVACSystemListManager`, `FanSystemListManager`, `HVACPlantListManager`, `WaterHeaterListManager` in `comcheck_api.managers.components.mechanical`
- `HVACSystem`, `FanSystem`, `Fan`, `HVACPlant`, `ServiceWaterHeatingSystem` added to `MODEL_TO_ID_INFO` with auto-generated descriptions ("HVAC System", "Fan System", "Fan", "Plant", "Water Heater")
- `get_default_hvac_system_template`, `get_default_fan_system_template`, `get_default_fan_template`, `get_default_hvac_plant_template`, and `get_default_water_heater_template` default templates for mechanical systems
- Mechanical API calls on `COMcheckClient` (`POST /{energyCode}/mechanical/...`): `calculate_water_heater_efficiency`, `calculate_hvac_system_efficiency`, `validate_hvac_system_economizer`, `calculate_plant_efficiency`, `calculate_fan_pressure_drop_credit`, `check_fan_system_compliance`, `calculate_fan_maximum_allowable_hp`
- `COMcheckClient.update_fan_system_compliance` and `COMcheckClient.update_fan_pressure_drop_credits` — calculate fan system compliance / pressure drop credits and write them onto the project's fan system
- Matching `COMCheckApiService` service methods, and `WaterHeaterEfficiency`, `HVACSystemEfficiency`, `EconomizerValidation`, `PlantEfficiency`, `FanSystemComplianceResult` result types
- `examples/client/mechanical.py` and `examples/project_operations/mechanical_operations.py` examples
- 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
Expand All @@ -31,6 +51,9 @@ All notable changes to this project are documented in this file.
### 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
- `CustomBaseModel._get_normalized_subcomponent_info` now extracts element type from list annotations when `subcomponent_name` is given, fixing type lookup for `hvacSystem`, `fanSystem`, `hvacPlant`, and `swhSystem`
- `CustomBaseModel._get_subcomponent_list` now normalizes `None` to `[]` for optional list fields (e.g., `hvac.fanSystem`)
- Skill validation no longer blocks `hvac` or `swhSystem` — mechanical operations are now fully supported

### 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 @@ -61,6 +61,7 @@
project_envelope_operations,
project_exterior_lighting_operations,
project_interior_lighting_operations,
project_mechanical_operations,
project_renewable_operations,
)

Expand Down Expand Up @@ -94,6 +95,7 @@
"project_envelope_operations",
"project_exterior_lighting_operations",
"project_interior_lighting_operations",
"project_mechanical_operations",
"project_renewable_operations",
# Introspection
"list_operations",
Expand Down
81 changes: 68 additions & 13 deletions comcheck_api/ai/skill/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,19 @@ 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`, `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
at template defaults. All other mutable areas have dedicated modules.
building areas), `hvac`, `swhSystem` (water heaters),
`renewable`, `efficiencyPackages` (energy credits), and `control`
(energy code). No `Project`/`Control` PascalCase aliases exist.
The shared `fixtureSchedule[]` exists on the model but has **no
operation functions** — leave it at template defaults. All other
mutable areas have dedicated modules.
- **Operation modules (functional)**: all mutations go through free
functions that take a `ComBuilding` and return a new `ComBuilding`:
- `project_building_area_operations` — `WholeBldgUse` items
- `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_mechanical_operations` — HVAC systems, fan systems (with fans), plants, water heaters
- `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
Expand Down Expand Up @@ -170,10 +171,9 @@ 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[]` 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`,
- Don't add, update, or remove `fixtureSchedule[]` — no operations
exist for it 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`,
Expand All @@ -183,10 +183,10 @@ print(result["performanceRating"])
(`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 / credits / simulation instead.
offer building-area / envelope / lighting / mechanical / renewable / credits / simulation instead.
`comcheck_api.list_operations()` enumerates the `building_area`,
`envelope`, `interior_lighting`, `exterior_lighting`, `renewable`, and
`credits` groups.
`envelope`, `interior_lighting`, `exterior_lighting`, `mechanical`,
`renewable`, and `credits` groups.

## Common patterns

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

### Adding mechanical systems

Mechanical operations cover HVAC systems, fan systems, HVAC plants, and service
water heating. The `hvac` singleton can be `MISSING` and is created automatically
on add. Fan systems link to HVAC systems via a UUID (`fanSystemKey`) that is
set by the library — use `assign_fan_system_to_hvac_system` to create the link
by description.

```python
from comcheck_api import project_mechanical_operations as mech_ops
from comcheck_api.defaults import (
get_default_hvac_system_template,
get_default_fan_system_template,
get_default_fan_template,
get_default_hvac_plant_template,
get_default_water_heater_template,
)

# 1. Add an HVAC system
hvac_system = get_default_hvac_system_template()
hvac_system.description = "Rooftop Unit 1"
hvac_system.quantity = 2
project = mech_ops.add_hvac_system_to_project(project, hvac_system)

# 2. Add a fan system
fan_system = get_default_fan_system_template()
fan_system.description = "Supply Fan 1"
project = mech_ops.add_fan_system_to_project(project, fan_system)

# 3. Add a fan to the fan system
fan = get_default_fan_template()
fan.description = "Fan 1"
project = mech_ops.add_fan_to_fan_system(project, "Supply Fan 1", fan)

# 4. Assign the fan system to the HVAC system (by description, not UUID)
project = mech_ops.assign_fan_system_to_hvac_system(
project, "Rooftop Unit 1", "Supply Fan 1"
)

# 5. Add a plant
plant = get_default_hvac_plant_template()
plant.description = "Chiller 1"
project = mech_ops.add_hvac_plant_to_project(project, plant)

# 6. Add a water heater (swhSystem is top-level, not under hvac)
water_heater = get_default_water_heater_template()
water_heater.description = "Water Heater 1"
project = mech_ops.add_water_heater_to_project(project, water_heater)
```

**Fan system keys**: the library sets a new UUID on `fanSystemKey` when you add
a fan system if it's empty or already used. You cannot update `fanSystemKey`
directly — use `assign_fan_system_to_hvac_system` or `remove_fan_system_from_project`
(which clears `fanSystemKey` on referencing HVAC systems).

### Adding energy credits

Energy credits apply to IECC 2021+, ASHRAE 90.1-2022, and state codes
Expand Down
57 changes: 56 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 six modules:
Operation functions are free functions in seven 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_mechanical_operations`
- `comcheck_api.project_operations.project_renewable_operations`
- `comcheck_api.project_operations.project_credit_operations`

Expand Down Expand Up @@ -176,6 +177,60 @@ 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.

## Mechanical operations

```python
from comcheck_api import project_mechanical_operations as mech_ops
```

Mechanical operations manage HVAC systems, fan systems, HVAC plants, and service
water heating. The `hvac` singleton can be `MISSING` and is created automatically
on add. `swhSystem[]` is a top-level list (not under `hvac`).

| Function | Purpose |
|---|---|
| **HVAC systems** (`hvac.hvacSystem[]`) | |
| `add_hvac_system_to_project(project, new_hvac_system)` | Add an `HVACSystem`. Raises `ValueError` if `fanSystemKey` references a non-existent fan system. |
| `update_hvac_system_in_project(project, hvac_system_description, updates)` | Update an `HVACSystem` by its `description`. Raises `ValueError` for engine-calculated fields (`requirementAnswer`) or invalid `fanSystemKey`. |
| `remove_hvac_system_from_project(project, hvac_system_description)` | Remove an `HVACSystem` by its `description`. |
| `assign_fan_system_to_hvac_system(project, hvac_system_description, fan_system_description)` | Assign a fan system to an HVAC system by copying its UUID into `fanSystemKey`. Pass `fan_system_description=None` to unassign. |
| `get_hvac_system_keys_from_project(project)` | List `[{description, systemType, quantity, fanSystemKey, fanSystemDescription}, …]` for the project. |
| **Fan systems** (`hvac.fanSystem[]`) | |
| `add_fan_system_to_project(project, new_fan_system)` | Add a `FanSystem`. The library sets a new UUID on `fanSystemKey` if it's empty or already used. |
| `update_fan_system_in_project(project, fan_system_description, updates)` | Update a `FanSystem` by its `description`. Raises `ValueError` for engine-calculated fields (`complies`, `complianceMessage`, `requirementAnswer`) or `fanSystemKey` changes. |
| `remove_fan_system_from_project(project, fan_system_description)` | Remove a `FanSystem` by its `description`. Also clears `fanSystemKey` on HVAC systems that reference it. |
| `get_fan_system_keys_from_project(project)` | List `[{description, fanSystemKey, complyMethod}, …]` for the project. |
| **Fans** (nested in `FanSystem.fan[]`) | |
| `add_fan_to_fan_system(project, fan_system_description, new_fan)` | Add a `Fan` to a fan system. |
| `update_fan_in_fan_system(project, fan_system_description, fan_description, updates)` | Update a `Fan` by its `description`. |
| `remove_fan_from_fan_system(project, fan_system_description, fan_description)` | Remove a `Fan` by its `description`. |
| `add_pressure_drop_credit_to_fan_system(project, fan_system_description, new_credit)` | Add a `FanSystemPressureDropCredits`. `type` is unique per fan system; `PRESSURE_DROP_CREDIT_TYPE_UNSPECIFIED` raises `ValueError`. Warns unless the fan system's `complyMethod` is `FAN_SYSTEM_MOTOR_BRAKE_HP`. |
| `update_pressure_drop_credit_in_fan_system(project, fan_system_description, credit_type, updates)` | Update a credit's inputs by its `type`. Raises `ValueError` for `type` or `pressureDropCredit` changes; changing an input clears `pressureDropCredit` (recalculate with `client.update_fan_pressure_drop_credits`). |
| `remove_pressure_drop_credit_from_fan_system(project, fan_system_description, credit_type)` | Remove a credit by its `type`. |
| **HVAC plants** (`hvac.hvacPlant[]`) | |
| `add_hvac_plant_to_project(project, new_hvac_plant)` | Add an `HVACPlant`. |
| `update_hvac_plant_in_project(project, hvac_plant_description, updates)` | Update an `HVACPlant` by its `description`. Raises `ValueError` for engine-calculated fields (`requirementAnswer`). |
| `remove_hvac_plant_from_project(project, hvac_plant_description)` | Remove an `HVACPlant` by its `description`. |
| `get_hvac_plant_keys_from_project(project)` | List `[{description, plantType, systemType, quantity}, …]` for the project. |
| **Water heaters** (`swhSystem[]`, top-level) | |
| `add_water_heater_to_project(project, new_water_heater)` | Add a `ServiceWaterHeatingSystem`. |
| `update_water_heater_in_project(project, water_heater_description, updates)` | Update a `ServiceWaterHeatingSystem` by its `description`. Raises `ValueError` for engine-calculated fields (`requirementAnswer`). |
| `remove_water_heater_from_project(project, water_heater_description)` | Remove a `ServiceWaterHeatingSystem` by its `description`. |
| `get_water_heater_keys_from_project(project)` | List `[{description, swhSystemType, fuelType, quantity}, …]` for the project. |

Use `get_default_hvac_system_template()`, `get_default_fan_system_template()`,
`get_default_fan_template()`, `get_default_hvac_plant_template()`, and
`get_default_water_heater_template()` from `comcheck_api.defaults` as starting
points (or construct models directly from `comcheck_api.types.core_types`, where
all fields are optional). `description` is the identifier — it is unique within
each list and is auto-generated if missing.

**Fan system keys**: the library sets a new UUID on `fanSystemKey` when you add
a fan system if it's empty or already used. You cannot update `fanSystemKey`
directly — use `assign_fan_system_to_hvac_system` or `remove_fan_system_from_project`
(which clears `fanSystemKey` on referencing HVAC systems). Callers use descriptions
and never handle the UUID directly.

## Renewable energy operations

```python
Expand Down
2 changes: 1 addition & 1 deletion comcheck_api/ai/skill/reference/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ for any of these models. Leave `project.hvac` at its template default.
| `HVACSystem` | One HVAC system definition. |
| `HVACPlant` | Central plant equipment. |
| `Fan` / `FanSystem` | Fan and fan-system models. |
| `ServiceWaterHeatingSystem` (SWH) | Service water heating. |
| `ServiceWaterHeatingSystem` (SWH) | Water heater (stored in `swhSystem[]`). |

## Enums (StrEnum)

Expand Down
19 changes: 13 additions & 6 deletions comcheck_api/ai/skill/scripts/validate_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,13 @@ def _read_input(arg: str) -> str:
return Path(arg).read_text()


UNSUPPORTED_PROJECT_ATTRS = {"hvac"}
UNSUPPORTED_PROJECT_ATTRS: set[str] = set()
# `wholeBldgUse` (building areas + interior lighting), `activityUse` (interior
# lighting spaces), `exteriorUse` (exterior lighting), `exteriorLightingZoneType`,
# `renewable` (renewable energy systems and procurement), and `efficiencyPackages`
# (energy credits) all have operation modules. Only `hvac` and `fixtureSchedule`
# have no operations yet.
# `renewable` (renewable energy systems and procurement), `efficiencyPackages`
# (energy credits), `hvac` (HVAC systems, fan systems, plants), and `swhSystem`
# (water heaters) all have operation modules. Only `fixtureSchedule`
# has no operations yet.
UNSUPPORTED_LIGHTING_ATTRS = {
"fixtureSchedule",
}
Expand Down Expand Up @@ -132,6 +133,9 @@ def validate(code: str) -> dict:
"project_building_area_operations",
"project_interior_lighting_operations",
"project_exterior_lighting_operations",
"project_mechanical_operations",
"project_renewable_operations",
"project_credit_operations",
}:
op_module_aliases.add(alias.asname or alias.name)
elif isinstance(node, ast.ImportFrom) and node.module == (
Expand All @@ -143,12 +147,15 @@ def validate(code: str) -> dict:
"project_building_area_operations",
"project_interior_lighting_operations",
"project_exterior_lighting_operations",
"project_mechanical_operations",
"project_renewable_operations",
"project_credit_operations",
}:
op_module_aliases.add(alias.asname or alias.name)

for node in ast.walk(tree):
# Forbid `project.hvac` / `project.renewable` access in any context.
if isinstance(node, ast.Attribute) and any(
# Check for unsupported project attrs (currently none — hvac and swhSystem now have operations)
if UNSUPPORTED_PROJECT_ATTRS and isinstance(node, ast.Attribute) and any(
_is_attr_chain(node, "project", attr) for attr in UNSUPPORTED_PROJECT_ATTRS
):
errors.append(
Expand Down
Loading
Loading