From 3ad47fcf2009d0e91f368830406e8a2e0e57464f Mon Sep 17 00:00:00 2001 From: peteryefi Date: Wed, 23 Oct 2024 12:15:03 -0400 Subject: [PATCH] Added fan coil units --- metamenth/enumerations/__init__.py | 2 + metamenth/enumerations/fcu_pipe_system.py | 14 +++ metamenth/enumerations/fcu_type.py | 14 +++ metamenth/misc/validate.py | 4 +- .../hvac_components/duct_connection.py | 28 +++--- .../hvac_components/fan_coil_unit.py | 96 +++++++++++++++++++ .../subsystem/test_building_control_system.py | 56 +++++++++++ 7 files changed, 202 insertions(+), 12 deletions(-) create mode 100644 metamenth/enumerations/fcu_pipe_system.py create mode 100644 metamenth/enumerations/fcu_type.py create mode 100644 metamenth/subsystem/hvac_components/fan_coil_unit.py diff --git a/metamenth/enumerations/__init__.py b/metamenth/enumerations/__init__.py index 75c2663..6ef32b4 100644 --- a/metamenth/enumerations/__init__.py +++ b/metamenth/enumerations/__init__.py @@ -60,3 +60,5 @@ from metamenth.enumerations.terrain_type import TerrainType from metamenth.enumerations.solar_distribution_type import SolarDistributionType from metamenth.enumerations.relationship_name import RelationshipName +from metamenth.enumerations.fcu_type import FCUType +from metamenth.enumerations.fcu_pipe_system import FCUPipeSystem diff --git a/metamenth/enumerations/fcu_pipe_system.py b/metamenth/enumerations/fcu_pipe_system.py new file mode 100644 index 0000000..24cdd3e --- /dev/null +++ b/metamenth/enumerations/fcu_pipe_system.py @@ -0,0 +1,14 @@ +from metamenth.enumerations.abstract_enum import AbstractEnum + + +class FCUPipeSystem(AbstractEnum): + """ + Piping system for FCUs + + Author: Peter Yefi + Email: peteryefi@gmail.com + """ + TWO_PIPE = "TwoPipe" + FOUR_PIPE = "FourPipe" + + diff --git a/metamenth/enumerations/fcu_type.py b/metamenth/enumerations/fcu_type.py new file mode 100644 index 0000000..406e2d7 --- /dev/null +++ b/metamenth/enumerations/fcu_type.py @@ -0,0 +1,14 @@ +from metamenth.enumerations.abstract_enum import AbstractEnum + + +class FCUType(AbstractEnum): + """ + Types of fan coil units + + Author: Peter Yefi + Email: peteryefi@gmail.com + """ + CEILING_MOUNTED = "CeilingMounted" + STANDALONE = "Standalone" + + diff --git a/metamenth/misc/validate.py b/metamenth/misc/validate.py index 8d62f16..347ee3b 100644 --- a/metamenth/misc/validate.py +++ b/metamenth/misc/validate.py @@ -190,12 +190,14 @@ def is_hvac_component_allowed_in_space(hvac_component, disallowed_entities: List from metamenth.subsystem.radiant_slab import RadiantSlab from metamenth.subsystem.baseboard_heater import BaseboardHeater from metamenth.subsystem.hvac_components.duct import Duct + from metamenth.subsystem.hvac_components.fan_coil_unit import FanCoilUnit if any(isinstance(hvac_component, cls) for cls in disallowed_entities): raise ValueError(f'{hvac_component.name} cannot be added to a space entity') elif isinstance(space_entity, Room): if (space_entity.room_type is not RoomType.MECHANICAL and - not any(isinstance(hvac_component, cls) for cls in [AirVolumeBox, BaseboardHeater, RadiantSlab, Duct])): + not any(isinstance(hvac_component, cls) for cls in [AirVolumeBox, BaseboardHeater, RadiantSlab, Duct]) + and not (isinstance(hvac_component, FanCoilUnit) and not hvac_component.is_ducted)): raise ValueError('You can only add HVAC components to mechanical rooms') elif (isinstance(space_entity, OpenSpace) and not any(isinstance(hvac_component, cls) for cls in [AirVolumeBox, BaseboardHeater, RadiantSlab, Duct])): diff --git a/metamenth/subsystem/hvac_components/duct_connection.py b/metamenth/subsystem/hvac_components/duct_connection.py index f19d05b..c027b1b 100644 --- a/metamenth/subsystem/hvac_components/duct_connection.py +++ b/metamenth/subsystem/hvac_components/duct_connection.py @@ -12,6 +12,7 @@ from metamenth.subsystem.hvac_components.boiler import Boiler from metamenth.utils import StructureEntitySearch from typing import Dict +from metamenth.subsystem.hvac_components.fan_coil_unit import FanCoilUnit class DuctConnection: @@ -28,18 +29,23 @@ def add_entity(self, entity_type: DuctConnectionEntityType, duct_entity): :return: """ from metamenth.subsystem.hvac_components.duct import Duct - allowed_entity_types = [HeatExchanger, AbstractSpace, Duct, Boiler, Chiller, CirculationPump, - Compressor, Pump, HeatPump, Condenser, AirVolumeBox, CoolingTower] + allowed_entity_types = [ + HeatExchanger, AbstractSpace, Duct, Boiler, Chiller, CirculationPump, + Compressor, Pump, HeatPump, Condenser, AirVolumeBox, CoolingTower, FanCoilUnit + ] - if any(isinstance(duct_entity, cls) for cls in allowed_entity_types): - if entity_type == DuctConnectionEntityType.SOURCE: - if duct_entity not in self._destination_entities: - self._source_entities.append(duct_entity) - elif entity_type == DuctConnectionEntityType.DESTINATION: - if duct_entity not in self._source_entities: - self._destination_entities.append(duct_entity) - else: - raise ValueError(f'{duct_entity} cannot be connected to a duct') + if not any(isinstance(duct_entity, cls) for cls in allowed_entity_types): + raise ValueError(f'{duct_entity.name} cannot be connected to a duct') + + if isinstance(duct_entity, FanCoilUnit) and not duct_entity.is_ducted: + raise ValueError(f'{duct_entity.name} cannot be connected to a duct') + + if entity_type == DuctConnectionEntityType.SOURCE: + if duct_entity not in self._destination_entities: + self._source_entities.append(duct_entity) + elif entity_type == DuctConnectionEntityType.DESTINATION: + if duct_entity not in self._source_entities: + self._destination_entities.append(duct_entity) def remove_entity(self, entity_type: DuctConnectionEntityType, duct_entity): """ diff --git a/metamenth/subsystem/hvac_components/fan_coil_unit.py b/metamenth/subsystem/hvac_components/fan_coil_unit.py new file mode 100644 index 0000000..e2bddea --- /dev/null +++ b/metamenth/subsystem/hvac_components/fan_coil_unit.py @@ -0,0 +1,96 @@ +from metamenth.subsystem.hvac_components.interfaces.abstract_duct_connected_component import ( + AbstractDuctConnectedComponent) +from metamenth.subsystem.hvac_components.heat_exchanger import HeatExchanger +from metamenth.subsystem.hvac_components.fan import Fan +from metamenth.enumerations import FCUType +from metamenth.enumerations import FCUPipeSystem + + +class FanCoilUnit(AbstractDuctConnectedComponent): + def __init__(self, name: str, heat_exchanger: HeatExchanger, + fan: Fan, fcu_type: FCUType, fcu_pipe_system: FCUPipeSystem, is_ducted: bool = True): + """ + Models a fan coil unit (FCU) in a built environment + :param name: the unique name of the heat exchanger + :param heat_exchanger: the heat exchanger which is part of the FCU + :param fan: the fan which is part of the FCU + :param fcu_type: the type of FCU + :param fcu_pipe_system: the piping system of the FCU + :param is_ducted: indicates if the FCU is connected to ventilation ducts or not + """ + super().__init__(name) + self._fan = None + self._heat_exchanger = None + self._fcu_type = None + self._fcu_pipe_system = None + self._is_ducted = is_ducted + + self.heat_exchanger = heat_exchanger + self.fan = fan + self.fcu_type = fcu_type + self.fcu_pipe_system = fcu_pipe_system + + @property + def heat_exchanger(self) -> HeatExchanger: + return self._heat_exchanger + + @heat_exchanger.setter + def heat_exchanger(self, value: HeatExchanger): + if value is not None: + self._heat_exchanger = value + else: + raise ValueError("heat_exchanger must be of type HeatExchanger") + + @property + def fan(self) -> Fan: + return self._fan + + @fan.setter + def fan(self, value: Fan): + if value is not None: + self._fan = value + else: + raise ValueError("fan must be of type Fan") + + @property + def fcu_type(self) -> FCUType: + return self._fcu_type + + @fcu_type.setter + def fcu_type(self, value: FCUType): + if value is not None: + self._fcu_type = value + else: + raise ValueError("fcu_type must be of type FCUType") + + @property + def fcu_pipe_system(self) -> FCUPipeSystem: + return self._fcu_pipe_system + + @fcu_pipe_system.setter + def fcu_pipe_system(self, value: FCUPipeSystem): + if value is not None: + self._fcu_pipe_system = value + else: + raise ValueError("fcu_pipe_system must be of type FCUPipeSystem") + + @property + def is_ducted(self) -> bool: + return self._is_ducted + + @is_ducted.setter + def is_ducted(self, value: bool): + if value is not None: + self._is_ducted = value + else: + raise ValueError("is_ducted must be of type bool") + + def __str__(self): + return ( + f"FanCoilUnit ({super().__str__()}" + f"FCU Type: {self.fcu_type}, " + f"FCU Pipe System: {self.fcu_pipe_system}, " + f"Is Ducted: {self.is_ducted}, " + f"Heat Exchanger: {self.heat_exchanger}" + f"Fan : {self.fan})" + ) diff --git a/tests/subsystem/test_building_control_system.py b/tests/subsystem/test_building_control_system.py index dcb5e50..2118287 100644 --- a/tests/subsystem/test_building_control_system.py +++ b/tests/subsystem/test_building_control_system.py @@ -55,6 +55,9 @@ from metamenth.enumerations import PumpType from metamenth.subsystem.hvac_components.pump import Pump from metamenth.enumerations import RoomType +from metamenth.enumerations import FCUType +from metamenth.enumerations import FCUPipeSystem +from metamenth.subsystem.hvac_components.fan_coil_unit import FanCoilUnit class TestBuildingControlSystem(BaseTest): @@ -192,6 +195,59 @@ def test_add_heat_exchanger_to_non_mechanical_room(self): except ValueError as err: self.assertEqual(err.__str__(), "You can only add HVAC components to mechanical rooms") + def test_non_ducted_fcu_to_mechanical_room(self): + vfd = VariableFrequencyDrive('PR.VNT.VRD.01') + fan = Fan("PR.VNT.FN.01", PowerState.ON, vfd) + heat_exchanger = HeatExchanger("PR.VNT.HE.01", HeatExchangerType.FIN_TUBE, HeatExchangerFlowType.PARALLEL) + fcu = FanCoilUnit('FCU.01', heat_exchanger, fan, FCUType.STANDALONE, FCUPipeSystem.FOUR_PIPE, False) + self.room.room_type = RoomType.MECHANICAL + self.room.add_hvac_component(fcu) + self.assertEqual(self.room.get_hvac_components({'name': 'FCU.01'}), [fcu]) + + def test_ducted_fcu_to_mechanical_room(self): + vfd = VariableFrequencyDrive('PR.VNT.VRD.01') + fan = Fan("PR.VNT.FN.01", PowerState.ON, vfd) + heat_exchanger = HeatExchanger("PR.VNT.HE.01", HeatExchangerType.FIN_TUBE, HeatExchangerFlowType.PARALLEL) + fcu = FanCoilUnit('FCU.01', heat_exchanger, fan, FCUType.STANDALONE, FCUPipeSystem.FOUR_PIPE) + self.room.room_type = RoomType.MECHANICAL + self.room.add_hvac_component(fcu) + self.assertEqual(self.room.get_hvac_components({'name': 'FCU.01'}), [fcu]) + + def test_ducted_fcu_to_non_mechanical_room(self): + try: + vfd = VariableFrequencyDrive('PR.VNT.VRD.01') + fan = Fan("PR.VNT.FN.01", PowerState.ON, vfd) + heat_exchanger = HeatExchanger("PR.VNT.HE.01", HeatExchangerType.FIN_TUBE, HeatExchangerFlowType.PARALLEL) + fcu = FanCoilUnit('FCU.01', heat_exchanger, fan, FCUType.STANDALONE, FCUPipeSystem.FOUR_PIPE) + self.room.add_hvac_component(fcu) + except ValueError as err: + self.assertEqual(err.__str__(), 'You can only add HVAC components to mechanical rooms') + + def test_add_non_ducted_fcu_to_duct(self): + try: + vfd = VariableFrequencyDrive('PR.VNT.VRD.01') + fan = Fan("PR.VNT.FN.01", PowerState.ON, vfd) + heat_exchanger = HeatExchanger("PR.VNT.HE.01", HeatExchangerType.FIN_TUBE, HeatExchangerFlowType.PARALLEL) + fcu = FanCoilUnit('FCU.01', heat_exchanger, fan, FCUType.STANDALONE, FCUPipeSystem.FOUR_PIPE, False) + + duct = Duct("VNT", DuctType.AIR) + conn = DuctConnection() + conn.add_entity(DuctConnectionEntityType.SOURCE, fcu) + duct.connections = conn + except ValueError as err: + self.assertEqual(err.__str__(), "FCU.01 cannot be connected to a duct") + + def test_add_ducted_fcu_to_duct(self): + vfd = VariableFrequencyDrive('PR.VNT.VRD.01') + fan = Fan("PR.VNT.FN.01", PowerState.ON, vfd) + heat_exchanger = HeatExchanger("PR.VNT.HE.01", HeatExchangerType.FIN_TUBE, HeatExchangerFlowType.PARALLEL) + fcu = FanCoilUnit('FCU.01', heat_exchanger, fan, FCUType.STANDALONE, FCUPipeSystem.FOUR_PIPE) + duct = Duct("VNT", DuctType.AIR) + conn = DuctConnection() + conn.add_entity(DuctConnectionEntityType.SOURCE, fcu) + duct.connections = conn + self.assertEqual(duct.connections.get_source_entities(), [fcu]) + def test_add_boiler_to_mechanical_room(self): boiler = Boiler('PR.VNT.BL.01', BoilerCategory.NATURAL_GAS, PowerState.ON) self.room.room_type = RoomType.MECHANICAL