From cf9be85ac5627ef83fccc9416f23b635c8d33b23 Mon Sep 17 00:00:00 2001 From: yanz571 Date: Tue, 14 Jul 2026 13:54:28 -0700 Subject: [PATCH 01/23] remove PR template from develop --- .github/pull_request_template.md | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md deleted file mode 100644 index 1e514af..0000000 --- a/.github/pull_request_template.md +++ /dev/null @@ -1,17 +0,0 @@ -# ⚠️ External Pull Requests Not Accepted - -Thank you for your interest in this project! - -However, **this project does not accept external contributions or pull requests** at this time. Development is managed internally by the PNNL team. - -## Alternative Ways to Help - -If you've found a bug or have a suggestion: - -1. **Report an Issue**: Open a bug report or feature request instead -2. **Share Feedback**: Your feedback helps us improve the library -3. **Use and Share**: Use the library and share your experiences - ---- - -We appreciate your understanding and interest in the COMcheck API Python client! From c1b8ed09d011a05886cd282a26666d47e5134f8e Mon Sep 17 00:00:00 2001 From: yanz571 Date: Tue, 14 Jul 2026 16:41:29 -0700 Subject: [PATCH 02/23] Add interior and exterior lighting --- comcheck_api/__init__.py | 4 + comcheck_api/ai/skill/SKILL.md | 84 +++++- .../constants/exterior_lighting_constants.py | 25 ++ .../constants/interior_lighting_constants.py | 41 +++ comcheck_api/defaults.py | 51 ++++ comcheck_api/managers/data_manager.py | 5 +- comcheck_api/project_operations/__init__.py | 9 +- .../project_exterior_lighting_operations.py | 199 +++++++++++++ .../project_interior_lighting_operations.py | 182 ++++++++++++ comcheck_api/utilities/project_utilities.py | 41 +++ docs_site/api/operations/exterior-lighting.md | 134 ++++++++- docs_site/api/operations/interior-lighting.md | 139 ++++++++- examples/README.md | 28 ++ .../exterior_lighting_operations.py | 110 +++++++ .../interior_lighting_operations.py | 109 +++++++ .../test_exterior_lighting_operations.py | 280 ++++++++++++++++++ .../test_interior_lighting_operations.py | 255 ++++++++++++++++ 17 files changed, 1670 insertions(+), 26 deletions(-) create mode 100644 comcheck_api/constants/exterior_lighting_constants.py create mode 100644 comcheck_api/constants/interior_lighting_constants.py create mode 100644 comcheck_api/project_operations/project_exterior_lighting_operations.py create mode 100644 comcheck_api/project_operations/project_interior_lighting_operations.py create mode 100644 examples/project_operations/exterior_lighting_operations.py create mode 100644 examples/project_operations/interior_lighting_operations.py create mode 100644 tests/project_operation_tests/test_exterior_lighting_operations.py create mode 100644 tests/project_operation_tests/test_interior_lighting_operations.py diff --git a/comcheck_api/__init__.py b/comcheck_api/__init__.py index ae96c5c..24cbd19 100644 --- a/comcheck_api/__init__.py +++ b/comcheck_api/__init__.py @@ -58,6 +58,8 @@ from .project_operations import ( project_building_area_operations, project_envelope_operations, + project_exterior_lighting_operations, + project_interior_lighting_operations, ) # Introspection helpers @@ -87,6 +89,8 @@ # Project Operations "project_building_area_operations", "project_envelope_operations", + "project_exterior_lighting_operations", + "project_interior_lighting_operations", # Introspection "list_operations", "lookup_type", diff --git a/comcheck_api/ai/skill/SKILL.md b/comcheck_api/ai/skill/SKILL.md index 4426ed2..2d5cb47 100644 --- a/comcheck_api/ai/skill/SKILL.md +++ b/comcheck_api/ai/skill/SKILL.md @@ -147,13 +147,12 @@ 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 interior lighting (the - `activityUse[]` fixtures), exterior lighting (`exteriorUse[]`, - `fixtureSchedule[]`), HVAC/mechanical, or renewable-energy - components — no operations exist for them. The whole-building - `interiorLightingSpace` singleton on each `WholeBldgUse` *is* - editable through `project_building_area_operations`; the per- - activity lighting nested under `activityUse[]` is not. The +- Don't add, update, or remove `fixtureSchedule[]`, HVAC/mechanical, or + renewable-energy components — no operations exist for them yet. + Interior lighting (`activityUse[]`) and exterior lighting + (`exteriorUse[]`) **are** supported via + `project_interior_lighting_operations` and + `project_exterior_lighting_operations` (see below). The `COMcheckClient` user methods (`list_projects`, `get_project`, `update_project`, `update_uvalues`, `start_run_simulation`, `get_simulation_status`, `get_simulation_result`, `set_api_key`) @@ -240,6 +239,77 @@ else: raise TimeoutError(f"Simulation {session_id} did not complete in 5 min") ``` +### Adding interior lighting (ActivityUse + fixtures) + +Interior lighting lives under `wholeBldgUse[i].activityUse[]`. Use +`project_interior_lighting_operations` — there are no fixture-level ops; +edit the `activityUse`'s `interiorLightingSpace.fixture[]` and pass the +whole `activityUse` through `update_interior_lighting_space_in_project`. + +```python +from comcheck_api import project_interior_lighting_operations as il_ops +from comcheck_api.defaults import get_default_interior_lighting_space_template, get_default_fixture_template +from comcheck_api.types.core_types import ActivityTypeOptions, LightingTypeOptions + +fixture = get_default_fixture_template() +fixture.description = "Recessed LED" +fixture.lightingType = LightingTypeOptions.LED +fixture.fixtureWattage = 20.0 +fixture.quantity = 10 + +activity_use = get_default_interior_lighting_space_template() +activity_use.areaDescription = "Open Office" +activity_use.activityType = ActivityTypeOptions.ACTIVITY_COMMON_OFFICE +activity_use.interiorLightingSpace = activity_use.interiorLightingSpace.model_copy( + deep=True, update={"fixture": [fixture]} +) +project = il_ops.add_interior_lighting_space_to_project(project, area_key, activity_use) + +# Update a field — fixtures are preserved unless you also pass interiorLightingSpace +project = il_ops.update_interior_lighting_space_in_project( + project, area_key, "Open Office", {"floorArea": 2500.0} +) + +# Remove +project = il_ops.remove_interior_lighting_space_from_project(project, area_key, "Open Office") +``` + +### Adding exterior lighting (ExteriorUse + zone type) + +Exterior lighting lives under `lighting.exteriorUse[]`. Set a real zone type +first, then add `ExteriorUse` items with fixtures inline. + +```python +from comcheck_api import project_exterior_lighting_operations as el_ops +from comcheck_api.defaults import get_default_exterior_lighting_area_template, get_default_fixture_template +from comcheck_api.types.core_types import ExteriorLightingZoneTypeOptions, ExteriorUseTypeOptions + +# Must set zone type before exterior compliance can be evaluated. +# EXT_ZONE_UNSPECIFIED raises ValueError; a raw string raises TypeError. +project = el_ops.set_exterior_lighting_zone_type_in_project( + project, ExteriorLightingZoneTypeOptions.EXT_ZONE_NEIGHBORHOOD_BUS_DISTRICT +) + +fixture = get_default_fixture_template() +fixture.description = "Parking LED" +fixture.fixtureWattage = 150.0 +fixture.quantity = 8 + +exterior_use = get_default_exterior_lighting_area_template() +exterior_use.areaDescription = "Main Parking Area" +exterior_use.exteriorType = ExteriorUseTypeOptions.EXTERIOR_PARKING_AREA +exterior_use.exteriorLightingSpace = exterior_use.exteriorLightingSpace.model_copy( + deep=True, update={"fixture": [fixture]} +) +project = el_ops.add_exterior_lighting_area_to_project(project, exterior_use) + +# Adding while zone is EXT_ZONE_UNSPECIFIED emits UserWarning (not an error) +project = el_ops.update_exterior_lighting_area_in_project( + project, "Main Parking Area", {"useQuantity": 6000.0} +) +project = el_ops.remove_exterior_lighting_area_from_project(project, "Main Parking Area") +``` + ### Checking compliance/requirements and generating a report These are synchronous (no polling). All three take a `ComBuilding` diff --git a/comcheck_api/constants/exterior_lighting_constants.py b/comcheck_api/constants/exterior_lighting_constants.py new file mode 100644 index 0000000..ed466ca --- /dev/null +++ b/comcheck_api/constants/exterior_lighting_constants.py @@ -0,0 +1,25 @@ +"""Exterior lighting constants for COMcheck projects.""" + +from comcheck_api.types.core_types import ( + ExteriorLightingSpace, + ExteriorUse, + ExteriorUseTypeOptions, +) + +DEFAULT_EXTERIOR_LIGHTING_AREA: ExteriorUse = ExteriorUse( + areaDescription="Ext Area 1", + exteriorType=ExteriorUseTypeOptions.EXTERIOR_PARKING_AREA, + isTradable=True, + powerDensity=0.0, + quantityUnits="sq ft", + useQuantity=1000.0, + exteriorLightingSpace=ExteriorLightingSpace( + description="", + numFixturesAlteredOrAdded=0, + postAltTotalWattage=0.0, + preAltNumberFixtures=0, + preAltTotalWattage=0.0, + altExemptType=None, + fixture=[], + ), +) diff --git a/comcheck_api/constants/interior_lighting_constants.py b/comcheck_api/constants/interior_lighting_constants.py new file mode 100644 index 0000000..6958338 --- /dev/null +++ b/comcheck_api/constants/interior_lighting_constants.py @@ -0,0 +1,41 @@ +"""Interior lighting constants for COMcheck projects.""" + +from comcheck_api.types.core_types import ( + ActivityUse, + ActivityTypeOptions, + Fixture, + InteriorLightingSpace, + LightingTypeOptions, +) + +# key is a placeholder — callers must set it to the parent WholeBldgUse.key +DEFAULT_INTERIOR_LIGHTING_SPACE_AREA: ActivityUse = ActivityUse( + key="__unset__", + areaDescription="Space 1", + activityType=ActivityTypeOptions.ACTIVITY_COMMON_OFFICE, + floorArea=1000.0, + ceilingHeight=9.0, + interiorLightingSpace=InteriorLightingSpace( + altExemptType=None, + description="", + numFixturesAlteredOrAdded=0, + postAltTotalWattage=0.0, + preAltNumberFixtures=0, + preAltTotalWattage=0.0, + allowanceType=None, + exemptionType=None, + allowanceFloorArea=0.0, + rcrFloorToWorkplaneHeight=0.0, + rcrPerimeter=0.0, + rcrWorkplaneToLuminaireHeight=0.0, + fixture=[], + ), +) + +DEFAULT_FIXTURE: Fixture = Fixture( + description="LED fixture", + lightingType=LightingTypeOptions.LED, + fixtureWattage=32.0, + quantity=1, + lightingControl=[], +) diff --git a/comcheck_api/defaults.py b/comcheck_api/defaults.py index 62b8610..b64d6e4 100644 --- a/comcheck_api/defaults.py +++ b/comcheck_api/defaults.py @@ -3,6 +3,13 @@ import copy from comcheck_api.constants.building_area_constants import DEFAULT_BUILDING_AREA +from comcheck_api.constants.interior_lighting_constants import ( + DEFAULT_INTERIOR_LIGHTING_SPACE_AREA, + DEFAULT_FIXTURE, +) +from comcheck_api.constants.exterior_lighting_constants import ( + DEFAULT_EXTERIOR_LIGHTING_AREA, +) from comcheck_api.constants.envelope_constants import ( DEFAULT_AG_WALL, DEFAULT_BG_WALL, @@ -139,6 +146,50 @@ def get_default_thermal_bridge_template(): return copy.deepcopy(DEFAULT_THERMAL_BRIDGE) +def get_default_interior_lighting_space_template(): + """Return a deep copy of the default interior lighting space template. + + In the COMcheck API schema, an interior lighting space is represented by + the :class:`~comcheck_api.types.core_types.ActivityUse` model. This + corresponds to what the COMcheck web app calls an *Interior Lighting Space*. + + The ``key`` field is set to ``"__unset__"`` — pass the template directly to + :func:`~comcheck_api.project_operations.project_interior_lighting_operations.add_interior_lighting_space_to_project`, + which sets the key to the parent building area automatically. + + Returns: + A new :class:`~comcheck_api.types.core_types.ActivityUse` instance. + """ + return copy.deepcopy(DEFAULT_INTERIOR_LIGHTING_SPACE_AREA) + + +def get_default_fixture_template(): + """Return a deep copy of the default :class:`~comcheck_api.types.core_types.Fixture` template. + + Defaults to an LED fixture at 32 W, quantity 1, with no lighting controls. + + Returns: + A new ``Fixture`` instance. + """ + return copy.deepcopy(DEFAULT_FIXTURE) + + +def get_default_exterior_lighting_area_template(): + """Return a deep copy of the default exterior lighting area template. + + In the COMcheck API schema, an exterior lighting area is represented by + the :class:`~comcheck_api.types.core_types.ExteriorUse` model. This + corresponds to what the COMcheck web app calls an *Exterior Lighting Area*. + + Defaults to a parking-area exterior lighting area with 1 000 sq ft quantity + and an empty :class:`~comcheck_api.types.core_types.ExteriorLightingSpace`. + + Returns: + A new :class:`~comcheck_api.types.core_types.ExteriorUse` instance. + """ + return copy.deepcopy(DEFAULT_EXTERIOR_LIGHTING_AREA) + + def get_default_fixture_schedule_template(): """Return a default fixture schedule template with a unique key. diff --git a/comcheck_api/managers/data_manager.py b/comcheck_api/managers/data_manager.py index a21fd71..a3465c4 100644 --- a/comcheck_api/managers/data_manager.py +++ b/comcheck_api/managers/data_manager.py @@ -182,7 +182,6 @@ def generate_identifier(self, item: T) -> None: or not isinstance(current, str) or current in {getattr(existing, self._identifier, None) for existing in self._data} - or (not current.startswith(self._id_prefix)) ) if not needs_new_identifier: @@ -344,7 +343,9 @@ def get_model_info(model_class: Type[BaseModel]) -> IdInfo | None: or ``None`` if the class is not registered. """ from comcheck_api.types.core_types import ( + ActivityUse, Door, + ExteriorUse, Roof, Window, BgWall, @@ -356,6 +357,8 @@ def get_model_info(model_class: Type[BaseModel]) -> IdInfo | None: ) MODEL_TO_ID_INFO = { + ActivityUse: IdInfo(identifier="areaDescription", id_prefix="Space"), + ExteriorUse: IdInfo(identifier="areaDescription", id_prefix="Ext Area"), Door: IdInfo(identifier="assemblyType", id_prefix="Door:Door"), Roof: IdInfo(identifier="assemblyType", id_prefix="Roof:Roof"), Window: IdInfo(identifier="assemblyType", id_prefix="Window:Window"), diff --git a/comcheck_api/project_operations/__init__.py b/comcheck_api/project_operations/__init__.py index c296a41..9de6550 100644 --- a/comcheck_api/project_operations/__init__.py +++ b/comcheck_api/project_operations/__init__.py @@ -1,8 +1,15 @@ """Project operations module.""" -from . import project_building_area_operations, project_envelope_operations +from . import ( + project_building_area_operations, + project_envelope_operations, + project_exterior_lighting_operations, + project_interior_lighting_operations, +) __all__ = [ "project_building_area_operations", "project_envelope_operations", + "project_exterior_lighting_operations", + "project_interior_lighting_operations", ] diff --git a/comcheck_api/project_operations/project_exterior_lighting_operations.py b/comcheck_api/project_operations/project_exterior_lighting_operations.py new file mode 100644 index 0000000..e2911ee --- /dev/null +++ b/comcheck_api/project_operations/project_exterior_lighting_operations.py @@ -0,0 +1,199 @@ +"""Project Exterior Lighting Operations. + +Manages exterior lighting at the ExteriorUse granularity. Each ExteriorUse +carries exactly one (singleton) ExteriorLightingSpace whose fixture[] holds +the fixtures. There are no fixture-level operations — to add, change, or +remove a fixture, edit the ExteriorUse's exteriorLightingSpace.fixture[] list +and pass the whole ExteriorUse through update_exterior_lighting_area_in_project. + +Zone type +--------- +exterior compliance requires a real zone type on lighting.exteriorLightingZoneType +(anything other than EXT_ZONE_UNSPECIFIED). Use +set_exterior_lighting_zone_type_in_project to set it. Adding an ExteriorUse +while the zone is still EXT_ZONE_UNSPECIFIED emits a warning — it is not a +hard error so the project can be built up incrementally. +""" + +import logging +import warnings +from typing import Any + +from comcheck_api.types.core_types import ( + ComBuilding, + ExteriorLightingSpace, + ExteriorLightingZoneTypeOptions, + ExteriorUse, +) +from comcheck_api.utilities.project_utilities import _require_exterior_use + +logger = logging.getLogger(__name__) + + +def set_exterior_lighting_zone_type_in_project( + project: ComBuilding, + zone_type: ExteriorLightingZoneTypeOptions, +) -> ComBuilding: + """Set the project-level exterior lighting zone type. + + Args: + project: The project to modify. + zone_type: An :class:`~comcheck_api.types.core_types.ExteriorLightingZoneTypeOptions` + value. Must not be ``EXT_ZONE_UNSPECIFIED`` — exterior compliance + cannot be evaluated without a real zone type. + + Returns: + Updated project with the zone type set. + + Raises: + TypeError: If zone_type is not an ExteriorLightingZoneTypeOptions member. + ValueError: If zone_type is EXT_ZONE_UNSPECIFIED. + """ + if not isinstance(zone_type, ExteriorLightingZoneTypeOptions): + raise TypeError( + f"zone_type must be an ExteriorLightingZoneTypeOptions member, " + f"got {type(zone_type).__name__!r}." + ) + if zone_type == ExteriorLightingZoneTypeOptions.EXT_ZONE_UNSPECIFIED: + raise ValueError( + "EXT_ZONE_UNSPECIFIED is not a valid zone type — exterior compliance " + "cannot be evaluated without a real zone type. " + "Choose a value from ExteriorLightingZoneTypeOptions other than " + "EXT_ZONE_UNSPECIFIED." + ) + + updated_project = project.model_copy(deep=True) + updated_project.lighting.exteriorLightingZoneType = zone_type + return updated_project + + +def add_exterior_lighting_area_to_project( + project: ComBuilding, + new_exterior_lighting_area: ExteriorUse, +) -> ComBuilding: + """Add a new ExteriorUse (exterior lighting space) to the project. + + Fixtures and the singleton ExteriorLightingSpace are carried inside + new_exterior_lighting_area — populate exteriorLightingSpace.fixture[] before + passing if you want fixtures on creation. + + Emits a :class:`UserWarning` if the project's + ``lighting.exteriorLightingZoneType`` is still ``EXT_ZONE_UNSPECIFIED``, + because exterior compliance cannot be evaluated until a real zone type is + set. Call :func:`set_exterior_lighting_zone_type_in_project` to fix it. + + Args: + project: The project to modify. + new_exterior_lighting_area: The ExteriorUse to add. + + Returns: + Updated project with the new ExteriorUse added. + """ + zone = project.lighting.exteriorLightingZoneType + if zone == ExteriorLightingZoneTypeOptions.EXT_ZONE_UNSPECIFIED: + warnings.warn( + "The project's exterior lighting zone type is EXT_ZONE_UNSPECIFIED. " + "Exterior compliance cannot be evaluated until a real zone type is set. " + "Call set_exterior_lighting_zone_type_in_project() to fix this.", + UserWarning, + stacklevel=2, + ) + + updated_project = project.model_copy(deep=True) + + new_exterior_lighting_area = new_exterior_lighting_area.model_copy(deep=True) + + # Ensure exteriorLightingSpace is initialised + if new_exterior_lighting_area.exteriorLightingSpace is None: + new_exterior_lighting_area = new_exterior_lighting_area.model_copy( + deep=True, + update={ + "exteriorLightingSpace": ExteriorLightingSpace( + description="", + numFixturesAlteredOrAdded=0, + postAltTotalWattage=0.0, + preAltNumberFixtures=0, + preAltTotalWattage=0.0, + altExemptType=None, + fixture=[], + ) + }, + ) + + updated_project.lighting.append_subcomponent(new_exterior_lighting_area) + return updated_project + + +def update_exterior_lighting_area_in_project( + project: ComBuilding, + area_description: str, + updates: dict[str, Any] | ExteriorUse, +) -> ComBuilding: + """Update an existing ExteriorUse. + + To add, change, or remove fixtures: set the desired + exteriorLightingSpace.fixture[] on the updates dict (or the full + ExteriorUse object) before calling this function. + + Args: + project: The project to modify. + area_description: The areaDescription of the ExteriorUse to update. + updates: Partial updates (dict) or full ExteriorUse to apply. + + Returns: + Updated project with the ExteriorUse modified. + """ + _require_exterior_use(project, area_description) + + updated_project = project.model_copy(deep=True) + updated_project.lighting.update_subcomponent_list( + subcomponent_updates=updates, + subcomponent_id=area_description, + subcomponent_name="exteriorUse", + ) + return updated_project + + +def remove_exterior_lighting_area_from_project( + project: ComBuilding, + area_description: str, +) -> ComBuilding: + """Remove an ExteriorUse (and its lighting space + fixtures) from the project. + + Args: + project: The project to modify. + area_description: The areaDescription of the ExteriorUse to remove. + + Returns: + Updated project with the ExteriorUse removed. + """ + _require_exterior_use(project, area_description) + + updated_project = project.model_copy(deep=True) + updated_project.lighting.remove_from_subcomponent_list( + subcomponent_id=area_description, + subcomponent_name="exteriorUse", + ) + return updated_project + + +def get_exterior_lighting_area_keys_from_project(project: ComBuilding) -> list[dict]: + """Return the areaDescription and exteriorType of all ExteriorUse items. + + Args: + project: The project to query. + + Returns: + List of dicts with keys ``areaDescription`` and ``exteriorType`` + for each ExteriorUse in the project. + """ + exterior_uses = project.get_by_path("lighting.exteriorUse") + if not isinstance(exterior_uses, list): + return [] + return [ + { + "areaDescription": getattr(eu, "areaDescription", None), + "exteriorType": getattr(eu, "exteriorType", None), + } + for eu in exterior_uses + ] diff --git a/comcheck_api/project_operations/project_interior_lighting_operations.py b/comcheck_api/project_operations/project_interior_lighting_operations.py new file mode 100644 index 0000000..058d211 --- /dev/null +++ b/comcheck_api/project_operations/project_interior_lighting_operations.py @@ -0,0 +1,182 @@ +"""Project Interior Lighting Operations. + +Manages interior lighting at the ActivityUse granularity. In the COMcheck +API schema, an interior lighting space is represented by the ``ActivityUse`` +model (``lighting.wholeBldgUse[i].activityUse[]``). Each ActivityUse carries +exactly one (singleton) InteriorLightingSpace whose fixture[] holds the +fixtures. There are no fixture-level operations — to add, change, or remove a +fixture, edit the ActivityUse's interiorLightingSpace.fixture[] list and pass +the whole ActivityUse through update_interior_lighting_space_in_project. +""" + +from typing import Any + +from comcheck_api.constants.interior_lighting_constants import ( + DEFAULT_INTERIOR_LIGHTING_SPACE_AREA, +) +from comcheck_api.types.core_types import ActivityUse, ComBuilding +from comcheck_api.utilities.project_utilities import _require_activity_use + + +def _find_building_area(project: ComBuilding, building_area_key: str): + """Return the WholeBldgUse with the given key, or raise.""" + whole_use = project.get_by_path("lighting.wholeBldgUse") or [] + area = next( + (a for a in whole_use if getattr(a, "key", None) == building_area_key), None + ) + if area is None: + raise ValueError( + f"Building area key '{building_area_key}' not found in lighting.wholeBldgUse." + ) + return area + + +def add_interior_lighting_space_to_project( + project: ComBuilding, + building_area_key: str, + new_activity_use: ActivityUse, +) -> ComBuilding: + """Add a new interior lighting space (``ActivityUse``) to a building area. + + Each interior lighting space is stored as an ``ActivityUse`` object in the + COMcheck API schema (``lighting.wholeBldgUse[i].activityUse[]``). + + Fixtures and the singleton InteriorLightingSpace are carried inside + new_activity_use — populate interiorLightingSpace.fixture[] before + passing if you want fixtures on creation. The activityUse.key is + automatically set to building_area_key. + + Args: + project: The project to modify. + building_area_key: Key of the WholeBldgUse to add the ActivityUse to. + new_activity_use: The :class:`~comcheck_api.types.core_types.ActivityUse` + to add (represents one interior lighting space in the web app). + Use :func:`~comcheck_api.defaults.get_default_interior_lighting_space_template` + as a starting point. + + Returns: + Updated project with the new ActivityUse added. + """ + updated_project = project.model_copy(deep=True) + + area = _find_building_area(updated_project, building_area_key) + + # Ensure the activityUse.key matches its parent building area key + new_activity_use = new_activity_use.model_copy( + deep=True, update={"key": building_area_key} + ) + + # Ensure interiorLightingSpace is initialized + if new_activity_use.interiorLightingSpace is None: + new_activity_use = new_activity_use.model_copy( + deep=True, + update={ + "interiorLightingSpace": DEFAULT_INTERIOR_LIGHTING_SPACE_AREA.interiorLightingSpace.model_copy( + deep=True + ) + }, + ) + + area.append_subcomponent(new_activity_use) + + return updated_project + + +def update_interior_lighting_space_in_project( + project: ComBuilding, + building_area_key: str, + area_description: str, + updates: dict[str, Any] | ActivityUse, +) -> ComBuilding: + """Update an existing interior lighting space (``ActivityUse``) in a building area. + + To add, change, or remove fixtures: set the desired + interiorLightingSpace.fixture[] on the updates dict (or the full + ActivityUse object) before calling this function. + + Args: + project: The project to modify. + building_area_key: Key of the WholeBldgUse that owns this ActivityUse. + area_description: The ``areaDescription`` of the + :class:`~comcheck_api.types.core_types.ActivityUse` to update. + updates: Partial updates (dict) or a full + :class:`~comcheck_api.types.core_types.ActivityUse` to apply. + + Returns: + Updated project with the ActivityUse modified. + """ + _require_activity_use(project, building_area_key, area_description) + + updated_project = project.model_copy(deep=True) + area = _find_building_area(updated_project, building_area_key) + + area.update_subcomponent_list( + subcomponent_updates=updates, + subcomponent_id=area_description, + subcomponent_name="activityUse", + ) + + return updated_project + + +def remove_interior_lighting_space_from_project( + project: ComBuilding, + building_area_key: str, + area_description: str, +) -> ComBuilding: + """Remove an interior lighting space (``ActivityUse``) and its fixtures from a building area. + + Args: + project: The project to modify. + building_area_key: Key of the WholeBldgUse that owns this ActivityUse. + area_description: The ``areaDescription`` of the + :class:`~comcheck_api.types.core_types.ActivityUse` to remove. + + Returns: + Updated project with the ActivityUse removed. + """ + _require_activity_use(project, building_area_key, area_description) + + updated_project = project.model_copy(deep=True) + area = _find_building_area(updated_project, building_area_key) + + area.remove_from_subcomponent_list( + subcomponent_id=area_description, + subcomponent_name="activityUse", + ) + + return updated_project + + +def get_interior_lighting_space_keys_from_project( + project: ComBuilding, building_area_key: str +) -> list[dict]: + """Return identifying fields for all interior lighting spaces (``ActivityUse`` items) in a building area. + + Args: + project: The project to query. + building_area_key: Key of the WholeBldgUse to query. + + Returns: + List of dicts with keys ``areaDescription`` and ``activityType`` + for each :class:`~comcheck_api.types.core_types.ActivityUse` in the + building area. + """ + whole_use = project.get_by_path("lighting.wholeBldgUse") + if not isinstance(whole_use, list): + return [] + + area = next( + (a for a in whole_use if getattr(a, "key", None) == building_area_key), None + ) + if area is None: + return [] + + activity_uses = getattr(area, "activityUse", []) or [] + return [ + { + "areaDescription": getattr(au, "areaDescription", None), + "activityType": getattr(au, "activityType", None), + } + for au in activity_uses + ] diff --git a/comcheck_api/utilities/project_utilities.py b/comcheck_api/utilities/project_utilities.py index d6a4b9a..2a68793 100644 --- a/comcheck_api/utilities/project_utilities.py +++ b/comcheck_api/utilities/project_utilities.py @@ -21,6 +21,47 @@ def _require_building_area(project: ComBuilding, building_area_key: str) -> None ) +def _require_activity_use( + project: ComBuilding, building_area_key: str, area_description: str +) -> None: + """Ensure the given building area contains an activityUse with the given areaDescription.""" + whole_use = project.get_by_path("lighting.wholeBldgUse") + if not isinstance(whole_use, list): + raise ValueError("No building areas (wholeBldgUse) found in project.") + + area = next( + (a for a in whole_use if getattr(a, "key", None) == building_area_key), None + ) + if area is None: + raise ValueError( + f"Building area key '{building_area_key}' not found in lighting.wholeBldgUse." + ) + + activity_uses = getattr(area, "activityUse", []) or [] + if not any( + getattr(au, "areaDescription", None) == area_description for au in activity_uses + ): + raise ValueError( + f"ActivityUse with areaDescription '{area_description}' " + f"not found in building area '{building_area_key}'." + ) + + +def _require_exterior_use(project: ComBuilding, area_description: str) -> None: + """Ensure project.lighting.exteriorUse contains an ExteriorUse with the given areaDescription.""" + exterior_uses = project.get_by_path("lighting.exteriorUse") + if not isinstance(exterior_uses, list): + raise ValueError("No exterior uses (lighting.exteriorUse) found in project.") + + if not any( + getattr(eu, "areaDescription", None) == area_description for eu in exterior_uses + ): + raise ValueError( + f"ExteriorUse with areaDescription '{area_description}' " + f"not found in lighting.exteriorUse." + ) + + def find_component_in_component_list( components: List[CustomBaseModel], component_id: str ): diff --git a/docs_site/api/operations/exterior-lighting.md b/docs_site/api/operations/exterior-lighting.md index 3989cd9..7427c18 100644 --- a/docs_site/api/operations/exterior-lighting.md +++ b/docs_site/api/operations/exterior-lighting.md @@ -1,12 +1,130 @@ -# Exterior Lighting +# Exterior Lighting Operations -!!! warning "Under Development" - Exterior lighting operations are not yet implemented. This section is planned for a future release. +Exterior lighting is managed at the **`ExteriorUse`** granularity. Each +`ExteriorUse` lives directly under `lighting.exteriorUse[]` (no parent +building area needed) and carries exactly one singleton +`ExteriorLightingSpace` whose `fixture[]` holds the fixtures. -This page will cover operations for managing exterior lighting in COMcheck projects, including: +## Zone type -- Adding and updating exterior lighting use areas -- Managing exterior fixture schedules -- Exterior lighting power allowance calculations +Exterior compliance requires a real zone type on +`lighting.exteriorLightingZoneType`. Set it with +`set_exterior_lighting_zone_type_in_project` **before** exterior compliance +can be evaluated. Adding an `ExteriorUse` while the zone is still +`EXT_ZONE_UNSPECIFIED` emits a `UserWarning` (not an error) so you can build +up a project incrementally. -The underlying types for exterior lighting (`ExteriorUse`, `ExteriorLightingSpace`, `Fixture`, `FixtureSchedule`) are already available. See the [Types Guide](types-guide.md) for details on importing and using them. +## Operations + +```python +from comcheck_api import project_exterior_lighting_operations as el_ops +``` + +| Function | Description | +|---|---| +| `set_exterior_lighting_zone_type_in_project(project, zone_type)` | Set the project-level exterior lighting zone type (rejects `EXT_ZONE_UNSPECIFIED`) | +| `add_exterior_lighting_area_to_project(project, new_exterior_lighting_area)` | Add a new ExteriorUse | +| `update_exterior_lighting_area_in_project(project, area_description, updates)` | Update an existing ExteriorUse (including its fixtures) | +| `remove_exterior_lighting_area_from_project(project, area_description)` | Remove an ExteriorUse and its fixtures | +| `get_exterior_lighting_area_keys_from_project(project)` | List all exterior uses in the project | + +## Setting the zone type + +```python +from comcheck_api import project_exterior_lighting_operations as el_ops +from comcheck_api.types.core_types import ExteriorLightingZoneTypeOptions + +project = el_ops.set_exterior_lighting_zone_type_in_project( + project, ExteriorLightingZoneTypeOptions.EXT_ZONE_NEIGHBORHOOD_BUS_DISTRICT +) +``` + +`EXT_ZONE_UNSPECIFIED` is rejected with a `ValueError`. A raw string raises +a `TypeError` — always use the enum. + +## Adding an ExteriorUse with fixtures + +```python +from comcheck_api.defaults import get_default_exterior_lighting_area_template, get_default_fixture_template +from comcheck_api.types.core_types import ExteriorUseTypeOptions, LightingTypeOptions + +fixture = get_default_fixture_template() +fixture.description = "Parking LED" +fixture.lightingType = LightingTypeOptions.LED +fixture.fixtureWattage = 150.0 +fixture.quantity = 8 + +exterior_use = get_default_exterior_lighting_area_template() +exterior_use.areaDescription = "Main Parking Area" +exterior_use.exteriorType = ExteriorUseTypeOptions.EXTERIOR_PARKING_AREA +exterior_use.useQuantity = 5000.0 +exterior_use.exteriorLightingSpace = exterior_use.exteriorLightingSpace.model_copy( + deep=True, update={"fixture": [fixture]} +) + +project = el_ops.add_exterior_lighting_area_to_project(project, exterior_use) +``` + +## Updating an ExteriorUse + +Pass only the fields you want to change — unchanged fields (including +fixtures) are preserved: + +```python +project = el_ops.update_exterior_lighting_area_in_project( + project, + "Main Parking Area", + {"useQuantity": 6000.0}, +) +``` + +## Adding a fixture to an existing ExteriorUse + +Retrieve the current `exteriorLightingSpace`, append to its `fixture[]`, then +pass it back through `update_exterior_lighting_area_in_project`: + +```python +exterior_uses = project.get_by_path("lighting.exteriorUse") +eu = next(e for e in exterior_uses if e.areaDescription == "Main Parking Area") +existing_fixtures = list(eu.exteriorLightingSpace.fixture or []) + +new_fixture = get_default_fixture_template() +new_fixture.description = "Entrance LED" +new_fixture.fixtureWattage = 80.0 + +updated_space = eu.exteriorLightingSpace.model_copy( + deep=True, + update={"fixture": existing_fixtures + [new_fixture]}, +) +project = el_ops.update_exterior_lighting_area_in_project( + project, + "Main Parking Area", + {"exteriorLightingSpace": updated_space.model_dump(mode="python", exclude_unset=True)}, +) +``` + +## Removing a fixture + +Pass `exteriorLightingSpace` with the desired `fixture[]` (omit the fixtures +you want to remove): + +```python +project = el_ops.update_exterior_lighting_area_in_project( + project, + "Main Parking Area", + {"exteriorLightingSpace": {"fixture": []}}, # removes all fixtures +) +``` + +## Removing an ExteriorUse + +```python +project = el_ops.remove_exterior_lighting_area_from_project(project, "Main Parking Area") +``` + +## Listing exterior uses + +```python +keys = el_ops.get_exterior_lighting_area_keys_from_project(project) +# [{"areaDescription": "Main Parking Area", "exteriorType": "EXTERIOR_PARKING_AREA"}, ...] +``` diff --git a/docs_site/api/operations/interior-lighting.md b/docs_site/api/operations/interior-lighting.md index 69301e5..78ed611 100644 --- a/docs_site/api/operations/interior-lighting.md +++ b/docs_site/api/operations/interior-lighting.md @@ -1,13 +1,134 @@ -# Interior Lighting +# Interior Lighting Operations -!!! warning "Under Development" - Interior lighting operations are not yet implemented. This section is planned for a future release. +Interior lighting is managed at the **`ActivityUse`** granularity. Each +`ActivityUse` belongs to a `WholeBldgUse` (building area) and carries exactly +one singleton `InteriorLightingSpace` whose `fixture[]` holds the fixtures. -This page will cover operations for managing interior lighting in COMcheck projects, including: +## Key concepts -- Adding and updating whole building use areas with lighting data -- Managing activity use areas and fixture schedules -- Lighting power allowance calculations -- Lighting controls +- **No fixture-level operations.** To add, change, or remove a fixture, edit + the `activityUse`'s `interiorLightingSpace.fixture[]` list and pass the whole + `ActivityUse` through `update_interior_lighting_space_in_project`. +- **ActivityUse.key** is always set to the parent `WholeBldgUse.key` — the + add operation sets this automatically. +- A building area must exist before adding activity uses — add one with + `project_building_area_operations.add_building_area_to_project` first. -The underlying types for interior lighting (`WholeBldgUse`, `ActivityUse`, `Fixture`, `FixtureSchedule`) are already available. See the [Types Guide](types-guide.md) for details on importing and using them. +## Operations + +```python +from comcheck_api import project_interior_lighting_operations as il_ops +``` + +| Function | Description | +|---|---| +| `add_interior_lighting_space_to_project(project, building_area_key, new_interior_lighting_space)` | Add a new ActivityUse to a building area | +| `update_interior_lighting_space_in_project(project, building_area_key, area_description, updates)` | Update an existing ActivityUse (including its fixtures) | +| `remove_interior_lighting_space_from_project(project, building_area_key, area_description)` | Remove an ActivityUse and its fixtures | +| `get_interior_lighting_space_keys_from_project(project, building_area_key)` | List all activity uses in a building area | + +## Adding an ActivityUse with fixtures + +```python +from comcheck_api import ( + project_building_area_operations as ba_ops, + project_interior_lighting_operations as il_ops, +) +from comcheck_api.defaults import ( + get_default_building_area_template, + get_default_interior_lighting_space_template, + get_default_fixture_template, +) +from comcheck_api.types.core_types import ActivityTypeOptions, LightingTypeOptions + +# A building area must exist first +area = get_default_building_area_template() +project = ba_ops.add_building_area_to_project(project, area) +area_key = area.key + +# Build the fixture +fixture = get_default_fixture_template() +fixture.description = "Recessed LED" +fixture.lightingType = LightingTypeOptions.LED +fixture.fixtureWattage = 20.0 +fixture.quantity = 10 + +# Attach the fixture to the activity use before adding +activity_use = get_default_interior_lighting_space_template() +activity_use.areaDescription = "Open Office" +activity_use.activityType = ActivityTypeOptions.ACTIVITY_COMMON_OFFICE +activity_use.floorArea = 2000.0 +activity_use.interiorLightingSpace = activity_use.interiorLightingSpace.model_copy( + deep=True, update={"fixture": [fixture]} +) + +project = il_ops.add_interior_lighting_space_to_project(project, area_key, activity_use) +``` + +## Updating an ActivityUse + +Pass only the fields you want to change — unchanged fields (including fixtures) +are preserved: + +```python +project = il_ops.update_interior_lighting_space_in_project( + project, + area_key, + "Open Office", + {"floorArea": 2500.0}, +) +``` + +## Adding a fixture to an existing ActivityUse + +Retrieve the current `interiorLightingSpace`, append to its `fixture[]`, then +pass it back through `update_interior_lighting_space_in_project`: + +```python +whole_use = project.get_by_path("lighting.wholeBldgUse") +ba = next(a for a in whole_use if a.key == area_key) +au = next(au for au in ba.activityUse if au.areaDescription == "Open Office") +existing_fixtures = list(au.interiorLightingSpace.fixture or []) + +new_fixture = get_default_fixture_template() +new_fixture.description = "Pendant LED" +new_fixture.fixtureWattage = 35.0 + +updated_space = au.interiorLightingSpace.model_copy( + deep=True, + update={"fixture": existing_fixtures + [new_fixture]}, +) +project = il_ops.update_interior_lighting_space_in_project( + project, + area_key, + "Open Office", + {"interiorLightingSpace": updated_space.model_dump(mode="python", exclude_unset=True)}, +) +``` + +## Removing a fixture + +Pass `interiorLightingSpace` with the desired `fixture[]` (simply omit the +fixture you want to remove): + +```python +project = il_ops.update_interior_lighting_space_in_project( + project, + area_key, + "Open Office", + {"interiorLightingSpace": {"fixture": []}}, # removes all fixtures +) +``` + +## Removing an ActivityUse + +```python +project = il_ops.remove_interior_lighting_space_from_project(project, area_key, "Open Office") +``` + +## Listing activity uses + +```python +keys = il_ops.get_interior_lighting_space_keys_from_project(project, area_key) +# [{"areaDescription": "Open Office", "activityType": "ACTIVITY_COMMON_OFFICE"}, ...] +``` diff --git a/examples/README.md b/examples/README.md index 36d6221..c780135 100644 --- a/examples/README.md +++ b/examples/README.md @@ -82,6 +82,34 @@ python examples/project_operations/building_area_operations.py - `testProjectJson/buildingAreaAddedProject.json` - `testProjectJson/buildingAreaUpdatedProject.json` +#### Interior Lighting Operations (`project_operations/interior_lighting_operations.py`) +Demonstrates adding and managing interior lighting (ActivityUse) in a project. + +**What it demonstrates:** +- `add_interior_lighting_space_to_project()` - Adding an ActivityUse with fixtures pre-populated +- `update_interior_lighting_space_in_project()` - Updating fields and adding/removing fixtures +- `remove_interior_lighting_space_from_project()` - Removing an ActivityUse and its fixtures +- `get_interior_lighting_space_keys_from_project()` - Listing activity uses in a building area + +**Usage:** +```bash +python examples/project_operations/interior_lighting_operations.py +``` + +#### Exterior Lighting Operations (`project_operations/exterior_lighting_operations.py`) +Demonstrates setting the exterior lighting zone type and managing ExteriorUse items with fixtures. + +**What it demonstrates:** +- `set_exterior_lighting_zone_type_in_project()` - Setting the project-level zone type +- `add_exterior_lighting_area_to_project()` - Adding an ExteriorUse with fixtures pre-populated +- `update_exterior_lighting_area_in_project()` - Updating fields and adding/removing fixtures +- `remove_exterior_lighting_area_from_project()` - Removing an ExteriorUse and its fixtures + +**Usage:** +```bash +python examples/project_operations/exterior_lighting_operations.py +``` + #### Envelope Operations (`project_operations/envelope_operations.py`) Comprehensive examples for all envelope operations including assemblies and nested components. This script demonstrates the complete workflow of adding and updating envelope components in a COMcheck project. diff --git a/examples/project_operations/exterior_lighting_operations.py b/examples/project_operations/exterior_lighting_operations.py new file mode 100644 index 0000000..7875652 --- /dev/null +++ b/examples/project_operations/exterior_lighting_operations.py @@ -0,0 +1,110 @@ +"""Example: exterior lighting operations. + +Exterior lighting is managed at the ExteriorUse granularity. Each ExteriorUse +lives directly under lighting.exteriorUse[] (no parent building area needed) +and carries exactly one ExteriorLightingSpace whose fixture[] holds the +fixtures. + +Zone type +--------- +Before exterior compliance can be evaluated, set a real exterior lighting zone +type on the project. Adding an ExteriorUse while the zone is still +EXT_ZONE_UNSPECIFIED emits a warning — call +set_exterior_lighting_zone_type_in_project to fix it. +""" + +import os +from dotenv import load_dotenv + +from comcheck_api import ( + COMcheckClient, + project_exterior_lighting_operations as el_ops, +) +from comcheck_api.defaults import ( + get_default_exterior_lighting_area_template, + get_default_fixture_template, + get_default_project_template, +) +from comcheck_api.types.core_types import ( + ExteriorLightingZoneTypeOptions, + ExteriorUseTypeOptions, + LightingTypeOptions, +) + +load_dotenv() +client = COMcheckClient() +client.set_api_key(os.getenv("COM_API_KEY") or "your-api-key-here") + +project = get_default_project_template() + +# ── Step 1: Set the exterior lighting zone type ─────────────────────────────── +# Must be set to a real zone before exterior compliance can be evaluated. +project = el_ops.set_exterior_lighting_zone_type_in_project( + project, ExteriorLightingZoneTypeOptions.EXT_ZONE_NEIGHBORHOOD_BUS_DISTRICT +) +print(f"Zone type set: {project.lighting.exteriorLightingZoneType}") + +# ── Step 2: Add an ExteriorUse with a fixture already populated ─────────────── +fixture = get_default_fixture_template() +fixture.description = "Parking LED" +fixture.lightingType = LightingTypeOptions.LED +fixture.fixtureWattage = 150.0 +fixture.quantity = 8 + +exterior_use = get_default_exterior_lighting_area_template() +exterior_use.areaDescription = "Main Parking Area" +exterior_use.exteriorType = ExteriorUseTypeOptions.EXTERIOR_PARKING_AREA +exterior_use.useQuantity = 5000.0 +exterior_use.quantityUnits = "sq ft" +exterior_use.exteriorLightingSpace = exterior_use.exteriorLightingSpace.model_copy( + deep=True, update={"fixture": [fixture]} +) + +project = el_ops.add_exterior_lighting_area_to_project(project, exterior_use) +print(f"ExteriorUse added: {exterior_use.areaDescription!r}") + +# ── Step 3: List all exterior uses ──────────────────────────────────────────── +keys = el_ops.get_exterior_lighting_area_keys_from_project(project) +print(f"Exterior uses: {keys}") + +# ── Step 4: Update the ExteriorUse (change quantity) ───────────────────────── +project = el_ops.update_exterior_lighting_area_in_project( + project, + "Main Parking Area", + {"useQuantity": 6000.0}, +) +print("ExteriorUse updated: useQuantity → 6000.0") + +# ── Step 5: Add a second fixture by updating the lighting space ─────────────── +exterior_uses = project.get_by_path("lighting.exteriorUse") +eu = next(e for e in exterior_uses if e.areaDescription == "Main Parking Area") +existing_fixtures = list(eu.exteriorLightingSpace.fixture or []) + +new_fixture = get_default_fixture_template() +new_fixture.description = "Entrance LED" +new_fixture.fixtureWattage = 80.0 +new_fixture.quantity = 2 + +updated_space = eu.exteriorLightingSpace.model_copy( + deep=True, + update={"fixture": existing_fixtures + [new_fixture]}, +) +project = el_ops.update_exterior_lighting_area_in_project( + project, + "Main Parking Area", + { + "exteriorLightingSpace": updated_space.model_dump( + mode="python", exclude_unset=True + ) + }, +) +print("Second fixture added to Main Parking Area") + +# ── Step 6: Remove the ExteriorUse ─────────────────────────────────────────── +project = el_ops.remove_exterior_lighting_area_from_project( + project, "Main Parking Area" +) +print("ExteriorUse removed: 'Main Parking Area'") + +keys = el_ops.get_exterior_lighting_area_keys_from_project(project) +print(f"Remaining exterior uses: {keys}") diff --git a/examples/project_operations/interior_lighting_operations.py b/examples/project_operations/interior_lighting_operations.py new file mode 100644 index 0000000..00f50c0 --- /dev/null +++ b/examples/project_operations/interior_lighting_operations.py @@ -0,0 +1,109 @@ +"""Example: interior lighting operations. + +Interior lighting is managed at the ActivityUse granularity. Each ActivityUse +belongs to a WholeBldgUse (building area) and carries exactly one +InteriorLightingSpace whose fixture[] holds the fixtures. + +There are no fixture-level operations — to add, change, or remove a fixture, +edit the activityUse's interiorLightingSpace.fixture[] list and pass the whole +ActivityUse through update_interior_lighting_space_in_project. +""" + +import os +from dotenv import load_dotenv + +from comcheck_api import ( + COMcheckClient, + project_building_area_operations as ba_ops, + project_interior_lighting_operations as il_ops, +) +from comcheck_api.defaults import ( + get_default_interior_lighting_space_template, + get_default_building_area_template, + get_default_fixture_template, +) +from comcheck_api.types.core_types import ActivityTypeOptions, LightingTypeOptions + +load_dotenv() +client = COMcheckClient() +client.set_api_key(os.getenv("COM_API_KEY") or "your-api-key-here") + +# Start from the default project template +from comcheck_api.defaults import get_default_project_template + +project = get_default_project_template() + +# ── Step 1: A building area must exist before adding activity uses ──────────── +area = get_default_building_area_template() +area.areaDescription = "Main Office" +project = ba_ops.add_building_area_to_project(project, area) +area_key = area.key +print(f"Building area added: {area.areaDescription!r} (key={area_key})") + +# ── Step 2: Add an ActivityUse with a fixture already populated ─────────────── +fixture = get_default_fixture_template() +fixture.description = "Recessed LED" +fixture.lightingType = LightingTypeOptions.LED +fixture.fixtureWattage = 20.0 +fixture.quantity = 10 + +activity_use = get_default_interior_lighting_space_template() +activity_use.areaDescription = "Open Office" +activity_use.activityType = ActivityTypeOptions.ACTIVITY_COMMON_OFFICE +activity_use.floorArea = 2000.0 +activity_use.interiorLightingSpace = activity_use.interiorLightingSpace.model_copy( + deep=True, update={"fixture": [fixture]} +) + +project = il_ops.add_interior_lighting_space_to_project(project, area_key, activity_use) +print(f"ActivityUse added: {activity_use.areaDescription!r}") + +# ── Step 3: List all activity uses in the building area ─────────────────────── +keys = il_ops.get_interior_lighting_space_keys_from_project(project, area_key) +print(f"Activity uses in {area.areaDescription!r}: {keys}") + +# ── Step 4: Update the ActivityUse (change floor area) ─────────────────────── +project = il_ops.update_interior_lighting_space_in_project( + project, + area_key, + "Open Office", + {"floorArea": 2500.0}, +) +print("ActivityUse updated: floorArea → 2500.0") + +# ── Step 5: Add a second fixture by updating the lighting space ─────────────── +# Retrieve current activityUse to get the existing fixtures +whole_use = project.get_by_path("lighting.wholeBldgUse") +ba = next(a for a in whole_use if a.key == area_key) +au = next(au for au in ba.activityUse if au.areaDescription == "Open Office") +existing_fixtures = list(au.interiorLightingSpace.fixture or []) + +new_fixture = get_default_fixture_template() +new_fixture.description = "Pendant LED" +new_fixture.fixtureWattage = 35.0 +new_fixture.quantity = 4 + +updated_space = au.interiorLightingSpace.model_copy( + deep=True, + update={"fixture": existing_fixtures + [new_fixture]}, +) +project = il_ops.update_interior_lighting_space_in_project( + project, + area_key, + "Open Office", + { + "interiorLightingSpace": updated_space.model_dump( + mode="python", exclude_unset=True + ) + }, +) +print("Second fixture added to Open Office") + +# ── Step 6: Remove the ActivityUse ─────────────────────────────────────────── +project = il_ops.remove_interior_lighting_space_from_project( + project, area_key, "Open Office" +) +print("ActivityUse removed: 'Open Office'") + +keys = il_ops.get_interior_lighting_space_keys_from_project(project, area_key) +print(f"Remaining activity uses: {keys}") diff --git a/tests/project_operation_tests/test_exterior_lighting_operations.py b/tests/project_operation_tests/test_exterior_lighting_operations.py new file mode 100644 index 0000000..f4022ab --- /dev/null +++ b/tests/project_operation_tests/test_exterior_lighting_operations.py @@ -0,0 +1,280 @@ +"""Tests for project_exterior_lighting_operations.""" + +import pytest + +from comcheck_api.defaults import ( + get_default_exterior_lighting_area_template, + get_default_fixture_template, +) +from comcheck_api.project_operations import ( + project_exterior_lighting_operations as el_ops, +) +from comcheck_api.types.core_types import ( + ComBuilding, + ExteriorLightingZoneTypeOptions, + ExteriorUseTypeOptions, + LightingTypeOptions, +) + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _fresh(project: ComBuilding) -> ComBuilding: + return project.model_copy(deep=True) + + +# --------------------------------------------------------------------------- +# set_exterior_lighting_zone_type_in_project +# --------------------------------------------------------------------------- + + +def test_set_zone_type(project: ComBuilding): + result = el_ops.set_exterior_lighting_zone_type_in_project( + project, ExteriorLightingZoneTypeOptions.EXT_ZONE_METRO_COMMERCIAL + ) + assert ( + result.lighting.exteriorLightingZoneType + == ExteriorLightingZoneTypeOptions.EXT_ZONE_METRO_COMMERCIAL + ) + + +def test_set_zone_type_rejects_unspecified(project: ComBuilding): + with pytest.raises(ValueError, match="EXT_ZONE_UNSPECIFIED"): + el_ops.set_exterior_lighting_zone_type_in_project( + project, ExteriorLightingZoneTypeOptions.EXT_ZONE_UNSPECIFIED + ) + + +def test_set_zone_type_rejects_non_enum(project: ComBuilding): + with pytest.raises(TypeError): + el_ops.set_exterior_lighting_zone_type_in_project(project, "EXT_ZONE_RURAL") + + +def test_set_zone_type_does_not_mutate_original(project: ComBuilding): + proj = _fresh(project) + original_zone = proj.lighting.exteriorLightingZoneType + el_ops.set_exterior_lighting_zone_type_in_project( + proj, ExteriorLightingZoneTypeOptions.EXT_ZONE_RURAL + ) + assert proj.lighting.exteriorLightingZoneType == original_zone + + +# --------------------------------------------------------------------------- +# add_exterior_lighting_area_to_project +# --------------------------------------------------------------------------- + + +def test_add_exterior_use(project: ComBuilding): + proj = el_ops.set_exterior_lighting_zone_type_in_project( + _fresh(project), ExteriorLightingZoneTypeOptions.EXT_ZONE_RURAL + ) + eu = get_default_exterior_lighting_area_template() + eu.areaDescription = "Parking lot" + + result = el_ops.add_exterior_lighting_area_to_project(proj, eu) + + keys = el_ops.get_exterior_lighting_area_keys_from_project(result) + assert any(k["areaDescription"] == "Parking lot" for k in keys) + + +def test_add_exterior_use_warns_when_zone_unspecified(project: ComBuilding): + proj = _fresh(project) + # Force zone to unspecified directly + proj.lighting.exteriorLightingZoneType = ( + ExteriorLightingZoneTypeOptions.EXT_ZONE_UNSPECIFIED + ) + + eu = get_default_exterior_lighting_area_template() + eu.areaDescription = "Entry" + + with pytest.warns(UserWarning, match="EXT_ZONE_UNSPECIFIED"): + el_ops.add_exterior_lighting_area_to_project(proj, eu) + + +def test_add_exterior_use_does_not_mutate_original(project: ComBuilding): + proj = el_ops.set_exterior_lighting_zone_type_in_project( + _fresh(project), ExteriorLightingZoneTypeOptions.EXT_ZONE_RURAL + ) + original_count = len(proj.lighting.exteriorUse) + eu = get_default_exterior_lighting_area_template() + el_ops.add_exterior_lighting_area_to_project(proj, eu) + assert len(proj.lighting.exteriorUse) == original_count + + +# --------------------------------------------------------------------------- +# update_exterior_lighting_area_in_project +# --------------------------------------------------------------------------- + + +def test_update_exterior_use(project: ComBuilding): + proj = el_ops.set_exterior_lighting_zone_type_in_project( + _fresh(project), ExteriorLightingZoneTypeOptions.EXT_ZONE_RURAL + ) + eu = get_default_exterior_lighting_area_template() + eu.areaDescription = "Loading dock" + proj = el_ops.add_exterior_lighting_area_to_project(proj, eu) + + result = el_ops.update_exterior_lighting_area_in_project( + proj, + "Loading dock", + { + "useQuantity": 500.0, + "exteriorType": ExteriorUseTypeOptions.EXTERIOR_LOADING_DOCK, + }, + ) + + exterior_uses = result.get_by_path("lighting.exteriorUse") + updated = next(e for e in exterior_uses if e.areaDescription == "Loading dock") + assert updated.useQuantity == 500.0 + assert updated.exteriorType == ExteriorUseTypeOptions.EXTERIOR_LOADING_DOCK + + +def test_update_exterior_use_not_found(project: ComBuilding): + with pytest.raises(ValueError, match="not found"): + el_ops.update_exterior_lighting_area_in_project( + _fresh(project), "Nonexistent", {"useQuantity": 100.0} + ) + + +# --------------------------------------------------------------------------- +# remove_exterior_lighting_area_from_project +# --------------------------------------------------------------------------- + + +def test_remove_exterior_use(project: ComBuilding): + proj = el_ops.set_exterior_lighting_zone_type_in_project( + _fresh(project), ExteriorLightingZoneTypeOptions.EXT_ZONE_RURAL + ) + eu = get_default_exterior_lighting_area_template() + eu.areaDescription = "Walkway" + proj = el_ops.add_exterior_lighting_area_to_project(proj, eu) + + result = el_ops.remove_exterior_lighting_area_from_project(proj, "Walkway") + + keys = el_ops.get_exterior_lighting_area_keys_from_project(result) + assert not any(k["areaDescription"] == "Walkway" for k in keys) + + +def test_remove_exterior_use_not_found(project: ComBuilding): + with pytest.raises(ValueError, match="not found"): + el_ops.remove_exterior_lighting_area_from_project( + _fresh(project), "Nonexistent" + ) + + +# --------------------------------------------------------------------------- +# Fixture editing via the ExteriorUse payload +# --------------------------------------------------------------------------- + + +def test_add_fixture_via_exterior_use_update(project: ComBuilding): + proj = el_ops.set_exterior_lighting_zone_type_in_project( + _fresh(project), ExteriorLightingZoneTypeOptions.EXT_ZONE_RURAL + ) + eu = get_default_exterior_lighting_area_template() + eu.areaDescription = "Canopy" + proj = el_ops.add_exterior_lighting_area_to_project(proj, eu) + + fixture = get_default_fixture_template() + fixture.description = "Canopy LED" + fixture.fixtureWattage = 60.0 + + exterior_uses = proj.get_by_path("lighting.exteriorUse") + added_eu = next(e for e in exterior_uses if e.areaDescription == "Canopy") + updated_space = added_eu.exteriorLightingSpace.model_copy( + deep=True, update={"fixture": [fixture]} + ) + result = el_ops.update_exterior_lighting_area_in_project( + proj, + "Canopy", + { + "exteriorLightingSpace": updated_space.model_dump( + mode="python", exclude_unset=True + ) + }, + ) + + exterior_uses = result.get_by_path("lighting.exteriorUse") + updated_eu = next(e for e in exterior_uses if e.areaDescription == "Canopy") + fixtures = updated_eu.exteriorLightingSpace.fixture or [] + assert len(fixtures) == 1 + assert fixtures[0].description == "Canopy LED" + assert fixtures[0].fixtureWattage == 60.0 + + +def test_fixture_fields_preserved_on_exterior_use_update(project: ComBuilding): + proj = el_ops.set_exterior_lighting_zone_type_in_project( + _fresh(project), ExteriorLightingZoneTypeOptions.EXT_ZONE_RURAL + ) + eu = get_default_exterior_lighting_area_template() + eu.areaDescription = "Plaza" + fixture = get_default_fixture_template() + fixture.description = "Plaza LED" + fixture.quantity = 6 + eu.exteriorLightingSpace = eu.exteriorLightingSpace.model_copy( + deep=True, update={"fixture": [fixture]} + ) + proj = el_ops.add_exterior_lighting_area_to_project(proj, eu) + + # Update only useQuantity — fixtures must be untouched + result = el_ops.update_exterior_lighting_area_in_project( + proj, "Plaza", {"useQuantity": 800.0} + ) + + exterior_uses = result.get_by_path("lighting.exteriorUse") + updated_eu = next(e for e in exterior_uses if e.areaDescription == "Plaza") + fixtures = updated_eu.exteriorLightingSpace.fixture or [] + assert len(fixtures) == 1 + assert fixtures[0].description == "Plaza LED" + assert fixtures[0].quantity == 6 + + +def test_remove_fixture_by_omitting_from_update(project: ComBuilding): + proj = el_ops.set_exterior_lighting_zone_type_in_project( + _fresh(project), ExteriorLightingZoneTypeOptions.EXT_ZONE_RURAL + ) + eu = get_default_exterior_lighting_area_template() + eu.areaDescription = "Driveway" + fixture = get_default_fixture_template() + fixture.description = "Driveway LED" + eu.exteriorLightingSpace = eu.exteriorLightingSpace.model_copy( + deep=True, update={"fixture": [fixture]} + ) + proj = el_ops.add_exterior_lighting_area_to_project(proj, eu) + + result = el_ops.update_exterior_lighting_area_in_project( + proj, "Driveway", {"exteriorLightingSpace": {"fixture": []}} + ) + + exterior_uses = result.get_by_path("lighting.exteriorUse") + updated_eu = next(e for e in exterior_uses if e.areaDescription == "Driveway") + assert (updated_eu.exteriorLightingSpace.fixture or []) == [] + + +# --------------------------------------------------------------------------- +# get_exterior_lighting_area_keys_from_project +# --------------------------------------------------------------------------- + + +def test_get_exterior_use_keys_empty(project: ComBuilding): + proj = _fresh(project) + proj.lighting.exteriorUse = [] + result = el_ops.get_exterior_lighting_area_keys_from_project(proj) + assert result == [] + + +def test_get_exterior_use_keys_returns_all(project: ComBuilding): + proj = el_ops.set_exterior_lighting_zone_type_in_project( + _fresh(project), ExteriorLightingZoneTypeOptions.EXT_ZONE_RURAL + ) + for desc in ["Entry A", "Entry B"]: + eu = get_default_exterior_lighting_area_template() + eu.areaDescription = desc + proj = el_ops.add_exterior_lighting_area_to_project(proj, eu) + + keys = el_ops.get_exterior_lighting_area_keys_from_project(proj) + descriptions = [k["areaDescription"] for k in keys] + assert "Entry A" in descriptions + assert "Entry B" in descriptions diff --git a/tests/project_operation_tests/test_interior_lighting_operations.py b/tests/project_operation_tests/test_interior_lighting_operations.py new file mode 100644 index 0000000..e8baf4f --- /dev/null +++ b/tests/project_operation_tests/test_interior_lighting_operations.py @@ -0,0 +1,255 @@ +"""Tests for project_interior_lighting_operations.""" + +import pytest + +from comcheck_api.defaults import ( + get_default_interior_lighting_space_template, + get_default_building_area_template, + get_default_fixture_template, +) +from comcheck_api.project_operations import ( + project_building_area_operations, + project_interior_lighting_operations as il_ops, +) +from comcheck_api.types.core_types import ( + ActivityTypeOptions, + ComBuilding, + LightingTypeOptions, +) + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _fresh_project_with_area(project: ComBuilding) -> tuple[ComBuilding, str]: + """Return a deep-copy of project with one new building area added.""" + proj = project.model_copy(deep=True) + area = get_default_building_area_template() + proj = project_building_area_operations.add_building_area_to_project(proj, area) + return proj, area.key + + +# --------------------------------------------------------------------------- +# Lifecycle: add / update / remove +# --------------------------------------------------------------------------- + + +def test_add_activity_use(project: ComBuilding): + proj, area_key = _fresh_project_with_area(project) + activity_use = get_default_interior_lighting_space_template() + activity_use.areaDescription = "Office space" + + result = il_ops.add_interior_lighting_space_to_project(proj, area_key, activity_use) + + keys = il_ops.get_interior_lighting_space_keys_from_project(result, area_key) + descriptions = [k["areaDescription"] for k in keys] + assert "Office space" in descriptions + + +def test_add_activity_use_sets_key(project: ComBuilding): + proj, area_key = _fresh_project_with_area(project) + activity_use = get_default_interior_lighting_space_template() + + result = il_ops.add_interior_lighting_space_to_project(proj, area_key, activity_use) + + whole_use = result.get_by_path("lighting.wholeBldgUse") + area = next(a for a in whole_use if a.key == area_key) + added = area.activityUse[-1] + assert added.key == area_key + + +def test_update_activity_use(project: ComBuilding): + proj, area_key = _fresh_project_with_area(project) + activity_use = get_default_interior_lighting_space_template() + activity_use.areaDescription = "Conference room" + proj = il_ops.add_interior_lighting_space_to_project(proj, area_key, activity_use) + + result = il_ops.update_interior_lighting_space_in_project( + proj, + area_key, + "Conference room", + { + "floorArea": 2500.0, + "activityType": ActivityTypeOptions.ACTIVITY_COMMON_CONFERENCE_HALL, + }, + ) + + whole_use = result.get_by_path("lighting.wholeBldgUse") + area = next(a for a in whole_use if a.key == area_key) + updated_au = next( + au for au in area.activityUse if au.areaDescription == "Conference room" + ) + assert updated_au.floorArea == 2500.0 + assert ( + updated_au.activityType == ActivityTypeOptions.ACTIVITY_COMMON_CONFERENCE_HALL + ) + + +def test_remove_activity_use(project: ComBuilding): + proj, area_key = _fresh_project_with_area(project) + activity_use = get_default_interior_lighting_space_template() + activity_use.areaDescription = "Storage room" + proj = il_ops.add_interior_lighting_space_to_project(proj, area_key, activity_use) + + result = il_ops.remove_interior_lighting_space_from_project( + proj, area_key, "Storage room" + ) + + keys = il_ops.get_interior_lighting_space_keys_from_project(result, area_key) + assert "Storage room" not in [k["areaDescription"] for k in keys] + + +# --------------------------------------------------------------------------- +# Fixture editing via the ActivityUse payload +# --------------------------------------------------------------------------- + + +def test_add_fixture_via_activity_use_update(project: ComBuilding): + proj, area_key = _fresh_project_with_area(project) + activity_use = get_default_interior_lighting_space_template() + activity_use.areaDescription = "Lab" + proj = il_ops.add_interior_lighting_space_to_project(proj, area_key, activity_use) + + fixture = get_default_fixture_template() + fixture.description = "Lab LED" + fixture.fixtureWattage = 48.0 + + # Retrieve the current activityUse, append the fixture, then update + whole_use = proj.get_by_path("lighting.wholeBldgUse") + area = next(a for a in whole_use if a.key == area_key) + au = next(au for au in area.activityUse if au.areaDescription == "Lab") + + updated_space = au.interiorLightingSpace.model_copy( + deep=True, + update={"fixture": [fixture]}, + ) + result = il_ops.update_interior_lighting_space_in_project( + proj, + area_key, + "Lab", + { + "interiorLightingSpace": updated_space.model_dump( + mode="python", exclude_unset=True + ) + }, + ) + + whole_use = result.get_by_path("lighting.wholeBldgUse") + area = next(a for a in whole_use if a.key == area_key) + au = next(au for au in area.activityUse if au.areaDescription == "Lab") + fixtures = au.interiorLightingSpace.fixture or [] + assert len(fixtures) == 1 + assert fixtures[0].description == "Lab LED" + assert fixtures[0].fixtureWattage == 48.0 + + +def test_remove_fixture_by_omitting_from_update(project: ComBuilding): + proj, area_key = _fresh_project_with_area(project) + activity_use = get_default_interior_lighting_space_template() + activity_use.areaDescription = "Lobby" + fixture = get_default_fixture_template() + fixture.description = "Lobby fixture" + activity_use.interiorLightingSpace = activity_use.interiorLightingSpace.model_copy( + deep=True, update={"fixture": [fixture]} + ) + proj = il_ops.add_interior_lighting_space_to_project(proj, area_key, activity_use) + + # Update with empty fixture list — effectively removes all fixtures + result = il_ops.update_interior_lighting_space_in_project( + proj, + area_key, + "Lobby", + {"interiorLightingSpace": {"fixture": []}}, + ) + + whole_use = result.get_by_path("lighting.wholeBldgUse") + area = next(a for a in whole_use if a.key == area_key) + au = next(au for au in area.activityUse if au.areaDescription == "Lobby") + assert (au.interiorLightingSpace.fixture or []) == [] + + +def test_fixture_fields_preserved_on_activity_use_update(project: ComBuilding): + proj, area_key = _fresh_project_with_area(project) + activity_use = get_default_interior_lighting_space_template() + activity_use.areaDescription = "Gym" + fixture = get_default_fixture_template() + fixture.description = "Gym LED" + fixture.lightingType = LightingTypeOptions.LED + fixture.quantity = 4 + activity_use.interiorLightingSpace = activity_use.interiorLightingSpace.model_copy( + deep=True, update={"fixture": [fixture]} + ) + proj = il_ops.add_interior_lighting_space_to_project(proj, area_key, activity_use) + + # Update only the floorArea — fixtures must be untouched + result = il_ops.update_interior_lighting_space_in_project( + proj, area_key, "Gym", {"floorArea": 3000.0} + ) + + whole_use = result.get_by_path("lighting.wholeBldgUse") + area = next(a for a in whole_use if a.key == area_key) + au = next(au for au in area.activityUse if au.areaDescription == "Gym") + fixtures = au.interiorLightingSpace.fixture or [] + assert len(fixtures) == 1 + assert fixtures[0].description == "Gym LED" + assert fixtures[0].quantity == 4 + + +# --------------------------------------------------------------------------- +# Error cases +# --------------------------------------------------------------------------- + + +def test_add_activity_use_invalid_building_area(project: ComBuilding): + with pytest.raises(ValueError, match="not found"): + il_ops.add_interior_lighting_space_to_project( + project, "nonexistent-key", get_default_interior_lighting_space_template() + ) + + +def test_update_activity_use_not_found(project: ComBuilding): + proj, area_key = _fresh_project_with_area(project) + with pytest.raises(ValueError, match="not found"): + il_ops.update_interior_lighting_space_in_project( + proj, area_key, "Nonexistent space", {"floorArea": 100.0} + ) + + +def test_remove_activity_use_not_found(project: ComBuilding): + proj, area_key = _fresh_project_with_area(project) + with pytest.raises(ValueError, match="not found"): + il_ops.remove_interior_lighting_space_from_project( + proj, area_key, "Nonexistent space" + ) + + +# --------------------------------------------------------------------------- +# get_interior_lighting_space_keys_from_project +# --------------------------------------------------------------------------- + + +def test_get_activity_use_keys_empty(project: ComBuilding): + proj, area_key = _fresh_project_with_area(project) + result = il_ops.get_interior_lighting_space_keys_from_project(proj, area_key) + assert result == [] + + +def test_get_activity_use_keys_unknown_area(project: ComBuilding): + result = il_ops.get_interior_lighting_space_keys_from_project( + project, "nonexistent-key" + ) + assert result == [] + + +def test_get_activity_use_keys_returns_all(project: ComBuilding): + proj, area_key = _fresh_project_with_area(project) + for desc in ["Space A", "Space B"]: + au = get_default_interior_lighting_space_template() + au.areaDescription = desc + proj = il_ops.add_interior_lighting_space_to_project(proj, area_key, au) + + keys = il_ops.get_interior_lighting_space_keys_from_project(proj, area_key) + descriptions = [k["areaDescription"] for k in keys] + assert "Space A" in descriptions + assert "Space B" in descriptions From 5c2e0aaf68b431717402ce184d84c8e56bf95b53 Mon Sep 17 00:00:00 2001 From: yanz571 Date: Fri, 24 Jul 2026 09:29:29 -0700 Subject: [PATCH 03/23] update to full name instead of abbreviations --- .../project_exterior_lighting_operations.py | 6 ++--- .../project_interior_lighting_operations.py | 12 +++++----- comcheck_api/utilities/project_utilities.py | 9 +++++--- .../exterior_lighting_operations.py | 15 +++++++++---- .../interior_lighting_operations.py | 22 ++++++++++++++----- 5 files changed, 43 insertions(+), 21 deletions(-) diff --git a/comcheck_api/project_operations/project_exterior_lighting_operations.py b/comcheck_api/project_operations/project_exterior_lighting_operations.py index e2911ee..607561e 100644 --- a/comcheck_api/project_operations/project_exterior_lighting_operations.py +++ b/comcheck_api/project_operations/project_exterior_lighting_operations.py @@ -192,8 +192,8 @@ def get_exterior_lighting_area_keys_from_project(project: ComBuilding) -> list[d return [] return [ { - "areaDescription": getattr(eu, "areaDescription", None), - "exteriorType": getattr(eu, "exteriorType", None), + "areaDescription": getattr(exterior_use, "areaDescription", None), + "exteriorType": getattr(exterior_use, "exteriorType", None), } - for eu in exterior_uses + for exterior_use in exterior_uses ] diff --git a/comcheck_api/project_operations/project_interior_lighting_operations.py b/comcheck_api/project_operations/project_interior_lighting_operations.py index 058d211..c3444cb 100644 --- a/comcheck_api/project_operations/project_interior_lighting_operations.py +++ b/comcheck_api/project_operations/project_interior_lighting_operations.py @@ -22,7 +22,8 @@ def _find_building_area(project: ComBuilding, building_area_key: str): """Return the WholeBldgUse with the given key, or raise.""" whole_use = project.get_by_path("lighting.wholeBldgUse") or [] area = next( - (a for a in whole_use if getattr(a, "key", None) == building_area_key), None + (area for area in whole_use if getattr(area, "key", None) == building_area_key), + None, ) if area is None: raise ValueError( @@ -167,7 +168,8 @@ def get_interior_lighting_space_keys_from_project( return [] area = next( - (a for a in whole_use if getattr(a, "key", None) == building_area_key), None + (area for area in whole_use if getattr(area, "key", None) == building_area_key), + None, ) if area is None: return [] @@ -175,8 +177,8 @@ def get_interior_lighting_space_keys_from_project( activity_uses = getattr(area, "activityUse", []) or [] return [ { - "areaDescription": getattr(au, "areaDescription", None), - "activityType": getattr(au, "activityType", None), + "areaDescription": getattr(activity_use, "areaDescription", None), + "activityType": getattr(activity_use, "activityType", None), } - for au in activity_uses + for activity_use in activity_uses ] diff --git a/comcheck_api/utilities/project_utilities.py b/comcheck_api/utilities/project_utilities.py index 2a68793..8e6a4b6 100644 --- a/comcheck_api/utilities/project_utilities.py +++ b/comcheck_api/utilities/project_utilities.py @@ -30,7 +30,8 @@ def _require_activity_use( raise ValueError("No building areas (wholeBldgUse) found in project.") area = next( - (a for a in whole_use if getattr(a, "key", None) == building_area_key), None + (area for area in whole_use if getattr(area, "key", None) == building_area_key), + None, ) if area is None: raise ValueError( @@ -39,7 +40,8 @@ def _require_activity_use( activity_uses = getattr(area, "activityUse", []) or [] if not any( - getattr(au, "areaDescription", None) == area_description for au in activity_uses + getattr(activity_use, "areaDescription", None) == area_description + for activity_use in activity_uses ): raise ValueError( f"ActivityUse with areaDescription '{area_description}' " @@ -54,7 +56,8 @@ def _require_exterior_use(project: ComBuilding, area_description: str) -> None: raise ValueError("No exterior uses (lighting.exteriorUse) found in project.") if not any( - getattr(eu, "areaDescription", None) == area_description for eu in exterior_uses + getattr(exterior_use, "areaDescription", None) == area_description + for exterior_use in exterior_uses ): raise ValueError( f"ExteriorUse with areaDescription '{area_description}' " diff --git a/examples/project_operations/exterior_lighting_operations.py b/examples/project_operations/exterior_lighting_operations.py index 7875652..dad1c82 100644 --- a/examples/project_operations/exterior_lighting_operations.py +++ b/examples/project_operations/exterior_lighting_operations.py @@ -76,16 +76,23 @@ print("ExteriorUse updated: useQuantity → 6000.0") # ── Step 5: Add a second fixture by updating the lighting space ─────────────── -exterior_uses = project.get_by_path("lighting.exteriorUse") -eu = next(e for e in exterior_uses if e.areaDescription == "Main Parking Area") -existing_fixtures = list(eu.exteriorLightingSpace.fixture or []) +# Use direct attribute access (not get_by_path, which returns Any) so +# `exterior_use` keeps its real type for editor autocomplete and type checking. +if not project.lighting or not project.lighting.exteriorUse: + raise ValueError("Project has no exterior uses (exteriorUse)") +exterior_use = next( + exterior_use + for exterior_use in project.lighting.exteriorUse + if exterior_use.areaDescription == "Main Parking Area" +) +existing_fixtures = list(exterior_use.exteriorLightingSpace.fixture or []) new_fixture = get_default_fixture_template() new_fixture.description = "Entrance LED" new_fixture.fixtureWattage = 80.0 new_fixture.quantity = 2 -updated_space = eu.exteriorLightingSpace.model_copy( +updated_space = exterior_use.exteriorLightingSpace.model_copy( deep=True, update={"fixture": existing_fixtures + [new_fixture]}, ) diff --git a/examples/project_operations/interior_lighting_operations.py b/examples/project_operations/interior_lighting_operations.py index 00f50c0..4532e80 100644 --- a/examples/project_operations/interior_lighting_operations.py +++ b/examples/project_operations/interior_lighting_operations.py @@ -72,18 +72,28 @@ print("ActivityUse updated: floorArea → 2500.0") # ── Step 5: Add a second fixture by updating the lighting space ─────────────── -# Retrieve current activityUse to get the existing fixtures -whole_use = project.get_by_path("lighting.wholeBldgUse") -ba = next(a for a in whole_use if a.key == area_key) -au = next(au for au in ba.activityUse if au.areaDescription == "Open Office") -existing_fixtures = list(au.interiorLightingSpace.fixture or []) +# Retrieve current activityUse to get the existing fixtures. +# Use direct attribute access (not get_by_path, which returns Any) so +# `building_area`, `activity_use`, etc. keep their real types for editor +# autocomplete and type checking. +if not project.lighting or not project.lighting.wholeBldgUse: + raise ValueError("Project has no building areas (wholeBldgUse)") +building_area = next( + area for area in project.lighting.wholeBldgUse if area.key == area_key +) +activity_use = next( + activity_use + for activity_use in building_area.activityUse + if activity_use.areaDescription == "Open Office" +) +existing_fixtures = list(activity_use.interiorLightingSpace.fixture or []) new_fixture = get_default_fixture_template() new_fixture.description = "Pendant LED" new_fixture.fixtureWattage = 35.0 new_fixture.quantity = 4 -updated_space = au.interiorLightingSpace.model_copy( +updated_space = activity_use.interiorLightingSpace.model_copy( deep=True, update={"fixture": existing_fixtures + [new_fixture]}, ) From 5d437a432b77b9a8e598cd2965b013a82b60f160 Mon Sep 17 00:00:00 2001 From: yanz571 Date: Fri, 24 Jul 2026 14:17:30 -0700 Subject: [PATCH 04/23] Fix interior lightin examples --- .../constants/exterior_lighting_constants.py | 2 +- .../constants/interior_lighting_constants.py | 5 +- .../project_interior_lighting_operations.py | 8 +- .../interior_lighting_operations.py | 86 +++++++++++++++++-- 4 files changed, 88 insertions(+), 13 deletions(-) diff --git a/comcheck_api/constants/exterior_lighting_constants.py b/comcheck_api/constants/exterior_lighting_constants.py index ed466ca..d4b9f6f 100644 --- a/comcheck_api/constants/exterior_lighting_constants.py +++ b/comcheck_api/constants/exterior_lighting_constants.py @@ -7,7 +7,7 @@ ) DEFAULT_EXTERIOR_LIGHTING_AREA: ExteriorUse = ExteriorUse( - areaDescription="Ext Area 1", + areaDescription="Ext Area 1", # identifier for the exterior area exteriorType=ExteriorUseTypeOptions.EXTERIOR_PARKING_AREA, isTradable=True, powerDensity=0.0, diff --git a/comcheck_api/constants/interior_lighting_constants.py b/comcheck_api/constants/interior_lighting_constants.py index 6958338..c73641a 100644 --- a/comcheck_api/constants/interior_lighting_constants.py +++ b/comcheck_api/constants/interior_lighting_constants.py @@ -8,10 +8,9 @@ LightingTypeOptions, ) -# key is a placeholder — callers must set it to the parent WholeBldgUse.key DEFAULT_INTERIOR_LIGHTING_SPACE_AREA: ActivityUse = ActivityUse( - key="__unset__", - areaDescription="Space 1", + key="__unset__", # key is a placeholder — callers must set it to the parent WholeBldgUse.key + areaDescription="Space 1", # identifier for the ActivityUse within its parent WholeBldgUse activityType=ActivityTypeOptions.ACTIVITY_COMMON_OFFICE, floorArea=1000.0, ceilingHeight=9.0, diff --git a/comcheck_api/project_operations/project_interior_lighting_operations.py b/comcheck_api/project_operations/project_interior_lighting_operations.py index c3444cb..fddad69 100644 --- a/comcheck_api/project_operations/project_interior_lighting_operations.py +++ b/comcheck_api/project_operations/project_interior_lighting_operations.py @@ -14,15 +14,15 @@ from comcheck_api.constants.interior_lighting_constants import ( DEFAULT_INTERIOR_LIGHTING_SPACE_AREA, ) -from comcheck_api.types.core_types import ActivityUse, ComBuilding +from comcheck_api.types.core_types import ActivityUse, ComBuilding, WholeBldgUse from comcheck_api.utilities.project_utilities import _require_activity_use -def _find_building_area(project: ComBuilding, building_area_key: str): +def _find_building_area(project: ComBuilding, building_area_key: str) -> WholeBldgUse: """Return the WholeBldgUse with the given key, or raise.""" - whole_use = project.get_by_path("lighting.wholeBldgUse") or [] + whole_use = project.lighting.wholeBldgUse if project.lighting else [] area = next( - (area for area in whole_use if getattr(area, "key", None) == building_area_key), + (area for area in whole_use if area.key == building_area_key), None, ) if area is None: diff --git a/examples/project_operations/interior_lighting_operations.py b/examples/project_operations/interior_lighting_operations.py index 4532e80..8950485 100644 --- a/examples/project_operations/interior_lighting_operations.py +++ b/examples/project_operations/interior_lighting_operations.py @@ -9,6 +9,7 @@ ActivityUse through update_interior_lighting_space_in_project. """ +import logging import os from dotenv import load_dotenv @@ -17,38 +18,98 @@ project_building_area_operations as ba_ops, project_interior_lighting_operations as il_ops, ) + +# The library logs API failures via logging.getLogger(__name__) but never +# configures a handler (as a library shouldn't). Configure logging here so +# those error logs — including the server's response body — are visible. +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(name)s %(levelname)s %(message)s", +) from comcheck_api.defaults import ( get_default_interior_lighting_space_template, get_default_building_area_template, get_default_fixture_template, ) from comcheck_api.types.core_types import ActivityTypeOptions, LightingTypeOptions +from comcheck_api.utilities.common import export_to_json load_dotenv() client = COMcheckClient() client.set_api_key(os.getenv("COM_API_KEY") or "your-api-key-here") -# Start from the default project template -from comcheck_api.defaults import get_default_project_template -project = get_default_project_template() +def normalize_numeric_nulls(model): + """Default every null numeric field on a model (recursively) to 0. + + The API declares many numeric fields non-nullable but still returns null + for them, then rejects those nulls on write. Rather than patch fields one + at a time, sweep the whole model tree and set any None-valued int/float + field to 0 (integers get 0, floats get 0.0 via Pydantic coercion). + + Because every ``update_project`` returns a freshly-fetched project (which + brings the server's nulls back), call this before *each* update, not just + once after the initial fetch. + + TODO: schema fix — these fields are typed number/integer but should allow null. + """ + from pydantic import BaseModel + + for name, field in type(model).model_fields.items(): + value = getattr(model, name, None) + annotation = str(field.annotation) + if value is None: + # Only purely-numeric fields (no str/enum in the union) — this + # leaves id-like fields (e.g. "str | int | None") untouched. + is_numeric = "int" in annotation or "float" in annotation + if is_numeric and "str" not in annotation: + setattr(model, name, 0) + elif isinstance(value, BaseModel): + normalize_numeric_nulls(value) + elif isinstance(value, list): + for item in value: + if isinstance(item, BaseModel): + normalize_numeric_nulls(item) + return model + + +# Fetch an existing project so changes can be saved back to the account. +# (update_project persists to the server; it requires a project that already +# exists there, so we start from a fetched project rather than a local +# template.) +project = client.get_project("43789") +if not project: + raise ValueError("Project not found") +project_id = str(project.id) +export_to_json(project, "interior_lighting_operations_before.json") +normalize_numeric_nulls(project) + # ── Step 1: A building area must exist before adding activity uses ──────────── area = get_default_building_area_template() -area.areaDescription = "Main Office" +area.areaDescription = "Main Office 1" project = ba_ops.add_building_area_to_project(project, area) area_key = area.key +export_to_json(project, "interior_lighting_operations_after_add_building_area.json") +print("exported") +# Persist the new building area to the account. +normalize_numeric_nulls(project) +project = client.update_project(project_id, project) +if not project: + raise ValueError("Project not found after update") print(f"Building area added: {area.areaDescription!r} (key={area_key})") # ── Step 2: Add an ActivityUse with a fixture already populated ─────────────── fixture = get_default_fixture_template() fixture.description = "Recessed LED" -fixture.lightingType = LightingTypeOptions.LED +# Todo: update schema fixtureType is required, lightingType is optional. +fixture.fixtureType = LightingTypeOptions.LED fixture.fixtureWattage = 20.0 fixture.quantity = 10 activity_use = get_default_interior_lighting_space_template() activity_use.areaDescription = "Open Office" +# Todo: check if activityType options are based on energy code, or if they are just generic options. activity_use.activityType = ActivityTypeOptions.ACTIVITY_COMMON_OFFICE activity_use.floorArea = 2000.0 activity_use.interiorLightingSpace = activity_use.interiorLightingSpace.model_copy( @@ -56,6 +117,11 @@ ) project = il_ops.add_interior_lighting_space_to_project(project, area_key, activity_use) +export_to_json(project, "interior_lighting_operations_after_add.json") +normalize_numeric_nulls(project) +project = client.update_project(project_id, project) +if not project: + raise ValueError("Project not found after update") print(f"ActivityUse added: {activity_use.areaDescription!r}") # ── Step 3: List all activity uses in the building area ─────────────────────── @@ -69,6 +135,10 @@ "Open Office", {"floorArea": 2500.0}, ) +normalize_numeric_nulls(project) +project = client.update_project(project_id, project) +if not project: + raise ValueError("Project not found after update") print("ActivityUse updated: floorArea → 2500.0") # ── Step 5: Add a second fixture by updating the lighting space ─────────────── @@ -107,12 +177,18 @@ ) }, ) +project = client.update_project(project_id, project) +if not project: + raise ValueError("Project not found after update") print("Second fixture added to Open Office") # ── Step 6: Remove the ActivityUse ─────────────────────────────────────────── project = il_ops.remove_interior_lighting_space_from_project( project, area_key, "Open Office" ) +project = client.update_project(project_id, project) +if not project: + raise ValueError("Project not found after update") print("ActivityUse removed: 'Open Office'") keys = il_ops.get_interior_lighting_space_keys_from_project(project, area_key) From d61e33c02b6e4189a15f09df83c3adea07e6322a Mon Sep 17 00:00:00 2001 From: yanz571 Date: Fri, 24 Jul 2026 14:50:01 -0700 Subject: [PATCH 05/23] Fix exterior examples and todo documentation --- examples/project_operations/LIGHTING_TODOS.md | 93 +++++++++++++++++++ .../exterior_lighting_operations.py | 77 ++++++++++++++- .../interior_lighting_operations.py | 1 + 3 files changed, 168 insertions(+), 3 deletions(-) create mode 100644 examples/project_operations/LIGHTING_TODOS.md diff --git a/examples/project_operations/LIGHTING_TODOS.md b/examples/project_operations/LIGHTING_TODOS.md new file mode 100644 index 0000000..00ac453 --- /dev/null +++ b/examples/project_operations/LIGHTING_TODOS.md @@ -0,0 +1,93 @@ +# Lighting Operations — Schema & API TODOs + +Open issues discovered while building the interior and exterior lighting +examples (`interior_lighting_operations.py`, `exterior_lighting_operations.py`). +Most are server-side schema mismatches that force workarounds in the example +code; the goal is to fix them upstream so the workarounds can be removed. + +## Schema issues + +- [ ] **Numeric fields typed non-nullable but returned/rejected as null.** + The API declares many numeric fields (`number`/`integer`) as non-nullable, + yet it returns `null` for them on read and then rejects those same nulls on + write. + - *Workaround:* `normalize_numeric_nulls()` sweeps the project and defaults + every null numeric field to `0` before each `update_project`. + - *Fix:* make these fields nullable in the schema (or have the API stop + returning null for non-nullable fields). + + The fields below are the optional, purely-numeric fields that + `normalize_numeric_nulls()` will coerce (any of them can trigger the + `"is not of a type(s) number/integer"` rejection). Grouped by model; those + reachable from each example are noted. + + **Interior lighting path** (`lighting.wholeBldgUse[]`): + - `WholeBldgUse`: `allowedWattage`, `ceilingHeight`, `floorArea`, + `internalLoad`, `powerDensity`, `proposedWattage` + - `ActivityUse`: `allowedWattage`, `ceilingHeight`, `floorArea`, + `internalLoad`, `powerDensity`, `proposedWattage`, + `roomCavityRatioThreshold` + - `InteriorLightingSpace`: `allowanceFloorArea`, `decorativeArea`, + `numFixturesAlteredOrAdded`, `postAltTotalWattage`, `preAltNumberFixtures`, + `preAltTotalWattage`, `primaryDaylight`, `rcrFloorToWorkplaneHeight`, + `rcrPerimeter`, `rcrWorkplaneToLuminaireHeight`, `roofMonitorToplight`, + `secondaryDaylight`, `skylightToplight` + + **Exterior lighting path** (`lighting.exteriorUse[]`): + - `ExteriorUse`: `powerDensity`, `useQuantity` + - `ExteriorLightingSpace`: `numFixturesAlteredOrAdded`, `postAltTotalWattage`, + `preAltNumberFixtures`, `preAltTotalWattage` + + **Fixtures** (nested under both, in `*.fixture[]`): + - `Fixture`: `advControlsAllowanceAperture`, `allowanceFloorArea`, + `fixtureWattage`, `numberOfLamps`, `powerAllowance`, + `quantityWithAdvControls`, `trackCircuitBreakerAmps`, + `trackCircuitBreakerVolts`, `trackCurrentLimiterWattage`, `trackLength`, + `trackTotalLuminaireWattage`, `trackTransformerWattage` + + **Fixture schedule** (`lighting.fixtureSchedule[]`): + - `FixtureSchedule`: `trackCircuitBreakerAmps`, `trackCircuitBreakerVolts`, + `trackCurrentLimiterWattage`, `trackLength`, `trackTotalLuminaireWattage`, + `trackTransformerWattage` + - ⚠️ `FixtureSchedule.id` and `FixtureSchedule.lightingId` are typed + `int | None` (no `str` in the union), so `normalize_numeric_nulls()` will + **also coerce these identifier fields to `0`** — almost certainly wrong. + This is a hazard of the blanket sweep: purely-`int` id fields are + indistinguishable from measurements by the annotation alone. + - *Fix:* either exclude known id fields by name in the helper, or make id + fields `str | int | None` in the schema so the sweep skips them (as it + does for other `id` fields). + + > Note: `normalize_numeric_nulls()` sweeps the whole `ComBuilding` tree, so it + > also touches numeric fields outside lighting (e.g. envelope, HVAC). The list + > above covers only the lighting models the two examples exercise. + +- [ ] **`fixture.fixtureType` vs `fixture.lightingType` requiredness.** + In the current schema `lightingType` is required and `fixtureType` is + optional, but `fixtureType` should be the required field and `lightingType` + should be optional (it is marked for deprecation). + - *Fix:* swap requiredness — make `fixtureType` required, `lightingType` + optional. + +- [ ] **Building-area / interior-lighting-space keying.** + A building area's `areaDescription` should be unique within a project. + Interior lighting spaces appear to key off `areaDescription` rather than the + dedicated `key` field. + - *Fix:* clarify/enforce which field is the identifier and ensure + uniqueness constraints match. + +## Open questions + +- [ ] **`activityType` options — code-dependent or generic?** + Confirm whether the valid `ActivityUse.activityType` options depend on the + project's energy code, or are a single generic set. + +## Source locations + +| Item | File | Line (approx.) | +|---|---|---| +| Numeric null workaround | `interior_lighting_operations.py` | `normalize_numeric_nulls` docstring | +| Numeric null workaround | `exterior_lighting_operations.py` | `normalize_numeric_nulls` docstring | +| `fixtureType` requiredness | `interior_lighting_operations.py` | Step 2 (fixture setup) | +| Building-area keying | `interior_lighting_operations.py` | after client setup | +| `activityType` options | `interior_lighting_operations.py` | Step 2 (activity use setup) | diff --git a/examples/project_operations/exterior_lighting_operations.py b/examples/project_operations/exterior_lighting_operations.py index dad1c82..46fce6f 100644 --- a/examples/project_operations/exterior_lighting_operations.py +++ b/examples/project_operations/exterior_lighting_operations.py @@ -13,6 +13,7 @@ set_exterior_lighting_zone_type_in_project to fix it. """ +import logging import os from dotenv import load_dotenv @@ -20,10 +21,17 @@ COMcheckClient, project_exterior_lighting_operations as el_ops, ) + +# The library logs API failures via logging.getLogger(__name__) but never +# configures a handler (as a library shouldn't). Configure logging here so +# those error logs — including the server's response body — are visible. +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(name)s %(levelname)s %(message)s", +) from comcheck_api.defaults import ( get_default_exterior_lighting_area_template, get_default_fixture_template, - get_default_project_template, ) from comcheck_api.types.core_types import ( ExteriorLightingZoneTypeOptions, @@ -35,19 +43,66 @@ client = COMcheckClient() client.set_api_key(os.getenv("COM_API_KEY") or "your-api-key-here") -project = get_default_project_template() + +def normalize_numeric_nulls(model): + """Default every null numeric field on a model (recursively) to 0. + + The API declares many numeric fields non-nullable but still returns null + for them, then rejects those nulls on write. Rather than patch fields one + at a time, sweep the whole model tree and set any None-valued int/float + field to 0 (integers get 0, floats get 0.0 via Pydantic coercion). + + Because every ``update_project`` returns a freshly-fetched project (which + brings the server's nulls back), call this before *each* update, not just + once after the initial fetch. + + TODO: schema fix — these fields are typed number/integer but should allow null. + """ + from pydantic import BaseModel + + for name, field in type(model).model_fields.items(): + value = getattr(model, name, None) + annotation = str(field.annotation) + if value is None: + # Only purely-numeric fields (no str/enum in the union) — this + # leaves id-like fields (e.g. "str | int | None") untouched. + is_numeric = "int" in annotation or "float" in annotation + if is_numeric and "str" not in annotation: + setattr(model, name, 0) + elif isinstance(value, BaseModel): + normalize_numeric_nulls(value) + elif isinstance(value, list): + for item in value: + if isinstance(item, BaseModel): + normalize_numeric_nulls(item) + return model + + +# Fetch an existing project so changes can be saved back to the account. +# (update_project persists to the server; it requires a project that already +# exists there, so we start from a fetched project rather than a local +# template.) +project = client.get_project("43789") +if not project: + raise ValueError("Project not found") +project_id = str(project.id) +normalize_numeric_nulls(project) # ── Step 1: Set the exterior lighting zone type ─────────────────────────────── # Must be set to a real zone before exterior compliance can be evaluated. project = el_ops.set_exterior_lighting_zone_type_in_project( project, ExteriorLightingZoneTypeOptions.EXT_ZONE_NEIGHBORHOOD_BUS_DISTRICT ) +normalize_numeric_nulls(project) +project = client.update_project(project_id, project) +if not project: + raise ValueError("Project not found after update") print(f"Zone type set: {project.lighting.exteriorLightingZoneType}") # ── Step 2: Add an ExteriorUse with a fixture already populated ─────────────── fixture = get_default_fixture_template() fixture.description = "Parking LED" -fixture.lightingType = LightingTypeOptions.LED +fixture.fixtureType = LightingTypeOptions.LED fixture.fixtureWattage = 150.0 fixture.quantity = 8 @@ -61,6 +116,10 @@ ) project = el_ops.add_exterior_lighting_area_to_project(project, exterior_use) +normalize_numeric_nulls(project) +project = client.update_project(project_id, project) +if not project: + raise ValueError("Project not found after update") print(f"ExteriorUse added: {exterior_use.areaDescription!r}") # ── Step 3: List all exterior uses ──────────────────────────────────────────── @@ -73,6 +132,10 @@ "Main Parking Area", {"useQuantity": 6000.0}, ) +normalize_numeric_nulls(project) +project = client.update_project(project_id, project) +if not project: + raise ValueError("Project not found after update") print("ExteriorUse updated: useQuantity → 6000.0") # ── Step 5: Add a second fixture by updating the lighting space ─────────────── @@ -105,12 +168,20 @@ ) }, ) +normalize_numeric_nulls(project) +project = client.update_project(project_id, project) +if not project: + raise ValueError("Project not found after update") print("Second fixture added to Main Parking Area") # ── Step 6: Remove the ExteriorUse ─────────────────────────────────────────── project = el_ops.remove_exterior_lighting_area_from_project( project, "Main Parking Area" ) +normalize_numeric_nulls(project) +project = client.update_project(project_id, project) +if not project: + raise ValueError("Project not found after update") print("ExteriorUse removed: 'Main Parking Area'") keys = el_ops.get_exterior_lighting_area_keys_from_project(project) diff --git a/examples/project_operations/interior_lighting_operations.py b/examples/project_operations/interior_lighting_operations.py index 8950485..391a4b4 100644 --- a/examples/project_operations/interior_lighting_operations.py +++ b/examples/project_operations/interior_lighting_operations.py @@ -37,6 +37,7 @@ load_dotenv() client = COMcheckClient() client.set_api_key(os.getenv("COM_API_KEY") or "your-api-key-here") +# TODO: building area description should be unique within a project, interior lighting spcaes seems using the descriptions as key, instead of the key field. def normalize_numeric_nulls(model): From 227fa9de51aafdb1736e88aa5db7450d95cea3bf Mon Sep 17 00:00:00 2001 From: yanz571 Date: Fri, 24 Jul 2026 14:51:34 -0700 Subject: [PATCH 06/23] remove doc --- examples/project_operations/LIGHTING_TODOS.md | 93 ------------------- 1 file changed, 93 deletions(-) delete mode 100644 examples/project_operations/LIGHTING_TODOS.md diff --git a/examples/project_operations/LIGHTING_TODOS.md b/examples/project_operations/LIGHTING_TODOS.md deleted file mode 100644 index 00ac453..0000000 --- a/examples/project_operations/LIGHTING_TODOS.md +++ /dev/null @@ -1,93 +0,0 @@ -# Lighting Operations — Schema & API TODOs - -Open issues discovered while building the interior and exterior lighting -examples (`interior_lighting_operations.py`, `exterior_lighting_operations.py`). -Most are server-side schema mismatches that force workarounds in the example -code; the goal is to fix them upstream so the workarounds can be removed. - -## Schema issues - -- [ ] **Numeric fields typed non-nullable but returned/rejected as null.** - The API declares many numeric fields (`number`/`integer`) as non-nullable, - yet it returns `null` for them on read and then rejects those same nulls on - write. - - *Workaround:* `normalize_numeric_nulls()` sweeps the project and defaults - every null numeric field to `0` before each `update_project`. - - *Fix:* make these fields nullable in the schema (or have the API stop - returning null for non-nullable fields). - - The fields below are the optional, purely-numeric fields that - `normalize_numeric_nulls()` will coerce (any of them can trigger the - `"is not of a type(s) number/integer"` rejection). Grouped by model; those - reachable from each example are noted. - - **Interior lighting path** (`lighting.wholeBldgUse[]`): - - `WholeBldgUse`: `allowedWattage`, `ceilingHeight`, `floorArea`, - `internalLoad`, `powerDensity`, `proposedWattage` - - `ActivityUse`: `allowedWattage`, `ceilingHeight`, `floorArea`, - `internalLoad`, `powerDensity`, `proposedWattage`, - `roomCavityRatioThreshold` - - `InteriorLightingSpace`: `allowanceFloorArea`, `decorativeArea`, - `numFixturesAlteredOrAdded`, `postAltTotalWattage`, `preAltNumberFixtures`, - `preAltTotalWattage`, `primaryDaylight`, `rcrFloorToWorkplaneHeight`, - `rcrPerimeter`, `rcrWorkplaneToLuminaireHeight`, `roofMonitorToplight`, - `secondaryDaylight`, `skylightToplight` - - **Exterior lighting path** (`lighting.exteriorUse[]`): - - `ExteriorUse`: `powerDensity`, `useQuantity` - - `ExteriorLightingSpace`: `numFixturesAlteredOrAdded`, `postAltTotalWattage`, - `preAltNumberFixtures`, `preAltTotalWattage` - - **Fixtures** (nested under both, in `*.fixture[]`): - - `Fixture`: `advControlsAllowanceAperture`, `allowanceFloorArea`, - `fixtureWattage`, `numberOfLamps`, `powerAllowance`, - `quantityWithAdvControls`, `trackCircuitBreakerAmps`, - `trackCircuitBreakerVolts`, `trackCurrentLimiterWattage`, `trackLength`, - `trackTotalLuminaireWattage`, `trackTransformerWattage` - - **Fixture schedule** (`lighting.fixtureSchedule[]`): - - `FixtureSchedule`: `trackCircuitBreakerAmps`, `trackCircuitBreakerVolts`, - `trackCurrentLimiterWattage`, `trackLength`, `trackTotalLuminaireWattage`, - `trackTransformerWattage` - - ⚠️ `FixtureSchedule.id` and `FixtureSchedule.lightingId` are typed - `int | None` (no `str` in the union), so `normalize_numeric_nulls()` will - **also coerce these identifier fields to `0`** — almost certainly wrong. - This is a hazard of the blanket sweep: purely-`int` id fields are - indistinguishable from measurements by the annotation alone. - - *Fix:* either exclude known id fields by name in the helper, or make id - fields `str | int | None` in the schema so the sweep skips them (as it - does for other `id` fields). - - > Note: `normalize_numeric_nulls()` sweeps the whole `ComBuilding` tree, so it - > also touches numeric fields outside lighting (e.g. envelope, HVAC). The list - > above covers only the lighting models the two examples exercise. - -- [ ] **`fixture.fixtureType` vs `fixture.lightingType` requiredness.** - In the current schema `lightingType` is required and `fixtureType` is - optional, but `fixtureType` should be the required field and `lightingType` - should be optional (it is marked for deprecation). - - *Fix:* swap requiredness — make `fixtureType` required, `lightingType` - optional. - -- [ ] **Building-area / interior-lighting-space keying.** - A building area's `areaDescription` should be unique within a project. - Interior lighting spaces appear to key off `areaDescription` rather than the - dedicated `key` field. - - *Fix:* clarify/enforce which field is the identifier and ensure - uniqueness constraints match. - -## Open questions - -- [ ] **`activityType` options — code-dependent or generic?** - Confirm whether the valid `ActivityUse.activityType` options depend on the - project's energy code, or are a single generic set. - -## Source locations - -| Item | File | Line (approx.) | -|---|---|---| -| Numeric null workaround | `interior_lighting_operations.py` | `normalize_numeric_nulls` docstring | -| Numeric null workaround | `exterior_lighting_operations.py` | `normalize_numeric_nulls` docstring | -| `fixtureType` requiredness | `interior_lighting_operations.py` | Step 2 (fixture setup) | -| Building-area keying | `interior_lighting_operations.py` | after client setup | -| `activityType` options | `interior_lighting_operations.py` | Step 2 (activity use setup) | From 7c3bd6dfaaebd2a9d167dddf83e183cb7631c40c Mon Sep 17 00:00:00 2001 From: yanz571 Date: Tue, 28 Jul 2026 13:36:46 -0700 Subject: [PATCH 07/23] a correct comment of fixtureType --- examples/project_operations/interior_lighting_operations.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/project_operations/interior_lighting_operations.py b/examples/project_operations/interior_lighting_operations.py index 391a4b4..8385292 100644 --- a/examples/project_operations/interior_lighting_operations.py +++ b/examples/project_operations/interior_lighting_operations.py @@ -104,6 +104,7 @@ def normalize_numeric_nulls(model): fixture = get_default_fixture_template() fixture.description = "Recessed LED" # Todo: update schema fixtureType is required, lightingType is optional. +# fixtureType is the identifier, lightingType is the type fixture.fixtureType = LightingTypeOptions.LED fixture.fixtureWattage = 20.0 fixture.quantity = 10 From d56653b8eaddccafa5d84f8f4989b6d240144c23 Mon Sep 17 00:00:00 2001 From: Julian Slane Date: Thu, 6 Aug 2026 10:39:35 -0700 Subject: [PATCH 08/23] WIP: update schema --- .gitignore | 3 + comcheck_api/constants/envelope_constants.py | 14 +- comcheck_api/schemas/comCheck.schema.json | 3234 +++++++++++++----- comcheck_api/types/core_types.py | 1911 +++++++---- compare_buildings.py | 560 +++ diff_ignore.txt | 14 + pyproject.toml | 3 +- schema_changes_notes.md | 58 + schema_ignore.txt | 24 + tools/generate_core_types.py | 68 +- uv.lock | 30 +- 11 files changed, 4362 insertions(+), 1557 deletions(-) create mode 100644 compare_buildings.py create mode 100644 diff_ignore.txt create mode 100644 schema_changes_notes.md create mode 100644 schema_ignore.txt diff --git a/.gitignore b/.gitignore index 1f38fac..c8e0141 100644 --- a/.gitignore +++ b/.gitignore @@ -62,3 +62,6 @@ site/ # Build artifacts dist/ reports/ + +# Temporary building files +buildings/* \ No newline at end of file diff --git a/comcheck_api/constants/envelope_constants.py b/comcheck_api/constants/envelope_constants.py index a033525..bc42250 100644 --- a/comcheck_api/constants/envelope_constants.py +++ b/comcheck_api/constants/envelope_constants.py @@ -50,9 +50,8 @@ DEFAULT_DOOR: Door = Door.model_validate( { - "adjacentSpaceBuildingType": None, "adjacentSpaceType": None, - "allowanceType": None, + "allowanceType": "ENV_ALLOWANCE_NONE", "altExemptType": None, "assemblyType": "Door:Default Door", "bldgUseKey": str(uuid4()), @@ -86,7 +85,7 @@ DEFAULT_SKYLIGHT: Skylight = Skylight.model_validate( { "adjacentSpaceType": None, - "allowanceType": None, + "allowanceType": "ENV_ALLOWANCE_NONE", "assemblyType": "Skylight:Default Skylight", "bldgUseKey": "", "curbType": "NO_CURB_SKYLIGHT", @@ -113,7 +112,7 @@ DEFAULT_ROOF: Roof = Roof.model_validate( { "adjacentSpaceType": None, - "allowanceType": None, + "allowanceType": "ENV_ALLOWANCE_NONE", "assemblyType": "Roof:Default Roof", "bldgUseKey": str(uuid4()), "cavityRValue": 0, @@ -137,7 +136,7 @@ DEFAULT_FLOOR: Floor = Floor.model_validate( { "adjacentSpaceType": None, - "allowanceType": None, + "allowanceType": "ENV_ALLOWANCE_NONE", "altExemptType": None, "assemblyType": "Floor:Default Floor", "bldgUseKey": str(uuid4()), @@ -163,7 +162,7 @@ "adjacentSpaceType": None, "agWallConstructionDetailsType": "NONE", "agWallExteriorFinishDetailsType": None, - "allowanceType": None, + "allowanceType": "ENV_ALLOWANCE_NONE", "assemblyType": "Ext Wall:Default Exterior Wall", "bldgUseKey": str(uuid4()), "cavityRValue": 20, @@ -206,9 +205,8 @@ DEFAULT_BG_WALL: BgWall = BgWall.model_validate( { - "adjacentSpaceBuildingType": None, "adjacentSpaceType": None, - "allowanceType": None, + "allowanceType": "ENV_ALLOWANCE_NONE", "altExemptType": None, "assemblyType": "Basement:Default Basement", "bldgUseKey": "", diff --git a/comcheck_api/schemas/comCheck.schema.json b/comcheck_api/schemas/comCheck.schema.json index 2099f4a..72cd31c 100644 --- a/comcheck_api/schemas/comCheck.schema.json +++ b/comcheck_api/schemas/comCheck.schema.json @@ -8,7 +8,10 @@ "properties": { "id": { "description": "Scope-unique reference identifier for instances of this data group.", - "type": ["string", "integer"], + "type": [ + "string", + "integer" + ], "$comment": "All id data elements are shown as required so that every data group can be explcitly identified." }, "control": { @@ -29,35 +32,43 @@ }, "semiheated": { "description": "A boolean to indicate whether the project is a semi-heated type", - "type": ["boolean", "null"] + "type": [ + "boolean", + "null" + ] }, "isNonresidentialConditioning": { "description": "Flag to indicate the building has conditional type of non-resindetial conditioning.", - "type": "boolean", + "type": ["boolean", "null"], "default": false, "$comment": "true: non-residential conditioning, false: not non-residential conditioning" }, "isResidentialConditioning": { "description": "Flag to indicate the building has conditional type of resindetial conditioning.", - "type": "boolean", + "type": ["boolean", "null"], "default": false, "$comment": "true: residential conditioning, false: not residential conditioning" }, "isSemiheatedConditioning": { "description": "Flag to indicate the building has conditional type of semiheated conditioning.", - "type": "boolean", + "type": ["boolean", "null"], "default": false, "$comment": "true: semiheated conditioning, false: not semiheated conditioning." }, "isHistoricBuilding": { "description": "Flag to indicate if the building is historic.", - "type": "boolean", - "default": false + "type": "integer", + "enum": [ + 0, + 1 + ] }, "performanceRating": { "description": "Appendix C compliance index", - "type": ["number", "null"], - "minimum": 0.0, + "type": [ + "number", + "null" + ], "default": null, "effective_energy_codes": [ "CEZ_90_1_2013", @@ -69,11 +80,17 @@ }, "energyCreditPerformanceRating": { "description": "Appendix C compliance index for energy credit calculation", - "type": ["number", "null"], - "minimum": 0.0, + "type": [ + "number", + "null" + ], "default": null, "$comment": "Applicable to 90.1 2022 for envelope performance energy credit calculation.", - "effective_energy_codes": ["CEZ_IECC2024", "CEZ_90_1_2022", "CEZ_MN"] + "effective_energy_codes": [ + "CEZ_IECC2024", + "CEZ_90_1_2022", + "CEZ_MN" + ] }, "lighting": { "description": "Lighting", @@ -87,6 +104,11 @@ "description": "Renewable systems", "$ref": "comCheck.schema.json#/definitions/Renewable" }, + "bldgUseType": { + "description": "Building Use Type", + "$ref": "comCheck.schema.json#/definitions/BuildingUseTypeOptions", + "$comment": "Legacy enum, only ACTIVITY is valid in the new ComCheck Web" + }, "buildingUseType": { "description": "Building Use Type", "$ref": "comCheck.schema.json#/definitions/BuildingUseTypeOptions", @@ -115,35 +137,58 @@ }, "constructionType": { "description": "Deprecated field, default to 'None'.", - "type": "string" + "type": [ + "string", + "null" + ] }, "allElectric": { "description": "Advanced reporting indicates whether the project is all electric", - "type": ["boolean", "integer", "null"], + "type": [ + "boolean", + "integer", + "null" + ], "default": null, "$comment": "true: all electric, false: otherwise, use integer 0 is false, 1 or above is true" }, "isRenewable": { "description": "Advanced reporting indicates whether the project has on-site renewable system", - "type": ["boolean", "integer", "null"], + "type": [ + "boolean", + "integer", + "null" + ], "default": null, "$comment": "true: has on-site renewable system, false: otherwise, use integer 0 is false, 1 or above is true" }, "hasBattery": { "description": "Advanced reporting indicates whether the project has installed on-site battery", - "type": ["boolean", "integer", "null"], + "type": [ + "boolean", + "integer", + "null" + ], "default": null, "$comment": "true: has onsite battery, false: otherwise, use integer 0 is false, 1 or above is true" }, "hasCharger": { "description": "Advanced reporting indicates whether the project has on-site generator", - "type": ["boolean", "integer", "null"], + "type": [ + "boolean", + "integer", + "null" + ], "default": null, "$comment": "true: has onsite generator, false: otherwise, use integer 0 is false, 1 or above is true" }, "hasHeatPump": { "description": "Advanced reporting indicates whether the building conditioned by heat pumps", - "type": ["boolean", "integer", "null"], + "type": [ + "boolean", + "integer", + "null" + ], "default": null, "$comment": "true: has heat pump, false: otherwise, use integer 0 is false, 1 or above is true" }, @@ -157,26 +202,40 @@ "$ref": "comCheck.schema.json#/definitions/ProjectSubTypeOptions", "default": "CONSTRUCTION_COMPLETE", "$comment": "The availability of sub-type is based on the project type selected.", - "effective_energy_codes": ["CEZ_90_1_2022", "CEZ_IECC2024"] + "effective_energy_codes": [ + "CEZ_90_1_2022", + "CEZ_IECC2024" + ] }, "projectMechanicalType": { "description": "Project mechanical system type", "$ref": "comCheck.schema.json#/definitions/ProjectMechanicalTypeOptions", "default": "PROJECT_HVAC_WITH_CENTRAL", "$comment": "The availability of sub-type is based on the project sub-type selected.", - "effective_energy_codes": ["CEZ_90_1_2022", "CEZ_IECC2024"] + "effective_energy_codes": [ + "CEZ_90_1_2022", + "CEZ_IECC2024" + ] }, "projectCoreAndShellCredit": { "description": "Project credits gained from core and shell", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0, "default": null, "$comment": "Only needed when $.projectSubType == CONSTRUCTION_CORE_AND_SHELL", - "effective_energy_codes": ["CEZ_90_1_2022"] + "effective_energy_codes": [ + "CEZ_90_1_2022" + ] }, "feetBldgHeight": { "description": "Building feet height", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0, "default": null }, @@ -192,6 +251,29 @@ "$ref": "comCheck.schema.json#/definitions/Requirements" }, "minItems": 0 + }, + "efficiencyPackageType": { + "description": "Efficiency Package Type", + "enum": [ + "EFF_PACKAGE_UNKNOWN", + "EFF_PACKAGE_HVAC_PERFORMANCE", + "EFF_PACKAGE_LIGHTING_REDUCED_LPD", + "EFF_PACKAGE_REDUCED_AIR_INFILTRATION", + "EFF_PACKAGE_ENHANCED_ENVELOPE_PERFORMANCE", + "EFF_PACKAGE_ENHANCED_LIGHTING_CONTROLS", + "EFF_PACKAGE_ONSITE_RENEWABLES", + null + ], + "default": null + }, + "energyCreditMultiplierException": { + "description": "Energy Credit Multiplier Exception", + "enum": [ + "NO_ENERGY_CREDIT_MULTIPLIER_EXCEPTION", + "ENERGY_CREDIT_MULTIPLIER_EXCEPTION_LOW_ENERGY_BUILDINGS", + "ENERGY_CREDIT_MULTIPLIER_EXCEPTION_PRIMARY_HEAT_PUMP", + null + ] } }, "required": [ @@ -213,7 +295,10 @@ "properties": { "id": { "description": "Scope-unique reference identifier for instances of this data group.", - "type": ["string", "integer"], + "type": [ + "string", + "integer" + ], "$comment": "All id data elements are shown as required so that every data group can be explcitly identified." }, "category": { @@ -233,12 +318,18 @@ }, "locationOnPlans": { "description": "Requirement Answer - Location On Plans", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": "" }, "exceptionName": { "description": "Requirement Answer - Exception Name", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": "" } }, @@ -256,19 +347,29 @@ "properties": { "id": { "description": "Scope-unique reference identifier for instances of this data group.", - "type": ["string", "integer"], + "type": [ + "string", + "integer" + ], "$comment": "All id data elements are shown as required so that every data group can be explcitly identified." }, "version": { "description": "Software version - shall always set to an empty string", - "type": "string", + "type": [ + "string", + "null" + ], "default": "" }, "code": { "description": "Energy code types", "anyOf": [ - { "$ref": "comCheck.schema.json#/definitions/EnergyCodeOptions" }, - { "$ref": "comCheck.schema.json#/definitions/StateRegionEnergyCodeOptions" } + { + "$ref": "comCheck.schema.json#/definitions/EnergyCodeOptions" + }, + { + "$ref": "comCheck.schema.json#/definitions/StateRegionEnergyCodeOptions" + } ] }, "complianceMode": { @@ -278,7 +379,11 @@ "$comment": "For COMcheck API call, this value should always be UA." } }, - "required": ["version", "code", "complianceMode"], + "required": [ + "version", + "code", + "complianceMode" + ], "additionalProperties": false }, "Project": { @@ -286,167 +391,265 @@ "properties": { "id": { "description": "Scope-unique reference identifier for instances of this data group.", - "type": ["string", "integer"], + "type": [ + "string", + "integer" + ], "$comment": "All id data elements are shown as required so that every data group can be explcitly identified." }, "projectTitle": { "description": "Project title", - "type": ["string"], + "type": [ + "string" + ], "default": "New Project" }, "projectPermitNumber": { "description": "Project permit ID", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": null }, "projectPermitDate": { "description": "Project permit submission date", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": null }, "projectTaxMap": { "description": "Project tax map", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": null }, "projectLotNumber": { "description": "Project lot number", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": null }, "projectAddress": { "description": "Project address - street", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": null }, "projectAddress2": { "description": "Project address - street number", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": null }, "projectCity": { "description": "City", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": null }, "projectState": { "description": "State", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": null }, "projectZipCode": { "description": "Zip code", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": null }, "projectComplete": { "description": "Project completion status", - "type": ["boolean", "null"], + "type": [ + "boolean", + "null" + ], "default": null }, "ownerFirstName": { "description": "Project owner first name", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": null }, "ownerLastName": { "description": "Project owner last name", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": null }, "ownerCompany": { "description": "Project owner's company", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": null }, "ownerAddress": { "description": "Project owner's address - street", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": null }, "ownerAddress2": { "description": "Project owner's address - street number", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": null }, "ownerCity": { "description": "City", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": null }, "ownerState": { "description": "State", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": null }, "ownerZipCode": { "description": "Zip code", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": null }, "ownerPhone": { "description": "Phone number", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": null }, "ownerEmail": { "description": "Email address", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": null }, "developerFirstName": { "description": "Project developer first name", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": null }, "developerLastName": { "description": "Project developer last name", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": null }, "developerCompany": { "description": "Project developer's company", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": null }, "developerAddress": { "description": "Project developer's address - street", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": null }, "developerAddress2": { "description": "Project developer's address - street number", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": null }, "developerCity": { "description": "City", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": null }, "developerState": { "description": "State", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": null }, "developerZipCode": { "description": "Zip code", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": null }, "developerPhone": { "description": "Phone number", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": null }, "developerEmail": { "description": "Email address", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": null }, "notes": { "description": "$comment", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": null } }, @@ -458,7 +661,10 @@ "properties": { "id": { "description": "Scope-unique reference identifier for instances of this data group.", - "type": ["string", "integer"], + "type": [ + "string", + "integer" + ], "$comment": "All id data elements are shown as required so that every data group can be explcitly identified." }, "agWall": { @@ -523,7 +729,7 @@ "useOrientationDetails": { "description": "use orientation details for calculation", "type": "boolean", - "default": true, + "const": true, "$comment": "This field shall always set to true" }, "useVltDetails": { @@ -545,7 +751,10 @@ }, "postAltWindowWallPct": { "description": "post alteration window to wall ratio in percentage", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "default": null, "minimum": 0.0, "maximum": 100.0, @@ -553,7 +762,10 @@ }, "postAltSkylightRoofPct": { "description": "post alteration skylight to roof ratio in percentage", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "default": null, "minimum": 0.0, "maximum": 100.0, @@ -561,7 +773,10 @@ }, "altPctGlazingAreaReplaced": { "description": "Alteration percentage of glazing area", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "default": null, "minimum": 0.0, "maximum": 100.0, @@ -569,7 +784,10 @@ }, "altPctSkylightAreaReplaced": { "description": "Alteration percentage of skylight area", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "default": null, "minimum": 0.0, "maximum": 100.0, @@ -600,7 +818,15 @@ "$comment": "Applicable when $.projectType == ALTERATION" } }, - "required": ["agWall", "bgWall", "roof", "floor", "door", "window", "skylight"], + "required": [ + "agWall", + "bgWall", + "roof", + "floor", + "door", + "window", + "skylight" + ], "additionalProperties": false }, "Location": { @@ -608,7 +834,10 @@ "properties": { "id": { "description": "Scope-unique reference identifier for instances of this data group.", - "type": ["string", "integer"], + "type": [ + "string", + "integer" + ], "$comment": "All id data elements are shown as required so that every data group can be explcitly identified." }, "state": { @@ -617,18 +846,27 @@ }, "epwFilename": { "description": "file name", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": null, "deprecated": true }, "county": { "description": "county name, spell the full name", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": null }, "city": { "description": "city, spell the full name", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "climateZone": { "description": "Climate zone number", @@ -637,7 +875,11 @@ "maximum": 8 } }, - "required": ["state", "city", "climateZone"], + "required": [ + "state", + "city", + "climateZone" + ], "additionalProperties": false }, "AgWall": { @@ -645,12 +887,18 @@ "properties": { "id": { "description": "Scope-unique reference identifier for instances of this data group.", - "type": ["string", "integer"], + "type": [ + "string", + "integer" + ], "$comment": "All id data elements are shown as required so that every data group can be explcitly identified." }, "description": { "description": "The name of the component", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": "", "$comment": "This needs to be unique across all ag walls." }, @@ -662,7 +910,10 @@ }, "bldgUseKey": { "description": "key reference of the building use area data group", - "type": "string", + "type": [ + "string", + "null" + ], "$comment": "String of values referencing :WholeBldgUse:" }, "wallType": { @@ -677,46 +928,63 @@ "agWallExteriorFinishDetailsType": { "description": "Above grade wall exterior finish details", "anyOf": [ - { "type": "null" }, + { + "type": "null" + }, { "$ref": "comCheck.schema.json#/definitions/AgWallExteriorFinishDetailsTypeOptions" } ], "default": null, - "effective_energy_codes": ["CEZ_MAS_STRETCH_IECC2021"] + "effective_energy_codes": [ + "CEZ_MAS_STRETCH_IECC2021" + ] }, "nextToUncondSpace": { "description": "Flag indicates whether the space is next to an unconditioned space", - "type": ["boolean", "null"], + "type": [ + "boolean", + "null" + ], "default": null }, "insulationPosition": { "description": "Not sure why basement has this data, likely deprecated. Use null", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": null }, "otherWallType": { "description": "other wall types", - "$ref": "comCheck.schema.json#/definitions/AgWallOtherTypeOptions", + "anyOf": [ + { + "type": "null" + }, + { + "$ref": "comCheck.schema.json#/definitions/AgWallOtherTypeOptions" + } + ], "default": "NONE", "$comment": "This data should be NONE unless the $.wallType == OTHER_AG_WALL" }, "adjacentSpaceType": { "description": "Space type of the adjacent space", "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/AdjacentSpaceTypeOptions" } + { + "type": "null" + }, + { + "$ref": "comCheck.schema.json#/definitions/AdjacentSpaceTypeOptions" + } ], "default": null, "$comment": "This data is used to identify the required maximum U factor in 90.1 energy codes. Be sure to set a not null value in 90.1 energy code" }, "adjacentSpaceBuildingType": { "description": "Building type of the adjacent space", - "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/WholeBuildingTypeOptions" } - ], - "default": null, + "$ref": "comCheck.schema.json#/definitions/WholeBuildingTypeOptions", "$comment": "This data is used for reference - no actual impact on calculations." }, "thermalBridge": { @@ -735,17 +1003,25 @@ "thermalBridgeExceptionType": { "description": "Type of thermal bridge exceptions", "anyOf": [ - { "type": "null" }, + { + "type": "null" + }, { "$ref": "comCheck.schema.json#/definitions/ThermalBridgeExceptionTypeOptions" } ], "default": null, - "effective_energy_codes": ["CEZ_90_1_2022", "CEZ_IECC2024"] + "effective_energy_codes": [ + "CEZ_90_1_2022", + "CEZ_IECC2024" + ] }, "effectiveUFactor": { "description": "The effective U factor after thermal bridge adjustment", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "default": null, "$comment": "This value is calculated from engine - modify this number does not impact compliance calculation", "effective_energy_codes": [ @@ -756,7 +1032,10 @@ }, "thermalBridgeAdjustmentFactor": { "description": "Thermal bridge adjustment factor", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "default": null, "$comment": "This value is calculated from engine - modify this number does not impact compliance calculation", "effective_energy_codes": [ @@ -767,20 +1046,18 @@ }, "allowanceType": { "description": "allowance type", - "anyOf": [ - { "type": "null" }, - { - "$ref": "comCheck.schema.json#/definitions/EnvelopeAssemblyAllowanceTypeOptions" - } - ], - "default": null, + "$ref": "comCheck.schema.json#/definitions/EnvelopeAssemblyAllowanceTypeOptions", "$comment": "TODO - need to fill in the background?" }, "cmuType": { "description": "CMU type", "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/CMUTypeOptions" } + { + "type": "null" + }, + { + "$ref": "comCheck.schema.json#/definitions/CMUTypeOptions" + } ], "default": null, "$comment": "Only used when $.wallType == MASONRY_AG_WALL || $.wallType == CONCRETE_AG_WALL" @@ -800,15 +1077,21 @@ "constructionType": { "description": "Construction types - compliance code specification", "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/ConstructionTypeOptions" } + { + "type": "null" + }, + { + "$ref": "comCheck.schema.json#/definitions/ConstructionTypeOptions" + } ], "$comment": "TODO - verify if this is still needed?" }, "exemptionType": { "descriptions": "exemption type", "anyOf": [ - { "type": "null" }, + { + "type": "null" + }, { "$ref": "comCheck.schema.json#/definitions/EnvelopeAssemblyExemptionOptions" } @@ -819,15 +1102,22 @@ "furringType": { "descriptions": "Type of furring installation, used for mass surfaces", "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/FurringTypeOptions" } + { + "type": "null" + }, + { + "$ref": "comCheck.schema.json#/definitions/FurringTypeOptions" + } ], "default": null, "$comment": "Only used when $.wallType == MASONRY_AG_WALL || $.wallType == CONCRETE_AG_WALL" }, "heatCapacity": { "description": "heat capacity of a mass wall. Used in other mass wall type", - "type": "number", + "type": [ + "number", + "null" + ], "unit": "Btu/ft2*F", "minimum": 0.0, "default": 0.0, @@ -856,31 +1146,41 @@ }, "cavityRValue": { "description": "Average insulation R-value in the cavity between two studs.", - "type": "number", + "type": [ + "number", + "null" + ], "unit": "h-ft2-F/Btu", - "minimum": 0.0, "default": 0.0 }, "continuousRValue": { "description": "Continuous insulation on the above grade wall. Can be exterior or interior or both.", - "type": "number", + "type": [ + "number", + "null" + ], "unit": "h-ft2-F/Btu", - "minimum": 0.0, "default": 0.0 }, "continuousDeratedRValue": { "description": "Continuous R value derated factor for thermal bridge effect", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0, - "default": 0.0, - "effective_energy_codes": ["CEZ_MAS_STRETCH_IECC2021"], + "effective_energy_codes": [ + "CEZ_MAS_STRETCH_IECC2021" + ], "$comment": "This factor is calculaed in the engine based on user inputs and is used for calculating the continuesR value in MAS code." }, "propUValue": { "description": "Proposed thermal transmittance of the above grade wall.", - "type": "number", + "type": [ + "number", + "null" + ], "unit": "Btu/h-ft2-F", - "minimum": 0.0, "default": 0.0 }, "altExemptType": { @@ -897,9 +1197,11 @@ }, "grossArea": { "description": "gross area", - "type": "number", + "type": [ + "number", + "null" + ], "unit": "ft2", - "minimum": 0.0, "default": 0.0 } }, @@ -936,12 +1238,18 @@ "properties": { "id": { "description": "Scope-unique reference identifier for instances of this data group.", - "type": ["string", "integer"], + "type": [ + "string", + "integer" + ], "$comment": "All id data elements are shown as required so that every data group can be explcitly identified." }, "description": { "description": "The name of the component", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": "" }, "assemblyType": { @@ -952,7 +1260,10 @@ }, "bldgUseKey": { "description": "key reference of the building use area data group", - "type": "string", + "type": [ + "string", + "null" + ], "$comment": "String of values referencing :WholeBldgUse:" }, "wallType": { @@ -977,37 +1288,35 @@ "adjacentSpaceType": { "description": "Space type of the adjacent space", "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/AdjacentSpaceTypeOptions" } + { + "type": "null" + }, + { + "$ref": "comCheck.schema.json#/definitions/AdjacentSpaceTypeOptions" + } ], "default": null, "$comment": "This data is used to identify the required maximum U factor in 90.1 energy codes. Be sure to set a not null value in 90.1 energy code" }, "adjacentSpaceBuildingType": { "description": "Building type of the adjacent space", - "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/WholeBuildingTypeOptions" } - ], - "default": null, + "$ref": "comCheck.schema.json#/definitions/WholeBuildingTypeOptions", "$comment": "This data is used for reference - no actual impact on calculations." }, "allowanceType": { "description": "allowance type", - "anyOf": [ - { "type": "null" }, - { - "$ref": "comCheck.schema.json#/definitions/EnvelopeAssemblyAllowanceTypeOptions" - } - ], - "default": null, + "$ref": "comCheck.schema.json#/definitions/EnvelopeAssemblyAllowanceTypeOptions", "$comment": "TODO - need to fill in the background?" }, "cmuType": { "description": "CMU type", "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/CMUTypeOptions" } + { + "type": "null" + }, + { + "$ref": "comCheck.schema.json#/definitions/CMUTypeOptions" + } ], "default": null }, @@ -1026,17 +1335,23 @@ "constructionType": { "description": "Construction types - compliance code specification", "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/ConstructionTypeOptions" } + { + "type": "null" + }, + { + "$ref": "comCheck.schema.json#/definitions/ConstructionTypeOptions" + } ], "$comment": "TODO - verify if this is still needed?" }, "exemptionType": { "descriptions": "exemption type", "anyOf": [ - { "type": "null" }, { - "$ref": "comCheck.schema.json#/definitions/EnvelopeAssemblyExemptionOptions" + "type": "null" + }, + { + "$ref": "comCheck.schema.json#/definitions/EnvelopeAssemblyExemptionOptions" } ], "default": null, @@ -1045,15 +1360,22 @@ "furringType": { "descriptions": "Type of furring installation, used for mass surfaces", "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/FurringTypeOptions" } + { + "type": "null" + }, + { + "$ref": "comCheck.schema.json#/definitions/FurringTypeOptions" + } ], "default": null, "$comment": "Only used when $.wallType == MASONRY_AG_WALL || $.wallType == CONCRETE_AG_WALL" }, "heatCapacity": { "description": "heat capacity of a mass wall. Used in other mass wall type", - "type": "number", + "type": [ + "number", + "null" + ], "unit": "Btu/ft2*F", "minimum": 0.0, "default": 0.0, @@ -1066,7 +1388,10 @@ }, "insulationPosition": { "description": "Not sure why basement has this data, likely deprecated. Use null", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": null }, "window": { @@ -1087,23 +1412,29 @@ }, "cavityRValue": { "description": "Average insulation R-value in the cavity between two studs.", - "type": "number", + "type": [ + "number", + "null" + ], "unit": "h-ft2-F/Btu", - "minimum": 0.0, "default": 0.0 }, "continuousRValue": { "description": "Continuous insulation on the below grade wall. Can be exterior or interior or both.", - "type": "number", + "type": [ + "number", + "null" + ], "unit": "h-ft2-F/Btu", - "minimum": 0.0, "default": 0.0 }, "propUValue": { "description": "Proposed thermal transmittance of the below grade wall.", - "type": "number", + "type": [ + "number", + "null" + ], "unit": "Btu/h-ft2-F", - "minimum": 0.0, "default": 0.0 }, "altExemptType": { @@ -1155,35 +1486,44 @@ "properties": { "id": { "description": "Scope-unique reference identifier for instances of this data group.", - "type": ["string", "integer"], + "type": [ + "string", + "integer" + ], "$comment": "All id data elements are shown as required so that every data group can be explcitly identified." }, "bldgUseKey": { "description": "key reference of the building use area data group", - "type": "string", + "type": [ + "string", + "null" + ], "$comment": "String of values referencing :WholeBldgUse:" }, "description": { "description": "The name of the component", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": "" }, "adjacentSpaceType": { "description": "Space type of the adjacent space", "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/AdjacentSpaceTypeOptions" } + { + "type": "null" + }, + { + "$ref": "comCheck.schema.json#/definitions/AdjacentSpaceTypeOptions" + } ], "default": null, "$comment": "This data is used to identify the required maximum U factor in 90.1 energy codes. Be sure to set a not null value in 90.1 energy code" }, "adjacentSpaceBuildingType": { "description": "Building type of the adjacent space", - "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/WholeBuildingTypeOptions" } - ], - "default": null, + "$ref": "comCheck.schema.json#/definitions/WholeBuildingTypeOptions", "$comment": "This data is used for reference - no actual impact on calculations." }, "assemblyType": { @@ -1194,14 +1534,19 @@ }, "propUValue": { "description": "Proposed thermal transmittance of the window.", - "type": "number", + "type": [ + "number", + "null" + ], "unit": "Btu/h-ft2-F", - "minimum": 0.0, "default": 0.0 }, "grossArea": { "description": "gross area", - "type": "number", + "type": [ + "number", + "null" + ], "unit": "ft2", "minimum": 0.0, "default": 0.0 @@ -1220,49 +1565,66 @@ }, "propShgc": { "description": "Proposed solar heat gain coefficient", - "type": "number", + "type": [ + "number", + "null" + ], "minimum": 0.0, "default": 0.0 }, "propProjectionFactor": { "description": "Proposed window projection factor", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0, "default": 0.0 }, "frameType": { "description": "Window frame type", - "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/FenestrationFrameTypeOptions" } - ], + "$ref": "comCheck.schema.json#/definitions/FenestrationFrameTypeOptions", "default": null }, "glazingType": { "description": "Glazing type - used when performance data option is energy code default (PERF_TYPE_DEFAULT)", "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/GlazingTypeOptions" } + { + "type": "null" + }, + { + "$ref": "comCheck.schema.json#/definitions/GlazingTypeOptions" + } ], "default": null }, "propVt": { "descriptions": "Proposed visible transmittance", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0, "default": null }, "preAltPropShgc": { "descriptions": "Pre-Alteration solar heat gain coefficient", - "type": "number", + "type": [ + "number", + "null" + ], "minimum": 0.0, "default": 0.0 }, "solarType": { "description": "Solar coating type - used when performance data option is energy code default (PERF_TYPE_DEFAULT)", "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/SolarTypeOptions" } + { + "type": "null" + }, + { + "$ref": "comCheck.schema.json#/definitions/SolarTypeOptions" + } ], "default": null }, @@ -1275,92 +1637,129 @@ "glazingMaterialType": { "description": "Glazing material type", "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/GlazingMaterialTypeOptions" } + { + "type": "null" + }, + { + "$ref": "comCheck.schema.json#/definitions/GlazingMaterialTypeOptions" + } ], "default": null }, "productType": { "description": "Product Type", "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/WindowProductionTypeOptions" } + { + "type": "null" + }, + { + "$ref": "comCheck.schema.json#/definitions/WindowProductionTypeOptions" + } ], "default": null }, "allowanceType": { "description": "allowance type", - "anyOf": [ - { "type": "null" }, - { - "$ref": "comCheck.schema.json#/definitions/EnvelopeAssemblyAllowanceTypeOptions" - } - ], - "default": null + "$ref": "comCheck.schema.json#/definitions/EnvelopeAssemblyAllowanceTypeOptions" }, "exemptionType": { "descriptions": "exemption type", "anyOf": [ - { "type": "null" }, + { + "type": "null" + }, { "$ref": "comCheck.schema.json#/definitions/EnvelopeAssemblyExemptionOptions" } ], "default": null }, + "feetAg": { + "description": "Feet above grade", + "type": [ + "number", + "null" + ], + "minimum": 0.0, + "default": null + }, "constructionType": { "description": "Construction types - compliance code specification", "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/ConstructionTypeOptions" } + { + "type": "null" + }, + { + "$ref": "comCheck.schema.json#/definitions/ConstructionTypeOptions" + } ], "$comment": "TODO - verify if this is still needed?" }, "isSiteShading": { "description": "Is the site shaded", - "type": ["boolean", "null"], + "type": [ + "boolean", + "null" + ], "default": null }, "perfDataType": { "description": "Performance data type option", "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/PerfDataTypeOptions" } + { + "type": "null" + }, + { + "$ref": "comCheck.schema.json#/definitions/PerfDataTypeOptions" + } ], "default": null }, "productId": { "description": "Product ID", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": null, "$comment": "Window product ID, used when the perfDataType is PERF_TYPE_NFRC" }, "preAltPropUval": { "descriptions": "Pre-Alteration U-factor", - "type": ["number", "null"], - "minimum": 0.0, - "default": 0.0 + "type": [ + "number", + "null" + ], + "minimum": 0.0 }, "windowOpenType": { "description": "Window open type", "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/WindowOpenTypeOptions" } + { + "type": "null" + }, + { + "$ref": "comCheck.schema.json#/definitions/WindowOpenTypeOptions" + } ], "default": null }, "cavityRValue": { "descriptions": "Average insulation R-value in the cavity between two studs.", - "type": "number", + "type": [ + "number", + "null" + ], "unit": "h-ft2-F/Btu", - "minimum": 0.0, "default": 0.0 }, "continuousRValue": { "description": "Continuous insulation on the door. Can be exterior or interior or both.", - "type": "number", + "type": [ + "number", + "null" + ], "unit": "h-ft2-F/Btu", - "minimum": 0.0, "default": 0.0 } }, @@ -1395,17 +1794,26 @@ "properties": { "id": { "description": "Scope-unique reference identifier for instances of this data group.", - "type": ["string", "integer"], + "type": [ + "string", + "integer" + ], "$comment": "All id data elements are shown as required so that every data group can be explcitly identified." }, "bldgUseKey": { "description": "key reference of the building use area data group", - "type": "string", + "type": [ + "string", + "null" + ], "$comment": "String of values referencing :WholeBldgUse:" }, "description": { "description": "The name of the component", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": "" }, "assemblyType": { @@ -1417,31 +1825,36 @@ "adjacentSpaceType": { "description": "Space type of the adjacent space", "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/AdjacentSpaceTypeOptions" } + { + "type": "null" + }, + { + "$ref": "comCheck.schema.json#/definitions/AdjacentSpaceTypeOptions" + } ], "default": null, "$comment": "This data is used to identify the required maximum U factor in 90.1 energy codes. Be sure to set a not null value in 90.1 energy code" }, "adjacentSpaceBuildingType": { "description": "Building type of the adjacent space", - "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/WholeBuildingTypeOptions" } - ], - "default": null, + "$ref": "comCheck.schema.json#/definitions/WholeBuildingTypeOptions", "$comment": "This data is used for reference - no actual impact on calculations." }, "propUValue": { "description": "Proposed thermal transmittance of the window.", - "type": "number", + "type": [ + "number", + "null" + ], "unit": "Btu/h-ft2-F", - "minimum": 0.0, "default": 0.0 }, "grossArea": { "description": "gross area", - "type": ["null", "number"], + "type": [ + "null", + "number" + ], "unit": "ft2", "minimum": 0.0, "default": null @@ -1459,49 +1872,66 @@ }, "propShgc": { "description": "Proposed solar heat gain coefficient", - "type": "number", + "type": [ + "number", + "null" + ], "minimum": 0.0, "default": 0.0 }, "propProjectionFactor": { "description": "Proposed window projection factor", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0, "default": 0.0 }, "frameType": { "description": "Glass door frame type", - "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/FenestrationFrameTypeOptions" } - ], + "$ref": "comCheck.schema.json#/definitions/FenestrationFrameTypeOptions", "default": null }, "glazingType": { "description": "Glazing type - used when performance data option is energy code default (PERF_TYPE_DEFAULT)", "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/GlazingTypeOptions" } + { + "type": "null" + }, + { + "$ref": "comCheck.schema.json#/definitions/GlazingTypeOptions" + } ], "default": null }, "propVt": { "descriptions": "Proposed visible transmittance", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0, "default": null }, "preAltPropShgc": { "descriptions": "Pre-Alteration solar heat gain coefficient", - "type": "number", + "type": [ + "number", + "null" + ], "minimum": 0.0, "default": 0.0 }, "solarType": { "description": "Solar coating type - used when performance data option is energy code default (PERF_TYPE_DEFAULT)", "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/SolarTypeOptions" } + { + "type": "null" + }, + { + "$ref": "comCheck.schema.json#/definitions/SolarTypeOptions" + } ], "default": null }, @@ -1514,33 +1944,37 @@ "glazingMaterialType": { "description": "Glazing material type", "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/GlazingMaterialTypeOptions" } + { + "type": "null" + }, + { + "$ref": "comCheck.schema.json#/definitions/GlazingMaterialTypeOptions" + } ], "default": null }, "productType": { "description": "Product Type", "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/WindowProductionTypeOptions" } + { + "type": "null" + }, + { + "$ref": "comCheck.schema.json#/definitions/WindowProductionTypeOptions" + } ], "default": null }, "allowanceType": { "description": "allowance type", - "anyOf": [ - { "type": "null" }, - { - "$ref": "comCheck.schema.json#/definitions/EnvelopeAssemblyAllowanceTypeOptions" - } - ], - "default": null + "$ref": "comCheck.schema.json#/definitions/EnvelopeAssemblyAllowanceTypeOptions" }, "exemptionType": { "descriptions": "exemption type", "anyOf": [ - { "type": "null" }, + { + "type": "null" + }, { "$ref": "comCheck.schema.json#/definitions/EnvelopeAssemblyExemptionOptions" } @@ -1550,72 +1984,104 @@ "constructionType": { "description": "Construction types - compliance code specification", "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/ConstructionTypeOptions" } + { + "type": "null" + }, + { + "$ref": "comCheck.schema.json#/definitions/ConstructionTypeOptions" + } ], "$comment": "TODO - verify if this is still needed?" }, "isSiteShading": { "description": "Is the site shaded", - "type": ["boolean", "null"], + "type": [ + "boolean", + "null" + ], "default": null }, "perfDataType": { "description": "Performance data type option", "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/PerfDataTypeOptions" } + { + "type": "null" + }, + { + "$ref": "comCheck.schema.json#/definitions/PerfDataTypeOptions" + } ], "default": null }, "productId": { "description": "Product ID", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": null, "$comment": "Window product ID, used when the perfDataType is PERF_TYPE_NFRC" }, "preAltPropUval": { "descriptions": "Pre-Alteration U-factor", - "type": ["number", "null"], - "minimum": 0.0, - "default": 0.0 + "type": [ + "number", + "null" + ], + "minimum": 0.0 }, "doorType": { "description": "Door types", "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/DoorTypeOptions" } + { + "type": "null" + }, + { + "$ref": "comCheck.schema.json#/definitions/DoorTypeOptions" + } ], "default": null }, "doorOpenType": { "description": "Door open types", "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/DoorOpenTypeOptions" } + { + "type": "null" + }, + { + "$ref": "comCheck.schema.json#/definitions/DoorOpenTypeOptions" + } ], "default": null }, "doorEntranceType": { "description": "Door entrance types", "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/DoorEntranceTypeOptions" } + { + "type": "null" + }, + { + "$ref": "comCheck.schema.json#/definitions/DoorEntranceTypeOptions" + } ], "default": null }, "cavityRValue": { "descriptions": "Average insulation R-value in the cavity between two studs.", - "type": "number", + "type": [ + "number", + "null" + ], "unit": "h-ft2-F/Btu", - "minimum": 0.0, "default": 0.0 }, "continuousRValue": { "description": "Continuous insulation on the door. Can be exterior or interior or both.", - "type": "number", + "type": [ + "number", + "null" + ], "unit": "h-ft2-F/Btu", - "minimum": 0.0, "default": 0.0 } }, @@ -1654,17 +2120,44 @@ "properties": { "id": { "description": "Scope-unique reference identifier for instances of this data group.", - "type": ["string", "integer"], + "type": [ + "string", + "integer" + ], "$comment": "All id data elements are shown as required so that every data group can be explcitly identified." }, "bldgUseKey": { "description": "key reference of the building use area data group", - "type": "string", + "type": [ + "string", + "null" + ], "$comment": "String of values referencing :WholeBldgUse:" }, + "cavityRValue": { + "description": "Average insulation R-value in the cavity between two studs.", + "type": [ + "number", + "null" + ], + "unit": "h-ft2-F/Btu", + "default": 0.0 + }, + "continuousRValue": { + "description": "Continuous insulation on the skylight. Can be exterior or interior or both.", + "type": [ + "number", + "null" + ], + "unit": "h-ft2-F/Btu", + "default": 0.0 + }, "description": { "description": "The name of the component", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": "" }, "assemblyType": { @@ -1676,31 +2169,36 @@ "adjacentSpaceType": { "description": "Space type of the adjacent space", "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/AdjacentSpaceTypeOptions" } + { + "type": "null" + }, + { + "$ref": "comCheck.schema.json#/definitions/AdjacentSpaceTypeOptions" + } ], "default": null, "$comment": "This data is used to identify the required maximum U factor in 90.1 energy codes. Be sure to set a not null value in 90.1 energy code" }, "adjacentSpaceBuildingType": { "description": "Building type of the adjacent space", - "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/WholeBuildingTypeOptions" } - ], - "default": null, + "$ref": "comCheck.schema.json#/definitions/WholeBuildingTypeOptions", "$comment": "This data is used for reference - no actual impact on calculations." }, "propUValue": { "description": "Proposed thermal transmittance of the window.", - "type": "number", + "type": [ + "number", + "null" + ], "unit": "Btu/h-ft2-F", - "minimum": 0.0, "default": 0.0 }, "grossArea": { "description": "gross area", - "type": "number", + "type": [ + "number", + "null" + ], "unit": "ft2", "minimum": 0.0, "default": 0.0 @@ -1724,82 +2222,103 @@ }, "propShgc": { "description": "Proposed solar heat gain coefficient", - "type": "number", + "type": [ + "number", + "null" + ], "minimum": 0.0, "default": 0.0 }, "propProjectionFactor": { "description": "Proposed window projection factor", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0, "default": 0.0 }, "frameType": { "description": "Window frame type", - "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/FenestrationFrameTypeOptions" } - ], + "$ref": "comCheck.schema.json#/definitions/FenestrationFrameTypeOptions", "default": null }, "glazingType": { "description": "Glazing type - used when performance data option is energy code default (PERF_TYPE_DEFAULT)", "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/GlazingTypeOptions" } + { + "type": "null" + }, + { + "$ref": "comCheck.schema.json#/definitions/GlazingTypeOptions" + } ], "default": null }, "propVt": { "descriptions": "Proposed visible transmittance", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0, "default": null }, "preAltPropShgc": { "descriptions": "Pre-Alteration solar heat gain coefficient", - "type": "number", + "type": [ + "number", + "null" + ], "minimum": 0.0, "default": 0.0 }, "solarType": { "description": "Solar coating type - used when performance data option is energy code default (PERF_TYPE_DEFAULT)", "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/SolarTypeOptions" } + { + "type": "null" + }, + { + "$ref": "comCheck.schema.json#/definitions/SolarTypeOptions" + } ], "default": null }, "glazingMaterialType": { "description": "Glazing material type", "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/GlazingMaterialTypeOptions" } + { + "type": "null" + }, + { + "$ref": "comCheck.schema.json#/definitions/GlazingMaterialTypeOptions" + } ], "default": null }, "productType": { "description": "Product Type", "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/WindowProductionTypeOptions" } + { + "type": "null" + }, + { + "$ref": "comCheck.schema.json#/definitions/WindowProductionTypeOptions" + } ], "default": null }, "allowanceType": { "description": "allowance type", - "anyOf": [ - { "type": "null" }, - { - "$ref": "comCheck.schema.json#/definitions/EnvelopeAssemblyAllowanceTypeOptions" - } - ], - "default": null + "$ref": "comCheck.schema.json#/definitions/EnvelopeAssemblyAllowanceTypeOptions" }, "exemptionType": { "descriptions": "exemption type", "anyOf": [ - { "type": "null" }, + { + "type": "null" + }, { "$ref": "comCheck.schema.json#/definitions/EnvelopeAssemblyExemptionOptions" } @@ -1809,41 +2328,61 @@ "constructionType": { "description": "Construction types - compliance code specification", "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/ConstructionTypeOptions" } + { + "type": "null" + }, + { + "$ref": "comCheck.schema.json#/definitions/ConstructionTypeOptions" + } ], "$comment": "TODO - verify if this is still needed?" }, "isSiteShading": { "description": "Is the site shaded", - "type": ["boolean", "null"], + "type": [ + "boolean", + "null" + ], "default": null }, "perfDataType": { "description": "Performance data type option", "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/PerfDataTypeOptions" } + { + "type": "null" + }, + { + "$ref": "comCheck.schema.json#/definitions/PerfDataTypeOptions" + } ], "default": null }, "productId": { "description": "Product ID", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": null, "$comment": "Window product ID, used when the perfDataType is PERF_TYPE_NFRC" }, "preAltPropUval": { "descriptions": "Pre-Alteration U-factor", - "type": ["number", "null"], - "minimum": 0.0, - "default": 0.0 + "type": [ + "number", + "null" + ], + "minimum": 0.0 }, "curbType": { "description": "Skylight curb type", "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/SkylightCurbTypeOptions" } + { + "type": "null" + }, + { + "$ref": "comCheck.schema.json#/definitions/SkylightCurbTypeOptions" + } ], "default": null } @@ -1878,12 +2417,18 @@ "properties": { "id": { "description": "Scope-unique reference identifier for instances of this data group.", - "type": ["string", "integer"], + "type": [ + "string", + "integer" + ], "$comment": "All id data elements are shown as required so that every data group can be explcitly identified." }, "description": { "description": "The name of the component", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": "", "$comment": "This needs to be unique across all ag walls." }, @@ -1895,50 +2440,53 @@ }, "bldgUseKey": { "description": "key reference of the building use area data group", - "type": "string", + "type": [ + "string", + "null" + ], "$comment": "String of values referencing :WholeBldgUse:" }, "adjacentSpaceType": { "description": "Space type of the adjacent space", "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/AdjacentSpaceTypeOptions" } + { + "type": "null" + }, + { + "$ref": "comCheck.schema.json#/definitions/AdjacentSpaceTypeOptions" + } ], "default": null, "$comment": "This data is used to identify the required maximum U factor in 90.1 energy codes. Be sure to set a not null value in 90.1 energy code" }, "adjacentSpaceBuildingType": { "description": "Building type of the adjacent space", - "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/WholeBuildingTypeOptions" } - ], - "default": null, + "$ref": "comCheck.schema.json#/definitions/WholeBuildingTypeOptions", "$comment": "This data is used for reference - no actual impact on calculations." }, "allowanceType": { "description": "allowance type", - "anyOf": [ - { "type": "null" }, - { - "$ref": "comCheck.schema.json#/definitions/EnvelopeAssemblyAllowanceTypeOptions" - } - ], - "default": null, + "$ref": "comCheck.schema.json#/definitions/EnvelopeAssemblyAllowanceTypeOptions", "$comment": "TODO - need to fill in the background?" }, "constructionType": { "description": "Construction types - compliance code specification", "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/ConstructionTypeOptions" } + { + "type": "null" + }, + { + "$ref": "comCheck.schema.json#/definitions/ConstructionTypeOptions" + } ], "$comment": "TODO - verify if this is still needed?" }, "exemptionType": { "descriptions": "exemption type", "anyOf": [ - { "type": "null" }, + { + "type": "null" + }, { "$ref": "comCheck.schema.json#/definitions/EnvelopeAssemblyExemptionOptions" } @@ -1961,23 +2509,29 @@ }, "cavityRValue": { "descriptions": "Average insulation R-value in the cavity between two studs.", - "type": "number", + "type": [ + "number", + "null" + ], "unit": "h-ft2-F/Btu", - "minimum": 0.0, "default": 0.0 }, "continuousRValue": { "description": "Continuous insulation on the above grade wall. Can be exterior or interior or both.", - "type": "number", + "type": [ + "number", + "null" + ], "unit": "h-ft2-F/Btu", - "minimum": 0.0, "default": 0.0 }, "propUValue": { "description": "Proposed thermal transmittance of the above grade wall.", - "type": "number", + "type": [ + "number", + "null" + ], "unit": "Btu/h-ft2-F", - "minimum": 0.0, "default": 0.0 }, "altExemptType": { @@ -1993,7 +2547,10 @@ }, "grossArea": { "description": "gross area", - "type": "number", + "type": [ + "number", + "null" + ], "unit": "ft2", "minimum": 0.0, "default": 0.0 @@ -2001,32 +2558,48 @@ "roofType": { "description": "roof type", "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/RoofTypeOptions" } + { + "type": "null" + }, + { + "$ref": "comCheck.schema.json#/definitions/RoofTypeOptions" + } ], "default": null }, "highAlbedoRoofReqType": { "description": "high albedo roof type - this include the albedo method and emeptions", "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/HighAlbedoRoofReqTypeOptions" } + { + "type": "null" + }, + { + "$ref": "comCheck.schema.json#/definitions/HighAlbedoRoofReqTypeOptions" + } ], - "defualt": null + "default": null }, "otherRoofType": { "description": "Roof types when selected Other RoofType - null if it is not Other RoofType", "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/OtherRoofTypeOptions" } + { + "type": "null" + }, + { + "$ref": "comCheck.schema.json#/definitions/OtherRoofTypeOptions" + } ], "default": null }, "roofInsulType": { "description": "roof insulation types", "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/RoofInsulationTypeOptions" } + { + "type": "null" + }, + { + "$ref": "comCheck.schema.json#/definitions/RoofInsulationTypeOptions" + } ], "default": null }, @@ -2052,7 +2625,10 @@ }, "purlinSpacing": { "description": "Roof purlin spacing", - "type": "number", + "type": [ + "number", + "null" + ], "minimum": 0.0, "default": 0.0, "unit": "ft" @@ -2085,12 +2661,18 @@ "properties": { "id": { "description": "Scope-unique reference identifier for instances of this data group.", - "type": ["string", "integer"], + "type": [ + "string", + "integer" + ], "$comment": "All id data elements are shown as required so that every data group can be explcitly identified." }, "description": { "description": "The name of the component", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "assemblyType": { "description": "The type of the component", @@ -2099,30 +2681,36 @@ }, "bldgUseKey": { "description": "key reference of the building use area data group", - "type": "string", + "type": [ + "string", + "null" + ], "$comment": "String of values referencing :WholeBldgUse:" }, "adjacentSpaceType": { "description": "Space type of the adjacent space", "anyOf": [ - { "$ref": "comCheck.schema.json#/definitions/AdjacentSpaceTypeOptions" }, - { "type": "null" } + { + "$ref": "comCheck.schema.json#/definitions/AdjacentSpaceTypeOptions" + }, + { + "type": "null" + } ] }, "allowanceType": { "description": "allowance type", - "anyOf": [ - { - "$ref": "comCheck.schema.json#/definitions/EnvelopeAssemblyAllowanceTypeOptions" - }, - { "type": "null" } - ] + "$ref": "comCheck.schema.json#/definitions/EnvelopeAssemblyAllowanceTypeOptions" }, "constructionType": { "description": "Construction types - compliance code specification", "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/ConstructionTypeOptions" } + { + "type": "null" + }, + { + "$ref": "comCheck.schema.json#/definitions/ConstructionTypeOptions" + } ], "$comment": "TODO - verify if this is still needed?" }, @@ -2132,7 +2720,9 @@ { "$ref": "comCheck.schema.json#/definitions/EnvelopeAssemblyExemptionOptions" }, - { "type": "null" } + { + "type": "null" + } ], "$comment": "Used in IECC 2012 only" }, @@ -2142,21 +2732,29 @@ }, "cavityRValue": { "descriptions": "Average insulation R-value in the cavity between two studs.", - "type": "number", + "type": [ + "number", + "null" + ], "unit": "h-ft2-F/Btu", "minimum": 0.0 }, "continuousRValue": { "description": "Continuous insulation on the above grade wall. Can be exterior or interior or both.", - "type": "number", + "type": [ + "number", + "null" + ], "unit": "h-ft2-F/Btu", "minimum": 0.0 }, "propUValue": { "description": "Proposed thermal transmittance of the above grade wall.", - "type": "number", - "unit": "Btu/h-ft2-F", - "minimum": 0.0 + "type": [ + "number", + "null" + ], + "unit": "Btu/h-ft2-F" }, "altExemptType": { "description": "alteration exemption type", @@ -2171,7 +2769,10 @@ }, "grossArea": { "description": "gross area", - "type": "number", + "type": [ + "number", + "null" + ], "unit": "ft2", "minimum": 0.0 }, @@ -2181,13 +2782,19 @@ }, "depthOfInsulation": { "description": "depth of insulation, it only works on a certain numbers including 1, 2, 3, 4, 93, 94, 95 ,96, 97, 98, 99. Mapping is in the $comment", - "type": ["null", "integer"], + "type": [ + "null", + "integer" + ], "minimum": 0, "$comment": "1 -> 1ft, 2 -> 2ft, 3 -> 3ft, 4 -> 4ft, 93 -> Fully insulated, 94 -> Fully Insulated (user specified perimeter R-value + R-3.5 under slab), 95 -> Fully Insulated (user specified perimeter R-value + R-5.0 under slab), 96 -> Fully Insulated (user specified perimeter R-value + R-7.5 under slab), 97 -> Fully Insulated (user specified perimeter R-value + R-10.0 under slab, 98 ->Fully Insulated (user specified perimeter R-value + R-15.0 under slab), 99 -> Fully Insulated (user specified perimeter R-value + R-20.0 under slab)" }, "slabFullInsulBelowMinRValue": { "description": "Full insulation R value below the slab -> the number if fixed based on the selection of depthOfInsulation", - "type": "number", + "type": [ + "number", + "null" + ], "unit": "h-ft2-F/Btu", "minimum": 0.0, "$comment": ", 93 -> 0.0, 94 -> 3.5, 95 -> 5.0, 96 -> 7.5, 97 -> 10.0, 98 -> 15.0, 99 -> 20.0" @@ -2198,24 +2805,37 @@ { "$ref": "comCheck.schema.json#/definitions/SlabInsulationPositionOptions" }, - { "type": "null" } + { + "type": "null" + } ] }, "hasEdgeInsul": { "description": "A boolean to indicate whether the bldg use area uses edge insulation", - "type": ["boolean", "null"], + "type": [ + "boolean", + "null" + ], "$comment": "user shall provide this data" }, "floorExposedFrameType": { "description": "Floor Exposed Frame type", "anyOf": [ - { "$ref": "comCheck.schema.json#/definitions/FloorExposedFrameType" }, - { "type": "null" } + { + "$ref": "comCheck.schema.json#/definitions/FloorExposedFrameType" + }, + { + "type": "null" + } ], "$comment": "user shall provide this data" } }, - "required": ["bldgUseKey", "description", "assemblyType"], + "required": [ + "bldgUseKey", + "description", + "assemblyType" + ], "additionalProperties": false }, "Lighting": { @@ -2223,7 +2843,10 @@ "properties": { "id": { "description": "Scope-unique reference identifier for instances of this data group.", - "type": ["string", "integer"], + "type": [ + "string", + "integer" + ], "$comment": "All id data elements are shown as required so that every data group can be explcitly identified." }, "wholeBldgUse": { @@ -2252,7 +2875,11 @@ } } }, - "required": ["exteriorLightingZoneType", "wholeBldgUse", "exteriorUse"], + "required": [ + "exteriorLightingZoneType", + "wholeBldgUse", + "exteriorUse" + ], "additionalProperties": true }, "HVAC": { @@ -2260,7 +2887,10 @@ "properties": { "id": { "description": "Scope-unique reference identifier for instances of this data group.", - "type": ["string", "integer"], + "type": [ + "string", + "integer" + ], "$comment": "All id data elements are shown as required so that every data group can be explcitly identified." }, "hvacSystem": { @@ -2278,14 +2908,22 @@ } }, "fanSystem": { - "description": "fan system", - "type": "array", + "description": "Fan system", + "type": [ + "array", + "null" + ], "items": { "$ref": "comCheck.schema.json#/definitions/FanSystem" - } + }, + "default": null } }, - "required": ["hvacSystem", "hvacPlant", "fanSystem"], + "required": [ + "hvacSystem", + "hvacPlant", + "fanSystem" + ], "additionalProperties": false }, "WholeBldgUse": { @@ -2293,7 +2931,10 @@ "properties": { "id": { "description": "Scope-unique reference identifier for instances of this data group.", - "type": ["string", "integer"], + "type": [ + "string", + "integer" + ], "$comment": "All id data elements are shown as required so that every data group can be explcitly identified." }, "areaDescription": { @@ -2309,20 +2950,29 @@ }, "ceilingHeight": { "description": "Average ceiling height", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "ft" }, "powerDensity": { "description": "Internal equipment power density", - "type": "number", + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "watt/ft2", "$comment": "Engine calculated value" }, "internalLoad": { "description": "Internal equipment load", - "type": "number", + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "watt", "$comment": "Engine calculated value" @@ -2330,8 +2980,12 @@ "constructionType": { "description": "Construction types - compliance code specification", "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/ConstructionTypeOptions" } + { + "type": "null" + }, + { + "$ref": "comCheck.schema.json#/definitions/ConstructionTypeOptions" + } ], "$comment": "TODO - verify if this is still needed?" }, @@ -2350,12 +3004,23 @@ }, "interiorLightingSpace": { "description": "Interior lighting space definition", - "$ref": "comCheck.schema.json#/definitions/InteriorLightingSpace", + "anyOf": [ + { + "$ref": "comCheck.schema.json#/definitions/InteriorLightingSpace" + }, + { + "type": "null" + } + ], "$comment": "This data is deprecated - however, it needs to be kept here for legacy code processing" }, "key": { "description": "Unique identifier for this building use area", - "type": ["string", "number", "null"] + "type": [ + "string", + "number", + "null" + ] }, "wholeBldgType": { "description": "Whole building type", @@ -2371,11 +3036,19 @@ }, "isTenantSpace": { "description": "A boolean to indicate whether the bldg use area is designed for tenant spaces", - "type": ["boolean", "null"], + "type": [ + "boolean", + "null" + ], "$comment": "Primarily used for IECC 2021 and the state codes based on this version of national code." } }, - "required": ["wholeBldgType", "activityUse", "key", "interiorLightingSpace"], + "required": [ + "wholeBldgType", + "activityUse", + "key", + "interiorLightingSpace" + ], "additionalProperties": true }, "ActivityUse": { @@ -2383,7 +3056,10 @@ "properties": { "id": { "description": "Scope-unique reference identifier for instances of this data group.", - "type": ["string", "integer"], + "type": [ + "string", + "integer" + ], "$comment": "All id data elements are shown as required so that every data group can be explcitly identified." }, "key": { @@ -2403,20 +3079,29 @@ }, "ceilingHeight": { "description": "Average ceiling height", - "type": "number", + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "ft" }, "powerDensity": { "description": "Internal equipment power density", - "type": "number", + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "watt/ft2", "$comment": "Engine calculated value" }, "internalLoad": { "description": "Internal equipment load", - "type": "number", + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "watt", "$comment": "Engine calculated value" @@ -2424,8 +3109,12 @@ "constructionType": { "description": "Construction types - compliance code specification", "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/ConstructionTypeOptions" } + { + "type": "null" + }, + { + "$ref": "comCheck.schema.json#/definitions/ConstructionTypeOptions" + } ], "$comment": "TODO - verify if this is still needed?" }, @@ -2444,7 +3133,14 @@ }, "interiorLightingSpace": { "description": "Interior lighting space definition", - "$ref": "comCheck.schema.json#/definitions/InteriorLightingSpace" + "anyOf": [ + { + "$ref": "comCheck.schema.json#/definitions/InteriorLightingSpace" + }, + { + "type": "null" + } + ] }, "activityType": { "description": "activity type", @@ -2452,15 +3148,27 @@ }, "roomCavityRatioThreshold": { "description": "Room Cavity Ratio threshold", - "type": ["number", "null"] + "type": [ + "number", + "null" + ] }, "isUnfinishedSpace": { "description": "A flag to indicate whether the space is unfinished or not.", - "type": ["boolean", "null"], - "effective_energy_codes": ["CEZ_IECC2024", "CEZ_IECC2021"] + "type": [ + "boolean", + "null" + ], + "effective_energy_codes": [ + "CEZ_IECC2024", + "CEZ_IECC2021" + ] } }, - "required": ["key", "interiorLightingSpace"], + "required": [ + "key", + "interiorLightingSpace" + ], "additionalProperties": true }, "ExteriorUse": { @@ -2468,7 +3176,10 @@ "properties": { "id": { "description": "Scope-unique reference identifier for instances of this data group.", - "type": ["string", "integer"], + "type": [ + "string", + "integer" + ], "$comment": "All id data elements are shown as required so that every data group can be explcitly identified." }, "areaDescription": { @@ -2492,7 +3203,10 @@ }, "quantityUnits": { "description": "Quantity units", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "$comment": "This string shows one of the units - typically ft2 or ft." }, "useQuantity": { @@ -2501,10 +3215,19 @@ }, "exteriorLightingSpace": { "description": "Exterior lighting space", - "$ref": "comCheck.schema.json#/definitions/ExteriorLightingSpace" + "anyOf": [ + { + "$ref": "comCheck.schema.json#/definitions/ExteriorLightingSpace" + }, + { + "type": "null" + } + ] } }, - "required": ["exteriorLightingSpace"], + "required": [ + "exteriorLightingSpace" + ], "additionalProperties": false }, "InteriorLightingSpace": { @@ -2512,36 +3235,53 @@ "properties": { "id": { "description": "Scope-unique reference identifier for instances of this data group.", - "type": ["string", "integer"], + "type": [ + "string", + "integer" + ], "$comment": "All id data elements are shown as required so that every data group can be explcitly identified." }, "description": { "description": "Description of the space", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "$comment": "It shall be unique in the project" }, "preAltNumberFixtures": { "description": "Number of fixtures to be altered", - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "minimum": 0, "$comment": "This field only used in alteration project - set to 0 if the project type is not alteration" }, "numFixturesAlteredOrAdded": { "description": "Number of fixtures added or altered", - "type": ["integer", "null"], - "minimum": 0, + "type": [ + "integer", + "null" + ], "$comment": "This field only used in alteration project - set to 0 if the project type is not alteration" }, "preAltTotalWattage": { "description": "Total wattage before the alteration", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "watt", "$comment": "This field only used in alteration project - set to 0.0 if the project type is not alteration" }, "postAltTotalWattage": { "description": "Total wattage after the alteration", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "watt", "$comment": "This field only used in alteration project - set to 0.0 if the project type is not alteration" @@ -2559,33 +3299,51 @@ }, "exemptionType": { "description": "deprecated, use null", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "allowanceType": { "description": "deprecated, use null", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "allowanceFloorArea": { "description": "The floor area that covered by allowance", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "ft2" }, "rcrPerimeter": { "description": "perimeter", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "ft" }, "rcrFloorToWorkplaneHeight": { "description": "Floor-to-workplane height", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "ft" }, "rcrWorkplaneToLuminaireHeight": { "description": "Workplane-to-luminaire height", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "ft" }, @@ -2635,36 +3393,54 @@ "properties": { "id": { "description": "Scope-unique reference identifier for instances of this data group.", - "type": ["string", "integer"], + "type": [ + "string", + "integer" + ], "$comment": "All id data elements are shown as required so that every data group can be explcitly identified." }, "description": { "description": "Description of the space", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "$comment": "It shall be unique in the project" }, "preAltNumberFixtures": { "description": "Number of fixtures to be altered", - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "minimum": 0, "$comment": "This field only used in alteration project - set to 0 if the project type is not alteration" }, "numFixturesAlteredOrAdded": { "description": "Number of fixtures added or altered", - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "minimum": 0, "$comment": "This field only used in alteration project - set to 0 if the project type is not alteration" }, "preAltTotalWattage": { "description": "Total wattage before the alteration", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "watt", "$comment": "This field only used in alteration project - set to 0.0 if the project type is not alteration" }, "postAltTotalWattage": { "description": "Total wattage after the alteration", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "watt", "$comment": "This field only used in alteration project - set to 0.0 if the project type is not alteration" @@ -2696,47 +3472,48 @@ "properties": { "id": { "description": "Scope-unique reference identifier for instances of this data group.", - "type": ["string", "integer"], + "type": [ + "string", + "integer" + ], "$comment": "All id data elements are shown as required so that every data group can be explcitly identified." }, "advControlsAllowanceAperture": { "description": "Advanced controls allowance aperture", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0, "$comment": "Applies to advanced controls allowance type: aperture." }, - "advControlAllowanceType": { - "description": "Advanced control allowance type", - "$ref": "comCheck.schema.json#/definitions/AdvancedControlsAllowanceTypeOptions" + "advControlsAllowanceType": { + "description": "Advanced controls allowance type", + "$ref": "comCheck.schema.json#/definitions/AdvancedControlsAllowanceTypeOptions", + "default": null }, "allowanceFloorArea": { "description": "Floor area covered by the allowance", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0 }, "allowanceType": { "description": "Lighting allowance type", - "anyOf": [ - { - "$ref": "comCheck.schema.json#/definitions/LightingAllowanceTypeOptions" - }, - { "type": "null" } - ] + "$ref": "comCheck.schema.json#/definitions/LightingAllowanceTypeOptions" }, "ballast": { "description": "Ballast type", - "anyOf": [ - { - "$ref": "comCheck.schema.json#/definitions/BallastTypeOptions" - }, - { - "type": "null" - } - ] + "$ref": "comCheck.schema.json#/definitions/BallastTypeOptions" }, "description": { "description": "Description of the fixture", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "$comment": "It shall be unique in the project" }, "exemptionType": { @@ -2752,7 +3529,7 @@ }, "fixtureType": { "description": "This field temporarily used to describe the fixture.", - "type": "string", + "type": ["string", "null"], "$comment": "It shall be unique among all fixtures in the interior lighting space." }, "fixtureWattage": { @@ -2763,7 +3540,10 @@ }, "lampType": { "description": "deprecated, use null", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "lightingType": { "description": "lighting fixture type", @@ -2772,75 +3552,112 @@ }, "numberOfLamps": { "description": "deprecated, use null", - "type": ["number", "null"] + "type": [ + "number", + "null" + ] }, "powerAllowance": { "description": "Advanced control power allowance", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "watt" }, "quantity": { "description": "Quantity of the fixture", - "type": "integer", + "type": [ + "integer", + "null" + ], "minimum": 0 }, "quantityWithAdvControls": { "description": "Quantity of the fixture that has advanced control", - "type": "integer", + "type": [ + "integer", + "null" + ], "minimum": 0 }, + "scheduleFixtureKey": { + "description": "UUID to identify this fixture schedule.", + "type": [ + "string", + "null" + ] + }, "trackCircuitBreakerAmps": { "description": "Track lighting circuit breaker amps", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "amps", "$comment": "Used when lightingType is set to TRACK_LIGHTING" }, "trackCircuitBreakerVolts": { "description": "Track lighting circuit breaker voltage", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "volts", "$comment": "Used when lightingType is set to TRACK_LIGHTING" }, "trackCurrentLimiterWattage": { "description": "Track current limiter wattage", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "watt", "$comment": "Used when lightingType is set to TRACK_LIGHTING" }, "trackLength": { "description": "Track lighting length", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "ft", "$comment": "Used when lightingType is set to TRACK_LIGHTING" }, "trackTotalLuminaireWattage": { "description": "Track lighting total luminaire wattage", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "watt", "$comment": "Used when lightingType is set to TRACK_LIGHTING" }, "trackTransformerWattage": { "description": "Track lighting former wattage", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "amps", "$comment": "Used when lightingType is set to TRACK_LIGHTING and the project type set to Alteration" }, "trackLightingWattageBasisType": { "description": "Track lighting wattage basis type", - "anyOf": [ - { - "$ref": "comCheck.schema.json#/definitions/TrackLightingWattageBasisTypeOptions" - }, - { - "type": "null" - } + "$ref": "comCheck.schema.json#/definitions/TrackLightingWattageBasisTypeOptions" + }, + "typeOfFixture": { + "description": "Type of the fixture", + "type": [ + "string", + "null" ] }, "lightingControl": { @@ -2851,7 +3668,12 @@ } } }, - "required": ["description", "quantity", "lightingType", "lightingControl"], + "required": [ + "description", + "quantity", + "lightingType", + "lightingControl" + ], "additionalProperties": true }, "FixtureSchedule": { @@ -2871,7 +3693,10 @@ }, "description": { "description": "Description of the fixture schedule", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "fixtureType": { "description": "This field temporarily used to describe the fixture schedule.", @@ -2880,7 +3705,10 @@ }, "fixtureWattage": { "description": "fixture wattage", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "watt" }, @@ -2891,59 +3719,76 @@ }, "trackCircuitBreakerAmps": { "description": "Track lighting circuit breaker amps", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "amps", "$comment": "Used when lightingType is set to TRACK_LIGHTING" }, "trackCircuitBreakerVolts": { "description": "Track lighting circuit breaker voltage", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "volts", "$comment": "Used when lightingType is set to TRACK_LIGHTING" }, "trackCurrentLimiterWattage": { "description": "Track current limiter wattage", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "watt", "$comment": "Used when lightingType is set to TRACK_LIGHTING" }, "trackLength": { "description": "Track lighting length", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "ft", "$comment": "Used when lightingType is set to TRACK_LIGHTING" }, "trackTotalLuminaireWattage": { "description": "Track lighting total luminaire wattage", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "watt", "$comment": "Used when lightingType is set to TRACK_LIGHTING" }, "trackTransformerWattage": { "description": "Track lighting former wattage", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "amps", "$comment": "Used when lightingType is set to TRACK_LIGHTING and the project type set to Alteration" }, "trackLightingWattageBasisType": { "description": "Track lighting wattage basis type", - "anyOf": [ - { - "$ref": "comCheck.schema.json#/definitions/TrackLightingWattageBasisTypeOptions" - }, - { - "type": "null" - } - ] + "$ref": "comCheck.schema.json#/definitions/TrackLightingWattageBasisTypeOptions" } }, - "required": ["description", "fixtureType", "fixtureWattage", "lightingType", "scheduleFixtureKey"], + "required": [ + "description", + "fixtureType", + "fixtureWattage", + "lightingType", + "scheduleFixtureKey" + ], "additionalProperties": true }, "HVACSystem": { @@ -2951,7 +3796,10 @@ "properties": { "id": { "description": "Scope-unique reference identifier for instances of this data group.", - "type": ["string", "integer"], + "type": [ + "string", + "integer" + ], "$comment": "All id data elements are shown as required so that every data group can be explcitly identified." }, "altExemptType": { @@ -2972,7 +3820,10 @@ }, "constVolMixingBox": { "description": "Flag to identify whether the HVAC system has constant volume mixing box (deprecated, set to false).", - "type": ["boolean", "null"], + "type": [ + "boolean", + "null" + ], "default": false }, "coolingEquipCapacity": { @@ -2989,7 +3840,10 @@ }, "description": { "description": "unique name of the HVAC system", - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": "" }, "descriptionCoolEquip": { @@ -3034,7 +3888,10 @@ }, "fanSystemKey": { "description": "Fan system key", - "type": ["null", "string"], + "type": [ + "null", + "string" + ], "default": null }, "fuel": { @@ -3048,13 +3905,18 @@ { "$ref": "comCheck.schema.json#/definitions/SpaceHeatingSystemExceptionOptions" }, - { "type": "null" } + { + "type": "null" + } ], "$comment": "Only effective and required if the energy code == CEZ_CO_DENVER_IECC2021" }, "heatingEquipCapacity": { "description": "Heating equipment capacity", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "kBtu/hr", "default": 0.0 @@ -3069,85 +3931,146 @@ }, "hydronicReheat": { "description": "Flag to identify whether the HVAC has a hydronic reheat system, (deprecated, set to false)", - "type": ["boolean", "null"] + "type": [ + "boolean", + "null" + ] }, "isHeatingSysWeatherized": { "description": "Flag to identify whether the heating system is weatherized, (deprecated, set to false)", - "type": ["boolean", "null"] + "type": [ + "boolean", + "null" + ] }, "perimeterSystem": { "description": "Flag to identify whether the HVAC system is used to condition perimeter zones", - "type": ["boolean", "null"] + "type": [ + "boolean", + "null" + ] }, "propCoolingEquipEfficiencyPartial": { "description": "Proposed system cooling equipment part load efficiency", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0 }, "propCoolingEquipEfficiency": { "description": "Proposed system cooling equipment efficiency", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0 }, "propHeatingEquipEfficiency": { "description": "Proposed system heating equipment efficiency", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0 }, "quantity": { "description": "Quantity of HVAC system", - "type": ["integer", "null"], - "minimum": 1 + "type": [ + "integer", + "null" + ], + "minimum": 0 }, "quantityCoolEquip": { "description": "Quantity of the cooling equipment", - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "minimum": 0 }, "quantityHeatEquip": { "description": "Quantity of the heating equipment", - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "minimum": 0 }, "reheatRecoolCoil": { "description": "Flag to identify whether the coils are reheat and recooled (deprecated, set to false)", - "type": ["boolean", "null"] + "type": [ + "boolean", + "null" + ] + }, + "requirementAnswer": { + "type": "array", + "items": { + "$ref": "comCheck.schema.json#/definitions/Requirements" + }, + "default": [] }, "returnFanHp": { "description": "Return fan HP (deprecated, set to 0)", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0 }, "steamReheat": { "description": "Flag to identify whether the HVAC system is steam reheated (deprecated, set to false)", - "type": ["boolean", "null"] + "type": [ + "boolean", + "null" + ] }, "supplyFanHp": { "description": "Supply fan HP (deprecated, set to 0)", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0 }, "supplyStaticPressure": { "description": "Supply static pressure (deprecated, set to 0)", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0 }, "systemType": { "description": "System type (deprecated, set to HVAC)", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "totalFanHp": { "description": "Total fan HP (deprecated, set to 0)", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0 }, "varAirBox": { "description": "VAR Air Box", - "type": ["boolean", "null"] + "type": [ + "boolean", + "null" + ] }, "varAirVolMixingBox": { "description": "VAR air volume mixing box", - "type": ["boolean", "null"] + "type": [ + "boolean", + "null" + ] }, "zone": { "description": "HVAC zone layout", @@ -3162,7 +4085,10 @@ "properties": { "id": { "description": "Scope-unique reference identifier for instances of this data group.", - "type": ["string", "integer"], + "type": [ + "string", + "integer" + ], "$comment": "All id data elements are shown as required so that every data group can be explcitly identified." }, "altExemptType": { @@ -3194,13 +4120,19 @@ }, "condenserFlowRate": { "description": "Condenser flow rate - this data comes from engine. User change this data does not have impact on compliance check", - "type": "number", + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "gpm/ton" }, "condenserLeavingTemperature": { "description": "leaving water temperature - this data comes from engine. User change this data does not have impact on compliance check", - "type": "number", + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "F" }, @@ -3210,23 +4142,35 @@ }, "coolingPlantCapacity": { "description": "Cooling plant capacity", - "type": "number", + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "tons" }, "description": { "description": "Plant unique name", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "enteringCondenserWaterTemperature": { "description": "Condenser entering water temperature - this data comes from engine. User change this data does not have impact on compliance check", - "type": "number", + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "F" }, "evaporatorLeavingTemperature": { "description": "Evaporator leaving water temperature - this data comes from engine. User change this data does not have impact on compliance check", - "type": "number", + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "F" }, @@ -3236,7 +4180,10 @@ }, "heatingPlantCapacity": { "description": "Heating Plant Capacity", - "type": "number", + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "kBtu/h" }, @@ -3254,11 +4201,19 @@ }, "heatRecovery": { "description": "Flag indicates whether the system has heat recovery feature", - "type": "boolean" + "type": [ + "integer", + "null" + ], + "enum": [0, 1, null] }, "heatPumpSimultaneousCoolingAndHeating": { "description": "Flag indicates whether the heat pump can do simultaneous cooling and heating", - "type": "boolean" + "type": [ + "integer", + "null" + ], + "enum": [0, 1, null] }, "heatRejection": { "description": "Heat rejection types", @@ -3266,7 +4221,10 @@ }, "leavingChilledWaterTemperature": { "description": "Leaving chiller water temperature - this data comes from engine. User change this data does not have impact on compliance check", - "type": "number", + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "F" }, @@ -3276,35 +4234,55 @@ }, "propCoolingPlantEfficiencyPartial": { "description": "Proposed cooling plant part load efficiency", - "type": "number", + "type": [ + "number", + "null" + ], "minimum": 0.0 }, "propCoolingPlantEfficiency": { "description": "Proposed cooling plant efficiency", - "type": "number", + "type": [ + "number", + "null" + ], "minimum": 0.0 }, "propHeatingPlantEfficiency": { "description": "Proposed heating plant efficiency", - "type": "number", + "type": [ + "number", + "null" + ], "minimum": 0.0 }, "quantity": { "description": "Quantity of the plant system", "type": "integer", - "minimum": 1.0 + "minimum": 0 }, "systemType": { "description": "Deprecated, system type", - "type": "string" + "type": [ + "string", + "null" + ] }, "twoPipeSystem": { "description": "Flag identifies if the plant system is a two pipe system", - "type": "boolean" + "type": [ + "integer", + "null" + ], + "enum": [0, 1, null] }, "waterloopHeatPump": { "description": "Flag identifies if the plant system is a water loop heat pump", - "type": "boolean" + "type": [ + "integer", + "null" + ], + "enum": [0, 1, null] }, "compliancePath": { "description": "Compliance path", @@ -3320,8 +4298,17 @@ "efficiencyRequirementException": { "description": "Natural gas boiler efficiency requirement exceptions", "$ref": "comCheck.schema.json#/definitions/EquipmentEfficiencyRequirementExceptionOptions", - "default": "EFF_EXCEPTION_UNSPECIFIED", - "effective_energy_codes": ["CEZ_90_1_2022", "CEZ_IECC2024"] + "effective_energy_codes": [ + "CEZ_90_1_2022", + "CEZ_IECC2024" + ] + }, + "requirementAnswer": { + "type": "array", + "items": { + "$ref": "comCheck.schema.json#/definitions/Requirements" + }, + "default": [] } }, "additionalProperties": false @@ -3331,16 +4318,26 @@ "properties": { "id": { "description": "Scope-unique reference identifier for instances of this data group.", - "type": ["string", "integer"], + "type": [ + "string", + "integer" + ], "$comment": "All id data elements are shown as required so that every data group can be explcitly identified." }, "complianceMessage": { "description": "Compliance message calculated from engine", - "type": ["null", "string"] + "type": [ + "null", + "string" + ] }, "complies": { "description": "Flag indicate whether the fan system complies or fail", - "type": ["null", "boolean", "integer"] + "type": [ + "null", + "boolean", + "integer" + ] }, "complyMethod": { "description": "Fan system compliance method", @@ -3348,11 +4345,17 @@ }, "description": { "description": "Unique name of this fan system", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "description2": { "description": "Number of areas served", - "type": "string" + "type": [ + "string", + "null" + ] }, "fan": { "description": "fans", @@ -3363,11 +4366,18 @@ }, "hasPressureDropCredits": { "description": "Flag indicates if the fan system has pressure drop credits", - "type": ["boolean", "integer"] + "enum": [ + 0, + 1, + null + ] }, "fanSystemKey": { "description": "Fan system key, used when reference a fan system in an HVAC system", - "type": "string" + "type": [ + "string", + "null" + ] }, "pressureDropCredits": { "description": "Pressure drop credits", @@ -3378,7 +4388,11 @@ }, "servesAllowanceAreaWithFlowControl": { "description": "Flag indicates if the fan system serves allowance area with flow control", - "type": ["null", "boolean", "integer"], + "type": [ + "null", + "boolean", + "integer" + ], "$comment": "integer 0 or 1, which 0 means false, and 1 mean true" } }, @@ -3390,7 +4404,10 @@ "properties": { "id": { "description": "Scope-unique reference identifier for instances of this data group.", - "type": ["string", "integer"], + "type": [ + "string", + "integer" + ], "$comment": "All id data elements are shown as required so that every data group can be explcitly identified." }, "brakeHp": { @@ -3401,7 +4418,10 @@ }, "description": { "description": "Unique name of the fan", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "designBrakeHp": { "description": "Fan design brake HP", @@ -3411,7 +4431,10 @@ }, "fanDesignEfficiency": { "description": "Fan design efficiency", - "type": "number", + "type": [ + "number", + "null" + ], "minimum": 0.0, "maximum": 100.0, "unit": "%" @@ -3435,19 +4458,23 @@ }, "maxNameplateHp": { "description": "Maximum name plate HP - this number shall be calculated by engine", - "type": "number", - "minimum": 0.0, + "type": [ + "number", + "null" + ], "unit": "HP" }, "nameplateHp": { "description": "Name plate HP", "type": "number", - "minimum": 0.0, "unit": "HP" }, "totalFanEfficiency": { "description": "Peak fan efficiency", - "type": "number", + "type": [ + "number", + "null" + ], "minimum": 0.0, "maximum": 100, "unit": "%" @@ -3467,7 +4494,10 @@ "properties": { "id": { "description": "Scope-unique reference identifier for instances of this data group.", - "type": ["string", "integer"], + "type": [ + "string", + "integer" + ], "$comment": "All id data elements are shown as required so that every data group can be explcitly identified." }, "cfm": { @@ -3488,13 +4518,18 @@ }, "recoveryEffectiveness": { "description": "Energy recovery effectiveness", - "type": "number", - "minimum": 0.0, - "maximum": 1.0 + "type": [ + "number", + "null" + ], + "minimum": 0.0 }, "verticalDuctLength": { "description": "Vertical duct length", - "type": "number", + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "ft" }, @@ -3503,7 +4538,9 @@ "$ref": "comCheck.schema.json#/definitions/PressureDropTypeOptions" } }, - "required": ["type"], + "required": [ + "type" + ], "additionalProperties": false }, "ServiceWaterHeatingSystem": { @@ -3511,28 +4548,60 @@ "properties": { "id": { "description": "Scope-unique reference identifier for instances of this data group.", - "type": ["string", "integer"], + "type": [ + "string", + "integer" + ], "$comment": "All id data elements are shown as required so that every data group can be explcitly identified." }, "circulationPump": { "description": "Flag identifies whether the SWH has a circulation pump", - "type": "boolean" + "type": "integer", + "enum": [ + 0, + 1 + ], + "default": 0 }, "heatTraceTapeInstalled": { "description": "Flag identifies whether the SWH has heat trace tape installed", - "type": "boolean" + "type": "integer", + "enum": [ + 0, + 1 + ], + "default": 0 }, "combinedSystem": { "description": "Flag identifies whether the SWH is a combined system", - "type": "boolean" + "type": "integer", + "enum": [ + 0, + 1 + ], + "default": 0 }, "poolSystem": { "description": "Flag identifies whether the SWH is part of pool system", - "type": "boolean" + "type": "integer", + "enum": [ + 0, + 1 + ], + "default": 0 }, - "heatpumpPoolHeater": { + "heatPumpPoolHeater": { "description": "Flag identifies whether the SWH uses heat pump to heat the pool. - Only used when poolSystem is true. False as default", - "type": "boolean" + "type": [ + "boolean", + "null" + ], + "enum": [ + 0, + 1, + null + ], + "default": null }, "inputRating": { "description": "Water heater rated input power. kBtu/h if fuel type is gas or oil, kW if fuel type is electric", @@ -3557,11 +4626,14 @@ "quantity": { "description": "Quantity of the water heater", "type": "integer", - "minimum": 1 + "minimum": 0 }, "description": { "description": "Unique name describes the SWH system", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "systemType": { "description": "Deprecated - set to Water Heater", @@ -3589,20 +4661,31 @@ "swhSystemSubType": { "description": "Electric storage water heater sub types", "$ref": "comCheck.schema.json#/definitions/SWHSystemSubTypeOptions", - "default": "UNKNOWN_SWH_SYSTEM_SUB_TYPE", - "effective_energy_codes": ["CEZ_90_1_2022", "CEZ_IECC2024"] + "effective_energy_codes": [ + "CEZ_90_1_2022", + "CEZ_IECC2024" + ] }, "listPosition": { - "type": ["integer", "null"] + "type": [ + "integer", + "null" + ] }, "efficiencyRequirementException": { "description": "High input natural gas service water heater efficiency requirement exceptions", "$ref": "comCheck.schema.json#/definitions/EquipmentEfficiencyRequirementExceptionOptions", - "default": "EFF_EXCEPTION_UNSPECIFIED", - "effective_energy_codes": ["CEZ_90_1_2022", "CEZ_IECC2024"] + "effective_energy_codes": [ + "CEZ_90_1_2022", + "CEZ_IECC2024" + ] }, "requirementAnswer": { - "type": "array" + "type": "array", + "items": { + "$ref": "comCheck.schema.json#/definitions/Requirements" + }, + "default": [] } }, "required": [], @@ -3613,37 +4696,54 @@ "properties": { "id": { "description": "Scope-unique reference identifier for instances of this data group.", - "type": ["string", "integer"], + "type": [ + "string", + "integer" + ], "$comment": "All id data elements are shown as required so that every data group can be explcitly identified." }, "bldgUseKey": { "description": "Building use Key, reference BuildingAreaUse key", - "type": "string" + "type": [ + "string", + "null" + ] }, "airInfiltration": { "description": "Air infiltration measured @75Pa", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "cfm/sqft", "$comment": "Used for energy credits: EFF_PACKAGE_REDUCED_AIR_INFILTRATION, EFF_PACKAGE_ENERGY_CREDIT_REDUCED_AIR_INFILTRATION, EFF_PACKAGE_ENERGY_CREDIT_MAS_REDUCED_AIR_INFILTRATION, EFF_PACKAGE_ENERGY_CREDIT_DENVER_REDUCED_AIR_INFILTRATION, EFF_PACKAGE_ENERGY_CREDIT_DENVER_FURTHER_REDUCED_AIR_INFILTRATION, EFF_PACKAGE_ENERGY_CREDIT_IECC2024_REDUCE_AIR_INFILTRATION" }, "kitchenSize": { "description": "Kitchen size", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "sqft", "$comment": "Used for energy credits: EFF_PACKAGE_ENERGY_CREDIT_EFFICIENT_KITCHEN_EQUIPMENT, EFF_PACKAGE_ENERGY_CREDIT_MAS_EFFICIENT_KITCHEN_EQUIPMENT, EFF_PACKAGE_ENERGY_CREDIT_DENVER_EFFICIENT_KITCHEN_EQUIPMENT, EFF_PACKAGE_ENERGY_CREDIT_IECC2024_EFFICIENT_COM_KITCHEN_EQUIPMENT" }, "renewableCapacity": { "description": "On-site renewable capacity, unit is Btu for thermal system and watts for electric system.", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0, "$comment": "Used when $.type is in [EFF_PACKAGE_ENERGY_CREDIT_ONSITE_RENEWABLES, EFF_PACKAGE_ENERGY_CREDIT_DENVER_ONSITE_RENEWABLES, EFF_PACKAGE_ENERGY_CREDIT_DENVER_ENHANCED_RENEWABLES, EFF_PACKAGE_ENERGY_CREDIT_MAS_ONSITE_RENEWABLES]" }, "renewableType": { "description": "Renewable system type", "anyOf": [ - { "type": "null" }, + { + "type": "null" + }, { "$ref": "comCheck.schema.json#/definitions/EnergyCreditRenewableTypeOptions" } @@ -3652,7 +4752,10 @@ }, "fractionGrossFloorAreaServedByGSHP": { "description": "Fraction of gross floor area served by GSHP, Added by 90.1 2022", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0, "maximum": 1.0, "$comment": "Used when $.type is in [EFF_PACKAGE_ENERGY_CREDIT_GSHP_SYSTEM]" @@ -3660,7 +4763,9 @@ "gshpFieldSourceCapacityType": { "description": "GSHP field source capacity type, Added by 90.1 2022", "anyOf": [ - { "type": "null" }, + { + "type": "null" + }, { "$ref": "comCheck.schema.json#/definitions/GSHPFieldSourceCapacityTypeOptions" } @@ -3669,95 +4774,137 @@ }, "fractionGrossFloorAreaServedByCAV": { "description": "Fraction of gross floor area served by CAV, Added by 90.1 2022", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0, "maximum": 1.0, "$comment": "Used when $.type is in [EFF_PACKAGE_ENERGY_CREDIT_DEDICATED_OUTDOOR_AIR_SYSTEM, EFF_PACKAGE_ENERGY_CREDIT_IECC2024_DEDICATED_OUTDOOR_AIR_SYSTEM]" }, "coolingEnergyRecoveryRatio": { "description": "Cooling energy recovery ratio, Added by 90.1 2022", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0, "maximum": 1.0, "$comment": "Used when $.type is in [EFF_PACKAGE_ENERGY_CREDIT_DEDICATED_OUTDOOR_AIR_SYSTEM, EFF_PACKAGE_ENERGY_CREDIT_IECC2024_DEDICATED_OUTDOOR_AIR_SYSTEM]" }, "heatingEnergyRecoveryRatio": { "description": "Heating energy recovery ratio, Added by 90.1 2022", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0, "maximum": 1.0, "$comment": "Used when $.type is in [EFF_PACKAGE_ENERGY_CREDIT_DEDICATED_OUTDOOR_AIR_SYSTEM, EFF_PACKAGE_ENERGY_CREDIT_IECC2024_DEDICATED_OUTDOOR_AIR_SYSTEM]" }, "percentageWaterPipingWithIncreasedInsulation": { "description": "Percentage water piping with increased insulation, Added by 90.1 2022", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0, "maximum": 100.0, "$comment": "Used when $.type is in [EFF_PACKAGE_ENERGY_CREDIT_PIPE_INSULATION_SHW, EFF_PACKAGE_ENERGY_CREDIT_IECC2024_PIPE_INSULATION]" }, "numberShowersWithDrainHeatRecovery": { "description": "Number of showers with drain heat recovery, Added by 90.1 2022", - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "minimum": 1, "$comment": "Used when $.type is in [EFF_PACKAGE_ENERGY_CREDIT_HEAT_RECOVERY_SHOWER_SHW, EFF_PACKAGE_ENERGY_CREDIT_IECC2024_HEAT_RECOVERY_SHOWER_SWH]" }, "totalNumberShowers": { "description": "Total number of showers, Added by 90.1 2022", - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "minimum": 1, "$comment": "Used when $.type is in [EFF_PACKAGE_ENERGY_CREDIT_HEAT_RECOVERY_SHOWER_SHW, EFF_PACKAGE_ENERGY_CREDIT_IECC2024_HEAT_RECOVERY_SHOWER_SWH]" }, "fractionTunedAreaOfGrossLightedFloorArea": { "description": "Fraction of tuned area of gross lighted floor area, Added by 90.1 2022", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0, "maximum": 1.0, "$comment": "Used when $.type is in [EFF_PACKAGE_ENERGY_CREDIT_DIM_HIGH_END_TRIM_LIGHT]" }, "grossLightedFloorArea": { "description": "Gross lighted floor area, Added by 90.1 2022", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "sqft", "$comment": "Used when $.type is in [EFF_PACKAGE_ENERGY_CREDIT_DAYLIGHT_AREA_LIGHT, EFF_PACKAGE_ENERGY_CREDIT_IECC2024_DAYLIGHT_AREA_LIGHT]" }, "actualDaylightAreaWithContinuousDim": { "description": "Actual daylight area with continous dimming control, Added by 90.1 2022", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "sqft", "$comment": "Used when $.type is in [EFF_PACKAGE_ENERGY_CREDIT_DAYLIGHT_AREA_LIGHT, EFF_PACKAGE_ENERGY_CREDIT_IECC2024_DAYLIGHT_AREA_LIGHT]" }, "percentageLightingLoadManagement": { "description": "Percentage lighting load management, Added by 90.1 2022", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0, "maximum": 1.0, "$comment": "Used when $.type is in [EFF_PACKAGE_ENERGY_CREDIT_LIGHT_LOAD_MANAGEMENT, EFF_PACKAGE_ENERGY_CREDIT_IECC2024_LIGHT_LOAD_MGMT]" }, "installedElectricStorageCapacity": { "description": "Installed electric storage capacity, Added by 90.1 2022", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "Wh/sqft", "$comment": "Used when $.type is in [EFF_PACKAGE_ENERGY_CREDIT_ELECTRIC_STORAGE_LOAD_MANAGEMENT, EFF_PACKAGE_ENERGY_CREDIT_IECC2024_ELECTRIC_STORAGE]" }, "storageRatio": { "description": "HVAC cooling energy storage ratio, unit in ton-hours storage per ton of design-day cooling load , Added by 90.1 2022", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.5, "maximum": 4.0, "$comment": "Used when $.type is in [EFF_PACKAGE_ENERGY_CREDIT_HVAC_COOL_STORAGE_LOAD_MANAGEMENT, EFF_PACKAGE_ENERGY_CREDIT_IECC2024_COOLING_STORAGE]" }, "sumOfFloorsServedByClassAElevators": { "description": "Sum of floors served by each Class A elevators", - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "minimum": 0, "$comment": "Used the type is in [EFF_PACKAGE_ENERGY_CREDIT_EFFICIENT_ELEVATOR, EFF_PACKAGE_ENERGY_CREDIT_IECC2024_EFFICIENT_ELEVATOR]" }, "sumOfFloorsServedByClassBElevators": { "description": "sum of floors served by all building elevators and escalators", - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "minimum": 0, "$comment": "Used when $.type is in [EFF_PACKAGE_ENERGY_CREDIT_EFFICIENT_ELEVATOR, EFF_PACKAGE_ENERGY_CREDIT_IECC2024_EFFICIENT_ELEVATOR]" }, @@ -3779,7 +4926,9 @@ ] } }, - "required": ["type"], + "required": [ + "type" + ], "additionalProperties": false }, "Renewable": { @@ -3787,7 +4936,10 @@ "properties": { "id": { "description": "Scope-unique reference identifier for instances of this data group.", - "type": ["string", "integer"], + "type": [ + "string", + "integer" + ], "$comment": "All id data elements are shown as required so that every data group can be explcitly identified." }, "renewableException": { @@ -3797,31 +4949,43 @@ "numberOfFloors": { "description": "number of floors", "type": "integer", - "minimum": 1 + "minimum": 0 }, "largestThreeFloorArea": { "description": "Gross floor area of the largest three floors", - "type": "number", + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "ft2" }, "requiredCapacity": { "description": "Code required minimum renewable capacity", - "type": "number", + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "watt", "$comment": "This number is calculated by engine - user input is not valid and will always overriden by the engine" }, "proposedCapacity": { "description": "Sum of the proposed renewable capacity", - "type": "number", + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "watt", "$comment": "This number is calculated by engine based on the renewable systems defined in the project" }, "roofAreaForRenewable": { "description": "roof area for renewable systems", - "type": "number", + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "ft2" }, @@ -3834,20 +4998,24 @@ }, "requiredOffsiteRenewableEnergy": { "description": "Sum of the required off-site renewable energy procurement", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "kWh", "$comment": "This number is calculated by engine - user input is not valid and will always overriden by the engine" }, - "proposedOffsiteRenewableEnergy": { "description": "Sum of the proposed off-site renewable energy procurement", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "kWh", "$comment": "This number is calculated by engine based on the offsite renewable energy defined in the project" }, - "offsiteRenewableProcurement": { "description": "List of off-site renewable procurement", "type": "array", @@ -3872,12 +5040,18 @@ "properties": { "id": { "description": "Scope-unique reference identifier for instances of this data group.", - "type": ["string", "integer"], + "type": [ + "string", + "integer" + ], "$comment": "All id data elements are shown as required so that every data group can be explcitly identified." }, "description": { "description": "description of the renewable system", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "renewableSystemType": { "description": "Renewable system type", @@ -3907,12 +5081,18 @@ "properties": { "id": { "description": "Scope-unique reference identifier for instances of this data group.", - "type": ["string", "integer"], + "type": [ + "string", + "integer" + ], "$comment": "All id data elements are shown as required so that every data group can be explcitly identified." }, "description": { "description": "description of the offsite renewable procurement", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "offsiteRenewableProcurementType": { "description": "Offsite renewable procurement type", @@ -3958,7 +5138,10 @@ "properties": { "id": { "description": "Scope-unique reference identifier for instances of this data group.", - "type": ["string", "integer"], + "type": [ + "string", + "integer" + ], "$comment": "All id data elements are shown as required so that every data group can be explcitly identified." }, "type": { @@ -3974,14 +5157,21 @@ "properties": { "id": { "description": "Scope-unique reference identifier for instances of this data group.", - "type": ["string", "integer"], + "type": [ + "string", + "integer" + ], "$comment": "All id data elements are shown as required so that every data group can be explcitly identified." }, "thermalBridgeType": { "description": "thermal bridge type", "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/ThermalBridgeTypeOptions" } + { + "type": "null" + }, + { + "$ref": "comCheck.schema.json#/definitions/ThermalBridgeTypeOptions" + } ], "effective_energy_codes": [ "CEZ_90_1_2022", @@ -3992,8 +5182,12 @@ "thermalBridgeCategory": { "description": "thermal bridge category", "anyOf": [ - { "type": "null" }, - { "$ref": "comCheck.schema.json#/definitions/ThermalBridgeCategoryOptions" } + { + "type": "null" + }, + { + "$ref": "comCheck.schema.json#/definitions/ThermalBridgeCategoryOptions" + } ], "effective_energy_codes": [ "CEZ_90_1_2022", @@ -4007,20 +5201,32 @@ }, "psiFactor": { "description": "Psi factor", - "type": ["number", "null"] + "type": [ + "number", + "null" + ] }, "thermalBridgeLength": { "description": "linear length of a thermal bridge - ft", - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "$comment": "Use in conjunction with psiFactor" }, "chiFactor": { "description": "Chi factor", - "type": ["number", "null"] + "type": [ + "number", + "null" + ] }, "numberOfPoints": { "description": "Number of points", - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "$comment": "Use in conjunction with ChiFactor" } }, @@ -4028,9 +5234,20 @@ "additionalProperties": false }, "ProjectTypeOptions": { - "type": "string", - "enum": ["ADDITION", "ALTERATION", "NEW_CONSTRUCTION"], - "descriptions": ["Addition", "Alteration", "New construction"] + "enum": [ + "ADDITION", + "ALTERATION", + "NEW_CONSTRUCTION", + "NONE", + null + ], + "descriptions": [ + "Addition", + "Alteration", + "New construction", + "Unspecified", + "Missing" + ] }, "ProjectSubTypeOptions": { "type": "string", @@ -4066,35 +5283,66 @@ }, "BuildingUseTypeOptions": { "type": "string", - "enum": ["NONE", "WHOLE_BLDG", "ACTIVITY", "UNKNOWN_BLDG_USE"], - "descriptions": ["None", "Whole Bldg", "Activity", "Unknown"] + "enum": [ + "NONE", + "WHOLE_BLDG", + "ACTIVITY", + "UNKNOWN_BLDG_USE" + ], + "descriptions": [ + "None", + "Whole Bldg", + "Activity", + "Unknown" + ] }, "ConditionTypeOptions": { "type": "string", - "enum": ["COOLING", "HEATING", "HEATING_AND_COOLING", "NONE"], - "descriptions": ["Cooling", "Heating", "Heating and Cooling", "None"] + "enum": [ + "COOLING", + "HEATING", + "HEATING_AND_COOLING", + "NONE" + ], + "descriptions": [ + "Cooling", + "Heating", + "Heating and Cooling", + "None" + ] }, "EnergyCodeOptions": { "type": "string", "enum": [ + "CEZ_IECC2009", + "CEZ_IECC2012", "CEZ_IECC2015", "CEZ_IECC2018", "CEZ_IECC2021", "CEZ_IECC2024", + "CEZ_IECC2024_APPXCF", + "CEZ_90_1_2007", + "CEZ_90_1_2010", "CEZ_90_1_2013", "CEZ_90_1_2016", "CEZ_90_1_2019", - "CEZ_90_1_2022" + "CEZ_90_1_2022", + "NONE" ], "descriptions": [ + "IECC 2012", "IECC 2015", "IECC 2018", "IECC 2021", "IECC 2024", + "IECC 2024 Appendix CF", + "ASHRAE 90.1 2007", + "ASHRAE 90.1 2010", "ASHRAE 90.1 2013", "ASHRAE 90.1 2016", "ASHRAE 90.1 2019", - "ASHRAE 90.1 2022" + "ASHRAE 90.1 2022", + "Unspecified" ] }, "StateRegionEnergyCodeOptions": { @@ -4103,6 +5351,10 @@ "CEZ_ONTARIO", "CEZ_DC2017", "CEZ_PUERTO_RICO", + "CEZ_NYS2024_IECC2024", + "CEZ_NYS2025_9012022", + "CEZ_NYC2025_IECC2024", + "CEZ_NYC2025_9012022", "CEZ_NYSTRETCH_90_1_2016", "CEZ_NYSTRETCH_NYC_90_1_2016", "CEZ_NYSTRETCH_90_1_11_G", @@ -4110,6 +5362,7 @@ "CEZ_CHICAGO_IECC2022", "CEZ_FL", "CEZ_VT", + "CEZ_VT2024_IECC2021", "CEZ_NY", "CEZ_NEWYORKCITY", "CEZ_NYSTRETCH_NYC_IECC2018", @@ -4117,15 +5370,21 @@ "CEZ_CO_BOULDER", "CEZ_CO_DENVER", "CEZ_CO_DENVER_90_1_2016", + "CEZ_LA2021_IECC2021", "CEZ_MN", "CEZ_MAS", "CEZ_MAS_STRETCH_IECC2021", - "CEZ_CO_DENVER_IECC2021" + "CEZ_CO_DENVER_IECC2021", + "NONE" ], "descriptions": [ "2012 Ontario Building Code and Chapter 2 of Division 3 of SB-10 (2017)", "2017 DC Energy Code", "Puerto Rico Commercial Energy Code", + "2024 New York State Energy Conservation Code - IECC 2024", + "2025 New York State Energy Conservation Code - 90.1 (2022)", + "2025 New York City Energy Conservation Code - 90.1 (2022)", + "2025 New York City Energy Conservation Code - IECC 2024", "2020 NYStretch Energy Code - 90.1 (2016) Standard", "2020 New York City Energy Conservation Code, Appendix CA (modified 90.1-2016)", "2020 NYStretch Energy Code - 90.1 (2016) Sec 11/App G, Modeling Envelope Backstop", @@ -4133,6 +5392,7 @@ "2022 Chicago Energy Transformation Code", "2020 Florida - 7th Edition", "2020 Vermont Commercial Building Energy Standards", + "2024 Vermont Commercial Building Energy Standards", "2020 New York Energy Conservation Construction Code", "2016 New York City Energy Conservation Code", "2020 New York City Energy Conservation Code", @@ -4140,26 +5400,38 @@ "2020 City of Boulder, Colorado Energy Conservation Code", "2019 Denver, Colorado Energy Conservation Code - 2018 IECC", "2019 Denver, Colorado Energy Conservation Code - 90.1 (2016)", + "2021 LA Energy Code - 2021 IECC", "2024 Minnesota Commercial Energy Code", "2020 Massachusetts Base Energy Code", "2023 Massachusetts Stretch Energy Code", - "2022 Denver Energy Code" + "2022 Denver Energy Code", + "Unspecified" ] }, "ComplianceModeOptions": { "type": "string", - "enum": ["UA", "PERFORMANCE", "PRESCRIPTIVE"], - "descriptions": ["UA", "Performance", "Prescriptive"] + "enum": [ + "UA", + "PERFORMANCE", + "PRESCRIPTIVE" + ], + "descriptions": [ + "UA", + "Performance", + "Prescriptive" + ] }, "AirBarrierComplianceTypeOptions": { "type": "string", "enum": [ + "AIR_BARRIER_OPTION_CONTINUITY_PLAN", "AIR_BARRIER_OPTION_UNKNOWN", "AIR_BARRIER_OPTION_MATERIAL_PERM", "AIR_BARRIER_OPTION_ASSEMBLY_PERM", "AIR_BARRIER_OPTION_LEAKAGE_TEST" ], "descriptions": [ + "Continuity Plan", "Unspecified", "Air Barrier Permeability", "Assembly Permeability", @@ -4177,20 +5449,27 @@ "METAL_BLDG_AG_WALL", "CONCRETE_AG_WALL", "MASONRY_AG_WALL", - "OTHER_AG_WALL" + "OTHER_AG_WALL", + "OTHER_BG_WALL", + "OTHER_FRAME", + null ], "descriptions": [ "Wood-Framed, 16in. o.c.", "Wood-Framed, 24in. o.c.", "Steel-Framed, 16in. o.c.", "Steel-Framed, 24in. o.c.", + "Metal Building Wall Without Thermal Break", "Metal Building Wall", "Solid Concrete Wall", - "Concrete Block" + "Concrete Block", + "Other Above Grade Wall Type", + "Other Below Grade Wall Type", + "Other Framing Type", + "Unspecified" ] }, "BgWallTypeOptions": { - "type": "string", "enum": [ "SOLID_CONCRETE_LE_8IN_BG_WALL", "SOLID_CONCRETE_GT_8IN_BG_WALL", @@ -4201,7 +5480,8 @@ "CONCRETE_BG_WALL", "WOOD_BG_WALL", "MASONRY_BG_WALL", - "OTHER_BG_WALL" + "OTHER_BG_WALL", + null ], "descriptions": [ "Solid concrete or masonry below grade wall <= 8 inches", @@ -4213,7 +5493,8 @@ "Concrete below grade wall", "Wood below grade wall", "mansory unit below grade wall", - "Other below grade wall type" + "Other below grade wall type", + "Unspecified" ] }, "RoofTypeOptions": { @@ -4222,6 +5503,7 @@ "ABOVE_DECK_ROOF", "METAL_BLDG_STANDING_SEAM_ROOF", "METAL_BLDG_SCREW_DOWN_ROOF", + "METAL_ROOF_W_THERMAL_BREAK", "WOOD_STD_JOIST_TRUSS", "NON_WOOD_JOIST_TRUSS", "OTHER_ROOF" @@ -4230,6 +5512,7 @@ "Insulation Entirely Above Deck", "Metal Building, Standing Seam", "Metal Building, Screw Down", + "Metal Roof with Thermal Break", "Attic Roof, Wood Joists", "Attic Roof, Steel Joists", "Other (U-Factor option)" @@ -4264,6 +5547,7 @@ "type": "string", "enum": [ "HA_ROOF_REQ_UNSPECIFIED", + "HA_ROOF_REQ_SOLAR_REFLECTANCE", "HA_ROOF_REQ_SOLAR_REFLECTANCE_INDEX", "HA_ROOF_REQ_SOLAR_REFLECTANCE_INDEX_THERMAL_EMITTANCE", "HA_ROOF_EXEMPTION_VENTILATED_ATTIC", @@ -4280,6 +5564,7 @@ ], "descriptions": [ "Unspecified", + "Minimum Solar Reflectance", "3-yr Aged Solar Reflectance Index", "3-yr Aged Solar Reflectance Index and Thermal Transmittance", "Increased Insulation", @@ -4314,14 +5599,14 @@ ] }, "FloorTypeOptions": { - "type": "string", "enum": [ "ALL_WOOD_JOIST_TRUSS_FLOOR", "NON_WOOD_JOIST_TRUSS_FLOOR", "STRUCTURAL_SLAB_FLOOR", "HEATED_SLAB_ON_GRADE", "UNHEATED_SLAB_ON_GRADE", - "OTHER_FLOOR" + "OTHER_FLOOR", + null ], "descriptions": [ "Wood-Framed", @@ -4329,7 +5614,8 @@ "Concrete Floor (over unconditioned space)", "Slab-On-Grade (Heated)", "Slab-On-Grade (Unheated)", - "Other (U-Factor option)" + "Other (U-Factor option)", + "Unspecified" ] }, "FloorExposedFrameType": { @@ -4349,18 +5635,36 @@ }, "SlabInsulationPositionOptions": { "type": "string", - "enum": ["HORIZONTAL", "VERTICAL", "NO_INSULATION"], - "descriptions": ["Horizontal with vertical Slab Insulation", "Vertical", "None"] + "enum": [ + "HORIZONTAL", + "VERTICAL", + "NO_INSULATION", + "NONE" + ], + "descriptions": [ + "Horizontal with vertical Slab Insulation", + "Vertical", + "None", + "None" + ] }, "AgWallConstructionDetailsTypeOptions": { "type": "string", "enum": [ + "AG_WALL_CONSTRUCTION_DETAILS_UNKNOWN", + "AG_WALL_CONSTRUCTION_DETAILS_HORIZONTAL_Z_GIRTS", + "AG_WALL_CONSTRUCTION_DETAILS_VERTICAL_Z_GIRTS", + "AG_WALL_CONSTRUCTION_DETAILS_Z_GIRTS_THERMAL_BROKEN", "AG_WALL_METAL_BLDG_SINGLE_LAYER_MINERAL_FIBER", "AG_WALL_METAL_BLDG_DOUBLE_LAYER_MINERAL_FIBER", "AG_WALL_METAL_BLDG_SINGLE_LAYER_MINERAL_FIBER_IN_CAVITY", "NONE" ], "descriptions": [ + "Unknown", + "Horizontal Z-Girts", + "Vertical Z-Girts", + "Z-Girts with Thermal Break", "Single Layer Mineral Fiber (comopressed at girt)", "Single Layer Mineral Fiber (in cavity, thermal block at girt)", "Double Layer Mineral Fiber (outer layer compressed at girt)", @@ -4402,60 +5706,114 @@ ] }, "EnvelopeAssemblyAllowanceTypeOptions": { - "type": "string", "enum": [ "ENV_ALLOWANCE_NONE", "ENV_ALLOWANCE_FEN_SIX_FEET_ABOVE_FLOOR", "ENV_ALLOWANCE_SKYLIGHT_HAS_DAYLIGHT_AND_AUTOCONTROLS", - "ENV_ALLOWANCE_DYNAMIC_GLAZING" + "ENV_ALLOWANCE_DYNAMIC_GLAZING", + "NONE", + null ], "descriptions": [ "None", "Fenestration six foot above floor", "Skylight has daylight and auto controls", - "Dyanmic glazing" + "Dyanmic glazing", + "None", + "Unspecified" ] }, "CMUTypeOptions": { - "type": "string", "enum": [ "CMU_SOLID_GROUT", "CMU_PARTIAL_GROUT_CELLS_EMPTY", "CMU_PARTIAL_GROUT_CELLS_INSULATED", "CMU_UNREINFORCED_CELLS_EMPTY", - "CMU_UNREINFORCED_CELLS_INSULATED" + "CMU_UNREINFORCED_CELLS_INSULATED", + "NONE", + null ], "descriptions": [ "Solid Grouted", "Partially Grouted, Cells Empty", "Partially Grouted, Cells Insulated", "Unreinforced, Cells Empty", - "Unrefinforced, Cells Insulated" + "Unreinforced, Cells Insulated", + "None", + "Unspecified" ] }, "ConcreteDensityOptions": { - "type": "integer", - "enum": [0, 95, 115, 144], - "descriptions": ["Not available", "Light Weight", "Medium Weight", "Normal Weight"] + "enum": [ + 0, + 85, + 95, + 115, + 135, + 144, + null + ], + "descriptions": [ + "Not available", + "Light Weight", + "Medium Weight", + "Normal Weight", + "Unspecified" + ] }, "ConcreteThicknessOptions": { - "type": "integer", - "enum": [0, 6, 8, 10, 12] + "enum": [ + 0, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + null + ] }, "ConstructionTypeOptions": { "type": "string", - "enum": ["NON_RESIDENTIAL", "RESIDENTIAL", "SEMI_HEATED"], - "descriptions": ["Non residential", "Residential", "Semi-heated"] + "enum": [ + "NON_RESIDENTIAL", + "RESIDENTIAL", + "SEMI_HEATED" + ], + "descriptions": [ + "Non residential", + "Residential", + "Semi-heated" + ] }, "EnvelopeAssemblyExemptionOptions": { "type": "string", - "enum": ["ENV_EXEMPTION_NONE", "ENV_EXEMPTION_DYNAMIC_GLAZING"], - "descriptions": ["None", "Dynamic glazing"] + "enum": [ + "ENV_EXEMPTION_NONE", + "ENV_EXEMPTION_DYNAMIC_GLAZING", + "NONE" + ], + "descriptions": [ + "None", + "Dynamic glazing", + "None" + ] }, "FurringTypeOptions": { "type": "string", - "enum": ["WOOD_FURRING", "METAL_FURRING", "NO_FURRING", "OTHER_FURRING"], + "enum": [ + "NONE", + "WOOD_FURRING", + "METAL_FURRING", + "NO_FURRING", + "OTHER_FURRING" + ], "descriptions": [ + "None", "Furring: Wood", "Furring: Metal", "Furring: None", @@ -4463,7 +5821,6 @@ ] }, "OrientationOptions": { - "type": "string", "enum": [ "NORTH", "EAST", @@ -4473,7 +5830,8 @@ "NORTHEAST", "SOUTHWEST", "SOUTHEAST", - "UNSPECIFIED_ORIENTATION" + "UNSPECIFIED_ORIENTATION", + null ], "descriptions": [ "North", @@ -4484,7 +5842,8 @@ "North east", "South west", "South east", - "Unspecified orientation" + "Unspecified orientation", + "Null" ], "comments": [ "Applicable to IECC and 90.1", @@ -4495,13 +5854,22 @@ "Applicable to 90.1 only", "Applicable to 90.1 only", "Applicable to 90.1 only", - "Applicable to IECC and 90.1" + "Applicable to IECC and 90.1", + "Null" ] }, "InsulationPositionOptions": { "type": "string", - "enum": ["HORIZONTAL", "VERTICAL", "NO_INSULATION"], - "descriptions": ["Horizontal", "Vertical", "None"] + "enum": [ + "HORIZONTAL", + "VERTICAL", + "NO_INSULATION" + ], + "descriptions": [ + "Horizontal", + "Vertical", + "None" + ] }, "AltExemptTypeOptions": { "type": "string", @@ -4518,6 +5886,8 @@ "EXEMPT_GLAZING_STORM_ONLY", "EXEMPT_GLAZING_GLAZING_REPLACEMENT", "EXEMPT_GLAZING_UNIT_REPLACEMENT_LT_25_PCT", + "EXEMPT_HISTORIC_CHARACTERISTIC", + "EXEMPT_LIGHTING_SPACE_REPLACEMENT_LT_20_PCT_LOAD", "EXEMPT_LIGHTING_SPACE_REPLACEMENT_LT_50_PCT", "EXEMPT_MECH_REPAIR_ONLY", "EXEMPT_MECH_EXTENSIVE_OTHER_MODS", @@ -4552,6 +5922,8 @@ "Storm window alteration.", "Glazing replacement in existing sash or frame.", "Less than 25% fenestration area alteration.", + "Alteration to the area is not applicable to historic characteristics.", + "Less than 20% fixture replacement.", "Less than 50% fixture replacement.", "Modification or repair without energy use change.", "Extensive ancillary requirements.", @@ -4575,7 +5947,6 @@ ] }, "FenestrationFrameTypeOptions": { - "type": "string", "enum": [ "METAL", "METAL_W_THERMAL_BREAK", @@ -4583,7 +5954,20 @@ "VINYL", "CURTAIN_WALL", "CURTAIN_WALL_STOREFRONT", - "OTHER_FRAME" + "NON_METAL", + "NONE", + "OTHER_FRAME", + "METAL_FRAME_24_AG_WALL", + "GLASS_DOOR", + "METAL_THERMAL_BREAK", + "OTHER_DOOR", + "INSUL_METAL_DOOR", + "NO_INSUL_SINGLE_METAL_DOOR", + "WOOD_FRAME_16_AG_WALL", + "ALL_WOOD_JOIST_TRUSS_FLOOR", + "METAL_FRAME_16_AG_WALL", + "WOOD_DOOR", + null ], "descriptions": [ "Metal frame", @@ -4591,7 +5975,21 @@ "Wood frame", "Vinyl frame", "Curtain wall", - "Curtain wall for store front" + "Curtain wall (storefront)", + "Non-metal frame", + "None", + "Other frame", + "24-gauge metal-framed wall", + "Glass door", + "Metal frame with thermal break", + "Other door", + "Insulated metal door", + "Non-insulated single metal door", + "16-gauge wood-framed wall", + "All-wood joist/truss floor", + "16-gauge metal-framed wall", + "Wood door", + "Unspecified" ] }, "GlazingTypeOptions": { @@ -4600,45 +5998,86 @@ "SINGLE_PANE", "DOUBLE_PANE", "DOUBLE_PANE_LOWE", + "OTHER_GLAZING", "TRIPLE_PANE", - "TRIPLE_PANE_LOWE" + "TRIPLE_PANE_LOWE", + "NONE" ], "descriptions": [ "Single pane", "Double pane", "Double pane low-e", "Triple pane", - "Tripe pane low-e" + "Tripe pane low-e", + "None" ] }, "SolarTypeOptions": { "type": "string", - "enum": ["CLEAR", "TINTED", "REFLECTIVE", "OTHER_SOLAR"], - "descriptions": ["Clear", "Tinted", "Reflective", "Other solar coating type"] + "enum": [ + "CLEAR", + "TINTED", + "REFLECTIVE", + "OTHER_SOLAR", + "NONE" + ], + "descriptions": [ + "Clear", + "Tinted", + "Reflective", + "Other solar coating type", + "None" + ] }, "WindowProductionTypeOptions": { "type": "string", - "enum": ["FACTORY_ASSEMBLED_WINDOW", "SITE_BUILT_WINDOW"], - "descriptions": ["Factory Assembled", "Site-Built"] + "enum": [ + "FACTORY_ASSEMBLED_WINDOW", + "SITE_BUILT_WINDOW" + ], + "descriptions": [ + "Factory Assembled", + "Site-Built" + ] }, "PerfDataTypeOptions": { "type": "string", - "enum": ["PERF_TYPE_NFRC", "PERF_TYPE_OTHER", "PERF_TYPE_DEFAULT"], + "enum": [ + "NONE", + "PERF_TYPE_NFRC", + "PERF_TYPE_OTHER", + "PERF_TYPE_DEFAULT", + "PERF_TYPE_UNSPECIFIED" + ], "descriptions": [ + "None", "NFRC site-built certified product", "Product performance evaluated in accordance with NFRC", - "Energy code defaults" + "Energy code defaults", + "Unspecified" ] }, "WindowOpenTypeOptions": { "type": "string", - "enum": ["NON_OPERABLE_WINDOW", "OPERABLE_WINDOW"], - "descriptions": ["Not operable", "Operable"] + "enum": [ + "NON_OPERABLE_WINDOW", + "OPERABLE_WINDOW" + ], + "descriptions": [ + "Not operable", + "Operable" + ] }, "SkylightCurbTypeOptions": { "type": "string", - "enum": ["CURB_SKYLIGHT", "NO_CURB_SKYLIGHT"], - "descriptions": ["Has curb", "No curb"] + "enum": [ + "CURB_SKYLIGHT", + "NO_CURB_SKYLIGHT" + ], + "descriptions": [ + "Has curb", + "No curb" + ] }, "AdjacentSpaceTypeOptions": { "type": "string", @@ -4659,8 +6098,16 @@ }, "GlazingMaterialTypeOptions": { "type": "string", - "enum": ["GLASS_GLAZING_MAT", "PLASTIC_GLAZING_MAT"], - "descriptions": ["Glazing", "Plastic"] + "enum": [ + "GLASS_GLAZING_MAT", + "PLASTIC_GLAZING_MAT", + "NONE" + ], + "descriptions": [ + "Glazing", + "Plastic", + "None" + ] }, "DoorTypeOptions": { "type": "string", @@ -4670,6 +6117,7 @@ "INSUL_METAL_DOOR", "WOOD_DOOR", "GLASS_DOOR", + "METAL_W_THERMAL_BREAK", "OTHER_DOOR", "UPWARD_ACTING_SECTIONAL" ], @@ -4679,43 +6127,80 @@ "Insulated Metal", "Wood", "Glass (over 50% glazing)", + "Metal with Thermal Break", "Other (U-Factor option)", "Upward acting section, VT 2020" ] }, "DoorOpenTypeOptions": { "type": "string", - "enum": ["SWINGING_DOOR", "NON_SWINGING_DOOR", "GARAGE_DOOR"], - "descriptions": ["Swinging door", "Non-swinging door", "Garage door"] + "enum": [ + "SWINGING_DOOR", + "NON_SWINGING_DOOR", + "GARAGE_DOOR" + ], + "descriptions": [ + "Swinging door", + "Non-swinging door", + "Garage door" + ] }, "DoorEntranceTypeOptions": { "type": "string", - "enum": ["ENTRANCE_DOOR", "NON_ENTRANCE_DOOR", "OTHER_ENTRANCE"], - "descriptions": ["Entrance door", "Non-Entrance door", "Other"] + "enum": [ + "ENTRANCE_DOOR", + "NON_ENTRANCE_DOOR", + "OTHER_ENTRANCE" + ], + "descriptions": [ + "Entrance door", + "Non-Entrance door", + "Other" + ] }, "DoorGlazingFrameTypeOptions": { "type": "string", - "enum": ["METAL", "NON_METAL"], - "descriptions": ["Metal", "Non-metal"] + "enum": [ + "METAL", + "NON_METAL" + ], + "descriptions": [ + "Metal", + "Non-metal" + ] }, "LightingAllowanceTypeOptions": { - "type": "string", "enum": [ "ALLOWANCE_NONE", + "ALLOWANCE_ADVANCED_CONTROLS", "ALLOWANCE_DECORATIVE_APPEARANCE", + "ALLOWANCE_DECORATIVE_APPEARANCE_LOBBIES", + "ALLOWANCE_DECORATIVE_APPEARANCE_OTHER", "ALLOWANCE_DISPLAY_TERMINAL", + "ALLOWANCE_ELECTRICAL_MECHANICAL", "ALLOWANCE_VEHICLE_SPORT_ELECT_HIGHLIGHT", "ALLOWANCE_FURNITURE_CLOTHES_COSMETIC_HIGHLIGHT", "ALLOWANCE_JEWELRY_CRYSTAL_CHINA_HIGHLIGHT", - "ALLOWANCE_OTHER_HIGHLIGHT" + "ALLOWANCE_OTHER_HIGHLIGHT", + "ALLOWANCE_VIDEO_CONFERENCE", + "NONE", + null ], "descriptions": [ "None", + "Advanced Controls", "Decorative Appearance", + "Decorative Appearance, Lobbies", + "Decorative Appearance, Other", + "Display Terminal", + "Electrical/Mechanical Equipment", "Vehicles, sporting goods, small electronics, highlighting", "Furniture, clothing, cosmetics highlighting", "Jewelry, crystal, china highlighting", - "Other retail highlighting" + "Other retail highlighting", + "Video conference", + "None", + "Unspecified" ] }, "LightingExemptionTypeOptions": { @@ -4724,30 +6209,50 @@ "EXEMPTION_NONE", "EXEMPTION_ADVERTISING_OR_DIRECTION_SIGN", "EXEMPTION_ATHLETIC_PLAY_AREA", + "EXEMPTION_APPROVED_SAFETY", "EXEMPTION_CASINO_GAMING", "EXEMPTION_DRESSING_ROOM_MIRROR", + "EXEMPTION_DWELL_UNIT_CONTROLLED", "EXEMPTION_EDUCATION", + "EXEMPTION_EMERGENCY_AUTOOFF", "EXEMPTION_EMERGENCY_LIGHT", "EXEMPTION_EQUIPMENT", "EXEMPTION_EXIT_SIGN", "EXEMPTION_FOOD_PREPERATION", "EXEMPTION_FURNITURE_SUPPLEMENTAL", + "EXEMPTION_HIGHLIGHT_HAZARDS", "EXEMPTION_HIGHLIGHT_LANDMARK", + "EXEMPTION_INDUSTRIAL_PRODUCTION", + "EXEMPTION_MANUFACTURER_AS_PART_OF_EQUIP", "EXEMPTION_MEDICAL_PROCEDURE", "EXEMPTION_PARKING_GARAGE_TRANSITION", "EXEMPTION_PLANT", + "EXEMPTION_POOLS_WATER", "EXEMPTION_REFRIGERATOR", "EXEMPTION_RELIGIOUS_PULPIT_CHOIR", + "EXEMPTION_REQUIRED_EGRESS", "EXEMPTION_RETAIL_DISPLAY", + "EXEMPTION_TEMP_LIGHTING", "EXEMPTION_THEATER_APPLICATION", - "EXEMPTION_HEALTH_SAFETY_REG" + "EXEMPTION_THEME_PARK_ELEMENTS", + "EXEMPTION_HEALTH_SAFETY_REG", + "EXEMPTION_HIGHLIGHT_MONUMENT", + "EXEMPTION_TRANSPORTATION_MARKER", + "EXEMPTION_TRANSPORTATION_SITE", + "EXEMPTION_EMERGENCY_LIGHT_OFF_NORMAL_BUSINESS_HRS", + "EXEMPTION_MUSEUM_DISPLAY", + "EXEMPTION_SEARCHLIGHTS", + "EXEMPTION_SLEEPING_UNIT", + "EXEMPTION_VISUALLY_IMPAIRED" ], "descriptions": [ "None", "Advertising/Directional Signage", "Athletic TV Broadcasting", + "Approved Safety Lighting", "Casino Gaming", "Dressing Room Mirror Lighting", + "Dwelling Unit Lighting Controlled by Occupant", "Lighting Sales or Educational Demonstration Systems", "Emergency Lighting Auto-off During Operating Hours", "Lighting Integral to Equipment", @@ -4755,14 +6260,25 @@ "Food Preparation Equipment", "Furniture-mounted Supplemental Task Lighting", "Registered Historical Landmark", + "Industrial Production", "Medical/Dental Procedure Lighting", "Parking Garage Transition Lighting", "Non-Human Life Support Lighting", "Lighting in Refrigerator/Freezer Cases", "Religious Pulpit and Choir Lighting", + "Lighting Required for Egress", + "Temporary Lighting", "Retail Display Window", "Theatrical Lighting", - "Health, Safety Required by Regulation" + "Theme Park Elements", + "Health, Safety Required by Regulation", + "Highlight Monument", + "Transportation Marker", + "Transporation Site Lighting", + "Emergency Lighting Auto-off During Operating Hours", + "Museum Display", + "Searchlights", + "Visually Impaired" ] }, "LightingTypeOptions": { @@ -4872,22 +6388,29 @@ "TrackLightingWattageBasisTypeOptions": { "type": "string", "enum": [ + "NONE", "WATTAGE_BASIS_NOT_SET", "TRACK_BASIS", "CIRCUIT_BREAKER_CAPACITY", "CURRENT_LIMITING_DEVICE_CAPACITY", - "TRANSFORMER_CAPACITY" + "TRANSFORMER_CAPACITY", + null ], "descriptions": [ + "None", "Unknown", "Line-voltage track luminaire wattage", "Line-voltage circuit breaker capacity", "Line-voltage current-limiter device capacity", - "Low-voltage transformer capacity" + "Low-voltage transformer capacity", + "Unspecified" ] }, "AdvancedControlsAllowanceTypeOptions": { - "type": "string", + "type": [ + "string", + "null" + ], "enum": [ "ALLOWANCE_ADV_CONTROLS_NOT_SPECIFIED", "ALLOWANCE_ADV_CONTROLS_DIM_MANUAL_CONT", @@ -4896,7 +6419,8 @@ "ALLOWANCE_ADV_CONTROLS_MULT_LEVEL_OCC_SENSORS", "ALLOWANCE_ADV_CONTROLS_OCC_SENSORS_WORKSTATION_CONT_DIM", "ALLOWANCE_ADV_CONTROLS_OCC_SENSORS_WORKSTATION_CONT_DIM_PLUS_MANUAL_DIM", - "ALLOWANCE_ADV_CONTROLS_AUTO_BILEVEL" + "ALLOWANCE_ADV_CONTROLS_AUTO_BILEVEL", + null ], "descriptions": [ "Unspecified", @@ -4981,6 +6505,7 @@ "WHOLE_BUILDING_HEALTH_CLINIC", "WHOLE_BUILDING_HOSPITAL", "WHOLE_BUILDING_HOTEL", + "WHOLE_BUILDING_INVALID_USE", "WHOLE_BUILDING_LIBRARY", "WHOLE_BUILDING_MANUFACTURING", "WHOLE_BUILDING_MOTEL", @@ -5021,6 +6546,7 @@ "Health clinic", "Hospital", "Hotel", + "Invalid Use", "Library", "Manufacturing", "Motel", @@ -5059,7 +6585,8 @@ "EXT_ZONE_PARKS", "EXT_ZONE_FOREST", "EXT_ZONE_RURAL", - "EXT_ZONE_OTHER" + "EXT_ZONE_OTHER", + "EXT_ZONE_UNDEVELOPED" ], "descriptions": [ "Unspecified", @@ -5071,7 +6598,8 @@ "Developed area in national or state park (LZ1)", "Developed area on forest land (LZ2)", "Developed rural area (LZ1)", - "Other (LZ3)" + "Other (LZ3)", + "Undeveloped area (LZ1)" ] }, "ThermalBridgeTypeOptions": { @@ -5141,13 +6669,18 @@ ] }, "ThermalBridgeComplianceTypeOptions": { - "type": "string", "enum": [ "THERMAL_BRIDGE_NON_PRESCRIPTIVE", "THERMAL_BRIDGE_PRESCRIPTIVE", - " THERMAL_BRIDGE_AS_DESIGNED" + "THERMAL_BRIDGE_AS_DESIGNED", + null ], - "descriptions": ["Non-Prescriptive", "Prescriptive", "As-Designed"] + "descriptions": [ + "Non-Prescriptive", + "Prescriptive", + "As-Designed", + "Unspecified" + ] }, "ThermalBridgeExceptionTypeOptions": { "type": "string", @@ -5186,7 +6719,6 @@ ] }, "CondenserTypeOptions": { - "type": "string", "enum": [ "UNKNOWN_CONDENSER", "NO_CONDENSER", @@ -5197,7 +6729,8 @@ "GLYCOL_COOLED", "AIR_COOLED_FAD_CONDENSER", "AIR_COOLED_DUCTED_CONDENSER", - "CHILLED_WATER" + "CHILLED_WATER", + null ], "descriptions": [ "Unknown", @@ -5209,7 +6742,8 @@ "Glycol Cooled", "Air Cooled Free Discharge", "Air Cooled Ducted", - "Chilled Water" + "Chilled Water", + "Unspecified" ] }, "CoolingEquipmentTypeOptions": { @@ -5295,8 +6829,20 @@ }, "EconomizerTypeOptions": { "type": "string", - "enum": ["UNKNOWN_ECONOMIZER", "NO_ECONOMIZER", "AIR_ECONOMIZER", "WATER_ECONOMIZER"], - "descriptions": ["Unknown", "None", "Air", "Water"] + "enum": [ + "UNKNOWN_ECONOMIZER", + "NO_ECONOMIZER", + "AIR_ECONOMIZER", + "WATER_ECONOMIZER", + "FLUID_ECONOMIZER" + ], + "descriptions": [ + "Unknown", + "None", + "Air", + "Water", + "Fluid" + ] }, "EconomizerExceptionOptions": { "type": "string", @@ -5353,26 +6899,29 @@ ] }, "FuelTypeOptions": { - "type": "string", "enum": [ "UNKNOWN_FUEL", "GAS", "ELECTRIC", "OIL", + "OIL_RESIDUAL", "PROPANE", "HOTWATER", "STEAM", - "OTHER_FOSSIL_FUEL" + "OTHER_FOSSIL_FUEL", + null ], "descriptions": [ "Unknown", "Natural Gas", "Electric", "Oil", + "Residual Oil", "Propane", "Hot Water", "Steam", - "Other Fossil Fuel" + "Other Fossil Fuel", + "Unspecified" ] }, "SpaceHeatingSystemExceptionOptions": { @@ -5424,8 +6973,16 @@ }, "ZoneLayoutOptions": { "type": "string", - "enum": ["UNKNOWN_ZONE", "SINGLE_ZONE", "MULTI_ZONE"], - "descriptions": ["Unknwon", "Single zone", "Multi zone"] + "enum": [ + "UNKNOWN_ZONE", + "SINGLE_ZONE", + "MULTI_ZONE" + ], + "descriptions": [ + "Unknwon", + "Single zone", + "Multi zone" + ] }, "HeatPumpTypeOptions": { "type": "string", @@ -5495,20 +7052,28 @@ ] }, "BoilerDraftTypeOptions": { - "type": "string", - "enum": ["NATURAL_DRAFT", "FORCED_DRAFT"], - "descriptions": ["manual draft", "froce draft"] + "enum": [ + "NATURAL_DRAFT", + "FORCED_DRAFT", + null + ], + "descriptions": [ + "manual draft", + "froce draft", + "Unspecified" + ] }, "ChillerTypeOptions": { - "type": "string", "enum": [ "NO_CHILLER", "CENTRIFUGAL", + "CENTRIFUGAL_NON_STANDARD", "ROTARY_SCREW_OR_SCROLL", "RECIPROCATING", "SINGLE_EFFECT_ABSORPTION", "DOUBLE_EFFECT_ABSORPTION_DIRECT_FIRED", - "DOUBLE_EFFECT_ABSORPTION_INDIRECT_FIRED" + "DOUBLE_EFFECT_ABSORPTION_INDIRECT_FIRED", + null ], "descriptions": [ "None", @@ -5518,76 +7083,97 @@ "Reciprocating", "Single Effect Absorption", "Double Effect Absorption Indirect Fired", - "Double Effect Absorption Direct Fired" + "Double Effect Absorption Direct Fired", + "Unspecified" ] }, "CoolingPlantTypeOptions": { - "type": "string", "enum": [ "UNKNOWN_PLANT_COOLING", "NO_PLANT_COOLING", "CONDENSER_UNIT", "WATER_CHILLER", - "HEATPUMP_CHILLER" + "HEATPUMP_CHILLER", + null ], "descriptions": [ "Unknown", "None", "Condenser Unit", "Water Chiller", - "Heat Pump Chiller" + "Heat Pump Chiller", + "Missing" ] }, "HeatingPlantTypeOptions": { - "type": "string", - "enum": ["UNKNOWN_PLANT_HEATING", "NO_PLANT_HEATING", "HOTWATER_PLANT", "STEAM_PLANT"], - "descriptions": ["Unknown", "None", "Hot Water Plant", "Steam Plant"] + "enum": [ + "UNKNOWN_PLANT_HEATING", + "NO_PLANT_HEATING", + "HOTWATER_PLANT", + "STEAM_PLANT", + null + ], + "descriptions": [ + "Unknown", + "None", + "Hot Water Plant", + "Steam Plant", + "Missing" + ] }, "HeatPumpChillerHeatingSourceConditionOptions": { - "type": "string", "enum": [ "UNKNOWN_SOURCE", "AIRSOURCE_47DB_43WB", "AIRSOURCE_17DB_15WB", "WATERSOURCE_54F_44F", - "WATERSOURCE_75F_65F" + "WATERSOURCE_75F_65F", + null ], "descriptions": [ "Unknown", "Air Source 17db/15wb", "Air Source 47db/43wb", "Water Source 54F/44F", - "Water Source 75F/65F" + "Water Source 75F/65F", + "Missing" ] }, "HeatPumpChillerLeavingHeatingWaterTempOptions": { - "type": "string", - "enum": ["UNKNOWN_TEMPERATURE", "LOW", "MEDIUM", "HIGH", "BOOST"], + "enum": [ + "UNKNOWN_TEMPERATURE", + "LOW", + "MEDIUM", + "HIGH", + "BOOST", + null + ], "descriptions": [ "Unknown", "Low (105F)", "Medium (120F)", "High (140F)", - "Boost (140F)" + "Boost (140F)", + "Missing" ] }, "HeatPumpChillerTypeOptions": { - "type": "string", "enum": [ "NO_HP_CHILLER", "AIRSOURCE_HEATPUMP_CHILLER", "WATERSOURCE_POSITIVE_DISPLACEMENT_HEATPUMP_CHILLER", - "WATERSOURCE_CENTRIFUGAL_HEATPUMP_CHILLER" + "WATERSOURCE_CENTRIFUGAL_HEATPUMP_CHILLER", + null ], "descriptions": [ "Unknown", "Air Source", "Water Source Positive Displacement", - "Water Source Centrifugal" + "Water Source Centrifugal", + "Missing" ] }, "HeatRejectionTypeOptions": { - "type": "string", "enum": [ "UNKNOWN_HEAT_REJECTION_DEVICE", "NO_HEAT_REJECTION_DEVICE", @@ -5603,7 +7189,8 @@ "PROPELLER_OR_AXIAL_FAN_EVAPORATIVE_CONDENSER_AMMONIA", "CENTRIFUGAL_FAN_FAN_EVAPORATIVE_CONDENSER_R_507A", "CENTRIFUGAL_FAN_FAN_EVAPORATIVE_CONDENSER_AMMONIA", - "PROPELLER_OR_AXIAL_FAN_DRY_COOLER" + "PROPELLER_OR_AXIAL_FAN_DRY_COOLER", + null ], "descriptions": [ "Unknown", @@ -5619,28 +7206,53 @@ "Propeller or Axial Fan Evaporative Condenser R-507A", "Propeller or Axial Fan Evaporative Condenser Ammonia", "Centrifugal Fan Evaporative Condenser R-507A", - "Centrifugal Fan Evaporative Condenser Ammonia" + "Centrifugal Fan Evaporative Condenser Ammonia", + "Missing" ] }, "PlantTypeOptions": { "type": "string", - "enum": ["UNKNOWN_PLANT", "HEATING_PLANT", "COOLING_PLANT", "HEAT_PUMP_CHILLER_PLANT"], - "descriptions": ["Unknown", "Heating", "Cooling", "Heat Pump Chiller"] + "enum": [ + "UNKNOWN_PLANT", + "HEATING_PLANT", + "COOLING_PLANT", + "HEAT_PUMP_CHILLER_PLANT" + ], + "descriptions": [ + "Unknown", + "Heating", + "Cooling", + "Heat Pump Chiller" + ] }, "CompliancePathOptions": { "type": "string", - "enum": ["pathA", "pathB", "NA"], - "descriptions": ["Path A", "Path B", "N/A"] + "enum": [ + "pathA", + "pathB", + "NA", + "COMPLIANCE_PATH_A", + "COMPLIANCE_PATH_B", + "COMPLIANCE_PATH_UNKNOWN" + ], + "descriptions": [ + "Path A", + "Path B", + "N/A", + "Path A", + "Path B", + "Unknown" + ] }, "EquipmentEfficiencyRequirementExceptionOptions": { - "type": "string", "enum": [ "EFF_EXCEPTION_BOILER_RADIANT_PERIMETER", "EFF_EXCEPTION_BOILER_DWELLING_UNIT", "EFF_EXCEPTION_BOILER_RENEWABLE_RECOVERED_ENERGY", "EFF_EXCEPTION_WATER_HEATER_DWELLING_UNIT", "EFF_EXCEPTION_WATER_HEATER_RENEWABLE_RECOVERED_ENERGY", - "EFF_EXCEPTION_UNSPECIFIED" + "EFF_EXCEPTION_UNSPECIFIED", + null ], "descriptions": [ "Majority design heating load is radiant heating in the perimeter.", @@ -5648,7 +7260,8 @@ "On-site renewable energy or recovered energy for space heating.", "Water heaters installed in individual dwelling units.", "On-site renewable energy or recovered energy for service water heating.", - "Unspecified" + "Unspecified", + "Missing" ], "comment": [ "Only applicable to {[HVACPlant]}", @@ -5656,13 +7269,22 @@ "Only applicable to {[HVACPlant]}", "Only applicable to {[ServiceWaterHeatingSystem]}", "Only applicable to {[ServiceWaterHeatingSystem]}", - "Applicable to {[HVACPlant]} and {[ServiceWaterHeatingSystem]}" + "Applicable to {[HVACPlant]} and {[ServiceWaterHeatingSystem]}", + "Unknown" ] }, "FanSystemComplianceMethodOptions": { "type": "string", - "enum": ["FAN_SYSTEM_MOTOR_NAMEPLATE_HP", "FAN_SYSTEM_MOTOR_BRAKE_HP"], - "descriptions": ["Motor Nameplate HP", "Brake HP (BHP)"] + "enum": [ + "FAN_SYSTEM_MOTOR_NAMEPLATE_HP", + "FAN_SYSTEM_MOTOR_BRAKE_HP", + null + ], + "descriptions": [ + "Motor Nameplate HP", + "Brake HP (BHP)", + "Missing" + ] }, "FanEfficiencyExceptionTypeOptions": { "type": "string", @@ -5681,7 +7303,9 @@ "FAN_EFFICIENCY_EXCEPTION_TYPE_EMERGENCY", "FAN_EFFICIENCY_EXCEPTION_TYPE_HIGH_TEMPERATURE", "FAN_EFFICIENCY_EXCEPTION_TYPE_EXPLOSIVE", - "FAN_EFFICIENCY_EXCEPTION_TYPE_REVERSIBLE_TUNNEL_VENT" + "FAN_EFFICIENCY_EXCEPTION_TYPE_REVERSIBLE_TUNNEL_VENT", + "NONE", + null ], "descriptions": [ "Unspecified", @@ -5698,7 +7322,9 @@ "Emergency function only", "Moves gases > 482 F", "Explosive atmosphere only", - "Reversible for tunnel ventilation" + "Reversible for tunnel ventilation", + "None", + "Missing" ] }, "FanTypeOptions": { @@ -5711,7 +7337,14 @@ "FAN_TYPE_EXHAUST", "FAN_TYPE_FAN_POWERED_TERMINAL_UNIT" ], - "descriptions": ["Unspecified", "Supply", "Return", "Relief", "Exhaust", "FPTU"] + "descriptions": [ + "Unspecified", + "Supply", + "Return", + "Relief", + "Exhaust", + "FPTU" + ] }, "FanVolumeTypeOptions": { "type": "string", @@ -5721,7 +7354,12 @@ "FAN_VOLUME_TYPE_SINGLE_ZONE_VAV", "FAN_VOLUME_TYPE_MULTI_ZONE_VAV" ], - "descriptions": ["Unspecified", "Constant", "Single Zone VAV", "Multi Zone VAV"] + "descriptions": [ + "Unspecified", + "Constant", + "Single Zone VAV", + "Multi Zone VAV" + ] }, "PressureDropTypeOptions": { "type": "string", @@ -5765,16 +7403,26 @@ ] }, "SWHSystemDrawPatternTypeOptions": { - "type": "string", "enum": [ "NONE", "UNKNOWN_DRAW_PATTERN", "VERY_SMALL_DRAW_PATTERN", "HIGH_DRAW_PATTERN", "LOW_DRAW_PATTERN", - "MEDIUM_DRAW_PATTERN" + "MEDIUM_DRAW_PATTERN", + "NO_COOLING_EQUIPMENT", + null ], - "descriptions": ["None", "Unknown", "Very Small", "Low", "Medium", "High"] + "descriptions": [ + "None", + "Unknown", + "Very Small", + "Low", + "Medium", + "High", + "No Cooling Equipment", + "Missing" + ] }, "SWHSystemSubTypeOptions": { "type": "string", @@ -5807,14 +7455,31 @@ ] }, "SWHFuelTypeOptions": { - "type": "string", - "enum": ["UNKNOWN_FUEL", "GAS", "ELECTRIC", "OIL"], - "descriptions": ["Unknown", "Gas", "Electric", "Oil"] + "enum": [ + "UNKNOWN_FUEL", + "GAS", + "ELECTRIC", + "OIL", + null + ], + "descriptions": [ + "Unknown", + "Gas", + "Electric", + "Oil", + "Unspecified" + ] }, "EnergyCreditRenewableTypeOptions": { "type": "string", - "enum": ["SOLAR_THERMAL", "PV_ELECTRIC"], - "descriptions": ["Thermal System (Btuh)", "Electric System (Watts)"] + "enum": [ + "SOLAR_THERMAL", + "PV_ELECTRIC" + ], + "descriptions": [ + "Thermal System (Btuh)", + "Electric System (Watts)" + ] }, "GSHPFieldSourceCapacityTypeOptions": { "type": "string", @@ -6162,20 +7827,23 @@ ] }, "RenewableExceptionOptions": { - "type": "string", "enum": [ "RENEWABLE_ONSITE_EXCEPTION_NONE", "RENEWABLE_ONSITE_EXCEPTION_LOW_SOLAR_RAD", "RENEWABLE_ONSITE_EXCEPTION_ROOF_COVERED80", "RENEWABLE_ONSITE_EXCEPTION_ROOF_COVERED50", - "RENEWABLE_ONSITE_EXCEPTION_LOW_FLOOR_AREA" + "RENEWABLE_ONSITE_EXCEPTION_LOW_FLOOR_AREA", + "RENEWABLE_ONSITE_EXCEPTION_IECC2024_LOW_FLOOR_AREA", + null ], "descriptions": [ "None", "Building receives less than 1.1 kBtu/ft2 average incident solar radiation daily", "Building with more than 80% roof area covered by equipment", "Building with more than 50% roof area shaded by natural objects or structures", - "Building effective floor area is less than 10,000 ft2" + "Building effective floor area is less than 10,000 ft2", + "Building effective floor area is less than 5,000 ft2", + "Unspecified" ] }, "RenewableSystemTypeOptions": { @@ -6201,8 +7869,14 @@ }, "RenewableSystemCapacityUnitOptions": { "type": "string", - "enum": ["RENEWABLE_SYSTEM_UNIT_WATT", "RENEWABLE_SYSTEM_UNIT_BTUH"], - "descriptions": ["Watt", "Btu/h"] + "enum": [ + "RENEWABLE_SYSTEM_UNIT_WATT", + "RENEWABLE_SYSTEM_UNIT_BTUH" + ], + "descriptions": [ + "Watt", + "Btu/h" + ] }, "ActivityTypeOptions": { "type": "string", @@ -6219,6 +7893,7 @@ "ACTIVITY_COMMON_ATRIUM_ABOVE_40", "ACTIVITY_COMMON_AUDIENCE_AREA", "ACTIVITY_COMMON_AUDITORIUM", + "ACTIVITY_COMMON_CONFERENCE_CELL", "ACTIVITY_COMMON_CONFERENCE_HALL", "ACTIVITY_COMMON_CORRIDOR", "ACTIVITY_COMMON_CORRIDOR_LT_8_FEET", @@ -6232,6 +7907,7 @@ "ACTIVITY_COMMON_EXHIBITION", "ACTIVITY_COMMON_FOOD_PREP", "ACTIVITY_COMMON_GROCERY_STORE", + "ACTIVITY_COMMON_GUESTROOM", "ACTIVITY_COMMON_HOSPITAL", "ACTIVITY_COMMON_HOTEL", "ACTIVITY_COMMON_INACTIVE_STORAGE", @@ -6252,6 +7928,7 @@ "ACTIVITY_COMMON_OFFICE_ENCLOSED", "ACTIVITY_COMMON_OFFICE_OPEN", "ACTIVITY_COMMON_OTHER", + "ACTIVITY_COMMON_PATIENT", "ACTIVITY_COMMON_POLICE", "ACTIVITY_COMMON_POST", "ACTIVITY_COMMON_RELIGIOUS", @@ -6263,6 +7940,7 @@ "ACTIVITY_COMMON_THEATER", "ACTIVITY_COMMON_TRANS", "ACTIVITY_COMMON_WAREHOUSE", + "ACTIVITY_COMMON_WELLNESS_LOUNGE", "ACTIVITY_COMMON_WORKSHOP", "ACTIVITY_CONVENTION_AUDIENCE", "ACTIVITY_CONVENTION_EXHIBIT_SPACE", @@ -6271,6 +7949,10 @@ "ACTIVITY_FOOD_CAFETERIA", "ACTIVITY_FOOD_FAMILY_RESTAURANT", "ACTIVITY_FOOD_LOUNGE", + "ACTIVITY_GAME_HIGH_LIMITS_GAME", + "ACTIVITY_GAME_SLOTS", + "ACTIVITY_GAME_SPORTSBOOK", + "ACTIVITY_GAME_TABLE_GAMES", "ACTIVITY_GYM_EXERCISE", "ACTIVITY_GYM_EXERCISE_CENTER_AUDIENCE", "ACTIVITY_GYM_GYM_AUDIENCE", @@ -6295,6 +7977,7 @@ "ACTIVITY_HOSPITAL_RECOVERY", "ACTIVITY_HOSPITAL_SUPPLY", "ACTIVITY_HOSPITAL_CONTROL", + "ACTIVITY_HOSPITAL_TELEMEDICINE_ROOM", "ACTIVITY_HOTEL_DINING", "ACTIVITY_HOTEL_FUNCTION", "ACTIVITY_HOTEL_LOBBY", @@ -6337,16 +8020,23 @@ "ACTIVITY_PARKING_GARAGE_AREA", "ACTIVITY_PARKING_PEDESTRIAN", "ACTIVITY_PARKING_DAYLIGHT_ZONE", + "ACTIVITY_PARKING_DAYLIGHT_TRANSITION_ZONE", "ACTIVITY_POST_SORTING", "ACTIVITY_RELIGIOUS_AUDIENCE", "ACTIVITY_RELIGIOUS_FELLOWSHIP_HALL", "ACTIVITY_RELIGIOUS_PULPIT", "ACTIVITY_RETAIL_FITTING", "ACTIVITY_RETAIL_MALL", + "ACTIVITY_RETAIL_MASSAGE_SPACE", + "ACTIVITY_RETAIL_NAIL_SALON", + "ACTIVITY_RETAIL_NAIL_SALON_MALL", "ACTIVITY_RETAIL_SALES", + "ACTIVITY_RETAIL_HAIR_SALON", "ACTIVITY_RETAIL_HAIRCARE", "ACTIVITY_RETAIL_MASSAGE", "ACTIVITY_RETAIL_NAILCARE", + "ACTIVITY_SECURITY_SCREEN_TRANSPORTATION_FACILITIES", + "ACTIVITY_SECURITY_SCREEN_TRANSPORTATION_WAIT_AREA", "ACTIVITY_SPORTS_AUDIENCE", "ACTIVITY_SPORTS_COURT", "ACTIVITY_SPORTS_COURT_CLASS1", @@ -6360,6 +8050,7 @@ "ACTIVITY_THEATER_DRESSING", "ACTIVITY_TRANS_BAGGAGE", "ACTIVITY_TRANS_CONCOURSE", + "ACTIVITY_TRANS_AIRPORT_HANGER", "ACTIVITY_TRANS_SEATING", "ACTIVITY_TRANS_TERMINAL", "ACTIVITY_TRANS_AIR_HANGER", @@ -6393,6 +8084,7 @@ "ACTIVITY_SCHOOL_OFFICE", "ACTIVITY_SCHOOL_STORAGE", "ACTIVITY_TRANS_DINING", + "ACTIVITY_TRANS_PASSENGER_LOAD", "ACTIVITY_HOSPITAL_PUBLIC_STAFF_LOUNGE", "ACTIVITY_ASSISTED_LIVING_FACILITY_CHAPEL", "ACTIVITY_ASSISTED_LIVING_FACILITY_DINING", @@ -6425,12 +8117,17 @@ "ACTIVITY_MOVIE_ELEVATOR", "ACTIVITY_UNFINISHED", "ACTIVITY_COMMON_OFFICE_ENCLOSED_GT250", - "ACTIVITY_COMMON_OFFICE_ENCLOSED_GT300" + "ACTIVITY_COMMON_OFFICE_ENCLOSED_GT300", + + "ACTIVITY_SECURITY_SCREEN_GENERAL_AREA", + "ACTIVITY_SPORTS_POOL_CLASS1", + "ACTIVITY_SPORTS_POOL_CLASS2", + "ACTIVITY_SPORTS_POOL_CLASS3", + "ACTIVITY_SPORTS_POOL_CLASS4" ], "descriptions": [] }, "BallastTypeOptions": { - "type": "string", "enum": [ "ELECTRONIC", "MAGNETIC", @@ -6439,7 +8136,8 @@ "PULSE_START", "STANDARD", "PREMIUM_EFF", - "DIMMING" + "DIMMING", + null ], "descriptions": [ "Electronic", @@ -6449,15 +8147,27 @@ "Pulse start", "Standard", "Premium efficiency", - "Dimming" + "Dimming", + "Unspecified" ] }, "RequirementAnswerStatus": { - "type": "string", - "enum": ["NOT_SATISFIED", "SATISFIED", "EXCEPTION", "MULTIPLE"], - "descriptions": ["Not Satisfied", "Satisfied", "Exception", "Multiple"] + "enum": [ + "NOT_SATISFIED", + "SATISFIED", + "EXCEPTION", + "MULTIPLE", + null + ], + "descriptions": [ + "Not Satisfied", + "Satisfied", + "Exception", + "Multiple", + "Missing" + ] } }, "version": "0.0.2", "$ref": "comCheck.schema.json#/definitions/ComBuilding" -} +} \ No newline at end of file diff --git a/comcheck_api/types/core_types.py b/comcheck_api/types/core_types.py index 6258ae2..572e924 100644 --- a/comcheck_api/types/core_types.py +++ b/comcheck_api/types/core_types.py @@ -1,32 +1,81 @@ # generated by datamodel-codegen: # filename: comCheck.schema.json -# timestamp: 2026-06-10T04:00:05+00:00 +# timestamp: 2026-08-05T21:47:01+00:00 from __future__ import annotations -from enum import IntEnum, StrEnum -from typing import Annotated, Any +from enum import Enum, IntEnum, StrEnum +from typing import Annotated, Any, Literal from comcheck_api.types.custom_base_model import CustomBaseModel from pydantic import ConfigDict, Field, RootModel +from pydantic.experimental.missing_sentinel import MISSING + + +class IsHistoricBuilding(IntEnum): + """ + Flag to indicate if the building is historic. + """ + + integer_0 = 0 + integer_1 = 1 + + +class EfficiencyPackageType(Enum): + """ + Efficiency Package Type + """ + + EFF_PACKAGE_UNKNOWN = 'EFF_PACKAGE_UNKNOWN' + EFF_PACKAGE_HVAC_PERFORMANCE = 'EFF_PACKAGE_HVAC_PERFORMANCE' + EFF_PACKAGE_LIGHTING_REDUCED_LPD = 'EFF_PACKAGE_LIGHTING_REDUCED_LPD' + EFF_PACKAGE_REDUCED_AIR_INFILTRATION = 'EFF_PACKAGE_REDUCED_AIR_INFILTRATION' + EFF_PACKAGE_ENHANCED_ENVELOPE_PERFORMANCE = ( + 'EFF_PACKAGE_ENHANCED_ENVELOPE_PERFORMANCE' + ) + EFF_PACKAGE_ENHANCED_LIGHTING_CONTROLS = 'EFF_PACKAGE_ENHANCED_LIGHTING_CONTROLS' + EFF_PACKAGE_ONSITE_RENEWABLES = 'EFF_PACKAGE_ONSITE_RENEWABLES' + NoneType_None = None + + +class EnergyCreditMultiplierException(Enum): + """ + Energy Credit Multiplier Exception + """ + + NO_ENERGY_CREDIT_MULTIPLIER_EXCEPTION = 'NO_ENERGY_CREDIT_MULTIPLIER_EXCEPTION' + ENERGY_CREDIT_MULTIPLIER_EXCEPTION_LOW_ENERGY_BUILDINGS = ( + 'ENERGY_CREDIT_MULTIPLIER_EXCEPTION_LOW_ENERGY_BUILDINGS' + ) + ENERGY_CREDIT_MULTIPLIER_EXCEPTION_PRIMARY_HEAT_PUMP = ( + 'ENERGY_CREDIT_MULTIPLIER_EXCEPTION_PRIMARY_HEAT_PUMP' + ) + NoneType_None = None class EnergyCodeOptions(StrEnum): + CEZ_IECC2009 = 'CEZ_IECC2009' + CEZ_IECC2012 = 'CEZ_IECC2012' CEZ_IECC2015 = 'CEZ_IECC2015' CEZ_IECC2018 = 'CEZ_IECC2018' CEZ_IECC2021 = 'CEZ_IECC2021' CEZ_IECC2024 = 'CEZ_IECC2024' + CEZ_IECC2024_APPXCF = 'CEZ_IECC2024_APPXCF' + CEZ_90_1_2007 = 'CEZ_90_1_2007' + CEZ_90_1_2010 = 'CEZ_90_1_2010' CEZ_90_1_2013 = 'CEZ_90_1_2013' CEZ_90_1_2016 = 'CEZ_90_1_2016' CEZ_90_1_2019 = 'CEZ_90_1_2019' CEZ_90_1_2022 = 'CEZ_90_1_2022' + NONE = 'NONE' -class RequirementAnswerStatus(StrEnum): +class RequirementAnswerStatus(Enum): NOT_SATISFIED = 'NOT_SATISFIED' SATISFIED = 'SATISFIED' EXCEPTION = 'EXCEPTION' MULTIPLE = 'MULTIPLE' + NoneType_None = None class Project(CustomBaseModel): @@ -34,11 +83,11 @@ class Project(CustomBaseModel): extra='ignore', ) id: Annotated[ - str | int | None, + str | int | MISSING, Field( description='Scope-unique reference identifier for instances of this data group.' ), - ] = None + ] = MISSING projectTitle: Annotated[str | None, Field(description='Project title')] = ( 'New Project' ) @@ -107,7 +156,7 @@ class Project(CustomBaseModel): notes: Annotated[str | None, Field(description='$comment')] = None -class WallTypeOptions(StrEnum): +class WallTypeOptionsEnum(StrEnum): WOOD_FRAME_16_AG_WALL = 'WOOD_FRAME_16_AG_WALL' WOOD_FRAME_24_AG_WALL = 'WOOD_FRAME_24_AG_WALL' METAL_FRAME_16_AG_WALL = 'METAL_FRAME_16_AG_WALL' @@ -117,18 +166,23 @@ class WallTypeOptions(StrEnum): CONCRETE_AG_WALL = 'CONCRETE_AG_WALL' MASONRY_AG_WALL = 'MASONRY_AG_WALL' OTHER_AG_WALL = 'OTHER_AG_WALL' + OTHER_BG_WALL = 'OTHER_BG_WALL' + OTHER_FRAME = 'OTHER_FRAME' + +class WallTypeOptions(RootModel[WallTypeOptionsEnum | None | MISSING]): + root: WallTypeOptionsEnum | None | MISSING = MISSING class Location(CustomBaseModel): model_config = ConfigDict( extra='ignore', ) id: Annotated[ - str | int | None, + str | int | MISSING, Field( description='Scope-unique reference identifier for instances of this data group.' ), - ] = None + ] = MISSING state: Annotated[ str, Field( @@ -145,7 +199,7 @@ class Location(CustomBaseModel): climateZone: Annotated[int, Field(description='Climate zone number', ge=1, le=8)] -class BgWallTypeOptions(StrEnum): +class BgWallTypeOptions(Enum): SOLID_CONCRETE_LE_8IN_BG_WALL = 'SOLID_CONCRETE_LE_8IN_BG_WALL' SOLID_CONCRETE_GT_8IN_BG_WALL = 'SOLID_CONCRETE_GT_8IN_BG_WALL' CMU_LE_8IN_EMPTY_CELLS_BG_WALL = 'CMU_LE_8IN_EMPTY_CELLS_BG_WALL' @@ -156,6 +210,7 @@ class BgWallTypeOptions(StrEnum): WOOD_BG_WALL = 'WOOD_BG_WALL' MASONRY_BG_WALL = 'MASONRY_BG_WALL' OTHER_BG_WALL = 'OTHER_BG_WALL' + NoneType_None = None class AdjacentSpaceTypeOptions(StrEnum): @@ -181,6 +236,7 @@ class WholeBuildingTypeOptions(StrEnum): WHOLE_BUILDING_HEALTH_CLINIC = 'WHOLE_BUILDING_HEALTH_CLINIC' WHOLE_BUILDING_HOSPITAL = 'WHOLE_BUILDING_HOSPITAL' WHOLE_BUILDING_HOTEL = 'WHOLE_BUILDING_HOTEL' + WHOLE_BUILDING_INVALID_USE = 'WHOLE_BUILDING_INVALID_USE' WHOLE_BUILDING_LIBRARY = 'WHOLE_BUILDING_LIBRARY' WHOLE_BUILDING_MANUFACTURING = 'WHOLE_BUILDING_MANUFACTURING' WHOLE_BUILDING_MOTEL = 'WHOLE_BUILDING_MOTEL' @@ -207,7 +263,7 @@ class WholeBuildingTypeOptions(StrEnum): WHOLE_BUILDING_WORKSHOP = 'WHOLE_BUILDING_WORKSHOP' -class OrientationOptions(StrEnum): +class OrientationOptions(Enum): NORTH = 'NORTH' EAST = 'EAST' SOUTH = 'SOUTH' @@ -217,15 +273,18 @@ class OrientationOptions(StrEnum): SOUTHWEST = 'SOUTHWEST' SOUTHEAST = 'SOUTHEAST' UNSPECIFIED_ORIENTATION = 'UNSPECIFIED_ORIENTATION' + NoneType_None = None -class EnvelopeAssemblyAllowanceTypeOptions(StrEnum): +class EnvelopeAssemblyAllowanceTypeOptions(Enum): ENV_ALLOWANCE_NONE = 'ENV_ALLOWANCE_NONE' ENV_ALLOWANCE_FEN_SIX_FEET_ABOVE_FLOOR = 'ENV_ALLOWANCE_FEN_SIX_FEET_ABOVE_FLOOR' ENV_ALLOWANCE_SKYLIGHT_HAS_DAYLIGHT_AND_AUTOCONTROLS = ( 'ENV_ALLOWANCE_SKYLIGHT_HAS_DAYLIGHT_AND_AUTOCONTROLS' ) ENV_ALLOWANCE_DYNAMIC_GLAZING = 'ENV_ALLOWANCE_DYNAMIC_GLAZING' + NONE = 'NONE' + NoneType_None = None class ConstructionTypeOptions(StrEnum): @@ -249,6 +308,10 @@ class AltExemptTypeOptions(StrEnum): EXEMPT_GLAZING_UNIT_REPLACEMENT_LT_25_PCT = ( 'EXEMPT_GLAZING_UNIT_REPLACEMENT_LT_25_PCT' ) + EXEMPT_HISTORIC_CHARACTERISTIC = 'EXEMPT_HISTORIC_CHARACTERISTIC' + EXEMPT_LIGHTING_SPACE_REPLACEMENT_LT_20_PCT_LOAD = ( + 'EXEMPT_LIGHTING_SPACE_REPLACEMENT_LT_20_PCT_LOAD' + ) EXEMPT_LIGHTING_SPACE_REPLACEMENT_LT_50_PCT = ( 'EXEMPT_LIGHTING_SPACE_REPLACEMENT_LT_50_PCT' ) @@ -291,7 +354,7 @@ class AltExemptTypeOptions(StrEnum): ) -class CondenserTypeOptions(StrEnum): +class CondenserTypeOptions(Enum): UNKNOWN_CONDENSER = 'UNKNOWN_CONDENSER' NO_CONDENSER = 'NO_CONDENSER' AIR_COOLED = 'AIR_COOLED' @@ -302,6 +365,7 @@ class CondenserTypeOptions(StrEnum): AIR_COOLED_FAD_CONDENSER = 'AIR_COOLED_FAD_CONDENSER' AIR_COOLED_DUCTED_CONDENSER = 'AIR_COOLED_DUCTED_CONDENSER' CHILLED_WATER = 'CHILLED_WATER' + NoneType_None = None class ActivityTypeOptions(StrEnum): @@ -319,6 +383,7 @@ class ActivityTypeOptions(StrEnum): ACTIVITY_COMMON_ATRIUM_ABOVE_40 = 'ACTIVITY_COMMON_ATRIUM_ABOVE_40' ACTIVITY_COMMON_AUDIENCE_AREA = 'ACTIVITY_COMMON_AUDIENCE_AREA' ACTIVITY_COMMON_AUDITORIUM = 'ACTIVITY_COMMON_AUDITORIUM' + ACTIVITY_COMMON_CONFERENCE_CELL = 'ACTIVITY_COMMON_CONFERENCE_CELL' ACTIVITY_COMMON_CONFERENCE_HALL = 'ACTIVITY_COMMON_CONFERENCE_HALL' ACTIVITY_COMMON_CORRIDOR = 'ACTIVITY_COMMON_CORRIDOR' ACTIVITY_COMMON_CORRIDOR_LT_8_FEET = 'ACTIVITY_COMMON_CORRIDOR_LT_8_FEET' @@ -332,6 +397,7 @@ class ActivityTypeOptions(StrEnum): ACTIVITY_COMMON_EXHIBITION = 'ACTIVITY_COMMON_EXHIBITION' ACTIVITY_COMMON_FOOD_PREP = 'ACTIVITY_COMMON_FOOD_PREP' ACTIVITY_COMMON_GROCERY_STORE = 'ACTIVITY_COMMON_GROCERY_STORE' + ACTIVITY_COMMON_GUESTROOM = 'ACTIVITY_COMMON_GUESTROOM' ACTIVITY_COMMON_HOSPITAL = 'ACTIVITY_COMMON_HOSPITAL' ACTIVITY_COMMON_HOTEL = 'ACTIVITY_COMMON_HOTEL' ACTIVITY_COMMON_INACTIVE_STORAGE = 'ACTIVITY_COMMON_INACTIVE_STORAGE' @@ -352,6 +418,7 @@ class ActivityTypeOptions(StrEnum): ACTIVITY_COMMON_OFFICE_ENCLOSED = 'ACTIVITY_COMMON_OFFICE_ENCLOSED' ACTIVITY_COMMON_OFFICE_OPEN = 'ACTIVITY_COMMON_OFFICE_OPEN' ACTIVITY_COMMON_OTHER = 'ACTIVITY_COMMON_OTHER' + ACTIVITY_COMMON_PATIENT = 'ACTIVITY_COMMON_PATIENT' ACTIVITY_COMMON_POLICE = 'ACTIVITY_COMMON_POLICE' ACTIVITY_COMMON_POST = 'ACTIVITY_COMMON_POST' ACTIVITY_COMMON_RELIGIOUS = 'ACTIVITY_COMMON_RELIGIOUS' @@ -363,6 +430,7 @@ class ActivityTypeOptions(StrEnum): ACTIVITY_COMMON_THEATER = 'ACTIVITY_COMMON_THEATER' ACTIVITY_COMMON_TRANS = 'ACTIVITY_COMMON_TRANS' ACTIVITY_COMMON_WAREHOUSE = 'ACTIVITY_COMMON_WAREHOUSE' + ACTIVITY_COMMON_WELLNESS_LOUNGE = 'ACTIVITY_COMMON_WELLNESS_LOUNGE' ACTIVITY_COMMON_WORKSHOP = 'ACTIVITY_COMMON_WORKSHOP' ACTIVITY_CONVENTION_AUDIENCE = 'ACTIVITY_CONVENTION_AUDIENCE' ACTIVITY_CONVENTION_EXHIBIT_SPACE = 'ACTIVITY_CONVENTION_EXHIBIT_SPACE' @@ -371,6 +439,10 @@ class ActivityTypeOptions(StrEnum): ACTIVITY_FOOD_CAFETERIA = 'ACTIVITY_FOOD_CAFETERIA' ACTIVITY_FOOD_FAMILY_RESTAURANT = 'ACTIVITY_FOOD_FAMILY_RESTAURANT' ACTIVITY_FOOD_LOUNGE = 'ACTIVITY_FOOD_LOUNGE' + ACTIVITY_GAME_HIGH_LIMITS_GAME = 'ACTIVITY_GAME_HIGH_LIMITS_GAME' + ACTIVITY_GAME_SLOTS = 'ACTIVITY_GAME_SLOTS' + ACTIVITY_GAME_SPORTSBOOK = 'ACTIVITY_GAME_SPORTSBOOK' + ACTIVITY_GAME_TABLE_GAMES = 'ACTIVITY_GAME_TABLE_GAMES' ACTIVITY_GYM_EXERCISE = 'ACTIVITY_GYM_EXERCISE' ACTIVITY_GYM_EXERCISE_CENTER_AUDIENCE = 'ACTIVITY_GYM_EXERCISE_CENTER_AUDIENCE' ACTIVITY_GYM_GYM_AUDIENCE = 'ACTIVITY_GYM_GYM_AUDIENCE' @@ -395,6 +467,7 @@ class ActivityTypeOptions(StrEnum): ACTIVITY_HOSPITAL_RECOVERY = 'ACTIVITY_HOSPITAL_RECOVERY' ACTIVITY_HOSPITAL_SUPPLY = 'ACTIVITY_HOSPITAL_SUPPLY' ACTIVITY_HOSPITAL_CONTROL = 'ACTIVITY_HOSPITAL_CONTROL' + ACTIVITY_HOSPITAL_TELEMEDICINE_ROOM = 'ACTIVITY_HOSPITAL_TELEMEDICINE_ROOM' ACTIVITY_HOTEL_DINING = 'ACTIVITY_HOTEL_DINING' ACTIVITY_HOTEL_FUNCTION = 'ACTIVITY_HOTEL_FUNCTION' ACTIVITY_HOTEL_LOBBY = 'ACTIVITY_HOTEL_LOBBY' @@ -441,16 +514,29 @@ class ActivityTypeOptions(StrEnum): ACTIVITY_PARKING_GARAGE_AREA = 'ACTIVITY_PARKING_GARAGE_AREA' ACTIVITY_PARKING_PEDESTRIAN = 'ACTIVITY_PARKING_PEDESTRIAN' ACTIVITY_PARKING_DAYLIGHT_ZONE = 'ACTIVITY_PARKING_DAYLIGHT_ZONE' + ACTIVITY_PARKING_DAYLIGHT_TRANSITION_ZONE = ( + 'ACTIVITY_PARKING_DAYLIGHT_TRANSITION_ZONE' + ) ACTIVITY_POST_SORTING = 'ACTIVITY_POST_SORTING' ACTIVITY_RELIGIOUS_AUDIENCE = 'ACTIVITY_RELIGIOUS_AUDIENCE' ACTIVITY_RELIGIOUS_FELLOWSHIP_HALL = 'ACTIVITY_RELIGIOUS_FELLOWSHIP_HALL' ACTIVITY_RELIGIOUS_PULPIT = 'ACTIVITY_RELIGIOUS_PULPIT' ACTIVITY_RETAIL_FITTING = 'ACTIVITY_RETAIL_FITTING' ACTIVITY_RETAIL_MALL = 'ACTIVITY_RETAIL_MALL' + ACTIVITY_RETAIL_MASSAGE_SPACE = 'ACTIVITY_RETAIL_MASSAGE_SPACE' + ACTIVITY_RETAIL_NAIL_SALON = 'ACTIVITY_RETAIL_NAIL_SALON' + ACTIVITY_RETAIL_NAIL_SALON_MALL = 'ACTIVITY_RETAIL_NAIL_SALON_MALL' ACTIVITY_RETAIL_SALES = 'ACTIVITY_RETAIL_SALES' + ACTIVITY_RETAIL_HAIR_SALON = 'ACTIVITY_RETAIL_HAIR_SALON' ACTIVITY_RETAIL_HAIRCARE = 'ACTIVITY_RETAIL_HAIRCARE' ACTIVITY_RETAIL_MASSAGE = 'ACTIVITY_RETAIL_MASSAGE' ACTIVITY_RETAIL_NAILCARE = 'ACTIVITY_RETAIL_NAILCARE' + ACTIVITY_SECURITY_SCREEN_TRANSPORTATION_FACILITIES = ( + 'ACTIVITY_SECURITY_SCREEN_TRANSPORTATION_FACILITIES' + ) + ACTIVITY_SECURITY_SCREEN_TRANSPORTATION_WAIT_AREA = ( + 'ACTIVITY_SECURITY_SCREEN_TRANSPORTATION_WAIT_AREA' + ) ACTIVITY_SPORTS_AUDIENCE = 'ACTIVITY_SPORTS_AUDIENCE' ACTIVITY_SPORTS_COURT = 'ACTIVITY_SPORTS_COURT' ACTIVITY_SPORTS_COURT_CLASS1 = 'ACTIVITY_SPORTS_COURT_CLASS1' @@ -464,6 +550,7 @@ class ActivityTypeOptions(StrEnum): ACTIVITY_THEATER_DRESSING = 'ACTIVITY_THEATER_DRESSING' ACTIVITY_TRANS_BAGGAGE = 'ACTIVITY_TRANS_BAGGAGE' ACTIVITY_TRANS_CONCOURSE = 'ACTIVITY_TRANS_CONCOURSE' + ACTIVITY_TRANS_AIRPORT_HANGER = 'ACTIVITY_TRANS_AIRPORT_HANGER' ACTIVITY_TRANS_SEATING = 'ACTIVITY_TRANS_SEATING' ACTIVITY_TRANS_TERMINAL = 'ACTIVITY_TRANS_TERMINAL' ACTIVITY_TRANS_AIR_HANGER = 'ACTIVITY_TRANS_AIR_HANGER' @@ -497,6 +584,7 @@ class ActivityTypeOptions(StrEnum): ACTIVITY_SCHOOL_OFFICE = 'ACTIVITY_SCHOOL_OFFICE' ACTIVITY_SCHOOL_STORAGE = 'ACTIVITY_SCHOOL_STORAGE' ACTIVITY_TRANS_DINING = 'ACTIVITY_TRANS_DINING' + ACTIVITY_TRANS_PASSENGER_LOAD = 'ACTIVITY_TRANS_PASSENGER_LOAD' ACTIVITY_HOSPITAL_PUBLIC_STAFF_LOUNGE = 'ACTIVITY_HOSPITAL_PUBLIC_STAFF_LOUNGE' ACTIVITY_ASSISTED_LIVING_FACILITY_CHAPEL = ( 'ACTIVITY_ASSISTED_LIVING_FACILITY_CHAPEL' @@ -548,6 +636,11 @@ class ActivityTypeOptions(StrEnum): ACTIVITY_UNFINISHED = 'ACTIVITY_UNFINISHED' ACTIVITY_COMMON_OFFICE_ENCLOSED_GT250 = 'ACTIVITY_COMMON_OFFICE_ENCLOSED_GT250' ACTIVITY_COMMON_OFFICE_ENCLOSED_GT300 = 'ACTIVITY_COMMON_OFFICE_ENCLOSED_GT300' + ACTIVITY_SECURITY_SCREEN_GENERAL_AREA = 'ACTIVITY_SECURITY_SCREEN_GENERAL_AREA' + ACTIVITY_SPORTS_POOL_CLASS1 = 'ACTIVITY_SPORTS_POOL_CLASS1' + ACTIVITY_SPORTS_POOL_CLASS2 = 'ACTIVITY_SPORTS_POOL_CLASS2' + ACTIVITY_SPORTS_POOL_CLASS3 = 'ACTIVITY_SPORTS_POOL_CLASS3' + ACTIVITY_SPORTS_POOL_CLASS4 = 'ACTIVITY_SPORTS_POOL_CLASS4' class ExteriorUseTypeOptions(StrEnum): @@ -589,7 +682,7 @@ class ExteriorUseTypeOptions(StrEnum): TOTAL_EXTERIOR_USES = 'TOTAL_EXTERIOR_USES' -class AdvancedControlsAllowanceTypeOptions(StrEnum): +class AdvancedControlsAllowanceTypeOptions(Enum): ALLOWANCE_ADV_CONTROLS_NOT_SPECIFIED = 'ALLOWANCE_ADV_CONTROLS_NOT_SPECIFIED' ALLOWANCE_ADV_CONTROLS_DIM_MANUAL_CONT = 'ALLOWANCE_ADV_CONTROLS_DIM_MANUAL_CONT' ALLOWANCE_ADV_CONTROLS_DIM_PROG_MULTI_LEVEL = ( @@ -608,6 +701,7 @@ class AdvancedControlsAllowanceTypeOptions(StrEnum): 'ALLOWANCE_ADV_CONTROLS_OCC_SENSORS_WORKSTATION_CONT_DIM_PLUS_MANUAL_DIM' ) ALLOWANCE_ADV_CONTROLS_AUTO_BILEVEL = 'ALLOWANCE_ADV_CONTROLS_AUTO_BILEVEL' + NoneType_None = None class LightingTypeOptions(StrEnum): @@ -622,17 +716,74 @@ class LightingTypeOptions(StrEnum): OTHER_LIGHTING_TYPE = 'OTHER_LIGHTING_TYPE' -class BoilerDraftTypeOptions(StrEnum): +class HeatRecovery(Enum): + """ + Flag indicates whether the system has heat recovery feature + """ + + int_0 = 0 + int_1 = 1 + NoneType_None = None + + +class HeatPumpSimultaneousCoolingAndHeating(Enum): + """ + Flag indicates whether the heat pump can do simultaneous cooling and heating + """ + + int_0 = 0 + int_1 = 1 + NoneType_None = None + + +class TwoPipeSystem(Enum): + """ + Flag identifies if the plant system is a two pipe system + """ + + int_0 = 0 + int_1 = 1 + NoneType_None = None + + +class WaterloopHeatPump(Enum): + """ + Flag identifies if the plant system is a water loop heat pump + """ + + int_0 = 0 + int_1 = 1 + NoneType_None = None + + +class BoilerDraftTypeOptions(Enum): NATURAL_DRAFT = 'NATURAL_DRAFT' FORCED_DRAFT = 'FORCED_DRAFT' + NoneType_None = None -class FanSystemComplianceMethodOptions(StrEnum): +class HasPressureDropCredits(Enum): + """ + Flag indicates if the fan system has pressure drop credits + """ + + int_0 = 0 + int_1 = 1 + NoneType_None = None + + +class FanSystemComplianceMethodOptionsEnum(StrEnum): FAN_SYSTEM_MOTOR_NAMEPLATE_HP = 'FAN_SYSTEM_MOTOR_NAMEPLATE_HP' FAN_SYSTEM_MOTOR_BRAKE_HP = 'FAN_SYSTEM_MOTOR_BRAKE_HP' -class FanEfficiencyExceptionTypeOptions(StrEnum): +class FanSystemComplianceMethodOptions( + RootModel[FanSystemComplianceMethodOptionsEnum | None | MISSING] +): + root: FanSystemComplianceMethodOptionsEnum | None | MISSING = MISSING + + +class FanEfficiencyExceptionTypeOptionsEnum(StrEnum): FAN_EFFICIENCY_EXCEPTION_TYPE_UNSPECIFIED = ( 'FAN_EFFICIENCY_EXCEPTION_TYPE_UNSPECIFIED' ) @@ -672,6 +823,13 @@ class FanEfficiencyExceptionTypeOptions(StrEnum): FAN_EFFICIENCY_EXCEPTION_TYPE_REVERSIBLE_TUNNEL_VENT = ( 'FAN_EFFICIENCY_EXCEPTION_TYPE_REVERSIBLE_TUNNEL_VENT' ) + NONE = 'NONE' + + +class FanEfficiencyExceptionTypeOptions( + RootModel[FanEfficiencyExceptionTypeOptionsEnum | None | MISSING] +): + root: FanEfficiencyExceptionTypeOptionsEnum | None | MISSING = MISSING class PressureDropTypeOptions(StrEnum): @@ -712,13 +870,61 @@ class PressureDropTypeOptions(StrEnum): ) -class SWHSystemDrawPatternTypeOptions(StrEnum): +class CirculationPump(IntEnum): + """ + Flag identifies whether the SWH has a circulation pump + """ + + integer_0 = 0 + integer_1 = 1 + + +class HeatTraceTapeInstalled(IntEnum): + """ + Flag identifies whether the SWH has heat trace tape installed + """ + + integer_0 = 0 + integer_1 = 1 + + +class CombinedSystem(IntEnum): + """ + Flag identifies whether the SWH is a combined system + """ + + integer_0 = 0 + integer_1 = 1 + + +class PoolSystem(IntEnum): + """ + Flag identifies whether the SWH is part of pool system + """ + + integer_0 = 0 + integer_1 = 1 + + +class HeatPumpPoolHeater(Enum): + """ + Flag identifies whether the SWH uses heat pump to heat the pool. - Only used when poolSystem is true. False as default + """ + + int_0 = 0 + int_1 = 1 + NoneType_None = None + + +class SWHSystemDrawPatternTypeOptions(Enum): NONE = 'NONE' UNKNOWN_DRAW_PATTERN = 'UNKNOWN_DRAW_PATTERN' VERY_SMALL_DRAW_PATTERN = 'VERY_SMALL_DRAW_PATTERN' HIGH_DRAW_PATTERN = 'HIGH_DRAW_PATTERN' LOW_DRAW_PATTERN = 'LOW_DRAW_PATTERN' MEDIUM_DRAW_PATTERN = 'MEDIUM_DRAW_PATTERN' + NO_COOLING_EQUIPMENT = 'NO_COOLING_EQUIPMENT' + NoneType_None = None class EnergyCreditRenewableTypeOptions(StrEnum): @@ -726,7 +932,7 @@ class EnergyCreditRenewableTypeOptions(StrEnum): PV_ELECTRIC = 'PV_ELECTRIC' -class RenewableExceptionOptions(StrEnum): +class RenewableExceptionOptions(Enum): RENEWABLE_ONSITE_EXCEPTION_NONE = 'RENEWABLE_ONSITE_EXCEPTION_NONE' RENEWABLE_ONSITE_EXCEPTION_LOW_SOLAR_RAD = ( 'RENEWABLE_ONSITE_EXCEPTION_LOW_SOLAR_RAD' @@ -740,6 +946,10 @@ class RenewableExceptionOptions(StrEnum): RENEWABLE_ONSITE_EXCEPTION_LOW_FLOOR_AREA = ( 'RENEWABLE_ONSITE_EXCEPTION_LOW_FLOOR_AREA' ) + RENEWABLE_ONSITE_EXCEPTION_IECC2024_LOW_FLOOR_AREA = ( + 'RENEWABLE_ONSITE_EXCEPTION_IECC2024_LOW_FLOOR_AREA' + ) + NoneType_None = None class RenewableSystemTypeOptions(StrEnum): @@ -844,10 +1054,12 @@ class ThermalBridgeTypeOptions(StrEnum): THERMAL_BRIDGE_WALL_TO_PLANE_TRANSITION = 'THERMAL_BRIDGE_WALL_TO_PLANE_TRANSITION' -class ProjectTypeOptions(StrEnum): +class ProjectTypeOptions(Enum): ADDITION = 'ADDITION' ALTERATION = 'ALTERATION' NEW_CONSTRUCTION = 'NEW_CONSTRUCTION' + NONE = 'NONE' + NoneType_None = None class ProjectSubTypeOptions(StrEnum): @@ -883,6 +1095,10 @@ class StateRegionEnergyCodeOptions(StrEnum): CEZ_ONTARIO = 'CEZ_ONTARIO' CEZ_DC2017 = 'CEZ_DC2017' CEZ_PUERTO_RICO = 'CEZ_PUERTO_RICO' + CEZ_NYS2024_IECC2024 = 'CEZ_NYS2024_IECC2024' + CEZ_NYS2025_9012022 = 'CEZ_NYS2025_9012022' + CEZ_NYC2025_IECC2024 = 'CEZ_NYC2025_IECC2024' + CEZ_NYC2025_9012022 = 'CEZ_NYC2025_9012022' CEZ_NYSTRETCH_90_1_2016 = 'CEZ_NYSTRETCH_90_1_2016' CEZ_NYSTRETCH_NYC_90_1_2016 = 'CEZ_NYSTRETCH_NYC_90_1_2016' CEZ_NYSTRETCH_90_1_11_G = 'CEZ_NYSTRETCH_90_1_11_G' @@ -890,6 +1106,7 @@ class StateRegionEnergyCodeOptions(StrEnum): CEZ_CHICAGO_IECC2022 = 'CEZ_CHICAGO_IECC2022' CEZ_FL = 'CEZ_FL' CEZ_VT = 'CEZ_VT' + CEZ_VT2024_IECC2021 = 'CEZ_VT2024_IECC2021' CEZ_NY = 'CEZ_NY' CEZ_NEWYORKCITY = 'CEZ_NEWYORKCITY' CEZ_NYSTRETCH_NYC_IECC2018 = 'CEZ_NYSTRETCH_NYC_IECC2018' @@ -897,10 +1114,12 @@ class StateRegionEnergyCodeOptions(StrEnum): CEZ_CO_BOULDER = 'CEZ_CO_BOULDER' CEZ_CO_DENVER = 'CEZ_CO_DENVER' CEZ_CO_DENVER_90_1_2016 = 'CEZ_CO_DENVER_90_1_2016' + CEZ_LA2021_IECC2021 = 'CEZ_LA2021_IECC2021' CEZ_MN = 'CEZ_MN' CEZ_MAS = 'CEZ_MAS' CEZ_MAS_STRETCH_IECC2021 = 'CEZ_MAS_STRETCH_IECC2021' CEZ_CO_DENVER_IECC2021 = 'CEZ_CO_DENVER_IECC2021' + NONE = 'NONE' class ComplianceModeOptions(StrEnum): @@ -910,6 +1129,7 @@ class ComplianceModeOptions(StrEnum): class AirBarrierComplianceTypeOptions(StrEnum): + AIR_BARRIER_OPTION_CONTINUITY_PLAN = 'AIR_BARRIER_OPTION_CONTINUITY_PLAN' AIR_BARRIER_OPTION_UNKNOWN = 'AIR_BARRIER_OPTION_UNKNOWN' AIR_BARRIER_OPTION_MATERIAL_PERM = 'AIR_BARRIER_OPTION_MATERIAL_PERM' AIR_BARRIER_OPTION_ASSEMBLY_PERM = 'AIR_BARRIER_OPTION_ASSEMBLY_PERM' @@ -920,6 +1140,7 @@ class RoofTypeOptions(StrEnum): ABOVE_DECK_ROOF = 'ABOVE_DECK_ROOF' METAL_BLDG_STANDING_SEAM_ROOF = 'METAL_BLDG_STANDING_SEAM_ROOF' METAL_BLDG_SCREW_DOWN_ROOF = 'METAL_BLDG_SCREW_DOWN_ROOF' + METAL_ROOF_W_THERMAL_BREAK = 'METAL_ROOF_W_THERMAL_BREAK' WOOD_STD_JOIST_TRUSS = 'WOOD_STD_JOIST_TRUSS' NON_WOOD_JOIST_TRUSS = 'NON_WOOD_JOIST_TRUSS' OTHER_ROOF = 'OTHER_ROOF' @@ -939,6 +1160,7 @@ class RoofInsulationTypeOptions(StrEnum): class HighAlbedoRoofReqTypeOptions(StrEnum): HA_ROOF_REQ_UNSPECIFIED = 'HA_ROOF_REQ_UNSPECIFIED' + HA_ROOF_REQ_SOLAR_REFLECTANCE = 'HA_ROOF_REQ_SOLAR_REFLECTANCE' HA_ROOF_REQ_SOLAR_REFLECTANCE_INDEX = 'HA_ROOF_REQ_SOLAR_REFLECTANCE_INDEX' HA_ROOF_REQ_SOLAR_REFLECTANCE_INDEX_THERMAL_EMITTANCE = ( 'HA_ROOF_REQ_SOLAR_REFLECTANCE_INDEX_THERMAL_EMITTANCE' @@ -964,13 +1186,14 @@ class OtherRoofTypeOptions(StrEnum): ROOF_OTHER_OTHER = 'ROOF_OTHER_OTHER' -class FloorTypeOptions(StrEnum): +class FloorTypeOptions(Enum): ALL_WOOD_JOIST_TRUSS_FLOOR = 'ALL_WOOD_JOIST_TRUSS_FLOOR' NON_WOOD_JOIST_TRUSS_FLOOR = 'NON_WOOD_JOIST_TRUSS_FLOOR' STRUCTURAL_SLAB_FLOOR = 'STRUCTURAL_SLAB_FLOOR' HEATED_SLAB_ON_GRADE = 'HEATED_SLAB_ON_GRADE' UNHEATED_SLAB_ON_GRADE = 'UNHEATED_SLAB_ON_GRADE' OTHER_FLOOR = 'OTHER_FLOOR' + NoneType_None = None class FloorExposedFrameType(StrEnum): @@ -984,9 +1207,20 @@ class SlabInsulationPositionOptions(StrEnum): HORIZONTAL = 'HORIZONTAL' VERTICAL = 'VERTICAL' NO_INSULATION = 'NO_INSULATION' + NONE = 'NONE' class AgWallConstructionDetailsTypeOptions(StrEnum): + AG_WALL_CONSTRUCTION_DETAILS_UNKNOWN = 'AG_WALL_CONSTRUCTION_DETAILS_UNKNOWN' + AG_WALL_CONSTRUCTION_DETAILS_HORIZONTAL_Z_GIRTS = ( + 'AG_WALL_CONSTRUCTION_DETAILS_HORIZONTAL_Z_GIRTS' + ) + AG_WALL_CONSTRUCTION_DETAILS_VERTICAL_Z_GIRTS = ( + 'AG_WALL_CONSTRUCTION_DETAILS_VERTICAL_Z_GIRTS' + ) + AG_WALL_CONSTRUCTION_DETAILS_Z_GIRTS_THERMAL_BROKEN = ( + 'AG_WALL_CONSTRUCTION_DETAILS_Z_GIRTS_THERMAL_BROKEN' + ) AG_WALL_METAL_BLDG_SINGLE_LAYER_MINERAL_FIBER = ( 'AG_WALL_METAL_BLDG_SINGLE_LAYER_MINERAL_FIBER' ) @@ -1021,35 +1255,49 @@ class AgWallOtherTypeOptions(StrEnum): NONE = 'NONE' -class CMUTypeOptions(StrEnum): +class CMUTypeOptions(Enum): CMU_SOLID_GROUT = 'CMU_SOLID_GROUT' CMU_PARTIAL_GROUT_CELLS_EMPTY = 'CMU_PARTIAL_GROUT_CELLS_EMPTY' CMU_PARTIAL_GROUT_CELLS_INSULATED = 'CMU_PARTIAL_GROUT_CELLS_INSULATED' CMU_UNREINFORCED_CELLS_EMPTY = 'CMU_UNREINFORCED_CELLS_EMPTY' CMU_UNREINFORCED_CELLS_INSULATED = 'CMU_UNREINFORCED_CELLS_INSULATED' - - -class ConcreteDensityOptions(IntEnum): - integer_0 = 0 - integer_95 = 95 - integer_115 = 115 - integer_144 = 144 - - -class ConcreteThicknessOptions(IntEnum): - integer_0 = 0 - integer_6 = 6 - integer_8 = 8 - integer_10 = 10 - integer_12 = 12 + NONE = 'NONE' + NoneType_None = None + + +class ConcreteDensityOptions(Enum): + int_0 = 0 + int_85 = 85 + int_95 = 95 + int_115 = 115 + int_135 = 135 + int_144 = 144 + NoneType_None = None + + +class ConcreteThicknessOptions(Enum): + int_0 = 0 + int_3 = 3 + int_4 = 4 + int_5 = 5 + int_6 = 6 + int_7 = 7 + int_8 = 8 + int_9 = 9 + int_10 = 10 + int_11 = 11 + int_12 = 12 + NoneType_None = None class EnvelopeAssemblyExemptionOptions(StrEnum): ENV_EXEMPTION_NONE = 'ENV_EXEMPTION_NONE' ENV_EXEMPTION_DYNAMIC_GLAZING = 'ENV_EXEMPTION_DYNAMIC_GLAZING' + NONE = 'NONE' class FurringTypeOptions(StrEnum): + NONE = 'NONE' WOOD_FURRING = 'WOOD_FURRING' METAL_FURRING = 'METAL_FURRING' NO_FURRING = 'NO_FURRING' @@ -1062,22 +1310,37 @@ class InsulationPositionOptions(StrEnum): NO_INSULATION = 'NO_INSULATION' -class FenestrationFrameTypeOptions(StrEnum): +class FenestrationFrameTypeOptions(Enum): METAL = 'METAL' METAL_W_THERMAL_BREAK = 'METAL_W_THERMAL_BREAK' WOOD = 'WOOD' VINYL = 'VINYL' CURTAIN_WALL = 'CURTAIN_WALL' CURTAIN_WALL_STOREFRONT = 'CURTAIN_WALL_STOREFRONT' + NON_METAL = 'NON_METAL' + NONE = 'NONE' OTHER_FRAME = 'OTHER_FRAME' + METAL_FRAME_24_AG_WALL = 'METAL_FRAME_24_AG_WALL' + GLASS_DOOR = 'GLASS_DOOR' + METAL_THERMAL_BREAK = 'METAL_THERMAL_BREAK' + OTHER_DOOR = 'OTHER_DOOR' + INSUL_METAL_DOOR = 'INSUL_METAL_DOOR' + NO_INSUL_SINGLE_METAL_DOOR = 'NO_INSUL_SINGLE_METAL_DOOR' + WOOD_FRAME_16_AG_WALL = 'WOOD_FRAME_16_AG_WALL' + ALL_WOOD_JOIST_TRUSS_FLOOR = 'ALL_WOOD_JOIST_TRUSS_FLOOR' + METAL_FRAME_16_AG_WALL = 'METAL_FRAME_16_AG_WALL' + WOOD_DOOR = 'WOOD_DOOR' + NoneType_None = None class GlazingTypeOptions(StrEnum): SINGLE_PANE = 'SINGLE_PANE' DOUBLE_PANE = 'DOUBLE_PANE' DOUBLE_PANE_LOWE = 'DOUBLE_PANE_LOWE' + OTHER_GLAZING = 'OTHER_GLAZING' TRIPLE_PANE = 'TRIPLE_PANE' TRIPLE_PANE_LOWE = 'TRIPLE_PANE_LOWE' + NONE = 'NONE' class SolarTypeOptions(StrEnum): @@ -1085,6 +1348,7 @@ class SolarTypeOptions(StrEnum): TINTED = 'TINTED' REFLECTIVE = 'REFLECTIVE' OTHER_SOLAR = 'OTHER_SOLAR' + NONE = 'NONE' class WindowProductionTypeOptions(StrEnum): @@ -1093,9 +1357,11 @@ class WindowProductionTypeOptions(StrEnum): class PerfDataTypeOptions(StrEnum): + NONE = 'NONE' PERF_TYPE_NFRC = 'PERF_TYPE_NFRC' PERF_TYPE_OTHER = 'PERF_TYPE_OTHER' PERF_TYPE_DEFAULT = 'PERF_TYPE_DEFAULT' + PERF_TYPE_UNSPECIFIED = 'PERF_TYPE_UNSPECIFIED' class WindowOpenTypeOptions(StrEnum): @@ -1111,6 +1377,7 @@ class SkylightCurbTypeOptions(StrEnum): class GlazingMaterialTypeOptions(StrEnum): GLASS_GLAZING_MAT = 'GLASS_GLAZING_MAT' PLASTIC_GLAZING_MAT = 'PLASTIC_GLAZING_MAT' + NONE = 'NONE' class DoorTypeOptions(StrEnum): @@ -1119,6 +1386,7 @@ class DoorTypeOptions(StrEnum): INSUL_METAL_DOOR = 'INSUL_METAL_DOOR' WOOD_DOOR = 'WOOD_DOOR' GLASS_DOOR = 'GLASS_DOOR' + METAL_W_THERMAL_BREAK = 'METAL_W_THERMAL_BREAK' OTHER_DOOR = 'OTHER_DOOR' UPWARD_ACTING_SECTIONAL = 'UPWARD_ACTING_SECTIONAL' @@ -1140,10 +1408,14 @@ class DoorGlazingFrameTypeOptions(StrEnum): NON_METAL = 'NON_METAL' -class LightingAllowanceTypeOptions(StrEnum): +class LightingAllowanceTypeOptions(Enum): ALLOWANCE_NONE = 'ALLOWANCE_NONE' + ALLOWANCE_ADVANCED_CONTROLS = 'ALLOWANCE_ADVANCED_CONTROLS' ALLOWANCE_DECORATIVE_APPEARANCE = 'ALLOWANCE_DECORATIVE_APPEARANCE' + ALLOWANCE_DECORATIVE_APPEARANCE_LOBBIES = 'ALLOWANCE_DECORATIVE_APPEARANCE_LOBBIES' + ALLOWANCE_DECORATIVE_APPEARANCE_OTHER = 'ALLOWANCE_DECORATIVE_APPEARANCE_OTHER' ALLOWANCE_DISPLAY_TERMINAL = 'ALLOWANCE_DISPLAY_TERMINAL' + ALLOWANCE_ELECTRICAL_MECHANICAL = 'ALLOWANCE_ELECTRICAL_MECHANICAL' ALLOWANCE_VEHICLE_SPORT_ELECT_HIGHLIGHT = 'ALLOWANCE_VEHICLE_SPORT_ELECT_HIGHLIGHT' ALLOWANCE_FURNITURE_CLOTHES_COSMETIC_HIGHLIGHT = ( 'ALLOWANCE_FURNITURE_CLOTHES_COSMETIC_HIGHLIGHT' @@ -1152,32 +1424,56 @@ class LightingAllowanceTypeOptions(StrEnum): 'ALLOWANCE_JEWELRY_CRYSTAL_CHINA_HIGHLIGHT' ) ALLOWANCE_OTHER_HIGHLIGHT = 'ALLOWANCE_OTHER_HIGHLIGHT' + ALLOWANCE_VIDEO_CONFERENCE = 'ALLOWANCE_VIDEO_CONFERENCE' + NONE = 'NONE' + NoneType_None = None class LightingExemptionTypeOptions(StrEnum): EXEMPTION_NONE = 'EXEMPTION_NONE' EXEMPTION_ADVERTISING_OR_DIRECTION_SIGN = 'EXEMPTION_ADVERTISING_OR_DIRECTION_SIGN' EXEMPTION_ATHLETIC_PLAY_AREA = 'EXEMPTION_ATHLETIC_PLAY_AREA' + EXEMPTION_APPROVED_SAFETY = 'EXEMPTION_APPROVED_SAFETY' EXEMPTION_CASINO_GAMING = 'EXEMPTION_CASINO_GAMING' EXEMPTION_DRESSING_ROOM_MIRROR = 'EXEMPTION_DRESSING_ROOM_MIRROR' + EXEMPTION_DWELL_UNIT_CONTROLLED = 'EXEMPTION_DWELL_UNIT_CONTROLLED' EXEMPTION_EDUCATION = 'EXEMPTION_EDUCATION' + EXEMPTION_EMERGENCY_AUTOOFF = 'EXEMPTION_EMERGENCY_AUTOOFF' EXEMPTION_EMERGENCY_LIGHT = 'EXEMPTION_EMERGENCY_LIGHT' EXEMPTION_EQUIPMENT = 'EXEMPTION_EQUIPMENT' EXEMPTION_EXIT_SIGN = 'EXEMPTION_EXIT_SIGN' EXEMPTION_FOOD_PREPERATION = 'EXEMPTION_FOOD_PREPERATION' EXEMPTION_FURNITURE_SUPPLEMENTAL = 'EXEMPTION_FURNITURE_SUPPLEMENTAL' + EXEMPTION_HIGHLIGHT_HAZARDS = 'EXEMPTION_HIGHLIGHT_HAZARDS' EXEMPTION_HIGHLIGHT_LANDMARK = 'EXEMPTION_HIGHLIGHT_LANDMARK' + EXEMPTION_INDUSTRIAL_PRODUCTION = 'EXEMPTION_INDUSTRIAL_PRODUCTION' + EXEMPTION_MANUFACTURER_AS_PART_OF_EQUIP = 'EXEMPTION_MANUFACTURER_AS_PART_OF_EQUIP' EXEMPTION_MEDICAL_PROCEDURE = 'EXEMPTION_MEDICAL_PROCEDURE' EXEMPTION_PARKING_GARAGE_TRANSITION = 'EXEMPTION_PARKING_GARAGE_TRANSITION' EXEMPTION_PLANT = 'EXEMPTION_PLANT' + EXEMPTION_POOLS_WATER = 'EXEMPTION_POOLS_WATER' EXEMPTION_REFRIGERATOR = 'EXEMPTION_REFRIGERATOR' EXEMPTION_RELIGIOUS_PULPIT_CHOIR = 'EXEMPTION_RELIGIOUS_PULPIT_CHOIR' + EXEMPTION_REQUIRED_EGRESS = 'EXEMPTION_REQUIRED_EGRESS' EXEMPTION_RETAIL_DISPLAY = 'EXEMPTION_RETAIL_DISPLAY' + EXEMPTION_TEMP_LIGHTING = 'EXEMPTION_TEMP_LIGHTING' EXEMPTION_THEATER_APPLICATION = 'EXEMPTION_THEATER_APPLICATION' + EXEMPTION_THEME_PARK_ELEMENTS = 'EXEMPTION_THEME_PARK_ELEMENTS' EXEMPTION_HEALTH_SAFETY_REG = 'EXEMPTION_HEALTH_SAFETY_REG' + EXEMPTION_HIGHLIGHT_MONUMENT = 'EXEMPTION_HIGHLIGHT_MONUMENT' + EXEMPTION_TRANSPORTATION_MARKER = 'EXEMPTION_TRANSPORTATION_MARKER' + EXEMPTION_TRANSPORTATION_SITE = 'EXEMPTION_TRANSPORTATION_SITE' + EXEMPTION_EMERGENCY_LIGHT_OFF_NORMAL_BUSINESS_HRS = ( + 'EXEMPTION_EMERGENCY_LIGHT_OFF_NORMAL_BUSINESS_HRS' + ) + EXEMPTION_MUSEUM_DISPLAY = 'EXEMPTION_MUSEUM_DISPLAY' + EXEMPTION_SEARCHLIGHTS = 'EXEMPTION_SEARCHLIGHTS' + EXEMPTION_SLEEPING_UNIT = 'EXEMPTION_SLEEPING_UNIT' + EXEMPTION_VISUALLY_IMPAIRED = 'EXEMPTION_VISUALLY_IMPAIRED' -class TrackLightingWattageBasisTypeOptions(StrEnum): +class TrackLightingWattageBasisTypeOptionsEnum(StrEnum): + NONE = 'NONE' WATTAGE_BASIS_NOT_SET = 'WATTAGE_BASIS_NOT_SET' TRACK_BASIS = 'TRACK_BASIS' CIRCUIT_BREAKER_CAPACITY = 'CIRCUIT_BREAKER_CAPACITY' @@ -1185,6 +1481,12 @@ class TrackLightingWattageBasisTypeOptions(StrEnum): TRANSFORMER_CAPACITY = 'TRANSFORMER_CAPACITY' +class TrackLightingWattageBasisTypeOptions( + RootModel[TrackLightingWattageBasisTypeOptionsEnum | None | MISSING] +): + root: TrackLightingWattageBasisTypeOptionsEnum | None | MISSING = MISSING + + class ExteriorLightingZoneTypeOptions(StrEnum): EXT_ZONE_UNSPECIFIED = 'EXT_ZONE_UNSPECIFIED' EXT_ZONE_RESIDENTIAL = 'EXT_ZONE_RESIDENTIAL' @@ -1196,6 +1498,7 @@ class ExteriorLightingZoneTypeOptions(StrEnum): EXT_ZONE_FOREST = 'EXT_ZONE_FOREST' EXT_ZONE_RURAL = 'EXT_ZONE_RURAL' EXT_ZONE_OTHER = 'EXT_ZONE_OTHER' + EXT_ZONE_UNDEVELOPED = 'EXT_ZONE_UNDEVELOPED' class ThermalBridgeCategoryOptions(StrEnum): @@ -1204,10 +1507,11 @@ class ThermalBridgeCategoryOptions(StrEnum): THERMAL_BRIDGE_POINT = 'THERMAL_BRIDGE_POINT' -class ThermalBridgeComplianceTypeOptions(StrEnum): +class ThermalBridgeComplianceTypeOptions(Enum): THERMAL_BRIDGE_NON_PRESCRIPTIVE = 'THERMAL_BRIDGE_NON_PRESCRIPTIVE' THERMAL_BRIDGE_PRESCRIPTIVE = 'THERMAL_BRIDGE_PRESCRIPTIVE' - field__THERMAL_BRIDGE_AS_DESIGNED = ' THERMAL_BRIDGE_AS_DESIGNED' + THERMAL_BRIDGE_AS_DESIGNED = 'THERMAL_BRIDGE_AS_DESIGNED' + NoneType_None = None class ThermalBridgeExceptionTypeOptions(StrEnum): @@ -1279,6 +1583,7 @@ class EconomizerTypeOptions(StrEnum): NO_ECONOMIZER = 'NO_ECONOMIZER' AIR_ECONOMIZER = 'AIR_ECONOMIZER' WATER_ECONOMIZER = 'WATER_ECONOMIZER' + FLUID_ECONOMIZER = 'FLUID_ECONOMIZER' class EconomizerExceptionOptions(StrEnum): @@ -1307,15 +1612,17 @@ class FanControlOptions(StrEnum): OTHER_FAN_CONTROL = 'OTHER_FAN_CONTROL' -class FuelTypeOptions(StrEnum): +class FuelTypeOptions(Enum): UNKNOWN_FUEL = 'UNKNOWN_FUEL' GAS = 'GAS' ELECTRIC = 'ELECTRIC' OIL = 'OIL' + OIL_RESIDUAL = 'OIL_RESIDUAL' PROPANE = 'PROPANE' HOTWATER = 'HOTWATER' STEAM = 'STEAM' OTHER_FOSSIL_FUEL = 'OTHER_FOSSIL_FUEL' + NoneType_None = None class SpaceHeatingSystemExceptionOptions(StrEnum): @@ -1410,48 +1717,54 @@ class HeatPumpTypeOptions(StrEnum): ) -class ChillerTypeOptions(StrEnum): +class ChillerTypeOptions(Enum): NO_CHILLER = 'NO_CHILLER' CENTRIFUGAL = 'CENTRIFUGAL' + CENTRIFUGAL_NON_STANDARD = 'CENTRIFUGAL_NON_STANDARD' ROTARY_SCREW_OR_SCROLL = 'ROTARY_SCREW_OR_SCROLL' RECIPROCATING = 'RECIPROCATING' SINGLE_EFFECT_ABSORPTION = 'SINGLE_EFFECT_ABSORPTION' DOUBLE_EFFECT_ABSORPTION_DIRECT_FIRED = 'DOUBLE_EFFECT_ABSORPTION_DIRECT_FIRED' DOUBLE_EFFECT_ABSORPTION_INDIRECT_FIRED = 'DOUBLE_EFFECT_ABSORPTION_INDIRECT_FIRED' + NoneType_None = None -class CoolingPlantTypeOptions(StrEnum): +class CoolingPlantTypeOptions(Enum): UNKNOWN_PLANT_COOLING = 'UNKNOWN_PLANT_COOLING' NO_PLANT_COOLING = 'NO_PLANT_COOLING' CONDENSER_UNIT = 'CONDENSER_UNIT' WATER_CHILLER = 'WATER_CHILLER' HEATPUMP_CHILLER = 'HEATPUMP_CHILLER' + NoneType_None = None -class HeatingPlantTypeOptions(StrEnum): +class HeatingPlantTypeOptions(Enum): UNKNOWN_PLANT_HEATING = 'UNKNOWN_PLANT_HEATING' NO_PLANT_HEATING = 'NO_PLANT_HEATING' HOTWATER_PLANT = 'HOTWATER_PLANT' STEAM_PLANT = 'STEAM_PLANT' + NoneType_None = None -class HeatPumpChillerHeatingSourceConditionOptions(StrEnum): +class HeatPumpChillerHeatingSourceConditionOptions(Enum): UNKNOWN_SOURCE = 'UNKNOWN_SOURCE' AIRSOURCE_47DB_43WB = 'AIRSOURCE_47DB_43WB' AIRSOURCE_17DB_15WB = 'AIRSOURCE_17DB_15WB' WATERSOURCE_54F_44F = 'WATERSOURCE_54F_44F' WATERSOURCE_75F_65F = 'WATERSOURCE_75F_65F' + NoneType_None = None -class HeatPumpChillerLeavingHeatingWaterTempOptions(StrEnum): +class HeatPumpChillerLeavingHeatingWaterTempOptions(Enum): UNKNOWN_TEMPERATURE = 'UNKNOWN_TEMPERATURE' LOW = 'LOW' MEDIUM = 'MEDIUM' HIGH = 'HIGH' BOOST = 'BOOST' + NoneType_None = None -class HeatPumpChillerTypeOptions(StrEnum): +class HeatPumpChillerTypeOptions(Enum): NO_HP_CHILLER = 'NO_HP_CHILLER' AIRSOURCE_HEATPUMP_CHILLER = 'AIRSOURCE_HEATPUMP_CHILLER' WATERSOURCE_POSITIVE_DISPLACEMENT_HEATPUMP_CHILLER = ( @@ -1460,9 +1773,10 @@ class HeatPumpChillerTypeOptions(StrEnum): WATERSOURCE_CENTRIFUGAL_HEATPUMP_CHILLER = ( 'WATERSOURCE_CENTRIFUGAL_HEATPUMP_CHILLER' ) + NoneType_None = None -class HeatRejectionTypeOptions(StrEnum): +class HeatRejectionTypeOptions(Enum): UNKNOWN_HEAT_REJECTION_DEVICE = 'UNKNOWN_HEAT_REJECTION_DEVICE' NO_HEAT_REJECTION_DEVICE = 'NO_HEAT_REJECTION_DEVICE' AIR_COOLED_CONDENSER = 'AIR_COOLED_CONDENSER' @@ -1496,6 +1810,7 @@ class HeatRejectionTypeOptions(StrEnum): 'CENTRIFUGAL_FAN_FAN_EVAPORATIVE_CONDENSER_AMMONIA' ) PROPELLER_OR_AXIAL_FAN_DRY_COOLER = 'PROPELLER_OR_AXIAL_FAN_DRY_COOLER' + NoneType_None = None class PlantTypeOptions(StrEnum): @@ -1509,9 +1824,12 @@ class CompliancePathOptions(StrEnum): pathA = 'pathA' pathB = 'pathB' NA = 'NA' + COMPLIANCE_PATH_A = 'COMPLIANCE_PATH_A' + COMPLIANCE_PATH_B = 'COMPLIANCE_PATH_B' + COMPLIANCE_PATH_UNKNOWN = 'COMPLIANCE_PATH_UNKNOWN' -class EquipmentEfficiencyRequirementExceptionOptions(StrEnum): +class EquipmentEfficiencyRequirementExceptionOptions(Enum): EFF_EXCEPTION_BOILER_RADIANT_PERIMETER = 'EFF_EXCEPTION_BOILER_RADIANT_PERIMETER' EFF_EXCEPTION_BOILER_DWELLING_UNIT = 'EFF_EXCEPTION_BOILER_DWELLING_UNIT' EFF_EXCEPTION_BOILER_RENEWABLE_RECOVERED_ENERGY = ( @@ -1524,6 +1842,7 @@ class EquipmentEfficiencyRequirementExceptionOptions(StrEnum): 'EFF_EXCEPTION_WATER_HEATER_RENEWABLE_RECOVERED_ENERGY' ) EFF_EXCEPTION_UNSPECIFIED = 'EFF_EXCEPTION_UNSPECIFIED' + NoneType_None = None class FanTypeOptions(StrEnum): @@ -1558,11 +1877,12 @@ class SWHSystemTypeOptions(StrEnum): STORAGE_WATER_HEATER = 'STORAGE_WATER_HEATER' -class SWHFuelTypeOptions(StrEnum): +class SWHFuelTypeOptions(Enum): UNKNOWN_FUEL = 'UNKNOWN_FUEL' GAS = 'GAS' ELECTRIC = 'ELECTRIC' OIL = 'OIL' + NoneType_None = None class GSHPFieldSourceCapacityTypeOptions(StrEnum): @@ -1992,7 +2312,7 @@ class RenewableSystemCapacityUnitOptions(StrEnum): RENEWABLE_SYSTEM_UNIT_BTUH = 'RENEWABLE_SYSTEM_UNIT_BTUH' -class BallastTypeOptions(StrEnum): +class BallastTypeOptions(Enum): ELECTRONIC = 'ELECTRONIC' MAGNETIC = 'MAGNETIC' HYBRID = 'HYBRID' @@ -2001,6 +2321,7 @@ class BallastTypeOptions(StrEnum): STANDARD = 'STANDARD' PREMIUM_EFF = 'PREMIUM_EFF' DIMMING = 'DIMMING' + NoneType_None = None class Control(CustomBaseModel): @@ -2008,11 +2329,11 @@ class Control(CustomBaseModel): extra='ignore', ) id: Annotated[ - str | int | None, + str | int | MISSING, Field( description='Scope-unique reference identifier for instances of this data group.' ), - ] = None + ] = MISSING version: Annotated[ str | None, Field(description='Software version - shall always set to an empty string'), @@ -2022,7 +2343,7 @@ class Control(CustomBaseModel): Field(description='Energy code types'), ] complianceMode: Annotated[ - ComplianceModeOptions | None, Field(description='Project compliance type') + ComplianceModeOptions, Field(description='Project compliance type') ] = 'UA' @@ -2031,19 +2352,15 @@ class Requirements(CustomBaseModel): extra='ignore', ) id: Annotated[ - str | int | None, + str | int | MISSING, Field( description='Scope-unique reference identifier for instances of this data group.' ), - ] = None - category: Annotated[ - str | None, Field(description='Requirement Answer - category') - ] = '' - requirementName: Annotated[ - str | None, Field(description='Requirement Answer - name') - ] = '' + ] = MISSING + category: Annotated[str, Field(description='Requirement Answer - category')] = '' + requirementName: Annotated[str, Field(description='Requirement Answer - name')] = '' status: Annotated[ - RequirementAnswerStatus | None, Field(description='Requirement Answer - status') + RequirementAnswerStatus, Field(description='Requirement Answer - status') ] = '' locationOnPlans: Annotated[ str | None, Field(description='Requirement Answer - Location On Plans') @@ -2058,13 +2375,14 @@ class Window(CustomBaseModel): extra='ignore', ) id: Annotated[ - str | int | None, + str | int | MISSING, Field( description='Scope-unique reference identifier for instances of this data group.' ), - ] = None + ] = MISSING bldgUseKey: Annotated[ - str, Field(description='key reference of the building use area data group') + str | None, + Field(description='key reference of the building use area data group'), ] description: Annotated[ str | None, Field(description='The name of the component') @@ -2074,20 +2392,20 @@ class Window(CustomBaseModel): Field(description='Space type of the adjacent space'), ] = None adjacentSpaceBuildingType: Annotated[ - WholeBuildingTypeOptions | None, + WholeBuildingTypeOptions | MISSING, Field(description='Building type of the adjacent space'), - ] = None - assemblyType: Annotated[ - str | None, Field(description='The type of the component') - ] = 'Window' + ] = MISSING + assemblyType: Annotated[str, Field(description='The type of the component')] = ( + 'Window' + ) propUValue: Annotated[ - float | None, - Field(description='Proposed thermal transmittance of the window.', ge=0.0), + float | None, Field(description='Proposed thermal transmittance of the window.') ] = 0.0 grossArea: Annotated[float | None, Field(description='gross area', ge=0.0)] = 0.0 altExemptType: Annotated[ - AltExemptTypeOptions | None, Field(description='alteration exemption type') - ] = None + AltExemptTypeOptions | None | MISSING, + Field(description='alteration exemption type'), + ] = MISSING propShgc: Annotated[ float | None, Field(description='Proposed solar heat gain coefficient', ge=0.0) ] = 0.0 @@ -2095,7 +2413,7 @@ class Window(CustomBaseModel): float | None, Field(description='Proposed window projection factor', ge=0.0) ] = 0.0 frameType: Annotated[ - FenestrationFrameTypeOptions | None, Field(description='Window frame type') + FenestrationFrameTypeOptions, Field(description='Window frame type') ] = None glazingType: Annotated[ GlazingTypeOptions | None, @@ -2111,7 +2429,7 @@ class Window(CustomBaseModel): description='Solar coating type - used when performance data option is energy code default (PERF_TYPE_DEFAULT)' ), ] = None - orientation: OrientationOptions | None = 'UNSPECIFIED_ORIENTATION' + orientation: OrientationOptions = 'UNSPECIFIED_ORIENTATION' glazingMaterialType: Annotated[ GlazingMaterialTypeOptions | None, Field(description='Glazing material type') ] = None @@ -2119,13 +2437,16 @@ class Window(CustomBaseModel): WindowProductionTypeOptions | None, Field(description='Product Type') ] = None allowanceType: Annotated[ - EnvelopeAssemblyAllowanceTypeOptions | None, Field(description='allowance type') - ] = None + EnvelopeAssemblyAllowanceTypeOptions, Field(description='allowance type') + ] exemptionType: EnvelopeAssemblyExemptionOptions | None = None + feetAg: Annotated[float | None, Field(description='Feet above grade', ge=0.0)] = ( + None + ) constructionType: Annotated[ - ConstructionTypeOptions | None, + ConstructionTypeOptions | None | MISSING, Field(description='Construction types - compliance code specification'), - ] = None + ] = MISSING isSiteShading: Annotated[bool | None, Field(description='Is the site shaded')] = ( None ) @@ -2133,16 +2454,15 @@ class Window(CustomBaseModel): PerfDataTypeOptions | None, Field(description='Performance data type option') ] = None productId: Annotated[str | None, Field(description='Product ID')] = None - preAltPropUval: Annotated[float | None, Field(ge=0.0)] = 0.0 + preAltPropUval: Annotated[float | None | MISSING, Field(ge=0.0)] = MISSING windowOpenType: Annotated[ WindowOpenTypeOptions | None, Field(description='Window open type') ] = None - cavityRValue: Annotated[float | None, Field(ge=0.0)] = 0.0 + cavityRValue: float | None = 0.0 continuousRValue: Annotated[ float | None, Field( - description='Continuous insulation on the door. Can be exterior or interior or both.', - ge=0.0, + description='Continuous insulation on the door. Can be exterior or interior or both.' ), ] = 0.0 @@ -2152,36 +2472,37 @@ class Door(CustomBaseModel): extra='ignore', ) id: Annotated[ - str | int | None, + str | int | MISSING, Field( description='Scope-unique reference identifier for instances of this data group.' ), - ] = None + ] = MISSING bldgUseKey: Annotated[ - str, Field(description='key reference of the building use area data group') + str | None, + Field(description='key reference of the building use area data group'), ] description: Annotated[ str | None, Field(description='The name of the component') ] = '' - assemblyType: Annotated[ - str | None, Field(description='The type of the component') - ] = 'Door' + assemblyType: Annotated[str, Field(description='The type of the component')] = ( + 'Door' + ) adjacentSpaceType: Annotated[ AdjacentSpaceTypeOptions | None, Field(description='Space type of the adjacent space'), ] = None adjacentSpaceBuildingType: Annotated[ - WholeBuildingTypeOptions | None, + WholeBuildingTypeOptions | MISSING, Field(description='Building type of the adjacent space'), - ] = None + ] = MISSING propUValue: Annotated[ - float | None, - Field(description='Proposed thermal transmittance of the window.', ge=0.0), + float | None, Field(description='Proposed thermal transmittance of the window.') ] = 0.0 grossArea: Annotated[float | None, Field(description='gross area', ge=0.0)] = None altExemptType: Annotated[ - AltExemptTypeOptions | None, Field(description='alteration exemption type') - ] = None + AltExemptTypeOptions | None | MISSING, + Field(description='alteration exemption type'), + ] = MISSING propShgc: Annotated[ float | None, Field(description='Proposed solar heat gain coefficient', ge=0.0) ] = 0.0 @@ -2189,7 +2510,7 @@ class Door(CustomBaseModel): float | None, Field(description='Proposed window projection factor', ge=0.0) ] = 0.0 frameType: Annotated[ - FenestrationFrameTypeOptions | None, Field(description='Glass door frame type') + FenestrationFrameTypeOptions, Field(description='Glass door frame type') ] = None glazingType: Annotated[ GlazingTypeOptions | None, @@ -2205,7 +2526,7 @@ class Door(CustomBaseModel): description='Solar coating type - used when performance data option is energy code default (PERF_TYPE_DEFAULT)' ), ] = None - orientation: OrientationOptions | None = 'UNSPECIFIED_ORIENTATION' + orientation: OrientationOptions = 'UNSPECIFIED_ORIENTATION' glazingMaterialType: Annotated[ GlazingMaterialTypeOptions | None, Field(description='Glazing material type') ] = None @@ -2213,13 +2534,13 @@ class Door(CustomBaseModel): WindowProductionTypeOptions | None, Field(description='Product Type') ] = None allowanceType: Annotated[ - EnvelopeAssemblyAllowanceTypeOptions | None, Field(description='allowance type') - ] = None + EnvelopeAssemblyAllowanceTypeOptions, Field(description='allowance type') + ] exemptionType: EnvelopeAssemblyExemptionOptions | None = None constructionType: Annotated[ - ConstructionTypeOptions | None, + ConstructionTypeOptions | None | MISSING, Field(description='Construction types - compliance code specification'), - ] = None + ] = MISSING isSiteShading: Annotated[bool | None, Field(description='Is the site shaded')] = ( None ) @@ -2227,7 +2548,7 @@ class Door(CustomBaseModel): PerfDataTypeOptions | None, Field(description='Performance data type option') ] = None productId: Annotated[str | None, Field(description='Product ID')] = None - preAltPropUval: Annotated[float | None, Field(ge=0.0)] = 0.0 + preAltPropUval: Annotated[float | None | MISSING, Field(ge=0.0)] = MISSING doorType: Annotated[DoorTypeOptions | None, Field(description='Door types')] = None doorOpenType: Annotated[ DoorOpenTypeOptions | None, Field(description='Door open types') @@ -2235,12 +2556,11 @@ class Door(CustomBaseModel): doorEntranceType: Annotated[ DoorEntranceTypeOptions | None, Field(description='Door entrance types') ] = None - cavityRValue: Annotated[float | None, Field(ge=0.0)] = 0.0 + cavityRValue: float | None = 0.0 continuousRValue: Annotated[ float | None, Field( - description='Continuous insulation on the door. Can be exterior or interior or both.', - ge=0.0, + description='Continuous insulation on the door. Can be exterior or interior or both.' ), ] = 0.0 @@ -2250,37 +2570,50 @@ class Skylight(CustomBaseModel): extra='ignore', ) id: Annotated[ - str | int | None, + str | int | MISSING, Field( description='Scope-unique reference identifier for instances of this data group.' ), - ] = None + ] = MISSING bldgUseKey: Annotated[ - str, Field(description='key reference of the building use area data group') + str | None, + Field(description='key reference of the building use area data group'), ] + cavityRValue: Annotated[ + float | None, + Field( + description='Average insulation R-value in the cavity between two studs.' + ), + ] = 0.0 + continuousRValue: Annotated[ + float | None, + Field( + description='Continuous insulation on the skylight. Can be exterior or interior or both.' + ), + ] = 0.0 description: Annotated[ str | None, Field(description='The name of the component') ] = '' - assemblyType: Annotated[ - str | None, Field(description='The type of the component') - ] = 'Skylight' + assemblyType: Annotated[str, Field(description='The type of the component')] = ( + 'Skylight' + ) adjacentSpaceType: Annotated[ AdjacentSpaceTypeOptions | None, Field(description='Space type of the adjacent space'), ] = None adjacentSpaceBuildingType: Annotated[ - WholeBuildingTypeOptions | None, + WholeBuildingTypeOptions | MISSING, Field(description='Building type of the adjacent space'), - ] = None + ] = MISSING propUValue: Annotated[ - float | None, - Field(description='Proposed thermal transmittance of the window.', ge=0.0), + float | None, Field(description='Proposed thermal transmittance of the window.') ] = 0.0 grossArea: Annotated[float | None, Field(description='gross area', ge=0.0)] = 0.0 - orientation: OrientationOptions | None = 'UNSPECIFIED_ORIENTATION' + orientation: OrientationOptions = 'UNSPECIFIED_ORIENTATION' altExemptType: Annotated[ - AltExemptTypeOptions | None, Field(description='alteration exemption type') - ] = None + AltExemptTypeOptions | None | MISSING, + Field(description='alteration exemption type'), + ] = MISSING propShgc: Annotated[ float | None, Field(description='Proposed solar heat gain coefficient', ge=0.0) ] = 0.0 @@ -2288,7 +2621,7 @@ class Skylight(CustomBaseModel): float | None, Field(description='Proposed window projection factor', ge=0.0) ] = 0.0 frameType: Annotated[ - FenestrationFrameTypeOptions | None, Field(description='Window frame type') + FenestrationFrameTypeOptions, Field(description='Window frame type') ] = None glazingType: Annotated[ GlazingTypeOptions | None, @@ -2311,13 +2644,13 @@ class Skylight(CustomBaseModel): WindowProductionTypeOptions | None, Field(description='Product Type') ] = None allowanceType: Annotated[ - EnvelopeAssemblyAllowanceTypeOptions | None, Field(description='allowance type') - ] = None + EnvelopeAssemblyAllowanceTypeOptions, Field(description='allowance type') + ] exemptionType: EnvelopeAssemblyExemptionOptions | None = None constructionType: Annotated[ - ConstructionTypeOptions | None, + ConstructionTypeOptions | None | MISSING, Field(description='Construction types - compliance code specification'), - ] = None + ] = MISSING isSiteShading: Annotated[bool | None, Field(description='Is the site shaded')] = ( None ) @@ -2325,7 +2658,7 @@ class Skylight(CustomBaseModel): PerfDataTypeOptions | None, Field(description='Performance data type option') ] = None productId: Annotated[str | None, Field(description='Product ID')] = None - preAltPropUval: Annotated[float | None, Field(ge=0.0)] = 0.0 + preAltPropUval: Annotated[float | None | MISSING, Field(ge=0.0)] = MISSING curbType: Annotated[ SkylightCurbTypeOptions | None, Field(description='Skylight curb type') ] = None @@ -2336,56 +2669,54 @@ class Roof(CustomBaseModel): extra='ignore', ) id: Annotated[ - str | int | None, + str | int | MISSING, Field( description='Scope-unique reference identifier for instances of this data group.' ), - ] = None + ] = MISSING description: Annotated[ str | None, Field(description='The name of the component') ] = '' - assemblyType: Annotated[ - str | None, Field(description='The type of the component') - ] = 'Roof' + assemblyType: Annotated[str, Field(description='The type of the component')] = ( + 'Roof' + ) bldgUseKey: Annotated[ - str, Field(description='key reference of the building use area data group') + str | None, + Field(description='key reference of the building use area data group'), ] adjacentSpaceType: Annotated[ AdjacentSpaceTypeOptions | None, Field(description='Space type of the adjacent space'), ] = None adjacentSpaceBuildingType: Annotated[ - WholeBuildingTypeOptions | None, + WholeBuildingTypeOptions | MISSING, Field(description='Building type of the adjacent space'), - ] = None + ] = MISSING allowanceType: Annotated[ - EnvelopeAssemblyAllowanceTypeOptions | None, Field(description='allowance type') - ] = None + EnvelopeAssemblyAllowanceTypeOptions, Field(description='allowance type') + ] constructionType: Annotated[ - ConstructionTypeOptions | None, + ConstructionTypeOptions | None | MISSING, Field(description='Construction types - compliance code specification'), - ] = None + ] = MISSING exemptionType: EnvelopeAssemblyExemptionOptions | None = None - orientation: OrientationOptions | None = 'UNSPECIFIED_ORIENTATION' + orientation: OrientationOptions = 'UNSPECIFIED_ORIENTATION' skylight: Annotated[list[Skylight], Field(description='Skylights on the roof')] - cavityRValue: Annotated[float | None, Field(ge=0.0)] = 0.0 + cavityRValue: float | None = 0.0 continuousRValue: Annotated[ float | None, Field( - description='Continuous insulation on the above grade wall. Can be exterior or interior or both.', - ge=0.0, + description='Continuous insulation on the above grade wall. Can be exterior or interior or both.' ), ] = 0.0 propUValue: Annotated[ float | None, - Field( - description='Proposed thermal transmittance of the above grade wall.', - ge=0.0, - ), + Field(description='Proposed thermal transmittance of the above grade wall.'), ] = 0.0 altExemptType: Annotated[ - AltExemptTypeOptions | None, Field(description='alteration exemption type') - ] = None + AltExemptTypeOptions | None | MISSING, + Field(description='alteration exemption type'), + ] = MISSING grossArea: Annotated[float | None, Field(description='gross area', ge=0.0)] = 0.0 roofType: Annotated[RoofTypeOptions | None, Field(description='roof type')] = None highAlbedoRoofReqType: Annotated[ @@ -2393,7 +2724,7 @@ class Roof(CustomBaseModel): Field( description='high albedo roof type - this include the albedo method and emeptions' ), - ] + ] = None otherRoofType: Annotated[ OtherRoofTypeOptions | None, Field( @@ -2404,13 +2735,13 @@ class Roof(CustomBaseModel): RoofInsulationTypeOptions | None, Field(description='roof insulation types') ] = None solarReflectance: Annotated[ - float | None, Field(description='solar reflectance', ge=0.0) + float, Field(description='solar reflectance', ge=0.0) ] = 0.0 solarReflectanceIndex: Annotated[ - float | None, Field(description='solar reflectance index', ge=0.0) + float, Field(description='solar reflectance index', ge=0.0) ] = 0.0 thermalEmittance: Annotated[ - float | None, Field(description='thermal emittance', ge=0.0) + float, Field(description='thermal emittance', ge=0.0) ] = 0.0 purlinSpacing: Annotated[ float | None, Field(description='Roof purlin spacing', ge=0.0) @@ -2422,78 +2753,81 @@ class Floor(CustomBaseModel): extra='ignore', ) id: Annotated[ - str | int | None, + str | int | MISSING, Field( description='Scope-unique reference identifier for instances of this data group.' ), - ] = None + ] = MISSING description: Annotated[str | None, Field(description='The name of the component')] assemblyType: Annotated[str, Field(description='The type of the component')] bldgUseKey: Annotated[ - str, Field(description='key reference of the building use area data group') + str | None, + Field(description='key reference of the building use area data group'), ] adjacentSpaceType: Annotated[ - AdjacentSpaceTypeOptions | None, + AdjacentSpaceTypeOptions | None | MISSING, Field(description='Space type of the adjacent space'), - ] = None + ] = MISSING allowanceType: Annotated[ - EnvelopeAssemblyAllowanceTypeOptions | None, Field(description='allowance type') - ] = None + EnvelopeAssemblyAllowanceTypeOptions | MISSING, + Field(description='allowance type'), + ] = MISSING constructionType: Annotated[ - ConstructionTypeOptions | None, + ConstructionTypeOptions | None | MISSING, Field(description='Construction types - compliance code specification'), - ] = None - exemptionType: EnvelopeAssemblyExemptionOptions | None = None - orientation: OrientationOptions | None = None - cavityRValue: Annotated[float | None, Field(ge=0.0)] = None + ] = MISSING + exemptionType: EnvelopeAssemblyExemptionOptions | None | MISSING = MISSING + orientation: OrientationOptions | MISSING = MISSING + cavityRValue: Annotated[float | None | MISSING, Field(ge=0.0)] = MISSING continuousRValue: Annotated[ - float | None, + float | None | MISSING, Field( description='Continuous insulation on the above grade wall. Can be exterior or interior or both.', ge=0.0, ), - ] = None + ] = MISSING propUValue: Annotated[ - float | None, - Field( - description='Proposed thermal transmittance of the above grade wall.', - ge=0.0, - ), - ] = None + float | None | MISSING, + Field(description='Proposed thermal transmittance of the above grade wall.'), + ] = MISSING altExemptType: Annotated[ - AltExemptTypeOptions | None, Field(description='alteration exemption type') - ] = None - grossArea: Annotated[float | None, Field(description='gross area', ge=0.0)] = None - floorType: Annotated[FloorTypeOptions | None, Field(description='Floor type')] = ( - None - ) + AltExemptTypeOptions | None | MISSING, + Field(description='alteration exemption type'), + ] = MISSING + grossArea: Annotated[ + float | None | MISSING, Field(description='gross area', ge=0.0) + ] = MISSING + floorType: Annotated[ + FloorTypeOptions | MISSING, Field(description='Floor type') + ] = MISSING depthOfInsulation: Annotated[ - int | None, + int | None | MISSING, Field( description='depth of insulation, it only works on a certain numbers including 1, 2, 3, 4, 93, 94, 95 ,96, 97, 98, 99. Mapping is in the $comment', ge=0, ), - ] = None + ] = MISSING slabFullInsulBelowMinRValue: Annotated[ - float | None, + float | None | MISSING, Field( description='Full insulation R value below the slab -> the number if fixed based on the selection of depthOfInsulation', ge=0.0, ), - ] = None + ] = MISSING insulationPosition: Annotated[ - SlabInsulationPositionOptions | None, + SlabInsulationPositionOptions | None | MISSING, Field(description='The position of the insulation'), - ] = None + ] = MISSING hasEdgeInsul: Annotated[ - bool | None, + bool | None | MISSING, Field( description='A boolean to indicate whether the bldg use area uses edge insulation' ), - ] = None + ] = MISSING floorExposedFrameType: Annotated[ - FloorExposedFrameType | None, Field(description='Floor Exposed Frame type') - ] = None + FloorExposedFrameType | None | MISSING, + Field(description='Floor Exposed Frame type'), + ] = MISSING class HVACSystem(CustomBaseModel): @@ -2501,14 +2835,15 @@ class HVACSystem(CustomBaseModel): extra='ignore', ) id: Annotated[ - str | int | None, + str | int | MISSING, Field( description='Scope-unique reference identifier for instances of this data group.' ), - ] = None + ] = MISSING altExemptType: Annotated[ - AltExemptTypeOptions | None, Field(description='alteration exemption type') - ] = None + AltExemptTypeOptions | None | MISSING, + Field(description='alteration exemption type'), + ] = MISSING condenser: Annotated[ CondenserTypeOptions | None, Field(description='condenser type') ] = 'UNKNOWN_CONDENSER' @@ -2560,96 +2895,107 @@ class HVACSystem(CustomBaseModel): 'UNKNOWN_FUEL' ) spaceHeatingSystemException: Annotated[ - SpaceHeatingSystemExceptionOptions | None, + SpaceHeatingSystemExceptionOptions | None | MISSING, Field( description='Exceptions for electric space heating system - Denver 2022 requirement. Can be null for other codes' ), - ] = None + ] = MISSING heatingEquipCapacity: Annotated[ float | None, Field(description='Heating equipment capacity', ge=0.0) ] = 0.0 heatingEquipment: Annotated[ - HeatingEquipmentTypeOptions | None, Field(description='Heating equipment type') - ] = None + HeatingEquipmentTypeOptions | MISSING, + Field(description='Heating equipment type'), + ] = MISSING heatPump: Annotated[ - HeatPumpTypeOptions | None, Field(description='Heat pump type') - ] = None + HeatPumpTypeOptions | MISSING, Field(description='Heat pump type') + ] = MISSING hydronicReheat: Annotated[ - bool | None, + bool | None | MISSING, Field( description='Flag to identify whether the HVAC has a hydronic reheat system, (deprecated, set to false)' ), - ] = None + ] = MISSING isHeatingSysWeatherized: Annotated[ - bool | None, + bool | None | MISSING, Field( description='Flag to identify whether the heating system is weatherized, (deprecated, set to false)' ), - ] = None + ] = MISSING perimeterSystem: Annotated[ - bool | None, + bool | None | MISSING, Field( description='Flag to identify whether the HVAC system is used to condition perimeter zones' ), - ] = None + ] = MISSING propCoolingEquipEfficiencyPartial: Annotated[ - float | None, + float | None | MISSING, Field( description='Proposed system cooling equipment part load efficiency', ge=0.0 ), - ] = None + ] = MISSING propCoolingEquipEfficiency: Annotated[ - float | None, + float | None | MISSING, Field(description='Proposed system cooling equipment efficiency', ge=0.0), - ] = None + ] = MISSING propHeatingEquipEfficiency: Annotated[ - float | None, + float | None | MISSING, Field(description='Proposed system heating equipment efficiency', ge=0.0), - ] = None + ] = MISSING quantity: Annotated[ - int | None, Field(description='Quantity of HVAC system', ge=1) - ] = None + int | None | MISSING, Field(description='Quantity of HVAC system', ge=0) + ] = MISSING quantityCoolEquip: Annotated[ - int | None, Field(description='Quantity of the cooling equipment', ge=0) - ] = None + int | None | MISSING, + Field(description='Quantity of the cooling equipment', ge=0), + ] = MISSING quantityHeatEquip: Annotated[ - int | None, Field(description='Quantity of the heating equipment', ge=0) - ] = None + int | None | MISSING, + Field(description='Quantity of the heating equipment', ge=0), + ] = MISSING reheatRecoolCoil: Annotated[ - bool | None, + bool | None | MISSING, Field( description='Flag to identify whether the coils are reheat and recooled (deprecated, set to false)' ), - ] = None + ] = MISSING + requirementAnswer: Annotated[ + list[Requirements] | None, Field(validate_default=True) + ] = [] returnFanHp: Annotated[ - float | None, Field(description='Return fan HP (deprecated, set to 0)', ge=0.0) - ] = None + float | None | MISSING, + Field(description='Return fan HP (deprecated, set to 0)', ge=0.0), + ] = MISSING steamReheat: Annotated[ - bool | None, + bool | None | MISSING, Field( description='Flag to identify whether the HVAC system is steam reheated (deprecated, set to false)' ), - ] = None + ] = MISSING supplyFanHp: Annotated[ - float | None, Field(description='Supply fan HP (deprecated, set to 0)', ge=0.0) - ] = None + float | None | MISSING, + Field(description='Supply fan HP (deprecated, set to 0)', ge=0.0), + ] = MISSING supplyStaticPressure: Annotated[ - float | None, + float | None | MISSING, Field(description='Supply static pressure (deprecated, set to 0)', ge=0.0), - ] = None + ] = MISSING systemType: Annotated[ - str | None, Field(description='System type (deprecated, set to HVAC)') - ] = None + str | None | MISSING, Field(description='System type (deprecated, set to HVAC)') + ] = MISSING totalFanHp: Annotated[ - float | None, Field(description='Total fan HP (deprecated, set to 0)', ge=0.0) - ] = None - varAirBox: Annotated[bool | None, Field(description='VAR Air Box')] = None - varAirVolMixingBox: Annotated[ - bool | None, Field(description='VAR air volume mixing box') - ] = None - zone: Annotated[ZoneLayoutOptions | None, Field(description='HVAC zone layout')] = ( - None + float | None | MISSING, + Field(description='Total fan HP (deprecated, set to 0)', ge=0.0), + ] = MISSING + varAirBox: Annotated[bool | None | MISSING, Field(description='VAR Air Box')] = ( + MISSING ) + varAirVolMixingBox: Annotated[ + bool | None | MISSING, Field(description='VAR air volume mixing box') + ] = MISSING + zone: Annotated[ + ZoneLayoutOptions | MISSING, Field(description='HVAC zone layout') + ] = MISSING class FixtureSchedule(CustomBaseModel): @@ -2657,17 +3003,17 @@ class FixtureSchedule(CustomBaseModel): extra='ignore', ) id: Annotated[ - int | None, + int | MISSING, Field( description='Scope-unique reference identifier for instances of this data group.' ), - ] = None + ] = MISSING lightingId: Annotated[ - int | None, + int | MISSING, Field( description='Scope-unique reference identifier for instances of the parent lighting group.' ), - ] = None + ] = MISSING scheduleFixtureKey: Annotated[ str, Field(description='UUID to identify this fixture schedule.') ] @@ -2687,29 +3033,32 @@ class FixtureSchedule(CustomBaseModel): LightingTypeOptions, Field(description='lighting fixture type') ] trackCircuitBreakerAmps: Annotated[ - float | None, Field(description='Track lighting circuit breaker amps', ge=0.0) - ] = None + float | None | MISSING, + Field(description='Track lighting circuit breaker amps', ge=0.0), + ] = MISSING trackCircuitBreakerVolts: Annotated[ - float | None, + float | None | MISSING, Field(description='Track lighting circuit breaker voltage', ge=0.0), - ] = None + ] = MISSING trackCurrentLimiterWattage: Annotated[ - float | None, Field(description='Track current limiter wattage', ge=0.0) - ] = None + float | None | MISSING, + Field(description='Track current limiter wattage', ge=0.0), + ] = MISSING trackLength: Annotated[ - float | None, Field(description='Track lighting length', ge=0.0) - ] = None + float | None | MISSING, Field(description='Track lighting length', ge=0.0) + ] = MISSING trackTotalLuminaireWattage: Annotated[ - float | None, + float | None | MISSING, Field(description='Track lighting total luminaire wattage', ge=0.0), - ] = None + ] = MISSING trackTransformerWattage: Annotated[ - float | None, Field(description='Track lighting former wattage', ge=0.0) - ] = None + float | None | MISSING, + Field(description='Track lighting former wattage', ge=0.0), + ] = MISSING trackLightingWattageBasisType: Annotated[ - TrackLightingWattageBasisTypeOptions | None, + TrackLightingWattageBasisTypeOptions | MISSING, Field(description='Track lighting wattage basis type'), - ] = None + ] = MISSING class HVACPlant(CustomBaseModel): @@ -2717,136 +3066,145 @@ class HVACPlant(CustomBaseModel): extra='ignore', ) id: Annotated[ - str | int | None, + str | int | MISSING, Field( description='Scope-unique reference identifier for instances of this data group.' ), - ] = None + ] = MISSING altExemptType: Annotated[ - AltExemptTypeOptions | None, Field(description='alteration exemption type') - ] = None + AltExemptTypeOptions | None | MISSING, + Field(description='alteration exemption type'), + ] = MISSING boilerDraftType: Annotated[ - BoilerDraftTypeOptions | None, Field(description='Boiler draft type') - ] = None + BoilerDraftTypeOptions | MISSING, Field(description='Boiler draft type') + ] = MISSING boilerFuel: Annotated[ - FuelTypeOptions | None, Field(description='Boiler fuel type') - ] = None - chiller: Annotated[ChillerTypeOptions | None, Field(description='Chiller type')] = ( - None - ) + FuelTypeOptions | MISSING, Field(description='Boiler fuel type') + ] = MISSING + chiller: Annotated[ + ChillerTypeOptions | MISSING, Field(description='Chiller type') + ] = MISSING condenser: Annotated[ - CondenserTypeOptions | None, Field(description='Condenser type') - ] = None + CondenserTypeOptions | MISSING, Field(description='Condenser type') + ] = MISSING condenserFlowRate: Annotated[ - float | None, + float | None | MISSING, Field( description='Condenser flow rate - this data comes from engine. User change this data does not have impact on compliance check', ge=0.0, ), - ] = None + ] = MISSING condenserLeavingTemperature: Annotated[ - float | None, + float | None | MISSING, Field( description='leaving water temperature - this data comes from engine. User change this data does not have impact on compliance check', ge=0.0, ), - ] = None + ] = MISSING coolingPlant: Annotated[ - CoolingPlantTypeOptions | None, Field(description='Cooling Plant Types') - ] = None + CoolingPlantTypeOptions | MISSING, Field(description='Cooling Plant Types') + ] = MISSING coolingPlantCapacity: Annotated[ - float | None, Field(description='Cooling plant capacity', ge=0.0) - ] = None - description: Annotated[str | None, Field(description='Plant unique name')] = None + float | None | MISSING, Field(description='Cooling plant capacity', ge=0.0) + ] = MISSING + description: Annotated[ + str | None | MISSING, Field(description='Plant unique name') + ] = MISSING enteringCondenserWaterTemperature: Annotated[ - float | None, + float | None | MISSING, Field( description='Condenser entering water temperature - this data comes from engine. User change this data does not have impact on compliance check', ge=0.0, ), - ] = None + ] = MISSING evaporatorLeavingTemperature: Annotated[ - float | None, + float | None | MISSING, Field( description='Evaporator leaving water temperature - this data comes from engine. User change this data does not have impact on compliance check', ge=0.0, ), - ] = None + ] = MISSING heatingPlant: Annotated[ - HeatingPlantTypeOptions | None, Field(description='Heating Plant Types') - ] = None + HeatingPlantTypeOptions | MISSING, Field(description='Heating Plant Types') + ] = MISSING heatingPlantCapacity: Annotated[ - float | None, Field(description='Heating Plant Capacity', ge=0.0) - ] = None + float | None | MISSING, Field(description='Heating Plant Capacity', ge=0.0) + ] = MISSING heatPumpChillerHeatingSourceConditions: Annotated[ - HeatPumpChillerHeatingSourceConditionOptions | None, + HeatPumpChillerHeatingSourceConditionOptions | MISSING, Field(description='Heat Pump Chiller Heating Source Condition'), - ] = None + ] = MISSING heatPumpChillerLeavingHeatingWaterTemp: Annotated[ - HeatPumpChillerLeavingHeatingWaterTempOptions | None, + HeatPumpChillerLeavingHeatingWaterTempOptions | MISSING, Field(description='Heat pump chiller leaving heating water temperature'), - ] = None + ] = MISSING heatPumpChillerType: Annotated[ - HeatPumpChillerTypeOptions | None, Field(description='Heat pump chiller type') - ] = None + HeatPumpChillerTypeOptions | MISSING, + Field(description='Heat pump chiller type'), + ] = MISSING heatRecovery: Annotated[ - bool | None, + HeatRecovery | None | MISSING, Field( description='Flag indicates whether the system has heat recovery feature' ), - ] = None + ] = MISSING heatPumpSimultaneousCoolingAndHeating: Annotated[ - bool | None, + HeatPumpSimultaneousCoolingAndHeating | None | MISSING, Field( description='Flag indicates whether the heat pump can do simultaneous cooling and heating' ), - ] = None + ] = MISSING heatRejection: Annotated[ - HeatRejectionTypeOptions | None, Field(description='Heat rejection types') - ] = None + HeatRejectionTypeOptions | MISSING, Field(description='Heat rejection types') + ] = MISSING leavingChilledWaterTemperature: Annotated[ - float | None, + float | None | MISSING, Field( description='Leaving chiller water temperature - this data comes from engine. User change this data does not have impact on compliance check', ge=0.0, ), - ] = None - plantType: Annotated[PlantTypeOptions | None, Field(description='Plant type')] = ( - None - ) + ] = MISSING + plantType: Annotated[ + PlantTypeOptions | MISSING, Field(description='Plant type') + ] = MISSING propCoolingPlantEfficiencyPartial: Annotated[ - float | None, + float | None | MISSING, Field(description='Proposed cooling plant part load efficiency', ge=0.0), - ] = None + ] = MISSING propCoolingPlantEfficiency: Annotated[ - float | None, Field(description='Proposed cooling plant efficiency', ge=0.0) - ] = None + float | None | MISSING, + Field(description='Proposed cooling plant efficiency', ge=0.0), + ] = MISSING propHeatingPlantEfficiency: Annotated[ - float | None, Field(description='Proposed heating plant efficiency', ge=0.0) - ] = None + float | None | MISSING, + Field(description='Proposed heating plant efficiency', ge=0.0), + ] = MISSING quantity: Annotated[ - int | None, Field(description='Quantity of the plant system', ge=1) - ] = None - systemType: Annotated[str | None, Field(description='Deprecated, system type')] = ( - None - ) + int | MISSING, Field(description='Quantity of the plant system', ge=0) + ] = MISSING + systemType: Annotated[ + str | None | MISSING, Field(description='Deprecated, system type') + ] = MISSING twoPipeSystem: Annotated[ - bool | None, + TwoPipeSystem | None | MISSING, Field(description='Flag identifies if the plant system is a two pipe system'), - ] = None + ] = MISSING waterloopHeatPump: Annotated[ - bool | None, + WaterloopHeatPump | None | MISSING, Field( description='Flag identifies if the plant system is a water loop heat pump' ), - ] = None + ] = MISSING compliancePath: Annotated[ - CompliancePathOptions | None, Field(description='Compliance path') - ] = None + CompliancePathOptions | None | MISSING, Field(description='Compliance path') + ] = MISSING efficiencyRequirementException: Annotated[ - EquipmentEfficiencyRequirementExceptionOptions | None, + EquipmentEfficiencyRequirementExceptionOptions | MISSING, Field(description='Natural gas boiler efficiency requirement exceptions'), - ] = 'EFF_EXCEPTION_UNSPECIFIED' + ] = MISSING + requirementAnswer: Annotated[ + list[Requirements] | None, Field(validate_default=True) + ] = [] class Fan(CustomBaseModel): @@ -2854,55 +3212,60 @@ class Fan(CustomBaseModel): extra='ignore', ) id: Annotated[ - str | int | None, + str | int | MISSING, Field( description='Scope-unique reference identifier for instances of this data group.' ), - ] = None + ] = MISSING brakeHp: Annotated[ - float | None, + float | MISSING, Field( description='Maximum brake HP, this value is calculated by engine.', ge=0.0 ), - ] = None - description: Annotated[str | None, Field(description='Unique name of the fan')] = ( - None - ) + ] = MISSING + description: Annotated[ + str | None | MISSING, Field(description='Unique name of the fan') + ] = MISSING designBrakeHp: Annotated[ - float | None, Field(description='Fan design brake HP', ge=0.0) - ] = None + float | MISSING, Field(description='Fan design brake HP', ge=0.0) + ] = MISSING fanDesignEfficiency: Annotated[ - float | None, Field(description='Fan design efficiency', ge=0.0, le=100.0) - ] = None + float | None | MISSING, + Field(description='Fan design efficiency', ge=0.0, le=100.0), + ] = MISSING fanEfficiency: Annotated[ - float | None, + float | MISSING, Field( description='Fan efficiency. In latest code, it is the FEI, legacy code (IECC 2015 e.g.,) it is FEG. Based on what index it is, the range is different.', ge=0.0, ), - ] = None + ] = MISSING fanEfficiencyExceptionType: Annotated[ - FanEfficiencyExceptionTypeOptions | None, + FanEfficiencyExceptionTypeOptions | MISSING, Field(description='Fan efficiency execption type'), - ] = None - fanType: Annotated[FanTypeOptions | None, Field(description='Fan type')] = None + ] = MISSING + fanType: Annotated[FanTypeOptions | MISSING, Field(description='Fan type')] = ( + MISSING + ) fanVolumeType: Annotated[ - FanVolumeTypeOptions | None, Field(description='Fan volume control type') - ] = None + FanVolumeTypeOptions | MISSING, Field(description='Fan volume control type') + ] = MISSING maxNameplateHp: Annotated[ - float | None, + float | None | MISSING, Field( - description='Maximum name plate HP - this number shall be calculated by engine', - ge=0.0, + description='Maximum name plate HP - this number shall be calculated by engine' ), - ] = None - nameplateHp: Annotated[float | None, Field(description='Name plate HP', ge=0.0)] = ( - None + ] = MISSING + nameplateHp: Annotated[float | MISSING, Field(description='Name plate HP')] = ( + MISSING ) totalFanEfficiency: Annotated[ - float | None, Field(description='Peak fan efficiency', ge=0.0, le=100.0) - ] = None - volume: Annotated[float | None, Field(description='Fan volume', ge=0.0)] = None + float | None | MISSING, + Field(description='Peak fan efficiency', ge=0.0, le=100.0), + ] = MISSING + volume: Annotated[float | MISSING, Field(description='Fan volume', ge=0.0)] = ( + MISSING + ) class FanSystemPressureDropCredits(CustomBaseModel): @@ -2910,38 +3273,39 @@ class FanSystemPressureDropCredits(CustomBaseModel): extra='ignore', ) id: Annotated[ - str | int | None, + str | int | MISSING, Field( description='Scope-unique reference identifier for instances of this data group.' ), - ] = None + ] = MISSING cfm: Annotated[ - float | None, + float | MISSING, Field( description='Effective air volume when applying pressure drop credit', ge=0.0, ), - ] = None + ] = MISSING designCredit: Annotated[ - float | None, + float | MISSING, Field( description='value for pressure drop credit at design conditions or apply to clean filter pressure drop', ge=0.0, ), - ] = None + ] = MISSING pressureDropCredit: Annotated[ - float | None, + float | MISSING, Field( description='The calculated pressure drop credits. This is calculated value from engine.', ge=0.0, ), - ] = None + ] = MISSING recoveryEffectiveness: Annotated[ - float | None, Field(description='Energy recovery effectiveness', ge=0.0, le=1.0) - ] = None + float | None | MISSING, + Field(description='Energy recovery effectiveness', ge=0.0), + ] = MISSING verticalDuctLength: Annotated[ - float | None, Field(description='Vertical duct length', ge=0.0) - ] = None + float | None | MISSING, Field(description='Vertical duct length', ge=0.0) + ] = MISSING type: Annotated[PressureDropTypeOptions, Field(description='Pressure drop type')] @@ -2950,86 +3314,88 @@ class ServiceWaterHeatingSystem(CustomBaseModel): extra='ignore', ) id: Annotated[ - str | int | None, + str | int | MISSING, Field( description='Scope-unique reference identifier for instances of this data group.' ), - ] = None + ] = MISSING circulationPump: Annotated[ - bool | None, + CirculationPump | None, Field(description='Flag identifies whether the SWH has a circulation pump'), - ] = None + ] = 0 heatTraceTapeInstalled: Annotated[ - bool | None, + HeatTraceTapeInstalled | None, Field( description='Flag identifies whether the SWH has heat trace tape installed' ), - ] = None + ] = 0 combinedSystem: Annotated[ - bool | None, + CombinedSystem | None, Field(description='Flag identifies whether the SWH is a combined system'), - ] = None + ] = 0 poolSystem: Annotated[ - bool | None, + PoolSystem | None, Field(description='Flag identifies whether the SWH is part of pool system'), - ] = None - heatpumpPoolHeater: Annotated[ - bool | None, + ] = 0 + heatPumpPoolHeater: Annotated[ + HeatPumpPoolHeater | None, Field( description='Flag identifies whether the SWH uses heat pump to heat the pool. - Only used when poolSystem is true. False as default' ), ] = None inputRating: Annotated[ - float | None, + float | MISSING, Field( description='Water heater rated input power. kBtu/h if fuel type is gas or oil, kW if fuel type is electric', ge=0.0, ), - ] = None + ] = MISSING swhSystemDrawPatternType: Annotated[ - SWHSystemDrawPatternTypeOptions | None, + SWHSystemDrawPatternTypeOptions | MISSING, Field(description='SWH system draw pattern type'), - ] = None + ] = MISSING storageCapacity: Annotated[ - float | None, Field(description='Water heater storage capacity', ge=0.0) - ] = None + float | MISSING, Field(description='Water heater storage capacity', ge=0.0) + ] = MISSING propSwhEquipEfficiency: Annotated[ - float | None, + float | MISSING, Field( description='Proposed SWH equipment efficiency. Standby Loss if fuel type is electric, %Et otherwise.', ge=0.0, ), - ] = None + ] = MISSING quantity: Annotated[ - int | None, Field(description='Quantity of the water heater', ge=1) - ] = None + int | MISSING, Field(description='Quantity of the water heater', ge=0) + ] = MISSING description: Annotated[ - str | None, Field(description='Unique name describes the SWH system') - ] = None + str | None | MISSING, Field(description='Unique name describes the SWH system') + ] = MISSING systemType: Annotated[ - str | None, Field(description='Deprecated - set to Water Heater') - ] = None + str | MISSING, Field(description='Deprecated - set to Water Heater') + ] = MISSING swhSystemType: Annotated[ - SWHSystemTypeOptions | None, Field(description='SWH system type.') - ] = None + SWHSystemTypeOptions | MISSING, Field(description='SWH system type.') + ] = MISSING fuelType: Annotated[ - SWHFuelTypeOptions | None, Field(description='SWH equipment fuel type') - ] = None + SWHFuelTypeOptions | MISSING, Field(description='SWH equipment fuel type') + ] = MISSING altExemptType: Annotated[ - AltExemptTypeOptions | None, Field(description='SWH exemptions') - ] = None + AltExemptTypeOptions | None | MISSING, Field(description='SWH exemptions') + ] = MISSING swhSystemSubType: Annotated[ - SWHSystemSubTypeOptions | None, + SWHSystemSubTypeOptions | MISSING, Field(description='Electric storage water heater sub types'), - ] = 'UNKNOWN_SWH_SYSTEM_SUB_TYPE' - listPosition: int | None = None + ] = MISSING + listPosition: int | None | MISSING = MISSING efficiencyRequirementException: Annotated[ - EquipmentEfficiencyRequirementExceptionOptions | None, + EquipmentEfficiencyRequirementExceptionOptions | MISSING, Field( description='High input natural gas service water heater efficiency requirement exceptions' ), - ] = 'EFF_EXCEPTION_UNSPECIFIED' - requirementAnswer: list[Any] | None = None + ] = MISSING + requirementAnswer: Annotated[ + list[Requirements] | None, Field(validate_default=True) + ] = [] class EfficiencyPackage(CustomBaseModel): @@ -3037,139 +3403,141 @@ class EfficiencyPackage(CustomBaseModel): extra='ignore', ) id: Annotated[ - str | int | None, + str | int | MISSING, Field( description='Scope-unique reference identifier for instances of this data group.' ), - ] = None + ] = MISSING bldgUseKey: Annotated[ - str | None, Field(description='Building use Key, reference BuildingAreaUse key') - ] = None + str | None | MISSING, + Field(description='Building use Key, reference BuildingAreaUse key'), + ] = MISSING airInfiltration: Annotated[ - float | None, Field(description='Air infiltration measured @75Pa', ge=0.0) - ] = None - kitchenSize: Annotated[float | None, Field(description='Kitchen size', ge=0.0)] = ( - None - ) + float | None | MISSING, + Field(description='Air infiltration measured @75Pa', ge=0.0), + ] = MISSING + kitchenSize: Annotated[ + float | None | MISSING, Field(description='Kitchen size', ge=0.0) + ] = MISSING renewableCapacity: Annotated[ - float | None, + float | None | MISSING, Field( description='On-site renewable capacity, unit is Btu for thermal system and watts for electric system.', ge=0.0, ), - ] = None + ] = MISSING renewableType: Annotated[ - EnergyCreditRenewableTypeOptions | None, + EnergyCreditRenewableTypeOptions | None | MISSING, Field(description='Renewable system type'), - ] = None + ] = MISSING fractionGrossFloorAreaServedByGSHP: Annotated[ - float | None, + float | None | MISSING, Field( description='Fraction of gross floor area served by GSHP, Added by 90.1 2022', ge=0.0, le=1.0, ), - ] = None + ] = MISSING gshpFieldSourceCapacityType: Annotated[ - GSHPFieldSourceCapacityTypeOptions | None, + GSHPFieldSourceCapacityTypeOptions | None | MISSING, Field(description='GSHP field source capacity type, Added by 90.1 2022'), - ] = None + ] = MISSING fractionGrossFloorAreaServedByCAV: Annotated[ - float | None, + float | None | MISSING, Field( description='Fraction of gross floor area served by CAV, Added by 90.1 2022', ge=0.0, le=1.0, ), - ] = None + ] = MISSING coolingEnergyRecoveryRatio: Annotated[ - float | None, + float | None | MISSING, Field( description='Cooling energy recovery ratio, Added by 90.1 2022', ge=0.0, le=1.0, ), - ] = None + ] = MISSING heatingEnergyRecoveryRatio: Annotated[ - float | None, + float | None | MISSING, Field( description='Heating energy recovery ratio, Added by 90.1 2022', ge=0.0, le=1.0, ), - ] = None + ] = MISSING percentageWaterPipingWithIncreasedInsulation: Annotated[ - float | None, + float | None | MISSING, Field( description='Percentage water piping with increased insulation, Added by 90.1 2022', ge=0.0, le=100.0, ), - ] = None + ] = MISSING numberShowersWithDrainHeatRecovery: Annotated[ - int | None, + int | None | MISSING, Field( description='Number of showers with drain heat recovery, Added by 90.1 2022', ge=1, ), - ] = None + ] = MISSING totalNumberShowers: Annotated[ - int | None, + int | None | MISSING, Field(description='Total number of showers, Added by 90.1 2022', ge=1), - ] = None + ] = MISSING fractionTunedAreaOfGrossLightedFloorArea: Annotated[ - float | None, + float | None | MISSING, Field( description='Fraction of tuned area of gross lighted floor area, Added by 90.1 2022', ge=0.0, le=1.0, ), - ] = None + ] = MISSING grossLightedFloorArea: Annotated[ - float | None, + float | None | MISSING, Field(description='Gross lighted floor area, Added by 90.1 2022', ge=0.0), - ] = None + ] = MISSING actualDaylightAreaWithContinuousDim: Annotated[ - float | None, + float | None | MISSING, Field( description='Actual daylight area with continous dimming control, Added by 90.1 2022', ge=0.0, ), - ] = None + ] = MISSING percentageLightingLoadManagement: Annotated[ - float | None, + float | None | MISSING, Field( description='Percentage lighting load management, Added by 90.1 2022', ge=0.0, le=1.0, ), - ] = None + ] = MISSING installedElectricStorageCapacity: Annotated[ - float | None, + float | None | MISSING, Field( description='Installed electric storage capacity, Added by 90.1 2022', ge=0.0, ), - ] = None + ] = MISSING storageRatio: Annotated[ - float | None, + float | None | MISSING, Field( description='HVAC cooling energy storage ratio, unit in ton-hours storage per ton of design-day cooling load , Added by 90.1 2022', ge=0.5, le=4.0, ), - ] = None + ] = MISSING sumOfFloorsServedByClassAElevators: Annotated[ - int | None, + int | None | MISSING, Field(description='Sum of floors served by each Class A elevators', ge=0), - ] = None + ] = MISSING sumOfFloorsServedByClassBElevators: Annotated[ - int | None, + int | None | MISSING, Field( description='sum of floors served by all building elevators and escalators', ge=0, ), - ] = None + ] = MISSING type: Annotated[ EfficiencyPackageTypeOptions | MAS2022EnergyCreditTypeOptions @@ -3184,11 +3552,11 @@ class RenewableSystem(CustomBaseModel): extra='ignore', ) id: Annotated[ - str | int | None, + str | int | MISSING, Field( description='Scope-unique reference identifier for instances of this data group.' ), - ] = None + ] = MISSING description: Annotated[ str | None, Field(description='description of the renewable system') ] @@ -3209,11 +3577,11 @@ class OffsiteRenewableProcurement(CustomBaseModel): extra='ignore', ) id: Annotated[ - str | int | None, + str | int | MISSING, Field( description='Scope-unique reference identifier for instances of this data group.' ), - ] = None + ] = MISSING description: Annotated[ str | None, Field(description='description of the offsite renewable procurement'), @@ -3235,14 +3603,15 @@ class LightingControls(CustomBaseModel): extra='ignore', ) id: Annotated[ - str | int | None, + str | int | MISSING, Field( description='Scope-unique reference identifier for instances of this data group.' ), - ] = None + ] = MISSING type: Annotated[ - LightingControlTypeOptions | None, Field(description='Lighting Control types') - ] = None + LightingControlTypeOptions | MISSING, + Field(description='Lighting Control types'), + ] = MISSING class ThermalBridge(CustomBaseModel): @@ -3250,28 +3619,36 @@ class ThermalBridge(CustomBaseModel): extra='ignore', ) id: Annotated[ - str | int | None, + str | int | MISSING, Field( description='Scope-unique reference identifier for instances of this data group.' ), - ] = None + ] = MISSING thermalBridgeType: Annotated[ - ThermalBridgeTypeOptions | None, Field(description='thermal bridge type') - ] = None + ThermalBridgeTypeOptions | None | MISSING, + Field(description='thermal bridge type'), + ] = MISSING thermalBridgeCategory: Annotated[ - ThermalBridgeCategoryOptions | None, + ThermalBridgeCategoryOptions | None | MISSING, Field(description='thermal bridge category'), - ] = None + ] = MISSING thermalBridgeComplianceType: Annotated[ - ThermalBridgeComplianceTypeOptions | None, + ThermalBridgeComplianceTypeOptions | MISSING, Field(description='thermal bridge compliance type'), - ] = None - psiFactor: Annotated[float | None, Field(description='Psi factor')] = None + ] = MISSING + psiFactor: Annotated[float | None | MISSING, Field(description='Psi factor')] = ( + MISSING + ) thermalBridgeLength: Annotated[ - float | None, Field(description='linear length of a thermal bridge - ft') - ] = None - chiFactor: Annotated[float | None, Field(description='Chi factor')] = None - numberOfPoints: Annotated[int | None, Field(description='Number of points')] = None + float | None | MISSING, + Field(description='linear length of a thermal bridge - ft'), + ] = MISSING + chiFactor: Annotated[float | None | MISSING, Field(description='Chi factor')] = ( + MISSING + ) + numberOfPoints: Annotated[ + int | None | MISSING, Field(description='Number of points') + ] = MISSING class AgWall(CustomBaseModel): @@ -3279,11 +3656,11 @@ class AgWall(CustomBaseModel): extra='ignore', ) id: Annotated[ - str | int | None, + str | int | MISSING, Field( description='Scope-unique reference identifier for instances of this data group.' ), - ] = None + ] = MISSING description: Annotated[ str | None, Field(description='The name of the component') ] = '' @@ -3291,13 +3668,14 @@ class AgWall(CustomBaseModel): str | None, Field(description='The type of the component') ] = 'Exterior Wall' bldgUseKey: Annotated[ - str, Field(description='key reference of the building use area data group') + str | None, + Field(description='key reference of the building use area data group'), ] wallType: Annotated[ - WallTypeOptions | None, Field(description='Above grade wall type') - ] = None + WallTypeOptions | MISSING, Field(description='Above grade wall type') + ] = MISSING agWallConstructionDetailsType: Annotated[ - AgWallConstructionDetailsTypeOptions | None, + AgWallConstructionDetailsTypeOptions, Field(description='Above grade wall construction details type'), ] = 'NONE' agWallExteriorFinishDetailsType: Annotated[ @@ -3324,9 +3702,9 @@ class AgWall(CustomBaseModel): Field(description='Space type of the adjacent space'), ] = None adjacentSpaceBuildingType: Annotated[ - WholeBuildingTypeOptions | None, + WholeBuildingTypeOptions | MISSING, Field(description='Building type of the adjacent space'), - ] = None + ] = MISSING thermalBridge: Annotated[ list[ThermalBridge], Field(description='Thermal bridges objects', min_length=0) ] @@ -3342,19 +3720,19 @@ class AgWall(CustomBaseModel): float | None, Field(description='Thermal bridge adjustment factor') ] = None allowanceType: Annotated[ - EnvelopeAssemblyAllowanceTypeOptions | None, Field(description='allowance type') - ] = None + EnvelopeAssemblyAllowanceTypeOptions, Field(description='allowance type') + ] cmuType: Annotated[CMUTypeOptions | None, Field(description='CMU type')] = None concreteDensity: Annotated[ - ConcreteDensityOptions | None, Field(description='Concrete density') + ConcreteDensityOptions, Field(description='Concrete density') ] = 0 concreteThickness: Annotated[ - ConcreteThicknessOptions | None, Field(description='Concrete thickness') + ConcreteThicknessOptions, Field(description='Concrete thickness') ] = 0 constructionType: Annotated[ - ConstructionTypeOptions | None, + ConstructionTypeOptions | None | MISSING, Field(description='Construction types - compliance code specification'), - ] = None + ] = MISSING exemptionType: EnvelopeAssemblyExemptionOptions | None = None furringType: FurringTypeOptions | None = None heatCapacity: Annotated[ @@ -3364,7 +3742,7 @@ class AgWall(CustomBaseModel): ge=0.0, ), ] = 0.0 - orientation: OrientationOptions | None = 'UNSPECIFIED_ORIENTATION' + orientation: OrientationOptions = 'UNSPECIFIED_ORIENTATION' window: Annotated[ list[Window], Field(description='Windows on the wall', min_length=0) ] @@ -3372,35 +3750,30 @@ class AgWall(CustomBaseModel): cavityRValue: Annotated[ float | None, Field( - description='Average insulation R-value in the cavity between two studs.', - ge=0.0, + description='Average insulation R-value in the cavity between two studs.' ), ] = 0.0 continuousRValue: Annotated[ float | None, Field( - description='Continuous insulation on the above grade wall. Can be exterior or interior or both.', - ge=0.0, + description='Continuous insulation on the above grade wall. Can be exterior or interior or both.' ), ] = 0.0 continuousDeratedRValue: Annotated[ - float | None, + float | None | MISSING, Field( description='Continuous R value derated factor for thermal bridge effect', ge=0.0, ), - ] = 0.0 + ] = MISSING propUValue: Annotated[ float | None, - Field( - description='Proposed thermal transmittance of the above grade wall.', - ge=0.0, - ), + Field(description='Proposed thermal transmittance of the above grade wall.'), ] = 0.0 altExemptType: Annotated[ AltExemptTypeOptions | None, Field(description='alteration exemption type') ] = None - grossArea: Annotated[float | None, Field(description='gross area', ge=0.0)] = 0.0 + grossArea: Annotated[float | None, Field(description='gross area')] = 0.0 class BgWall(CustomBaseModel): @@ -3408,51 +3781,52 @@ class BgWall(CustomBaseModel): extra='ignore', ) id: Annotated[ - str | int | None, + str | int | MISSING, Field( description='Scope-unique reference identifier for instances of this data group.' ), - ] = None + ] = MISSING description: Annotated[ str | None, Field(description='The name of the component') ] = '' - assemblyType: Annotated[ - str | None, Field(description='The type of the component') - ] = 'Basement' + assemblyType: Annotated[str, Field(description='The type of the component')] = ( + 'Basement' + ) bldgUseKey: Annotated[ - str, Field(description='key reference of the building use area data group') + str | None, + Field(description='key reference of the building use area data group'), ] wallType: Annotated[ - BgWallTypeOptions | None, Field(description='Below grade wall type') - ] = None + BgWallTypeOptions | MISSING, Field(description='Below grade wall type') + ] = MISSING wallHeight: Annotated[ - float | None, Field(description='Total height of a below grade wall') + float, Field(description='Total height of a below grade wall') ] = 0.0 wallHeightBelowGrade: Annotated[ - float | None, Field(description='Wall height below grade') + float, Field(description='Wall height below grade') ] = 0.0 adjacentSpaceType: Annotated[ AdjacentSpaceTypeOptions | None, Field(description='Space type of the adjacent space'), ] = None adjacentSpaceBuildingType: Annotated[ - WholeBuildingTypeOptions | None, + WholeBuildingTypeOptions | MISSING, Field(description='Building type of the adjacent space'), - ] = None + ] = MISSING allowanceType: Annotated[ - EnvelopeAssemblyAllowanceTypeOptions | None, Field(description='allowance type') - ] = None + EnvelopeAssemblyAllowanceTypeOptions, Field(description='allowance type') + ] cmuType: Annotated[CMUTypeOptions | None, Field(description='CMU type')] = None concreteDensity: Annotated[ - ConcreteDensityOptions | None, Field(description='Concrete density') + ConcreteDensityOptions, Field(description='Concrete density') ] = 0 concreteThickness: Annotated[ - ConcreteThicknessOptions | None, Field(description='Concrete thickness') + ConcreteThicknessOptions, Field(description='Concrete thickness') ] = 0 constructionType: Annotated[ - ConstructionTypeOptions | None, + ConstructionTypeOptions | None | MISSING, Field(description='Construction types - compliance code specification'), - ] = None + ] = MISSING exemptionType: EnvelopeAssemblyExemptionOptions | None = None furringType: FurringTypeOptions | None = None heatCapacity: Annotated[ @@ -3462,7 +3836,7 @@ class BgWall(CustomBaseModel): ge=0.0, ), ] = 0.0 - orientation: OrientationOptions | None = 'UNSPECIFIED_ORIENTATION' + orientation: OrientationOptions = 'UNSPECIFIED_ORIENTATION' insulationPosition: Annotated[ str | None, Field( @@ -3476,28 +3850,23 @@ class BgWall(CustomBaseModel): cavityRValue: Annotated[ float | None, Field( - description='Average insulation R-value in the cavity between two studs.', - ge=0.0, + description='Average insulation R-value in the cavity between two studs.' ), ] = 0.0 continuousRValue: Annotated[ float | None, Field( - description='Continuous insulation on the below grade wall. Can be exterior or interior or both.', - ge=0.0, + description='Continuous insulation on the below grade wall. Can be exterior or interior or both.' ), ] = 0.0 propUValue: Annotated[ float | None, - Field( - description='Proposed thermal transmittance of the below grade wall.', - ge=0.0, - ), + Field(description='Proposed thermal transmittance of the below grade wall.'), ] = 0.0 altExemptType: Annotated[ AltExemptTypeOptions | None, Field(description='alteration exemption type') ] = None - grossArea: Annotated[float | None, Field(description='gross area', ge=0.0)] = 0.0 + grossArea: Annotated[float, Field(description='gross area', ge=0.0)] = 0.0 class Fixture(CustomBaseModel): @@ -3505,79 +3874,94 @@ class Fixture(CustomBaseModel): extra='ignore', ) id: Annotated[ - str | int | None, + str | int | MISSING, Field( description='Scope-unique reference identifier for instances of this data group.' ), - ] = None + ] = MISSING advControlsAllowanceAperture: Annotated[ - float | None, Field(description='Advanced controls allowance aperture', ge=0.0) - ] = None - advControlAllowanceType: Annotated[ + float | None | MISSING, + Field(description='Advanced controls allowance aperture', ge=0.0), + ] = MISSING + advControlsAllowanceType: Annotated[ AdvancedControlsAllowanceTypeOptions | None, - Field(description='Advanced control allowance type'), + Field(description='Advanced controls allowance type'), ] = None allowanceFloorArea: Annotated[ - float | None, Field(description='Floor area covered by the allowance', ge=0.0) - ] = None + float | None | MISSING, + Field(description='Floor area covered by the allowance', ge=0.0), + ] = MISSING allowanceType: Annotated[ - LightingAllowanceTypeOptions | None, + LightingAllowanceTypeOptions | MISSING, Field(description='Lighting allowance type'), - ] = None - ballast: Annotated[BallastTypeOptions | None, Field(description='Ballast type')] = ( - None - ) + ] = MISSING + ballast: Annotated[ + BallastTypeOptions | MISSING, Field(description='Ballast type') + ] = MISSING description: Annotated[str | None, Field(description='Description of the fixture')] exemptionType: Annotated[ - LightingExemptionTypeOptions | None, + LightingExemptionTypeOptions | None | MISSING, Field(description='lighting exemption type'), - ] = None + ] = MISSING fixtureType: Annotated[ - str | None, + str | None | MISSING, Field(description='This field temporarily used to describe the fixture.'), - ] = None + ] = MISSING fixtureWattage: Annotated[ - float | None, Field(description='fixture wattage', ge=0.0) - ] = None - lampType: Annotated[str | None, Field(description='deprecated, use null')] = None + float | MISSING, Field(description='fixture wattage', ge=0.0) + ] = MISSING + lampType: Annotated[ + str | None | MISSING, Field(description='deprecated, use null') + ] = MISSING lightingType: Annotated[ LightingTypeOptions, Field(description='lighting fixture type') ] numberOfLamps: Annotated[ - float | None, Field(description='deprecated, use null') - ] = None + float | None | MISSING, Field(description='deprecated, use null') + ] = MISSING powerAllowance: Annotated[ - float | None, Field(description='Advanced control power allowance', ge=0.0) - ] = None - quantity: Annotated[int, Field(description='Quantity of the fixture', ge=0)] + float | None | MISSING, + Field(description='Advanced control power allowance', ge=0.0), + ] = MISSING + quantity: Annotated[int | None, Field(description='Quantity of the fixture', ge=0)] quantityWithAdvControls: Annotated[ - int | None, + int | None | MISSING, Field(description='Quantity of the fixture that has advanced control', ge=0), - ] = None + ] = MISSING + scheduleFixtureKey: Annotated[ + str | None | MISSING, + Field(description='UUID to identify this fixture schedule.'), + ] = MISSING trackCircuitBreakerAmps: Annotated[ - float | None, Field(description='Track lighting circuit breaker amps', ge=0.0) - ] = None + float | None | MISSING, + Field(description='Track lighting circuit breaker amps', ge=0.0), + ] = MISSING trackCircuitBreakerVolts: Annotated[ - float | None, + float | None | MISSING, Field(description='Track lighting circuit breaker voltage', ge=0.0), - ] = None + ] = MISSING trackCurrentLimiterWattage: Annotated[ - float | None, Field(description='Track current limiter wattage', ge=0.0) - ] = None + float | None | MISSING, + Field(description='Track current limiter wattage', ge=0.0), + ] = MISSING trackLength: Annotated[ - float | None, Field(description='Track lighting length', ge=0.0) - ] = None + float | None | MISSING, Field(description='Track lighting length', ge=0.0) + ] = MISSING trackTotalLuminaireWattage: Annotated[ - float | None, + float | None | MISSING, Field(description='Track lighting total luminaire wattage', ge=0.0), - ] = None + ] = MISSING trackTransformerWattage: Annotated[ - float | None, Field(description='Track lighting former wattage', ge=0.0) - ] = None + float | None | MISSING, + Field(description='Track lighting former wattage', ge=0.0), + ] = MISSING trackLightingWattageBasisType: Annotated[ - TrackLightingWattageBasisTypeOptions | None, + TrackLightingWattageBasisTypeOptions | MISSING, Field(description='Track lighting wattage basis type'), - ] = None + ] = MISSING + typeOfFixture: Annotated[ + str | None | MISSING, Field(description='Type of the fixture') + ] = MISSING lightingControl: Annotated[ list[LightingControls], Field(description='lighting controls apply to this fixture'), @@ -3589,49 +3973,50 @@ class FanSystem(CustomBaseModel): extra='ignore', ) id: Annotated[ - str | int | None, + str | int | MISSING, Field( description='Scope-unique reference identifier for instances of this data group.' ), - ] = None + ] = MISSING complianceMessage: Annotated[ - str | None, Field(description='Compliance message calculated from engine') - ] = None + str | None | MISSING, + Field(description='Compliance message calculated from engine'), + ] = MISSING complies: Annotated[ - bool | int | None, + bool | int | None | MISSING, Field(description='Flag indicate whether the fan system complies or fail'), - ] = None + ] = MISSING complyMethod: Annotated[ - FanSystemComplianceMethodOptions | None, + FanSystemComplianceMethodOptions | MISSING, Field(description='Fan system compliance method'), - ] = None + ] = MISSING description: Annotated[ - str | None, Field(description='Unique name of this fan system') - ] = None - description2: Annotated[str | None, Field(description='Number of areas served')] = ( - None - ) - fan: Annotated[list[Fan] | None, Field(description='fans')] = None + str | None | MISSING, Field(description='Unique name of this fan system') + ] = MISSING + description2: Annotated[ + str | None | MISSING, Field(description='Number of areas served') + ] = MISSING + fan: Annotated[list[Fan] | MISSING, Field(description='fans')] = MISSING hasPressureDropCredits: Annotated[ - bool | int | None, + HasPressureDropCredits | MISSING, Field(description='Flag indicates if the fan system has pressure drop credits'), - ] = None + ] = MISSING fanSystemKey: Annotated[ - str | None, + str | None | MISSING, Field( description='Fan system key, used when reference a fan system in an HVAC system' ), - ] = None + ] = MISSING pressureDropCredits: Annotated[ - list[FanSystemPressureDropCredits] | None, + list[FanSystemPressureDropCredits] | MISSING, Field(description='Pressure drop credits'), - ] = None + ] = MISSING servesAllowanceAreaWithFlowControl: Annotated[ - bool | int | None, + bool | int | None | MISSING, Field( description='Flag indicates if the fan system serves allowance area with flow control' ), - ] = None + ] = MISSING class Renewable(CustomBaseModel): @@ -3639,48 +4024,51 @@ class Renewable(CustomBaseModel): extra='ignore', ) id: Annotated[ - str | int | None, + str | int | MISSING, Field( description='Scope-unique reference identifier for instances of this data group.' ), - ] = None + ] = MISSING renewableException: Annotated[ RenewableExceptionOptions, Field(description='Renewable exceptions') ] - numberOfFloors: Annotated[int, Field(description='number of floors', ge=1)] + numberOfFloors: Annotated[int, Field(description='number of floors', ge=0)] largestThreeFloorArea: Annotated[ - float, Field(description='Gross floor area of the largest three floors', ge=0.0) + float | None, + Field(description='Gross floor area of the largest three floors', ge=0.0), ] requiredCapacity: Annotated[ - float, Field(description='Code required minimum renewable capacity', ge=0.0) + float | None, + Field(description='Code required minimum renewable capacity', ge=0.0), ] proposedCapacity: Annotated[ - float, Field(description='Sum of the proposed renewable capacity', ge=0.0) + float | None, + Field(description='Sum of the proposed renewable capacity', ge=0.0), ] roofAreaForRenewable: Annotated[ - float, Field(description='roof area for renewable systems', ge=0.0) + float | None, Field(description='roof area for renewable systems', ge=0.0) ] renewableSystem: Annotated[ list[RenewableSystem], Field(description='List of renewable systems') ] requiredOffsiteRenewableEnergy: Annotated[ - float | None, + float | None | MISSING, Field( description='Sum of the required off-site renewable energy procurement', ge=0.0, ), - ] = None + ] = MISSING proposedOffsiteRenewableEnergy: Annotated[ - float | None, + float | None | MISSING, Field( description='Sum of the proposed off-site renewable energy procurement', ge=0.0, ), - ] = None + ] = MISSING offsiteRenewableProcurement: Annotated[ - list[OffsiteRenewableProcurement] | None, + list[OffsiteRenewableProcurement] | MISSING, Field(description='List of off-site renewable procurement'), - ] = None + ] = MISSING class Envelope(CustomBaseModel): @@ -3688,11 +4076,11 @@ class Envelope(CustomBaseModel): extra='ignore', ) id: Annotated[ - str | int | None, + str | int | MISSING, Field( description='Scope-unique reference identifier for instances of this data group.' ), - ] = None + ] = MISSING agWall: Annotated[ list[AgWall], Field(description='above grade walls', min_length=0) ] @@ -3708,8 +4096,9 @@ class Envelope(CustomBaseModel): Field(description='Skylights, must be assigned to a roof', min_length=0), ] useOrientationDetails: Annotated[ - bool | None, Field(description='use orientation details for calculation') - ] = True + Literal[True] | MISSING, + Field(description='use orientation details for calculation'), + ] = MISSING useVltDetails: Annotated[ bool | None, Field( @@ -3720,7 +4109,9 @@ class Envelope(CustomBaseModel): bool | None, Field(description='use cool roof / high albedo roof details for calculation'), ] = True - useCoolRoofDetails: Annotated[None, Field(description='cool roof details')] = None + useCoolRoofDetails: Annotated[ + None | MISSING, Field(description='cool roof details') + ] = MISSING postAltWindowWallPct: Annotated[ float | None, Field( @@ -3771,72 +4162,79 @@ class InteriorLightingSpace(CustomBaseModel): extra='ignore', ) id: Annotated[ - str | int | None, + str | int | MISSING, Field( description='Scope-unique reference identifier for instances of this data group.' ), - ] = None + ] = MISSING description: Annotated[ - str | None, Field(description='Description of the space') - ] = None + str | None | MISSING, Field(description='Description of the space') + ] = MISSING preAltNumberFixtures: Annotated[ - int | None, Field(description='Number of fixtures to be altered', ge=0) - ] = None + int | None | MISSING, + Field(description='Number of fixtures to be altered', ge=0), + ] = MISSING numFixturesAlteredOrAdded: Annotated[ - int | None, Field(description='Number of fixtures added or altered', ge=0) - ] = None + int | None | MISSING, Field(description='Number of fixtures added or altered') + ] = MISSING preAltTotalWattage: Annotated[ - float | None, Field(description='Total wattage before the alteration', ge=0.0) - ] = None + float | None | MISSING, + Field(description='Total wattage before the alteration', ge=0.0), + ] = MISSING postAltTotalWattage: Annotated[ - float | None, Field(description='Total wattage after the alteration', ge=0.0) - ] = None + float | None | MISSING, + Field(description='Total wattage after the alteration', ge=0.0), + ] = MISSING altExemptType: Annotated[ - AltExemptTypeOptions | None, Field(description='alteration exemption type') - ] = None - exemptionType: Annotated[str | None, Field(description='deprecated, use null')] = ( - None - ) - allowanceType: Annotated[str | None, Field(description='deprecated, use null')] = ( - None - ) + AltExemptTypeOptions | None | MISSING, + Field(description='alteration exemption type'), + ] = MISSING + exemptionType: Annotated[ + str | None | MISSING, Field(description='deprecated, use null') + ] = MISSING + allowanceType: Annotated[ + str | None | MISSING, Field(description='deprecated, use null') + ] = MISSING allowanceFloorArea: Annotated[ - float | None, + float | None | MISSING, Field(description='The floor area that covered by allowance', ge=0.0), - ] = None - rcrPerimeter: Annotated[float | None, Field(description='perimeter', ge=0.0)] = None + ] = MISSING + rcrPerimeter: Annotated[ + float | None | MISSING, Field(description='perimeter', ge=0.0) + ] = MISSING rcrFloorToWorkplaneHeight: Annotated[ - float | None, Field(description='Floor-to-workplane height', ge=0.0) - ] = None + float | None | MISSING, Field(description='Floor-to-workplane height', ge=0.0) + ] = MISSING rcrWorkplaneToLuminaireHeight: Annotated[ - float | None, Field(description='Workplane-to-luminaire height', ge=0.0) - ] = None + float | None | MISSING, + Field(description='Workplane-to-luminaire height', ge=0.0), + ] = MISSING primaryDaylight: Annotated[ - float | None, + float | MISSING, Field(description='Daylighting on the primary sidelight area', ge=0.0), - ] = None + ] = MISSING secondaryDaylight: Annotated[ - float | None, + float | MISSING, Field(description='Daylighting on the secondary sidelight area', ge=0.0), - ] = None + ] = MISSING skylightToplight: Annotated[ - float | None, + float | MISSING, Field(description='Daylighting on the skylight toplight area', ge=0.0), - ] = None + ] = MISSING roofMonitorToplight: Annotated[ - float | None, + float | MISSING, Field(description='Daylighting on the roof monitor top light area', ge=0.0), - ] = None + ] = MISSING decorativeArea: Annotated[ - float | None, + float | MISSING, Field( description='The floor area that covered by decorative lightings', ge=0.0 ), - ] = None + ] = MISSING fixture: Annotated[ - list[Fixture] | None, + list[Fixture] | MISSING, Field(description='List of fixtures in the lighting space'), - ] = None + ] = MISSING class HVAC(CustomBaseModel): @@ -3844,17 +4242,17 @@ class HVAC(CustomBaseModel): extra='ignore', ) id: Annotated[ - str | int | None, + str | int | MISSING, Field( description='Scope-unique reference identifier for instances of this data group.' ), - ] = None + ] = MISSING hvacSystem: Annotated[ list[HVACSystem], Field(description='HVAC systems - air-based or radiant-based system'), ] hvacPlant: Annotated[list[HVACPlant], Field(description='HVAC Plant - source loop')] - fanSystem: Annotated[list[FanSystem], Field(description='fan system')] + fanSystem: Annotated[list[FanSystem] | None, Field(description='Fan system')] = None class ActivityUse(CustomBaseModel): @@ -3862,52 +4260,54 @@ class ActivityUse(CustomBaseModel): extra='ignore', ) id: Annotated[ - str | int | None, + str | int | MISSING, Field( description='Scope-unique reference identifier for instances of this data group.' ), - ] = None + ] = MISSING key: Annotated[ str, Field(description='Reference to the building use key :BuildingAreaUse.id:') ] areaDescription: Annotated[ - str | None, Field(description='Text description of the area') - ] = None + str | MISSING, Field(description='Text description of the area') + ] = MISSING floorArea: Annotated[ - float | None, Field(description='Whole building use floor area', ge=0.0) - ] = None + float | MISSING, Field(description='Whole building use floor area', ge=0.0) + ] = MISSING ceilingHeight: Annotated[ - float | None, Field(description='Average ceiling height', ge=0.0) - ] = None + float | None | MISSING, Field(description='Average ceiling height', ge=0.0) + ] = MISSING powerDensity: Annotated[ - float | None, Field(description='Internal equipment power density', ge=0.0) - ] = None + float | None | MISSING, + Field(description='Internal equipment power density', ge=0.0), + ] = MISSING internalLoad: Annotated[ - float | None, Field(description='Internal equipment load', ge=0.0) - ] = None + float | None | MISSING, Field(description='Internal equipment load', ge=0.0) + ] = MISSING constructionType: Annotated[ - ConstructionTypeOptions | None, + ConstructionTypeOptions | None | MISSING, Field(description='Construction types - compliance code specification'), - ] = None + ] = MISSING allowedWattage: Annotated[ - float | None, Field(description='Allowed wattage', ge=0.0) - ] = None + float | MISSING, Field(description='Allowed wattage', ge=0.0) + ] = MISSING proposedWattage: Annotated[ - float | None, Field(description='Proposed wattage', ge=0.0) - ] = None + float | MISSING, Field(description='Proposed wattage', ge=0.0) + ] = MISSING interiorLightingSpace: Annotated[ - InteriorLightingSpace, Field(description='Interior lighting space definition') + InteriorLightingSpace | None, + Field(description='Interior lighting space definition'), ] activityType: Annotated[ - ActivityTypeOptions | None, Field(description='activity type') - ] = None + ActivityTypeOptions | MISSING, Field(description='activity type') + ] = MISSING roomCavityRatioThreshold: Annotated[ - float | None, Field(description='Room Cavity Ratio threshold') - ] = None + float | None | MISSING, Field(description='Room Cavity Ratio threshold') + ] = MISSING isUnfinishedSpace: Annotated[ - bool | None, + bool | None | MISSING, Field(description='A flag to indicate whether the space is unfinished or not.'), - ] = None + ] = MISSING class ExteriorLightingSpace(CustomBaseModel): @@ -3915,33 +4315,38 @@ class ExteriorLightingSpace(CustomBaseModel): extra='ignore', ) id: Annotated[ - str | int | None, + str | int | MISSING, Field( description='Scope-unique reference identifier for instances of this data group.' ), - ] = None + ] = MISSING description: Annotated[ - str | None, Field(description='Description of the space') - ] = None + str | None | MISSING, Field(description='Description of the space') + ] = MISSING preAltNumberFixtures: Annotated[ - int | None, Field(description='Number of fixtures to be altered', ge=0) - ] = None + int | None | MISSING, + Field(description='Number of fixtures to be altered', ge=0), + ] = MISSING numFixturesAlteredOrAdded: Annotated[ - int | None, Field(description='Number of fixtures added or altered', ge=0) - ] = None + int | None | MISSING, + Field(description='Number of fixtures added or altered', ge=0), + ] = MISSING preAltTotalWattage: Annotated[ - float | None, Field(description='Total wattage before the alteration', ge=0.0) - ] = None + float | None | MISSING, + Field(description='Total wattage before the alteration', ge=0.0), + ] = MISSING postAltTotalWattage: Annotated[ - float | None, Field(description='Total wattage after the alteration', ge=0.0) - ] = None + float | None | MISSING, + Field(description='Total wattage after the alteration', ge=0.0), + ] = MISSING altExemptType: Annotated[ - AltExemptTypeOptions | None, Field(description='alteration exemption type') - ] = None + AltExemptTypeOptions | None | MISSING, + Field(description='alteration exemption type'), + ] = MISSING fixture: Annotated[ - list[Fixture] | None, + list[Fixture] | MISSING, Field(description='List of fixtures in the lighting space'), - ] = None + ] = MISSING class WholeBldgUse(CustomBaseModel): @@ -3949,38 +4354,40 @@ class WholeBldgUse(CustomBaseModel): extra='ignore', ) id: Annotated[ - str | int | None, + str | int | MISSING, Field( description='Scope-unique reference identifier for instances of this data group.' ), - ] = None + ] = MISSING areaDescription: Annotated[ - str | None, Field(description='Text description of the area') - ] = None + str | MISSING, Field(description='Text description of the area') + ] = MISSING floorArea: Annotated[ - float | None, Field(description='Whole building use floor area', ge=0.0) - ] = None + float | MISSING, Field(description='Whole building use floor area', ge=0.0) + ] = MISSING ceilingHeight: Annotated[ - float | None, Field(description='Average ceiling height', ge=0.0) - ] = None + float | None | MISSING, Field(description='Average ceiling height', ge=0.0) + ] = MISSING powerDensity: Annotated[ - float | None, Field(description='Internal equipment power density', ge=0.0) - ] = None + float | None | MISSING, + Field(description='Internal equipment power density', ge=0.0), + ] = MISSING internalLoad: Annotated[ - float | None, Field(description='Internal equipment load', ge=0.0) - ] = None + float | None | MISSING, Field(description='Internal equipment load', ge=0.0) + ] = MISSING constructionType: Annotated[ - ConstructionTypeOptions | None, + ConstructionTypeOptions | None | MISSING, Field(description='Construction types - compliance code specification'), - ] = None + ] = MISSING allowedWattage: Annotated[ - float | None, Field(description='Allowed wattage', ge=0.0) - ] = None + float | MISSING, Field(description='Allowed wattage', ge=0.0) + ] = MISSING proposedWattage: Annotated[ - float | None, Field(description='Proposed wattage', ge=0.0) - ] = None + float | MISSING, Field(description='Proposed wattage', ge=0.0) + ] = MISSING interiorLightingSpace: Annotated[ - InteriorLightingSpace, Field(description='Interior lighting space definition') + InteriorLightingSpace | None, + Field(description='Interior lighting space definition'), ] key: Annotated[ str | float | None, @@ -3994,11 +4401,11 @@ class WholeBldgUse(CustomBaseModel): Field(description='Interior lighting use based on space activity type'), ] isTenantSpace: Annotated[ - bool | None, + bool | None | MISSING, Field( description='A boolean to indicate whether the bldg use area is designed for tenant spaces' ), - ] = None + ] = MISSING class ExteriorUse(CustomBaseModel): @@ -4006,32 +4413,34 @@ class ExteriorUse(CustomBaseModel): extra='ignore', ) id: Annotated[ - str | int | None, + str | int | MISSING, Field( description='Scope-unique reference identifier for instances of this data group.' ), - ] = None + ] = MISSING areaDescription: Annotated[ - str | None, Field(description='Text description of the area') - ] = None + str | MISSING, Field(description='Text description of the area') + ] = MISSING exteriorType: Annotated[ - ExteriorUseTypeOptions | None, Field(description='Exterior Use Type') - ] = None + ExteriorUseTypeOptions | MISSING, Field(description='Exterior Use Type') + ] = MISSING isTradable: Annotated[ - bool | None, + bool | MISSING, Field( description='Boolean flag indicate whether the exterior is tradable or not' ), - ] = None + ] = MISSING powerDensity: Annotated[ - float | None, Field(description='Internal equipment power density', ge=0.0) - ] = None - quantityUnits: Annotated[str | None, Field(description='Quantity units')] = None + float | MISSING, Field(description='Internal equipment power density', ge=0.0) + ] = MISSING + quantityUnits: Annotated[ + str | None | MISSING, Field(description='Quantity units') + ] = MISSING useQuantity: Annotated[ - float | None, Field(description='The take-off quantity of the exterior use') - ] = None + float | MISSING, Field(description='The take-off quantity of the exterior use') + ] = MISSING exteriorLightingSpace: Annotated[ - ExteriorLightingSpace, Field(description='Exterior lighting space') + ExteriorLightingSpace | None, Field(description='Exterior lighting space') ] @@ -4040,11 +4449,11 @@ class Lighting(CustomBaseModel): extra='ignore', ) id: Annotated[ - str | int | None, + str | int | MISSING, Field( description='Scope-unique reference identifier for instances of this data group.' ), - ] = None + ] = MISSING wholeBldgUse: Annotated[ list[WholeBldgUse], Field(description='Whole building use areas') ] @@ -4054,8 +4463,8 @@ class Lighting(CustomBaseModel): ] exteriorUse: Annotated[list[ExteriorUse], Field(description='Exterior use areas')] fixtureSchedule: Annotated[ - list[FixtureSchedule] | None, Field(description='Fixture schedules') - ] = None + list[FixtureSchedule] | MISSING, Field(description='Fixture schedules') + ] = MISSING class ComBuilding(CustomBaseModel): @@ -4063,11 +4472,11 @@ class ComBuilding(CustomBaseModel): extra='ignore', ) id: Annotated[ - str | int | None, + str | int | MISSING, Field( description='Scope-unique reference identifier for instances of this data group.' ), - ] = None + ] = MISSING control: Annotated[ Control, Field(description='project control that contains the project meta data'), @@ -4076,11 +4485,11 @@ class ComBuilding(CustomBaseModel): envelope: Annotated[Envelope, Field(description='envelope elements in the project')] location: Annotated[Location, Field(description='location of the project')] semiheated: Annotated[ - bool | None, + bool | None | MISSING, Field( description='A boolean to indicate whether the project is a semi-heated type' ), - ] = None + ] = MISSING isNonresidentialConditioning: Annotated[ bool | None, Field( @@ -4100,39 +4509,40 @@ class ComBuilding(CustomBaseModel): ), ] = False isHistoricBuilding: Annotated[ - bool | None, Field(description='Flag to indicate if the building is historic.') - ] = False + IsHistoricBuilding | MISSING, + Field(description='Flag to indicate if the building is historic.'), + ] = MISSING performanceRating: Annotated[ - float | None, Field(description='Appendix C compliance index', ge=0.0) + float | None, Field(description='Appendix C compliance index') ] = None energyCreditPerformanceRating: Annotated[ float | None, - Field( - description='Appendix C compliance index for energy credit calculation', - ge=0.0, - ), + Field(description='Appendix C compliance index for energy credit calculation'), ] = None lighting: Annotated[Lighting, Field(description='Lighting')] - hvac: Annotated[HVAC | None, Field(description='HVAC')] = None + hvac: Annotated[HVAC | MISSING, Field(description='HVAC')] = MISSING renewable: Annotated[Renewable, Field(description='Renewable systems')] + bldgUseType: Annotated[ + BuildingUseTypeOptions | MISSING, Field(description='Building Use Type') + ] = MISSING buildingUseType: Annotated[ - BuildingUseTypeOptions | None, Field(description='Building Use Type') - ] = None + BuildingUseTypeOptions | MISSING, Field(description='Building Use Type') + ] = MISSING conditioningType: Annotated[ ConditionTypeOptions | None, Field(description='Building primary condition type'), ] = 'HEATING_AND_COOLING' swhSystem: Annotated[ - list[ServiceWaterHeatingSystem] | None, + list[ServiceWaterHeatingSystem] | MISSING, Field(description='Service water heating system', min_length=0), - ] = None + ] = MISSING efficiencyPackages: Annotated[ - list[Any] | None, + list[Any] | MISSING, Field(description='Efficiency Package or Energy Credits', min_length=0), - ] = None + ] = MISSING constructionType: Annotated[ - str | None, Field(description="Deprecated field, default to 'None'.") - ] = None + str | None | MISSING, Field(description="Deprecated field, default to 'None'.") + ] = MISSING allElectric: Annotated[ bool | int | None, Field( @@ -4163,9 +4573,9 @@ class ComBuilding(CustomBaseModel): description='Advanced reporting indicates whether the building conditioned by heat pumps' ), ] = None - projectType: Annotated[ - ProjectTypeOptions | None, Field(description='Project type') - ] = 'NEW_CONSTRUCTION' + projectType: Annotated[ProjectTypeOptions, Field(description='Project type')] = ( + 'NEW_CONSTRUCTION' + ) projectSubType: Annotated[ ProjectSubTypeOptions | None, Field(description='Project sub-type') ] = 'CONSTRUCTION_COMPLETE' @@ -4184,8 +4594,15 @@ class ComBuilding(CustomBaseModel): FuelTypeOptions | None, Field(description='Building primary heating fuel type') ] = 'UNKNOWN_FUEL' requirements: Annotated[ - list[Requirements] | None, Field(description='Requirements', min_length=0) - ] = None + list[Requirements] | MISSING, Field(description='Requirements', min_length=0) + ] = MISSING + efficiencyPackageType: Annotated[ + EfficiencyPackageType | None, Field(description='Efficiency Package Type') + ] = None + energyCreditMultiplierException: Annotated[ + EnergyCreditMultiplierException | MISSING, + Field(description='Energy Credit Multiplier Exception'), + ] = MISSING class CheckToolJsonSchema(RootModel[ComBuilding]): diff --git a/compare_buildings.py b/compare_buildings.py new file mode 100644 index 0000000..8a50218 --- /dev/null +++ b/compare_buildings.py @@ -0,0 +1,560 @@ +"""Compare building JSON exports against their Python (ComBuilding) round-trip. + +The JSON -> ComBuilding -> JSON round-trip introduces a set of *known* / +expected differences (defaulted fields, dropped metadata like ``userProject``, +etc.). Those live in ``diff_ignore.json`` and are filtered out so that only +*new* discrepancies are surfaced when comparing additional buildings. + +Usage +----- +Seed / update the ignore list from an existing diff file:: + + python compare_buildings.py --update-ignore building_diff.json + +Compare buildings (defaults to every ``*.json`` in ``buildings/`` if present, +otherwise ``building_json.json``):: + + python compare_buildings.py + python compare_buildings.py path/to/one_building.json another.json + +Get an actionable list of schema fixes for buildings that fail to validate:: + + python compare_buildings.py --report + +Two independent ignore lists (plain text, ``#`` for notes): + - ``diff_ignore.txt`` -- round-trip diff paths (used by the default mode) + - ``schema_ignore.txt`` -- validation failures to skip in ``--report`` +""" + +import argparse +import glob +import json +import os +import re +import sys +from collections import defaultdict +from typing import Any, Dict, List, Set, Tuple + +from jsondiff import diff + +from tools.generate_core_types import main as generate_core_types + +IGNORE_FILE = "diff_ignore.txt" +# Separate ignore list for the --report (schema validation) mode. These are +# validation failures you've decided not to act on, kept apart from the +# round-trip diff ignore list since the two mean different things. +SCHEMA_IGNORE_FILE = "schema_ignore.txt" +BUILDINGS_GLOB = "buildings/*.json" +DEFAULT_BUILDING = "building_json.json" + +# jsondiff (symmetric, marshalled) operator keys. +_INSERT_DELETE = ("$insert", "$delete") + + +def normalize_path(parts: Tuple[str, ...]) -> str: + """Join a path, collapsing numeric array indices to ``[]``. + + Array positions vary between buildings, so an ignored discrepancy at + ``hvac.hvacSystem.0.fanSystem`` should also match index ``1``, ``2``, ... + """ + return ".".join("[]" if p.isdigit() else p for p in parts) + + +def walk_diff(d: Any, prefix: Tuple[str, ...] = ()) -> List[Tuple[str, str, Any]]: + """Flatten a jsondiff (symmetric, marshalled) result into leaf findings. + + Returns a list of ``(normalized_path, op, value)`` tuples where ``op`` is + one of ``insert``, ``delete``, ``replace`` or ``change``. + """ + findings: List[Tuple[str, str, Any]] = [] + + if isinstance(d, dict): + for key, value in d.items(): + if key in _INSERT_DELETE: + op = key.lstrip("$") # "insert" / "delete" + if isinstance(value, dict): + # Object keys added/removed. + for subkey, subval in value.items(): + path = prefix + (str(subkey),) + findings.append((normalize_path(path), op, subval)) + elif isinstance(value, list): + # Array elements added/removed. + path = prefix + ("[]",) + for item in value: + findings.append((normalize_path(path), op, item)) + else: + findings.append((normalize_path(prefix), op, value)) + elif key == "$replace": + findings.append((normalize_path(prefix), "replace", value)) + elif isinstance(key, str) and key.startswith("$"): + # Any other operator ($update, etc.) -> recurse without + # extending the path. + findings.extend(walk_diff(value, prefix)) + else: + findings.extend(walk_diff(value, prefix + (str(key),))) + else: + findings.append((normalize_path(prefix), "change", d)) + + return findings + + +def load_ignore(ignore_file: str = IGNORE_FILE) -> Set[str]: + """Load the set of normalized paths to ignore from ``ignore_file``. + + The ignore file is plain text: one path per line. Blank lines and anything + after a ``#`` are treated as comments, so you can annotate entries inline + (e.g. ``userProject # dropped on purpose, not part of ComBuilding``). + """ + if not os.path.exists(ignore_file): + return set() + paths: Set[str] = set() + with open(ignore_file) as f: + for line in f: + entry = line.split("#", 1)[0].strip() + if entry: + paths.add(entry) + return paths + + +def save_ignore(paths: Set[str]) -> None: + """Append new paths to the ignore file, preserving existing notes/comments. + + Existing lines (including comments) are kept verbatim; only paths not + already present are appended, so hand-written notes are never clobbered. + """ + existing = load_ignore() + new = sorted(p for p in paths if p not in existing) + if not new: + return + header_needed = not os.path.exists(IGNORE_FILE) + with open(IGNORE_FILE, "a") as f: + if header_needed: + f.write("# Diff paths to ignore (one per line). " + "Use '#' for inline notes.\n") + for p in new: + f.write(f"{p}\n") + + +def is_ignored(path: str, ignore: Set[str]) -> bool: + """A path is ignored if it matches an ignore entry. + + An entry matches when it (a) equals the path, (b) is a prefix of it (so an + ignored subtree covers its descendants), or (c) is a ``*.suffix`` wildcard + that matches the trailing segment(s) at any depth -- e.g. ``*.listPosition`` + ignores ``listPosition`` wherever it appears. + """ + for ig in ignore: + if ig.startswith("*."): + suffix = ig[1:] # ".listPosition" + if path == ig[2:] or path.endswith(suffix): + return True + elif path == ig or path.startswith(ig + "."): + return True + return False + + +def diff_building(building_json: Dict[str, Any]) -> Any: + """Round-trip a building through ComBuilding and diff it against the raw JSON.""" + # Imported lazily: core_types is (re)generated by generate_core_types(). + from comcheck_api.types.core_types import ComBuilding + + building_python = ComBuilding(**building_json).model_dump(mode="json") + return diff(building_python, building_json, marshal=True, syntax="symmetric") + + +def update_ignore(diff_files: List[str]) -> None: + """Extend the ignore list with every leaf path found in ``diff_files``.""" + ignore = load_ignore() + added: Set[str] = set() + for path in diff_files: + with open(path) as f: + d = json.load(f) + for norm_path, _op, _value in walk_diff(d): + if norm_path and norm_path not in ignore: + added.add(norm_path) + ignore |= added + save_ignore(ignore) + print(f"Ignore list now has {len(ignore)} paths ({len(added)} added).") + for path in sorted(added): + print(f" + {path}") + + +def compare(building_files: List[str]) -> int: + """Compare each building JSON against its round-trip; report un-ignored diffs. + + Returns the total number of flagged (non-ignored) discrepancies. + """ + generate_core_types() + from pydantic import ValidationError + + ignore = load_ignore() + schema_ignore = load_ignore(SCHEMA_IGNORE_FILE) + # Definition names leak into Pydantic union-error paths; strip them. + global _DEF_NAMES + _DEF_NAMES = set(_load_schema().get("definitions", {})) + total_flagged = 0 + + errored = 0 + for path in building_files: + with open(path) as f: + building_json = json.load(f) + + try: + raw_diff = diff_building(building_json) + except ValidationError as exc: + errored += 1 + total_flagged += 1 + print(f"\n=== {path} ===") + # List the validation errors, noting any suppressed via the + # schema ignore list rather than hiding them silently. + shown, ignored = [], 0 + for err in exc.errors(): + _kind, epath, detail, _value = classify_error(err) + if is_ignored(epath, schema_ignore): + ignored += 1 + else: + shown.append((epath, detail)) + print(f" ! failed to validate as ComBuilding: " + f"{len(shown)} error(s), {ignored} ignored") + for epath, detail in shown: + print(f" - {epath}: {detail}") + continue + except Exception as exc: # non-validation failure + errored += 1 + total_flagged += 1 + print(f"\n=== {path} ===") + print(f" ! failed to round-trip through ComBuilding: " + f"{type(exc).__name__}") + first_line = str(exc).splitlines()[0] if str(exc) else "" + if first_line: + print(f" {first_line}") + continue + + findings = walk_diff(raw_diff) + flagged = [f for f in findings if not is_ignored(f[0], ignore)] + + ignored_count = len(findings) - len(flagged) + total_flagged += len(flagged) + + print(f"\n=== {path} ===") + print(f" {len(flagged)} flagged, {ignored_count} ignored") + for norm_path, op, value in flagged: + print(f" [{op}] {norm_path} = {json.dumps(value, default=str)}") + + print(f"\nTotal flagged across {len(building_files)} building(s): " + f"{total_flagged} ({errored} failed to round-trip)") + return total_flagged + + +# --- Schema-fix report ------------------------------------------------------ + +# datamodel-codegen tags union branches in error locations, e.g. +# "fanEfficiencyExceptionType.str-enum[FanEfficiencyExceptionTypeOptions]" or +# "cavityRValue.float". These aren't real data keys, so we strip them when +# building a clean dotted path, but we mine them for the enum/type name. +_BRANCH_TAG = re.compile(r"^(str-enum|int-enum|enum)\[(?P[^\]]+)\]$") +# Pydantic union-branch tags that are not real data keys: plain type names, +# ``list[Fan]`` / ``dict[...]`` shapes, etc. +_TYPE_BRANCH = {"str", "int", "float", "bool", "constrained-str", "list", "dict"} +_SHAPE_TAG = re.compile(r"^(list|dict|tuple)\[.*\]$") + + +# Populated by report() from the schema's definition names; these leak into +# Pydantic union-error locations (e.g. the "HVAC" in "hvac.HVAC.fanSystem"). +_DEF_NAMES: Set[str] = set() + + +def _is_branch_tag(s: str) -> bool: + return ( + s == "missing-sentinel" + or _BRANCH_TAG.match(s) is not None + or _SHAPE_TAG.match(s) is not None + or s in _TYPE_BRANCH + or s in _DEF_NAMES + ) + + +def _clean_loc(loc: Tuple[Any, ...]) -> str: + """Turn a Pydantic error location into a normalized dotted path. + + Array indices collapse to ``[]`` and pydantic union-branch tags + (``str-enum[...]``, ``list[Fan]``, ``.float``, ``missing-sentinel``) are + dropped so the path reads as real data keys. + """ + parts: List[str] = [] + for p in loc: + if isinstance(p, int): + parts.append("[]") + continue + s = str(p) + if _is_branch_tag(s): + continue + parts.append(s) + return ".".join(parts) + + +SCHEMA_FILE = os.path.join("comcheck_api", "schemas", "comCheck.schema.json") + + +def _load_schema() -> Dict[str, Any]: + with open(SCHEMA_FILE) as f: + return json.load(f) + + +def _deref(node: Dict[str, Any], schema: Dict[str, Any]) -> Tuple[Dict[str, Any], str]: + """Follow a ``$ref`` (if present) and return (target_node, definition_name).""" + ref = node.get("$ref") + if not ref: + return node, "" + name = ref.split("/")[-1] + return schema.get("definitions", {}).get(name, {}), name + + +def resolve_schema_target( + dotted_path: str, schema: Dict[str, Any] +) -> Tuple[str, str]: + """Walk the schema along a cleaned data path. + + Returns ``(location, enum_def_name)`` where ``location`` is a string like + ``definitions/AgWall -> properties/wallType`` pinpointing the node to edit, + and ``enum_def_name`` is the ``*Options`` definition backing the field if + it is an enum reference ("" for inline enums / non-enums). Returns + ``("", "")`` when the path can't be resolved against the schema. + """ + node = schema.get("definitions", {}).get("ComBuilding", {}) + def_name = "ComBuilding" # the enclosing definition + prop_seg = "" # the final property key within that definition + enum_def = "" + + for seg in dotted_path.split("."): + if seg == "[]": + items = node.get("items") or node.get("item") or {} + node, ref_name = _deref(items, schema) + if ref_name: + def_name, prop_seg = ref_name, "" + continue + props = node.get("properties", {}) + if seg not in props: + return ("", "") # path diverges (unknown / extra field) + raw = props[seg] + # Capture the enum def name from a $ref before dereferencing. + ref = raw.get("$ref", "") + target, ref_name = _deref(raw, schema) + if ref_name and target.get("enum") is not None: + enum_def = ref_name + elif ref_name: + # Non-enum sub-object: descend into it as the new enclosing def. + def_name, prop_seg = ref_name, "" + node = target + continue + node = target + prop_seg = seg + + loc = f"definitions/{def_name}" + if prop_seg: + loc += f" -> properties/{prop_seg}" + return (loc, enum_def) + + +def classify_error(err: Dict[str, Any]) -> Tuple[str, str, str, Any]: + """Map one Pydantic error to (fix_kind, path, detail, offending_value). + + ``fix_kind`` is one of: + - ``enum-missing-value`` : add the value to the enum ``*Options`` def + - ``needs-null`` : field arrives as null but schema forbids it + - ``constraint-too-strict`` : a min/max/etc. constraint rejects real data + - ``other`` : anything not auto-classified + """ + etype = err["type"] + path = _clean_loc(err["loc"]) + value = err.get("input") + + # Which enum definition is implicated (from the branch tag), if any. + enum_name = "" + for p in err["loc"]: + m = _BRANCH_TAG.match(str(p)) + if m: + enum_name = m.group("name") + break + + if etype == "enum": + if value is None: + return ("needs-null", path, "enum should allow null", value) + target = enum_name or "" + return ("enum-missing-value", path, + f"add {value!r} to enum '{target}'", value) + + if etype in ("string_type", "int_type", "float_type", "bool_type", + "int_parsing", "float_parsing") and value is None: + return ("needs-null", path, "field arrives as null", value) + + if etype == "missing_sentinel_error": + # Secondary branch of a union; the real story is told by the sibling + # enum/type error. Mark as such so we can dedupe it away. + return ("secondary", path, "union branch (see sibling error)", value) + + if etype in ("greater_than", "greater_than_equal", "less_than", + "less_than_equal", "multiple_of"): + ctx = err.get("ctx", {}) + return ("constraint-too-strict", path, + f"{etype} {ctx} rejects value {value!r}", value) + + return ("other", path, f"{etype}: {err.get('msg', '')}", value) + + +def report(building_files: List[str]) -> int: + """Collect round-trip validation failures and print actionable schema fixes. + + Returns the number of distinct issues found. + """ + generate_core_types() + from comcheck_api.types.core_types import ComBuilding + from pydantic import ValidationError + + # Pydantic injects the model class name as a path segment in union errors + # (e.g. "hvac.HVAC.fanSystem..."). Those definition names aren't data keys, + # so strip them before cleaning paths. + global _DEF_NAMES + _DEF_NAMES = set(_load_schema().get("definitions", {})) + + # Validation failures you've decided not to act on, kept in a file separate + # from the round-trip diff ignore list. + schema_ignore = load_ignore(SCHEMA_IGNORE_FILE) + + # fix_kind -> (path, detail) -> {values seen, files affected} + issues: Dict[str, Dict[Tuple[str, str], Dict[str, set]]] = defaultdict( + lambda: defaultdict(lambda: {"values": set(), "files": set()}) + ) + failed_files: Set[str] = set() + ignored_count = 0 + + for path in building_files: + with open(path) as f: + building_json = json.load(f) + try: + ComBuilding(**building_json) + except ValidationError as exc: + fname = os.path.basename(path) + for err in exc.errors(): + kind, epath, detail, value = classify_error(err) + if is_ignored(epath, schema_ignore): + ignored_count += 1 + continue + failed_files.add(path) + bucket = issues[kind][(epath, detail)] + if value is not None: + bucket["values"].add(repr(value)[:60]) + bucket["files"].add(fname) + + # Drop "secondary" union-branch noise where a real sibling error exists for + # the same path (keeps the report focused on the actual fix). + real_paths = { + epath + for kind in ("enum-missing-value", "needs-null", "constraint-too-strict") + for (epath, _detail) in issues.get(kind, {}) + } + for (epath, detail) in list(issues.get("secondary", {})): + if epath in real_paths: + del issues["secondary"][(epath, detail)] + + # --- render --- + schema = _load_schema() + print(f"\n{'=' * 70}") + print(f"SCHEMA FIX REPORT ({len(failed_files)}/{len(building_files)} " + f"buildings failed to validate)") + print(f"file: {SCHEMA_FILE} (regenerate types after editing)") + if ignored_count: + print(f"({ignored_count} error(s) suppressed via {SCHEMA_IGNORE_FILE})") + print(f"{'=' * 70}") + + order = [ + ("enum-missing-value", "1. MISSING ENUM VALUES (add to the *Options enum)"), + ("needs-null", "2. FIELDS THAT MUST ALLOW null (use [\"\", \"null\"])"), + ("constraint-too-strict", "3. CONSTRAINTS TOO STRICT (relax min/max)"), + ("other", "4. OTHER (needs manual review)"), + ("secondary", "5. UNRESOLVED union-branch errors (no sibling fix found)"), + ] + + total_issues = 0 + for kind, header in order: + entries = issues.get(kind, {}) + if not entries: + continue + print(f"\n{header}") + print("-" * 70) + for (epath, detail) in sorted(entries): + info = entries[(epath, detail)] + total_issues += 1 + vals = ", ".join(sorted(info["values"])[:6]) if info["values"] else "" + nfiles = len(info["files"]) + loc, enum_def = resolve_schema_target(epath, schema) + # Prefer the resolved enum definition name over the branch-tag guess. + if kind == "enum-missing-value" and enum_def: + detail = detail.replace("''", f"'{enum_def}'") + print(f" • {epath}") + print(f" fix: {detail}") + if loc: + where = loc + if kind == "enum-missing-value" and enum_def: + where += f" -> definitions/{enum_def}/enum" + print(f" where: {where}") + else: + print(" where: ") + if vals: + print(f" values seen: {vals}") + print(f" seen in {nfiles} file(s)") + + print(f"\n{'=' * 70}") + print(f"{total_issues} distinct issue(s) to fix across " + f"{len(failed_files)} failing building(s).") + print(f"{'=' * 70}") + return total_issues + + +def resolve_building_files(args_files: List[str]) -> List[str]: + if args_files: + return args_files + globbed = sorted(glob.glob(BUILDINGS_GLOB)) + if globbed: + return globbed + return [DEFAULT_BUILDING] + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--update-ignore", + nargs="+", + metavar="DIFF_JSON", + help="Add every path in the given diff file(s) to the ignore list.", + ) + parser.add_argument( + "--report", + action="store_true", + help="Instead of diffing, classify round-trip validation failures " + "into actionable schema fixes (missing enum values, nullable fields, " + "over-strict constraints).", + ) + parser.add_argument( + "files", + nargs="*", + help="Building JSON export files to compare " + f"(default: {BUILDINGS_GLOB} or {DEFAULT_BUILDING}).", + ) + args = parser.parse_args() + + if args.update_ignore: + update_ignore(args.update_ignore) + return + + if args.report: + issues = report(resolve_building_files(args.files)) + sys.exit(1 if issues else 0) + + flagged = compare(resolve_building_files(args.files)) + sys.exit(1 if flagged else 0) + + +if __name__ == "__main__": + main() diff --git a/diff_ignore.txt b/diff_ignore.txt new file mode 100644 index 0000000..364d632 --- /dev/null +++ b/diff_ignore.txt @@ -0,0 +1,14 @@ +# Diff paths to ignore (one per line). Use '#' for inline notes. +# +# A path matches if it equals an entry, is nested under one, or matches a +# '*.suffix' wildcard at any depth. Array indices are normalized to '[]'. + +envelope.roof.[].highAlbedoRoofType # Don't think this is needed +hvac.hvacSystem.[].fanSystem # Not sure where this and the following come from - not in sample.json +hvac.hvacSystem.[].fanSystemId +hvac.hvacSystem.[].fanSystemObjNum +hvac.fanSystem.[].number # not modeled +lighting.activityUse # empty list added on round-trip +userProject # API metadata, not part of ComBuilding +*.listPosition # stripped everywhere +hvac.hvacPlant.[].fanSystem # Should be stripped diff --git a/pyproject.toml b/pyproject.toml index 57e1a55..930e62e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ keywords = [ dependencies = [ "httpx>=0.27.0", + "jsondiff>=2.2.1", "jsonschema>=4.23.0", "pydantic>=2.12.5", "python-dotenv>=1.0.0", @@ -60,7 +61,7 @@ members = [] [dependency-groups] dev = [ "black>=26.1.0", - "datamodel-code-generator>=0.54.1", + "datamodel-code-generator>=0.71.0", "mypy>=1.19.1", "pre-commit>=4.5.1", "pytest>=9.0.2", diff --git a/schema_changes_notes.md b/schema_changes_notes.md new file mode 100644 index 0000000..aff1914 --- /dev/null +++ b/schema_changes_notes.md @@ -0,0 +1,58 @@ +# Notes: comCheck.schema.json & Pydantic Generation Changes + +Takeaways from the most recent round of changes to `comcheck_api/schemas/comCheck.schema.json` +and the Pydantic model generation. + +## 1. Use `--use-missing-sentinel` for optional fields + +Passing `--use-missing-sentinel` to the generator lets fields be marked with a `MISSING` +identifier instead of defaulting to a JSON value. When the model is exported back to JSON, +`MISSING` fields are omitted entirely. + +**Why it matters:** we were hitting problems where fields that weren't present in the original +export would default to values we didn't want. The sentinel avoids inventing data — absent stays +absent on round-trip. + +## 2. Avoid adding redundant `NONE` to enumerations + +Adding `NONE` to enums caused a lot of churn, and in many cases an option that already means "none" +existed. Example: `SlabInsulationPositionOptions` already has `NO_INSULATION` **and** `NONE`. + +**Action:** before adding `NONE`, check whether the enum already has an equivalent member and reuse it +rather than introducing a duplicate. + +## 3. Allow `null` as a valid type for many fields + +A lot of values arrive as `null` and should *not* be coerced to a default. To handle this correctly +we had to explicitly allow `null` as a type for many fields in the JSON schema. + +## 4. Exemption types and activity types are likely incomplete + +I added a number of exemption types and activity types, but I'm not confident the coverage is complete. + +**Action:** cross-reference the enumeration directly in the backend code to confirm all valid +exemption/activity types are represented. + +## 5. Put `null` inside the enumeration instead of `anyOf: [enum, null]` + +Rather than repeating `anyOf: [enumeration, null]` across many fields, I added `null` directly to the +enumeration itself. This avoids repetition and is simpler to write. + +**Note:** when the enum carries the type, you can also drop the `type` keyword on the field — the type +is inferred from the enumeration. + +## 6. Reconsider `minimum` constraints + +`minimum` flags caused failures — e.g. a building with `preAltPropUval` less than 0 failed schema +validation and couldn't be loaded into the `ComBuilding` object. + +**Open question:** do we actually need `minimum` in the schema? It isn't necessarily enforced by the +backend, and its main effect right now is blocking otherwise-valid projects from loading. Consider +removing these unless the constraint is genuinely required. + +## 7. Drop non-informative descriptions; prefer `title` + +Many `description` fields just restate the field name and add nothing. + +**Action:** remove descriptions that don't add information. If the text is just a formatted version of +the field name, use the `title` keyword instead of `description`. diff --git a/schema_ignore.txt b/schema_ignore.txt new file mode 100644 index 0000000..0621f50 --- /dev/null +++ b/schema_ignore.txt @@ -0,0 +1,24 @@ +# Schema-validation failures to ignore in `compare_buildings.py --report`. +# One path per line; blank lines and text after '#' are notes. +# +# Matching is the same as the diff ignore list: exact, prefix (covers a whole +# subtree), or '*.suffix' wildcard at any depth. Array indices normalize to '[]'. +# +# Use this for validation errors you've decided NOT to fix in the schema +# (bad/legacy data, deprecated fields, etc.) so they stop cluttering the report. +# Keep this separate from diff_ignore.txt, which is for round-trip diffs. + +*.constructionType # odd/legacy values (e.g. "2","5"); deprecated field +envelope.floor.floorType # ['OTHER_BG_WALL', 'OTHER_DOOR', 'OTHER_FRAME'] to enum 'FloorTypeOptions' +envelope.roof.roofType # add 'OTHER_FLOOR' to enum +lighting.wholeBldgUse.activityUse.interiorLightingSpace.fixture.advControlsAllowanceType # ['TEST'] to enum 'AdvancedControlsAllowanceTypeOptions' +*.continuousRValue # Negative values +*.propShgc # Negative values +*.propProjectionFactor # Negative values +*.cavityRValue # Negative values +envelope.roof.[].roofType # OTHER_DOOR +envelope.floor.[].floorType # OTHER_FRAME +envelope.agWall.[].door.[].grossArea # -1 +envelope.altPctGlazingAreaReplaced # -1 +envelope.postAltWindowWallPct # -1, -2 +hvac.fanSystem.[].fan.[].fanDesignEfficiency # 900 \ No newline at end of file diff --git a/tools/generate_core_types.py b/tools/generate_core_types.py index 055915f..e234ec6 100644 --- a/tools/generate_core_types.py +++ b/tools/generate_core_types.py @@ -14,35 +14,41 @@ # Ensure output directory exists OUTPUT_TYPES.parent.mkdir(parents=True, exist_ok=True) -# Run datamodel-codegen CLI -result = subprocess.run( - [ - "datamodel-codegen", - "--input", - str(INPUT_SCHEMA), - "--input-file-type", - "jsonschema", - "--output", - str(OUTPUT_TYPES), - "--extra-fields", - "ignore", - "--output-model-type", - "pydantic_v2.BaseModel", - "--base-class", - "comcheck_api.types.custom_base_model.CustomBaseModel", - "--target-python-version", - "3.13", - "--use-standard-collections", - "--use-schema-description", - "--use-default", # Use default values from the schema - "--field-constraints", # Generate validation constraints (e.g., max_length, minItems) - "--use-annotated", # Best practice for Pydantic V2 validations - ], - check=False, -) +def main(): + """Main function to run the script.""" + # Run the datamodel-codegen command + result = subprocess.run( + [ + "datamodel-codegen", + "--input", + str(INPUT_SCHEMA), + "--input-file-type", + "jsonschema", + "--output", + str(OUTPUT_TYPES), + "--extra-fields", + "ignore", + "--output-model-type", + "pydantic_v2.BaseModel", + "--base-class", + "comcheck_api.types.custom_base_model.CustomBaseModel", + "--target-python-version", + "3.13", + "--use-standard-collections", + "--use-schema-description", + "--use-missing-sentinel", + "--use-default", # Use default values from the schema + "--field-constraints", # Generate validation constraints (e.g., max_length, minItems) + "--use-annotated", # Best practice for Pydantic V2 validations + ], + check=False, + ) + + if result.returncode == 0: + print(f"Generated: {OUTPUT_TYPES}") + else: + print(f"Generation failed with exit code {result.returncode}", file=sys.stderr) + sys.exit(result.returncode) -if result.returncode == 0: - print(f"Generated: {OUTPUT_TYPES}") -else: - print(f"Generation failed with exit code {result.returncode}", file=sys.stderr) - sys.exit(result.returncode) +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/uv.lock b/uv.lock index 1cf6932..2f84e76 100644 --- a/uv.lock +++ b/uv.lock @@ -2,7 +2,8 @@ version = 1 revision = 3 requires-python = ">=3.12" resolution-markers = [ - "python_full_version < '4'", + "python_full_version >= '3.14' and python_full_version < '4'", + "python_full_version < '3.14'", "python_full_version >= '4'", ] @@ -220,6 +221,7 @@ version = "1.0.1" source = { editable = "." } dependencies = [ { name = "httpx" }, + { name = "jsondiff" }, { name = "jsonschema" }, { name = "pydantic" }, { name = "python-dotenv" }, @@ -243,6 +245,7 @@ docs = [ [package.metadata] requires-dist = [ { name = "httpx", specifier = ">=0.27.0" }, + { name = "jsondiff", specifier = ">=2.2.1" }, { name = "jsonschema", specifier = ">=4.23.0" }, { name = "pydantic", specifier = ">=2.12.5" }, { name = "python-dotenv", specifier = ">=1.0.0" }, @@ -252,7 +255,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ { name = "black", specifier = ">=26.1.0" }, - { name = "datamodel-code-generator", specifier = ">=0.54.1" }, + { name = "datamodel-code-generator", specifier = ">=0.71.0" }, { name = "mypy", specifier = ">=1.19.1" }, { name = "pre-commit", specifier = ">=4.5.1" }, { name = "pytest", specifier = ">=9.0.2" }, @@ -265,22 +268,21 @@ docs = [ [[package]] name = "datamodel-code-generator" -version = "0.54.1" +version = "0.71.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "argcomplete" }, - { name = "black" }, + { name = "black", marker = "sys_platform != 'emscripten'" }, { name = "genson" }, { name = "inflect" }, - { name = "isort" }, + { name = "isort", marker = "sys_platform != 'emscripten'" }, { name = "jinja2" }, - { name = "packaging" }, { name = "pydantic" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/79/4b/6a63ea00c65402576e05e8cc963349ffe58db07d8c8183ab51488dbfb67a/datamodel_code_generator-0.54.1.tar.gz", hash = "sha256:dd9eb7594f94a8b85d7e410f4d997a443cf7a52a1dcc049fae6cf35660f18803", size = 829716, upload-time = "2026-03-04T04:15:02.582Z" } +sdist = { url = "https://files.pythonhosted.org/packages/54/f5/f4ce23d99503b147c9ec514dc995a96d3b4d2a3284252ad665f875a3145d/datamodel_code_generator-0.71.0.tar.gz", hash = "sha256:d27cd7a0d10f9b2db74a41db7f3e050c226da9cf0afb4916a7ab56275ebacbf2", size = 1684916, upload-time = "2026-07-24T15:32:04.334Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/ce/8a8aadbb2fb428109949d0f7a42232d1d452dab0b8550f6e8c5843afa93d/datamodel_code_generator-0.54.1-py3-none-any.whl", hash = "sha256:67c59ff2368eb2ec96ba11441bc8957bb68a71459dd37275a78ea90238ad5f01", size = 264344, upload-time = "2026-03-04T04:15:00.721Z" }, + { url = "https://files.pythonhosted.org/packages/c6/4d/556cb290170f41b97ce50fd872e10a266f141d7a38352bb3071e4ae61f41/datamodel_code_generator-0.71.0-py3-none-any.whl", hash = "sha256:680b68338d59e98a0559eeb54d8e5ca33c35b3ec0bef922ec2cc783f2cb28e9a", size = 452379, upload-time = "2026-07-24T15:32:02.467Z" }, ] [[package]] @@ -429,6 +431,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] +[[package]] +name = "jsondiff" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/35/48/841137f1843fa215ea284834d1514b8e9e20962bda63a636c7417e02f8fb/jsondiff-2.2.1.tar.gz", hash = "sha256:658d162c8a86ba86de26303cd86a7b37e1b2c1ec98b569a60e2ca6180545f7fe", size = 26649, upload-time = "2024-08-29T04:09:06.201Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/94/a8066f84d62ab666d61ef97deba1a33126e3e5c0c0da2c458ada17053ed6/jsondiff-2.2.1-py3-none-any.whl", hash = "sha256:b1f0f7e2421881848b1d556d541ac01a91680cfcc14f51a9b62cdf4da0e56722", size = 13440, upload-time = "2024-08-29T04:09:04.955Z" }, +] + [[package]] name = "jsonschema" version = "4.25.1" From d090464e1bb7413b29d37470110edc176a9dc98d Mon Sep 17 00:00:00 2001 From: Julian Slane Date: Thu, 6 Aug 2026 10:43:29 -0700 Subject: [PATCH 09/23] Move temp files to scratch folder --- compare_buildings.py => scratch/compare_buildings.py | 0 diff_ignore.txt => scratch/diff_ignore.txt | 0 schema_changes_notes.md => scratch/schema_changes_notes.md | 0 schema_ignore.txt => scratch/schema_ignore.txt | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename compare_buildings.py => scratch/compare_buildings.py (100%) rename diff_ignore.txt => scratch/diff_ignore.txt (100%) rename schema_changes_notes.md => scratch/schema_changes_notes.md (100%) rename schema_ignore.txt => scratch/schema_ignore.txt (100%) diff --git a/compare_buildings.py b/scratch/compare_buildings.py similarity index 100% rename from compare_buildings.py rename to scratch/compare_buildings.py diff --git a/diff_ignore.txt b/scratch/diff_ignore.txt similarity index 100% rename from diff_ignore.txt rename to scratch/diff_ignore.txt diff --git a/schema_changes_notes.md b/scratch/schema_changes_notes.md similarity index 100% rename from schema_changes_notes.md rename to scratch/schema_changes_notes.md diff --git a/schema_ignore.txt b/scratch/schema_ignore.txt similarity index 100% rename from schema_ignore.txt rename to scratch/schema_ignore.txt From 7a1e8ffa6000dfe291a399293a49b44a2c2466a6 Mon Sep 17 00:00:00 2001 From: Julian Slane Date: Thu, 6 Aug 2026 11:22:19 -0700 Subject: [PATCH 10/23] remove exclude_unset=True --- comcheck_api/client/comcheck_client.py | 12 ++++++------ comcheck_api/managers/data_manager.py | 2 +- comcheck_api/utilities/common.py | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/comcheck_api/client/comcheck_client.py b/comcheck_api/client/comcheck_client.py index 0e1c8a5..30eca6d 100644 --- a/comcheck_api/client/comcheck_client.py +++ b/comcheck_api/client/comcheck_client.py @@ -167,7 +167,7 @@ def update_project( if not old_project: raise COMCheckProjectNotFoundError(project_id) - project_data_json = project_data.model_dump(mode="json", exclude_unset=True) + project_data_json = project_data.model_dump(mode="json") # Preserve user project reference user_project = old_project["userProject"] @@ -262,7 +262,7 @@ def update_uvalues(self, project: ComBuilding) -> ComBuilding: The same ``project`` instance, with u-values updated. """ energy_code = str(project.control.code) - envelope_data = project.envelope.model_dump(mode="json", exclude_unset=True) + envelope_data = project.envelope.model_dump(mode="json") updated_assembly_uvalues = self._service.assemblies_uvalue( envelope_data, energy_code )["data"] @@ -296,7 +296,7 @@ def check_UA_compliance(self, project: ComBuilding) -> Any: Returns: The compliance results payload returned by the API. """ - project_data = project.model_dump(mode="json", exclude_unset=True) + project_data = project.model_dump(mode="json") response = self._service.check_UA_compliance(project_data) return response.get("data") @@ -309,7 +309,7 @@ def check_requirements(self, project: ComBuilding) -> Any: Returns: The requirements payload returned by the API. """ - project_data = project.model_dump(mode="json", exclude_unset=True) + project_data = project.model_dump(mode="json") response = self._service.check_requirements(project_data) return response.get("data") @@ -349,7 +349,7 @@ def generate_report( ``expires``, and ``fileName``. """ report_data = { - "building": project.model_dump(mode="json", exclude_unset=True), + "building": project.model_dump(mode="json"), "envelope": envelope, "extlighting": extlighting, "intlighting": intlighting, @@ -396,7 +396,7 @@ def start_run_simulation( logger.info("Updating project: %s", project_id) project = self.update_project(str(project_id), project) - project_data = project.model_dump(mode="json", exclude_unset=True) + project_data = project.model_dump(mode="json") run_result = self._service.start_run_simulation(project_data) if run_result.data is None: raise COMCheckSimulationError( diff --git a/comcheck_api/managers/data_manager.py b/comcheck_api/managers/data_manager.py index c45d07a..7b5b845 100644 --- a/comcheck_api/managers/data_manager.py +++ b/comcheck_api/managers/data_manager.py @@ -299,7 +299,7 @@ def modify_one(self, id_value: Any, updates: T | dict[str, Any]) -> T: # Convert updates to dict if it's a model object updates_dict: dict[str, Any] = ( - updates.model_dump(mode="json", exclude_unset=True) + updates.model_dump(mode="json") if isinstance(updates, BaseModel) else updates ) diff --git a/comcheck_api/utilities/common.py b/comcheck_api/utilities/common.py index 4e3670f..4b52675 100644 --- a/comcheck_api/utilities/common.py +++ b/comcheck_api/utilities/common.py @@ -18,7 +18,7 @@ def _json_default(obj: Any) -> Any: aliases, and nested models serialize the same way the API expects. """ if isinstance(obj, BaseModel): - return obj.model_dump(mode="json", exclude_unset=True) + return obj.model_dump(mode="json") raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable") From 1a39db64df3e769a75304eaa295f138689c59ac8 Mon Sep 17 00:00:00 2001 From: yanz571 Date: Fri, 7 Aug 2026 11:43:07 -0700 Subject: [PATCH 11/23] update lighting related fields --- comcheck_api/schemas/comCheck.schema.json | 83 ++++++++++++++++++----- 1 file changed, 65 insertions(+), 18 deletions(-) diff --git a/comcheck_api/schemas/comCheck.schema.json b/comcheck_api/schemas/comCheck.schema.json index 72cd31c..55f7ae1 100644 --- a/comcheck_api/schemas/comCheck.schema.json +++ b/comcheck_api/schemas/comCheck.schema.json @@ -2943,7 +2943,10 @@ }, "floorArea": { "description": "Whole building use floor area", - "type": "number", + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "ft2", "$comment": "User shall provide this data" @@ -2991,13 +2994,19 @@ }, "allowedWattage": { "description": "Allowed wattage", - "type": "number", + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "watt" }, "proposedWattage": { "description": "Proposed wattage", - "type": "number", + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "watt", "$comment": "user shall provide this data" @@ -3072,7 +3081,10 @@ }, "floorArea": { "description": "Whole building use floor area", - "type": "number", + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "ft2", "$comment": "User shall provide this data" @@ -3120,13 +3132,19 @@ }, "allowedWattage": { "description": "Allowed wattage", - "type": "number", + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "watt" }, "proposedWattage": { "description": "Proposed wattage", - "type": "number", + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "watt", "$comment": "user shall provide this data" @@ -3196,7 +3214,10 @@ }, "powerDensity": { "description": "Internal equipment power density", - "type": "number", + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "watt/ft2", "$comment": "Engine calculated value" @@ -3211,7 +3232,10 @@ }, "useQuantity": { "description": "The take-off quantity of the exterior use", - "type": "number" + "type": [ + "number", + "null" + ] }, "exteriorLightingSpace": { "description": "Exterior lighting space", @@ -3349,31 +3373,46 @@ }, "primaryDaylight": { "description": "Daylighting on the primary sidelight area", - "type": "number", + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "ft2" }, "secondaryDaylight": { "description": "Daylighting on the secondary sidelight area", - "type": "number", + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "ft2" }, "skylightToplight": { "description": "Daylighting on the skylight toplight area", - "type": "number", + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "ft2" }, "roofMonitorToplight": { "description": "Daylighting on the roof monitor top light area", - "type": "number", + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "ft2" }, "decorativeArea": { "description": "The floor area that covered by decorative lightings", - "type": "number", + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "ft2" }, @@ -3534,7 +3573,10 @@ }, "fixtureWattage": { "description": "fixture wattage", - "type": "number", + "type": [ + "number", + "null" + ], "minimum": 0.0, "unit": "watt" }, @@ -3671,7 +3713,7 @@ "required": [ "description", "quantity", - "lightingType", + "fixtureType", "lightingControl" ], "additionalProperties": true @@ -3681,11 +3723,17 @@ "properties": { "id": { "description": "Scope-unique reference identifier for instances of this data group.", - "type": "integer" + "type": [ + "string", + "integer" + ] }, "lightingId": { "description": "Scope-unique reference identifier for instances of the parent lighting group.", - "type": "integer" + "type": [ + "string", + "integer" + ] }, "scheduleFixtureKey": { "description": "UUID to identify this fixture schedule.", @@ -3786,7 +3834,6 @@ "description", "fixtureType", "fixtureWattage", - "lightingType", "scheduleFixtureKey" ], "additionalProperties": true From c42a95e4951aef8d7e09f7d7668b23559cc07996 Mon Sep 17 00:00:00 2001 From: yanz571 Date: Fri, 7 Aug 2026 11:49:22 -0700 Subject: [PATCH 12/23] update core_types --- comcheck_api/types/core_types.py | 51 ++++++++++++++++++-------------- tools/generate_core_types.py | 7 ++++- 2 files changed, 34 insertions(+), 24 deletions(-) diff --git a/comcheck_api/types/core_types.py b/comcheck_api/types/core_types.py index 572e924..717aed1 100644 --- a/comcheck_api/types/core_types.py +++ b/comcheck_api/types/core_types.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: comCheck.schema.json -# timestamp: 2026-08-05T21:47:01+00:00 +# timestamp: 2026-08-07T18:48:17+00:00 from __future__ import annotations @@ -173,6 +173,7 @@ class WallTypeOptionsEnum(StrEnum): class WallTypeOptions(RootModel[WallTypeOptionsEnum | None | MISSING]): root: WallTypeOptionsEnum | None | MISSING = MISSING + class Location(CustomBaseModel): model_config = ConfigDict( extra='ignore', @@ -3003,13 +3004,13 @@ class FixtureSchedule(CustomBaseModel): extra='ignore', ) id: Annotated[ - int | MISSING, + str | int | MISSING, Field( description='Scope-unique reference identifier for instances of this data group.' ), ] = MISSING lightingId: Annotated[ - int | MISSING, + str | int | MISSING, Field( description='Scope-unique reference identifier for instances of the parent lighting group.' ), @@ -3030,8 +3031,8 @@ class FixtureSchedule(CustomBaseModel): float | None, Field(description='fixture wattage', ge=0.0) ] lightingType: Annotated[ - LightingTypeOptions, Field(description='lighting fixture type') - ] + LightingTypeOptions | MISSING, Field(description='lighting fixture type') + ] = MISSING trackCircuitBreakerAmps: Annotated[ float | None | MISSING, Field(description='Track lighting circuit breaker amps', ge=0.0), @@ -3904,18 +3905,18 @@ class Fixture(CustomBaseModel): Field(description='lighting exemption type'), ] = MISSING fixtureType: Annotated[ - str | None | MISSING, + str | None, Field(description='This field temporarily used to describe the fixture.'), - ] = MISSING + ] fixtureWattage: Annotated[ - float | MISSING, Field(description='fixture wattage', ge=0.0) + float | None | MISSING, Field(description='fixture wattage', ge=0.0) ] = MISSING lampType: Annotated[ str | None | MISSING, Field(description='deprecated, use null') ] = MISSING lightingType: Annotated[ - LightingTypeOptions, Field(description='lighting fixture type') - ] + LightingTypeOptions | MISSING, Field(description='lighting fixture type') + ] = MISSING numberOfLamps: Annotated[ float | None | MISSING, Field(description='deprecated, use null') ] = MISSING @@ -4210,23 +4211,23 @@ class InteriorLightingSpace(CustomBaseModel): Field(description='Workplane-to-luminaire height', ge=0.0), ] = MISSING primaryDaylight: Annotated[ - float | MISSING, + float | None | MISSING, Field(description='Daylighting on the primary sidelight area', ge=0.0), ] = MISSING secondaryDaylight: Annotated[ - float | MISSING, + float | None | MISSING, Field(description='Daylighting on the secondary sidelight area', ge=0.0), ] = MISSING skylightToplight: Annotated[ - float | MISSING, + float | None | MISSING, Field(description='Daylighting on the skylight toplight area', ge=0.0), ] = MISSING roofMonitorToplight: Annotated[ - float | MISSING, + float | None | MISSING, Field(description='Daylighting on the roof monitor top light area', ge=0.0), ] = MISSING decorativeArea: Annotated[ - float | MISSING, + float | None | MISSING, Field( description='The floor area that covered by decorative lightings', ge=0.0 ), @@ -4272,7 +4273,8 @@ class ActivityUse(CustomBaseModel): str | MISSING, Field(description='Text description of the area') ] = MISSING floorArea: Annotated[ - float | MISSING, Field(description='Whole building use floor area', ge=0.0) + float | None | MISSING, + Field(description='Whole building use floor area', ge=0.0), ] = MISSING ceilingHeight: Annotated[ float | None | MISSING, Field(description='Average ceiling height', ge=0.0) @@ -4289,10 +4291,10 @@ class ActivityUse(CustomBaseModel): Field(description='Construction types - compliance code specification'), ] = MISSING allowedWattage: Annotated[ - float | MISSING, Field(description='Allowed wattage', ge=0.0) + float | None | MISSING, Field(description='Allowed wattage', ge=0.0) ] = MISSING proposedWattage: Annotated[ - float | MISSING, Field(description='Proposed wattage', ge=0.0) + float | None | MISSING, Field(description='Proposed wattage', ge=0.0) ] = MISSING interiorLightingSpace: Annotated[ InteriorLightingSpace | None, @@ -4363,7 +4365,8 @@ class WholeBldgUse(CustomBaseModel): str | MISSING, Field(description='Text description of the area') ] = MISSING floorArea: Annotated[ - float | MISSING, Field(description='Whole building use floor area', ge=0.0) + float | None | MISSING, + Field(description='Whole building use floor area', ge=0.0), ] = MISSING ceilingHeight: Annotated[ float | None | MISSING, Field(description='Average ceiling height', ge=0.0) @@ -4380,10 +4383,10 @@ class WholeBldgUse(CustomBaseModel): Field(description='Construction types - compliance code specification'), ] = MISSING allowedWattage: Annotated[ - float | MISSING, Field(description='Allowed wattage', ge=0.0) + float | None | MISSING, Field(description='Allowed wattage', ge=0.0) ] = MISSING proposedWattage: Annotated[ - float | MISSING, Field(description='Proposed wattage', ge=0.0) + float | None | MISSING, Field(description='Proposed wattage', ge=0.0) ] = MISSING interiorLightingSpace: Annotated[ InteriorLightingSpace | None, @@ -4431,13 +4434,15 @@ class ExteriorUse(CustomBaseModel): ), ] = MISSING powerDensity: Annotated[ - float | MISSING, Field(description='Internal equipment power density', ge=0.0) + float | None | MISSING, + Field(description='Internal equipment power density', ge=0.0), ] = MISSING quantityUnits: Annotated[ str | None | MISSING, Field(description='Quantity units') ] = MISSING useQuantity: Annotated[ - float | MISSING, Field(description='The take-off quantity of the exterior use') + float | None | MISSING, + Field(description='The take-off quantity of the exterior use'), ] = MISSING exteriorLightingSpace: Annotated[ ExteriorLightingSpace | None, Field(description='Exterior lighting space') diff --git a/tools/generate_core_types.py b/tools/generate_core_types.py index e234ec6..d1c1130 100644 --- a/tools/generate_core_types.py +++ b/tools/generate_core_types.py @@ -14,6 +14,7 @@ # Ensure output directory exists OUTPUT_TYPES.parent.mkdir(parents=True, exist_ok=True) + def main(): """Main function to run the script.""" # Run the datamodel-codegen command @@ -40,6 +41,9 @@ def main(): "--use-default", # Use default values from the schema "--field-constraints", # Generate validation constraints (e.g., max_length, minItems) "--use-annotated", # Best practice for Pydantic V2 validations + "--formatters", + "black", + "isort", ], check=False, ) @@ -50,5 +54,6 @@ def main(): print(f"Generation failed with exit code {result.returncode}", file=sys.stderr) sys.exit(result.returncode) + if __name__ == "__main__": - main() \ No newline at end of file + main() From 41ffeb2f5f7e0b3df5baff52b7a42d2d0ba7f7b7 Mon Sep 17 00:00:00 2001 From: yanz571 Date: Fri, 7 Aug 2026 11:50:49 -0700 Subject: [PATCH 13/23] remove ACTIVITY_COMMON_OFFIC --- comcheck_api/schemas/comCheck.schema.json | 1 - comcheck_api/types/core_types.py | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/comcheck_api/schemas/comCheck.schema.json b/comcheck_api/schemas/comCheck.schema.json index 55f7ae1..5db06a4 100644 --- a/comcheck_api/schemas/comCheck.schema.json +++ b/comcheck_api/schemas/comCheck.schema.json @@ -7971,7 +7971,6 @@ "ACTIVITY_COMMON_MANUFACTURING", "ACTIVITY_COMMON_MOTEL", "ACTIVITY_COMMON_MOVIE", - "ACTIVITY_COMMON_OFFICE", "ACTIVITY_COMMON_OFFICE_ENCLOSED", "ACTIVITY_COMMON_OFFICE_OPEN", "ACTIVITY_COMMON_OTHER", diff --git a/comcheck_api/types/core_types.py b/comcheck_api/types/core_types.py index 717aed1..755bf43 100644 --- a/comcheck_api/types/core_types.py +++ b/comcheck_api/types/core_types.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: comCheck.schema.json -# timestamp: 2026-08-07T18:48:17+00:00 +# timestamp: 2026-08-07T18:50:23+00:00 from __future__ import annotations @@ -415,7 +415,6 @@ class ActivityTypeOptions(StrEnum): ACTIVITY_COMMON_MANUFACTURING = 'ACTIVITY_COMMON_MANUFACTURING' ACTIVITY_COMMON_MOTEL = 'ACTIVITY_COMMON_MOTEL' ACTIVITY_COMMON_MOVIE = 'ACTIVITY_COMMON_MOVIE' - ACTIVITY_COMMON_OFFICE = 'ACTIVITY_COMMON_OFFICE' ACTIVITY_COMMON_OFFICE_ENCLOSED = 'ACTIVITY_COMMON_OFFICE_ENCLOSED' ACTIVITY_COMMON_OFFICE_OPEN = 'ACTIVITY_COMMON_OFFICE_OPEN' ACTIVITY_COMMON_OTHER = 'ACTIVITY_COMMON_OTHER' From c3402aaf5f271e4a987129882674d98406cbac3f Mon Sep 17 00:00:00 2001 From: yanz571 Date: Mon, 10 Aug 2026 11:50:18 -0700 Subject: [PATCH 14/23] put exclude_unset back --- comcheck_api/client/comcheck_client.py | 12 ++++++------ comcheck_api/managers/data_manager.py | 2 +- comcheck_api/utilities/common.py | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/comcheck_api/client/comcheck_client.py b/comcheck_api/client/comcheck_client.py index 30eca6d..0e1c8a5 100644 --- a/comcheck_api/client/comcheck_client.py +++ b/comcheck_api/client/comcheck_client.py @@ -167,7 +167,7 @@ def update_project( if not old_project: raise COMCheckProjectNotFoundError(project_id) - project_data_json = project_data.model_dump(mode="json") + project_data_json = project_data.model_dump(mode="json", exclude_unset=True) # Preserve user project reference user_project = old_project["userProject"] @@ -262,7 +262,7 @@ def update_uvalues(self, project: ComBuilding) -> ComBuilding: The same ``project`` instance, with u-values updated. """ energy_code = str(project.control.code) - envelope_data = project.envelope.model_dump(mode="json") + envelope_data = project.envelope.model_dump(mode="json", exclude_unset=True) updated_assembly_uvalues = self._service.assemblies_uvalue( envelope_data, energy_code )["data"] @@ -296,7 +296,7 @@ def check_UA_compliance(self, project: ComBuilding) -> Any: Returns: The compliance results payload returned by the API. """ - project_data = project.model_dump(mode="json") + project_data = project.model_dump(mode="json", exclude_unset=True) response = self._service.check_UA_compliance(project_data) return response.get("data") @@ -309,7 +309,7 @@ def check_requirements(self, project: ComBuilding) -> Any: Returns: The requirements payload returned by the API. """ - project_data = project.model_dump(mode="json") + project_data = project.model_dump(mode="json", exclude_unset=True) response = self._service.check_requirements(project_data) return response.get("data") @@ -349,7 +349,7 @@ def generate_report( ``expires``, and ``fileName``. """ report_data = { - "building": project.model_dump(mode="json"), + "building": project.model_dump(mode="json", exclude_unset=True), "envelope": envelope, "extlighting": extlighting, "intlighting": intlighting, @@ -396,7 +396,7 @@ def start_run_simulation( logger.info("Updating project: %s", project_id) project = self.update_project(str(project_id), project) - project_data = project.model_dump(mode="json") + project_data = project.model_dump(mode="json", exclude_unset=True) run_result = self._service.start_run_simulation(project_data) if run_result.data is None: raise COMCheckSimulationError( diff --git a/comcheck_api/managers/data_manager.py b/comcheck_api/managers/data_manager.py index 8fa7c48..c46bb76 100644 --- a/comcheck_api/managers/data_manager.py +++ b/comcheck_api/managers/data_manager.py @@ -298,7 +298,7 @@ def modify_one(self, id_value: Any, updates: T | dict[str, Any]) -> T: # Convert updates to dict if it's a model object updates_dict: dict[str, Any] = ( - updates.model_dump(mode="json") + updates.model_dump(mode="json", exclude_unset=True) if isinstance(updates, BaseModel) else updates ) diff --git a/comcheck_api/utilities/common.py b/comcheck_api/utilities/common.py index 4b52675..4e3670f 100644 --- a/comcheck_api/utilities/common.py +++ b/comcheck_api/utilities/common.py @@ -18,7 +18,7 @@ def _json_default(obj: Any) -> Any: aliases, and nested models serialize the same way the API expects. """ if isinstance(obj, BaseModel): - return obj.model_dump(mode="json") + return obj.model_dump(mode="json", exclude_unset=True) raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable") From 1be16f8f87e1242ef3a330aa6a0ec07711383827 Mon Sep 17 00:00:00 2001 From: yanz571 Date: Mon, 10 Aug 2026 11:50:35 -0700 Subject: [PATCH 15/23] update docs/ --- docs/missing-vs-exclude-unset.md | 61 +++++++++++ docs/schema-compatibility.md | 168 +++++++++++++++++++++++++++++++ 2 files changed, 229 insertions(+) create mode 100644 docs/missing-vs-exclude-unset.md create mode 100644 docs/schema-compatibility.md diff --git a/docs/missing-vs-exclude-unset.md b/docs/missing-vs-exclude-unset.md new file mode 100644 index 0000000..36bf825 --- /dev/null +++ b/docs/missing-vs-exclude-unset.md @@ -0,0 +1,61 @@ +# `MISSING` vs `exclude_unset=True` + +Both mechanisms control which fields are included when serializing a Pydantic model to JSON, but they operate at different layers and serve different purposes. + +## `MISSING` sentinel + +Many fields in `core_types.py` use `MISSING` (from `pydantic.experimental.missing_sentinel`) as their default: + +```python +preAltPropUval: Annotated[float | None | MISSING, Field(ge=0.0)] = MISSING +``` + +This gives a field three distinct states: + +| Value | Meaning | +|---|---| +| `1.5` | Server sent a real value | +| `None` | Server explicitly sent `null` | +| `MISSING` | Server omitted the key entirely | + +On `model_dump(mode='json')`, Pydantic drops `MISSING` fields automatically — they are never included in the output and never sent back to the server. + +## `exclude_unset=True` + +`exclude_unset=True` is a Pydantic dump option that drops any field not explicitly assigned during construction. Pydantic tracks this via `__pydantic_fields_set__` — a set that records which fields the caller actually provided. It operates regardless of what default value a field holds. + +## Where they diverge + +**Fields with real defaults** — `MISSING` only protects fields that explicitly use it as their default. Fields with ordinary defaults (`0`, `""`, `False`, `None`) are still included unless `exclude_unset=True` is used: + +```python +class Foo(BaseModel): + name: str = "default" # real default + count: int | MISSING = MISSING # sentinel default + +f = Foo() # neither field set by the caller + +f.model_dump() # → {"name": "default"} (count dropped, name included) +f.model_dump(exclude_unset=True) # → {} (both dropped) +``` + +**Partial updates** — this is the critical case for `DataManager.update_item` and every outbound API call site. When building a model to describe only the fields you want to change, unset fields hold real defaults (`0`, `False`, etc.) — not `MISSING`. Only `exclude_unset=True` knows the caller never touched them: + +```python +# Only want to change the roof type — everything else should be left alone +update = Roof(roofType=RoofTypeOptions.METAL_ROOF_WITH_THERMAL_BLOCKS) + +update.model_dump(mode="json") # includes propUValue=0, grossArea=0, ... +update.model_dump(mode="json", exclude_unset=True) # → {"roofType": "METAL_ROOF_WITH_THERMAL_BLOCKS"} +``` + +## Summary + +| | `MISSING` default | `exclude_unset=True` | +|---|---|---| +| Mechanism | sentinel value on the field | field-set tracking on the instance | +| Drops server-omitted fields | yes | yes (if server did not provide them) | +| Drops fields with real defaults | no | yes | +| Needed for partial updates | no | yes | + +They complement each other. `MISSING` is for modeling fields the server may legitimately omit (keeping `None` and "absent" distinct). `exclude_unset=True` is for controlling the outbound payload based on what the caller explicitly set — which is why every API write call site in `comcheck_client.py`, `data_manager.py`, and `utilities/common.py` uses it. diff --git a/docs/schema-compatibility.md b/docs/schema-compatibility.md new file mode 100644 index 0000000..2ff0166 --- /dev/null +++ b/docs/schema-compatibility.md @@ -0,0 +1,168 @@ +# Schema Compatibility + +The `comcheck_api` library bridges between the Python Pydantic models in `core_types.py` (generated from `comCheck.schema.json`) and the live COMcheck backend API. Because the server may return data that predates or diverges from the current schema, `CustomBaseModel` contains several sanitization layers that run automatically on every parse and serialize cycle. + +This document describes each known compatibility issue, why it occurs, and how it is handled. + +--- + +## Background: `MISSING` Sentinel + +Many fields in the generated models use `MISSING` (from `pydantic.experimental.missing_sentinel`) as a default instead of `None`. A field with `= MISSING` means: + +- **On parse:** the server did not include this field — the model holds `MISSING` rather than failing validation. +- **On serialize:** `model_dump(mode='json')` omits the key entirely — the server does not receive it. + +This is the intentional "sparse update" pattern: only fields the server actually sent are round-tripped back. Issues arise when the server *requires* a field on write but omits it on read, or when `MISSING` leaks into the JSON payload. + +--- + +## Issue 1 — `deepcopy` fails on models with `MISSING` fields + +**Symptom:** `TypeError: Cannot pickle 'Sentinel' object` when calling `copy.deepcopy()` or `model.model_copy(deep=True)`. + +**Root cause:** `MISSING` is a `typing_extensions.Sentinel` that is not picklable. Pydantic's `__deepcopy__` internally uses pickle for nested objects. + +**Fix:** `CustomBaseModel.__deepcopy__` copies fields one-by-one, passing `MISSING` through unchanged rather than attempting to deep-copy it. + +--- + +## Issue 2 — Boolean flags serialized as integers + +**Symptom:** `HTTP 400: 'instance.isHistoricBuilding' is not of a type(s) boolean` + +**Root cause:** The generated schema models boolean flags as `IntEnum` with values `{0, 1}` (e.g. `IsHistoricBuilding`, `CirculationPump`, `HeatTraceTapeInstalled`, `CombinedSystem`, `PoolSystem`). `model_dump(mode='json')` serializes them as integers; the server's JSON Schema validator requires `true`/`false`. + +**Affected fields:** `isHistoricBuilding`, `allElectric`, `isRenewable`, `hasBattery`, `hasCharger`, `hasHeatPump` + +**Fix:** `CustomBaseModel.model_dump` runs both a `mode='python'` and `mode='json'` dump, then walks them in parallel. Wherever the Python value is an `IntEnum` instance, the corresponding JSON integer is replaced with `bool(value)`. + +--- + +## Issue 3 — Unknown enum values crash on parse + +**Symptom:** `ValidationError: Input should be 'ACTIVITY_INVALID_USE', 'ACTIVITY_AUTO_REPAIR', ... [type=enum]` for a value like `'ACTIVITY_COMMON_OFFICE'`. + +**Root cause:** Projects created under older schema versions may reference enum values that have since been renamed or removed (e.g. `ACTIVITY_COMMON_OFFICE` → `ACTIVITY_COMMON_OFFICE_OPEN`). The server stores these values verbatim and returns them unchanged. + +**Fix — two-validator approach:** + +Pydantic runs validators in order: `wrap` → `before` → field validation → `after`. This ordering is used to preserve the original value while still satisfying Pydantic's type checker: + +1. **`_preserve_invalid_enum_strings` (`mode='wrap'`)** runs first. It inspects the raw input dict and stashes any string values that are not valid members of their declared `StrEnum` type. +2. **`_sanitize_server_data` (`mode='before'`)** runs next (inside the `handler` call). It replaces the unknown string with the first valid enum member so Pydantic can construct the model without raising a `ValidationError`. +3. After `handler()` returns the constructed model object, `_preserve_invalid_enum_strings` writes the original (unknown) string back onto the field using `object.__setattr__`, bypassing field validation entirely. + +The result: the model field holds exactly what the server sent. `model_dump(mode='json')` serializes it as-is, so the value round-trips back to the server unchanged. A `WARNING` is still logged so the drift is visible. + +```python +# Server returns an old enum value +project = client.get_project("project-id") +area = project.lighting.wholeBldgUse[0].activityUse[0] + +print(area.activityType) # 'ACTIVITY_COMMON_OFFICE' ← original value preserved +print(type(area.activityType)) # ← not an enum member +``` + +> **Schema action needed:** When the backend migrates old records to use current enum values, this fallback will no longer be triggered. + +--- + +## Issue 4 — Pydantic serializer `UserWarning` spam + +**Symptom:** Dozens of `PydanticSerializationUnexpectedValue: Expected 'MISSING' sentinel` warnings on every `model_dump` call. + +**Root cause:** Pydantic's built-in serializer emits a warning for each `SomeEnum | MISSING` union variant it tries during serialization. + +**Fix:** `CustomBaseModel.model_dump` wraps both internal dump calls in `warnings.catch_warnings()` suppressing `UserWarning` from pydantic only. No behavior change. + +--- + +## Issue 5 — Server sends negative sentinels for unset numeric fields + +**Symptom:** `HTTP 400: 'instance.envelope.roof[1].continuousRValue' must be greater than or equal to 0` + +**Root cause:** The server uses `-1` (and similar negative values like `-4.545`) as a "not set" sentinel on fields like `continuousRValue`, `propUValue`, `cavityRValue`. The server enforces `>= 0` on write but does not enforce it on read. + +**Fix:** `CustomBaseModel._sanitize_server_data` replaces any incoming negative number with the field's declared `default` value, when that default is `>= 0`. A warning is logged. + +**Schema fix applied:** Added `"minimum": 0.0` to the following fields in `comCheck.schema.json` where it was missing: + +| Definition | Fields updated | +|---|---| +| `AboveGradeWall` (all 3 definitions) | `cavityRValue`, `continuousRValue`, `propUValue`, `grossArea` | +| `BelowGradeWall` | `cavityRValue`, `continuousRValue` | +| `Window` | `cavityRValue`, `continuousRValue` | +| `Door` | `cavityRValue`, `continuousRValue` | +| `Skylight` | `cavityRValue`, `continuousRValue` | +| `Roof` (second definition) | `cavityRValue`, `continuousRValue`, `propUValue` | + +--- + +## Issue 6 — `MISSING` fields included in outbound JSON payload + +**Symptom:** `HTTP 400: 'instance.lighting.wholeBldgUse[0].allowedWattage' is not of a type(s) number` + +**Root cause:** `MISSING` fields — those the server never sent — were being included in `model_dump(mode='json')` output as unexpected non-typed values (not `null`, not a number). The server's JSON Schema validator rejected them. + +**Fix:** The `model_dump` post-processing step (which already diffs Python vs JSON output for Issue 2) now also drops any key whose Python-side value is `MISSING`. The field is simply omitted from the outbound payload. + +--- + +## Issue 7 — `allowanceType` absent on GET, required on PUT + +**Symptom:** `ValidationError: Field required [type=missing]` on parse, then `HTTP 400: requires property "allowanceType"` on PUT. + +**Root cause:** The server omits `allowanceType` for older envelope records (sends `null` or omits the key entirely), but its write validator requires the field to be present with a valid string value. The field type is `EnvelopeAssemblyAllowanceTypeOptions | MISSING`. + +**Affected components:** `AgWall`, `BgWall`, `Window`, `Door`, `Skylight`, `Roof` + +**Fix — inbound (parse):** `_sanitize_server_data` maps an incoming `null` for a non-nullable enum field to the first non-null enum member. For `EnvelopeAssemblyAllowanceTypeOptions`, this is `ENV_ALLOWANCE_NONE`. + +**Fix — schema:** Removed `allowanceType` from the `required` arrays of all six component definitions in `comCheck.schema.json`. The field remains defined in `properties` — it is optional on read, required on write (enforced by the server). + +> **Note:** This is a server-side inconsistency. The long-term fix is for the server to always populate `allowanceType` when returning records, and for the schema to reflect that it is always present. Once the server is updated, the `required` entries can be restored. + +--- + +## Issue 8 — `null` values dropped for non-optional enum fields + +**Symptom:** Fields like `adjacentSpaceType`, `exemptionType` (typed as `SomeEnum`, not `SomeEnum | None`) arrive as `null` from the server, causing the Pydantic model to lose them. + +**Root cause:** The server sends `null` for fields it considers "not set" even when the generated schema does not allow `null`. The `_sanitize_server_data` validator was dropping these fields (reverting to `MISSING`), which then caused serialization failures or missing required fields on write. + +**Fix:** When the field has an enum with a `None`-valued member (`NoneType_None = None`), the incoming `null` is mapped to that enum member rather than dropped. When no such member exists, the field is dropped and a warning is logged. + +--- + +## Summary of `comCheck.schema.json` changes + +All changes align the schema with the server's actual write-time validation behavior: + +| Change | Location | Reason | +|---|---|---| +| Added `"minimum": 0.0` | `cavityRValue`, `continuousRValue` in all envelope definitions | Server rejects negative values on write | +| Added `"minimum": 0.0` | `propUValue` in Roof (alt definition), AgWall (alt definitions) | Server rejects negative values on write | +| Added `"minimum": 0.0` | `grossArea` in first AgWall definition | Consistent with all other `grossArea` definitions | +| Removed `allowanceType` from `required` | AgWall, BgWall, Window, Door, Skylight, Roof | Server omits this field on read for older records | + +--- + +## Logging + +All sanitization actions emit `WARNING`-level log messages via `comcheck_api.types.custom_base_model`. To see them: + +```python +import logging +logging.basicConfig(level=logging.WARNING) +``` + +Example output: +``` +WARNING comcheck_api.types.custom_base_model:custom_base_model.py:142 + Replacing unknown enum value 'ACTIVITY_COMMON_OFFICE' with fallback 'ACTIVITY_INVALID_USE' + for field ActivityUse.activityType + +WARNING comcheck_api.types.custom_base_model:custom_base_model.py:160 + Replacing server sentinel -1 with default 0.0 for field Roof.continuousRValue +``` From 1153123c258aca3f061355a9d24ce2ca0e17a9fa Mon Sep 17 00:00:00 2001 From: yanz571 Date: Mon, 10 Aug 2026 15:47:22 -0700 Subject: [PATCH 16/23] post merge schema correction --- .gitignore | 3 - .../constants/interior_lighting_constants.py | 3 +- comcheck_api/schemas/comCheck.schema.json | 59 +- comcheck_api/types/core_types.py | 118 +-- comcheck_api/types/custom_base_model.py | 33 + docs/schema-changes.md | 861 ++++++++++++++++++ 6 files changed, 930 insertions(+), 147 deletions(-) create mode 100644 docs/schema-changes.md diff --git a/.gitignore b/.gitignore index c8e0141..1f38fac 100644 --- a/.gitignore +++ b/.gitignore @@ -62,6 +62,3 @@ site/ # Build artifacts dist/ reports/ - -# Temporary building files -buildings/* \ No newline at end of file diff --git a/comcheck_api/constants/interior_lighting_constants.py b/comcheck_api/constants/interior_lighting_constants.py index c73641a..9553b50 100644 --- a/comcheck_api/constants/interior_lighting_constants.py +++ b/comcheck_api/constants/interior_lighting_constants.py @@ -11,7 +11,7 @@ DEFAULT_INTERIOR_LIGHTING_SPACE_AREA: ActivityUse = ActivityUse( key="__unset__", # key is a placeholder — callers must set it to the parent WholeBldgUse.key areaDescription="Space 1", # identifier for the ActivityUse within its parent WholeBldgUse - activityType=ActivityTypeOptions.ACTIVITY_COMMON_OFFICE, + activityType=ActivityTypeOptions.ACTIVITY_COMMON_OFFICE_OPEN, floorArea=1000.0, ceilingHeight=9.0, interiorLightingSpace=InteriorLightingSpace( @@ -33,6 +33,7 @@ DEFAULT_FIXTURE: Fixture = Fixture( description="LED fixture", + fixtureType=None, lightingType=LightingTypeOptions.LED, fixtureWattage=32.0, quantity=1, diff --git a/comcheck_api/schemas/comCheck.schema.json b/comcheck_api/schemas/comCheck.schema.json index 5db06a4..59146ed 100644 --- a/comcheck_api/schemas/comCheck.schema.json +++ b/comcheck_api/schemas/comCheck.schema.json @@ -57,11 +57,8 @@ }, "isHistoricBuilding": { "description": "Flag to indicate if the building is historic.", - "type": "integer", - "enum": [ - 0, - 1 - ] + "type": "boolean", + "default": false }, "performanceRating": { "description": "Appendix C compliance index", @@ -4249,18 +4246,16 @@ "heatRecovery": { "description": "Flag indicates whether the system has heat recovery feature", "type": [ - "integer", + "boolean", "null" - ], - "enum": [0, 1, null] + ] }, "heatPumpSimultaneousCoolingAndHeating": { "description": "Flag indicates whether the heat pump can do simultaneous cooling and heating", "type": [ - "integer", + "boolean", "null" - ], - "enum": [0, 1, null] + ] }, "heatRejection": { "description": "Heat rejection types", @@ -4318,18 +4313,16 @@ "twoPipeSystem": { "description": "Flag identifies if the plant system is a two pipe system", "type": [ - "integer", + "boolean", "null" - ], - "enum": [0, 1, null] + ] }, "waterloopHeatPump": { "description": "Flag identifies if the plant system is a water loop heat pump", "type": [ - "integer", + "boolean", "null" - ], - "enum": [0, 1, null] + ] }, "compliancePath": { "description": "Compliance path", @@ -4603,39 +4596,23 @@ }, "circulationPump": { "description": "Flag identifies whether the SWH has a circulation pump", - "type": "integer", - "enum": [ - 0, - 1 - ], - "default": 0 + "type": "boolean", + "default": false }, "heatTraceTapeInstalled": { "description": "Flag identifies whether the SWH has heat trace tape installed", - "type": "integer", - "enum": [ - 0, - 1 - ], - "default": 0 + "type": "boolean", + "default": false }, "combinedSystem": { "description": "Flag identifies whether the SWH is a combined system", - "type": "integer", - "enum": [ - 0, - 1 - ], - "default": 0 + "type": "boolean", + "default": false }, "poolSystem": { "description": "Flag identifies whether the SWH is part of pool system", - "type": "integer", - "enum": [ - 0, - 1 - ], - "default": 0 + "type": "boolean", + "default": false }, "heatPumpPoolHeater": { "description": "Flag identifies whether the SWH uses heat pump to heat the pool. - Only used when poolSystem is true. False as default", diff --git a/comcheck_api/types/core_types.py b/comcheck_api/types/core_types.py index 755bf43..1c07653 100644 --- a/comcheck_api/types/core_types.py +++ b/comcheck_api/types/core_types.py @@ -1,10 +1,10 @@ # generated by datamodel-codegen: # filename: comCheck.schema.json -# timestamp: 2026-08-07T18:50:23+00:00 +# timestamp: 2026-08-10T22:27:41+00:00 from __future__ import annotations -from enum import Enum, IntEnum, StrEnum +from enum import Enum, StrEnum from typing import Annotated, Any, Literal from comcheck_api.types.custom_base_model import CustomBaseModel @@ -12,15 +12,6 @@ from pydantic.experimental.missing_sentinel import MISSING -class IsHistoricBuilding(IntEnum): - """ - Flag to indicate if the building is historic. - """ - - integer_0 = 0 - integer_1 = 1 - - class EfficiencyPackageType(Enum): """ Efficiency Package Type @@ -716,46 +707,6 @@ class LightingTypeOptions(StrEnum): OTHER_LIGHTING_TYPE = 'OTHER_LIGHTING_TYPE' -class HeatRecovery(Enum): - """ - Flag indicates whether the system has heat recovery feature - """ - - int_0 = 0 - int_1 = 1 - NoneType_None = None - - -class HeatPumpSimultaneousCoolingAndHeating(Enum): - """ - Flag indicates whether the heat pump can do simultaneous cooling and heating - """ - - int_0 = 0 - int_1 = 1 - NoneType_None = None - - -class TwoPipeSystem(Enum): - """ - Flag identifies if the plant system is a two pipe system - """ - - int_0 = 0 - int_1 = 1 - NoneType_None = None - - -class WaterloopHeatPump(Enum): - """ - Flag identifies if the plant system is a water loop heat pump - """ - - int_0 = 0 - int_1 = 1 - NoneType_None = None - - class BoilerDraftTypeOptions(Enum): NATURAL_DRAFT = 'NATURAL_DRAFT' FORCED_DRAFT = 'FORCED_DRAFT' @@ -870,42 +821,6 @@ class PressureDropTypeOptions(StrEnum): ) -class CirculationPump(IntEnum): - """ - Flag identifies whether the SWH has a circulation pump - """ - - integer_0 = 0 - integer_1 = 1 - - -class HeatTraceTapeInstalled(IntEnum): - """ - Flag identifies whether the SWH has heat trace tape installed - """ - - integer_0 = 0 - integer_1 = 1 - - -class CombinedSystem(IntEnum): - """ - Flag identifies whether the SWH is a combined system - """ - - integer_0 = 0 - integer_1 = 1 - - -class PoolSystem(IntEnum): - """ - Flag identifies whether the SWH is part of pool system - """ - - integer_0 = 0 - integer_1 = 1 - - class HeatPumpPoolHeater(Enum): """ Flag identifies whether the SWH uses heat pump to heat the pool. - Only used when poolSystem is true. False as default @@ -3143,13 +3058,13 @@ class HVACPlant(CustomBaseModel): Field(description='Heat pump chiller type'), ] = MISSING heatRecovery: Annotated[ - HeatRecovery | None | MISSING, + bool | None | MISSING, Field( description='Flag indicates whether the system has heat recovery feature' ), ] = MISSING heatPumpSimultaneousCoolingAndHeating: Annotated[ - HeatPumpSimultaneousCoolingAndHeating | None | MISSING, + bool | None | MISSING, Field( description='Flag indicates whether the heat pump can do simultaneous cooling and heating' ), @@ -3186,11 +3101,11 @@ class HVACPlant(CustomBaseModel): str | None | MISSING, Field(description='Deprecated, system type') ] = MISSING twoPipeSystem: Annotated[ - TwoPipeSystem | None | MISSING, + bool | None | MISSING, Field(description='Flag identifies if the plant system is a two pipe system'), ] = MISSING waterloopHeatPump: Annotated[ - WaterloopHeatPump | None | MISSING, + bool | None | MISSING, Field( description='Flag identifies if the plant system is a water loop heat pump' ), @@ -3320,23 +3235,23 @@ class ServiceWaterHeatingSystem(CustomBaseModel): ), ] = MISSING circulationPump: Annotated[ - CirculationPump | None, + bool | None, Field(description='Flag identifies whether the SWH has a circulation pump'), - ] = 0 + ] = False heatTraceTapeInstalled: Annotated[ - HeatTraceTapeInstalled | None, + bool | None, Field( description='Flag identifies whether the SWH has heat trace tape installed' ), - ] = 0 + ] = False combinedSystem: Annotated[ - CombinedSystem | None, + bool | None, Field(description='Flag identifies whether the SWH is a combined system'), - ] = 0 + ] = False poolSystem: Annotated[ - PoolSystem | None, + bool | None, Field(description='Flag identifies whether the SWH is part of pool system'), - ] = 0 + ] = False heatPumpPoolHeater: Annotated[ HeatPumpPoolHeater | None, Field( @@ -4513,9 +4428,8 @@ class ComBuilding(CustomBaseModel): ), ] = False isHistoricBuilding: Annotated[ - IsHistoricBuilding | MISSING, - Field(description='Flag to indicate if the building is historic.'), - ] = MISSING + bool | None, Field(description='Flag to indicate if the building is historic.') + ] = False performanceRating: Annotated[ float | None, Field(description='Appendix C compliance index') ] = None diff --git a/comcheck_api/types/custom_base_model.py b/comcheck_api/types/custom_base_model.py index 8363d04..c75e461 100644 --- a/comcheck_api/types/custom_base_model.py +++ b/comcheck_api/types/custom_base_model.py @@ -1,11 +1,17 @@ import logging import re +from copy import deepcopy from typing import Any, Optional, TypeVar from pydantic.main import _model_construction from pydantic import BaseModel from comcheck_api.managers.data_manager import DataManager +try: + from pydantic.experimental.missing_sentinel import MISSING as _PYDANTIC_MISSING +except ImportError: + _PYDANTIC_MISSING = None + logger = logging.getLogger(__name__) T = TypeVar("T") @@ -24,6 +30,33 @@ class CustomBaseModel(BaseModel): _identifier: str = "id" + def __deepcopy__(self, memo=None): + # MISSING (Sentinel) is not picklable, so copy field-by-field, passing it through as-is. + if memo is None: + memo = {} + cls = self.__class__ + new_obj = cls.__new__(cls) + memo[id(self)] = new_obj + new_dict = {} + for k, v in self.__dict__.items(): + if _PYDANTIC_MISSING is not None and v is _PYDANTIC_MISSING: + new_dict[k] = v + else: + new_dict[k] = deepcopy(v, memo) + object.__setattr__(new_obj, "__dict__", new_dict) + object.__setattr__( + new_obj, + "__pydantic_fields_set__", + deepcopy(self.__pydantic_fields_set__, memo), + ) + for attr in ("__pydantic_extra__", "__pydantic_private__"): + try: + val = object.__getattribute__(self, attr) + object.__setattr__(new_obj, attr, deepcopy(val, memo)) + except AttributeError: + pass + return new_obj + @classmethod def __pydantic_init_subclass__(cls, **kwargs): """Automatically generate `add_` methods for BaseModel-typed fields on subclasses.""" diff --git a/docs/schema-changes.md b/docs/schema-changes.md new file mode 100644 index 0000000..5174da3 --- /dev/null +++ b/docs/schema-changes.md @@ -0,0 +1,861 @@ +# Schema Changelog — comCheck.schema.json + +This document describes every meaningful change introduced in the schema update merged via PR #25. +Changes are grouped by category. Within each category entries are listed by definition and field name. + +--- + +## 1. Field Additions + +### `ComBuilding` + +| Field | Type | Notes | +|---|---|---| +| `bldgUseType` | `$ref BuildingUseTypeOptions` | Legacy alias for `buildingUseType`. Comment: "Legacy enum, only ACTIVITY is valid in the new ComCheck Web." | +| `efficiencyPackageType` | `enum` (string or `null`) | New field. Values: `EFF_PACKAGE_UNKNOWN`, `EFF_PACKAGE_HVAC_PERFORMANCE`, `EFF_PACKAGE_LIGHTING_REDUCED_LPD`, `EFF_PACKAGE_REDUCED_AIR_INFILTRATION`, `EFF_PACKAGE_ENHANCED_ENVELOPE_PERFORMANCE`, `EFF_PACKAGE_ENHANCED_LIGHTING_CONTROLS`, `EFF_PACKAGE_ONSITE_RENEWABLES`, `null`. Default: `null`. | +| `energyCreditMultiplierException` | `enum` (or `null`) | New field. Values: `NO_ENERGY_CREDIT_MULTIPLIER_EXCEPTION`, `ENERGY_CREDIT_MULTIPLIER_EXCEPTION_LOW_ENERGY_BUILDINGS`, `ENERGY_CREDIT_MULTIPLIER_EXCEPTION_PRIMARY_HEAT_PUMP`, `null`. No default declared. | + +### `Door` (fenestration) + +| Field | Type | Notes | +|---|---|---| +| `feetAg` | `["number", "null"]` | New field: feet above grade. `minimum: 0.0`, `default: null`. | + +### `Skylight` + +| Field | Type | Notes | +|---|---|---| +| `cavityRValue` | `["number", "null"]` | New field: average insulation R-value in cavity. Unit: `h-ft2-F/Btu`, `default: 0.0`. | +| `continuousRValue` | `["number", "null"]` | New field: continuous insulation on the skylight. Unit: `h-ft2-F/Btu`, `default: 0.0`. | + +### `HVACSystem` + +| Field | Type | Notes | +|---|---|---| +| `requirementAnswer` | `array` of `$ref Requirements` | New field. `default: []`. | + +### `HVACPlant` + +| Field | Type | Notes | +|---|---|---| +| `requirementAnswer` | `array` of `$ref Requirements` | New field. `default: []`. | + +### `InteriorLightingFixture` + +| Field | Type | Notes | +|---|---|---| +| `scheduleFixtureKey` | `["string", "null"]` | New field: UUID to identify this fixture schedule. | +| `typeOfFixture` | `["string", "null"]` | New field: type of the fixture. | + +--- + +## 2. Removed Fields / Enum Values Removed + +### `ActivityTypeOptions` + +| Removed value | Notes | +|---|---| +| `ACTIVITY_COMMON_OFFICE` | Removed from enum. `ACTIVITY_COMMON_OFFICE_ENCLOSED` and `ACTIVITY_COMMON_OFFICE_OPEN` remain. | + +--- + +## 3. Type Changes + +### `ComBuilding` + +| Field | Old type | New type | Notes | +|---|---|---|---| +| `isHistoricBuilding` | `"boolean"` | `"integer"`, `enum: [0, 1]` | Changed from boolean to integer flag. | +| `isNonresidentialConditioning` | `"boolean"` | `["boolean", "null"]` | Made nullable. | +| `isResidentialConditioning` | `"boolean"` | `["boolean", "null"]` | Made nullable. | +| `isSemiheatedConditioning` | `"boolean"` | `["boolean", "null"]` | Made nullable. | +| `constructionType` | `"string"` | `["string", "null"]` | Made nullable. | + +### `CodeData` + +| Field | Old type | New type | Notes | +|---|---|---|---| +| `version` | `"string"` | `["string", "null"]` | Made nullable. | + +### `AgWall` + +| Field | Old type | New type | Notes | +|---|---|---|---| +| `bldgUseKey` | `"string"` | `["string", "null"]` | Made nullable. | +| `heatCapacity` | `"number"` | `["number", "null"]` | Made nullable. | +| `cavityRValue` | `"number"` | `["number", "null"]` | Made nullable. | +| `continuousRValue` | `"number"` | `["number", "null"]` | Made nullable. | +| `propUValue` | `"number"` | `["number", "null"]` | Made nullable. | +| `grossArea` | `"number"` | `["number", "null"]` | Made nullable. | + +### `BgWall` + +| Field | Old type | New type | Notes | +|---|---|---|---| +| `bldgUseKey` | `"string"` | `["string", "null"]` | Made nullable. | +| `heatCapacity` | `"number"` | `["number", "null"]` | Made nullable. | +| `cavityRValue` | `"number"` | `["number", "null"]` | Made nullable. | +| `continuousRValue` | `"number"` | `["number", "null"]` | Made nullable. | +| `propUValue` | `"number"` | `["number", "null"]` | Made nullable. | + +### `Window` + +| Field | Old type | New type | Notes | +|---|---|---|---| +| `bldgUseKey` | `"string"` | `["string", "null"]` | Made nullable. | +| `propUValue` | `"number"` | `["number", "null"]` | Made nullable. | +| `grossArea` | `"number"` | `["number", "null"]` | Made nullable. | +| `propShgc` | `"number"` | `["number", "null"]` | Made nullable. | +| `preAltPropShgc` | `"number"` | `["number", "null"]` | Made nullable. | +| `cavityRValue` | `"number"` | `["number", "null"]` | Made nullable. | +| `continuousRValue` | `"number"` | `["number", "null"]` | Made nullable. | + +### `Door` + +| Field | Old type | New type | Notes | +|---|---|---|---| +| `bldgUseKey` | `"string"` | `["string", "null"]` | Made nullable. | +| `propUValue` | `"number"` | `["number", "null"]` | Made nullable. | +| `propShgc` | `"number"` | `["number", "null"]` | Made nullable. | +| `preAltPropShgc` | `"number"` | `["number", "null"]` | Made nullable. | +| `cavityRValue` | `"number"` | `["number", "null"]` | Made nullable. | +| `continuousRValue` | `"number"` | `["number", "null"]` | Made nullable. | + +### `Skylight` + +| Field | Old type | New type | Notes | +|---|---|---|---| +| `bldgUseKey` | `"string"` | `["string", "null"]` | Made nullable. | +| `propUValue` | `"number"` | `["number", "null"]` | Made nullable. | +| `grossArea` | `"number"` | `["number", "null"]` | Made nullable. | +| `propShgc` | `"number"` | `["number", "null"]` | Made nullable. | +| `preAltPropShgc` | `"number"` | `["number", "null"]` | Made nullable. | + +### `Roof` + +| Field | Old type | New type | Notes | +|---|---|---|---| +| `bldgUseKey` | `"string"` | `["string", "null"]` | Made nullable. | +| `cavityRValue` | `"number"` | `["number", "null"]` | Made nullable. | +| `continuousRValue` | `"number"` | `["number", "null"]` | Made nullable. | +| `propUValue` | `"number"` | `["number", "null"]` | Made nullable. | +| `grossArea` | `"number"` | `["number", "null"]` | Made nullable. | +| `purlinSpacing` | `"number"` | `["number", "null"]` | Made nullable. | + +### `Floor` + +| Field | Old type | New type | Notes | +|---|---|---|---| +| `bldgUseKey` | `"string"` | `["string", "null"]` | Made nullable. | +| `cavityRValue` | `"number"` | `["number", "null"]` | Made nullable. | +| `continuousRValue` | `"number"` | `["number", "null"]` | Made nullable. | +| `propUValue` | `"number"` | `["number", "null"]` | Made nullable. | +| `grossArea` | `"number"` | `["number", "null"]` | Made nullable. | +| `slabFullInsulBelowMinRValue` | `"number"` | `["number", "null"]` | Made nullable. | + +### `WholeBldgUse` + +| Field | Old type | New type | Notes | +|---|---|---|---| +| `floorArea` | `"number"` | `["number", "null"]` | Made nullable. | +| `powerDensity` | `"number"` | `["number", "null"]` | Made nullable. | +| `internalLoad` | `"number"` | `["number", "null"]` | Made nullable. | +| `allowedWattage` | `"number"` | `["number", "null"]` | Made nullable. | +| `proposedWattage` | `"number"` | `["number", "null"]` | Made nullable. | + +### `ActivityUse` + +| Field | Old type | New type | Notes | +|---|---|---|---| +| `floorArea` | `"number"` | `["number", "null"]` | Made nullable. | +| `ceilingHeight` | `"number"` | `["number", "null"]` | Made nullable. | +| `powerDensity` | `"number"` | `["number", "null"]` | Made nullable. | +| `internalLoad` | `"number"` | `["number", "null"]` | Made nullable. | +| `allowedWattage` | `"number"` | `["number", "null"]` | Made nullable. | +| `proposedWattage` | `"number"` | `["number", "null"]` | Made nullable. | + +### `ExteriorUse` + +| Field | Old type | New type | Notes | +|---|---|---|---| +| `powerDensity` | `"number"` | `["number", "null"]` | Made nullable. | +| `useQuantity` | `"number"` | `["number", "null"]` | Made nullable. | + +### `InteriorLightingSpace` + +| Field | Old type | New type | Notes | +|---|---|---|---| +| `numFixturesAlteredOrAdded` | `["integer", "null"]` with `minimum: 0` | `["integer", "null"]` | `minimum` constraint removed. | +| `primaryDaylight` | `"number"` | `["number", "null"]` | Made nullable. | +| `secondaryDaylight` | `"number"` | `["number", "null"]` | Made nullable. | +| `skylightToplight` | `"number"` | `["number", "null"]` | Made nullable. | +| `roofMonitorToplight` | `"number"` | `["number", "null"]` | Made nullable. | +| `decorativeArea` | `"number"` | `["number", "null"]` | Made nullable. | + +### `InteriorLightingFixture` + +| Field | Old type | New type | Notes | +|---|---|---|---| +| `fixtureType` | `"string"` | `["string", "null"]` | Made nullable. | +| `fixtureWattage` | `"number"` | `["number", "null"]` | Made nullable. | +| `quantity` | `"integer"` | `["integer", "null"]` | Made nullable. | +| `quantityWithAdvControls` | `"integer"` | `["integer", "null"]` | Made nullable. | + +### `FixtureSchedule` + +| Field | Old type | New type | Notes | +|---|---|---|---| +| `id` | `"integer"` | `["string", "integer"]` | Now also accepts string. | +| `lightingId` | `"integer"` | `["string", "integer"]` | Now also accepts string. | + +### `HVAC` + +| Field | Old type | New type | Notes | +|---|---|---|---| +| `fanSystem` | `"array"` | `["array", "null"]` | Made nullable. Added `default: null`. Description capitalised from "fan system" to "Fan system". | + +### `HVACSystem` + +| Field | Old type | New type | Notes | +|---|---|---|---| +| `quantity` minimum | `1` | `0` | Minimum quantity lowered from 1 to 0. | + +### `HVACPlant` + +| Field | Old type | New type | Notes | +|---|---|---|---| +| `condenserFlowRate` | `"number"` | `["number", "null"]` | Made nullable. | +| `condenserLeavingTemperature` | `"number"` | `["number", "null"]` | Made nullable. | +| `coolingPlantCapacity` | `"number"` | `["number", "null"]` | Made nullable. | +| `enteringCondenserWaterTemperature` | `"number"` | `["number", "null"]` | Made nullable. | +| `evaporatorLeavingTemperature` | `"number"` | `["number", "null"]` | Made nullable. | +| `heatingPlantCapacity` | `"number"` | `["number", "null"]` | Made nullable. | +| `heatRecovery` | `"boolean"` | `["integer", "null"]`, `enum: [0, 1, null]` | Changed from boolean to integer flag with null support. | +| `heatPumpSimultaneousCoolingAndHeating` | `"boolean"` | `["integer", "null"]`, `enum: [0, 1, null]` | Changed from boolean to integer flag with null support. | +| `leavingChilledWaterTemperature` | `"number"` | `["number", "null"]` | Made nullable. | +| `propCoolingPlantEfficiencyPartial` | `"number"` | `["number", "null"]` | Made nullable. | +| `propCoolingPlantEfficiency` | `"number"` | `["number", "null"]` | Made nullable. | +| `propHeatingPlantEfficiency` | `"number"` | `["number", "null"]` | Made nullable. | +| `quantity` minimum | `1.0` | `0` | Minimum quantity lowered from 1 to 0. | +| `systemType` | `"string"` | `["string", "null"]` | Made nullable. | +| `twoPipeSystem` | `"boolean"` | `["integer", "null"]`, `enum: [0, 1, null]` | Changed from boolean to integer flag with null support. | +| `waterloopHeatPump` | `"boolean"` | `["integer", "null"]`, `enum: [0, 1, null]` | Changed from boolean to integer flag with null support. | + +### `FanSystem` + +| Field | Old type | New type | Notes | +|---|---|---|---| +| `description2` | `"string"` | `["string", "null"]` | Made nullable. | +| `fanSystemKey` | `"string"` | `["string", "null"]` | Made nullable. | +| `hasPressureDropCredits` | `["boolean", "integer"]` | `enum: [0, 1, null]` | Changed to nullable enum. | + +### `Fan` + +| Field | Old type | New type | Notes | +|---|---|---|---| +| `fanDesignEfficiency` | `"number"` | `["number", "null"]` | Made nullable. | +| `maxNameplateHp` | `"number"` with `minimum: 0.0` | `["number", "null"]` | Made nullable; `minimum` constraint removed. | +| `nameplateHp` | `"number"` with `minimum: 0.0` | `"number"` | `minimum` constraint removed (type unchanged). | +| `totalFanEfficiency` | `"number"` | `["number", "null"]` | Made nullable. | + +### `PressureDrop` + +| Field | Old type | New type | Notes | +|---|---|---|---| +| `recoveryEffectiveness` | `"number"`, `minimum: 0.0`, `maximum: 1.0` | `["number", "null"]`, `minimum: 0.0` | Made nullable; `maximum: 1.0` constraint removed. | +| `verticalDuctLength` | `"number"` | `["number", "null"]` | Made nullable. | + +### `ServiceWaterHeatingSystem` + +| Field | Old type | New type | Notes | +|---|---|---|---| +| `circulationPump` | `"boolean"` | `"integer"`, `enum: [0, 1]`, `default: 0` | Changed from boolean to integer flag. | +| `heatTraceTapeInstalled` | `"boolean"` | `"integer"`, `enum: [0, 1]`, `default: 0` | Changed from boolean to integer flag. | +| `combinedSystem` | `"boolean"` | `"integer"`, `enum: [0, 1]`, `default: 0` | Changed from boolean to integer flag. | +| `poolSystem` | `"boolean"` | `"integer"`, `enum: [0, 1]`, `default: 0` | Changed from boolean to integer flag. | +| `heatPumpPoolHeater` (renamed from `heatpumpPoolHeater`) | `"boolean"` | `["boolean", "null"]`, `enum: [0, 1, null]`, `default: null` | Renamed (camelCase fix) and made nullable. | +| `quantity` minimum | `1` | `0` | Minimum quantity lowered from 1 to 0. | +| `requirementAnswer` | `"array"` (no items defined) | `"array"` with `items: $ref Requirements`, `default: []` | Items type now specified. | + +### `EnergyCreditPackage` + +| Field | Old type | New type | Notes | +|---|---|---|---| +| `bldgUseKey` | `"string"` | `["string", "null"]` | Made nullable. | + +### `Renewable` + +| Field | Old type | New type | Notes | +|---|---|---|---| +| `numberOfFloors` minimum | `1` | `0` | Minimum floors lowered from 1 to 0. | +| `largestThreeFloorArea` | `"number"` | `["number", "null"]` | Made nullable. | +| `requiredCapacity` | `"number"` | `["number", "null"]` | Made nullable. | +| `proposedCapacity` | `"number"` | `["number", "null"]` | Made nullable. | +| `roofAreaForRenewable` | `"number"` | `["number", "null"]` | Made nullable. | + +--- + +## 4. Constraint Changes + +### `ComBuilding` + +| Field | Change | +|---|---| +| `performanceRating` | `minimum: 0.0` removed. | +| `energyCreditPerformanceRating` | `minimum: 0.0` removed. | + +### `AgWall` + +| Field | Change | +|---|---| +| `cavityRValue` | `minimum: 0.0` removed. | +| `continuousRValue` | `minimum: 0.0` removed. | +| `continuousDeratedRValue` | `default: 0.0` removed (now has no default). | +| `propUValue` | `minimum: 0.0` removed. | +| `grossArea` | `minimum: 0.0` removed. | + +### `BgWall` + +| Field | Change | +|---|---| +| `cavityRValue` | `minimum: 0.0` removed. | +| `continuousRValue` | `minimum: 0.0` removed. | +| `propUValue` | `minimum: 0.0` removed. | + +### `Window` + +| Field | Change | +|---|---| +| `propUValue` | `minimum: 0.0` removed. | +| `propShgc` | No constraint change (minimum still 0.0). | +| `preAltPropUval` | `default: 0.0` removed. | +| `cavityRValue` | `minimum: 0.0` removed. | +| `continuousRValue` | `minimum: 0.0` removed. | + +### `Door` + +| Field | Change | +|---|---| +| `propUValue` | `minimum: 0.0` removed. | +| `preAltPropUval` | `default: 0.0` removed. | +| `cavityRValue` | `minimum: 0.0` removed. | +| `continuousRValue` | `minimum: 0.0` removed. | + +### `Skylight` + +| Field | Change | +|---|---| +| `propUValue` | `minimum: 0.0` removed. | +| `preAltPropUval` | `default: 0.0` removed. | + +### `Roof` + +| Field | Change | +|---|---| +| `highAlbedoRoofReqType` | Typo `"defualt"` corrected to `"default"`. | +| `cavityRValue` | `minimum: 0.0` removed. | +| `continuousRValue` | `minimum: 0.0` removed. | +| `propUValue` | `minimum: 0.0` removed. | + +### `Floor` + +| Field | Change | +|---|---| +| `cavityRValue` (Floor def) | `minimum: 0.0` present in context line only; type changed to nullable. | +| `propUValue` (Floor def) | `minimum: 0.0` removed. | + +### `InteriorLightingSpace` + +| Field | Change | +|---|---| +| `numFixturesAlteredOrAdded` | `minimum: 0` removed. | + +### `HVACSystem` + +| Field | Change | +|---|---| +| `quantity` | `minimum` changed from `1` to `0`. | + +### `HVACPlant` + +| Field | Change | +|---|---| +| `quantity` | `minimum` changed from `1.0` to `0`. | +| `efficiencyRequirementException` | `default: "EFF_EXCEPTION_UNSPECIFIED"` removed. | + +### `ServiceWaterHeatingSystem` + +| Field | Change | +|---|---| +| `quantity` | `minimum` changed from `1` to `0`. | +| `swhSystemSubType` | `default: "UNKNOWN_SWH_SYSTEM_SUB_TYPE"` removed. | +| `efficiencyRequirementException` | `default: "EFF_EXCEPTION_UNSPECIFIED"` removed. | + +### `Envelope` (`useOrientationDetails`) + +| Field | Change | +|---|---| +| `useOrientationDetails` | `default: true` replaced with `const: true`. This field must now always equal `true` (no other value is valid). | + +--- + +## 5. Structural / `$ref` Changes + +### `anyOf` → direct `$ref` (null support removed from schema, now provided by enum) + +Several fields that previously used `anyOf: [{type: null}, {$ref: ...}]` have been changed to a bare `$ref`. This means the field **no longer explicitly allows `null` in the JSON Schema sense** — nullability is now expected to come from the enum definition itself (which has had `null` added as an enum value). + +Affected fields (definition → field): + +| Definition | Field | +|---|---| +| `AgWall` | `adjacentSpaceBuildingType` | +| `AgWall` | `allowanceType` | +| `BgWall` | `adjacentSpaceBuildingType` | +| `BgWall` | `allowanceType` | +| `Roof` | `adjacentSpaceBuildingType` | +| `Roof` | `allowanceType` | +| `Window` | `adjacentSpaceBuildingType` | +| `Window` | `allowanceType` | +| `Window` | `frameType` | +| `Door` | `adjacentSpaceBuildingType` | +| `Door` | `allowanceType` | +| `Door` | `frameType` | +| `Skylight` | `adjacentSpaceBuildingType` | +| `Skylight` | `allowanceType` | +| `Skylight` | `frameType` | +| `Floor` | `allowanceType` | +| `InteriorLightingFixture` | `allowanceType` | +| `InteriorLightingFixture` | `ballast` | +| `InteriorLightingFixture` | `trackLightingWattageBasisType` | +| `FixtureSchedule` | `trackLightingWattageBasisType` | + +### `AgWall.otherWallType` + +Changed from bare `$ref AgWallOtherTypeOptions` to `anyOf: [{type: null}, {$ref: ...}]`. Null is now explicitly permitted. + +### `WholeBldgUse.interiorLightingSpace` + +Changed from bare `$ref InteriorLightingSpace` to `anyOf: [{$ref: ...}, {type: null}]`. Null is now explicitly permitted. + +### `ActivityUse.interiorLightingSpace` + +Changed from bare `$ref InteriorLightingSpace` to `anyOf: [{$ref: ...}, {type: null}]`. Null is now explicitly permitted. + +### `ExteriorUse.exteriorLightingSpace` + +Changed from bare `$ref ExteriorLightingSpace` to `anyOf: [{$ref: ...}, {type: null}]`. Null is now explicitly permitted. + +### `InteriorLightingFixture.advControlAllowanceType` → renamed to `advControlsAllowanceType` + +Field renamed from `advControlAllowanceType` to `advControlsAllowanceType`. Also changed from bare `$ref` with no default to `$ref` with `default: null`. + +--- + +## 6. `required` Array Changes + +### `InteriorLightingFixture` + +| Change | Notes | +|---|---| +| `lightingType` removed from required | `lightingType` is no longer required. | +| `fixtureType` added to required | `fixtureType` is now required (replacing `lightingType`). | + +### `FixtureSchedule` + +| Change | Notes | +|---|---| +| `lightingType` removed from required | `lightingType` is no longer required. | + +*All other `required` array changes in the diff are purely formatting (inline → multi-line) with no semantic difference.* + +--- + +## 7. New Enum Values + +### `EnergyCodeOptions` (national codes) + +Added values: +- `CEZ_IECC2009` +- `CEZ_IECC2012` +- `CEZ_IECC2024_APPXCF` ("IECC 2024 Appendix CF") +- `CEZ_90_1_2007` +- `CEZ_90_1_2010` +- `NONE` ("Unspecified") + +### `StateRegionEnergyCodeOptions` + +Added values: +- `CEZ_NYS2024_IECC2024` ("2024 New York State Energy Conservation Code - IECC 2024") +- `CEZ_NYS2025_9012022` ("2025 New York State Energy Conservation Code - 90.1 (2022)") +- `CEZ_NYC2025_IECC2024` ("2025 New York City Energy Conservation Code - IECC 2024") +- `CEZ_NYC2025_9012022` ("2025 New York City Energy Conservation Code - 90.1 (2022)") +- `CEZ_VT2024_IECC2021` ("2024 Vermont Commercial Building Energy Standards") +- `CEZ_LA2021_IECC2021` ("2021 LA Energy Code - 2021 IECC") +- `NONE` ("Unspecified") + +### `ProjectTypeOptions` + +Added values: +- `NONE` ("Unspecified") +- `null` ("Missing") +- The definition also dropped the explicit `"type": "string"` constraint. + +### `AirBarrierComplianceTypeOptions` + +Added: `AIR_BARRIER_OPTION_CONTINUITY_PLAN` ("Continuity Plan") + +### `AgWallTypeOptions` + +Added: +- `OTHER_BG_WALL` ("Other Above Grade Wall Type" — note: description says "Above Grade" but value name says "BG", may be intentional) +- `OTHER_FRAME` ("Other Framing Type") +- `null` ("Unspecified") +- The existing `METAL_BLDG_AG_WALL` description changed from "Metal Building Wall" to "Metal Building Wall Without Thermal Break" + +### `BgWallTypeOptions` + +Added: `null` ("Unspecified"). Also dropped the explicit `"type": "string"` constraint. + +### `RoofTypeOptions` + +Added: `METAL_ROOF_W_THERMAL_BREAK` ("Metal Roof with Thermal Break") + +### `HighAlbedoRoofReqTypeOptions` + +Added: `HA_ROOF_REQ_SOLAR_REFLECTANCE` ("Minimum Solar Reflectance") + +### `FloorTypeOptions` + +Added: `null` ("Unspecified"). Also dropped the explicit `"type": "string"` constraint. + +### `SlabInsulationPositionOptions` + +Added: `NONE` ("None") as an additional alias. + +### `AgWallConstructionDetailsTypeOptions` + +Added: +- `AG_WALL_CONSTRUCTION_DETAILS_UNKNOWN` ("Unknown") +- `AG_WALL_CONSTRUCTION_DETAILS_HORIZONTAL_Z_GIRTS` ("Horizontal Z-Girts") +- `AG_WALL_CONSTRUCTION_DETAILS_VERTICAL_Z_GIRTS` ("Vertical Z-Girts") +- `AG_WALL_CONSTRUCTION_DETAILS_Z_GIRTS_THERMAL_BROKEN` ("Z-Girts with Thermal Break") + +### `EnvelopeAssemblyAllowanceTypeOptions` + +Added: +- `NONE` ("None") +- `null` ("Unspecified") +- Dropped explicit `"type": "string"` constraint. + +### `CMUTypeOptions` + +Added: +- `NONE` ("None") +- `null` ("Unspecified") +- Dropped explicit `"type": "string"` constraint. + +### `ConcreteDensityOptions` + +Added values: `85` ("Light Weight"), `135` (no description added), `null`. +Dropped explicit `"type": "integer"` constraint. + +### `ConcreteThicknessOptions` + +Added values: `3`, `4`, `5`, `7`, `9`, `11`, `null`. +Dropped explicit `"type": "integer"` constraint. + +### `EnvelopeAssemblyExemptionOptions` + +Added: `NONE` ("None") + +### `FurringTypeOptions` + +Added: `NONE` ("None") at the beginning of the enum. + +### `OrientationOptions` + +Added: `null` ("Null"). Dropped explicit `"type": "string"` constraint. + +### `AltExemptTypeOptions` + +Added: +- `EXEMPT_HISTORIC_CHARACTERISTIC` ("Alteration to the area is not applicable to historic characteristics.") +- `EXEMPT_LIGHTING_SPACE_REPLACEMENT_LT_20_PCT_LOAD` ("Less than 20% fixture replacement.") + +### `FenestrationFrameTypeOptions` + +Added values: +- `NON_METAL` ("Non-metal frame") +- `NONE` ("None") +- `METAL_FRAME_24_AG_WALL` ("24-gauge metal-framed wall") +- `GLASS_DOOR` ("Glass door") +- `METAL_THERMAL_BREAK` ("Metal frame with thermal break") +- `OTHER_DOOR` ("Other door") +- `INSUL_METAL_DOOR` ("Insulated metal door") +- `NO_INSUL_SINGLE_METAL_DOOR` ("Non-insulated single metal door") +- `WOOD_FRAME_16_AG_WALL` ("16-gauge wood-framed wall") +- `ALL_WOOD_JOIST_TRUSS_FLOOR` ("All-wood joist/truss floor") +- `METAL_FRAME_16_AG_WALL` ("16-gauge metal-framed wall") +- `WOOD_DOOR` ("Wood door") +- `null` ("Unspecified") + +Also dropped explicit `"type": "string"` constraint. + +### `GlazingTypeOptions` + +Added: +- `OTHER_GLAZING` +- `NONE` ("None") + +### `SolarTypeOptions` + +Added: `NONE` ("None") + +### `PerfDataTypeOptions` + +Added: +- `NONE` ("None") +- `PERF_TYPE_UNSPECIFIED` ("Unspecified") + +### `GlazingMaterialTypeOptions` + +Added: `NONE` ("None") + +### `DoorTypeOptions` + +Added: `METAL_W_THERMAL_BREAK` ("Metal with Thermal Break") + +### `LightingAllowanceTypeOptions` + +Added: +- `ALLOWANCE_ADVANCED_CONTROLS` ("Advanced Controls") +- `ALLOWANCE_DECORATIVE_APPEARANCE_LOBBIES` ("Decorative Appearance, Lobbies") +- `ALLOWANCE_DECORATIVE_APPEARANCE_OTHER` ("Decorative Appearance, Other") +- `ALLOWANCE_ELECTRICAL_MECHANICAL` ("Electrical/Mechanical Equipment") +- `ALLOWANCE_VIDEO_CONFERENCE` ("Video conference") +- `NONE` ("None") +- `null` ("Unspecified") +- Dropped explicit `"type": "string"` constraint. + +### `LightingExemptionTypeOptions` + +Added: +- `EXEMPTION_APPROVED_SAFETY` ("Approved Safety Lighting") +- `EXEMPTION_DWELL_UNIT_CONTROLLED` ("Dwelling Unit Lighting Controlled by Occupant") +- `EXEMPTION_EMERGENCY_AUTOOFF` (description "Emergency Lighting Auto-off During Operating Hours" — duplicated from existing entry) +- `EXEMPTION_HIGHLIGHT_HAZARDS` (no description added in diff) +- `EXEMPTION_INDUSTRIAL_PRODUCTION` ("Industrial Production") +- `EXEMPTION_MANUFACTURER_AS_PART_OF_EQUIP` +- `EXEMPTION_POOLS_WATER` +- `EXEMPTION_REQUIRED_EGRESS` ("Lighting Required for Egress") +- `EXEMPTION_TEMP_LIGHTING` ("Temporary Lighting") +- `EXEMPTION_THEME_PARK_ELEMENTS` ("Theme Park Elements") +- `EXEMPTION_HIGHLIGHT_MONUMENT` ("Highlight Monument") +- `EXEMPTION_TRANSPORTATION_MARKER` ("Transportation Marker") +- `EXEMPTION_TRANSPORTATION_SITE` ("Transporation Site Lighting" — typo in source) +- `EXEMPTION_EMERGENCY_LIGHT_OFF_NORMAL_BUSINESS_HRS` ("Emergency Lighting Auto-off During Operating Hours") +- `EXEMPTION_MUSEUM_DISPLAY` ("Museum Display") +- `EXEMPTION_SEARCHLIGHTS` ("Searchlights") +- `EXEMPTION_SLEEPING_UNIT` +- `EXEMPTION_VISUALLY_IMPAIRED` ("Visually Impaired") + +### `TrackLightingWattageBasisTypeOptions` + +Added: +- `NONE` ("None") +- `null` ("Unspecified") + +### `AdvancedControlsAllowanceTypeOptions` + +Added: `null`. Also changed `type` from `"string"` to `["string", "null"]`. + +### `WholeBuildingTypeOptions` + +Added: `WHOLE_BUILDING_INVALID_USE` ("Invalid Use") + +### `ExteriorLightingZoneTypeOptions` + +Added: `EXT_ZONE_UNDEVELOPED` ("Undeveloped area (LZ1)") + +### `ThermalBridgeComplianceTypeOptions` + +- Fixed typo: `" THERMAL_BRIDGE_AS_DESIGNED"` (leading spaces) corrected to `"THERMAL_BRIDGE_AS_DESIGNED"`. +- Added `null` ("Unspecified"). +- Dropped explicit `"type": "string"` constraint. + +### `CondenserTypeOptions` + +Added: `null` ("Unspecified"). Dropped explicit `"type": "string"` constraint. + +### `EconomizerTypeOptions` + +Added: `FLUID_ECONOMIZER` ("Fluid") + +### `FuelTypeOptions` + +Added: +- `OIL_RESIDUAL` ("Residual Oil") +- `null` ("Unspecified") +- Dropped explicit `"type": "string"` constraint. + +### `BoilerDraftTypeOptions` + +Added: `null` ("Unspecified"). Dropped explicit `"type": "string"` constraint. + +### `ChillerTypeOptions` + +Added: +- `CENTRIFUGAL_NON_STANDARD` +- `null` ("Unspecified") +- Dropped explicit `"type": "string"` constraint. + +### `CoolingPlantTypeOptions` + +Added: `null` ("Missing"). Dropped explicit `"type": "string"` constraint. + +### `HeatingPlantTypeOptions` + +Added: `null` ("Missing"). Dropped explicit `"type": "string"` constraint. + +### `HeatPumpChillerHeatingSourceConditionOptions` + +Added: `null` ("Missing"). Dropped explicit `"type": "string"` constraint. + +### `HeatPumpChillerLeavingHeatingWaterTempOptions` + +Added: `null` ("Missing"). Dropped explicit `"type": "string"` constraint. + +### `HeatPumpChillerTypeOptions` + +Added: `null` ("Missing"). Dropped explicit `"type": "string"` constraint. + +### `HeatRejectionTypeOptions` + +Added: `null` ("Missing"). Dropped explicit `"type": "string"` constraint. + +### `FanSystemComplianceMethodOptions` + +Added: `null` ("Missing") + +### `FanEfficiencyExceptionTypeOptions` + +Added: +- `NONE` ("None") +- `null` ("Missing") + +### `SWHSystemDrawPatternTypeOptions` + +Added: +- `NO_COOLING_EQUIPMENT` ("No Cooling Equipment") +- `null` ("Missing") +- Dropped explicit `"type": "string"` constraint. + +### `SWHFuelTypeOptions` + +Added: `null` ("Unspecified"). Dropped explicit `"type": "string"` constraint. + +### `EquipmentEfficiencyRequirementExceptionOptions` + +Added: `null` ("Missing"). Dropped explicit `"type": "string"` constraint. + +### `RenewableExceptionOptions` + +Added: +- `RENEWABLE_ONSITE_EXCEPTION_IECC2024_LOW_FLOOR_AREA` ("Building effective floor area is less than 5,000 ft2") +- `null` ("Unspecified") +- Dropped explicit `"type": "string"` constraint. + +### `BallastTypeOptions` + +Added: `null` ("Unspecified"). Dropped explicit `"type": "string"` constraint. + +### `RequirementAnswerStatus` + +Added: `null` ("Missing"). Dropped explicit `"type": "string"` constraint. + +### `CompliancePathOptions` + +Added: +- `COMPLIANCE_PATH_A` ("Path A") — new canonical form +- `COMPLIANCE_PATH_B` ("Path B") — new canonical form +- `COMPLIANCE_PATH_UNKNOWN` ("Unknown") + +### `ActivityTypeOptions` + +Added: +- `ACTIVITY_COMMON_CONFERENCE_CELL` +- `ACTIVITY_COMMON_GUESTROOM` +- `ACTIVITY_COMMON_PATIENT` +- `ACTIVITY_COMMON_WELLNESS_LOUNGE` +- `ACTIVITY_GAME_HIGH_LIMITS_GAME` +- `ACTIVITY_GAME_SLOTS` +- `ACTIVITY_GAME_SPORTSBOOK` +- `ACTIVITY_GAME_TABLE_GAMES` +- `ACTIVITY_HOSPITAL_TELEMEDICINE_ROOM` +- `ACTIVITY_PARKING_DAYLIGHT_TRANSITION_ZONE` +- `ACTIVITY_RETAIL_MASSAGE_SPACE` +- `ACTIVITY_RETAIL_NAIL_SALON` +- `ACTIVITY_RETAIL_NAIL_SALON_MALL` +- `ACTIVITY_RETAIL_HAIR_SALON` +- `ACTIVITY_SECURITY_SCREEN_TRANSPORTATION_FACILITIES` +- `ACTIVITY_SECURITY_SCREEN_TRANSPORTATION_WAIT_AREA` +- `ACTIVITY_TRANS_AIRPORT_HANGER` +- `ACTIVITY_TRANS_PASSENGER_LOAD` +- `ACTIVITY_SECURITY_SCREEN_GENERAL_AREA` +- `ACTIVITY_SPORTS_POOL_CLASS1` +- `ACTIVITY_SPORTS_POOL_CLASS2` +- `ACTIVITY_SPORTS_POOL_CLASS3` +- `ACTIVITY_SPORTS_POOL_CLASS4` + +Removed: +- `ACTIVITY_COMMON_OFFICE` + +--- + +## 8. Default Value Changes + +| Definition | Field | Old default | New default | +|---|---|---|---| +| `AgWall.continuousDeratedRValue` | — | `0.0` | *(removed)* | +| `Window.preAltPropUval` | — | `0.0` | *(removed)* | +| `Door.preAltPropUval` | — | `0.0` | *(removed)* | +| `Skylight.preAltPropUval` | — | `0.0` | *(removed)* | +| `HVACPlant.efficiencyRequirementException` | — | `"EFF_EXCEPTION_UNSPECIFIED"` | *(removed)* | +| `ServiceWaterHeatingSystem.swhSystemSubType` | — | `"UNKNOWN_SWH_SYSTEM_SUB_TYPE"` | *(removed)* | +| `ServiceWaterHeatingSystem.efficiencyRequirementException` | — | `"EFF_EXCEPTION_UNSPECIFIED"` | *(removed)* | +| `ServiceWaterHeatingSystem.heatPumpPoolHeater` (renamed) | — | *(none)* | `null` | +| `ServiceWaterHeatingSystem.circulationPump` | — | *(none)* | `0` | +| `ServiceWaterHeatingSystem.heatTraceTapeInstalled` | — | *(none)* | `0` | +| `ServiceWaterHeatingSystem.combinedSystem` | — | *(none)* | `0` | +| `ServiceWaterHeatingSystem.poolSystem` | — | *(none)* | `0` | +| `InteriorLightingFixture.advControlsAllowanceType` | — | *(none)* | `null` | +| `HVAC.fanSystem` | — | *(none)* | `null` | + +--- + +## 9. Miscellaneous / Formatting-only + +A large portion of the diff consists of converting compact inline JSON arrays like `["string", "null"]` into multi-line form. These are cosmetic changes with **no semantic impact** on validation. + +Additionally, the schema `version` field at the root was bumped from `"0.0.1"` to `"0.0.2"`. + +--- + +## 10. Post-merge corrections + +After review, the boolean → integer changes in PR #25 were reverted. The backend treats these fields as booleans semantically (`0 = false`, `1 = true`), and the server accepts and returns `true`/`false`. Modeling them as `integer enum [0, 1]` required a runtime `IntEnum → bool` conversion workaround in `CustomBaseModel.model_dump` and introduced unnecessary `IntEnum` wrapper classes in `core_types.py`. All nine fields were restored to their original `boolean` types. + +| Definition | Field | PR #25 change | Reverted to | +|---|---|---|---| +| `ComBuilding` | `isHistoricBuilding` | `integer enum [0, 1]` | `boolean`, default `false` | +| `HVACSystem` | `heatRecovery` | `integer enum [0, 1, null]` | `["boolean", "null"]` | +| `HVACSystem` | `heatPumpSimultaneousCoolingAndHeating` | `integer enum [0, 1, null]` | `["boolean", "null"]` | +| `HVACPlant` | `twoPipeSystem` | `integer enum [0, 1, null]` | `["boolean", "null"]` | +| `HVACPlant` | `waterloopHeatPump` | `integer enum [0, 1, null]` | `["boolean", "null"]` | +| `ServiceWaterHeatingSystem` | `circulationPump` | `integer enum [0, 1]`, default `0` | `boolean`, default `false` | +| `ServiceWaterHeatingSystem` | `heatTraceTapeInstalled` | `integer enum [0, 1]`, default `0` | `boolean`, default `false` | +| `ServiceWaterHeatingSystem` | `combinedSystem` | `integer enum [0, 1]`, default `0` | `boolean`, default `false` | +| `ServiceWaterHeatingSystem` | `poolSystem` | `integer enum [0, 1]`, default `0` | `boolean`, default `false` | + +The `IsHistoricBuilding`, `CirculationPump`, `HeatTraceTapeInstalled`, `CombinedSystem`, and `PoolSystem` `IntEnum` classes were removed from `core_types.py` as a result. From 966b3fef5fcb4016605f33c2600eed5aad8ef230 Mon Sep 17 00:00:00 2001 From: Julian Slane Date: Wed, 12 Aug 2026 12:32:58 -0700 Subject: [PATCH 17/23] Remove use-default for model generation and exclude_unset on export --- comcheck_api/ai/skill/SKILL.md | 2 +- comcheck_api/client/comcheck_client.py | 12 +- .../constants/interior_lighting_constants.py | 2 +- comcheck_api/managers/data_manager.py | 2 +- comcheck_api/types/core_types.py | 288 ++++++++---------- comcheck_api/utilities/common.py | 2 +- docs/missing-vs-exclude-unset.md | 2 +- docs_site/api/operations/exterior-lighting.md | 2 +- docs_site/api/operations/interior-lighting.md | 2 +- pyproject.toml | 2 +- .../test_exterior_lighting_operations.py | 2 +- .../test_interior_lighting_operations.py | 2 +- tools/generate_core_types.py | 1 - uv.lock | 8 +- 14 files changed, 146 insertions(+), 183 deletions(-) diff --git a/comcheck_api/ai/skill/SKILL.md b/comcheck_api/ai/skill/SKILL.md index 2d5cb47..1b9997e 100644 --- a/comcheck_api/ai/skill/SKILL.md +++ b/comcheck_api/ai/skill/SKILL.md @@ -259,7 +259,7 @@ fixture.quantity = 10 activity_use = get_default_interior_lighting_space_template() activity_use.areaDescription = "Open Office" -activity_use.activityType = ActivityTypeOptions.ACTIVITY_COMMON_OFFICE +activity_use.activityType = ActivityTypeOptions.ACTIVITY_COMMON_OFFICE_OPEN activity_use.interiorLightingSpace = activity_use.interiorLightingSpace.model_copy( deep=True, update={"fixture": [fixture]} ) diff --git a/comcheck_api/client/comcheck_client.py b/comcheck_api/client/comcheck_client.py index 0e1c8a5..30eca6d 100644 --- a/comcheck_api/client/comcheck_client.py +++ b/comcheck_api/client/comcheck_client.py @@ -167,7 +167,7 @@ def update_project( if not old_project: raise COMCheckProjectNotFoundError(project_id) - project_data_json = project_data.model_dump(mode="json", exclude_unset=True) + project_data_json = project_data.model_dump(mode="json") # Preserve user project reference user_project = old_project["userProject"] @@ -262,7 +262,7 @@ def update_uvalues(self, project: ComBuilding) -> ComBuilding: The same ``project`` instance, with u-values updated. """ energy_code = str(project.control.code) - envelope_data = project.envelope.model_dump(mode="json", exclude_unset=True) + envelope_data = project.envelope.model_dump(mode="json") updated_assembly_uvalues = self._service.assemblies_uvalue( envelope_data, energy_code )["data"] @@ -296,7 +296,7 @@ def check_UA_compliance(self, project: ComBuilding) -> Any: Returns: The compliance results payload returned by the API. """ - project_data = project.model_dump(mode="json", exclude_unset=True) + project_data = project.model_dump(mode="json") response = self._service.check_UA_compliance(project_data) return response.get("data") @@ -309,7 +309,7 @@ def check_requirements(self, project: ComBuilding) -> Any: Returns: The requirements payload returned by the API. """ - project_data = project.model_dump(mode="json", exclude_unset=True) + project_data = project.model_dump(mode="json") response = self._service.check_requirements(project_data) return response.get("data") @@ -349,7 +349,7 @@ def generate_report( ``expires``, and ``fileName``. """ report_data = { - "building": project.model_dump(mode="json", exclude_unset=True), + "building": project.model_dump(mode="json"), "envelope": envelope, "extlighting": extlighting, "intlighting": intlighting, @@ -396,7 +396,7 @@ def start_run_simulation( logger.info("Updating project: %s", project_id) project = self.update_project(str(project_id), project) - project_data = project.model_dump(mode="json", exclude_unset=True) + project_data = project.model_dump(mode="json") run_result = self._service.start_run_simulation(project_data) if run_result.data is None: raise COMCheckSimulationError( diff --git a/comcheck_api/constants/interior_lighting_constants.py b/comcheck_api/constants/interior_lighting_constants.py index 9553b50..75f8b01 100644 --- a/comcheck_api/constants/interior_lighting_constants.py +++ b/comcheck_api/constants/interior_lighting_constants.py @@ -33,7 +33,7 @@ DEFAULT_FIXTURE: Fixture = Fixture( description="LED fixture", - fixtureType=None, + fixtureType="Fixture 1", lightingType=LightingTypeOptions.LED, fixtureWattage=32.0, quantity=1, diff --git a/comcheck_api/managers/data_manager.py b/comcheck_api/managers/data_manager.py index c46bb76..6ce77e7 100644 --- a/comcheck_api/managers/data_manager.py +++ b/comcheck_api/managers/data_manager.py @@ -326,7 +326,7 @@ def modify_one(self, id_value: Any, updates: T | dict[str, Any]) -> T: # 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, exclude_unset=True + mode="python", by_alias=False ) merged = {**original_dict, **updates_dict} try: diff --git a/comcheck_api/types/core_types.py b/comcheck_api/types/core_types.py index 1c07653..b9bbfad 100644 --- a/comcheck_api/types/core_types.py +++ b/comcheck_api/types/core_types.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: comCheck.schema.json -# timestamp: 2026-08-10T22:27:41+00:00 +# timestamp: 2026-08-11T17:22:04+00:00 from __future__ import annotations @@ -2252,14 +2252,14 @@ class Control(CustomBaseModel): version: Annotated[ str | None, Field(description='Software version - shall always set to an empty string'), - ] = '' + ] code: Annotated[ EnergyCodeOptions | StateRegionEnergyCodeOptions, Field(description='Energy code types'), ] complianceMode: Annotated[ ComplianceModeOptions, Field(description='Project compliance type') - ] = 'UA' + ] class Requirements(CustomBaseModel): @@ -2272,17 +2272,17 @@ class Requirements(CustomBaseModel): description='Scope-unique reference identifier for instances of this data group.' ), ] = MISSING - category: Annotated[str, Field(description='Requirement Answer - category')] = '' - requirementName: Annotated[str, Field(description='Requirement Answer - name')] = '' + category: Annotated[str, Field(description='Requirement Answer - category')] + requirementName: Annotated[str, Field(description='Requirement Answer - name')] status: Annotated[ RequirementAnswerStatus, Field(description='Requirement Answer - status') - ] = '' + ] locationOnPlans: Annotated[ str | None, Field(description='Requirement Answer - Location On Plans') - ] = '' + ] exceptionName: Annotated[ str | None, Field(description='Requirement Answer - Exception Name') - ] = '' + ] class Window(CustomBaseModel): @@ -2299,62 +2299,58 @@ class Window(CustomBaseModel): str | None, Field(description='key reference of the building use area data group'), ] - description: Annotated[ - str | None, Field(description='The name of the component') - ] = '' + description: Annotated[str | None, Field(description='The name of the component')] adjacentSpaceType: Annotated[ AdjacentSpaceTypeOptions | None, Field(description='Space type of the adjacent space'), - ] = None + ] adjacentSpaceBuildingType: Annotated[ WholeBuildingTypeOptions | MISSING, Field(description='Building type of the adjacent space'), ] = MISSING - assemblyType: Annotated[str, Field(description='The type of the component')] = ( - 'Window' - ) + assemblyType: Annotated[str, Field(description='The type of the component')] propUValue: Annotated[ float | None, Field(description='Proposed thermal transmittance of the window.') - ] = 0.0 - grossArea: Annotated[float | None, Field(description='gross area', ge=0.0)] = 0.0 + ] + grossArea: Annotated[float | None, Field(description='gross area', ge=0.0)] altExemptType: Annotated[ AltExemptTypeOptions | None | MISSING, Field(description='alteration exemption type'), ] = MISSING propShgc: Annotated[ float | None, Field(description='Proposed solar heat gain coefficient', ge=0.0) - ] = 0.0 + ] propProjectionFactor: Annotated[ float | None, Field(description='Proposed window projection factor', ge=0.0) - ] = 0.0 + ] frameType: Annotated[ FenestrationFrameTypeOptions, Field(description='Window frame type') - ] = None + ] glazingType: Annotated[ GlazingTypeOptions | None, Field( description='Glazing type - used when performance data option is energy code default (PERF_TYPE_DEFAULT)' ), - ] = None - propVt: Annotated[float | None, Field(ge=0.0)] = None - preAltPropShgc: Annotated[float | None, Field(ge=0.0)] = 0.0 + ] + propVt: Annotated[float | None, Field(ge=0.0)] + preAltPropShgc: Annotated[float | None, Field(ge=0.0)] solarType: Annotated[ SolarTypeOptions | None, Field( description='Solar coating type - used when performance data option is energy code default (PERF_TYPE_DEFAULT)' ), - ] = None - orientation: OrientationOptions = 'UNSPECIFIED_ORIENTATION' + ] + orientation: OrientationOptions glazingMaterialType: Annotated[ GlazingMaterialTypeOptions | None, Field(description='Glazing material type') - ] = None + ] productType: Annotated[ WindowProductionTypeOptions | None, Field(description='Product Type') ] = None allowanceType: Annotated[ EnvelopeAssemblyAllowanceTypeOptions, Field(description='allowance type') ] - exemptionType: EnvelopeAssemblyExemptionOptions | None = None + exemptionType: EnvelopeAssemblyExemptionOptions | None feetAg: Annotated[float | None, Field(description='Feet above grade', ge=0.0)] = ( None ) @@ -2362,24 +2358,22 @@ class Window(CustomBaseModel): ConstructionTypeOptions | None | MISSING, Field(description='Construction types - compliance code specification'), ] = MISSING - isSiteShading: Annotated[bool | None, Field(description='Is the site shaded')] = ( - None - ) + isSiteShading: Annotated[bool | None, Field(description='Is the site shaded')] perfDataType: Annotated[ PerfDataTypeOptions | None, Field(description='Performance data type option') ] = None - productId: Annotated[str | None, Field(description='Product ID')] = None + productId: Annotated[str | None, Field(description='Product ID')] preAltPropUval: Annotated[float | None | MISSING, Field(ge=0.0)] = MISSING windowOpenType: Annotated[ WindowOpenTypeOptions | None, Field(description='Window open type') - ] = None - cavityRValue: float | None = 0.0 + ] + cavityRValue: float | None continuousRValue: Annotated[ float | None, Field( description='Continuous insulation on the door. Can be exterior or interior or both.' ), - ] = 0.0 + ] class Door(CustomBaseModel): @@ -2396,88 +2390,82 @@ class Door(CustomBaseModel): str | None, Field(description='key reference of the building use area data group'), ] - description: Annotated[ - str | None, Field(description='The name of the component') - ] = '' - assemblyType: Annotated[str, Field(description='The type of the component')] = ( - 'Door' - ) + description: Annotated[str | None, Field(description='The name of the component')] + assemblyType: Annotated[str, Field(description='The type of the component')] adjacentSpaceType: Annotated[ AdjacentSpaceTypeOptions | None, Field(description='Space type of the adjacent space'), - ] = None + ] adjacentSpaceBuildingType: Annotated[ WholeBuildingTypeOptions | MISSING, Field(description='Building type of the adjacent space'), ] = MISSING propUValue: Annotated[ float | None, Field(description='Proposed thermal transmittance of the window.') - ] = 0.0 - grossArea: Annotated[float | None, Field(description='gross area', ge=0.0)] = None + ] + grossArea: Annotated[float | None, Field(description='gross area', ge=0.0)] altExemptType: Annotated[ AltExemptTypeOptions | None | MISSING, Field(description='alteration exemption type'), ] = MISSING propShgc: Annotated[ float | None, Field(description='Proposed solar heat gain coefficient', ge=0.0) - ] = 0.0 + ] propProjectionFactor: Annotated[ float | None, Field(description='Proposed window projection factor', ge=0.0) - ] = 0.0 + ] frameType: Annotated[ FenestrationFrameTypeOptions, Field(description='Glass door frame type') - ] = None + ] glazingType: Annotated[ GlazingTypeOptions | None, Field( description='Glazing type - used when performance data option is energy code default (PERF_TYPE_DEFAULT)' ), - ] = None - propVt: Annotated[float | None, Field(ge=0.0)] = None - preAltPropShgc: Annotated[float | None, Field(ge=0.0)] = 0.0 + ] + propVt: Annotated[float | None, Field(ge=0.0)] + preAltPropShgc: Annotated[float | None, Field(ge=0.0)] solarType: Annotated[ SolarTypeOptions | None, Field( description='Solar coating type - used when performance data option is energy code default (PERF_TYPE_DEFAULT)' ), - ] = None - orientation: OrientationOptions = 'UNSPECIFIED_ORIENTATION' + ] + orientation: OrientationOptions glazingMaterialType: Annotated[ GlazingMaterialTypeOptions | None, Field(description='Glazing material type') - ] = None + ] productType: Annotated[ WindowProductionTypeOptions | None, Field(description='Product Type') - ] = None + ] allowanceType: Annotated[ EnvelopeAssemblyAllowanceTypeOptions, Field(description='allowance type') ] - exemptionType: EnvelopeAssemblyExemptionOptions | None = None + exemptionType: EnvelopeAssemblyExemptionOptions | None constructionType: Annotated[ ConstructionTypeOptions | None | MISSING, Field(description='Construction types - compliance code specification'), ] = MISSING - isSiteShading: Annotated[bool | None, Field(description='Is the site shaded')] = ( - None - ) + isSiteShading: Annotated[bool | None, Field(description='Is the site shaded')] perfDataType: Annotated[ PerfDataTypeOptions | None, Field(description='Performance data type option') - ] = None - productId: Annotated[str | None, Field(description='Product ID')] = None + ] + productId: Annotated[str | None, Field(description='Product ID')] preAltPropUval: Annotated[float | None | MISSING, Field(ge=0.0)] = MISSING - doorType: Annotated[DoorTypeOptions | None, Field(description='Door types')] = None + doorType: Annotated[DoorTypeOptions | None, Field(description='Door types')] doorOpenType: Annotated[ DoorOpenTypeOptions | None, Field(description='Door open types') - ] = None + ] doorEntranceType: Annotated[ DoorEntranceTypeOptions | None, Field(description='Door entrance types') - ] = None - cavityRValue: float | None = 0.0 + ] + cavityRValue: float | None continuousRValue: Annotated[ float | None, Field( description='Continuous insulation on the door. Can be exterior or interior or both.' ), - ] = 0.0 + ] class Skylight(CustomBaseModel): @@ -2506,46 +2494,42 @@ class Skylight(CustomBaseModel): description='Continuous insulation on the skylight. Can be exterior or interior or both.' ), ] = 0.0 - description: Annotated[ - str | None, Field(description='The name of the component') - ] = '' - assemblyType: Annotated[str, Field(description='The type of the component')] = ( - 'Skylight' - ) + description: Annotated[str | None, Field(description='The name of the component')] + assemblyType: Annotated[str, Field(description='The type of the component')] adjacentSpaceType: Annotated[ AdjacentSpaceTypeOptions | None, Field(description='Space type of the adjacent space'), - ] = None + ] adjacentSpaceBuildingType: Annotated[ WholeBuildingTypeOptions | MISSING, Field(description='Building type of the adjacent space'), ] = MISSING propUValue: Annotated[ float | None, Field(description='Proposed thermal transmittance of the window.') - ] = 0.0 - grossArea: Annotated[float | None, Field(description='gross area', ge=0.0)] = 0.0 - orientation: OrientationOptions = 'UNSPECIFIED_ORIENTATION' + ] + grossArea: Annotated[float | None, Field(description='gross area', ge=0.0)] + orientation: OrientationOptions altExemptType: Annotated[ AltExemptTypeOptions | None | MISSING, Field(description='alteration exemption type'), ] = MISSING propShgc: Annotated[ float | None, Field(description='Proposed solar heat gain coefficient', ge=0.0) - ] = 0.0 + ] propProjectionFactor: Annotated[ float | None, Field(description='Proposed window projection factor', ge=0.0) - ] = 0.0 + ] frameType: Annotated[ FenestrationFrameTypeOptions, Field(description='Window frame type') - ] = None + ] glazingType: Annotated[ GlazingTypeOptions | None, Field( description='Glazing type - used when performance data option is energy code default (PERF_TYPE_DEFAULT)' ), - ] = None - propVt: Annotated[float | None, Field(ge=0.0)] = None - preAltPropShgc: Annotated[float | None, Field(ge=0.0)] = 0.0 + ] + propVt: Annotated[float | None, Field(ge=0.0)] + preAltPropShgc: Annotated[float | None, Field(ge=0.0)] solarType: Annotated[ SolarTypeOptions | None, Field( @@ -2554,29 +2538,27 @@ class Skylight(CustomBaseModel): ] = None glazingMaterialType: Annotated[ GlazingMaterialTypeOptions | None, Field(description='Glazing material type') - ] = None + ] productType: Annotated[ WindowProductionTypeOptions | None, Field(description='Product Type') - ] = None + ] allowanceType: Annotated[ EnvelopeAssemblyAllowanceTypeOptions, Field(description='allowance type') ] - exemptionType: EnvelopeAssemblyExemptionOptions | None = None + exemptionType: EnvelopeAssemblyExemptionOptions | None constructionType: Annotated[ ConstructionTypeOptions | None | MISSING, Field(description='Construction types - compliance code specification'), ] = MISSING - isSiteShading: Annotated[bool | None, Field(description='Is the site shaded')] = ( - None - ) + isSiteShading: Annotated[bool | None, Field(description='Is the site shaded')] perfDataType: Annotated[ PerfDataTypeOptions | None, Field(description='Performance data type option') - ] = None - productId: Annotated[str | None, Field(description='Product ID')] = None + ] + productId: Annotated[str | None, Field(description='Product ID')] preAltPropUval: Annotated[float | None | MISSING, Field(ge=0.0)] = MISSING curbType: Annotated[ SkylightCurbTypeOptions | None, Field(description='Skylight curb type') - ] = None + ] class Roof(CustomBaseModel): @@ -2589,12 +2571,8 @@ class Roof(CustomBaseModel): description='Scope-unique reference identifier for instances of this data group.' ), ] = MISSING - description: Annotated[ - str | None, Field(description='The name of the component') - ] = '' - assemblyType: Annotated[str, Field(description='The type of the component')] = ( - 'Roof' - ) + description: Annotated[str | None, Field(description='The name of the component')] + assemblyType: Annotated[str, Field(description='The type of the component')] bldgUseKey: Annotated[ str | None, Field(description='key reference of the building use area data group'), @@ -2602,7 +2580,7 @@ class Roof(CustomBaseModel): adjacentSpaceType: Annotated[ AdjacentSpaceTypeOptions | None, Field(description='Space type of the adjacent space'), - ] = None + ] adjacentSpaceBuildingType: Annotated[ WholeBuildingTypeOptions | MISSING, Field(description='Building type of the adjacent space'), @@ -2614,32 +2592,32 @@ class Roof(CustomBaseModel): ConstructionTypeOptions | None | MISSING, Field(description='Construction types - compliance code specification'), ] = MISSING - exemptionType: EnvelopeAssemblyExemptionOptions | None = None - orientation: OrientationOptions = 'UNSPECIFIED_ORIENTATION' + exemptionType: EnvelopeAssemblyExemptionOptions | None + orientation: OrientationOptions skylight: Annotated[list[Skylight], Field(description='Skylights on the roof')] - cavityRValue: float | None = 0.0 + cavityRValue: float | None continuousRValue: Annotated[ float | None, Field( description='Continuous insulation on the above grade wall. Can be exterior or interior or both.' ), - ] = 0.0 + ] propUValue: Annotated[ float | None, Field(description='Proposed thermal transmittance of the above grade wall.'), - ] = 0.0 + ] altExemptType: Annotated[ AltExemptTypeOptions | None | MISSING, Field(description='alteration exemption type'), ] = MISSING - grossArea: Annotated[float | None, Field(description='gross area', ge=0.0)] = 0.0 + grossArea: Annotated[float | None, Field(description='gross area', ge=0.0)] roofType: Annotated[RoofTypeOptions | None, Field(description='roof type')] = None highAlbedoRoofReqType: Annotated[ HighAlbedoRoofReqTypeOptions | None, Field( description='high albedo roof type - this include the albedo method and emeptions' ), - ] = None + ] otherRoofType: Annotated[ OtherRoofTypeOptions | None, Field( @@ -2648,19 +2626,15 @@ class Roof(CustomBaseModel): ] = None roofInsulType: Annotated[ RoofInsulationTypeOptions | None, Field(description='roof insulation types') - ] = None - solarReflectance: Annotated[ - float, Field(description='solar reflectance', ge=0.0) - ] = 0.0 + ] + solarReflectance: Annotated[float, Field(description='solar reflectance', ge=0.0)] solarReflectanceIndex: Annotated[ float, Field(description='solar reflectance index', ge=0.0) - ] = 0.0 - thermalEmittance: Annotated[ - float, Field(description='thermal emittance', ge=0.0) - ] = 0.0 + ] + thermalEmittance: Annotated[float, Field(description='thermal emittance', ge=0.0)] purlinSpacing: Annotated[ float | None, Field(description='Roof purlin spacing', ge=0.0) - ] = 0.0 + ] class Floor(CustomBaseModel): @@ -3576,9 +3550,7 @@ class AgWall(CustomBaseModel): description='Scope-unique reference identifier for instances of this data group.' ), ] = MISSING - description: Annotated[ - str | None, Field(description='The name of the component') - ] = '' + description: Annotated[str | None, Field(description='The name of the component')] assemblyType: Annotated[ str | None, Field(description='The type of the component') ] = 'Exterior Wall' @@ -3592,11 +3564,11 @@ class AgWall(CustomBaseModel): agWallConstructionDetailsType: Annotated[ AgWallConstructionDetailsTypeOptions, Field(description='Above grade wall construction details type'), - ] = 'NONE' + ] agWallExteriorFinishDetailsType: Annotated[ AgWallExteriorFinishDetailsTypeOptions | None, Field(description='Above grade wall exterior finish details'), - ] = None + ] nextToUncondSpace: Annotated[ bool | None, Field( @@ -3608,14 +3580,14 @@ class AgWall(CustomBaseModel): Field( description='Not sure why basement has this data, likely deprecated. Use null' ), - ] = None + ] otherWallType: Annotated[ AgWallOtherTypeOptions | None, Field(description='other wall types') - ] = 'NONE' + ] adjacentSpaceType: Annotated[ AdjacentSpaceTypeOptions | None, Field(description='Space type of the adjacent space'), - ] = None + ] adjacentSpaceBuildingType: Annotated[ WholeBuildingTypeOptions | MISSING, Field(description='Building type of the adjacent space'), @@ -3626,38 +3598,38 @@ class AgWall(CustomBaseModel): thermalBridgeExceptionType: Annotated[ ThermalBridgeExceptionTypeOptions | None, Field(description='Type of thermal bridge exceptions'), - ] = None + ] effectiveUFactor: Annotated[ float | None, Field(description='The effective U factor after thermal bridge adjustment'), - ] = None + ] thermalBridgeAdjustmentFactor: Annotated[ float | None, Field(description='Thermal bridge adjustment factor') ] = None allowanceType: Annotated[ EnvelopeAssemblyAllowanceTypeOptions, Field(description='allowance type') ] - cmuType: Annotated[CMUTypeOptions | None, Field(description='CMU type')] = None + cmuType: Annotated[CMUTypeOptions | None, Field(description='CMU type')] concreteDensity: Annotated[ ConcreteDensityOptions, Field(description='Concrete density') - ] = 0 + ] concreteThickness: Annotated[ ConcreteThicknessOptions, Field(description='Concrete thickness') - ] = 0 + ] constructionType: Annotated[ ConstructionTypeOptions | None | MISSING, Field(description='Construction types - compliance code specification'), ] = MISSING - exemptionType: EnvelopeAssemblyExemptionOptions | None = None - furringType: FurringTypeOptions | None = None + exemptionType: EnvelopeAssemblyExemptionOptions | None + furringType: FurringTypeOptions | None heatCapacity: Annotated[ float | None, Field( description='heat capacity of a mass wall. Used in other mass wall type', ge=0.0, ), - ] = 0.0 - orientation: OrientationOptions = 'UNSPECIFIED_ORIENTATION' + ] + orientation: OrientationOptions window: Annotated[ list[Window], Field(description='Windows on the wall', min_length=0) ] @@ -3667,13 +3639,13 @@ class AgWall(CustomBaseModel): Field( description='Average insulation R-value in the cavity between two studs.' ), - ] = 0.0 + ] continuousRValue: Annotated[ float | None, Field( description='Continuous insulation on the above grade wall. Can be exterior or interior or both.' ), - ] = 0.0 + ] continuousDeratedRValue: Annotated[ float | None | MISSING, Field( @@ -3684,11 +3656,11 @@ class AgWall(CustomBaseModel): propUValue: Annotated[ float | None, Field(description='Proposed thermal transmittance of the above grade wall.'), - ] = 0.0 + ] altExemptType: Annotated[ AltExemptTypeOptions | None, Field(description='alteration exemption type') ] = None - grossArea: Annotated[float | None, Field(description='gross area')] = 0.0 + grossArea: Annotated[float | None, Field(description='gross area')] class BgWall(CustomBaseModel): @@ -3701,12 +3673,8 @@ class BgWall(CustomBaseModel): description='Scope-unique reference identifier for instances of this data group.' ), ] = MISSING - description: Annotated[ - str | None, Field(description='The name of the component') - ] = '' - assemblyType: Annotated[str, Field(description='The type of the component')] = ( - 'Basement' - ) + description: Annotated[str | None, Field(description='The name of the component')] + assemblyType: Annotated[str, Field(description='The type of the component')] bldgUseKey: Annotated[ str | None, Field(description='key reference of the building use area data group'), @@ -3716,14 +3684,12 @@ class BgWall(CustomBaseModel): ] = MISSING wallHeight: Annotated[ float, Field(description='Total height of a below grade wall') - ] = 0.0 - wallHeightBelowGrade: Annotated[ - float, Field(description='Wall height below grade') - ] = 0.0 + ] + wallHeightBelowGrade: Annotated[float, Field(description='Wall height below grade')] adjacentSpaceType: Annotated[ AdjacentSpaceTypeOptions | None, Field(description='Space type of the adjacent space'), - ] = None + ] adjacentSpaceBuildingType: Annotated[ WholeBuildingTypeOptions | MISSING, Field(description='Building type of the adjacent space'), @@ -3731,33 +3697,33 @@ class BgWall(CustomBaseModel): allowanceType: Annotated[ EnvelopeAssemblyAllowanceTypeOptions, Field(description='allowance type') ] - cmuType: Annotated[CMUTypeOptions | None, Field(description='CMU type')] = None + cmuType: Annotated[CMUTypeOptions | None, Field(description='CMU type')] concreteDensity: Annotated[ ConcreteDensityOptions, Field(description='Concrete density') - ] = 0 + ] concreteThickness: Annotated[ ConcreteThicknessOptions, Field(description='Concrete thickness') - ] = 0 + ] constructionType: Annotated[ ConstructionTypeOptions | None | MISSING, Field(description='Construction types - compliance code specification'), ] = MISSING - exemptionType: EnvelopeAssemblyExemptionOptions | None = None - furringType: FurringTypeOptions | None = None + exemptionType: EnvelopeAssemblyExemptionOptions | None + furringType: FurringTypeOptions | None heatCapacity: Annotated[ float | None, Field( description='heat capacity of a mass wall. Used in other mass wall type', ge=0.0, ), - ] = 0.0 - orientation: OrientationOptions = 'UNSPECIFIED_ORIENTATION' + ] + orientation: OrientationOptions insulationPosition: Annotated[ str | None, Field( description='Not sure why basement has this data, likely deprecated. Use null' ), - ] = None + ] window: Annotated[ list[Window], Field(description='Windows on the wall', min_length=0) ] @@ -3767,7 +3733,7 @@ class BgWall(CustomBaseModel): Field( description='Average insulation R-value in the cavity between two studs.' ), - ] = 0.0 + ] continuousRValue: Annotated[ float | None, Field( @@ -3777,11 +3743,11 @@ class BgWall(CustomBaseModel): propUValue: Annotated[ float | None, Field(description='Proposed thermal transmittance of the below grade wall.'), - ] = 0.0 + ] altExemptType: Annotated[ AltExemptTypeOptions | None, Field(description='alteration exemption type') ] = None - grossArea: Annotated[float, Field(description='gross area', ge=0.0)] = 0.0 + grossArea: Annotated[float, Field(description='gross area', ge=0.0)] class Fixture(CustomBaseModel): @@ -4167,7 +4133,7 @@ class HVAC(CustomBaseModel): Field(description='HVAC systems - air-based or radiant-based system'), ] hvacPlant: Annotated[list[HVACPlant], Field(description='HVAC Plant - source loop')] - fanSystem: Annotated[list[FanSystem] | None, Field(description='Fan system')] = None + fanSystem: Annotated[list[FanSystem] | None, Field(description='Fan system')] class ActivityUse(CustomBaseModel): @@ -4414,19 +4380,19 @@ class ComBuilding(CustomBaseModel): Field( description='Flag to indicate the building has conditional type of non-resindetial conditioning.' ), - ] = False + ] isResidentialConditioning: Annotated[ bool | None, Field( description='Flag to indicate the building has conditional type of resindetial conditioning.' ), - ] = False + ] isSemiheatedConditioning: Annotated[ bool | None, Field( description='Flag to indicate the building has conditional type of semiheated conditioning.' ), - ] = False + ] isHistoricBuilding: Annotated[ bool | None, Field(description='Flag to indicate if the building is historic.') ] = False @@ -4491,9 +4457,7 @@ class ComBuilding(CustomBaseModel): description='Advanced reporting indicates whether the building conditioned by heat pumps' ), ] = None - projectType: Annotated[ProjectTypeOptions, Field(description='Project type')] = ( - 'NEW_CONSTRUCTION' - ) + projectType: Annotated[ProjectTypeOptions, Field(description='Project type')] projectSubType: Annotated[ ProjectSubTypeOptions | None, Field(description='Project sub-type') ] = 'CONSTRUCTION_COMPLETE' diff --git a/comcheck_api/utilities/common.py b/comcheck_api/utilities/common.py index 4e3670f..4b52675 100644 --- a/comcheck_api/utilities/common.py +++ b/comcheck_api/utilities/common.py @@ -18,7 +18,7 @@ def _json_default(obj: Any) -> Any: aliases, and nested models serialize the same way the API expects. """ if isinstance(obj, BaseModel): - return obj.model_dump(mode="json", exclude_unset=True) + return obj.model_dump(mode="json") raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable") diff --git a/docs/missing-vs-exclude-unset.md b/docs/missing-vs-exclude-unset.md index 36bf825..200bc66 100644 --- a/docs/missing-vs-exclude-unset.md +++ b/docs/missing-vs-exclude-unset.md @@ -46,7 +46,7 @@ f.model_dump(exclude_unset=True) # → {} (both dropped) update = Roof(roofType=RoofTypeOptions.METAL_ROOF_WITH_THERMAL_BLOCKS) update.model_dump(mode="json") # includes propUValue=0, grossArea=0, ... -update.model_dump(mode="json", exclude_unset=True) # → {"roofType": "METAL_ROOF_WITH_THERMAL_BLOCKS"} +update.model_dump(mode="json") # → {"roofType": "METAL_ROOF_WITH_THERMAL_BLOCKS"} ``` ## Summary diff --git a/docs_site/api/operations/exterior-lighting.md b/docs_site/api/operations/exterior-lighting.md index 7427c18..04d450b 100644 --- a/docs_site/api/operations/exterior-lighting.md +++ b/docs_site/api/operations/exterior-lighting.md @@ -99,7 +99,7 @@ updated_space = eu.exteriorLightingSpace.model_copy( project = el_ops.update_exterior_lighting_area_in_project( project, "Main Parking Area", - {"exteriorLightingSpace": updated_space.model_dump(mode="python", exclude_unset=True)}, + {"exteriorLightingSpace": updated_space.model_dump(mode="python")}, ) ``` diff --git a/docs_site/api/operations/interior-lighting.md b/docs_site/api/operations/interior-lighting.md index 78ed611..ae4ae02 100644 --- a/docs_site/api/operations/interior-lighting.md +++ b/docs_site/api/operations/interior-lighting.md @@ -102,7 +102,7 @@ project = il_ops.update_interior_lighting_space_in_project( project, area_key, "Open Office", - {"interiorLightingSpace": updated_space.model_dump(mode="python", exclude_unset=True)}, + {"interiorLightingSpace": updated_space.model_dump(mode="python")}, ) ``` diff --git a/pyproject.toml b/pyproject.toml index 930e62e..1b153b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,7 +61,7 @@ members = [] [dependency-groups] dev = [ "black>=26.1.0", - "datamodel-code-generator>=0.71.0", + "datamodel-code-generator>=0.72.3", "mypy>=1.19.1", "pre-commit>=4.5.1", "pytest>=9.0.2", diff --git a/tests/project_operation_tests/test_exterior_lighting_operations.py b/tests/project_operation_tests/test_exterior_lighting_operations.py index f4022ab..3b36588 100644 --- a/tests/project_operation_tests/test_exterior_lighting_operations.py +++ b/tests/project_operation_tests/test_exterior_lighting_operations.py @@ -191,7 +191,7 @@ def test_add_fixture_via_exterior_use_update(project: ComBuilding): "Canopy", { "exteriorLightingSpace": updated_space.model_dump( - mode="python", exclude_unset=True + mode="python" ) }, ) diff --git a/tests/project_operation_tests/test_interior_lighting_operations.py b/tests/project_operation_tests/test_interior_lighting_operations.py index e8baf4f..5faa5bb 100644 --- a/tests/project_operation_tests/test_interior_lighting_operations.py +++ b/tests/project_operation_tests/test_interior_lighting_operations.py @@ -130,7 +130,7 @@ def test_add_fixture_via_activity_use_update(project: ComBuilding): "Lab", { "interiorLightingSpace": updated_space.model_dump( - mode="python", exclude_unset=True + mode="python" ) }, ) diff --git a/tools/generate_core_types.py b/tools/generate_core_types.py index d1c1130..f8cab9a 100644 --- a/tools/generate_core_types.py +++ b/tools/generate_core_types.py @@ -38,7 +38,6 @@ def main(): "--use-standard-collections", "--use-schema-description", "--use-missing-sentinel", - "--use-default", # Use default values from the schema "--field-constraints", # Generate validation constraints (e.g., max_length, minItems) "--use-annotated", # Best practice for Pydantic V2 validations "--formatters", diff --git a/uv.lock b/uv.lock index 2f84e76..dc35bf9 100644 --- a/uv.lock +++ b/uv.lock @@ -255,7 +255,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ { name = "black", specifier = ">=26.1.0" }, - { name = "datamodel-code-generator", specifier = ">=0.71.0" }, + { name = "datamodel-code-generator", specifier = ">=0.72.3" }, { name = "mypy", specifier = ">=1.19.1" }, { name = "pre-commit", specifier = ">=4.5.1" }, { name = "pytest", specifier = ">=9.0.2" }, @@ -268,7 +268,7 @@ docs = [ [[package]] name = "datamodel-code-generator" -version = "0.71.0" +version = "0.72.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "argcomplete" }, @@ -280,9 +280,9 @@ dependencies = [ { name = "pydantic" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/54/f5/f4ce23d99503b147c9ec514dc995a96d3b4d2a3284252ad665f875a3145d/datamodel_code_generator-0.71.0.tar.gz", hash = "sha256:d27cd7a0d10f9b2db74a41db7f3e050c226da9cf0afb4916a7ab56275ebacbf2", size = 1684916, upload-time = "2026-07-24T15:32:04.334Z" } +sdist = { url = "https://files.pythonhosted.org/packages/14/4b/4652f0bb085a564982c3e515a0b02bcee21765ddac967bcf152a70d2af14/datamodel_code_generator-0.72.3.tar.gz", hash = "sha256:a20160de09b76d4a293ccba5a9ee5c341b5365890237ac1ff812f420e4c09f3b", size = 1965678, upload-time = "2026-08-10T18:58:41.636Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/4d/556cb290170f41b97ce50fd872e10a266f141d7a38352bb3071e4ae61f41/datamodel_code_generator-0.71.0-py3-none-any.whl", hash = "sha256:680b68338d59e98a0559eeb54d8e5ca33c35b3ec0bef922ec2cc783f2cb28e9a", size = 452379, upload-time = "2026-07-24T15:32:02.467Z" }, + { url = "https://files.pythonhosted.org/packages/84/fd/9d8a0594aedcc6ee8133b21dd3543d6021842e013058803a69918720d0aa/datamodel_code_generator-0.72.3-py3-none-any.whl", hash = "sha256:4536f6dd12dd86c9c7a78b16ef83ae9aa7a9cbdb8bef8a3b63baa4ad9d3d3829", size = 554226, upload-time = "2026-08-10T18:58:40.087Z" }, ] [[package]] From b15752e8ab386340b126533330939d7db152b4c5 Mon Sep 17 00:00:00 2001 From: yanz571 Date: Thu, 13 Aug 2026 11:18:45 -0700 Subject: [PATCH 18/23] silent pydantic model warning, clenup examples --- comcheck_api/types/custom_base_model.py | 8 ++- examples/client/assemblies.py | 2 +- examples/client/compliance_and_report.py | 2 +- examples/client/simulation.py | 2 +- examples/client/user_functions.py | 4 +- .../building_area_operations.py | 2 +- .../project_operations/envelope_operations.py | 2 +- .../exterior_lighting_operations.py | 51 +++-------------- .../interior_lighting_operations.py | 56 ++----------------- 9 files changed, 28 insertions(+), 101 deletions(-) diff --git a/comcheck_api/types/custom_base_model.py b/comcheck_api/types/custom_base_model.py index c75e461..09e84da 100644 --- a/comcheck_api/types/custom_base_model.py +++ b/comcheck_api/types/custom_base_model.py @@ -4,7 +4,7 @@ from typing import Any, Optional, TypeVar from pydantic.main import _model_construction -from pydantic import BaseModel +from pydantic import BaseModel, model_serializer from comcheck_api.managers.data_manager import DataManager try: @@ -30,6 +30,12 @@ class CustomBaseModel(BaseModel): _identifier: str = "id" + @model_serializer(mode="plain") + def _skip_missing_fields(self): + if _PYDANTIC_MISSING is None: + return self.__dict__ + return {k: v for k, v in self.__dict__.items() if v is not _PYDANTIC_MISSING} + def __deepcopy__(self, memo=None): # MISSING (Sentinel) is not picklable, so copy field-by-field, passing it through as-is. if memo is None: diff --git a/examples/client/assemblies.py b/examples/client/assemblies.py index ba8f07c..9d0538d 100644 --- a/examples/client/assemblies.py +++ b/examples/client/assemblies.py @@ -18,7 +18,7 @@ from comcheck_api.types.core_types import EnergyCodeOptions # Initialize client -load_dotenv() +load_dotenv(override=True) client = COMcheckClient() api_key = os.getenv("COM_API_KEY") or "your-api-key-here" client.set_api_key(api_key) diff --git a/examples/client/compliance_and_report.py b/examples/client/compliance_and_report.py index 59fb05f..431974a 100644 --- a/examples/client/compliance_and_report.py +++ b/examples/client/compliance_and_report.py @@ -28,7 +28,7 @@ from comcheck_api.types import SimulationStatus # Initialize client -load_dotenv() +load_dotenv(override=True) client = COMcheckClient() api_key = os.getenv("COM_API_KEY") or "your-api-key-here" client.set_api_key(api_key) diff --git a/examples/client/simulation.py b/examples/client/simulation.py index 5631307..860ffd0 100644 --- a/examples/client/simulation.py +++ b/examples/client/simulation.py @@ -7,7 +7,7 @@ from comcheck_api.types.core_types import EnergyCodeOptions # Initialize client -load_dotenv() +load_dotenv(override=True) client = COMcheckClient() api_key = os.getenv("COM_API_KEY") or "your-api-key-here" client.set_api_key(api_key) diff --git a/examples/client/user_functions.py b/examples/client/user_functions.py index 355658f..26f3ed8 100644 --- a/examples/client/user_functions.py +++ b/examples/client/user_functions.py @@ -6,7 +6,7 @@ from comcheck_api.defaults import get_default_project_template # Initialize client -load_dotenv() +load_dotenv(override=True) client = COMcheckClient() api_key = os.getenv("COM_API_KEY") or "your-api-key-here" client.set_api_key(api_key) @@ -26,7 +26,7 @@ project_id = projects[0]["_id"] project = client.get_project(project_id) print(f"\nProject {project_id} details:") - print(f"Name: {getattr(project, 'projectName', 'N/A')}") + print(f"Project Title: {getattr(project.project, 'projectTitle', 'N/A')}") print(f"Type: {getattr(project, 'projectType', 'N/A')}") # Example 4: Update a project with default template diff --git a/examples/project_operations/building_area_operations.py b/examples/project_operations/building_area_operations.py index 6397a20..9cb2d28 100644 --- a/examples/project_operations/building_area_operations.py +++ b/examples/project_operations/building_area_operations.py @@ -9,7 +9,7 @@ from comcheck_api.utilities.common import export_to_json # Initialize client -load_dotenv() +load_dotenv(override=True) client = COMcheckClient() api_key = os.getenv("COM_API_KEY") or "your-api-key-here" client.set_api_key(api_key) diff --git a/examples/project_operations/envelope_operations.py b/examples/project_operations/envelope_operations.py index b4eafb4..dd5af44 100644 --- a/examples/project_operations/envelope_operations.py +++ b/examples/project_operations/envelope_operations.py @@ -12,7 +12,7 @@ from comcheck_api.types.core_types import Roof, ThermalBridgeTypeOptions # Initialize client -load_dotenv() +load_dotenv(override=True) api_key = os.getenv("COM_API_KEY") if not api_key: raise ValueError("COM_API_KEY environment variable is not set") diff --git a/examples/project_operations/exterior_lighting_operations.py b/examples/project_operations/exterior_lighting_operations.py index 46fce6f..09d4626 100644 --- a/examples/project_operations/exterior_lighting_operations.py +++ b/examples/project_operations/exterior_lighting_operations.py @@ -39,61 +39,26 @@ LightingTypeOptions, ) -load_dotenv() +load_dotenv(override=True) client = COMcheckClient() client.set_api_key(os.getenv("COM_API_KEY") or "your-api-key-here") - -def normalize_numeric_nulls(model): - """Default every null numeric field on a model (recursively) to 0. - - The API declares many numeric fields non-nullable but still returns null - for them, then rejects those nulls on write. Rather than patch fields one - at a time, sweep the whole model tree and set any None-valued int/float - field to 0 (integers get 0, floats get 0.0 via Pydantic coercion). - - Because every ``update_project`` returns a freshly-fetched project (which - brings the server's nulls back), call this before *each* update, not just - once after the initial fetch. - - TODO: schema fix — these fields are typed number/integer but should allow null. - """ - from pydantic import BaseModel - - for name, field in type(model).model_fields.items(): - value = getattr(model, name, None) - annotation = str(field.annotation) - if value is None: - # Only purely-numeric fields (no str/enum in the union) — this - # leaves id-like fields (e.g. "str | int | None") untouched. - is_numeric = "int" in annotation or "float" in annotation - if is_numeric and "str" not in annotation: - setattr(model, name, 0) - elif isinstance(value, BaseModel): - normalize_numeric_nulls(value) - elif isinstance(value, list): - for item in value: - if isinstance(item, BaseModel): - normalize_numeric_nulls(item) - return model - - # Fetch an existing project so changes can be saved back to the account. # (update_project persists to the server; it requires a project that already # exists there, so we start from a fetched project rather than a local # template.) -project = client.get_project("43789") +project = client.get_project("your-project-id") if not project: raise ValueError("Project not found") project_id = str(project.id) -normalize_numeric_nulls(project) + # ── Step 1: Set the exterior lighting zone type ─────────────────────────────── # Must be set to a real zone before exterior compliance can be evaluated. project = el_ops.set_exterior_lighting_zone_type_in_project( project, ExteriorLightingZoneTypeOptions.EXT_ZONE_NEIGHBORHOOD_BUS_DISTRICT ) -normalize_numeric_nulls(project) + project = client.update_project(project_id, project) if not project: raise ValueError("Project not found after update") @@ -116,7 +81,7 @@ def normalize_numeric_nulls(model): ) project = el_ops.add_exterior_lighting_area_to_project(project, exterior_use) -normalize_numeric_nulls(project) + project = client.update_project(project_id, project) if not project: raise ValueError("Project not found after update") @@ -132,7 +97,7 @@ def normalize_numeric_nulls(model): "Main Parking Area", {"useQuantity": 6000.0}, ) -normalize_numeric_nulls(project) + project = client.update_project(project_id, project) if not project: raise ValueError("Project not found after update") @@ -168,7 +133,7 @@ def normalize_numeric_nulls(model): ) }, ) -normalize_numeric_nulls(project) + project = client.update_project(project_id, project) if not project: raise ValueError("Project not found after update") @@ -178,7 +143,7 @@ def normalize_numeric_nulls(model): project = el_ops.remove_exterior_lighting_area_from_project( project, "Main Parking Area" ) -normalize_numeric_nulls(project) + project = client.update_project(project_id, project) if not project: raise ValueError("Project not found after update") diff --git a/examples/project_operations/interior_lighting_operations.py b/examples/project_operations/interior_lighting_operations.py index 8385292..d682dee 100644 --- a/examples/project_operations/interior_lighting_operations.py +++ b/examples/project_operations/interior_lighting_operations.py @@ -19,13 +19,6 @@ project_interior_lighting_operations as il_ops, ) -# The library logs API failures via logging.getLogger(__name__) but never -# configures a handler (as a library shouldn't). Configure logging here so -# those error logs — including the server's response body — are visible. -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s %(name)s %(levelname)s %(message)s", -) from comcheck_api.defaults import ( get_default_interior_lighting_space_template, get_default_building_area_template, @@ -34,56 +27,19 @@ from comcheck_api.types.core_types import ActivityTypeOptions, LightingTypeOptions from comcheck_api.utilities.common import export_to_json -load_dotenv() +load_dotenv(override=True) client = COMcheckClient() client.set_api_key(os.getenv("COM_API_KEY") or "your-api-key-here") -# TODO: building area description should be unique within a project, interior lighting spcaes seems using the descriptions as key, instead of the key field. - - -def normalize_numeric_nulls(model): - """Default every null numeric field on a model (recursively) to 0. - - The API declares many numeric fields non-nullable but still returns null - for them, then rejects those nulls on write. Rather than patch fields one - at a time, sweep the whole model tree and set any None-valued int/float - field to 0 (integers get 0, floats get 0.0 via Pydantic coercion). - - Because every ``update_project`` returns a freshly-fetched project (which - brings the server's nulls back), call this before *each* update, not just - once after the initial fetch. - - TODO: schema fix — these fields are typed number/integer but should allow null. - """ - from pydantic import BaseModel - - for name, field in type(model).model_fields.items(): - value = getattr(model, name, None) - annotation = str(field.annotation) - if value is None: - # Only purely-numeric fields (no str/enum in the union) — this - # leaves id-like fields (e.g. "str | int | None") untouched. - is_numeric = "int" in annotation or "float" in annotation - if is_numeric and "str" not in annotation: - setattr(model, name, 0) - elif isinstance(value, BaseModel): - normalize_numeric_nulls(value) - elif isinstance(value, list): - for item in value: - if isinstance(item, BaseModel): - normalize_numeric_nulls(item) - return model - # Fetch an existing project so changes can be saved back to the account. # (update_project persists to the server; it requires a project that already # exists there, so we start from a fetched project rather than a local # template.) -project = client.get_project("43789") +project = client.get_project("your-project-id") if not project: raise ValueError("Project not found") project_id = str(project.id) export_to_json(project, "interior_lighting_operations_before.json") -normalize_numeric_nulls(project) # ── Step 1: A building area must exist before adding activity uses ──────────── @@ -94,7 +50,7 @@ def normalize_numeric_nulls(model): export_to_json(project, "interior_lighting_operations_after_add_building_area.json") print("exported") # Persist the new building area to the account. -normalize_numeric_nulls(project) + project = client.update_project(project_id, project) if not project: raise ValueError("Project not found after update") @@ -112,7 +68,7 @@ def normalize_numeric_nulls(model): activity_use = get_default_interior_lighting_space_template() activity_use.areaDescription = "Open Office" # Todo: check if activityType options are based on energy code, or if they are just generic options. -activity_use.activityType = ActivityTypeOptions.ACTIVITY_COMMON_OFFICE +activity_use.activityType = ActivityTypeOptions.ACTIVITY_COMMON_OFFICE_OPEN activity_use.floorArea = 2000.0 activity_use.interiorLightingSpace = activity_use.interiorLightingSpace.model_copy( deep=True, update={"fixture": [fixture]} @@ -120,7 +76,7 @@ def normalize_numeric_nulls(model): project = il_ops.add_interior_lighting_space_to_project(project, area_key, activity_use) export_to_json(project, "interior_lighting_operations_after_add.json") -normalize_numeric_nulls(project) + project = client.update_project(project_id, project) if not project: raise ValueError("Project not found after update") @@ -137,7 +93,7 @@ def normalize_numeric_nulls(model): "Open Office", {"floorArea": 2500.0}, ) -normalize_numeric_nulls(project) + project = client.update_project(project_id, project) if not project: raise ValueError("Project not found after update") From 30130b6369b5227dfa10f4e62811f3afddce868c Mon Sep 17 00:00:00 2001 From: yanz571 Date: Thu, 13 Aug 2026 11:20:45 -0700 Subject: [PATCH 19/23] clean up scratch --- {scratch => docs}/schema_changes_notes.md | 0 scratch/compare_buildings.py | 560 ---------------------- scratch/diff_ignore.txt | 14 - scratch/schema_ignore.txt | 24 - 4 files changed, 598 deletions(-) rename {scratch => docs}/schema_changes_notes.md (100%) delete mode 100644 scratch/compare_buildings.py delete mode 100644 scratch/diff_ignore.txt delete mode 100644 scratch/schema_ignore.txt diff --git a/scratch/schema_changes_notes.md b/docs/schema_changes_notes.md similarity index 100% rename from scratch/schema_changes_notes.md rename to docs/schema_changes_notes.md diff --git a/scratch/compare_buildings.py b/scratch/compare_buildings.py deleted file mode 100644 index 8a50218..0000000 --- a/scratch/compare_buildings.py +++ /dev/null @@ -1,560 +0,0 @@ -"""Compare building JSON exports against their Python (ComBuilding) round-trip. - -The JSON -> ComBuilding -> JSON round-trip introduces a set of *known* / -expected differences (defaulted fields, dropped metadata like ``userProject``, -etc.). Those live in ``diff_ignore.json`` and are filtered out so that only -*new* discrepancies are surfaced when comparing additional buildings. - -Usage ------ -Seed / update the ignore list from an existing diff file:: - - python compare_buildings.py --update-ignore building_diff.json - -Compare buildings (defaults to every ``*.json`` in ``buildings/`` if present, -otherwise ``building_json.json``):: - - python compare_buildings.py - python compare_buildings.py path/to/one_building.json another.json - -Get an actionable list of schema fixes for buildings that fail to validate:: - - python compare_buildings.py --report - -Two independent ignore lists (plain text, ``#`` for notes): - - ``diff_ignore.txt`` -- round-trip diff paths (used by the default mode) - - ``schema_ignore.txt`` -- validation failures to skip in ``--report`` -""" - -import argparse -import glob -import json -import os -import re -import sys -from collections import defaultdict -from typing import Any, Dict, List, Set, Tuple - -from jsondiff import diff - -from tools.generate_core_types import main as generate_core_types - -IGNORE_FILE = "diff_ignore.txt" -# Separate ignore list for the --report (schema validation) mode. These are -# validation failures you've decided not to act on, kept apart from the -# round-trip diff ignore list since the two mean different things. -SCHEMA_IGNORE_FILE = "schema_ignore.txt" -BUILDINGS_GLOB = "buildings/*.json" -DEFAULT_BUILDING = "building_json.json" - -# jsondiff (symmetric, marshalled) operator keys. -_INSERT_DELETE = ("$insert", "$delete") - - -def normalize_path(parts: Tuple[str, ...]) -> str: - """Join a path, collapsing numeric array indices to ``[]``. - - Array positions vary between buildings, so an ignored discrepancy at - ``hvac.hvacSystem.0.fanSystem`` should also match index ``1``, ``2``, ... - """ - return ".".join("[]" if p.isdigit() else p for p in parts) - - -def walk_diff(d: Any, prefix: Tuple[str, ...] = ()) -> List[Tuple[str, str, Any]]: - """Flatten a jsondiff (symmetric, marshalled) result into leaf findings. - - Returns a list of ``(normalized_path, op, value)`` tuples where ``op`` is - one of ``insert``, ``delete``, ``replace`` or ``change``. - """ - findings: List[Tuple[str, str, Any]] = [] - - if isinstance(d, dict): - for key, value in d.items(): - if key in _INSERT_DELETE: - op = key.lstrip("$") # "insert" / "delete" - if isinstance(value, dict): - # Object keys added/removed. - for subkey, subval in value.items(): - path = prefix + (str(subkey),) - findings.append((normalize_path(path), op, subval)) - elif isinstance(value, list): - # Array elements added/removed. - path = prefix + ("[]",) - for item in value: - findings.append((normalize_path(path), op, item)) - else: - findings.append((normalize_path(prefix), op, value)) - elif key == "$replace": - findings.append((normalize_path(prefix), "replace", value)) - elif isinstance(key, str) and key.startswith("$"): - # Any other operator ($update, etc.) -> recurse without - # extending the path. - findings.extend(walk_diff(value, prefix)) - else: - findings.extend(walk_diff(value, prefix + (str(key),))) - else: - findings.append((normalize_path(prefix), "change", d)) - - return findings - - -def load_ignore(ignore_file: str = IGNORE_FILE) -> Set[str]: - """Load the set of normalized paths to ignore from ``ignore_file``. - - The ignore file is plain text: one path per line. Blank lines and anything - after a ``#`` are treated as comments, so you can annotate entries inline - (e.g. ``userProject # dropped on purpose, not part of ComBuilding``). - """ - if not os.path.exists(ignore_file): - return set() - paths: Set[str] = set() - with open(ignore_file) as f: - for line in f: - entry = line.split("#", 1)[0].strip() - if entry: - paths.add(entry) - return paths - - -def save_ignore(paths: Set[str]) -> None: - """Append new paths to the ignore file, preserving existing notes/comments. - - Existing lines (including comments) are kept verbatim; only paths not - already present are appended, so hand-written notes are never clobbered. - """ - existing = load_ignore() - new = sorted(p for p in paths if p not in existing) - if not new: - return - header_needed = not os.path.exists(IGNORE_FILE) - with open(IGNORE_FILE, "a") as f: - if header_needed: - f.write("# Diff paths to ignore (one per line). " - "Use '#' for inline notes.\n") - for p in new: - f.write(f"{p}\n") - - -def is_ignored(path: str, ignore: Set[str]) -> bool: - """A path is ignored if it matches an ignore entry. - - An entry matches when it (a) equals the path, (b) is a prefix of it (so an - ignored subtree covers its descendants), or (c) is a ``*.suffix`` wildcard - that matches the trailing segment(s) at any depth -- e.g. ``*.listPosition`` - ignores ``listPosition`` wherever it appears. - """ - for ig in ignore: - if ig.startswith("*."): - suffix = ig[1:] # ".listPosition" - if path == ig[2:] or path.endswith(suffix): - return True - elif path == ig or path.startswith(ig + "."): - return True - return False - - -def diff_building(building_json: Dict[str, Any]) -> Any: - """Round-trip a building through ComBuilding and diff it against the raw JSON.""" - # Imported lazily: core_types is (re)generated by generate_core_types(). - from comcheck_api.types.core_types import ComBuilding - - building_python = ComBuilding(**building_json).model_dump(mode="json") - return diff(building_python, building_json, marshal=True, syntax="symmetric") - - -def update_ignore(diff_files: List[str]) -> None: - """Extend the ignore list with every leaf path found in ``diff_files``.""" - ignore = load_ignore() - added: Set[str] = set() - for path in diff_files: - with open(path) as f: - d = json.load(f) - for norm_path, _op, _value in walk_diff(d): - if norm_path and norm_path not in ignore: - added.add(norm_path) - ignore |= added - save_ignore(ignore) - print(f"Ignore list now has {len(ignore)} paths ({len(added)} added).") - for path in sorted(added): - print(f" + {path}") - - -def compare(building_files: List[str]) -> int: - """Compare each building JSON against its round-trip; report un-ignored diffs. - - Returns the total number of flagged (non-ignored) discrepancies. - """ - generate_core_types() - from pydantic import ValidationError - - ignore = load_ignore() - schema_ignore = load_ignore(SCHEMA_IGNORE_FILE) - # Definition names leak into Pydantic union-error paths; strip them. - global _DEF_NAMES - _DEF_NAMES = set(_load_schema().get("definitions", {})) - total_flagged = 0 - - errored = 0 - for path in building_files: - with open(path) as f: - building_json = json.load(f) - - try: - raw_diff = diff_building(building_json) - except ValidationError as exc: - errored += 1 - total_flagged += 1 - print(f"\n=== {path} ===") - # List the validation errors, noting any suppressed via the - # schema ignore list rather than hiding them silently. - shown, ignored = [], 0 - for err in exc.errors(): - _kind, epath, detail, _value = classify_error(err) - if is_ignored(epath, schema_ignore): - ignored += 1 - else: - shown.append((epath, detail)) - print(f" ! failed to validate as ComBuilding: " - f"{len(shown)} error(s), {ignored} ignored") - for epath, detail in shown: - print(f" - {epath}: {detail}") - continue - except Exception as exc: # non-validation failure - errored += 1 - total_flagged += 1 - print(f"\n=== {path} ===") - print(f" ! failed to round-trip through ComBuilding: " - f"{type(exc).__name__}") - first_line = str(exc).splitlines()[0] if str(exc) else "" - if first_line: - print(f" {first_line}") - continue - - findings = walk_diff(raw_diff) - flagged = [f for f in findings if not is_ignored(f[0], ignore)] - - ignored_count = len(findings) - len(flagged) - total_flagged += len(flagged) - - print(f"\n=== {path} ===") - print(f" {len(flagged)} flagged, {ignored_count} ignored") - for norm_path, op, value in flagged: - print(f" [{op}] {norm_path} = {json.dumps(value, default=str)}") - - print(f"\nTotal flagged across {len(building_files)} building(s): " - f"{total_flagged} ({errored} failed to round-trip)") - return total_flagged - - -# --- Schema-fix report ------------------------------------------------------ - -# datamodel-codegen tags union branches in error locations, e.g. -# "fanEfficiencyExceptionType.str-enum[FanEfficiencyExceptionTypeOptions]" or -# "cavityRValue.float". These aren't real data keys, so we strip them when -# building a clean dotted path, but we mine them for the enum/type name. -_BRANCH_TAG = re.compile(r"^(str-enum|int-enum|enum)\[(?P[^\]]+)\]$") -# Pydantic union-branch tags that are not real data keys: plain type names, -# ``list[Fan]`` / ``dict[...]`` shapes, etc. -_TYPE_BRANCH = {"str", "int", "float", "bool", "constrained-str", "list", "dict"} -_SHAPE_TAG = re.compile(r"^(list|dict|tuple)\[.*\]$") - - -# Populated by report() from the schema's definition names; these leak into -# Pydantic union-error locations (e.g. the "HVAC" in "hvac.HVAC.fanSystem"). -_DEF_NAMES: Set[str] = set() - - -def _is_branch_tag(s: str) -> bool: - return ( - s == "missing-sentinel" - or _BRANCH_TAG.match(s) is not None - or _SHAPE_TAG.match(s) is not None - or s in _TYPE_BRANCH - or s in _DEF_NAMES - ) - - -def _clean_loc(loc: Tuple[Any, ...]) -> str: - """Turn a Pydantic error location into a normalized dotted path. - - Array indices collapse to ``[]`` and pydantic union-branch tags - (``str-enum[...]``, ``list[Fan]``, ``.float``, ``missing-sentinel``) are - dropped so the path reads as real data keys. - """ - parts: List[str] = [] - for p in loc: - if isinstance(p, int): - parts.append("[]") - continue - s = str(p) - if _is_branch_tag(s): - continue - parts.append(s) - return ".".join(parts) - - -SCHEMA_FILE = os.path.join("comcheck_api", "schemas", "comCheck.schema.json") - - -def _load_schema() -> Dict[str, Any]: - with open(SCHEMA_FILE) as f: - return json.load(f) - - -def _deref(node: Dict[str, Any], schema: Dict[str, Any]) -> Tuple[Dict[str, Any], str]: - """Follow a ``$ref`` (if present) and return (target_node, definition_name).""" - ref = node.get("$ref") - if not ref: - return node, "" - name = ref.split("/")[-1] - return schema.get("definitions", {}).get(name, {}), name - - -def resolve_schema_target( - dotted_path: str, schema: Dict[str, Any] -) -> Tuple[str, str]: - """Walk the schema along a cleaned data path. - - Returns ``(location, enum_def_name)`` where ``location`` is a string like - ``definitions/AgWall -> properties/wallType`` pinpointing the node to edit, - and ``enum_def_name`` is the ``*Options`` definition backing the field if - it is an enum reference ("" for inline enums / non-enums). Returns - ``("", "")`` when the path can't be resolved against the schema. - """ - node = schema.get("definitions", {}).get("ComBuilding", {}) - def_name = "ComBuilding" # the enclosing definition - prop_seg = "" # the final property key within that definition - enum_def = "" - - for seg in dotted_path.split("."): - if seg == "[]": - items = node.get("items") or node.get("item") or {} - node, ref_name = _deref(items, schema) - if ref_name: - def_name, prop_seg = ref_name, "" - continue - props = node.get("properties", {}) - if seg not in props: - return ("", "") # path diverges (unknown / extra field) - raw = props[seg] - # Capture the enum def name from a $ref before dereferencing. - ref = raw.get("$ref", "") - target, ref_name = _deref(raw, schema) - if ref_name and target.get("enum") is not None: - enum_def = ref_name - elif ref_name: - # Non-enum sub-object: descend into it as the new enclosing def. - def_name, prop_seg = ref_name, "" - node = target - continue - node = target - prop_seg = seg - - loc = f"definitions/{def_name}" - if prop_seg: - loc += f" -> properties/{prop_seg}" - return (loc, enum_def) - - -def classify_error(err: Dict[str, Any]) -> Tuple[str, str, str, Any]: - """Map one Pydantic error to (fix_kind, path, detail, offending_value). - - ``fix_kind`` is one of: - - ``enum-missing-value`` : add the value to the enum ``*Options`` def - - ``needs-null`` : field arrives as null but schema forbids it - - ``constraint-too-strict`` : a min/max/etc. constraint rejects real data - - ``other`` : anything not auto-classified - """ - etype = err["type"] - path = _clean_loc(err["loc"]) - value = err.get("input") - - # Which enum definition is implicated (from the branch tag), if any. - enum_name = "" - for p in err["loc"]: - m = _BRANCH_TAG.match(str(p)) - if m: - enum_name = m.group("name") - break - - if etype == "enum": - if value is None: - return ("needs-null", path, "enum should allow null", value) - target = enum_name or "" - return ("enum-missing-value", path, - f"add {value!r} to enum '{target}'", value) - - if etype in ("string_type", "int_type", "float_type", "bool_type", - "int_parsing", "float_parsing") and value is None: - return ("needs-null", path, "field arrives as null", value) - - if etype == "missing_sentinel_error": - # Secondary branch of a union; the real story is told by the sibling - # enum/type error. Mark as such so we can dedupe it away. - return ("secondary", path, "union branch (see sibling error)", value) - - if etype in ("greater_than", "greater_than_equal", "less_than", - "less_than_equal", "multiple_of"): - ctx = err.get("ctx", {}) - return ("constraint-too-strict", path, - f"{etype} {ctx} rejects value {value!r}", value) - - return ("other", path, f"{etype}: {err.get('msg', '')}", value) - - -def report(building_files: List[str]) -> int: - """Collect round-trip validation failures and print actionable schema fixes. - - Returns the number of distinct issues found. - """ - generate_core_types() - from comcheck_api.types.core_types import ComBuilding - from pydantic import ValidationError - - # Pydantic injects the model class name as a path segment in union errors - # (e.g. "hvac.HVAC.fanSystem..."). Those definition names aren't data keys, - # so strip them before cleaning paths. - global _DEF_NAMES - _DEF_NAMES = set(_load_schema().get("definitions", {})) - - # Validation failures you've decided not to act on, kept in a file separate - # from the round-trip diff ignore list. - schema_ignore = load_ignore(SCHEMA_IGNORE_FILE) - - # fix_kind -> (path, detail) -> {values seen, files affected} - issues: Dict[str, Dict[Tuple[str, str], Dict[str, set]]] = defaultdict( - lambda: defaultdict(lambda: {"values": set(), "files": set()}) - ) - failed_files: Set[str] = set() - ignored_count = 0 - - for path in building_files: - with open(path) as f: - building_json = json.load(f) - try: - ComBuilding(**building_json) - except ValidationError as exc: - fname = os.path.basename(path) - for err in exc.errors(): - kind, epath, detail, value = classify_error(err) - if is_ignored(epath, schema_ignore): - ignored_count += 1 - continue - failed_files.add(path) - bucket = issues[kind][(epath, detail)] - if value is not None: - bucket["values"].add(repr(value)[:60]) - bucket["files"].add(fname) - - # Drop "secondary" union-branch noise where a real sibling error exists for - # the same path (keeps the report focused on the actual fix). - real_paths = { - epath - for kind in ("enum-missing-value", "needs-null", "constraint-too-strict") - for (epath, _detail) in issues.get(kind, {}) - } - for (epath, detail) in list(issues.get("secondary", {})): - if epath in real_paths: - del issues["secondary"][(epath, detail)] - - # --- render --- - schema = _load_schema() - print(f"\n{'=' * 70}") - print(f"SCHEMA FIX REPORT ({len(failed_files)}/{len(building_files)} " - f"buildings failed to validate)") - print(f"file: {SCHEMA_FILE} (regenerate types after editing)") - if ignored_count: - print(f"({ignored_count} error(s) suppressed via {SCHEMA_IGNORE_FILE})") - print(f"{'=' * 70}") - - order = [ - ("enum-missing-value", "1. MISSING ENUM VALUES (add to the *Options enum)"), - ("needs-null", "2. FIELDS THAT MUST ALLOW null (use [\"\", \"null\"])"), - ("constraint-too-strict", "3. CONSTRAINTS TOO STRICT (relax min/max)"), - ("other", "4. OTHER (needs manual review)"), - ("secondary", "5. UNRESOLVED union-branch errors (no sibling fix found)"), - ] - - total_issues = 0 - for kind, header in order: - entries = issues.get(kind, {}) - if not entries: - continue - print(f"\n{header}") - print("-" * 70) - for (epath, detail) in sorted(entries): - info = entries[(epath, detail)] - total_issues += 1 - vals = ", ".join(sorted(info["values"])[:6]) if info["values"] else "" - nfiles = len(info["files"]) - loc, enum_def = resolve_schema_target(epath, schema) - # Prefer the resolved enum definition name over the branch-tag guess. - if kind == "enum-missing-value" and enum_def: - detail = detail.replace("''", f"'{enum_def}'") - print(f" • {epath}") - print(f" fix: {detail}") - if loc: - where = loc - if kind == "enum-missing-value" and enum_def: - where += f" -> definitions/{enum_def}/enum" - print(f" where: {where}") - else: - print(" where: ") - if vals: - print(f" values seen: {vals}") - print(f" seen in {nfiles} file(s)") - - print(f"\n{'=' * 70}") - print(f"{total_issues} distinct issue(s) to fix across " - f"{len(failed_files)} failing building(s).") - print(f"{'=' * 70}") - return total_issues - - -def resolve_building_files(args_files: List[str]) -> List[str]: - if args_files: - return args_files - globbed = sorted(glob.glob(BUILDINGS_GLOB)) - if globbed: - return globbed - return [DEFAULT_BUILDING] - - -def main() -> None: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument( - "--update-ignore", - nargs="+", - metavar="DIFF_JSON", - help="Add every path in the given diff file(s) to the ignore list.", - ) - parser.add_argument( - "--report", - action="store_true", - help="Instead of diffing, classify round-trip validation failures " - "into actionable schema fixes (missing enum values, nullable fields, " - "over-strict constraints).", - ) - parser.add_argument( - "files", - nargs="*", - help="Building JSON export files to compare " - f"(default: {BUILDINGS_GLOB} or {DEFAULT_BUILDING}).", - ) - args = parser.parse_args() - - if args.update_ignore: - update_ignore(args.update_ignore) - return - - if args.report: - issues = report(resolve_building_files(args.files)) - sys.exit(1 if issues else 0) - - flagged = compare(resolve_building_files(args.files)) - sys.exit(1 if flagged else 0) - - -if __name__ == "__main__": - main() diff --git a/scratch/diff_ignore.txt b/scratch/diff_ignore.txt deleted file mode 100644 index 364d632..0000000 --- a/scratch/diff_ignore.txt +++ /dev/null @@ -1,14 +0,0 @@ -# Diff paths to ignore (one per line). Use '#' for inline notes. -# -# A path matches if it equals an entry, is nested under one, or matches a -# '*.suffix' wildcard at any depth. Array indices are normalized to '[]'. - -envelope.roof.[].highAlbedoRoofType # Don't think this is needed -hvac.hvacSystem.[].fanSystem # Not sure where this and the following come from - not in sample.json -hvac.hvacSystem.[].fanSystemId -hvac.hvacSystem.[].fanSystemObjNum -hvac.fanSystem.[].number # not modeled -lighting.activityUse # empty list added on round-trip -userProject # API metadata, not part of ComBuilding -*.listPosition # stripped everywhere -hvac.hvacPlant.[].fanSystem # Should be stripped diff --git a/scratch/schema_ignore.txt b/scratch/schema_ignore.txt deleted file mode 100644 index 0621f50..0000000 --- a/scratch/schema_ignore.txt +++ /dev/null @@ -1,24 +0,0 @@ -# Schema-validation failures to ignore in `compare_buildings.py --report`. -# One path per line; blank lines and text after '#' are notes. -# -# Matching is the same as the diff ignore list: exact, prefix (covers a whole -# subtree), or '*.suffix' wildcard at any depth. Array indices normalize to '[]'. -# -# Use this for validation errors you've decided NOT to fix in the schema -# (bad/legacy data, deprecated fields, etc.) so they stop cluttering the report. -# Keep this separate from diff_ignore.txt, which is for round-trip diffs. - -*.constructionType # odd/legacy values (e.g. "2","5"); deprecated field -envelope.floor.floorType # ['OTHER_BG_WALL', 'OTHER_DOOR', 'OTHER_FRAME'] to enum 'FloorTypeOptions' -envelope.roof.roofType # add 'OTHER_FLOOR' to enum -lighting.wholeBldgUse.activityUse.interiorLightingSpace.fixture.advControlsAllowanceType # ['TEST'] to enum 'AdvancedControlsAllowanceTypeOptions' -*.continuousRValue # Negative values -*.propShgc # Negative values -*.propProjectionFactor # Negative values -*.cavityRValue # Negative values -envelope.roof.[].roofType # OTHER_DOOR -envelope.floor.[].floorType # OTHER_FRAME -envelope.agWall.[].door.[].grossArea # -1 -envelope.altPctGlazingAreaReplaced # -1 -envelope.postAltWindowWallPct # -1, -2 -hvac.fanSystem.[].fan.[].fanDesignEfficiency # 900 \ No newline at end of file From 1c4509e00ed4cf76e95905b134132cb69162b077 Mon Sep 17 00:00:00 2001 From: yanz571 Date: Thu, 13 Aug 2026 15:23:16 -0700 Subject: [PATCH 20/23] building area description should be unique --- comcheck_api/defaults.py | 4 +- .../project_building_area_operations.py | 44 +- comcheck_api/utilities/project_utilities.py | 29 + docs/missing-vs-exclude-unset.md | 61 -- docs/schema-changes.md | 861 ---------------- docs/schema-compatibility.md | 168 ---- docs/schema.md | 944 ++++++++++++++++++ docs/schema_changes_notes.md | 58 -- 8 files changed, 1013 insertions(+), 1156 deletions(-) delete mode 100644 docs/missing-vs-exclude-unset.md delete mode 100644 docs/schema-changes.md delete mode 100644 docs/schema-compatibility.md create mode 100644 docs/schema.md delete mode 100644 docs/schema_changes_notes.md diff --git a/comcheck_api/defaults.py b/comcheck_api/defaults.py index 4229c73..ce1933d 100644 --- a/comcheck_api/defaults.py +++ b/comcheck_api/defaults.py @@ -50,7 +50,9 @@ def get_default_building_area_template(): A new ``WholeBldgUse`` instance. """ area = copy.deepcopy(DEFAULT_BUILDING_AREA) - area.key = str(uuid4()) + uid = str(uuid4()) + area.key = uid + area.areaDescription = f"{DEFAULT_BUILDING_AREA.areaDescription} {uid[:8]}" return area diff --git a/comcheck_api/project_operations/project_building_area_operations.py b/comcheck_api/project_operations/project_building_area_operations.py index 26fe2db..68271cb 100644 --- a/comcheck_api/project_operations/project_building_area_operations.py +++ b/comcheck_api/project_operations/project_building_area_operations.py @@ -5,7 +5,10 @@ from comcheck_api.constants.building_area_constants import DEFAULT_BUILDING_AREA from comcheck_api.types.core_types import ComBuilding, WholeBldgUse -from comcheck_api.utilities.project_utilities import _require_building_area +from comcheck_api.utilities.project_utilities import ( + _require_building_area, + _require_unique_area_description, +) def add_building_area_to_project( @@ -22,6 +25,10 @@ def add_building_area_to_project( """ + desc = getattr(new_building_area, "areaDescription", None) + if desc: + _require_unique_area_description(project, desc) + updated_project = project.model_copy(deep=True) # Ensure interiorLightingSpace is initialized @@ -51,14 +58,30 @@ def update_building_area_in_project( """ _require_building_area(project, building_area_key) + new_desc = ( + updates.get("areaDescription") + if isinstance(updates, dict) + else getattr(updates, "areaDescription", None) + ) + if new_desc: + _require_unique_area_description( + project, new_desc, exclude_key=building_area_key + ) + updated_project = project.model_copy(deep=True) - updated_project.lighting.update_subcomponent_list(subcomponent_updates=updates, subcomponent_id=building_area_key, subcomponent_name="wholeBldgUse") + updated_project.lighting.update_subcomponent_list( + subcomponent_updates=updates, + subcomponent_id=building_area_key, + subcomponent_name="wholeBldgUse", + ) return updated_project + def remove_building_area_from_project( - project: ComBuilding, building_area_key: str) -> ComBuilding: + project: ComBuilding, building_area_key: str +) -> ComBuilding: """Remove an existing building area in the project. Args: @@ -73,10 +96,13 @@ def remove_building_area_from_project( updated_project = project.model_copy(deep=True) - updated_project.lighting.remove_from_subcomponent_list(subcomponent_id=building_area_key, subcomponent_name="wholeBldgUse") + updated_project.lighting.remove_from_subcomponent_list( + subcomponent_id=building_area_key, subcomponent_name="wholeBldgUse" + ) return updated_project + def get_building_area_keys_from_project(project: ComBuilding) -> list[dict]: """ Extract valid building area identifiers from a COMcheck project. @@ -101,7 +127,11 @@ def get_building_area_keys_from_project(project: ComBuilding) -> list[dict]: return [] return [ - {"key": getattr(area, "key"), "areaDescription": getattr(area, "areaDescription")} + { + "key": getattr(area, "key"), + "areaDescription": getattr(area, "areaDescription"), + } for area in whole_use - if getattr(area, "key", None) is not None and getattr(area, "areaDescription", None) is not None - ] \ No newline at end of file + if getattr(area, "key", None) is not None + and getattr(area, "areaDescription", None) is not None + ] diff --git a/comcheck_api/utilities/project_utilities.py b/comcheck_api/utilities/project_utilities.py index 8270477..67bf54e 100644 --- a/comcheck_api/utilities/project_utilities.py +++ b/comcheck_api/utilities/project_utilities.py @@ -6,6 +6,35 @@ from comcheck_api.managers.data_manager import DataManager, get_model_info +def _require_unique_area_description( + project: ComBuilding, + area_description: str, + exclude_key: str | float | None = None, +) -> None: + """Raise if area_description already exists in wholeBldgUse (case-sensitive). + + Args: + project: The project to check. + area_description: The description to validate for uniqueness. + exclude_key: Skip the WholeBldgUse item with this key (used during updates + so the item being edited doesn't conflict with itself). + + Raises: + ValueError: If another WholeBldgUse item has the same areaDescription. + """ + whole_use = project.get_by_path("lighting.wholeBldgUse") + if not isinstance(whole_use, list): + return + + for area in whole_use: + if exclude_key is not None and getattr(area, "key", None) == exclude_key: + continue + if getattr(area, "areaDescription", None) == area_description: + raise ValueError( + f"areaDescription '{area_description}' already exists in wholeBldgUse." + ) + + def _require_building_area(project: ComBuilding, building_area_key: str) -> None: """ Ensure that project.lighting.wholeBldgUse exists and contains the given key. diff --git a/docs/missing-vs-exclude-unset.md b/docs/missing-vs-exclude-unset.md deleted file mode 100644 index 200bc66..0000000 --- a/docs/missing-vs-exclude-unset.md +++ /dev/null @@ -1,61 +0,0 @@ -# `MISSING` vs `exclude_unset=True` - -Both mechanisms control which fields are included when serializing a Pydantic model to JSON, but they operate at different layers and serve different purposes. - -## `MISSING` sentinel - -Many fields in `core_types.py` use `MISSING` (from `pydantic.experimental.missing_sentinel`) as their default: - -```python -preAltPropUval: Annotated[float | None | MISSING, Field(ge=0.0)] = MISSING -``` - -This gives a field three distinct states: - -| Value | Meaning | -|---|---| -| `1.5` | Server sent a real value | -| `None` | Server explicitly sent `null` | -| `MISSING` | Server omitted the key entirely | - -On `model_dump(mode='json')`, Pydantic drops `MISSING` fields automatically — they are never included in the output and never sent back to the server. - -## `exclude_unset=True` - -`exclude_unset=True` is a Pydantic dump option that drops any field not explicitly assigned during construction. Pydantic tracks this via `__pydantic_fields_set__` — a set that records which fields the caller actually provided. It operates regardless of what default value a field holds. - -## Where they diverge - -**Fields with real defaults** — `MISSING` only protects fields that explicitly use it as their default. Fields with ordinary defaults (`0`, `""`, `False`, `None`) are still included unless `exclude_unset=True` is used: - -```python -class Foo(BaseModel): - name: str = "default" # real default - count: int | MISSING = MISSING # sentinel default - -f = Foo() # neither field set by the caller - -f.model_dump() # → {"name": "default"} (count dropped, name included) -f.model_dump(exclude_unset=True) # → {} (both dropped) -``` - -**Partial updates** — this is the critical case for `DataManager.update_item` and every outbound API call site. When building a model to describe only the fields you want to change, unset fields hold real defaults (`0`, `False`, etc.) — not `MISSING`. Only `exclude_unset=True` knows the caller never touched them: - -```python -# Only want to change the roof type — everything else should be left alone -update = Roof(roofType=RoofTypeOptions.METAL_ROOF_WITH_THERMAL_BLOCKS) - -update.model_dump(mode="json") # includes propUValue=0, grossArea=0, ... -update.model_dump(mode="json") # → {"roofType": "METAL_ROOF_WITH_THERMAL_BLOCKS"} -``` - -## Summary - -| | `MISSING` default | `exclude_unset=True` | -|---|---|---| -| Mechanism | sentinel value on the field | field-set tracking on the instance | -| Drops server-omitted fields | yes | yes (if server did not provide them) | -| Drops fields with real defaults | no | yes | -| Needed for partial updates | no | yes | - -They complement each other. `MISSING` is for modeling fields the server may legitimately omit (keeping `None` and "absent" distinct). `exclude_unset=True` is for controlling the outbound payload based on what the caller explicitly set — which is why every API write call site in `comcheck_client.py`, `data_manager.py`, and `utilities/common.py` uses it. diff --git a/docs/schema-changes.md b/docs/schema-changes.md deleted file mode 100644 index 5174da3..0000000 --- a/docs/schema-changes.md +++ /dev/null @@ -1,861 +0,0 @@ -# Schema Changelog — comCheck.schema.json - -This document describes every meaningful change introduced in the schema update merged via PR #25. -Changes are grouped by category. Within each category entries are listed by definition and field name. - ---- - -## 1. Field Additions - -### `ComBuilding` - -| Field | Type | Notes | -|---|---|---| -| `bldgUseType` | `$ref BuildingUseTypeOptions` | Legacy alias for `buildingUseType`. Comment: "Legacy enum, only ACTIVITY is valid in the new ComCheck Web." | -| `efficiencyPackageType` | `enum` (string or `null`) | New field. Values: `EFF_PACKAGE_UNKNOWN`, `EFF_PACKAGE_HVAC_PERFORMANCE`, `EFF_PACKAGE_LIGHTING_REDUCED_LPD`, `EFF_PACKAGE_REDUCED_AIR_INFILTRATION`, `EFF_PACKAGE_ENHANCED_ENVELOPE_PERFORMANCE`, `EFF_PACKAGE_ENHANCED_LIGHTING_CONTROLS`, `EFF_PACKAGE_ONSITE_RENEWABLES`, `null`. Default: `null`. | -| `energyCreditMultiplierException` | `enum` (or `null`) | New field. Values: `NO_ENERGY_CREDIT_MULTIPLIER_EXCEPTION`, `ENERGY_CREDIT_MULTIPLIER_EXCEPTION_LOW_ENERGY_BUILDINGS`, `ENERGY_CREDIT_MULTIPLIER_EXCEPTION_PRIMARY_HEAT_PUMP`, `null`. No default declared. | - -### `Door` (fenestration) - -| Field | Type | Notes | -|---|---|---| -| `feetAg` | `["number", "null"]` | New field: feet above grade. `minimum: 0.0`, `default: null`. | - -### `Skylight` - -| Field | Type | Notes | -|---|---|---| -| `cavityRValue` | `["number", "null"]` | New field: average insulation R-value in cavity. Unit: `h-ft2-F/Btu`, `default: 0.0`. | -| `continuousRValue` | `["number", "null"]` | New field: continuous insulation on the skylight. Unit: `h-ft2-F/Btu`, `default: 0.0`. | - -### `HVACSystem` - -| Field | Type | Notes | -|---|---|---| -| `requirementAnswer` | `array` of `$ref Requirements` | New field. `default: []`. | - -### `HVACPlant` - -| Field | Type | Notes | -|---|---|---| -| `requirementAnswer` | `array` of `$ref Requirements` | New field. `default: []`. | - -### `InteriorLightingFixture` - -| Field | Type | Notes | -|---|---|---| -| `scheduleFixtureKey` | `["string", "null"]` | New field: UUID to identify this fixture schedule. | -| `typeOfFixture` | `["string", "null"]` | New field: type of the fixture. | - ---- - -## 2. Removed Fields / Enum Values Removed - -### `ActivityTypeOptions` - -| Removed value | Notes | -|---|---| -| `ACTIVITY_COMMON_OFFICE` | Removed from enum. `ACTIVITY_COMMON_OFFICE_ENCLOSED` and `ACTIVITY_COMMON_OFFICE_OPEN` remain. | - ---- - -## 3. Type Changes - -### `ComBuilding` - -| Field | Old type | New type | Notes | -|---|---|---|---| -| `isHistoricBuilding` | `"boolean"` | `"integer"`, `enum: [0, 1]` | Changed from boolean to integer flag. | -| `isNonresidentialConditioning` | `"boolean"` | `["boolean", "null"]` | Made nullable. | -| `isResidentialConditioning` | `"boolean"` | `["boolean", "null"]` | Made nullable. | -| `isSemiheatedConditioning` | `"boolean"` | `["boolean", "null"]` | Made nullable. | -| `constructionType` | `"string"` | `["string", "null"]` | Made nullable. | - -### `CodeData` - -| Field | Old type | New type | Notes | -|---|---|---|---| -| `version` | `"string"` | `["string", "null"]` | Made nullable. | - -### `AgWall` - -| Field | Old type | New type | Notes | -|---|---|---|---| -| `bldgUseKey` | `"string"` | `["string", "null"]` | Made nullable. | -| `heatCapacity` | `"number"` | `["number", "null"]` | Made nullable. | -| `cavityRValue` | `"number"` | `["number", "null"]` | Made nullable. | -| `continuousRValue` | `"number"` | `["number", "null"]` | Made nullable. | -| `propUValue` | `"number"` | `["number", "null"]` | Made nullable. | -| `grossArea` | `"number"` | `["number", "null"]` | Made nullable. | - -### `BgWall` - -| Field | Old type | New type | Notes | -|---|---|---|---| -| `bldgUseKey` | `"string"` | `["string", "null"]` | Made nullable. | -| `heatCapacity` | `"number"` | `["number", "null"]` | Made nullable. | -| `cavityRValue` | `"number"` | `["number", "null"]` | Made nullable. | -| `continuousRValue` | `"number"` | `["number", "null"]` | Made nullable. | -| `propUValue` | `"number"` | `["number", "null"]` | Made nullable. | - -### `Window` - -| Field | Old type | New type | Notes | -|---|---|---|---| -| `bldgUseKey` | `"string"` | `["string", "null"]` | Made nullable. | -| `propUValue` | `"number"` | `["number", "null"]` | Made nullable. | -| `grossArea` | `"number"` | `["number", "null"]` | Made nullable. | -| `propShgc` | `"number"` | `["number", "null"]` | Made nullable. | -| `preAltPropShgc` | `"number"` | `["number", "null"]` | Made nullable. | -| `cavityRValue` | `"number"` | `["number", "null"]` | Made nullable. | -| `continuousRValue` | `"number"` | `["number", "null"]` | Made nullable. | - -### `Door` - -| Field | Old type | New type | Notes | -|---|---|---|---| -| `bldgUseKey` | `"string"` | `["string", "null"]` | Made nullable. | -| `propUValue` | `"number"` | `["number", "null"]` | Made nullable. | -| `propShgc` | `"number"` | `["number", "null"]` | Made nullable. | -| `preAltPropShgc` | `"number"` | `["number", "null"]` | Made nullable. | -| `cavityRValue` | `"number"` | `["number", "null"]` | Made nullable. | -| `continuousRValue` | `"number"` | `["number", "null"]` | Made nullable. | - -### `Skylight` - -| Field | Old type | New type | Notes | -|---|---|---|---| -| `bldgUseKey` | `"string"` | `["string", "null"]` | Made nullable. | -| `propUValue` | `"number"` | `["number", "null"]` | Made nullable. | -| `grossArea` | `"number"` | `["number", "null"]` | Made nullable. | -| `propShgc` | `"number"` | `["number", "null"]` | Made nullable. | -| `preAltPropShgc` | `"number"` | `["number", "null"]` | Made nullable. | - -### `Roof` - -| Field | Old type | New type | Notes | -|---|---|---|---| -| `bldgUseKey` | `"string"` | `["string", "null"]` | Made nullable. | -| `cavityRValue` | `"number"` | `["number", "null"]` | Made nullable. | -| `continuousRValue` | `"number"` | `["number", "null"]` | Made nullable. | -| `propUValue` | `"number"` | `["number", "null"]` | Made nullable. | -| `grossArea` | `"number"` | `["number", "null"]` | Made nullable. | -| `purlinSpacing` | `"number"` | `["number", "null"]` | Made nullable. | - -### `Floor` - -| Field | Old type | New type | Notes | -|---|---|---|---| -| `bldgUseKey` | `"string"` | `["string", "null"]` | Made nullable. | -| `cavityRValue` | `"number"` | `["number", "null"]` | Made nullable. | -| `continuousRValue` | `"number"` | `["number", "null"]` | Made nullable. | -| `propUValue` | `"number"` | `["number", "null"]` | Made nullable. | -| `grossArea` | `"number"` | `["number", "null"]` | Made nullable. | -| `slabFullInsulBelowMinRValue` | `"number"` | `["number", "null"]` | Made nullable. | - -### `WholeBldgUse` - -| Field | Old type | New type | Notes | -|---|---|---|---| -| `floorArea` | `"number"` | `["number", "null"]` | Made nullable. | -| `powerDensity` | `"number"` | `["number", "null"]` | Made nullable. | -| `internalLoad` | `"number"` | `["number", "null"]` | Made nullable. | -| `allowedWattage` | `"number"` | `["number", "null"]` | Made nullable. | -| `proposedWattage` | `"number"` | `["number", "null"]` | Made nullable. | - -### `ActivityUse` - -| Field | Old type | New type | Notes | -|---|---|---|---| -| `floorArea` | `"number"` | `["number", "null"]` | Made nullable. | -| `ceilingHeight` | `"number"` | `["number", "null"]` | Made nullable. | -| `powerDensity` | `"number"` | `["number", "null"]` | Made nullable. | -| `internalLoad` | `"number"` | `["number", "null"]` | Made nullable. | -| `allowedWattage` | `"number"` | `["number", "null"]` | Made nullable. | -| `proposedWattage` | `"number"` | `["number", "null"]` | Made nullable. | - -### `ExteriorUse` - -| Field | Old type | New type | Notes | -|---|---|---|---| -| `powerDensity` | `"number"` | `["number", "null"]` | Made nullable. | -| `useQuantity` | `"number"` | `["number", "null"]` | Made nullable. | - -### `InteriorLightingSpace` - -| Field | Old type | New type | Notes | -|---|---|---|---| -| `numFixturesAlteredOrAdded` | `["integer", "null"]` with `minimum: 0` | `["integer", "null"]` | `minimum` constraint removed. | -| `primaryDaylight` | `"number"` | `["number", "null"]` | Made nullable. | -| `secondaryDaylight` | `"number"` | `["number", "null"]` | Made nullable. | -| `skylightToplight` | `"number"` | `["number", "null"]` | Made nullable. | -| `roofMonitorToplight` | `"number"` | `["number", "null"]` | Made nullable. | -| `decorativeArea` | `"number"` | `["number", "null"]` | Made nullable. | - -### `InteriorLightingFixture` - -| Field | Old type | New type | Notes | -|---|---|---|---| -| `fixtureType` | `"string"` | `["string", "null"]` | Made nullable. | -| `fixtureWattage` | `"number"` | `["number", "null"]` | Made nullable. | -| `quantity` | `"integer"` | `["integer", "null"]` | Made nullable. | -| `quantityWithAdvControls` | `"integer"` | `["integer", "null"]` | Made nullable. | - -### `FixtureSchedule` - -| Field | Old type | New type | Notes | -|---|---|---|---| -| `id` | `"integer"` | `["string", "integer"]` | Now also accepts string. | -| `lightingId` | `"integer"` | `["string", "integer"]` | Now also accepts string. | - -### `HVAC` - -| Field | Old type | New type | Notes | -|---|---|---|---| -| `fanSystem` | `"array"` | `["array", "null"]` | Made nullable. Added `default: null`. Description capitalised from "fan system" to "Fan system". | - -### `HVACSystem` - -| Field | Old type | New type | Notes | -|---|---|---|---| -| `quantity` minimum | `1` | `0` | Minimum quantity lowered from 1 to 0. | - -### `HVACPlant` - -| Field | Old type | New type | Notes | -|---|---|---|---| -| `condenserFlowRate` | `"number"` | `["number", "null"]` | Made nullable. | -| `condenserLeavingTemperature` | `"number"` | `["number", "null"]` | Made nullable. | -| `coolingPlantCapacity` | `"number"` | `["number", "null"]` | Made nullable. | -| `enteringCondenserWaterTemperature` | `"number"` | `["number", "null"]` | Made nullable. | -| `evaporatorLeavingTemperature` | `"number"` | `["number", "null"]` | Made nullable. | -| `heatingPlantCapacity` | `"number"` | `["number", "null"]` | Made nullable. | -| `heatRecovery` | `"boolean"` | `["integer", "null"]`, `enum: [0, 1, null]` | Changed from boolean to integer flag with null support. | -| `heatPumpSimultaneousCoolingAndHeating` | `"boolean"` | `["integer", "null"]`, `enum: [0, 1, null]` | Changed from boolean to integer flag with null support. | -| `leavingChilledWaterTemperature` | `"number"` | `["number", "null"]` | Made nullable. | -| `propCoolingPlantEfficiencyPartial` | `"number"` | `["number", "null"]` | Made nullable. | -| `propCoolingPlantEfficiency` | `"number"` | `["number", "null"]` | Made nullable. | -| `propHeatingPlantEfficiency` | `"number"` | `["number", "null"]` | Made nullable. | -| `quantity` minimum | `1.0` | `0` | Minimum quantity lowered from 1 to 0. | -| `systemType` | `"string"` | `["string", "null"]` | Made nullable. | -| `twoPipeSystem` | `"boolean"` | `["integer", "null"]`, `enum: [0, 1, null]` | Changed from boolean to integer flag with null support. | -| `waterloopHeatPump` | `"boolean"` | `["integer", "null"]`, `enum: [0, 1, null]` | Changed from boolean to integer flag with null support. | - -### `FanSystem` - -| Field | Old type | New type | Notes | -|---|---|---|---| -| `description2` | `"string"` | `["string", "null"]` | Made nullable. | -| `fanSystemKey` | `"string"` | `["string", "null"]` | Made nullable. | -| `hasPressureDropCredits` | `["boolean", "integer"]` | `enum: [0, 1, null]` | Changed to nullable enum. | - -### `Fan` - -| Field | Old type | New type | Notes | -|---|---|---|---| -| `fanDesignEfficiency` | `"number"` | `["number", "null"]` | Made nullable. | -| `maxNameplateHp` | `"number"` with `minimum: 0.0` | `["number", "null"]` | Made nullable; `minimum` constraint removed. | -| `nameplateHp` | `"number"` with `minimum: 0.0` | `"number"` | `minimum` constraint removed (type unchanged). | -| `totalFanEfficiency` | `"number"` | `["number", "null"]` | Made nullable. | - -### `PressureDrop` - -| Field | Old type | New type | Notes | -|---|---|---|---| -| `recoveryEffectiveness` | `"number"`, `minimum: 0.0`, `maximum: 1.0` | `["number", "null"]`, `minimum: 0.0` | Made nullable; `maximum: 1.0` constraint removed. | -| `verticalDuctLength` | `"number"` | `["number", "null"]` | Made nullable. | - -### `ServiceWaterHeatingSystem` - -| Field | Old type | New type | Notes | -|---|---|---|---| -| `circulationPump` | `"boolean"` | `"integer"`, `enum: [0, 1]`, `default: 0` | Changed from boolean to integer flag. | -| `heatTraceTapeInstalled` | `"boolean"` | `"integer"`, `enum: [0, 1]`, `default: 0` | Changed from boolean to integer flag. | -| `combinedSystem` | `"boolean"` | `"integer"`, `enum: [0, 1]`, `default: 0` | Changed from boolean to integer flag. | -| `poolSystem` | `"boolean"` | `"integer"`, `enum: [0, 1]`, `default: 0` | Changed from boolean to integer flag. | -| `heatPumpPoolHeater` (renamed from `heatpumpPoolHeater`) | `"boolean"` | `["boolean", "null"]`, `enum: [0, 1, null]`, `default: null` | Renamed (camelCase fix) and made nullable. | -| `quantity` minimum | `1` | `0` | Minimum quantity lowered from 1 to 0. | -| `requirementAnswer` | `"array"` (no items defined) | `"array"` with `items: $ref Requirements`, `default: []` | Items type now specified. | - -### `EnergyCreditPackage` - -| Field | Old type | New type | Notes | -|---|---|---|---| -| `bldgUseKey` | `"string"` | `["string", "null"]` | Made nullable. | - -### `Renewable` - -| Field | Old type | New type | Notes | -|---|---|---|---| -| `numberOfFloors` minimum | `1` | `0` | Minimum floors lowered from 1 to 0. | -| `largestThreeFloorArea` | `"number"` | `["number", "null"]` | Made nullable. | -| `requiredCapacity` | `"number"` | `["number", "null"]` | Made nullable. | -| `proposedCapacity` | `"number"` | `["number", "null"]` | Made nullable. | -| `roofAreaForRenewable` | `"number"` | `["number", "null"]` | Made nullable. | - ---- - -## 4. Constraint Changes - -### `ComBuilding` - -| Field | Change | -|---|---| -| `performanceRating` | `minimum: 0.0` removed. | -| `energyCreditPerformanceRating` | `minimum: 0.0` removed. | - -### `AgWall` - -| Field | Change | -|---|---| -| `cavityRValue` | `minimum: 0.0` removed. | -| `continuousRValue` | `minimum: 0.0` removed. | -| `continuousDeratedRValue` | `default: 0.0` removed (now has no default). | -| `propUValue` | `minimum: 0.0` removed. | -| `grossArea` | `minimum: 0.0` removed. | - -### `BgWall` - -| Field | Change | -|---|---| -| `cavityRValue` | `minimum: 0.0` removed. | -| `continuousRValue` | `minimum: 0.0` removed. | -| `propUValue` | `minimum: 0.0` removed. | - -### `Window` - -| Field | Change | -|---|---| -| `propUValue` | `minimum: 0.0` removed. | -| `propShgc` | No constraint change (minimum still 0.0). | -| `preAltPropUval` | `default: 0.0` removed. | -| `cavityRValue` | `minimum: 0.0` removed. | -| `continuousRValue` | `minimum: 0.0` removed. | - -### `Door` - -| Field | Change | -|---|---| -| `propUValue` | `minimum: 0.0` removed. | -| `preAltPropUval` | `default: 0.0` removed. | -| `cavityRValue` | `minimum: 0.0` removed. | -| `continuousRValue` | `minimum: 0.0` removed. | - -### `Skylight` - -| Field | Change | -|---|---| -| `propUValue` | `minimum: 0.0` removed. | -| `preAltPropUval` | `default: 0.0` removed. | - -### `Roof` - -| Field | Change | -|---|---| -| `highAlbedoRoofReqType` | Typo `"defualt"` corrected to `"default"`. | -| `cavityRValue` | `minimum: 0.0` removed. | -| `continuousRValue` | `minimum: 0.0` removed. | -| `propUValue` | `minimum: 0.0` removed. | - -### `Floor` - -| Field | Change | -|---|---| -| `cavityRValue` (Floor def) | `minimum: 0.0` present in context line only; type changed to nullable. | -| `propUValue` (Floor def) | `minimum: 0.0` removed. | - -### `InteriorLightingSpace` - -| Field | Change | -|---|---| -| `numFixturesAlteredOrAdded` | `minimum: 0` removed. | - -### `HVACSystem` - -| Field | Change | -|---|---| -| `quantity` | `minimum` changed from `1` to `0`. | - -### `HVACPlant` - -| Field | Change | -|---|---| -| `quantity` | `minimum` changed from `1.0` to `0`. | -| `efficiencyRequirementException` | `default: "EFF_EXCEPTION_UNSPECIFIED"` removed. | - -### `ServiceWaterHeatingSystem` - -| Field | Change | -|---|---| -| `quantity` | `minimum` changed from `1` to `0`. | -| `swhSystemSubType` | `default: "UNKNOWN_SWH_SYSTEM_SUB_TYPE"` removed. | -| `efficiencyRequirementException` | `default: "EFF_EXCEPTION_UNSPECIFIED"` removed. | - -### `Envelope` (`useOrientationDetails`) - -| Field | Change | -|---|---| -| `useOrientationDetails` | `default: true` replaced with `const: true`. This field must now always equal `true` (no other value is valid). | - ---- - -## 5. Structural / `$ref` Changes - -### `anyOf` → direct `$ref` (null support removed from schema, now provided by enum) - -Several fields that previously used `anyOf: [{type: null}, {$ref: ...}]` have been changed to a bare `$ref`. This means the field **no longer explicitly allows `null` in the JSON Schema sense** — nullability is now expected to come from the enum definition itself (which has had `null` added as an enum value). - -Affected fields (definition → field): - -| Definition | Field | -|---|---| -| `AgWall` | `adjacentSpaceBuildingType` | -| `AgWall` | `allowanceType` | -| `BgWall` | `adjacentSpaceBuildingType` | -| `BgWall` | `allowanceType` | -| `Roof` | `adjacentSpaceBuildingType` | -| `Roof` | `allowanceType` | -| `Window` | `adjacentSpaceBuildingType` | -| `Window` | `allowanceType` | -| `Window` | `frameType` | -| `Door` | `adjacentSpaceBuildingType` | -| `Door` | `allowanceType` | -| `Door` | `frameType` | -| `Skylight` | `adjacentSpaceBuildingType` | -| `Skylight` | `allowanceType` | -| `Skylight` | `frameType` | -| `Floor` | `allowanceType` | -| `InteriorLightingFixture` | `allowanceType` | -| `InteriorLightingFixture` | `ballast` | -| `InteriorLightingFixture` | `trackLightingWattageBasisType` | -| `FixtureSchedule` | `trackLightingWattageBasisType` | - -### `AgWall.otherWallType` - -Changed from bare `$ref AgWallOtherTypeOptions` to `anyOf: [{type: null}, {$ref: ...}]`. Null is now explicitly permitted. - -### `WholeBldgUse.interiorLightingSpace` - -Changed from bare `$ref InteriorLightingSpace` to `anyOf: [{$ref: ...}, {type: null}]`. Null is now explicitly permitted. - -### `ActivityUse.interiorLightingSpace` - -Changed from bare `$ref InteriorLightingSpace` to `anyOf: [{$ref: ...}, {type: null}]`. Null is now explicitly permitted. - -### `ExteriorUse.exteriorLightingSpace` - -Changed from bare `$ref ExteriorLightingSpace` to `anyOf: [{$ref: ...}, {type: null}]`. Null is now explicitly permitted. - -### `InteriorLightingFixture.advControlAllowanceType` → renamed to `advControlsAllowanceType` - -Field renamed from `advControlAllowanceType` to `advControlsAllowanceType`. Also changed from bare `$ref` with no default to `$ref` with `default: null`. - ---- - -## 6. `required` Array Changes - -### `InteriorLightingFixture` - -| Change | Notes | -|---|---| -| `lightingType` removed from required | `lightingType` is no longer required. | -| `fixtureType` added to required | `fixtureType` is now required (replacing `lightingType`). | - -### `FixtureSchedule` - -| Change | Notes | -|---|---| -| `lightingType` removed from required | `lightingType` is no longer required. | - -*All other `required` array changes in the diff are purely formatting (inline → multi-line) with no semantic difference.* - ---- - -## 7. New Enum Values - -### `EnergyCodeOptions` (national codes) - -Added values: -- `CEZ_IECC2009` -- `CEZ_IECC2012` -- `CEZ_IECC2024_APPXCF` ("IECC 2024 Appendix CF") -- `CEZ_90_1_2007` -- `CEZ_90_1_2010` -- `NONE` ("Unspecified") - -### `StateRegionEnergyCodeOptions` - -Added values: -- `CEZ_NYS2024_IECC2024` ("2024 New York State Energy Conservation Code - IECC 2024") -- `CEZ_NYS2025_9012022` ("2025 New York State Energy Conservation Code - 90.1 (2022)") -- `CEZ_NYC2025_IECC2024` ("2025 New York City Energy Conservation Code - IECC 2024") -- `CEZ_NYC2025_9012022` ("2025 New York City Energy Conservation Code - 90.1 (2022)") -- `CEZ_VT2024_IECC2021` ("2024 Vermont Commercial Building Energy Standards") -- `CEZ_LA2021_IECC2021` ("2021 LA Energy Code - 2021 IECC") -- `NONE` ("Unspecified") - -### `ProjectTypeOptions` - -Added values: -- `NONE` ("Unspecified") -- `null` ("Missing") -- The definition also dropped the explicit `"type": "string"` constraint. - -### `AirBarrierComplianceTypeOptions` - -Added: `AIR_BARRIER_OPTION_CONTINUITY_PLAN` ("Continuity Plan") - -### `AgWallTypeOptions` - -Added: -- `OTHER_BG_WALL` ("Other Above Grade Wall Type" — note: description says "Above Grade" but value name says "BG", may be intentional) -- `OTHER_FRAME` ("Other Framing Type") -- `null` ("Unspecified") -- The existing `METAL_BLDG_AG_WALL` description changed from "Metal Building Wall" to "Metal Building Wall Without Thermal Break" - -### `BgWallTypeOptions` - -Added: `null` ("Unspecified"). Also dropped the explicit `"type": "string"` constraint. - -### `RoofTypeOptions` - -Added: `METAL_ROOF_W_THERMAL_BREAK` ("Metal Roof with Thermal Break") - -### `HighAlbedoRoofReqTypeOptions` - -Added: `HA_ROOF_REQ_SOLAR_REFLECTANCE` ("Minimum Solar Reflectance") - -### `FloorTypeOptions` - -Added: `null` ("Unspecified"). Also dropped the explicit `"type": "string"` constraint. - -### `SlabInsulationPositionOptions` - -Added: `NONE` ("None") as an additional alias. - -### `AgWallConstructionDetailsTypeOptions` - -Added: -- `AG_WALL_CONSTRUCTION_DETAILS_UNKNOWN` ("Unknown") -- `AG_WALL_CONSTRUCTION_DETAILS_HORIZONTAL_Z_GIRTS` ("Horizontal Z-Girts") -- `AG_WALL_CONSTRUCTION_DETAILS_VERTICAL_Z_GIRTS` ("Vertical Z-Girts") -- `AG_WALL_CONSTRUCTION_DETAILS_Z_GIRTS_THERMAL_BROKEN` ("Z-Girts with Thermal Break") - -### `EnvelopeAssemblyAllowanceTypeOptions` - -Added: -- `NONE` ("None") -- `null` ("Unspecified") -- Dropped explicit `"type": "string"` constraint. - -### `CMUTypeOptions` - -Added: -- `NONE` ("None") -- `null` ("Unspecified") -- Dropped explicit `"type": "string"` constraint. - -### `ConcreteDensityOptions` - -Added values: `85` ("Light Weight"), `135` (no description added), `null`. -Dropped explicit `"type": "integer"` constraint. - -### `ConcreteThicknessOptions` - -Added values: `3`, `4`, `5`, `7`, `9`, `11`, `null`. -Dropped explicit `"type": "integer"` constraint. - -### `EnvelopeAssemblyExemptionOptions` - -Added: `NONE` ("None") - -### `FurringTypeOptions` - -Added: `NONE` ("None") at the beginning of the enum. - -### `OrientationOptions` - -Added: `null` ("Null"). Dropped explicit `"type": "string"` constraint. - -### `AltExemptTypeOptions` - -Added: -- `EXEMPT_HISTORIC_CHARACTERISTIC` ("Alteration to the area is not applicable to historic characteristics.") -- `EXEMPT_LIGHTING_SPACE_REPLACEMENT_LT_20_PCT_LOAD` ("Less than 20% fixture replacement.") - -### `FenestrationFrameTypeOptions` - -Added values: -- `NON_METAL` ("Non-metal frame") -- `NONE` ("None") -- `METAL_FRAME_24_AG_WALL` ("24-gauge metal-framed wall") -- `GLASS_DOOR` ("Glass door") -- `METAL_THERMAL_BREAK` ("Metal frame with thermal break") -- `OTHER_DOOR` ("Other door") -- `INSUL_METAL_DOOR` ("Insulated metal door") -- `NO_INSUL_SINGLE_METAL_DOOR` ("Non-insulated single metal door") -- `WOOD_FRAME_16_AG_WALL` ("16-gauge wood-framed wall") -- `ALL_WOOD_JOIST_TRUSS_FLOOR` ("All-wood joist/truss floor") -- `METAL_FRAME_16_AG_WALL` ("16-gauge metal-framed wall") -- `WOOD_DOOR` ("Wood door") -- `null` ("Unspecified") - -Also dropped explicit `"type": "string"` constraint. - -### `GlazingTypeOptions` - -Added: -- `OTHER_GLAZING` -- `NONE` ("None") - -### `SolarTypeOptions` - -Added: `NONE` ("None") - -### `PerfDataTypeOptions` - -Added: -- `NONE` ("None") -- `PERF_TYPE_UNSPECIFIED` ("Unspecified") - -### `GlazingMaterialTypeOptions` - -Added: `NONE` ("None") - -### `DoorTypeOptions` - -Added: `METAL_W_THERMAL_BREAK` ("Metal with Thermal Break") - -### `LightingAllowanceTypeOptions` - -Added: -- `ALLOWANCE_ADVANCED_CONTROLS` ("Advanced Controls") -- `ALLOWANCE_DECORATIVE_APPEARANCE_LOBBIES` ("Decorative Appearance, Lobbies") -- `ALLOWANCE_DECORATIVE_APPEARANCE_OTHER` ("Decorative Appearance, Other") -- `ALLOWANCE_ELECTRICAL_MECHANICAL` ("Electrical/Mechanical Equipment") -- `ALLOWANCE_VIDEO_CONFERENCE` ("Video conference") -- `NONE` ("None") -- `null` ("Unspecified") -- Dropped explicit `"type": "string"` constraint. - -### `LightingExemptionTypeOptions` - -Added: -- `EXEMPTION_APPROVED_SAFETY` ("Approved Safety Lighting") -- `EXEMPTION_DWELL_UNIT_CONTROLLED` ("Dwelling Unit Lighting Controlled by Occupant") -- `EXEMPTION_EMERGENCY_AUTOOFF` (description "Emergency Lighting Auto-off During Operating Hours" — duplicated from existing entry) -- `EXEMPTION_HIGHLIGHT_HAZARDS` (no description added in diff) -- `EXEMPTION_INDUSTRIAL_PRODUCTION` ("Industrial Production") -- `EXEMPTION_MANUFACTURER_AS_PART_OF_EQUIP` -- `EXEMPTION_POOLS_WATER` -- `EXEMPTION_REQUIRED_EGRESS` ("Lighting Required for Egress") -- `EXEMPTION_TEMP_LIGHTING` ("Temporary Lighting") -- `EXEMPTION_THEME_PARK_ELEMENTS` ("Theme Park Elements") -- `EXEMPTION_HIGHLIGHT_MONUMENT` ("Highlight Monument") -- `EXEMPTION_TRANSPORTATION_MARKER` ("Transportation Marker") -- `EXEMPTION_TRANSPORTATION_SITE` ("Transporation Site Lighting" — typo in source) -- `EXEMPTION_EMERGENCY_LIGHT_OFF_NORMAL_BUSINESS_HRS` ("Emergency Lighting Auto-off During Operating Hours") -- `EXEMPTION_MUSEUM_DISPLAY` ("Museum Display") -- `EXEMPTION_SEARCHLIGHTS` ("Searchlights") -- `EXEMPTION_SLEEPING_UNIT` -- `EXEMPTION_VISUALLY_IMPAIRED` ("Visually Impaired") - -### `TrackLightingWattageBasisTypeOptions` - -Added: -- `NONE` ("None") -- `null` ("Unspecified") - -### `AdvancedControlsAllowanceTypeOptions` - -Added: `null`. Also changed `type` from `"string"` to `["string", "null"]`. - -### `WholeBuildingTypeOptions` - -Added: `WHOLE_BUILDING_INVALID_USE` ("Invalid Use") - -### `ExteriorLightingZoneTypeOptions` - -Added: `EXT_ZONE_UNDEVELOPED` ("Undeveloped area (LZ1)") - -### `ThermalBridgeComplianceTypeOptions` - -- Fixed typo: `" THERMAL_BRIDGE_AS_DESIGNED"` (leading spaces) corrected to `"THERMAL_BRIDGE_AS_DESIGNED"`. -- Added `null` ("Unspecified"). -- Dropped explicit `"type": "string"` constraint. - -### `CondenserTypeOptions` - -Added: `null` ("Unspecified"). Dropped explicit `"type": "string"` constraint. - -### `EconomizerTypeOptions` - -Added: `FLUID_ECONOMIZER` ("Fluid") - -### `FuelTypeOptions` - -Added: -- `OIL_RESIDUAL` ("Residual Oil") -- `null` ("Unspecified") -- Dropped explicit `"type": "string"` constraint. - -### `BoilerDraftTypeOptions` - -Added: `null` ("Unspecified"). Dropped explicit `"type": "string"` constraint. - -### `ChillerTypeOptions` - -Added: -- `CENTRIFUGAL_NON_STANDARD` -- `null` ("Unspecified") -- Dropped explicit `"type": "string"` constraint. - -### `CoolingPlantTypeOptions` - -Added: `null` ("Missing"). Dropped explicit `"type": "string"` constraint. - -### `HeatingPlantTypeOptions` - -Added: `null` ("Missing"). Dropped explicit `"type": "string"` constraint. - -### `HeatPumpChillerHeatingSourceConditionOptions` - -Added: `null` ("Missing"). Dropped explicit `"type": "string"` constraint. - -### `HeatPumpChillerLeavingHeatingWaterTempOptions` - -Added: `null` ("Missing"). Dropped explicit `"type": "string"` constraint. - -### `HeatPumpChillerTypeOptions` - -Added: `null` ("Missing"). Dropped explicit `"type": "string"` constraint. - -### `HeatRejectionTypeOptions` - -Added: `null` ("Missing"). Dropped explicit `"type": "string"` constraint. - -### `FanSystemComplianceMethodOptions` - -Added: `null` ("Missing") - -### `FanEfficiencyExceptionTypeOptions` - -Added: -- `NONE` ("None") -- `null` ("Missing") - -### `SWHSystemDrawPatternTypeOptions` - -Added: -- `NO_COOLING_EQUIPMENT` ("No Cooling Equipment") -- `null` ("Missing") -- Dropped explicit `"type": "string"` constraint. - -### `SWHFuelTypeOptions` - -Added: `null` ("Unspecified"). Dropped explicit `"type": "string"` constraint. - -### `EquipmentEfficiencyRequirementExceptionOptions` - -Added: `null` ("Missing"). Dropped explicit `"type": "string"` constraint. - -### `RenewableExceptionOptions` - -Added: -- `RENEWABLE_ONSITE_EXCEPTION_IECC2024_LOW_FLOOR_AREA` ("Building effective floor area is less than 5,000 ft2") -- `null` ("Unspecified") -- Dropped explicit `"type": "string"` constraint. - -### `BallastTypeOptions` - -Added: `null` ("Unspecified"). Dropped explicit `"type": "string"` constraint. - -### `RequirementAnswerStatus` - -Added: `null` ("Missing"). Dropped explicit `"type": "string"` constraint. - -### `CompliancePathOptions` - -Added: -- `COMPLIANCE_PATH_A` ("Path A") — new canonical form -- `COMPLIANCE_PATH_B` ("Path B") — new canonical form -- `COMPLIANCE_PATH_UNKNOWN` ("Unknown") - -### `ActivityTypeOptions` - -Added: -- `ACTIVITY_COMMON_CONFERENCE_CELL` -- `ACTIVITY_COMMON_GUESTROOM` -- `ACTIVITY_COMMON_PATIENT` -- `ACTIVITY_COMMON_WELLNESS_LOUNGE` -- `ACTIVITY_GAME_HIGH_LIMITS_GAME` -- `ACTIVITY_GAME_SLOTS` -- `ACTIVITY_GAME_SPORTSBOOK` -- `ACTIVITY_GAME_TABLE_GAMES` -- `ACTIVITY_HOSPITAL_TELEMEDICINE_ROOM` -- `ACTIVITY_PARKING_DAYLIGHT_TRANSITION_ZONE` -- `ACTIVITY_RETAIL_MASSAGE_SPACE` -- `ACTIVITY_RETAIL_NAIL_SALON` -- `ACTIVITY_RETAIL_NAIL_SALON_MALL` -- `ACTIVITY_RETAIL_HAIR_SALON` -- `ACTIVITY_SECURITY_SCREEN_TRANSPORTATION_FACILITIES` -- `ACTIVITY_SECURITY_SCREEN_TRANSPORTATION_WAIT_AREA` -- `ACTIVITY_TRANS_AIRPORT_HANGER` -- `ACTIVITY_TRANS_PASSENGER_LOAD` -- `ACTIVITY_SECURITY_SCREEN_GENERAL_AREA` -- `ACTIVITY_SPORTS_POOL_CLASS1` -- `ACTIVITY_SPORTS_POOL_CLASS2` -- `ACTIVITY_SPORTS_POOL_CLASS3` -- `ACTIVITY_SPORTS_POOL_CLASS4` - -Removed: -- `ACTIVITY_COMMON_OFFICE` - ---- - -## 8. Default Value Changes - -| Definition | Field | Old default | New default | -|---|---|---|---| -| `AgWall.continuousDeratedRValue` | — | `0.0` | *(removed)* | -| `Window.preAltPropUval` | — | `0.0` | *(removed)* | -| `Door.preAltPropUval` | — | `0.0` | *(removed)* | -| `Skylight.preAltPropUval` | — | `0.0` | *(removed)* | -| `HVACPlant.efficiencyRequirementException` | — | `"EFF_EXCEPTION_UNSPECIFIED"` | *(removed)* | -| `ServiceWaterHeatingSystem.swhSystemSubType` | — | `"UNKNOWN_SWH_SYSTEM_SUB_TYPE"` | *(removed)* | -| `ServiceWaterHeatingSystem.efficiencyRequirementException` | — | `"EFF_EXCEPTION_UNSPECIFIED"` | *(removed)* | -| `ServiceWaterHeatingSystem.heatPumpPoolHeater` (renamed) | — | *(none)* | `null` | -| `ServiceWaterHeatingSystem.circulationPump` | — | *(none)* | `0` | -| `ServiceWaterHeatingSystem.heatTraceTapeInstalled` | — | *(none)* | `0` | -| `ServiceWaterHeatingSystem.combinedSystem` | — | *(none)* | `0` | -| `ServiceWaterHeatingSystem.poolSystem` | — | *(none)* | `0` | -| `InteriorLightingFixture.advControlsAllowanceType` | — | *(none)* | `null` | -| `HVAC.fanSystem` | — | *(none)* | `null` | - ---- - -## 9. Miscellaneous / Formatting-only - -A large portion of the diff consists of converting compact inline JSON arrays like `["string", "null"]` into multi-line form. These are cosmetic changes with **no semantic impact** on validation. - -Additionally, the schema `version` field at the root was bumped from `"0.0.1"` to `"0.0.2"`. - ---- - -## 10. Post-merge corrections - -After review, the boolean → integer changes in PR #25 were reverted. The backend treats these fields as booleans semantically (`0 = false`, `1 = true`), and the server accepts and returns `true`/`false`. Modeling them as `integer enum [0, 1]` required a runtime `IntEnum → bool` conversion workaround in `CustomBaseModel.model_dump` and introduced unnecessary `IntEnum` wrapper classes in `core_types.py`. All nine fields were restored to their original `boolean` types. - -| Definition | Field | PR #25 change | Reverted to | -|---|---|---|---| -| `ComBuilding` | `isHistoricBuilding` | `integer enum [0, 1]` | `boolean`, default `false` | -| `HVACSystem` | `heatRecovery` | `integer enum [0, 1, null]` | `["boolean", "null"]` | -| `HVACSystem` | `heatPumpSimultaneousCoolingAndHeating` | `integer enum [0, 1, null]` | `["boolean", "null"]` | -| `HVACPlant` | `twoPipeSystem` | `integer enum [0, 1, null]` | `["boolean", "null"]` | -| `HVACPlant` | `waterloopHeatPump` | `integer enum [0, 1, null]` | `["boolean", "null"]` | -| `ServiceWaterHeatingSystem` | `circulationPump` | `integer enum [0, 1]`, default `0` | `boolean`, default `false` | -| `ServiceWaterHeatingSystem` | `heatTraceTapeInstalled` | `integer enum [0, 1]`, default `0` | `boolean`, default `false` | -| `ServiceWaterHeatingSystem` | `combinedSystem` | `integer enum [0, 1]`, default `0` | `boolean`, default `false` | -| `ServiceWaterHeatingSystem` | `poolSystem` | `integer enum [0, 1]`, default `0` | `boolean`, default `false` | - -The `IsHistoricBuilding`, `CirculationPump`, `HeatTraceTapeInstalled`, `CombinedSystem`, and `PoolSystem` `IntEnum` classes were removed from `core_types.py` as a result. diff --git a/docs/schema-compatibility.md b/docs/schema-compatibility.md deleted file mode 100644 index 2ff0166..0000000 --- a/docs/schema-compatibility.md +++ /dev/null @@ -1,168 +0,0 @@ -# Schema Compatibility - -The `comcheck_api` library bridges between the Python Pydantic models in `core_types.py` (generated from `comCheck.schema.json`) and the live COMcheck backend API. Because the server may return data that predates or diverges from the current schema, `CustomBaseModel` contains several sanitization layers that run automatically on every parse and serialize cycle. - -This document describes each known compatibility issue, why it occurs, and how it is handled. - ---- - -## Background: `MISSING` Sentinel - -Many fields in the generated models use `MISSING` (from `pydantic.experimental.missing_sentinel`) as a default instead of `None`. A field with `= MISSING` means: - -- **On parse:** the server did not include this field — the model holds `MISSING` rather than failing validation. -- **On serialize:** `model_dump(mode='json')` omits the key entirely — the server does not receive it. - -This is the intentional "sparse update" pattern: only fields the server actually sent are round-tripped back. Issues arise when the server *requires* a field on write but omits it on read, or when `MISSING` leaks into the JSON payload. - ---- - -## Issue 1 — `deepcopy` fails on models with `MISSING` fields - -**Symptom:** `TypeError: Cannot pickle 'Sentinel' object` when calling `copy.deepcopy()` or `model.model_copy(deep=True)`. - -**Root cause:** `MISSING` is a `typing_extensions.Sentinel` that is not picklable. Pydantic's `__deepcopy__` internally uses pickle for nested objects. - -**Fix:** `CustomBaseModel.__deepcopy__` copies fields one-by-one, passing `MISSING` through unchanged rather than attempting to deep-copy it. - ---- - -## Issue 2 — Boolean flags serialized as integers - -**Symptom:** `HTTP 400: 'instance.isHistoricBuilding' is not of a type(s) boolean` - -**Root cause:** The generated schema models boolean flags as `IntEnum` with values `{0, 1}` (e.g. `IsHistoricBuilding`, `CirculationPump`, `HeatTraceTapeInstalled`, `CombinedSystem`, `PoolSystem`). `model_dump(mode='json')` serializes them as integers; the server's JSON Schema validator requires `true`/`false`. - -**Affected fields:** `isHistoricBuilding`, `allElectric`, `isRenewable`, `hasBattery`, `hasCharger`, `hasHeatPump` - -**Fix:** `CustomBaseModel.model_dump` runs both a `mode='python'` and `mode='json'` dump, then walks them in parallel. Wherever the Python value is an `IntEnum` instance, the corresponding JSON integer is replaced with `bool(value)`. - ---- - -## Issue 3 — Unknown enum values crash on parse - -**Symptom:** `ValidationError: Input should be 'ACTIVITY_INVALID_USE', 'ACTIVITY_AUTO_REPAIR', ... [type=enum]` for a value like `'ACTIVITY_COMMON_OFFICE'`. - -**Root cause:** Projects created under older schema versions may reference enum values that have since been renamed or removed (e.g. `ACTIVITY_COMMON_OFFICE` → `ACTIVITY_COMMON_OFFICE_OPEN`). The server stores these values verbatim and returns them unchanged. - -**Fix — two-validator approach:** - -Pydantic runs validators in order: `wrap` → `before` → field validation → `after`. This ordering is used to preserve the original value while still satisfying Pydantic's type checker: - -1. **`_preserve_invalid_enum_strings` (`mode='wrap'`)** runs first. It inspects the raw input dict and stashes any string values that are not valid members of their declared `StrEnum` type. -2. **`_sanitize_server_data` (`mode='before'`)** runs next (inside the `handler` call). It replaces the unknown string with the first valid enum member so Pydantic can construct the model without raising a `ValidationError`. -3. After `handler()` returns the constructed model object, `_preserve_invalid_enum_strings` writes the original (unknown) string back onto the field using `object.__setattr__`, bypassing field validation entirely. - -The result: the model field holds exactly what the server sent. `model_dump(mode='json')` serializes it as-is, so the value round-trips back to the server unchanged. A `WARNING` is still logged so the drift is visible. - -```python -# Server returns an old enum value -project = client.get_project("project-id") -area = project.lighting.wholeBldgUse[0].activityUse[0] - -print(area.activityType) # 'ACTIVITY_COMMON_OFFICE' ← original value preserved -print(type(area.activityType)) # ← not an enum member -``` - -> **Schema action needed:** When the backend migrates old records to use current enum values, this fallback will no longer be triggered. - ---- - -## Issue 4 — Pydantic serializer `UserWarning` spam - -**Symptom:** Dozens of `PydanticSerializationUnexpectedValue: Expected 'MISSING' sentinel` warnings on every `model_dump` call. - -**Root cause:** Pydantic's built-in serializer emits a warning for each `SomeEnum | MISSING` union variant it tries during serialization. - -**Fix:** `CustomBaseModel.model_dump` wraps both internal dump calls in `warnings.catch_warnings()` suppressing `UserWarning` from pydantic only. No behavior change. - ---- - -## Issue 5 — Server sends negative sentinels for unset numeric fields - -**Symptom:** `HTTP 400: 'instance.envelope.roof[1].continuousRValue' must be greater than or equal to 0` - -**Root cause:** The server uses `-1` (and similar negative values like `-4.545`) as a "not set" sentinel on fields like `continuousRValue`, `propUValue`, `cavityRValue`. The server enforces `>= 0` on write but does not enforce it on read. - -**Fix:** `CustomBaseModel._sanitize_server_data` replaces any incoming negative number with the field's declared `default` value, when that default is `>= 0`. A warning is logged. - -**Schema fix applied:** Added `"minimum": 0.0` to the following fields in `comCheck.schema.json` where it was missing: - -| Definition | Fields updated | -|---|---| -| `AboveGradeWall` (all 3 definitions) | `cavityRValue`, `continuousRValue`, `propUValue`, `grossArea` | -| `BelowGradeWall` | `cavityRValue`, `continuousRValue` | -| `Window` | `cavityRValue`, `continuousRValue` | -| `Door` | `cavityRValue`, `continuousRValue` | -| `Skylight` | `cavityRValue`, `continuousRValue` | -| `Roof` (second definition) | `cavityRValue`, `continuousRValue`, `propUValue` | - ---- - -## Issue 6 — `MISSING` fields included in outbound JSON payload - -**Symptom:** `HTTP 400: 'instance.lighting.wholeBldgUse[0].allowedWattage' is not of a type(s) number` - -**Root cause:** `MISSING` fields — those the server never sent — were being included in `model_dump(mode='json')` output as unexpected non-typed values (not `null`, not a number). The server's JSON Schema validator rejected them. - -**Fix:** The `model_dump` post-processing step (which already diffs Python vs JSON output for Issue 2) now also drops any key whose Python-side value is `MISSING`. The field is simply omitted from the outbound payload. - ---- - -## Issue 7 — `allowanceType` absent on GET, required on PUT - -**Symptom:** `ValidationError: Field required [type=missing]` on parse, then `HTTP 400: requires property "allowanceType"` on PUT. - -**Root cause:** The server omits `allowanceType` for older envelope records (sends `null` or omits the key entirely), but its write validator requires the field to be present with a valid string value. The field type is `EnvelopeAssemblyAllowanceTypeOptions | MISSING`. - -**Affected components:** `AgWall`, `BgWall`, `Window`, `Door`, `Skylight`, `Roof` - -**Fix — inbound (parse):** `_sanitize_server_data` maps an incoming `null` for a non-nullable enum field to the first non-null enum member. For `EnvelopeAssemblyAllowanceTypeOptions`, this is `ENV_ALLOWANCE_NONE`. - -**Fix — schema:** Removed `allowanceType` from the `required` arrays of all six component definitions in `comCheck.schema.json`. The field remains defined in `properties` — it is optional on read, required on write (enforced by the server). - -> **Note:** This is a server-side inconsistency. The long-term fix is for the server to always populate `allowanceType` when returning records, and for the schema to reflect that it is always present. Once the server is updated, the `required` entries can be restored. - ---- - -## Issue 8 — `null` values dropped for non-optional enum fields - -**Symptom:** Fields like `adjacentSpaceType`, `exemptionType` (typed as `SomeEnum`, not `SomeEnum | None`) arrive as `null` from the server, causing the Pydantic model to lose them. - -**Root cause:** The server sends `null` for fields it considers "not set" even when the generated schema does not allow `null`. The `_sanitize_server_data` validator was dropping these fields (reverting to `MISSING`), which then caused serialization failures or missing required fields on write. - -**Fix:** When the field has an enum with a `None`-valued member (`NoneType_None = None`), the incoming `null` is mapped to that enum member rather than dropped. When no such member exists, the field is dropped and a warning is logged. - ---- - -## Summary of `comCheck.schema.json` changes - -All changes align the schema with the server's actual write-time validation behavior: - -| Change | Location | Reason | -|---|---|---| -| Added `"minimum": 0.0` | `cavityRValue`, `continuousRValue` in all envelope definitions | Server rejects negative values on write | -| Added `"minimum": 0.0` | `propUValue` in Roof (alt definition), AgWall (alt definitions) | Server rejects negative values on write | -| Added `"minimum": 0.0` | `grossArea` in first AgWall definition | Consistent with all other `grossArea` definitions | -| Removed `allowanceType` from `required` | AgWall, BgWall, Window, Door, Skylight, Roof | Server omits this field on read for older records | - ---- - -## Logging - -All sanitization actions emit `WARNING`-level log messages via `comcheck_api.types.custom_base_model`. To see them: - -```python -import logging -logging.basicConfig(level=logging.WARNING) -``` - -Example output: -``` -WARNING comcheck_api.types.custom_base_model:custom_base_model.py:142 - Replacing unknown enum value 'ACTIVITY_COMMON_OFFICE' with fallback 'ACTIVITY_INVALID_USE' - for field ActivityUse.activityType - -WARNING comcheck_api.types.custom_base_model:custom_base_model.py:160 - Replacing server sentinel -1 with default 0.0 for field Roof.continuousRValue -``` diff --git a/docs/schema.md b/docs/schema.md new file mode 100644 index 0000000..a934724 --- /dev/null +++ b/docs/schema.md @@ -0,0 +1,944 @@ +# Schema Reference + +This document covers three related topics: + +1. **Runtime compatibility** — known gaps between the server's actual behavior and the generated models, and how they are handled automatically. +2. **Authoring guidelines** — lessons learned when modifying `comCheck.schema.json` or re-running Pydantic model generation. +3. **Changelog** — every meaningful change introduced in the schema update merged via PR #25. + +--- + +## Background: `MISSING` Sentinel + +Many fields in the generated models use `MISSING` (from `pydantic.experimental.missing_sentinel`) as a default instead of `None`. A field with `= MISSING` means: + +- **On parse:** the server did not include this field — the model holds `MISSING` rather than failing validation. +- **On serialize:** `model_dump(mode='json')` omits the key entirely — the server does not receive it. + +This is the intentional "sparse update" pattern: only fields the server actually sent are round-tripped back. Issues arise when the server *requires* a field on write but omits it on read, or when `MISSING` leaks into the JSON payload. + +--- + +## Part 1: Runtime Compatibility + +The `comcheck_api` library bridges between the Python Pydantic models in `core_types.py` (generated from `comCheck.schema.json`) and the live COMcheck backend API. Because the server may return data that predates or diverges from the current schema, `CustomBaseModel` contains several sanitization layers that run automatically on every parse and serialize cycle. + +### Issue 1 — `deepcopy` fails on models with `MISSING` fields + +**Symptom:** `TypeError: Cannot pickle 'Sentinel' object` when calling `copy.deepcopy()` or `model.model_copy(deep=True)`. + +**Root cause:** `MISSING` is a `typing_extensions.Sentinel` that is not picklable. Pydantic's `__deepcopy__` internally uses pickle for nested objects. + +**Fix:** `CustomBaseModel.__deepcopy__` copies fields one-by-one, passing `MISSING` through unchanged rather than attempting to deep-copy it. + +--- + +### Issue 2 — Boolean flags serialized as integers + +**Symptom:** `HTTP 400: 'instance.isHistoricBuilding' is not of a type(s) boolean` + +**Root cause:** The generated schema models boolean flags as `IntEnum` with values `{0, 1}` (e.g. `IsHistoricBuilding`, `CirculationPump`, `HeatTraceTapeInstalled`, `CombinedSystem`, `PoolSystem`). `model_dump(mode='json')` serializes them as integers; the server's JSON Schema validator requires `true`/`false`. + +**Affected fields:** `isHistoricBuilding`, `allElectric`, `isRenewable`, `hasBattery`, `hasCharger`, `hasHeatPump` + +**Fix:** `CustomBaseModel.model_dump` runs both a `mode='python'` and `mode='json'` dump, then walks them in parallel. Wherever the Python value is an `IntEnum` instance, the corresponding JSON integer is replaced with `bool(value)`. + +--- + +### Issue 3 — Unknown enum values crash on parse + +**Symptom:** `ValidationError: Input should be 'ACTIVITY_INVALID_USE', 'ACTIVITY_AUTO_REPAIR', ... [type=enum]` for a value like `'ACTIVITY_COMMON_OFFICE'`. + +**Root cause:** Projects created under older schema versions may reference enum values that have since been renamed or removed (e.g. `ACTIVITY_COMMON_OFFICE` → `ACTIVITY_COMMON_OFFICE_OPEN`). The server stores these values verbatim and returns them unchanged. + +**Fix — two-validator approach:** + +Pydantic runs validators in order: `wrap` → `before` → field validation → `after`. This ordering is used to preserve the original value while still satisfying Pydantic's type checker: + +1. **`_preserve_invalid_enum_strings` (`mode='wrap'`)** runs first. It inspects the raw input dict and stashes any string values that are not valid members of their declared `StrEnum` type. +2. **`_sanitize_server_data` (`mode='before'`)** runs next (inside the `handler` call). It replaces the unknown string with the first valid enum member so Pydantic can construct the model without raising a `ValidationError`. +3. After `handler()` returns the constructed model object, `_preserve_invalid_enum_strings` writes the original (unknown) string back onto the field using `object.__setattr__`, bypassing field validation entirely. + +The result: the model field holds exactly what the server sent. `model_dump(mode='json')` serializes it as-is, so the value round-trips back to the server unchanged. A `WARNING` is still logged so the drift is visible. + +```python +# Server returns an old enum value +project = client.get_project("project-id") +area = project.lighting.wholeBldgUse[0].activityUse[0] + +print(area.activityType) # 'ACTIVITY_COMMON_OFFICE' ← original value preserved +print(type(area.activityType)) # ← not an enum member +``` + +> **Schema action needed:** When the backend migrates old records to use current enum values, this fallback will no longer be triggered. + +--- + +### Issue 4 — Pydantic serializer `UserWarning` spam + +**Symptom:** Dozens of `PydanticSerializationUnexpectedValue: Expected 'MISSING' sentinel` warnings on every `model_dump` call. + +**Root cause:** Pydantic's Rust serializer is compiled with the full field schema. When a model instance has `MISSING`-valued fields in `__dict__`, it sees a field count mismatch and warns. + +**Fix:** `CustomBaseModel` defines a `model_serializer(mode='plain')` that filters out any `MISSING`-valued entries before the Rust serializer sees them. The serialization output is identical — MISSING fields were already excluded — but the warning is eliminated. + +--- + +### Issue 5 — Server sends negative sentinels for unset numeric fields + +**Symptom:** `HTTP 400: 'instance.envelope.roof[1].continuousRValue' must be greater than or equal to 0` + +**Root cause:** The server uses `-1` (and similar negative values like `-4.545`) as a "not set" sentinel on fields like `continuousRValue`, `propUValue`, `cavityRValue`. The server enforces `>= 0` on write but does not enforce it on read. + +**Fix:** `CustomBaseModel._sanitize_server_data` replaces any incoming negative number with the field's declared `default` value, when that default is `>= 0`. A warning is logged. + +**Schema fix applied:** Added `"minimum": 0.0` to the following fields in `comCheck.schema.json` where it was missing: + +| Definition | Fields updated | +|---|---| +| `AboveGradeWall` (all 3 definitions) | `cavityRValue`, `continuousRValue`, `propUValue`, `grossArea` | +| `BelowGradeWall` | `cavityRValue`, `continuousRValue` | +| `Window` | `cavityRValue`, `continuousRValue` | +| `Door` | `cavityRValue`, `continuousRValue` | +| `Skylight` | `cavityRValue`, `continuousRValue` | +| `Roof` (second definition) | `cavityRValue`, `continuousRValue`, `propUValue` | + +--- + +### Issue 6 — `MISSING` fields included in outbound JSON payload + +**Symptom:** `HTTP 400: 'instance.lighting.wholeBldgUse[0].allowedWattage' is not of a type(s) number` + +**Root cause:** `MISSING` fields — those the server never sent — were being included in `model_dump(mode='json')` output as unexpected non-typed values (not `null`, not a number). The server's JSON Schema validator rejected them. + +**Fix:** The `model_dump` post-processing step (which already diffs Python vs JSON output for Issue 2) now also drops any key whose Python-side value is `MISSING`. The field is simply omitted from the outbound payload. + +--- + +### Issue 7 — `allowanceType` absent on GET, required on PUT + +**Symptom:** `ValidationError: Field required [type=missing]` on parse, then `HTTP 400: requires property "allowanceType"` on PUT. + +**Root cause:** The server omits `allowanceType` for older envelope records (sends `null` or omits the key entirely), but its write validator requires the field to be present with a valid string value. The field type is `EnvelopeAssemblyAllowanceTypeOptions | MISSING`. + +**Affected components:** `AgWall`, `BgWall`, `Window`, `Door`, `Skylight`, `Roof` + +**Fix — inbound (parse):** `_sanitize_server_data` maps an incoming `null` for a non-nullable enum field to the first non-null enum member. For `EnvelopeAssemblyAllowanceTypeOptions`, this is `ENV_ALLOWANCE_NONE`. + +**Fix — schema:** Removed `allowanceType` from the `required` arrays of all six component definitions in `comCheck.schema.json`. The field remains defined in `properties` — it is optional on read, required on write (enforced by the server). + +> **Note:** This is a server-side inconsistency. The long-term fix is for the server to always populate `allowanceType` when returning records, and for the schema to reflect that it is always present. Once the server is updated, the `required` entries can be restored. + +--- + +### Issue 8 — `null` values dropped for non-optional enum fields + +**Symptom:** Fields like `adjacentSpaceType`, `exemptionType` (typed as `SomeEnum`, not `SomeEnum | None`) arrive as `null` from the server, causing the Pydantic model to lose them. + +**Root cause:** The server sends `null` for fields it considers "not set" even when the generated schema does not allow `null`. The `_sanitize_server_data` validator was dropping these fields (reverting to `MISSING`), which then caused serialization failures or missing required fields on write. + +**Fix:** When the field has an enum with a `None`-valued member (`NoneType_None = None`), the incoming `null` is mapped to that enum member rather than dropped. When no such member exists, the field is dropped and a warning is logged. + +--- + +### Logging + +All sanitization actions emit `WARNING`-level log messages via `comcheck_api.types.custom_base_model`. To see them: + +```python +import logging +logging.basicConfig(level=logging.WARNING) +``` + +Example output: +``` +WARNING comcheck_api.types.custom_base_model:custom_base_model.py:142 + Replacing unknown enum value 'ACTIVITY_COMMON_OFFICE' with fallback 'ACTIVITY_INVALID_USE' + for field ActivityUse.activityType + +WARNING comcheck_api.types.custom_base_model:custom_base_model.py:160 + Replacing server sentinel -1 with default 0.0 for field Roof.continuousRValue +``` + +--- + +## Part 2: Schema Authoring Guidelines + +Takeaways from the most recent round of changes to `comcheck_api/schemas/comCheck.schema.json` and the Pydantic model generation. + +### 1. Use `--use-missing-sentinel` for optional fields + +Passing `--use-missing-sentinel` to the generator lets fields be marked with a `MISSING` identifier instead of defaulting to a JSON value. When the model is exported back to JSON, `MISSING` fields are omitted entirely. + +**Why it matters:** we were hitting problems where fields that weren't present in the original export would default to values we didn't want. The sentinel avoids inventing data — absent stays absent on round-trip. + +### 2. Avoid adding redundant `NONE` to enumerations + +Adding `NONE` to enums caused a lot of churn, and in many cases an option that already means "none" existed. Example: `SlabInsulationPositionOptions` already has `NO_INSULATION` **and** `NONE`. + +**Action:** before adding `NONE`, check whether the enum already has an equivalent member and reuse it rather than introducing a duplicate. + +### 3. Allow `null` as a valid type for many fields + +A lot of values arrive as `null` and should *not* be coerced to a default. To handle this correctly we had to explicitly allow `null` as a type for many fields in the JSON schema. + +### 4. Exemption types and activity types are likely incomplete + +A number of exemption types and activity types were added in PR #25, but coverage may not be complete. + +**Action:** cross-reference the enumeration directly in the backend code to confirm all valid exemption/activity types are represented. + +### 5. Put `null` inside the enumeration instead of `anyOf: [enum, null]` + +Rather than repeating `anyOf: [enumeration, null]` across many fields, add `null` directly to the enumeration itself. This avoids repetition and is simpler to write. + +**Note:** when the enum carries the type, you can also drop the `type` keyword on the field — the type is inferred from the enumeration. + +### 6. Reconsider `minimum` constraints + +`minimum` flags caused failures — e.g. a building with `preAltPropUval` less than 0 failed schema validation and couldn't be loaded into the `ComBuilding` object. + +**Open question:** do we actually need `minimum` in the schema? It isn't necessarily enforced by the backend, and its main effect right now is blocking otherwise-valid projects from loading. Consider removing these unless the constraint is genuinely required. + +### 7. Drop non-informative descriptions; prefer `title` + +Many `description` fields just restate the field name and add nothing. + +**Action:** remove descriptions that don't add information. If the text is just a formatted version of the field name, use the `title` keyword instead of `description`. + +--- + +## Part 3: Changelog — PR #25 + +Every meaningful change introduced in the schema update merged via PR #25. Changes are grouped by category. + +--- + +### 1. Field Additions + +#### `ComBuilding` + +| Field | Type | Notes | +|---|---|---| +| `bldgUseType` | `$ref BuildingUseTypeOptions` | Legacy alias for `buildingUseType`. Comment: "Legacy enum, only ACTIVITY is valid in the new ComCheck Web." | +| `efficiencyPackageType` | `enum` (string or `null`) | New field. Values: `EFF_PACKAGE_UNKNOWN`, `EFF_PACKAGE_HVAC_PERFORMANCE`, `EFF_PACKAGE_LIGHTING_REDUCED_LPD`, `EFF_PACKAGE_REDUCED_AIR_INFILTRATION`, `EFF_PACKAGE_ENHANCED_ENVELOPE_PERFORMANCE`, `EFF_PACKAGE_ENHANCED_LIGHTING_CONTROLS`, `EFF_PACKAGE_ONSITE_RENEWABLES`, `null`. Default: `null`. | +| `energyCreditMultiplierException` | `enum` (or `null`) | New field. Values: `NO_ENERGY_CREDIT_MULTIPLIER_EXCEPTION`, `ENERGY_CREDIT_MULTIPLIER_EXCEPTION_LOW_ENERGY_BUILDINGS`, `ENERGY_CREDIT_MULTIPLIER_EXCEPTION_PRIMARY_HEAT_PUMP`, `null`. No default declared. | + +#### `Door` (fenestration) + +| Field | Type | Notes | +|---|---|---| +| `feetAg` | `["number", "null"]` | New field: feet above grade. `minimum: 0.0`, `default: null`. | + +#### `Skylight` + +| Field | Type | Notes | +|---|---|---| +| `cavityRValue` | `["number", "null"]` | New field: average insulation R-value in cavity. Unit: `h-ft2-F/Btu`, `default: 0.0`. | +| `continuousRValue` | `["number", "null"]` | New field: continuous insulation on the skylight. Unit: `h-ft2-F/Btu`, `default: 0.0`. | + +#### `HVACSystem` + +| Field | Type | Notes | +|---|---|---| +| `requirementAnswer` | `array` of `$ref Requirements` | New field. `default: []`. | + +#### `HVACPlant` + +| Field | Type | Notes | +|---|---|---| +| `requirementAnswer` | `array` of `$ref Requirements` | New field. `default: []`. | + +#### `InteriorLightingFixture` + +| Field | Type | Notes | +|---|---|---| +| `scheduleFixtureKey` | `["string", "null"]` | New field: UUID to identify this fixture schedule. | +| `typeOfFixture` | `["string", "null"]` | New field: type of the fixture. | + +--- + +### 2. Removed Fields / Enum Values Removed + +#### `ActivityTypeOptions` + +| Removed value | Notes | +|---|---| +| `ACTIVITY_COMMON_OFFICE` | Removed from enum. `ACTIVITY_COMMON_OFFICE_ENCLOSED` and `ACTIVITY_COMMON_OFFICE_OPEN` remain. | + +--- + +### 3. Type Changes + +#### `ComBuilding` + +| Field | Old type | New type | Notes | +|---|---|---|---| +| `isHistoricBuilding` | `"boolean"` | `"integer"`, `enum: [0, 1]` | Changed from boolean to integer flag. | +| `isNonresidentialConditioning` | `"boolean"` | `["boolean", "null"]` | Made nullable. | +| `isResidentialConditioning` | `"boolean"` | `["boolean", "null"]` | Made nullable. | +| `isSemiheatedConditioning` | `"boolean"` | `["boolean", "null"]` | Made nullable. | +| `constructionType` | `"string"` | `["string", "null"]` | Made nullable. | + +#### `CodeData` + +| Field | Old type | New type | Notes | +|---|---|---|---| +| `version` | `"string"` | `["string", "null"]` | Made nullable. | + +#### `AgWall` + +| Field | Old type | New type | Notes | +|---|---|---|---| +| `bldgUseKey` | `"string"` | `["string", "null"]` | Made nullable. | +| `heatCapacity` | `"number"` | `["number", "null"]` | Made nullable. | +| `cavityRValue` | `"number"` | `["number", "null"]` | Made nullable. | +| `continuousRValue` | `"number"` | `["number", "null"]` | Made nullable. | +| `propUValue` | `"number"` | `["number", "null"]` | Made nullable. | +| `grossArea` | `"number"` | `["number", "null"]` | Made nullable. | + +#### `BgWall` + +| Field | Old type | New type | Notes | +|---|---|---|---| +| `bldgUseKey` | `"string"` | `["string", "null"]` | Made nullable. | +| `heatCapacity` | `"number"` | `["number", "null"]` | Made nullable. | +| `cavityRValue` | `"number"` | `["number", "null"]` | Made nullable. | +| `continuousRValue` | `"number"` | `["number", "null"]` | Made nullable. | +| `propUValue` | `"number"` | `["number", "null"]` | Made nullable. | + +#### `Window` + +| Field | Old type | New type | Notes | +|---|---|---|---| +| `bldgUseKey` | `"string"` | `["string", "null"]` | Made nullable. | +| `propUValue` | `"number"` | `["number", "null"]` | Made nullable. | +| `grossArea` | `"number"` | `["number", "null"]` | Made nullable. | +| `propShgc` | `"number"` | `["number", "null"]` | Made nullable. | +| `preAltPropShgc` | `"number"` | `["number", "null"]` | Made nullable. | +| `cavityRValue` | `"number"` | `["number", "null"]` | Made nullable. | +| `continuousRValue` | `"number"` | `["number", "null"]` | Made nullable. | + +#### `Door` + +| Field | Old type | New type | Notes | +|---|---|---|---| +| `bldgUseKey` | `"string"` | `["string", "null"]` | Made nullable. | +| `propUValue` | `"number"` | `["number", "null"]` | Made nullable. | +| `propShgc` | `"number"` | `["number", "null"]` | Made nullable. | +| `preAltPropShgc` | `"number"` | `["number", "null"]` | Made nullable. | +| `cavityRValue` | `"number"` | `["number", "null"]` | Made nullable. | +| `continuousRValue` | `"number"` | `["number", "null"]` | Made nullable. | + +#### `Skylight` + +| Field | Old type | New type | Notes | +|---|---|---|---| +| `bldgUseKey` | `"string"` | `["string", "null"]` | Made nullable. | +| `propUValue` | `"number"` | `["number", "null"]` | Made nullable. | +| `grossArea` | `"number"` | `["number", "null"]` | Made nullable. | +| `propShgc` | `"number"` | `["number", "null"]` | Made nullable. | +| `preAltPropShgc` | `"number"` | `["number", "null"]` | Made nullable. | + +#### `Roof` + +| Field | Old type | New type | Notes | +|---|---|---|---| +| `bldgUseKey` | `"string"` | `["string", "null"]` | Made nullable. | +| `cavityRValue` | `"number"` | `["number", "null"]` | Made nullable. | +| `continuousRValue` | `"number"` | `["number", "null"]` | Made nullable. | +| `propUValue` | `"number"` | `["number", "null"]` | Made nullable. | +| `grossArea` | `"number"` | `["number", "null"]` | Made nullable. | +| `purlinSpacing` | `"number"` | `["number", "null"]` | Made nullable. | + +#### `Floor` + +| Field | Old type | New type | Notes | +|---|---|---|---| +| `bldgUseKey` | `"string"` | `["string", "null"]` | Made nullable. | +| `cavityRValue` | `"number"` | `["number", "null"]` | Made nullable. | +| `continuousRValue` | `"number"` | `["number", "null"]` | Made nullable. | +| `propUValue` | `"number"` | `["number", "null"]` | Made nullable. | +| `grossArea` | `"number"` | `["number", "null"]` | Made nullable. | +| `slabFullInsulBelowMinRValue` | `"number"` | `["number", "null"]` | Made nullable. | + +#### `WholeBldgUse` + +| Field | Old type | New type | Notes | +|---|---|---|---| +| `floorArea` | `"number"` | `["number", "null"]` | Made nullable. | +| `powerDensity` | `"number"` | `["number", "null"]` | Made nullable. | +| `internalLoad` | `"number"` | `["number", "null"]` | Made nullable. | +| `allowedWattage` | `"number"` | `["number", "null"]` | Made nullable. | +| `proposedWattage` | `"number"` | `["number", "null"]` | Made nullable. | + +#### `ActivityUse` + +| Field | Old type | New type | Notes | +|---|---|---|---| +| `floorArea` | `"number"` | `["number", "null"]` | Made nullable. | +| `ceilingHeight` | `"number"` | `["number", "null"]` | Made nullable. | +| `powerDensity` | `"number"` | `["number", "null"]` | Made nullable. | +| `internalLoad` | `"number"` | `["number", "null"]` | Made nullable. | +| `allowedWattage` | `"number"` | `["number", "null"]` | Made nullable. | +| `proposedWattage` | `"number"` | `["number", "null"]` | Made nullable. | + +#### `ExteriorUse` + +| Field | Old type | New type | Notes | +|---|---|---|---| +| `powerDensity` | `"number"` | `["number", "null"]` | Made nullable. | +| `useQuantity` | `"number"` | `["number", "null"]` | Made nullable. | + +#### `InteriorLightingSpace` + +| Field | Old type | New type | Notes | +|---|---|---|---| +| `numFixturesAlteredOrAdded` | `["integer", "null"]` with `minimum: 0` | `["integer", "null"]` | `minimum` constraint removed. | +| `primaryDaylight` | `"number"` | `["number", "null"]` | Made nullable. | +| `secondaryDaylight` | `"number"` | `["number", "null"]` | Made nullable. | +| `skylightToplight` | `"number"` | `["number", "null"]` | Made nullable. | +| `roofMonitorToplight` | `"number"` | `["number", "null"]` | Made nullable. | +| `decorativeArea` | `"number"` | `["number", "null"]` | Made nullable. | + +#### `InteriorLightingFixture` + +| Field | Old type | New type | Notes | +|---|---|---|---| +| `fixtureType` | `"string"` | `["string", "null"]` | Made nullable. | +| `fixtureWattage` | `"number"` | `["number", "null"]` | Made nullable. | +| `quantity` | `"integer"` | `["integer", "null"]` | Made nullable. | +| `quantityWithAdvControls` | `"integer"` | `["integer", "null"]` | Made nullable. | + +#### `FixtureSchedule` + +| Field | Old type | New type | Notes | +|---|---|---|---| +| `id` | `"integer"` | `["string", "integer"]` | Now also accepts string. | +| `lightingId` | `"integer"` | `["string", "integer"]` | Now also accepts string. | + +#### `HVAC` + +| Field | Old type | New type | Notes | +|---|---|---|---| +| `fanSystem` | `"array"` | `["array", "null"]` | Made nullable. Added `default: null`. Description capitalised from "fan system" to "Fan system". | + +#### `HVACSystem` + +| Field | Old type | New type | Notes | +|---|---|---|---| +| `quantity` minimum | `1` | `0` | Minimum quantity lowered from 1 to 0. | + +#### `HVACPlant` + +| Field | Old type | New type | Notes | +|---|---|---|---| +| `condenserFlowRate` | `"number"` | `["number", "null"]` | Made nullable. | +| `condenserLeavingTemperature` | `"number"` | `["number", "null"]` | Made nullable. | +| `coolingPlantCapacity` | `"number"` | `["number", "null"]` | Made nullable. | +| `enteringCondenserWaterTemperature` | `"number"` | `["number", "null"]` | Made nullable. | +| `evaporatorLeavingTemperature` | `"number"` | `["number", "null"]` | Made nullable. | +| `heatingPlantCapacity` | `"number"` | `["number", "null"]` | Made nullable. | +| `heatRecovery` | `"boolean"` | `["integer", "null"]`, `enum: [0, 1, null]` | Changed from boolean to integer flag with null support. | +| `heatPumpSimultaneousCoolingAndHeating` | `"boolean"` | `["integer", "null"]`, `enum: [0, 1, null]` | Changed from boolean to integer flag with null support. | +| `leavingChilledWaterTemperature` | `"number"` | `["number", "null"]` | Made nullable. | +| `propCoolingPlantEfficiencyPartial` | `"number"` | `["number", "null"]` | Made nullable. | +| `propCoolingPlantEfficiency` | `"number"` | `["number", "null"]` | Made nullable. | +| `propHeatingPlantEfficiency` | `"number"` | `["number", "null"]` | Made nullable. | +| `quantity` minimum | `1.0` | `0` | Minimum quantity lowered from 1 to 0. | +| `systemType` | `"string"` | `["string", "null"]` | Made nullable. | +| `twoPipeSystem` | `"boolean"` | `["integer", "null"]`, `enum: [0, 1, null]` | Changed from boolean to integer flag with null support. | +| `waterloopHeatPump` | `"boolean"` | `["integer", "null"]`, `enum: [0, 1, null]` | Changed from boolean to integer flag with null support. | + +#### `FanSystem` + +| Field | Old type | New type | Notes | +|---|---|---|---| +| `description2` | `"string"` | `["string", "null"]` | Made nullable. | +| `fanSystemKey` | `"string"` | `["string", "null"]` | Made nullable. | +| `hasPressureDropCredits` | `["boolean", "integer"]` | `enum: [0, 1, null]` | Changed to nullable enum. | + +#### `Fan` + +| Field | Old type | New type | Notes | +|---|---|---|---| +| `fanDesignEfficiency` | `"number"` | `["number", "null"]` | Made nullable. | +| `maxNameplateHp` | `"number"` with `minimum: 0.0` | `["number", "null"]` | Made nullable; `minimum` constraint removed. | +| `nameplateHp` | `"number"` with `minimum: 0.0` | `"number"` | `minimum` constraint removed (type unchanged). | +| `totalFanEfficiency` | `"number"` | `["number", "null"]` | Made nullable. | + +#### `PressureDrop` + +| Field | Old type | New type | Notes | +|---|---|---|---| +| `recoveryEffectiveness` | `"number"`, `minimum: 0.0`, `maximum: 1.0` | `["number", "null"]`, `minimum: 0.0` | Made nullable; `maximum: 1.0` constraint removed. | +| `verticalDuctLength` | `"number"` | `["number", "null"]` | Made nullable. | + +#### `ServiceWaterHeatingSystem` + +| Field | Old type | New type | Notes | +|---|---|---|---| +| `circulationPump` | `"boolean"` | `"integer"`, `enum: [0, 1]`, `default: 0` | Changed from boolean to integer flag. | +| `heatTraceTapeInstalled` | `"boolean"` | `"integer"`, `enum: [0, 1]`, `default: 0` | Changed from boolean to integer flag. | +| `combinedSystem` | `"boolean"` | `"integer"`, `enum: [0, 1]`, `default: 0` | Changed from boolean to integer flag. | +| `poolSystem` | `"boolean"` | `"integer"`, `enum: [0, 1]`, `default: 0` | Changed from boolean to integer flag. | +| `heatPumpPoolHeater` (renamed from `heatpumpPoolHeater`) | `"boolean"` | `["boolean", "null"]`, `enum: [0, 1, null]`, `default: null` | Renamed (camelCase fix) and made nullable. | +| `quantity` minimum | `1` | `0` | Minimum quantity lowered from 1 to 0. | +| `requirementAnswer` | `"array"` (no items defined) | `"array"` with `items: $ref Requirements`, `default: []` | Items type now specified. | + +#### `EnergyCreditPackage` + +| Field | Old type | New type | Notes | +|---|---|---|---| +| `bldgUseKey` | `"string"` | `["string", "null"]` | Made nullable. | + +#### `Renewable` + +| Field | Old type | New type | Notes | +|---|---|---|---| +| `numberOfFloors` minimum | `1` | `0` | Minimum floors lowered from 1 to 0. | +| `largestThreeFloorArea` | `"number"` | `["number", "null"]` | Made nullable. | +| `requiredCapacity` | `"number"` | `["number", "null"]` | Made nullable. | +| `proposedCapacity` | `"number"` | `["number", "null"]` | Made nullable. | +| `roofAreaForRenewable` | `"number"` | `["number", "null"]` | Made nullable. | + +--- + +### 4. Constraint Changes + +#### `ComBuilding` + +| Field | Change | +|---|---| +| `performanceRating` | `minimum: 0.0` removed. | +| `energyCreditPerformanceRating` | `minimum: 0.0` removed. | + +#### `AgWall` + +| Field | Change | +|---|---| +| `cavityRValue` | `minimum: 0.0` removed. | +| `continuousRValue` | `minimum: 0.0` removed. | +| `continuousDeratedRValue` | `default: 0.0` removed (now has no default). | +| `propUValue` | `minimum: 0.0` removed. | +| `grossArea` | `minimum: 0.0` removed. | + +#### `BgWall` + +| Field | Change | +|---|---| +| `cavityRValue` | `minimum: 0.0` removed. | +| `continuousRValue` | `minimum: 0.0` removed. | +| `propUValue` | `minimum: 0.0` removed. | + +#### `Window` + +| Field | Change | +|---|---| +| `propUValue` | `minimum: 0.0` removed. | +| `propShgc` | No constraint change (minimum still 0.0). | +| `preAltPropUval` | `default: 0.0` removed. | +| `cavityRValue` | `minimum: 0.0` removed. | +| `continuousRValue` | `minimum: 0.0` removed. | + +#### `Door` + +| Field | Change | +|---|---| +| `propUValue` | `minimum: 0.0` removed. | +| `preAltPropUval` | `default: 0.0` removed. | +| `cavityRValue` | `minimum: 0.0` removed. | +| `continuousRValue` | `minimum: 0.0` removed. | + +#### `Skylight` + +| Field | Change | +|---|---| +| `propUValue` | `minimum: 0.0` removed. | +| `preAltPropUval` | `default: 0.0` removed. | + +#### `Roof` + +| Field | Change | +|---|---| +| `highAlbedoRoofReqType` | Typo `"defualt"` corrected to `"default"`. | +| `cavityRValue` | `minimum: 0.0` removed. | +| `continuousRValue` | `minimum: 0.0` removed. | +| `propUValue` | `minimum: 0.0` removed. | + +#### `Floor` + +| Field | Change | +|---|---| +| `cavityRValue` | Type changed to nullable. | +| `propUValue` | `minimum: 0.0` removed. | + +#### `InteriorLightingSpace` + +| Field | Change | +|---|---| +| `numFixturesAlteredOrAdded` | `minimum: 0` removed. | + +#### `HVACSystem` + +| Field | Change | +|---|---| +| `quantity` | `minimum` changed from `1` to `0`. | + +#### `HVACPlant` + +| Field | Change | +|---|---| +| `quantity` | `minimum` changed from `1.0` to `0`. | +| `efficiencyRequirementException` | `default: "EFF_EXCEPTION_UNSPECIFIED"` removed. | + +#### `ServiceWaterHeatingSystem` + +| Field | Change | +|---|---| +| `quantity` | `minimum` changed from `1` to `0`. | +| `swhSystemSubType` | `default: "UNKNOWN_SWH_SYSTEM_SUB_TYPE"` removed. | +| `efficiencyRequirementException` | `default: "EFF_EXCEPTION_UNSPECIFIED"` removed. | + +#### `Envelope` (`useOrientationDetails`) + +| Field | Change | +|---|---| +| `useOrientationDetails` | `default: true` replaced with `const: true`. This field must now always equal `true`. | + +--- + +### 5. Structural / `$ref` Changes + +#### `anyOf` → direct `$ref` (null support moved into enum) + +Several fields that previously used `anyOf: [{type: null}, {$ref: ...}]` have been changed to a bare `$ref`. Nullability is now provided by the enum definition itself (which has had `null` added as an enum value). + +| Definition | Field | +|---|---| +| `AgWall` | `adjacentSpaceBuildingType` | +| `AgWall` | `allowanceType` | +| `BgWall` | `adjacentSpaceBuildingType` | +| `BgWall` | `allowanceType` | +| `Roof` | `adjacentSpaceBuildingType` | +| `Roof` | `allowanceType` | +| `Window` | `adjacentSpaceBuildingType` | +| `Window` | `allowanceType` | +| `Window` | `frameType` | +| `Door` | `adjacentSpaceBuildingType` | +| `Door` | `allowanceType` | +| `Door` | `frameType` | +| `Skylight` | `adjacentSpaceBuildingType` | +| `Skylight` | `allowanceType` | +| `Skylight` | `frameType` | +| `Floor` | `allowanceType` | +| `InteriorLightingFixture` | `allowanceType` | +| `InteriorLightingFixture` | `ballast` | +| `InteriorLightingFixture` | `trackLightingWattageBasisType` | +| `FixtureSchedule` | `trackLightingWattageBasisType` | + +#### `AgWall.otherWallType` + +Changed from bare `$ref AgWallOtherTypeOptions` to `anyOf: [{type: null}, {$ref: ...}]`. Null is now explicitly permitted. + +#### `WholeBldgUse.interiorLightingSpace` + +Changed from bare `$ref InteriorLightingSpace` to `anyOf: [{$ref: ...}, {type: null}]`. Null is now explicitly permitted. + +#### `ActivityUse.interiorLightingSpace` + +Changed from bare `$ref InteriorLightingSpace` to `anyOf: [{$ref: ...}, {type: null}]`. Null is now explicitly permitted. + +#### `ExteriorUse.exteriorLightingSpace` + +Changed from bare `$ref ExteriorLightingSpace` to `anyOf: [{$ref: ...}, {type: null}]`. Null is now explicitly permitted. + +#### `InteriorLightingFixture.advControlAllowanceType` → renamed to `advControlsAllowanceType` + +Field renamed from `advControlAllowanceType` to `advControlsAllowanceType`. Also changed from bare `$ref` with no default to `$ref` with `default: null`. + +--- + +### 6. `required` Array Changes + +#### `InteriorLightingFixture` + +| Change | Notes | +|---|---| +| `lightingType` removed from required | `lightingType` is no longer required. | +| `fixtureType` added to required | `fixtureType` is now required (replacing `lightingType`). | + +#### `FixtureSchedule` + +| Change | Notes | +|---|---| +| `lightingType` removed from required | `lightingType` is no longer required. | + +*All other `required` array changes in the diff are purely formatting (inline → multi-line) with no semantic difference.* + +--- + +### 7. New Enum Values + +#### `EnergyCodeOptions` + +Added: `CEZ_IECC2009`, `CEZ_IECC2012`, `CEZ_IECC2024_APPXCF`, `CEZ_90_1_2007`, `CEZ_90_1_2010`, `NONE` + +#### `StateRegionEnergyCodeOptions` + +Added: `CEZ_NYS2024_IECC2024`, `CEZ_NYS2025_9012022`, `CEZ_NYC2025_IECC2024`, `CEZ_NYC2025_9012022`, `CEZ_VT2024_IECC2021`, `CEZ_LA2021_IECC2021`, `NONE` + +#### `ProjectTypeOptions` + +Added: `NONE`, `null`. Also dropped the explicit `"type": "string"` constraint. + +#### `AirBarrierComplianceTypeOptions` + +Added: `AIR_BARRIER_OPTION_CONTINUITY_PLAN` + +#### `AgWallTypeOptions` + +Added: `OTHER_BG_WALL`, `OTHER_FRAME`, `null`. `METAL_BLDG_AG_WALL` description changed from "Metal Building Wall" to "Metal Building Wall Without Thermal Break". + +#### `BgWallTypeOptions` + +Added: `null`. Dropped explicit `"type": "string"` constraint. + +#### `RoofTypeOptions` + +Added: `METAL_ROOF_W_THERMAL_BREAK` + +#### `HighAlbedoRoofReqTypeOptions` + +Added: `HA_ROOF_REQ_SOLAR_REFLECTANCE` + +#### `FloorTypeOptions` + +Added: `null`. Dropped explicit `"type": "string"` constraint. + +#### `SlabInsulationPositionOptions` + +Added: `NONE` as an additional alias. + +#### `AgWallConstructionDetailsTypeOptions` + +Added: `AG_WALL_CONSTRUCTION_DETAILS_UNKNOWN`, `AG_WALL_CONSTRUCTION_DETAILS_HORIZONTAL_Z_GIRTS`, `AG_WALL_CONSTRUCTION_DETAILS_VERTICAL_Z_GIRTS`, `AG_WALL_CONSTRUCTION_DETAILS_Z_GIRTS_THERMAL_BROKEN` + +#### `EnvelopeAssemblyAllowanceTypeOptions` + +Added: `NONE`, `null`. Dropped explicit `"type": "string"` constraint. + +#### `CMUTypeOptions` + +Added: `NONE`, `null`. Dropped explicit `"type": "string"` constraint. + +#### `ConcreteDensityOptions` + +Added: `85`, `135`, `null`. Dropped explicit `"type": "integer"` constraint. + +#### `ConcreteThicknessOptions` + +Added: `3`, `4`, `5`, `7`, `9`, `11`, `null`. Dropped explicit `"type": "integer"` constraint. + +#### `EnvelopeAssemblyExemptionOptions` + +Added: `NONE` + +#### `FurringTypeOptions` + +Added: `NONE` at the beginning of the enum. + +#### `OrientationOptions` + +Added: `null`. Dropped explicit `"type": "string"` constraint. + +#### `AltExemptTypeOptions` + +Added: `EXEMPT_HISTORIC_CHARACTERISTIC`, `EXEMPT_LIGHTING_SPACE_REPLACEMENT_LT_20_PCT_LOAD` + +#### `FenestrationFrameTypeOptions` + +Added: `NON_METAL`, `NONE`, `METAL_FRAME_24_AG_WALL`, `GLASS_DOOR`, `METAL_THERMAL_BREAK`, `OTHER_DOOR`, `INSUL_METAL_DOOR`, `NO_INSUL_SINGLE_METAL_DOOR`, `WOOD_FRAME_16_AG_WALL`, `ALL_WOOD_JOIST_TRUSS_FLOOR`, `METAL_FRAME_16_AG_WALL`, `WOOD_DOOR`, `null`. Dropped explicit `"type": "string"` constraint. + +#### `GlazingTypeOptions` + +Added: `OTHER_GLAZING`, `NONE` + +#### `SolarTypeOptions` + +Added: `NONE` + +#### `PerfDataTypeOptions` + +Added: `NONE`, `PERF_TYPE_UNSPECIFIED` + +#### `GlazingMaterialTypeOptions` + +Added: `NONE` + +#### `DoorTypeOptions` + +Added: `METAL_W_THERMAL_BREAK` + +#### `LightingAllowanceTypeOptions` + +Added: `ALLOWANCE_ADVANCED_CONTROLS`, `ALLOWANCE_DECORATIVE_APPEARANCE_LOBBIES`, `ALLOWANCE_DECORATIVE_APPEARANCE_OTHER`, `ALLOWANCE_ELECTRICAL_MECHANICAL`, `ALLOWANCE_VIDEO_CONFERENCE`, `NONE`, `null`. Dropped explicit `"type": "string"` constraint. + +#### `LightingExemptionTypeOptions` + +Added: `EXEMPTION_APPROVED_SAFETY`, `EXEMPTION_DWELL_UNIT_CONTROLLED`, `EXEMPTION_EMERGENCY_AUTOOFF`, `EXEMPTION_HIGHLIGHT_HAZARDS`, `EXEMPTION_INDUSTRIAL_PRODUCTION`, `EXEMPTION_MANUFACTURER_AS_PART_OF_EQUIP`, `EXEMPTION_POOLS_WATER`, `EXEMPTION_REQUIRED_EGRESS`, `EXEMPTION_TEMP_LIGHTING`, `EXEMPTION_THEME_PARK_ELEMENTS`, `EXEMPTION_HIGHLIGHT_MONUMENT`, `EXEMPTION_TRANSPORTATION_MARKER`, `EXEMPTION_TRANSPORTATION_SITE`, `EXEMPTION_EMERGENCY_LIGHT_OFF_NORMAL_BUSINESS_HRS`, `EXEMPTION_MUSEUM_DISPLAY`, `EXEMPTION_SEARCHLIGHTS`, `EXEMPTION_SLEEPING_UNIT`, `EXEMPTION_VISUALLY_IMPAIRED` + +#### `TrackLightingWattageBasisTypeOptions` + +Added: `NONE`, `null` + +#### `AdvancedControlsAllowanceTypeOptions` + +Added: `null`. Changed `type` from `"string"` to `["string", "null"]`. + +#### `WholeBuildingTypeOptions` + +Added: `WHOLE_BUILDING_INVALID_USE` + +#### `ExteriorLightingZoneTypeOptions` + +Added: `EXT_ZONE_UNDEVELOPED` + +#### `ThermalBridgeComplianceTypeOptions` + +Fixed typo: `" THERMAL_BRIDGE_AS_DESIGNED"` (leading spaces) corrected to `"THERMAL_BRIDGE_AS_DESIGNED"`. Added `null`. Dropped explicit `"type": "string"` constraint. + +#### `CondenserTypeOptions` + +Added: `null`. Dropped explicit `"type": "string"` constraint. + +#### `EconomizerTypeOptions` + +Added: `FLUID_ECONOMIZER` + +#### `FuelTypeOptions` + +Added: `OIL_RESIDUAL`, `null`. Dropped explicit `"type": "string"` constraint. + +#### `BoilerDraftTypeOptions` + +Added: `null`. Dropped explicit `"type": "string"` constraint. + +#### `ChillerTypeOptions` + +Added: `CENTRIFUGAL_NON_STANDARD`, `null`. Dropped explicit `"type": "string"` constraint. + +#### `CoolingPlantTypeOptions` + +Added: `null`. Dropped explicit `"type": "string"` constraint. + +#### `HeatingPlantTypeOptions` + +Added: `null`. Dropped explicit `"type": "string"` constraint. + +#### `HeatPumpChillerHeatingSourceConditionOptions` + +Added: `null`. Dropped explicit `"type": "string"` constraint. + +#### `HeatPumpChillerLeavingHeatingWaterTempOptions` + +Added: `null`. Dropped explicit `"type": "string"` constraint. + +#### `HeatPumpChillerTypeOptions` + +Added: `null`. Dropped explicit `"type": "string"` constraint. + +#### `HeatRejectionTypeOptions` + +Added: `null`. Dropped explicit `"type": "string"` constraint. + +#### `FanSystemComplianceMethodOptions` + +Added: `null` + +#### `FanEfficiencyExceptionTypeOptions` + +Added: `NONE`, `null` + +#### `SWHSystemDrawPatternTypeOptions` + +Added: `NO_COOLING_EQUIPMENT`, `null`. Dropped explicit `"type": "string"` constraint. + +#### `SWHFuelTypeOptions` + +Added: `null`. Dropped explicit `"type": "string"` constraint. + +#### `EquipmentEfficiencyRequirementExceptionOptions` + +Added: `null`. Dropped explicit `"type": "string"` constraint. + +#### `RenewableExceptionOptions` + +Added: `RENEWABLE_ONSITE_EXCEPTION_IECC2024_LOW_FLOOR_AREA`, `null`. Dropped explicit `"type": "string"` constraint. + +#### `BallastTypeOptions` + +Added: `null`. Dropped explicit `"type": "string"` constraint. + +#### `RequirementAnswerStatus` + +Added: `null`. Dropped explicit `"type": "string"` constraint. + +#### `CompliancePathOptions` + +Added: `COMPLIANCE_PATH_A`, `COMPLIANCE_PATH_B`, `COMPLIANCE_PATH_UNKNOWN` + +#### `ActivityTypeOptions` + +Added: `ACTIVITY_COMMON_CONFERENCE_CELL`, `ACTIVITY_COMMON_GUESTROOM`, `ACTIVITY_COMMON_PATIENT`, `ACTIVITY_COMMON_WELLNESS_LOUNGE`, `ACTIVITY_GAME_HIGH_LIMITS_GAME`, `ACTIVITY_GAME_SLOTS`, `ACTIVITY_GAME_SPORTSBOOK`, `ACTIVITY_GAME_TABLE_GAMES`, `ACTIVITY_HOSPITAL_TELEMEDICINE_ROOM`, `ACTIVITY_PARKING_DAYLIGHT_TRANSITION_ZONE`, `ACTIVITY_RETAIL_MASSAGE_SPACE`, `ACTIVITY_RETAIL_NAIL_SALON`, `ACTIVITY_RETAIL_NAIL_SALON_MALL`, `ACTIVITY_RETAIL_HAIR_SALON`, `ACTIVITY_SECURITY_SCREEN_TRANSPORTATION_FACILITIES`, `ACTIVITY_SECURITY_SCREEN_TRANSPORTATION_WAIT_AREA`, `ACTIVITY_TRANS_AIRPORT_HANGER`, `ACTIVITY_TRANS_PASSENGER_LOAD`, `ACTIVITY_SECURITY_SCREEN_GENERAL_AREA`, `ACTIVITY_SPORTS_POOL_CLASS1`, `ACTIVITY_SPORTS_POOL_CLASS2`, `ACTIVITY_SPORTS_POOL_CLASS3`, `ACTIVITY_SPORTS_POOL_CLASS4` + +Removed: `ACTIVITY_COMMON_OFFICE` + +--- + +### 8. Default Value Changes + +| Definition | Field | Old default | New default | +|---|---|---|---| +| `AgWall` | `continuousDeratedRValue` | `0.0` | *(removed)* | +| `Window` | `preAltPropUval` | `0.0` | *(removed)* | +| `Door` | `preAltPropUval` | `0.0` | *(removed)* | +| `Skylight` | `preAltPropUval` | `0.0` | *(removed)* | +| `HVACPlant` | `efficiencyRequirementException` | `"EFF_EXCEPTION_UNSPECIFIED"` | *(removed)* | +| `ServiceWaterHeatingSystem` | `swhSystemSubType` | `"UNKNOWN_SWH_SYSTEM_SUB_TYPE"` | *(removed)* | +| `ServiceWaterHeatingSystem` | `efficiencyRequirementException` | `"EFF_EXCEPTION_UNSPECIFIED"` | *(removed)* | +| `ServiceWaterHeatingSystem` | `heatPumpPoolHeater` (renamed) | *(none)* | `null` | +| `ServiceWaterHeatingSystem` | `circulationPump` | *(none)* | `0` | +| `ServiceWaterHeatingSystem` | `heatTraceTapeInstalled` | *(none)* | `0` | +| `ServiceWaterHeatingSystem` | `combinedSystem` | *(none)* | `0` | +| `ServiceWaterHeatingSystem` | `poolSystem` | *(none)* | `0` | +| `InteriorLightingFixture` | `advControlsAllowanceType` | *(none)* | `null` | +| `HVAC` | `fanSystem` | *(none)* | `null` | + +--- + +### 9. Miscellaneous / Formatting-only + +A large portion of the diff consists of converting compact inline JSON arrays like `["string", "null"]` into multi-line form. These are cosmetic changes with **no semantic impact** on validation. + +The schema `version` field at the root was bumped from `"0.0.1"` to `"0.0.2"`. + +--- + +### 10. Post-merge corrections + +After review, the boolean → integer changes in PR #25 were reverted. The backend treats these fields as booleans semantically (`0 = false`, `1 = true`), and the server accepts and returns `true`/`false`. Modeling them as `integer enum [0, 1]` required a runtime `IntEnum → bool` conversion workaround in `CustomBaseModel.model_dump` and introduced unnecessary `IntEnum` wrapper classes in `core_types.py`. All nine fields were restored to their original `boolean` types. + +| Definition | Field | PR #25 change | Reverted to | +|---|---|---|---| +| `ComBuilding` | `isHistoricBuilding` | `integer enum [0, 1]` | `boolean`, default `false` | +| `HVACSystem` | `heatRecovery` | `integer enum [0, 1, null]` | `["boolean", "null"]` | +| `HVACSystem` | `heatPumpSimultaneousCoolingAndHeating` | `integer enum [0, 1, null]` | `["boolean", "null"]` | +| `HVACPlant` | `twoPipeSystem` | `integer enum [0, 1, null]` | `["boolean", "null"]` | +| `HVACPlant` | `waterloopHeatPump` | `integer enum [0, 1, null]` | `["boolean", "null"]` | +| `ServiceWaterHeatingSystem` | `circulationPump` | `integer enum [0, 1]`, default `0` | `boolean`, default `false` | +| `ServiceWaterHeatingSystem` | `heatTraceTapeInstalled` | `integer enum [0, 1]`, default `0` | `boolean`, default `false` | +| `ServiceWaterHeatingSystem` | `combinedSystem` | `integer enum [0, 1]`, default `0` | `boolean`, default `false` | +| `ServiceWaterHeatingSystem` | `poolSystem` | `integer enum [0, 1]`, default `0` | `boolean`, default `false` | + +The `IsHistoricBuilding`, `CirculationPump`, `HeatTraceTapeInstalled`, `CombinedSystem`, and `PoolSystem` `IntEnum` classes were removed from `core_types.py` as a result. diff --git a/docs/schema_changes_notes.md b/docs/schema_changes_notes.md deleted file mode 100644 index aff1914..0000000 --- a/docs/schema_changes_notes.md +++ /dev/null @@ -1,58 +0,0 @@ -# Notes: comCheck.schema.json & Pydantic Generation Changes - -Takeaways from the most recent round of changes to `comcheck_api/schemas/comCheck.schema.json` -and the Pydantic model generation. - -## 1. Use `--use-missing-sentinel` for optional fields - -Passing `--use-missing-sentinel` to the generator lets fields be marked with a `MISSING` -identifier instead of defaulting to a JSON value. When the model is exported back to JSON, -`MISSING` fields are omitted entirely. - -**Why it matters:** we were hitting problems where fields that weren't present in the original -export would default to values we didn't want. The sentinel avoids inventing data — absent stays -absent on round-trip. - -## 2. Avoid adding redundant `NONE` to enumerations - -Adding `NONE` to enums caused a lot of churn, and in many cases an option that already means "none" -existed. Example: `SlabInsulationPositionOptions` already has `NO_INSULATION` **and** `NONE`. - -**Action:** before adding `NONE`, check whether the enum already has an equivalent member and reuse it -rather than introducing a duplicate. - -## 3. Allow `null` as a valid type for many fields - -A lot of values arrive as `null` and should *not* be coerced to a default. To handle this correctly -we had to explicitly allow `null` as a type for many fields in the JSON schema. - -## 4. Exemption types and activity types are likely incomplete - -I added a number of exemption types and activity types, but I'm not confident the coverage is complete. - -**Action:** cross-reference the enumeration directly in the backend code to confirm all valid -exemption/activity types are represented. - -## 5. Put `null` inside the enumeration instead of `anyOf: [enum, null]` - -Rather than repeating `anyOf: [enumeration, null]` across many fields, I added `null` directly to the -enumeration itself. This avoids repetition and is simpler to write. - -**Note:** when the enum carries the type, you can also drop the `type` keyword on the field — the type -is inferred from the enumeration. - -## 6. Reconsider `minimum` constraints - -`minimum` flags caused failures — e.g. a building with `preAltPropUval` less than 0 failed schema -validation and couldn't be loaded into the `ComBuilding` object. - -**Open question:** do we actually need `minimum` in the schema? It isn't necessarily enforced by the -backend, and its main effect right now is blocking otherwise-valid projects from loading. Consider -removing these unless the constraint is genuinely required. - -## 7. Drop non-informative descriptions; prefer `title` - -Many `description` fields just restate the field name and add nothing. - -**Action:** remove descriptions that don't add information. If the text is just a formatted version of -the field name, use the `title` keyword instead of `description`. From 5c12c3db9334f842ece91220c39834e13ed3bc2a Mon Sep 17 00:00:00 2001 From: yanz571 Date: Thu, 13 Aug 2026 15:30:38 -0700 Subject: [PATCH 21/23] verify areaDescription uniqueness --- examples/project_operations/interior_lighting_operations.py | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/project_operations/interior_lighting_operations.py b/examples/project_operations/interior_lighting_operations.py index d682dee..ec1bb28 100644 --- a/examples/project_operations/interior_lighting_operations.py +++ b/examples/project_operations/interior_lighting_operations.py @@ -48,7 +48,6 @@ project = ba_ops.add_building_area_to_project(project, area) area_key = area.key export_to_json(project, "interior_lighting_operations_after_add_building_area.json") -print("exported") # Persist the new building area to the account. project = client.update_project(project_id, project) From 6530776c6d886899abe289c5f1d508a1abb0c865 Mon Sep 17 00:00:00 2001 From: yanz571 Date: Thu, 13 Aug 2026 15:46:00 -0700 Subject: [PATCH 22/23] initial update for github pages and skills --- comcheck_api/ai/skill/SKILL.md | 42 ++++++------- comcheck_api/ai/skill/reference/operations.md | 60 +++++++++++++++++-- comcheck_api/defaults.py | 7 ++- .../project_building_area_operations.py | 25 +++++--- docs_site/api/operations/exterior-lighting.md | 6 ++ docs_site/api/operations/interior-lighting.md | 7 ++- .../test_building_area_operations.py | 29 +++++++++ 7 files changed, 132 insertions(+), 44 deletions(-) diff --git a/comcheck_api/ai/skill/SKILL.md b/comcheck_api/ai/skill/SKILL.md index 1b9997e..9910559 100644 --- a/comcheck_api/ai/skill/SKILL.md +++ b/comcheck_api/ai/skill/SKILL.md @@ -30,17 +30,15 @@ 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 **interior-lighting fixtures - inside `activityUse[]`**, plus exterior lighting (`exteriorUse[]`) - and the shared `fixtureSchedule[]`, exist on the model but have - **no operation functions** — leave them at template defaults. Only - `lighting.wholeBldgUse[]` (building areas, including each area's - own `interiorLightingSpace` singleton) is mutable, via - `project_building_area_operations`. -- **Operation modules (functional)**: building areas and envelope - components are added/updated/removed via free functions in - `project_building_area_operations` and `project_envelope_operations`. - Each function takes a `ComBuilding` and returns a new `ComBuilding`. + The fields `hvac`, `renewable`, 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 + 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 (interior lighting spaces + fixtures) + - `project_exterior_lighting_operations` — `ExteriorUse` items + zone type - **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 @@ -149,21 +147,17 @@ print(result["performanceRating"]) `project_building_area_operations` instead. - Don't add, update, or remove `fixtureSchedule[]`, HVAC/mechanical, or renewable-energy components — no operations exist for them yet. - Interior lighting (`activityUse[]`) and exterior lighting - (`exteriorUse[]`) **are** supported via - `project_interior_lighting_operations` and - `project_exterior_lighting_operations` (see below). The - `COMcheckClient` user methods (`list_projects`, `get_project`, + 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. 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). + 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 / lighting / simulation instead. + Note: `comcheck_api.list_operations()` only enumerates `building_area` + and `envelope` groups — lighting operations are not yet registered + there but are fully implemented in their respective modules. ## Common patterns diff --git a/comcheck_api/ai/skill/reference/operations.md b/comcheck_api/ai/skill/reference/operations.md index f290618..92ed5eb 100644 --- a/comcheck_api/ai/skill/reference/operations.md +++ b/comcheck_api/ai/skill/reference/operations.md @@ -1,9 +1,11 @@ # Project Operations Reference -Operation functions are free functions in two modules: +Operation functions are free functions in four 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` Each function takes a `ComBuilding` and a payload, and returns a new `ComBuilding`. Treat them as immutable transformations. @@ -16,8 +18,8 @@ from comcheck_api import project_building_area_operations as ba_ops | Function | Purpose | |---|---| -| `add_building_area_to_project(project, new_building_area)` | Add a `WholeBldgUse` building area to the project. | -| `update_building_area_in_project(project, building_area_key, updates)` | Update fields of an existing building area by key. | +| `add_building_area_to_project(project, new_building_area)` | Add a `WholeBldgUse` building area to the project. Raises `ValueError` if `areaDescription` already exists. | +| `update_building_area_in_project(project, building_area_key, updates)` | Update fields of an existing building area by key. Raises `ValueError` if the new `areaDescription` collides with another area. | | `remove_building_area_from_project(project, building_area_key)` | Remove a building area by key. | | `get_building_area_keys_from_project(project)` | List `[{key, areaDescription}, …]` for the project. | @@ -33,9 +35,9 @@ a building-area key. Default projects have no areas — add one first: ```python from comcheck_api.defaults import get_default_building_area_template -area = get_default_building_area_template() -area.areaDescription = "Open office" -project = ba_ops.add_building_area_to_project(project, area) +area = get_default_building_area_template() # unique key + areaDescription per call +area.areaDescription = "Open office" # optional override — must be unique within the project +project = ba_ops.add_building_area_to_project(project, area) # raises ValueError if areaDescription already exists area_key = ba_ops.get_building_area_keys_from_project(project)[0]["key"] ``` @@ -100,6 +102,52 @@ roof.orientation = OrientationOptions.UNSPECIFIED_ORIENTATION project = env_ops.add_roof_to_project(project, area_key, roof) ``` +## Interior lighting operations + +```python +from comcheck_api import project_interior_lighting_operations as il_ops +``` + +Interior lighting spaces are `ActivityUse` objects nested under +`lighting.wholeBldgUse[i].activityUse[]`. There are no fixture-level ops — +edit `activityUse.interiorLightingSpace.fixture[]` and pass the whole +`ActivityUse` through `update_interior_lighting_space_in_project`. + +| Function | Purpose | +|---|---| +| `add_interior_lighting_space_to_project(project, building_area_key, new_activity_use)` | Add an `ActivityUse` to a building area. `activityUse.key` is auto-set to `building_area_key`. | +| `update_interior_lighting_space_in_project(project, building_area_key, area_description, updates)` | Update an `ActivityUse` by its `areaDescription`. | +| `remove_interior_lighting_space_from_project(project, building_area_key, area_description)` | Remove an `ActivityUse` by its `areaDescription`. | +| `get_interior_lighting_space_keys_from_project(project, building_area_key)` | List `[{areaDescription, activityType}, …]` for a building area. | + +Use `get_default_interior_lighting_space_template()` as a starting point. +`areaDescription` is the identifier — it is unique within a building area's +`activityUse[]` list and is auto-generated if missing. + +## Exterior lighting operations + +```python +from comcheck_api import project_exterior_lighting_operations as el_ops +``` + +Exterior lighting spaces are `ExteriorUse` objects in +`lighting.exteriorUse[]`. Set a real zone type before exterior compliance +can be evaluated. There are no fixture-level ops — edit +`exteriorUse.exteriorLightingSpace.fixture[]` and pass the whole +`ExteriorUse` through `update_exterior_lighting_area_in_project`. + +| Function | Purpose | +|---|---| +| `set_exterior_lighting_zone_type_in_project(project, zone_type)` | Set `lighting.exteriorLightingZoneType`. Raises `ValueError` for `EXT_ZONE_UNSPECIFIED`, `TypeError` for non-enum values. | +| `add_exterior_lighting_area_to_project(project, new_exterior_lighting_area)` | Add an `ExteriorUse`. Emits `UserWarning` if zone type is still `EXT_ZONE_UNSPECIFIED`. | +| `update_exterior_lighting_area_in_project(project, area_description, updates)` | Update an `ExteriorUse` by its `areaDescription`. | +| `remove_exterior_lighting_area_from_project(project, area_description)` | Remove an `ExteriorUse` by its `areaDescription`. | +| `get_exterior_lighting_area_keys_from_project(project)` | List `[{areaDescription, exteriorType}, …]` for the project. | + +Use `get_default_exterior_lighting_area_template()` as a starting point. +`areaDescription` is the identifier — it is unique within `exteriorUse[]` +and is auto-generated if missing. + ## U-value calculation requires a construction type When `update_uvalues` (or `start_run_simulation`) recalculates assembly diff --git a/comcheck_api/defaults.py b/comcheck_api/defaults.py index ce1933d..a5768d7 100644 --- a/comcheck_api/defaults.py +++ b/comcheck_api/defaults.py @@ -42,9 +42,10 @@ def get_default_project_template(): def get_default_building_area_template(): """Return a deep copy of the default :class:`~comcheck_api.types.core_types.WholeBldgUse` template. - Defaults to an *Automotive Facility* with 1 000 sq ft floor area - and interior lighting space initialized. Each call gets a fresh unique - ``key`` so multiple areas can be added to a project without colliding. + Defaults to an *Automotive Facility* with 1 000 sq ft floor area and + interior lighting space initialized. Each call gets a fresh unique ``key`` + and a unique ``areaDescription`` (UUID suffix appended) so multiple areas + can be added to a project without colliding on either field. Returns: A new ``WholeBldgUse`` instance. diff --git a/comcheck_api/project_operations/project_building_area_operations.py b/comcheck_api/project_operations/project_building_area_operations.py index 68271cb..5e3f3f0 100644 --- a/comcheck_api/project_operations/project_building_area_operations.py +++ b/comcheck_api/project_operations/project_building_area_operations.py @@ -14,15 +14,18 @@ def add_building_area_to_project( project: ComBuilding, new_building_area: WholeBldgUse ) -> ComBuilding: - """Add a new building area to the project using buildingAreaListManager. + """Add a new building area to the project. Args: - project: The project object to modify - new_building_area: The building area object to add + project: The project object to modify. + new_building_area: The building area object to add. Returns: - Updated project object with the building area added + Updated project object with the building area added. + Raises: + ValueError: If another building area with the same ``areaDescription`` + already exists in ``lighting.wholeBldgUse``. """ desc = getattr(new_building_area, "areaDescription", None) @@ -45,16 +48,20 @@ def add_building_area_to_project( def update_building_area_in_project( project: ComBuilding, building_area_key: str, updates: dict[str, Any] | WholeBldgUse ) -> ComBuilding: - """Update an existing building area in the project using buildingAreaListManager. + """Update an existing building area in the project. Args: - project: The project object to modify - building_area_key: The key of the building area to update - updates: Partial updates (dict) or full building area object to apply + project: The project object to modify. + building_area_key: The ``key`` of the building area to update. + updates: Partial updates (dict) or full building area object to apply. Returns: - Updated project object with the building area updated + Updated project object with the building area updated. + Raises: + ValueError: If ``building_area_key`` is not found, or if ``updates`` + contains an ``areaDescription`` that already belongs to a different + building area in ``lighting.wholeBldgUse``. """ _require_building_area(project, building_area_key) diff --git a/docs_site/api/operations/exterior-lighting.md b/docs_site/api/operations/exterior-lighting.md index 04d450b..3f5f7e6 100644 --- a/docs_site/api/operations/exterior-lighting.md +++ b/docs_site/api/operations/exterior-lighting.md @@ -14,6 +14,12 @@ can be evaluated. Adding an `ExteriorUse` while the zone is still `EXT_ZONE_UNSPECIFIED` emits a `UserWarning` (not an error) so you can build up a project incrementally. +## areaDescription uniqueness + +`areaDescription` must be unique within `lighting.exteriorUse[]`. It is the +identifier used by update and remove operations. If missing, a unique value is +auto-generated with the prefix `"Ext Area"`. + ## Operations ```python diff --git a/docs_site/api/operations/interior-lighting.md b/docs_site/api/operations/interior-lighting.md index ae4ae02..8746b56 100644 --- a/docs_site/api/operations/interior-lighting.md +++ b/docs_site/api/operations/interior-lighting.md @@ -11,6 +11,9 @@ one singleton `InteriorLightingSpace` whose `fixture[]` holds the fixtures. `ActivityUse` through `update_interior_lighting_space_in_project`. - **ActivityUse.key** is always set to the parent `WholeBldgUse.key` — the add operation sets this automatically. +- **`areaDescription` must be unique** within a building area's `activityUse[]` + list. It is the identifier used by update and remove operations. If missing, + a unique value is auto-generated with the prefix `"Space"`. - A building area must exist before adding activity uses — add one with `project_building_area_operations.add_building_area_to_project` first. @@ -56,7 +59,7 @@ fixture.quantity = 10 # Attach the fixture to the activity use before adding activity_use = get_default_interior_lighting_space_template() activity_use.areaDescription = "Open Office" -activity_use.activityType = ActivityTypeOptions.ACTIVITY_COMMON_OFFICE +activity_use.activityType = ActivityTypeOptions.ACTIVITY_COMMON_OFFICE_OPEN activity_use.floorArea = 2000.0 activity_use.interiorLightingSpace = activity_use.interiorLightingSpace.model_copy( deep=True, update={"fixture": [fixture]} @@ -130,5 +133,5 @@ project = il_ops.remove_interior_lighting_space_from_project(project, area_key, ```python keys = il_ops.get_interior_lighting_space_keys_from_project(project, area_key) -# [{"areaDescription": "Open Office", "activityType": "ACTIVITY_COMMON_OFFICE"}, ...] +# [{"areaDescription": "Open Office", "activityType": "ACTIVITY_COMMON_OFFICE_OPEN"}, ...] ``` diff --git a/tests/project_operation_tests/test_building_area_operations.py b/tests/project_operation_tests/test_building_area_operations.py index 7964fe6..ae9130f 100644 --- a/tests/project_operation_tests/test_building_area_operations.py +++ b/tests/project_operation_tests/test_building_area_operations.py @@ -1,3 +1,4 @@ +import pytest from copy import deepcopy from comcheck_api.client import COMcheckClient @@ -38,6 +39,34 @@ def test_building_area_operations( ) +def test_add_building_area_duplicate_description_raises(project: ComBuilding): + existing = project.get_by_path("lighting.wholeBldgUse", []) + assert existing, "fixture project must have at least one building area" + duplicate_desc = getattr(existing[0], "areaDescription") + + duplicate = get_default_building_area_template() + duplicate.areaDescription = duplicate_desc + + with pytest.raises(ValueError, match="areaDescription"): + project_building_area_operations.add_building_area_to_project( + project, duplicate + ) + + +def test_update_building_area_duplicate_description_raises(project: ComBuilding): + local = project.model_copy(deep=True) + + area_a = get_default_building_area_template() + area_b = get_default_building_area_template() + local = project_building_area_operations.add_building_area_to_project(local, area_a) + local = project_building_area_operations.add_building_area_to_project(local, area_b) + + with pytest.raises(ValueError, match="areaDescription"): + project_building_area_operations.update_building_area_in_project( + local, area_b.key, {"areaDescription": area_a.areaDescription} + ) + + def test_get_building_area_keys(project: ComBuilding): local_project = project.model_copy(deep=True) local_project.lighting.wholeBldgUse = [ From dc12c522340dcc5a1f9b332dc77bc3bfe9fe69fc Mon Sep 17 00:00:00 2001 From: yanz571 Date: Thu, 13 Aug 2026 16:12:35 -0700 Subject: [PATCH 23/23] update of github pages, skills --- comcheck_api/ai/skill/SKILL.md | 11 ++++++----- comcheck_api/ai/skill/scripts/validate_code.py | 15 +++++++++++---- comcheck_api/introspection.py | 4 ++++ docs_site/api/operations/exterior-lighting.md | 6 ++++-- docs_site/api/operations/interior-lighting.md | 7 ++++--- docs_site/getting-started.md | 12 ++++++++---- docs_site/index.md | 7 ++++--- .../exterior_lighting_operations.py | 13 ++++++------- .../interior_lighting_operations.py | 16 +++++++--------- 9 files changed, 54 insertions(+), 37 deletions(-) diff --git a/comcheck_api/ai/skill/SKILL.md b/comcheck_api/ai/skill/SKILL.md index 9910559..2d22aa1 100644 --- a/comcheck_api/ai/skill/SKILL.md +++ b/comcheck_api/ai/skill/SKILL.md @@ -155,9 +155,8 @@ print(result["performanceRating"]) `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 / lighting / simulation instead. - Note: `comcheck_api.list_operations()` only enumerates `building_area` - and `envelope` groups — lighting operations are not yet registered - there but are fully implemented in their respective modules. + `comcheck_api.list_operations()` enumerates the `building_area`, + `envelope`, `interior_lighting`, and `exterior_lighting` groups. ## Common patterns @@ -243,11 +242,13 @@ whole `activityUse` through `update_interior_lighting_space_in_project`. ```python from comcheck_api import project_interior_lighting_operations as il_ops from comcheck_api.defaults import get_default_interior_lighting_space_template, get_default_fixture_template -from comcheck_api.types.core_types import ActivityTypeOptions, LightingTypeOptions +from comcheck_api.types.core_types import ActivityTypeOptions +# fixtureType is the required identifier (a description string); lightingType +# is optional and marked for deprecation, so leave it unset. fixture = get_default_fixture_template() fixture.description = "Recessed LED" -fixture.lightingType = LightingTypeOptions.LED +fixture.fixtureType = "Recessed LED" fixture.fixtureWattage = 20.0 fixture.quantity = 10 diff --git a/comcheck_api/ai/skill/scripts/validate_code.py b/comcheck_api/ai/skill/scripts/validate_code.py index 9712eec..cd40890 100644 --- a/comcheck_api/ai/skill/scripts/validate_code.py +++ b/comcheck_api/ai/skill/scripts/validate_code.py @@ -29,9 +29,11 @@ def _read_input(arg: str) -> str: UNSUPPORTED_PROJECT_ATTRS = {"hvac", "renewable"} +# `wholeBldgUse` (building areas + interior lighting), `activityUse` (interior +# lighting spaces), `exteriorUse` (exterior lighting), and +# `exteriorLightingZoneType` all have operation modules. Only `fixtureSchedule` +# has no operations yet. UNSUPPORTED_LIGHTING_ATTRS = { - "activityUse", - "exteriorUse", "fixtureSchedule", } @@ -79,7 +81,7 @@ def validate(code: str) -> dict: 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 non-`wholeBldgUse` lighting subtrees. + `renewable`, or `lighting.fixtureSchedule` subtrees. """ errors: list[dict] = [] @@ -127,6 +129,8 @@ def validate(code: str) -> dict: if alias.name in { "project_envelope_operations", "project_building_area_operations", + "project_interior_lighting_operations", + "project_exterior_lighting_operations", }: op_module_aliases.add(alias.asname or alias.name) elif isinstance(node, ast.ImportFrom) and node.module == ( @@ -136,6 +140,8 @@ def validate(code: str) -> dict: if alias.name in { "project_envelope_operations", "project_building_area_operations", + "project_interior_lighting_operations", + "project_exterior_lighting_operations", }: op_module_aliases.add(alias.asname or alias.name) @@ -165,7 +171,8 @@ def validate(code: str) -> dict: "line": node.lineno, "message": ( f"`project.lighting.{node.attr}` has no operations; " - "only `lighting.wholeBldgUse[]` is editable." + "edit lighting via the building-area, interior-lighting, " + "and exterior-lighting operation modules instead." ), } ) diff --git a/comcheck_api/introspection.py b/comcheck_api/introspection.py index d30fbba..3e0df6f 100644 --- a/comcheck_api/introspection.py +++ b/comcheck_api/introspection.py @@ -23,11 +23,15 @@ from comcheck_api import ( project_building_area_operations, project_envelope_operations, + project_exterior_lighting_operations, + project_interior_lighting_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, } diff --git a/docs_site/api/operations/exterior-lighting.md b/docs_site/api/operations/exterior-lighting.md index 3f5f7e6..84b0999 100644 --- a/docs_site/api/operations/exterior-lighting.md +++ b/docs_site/api/operations/exterior-lighting.md @@ -52,11 +52,13 @@ a `TypeError` — always use the enum. ```python from comcheck_api.defaults import get_default_exterior_lighting_area_template, get_default_fixture_template -from comcheck_api.types.core_types import ExteriorUseTypeOptions, LightingTypeOptions +from comcheck_api.types.core_types import ExteriorUseTypeOptions +# fixtureType is the required identifier (a description string); lightingType +# is optional and marked for deprecation. fixture = get_default_fixture_template() fixture.description = "Parking LED" -fixture.lightingType = LightingTypeOptions.LED +fixture.fixtureType = "Parking LED" fixture.fixtureWattage = 150.0 fixture.quantity = 8 diff --git a/docs_site/api/operations/interior-lighting.md b/docs_site/api/operations/interior-lighting.md index 8746b56..4faf13d 100644 --- a/docs_site/api/operations/interior-lighting.md +++ b/docs_site/api/operations/interior-lighting.md @@ -42,17 +42,18 @@ from comcheck_api.defaults import ( get_default_interior_lighting_space_template, get_default_fixture_template, ) -from comcheck_api.types.core_types import ActivityTypeOptions, LightingTypeOptions +from comcheck_api.types.core_types import ActivityTypeOptions # A building area must exist first area = get_default_building_area_template() project = ba_ops.add_building_area_to_project(project, area) area_key = area.key -# Build the fixture +# Build the fixture. fixtureType is the required identifier (a description +# string); lightingType is optional and marked for deprecation. fixture = get_default_fixture_template() fixture.description = "Recessed LED" -fixture.lightingType = LightingTypeOptions.LED +fixture.fixtureType = "Recessed LED" fixture.fixtureWattage = 20.0 fixture.quantity = 10 diff --git a/docs_site/getting-started.md b/docs_site/getting-started.md index acf35c4..68d8dd8 100644 --- a/docs_site/getting-started.md +++ b/docs_site/getting-started.md @@ -1,7 +1,7 @@ # Getting Started !!! note "Supported Sections" - Currently, only **Building Area**, **Envelope**, and **Compliance Simulation** operations are fully supported. Interior lighting, exterior lighting, mechanical, credits, and renewable energy sections are planned but not yet implemented. See the [home page](index.md#current-status) for the full status table. + Currently, **Building Area**, **Envelope**, **Interior Lighting**, **Exterior Lighting**, and **Compliance Simulation** operations are fully supported. Mechanical, credits, and renewable energy sections are planned but not yet implemented. See the [home page](index.md#current-status) for the full status table. ## Setup @@ -105,6 +105,7 @@ client.update_project("project-id", project) ```python import time from comcheck_api import COMcheckClient +from comcheck_api.types import SimulationStatus client = COMcheckClient(api_key="your-key") project = client.get_project("project-id") @@ -112,12 +113,15 @@ project = client.get_project("project-id") # Start simulation session_id = client.start_run_simulation(project) -# Poll for completion +# Poll until terminal — only SUCCESS and FAILED are guaranteed terminal states. +# Don't poll faster than every 5 seconds. while True: status = client.get_simulation_status(session_id) - if status["status"] == "COMPLETED": + if status["status"] == SimulationStatus.SUCCESS: break - time.sleep(2) + if status["status"] == SimulationStatus.FAILED: + raise RuntimeError(f"Simulation failed: {status.get('message')}") + time.sleep(5) # Get results result = client.get_simulation_result(session_id) diff --git a/docs_site/index.md b/docs_site/index.md index 05ed169..b5623a8 100644 --- a/docs_site/index.md +++ b/docs_site/index.md @@ -10,9 +10,9 @@ This package is under active development. Here is the current support status for |---|---| | Building Area | Supported | | Envelope (roofs, walls, floors, windows, doors, skylights, thermal bridges) | Supported | +| Interior Lighting | Supported | +| Exterior Lighting | Supported | | Compliance Simulation | Supported | -| Interior Lighting | TBD | -| Exterior Lighting | TBD | | Mechanical | TBD | | Credits | TBD | | Renewable Energy | TBD | @@ -23,6 +23,7 @@ Operations and data managers for the TBD sections are not yet implemented. The u - **Type-safe** --- Pydantic models for all API inputs and outputs - **Envelope management** --- Roofs, walls, floors, windows, doors, skylights, and thermal bridges +- **Lighting management** --- Interior lighting (activity uses + fixtures) and exterior lighting (uses, zone type + fixtures) - **Compliance simulation** --- Start simulations and retrieve results programmatically - **Validation** --- JSON schema validation and Pydantic type checking at every boundary @@ -65,4 +66,4 @@ uv add comcheck_api | Module | Description | |--------|-------------| | [`comcheck_api.client`](api/client.md) | High-level client interface | -| [`comcheck_api.project_operations`](api/operations/building-area.md) | [Building area](api/operations/building-area.md) and [envelope](api/operations/envelope.md) operations | +| [`comcheck_api.project_operations`](api/operations/building-area.md) | [Building area](api/operations/building-area.md), [envelope](api/operations/envelope.md), [interior lighting](api/operations/interior-lighting.md), and [exterior lighting](api/operations/exterior-lighting.md) operations | diff --git a/examples/project_operations/exterior_lighting_operations.py b/examples/project_operations/exterior_lighting_operations.py index 09d4626..c7b65ba 100644 --- a/examples/project_operations/exterior_lighting_operations.py +++ b/examples/project_operations/exterior_lighting_operations.py @@ -36,7 +36,6 @@ from comcheck_api.types.core_types import ( ExteriorLightingZoneTypeOptions, ExteriorUseTypeOptions, - LightingTypeOptions, ) load_dotenv(override=True) @@ -67,7 +66,9 @@ # ── Step 2: Add an ExteriorUse with a fixture already populated ─────────────── fixture = get_default_fixture_template() fixture.description = "Parking LED" -fixture.fixtureType = LightingTypeOptions.LED +# fixtureType is the required identifier (a description string). lightingType +# is optional and marked for deprecation, so it is left unset here. +fixture.fixtureType = "Parking LED" fixture.fixtureWattage = 150.0 fixture.quantity = 8 @@ -113,6 +114,8 @@ for exterior_use in project.lighting.exteriorUse if exterior_use.areaDescription == "Main Parking Area" ) +if exterior_use.exteriorLightingSpace is None: + raise ValueError("ExteriorUse has no exteriorLightingSpace") existing_fixtures = list(exterior_use.exteriorLightingSpace.fixture or []) new_fixture = get_default_fixture_template() @@ -127,11 +130,7 @@ project = el_ops.update_exterior_lighting_area_in_project( project, "Main Parking Area", - { - "exteriorLightingSpace": updated_space.model_dump( - mode="python", exclude_unset=True - ) - }, + {"exteriorLightingSpace": updated_space.model_dump(mode="python")}, ) project = client.update_project(project_id, project) diff --git a/examples/project_operations/interior_lighting_operations.py b/examples/project_operations/interior_lighting_operations.py index ec1bb28..6bba1a3 100644 --- a/examples/project_operations/interior_lighting_operations.py +++ b/examples/project_operations/interior_lighting_operations.py @@ -24,7 +24,7 @@ get_default_building_area_template, get_default_fixture_template, ) -from comcheck_api.types.core_types import ActivityTypeOptions, LightingTypeOptions +from comcheck_api.types.core_types import ActivityTypeOptions from comcheck_api.utilities.common import export_to_json load_dotenv(override=True) @@ -58,9 +58,9 @@ # ── Step 2: Add an ActivityUse with a fixture already populated ─────────────── fixture = get_default_fixture_template() fixture.description = "Recessed LED" -# Todo: update schema fixtureType is required, lightingType is optional. -# fixtureType is the identifier, lightingType is the type -fixture.fixtureType = LightingTypeOptions.LED +# fixtureType is the required identifier (a description string). lightingType +# is optional and marked for deprecation, so it is left unset here. +fixture.fixtureType = "Recessed LED" fixture.fixtureWattage = 20.0 fixture.quantity = 10 @@ -113,6 +113,8 @@ for activity_use in building_area.activityUse if activity_use.areaDescription == "Open Office" ) +if activity_use.interiorLightingSpace is None: + raise ValueError("ActivityUse has no interiorLightingSpace") existing_fixtures = list(activity_use.interiorLightingSpace.fixture or []) new_fixture = get_default_fixture_template() @@ -128,11 +130,7 @@ project, area_key, "Open Office", - { - "interiorLightingSpace": updated_space.model_dump( - mode="python", exclude_unset=True - ) - }, + {"interiorLightingSpace": updated_space.model_dump(mode="python")}, ) project = client.update_project(project_id, project) if not project: