Repository: NREL/hescore-hpxml

Issue:
Better handle windows attached to foundation walls
If a window is attached to a foundation wall it will try to determine the orientation from the wall but won't be able to find it (It's not looking for foundation walls and they don't have orientation anyway). We should fall back to using the orientation from the window itself in that case. 

Discussion:

Patch:
diff --git a/docs/source/translation/zone_window.rst b/docs/source/translation/zone_window.rst
index 480fe5db..5062926c 100644
--- a/docs/source/translation/zone_window.rst
+++ b/docs/source/translation/zone_window.rst
@@ -13,17 +13,21 @@ assumed to mean the sum of the areas of the windows that the ``Window`` element
 represents. Each ``Window`` is then assigned to a side of the building in one of
 two ways:
 
-   #. By association with a particular wall.
    #. By inspecting the azimuth or orientation of the window.
+   #. By association with a particular wall.
    
-If an HPXML window has the ``AttachedToWall`` element, the id reference in that
-element is used to find the associated wall and the side of the building that
-the window faces is inferred from the :ref:`wall orientation <wallorientation>`.
-
-If there is not an ``AttachedToWall`` element, the side is determined via the
-``Azimuth`` or ``Orientation`` elements with preference given to the
-``Azimuth`` if present. If the window falls between two sides of the house, the
-window area is divided between the sides of the house evenly. 
+If there is an ``Orientation`` or ``Azimuth`` element, the side is determined
+via the one of those elements with preference given to the ``Azimuth`` if
+present. If the window falls between two sides of the house, the window area is
+divided between the sides of the house evenly. 
+
+If ``Orientation`` or ``Azimuth`` are missing and the HPXML window has the
+``AttachedToWall`` element, the id reference in that element is used to find the
+associated wall and the side of the building that the window faces is inferred
+from the :ref:`wall orientation <wallorientation>`. If the window is attached to
+a foundation wall, the orientation/azimuth must be provided on the ``Window``
+element because foundation walls do not have orientation or azimuth elements
+available.
 
 The areas on each side of the house are summed and the :ref:`window-prop` are
 determined independently for each side of the house. Since HPXML requires that
diff --git a/hescorehpxml/__init__.py b/hescorehpxml/__init__.py
index 55a58b86..102d0f48 100644
--- a/hescorehpxml/__init__.py
+++ b/hescorehpxml/__init__.py
@@ -1530,29 +1530,33 @@ class HPXMLtoHEScoreTranslator(object):
 
             # Window side
             window_sides = []
-            attached_to_wall_id = xpath(hpxmlwndw, 'h:AttachedToWall/@idref')
-            if attached_to_wall_id is not None:
-                # Give preference to the Attached to Wall element to determine the side of the house.
-                for side, walls in list(hpxmlwalls.items()):
-                    for wall in walls:
-                        if attached_to_wall_id == wall['id']:
-                            window_sides.append(side)
-                            break
+            window_id = xpath(hpxmlwndw, 'h:SystemIdentifier/@id')
+            try:
+                # Get the aziumuth or orientation if they exist
+                wndw_azimuth = self.get_nearest_azimuth(xpath(hpxmlwndw, 'h:Azimuth/text()'),
+                                                        xpath(hpxmlwndw, 'h:Orientation/text()'))
+            except TranslationError:
+                # The window doesn't have orientation/azimuth information, get from wall
+                attached_to_wall_id = xpath(hpxmlwndw, 'h:AttachedToWall/@idref')
+                if attached_to_wall_id is not None:
+                    for side, walls in list(hpxmlwalls.items()):
+                        for wall in walls:
+                            if attached_to_wall_id == wall['id']:
+                                window_sides.append(side)
+                                break
+                    if not window_sides:
+                        raise TranslationError('The Window[SystemIdentifier/@id="{}"] has no Azimuth or Orientation, and the Window/AttachedToWall/@idref of "{}" didn\'t reference a Wall element.'.format(window_id, attached_to_wall_id))  # noqa: E501
+                else:
+                    raise TranslationError(
+                        'Window[SystemIdentifier/@id="{}"] doesn\'t have Azimuth, Orientation, or AttachedToWall. At least one is required.'.format(window_id)  # noqa: E501
+                    )
             else:
-                # If there's not Attached to Wall element, figure it out from the Azimuth/Orientation
+                # Azimuth found, associate with a side
                 try:
-                    wndw_azimuth = self.get_nearest_azimuth(xpath(hpxmlwndw, 'h:Azimuth/text()'),
-                                                            xpath(hpxmlwndw, 'h:Orientation/text()'))
-                except TranslationError:
-                    # there's no directional information in the window
-                    raise TranslationError(
-                        'All windows need to have either an AttachedToWall, Orientation, or Azimuth sub element.')
-                else:
-                    try:
-                        window_sides = [sidemap[wndw_azimuth]]
-                    except KeyError:
-                        # the direction of the window is between sides, split area
-                        window_sides = [sidemap[unspin_azimuth(wndw_azimuth + x)] for x in (-45, 45)]
+                    window_sides = [sidemap[wndw_azimuth]]
+                except KeyError:
+                    # the direction of the window is between sides, split area
+                    window_sides = [sidemap[unspin_azimuth(wndw_azimuth + x)] for x in (-45, 45)]
 
             # Assign properties and areas to the correct side of the house
             windowd['area'] /= float(len(window_sides))


Test patch:
diff --git a/tests/tests.py b/tests/tests.py
index 1f6fd6cb..31a901bc 100644
--- a/tests/tests.py
+++ b/tests/tests.py
@@ -333,9 +333,23 @@ class TestOtherHouses(unittest.TestCase, ComparatorBase):
         el.getparent().remove(el)
         self.assertRaisesRegexp(
             TranslationError,
-            r'All windows need to have either an AttachedToWall, Orientation, or Azimuth sub element\.',
+            r'Window\[SystemIdentifier/@id="\w+"\] doesn\'t have Azimuth, Orientation, or AttachedToWall. At least one is required.',  # noqa E501
             tr.hpxml_to_hescore_dict)
 
+    def test_window_only_attached_to_foundation_wall(self):
+        tr = self._load_xmlfile('house4')
+        window_orientation = self.xpath('//h:Window[1]/h:Orientation')
+        window = window_orientation.getparent()
+        etree.SubElement(window, tr.addns('h:AttachedToWall'), attrib={'idref': 'crawlwall'})
+        self._do_compare('house4')
+
+        window.remove(window_orientation)
+        self.assertRaisesRegexp(
+            TranslationError,
+            r'The Window\[SystemIdentifier/@id="\w+"\] has no Azimuth or Orientation, and the .* didn\'t reference a Wall element.',  # noqa E501
+            tr.hpxml_to_hescore_dict
+        )
+
     def test_window_attached_to_wall(self):
         filebase = 'house6'
         tr = self._load_xmlfile(filebase)