comcheck-api 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- comcheck_api/DISCLAIMER.md +24 -0
- comcheck_api/__init__.py +99 -0
- comcheck_api/ai/__init__.py +30 -0
- comcheck_api/ai/skill/SKILL.md +285 -0
- comcheck_api/ai/skill/__init__.py +5 -0
- comcheck_api/ai/skill/reference/operations.md +101 -0
- comcheck_api/ai/skill/reference/simulation.md +99 -0
- comcheck_api/ai/skill/reference/types.md +90 -0
- comcheck_api/ai/skill/scripts/__init__.py +1 -0
- comcheck_api/ai/skill/scripts/validate_code.py +210 -0
- comcheck_api/api/__init__.py +1 -0
- comcheck_api/api/api_services.py +273 -0
- comcheck_api/cli.py +136 -0
- comcheck_api/client/__init__.py +1 -0
- comcheck_api/client/comcheck_client.py +335 -0
- comcheck_api/constants/__init__.py +0 -0
- comcheck_api/constants/building_area_constants.py +35 -0
- comcheck_api/constants/common_constants.py +116 -0
- comcheck_api/constants/envelope_constants.py +250 -0
- comcheck_api/defaults.py +150 -0
- comcheck_api/exceptions.py +54 -0
- comcheck_api/introspection.py +188 -0
- comcheck_api/managers/__init__.py +0 -0
- comcheck_api/managers/components/__init__.py +0 -0
- comcheck_api/managers/components/building_area.py +11 -0
- comcheck_api/managers/components/envelope/__init__.py +0 -0
- comcheck_api/managers/components/envelope/ag_wall.py +97 -0
- comcheck_api/managers/components/envelope/bg_wall.py +39 -0
- comcheck_api/managers/components/envelope/door.py +11 -0
- comcheck_api/managers/components/envelope/floor.py +11 -0
- comcheck_api/managers/components/envelope/roof.py +30 -0
- comcheck_api/managers/components/envelope/skylight.py +11 -0
- comcheck_api/managers/components/envelope/window.py +11 -0
- comcheck_api/managers/data_manager.py +369 -0
- comcheck_api/project_operations/__init__.py +8 -0
- comcheck_api/project_operations/project_building_area_operations.py +107 -0
- comcheck_api/project_operations/project_envelope_operations.py +899 -0
- comcheck_api/schemas/comCheck.schema.json +6463 -0
- comcheck_api/types/__init__.py +49 -0
- comcheck_api/types/api_types.py +127 -0
- comcheck_api/types/common_types.py +32 -0
- comcheck_api/types/core_types.py +4198 -0
- comcheck_api/types/custom_base_model.py +314 -0
- comcheck_api/utilities/__init__.py +5 -0
- comcheck_api/utilities/common.py +50 -0
- comcheck_api/utilities/envelope_utilities.py +46 -0
- comcheck_api/utilities/id_registry.py +79 -0
- comcheck_api/utilities/project_utilities.py +60 -0
- comcheck_api/validation.py +64 -0
- comcheck_api-1.0.0.dist-info/METADATA +244 -0
- comcheck_api-1.0.0.dist-info/RECORD +54 -0
- comcheck_api-1.0.0.dist-info/WHEEL +4 -0
- comcheck_api-1.0.0.dist-info/entry_points.txt +2 -0
- comcheck_api-1.0.0.dist-info/licenses/LICENSE +24 -0
|
@@ -0,0 +1,899 @@
|
|
|
1
|
+
"""Project Envelope Operations."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Union
|
|
4
|
+
|
|
5
|
+
from comcheck_api.managers.components.envelope.ag_wall import AgWallListManager
|
|
6
|
+
from comcheck_api.types.core_types import (
|
|
7
|
+
AgWall,
|
|
8
|
+
BgWall,
|
|
9
|
+
ComBuilding,
|
|
10
|
+
Door,
|
|
11
|
+
Floor,
|
|
12
|
+
ProjectTypeOptions,
|
|
13
|
+
Roof,
|
|
14
|
+
Skylight,
|
|
15
|
+
ThermalBridgeCategoryOptions,
|
|
16
|
+
ThermalBridgeComplianceTypeOptions,
|
|
17
|
+
ThermalBridgeTypeOptions,
|
|
18
|
+
Window,
|
|
19
|
+
)
|
|
20
|
+
from comcheck_api.utilities.project_utilities import _require_building_area
|
|
21
|
+
|
|
22
|
+
# *********** Roof, agWall, bgWall and floor add/update operations ***********
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def add_roof_to_project(
|
|
26
|
+
project: ComBuilding, building_area_key: str, new_roof: Roof
|
|
27
|
+
) -> ComBuilding:
|
|
28
|
+
"""Add a new roof to the envelope in any project object using RoofListManager.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
project: The project object to modify
|
|
32
|
+
building_area_key: The building area key to validate against lighting.wholeBldgUse list
|
|
33
|
+
new_roof: The roof object to add
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Updated project object with the roof added
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
ValueError: If buildingAreaKey is not found in lighting.wholeBldgUse list
|
|
40
|
+
"""
|
|
41
|
+
# Deep clone to avoid mutating the original project
|
|
42
|
+
_require_building_area(project, building_area_key)
|
|
43
|
+
|
|
44
|
+
updated_project = project.model_copy(deep=True)
|
|
45
|
+
|
|
46
|
+
new_roof.bldgUseKey = building_area_key
|
|
47
|
+
updated_project.envelope.append_subcomponent(new_roof)
|
|
48
|
+
|
|
49
|
+
return updated_project
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def update_roof_in_project(
|
|
53
|
+
project: ComBuilding, roof_assembly_type: str, updates: dict[str, Any] | Roof
|
|
54
|
+
) -> ComBuilding:
|
|
55
|
+
"""Update a roof in the project's envelope.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
project: The project object to modify
|
|
59
|
+
roof_assembly_type: The assemblyType of the roof to update in project.envelope.roof list
|
|
60
|
+
updates: Partial updates (dict) or full Roof object to apply
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Updated project object with the roof added
|
|
64
|
+
"""
|
|
65
|
+
updated_project = project.model_copy(deep=True)
|
|
66
|
+
|
|
67
|
+
updated_project.envelope.update_subcomponent_list(
|
|
68
|
+
subcomponent_updates=updates,
|
|
69
|
+
subcomponent_id=roof_assembly_type,
|
|
70
|
+
subcomponent_name="roof",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
return updated_project
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def remove_roof_from_project(
|
|
77
|
+
project: ComBuilding, roof_assembly_type: str
|
|
78
|
+
) -> ComBuilding:
|
|
79
|
+
"""Remove a roof from the envelope in any project object.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
project: The project object to modify
|
|
83
|
+
roof_assembly_type: The assemblyType of the roof to update in project.envelope.roof list
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Project object with the roof removed
|
|
87
|
+
|
|
88
|
+
Raises:
|
|
89
|
+
ValueError: If buildingAreaKey is not found in lighting.wholeBldgUse list
|
|
90
|
+
"""
|
|
91
|
+
updated_project = project.model_copy(deep=True)
|
|
92
|
+
|
|
93
|
+
updated_project.envelope.remove_from_subcomponent_list(
|
|
94
|
+
subcomponent_id=roof_assembly_type, subcomponent_name="roof"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return updated_project
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def add_ag_wall_to_project(
|
|
101
|
+
project: ComBuilding, building_area_key: str, new_ag_wall: AgWall
|
|
102
|
+
) -> ComBuilding:
|
|
103
|
+
"""Add a new agWall to the envelope in any project object using AgWallListManager.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
project: The project object to modify
|
|
107
|
+
building_area_key: The building area key to validate against lighting.wholeBldgUse list
|
|
108
|
+
new_ag_wall: The agWall object to add
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Updated project object with the agWall added
|
|
112
|
+
|
|
113
|
+
Raises:
|
|
114
|
+
ValueError: If buildingAreaKey is not found in lighting.wholeBldgUse list
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
_require_building_area(project, building_area_key)
|
|
118
|
+
|
|
119
|
+
updated_project = project.model_copy(deep=True)
|
|
120
|
+
|
|
121
|
+
new_ag_wall.bldgUseKey = building_area_key
|
|
122
|
+
updated_project.envelope.append_subcomponent(new_ag_wall)
|
|
123
|
+
|
|
124
|
+
return updated_project
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def update_ag_wall_in_project(
|
|
128
|
+
project: ComBuilding, ag_wall_assembly_type: str, updates: dict[str, Any] | AgWall
|
|
129
|
+
) -> ComBuilding:
|
|
130
|
+
"""Update an agWall in the project's envelope.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
project: The project object to modify
|
|
134
|
+
ag_wall_assembly_type: The assemblyType of the agWall to update in project.envelope.agWall list
|
|
135
|
+
updates: Partial updates (dict) or full AgWall object to apply
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Updated project object with the agWall updated
|
|
139
|
+
"""
|
|
140
|
+
updated_project = project.model_copy(deep=True)
|
|
141
|
+
|
|
142
|
+
updated_project.envelope.update_subcomponent_list(
|
|
143
|
+
subcomponent_updates=updates,
|
|
144
|
+
subcomponent_id=ag_wall_assembly_type,
|
|
145
|
+
subcomponent_name="agWall",
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
return updated_project
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def remove_ag_wall_from_project(
|
|
152
|
+
project: ComBuilding, ag_wall_assembly_type: str
|
|
153
|
+
) -> ComBuilding:
|
|
154
|
+
"""Remove an agWall from the envelope in any project object.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
project: The project object to modify
|
|
158
|
+
ag_wall_assembly_type: The assemblyType of the agWall to update in project.envelope.agWall list
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Project object with the agWall removed
|
|
162
|
+
|
|
163
|
+
Raises:
|
|
164
|
+
ValueError: If buildingAreaKey is not found in lighting.wholeBldgUse list
|
|
165
|
+
"""
|
|
166
|
+
updated_project = project.model_copy(deep=True)
|
|
167
|
+
|
|
168
|
+
updated_project.envelope.remove_from_subcomponent_list(
|
|
169
|
+
subcomponent_id=ag_wall_assembly_type, subcomponent_name="agWall"
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
return updated_project
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def add_bg_wall_to_project(
|
|
176
|
+
project: ComBuilding, building_area_key: str, new_bg_wall: BgWall
|
|
177
|
+
) -> ComBuilding:
|
|
178
|
+
"""Add a new bgWall to the envelope in any project object using BgWallListManager.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
project: The project object to modify
|
|
182
|
+
building_area_key: The building area key to validate against lighting.wholeBldgUse list
|
|
183
|
+
new_bg_wall: The bgWall object to add
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Updated project object with the bgWall added
|
|
187
|
+
|
|
188
|
+
Raises:
|
|
189
|
+
ValueError: If buildingAreaKey is not found in lighting.wholeBldgUse list
|
|
190
|
+
"""
|
|
191
|
+
_require_building_area(project, building_area_key)
|
|
192
|
+
|
|
193
|
+
updated_project = project.model_copy(deep=True)
|
|
194
|
+
|
|
195
|
+
new_bg_wall.bldgUseKey = building_area_key
|
|
196
|
+
updated_project.envelope.append_subcomponent(new_bg_wall)
|
|
197
|
+
|
|
198
|
+
return updated_project
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def update_bg_wall_in_project(
|
|
202
|
+
project: ComBuilding, bg_wall_assembly_type: str, updates: dict[str, Any] | BgWall
|
|
203
|
+
) -> ComBuilding:
|
|
204
|
+
"""Update an bgWall in the project's envelope.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
project: The project object to modify
|
|
208
|
+
bg_wall_assembly_type: The assemblyType of the bgWall to update in project.envelope.bgWall list
|
|
209
|
+
updates: Partial updates (dict) or full BgWall object to apply
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
Updated project object with the bgWall updated
|
|
213
|
+
"""
|
|
214
|
+
updated_project = project.model_copy(deep=True)
|
|
215
|
+
|
|
216
|
+
updated_project.envelope.update_subcomponent_list(
|
|
217
|
+
subcomponent_updates=updates,
|
|
218
|
+
subcomponent_id=bg_wall_assembly_type,
|
|
219
|
+
subcomponent_name="bgWall",
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
return updated_project
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def remove_bg_wall_from_project(
|
|
226
|
+
project: ComBuilding, bg_wall_assembly_type: str
|
|
227
|
+
) -> ComBuilding:
|
|
228
|
+
"""Remove an bgWall from the envelope in any project object.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
project: The project object to modify
|
|
232
|
+
bg_wall_assembly_type: The assemblyType of the bgWall to update in project.envelope.bgWall list
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
Project object with the bgWall removed
|
|
236
|
+
|
|
237
|
+
Raises:
|
|
238
|
+
ValueError: If buildingAreaKey is not found in lighting.wholeBldgUse list
|
|
239
|
+
"""
|
|
240
|
+
updated_project = project.model_copy(deep=True)
|
|
241
|
+
|
|
242
|
+
updated_project.envelope.remove_from_subcomponent_list(
|
|
243
|
+
subcomponent_id=bg_wall_assembly_type, subcomponent_name="bgWall"
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
return updated_project
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def add_floor_to_project(
|
|
250
|
+
project: ComBuilding, building_area_key: str, new_floor: Floor
|
|
251
|
+
) -> ComBuilding:
|
|
252
|
+
"""Add a new floor to the envelope in any project object using FloorListManager.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
project: The project object to modify
|
|
256
|
+
building_area_key: The building area key to validate against lighting.wholeBldgUse list
|
|
257
|
+
new_floor: The floor object to add
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
Updated project object with the floor added
|
|
261
|
+
|
|
262
|
+
Raises:
|
|
263
|
+
ValueError: If buildingAreaKey is not found in lighting.wholeBldgUse list
|
|
264
|
+
"""
|
|
265
|
+
_require_building_area(project, building_area_key)
|
|
266
|
+
|
|
267
|
+
updated_project = project.model_copy(deep=True)
|
|
268
|
+
|
|
269
|
+
new_floor.bldgUseKey = building_area_key
|
|
270
|
+
updated_project.envelope.append_subcomponent(new_floor)
|
|
271
|
+
|
|
272
|
+
return updated_project
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def update_floor_in_project(
|
|
276
|
+
project: ComBuilding, floor_assembly_type: str, updates: dict[str, Any] | Floor
|
|
277
|
+
) -> ComBuilding:
|
|
278
|
+
"""Update an floor in the project's envelope.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
project: The project object to modify
|
|
282
|
+
floor_assembly_type: The assemblyType of the floor to update in project.envelope.floor list
|
|
283
|
+
updates: Partial updates (dict) or full Floor object to apply
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
Updated project object with the floor updated
|
|
287
|
+
"""
|
|
288
|
+
updated_project = project.model_copy(deep=True)
|
|
289
|
+
|
|
290
|
+
updated_project.envelope.update_subcomponent_list(
|
|
291
|
+
subcomponent_updates=updates,
|
|
292
|
+
subcomponent_id=floor_assembly_type,
|
|
293
|
+
subcomponent_name="floor",
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
return updated_project
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def remove_floor_from_project(
|
|
300
|
+
project: ComBuilding, floor_assembly_type: str
|
|
301
|
+
) -> ComBuilding:
|
|
302
|
+
"""Remove an bgWall from the envelope in any project object.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
project: The project object to modify
|
|
306
|
+
floor_assembly_type: The assemblyType of the floor to remove in project.envelope.floor list
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
Project object with the floor removed
|
|
310
|
+
|
|
311
|
+
Raises:
|
|
312
|
+
ValueError: If buildingAreaKey is not found in lighting.wholeBldgUse list
|
|
313
|
+
"""
|
|
314
|
+
updated_project = project.model_copy(deep=True)
|
|
315
|
+
|
|
316
|
+
updated_project.envelope.remove_from_subcomponent_list(
|
|
317
|
+
subcomponent_id=floor_assembly_type, subcomponent_name="floor"
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
return updated_project
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
# *********** Assemblies or Components attached to wall or roof ***********
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def add_skylight_to_project(
|
|
327
|
+
project: ComBuilding,
|
|
328
|
+
building_area_key: str,
|
|
329
|
+
new_skylight: Skylight,
|
|
330
|
+
roof: Roof | None = None,
|
|
331
|
+
) -> ComBuilding:
|
|
332
|
+
"""Add a new skylight to a specific roof in the envelope.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
project: The project object to modify
|
|
336
|
+
building_area_key: The building area key to validate against lighting.wholeBldgUse list
|
|
337
|
+
new_skylight: The skylight object to add
|
|
338
|
+
roof: The roof object to which the skylight will be added (required for non-alteration projects)
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
Updated project object with the skylight added to the specified roof
|
|
342
|
+
|
|
343
|
+
Raises:
|
|
344
|
+
ValueError: If buildingAreaKey is not found or if roof's bldgUseKey doesn't match buildingAreaKey
|
|
345
|
+
"""
|
|
346
|
+
updated_project = project.model_copy(deep=True)
|
|
347
|
+
|
|
348
|
+
_require_building_area(project, building_area_key)
|
|
349
|
+
updated_project.require_attribute("envelope")
|
|
350
|
+
|
|
351
|
+
new_skylight.bldgUseKey = building_area_key
|
|
352
|
+
|
|
353
|
+
if updated_project.projectType != ProjectTypeOptions.ALTERATION:
|
|
354
|
+
# Add to existing roof for non-alteration projects
|
|
355
|
+
if roof is None:
|
|
356
|
+
raise ValueError("Roof must be specified for non-alteration projects.")
|
|
357
|
+
|
|
358
|
+
if (roof_use_key := getattr(roof, "bldgUseKey")) != building_area_key:
|
|
359
|
+
raise ValueError(
|
|
360
|
+
f"Roof's bldgUseKey '{roof_use_key}' does not match buildingAreaKey '{building_area_key}'."
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
roof.append_subcomponent(new_skylight, "skylight")
|
|
364
|
+
updated_project.envelope.update_subcomponent_list(
|
|
365
|
+
subcomponent_updates=roof,
|
|
366
|
+
subcomponent_id=getattr(roof, "assemblyType"),
|
|
367
|
+
subcomponent_name=roof.json_key(),
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
return updated_project
|
|
371
|
+
else:
|
|
372
|
+
# Alteration projects: add orphaned skylight directly
|
|
373
|
+
updated_project.envelope.append_subcomponent(new_skylight, "skylight")
|
|
374
|
+
return updated_project
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def remove_skylight_from_project(
|
|
378
|
+
project: ComBuilding,
|
|
379
|
+
skylight_assembly_type: str,
|
|
380
|
+
) -> ComBuilding:
|
|
381
|
+
"""Add a new skylight to a specific roof in the envelope.
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
project: ComBuilding,
|
|
385
|
+
skylight_assembly_type: str
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
Updated project object with the skylight added to the specified roof
|
|
389
|
+
|
|
390
|
+
Raises:
|
|
391
|
+
ValueError: If buildingAreaKey is not found or if roof's bldgUseKey doesn't match buildingAreaKey
|
|
392
|
+
"""
|
|
393
|
+
updated_project = project.model_copy(deep=True)
|
|
394
|
+
|
|
395
|
+
# Find where the skylight is located
|
|
396
|
+
location_type, roof_index, _ = _find_component_location(
|
|
397
|
+
project, "skylight", skylight_assembly_type
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
# Update skylight based on its location
|
|
401
|
+
if location_type == "orphaned":
|
|
402
|
+
parent_obj = updated_project.envelope
|
|
403
|
+
elif location_type == "roof":
|
|
404
|
+
parent_obj = updated_project.envelope.roof[roof_index] # type: ignore
|
|
405
|
+
parent_obj.remove_from_subcomponent_list(
|
|
406
|
+
subcomponent_id=skylight_assembly_type, subcomponent_name="skylight"
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
return updated_project
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def update_skylight_in_project(
|
|
413
|
+
project: ComBuilding,
|
|
414
|
+
skylight_assembly_type: str,
|
|
415
|
+
updates: dict[str, Any] | Skylight,
|
|
416
|
+
) -> ComBuilding:
|
|
417
|
+
"""Update a skylight in the project's envelope.
|
|
418
|
+
|
|
419
|
+
Skylights can exist in two locations:
|
|
420
|
+
1. Orphaned skylights in envelope.skylight (ALTERATION projects)
|
|
421
|
+
2. Skylights nested in roof list
|
|
422
|
+
|
|
423
|
+
Args:
|
|
424
|
+
project: The project object to modify
|
|
425
|
+
skylight_assembly_type: The assemblyType of the skylight to update
|
|
426
|
+
updates: Partial updates (dict) or full Skylight object to apply
|
|
427
|
+
|
|
428
|
+
Returns:
|
|
429
|
+
Updated project object with the skylight updated
|
|
430
|
+
|
|
431
|
+
Raises:
|
|
432
|
+
ValueError: If the skylight is not found in any location
|
|
433
|
+
"""
|
|
434
|
+
updated_project = project.model_copy(deep=True)
|
|
435
|
+
|
|
436
|
+
# Find where the skylight is located
|
|
437
|
+
location_type, roof_index, _ = _find_component_location(
|
|
438
|
+
project, "skylight", skylight_assembly_type
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
# Update skylight based on its location
|
|
442
|
+
if location_type == "orphaned":
|
|
443
|
+
parent_obj = updated_project.envelope
|
|
444
|
+
elif location_type == "roof":
|
|
445
|
+
parent_obj = updated_project.envelope.roof[roof_index] # type: ignore
|
|
446
|
+
parent_obj.update_subcomponent_list(
|
|
447
|
+
subcomponent_updates=updates,
|
|
448
|
+
subcomponent_id=skylight_assembly_type,
|
|
449
|
+
subcomponent_name="skylight",
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
return updated_project
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def add_window_to_project(
|
|
456
|
+
project: ComBuilding,
|
|
457
|
+
building_area_key: str,
|
|
458
|
+
new_window: Window,
|
|
459
|
+
wall: AgWall | BgWall | None = None,
|
|
460
|
+
) -> ComBuilding:
|
|
461
|
+
"""Add a new window to a wall (AgWall or BgWall) in the envelope.
|
|
462
|
+
|
|
463
|
+
Args:
|
|
464
|
+
project: The project object to modify
|
|
465
|
+
building_area_key: The building area key to validate against lighting.wholeBldgUse list
|
|
466
|
+
new_window: The window object to add
|
|
467
|
+
wall: The AgWall or BgWall object to which the window will be added - required for non-alteration projects
|
|
468
|
+
|
|
469
|
+
Returns:
|
|
470
|
+
Updated project object with the window added to the specified wall
|
|
471
|
+
|
|
472
|
+
Raises:
|
|
473
|
+
ValueError: If buildingAreaKey is not found or if wall's bldgUseKey doesn't match buildingAreaKey
|
|
474
|
+
"""
|
|
475
|
+
updated_project = project.model_copy(deep=True)
|
|
476
|
+
|
|
477
|
+
_require_building_area(project, building_area_key)
|
|
478
|
+
updated_project.require_attribute("envelope")
|
|
479
|
+
|
|
480
|
+
new_window.bldgUseKey = building_area_key
|
|
481
|
+
|
|
482
|
+
if updated_project.projectType != ProjectTypeOptions.ALTERATION:
|
|
483
|
+
if wall is None:
|
|
484
|
+
raise ValueError(
|
|
485
|
+
"Wall (AgWall or BgWall) must be specified for non-alteration projects."
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
if (wall_use_key := getattr(wall, "bldgUseKey")) != building_area_key:
|
|
489
|
+
raise ValueError(
|
|
490
|
+
f"Wall's bldgUseKey '{wall_use_key}' does not match buildingAreaKey '{building_area_key}'."
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
wall.append_subcomponent(new_window, "window")
|
|
494
|
+
updated_project.envelope.update_subcomponent_list(
|
|
495
|
+
subcomponent_updates=wall,
|
|
496
|
+
subcomponent_id=getattr(wall, "assemblyType"),
|
|
497
|
+
subcomponent_name=wall.json_key(),
|
|
498
|
+
)
|
|
499
|
+
return updated_project
|
|
500
|
+
else:
|
|
501
|
+
# Alteration projects: orphaned windows
|
|
502
|
+
updated_project.envelope.append_subcomponent(new_window, "window")
|
|
503
|
+
return updated_project
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def remove_window_from_project(
|
|
507
|
+
project: ComBuilding,
|
|
508
|
+
window_assembly_type: str,
|
|
509
|
+
) -> ComBuilding:
|
|
510
|
+
"""Remove a window from the project.
|
|
511
|
+
|
|
512
|
+
Args:
|
|
513
|
+
project: ComBuilding,
|
|
514
|
+
window_assembly_type: str
|
|
515
|
+
|
|
516
|
+
Returns:
|
|
517
|
+
Updated project object with the window removed
|
|
518
|
+
"""
|
|
519
|
+
updated_project = project.model_copy(deep=True)
|
|
520
|
+
|
|
521
|
+
# Find where the window is located
|
|
522
|
+
location_type, wall_index, _ = _find_component_location(
|
|
523
|
+
project, "window", window_assembly_type
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
# Update window based on its location
|
|
527
|
+
if location_type == "orphaned":
|
|
528
|
+
parent_obj = updated_project.envelope
|
|
529
|
+
elif location_type in ["agWall", "bgWall"]:
|
|
530
|
+
parent_obj = updated_project.envelope.get_by_path(f"{location_type}[{wall_index}]") # type: ignore[assignment]
|
|
531
|
+
parent_obj.remove_from_subcomponent_list(
|
|
532
|
+
subcomponent_id=window_assembly_type, subcomponent_name="window"
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
return updated_project
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def update_window_in_project(
|
|
539
|
+
project: ComBuilding, window_assembly_type: str, updates: dict[str, Any] | Window
|
|
540
|
+
) -> ComBuilding:
|
|
541
|
+
"""Update a window in the project's envelope.
|
|
542
|
+
|
|
543
|
+
Windows can exist in three locations:
|
|
544
|
+
1. Orphaned windows in envelope.window (ALTERATION projects)
|
|
545
|
+
2. Windows nested in agWall list
|
|
546
|
+
3. Windows nested in bgWall list
|
|
547
|
+
|
|
548
|
+
Args:
|
|
549
|
+
project: The project object to modify
|
|
550
|
+
window_assembly_type: The assemblyType of the window to update
|
|
551
|
+
updates: Partial updates (dict) or full Window object to apply
|
|
552
|
+
|
|
553
|
+
Returns:
|
|
554
|
+
Updated project object with the window updated
|
|
555
|
+
|
|
556
|
+
Raises:
|
|
557
|
+
ValueError: If the window is not found in any location
|
|
558
|
+
"""
|
|
559
|
+
updated_project = project.model_copy(deep=True)
|
|
560
|
+
|
|
561
|
+
# Find where the window is located
|
|
562
|
+
location_type, wall_index, _ = _find_component_location(
|
|
563
|
+
project, "window", window_assembly_type
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
# Update window based on its location
|
|
567
|
+
if location_type == "orphaned":
|
|
568
|
+
parent_obj = updated_project.envelope
|
|
569
|
+
elif location_type in ["agWall", "bgWall"]:
|
|
570
|
+
parent_obj = updated_project.envelope.get_by_path(f"{location_type}[{wall_index}]") # type: ignore[assignment]
|
|
571
|
+
parent_obj.update_subcomponent_list(
|
|
572
|
+
subcomponent_updates=updates,
|
|
573
|
+
subcomponent_id=window_assembly_type,
|
|
574
|
+
subcomponent_name="window",
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
return updated_project
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def add_door_to_project(
|
|
581
|
+
project: ComBuilding,
|
|
582
|
+
building_area_key: str,
|
|
583
|
+
new_door: Door,
|
|
584
|
+
wall: AgWall | BgWall | None = None,
|
|
585
|
+
) -> ComBuilding:
|
|
586
|
+
"""Add a new door to a wall (AgWall or BgWall) in the envelope.
|
|
587
|
+
|
|
588
|
+
Args:
|
|
589
|
+
project: The project object to modify
|
|
590
|
+
building_area_key: The building area key to validate against lighting.wholeBldgUse list
|
|
591
|
+
new_door: The door object to add
|
|
592
|
+
wall: The AgWall or BgWall object to which the door will be added - required for non-alteration projects
|
|
593
|
+
|
|
594
|
+
Returns:
|
|
595
|
+
Updated project object with the door added to the specified wall
|
|
596
|
+
|
|
597
|
+
Raises:
|
|
598
|
+
ValueError: If buildingAreaKey is not found or if wall's bldgUseKey doesn't match buildingAreaKey
|
|
599
|
+
"""
|
|
600
|
+
|
|
601
|
+
updated_project = project.model_copy(deep=True)
|
|
602
|
+
|
|
603
|
+
_require_building_area(project, building_area_key)
|
|
604
|
+
updated_project.require_attribute("envelope")
|
|
605
|
+
|
|
606
|
+
new_door.bldgUseKey = building_area_key
|
|
607
|
+
|
|
608
|
+
if updated_project.projectType != ProjectTypeOptions.ALTERATION:
|
|
609
|
+
# Add to existing wall for non-alteration projects
|
|
610
|
+
if not wall:
|
|
611
|
+
raise ValueError(
|
|
612
|
+
"Wall (AgWall or BgWall) must be specified for non-alteration projects."
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
if (wall_use_key := getattr(wall, "bldgUseKey")) != building_area_key:
|
|
616
|
+
raise ValueError(
|
|
617
|
+
f"Wall's bldgUseKey '{wall_use_key}' does not match buildingAreaKey '{building_area_key}'."
|
|
618
|
+
)
|
|
619
|
+
wall.append_subcomponent(new_door, "door")
|
|
620
|
+
updated_project.envelope.update_subcomponent_list(
|
|
621
|
+
subcomponent_updates=wall,
|
|
622
|
+
subcomponent_id=getattr(wall, "assemblyType"),
|
|
623
|
+
subcomponent_name=wall.json_key(),
|
|
624
|
+
)
|
|
625
|
+
return updated_project
|
|
626
|
+
else:
|
|
627
|
+
updated_project.envelope.append_subcomponent(new_door)
|
|
628
|
+
|
|
629
|
+
return updated_project
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def remove_door_from_project(
|
|
633
|
+
project: ComBuilding,
|
|
634
|
+
door_assembly_type: str,
|
|
635
|
+
) -> ComBuilding:
|
|
636
|
+
"""Remove a door from the project.
|
|
637
|
+
|
|
638
|
+
Args:
|
|
639
|
+
project: ComBuilding,
|
|
640
|
+
door_assembly_type: str
|
|
641
|
+
|
|
642
|
+
Returns:
|
|
643
|
+
Updated project object with the door removed
|
|
644
|
+
"""
|
|
645
|
+
updated_project = project.model_copy(deep=True)
|
|
646
|
+
|
|
647
|
+
# Find where the door is located
|
|
648
|
+
location_type, wall_index, _ = _find_component_location(
|
|
649
|
+
project, "door", door_assembly_type
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
# Remove door based on its location
|
|
653
|
+
if location_type == "orphaned":
|
|
654
|
+
parent_obj = updated_project.envelope
|
|
655
|
+
elif location_type in ["agWall", "bgWall"]:
|
|
656
|
+
parent_obj = updated_project.envelope.get_by_path(f"{location_type}[{wall_index}]") # type: ignore[assignment]
|
|
657
|
+
parent_obj.remove_from_subcomponent_list(
|
|
658
|
+
subcomponent_id=door_assembly_type, subcomponent_name="door"
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
return updated_project
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def update_door_in_project(
|
|
665
|
+
project: ComBuilding, door_assembly_type: str, updates: dict[str, Any] | Door
|
|
666
|
+
) -> ComBuilding:
|
|
667
|
+
"""Update a door in the project's envelope.
|
|
668
|
+
|
|
669
|
+
Doors can exist in three locations:
|
|
670
|
+
1. Orphaned doors in envelope.door (ALTERATION projects)
|
|
671
|
+
2. Doors nested in agWall list
|
|
672
|
+
3. Doors nested in bgWall list
|
|
673
|
+
|
|
674
|
+
Args:
|
|
675
|
+
project: The project object to modify
|
|
676
|
+
door_assembly_type: The assemblyType of the door to update
|
|
677
|
+
updates: Partial updates (dict) or full Door object to apply
|
|
678
|
+
|
|
679
|
+
Returns:
|
|
680
|
+
Updated project object with the door updated
|
|
681
|
+
|
|
682
|
+
Raises:
|
|
683
|
+
ValueError: If the door is not found in any location
|
|
684
|
+
"""
|
|
685
|
+
updated_project = project.model_copy(deep=True)
|
|
686
|
+
|
|
687
|
+
# Find where the door is located
|
|
688
|
+
location_type, wall_index, _ = _find_component_location(
|
|
689
|
+
project, "door", door_assembly_type
|
|
690
|
+
)
|
|
691
|
+
|
|
692
|
+
# Update door based on its location
|
|
693
|
+
if location_type == "orphaned":
|
|
694
|
+
parent_obj = updated_project.envelope
|
|
695
|
+
elif location_type in ["agWall", "bgWall"]:
|
|
696
|
+
parent_obj = updated_project.envelope.get_by_path(f"{location_type}[{wall_index}]") # type: ignore[assignment]
|
|
697
|
+
parent_obj.update_subcomponent_list(
|
|
698
|
+
subcomponent_updates=updates,
|
|
699
|
+
subcomponent_id=door_assembly_type,
|
|
700
|
+
subcomponent_name="door",
|
|
701
|
+
)
|
|
702
|
+
|
|
703
|
+
return updated_project
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
# TODO: verify when thermal bridges are needed
|
|
707
|
+
def add_thermal_bridge_to_project(
|
|
708
|
+
project: ComBuilding,
|
|
709
|
+
building_area_key: str,
|
|
710
|
+
ag_wall: AgWall,
|
|
711
|
+
thermal_bridge_type: (
|
|
712
|
+
ThermalBridgeTypeOptions | None
|
|
713
|
+
) = ThermalBridgeTypeOptions.THERMAL_BRIDGE_OTHER,
|
|
714
|
+
thermal_bridge_category: Union[
|
|
715
|
+
ThermalBridgeCategoryOptions | None
|
|
716
|
+
] = ThermalBridgeCategoryOptions.THERMAL_BRIDGE_UNCATEGORIZED,
|
|
717
|
+
thermal_bridge_compliance_type: Union[
|
|
718
|
+
ThermalBridgeComplianceTypeOptions | None
|
|
719
|
+
] = ThermalBridgeComplianceTypeOptions.THERMAL_BRIDGE_NON_PRESCRIPTIVE,
|
|
720
|
+
psi_factor: float = 0.0,
|
|
721
|
+
chi_factor: float = 0.0,
|
|
722
|
+
thermal_bridge_length: float = 0.0,
|
|
723
|
+
number_of_points: int = 0,
|
|
724
|
+
) -> ComBuilding:
|
|
725
|
+
"""Add a new thermal bridge to an AgWall in the envelope.
|
|
726
|
+
|
|
727
|
+
Note: Thermal bridges can only be added to AgWalls and cannot be orphaned.
|
|
728
|
+
|
|
729
|
+
Args:
|
|
730
|
+
project: The project object to modify
|
|
731
|
+
building_area_key: The building area key to validate against lighting.wholeBldgUse list
|
|
732
|
+
ag_wall: The AgWall object to which the thermal bridge will be added (required)
|
|
733
|
+
thermal_bridge_type: Type of thermal bridge (defaults to 'THERMAL_BRIDGE_OTHER')
|
|
734
|
+
thermal_bridge_category: Category of thermal bridge (defaults to 'THERMAL_BRIDGE_UNCATEGORIZED')
|
|
735
|
+
thermal_bridge_compliance_type: Compliance type (defaults to 'THERMAL_BRIDGE_NON_PRESCRIPTIVE')
|
|
736
|
+
psi_factor: Psi factor (defaults to 0.0)
|
|
737
|
+
chi_factor: Chi factor (defaults to 0.0)
|
|
738
|
+
thermal_bridge_length: Length of thermal bridge (defaults to 0.0)
|
|
739
|
+
number_of_points: Number of points for thermal bridge (defaults to 0)
|
|
740
|
+
|
|
741
|
+
Returns:
|
|
742
|
+
Updated project object with the thermal bridge added to the specified AgWall
|
|
743
|
+
|
|
744
|
+
Raises:
|
|
745
|
+
ValueError: If buildingAreaKey is not found, agWall is not provided, or wall's bldgUseKey doesn't match buildingAreaKey
|
|
746
|
+
"""
|
|
747
|
+
updated_project = project.model_copy(deep=True)
|
|
748
|
+
|
|
749
|
+
_require_building_area(
|
|
750
|
+
project, building_area_key
|
|
751
|
+
) # assuming this works with models
|
|
752
|
+
|
|
753
|
+
if ag_wall is None:
|
|
754
|
+
raise ValueError("AgWall must be specified for thermal bridges.")
|
|
755
|
+
|
|
756
|
+
if getattr(ag_wall, "bldgUseKey", None) != building_area_key:
|
|
757
|
+
raise ValueError(
|
|
758
|
+
f"AgWall's bldgUseKey '{getattr(ag_wall, 'bldgUseKey')}' does not match buildingAreaKey '{building_area_key}'."
|
|
759
|
+
)
|
|
760
|
+
|
|
761
|
+
envelope = getattr(updated_project, "envelope", None)
|
|
762
|
+
if envelope is None:
|
|
763
|
+
raise ValueError("Envelope not found in project.")
|
|
764
|
+
|
|
765
|
+
ag_wall_list = getattr(envelope, "agWall", None)
|
|
766
|
+
if not isinstance(ag_wall_list, list):
|
|
767
|
+
raise ValueError("agWall list not found or invalid.")
|
|
768
|
+
|
|
769
|
+
# Find index of matching agWall by assemblyType (attribute access)
|
|
770
|
+
ag_wall_index = next(
|
|
771
|
+
(
|
|
772
|
+
i
|
|
773
|
+
for i, w in enumerate(ag_wall_list)
|
|
774
|
+
if getattr(w, "assemblyType", None)
|
|
775
|
+
== getattr(ag_wall, "assemblyType", None)
|
|
776
|
+
),
|
|
777
|
+
-1,
|
|
778
|
+
)
|
|
779
|
+
|
|
780
|
+
if ag_wall_index == -1:
|
|
781
|
+
raise ValueError("Specified agWall not found in project.")
|
|
782
|
+
|
|
783
|
+
# Use your manager on Pydantic list
|
|
784
|
+
manager = AgWallListManager(ag_wall_list)
|
|
785
|
+
updated_wall = manager.add_new_thermal_bridge(
|
|
786
|
+
ag_wall_list[ag_wall_index],
|
|
787
|
+
thermal_bridge_type,
|
|
788
|
+
thermal_bridge_category,
|
|
789
|
+
thermal_bridge_compliance_type,
|
|
790
|
+
psi_factor,
|
|
791
|
+
chi_factor,
|
|
792
|
+
thermal_bridge_length,
|
|
793
|
+
number_of_points,
|
|
794
|
+
)
|
|
795
|
+
|
|
796
|
+
# Replace the agWall in the list
|
|
797
|
+
ag_wall_list[ag_wall_index] = updated_wall
|
|
798
|
+
|
|
799
|
+
# If envelope.agWall is a Pydantic field with validation, reassign updated list
|
|
800
|
+
envelope.agWall = ag_wall_list
|
|
801
|
+
|
|
802
|
+
# If updated_project.envelope is frozen or uses validators, reassign as needed
|
|
803
|
+
updated_project.envelope = envelope
|
|
804
|
+
|
|
805
|
+
return updated_project
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
# *********** Helper Functions ***********
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
def _find_component_location(
|
|
812
|
+
project: ComBuilding, component_type: str, assembly_type: str
|
|
813
|
+
) -> tuple[str, int | None, int | None]:
|
|
814
|
+
"""Find the location of a window, door, or skylight component.
|
|
815
|
+
|
|
816
|
+
Components can exist in different locations:
|
|
817
|
+
- Windows/Doors: orphaned (envelope.window/door) or nested in agWall/bgWall
|
|
818
|
+
- Skylights: orphaned (envelope.skylight) or nested in roof
|
|
819
|
+
|
|
820
|
+
Args:
|
|
821
|
+
project: The project to search in
|
|
822
|
+
component_type: Either "window", "door", or "skylight"
|
|
823
|
+
assembly_type: The assemblyType to search for
|
|
824
|
+
|
|
825
|
+
Returns:
|
|
826
|
+
Tuple of (location_type, parent_index, component_index) where:
|
|
827
|
+
- location_type is "orphaned", "agWall", "bgWall", or "roof"
|
|
828
|
+
- parent_index is the index in agWall/bgWall/roof list (None for orphaned)
|
|
829
|
+
- component_index is the index in the component list
|
|
830
|
+
|
|
831
|
+
Raises:
|
|
832
|
+
ValueError: If component is not found anywhere
|
|
833
|
+
"""
|
|
834
|
+
# Check orphaned components first
|
|
835
|
+
|
|
836
|
+
if project.projectType == ProjectTypeOptions.ALTERATION:
|
|
837
|
+
orphaned_list = getattr(project.envelope, component_type, [])
|
|
838
|
+
component_index = next(
|
|
839
|
+
(
|
|
840
|
+
i
|
|
841
|
+
for i, c in enumerate(orphaned_list)
|
|
842
|
+
if getattr(c, "assemblyType", None) == assembly_type
|
|
843
|
+
),
|
|
844
|
+
-1,
|
|
845
|
+
)
|
|
846
|
+
if component_index != -1:
|
|
847
|
+
return ("orphaned", None, component_index)
|
|
848
|
+
else:
|
|
849
|
+
# For windows/doors: check agWall and bgWall components
|
|
850
|
+
if component_type in ("window", "door"):
|
|
851
|
+
# Check agWall components
|
|
852
|
+
for ag_wall_index, ag_wall in enumerate(project.envelope.agWall):
|
|
853
|
+
wall_components = getattr(ag_wall, component_type, [])
|
|
854
|
+
component_index = next(
|
|
855
|
+
(
|
|
856
|
+
i
|
|
857
|
+
for i, c in enumerate(wall_components)
|
|
858
|
+
if getattr(c, "assemblyType", None) == assembly_type
|
|
859
|
+
),
|
|
860
|
+
-1,
|
|
861
|
+
)
|
|
862
|
+
if component_index != -1:
|
|
863
|
+
return ("agWall", ag_wall_index, component_index)
|
|
864
|
+
|
|
865
|
+
# Check bgWall components
|
|
866
|
+
for bg_wall_index, bg_wall in enumerate(project.envelope.bgWall):
|
|
867
|
+
wall_components = getattr(bg_wall, component_type, [])
|
|
868
|
+
component_index = next(
|
|
869
|
+
(
|
|
870
|
+
i
|
|
871
|
+
for i, c in enumerate(wall_components)
|
|
872
|
+
if getattr(c, "assemblyType", None) == assembly_type
|
|
873
|
+
),
|
|
874
|
+
-1,
|
|
875
|
+
)
|
|
876
|
+
if component_index != -1:
|
|
877
|
+
return ("bgWall", bg_wall_index, component_index)
|
|
878
|
+
|
|
879
|
+
# For skylights: check roof components
|
|
880
|
+
elif component_type == "skylight":
|
|
881
|
+
for roof_index, roof in enumerate(project.envelope.roof or []):
|
|
882
|
+
roof_skylights = getattr(roof, "skylight", [])
|
|
883
|
+
skylight_index = next(
|
|
884
|
+
(
|
|
885
|
+
i
|
|
886
|
+
for i, s in enumerate(roof_skylights)
|
|
887
|
+
if getattr(s, "assemblyType", None) == assembly_type
|
|
888
|
+
),
|
|
889
|
+
-1,
|
|
890
|
+
)
|
|
891
|
+
if skylight_index != -1:
|
|
892
|
+
return ("roof", roof_index, skylight_index)
|
|
893
|
+
|
|
894
|
+
raise ValueError(
|
|
895
|
+
f"{component_type.capitalize()} with assemblyType '{assembly_type}' not found in project"
|
|
896
|
+
)
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
# *********** End of Project Envelope Operations ***********
|