Repository: pyenergyplus/witheppy (language: python)

Issue:
write function to add or remove an exhaust fan
In principle it should be easy to add or remove an exhaust fan.
Well ... write a function that can do it.


Discussion / hints:

Fix patch:
diff --git a/docs/witheppy.eppyhelpers.rst b/docs/witheppy.eppyhelpers.rst
index add62b4..c6612f3 100644
--- a/docs/witheppy.eppyhelpers.rst
+++ b/docs/witheppy.eppyhelpers.rst
@@ -20,6 +20,14 @@ witheppy.eppyhelpers.geometry module
    :undoc-members:
    :show-inheritance:
 
+witheppy.eppyhelpers.hvac module
+--------------------------------
+
+.. automodule:: witheppy.eppyhelpers.hvac
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
 witheppy.eppyhelpers.iddhelpers module
 --------------------------------------
 
diff --git a/runningnotes.txt b/runningnotes.txt
index 59b54f6..58628fc 100644
--- a/runningnotes.txt
+++ b/runningnotes.txt
@@ -1,7 +1,15 @@
+2021-12-06
+----------
+
+- test hvac.py
+	- open base file, remove extra design days (do manually)
+	- set run period to 1 week (do manually)
+	run using eplaunch
+
 2021-12-03
 ----------
 
-idf.copyidfobject has a documentaiton error in the func. docs.
+idf.copyidfobject has a documentaiton error in the docstring
 wold be good to have idf.copyidfobject(origobject, Name="Something") like DUp and edit in IDFEditor
 
 2021-11-26
diff --git a/witheppy/eppyhelpers/geometry.py b/witheppy/eppyhelpers/geometry.py
index 3a730ce..1f0b206 100644
--- a/witheppy/eppyhelpers/geometry.py
+++ b/witheppy/eppyhelpers/geometry.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2018 Santosh Philip
+# Copyright (c) 2021 Santosh Philip
 # =======================================================================
 #  Distributed under the MIT License.
 #  (See accompanying file LICENSE or copy at
diff --git a/witheppy/eppyhelpers/hvac.py b/witheppy/eppyhelpers/hvac.py
new file mode 100644
index 0000000..4b43020
--- /dev/null
+++ b/witheppy/eppyhelpers/hvac.py
@@ -0,0 +1,243 @@
+# Copyright (c) 2021 Santosh Philip
+# =======================================================================
+#  Distributed under the MIT License.
+#  (See accompanying file LICENSE or copy at
+#  http://opensource.org/licenses/MIT)
+# =======================================================================
+"""useful hvac funtions
+
+Exhaust Fan code has been written based on 'TermReheatZoneExh.idf' from the ExampleFiles in E+ v9.6.0"""
+
+import witheppy.eppyhelpers.extfields as extfields
+
+class HasExhaustFanError(Exception):
+    pass
+
+
+def hasexhaust(idf, zone):
+    """return False if the zone has no exhaust fan
+    
+    Returns field 'Zone Air Exhaust Node or NodeList Name' from ZONEHVAC:EQUIPMENTCONNECTIONS for this zone if it has an exhaust fan. If there is no Exhaust fan, it returns False
+
+    Usage:: 
+    
+        if hasexhaust(idf, zone):
+            dosomething()
+        # OR
+        if not hasexhaust(idf, zone):
+            dosomethingelse()
+
+
+    Parameters
+    ----------
+    idf: eppy.modeleditor.IDF
+        this idf model
+    zone : eppy.bunch_subclass.EpBunch
+        the zone you are checking for exhaust (type ZONE)
+
+    Returns
+    -------
+    node: string, boolean:
+        field 'Zone Air Exhaust Node or NodeList Name' from ZONEHVAC:EQUIPMENTCONNECTIONS for this zone if it has an exhaust fan. If there is no Exhaust fan, it returns False
+    
+    """
+    
+    econnection = findequipmentconnection(idf, zone)
+    node = econnection.Zone_Air_Exhaust_Node_or_NodeList_Name
+    if node:
+        return node
+    else:
+        return False
+
+
+def findequipmentconnection(idf, zone):
+    """return ZONEHVAC:EQUIPMENTCONNECTIONS object for this zone
+        
+    Returns None if the zone does not have a ZONEHVAC:EQUIPMENTCONNECTIONS object
+
+    Parameters
+    ----------
+    idf: eppy.modeleditor.IDF
+        this idf model
+    zone : eppy.bunch_subclass.EpBunch
+        the zone you are checking for exhaust (type ZONE)
+
+    Returns
+    -------
+    equip_connections: eppy.bunch_subclass.EpBunch
+        ZONEHVAC:EQUIPMENTCONNECTIONS object for this zone
+    
+    """
+    econs = [
+        con
+        for con in idf.idfobjects["ZoneHVAC:EquipmentConnections"]
+        if con.Zone_Name == zone.Name
+    ]
+    if econs:
+        return econs[0]
+    else:
+        return None
+
+
+def findequipmentlist(idf, zone, testing=False):
+    """return ZONEHVAC:EQUIPMENTLIST object for this zone
+        
+    Returns None if the zone does not have a ZONEHVAC:EQUIPMENTLIST object
+
+    Parameters
+    ----------
+    idf: eppy.modeleditor.IDF
+        this idf model
+    zone : eppy.bunch_subclass.EpBunch
+        the zone you are checking for exhaust (type ZONE)
+
+    Returns
+    -------
+    equip_list: eppy.bunch_subclass.EpBunch
+        ZONEHVAC:EQUIPMENTLIST object for this zone
+    
+    """
+    econnection = findequipmentconnection(idf, zone)
+    if econnection:
+        elistname = econnection.Zone_Conditioning_Equipment_List_Name
+        elists = idf.idfobjects["ZoneHVAC:EquipmentList"]
+        elists = [elist for elist in elists if elist.Name == elistname]
+        if elists:
+            if testing:
+                return elists[0], econnection
+            else:
+                return elists[0]
+        else:
+            return None
+    else:
+        return None
+
+
+def findexhaust(idf, zone):
+    """return FAN:ZONEEXHAUST object for this zone
+        
+    Returns None if the zone does not have a FAN:ZONEEXHAUST object
+
+    Parameters
+    ----------
+    idf: eppy.modeleditor.IDF
+        this idf model
+    zone : eppy.bunch_subclass.EpBunch
+        the zone you are checking for exhaust (type ZONE)
+
+    Returns
+    -------
+    exhaust_fan: eppy.bunch_subclass.EpBunch
+        FAN:ZONEEXHAUST object for this zone
+    """
+    if hasexhaust(idf, zone):
+        exh_nodelist_name = hasexhaust(idf, zone)  # will fail if it is just node
+        nlist = idf.getobject("NodeList", exh_nodelist_name)
+        nname = nlist.Node_1_Name  # things may break here. is it first node item ?
+        exfans = idf.idfobjects["Fan:ZoneExhaust"]
+        myfans = [fan for fan in exfans if fan.Air_Inlet_Node_Name == nname]
+        myfan = myfans[0]
+        return myfan
+    else:
+        return None
+
+
+def putexhaust(idf, zone, exhaust_fan):
+    """plug the exhaust_fan into the zone
+    
+    Will raise HasExhaustFanError exception, the zone has and exhaust fan
+
+    Parameters
+    ----------
+    idf: eppy.modeleditor.IDF
+        this idf model
+    zone : eppy.bunch_subclass.EpBunch
+        the zone you are checking for exhaust (type ZONE)
+
+    Returns
+    -------
+    exhaust_fan: eppy.bunch_subclass.EpBunch
+        FAN:ZONEEXHAUST object for this zone
+    
+    """
+    if hasexhaust(idf, zone):
+        raise HasExhaustFanError
+        
+    # - in exhaust fan, complete
+    #     - Air inlet node name
+    #     - air outlet node name -> throwaway name
+    exhaust_fan.Air_Inlet_Node_Name = f"{exhaust_fan.Name} Node"
+    exhaust_fan.Air_Outlet_Node_Name = f"{exhaust_fan.Name} Outlet Node"
+    # - make nodelist
+    #     - make nodelist name
+    #     - put f"{exhaust_fan.Name} Node" in it as single item.
+    nodelist = idf.newidfobject(
+        "NodeList",
+        Name=f"{exhaust_fan.Name} Node List",
+        Node_1_Name=f"{exhaust_fan.Name} Node",
+    )
+    # - in ZONEHVAC:EQUIPMENTCONNECTIONS
+    #     - put the nodelist name in "Zone Air Exhaust Node or NodeList Name"
+    #     - it may ableady be there. In which case use that name in above nodelist and track it back to exhaust fan.
+    econns = findequipmentconnection(idf, zone)
+    econns.Zone_Air_Exhaust_Node_or_NodeList_Name = f"{exhaust_fan.Name} Node List"
+    # - in ZONEHVAC:EQUIPMENTLIST
+    #     - put in the exhaust fan with priority lowest
+    eqlist = findequipmentlist(idf, zone)
+    extlist = extfields.extensiblefields2list(eqlist)
+    # next two lines can break if IDD changes
+    cooling_sequence_max = max([item[2] for item in extlist])
+    heating_sequence_max = max([item[3] for item in extlist])
+    exfanitem = [
+        "Fan:ZoneExhaust",
+        exhaust_fan.Name,
+        cooling_sequence_max + 1,
+        heating_sequence_max + 1,
+    ]
+    extlist.append(exfanitem)
+    extfields.list2extensiblefields(eqlist, extlist)
+    return exhaust_fan
+
+
+def removeexhaust(idf, zone):
+    """remove the exhaust fan from zone if the zone has one
+    Parameters
+    ----------
+    idf: eppy.modeleditor.IDF
+        this idf model
+    zone : eppy.bunch_subclass.EpBunch
+        the zone you are checking for exhaust (type ZONE)
+
+    Returns
+    -------
+    exhaust_fan: eppy.bunch_subclass.EpBunch
+        FAN:ZONEEXHAUST object removed from this zone
+    
+    """
+    if hasexhaust(idf, zone):
+        nodelistname = hasexhaust(idf, zone)
+        nlist = idf.getobject("nodelist", nodelistname)
+        exhfan = findexhaust(idf, zone)
+        econn = findequipmentconnection(idf, zone)
+        econn.Zone_Air_Exhaust_Node_or_NodeList_Name = ""
+        idf.removeidfobject(exhfan)
+        idf.removeidfobject(nlist)
+        # need to remove the exfan from the equipment list
+        eqlist = findequipmentlist(idf, zone)
+        extlist = extfields.extensiblefields2list(eqlist)
+        new_list = [
+            item for item in extlist if item[0].upper() != "Fan:ZoneExhaust".upper()
+        ]
+        extfields.list2extensiblefields(eqlist, new_list)
+        return exhfan
+    else:
+        return None
+
+# TODO putexhaust has a frigle point related to IDD. See:
+    # next two lines can break if IDD changes (see in putexhaust())
+    # find a way to harden this to IDD changes.
+
+# DONE
+# DONE : findequipmentlist should not return 2 values. It is confusing - DONE
+# DONE : some functions have to be tested with hasexhaust() before running - DONE
+# DONE : add docstrings in fucntions - DONE


Test patch:
diff --git a/tests/test_eppyhelpers/test_hvac.py b/tests/test_eppyhelpers/test_hvac.py
new file mode 100644
index 0000000..5fb2f5e
--- /dev/null
+++ b/tests/test_eppyhelpers/test_hvac.py
@@ -0,0 +1,260 @@
+# Copyright (c) 2021 Santosh Philip
+# =======================================================================
+#  Distributed under the MIT License.
+#  (See accompanying file LICENSE or copy at
+#  http://opensource.org/licenses/MIT)
+# =======================================================================
+"""py.test for hvac.py"""
+
+import pytest
+from io import StringIO
+from witheppy.eppyhelpers import hvac
+import witheppy.eppyhelpers.extfields as extfields
+
+idftxt = """
+Zone,
+    West Zone,                !- Name
+    0,                        !- Direction of Relative North
+    0,                        !- X Origin
+    0,                        !- Y Origin
+    0,                        !- Z Origin
+    1,                        !- Type
+    1,                        !- Multiplier
+    autocalculate,            !- Ceiling Height
+    autocalculate;            !- Volume
+
+Zone,
+    EAST ZONE,                !- Name
+    0,                        !- Direction of Relative North
+    0,                        !- X Origin
+    0,                        !- Y Origin
+    0,                        !- Z Origin
+    1,                        !- Type
+    1,                        !- Multiplier
+    autocalculate,            !- Ceiling Height
+    autocalculate;            !- Volume
+
+Zone,
+    NORTH ZONE,               !- Name
+    0,                        !- Direction of Relative North
+    0,                        !- X Origin
+    0,                        !- Y Origin
+    0,                        !- Z Origin
+    1,                        !- Type
+    1,                        !- Multiplier
+    autocalculate,            !- Ceiling Height
+    autocalculate;            !- Volume
+
+ZoneHVAC:EquipmentList,
+    Zone1Equipment,           !- Name
+    SequentialLoad,           !- Load Distribution Scheme
+    ZoneHVAC:AirDistributionUnit,    !- Zone Equipment 1 Object Type
+    Zone1TermReheat,          !- Zone Equipment 1 Name
+    1,                        !- Zone Equipment 1 Cooling Sequence
+    1,                        !- Zone Equipment 1 Heating or NoLoad Sequence
+    ,                         !- Zone Equipment 1 Sequential Cooling Fraction Schedule Name
+    ;                         !- Zone Equipment 1 Sequential Heating Fraction Schedule Name
+
+ZoneHVAC:EquipmentList,
+    Zone2Equipment,           !- Name
+    SequentialLoad,           !- Load Distribution Scheme
+    Fan:ZoneExhaust,          !- Zone Equipment 1 Object Type
+    Zone 2 Exhaust Fan,       !- Zone Equipment 1 Name
+    2,                        !- Zone Equipment 1 Cooling Sequence
+    2,                        !- Zone Equipment 1 Heating or NoLoad Sequence
+    ,                         !- Zone Equipment 1 Sequential Cooling Fraction Schedule Name
+    ,                         !- Zone Equipment 1 Sequential Heating Fraction Schedule Name
+    ZoneHVAC:AirDistributionUnit,    !- Zone Equipment 2 Object Type
+    Zone2TermReheat,          !- Zone Equipment 2 Name
+    1,                        !- Zone Equipment 2 Cooling Sequence
+    1,                        !- Zone Equipment 2 Heating or NoLoad Sequence
+    ,                         !- Zone Equipment 2 Sequential Cooling Fraction Schedule Name
+    ;                         !- Zone Equipment 2 Sequential Heating Fraction Schedule Name
+
+ZoneHVAC:EquipmentList,
+    Zone3Equipment,           !- Name
+    SequentialLoad,           !- Load Distribution Scheme
+    Fan:ZoneExhaust,          !- Zone Equipment 1 Object Type
+    Zone 3 Exhaust Fan,       !- Zone Equipment 1 Name
+    2,                        !- Zone Equipment 1 Cooling Sequence
+    2,                        !- Zone Equipment 1 Heating or NoLoad Sequence
+    ,                         !- Zone Equipment 1 Sequential Cooling Fraction Schedule Name
+    ,                         !- Zone Equipment 1 Sequential Heating Fraction Schedule Name
+    ZoneHVAC:AirDistributionUnit,    !- Zone Equipment 2 Object Type
+    Zone3TermReheat,          !- Zone Equipment 2 Name
+    1,                        !- Zone Equipment 2 Cooling Sequence
+    1,                        !- Zone Equipment 2 Heating or NoLoad Sequence
+    ,                         !- Zone Equipment 2 Sequential Cooling Fraction Schedule Name
+    ;                         !- Zone Equipment 2 Sequential Heating Fraction Schedule Name
+
+ZoneHVAC:EquipmentConnections,
+    West Zone,                !- Zone Name
+    Zone1Equipment,           !- Zone Conditioning Equipment List Name
+    Zone1Inlets,              !- Zone Air Inlet Node or NodeList Name
+    ,                         !- Zone Air Exhaust Node or NodeList Name
+    Zone 1 Node,              !- Zone Air Node Name
+    Zone 1 Outlet Node;       !- Zone Return Air Node or NodeList Name
+
+ZoneHVAC:EquipmentConnections,
+    EAST ZONE,                !- Zone Name
+    Zone2Equipment,           !- Zone Conditioning Equipment List Name
+    Zone2Inlets,              !- Zone Air Inlet Node or NodeList Name
+    Zone 2 Exhausts,          !- Zone Air Exhaust Node or NodeList Name
+    Zone 2 Node,              !- Zone Air Node Name
+    Zone 2 Outlet Node;       !- Zone Return Air Node or NodeList Name
+
+ZoneHVAC:EquipmentConnections,
+    NORTH ZONE,               !- Zone Name
+    Zone3Equipment,           !- Zone Conditioning Equipment List Name
+    Zone3Inlets,              !- Zone Air Inlet Node or NodeList Name
+    Zone 3 Exhausts,          !- Zone Air Exhaust Node or NodeList Name
+    Zone 3 Node,              !- Zone Air Node Name
+    Zone 3 Outlet Node;       !- Zone Return Air Node or NodeList Name
+
+Fan:ConstantVolume,
+    Supply Fan 1,             !- Name
+    FanAndCoilAvailSched,     !- Availability Schedule Name
+    0.7,                      !- Fan Total Efficiency
+    600,                      !- Pressure Rise
+    1.3,                      !- Maximum Flow Rate
+    0.9,                      !- Motor Efficiency
+    1,                        !- Motor In Airstream Fraction
+    Mixed Air Node,           !- Air Inlet Node Name
+    Cooling Coil Air Inlet Node;    !- Air Outlet Node Name
+
+Fan:ZoneExhaust,
+    Zone 2 Exhaust Fan,       !- Name
+    FanAndCoilAvailSched,     !- Availability Schedule Name
+    0.6,                      !- Fan Total Efficiency
+    125,                      !- Pressure Rise
+    0.1,                      !- Maximum Flow Rate
+    Zone 2 Exhaust Node,      !- Air Inlet Node Name
+    Zone 2 Exhaust Fan Outlet Node;    !- Air Outlet Node Name
+
+Fan:ZoneExhaust,
+    Zone 3 Exhaust Fan,       !- Name
+    FanAndCoilAvailSched,     !- Availability Schedule Name
+    0.6,                      !- Fan Total Efficiency
+    125,                      !- Pressure Rise
+    0.15,                     !- Maximum Flow Rate
+    Zone 3 Exhaust Node,      !- Air Inlet Node Name
+    Zone 3 Exhaust Fan Outlet Node;    !- Air Outlet Node Name
+
+  NodeList,
+    Zone 2 Exhausts,         !- Name
+    Zone 2 Exhaust Node;     !- Node 1 Name
+
+  NodeList,
+    Zone 3 Exhausts,         !- Name
+    Zone 3 Exhaust Node;     !- Node 1 Name
+
+
+"""
+
+
+@pytest.fixture(scope="function")
+def idfsnippet(makeIDFfortesting):
+    fIDF = makeIDFfortesting
+    idf = fIDF(StringIO(idftxt))
+    idf.saveas("a.idf")
+    return idf
+
+
+def test_findequipmentconnection(idfsnippet):
+    """py.test for findequipmentconnection"""
+    idf = idfsnippet
+    zone = idf.idfobjects["zone"][0]
+    result = hvac.findequipmentconnection(idf, zone)
+    assert result.Zone_Name == zone.Name
+    zone = idf.newidfobject("zone", "gumby")
+    result = hvac.findequipmentconnection(idf, zone)
+    assert result == None
+
+
+def test_findequipmentlist(idfsnippet):
+    """py.test for findequipmentlist"""
+    idf = idfsnippet
+    zone = idf.idfobjects["zone"][0]
+    result = hvac.findequipmentlist(idf, zone, testing=True)
+    assert result[-1].Zone_Name == zone.Name
+    assert result[-1].Zone_Conditioning_Equipment_List_Name == result[0].Name
+    zone = idf.newidfobject("zone", "gumby")
+    result = hvac.findequipmentlist(idf, zone)
+    assert result == None
+    econnction = idf.newidfobject("ZoneHVAC:EquipmentConnections", Zone_Name="gumby")
+    result = hvac.findequipmentlist(idf, zone)
+    assert result == None
+
+
+def test_hasexhaust(idfsnippet):
+    """py.test for hasexhaust"""
+    idf = idfsnippet
+    zone = idf.idfobjects["zone"][0]
+    result = hvac.hasexhaust(idf, zone)
+    assert result == False
+    zone = idf.idfobjects["zone"][1]
+    result = hvac.hasexhaust(idf, zone)
+    assert result == "Zone 2 Exhausts"
+    zone = idf.idfobjects["zone"][2]
+    result = hvac.hasexhaust(idf, zone)
+    assert result == "Zone 3 Exhausts"
+
+
+def test_findexhaust(idfsnippet):
+    """py.test for findexhaust"""
+    idf = idfsnippet
+    zone = idf.idfobjects["zone"][0]
+    result = hvac.findexhaust(idf, zone)
+    assert result == None
+    # ---
+    exfanname = "Zone 2 Exhaust Fan"
+    zone = idf.idfobjects["zone"][1]
+    result = hvac.findexhaust(idf, zone)
+    assert result.Name == exfanname
+
+
+def test_removeexhaust(idfsnippet):
+    """py.test for removeexhaust"""
+    idf = idfsnippet
+    zones = idf.idfobjects["zone"]
+    zone = zones[1]
+    eqlist = hvac.findequipmentlist(idf, zone)
+    exfan = hvac.removeexhaust(idf, zone)
+    expectedname = "Zone 2 Exhaust Fan"
+    assert exfan.Name == expectedname
+    assert hvac.hasexhaust(idf, zone) == False
+    # test if exhaust fan has been removed from 'ZONEHVAC:EQUIPMENTLIST'
+    extlist = extfields.extensiblefields2list(eqlist)
+    eqlistitems = [
+        item for item in extlist if item[0].upper() == "Fan:ZoneExhaust".upper()
+    ]
+    assert not eqlistitems  # list should be empty
+    # test when you try to remove a nonexistent 
+    zone = zones[0]
+    exfan = hvac.removeexhaust(idf, zone)
+    assert exfan == None
+
+def test_putexhaust(idfsnippet):
+    """py.test for putexhaust"""
+    idf = idfsnippet
+    zones = idf.idfobjects["zone"]
+    zone = zones[0]
+    exfan = idf.newidfobject("Fan:ZoneExhaust", Name="zone1_exhaust_fan")
+    hvac.putexhaust(idf, zone, exfan)
+    assert hvac.hasexhaust(idf, zone) == "zone1_exhaust_fan Node List"
+    assert hvac.findexhaust(idf, zone) == exfan
+    # test if ex fan is in equiplist
+    eqlist = hvac.findequipmentlist(idf, zone)
+    extlist = extfields.extensiblefields2list(eqlist)
+    objecttypes = [item for item in extlist if item[0].upper() == "FAN:ZONEEXHAUST"]
+    idf.saveas("b.idf")
+    assert objecttypes # has an item in it ie. "FAN:ZONEEXHAUST"
+    # test when there is an existing exhaust
+    with pytest.raises(hvac.HasExhaustFanError):
+        zone = zones[1]
+        exfan = idf.newidfobject("Fan:ZoneExhaust", Name="zone2_exhaust_fan")
+        hvac.putexhaust(idf, zone, exfan)
+        
+
+# TODO add and remove exhaust fans in multiple example files - to see how robust it is.
+# DONE use the functions to change TermReheatZoneExh.idf and run the changed file