# frozen_string_literal: true

'''
Example Usage:

-----------------
Reading from file
-----------------

hpxml = HPXML.new(hpxml_path: ...)

# Singleton elements
puts hpxml.building_construction.number_of_bedrooms

# Array elements
hpxml.walls.each do |wall|
  wall.windows.each do |window|
    puts window.area
  end
end

---------------------
Creating from scratch
---------------------

hpxml = HPXML.new()

# Singleton elements
hpxml.building_construction.number_of_bedrooms = 3
hpxml.building_construction.conditioned_floor_area = 2400

# Array elements
hpxml.walls.clear
hpxml.walls.add(id: "WallNorth", area: 500)
hpxml.walls.add(id: "WallSouth", area: 500)
hpxml.walls.add
hpxml.walls[-1].id = "WallEastWest"
hpxml.walls[-1].area = 1000

# Write file
XMLHelper.write_file(hpxml.to_oga, "out.xml")

'''

# FUTURE: Remove all idref attributes, make object attributes instead
#         E.g., in class Window, :wall_idref => :wall

class HPXML < Object
  HPXML_ATTRS = [:header, :site, :neighbor_buildings, :building_occupancy, :building_construction,
                 :climate_and_risk_zones, :air_infiltration_measurements, :attics, :foundations,
                 :roofs, :rim_joists, :walls, :foundation_walls, :frame_floors, :slabs, :windows,
                 :skylights, :doors, :heating_systems, :cooling_systems, :heat_pumps, :hvac_controls,
                 :hvac_distributions, :ventilation_fans, :water_heating_systems, :hot_water_distributions,
                 :water_fixtures, :water_heating, :solar_thermal_systems, :pv_systems, :clothes_washers,
                 :clothes_dryers, :dishwashers, :refrigerators, :freezers, :dehumidifiers, :cooking_ranges, :ovens,
                 :lighting_groups, :lighting, :ceiling_fans, :pools, :hot_tubs, :plug_loads, :fuel_loads]
  attr_reader(*HPXML_ATTRS, :doc)

  # Constants
  AtticTypeCathedral = 'CathedralCeiling'
  AtticTypeConditioned = 'ConditionedAttic'
  AtticTypeFlatRoof = 'FlatRoof'
  AtticTypeUnvented = 'UnventedAttic'
  AtticTypeVented = 'VentedAttic'
  ClothesDryerControlTypeMoisture = 'moisture'
  ClothesDryerControlTypeTimer = 'timer'
  ColorDark = 'dark'
  ColorLight = 'light'
  ColorMedium = 'medium'
  ColorMediumDark = 'medium dark'
  ColorReflective = 'reflective'
  DHWRecirControlTypeManual = 'manual demand control'
  DHWRecirControlTypeNone = 'no control'
  DHWRecirControlTypeSensor = 'presence sensor demand control'
  DHWRecirControlTypeTemperature = 'temperature'
  DHWRecirControlTypeTimer = 'timer'
  DHWDistTypeRecirc = 'Recirculation'
  DHWDistTypeStandard = 'Standard'
  DuctInsulationMaterialUnknown = 'Unknown'
  DuctInsulationMaterialNone = 'None'
  DuctLeakageTotal = 'total'
  DuctLeakageToOutside = 'to outside'
  DuctTypeReturn = 'return'
  DuctTypeSupply = 'supply'
  DWHRFacilitiesConnectedAll = 'all'
  DWHRFacilitiesConnectedOne = 'one'
  FoundationTypeAmbient = 'Ambient'
  FoundationTypeBasementConditioned = 'ConditionedBasement'
  FoundationTypeBasementUnconditioned = 'UnconditionedBasement'
  FoundationTypeCrawlspaceUnvented = 'UnventedCrawlspace'
  FoundationTypeCrawlspaceVented = 'VentedCrawlspace'
  FoundationTypeSlab = 'SlabOnGrade'
  FrameFloorOtherSpaceAbove = 'above'
  FrameFloorOtherSpaceBelow = 'below'
  FuelLoadTypeGrill = 'grill'
  FuelLoadTypeLighting = 'lighting'
  FuelLoadTypeFireplace = 'fireplace'
  FuelTypeCoal = 'coal'
  FuelTypeCoalAnthracite = 'anthracite coal'
  FuelTypeCoalBituminous = 'bituminous coal'
  FuelTypeCoke = 'coke'
  FuelTypeDiesel = 'diesel'
  FuelTypeElectricity = 'electricity'
  FuelTypeKerosene = 'kerosene'
  FuelTypeNaturalGas = 'natural gas'
  FuelTypeOil = 'fuel oil'
  FuelTypeOil1 = 'fuel oil 1'
  FuelTypeOil2 = 'fuel oil 2'
  FuelTypeOil4 = 'fuel oil 4'
  FuelTypeOil5or6 = 'fuel oil 5/6'
  FuelTypePropane = 'propane'
  FuelTypeWoodCord = 'wood'
  FuelTypeWoodPellets = 'wood pellets'
  HeaterTypeElectricResistance = 'electric resistance'
  HeaterTypeGas = 'gas fired'
  HeaterTypeHeatPump = 'heat pump'
  HVACCompressorTypeSingleStage = 'single stage'
  HVACCompressorTypeTwoStage = 'two stage'
  HVACCompressorTypeVariableSpeed = 'variable speed'
  HVACControlTypeManual = 'manual thermostat'
  HVACControlTypeProgrammable = 'programmable thermostat'
  HVACDistributionTypeAir = 'AirDistribution'
  HVACDistributionTypeDSE = 'DSE'
  HVACDistributionTypeHydronic = 'HydronicDistribution'
  HVACDistributionTypeHydronicAndAir = 'HydronicAndAirDistribution'
  HVACTypeBoiler = 'Boiler'
  HVACTypeCentralAirConditioner = 'central air conditioner'
  HVACTypeChiller = 'chiller'
  HVACTypeCoolingTower = 'cooling tower'
  HVACTypeElectricResistance = 'ElectricResistance'
  HVACTypeEvaporativeCooler = 'evaporative cooler'
  HVACTypeFireplace = 'Fireplace'
  HVACTypeFixedHeater = 'FixedHeater'
  HVACTypeFloorFurnace = 'FloorFurnace'
  HVACTypeFurnace = 'Furnace'
  HVACTypeHeatPumpAirToAir = 'air-to-air'
  HVACTypeHeatPumpGroundToAir = 'ground-to-air'
  HVACTypeHeatPumpMiniSplit = 'mini-split'
  HVACTypeHeatPumpWaterLoopToAir = 'water-loop-to-air'
  HVACTypeMiniSplitAirConditioner = 'mini-split'
  HVACTypePortableHeater = 'PortableHeater'
  HVACTypeRoomAirConditioner = 'room air conditioner'
  HVACTypeStove = 'Stove'
  HVACTypeWallFurnace = 'WallFurnace'
  HydronicAndAirTypeFanCoil = 'fan coil'
  HydronicAndAirTypeWaterLoopHeatPump = 'water loop heat pump'
  HydronicTypeBaseboard = 'baseboard'
  HydronicTypeRadiantCeiling = 'radiant ceiling'
  HydronicTypeRadiantFloor = 'radiant floor'
  HydronicTypeRadiator = 'radiator'
  LeakinessTight = 'tight'
  LeakinessAverage = 'average'
  LightingTypeCFL = 'CompactFluorescent'
  LightingTypeLED = 'LightEmittingDiode'
  LightingTypeLFL = 'FluorescentTube'
  LocationAtticUnconditioned = 'attic - unconditioned'
  LocationAtticUnvented = 'attic - unvented'
  LocationAtticVented = 'attic - vented'
  LocationBasementConditioned = 'basement - conditioned'
  LocationBasementUnconditioned = 'basement - unconditioned'
  LocationBath = 'bath'
  LocationCrawlspaceUnvented = 'crawlspace - unvented'
  LocationCrawlspaceVented = 'crawlspace - vented'
  LocationExterior = 'exterior'
  LocationExteriorWall = 'exterior wall'
  LocationGarage = 'garage'
  LocationGround = 'ground'
  LocationInterior = 'interior'
  LocationKitchen = 'kitchen'
  LocationLivingSpace = 'living space'
  LocationOtherExterior = 'other exterior'
  LocationOtherHousingUnit = 'other housing unit'
  LocationOtherHeatedSpace = 'other heated space'
  LocationOtherMultifamilyBufferSpace = 'other multifamily buffer space'
  LocationOtherNonFreezingSpace = 'other non-freezing space'
  LocationOutside = 'outside'
  LocationRoof = 'roof'
  LocationRoofDeck = 'roof deck'
  LocationUnderSlab = 'under slab'
  MechVentTypeBalanced = 'balanced'
  MechVentTypeCFIS = 'central fan integrated supply'
  MechVentTypeERV = 'energy recovery ventilator'
  MechVentTypeExhaust = 'exhaust only'
  MechVentTypeHRV = 'heat recovery ventilator'
  MechVentTypeSupply = 'supply only'
  OrientationEast = 'east'
  OrientationNorth = 'north'
  OrientationNortheast = 'northeast'
  OrientationNorthwest = 'northwest'
  OrientationSouth = 'south'
  OrientationSoutheast = 'southeast'
  OrientationSouthwest = 'southwest'
  OrientationWest = 'west'
  PlugLoadTypeElectricVehicleCharging = 'electric vehicle charging'
  PlugLoadTypeOther = 'other'
  PlugLoadTypeTelevision = 'TV other'
  PlugLoadTypeWellPump = 'well pump'
  PVModuleTypePremium = 'premium'
  PVModuleTypeStandard = 'standard'
  PVModuleTypeThinFilm = 'thin film'
  PVTrackingTypeFixed = 'fixed'
  PVTrackingType1Axis = '1-axis'
  PVTrackingType1AxisBacktracked = '1-axis backtracked'
  PVTrackingType2Axis = '2-axis'
  ResidentialTypeApartment = 'apartment unit'
  ResidentialTypeManufactured = 'manufactured home'
  ResidentialTypeSFA = 'single-family attached'
  ResidentialTypeSFD = 'single-family detached'
  RoofTypeAsphaltShingles = 'asphalt or fiberglass shingles'
  RoofTypeConcrete = 'concrete'
  RoofTypeClayTile = 'slate or tile shingles'
  RoofTypeMetal = 'metal surfacing'
  RoofTypePlasticRubber = 'plastic/rubber/synthetic sheeting'
  RoofTypeWoodShingles = 'wood shingles or shakes'
  SidingTypeAluminum = 'aluminum siding'
  SidingTypeBrick = 'brick veneer'
  SidingTypeFiberCement = 'fiber cement siding'
  SidingTypeStucco = 'stucco'
  SidingTypeVinyl = 'vinyl siding'
  SidingTypeWood = 'wood siding'
  SiteTypeUrban = 'urban'
  SiteTypeSuburban = 'suburban'
  SiteTypeRural = 'rural'
  SolarThermalLoopTypeDirect = 'liquid direct'
  SolarThermalLoopTypeIndirect = 'liquid indirect'
  SolarThermalLoopTypeThermosyphon = 'passive thermosyphon'
  SolarThermalTypeDoubleGlazing = 'double glazing black'
  SolarThermalTypeEvacuatedTube = 'evacuated tube'
  SolarThermalTypeICS = 'integrated collector storage'
  SolarThermalTypeSingleGlazing = 'single glazing black'
  UnitsACH = 'ACH'
  UnitsACHNatural = 'ACHnatural'
  UnitsAFUE = 'AFUE'
  UnitsCFM = 'CFM'
  UnitsCFM25 = 'CFM25'
  UnitsCOP = 'COP'
  UnitsEER = 'EER'
  UnitsHSPF = 'HSPF'
  UnitsKwhPerYear = 'kWh/year'
  UnitsKwPerTon = 'kW/ton'
  UnitsPercent = 'Percent'
  UnitsSEER = 'SEER'
  UnitsSLA = 'SLA'
  UnitsThermPerYear = 'therm/year'
  WallTypeAdobe = 'Adobe'
  WallTypeBrick = 'StructuralBrick'
  WallTypeCMU = 'ConcreteMasonryUnit'
  WallTypeConcrete = 'SolidConcrete'
  WallTypeDoubleWoodStud = 'DoubleWoodStud'
  WallTypeICF = 'InsulatedConcreteForms'
  WallTypeLog = 'LogWall'
  WallTypeSIP = 'StructurallyInsulatedPanel'
  WallTypeSteelStud = 'SteelFrame'
  WallTypeStone = 'Stone'
  WallTypeStrawBale = 'StrawBale'
  WallTypeWoodStud = 'WoodStud'
  WaterFixtureTypeFaucet = 'faucet'
  WaterFixtureTypeShowerhead = 'shower head'
  WaterHeaterTypeCombiStorage = 'space-heating boiler with storage tank'
  WaterHeaterTypeCombiTankless = 'space-heating boiler with tankless coil'
  WaterHeaterTypeHeatPump = 'heat pump water heater'
  WaterHeaterTypeTankless = 'instantaneous water heater'
  WaterHeaterTypeStorage = 'storage water heater'
  WindowFrameTypeAluminum = 'Aluminum'
  WindowFrameTypeWood = 'Wood'
  WindowGasAir = 'air'
  WindowGasArgon = 'argon'
  WindowGlazingLowE = 'low-e'
  WindowGlazingReflective = 'reflective'
  WindowGlazingTintedReflective = 'tinted/reflective'
  WindowLayersDoublePane = 'double-pane'
  WindowLayersSinglePane = 'single-pane'
  WindowLayersTriplePane = 'triple-pane'

  def initialize(hpxml_path: nil, collapse_enclosure: true)
    @doc = nil
    @hpxml_path = hpxml_path

    # Create/populate child objects
    hpxml = nil
    if not hpxml_path.nil?
      @doc = XMLHelper.parse_file(hpxml_path)
      hpxml = XMLHelper.get_element(@doc, '/HPXML')
    end
    from_oga(hpxml)

    # Clean up
    delete_tiny_surfaces()
    delete_adiabatic_subsurfaces()
    if collapse_enclosure
      collapse_enclosure_surfaces()
    end
  end

  def has_space_type(space_type)
    # Look for surfaces attached to this space type
    (@roofs + @rim_joists + @walls + @foundation_walls + @frame_floors + @slabs).each do |surface|
      return true if surface.interior_adjacent_to == space_type
      return true if surface.exterior_adjacent_to == space_type
    end
    return false
  end

  def has_fuel_access
    @site.fuels.each do |fuel|
      if fuel != FuelTypeElectricity
        return true
      end
    end
    return false
  end

  def predominant_heating_fuel
    fuel_fracs = {}
    @heating_systems.each do |heating_system|
      fuel = heating_system.heating_system_fuel
      fuel_fracs[fuel] = 0.0 if fuel_fracs[fuel].nil?
      fuel_fracs[fuel] += heating_system.fraction_heat_load_served.to_f
    end
    @heat_pumps.each do |heat_pump|
      fuel = heat_pump.heat_pump_fuel
      fuel_fracs[fuel] = 0.0 if fuel_fracs[fuel].nil?
      fuel_fracs[fuel] += heat_pump.fraction_heat_load_served.to_f
    end
    return FuelTypeElectricity if fuel_fracs.empty?

    return fuel_fracs.key(fuel_fracs.values.max)
  end

  def predominant_water_heating_fuel
    fuel_fracs = {}
    @water_heating_systems.each do |water_heating_system|
      fuel = water_heating_system.fuel_type
      fuel_fracs[fuel] = 0.0 if fuel_fracs[fuel].nil?
      fuel_fracs[fuel] += water_heating_system.fraction_dhw_load_served
    end
    return FuelTypeElectricity if fuel_fracs.empty?

    return fuel_fracs.key(fuel_fracs.values.max)
  end

  def fraction_of_windows_operable()
    # Calculates the fraction of windows that are operable.
    # Since we don't have quantity available, we use area as an approximation.
    window_area_total = @windows.map { |w| w.area }.sum(0.0)
    window_area_operable = @windows.map { |w| w.fraction_operable * w.area }.sum(0.0)
    if window_area_total <= 0
      return 0.0
    end

    return window_area_operable / window_area_total
  end

  def total_fraction_cool_load_served()
    return @cooling_systems.total_fraction_cool_load_served + @heat_pumps.total_fraction_cool_load_served
  end

  def total_fraction_heat_load_served()
    return @heating_systems.total_fraction_heat_load_served + @heat_pumps.total_fraction_heat_load_served
  end

  def has_walkout_basement()
    has_conditioned_basement = has_space_type(LocationBasementConditioned)
    ncfl = @building_construction.number_of_conditioned_floors
    ncfl_ag = @building_construction.number_of_conditioned_floors_above_grade
    return (has_conditioned_basement && (ncfl == ncfl_ag))
  end

  def thermal_boundary_wall_areas()
    above_grade_area = 0.0 # Thermal boundary walls not in contact with soil
    below_grade_area = 0.0 # Thermal boundary walls in contact with soil

    (@walls + @rim_joists).each do |wall|
      if wall.is_thermal_boundary
        above_grade_area += wall.area
      end
    end

    @foundation_walls.each do |foundation_wall|
      next unless foundation_wall.is_thermal_boundary

      height = foundation_wall.height
      bg_depth = foundation_wall.depth_below_grade
      above_grade_area += (height - bg_depth) / height * foundation_wall.area
      below_grade_area += bg_depth / height * foundation_wall.area
    end

    return above_grade_area, below_grade_area
  end

  def common_wall_area()
    # Wall area for walls adjacent to Unrated Conditioned Space, not including
    # foundation walls.
    area = 0.0

    (@walls + @rim_joists).each do |wall|
      if wall.exterior_adjacent_to == HPXML::LocationOtherHousingUnit
        area += wall.area
      end
    end

    return area
  end

  def compartmentalization_boundary_areas()
    # Returns the infiltration compartmentalization boundary areas
    total_area = 0.0 # Total surface area that bounds the Infiltration Volume
    exterior_area = 0.0 # Same as above excluding surfaces attached to garage or other housing units

    # Determine which spaces are within infiltration volume
    spaces_within_infil_volume = [LocationLivingSpace, LocationBasementConditioned]
    @attics.each do |attic|
      next unless [AtticTypeUnvented].include? attic.attic_type
      next unless attic.within_infiltration_volume

      spaces_within_infil_volume << attic.to_location
    end
    @foundations.each do |foundation|
      next unless [FoundationTypeBasementUnconditioned, FoundationTypeCrawlspaceUnvented].include? foundation.foundation_type
      next unless foundation.within_infiltration_volume

      spaces_within_infil_volume << foundation.to_location
    end

    # Get surfaces bounding infiltration volume
    spaces_within_infil_volume.each do |space_type|
      (@roofs + @rim_joists + @walls + @foundation_walls + @frame_floors + @slabs).each do |surface|
        next unless [surface.interior_adjacent_to, surface.exterior_adjacent_to].include? space_type

        # Exclude surfaces between two spaces that are both within infiltration volume
        next if spaces_within_infil_volume.include?(surface.interior_adjacent_to) && spaces_within_infil_volume.include?(surface.exterior_adjacent_to)

        # Update Compartmentalization Boundary areas
        total_area += surface.area
        if not [LocationGarage, LocationOtherHousingUnit, LocationOtherHeatedSpace,
                LocationOtherMultifamilyBufferSpace, LocationOtherNonFreezingSpace].include? surface.exterior_adjacent_to
          exterior_area += surface.area
        end
      end
    end

    return total_area, exterior_area
  end

  def inferred_infiltration_height(infil_volume)
    # Infiltration height: vertical distance between lowest and highest above-grade points within the pressure boundary.
    # Height is inferred from available HPXML properties.
    # The WithinInfiltrationVolume properties are intentionally ignored for now.
    # FUTURE: Move into AirInfiltrationMeasurement class?
    cfa = @building_construction.conditioned_floor_area
    ncfl = @building_construction.number_of_conditioned_floors
    ncfl_ag = @building_construction.number_of_conditioned_floors_above_grade
    if has_walkout_basement()
      infil_height = Float(ncfl_ag) * infil_volume / cfa
    else
      # Calculate maximum above-grade height of conditioned basement walls
      max_cond_bsmt_wall_height_ag = 0.0
      @foundation_walls.each do |foundation_wall|
        next unless foundation_wall.is_exterior && (foundation_wall.interior_adjacent_to == LocationBasementConditioned)

        height_ag = foundation_wall.height - foundation_wall.depth_below_grade
        next unless height_ag > max_cond_bsmt_wall_height_ag

        max_cond_bsmt_wall_height_ag = height_ag
      end
      # Add assumed rim joist height
      cond_bsmt_rim_joist_height = 0
      @rim_joists.each do |rim_joist|
        next unless rim_joist.is_exterior && (rim_joist.interior_adjacent_to == LocationBasementConditioned)

        cond_bsmt_rim_joist_height = UnitConversions.convert(9, 'in', 'ft')
      end
      infil_height = Float(ncfl_ag) * infil_volume / cfa + max_cond_bsmt_wall_height_ag + cond_bsmt_rim_joist_height
    end
    return infil_height
  end

  def to_oga()
    @doc = _create_oga_document()
    @header.to_oga(@doc)
    @site.to_oga(@doc)
    @neighbor_buildings.to_oga(@doc)
    @building_occupancy.to_oga(@doc)
    @building_construction.to_oga(@doc)
    @climate_and_risk_zones.to_oga(@doc)
    @air_infiltration_measurements.to_oga(@doc)
    @attics.to_oga(@doc)
    @foundations.to_oga(@doc)
    @roofs.to_oga(@doc)
    @rim_joists.to_oga(@doc)
    @walls.to_oga(@doc)
    @foundation_walls.to_oga(@doc)
    @frame_floors.to_oga(@doc)
    @slabs.to_oga(@doc)
    @windows.to_oga(@doc)
    @skylights.to_oga(@doc)
    @doors.to_oga(@doc)
    @heating_systems.to_oga(@doc)
    @cooling_systems.to_oga(@doc)
    @heat_pumps.to_oga(@doc)
    @hvac_controls.to_oga(@doc)
    @hvac_distributions.to_oga(@doc)
    @ventilation_fans.to_oga(@doc)
    @water_heating_systems.to_oga(@doc)
    @hot_water_distributions.to_oga(@doc)
    @water_fixtures.to_oga(@doc)
    @water_heating.to_oga(@doc)
    @solar_thermal_systems.to_oga(@doc)
    @pv_systems.to_oga(@doc)
    @clothes_washers.to_oga(@doc)
    @clothes_dryers.to_oga(@doc)
    @dishwashers.to_oga(@doc)
    @refrigerators.to_oga(@doc)
    @freezers.to_oga(@doc)
    @dehumidifiers.to_oga(@doc)
    @cooking_ranges.to_oga(@doc)
    @ovens.to_oga(@doc)
    @lighting_groups.to_oga(@doc)
    @ceiling_fans.to_oga(@doc)
    @lighting.to_oga(@doc)
    @pools.to_oga(@doc)
    @hot_tubs.to_oga(@doc)
    @plug_loads.to_oga(@doc)
    @fuel_loads.to_oga(@doc)
    return @doc
  end

  def from_oga(hpxml)
    @header = Header.new(self, hpxml)
    @site = Site.new(self, hpxml)
    @neighbor_buildings = NeighborBuildings.new(self, hpxml)
    @building_occupancy = BuildingOccupancy.new(self, hpxml)
    @building_construction = BuildingConstruction.new(self, hpxml)
    @climate_and_risk_zones = ClimateandRiskZones.new(self, hpxml)
    @air_infiltration_measurements = AirInfiltrationMeasurements.new(self, hpxml)
    @attics = Attics.new(self, hpxml)
    @foundations = Foundations.new(self, hpxml)
    @roofs = Roofs.new(self, hpxml)
    @rim_joists = RimJoists.new(self, hpxml)
    @walls = Walls.new(self, hpxml)
    @foundation_walls = FoundationWalls.new(self, hpxml)
    @frame_floors = FrameFloors.new(self, hpxml)
    @slabs = Slabs.new(self, hpxml)
    @windows = Windows.new(self, hpxml)
    @skylights = Skylights.new(self, hpxml)
    @doors = Doors.new(self, hpxml)
    @heating_systems = HeatingSystems.new(self, hpxml)
    @cooling_systems = CoolingSystems.new(self, hpxml)
    @heat_pumps = HeatPumps.new(self, hpxml)
    @hvac_controls = HVACControls.new(self, hpxml)
    @hvac_distributions = HVACDistributions.new(self, hpxml)
    @ventilation_fans = VentilationFans.new(self, hpxml)
    @water_heating_systems = WaterHeatingSystems.new(self, hpxml)
    @hot_water_distributions = HotWaterDistributions.new(self, hpxml)
    @water_fixtures = WaterFixtures.new(self, hpxml)
    @water_heating = WaterHeating.new(self, hpxml)
    @solar_thermal_systems = SolarThermalSystems.new(self, hpxml)
    @pv_systems = PVSystems.new(self, hpxml)
    @clothes_washers = ClothesWashers.new(self, hpxml)
    @clothes_dryers = ClothesDryers.new(self, hpxml)
    @dishwashers = Dishwashers.new(self, hpxml)
    @refrigerators = Refrigerators.new(self, hpxml)
    @freezers = Freezers.new(self, hpxml)
    @dehumidifiers = Dehumidifiers.new(self, hpxml)
    @cooking_ranges = CookingRanges.new(self, hpxml)
    @ovens = Ovens.new(self, hpxml)
    @lighting_groups = LightingGroups.new(self, hpxml)
    @ceiling_fans = CeilingFans.new(self, hpxml)
    @lighting = Lighting.new(self, hpxml)
    @pools = Pools.new(self, hpxml)
    @hot_tubs = HotTubs.new(self, hpxml)
    @plug_loads = PlugLoads.new(self, hpxml)
    @fuel_loads = FuelLoads.new(self, hpxml)
  end

  # Class to store additional properties on an HPXML object that are not intended
  # to end up in the HPXML file. For example, you can store the OpenStudio::Model::Space
  # object for an appliance.
  class AdditionalProperties < OpenStruct
    def method_missing(meth, *args)
      # Complain if no value has been set rather than just returning nil
      raise NoMethodError, "undefined method '#{meth}' for #{self}" unless meth.to_s.end_with?('=')

      super
    end
  end

  # HPXML Standard Element (e.g., Roof)
  class BaseElement
    attr_accessor(:hpxml_object, :additional_properties)

    def initialize(hpxml_object, oga_element = nil, **kwargs)
      @hpxml_object = hpxml_object
      @additional_properties = AdditionalProperties.new
      if not oga_element.nil?
        # Set values from HPXML Oga element
        from_oga(oga_element)
      else
        # Set values from **kwargs
        kwargs.each do |k, v|
          send(k.to_s + '=', v)
        end
      end
    end

    def to_h
      h = {}
      self.class::ATTRS.each do |attribute|
        h[attribute] = send(attribute)
      end
      return h
    end

    def to_s
      return to_h.to_s
    end

    def nil?
      # Returns true if all attributes are nil
      to_h.each do |k, v|
        return false if not v.nil?
      end
      return true
    end
  end

  # HPXML Array Element (e.g., Roofs)
  class BaseArrayElement < Array
    attr_accessor(:hpxml_object, :additional_properties)

    def initialize(hpxml_object, oga_element = nil)
      @hpxml_object = hpxml_object
      @additional_properties = AdditionalProperties.new
      if not oga_element.nil?
        # Set values from HPXML Oga element
        from_oga(oga_element)
      end
    end

    def check_for_errors
      errors = []
      each do |child|
        if not child.respond_to? :check_for_errors
          fail "Need to add 'check_for_errors' method to #{child.class} class."
        end

        errors += child.check_for_errors
      end
      return errors
    end

    def to_oga(doc)
      each do |child|
        child.to_oga(doc)
      end
    end

    def to_s
      return map { |x| x.to_s }
    end
  end

  class Header < BaseElement
    ATTRS = [:xml_type, :xml_generated_by, :created_date_and_time, :transaction,
             :software_program_used, :software_program_version, :eri_calculation_version,
             :eri_design, :timestep, :building_id, :event_type, :state_code,
             :sim_begin_month, :sim_begin_day_of_month, :sim_end_month, :sim_end_day_of_month,
             :dst_enabled, :dst_begin_month, :dst_begin_day_of_month, :dst_end_month, :dst_end_day_of_month,
             :use_max_load_for_heat_pumps, :allow_increased_fixed_capacities,
             :apply_ashrae140_assumptions]
    attr_accessor(*ATTRS)

    def check_for_errors
      errors = []

      if not @timestep.nil?
        valid_tsteps = [60, 30, 20, 15, 12, 10, 6, 5, 4, 3, 2, 1]
        if not valid_tsteps.include? @timestep
          fail "Timestep (#{@timestep}) must be one of: #{valid_tsteps.join(', ')}."
        end
      end

      { 'Run Period' => @sim_begin_month, 'Daylight Saving' => @dst_begin_month }.each do |sim_ctl, begin_month|
        next unless not begin_month.nil?

        valid_months = (1..12).to_a
        if not valid_months.include? begin_month
          fail "#{sim_ctl} Begin Month (#{begin_month}) must be one of: #{valid_months.join(', ')}."
        end
      end

      { 'Run Period' => @sim_end_month, 'Daylight Saving' => @dst_end_month }.each do |sim_ctl, end_month|
        next unless not end_month.nil?

        valid_months = (1..12).to_a
        if not valid_months.include? end_month
          fail "#{sim_ctl} End Month (#{end_month}) must be one of: #{valid_months.join(', ')}."
        end
      end

      months_days = { [1, 3, 5, 7, 8, 10, 12] => (1..31).to_a, [4, 6, 9, 11] => (1..30).to_a, [2] => (1..28).to_a }
      months_days.each do |months, valid_days|
        { 'Run Period' => [@sim_begin_month, @sim_begin_day_of_month, @sim_end_month, @sim_end_day_of_month], 'Daylight Saving' => [@dst_begin_month, @dst_begin_day_of_month, @dst_end_month, @dst_end_day_of_month] }.each do |sim_ctl, months_and_days|
          begin_month, begin_day_of_month, end_month, end_day_of_month = months_and_days
          if (not begin_day_of_month.nil?) && (months.include? begin_month)
            if not valid_days.include? begin_day_of_month
              fail "#{sim_ctl} Begin Day of Month (#{begin_day_of_month}) must be one of: #{valid_days.join(', ')}."
            end
          end
          next unless (not end_day_of_month.nil?) && (months.include? end_month)
          if not valid_days.include? end_day_of_month
            fail "#{sim_ctl} End Day of Month (#{end_day_of_month}) must be one of: #{valid_days.join(', ')}."
          end
        end
      end

      { 'Run Period' => [@sim_begin_month, @sim_begin_day_of_month, @sim_end_month, @sim_end_day_of_month] }.each do |sim_ctl, months_and_days|
        begin_month, begin_day_of_month, end_month, end_day_of_month = months_and_days
        next unless (not begin_month.nil?) && (not end_month.nil?)
        if begin_month > end_month
          fail "#{sim_ctl} Begin Month (#{begin_month}) cannot come after #{sim_ctl} End Month (#{end_month})."
        end

        next unless (not begin_day_of_month.nil?) && (not end_day_of_month.nil?)
        next unless begin_month == end_month
        if begin_day_of_month > end_day_of_month
          fail "#{sim_ctl} Begin Day of Month (#{begin_day_of_month}) cannot come after #{sim_ctl} End Day of Month (#{end_day_of_month}) for the same month (#{begin_month})."
        end
      end

      return errors
    end

    def to_oga(doc)
      return if nil?

      hpxml = XMLHelper.get_element(doc, '/HPXML')
      header = XMLHelper.add_element(hpxml, 'XMLTransactionHeaderInformation')
      XMLHelper.add_element(header, 'XMLType', @xml_type)
      XMLHelper.add_element(header, 'XMLGeneratedBy', @xml_generated_by)
      if not @created_date_and_time.nil?
        XMLHelper.add_element(header, 'CreatedDateAndTime', @created_date_and_time)
      else
        XMLHelper.add_element(header, 'CreatedDateAndTime', Time.now.strftime('%Y-%m-%dT%H:%M:%S%:z'))
      end
      XMLHelper.add_element(header, 'Transaction', @transaction)

      software_info = XMLHelper.add_element(hpxml, 'SoftwareInfo')
      XMLHelper.add_element(software_info, 'SoftwareProgramUsed', @software_program_used) unless @software_program_used.nil?
      XMLHelper.add_element(software_info, 'SoftwareProgramVersion', software_program_version) unless software_program_version.nil?
      if not @apply_ashrae140_assumptions.nil?
        extension = XMLHelper.create_elements_as_needed(software_info, ['extension'])
        XMLHelper.add_element(extension, 'ApplyASHRAE140Assumptions', to_boolean(@apply_ashrae140_assumptions)) unless @apply_ashrae140_assumptions.nil?
      end
      if (not @eri_calculation_version.nil?) || (not @eri_design.nil?)
        extension = XMLHelper.create_elements_as_needed(software_info, ['extension'])
        eri_calculation = XMLHelper.add_element(extension, 'ERICalculation')
        XMLHelper.add_element(eri_calculation, 'Version', @eri_calculation_version) unless @eri_calculation_version.nil?
        XMLHelper.add_element(eri_calculation, 'Design', @eri_design) unless @eri_design.nil?
      end
      if (not @timestep.nil?) || (not @sim_begin_month.nil?) || (not @sim_begin_day_of_month.nil?) || (not @sim_end_month.nil?) || (not @sim_end_day_of_month.nil?) || (not @dst_enabled.nil?) || (not @dst_begin_month.nil?) || (not @dst_begin_day_of_month.nil?) || (not @dst_end_month.nil?) || (not @dst_end_day_of_month.nil?)
        extension = XMLHelper.create_elements_as_needed(software_info, ['extension'])
        simulation_control = XMLHelper.add_element(extension, 'SimulationControl')
        XMLHelper.add_element(simulation_control, 'Timestep', to_integer(@timestep)) unless @timestep.nil?
        XMLHelper.add_element(simulation_control, 'BeginMonth', to_integer(@sim_begin_month)) unless @sim_begin_month.nil?
        XMLHelper.add_element(simulation_control, 'BeginDayOfMonth', to_integer(@sim_begin_day_of_month)) unless @sim_begin_day_of_month.nil?
        XMLHelper.add_element(simulation_control, 'EndMonth', to_integer(@sim_end_month)) unless @sim_end_month.nil?
        XMLHelper.add_element(simulation_control, 'EndDayOfMonth', to_integer(@sim_end_day_of_month)) unless @sim_end_day_of_month.nil?
        if (not @dst_enabled.nil?) || (not @dst_begin_month.nil?) || (not @dst_begin_day_of_month.nil?) || (not @dst_end_month.nil?) || (not @dst_end_day_of_month.nil?)
          daylight_saving = XMLHelper.add_element(simulation_control, 'DaylightSaving')
          XMLHelper.add_element(daylight_saving, 'Enabled', to_boolean(@dst_enabled)) unless @dst_enabled.nil?
          XMLHelper.add_element(daylight_saving, 'BeginMonth', to_integer(@dst_begin_month)) unless @dst_begin_month.nil?
          XMLHelper.add_element(daylight_saving, 'BeginDayOfMonth', to_integer(@dst_begin_day_of_month)) unless @dst_begin_day_of_month.nil?
          XMLHelper.add_element(daylight_saving, 'EndMonth', to_integer(@dst_end_month)) unless @dst_end_month.nil?
          XMLHelper.add_element(daylight_saving, 'EndDayOfMonth', to_integer(@dst_end_day_of_month)) unless @dst_end_day_of_month.nil?
        end
      end
      if (not @use_max_load_for_heat_pumps.nil?) || (not @allow_increased_fixed_capacities.nil?)
        extension = XMLHelper.create_elements_as_needed(software_info, ['extension'])
        hvac_sizing_control = XMLHelper.add_element(extension, 'HVACSizingControl')
        XMLHelper.add_element(hvac_sizing_control, 'UseMaxLoadForHeatPumps', to_boolean(@use_max_load_for_heat_pumps)) unless @use_max_load_for_heat_pumps.nil?
        XMLHelper.add_element(hvac_sizing_control, 'AllowIncreasedFixedCapacities', to_boolean(@allow_increased_fixed_capacities)) unless @allow_increased_fixed_capacities.nil?
      end

      building = XMLHelper.add_element(hpxml, 'Building')
      building_building_id = XMLHelper.add_element(building, 'BuildingID')
      XMLHelper.add_attribute(building_building_id, 'id', @building_id)
      if not @state_code.nil?
        site = XMLHelper.add_element(building, 'Site')
        site_id = XMLHelper.add_element(site, 'SiteID')
        XMLHelper.add_attribute(site_id, 'id', 'SiteID')
        address = XMLHelper.add_element(site, 'Address')
        XMLHelper.add_element(address, 'StateCode', @state_code)
      end
      project_status = XMLHelper.add_element(building, 'ProjectStatus')
      XMLHelper.add_element(project_status, 'EventType', @event_type)
    end

    def from_oga(hpxml)
      return if hpxml.nil?

      @xml_type = XMLHelper.get_value(hpxml, 'XMLTransactionHeaderInformation/XMLType')
      @xml_generated_by = XMLHelper.get_value(hpxml, 'XMLTransactionHeaderInformation/XMLGeneratedBy')
      @created_date_and_time = XMLHelper.get_value(hpxml, 'XMLTransactionHeaderInformation/CreatedDateAndTime')
      @transaction = XMLHelper.get_value(hpxml, 'XMLTransactionHeaderInformation/Transaction')
      @software_program_used = XMLHelper.get_value(hpxml, 'SoftwareInfo/SoftwareProgramUsed')
      @software_program_version = XMLHelper.get_value(hpxml, 'SoftwareInfo/SoftwareProgramVersion')
      @eri_calculation_version = XMLHelper.get_value(hpxml, 'SoftwareInfo/extension/ERICalculation/Version')
      @eri_design = XMLHelper.get_value(hpxml, 'SoftwareInfo/extension/ERICalculation/Design')
      @timestep = to_integer_or_nil(XMLHelper.get_value(hpxml, 'SoftwareInfo/extension/SimulationControl/Timestep'))
      @sim_begin_month = to_integer_or_nil(XMLHelper.get_value(hpxml, 'SoftwareInfo/extension/SimulationControl/BeginMonth'))
      @sim_begin_day_of_month = to_integer_or_nil(XMLHelper.get_value(hpxml, 'SoftwareInfo/extension/SimulationControl/BeginDayOfMonth'))
      @sim_end_month = to_integer_or_nil(XMLHelper.get_value(hpxml, 'SoftwareInfo/extension/SimulationControl/EndMonth'))
      @sim_end_day_of_month = to_integer_or_nil(XMLHelper.get_value(hpxml, 'SoftwareInfo/extension/SimulationControl/EndDayOfMonth'))
      @dst_enabled = to_boolean_or_nil(XMLHelper.get_value(hpxml, 'SoftwareInfo/extension/SimulationControl/DaylightSaving/Enabled'))
      @dst_begin_month = to_integer_or_nil(XMLHelper.get_value(hpxml, 'SoftwareInfo/extension/SimulationControl/DaylightSaving/BeginMonth'))
      @dst_begin_day_of_month = to_integer_or_nil(XMLHelper.get_value(hpxml, 'SoftwareInfo/extension/SimulationControl/DaylightSaving/BeginDayOfMonth'))
      @dst_end_month = to_integer_or_nil(XMLHelper.get_value(hpxml, 'SoftwareInfo/extension/SimulationControl/DaylightSaving/EndMonth'))
      @dst_end_day_of_month = to_integer_or_nil(XMLHelper.get_value(hpxml, 'SoftwareInfo/extension/SimulationControl/DaylightSaving/EndDayOfMonth'))
      @apply_ashrae140_assumptions = to_boolean_or_nil(XMLHelper.get_value(hpxml, 'SoftwareInfo/extension/ApplyASHRAE140Assumptions'))
      @use_max_load_for_heat_pumps = to_boolean_or_nil(XMLHelper.get_value(hpxml, 'SoftwareInfo/extension/HVACSizingControl/UseMaxLoadForHeatPumps'))
      @allow_increased_fixed_capacities = to_boolean_or_nil(XMLHelper.get_value(hpxml, 'SoftwareInfo/extension/HVACSizingControl/AllowIncreasedFixedCapacities'))
      @building_id = HPXML::get_id(hpxml, 'Building/BuildingID')
      @event_type = XMLHelper.get_value(hpxml, 'Building/ProjectStatus/EventType')
      @state_code = XMLHelper.get_value(hpxml, 'Building/Site/Address/StateCode')
    end
  end

  class Site < BaseElement
    ATTRS = [:site_type, :surroundings, :orientation_of_front_of_home, :fuels, :shelter_coefficient]
    attr_accessor(*ATTRS)

    def check_for_errors
      errors = []
      return errors
    end

    def to_oga(doc)
      return if nil?

      site = XMLHelper.create_elements_as_needed(doc, ['HPXML', 'Building', 'BuildingDetails', 'BuildingSummary', 'Site'])
      XMLHelper.add_element(site, 'SiteType', @site_type) unless @site_type.nil?
      XMLHelper.add_element(site, 'Surroundings', @surroundings) unless @surroundings.nil?
      XMLHelper.add_element(site, 'OrientationOfFrontOfHome', @orientation_of_front_of_home) unless @orientation_of_front_of_home.nil?
      if (not @fuels.nil?) && (not @fuels.empty?)
        fuel_types_available = XMLHelper.add_element(site, 'FuelTypesAvailable')
        @fuels.each do |fuel|
          XMLHelper.add_element(fuel_types_available, 'Fuel', fuel)
        end
      end
      XMLHelper.add_extension(site, 'ShelterCoefficient', to_float(@shelter_coefficient)) unless @shelter_coefficient.nil?
    end

    def from_oga(hpxml)
      return if hpxml.nil?

      site = XMLHelper.get_element(hpxml, 'Building/BuildingDetails/BuildingSummary/Site')
      return if site.nil?

      @site_type = XMLHelper.get_value(site, 'SiteType')
      @surroundings = XMLHelper.get_value(site, 'Surroundings')
      @orientation_of_front_of_home = XMLHelper.get_value(site, 'OrientationOfFrontOfHome')
      @fuels = XMLHelper.get_values(site, 'FuelTypesAvailable/Fuel')
      @shelter_coefficient = to_float_or_nil(XMLHelper.get_value(site, 'extension/ShelterCoefficient'))
    end
  end

  class NeighborBuildings < BaseArrayElement
    def add(**kwargs)
      self << NeighborBuilding.new(@hpxml_object, **kwargs)
    end

    def from_oga(hpxml)
      return if hpxml.nil?

      XMLHelper.get_elements(hpxml, 'Building/BuildingDetails/BuildingSummary/Site/extension/Neighbors/NeighborBuilding').each do |neighbor_building|
        self << NeighborBuilding.new(@hpxml_object, neighbor_building)
      end
    end
  end

  class NeighborBuilding < BaseElement
    ATTRS = [:azimuth, :distance, :height]
    attr_accessor(*ATTRS)

    def check_for_errors
      errors = []
      return errors
    end

    def to_oga(doc)
      return if nil?

      neighbors = XMLHelper.create_elements_as_needed(doc, ['HPXML', 'Building', 'BuildingDetails', 'BuildingSummary', 'Site', 'extension', 'Neighbors'])
      neighbor_building = XMLHelper.add_element(neighbors, 'NeighborBuilding')
      XMLHelper.add_element(neighbor_building, 'Azimuth', to_integer(@azimuth)) unless @azimuth.nil?
      XMLHelper.add_element(neighbor_building, 'Distance', to_float(@distance)) unless @distance.nil?
      XMLHelper.add_element(neighbor_building, 'Height', to_float(@height)) unless @height.nil?
    end

    def from_oga(neighbor_building)
      return if neighbor_building.nil?

      @azimuth = to_integer_or_nil(XMLHelper.get_value(neighbor_building, 'Azimuth'))
      @distance = to_float_or_nil(XMLHelper.get_value(neighbor_building, 'Distance'))
      @height = to_float_or_nil(XMLHelper.get_value(neighbor_building, 'Height'))
    end
  end

  class BuildingOccupancy < BaseElement
    ATTRS = [:number_of_residents]
    attr_accessor(*ATTRS)

    def check_for_errors
      errors = []
      return errors
    end

    def to_oga(doc)
      return if nil?

      building_occupancy = XMLHelper.create_elements_as_needed(doc, ['HPXML', 'Building', 'BuildingDetails', 'BuildingSummary', 'BuildingOccupancy'])
      XMLHelper.add_element(building_occupancy, 'NumberofResidents', to_float(@number_of_residents)) unless @number_of_residents.nil?
    end

    def from_oga(hpxml)
      return if hpxml.nil?

      building_occupancy = XMLHelper.get_element(hpxml, 'Building/BuildingDetails/BuildingSummary/BuildingOccupancy')
      return if building_occupancy.nil?

      @number_of_residents = to_float_or_nil(XMLHelper.get_value(building_occupancy, 'NumberofResidents'))
    end
  end

  class BuildingConstruction < BaseElement
    ATTRS = [:year_built, :number_of_conditioned_floors, :number_of_conditioned_floors_above_grade,
             :average_ceiling_height, :number_of_bedrooms, :number_of_bathrooms,
             :conditioned_floor_area, :conditioned_building_volume, :use_only_ideal_air_system,
             :residential_facility_type, :has_flue_or_chimney]
    attr_accessor(*ATTRS)

    def check_for_errors
      errors = []
      return errors
    end

    def to_oga(doc)
      return if nil?

      building_construction = XMLHelper.create_elements_as_needed(doc, ['HPXML', 'Building', 'BuildingDetails', 'BuildingSummary', 'BuildingConstruction'])
      XMLHelper.add_element(building_construction, 'ResidentialFacilityType', @residential_facility_type) unless @residential_facility_type.nil?
      XMLHelper.add_element(building_construction, 'NumberofConditionedFloors', to_integer(@number_of_conditioned_floors)) unless @number_of_conditioned_floors.nil?
      XMLHelper.add_element(building_construction, 'NumberofConditionedFloorsAboveGrade', to_integer(@number_of_conditioned_floors_above_grade)) unless @number_of_conditioned_floors_above_grade.nil?
      XMLHelper.add_element(building_construction, 'AverageCeilingHeight', to_float(@average_ceiling_height)) unless @average_ceiling_height.nil?
      XMLHelper.add_element(building_construction, 'NumberofBedrooms', to_integer(@number_of_bedrooms)) unless @number_of_bedrooms.nil?
      XMLHelper.add_element(building_construction, 'NumberofBathrooms', to_integer(@number_of_bathrooms)) unless @number_of_bathrooms.nil?
      XMLHelper.add_element(building_construction, 'ConditionedFloorArea', to_float(@conditioned_floor_area)) unless @conditioned_floor_area.nil?
      XMLHelper.add_element(building_construction, 'ConditionedBuildingVolume', to_float(@conditioned_building_volume)) unless @conditioned_building_volume.nil?
      XMLHelper.add_extension(building_construction, 'UseOnlyIdealAirSystem', to_boolean(@use_only_ideal_air_system)) unless @use_only_ideal_air_system.nil?
      XMLHelper.add_extension(building_construction, 'HasFlueOrChimney', to_boolean(@has_flue_or_chimney)) unless @has_flue_or_chimney.nil?
    end

    def from_oga(hpxml)
      return if hpxml.nil?

      building_construction = XMLHelper.get_element(hpxml, 'Building/BuildingDetails/BuildingSummary/BuildingConstruction')
      return if building_construction.nil?

      @year_built = to_integer_or_nil(XMLHelper.get_value(building_construction, 'YearBuilt'))
      @number_of_conditioned_floors = to_integer_or_nil(XMLHelper.get_value(building_construction, 'NumberofConditionedFloors'))
      @number_of_conditioned_floors_above_grade = to_integer_or_nil(XMLHelper.get_value(building_construction, 'NumberofConditionedFloorsAboveGrade'))
      @average_ceiling_height = to_float_or_nil(XMLHelper.get_value(building_construction, 'AverageCeilingHeight'))
      @number_of_bedrooms = to_integer_or_nil(XMLHelper.get_value(building_construction, 'NumberofBedrooms'))
      @number_of_bathrooms = to_integer_or_nil(XMLHelper.get_value(building_construction, 'NumberofBathrooms'))
      @conditioned_floor_area = to_float_or_nil(XMLHelper.get_value(building_construction, 'ConditionedFloorArea'))
      @conditioned_building_volume = to_float_or_nil(XMLHelper.get_value(building_construction, 'ConditionedBuildingVolume'))
      @use_only_ideal_air_system = to_boolean_or_nil(XMLHelper.get_value(building_construction, 'extension/UseOnlyIdealAirSystem'))
      @residential_facility_type = XMLHelper.get_value(building_construction, 'ResidentialFacilityType')
      @has_flue_or_chimney = to_boolean_or_nil(XMLHelper.get_value(building_construction, 'extension/HasFlueOrChimney'))
    end
  end

  class ClimateandRiskZones < BaseElement
    ATTRS = [:iecc_year, :iecc_zone, :weather_station_id, :weather_station_name, :weather_station_wmo,
             :weather_station_epw_filepath]
    attr_accessor(*ATTRS)

    def check_for_errors
      errors = []
      return errors
    end

    def to_oga(doc)
      return if nil?

      climate_and_risk_zones = XMLHelper.create_elements_as_needed(doc, ['HPXML', 'Building', 'BuildingDetails', 'ClimateandRiskZones'])

      if (not @iecc_year.nil?) && (not @iecc_zone.nil?)
        climate_zone_iecc = XMLHelper.add_element(climate_and_risk_zones, 'ClimateZoneIECC')
        XMLHelper.add_element(climate_zone_iecc, 'Year', to_integer(@iecc_year)) unless @iecc_year.nil?
        XMLHelper.add_element(climate_zone_iecc, 'ClimateZone', @iecc_zone) unless @iecc_zone.nil?
      end

      if not @weather_station_id.nil?
        weather_station = XMLHelper.add_element(climate_and_risk_zones, 'WeatherStation')
        sys_id = XMLHelper.add_element(weather_station, 'SystemIdentifier')
        XMLHelper.add_attribute(sys_id, 'id', @weather_station_id)
        XMLHelper.add_element(weather_station, 'Name', @weather_station_name) unless @weather_station_name.nil?
        XMLHelper.add_element(weather_station, 'WMO', @weather_station_wmo) unless @weather_station_wmo.nil?
        XMLHelper.add_extension(weather_station, 'EPWFilePath', @weather_station_epw_filepath) unless @weather_station_epw_filepath.nil?
      end
    end

    def from_oga(hpxml)
      return if hpxml.nil?

      climate_and_risk_zones = XMLHelper.get_element(hpxml, 'Building/BuildingDetails/ClimateandRiskZones')
      return if climate_and_risk_zones.nil?

      @iecc_year = XMLHelper.get_value(climate_and_risk_zones, 'ClimateZoneIECC/Year')
      @iecc_zone = XMLHelper.get_value(climate_and_risk_zones, 'ClimateZoneIECC/ClimateZone')
      weather_station = XMLHelper.get_element(climate_and_risk_zones, 'WeatherStation')
      if not weather_station.nil?
        @weather_station_id = HPXML::get_id(weather_station)
        @weather_station_name = XMLHelper.get_value(weather_station, 'Name')
        @weather_station_wmo = XMLHelper.get_value(weather_station, 'WMO')
        @weather_station_epw_filepath = XMLHelper.get_value(weather_station, 'extension/EPWFilePath')
      end
    end
  end

  class AirInfiltrationMeasurements < BaseArrayElement
    def add(**kwargs)
      self << AirInfiltrationMeasurement.new(@hpxml_object, **kwargs)
    end

    def from_oga(hpxml)
      return if hpxml.nil?

      XMLHelper.get_elements(hpxml, 'Building/BuildingDetails/Enclosure/AirInfiltration/AirInfiltrationMeasurement').each do |air_infiltration_measurement|
        self << AirInfiltrationMeasurement.new(@hpxml_object, air_infiltration_measurement)
      end
    end
  end

  class AirInfiltrationMeasurement < BaseElement
    ATTRS = [:id, :house_pressure, :unit_of_measure, :air_leakage, :effective_leakage_area,
             :infiltration_volume, :leakiness_description, :infiltration_height, :a_ext]
    attr_accessor(*ATTRS)

    def check_for_errors
      errors = []
      return errors
    end

    def to_oga(doc)
      return if nil?

      air_infiltration = XMLHelper.create_elements_as_needed(doc, ['HPXML', 'Building', 'BuildingDetails', 'Enclosure', 'AirInfiltration'])
      air_infiltration_measurement = XMLHelper.add_element(air_infiltration, 'AirInfiltrationMeasurement')
      sys_id = XMLHelper.add_element(air_infiltration_measurement, 'SystemIdentifier')
      XMLHelper.add_attribute(sys_id, 'id', @id)
      XMLHelper.add_element(air_infiltration_measurement, 'HousePressure', to_float(@house_pressure)) unless @house_pressure.nil?
      if (not @unit_of_measure.nil?) && (not @air_leakage.nil?)
        building_air_leakage = XMLHelper.add_element(air_infiltration_measurement, 'BuildingAirLeakage')
        XMLHelper.add_element(building_air_leakage, 'UnitofMeasure', @unit_of_measure)
        XMLHelper.add_element(building_air_leakage, 'AirLeakage', to_float(@air_leakage))
      end
      XMLHelper.add_element(air_infiltration_measurement, 'EffectiveLeakageArea', to_float(@effective_leakage_area)) unless @effective_leakage_area.nil?
      XMLHelper.add_element(air_infiltration_measurement, 'InfiltrationVolume', to_float(@infiltration_volume)) unless @infiltration_volume.nil?
      XMLHelper.add_extension(air_infiltration_measurement, 'InfiltrationHeight', to_float(@infiltration_height)) unless @infiltration_height.nil?
      XMLHelper.add_extension(air_infiltration_measurement, 'Aext', to_float(@a_ext)) unless @a_ext.nil?
    end

    def from_oga(air_infiltration_measurement)
      return if air_infiltration_measurement.nil?

      @id = HPXML::get_id(air_infiltration_measurement)
      @house_pressure = to_float_or_nil(XMLHelper.get_value(air_infiltration_measurement, 'HousePressure'))
      @unit_of_measure = XMLHelper.get_value(air_infiltration_measurement, 'BuildingAirLeakage/UnitofMeasure')
      @air_leakage = to_float_or_nil(XMLHelper.get_value(air_infiltration_measurement, 'BuildingAirLeakage/AirLeakage'))
      @effective_leakage_area = to_float_or_nil(XMLHelper.get_value(air_infiltration_measurement, 'EffectiveLeakageArea'))
      @infiltration_volume = to_float_or_nil(XMLHelper.get_value(air_infiltration_measurement, 'InfiltrationVolume'))
      @leakiness_description = XMLHelper.get_value(air_infiltration_measurement, 'LeakinessDescription')
      @infiltration_height = to_float_or_nil(XMLHelper.get_value(air_infiltration_measurement, 'extension/InfiltrationHeight'))
      @a_ext = to_float_or_nil(XMLHelper.get_value(air_infiltration_measurement, 'extension/Aext'))
    end
  end

  class Attics < BaseArrayElement
    def add(**kwargs)
      self << Attic.new(@hpxml_object, **kwargs)
    end

    def from_oga(hpxml)
      return if hpxml.nil?

      XMLHelper.get_elements(hpxml, 'Building/BuildingDetails/Enclosure/Attics/Attic').each do |attic|
        self << Attic.new(@hpxml_object, attic)
      end
    end
  end

  class Attic < BaseElement
    ATTRS = [:id, :attic_type, :vented_attic_sla, :vented_attic_ach, :within_infiltration_volume,
             :attached_to_roof_idrefs, :attached_to_frame_floor_idrefs]
    attr_accessor(*ATTRS)

    def attached_roofs
      return [] if @attached_to_roof_idrefs.nil?

      list = @hpxml_object.roofs.select { |roof| @attached_to_roof_idrefs.include? roof.id }
      if @attached_to_roof_idrefs.size > list.size
        fail "Attached roof not found for attic '#{@id}'."
      end

      return list
    end

    def attached_frame_floors
      return [] if @attached_to_frame_floor_idrefs.nil?

      list = @hpxml_object.frame_floors.select { |frame_floor| @attached_to_frame_floor_idrefs.include? frame_floor.id }
      if @attached_to_frame_floor_idrefs.size > list.size
        fail "Attached frame floor not found for attic '#{@id}'."
      end

      return list
    end

    def to_location
      return if @attic_type.nil?

      if @attic_type == AtticTypeCathedral
        return LocationLivingSpace
      elsif @attic_type == AtticTypeConditioned
        return LocationLivingSpace
      elsif @attic_type == AtticTypeFlatRoof
        return LocationLivingSpace
      elsif @attic_type == AtticTypeUnvented
        return LocationAtticUnvented
      elsif @attic_type == AtticTypeVented
        return LocationAtticVented
      else
        fail "Unexpected attic type: '#{@attic_type}'."
      end
    end

    def check_for_errors
      errors = []
      begin; attached_roofs; rescue StandardError => e; errors << e.message; end
      begin; attached_frame_floors; rescue StandardError => e; errors << e.message; end
      begin; to_location; rescue StandardError => e; errors << e.message; end
      return errors
    end

    def to_oga(doc)
      return if nil?

      attics = XMLHelper.create_elements_as_needed(doc, ['HPXML', 'Building', 'BuildingDetails', 'Enclosure', 'Attics'])
      attic = XMLHelper.add_element(attics, 'Attic')
      sys_id = XMLHelper.add_element(attic, 'SystemIdentifier')
      XMLHelper.add_attribute(sys_id, 'id', @id)
      if not @attic_type.nil?
        attic_type_e = XMLHelper.add_element(attic, 'AtticType')
        if @attic_type == AtticTypeUnvented
          attic_type_attic = XMLHelper.add_element(attic_type_e, 'Attic')
          XMLHelper.add_element(attic_type_attic, 'Vented', false)
        elsif @attic_type == AtticTypeVented
          attic_type_attic = XMLHelper.add_element(attic_type_e, 'Attic')
          XMLHelper.add_element(attic_type_attic, 'Vented', true)
          if not @vented_attic_sla.nil?
            ventilation_rate = XMLHelper.add_element(attic, 'VentilationRate')
            XMLHelper.add_element(ventilation_rate, 'UnitofMeasure', UnitsSLA)
            XMLHelper.add_element(ventilation_rate, 'Value', to_float(@vented_attic_sla))
          elsif not @vented_attic_ach.nil?
            ventilation_rate = XMLHelper.add_element(attic, 'VentilationRate')
            XMLHelper.add_element(ventilation_rate, 'UnitofMeasure', UnitsACHNatural)
            XMLHelper.add_element(ventilation_rate, 'Value', to_float(@vented_attic_ach))
          end
        elsif @attic_type == AtticTypeConditioned
          attic_type_attic = XMLHelper.add_element(attic_type_e, 'Attic')
          XMLHelper.add_element(attic_type_attic, 'Conditioned', true)
        elsif (@attic_type == AtticTypeFlatRoof) || (@attic_type == AtticTypeCathedral)
          XMLHelper.add_element(attic_type_e, @attic_type)
        else
          fail "Unhandled attic type '#{@attic_type}'."
        end
      end
      XMLHelper.add_element(attic, 'WithinInfiltrationVolume', to_boolean(@within_infiltration_volume)) unless @within_infiltration_volume.nil?
    end

    def from_oga(attic)
      return if attic.nil?

      @id = HPXML::get_id(attic)
      if XMLHelper.has_element(attic, "AtticType/Attic[Vented='false']")
        @attic_type = AtticTypeUnvented
      elsif XMLHelper.has_element(attic, "AtticType/Attic[Vented='true']")
        @attic_type = AtticTypeVented
      elsif XMLHelper.has_element(attic, "AtticType/Attic[Conditioned='true']")
        @attic_type = AtticTypeConditioned
      elsif XMLHelper.has_element(attic, 'AtticType/FlatRoof')
        @attic_type = AtticTypeFlatRoof
      elsif XMLHelper.has_element(attic, 'AtticType/CathedralCeiling')
        @attic_type = AtticTypeCathedral
      end
      if @attic_type == AtticTypeVented
        @vented_attic_sla = to_float_or_nil(XMLHelper.get_value(attic, "VentilationRate[UnitofMeasure='#{UnitsSLA}']/Value"))
        @vented_attic_ach = to_float_or_nil(XMLHelper.get_value(attic, "VentilationRate[UnitofMeasure='#{UnitsACHNatural}']/Value"))
      end
      @within_infiltration_volume = to_boolean_or_nil(XMLHelper.get_value(attic, 'WithinInfiltrationVolume'))
      @attached_to_roof_idrefs = []
      XMLHelper.get_elements(attic, 'AttachedToRoof').each do |roof|
        @attached_to_roof_idrefs << HPXML::get_idref(roof)
      end
      @attached_to_frame_floor_idrefs = []
      XMLHelper.get_elements(attic, 'AttachedToFrameFloor').each do |frame_floor|
        @attached_to_frame_floor_idrefs << HPXML::get_idref(frame_floor)
      end
    end
  end

  class Foundations < BaseArrayElement
    def add(**kwargs)
      self << Foundation.new(@hpxml_object, **kwargs)
    end

    def from_oga(hpxml)
      return if hpxml.nil?

      XMLHelper.get_elements(hpxml, 'Building/BuildingDetails/Enclosure/Foundations/Foundation').each do |foundation|
        self << Foundation.new(@hpxml_object, foundation)
      end
    end
  end

  class Foundation < BaseElement
    ATTRS = [:id, :foundation_type, :vented_crawlspace_sla, :within_infiltration_volume,
             :attached_to_slab_idrefs, :attached_to_frame_floor_idrefs, :attached_to_foundation_wall_idrefs]
    attr_accessor(*ATTRS)

    def attached_slabs
      return [] if @attached_to_slab_idrefs.nil?

      list = @hpxml_object.slabs.select { |slab| @attached_to_slab_idrefs.include? slab.id }
      if @attached_to_slab_idrefs.size > list.size
        fail "Attached slab not found for foundation '#{@id}'."
      end

      return list
    end

    def attached_frame_floors
      return [] if @attached_to_frame_floor_idrefs.nil?

      list = @hpxml_object.frame_floors.select { |frame_floor| @attached_to_frame_floor_idrefs.include? frame_floor.id }
      if @attached_to_frame_floor_idrefs.size > list.size
        fail "Attached frame floor not found for foundation '#{@id}'."
      end

      return list
    end

    def attached_foundation_walls
      return [] if @attached_to_foundation_wall_idrefs.nil?

      list = @hpxml_object.foundation_walls.select { |foundation_wall| @attached_to_foundation_wall_idrefs.include? foundation_wall.id }
      if @attached_to_foundation_wall_idrefs.size > list.size
        fail "Attached foundation wall not found for foundation '#{@id}'."
      end

      return list
    end

    def to_location
      return if @foundation_type.nil?

      if @foundation_type == FoundationTypeAmbient
        return LocationOutside
      elsif @foundation_type == FoundationTypeBasementConditioned
        return LocationBasementConditioned
      elsif @foundation_type == FoundationTypeBasementUnconditioned
        return LocationBasementUnconditioned
      elsif @foundation_type == FoundationTypeCrawlspaceUnvented
        return LocationCrawlspaceUnvented
      elsif @foundation_type == FoundationTypeCrawlspaceVented
        return LocationCrawlspaceVented
      elsif @foundation_type == FoundationTypeSlab
        return LocationLivingSpace
      else
        fail "Unexpected foundation type: '#{@foundation_type}'."
      end
    end

    def area
      sum_area = 0.0
      # Check Slabs first
      attached_slabs.each do |slab|
        sum_area += slab.area
      end
      if sum_area <= 0
        # Check FrameFloors next
        attached_frame_floors.each do |frame_floor|
          sum_area += frame_floor.area
        end
      end
      return sum_area
    end

    def check_for_errors
      errors = []
      begin; attached_slabs; rescue StandardError => e; errors << e.message; end
      begin; attached_frame_floors; rescue StandardError => e; errors << e.message; end
      begin; attached_foundation_walls; rescue StandardError => e; errors << e.message; end
      begin; to_location; rescue StandardError => e; errors << e.message; end
      return errors
    end

    def to_oga(doc)
      return if nil?

      foundations = XMLHelper.create_elements_as_needed(doc, ['HPXML', 'Building', 'BuildingDetails', 'Enclosure', 'Foundations'])
      foundation = XMLHelper.add_element(foundations, 'Foundation')
      sys_id = XMLHelper.add_element(foundation, 'SystemIdentifier')
      XMLHelper.add_attribute(sys_id, 'id', @id)
      if not @foundation_type.nil?
        foundation_type_e = XMLHelper.add_element(foundation, 'FoundationType')
        if [FoundationTypeSlab, FoundationTypeAmbient].include? @foundation_type
          XMLHelper.add_element(foundation_type_e, @foundation_type)
        elsif @foundation_type == FoundationTypeBasementConditioned
          basement = XMLHelper.add_element(foundation_type_e, 'Basement')
          XMLHelper.add_element(basement, 'Conditioned', true)
        elsif @foundation_type == FoundationTypeBasementUnconditioned
          basement = XMLHelper.add_element(foundation_type_e, 'Basement')
          XMLHelper.add_element(basement, 'Conditioned', false)
        elsif @foundation_type == FoundationTypeCrawlspaceVented
          crawlspace = XMLHelper.add_element(foundation_type_e, 'Crawlspace')
          XMLHelper.add_element(crawlspace, 'Vented', true)
          if not @vented_crawlspace_sla.nil?
            ventilation_rate = XMLHelper.add_element(foundation, 'VentilationRate')
            XMLHelper.add_element(ventilation_rate, 'UnitofMeasure', UnitsSLA)
            XMLHelper.add_element(ventilation_rate, 'Value', to_float(@vented_crawlspace_sla))
          end
        elsif @foundation_type == FoundationTypeCrawlspaceUnvented
          crawlspace = XMLHelper.add_element(foundation_type_e, 'Crawlspace')
          XMLHelper.add_element(crawlspace, 'Vented', false)
        else
          fail "Unhandled foundation type '#{@foundation_type}'."
        end
      end
      XMLHelper.add_element(foundation, 'WithinInfiltrationVolume', to_boolean(@within_infiltration_volume)) unless @within_infiltration_volume.nil?
    end

    def from_oga(foundation)
      return if foundation.nil?

      @id = HPXML::get_id(foundation)
      if XMLHelper.has_element(foundation, 'FoundationType/SlabOnGrade')
        @foundation_type = FoundationTypeSlab
      elsif XMLHelper.has_element(foundation, "FoundationType/Basement[Conditioned='false']")
        @foundation_type = FoundationTypeBasementUnconditioned
      elsif XMLHelper.has_element(foundation, "FoundationType/Basement[Conditioned='true']")
        @foundation_type = FoundationTypeBasementConditioned
      elsif XMLHelper.has_element(foundation, "FoundationType/Crawlspace[Vented='false']")
        @foundation_type = FoundationTypeCrawlspaceUnvented
      elsif XMLHelper.has_element(foundation, "FoundationType/Crawlspace[Vented='true']")
        @foundation_type = FoundationTypeCrawlspaceVented
      elsif XMLHelper.has_element(foundation, 'FoundationType/Ambient')
        @foundation_type = FoundationTypeAmbient
      end
      if @foundation_type == FoundationTypeCrawlspaceVented
        @vented_crawlspace_sla = to_float_or_nil(XMLHelper.get_value(foundation, "VentilationRate[UnitofMeasure='SLA']/Value"))
      end
      @within_infiltration_volume = to_boolean_or_nil(XMLHelper.get_value(foundation, 'WithinInfiltrationVolume'))
      @attached_to_slab_idrefs = []
      XMLHelper.get_elements(foundation, 'AttachedToSlab').each do |slab|
        @attached_to_slab_idrefs << HPXML::get_idref(slab)
      end
      @attached_to_frame_floor_idrefs = []
      XMLHelper.get_elements(foundation, 'AttachedToFrameFloor').each do |frame_floor|
        @attached_to_frame_floor_idrefs << HPXML::get_idref(frame_floor)
      end
      @attached_to_foundation_wall_idrefs = []
      XMLHelper.get_elements(foundation, 'AttachedToFoundationWall').each do |foundation_wall|
        @attached_to_foundation_wall_idrefs << HPXML::get_idref(foundation_wall)
      end
    end
  end

  class Roofs < BaseArrayElement
    def add(**kwargs)
      self << Roof.new(@hpxml_object, **kwargs)
    end

    def from_oga(hpxml)
      return if hpxml.nil?

      XMLHelper.get_elements(hpxml, 'Building/BuildingDetails/Enclosure/Roofs/Roof').each do |roof|
        self << Roof.new(@hpxml_object, roof)
      end
    end
  end

  class Roof < BaseElement
    ATTRS = [:id, :interior_adjacent_to, :area, :azimuth, :roof_type,
             :roof_color, :solar_absorptance, :emittance, :pitch, :radiant_barrier,
             :insulation_id, :insulation_assembly_r_value, :insulation_cavity_r_value,
             :insulation_continuous_r_value, :radiant_barrier_grade]
    attr_accessor(*ATTRS)

    def skylights
      return @hpxml_object.skylights.select { |skylight| skylight.roof_idref == @id }
    end

    def net_area
      return if nil?

      val = @area
      skylights.each do |skylight|
        val -= skylight.area
      end
      fail "Calculated a negative net surface area for surface '#{@id}'." if val < 0

      return val
    end

    def exterior_adjacent_to
      return LocationOutside
    end

    def is_exterior
      return true
    end

    def is_interior
      return !is_exterior
    end

    def is_thermal_boundary
      return HPXML::is_thermal_boundary(self)
    end

    def is_exterior_thermal_boundary
      return (is_exterior && is_thermal_boundary)
    end

    def delete
      @hpxml_object.roofs.delete(self)
      skylights.reverse_each do |skylight|
        skylight.delete
      end
      @hpxml_object.attics.each do |attic|
        attic.attached_to_roof_idrefs.delete(@id) unless attic.attached_to_roof_idrefs.nil?
      end
    end

    def check_for_errors
      errors = []
      begin; net_area; rescue StandardError => e; errors << e.message; end
      return errors
    end

    def to_oga(doc)
      return if nil?

      roofs = XMLHelper.create_elements_as_needed(doc, ['HPXML', 'Building', 'BuildingDetails', 'Enclosure', 'Roofs'])
      roof = XMLHelper.add_element(roofs, 'Roof')
      sys_id = XMLHelper.add_element(roof, 'SystemIdentifier')
      XMLHelper.add_attribute(sys_id, 'id', @id)
      XMLHelper.add_element(roof, 'InteriorAdjacentTo', @interior_adjacent_to) unless @interior_adjacent_to.nil?
      XMLHelper.add_element(roof, 'Area', to_float(@area)) unless @area.nil?
      XMLHelper.add_element(roof, 'Azimuth', to_integer(@azimuth)) unless @azimuth.nil?
      XMLHelper.add_element(roof, 'RoofType', @roof_type) unless @roof_type.nil?
      XMLHelper.add_element(roof, 'RoofColor', @roof_color) unless @roof_color.nil?
      XMLHelper.add_element(roof, 'SolarAbsorptance', to_float(@solar_absorptance)) unless @solar_absorptance.nil?
      XMLHelper.add_element(roof, 'Emittance', to_float(@emittance)) unless @emittance.nil?
      XMLHelper.add_element(roof, 'Pitch', to_float(@pitch)) unless @pitch.nil?
      XMLHelper.add_element(roof, 'RadiantBarrier', to_boolean(@radiant_barrier)) unless @radiant_barrier.nil?
      XMLHelper.add_element(roof, 'RadiantBarrierGrade', to_integer(@radiant_barrier_grade)) unless @radiant_barrier_grade.nil?
      insulation = XMLHelper.add_element(roof, 'Insulation')
      sys_id = XMLHelper.add_element(insulation, 'SystemIdentifier')
      if not @insulation_id.nil?
        XMLHelper.add_attribute(sys_id, 'id', @insulation_id)
      else
        XMLHelper.add_attribute(sys_id, 'id', @id + 'Insulation')
      end
      XMLHelper.add_element(insulation, 'AssemblyEffectiveRValue', to_float(@insulation_assembly_r_value)) unless @insulation_assembly_r_value.nil?
    end

    def from_oga(roof)
      return if roof.nil?

      @id = HPXML::get_id(roof)
      @interior_adjacent_to = XMLHelper.get_value(roof, 'InteriorAdjacentTo')
      @area = to_float_or_nil(XMLHelper.get_value(roof, 'Area'))
      @azimuth = to_integer_or_nil(XMLHelper.get_value(roof, 'Azimuth'))
      @roof_type = XMLHelper.get_value(roof, 'RoofType')
      @roof_color = XMLHelper.get_value(roof, 'RoofColor')
      @solar_absorptance = to_float_or_nil(XMLHelper.get_value(roof, 'SolarAbsorptance'))
      @emittance = to_float_or_nil(XMLHelper.get_value(roof, 'Emittance'))
      @pitch = to_float_or_nil(XMLHelper.get_value(roof, 'Pitch'))
      @radiant_barrier = to_boolean_or_nil(XMLHelper.get_value(roof, 'RadiantBarrier'))
      @radiant_barrier_grade = to_integer_or_nil(XMLHelper.get_value(roof, 'RadiantBarrierGrade'))
      insulation = XMLHelper.get_element(roof, 'Insulation')
      if not insulation.nil?
        @insulation_id = HPXML::get_id(insulation)
        @insulation_assembly_r_value = to_float_or_nil(XMLHelper.get_value(insulation, 'AssemblyEffectiveRValue'))
        @insulation_cavity_r_value = to_float_or_nil(XMLHelper.get_value(insulation, "Layer[InstallationType='cavity']/NominalRValue"))
        @insulation_continuous_r_value = to_float_or_nil(XMLHelper.get_value(insulation, "Layer[InstallationType='continuous']/NominalRValue"))
      end
    end
  end

  class RimJoists < BaseArrayElement
    def add(**kwargs)
      self << RimJoist.new(@hpxml_object, **kwargs)
    end

    def from_oga(hpxml)
      return if hpxml.nil?

      XMLHelper.get_elements(hpxml, 'Building/BuildingDetails/Enclosure/RimJoists/RimJoist').each do |rim_joist|
        self << RimJoist.new(@hpxml_object, rim_joist)
      end
    end
  end

  class RimJoist < BaseElement
    ATTRS = [:id, :exterior_adjacent_to, :interior_adjacent_to, :area, :azimuth, :siding, :color,
             :solar_absorptance, :emittance, :insulation_id, :insulation_assembly_r_value,
             :insulation_cavity_r_value, :insulation_continuous_r_value]
    attr_accessor(*ATTRS)

    def is_exterior
      if @exterior_adjacent_to == LocationOutside
        return true
      end

      return false
    end

    def is_interior
      return !is_exterior
    end

    def is_adiabatic
      return HPXML::is_adiabatic(self)
    end

    def is_thermal_boundary
      return HPXML::is_thermal_boundary(self)
    end

    def is_exterior_thermal_boundary
      return (is_exterior && is_thermal_boundary)
    end

    def delete
      @hpxml_object.rim_joists.delete(self)
    end

    def check_for_errors
      errors = []
      return errors
    end

    def to_oga(doc)
      return if nil?

      rim_joists = XMLHelper.create_elements_as_needed(doc, ['HPXML', 'Building', 'BuildingDetails', 'Enclosure', 'RimJoists'])
      rim_joist = XMLHelper.add_element(rim_joists, 'RimJoist')
      sys_id = XMLHelper.add_element(rim_joist, 'SystemIdentifier')
      XMLHelper.add_attribute(sys_id, 'id', @id)
      XMLHelper.add_element(rim_joist, 'ExteriorAdjacentTo', @exterior_adjacent_to) unless @exterior_adjacent_to.nil?
      XMLHelper.add_element(rim_joist, 'InteriorAdjacentTo', @interior_adjacent_to) unless @interior_adjacent_to.nil?
      XMLHelper.add_element(rim_joist, 'Area', to_float(@area)) unless @area.nil?
      XMLHelper.add_element(rim_joist, 'Azimuth', to_integer(@azimuth)) unless @azimuth.nil?
      XMLHelper.add_element(rim_joist, 'Siding', @siding) unless @siding.nil?
      XMLHelper.add_element(rim_joist, 'Color', @color) unless @color.nil?
      XMLHelper.add_element(rim_joist, 'SolarAbsorptance', to_float(@solar_absorptance)) unless @solar_absorptance.nil?
      XMLHelper.add_element(rim_joist, 'Emittance', to_float(@emittance)) unless @emittance.nil?
      insulation = XMLHelper.add_element(rim_joist, 'Insulation')
      sys_id = XMLHelper.add_element(insulation, 'SystemIdentifier')
      if not @insulation_id.nil?
        XMLHelper.add_attribute(sys_id, 'id', @insulation_id)
      else
        XMLHelper.add_attribute(sys_id, 'id', @id + 'Insulation')
      end
      XMLHelper.add_element(insulation, 'AssemblyEffectiveRValue', to_float(@insulation_assembly_r_value)) unless @insulation_assembly_r_value.nil?
    end

    def from_oga(rim_joist)
      return if rim_joist.nil?

      @id = HPXML::get_id(rim_joist)
      @exterior_adjacent_to = XMLHelper.get_value(rim_joist, 'ExteriorAdjacentTo')
      @interior_adjacent_to = XMLHelper.get_value(rim_joist, 'InteriorAdjacentTo')
      @area = to_float_or_nil(XMLHelper.get_value(rim_joist, 'Area'))
      @azimuth = to_integer_or_nil(XMLHelper.get_value(rim_joist, 'Azimuth'))
      @siding = XMLHelper.get_value(rim_joist, 'Siding')
      @color = XMLHelper.get_value(rim_joist, 'Color')
      @solar_absorptance = to_float_or_nil(XMLHelper.get_value(rim_joist, 'SolarAbsorptance'))
      @emittance = to_float_or_nil(XMLHelper.get_value(rim_joist, 'Emittance'))
      insulation = XMLHelper.get_element(rim_joist, 'Insulation')
      if not insulation.nil?
        @insulation_id = HPXML::get_id(insulation)
        @insulation_assembly_r_value = to_float_or_nil(XMLHelper.get_value(insulation, 'AssemblyEffectiveRValue'))
        @insulation_cavity_r_value = to_float_or_nil(XMLHelper.get_value(insulation, "Layer[InstallationType='cavity']/NominalRValue"))
        @insulation_continuous_r_value = to_float_or_nil(XMLHelper.get_value(insulation, "Layer[InstallationType='continuous']/NominalRValue"))
      end
    end
  end

  class Walls < BaseArrayElement
    def add(**kwargs)
      self << Wall.new(@hpxml_object, **kwargs)
    end

    def from_oga(hpxml)
      return if hpxml.nil?

      XMLHelper.get_elements(hpxml, 'Building/BuildingDetails/Enclosure/Walls/Wall').each do |wall|
        self << Wall.new(@hpxml_object, wall)
      end
    end
  end

  class Wall < BaseElement
    ATTRS = [:id, :exterior_adjacent_to, :interior_adjacent_to, :wall_type, :optimum_value_engineering,
             :area, :orientation, :azimuth, :siding, :color, :solar_absorptance, :emittance, :insulation_id,
             :insulation_assembly_r_value, :insulation_cavity_r_value, :insulation_continuous_r_value]
    attr_accessor(*ATTRS)

    def windows
      return @hpxml_object.windows.select { |window| window.wall_idref == @id }
    end

    def doors
      return @hpxml_object.doors.select { |door| door.wall_idref == @id }
    end

    def net_area
      return if nil?

      val = @area
      (windows + doors).each do |subsurface|
        val -= subsurface.area
      end
      fail "Calculated a negative net surface area for surface '#{@id}'." if val < 0

      return val
    end

    def is_exterior
      if @exterior_adjacent_to == LocationOutside
        return true
      end

      return false
    end

    def is_interior
      return !is_exterior
    end

    def is_adiabatic
      return HPXML::is_adiabatic(self)
    end

    def is_thermal_boundary
      return HPXML::is_thermal_boundary(self)
    end

    def is_exterior_thermal_boundary
      return (is_exterior && is_thermal_boundary)
    end

    def delete
      @hpxml_object.walls.delete(self)
      windows.reverse_each do |window|
        window.delete
      end
      doors.reverse_each do |door|
        door.delete
      end
    end

    def check_for_errors
      errors = []
      begin; net_area; rescue StandardError => e; errors << e.message; end
      return errors
    end

    def to_oga(doc)
      return if nil?

      walls = XMLHelper.create_elements_as_needed(doc, ['HPXML', 'Building', 'BuildingDetails', 'Enclosure', 'Walls'])
      wall = XMLHelper.add_element(walls, 'Wall')
      sys_id = XMLHelper.add_element(wall, 'SystemIdentifier')
      XMLHelper.add_attribute(sys_id, 'id', @id)
      XMLHelper.add_element(wall, 'ExteriorAdjacentTo', @exterior_adjacent_to) unless @exterior_adjacent_to.nil?
      XMLHelper.add_element(wall, 'InteriorAdjacentTo', @interior_adjacent_to) unless @interior_adjacent_to.nil?
      if not @wall_type.nil?
        wall_type_e = XMLHelper.add_element(wall, 'WallType')
        XMLHelper.add_element(wall_type_e, @wall_type)
      end
      XMLHelper.add_element(wall, 'Area', to_float(@area)) unless @area.nil?
      XMLHelper.add_element(wall, 'Azimuth', to_integer(@azimuth)) unless @azimuth.nil?
      XMLHelper.add_element(wall, 'Siding', @siding) unless @siding.nil?
      XMLHelper.add_element(wall, 'Color', @color) unless @color.nil?
      XMLHelper.add_element(wall, 'SolarAbsorptance', to_float(@solar_absorptance)) unless @solar_absorptance.nil?
      XMLHelper.add_element(wall, 'Emittance', to_float(@emittance)) unless @emittance.nil?
      insulation = XMLHelper.add_element(wall, 'Insulation')
      sys_id = XMLHelper.add_element(insulation, 'SystemIdentifier')
      if not @insulation_id.nil?
        XMLHelper.add_attribute(sys_id, 'id', @insulation_id)
      else
        XMLHelper.add_attribute(sys_id, 'id', @id + 'Insulation')
      end
      XMLHelper.add_element(insulation, 'AssemblyEffectiveRValue', to_float(@insulation_assembly_r_value)) unless @insulation_assembly_r_value.nil?
    end

    def from_oga(wall)
      return if wall.nil?

      @id = HPXML::get_id(wall)
      @exterior_adjacent_to = XMLHelper.get_value(wall, 'ExteriorAdjacentTo')
      @interior_adjacent_to = XMLHelper.get_value(wall, 'InteriorAdjacentTo')
      @wall_type = XMLHelper.get_child_name(wall, 'WallType')
      @optimum_value_engineering = to_boolean_or_nil(XMLHelper.get_value(wall, 'WallType/WoodStud/OptimumValueEngineering'))
      @area = to_float_or_nil(XMLHelper.get_value(wall, 'Area'))
      @orientation = XMLHelper.get_value(wall, 'Orientation')
      @azimuth = to_integer_or_nil(XMLHelper.get_value(wall, 'Azimuth'))
      @siding = XMLHelper.get_value(wall, 'Siding')
      @color = XMLHelper.get_value(wall, 'Color')
      @solar_absorptance = to_float_or_nil(XMLHelper.get_value(wall, 'SolarAbsorptance'))
      @emittance = to_float_or_nil(XMLHelper.get_value(wall, 'Emittance'))
      insulation = XMLHelper.get_element(wall, 'Insulation')
      if not insulation.nil?
        @insulation_id = HPXML::get_id(insulation)
        @insulation_assembly_r_value = to_float_or_nil(XMLHelper.get_value(insulation, 'AssemblyEffectiveRValue'))
        @insulation_cavity_r_value = to_float_or_nil(XMLHelper.get_value(insulation, "Layer[InstallationType='cavity']/NominalRValue"))
        @insulation_continuous_r_value = to_float_or_nil(XMLHelper.get_value(insulation, "Layer[InstallationType='continuous']/NominalRValue"))
      end
    end
  end

  class FoundationWalls < BaseArrayElement
    def add(**kwargs)
      self << FoundationWall.new(@hpxml_object, **kwargs)
    end

    def from_oga(hpxml)
      return if hpxml.nil?

      XMLHelper.get_elements(hpxml, 'Building/BuildingDetails/Enclosure/FoundationWalls/FoundationWall').each do |foundation_wall|
        self << FoundationWall.new(@hpxml_object, foundation_wall)
      end
    end
  end

  class FoundationWall < BaseElement
    ATTRS = [:id, :exterior_adjacent_to, :interior_adjacent_to, :height, :area, :azimuth, :thickness,
             :depth_below_grade, :insulation_id, :insulation_r_value, :insulation_interior_r_value,
             :insulation_interior_distance_to_top, :insulation_interior_distance_to_bottom,
             :insulation_exterior_r_value, :insulation_exterior_distance_to_top,
             :insulation_exterior_distance_to_bottom, :insulation_assembly_r_value,
             :insulation_continuous_r_value]
    attr_accessor(*ATTRS)

    def windows
      return @hpxml_object.windows.select { |window| window.wall_idref == @id }
    end

    def doors
      return @hpxml_object.doors.select { |door| door.wall_idref == @id }
    end

    def net_area
      return if nil?

      val = @area
      (@hpxml_object.windows + @hpxml_object.doors).each do |subsurface|
        next unless subsurface.wall_idref == @id

        val -= subsurface.area
      end
      fail "Calculated a negative net surface area for surface '#{@id}'." if val < 0

      return val
    end

    def is_exterior
      if @exterior_adjacent_to == LocationGround
        return true
      end

      return false
    end

    def is_interior
      return !is_exterior
    end

    def is_adiabatic
      return HPXML::is_adiabatic(self)
    end

    def is_thermal_boundary
      return HPXML::is_thermal_boundary(self)
    end

    def is_exterior_thermal_boundary
      return (is_exterior && is_thermal_boundary)
    end

    def delete
      @hpxml_object.foundation_walls.delete(self)
      windows.reverse_each do |window|
        window.delete
      end
      doors.reverse_each do |door|
        door.delete
      end
      @hpxml_object.foundations.each do |foundation|
        foundation.attached_to_foundation_wall_idrefs.delete(@id) unless foundation.attached_to_foundation_wall_idrefs.nil?
      end
    end

    def check_for_errors
      errors = []
      begin; net_area; rescue StandardError => e; errors << e.message; end
      return errors
    end

    def to_oga(doc)
      return if nil?

      foundation_walls = XMLHelper.create_elements_as_needed(doc, ['HPXML', 'Building', 'BuildingDetails', 'Enclosure', 'FoundationWalls'])
      foundation_wall = XMLHelper.add_element(foundation_walls, 'FoundationWall')
      sys_id = XMLHelper.add_element(foundation_wall, 'SystemIdentifier')
      XMLHelper.add_attribute(sys_id, 'id', @id)
      XMLHelper.add_element(foundation_wall, 'ExteriorAdjacentTo', @exterior_adjacent_to) unless @exterior_adjacent_to.nil?
      XMLHelper.add_element(foundation_wall, 'InteriorAdjacentTo', @interior_adjacent_to) unless @interior_adjacent_to.nil?
      XMLHelper.add_element(foundation_wall, 'Height', to_float(@height)) unless @height.nil?
      XMLHelper.add_element(foundation_wall, 'Area', to_float(@area)) unless @area.nil?
      XMLHelper.add_element(foundation_wall, 'Azimuth', to_integer(@azimuth)) unless @azimuth.nil?
      XMLHelper.add_element(foundation_wall, 'Thickness', to_float(@thickness)) unless @thickness.nil?
      XMLHelper.add_element(foundation_wall, 'DepthBelowGrade', to_float(@depth_below_grade)) unless @depth_below_grade.nil?
      insulation = XMLHelper.add_element(foundation_wall, 'Insulation')
      sys_id = XMLHelper.add_element(insulation, 'SystemIdentifier')
      if not @insulation_id.nil?
        XMLHelper.add_attribute(sys_id, 'id', @insulation_id)
      else
        XMLHelper.add_attribute(sys_id, 'id', @id + 'Insulation')
      end
      XMLHelper.add_element(insulation, 'AssemblyEffectiveRValue', to_float(@insulation_assembly_r_value)) unless @insulation_assembly_r_value.nil?
      if not @insulation_exterior_r_value.nil?
        layer = XMLHelper.add_element(insulation, 'Layer')
        XMLHelper.add_element(layer, 'InstallationType', 'continuous - exterior')
        XMLHelper.add_element(layer, 'NominalRValue', to_float(@insulation_exterior_r_value))
        XMLHelper.add_extension(layer, 'DistanceToTopOfInsulation', to_float(@insulation_exterior_distance_to_top)) unless @insulation_exterior_distance_to_top.nil?
        XMLHelper.add_extension(layer, 'DistanceToBottomOfInsulation', to_float(@insulation_exterior_distance_to_bottom)) unless @insulation_exterior_distance_to_bottom.nil?
      end
      if not @insulation_interior_r_value.nil?
        layer = XMLHelper.add_element(insulation, 'Layer')
        XMLHelper.add_element(layer, 'InstallationType', 'continuous - interior')
        XMLHelper.add_element(layer, 'NominalRValue', to_float(@insulation_interior_r_value))
        XMLHelper.add_extension(layer, 'DistanceToTopOfInsulation', to_float(@insulation_interior_distance_to_top)) unless @insulation_interior_distance_to_top.nil?
        XMLHelper.add_extension(layer, 'DistanceToBottomOfInsulation', to_float(@insulation_interior_distance_to_bottom)) unless @insulation_interior_distance_to_bottom.nil?
      end
    end

    def from_oga(foundation_wall)
      return if foundation_wall.nil?

      @id = HPXML::get_id(foundation_wall)
      @exterior_adjacent_to = XMLHelper.get_value(foundation_wall, 'ExteriorAdjacentTo')
      @interior_adjacent_to = XMLHelper.get_value(foundation_wall, 'InteriorAdjacentTo')
      @height = to_float_or_nil(XMLHelper.get_value(foundation_wall, 'Height'))
      @area = to_float_or_nil(XMLHelper.get_value(foundation_wall, 'Area'))
      @azimuth = to_integer_or_nil(XMLHelper.get_value(foundation_wall, 'Azimuth'))
      @thickness = to_float_or_nil(XMLHelper.get_value(foundation_wall, 'Thickness'))
      @depth_below_grade = to_float_or_nil(XMLHelper.get_value(foundation_wall, 'DepthBelowGrade'))
      insulation = XMLHelper.get_element(foundation_wall, 'Insulation')
      if not insulation.nil?
        @insulation_id = HPXML::get_id(insulation)
        @insulation_r_value = to_float_or_nil(XMLHelper.get_value(insulation, "Layer[InstallationType='continuous']/NominalRValue"))
        @insulation_interior_r_value = to_float_or_nil(XMLHelper.get_value(insulation, "Layer[InstallationType='continuous - interior']/NominalRValue"))
        @insulation_interior_distance_to_top = to_float_or_nil(XMLHelper.get_value(insulation, "Layer[InstallationType='continuous - interior']/extension/DistanceToTopOfInsulation"))
        @insulation_interior_distance_to_bottom = to_float_or_nil(XMLHelper.get_value(insulation, "Layer[InstallationType='continuous - interior']/extension/DistanceToBottomOfInsulation"))
        @insulation_exterior_r_value = to_float_or_nil(XMLHelper.get_value(insulation, "Layer[InstallationType='continuous - exterior']/NominalRValue"))
        @insulation_exterior_distance_to_top = to_float_or_nil(XMLHelper.get_value(insulation, "Layer[InstallationType='continuous - exterior']/extension/DistanceToTopOfInsulation"))
        @insulation_exterior_distance_to_bottom = to_float_or_nil(XMLHelper.get_value(insulation, "Layer[InstallationType='continuous - exterior']/extension/DistanceToBottomOfInsulation"))
        @insulation_continuous_r_value = to_float_or_nil(XMLHelper.get_value(insulation, "Layer[InstallationType='continuous']/NominalRValue"))
        @insulation_assembly_r_value = to_float_or_nil(XMLHelper.get_value(insulation, 'AssemblyEffectiveRValue'))
      end
    end
  end

  class FrameFloors < BaseArrayElement
    def add(**kwargs)
      self << FrameFloor.new(@hpxml_object, **kwargs)
    end

    def from_oga(hpxml)
      return if hpxml.nil?

      XMLHelper.get_elements(hpxml, 'Building/BuildingDetails/Enclosure/FrameFloors/FrameFloor').each do |frame_floor|
        self << FrameFloor.new(@hpxml_object, frame_floor)
      end
    end
  end

  class FrameFloor < BaseElement
    ATTRS = [:id, :exterior_adjacent_to, :interior_adjacent_to, :area, :insulation_id,
             :insulation_assembly_r_value, :insulation_cavity_r_value, :insulation_continuous_r_value,
             :other_space_above_or_below]
    attr_accessor(*ATTRS)

    def is_ceiling
      if [LocationAtticVented, LocationAtticUnvented].include? @interior_adjacent_to
        return true
      elsif [LocationAtticVented, LocationAtticUnvented].include? @exterior_adjacent_to
        return true
      elsif [LocationOtherHousingUnit, LocationOtherHeatedSpace, LocationOtherMultifamilyBufferSpace, LocationOtherNonFreezingSpace].include?(@exterior_adjacent_to) && (@other_space_above_or_below == FrameFloorOtherSpaceAbove)
        return true
      end

      return false
    end

    def is_floor
      !is_ceiling
    end

    def is_exterior
      if @exterior_adjacent_to == LocationOutside
        return true
      end

      return false
    end

    def is_interior
      return !is_exterior
    end

    def is_adiabatic
      return HPXML::is_adiabatic(self)
    end

    def is_thermal_boundary
      return HPXML::is_thermal_boundary(self)
    end

    def is_exterior_thermal_boundary
      return (is_exterior && is_thermal_boundary)
    end

    def delete
      @hpxml_object.frame_floors.delete(self)
      @hpxml_object.attics.each do |attic|
        attic.attached_to_frame_floor_idrefs.delete(@id) unless attic.attached_to_frame_floor_idrefs.nil?
      end
      @hpxml_object.foundations.each do |foundation|
        foundation.attached_to_frame_floor_idrefs.delete(@id) unless foundation.attached_to_frame_floor_idrefs.nil?
      end
    end

    def check_for_errors
      errors = []
      return errors
    end

    def to_oga(doc)
      return if nil?

      frame_floors = XMLHelper.create_elements_as_needed(doc, ['HPXML', 'Building', 'BuildingDetails', 'Enclosure', 'FrameFloors'])
      frame_floor = XMLHelper.add_element(frame_floors, 'FrameFloor')
      sys_id = XMLHelper.add_element(frame_floor, 'SystemIdentifier')
      XMLHelper.add_attribute(sys_id, 'id', @id)
      XMLHelper.add_element(frame_floor, 'ExteriorAdjacentTo', @exterior_adjacent_to) unless @exterior_adjacent_to.nil?
      XMLHelper.add_element(frame_floor, 'InteriorAdjacentTo', @interior_adjacent_to) unless @interior_adjacent_to.nil?
      XMLHelper.add_element(frame_floor, 'Area', to_float(@area)) unless @area.nil?
      insulation = XMLHelper.add_element(frame_floor, 'Insulation')
      sys_id = XMLHelper.add_element(insulation, 'SystemIdentifier')
      if not @insulation_id.nil?
        XMLHelper.add_attribute(sys_id, 'id', @insulation_id)
      else
        XMLHelper.add_attribute(sys_id, 'id', @id + 'Insulation')
      end
      XMLHelper.add_element(insulation, 'AssemblyEffectiveRValue', to_float(@insulation_assembly_r_value)) unless @insulation_assembly_r_value.nil?
      XMLHelper.add_extension(frame_floor, 'OtherSpaceAboveOrBelow', @other_space_above_or_below) unless @other_space_above_or_below.nil?
    end

    def from_oga(frame_floor)
      return if frame_floor.nil?

      @id = HPXML::get_id(frame_floor)
      @exterior_adjacent_to = XMLHelper.get_value(frame_floor, 'ExteriorAdjacentTo')
      @interior_adjacent_to = XMLHelper.get_value(frame_floor, 'InteriorAdjacentTo')
      @area = to_float_or_nil(XMLHelper.get_value(frame_floor, 'Area'))
      insulation = XMLHelper.get_element(frame_floor, 'Insulation')
      if not insulation.nil?
        @insulation_id = HPXML::get_id(insulation)
        @insulation_assembly_r_value = to_float_or_nil(XMLHelper.get_value(insulation, 'AssemblyEffectiveRValue'))
        @insulation_cavity_r_value = to_float_or_nil(XMLHelper.get_value(insulation, "Layer[InstallationType='cavity']/NominalRValue"))
        @insulation_continuous_r_value = to_float_or_nil(XMLHelper.get_value(insulation, "Layer[InstallationType='continuous']/NominalRValue"))
      end
      @other_space_above_or_below = XMLHelper.get_value(frame_floor, 'extension/OtherSpaceAboveOrBelow')
    end
  end

  class Slabs < BaseArrayElement
    def add(**kwargs)
      self << Slab.new(@hpxml_object, **kwargs)
    end

    def from_oga(hpxml)
      return if hpxml.nil?

      XMLHelper.get_elements(hpxml, 'Building/BuildingDetails/Enclosure/Slabs/Slab').each do |slab|
        self << Slab.new(@hpxml_object, slab)
      end
    end
  end

  class Slab < BaseElement
    ATTRS = [:id, :interior_adjacent_to, :exterior_adjacent_to, :area, :thickness, :exposed_perimeter,
             :perimeter_insulation_depth, :under_slab_insulation_width,
             :under_slab_insulation_spans_entire_slab, :depth_below_grade, :carpet_fraction,
             :carpet_r_value, :perimeter_insulation_id, :perimeter_insulation_r_value,
             :under_slab_insulation_id, :under_slab_insulation_r_value]
    attr_accessor(*ATTRS)

    def exterior_adjacent_to
      return LocationGround
    end

    def is_exterior
      return true
    end

    def is_interior
      return !is_exterior
    end

    def is_thermal_boundary
      return HPXML::is_thermal_boundary(self)
    end

    def is_exterior_thermal_boundary
      return (is_exterior && is_thermal_boundary)
    end

    def delete
      @hpxml_object.slabs.delete(self)
      @hpxml_object.foundations.each do |foundation|
        foundation.attached_to_slab_idrefs.delete(@id) unless foundation.attached_to_slab_idrefs.nil?
      end
    end

    def check_for_errors
      errors = []

      if not @exposed_perimeter.nil?
        if @exposed_perimeter <= 0
          fail "Exposed perimeter for Slab '#{@id}' must be greater than zero."
        end
      end

      return errors
    end

    def to_oga(doc)
      return if nil?

      slabs = XMLHelper.create_elements_as_needed(doc, ['HPXML', 'Building', 'BuildingDetails', 'Enclosure', 'Slabs'])
      slab = XMLHelper.add_element(slabs, 'Slab')
      sys_id = XMLHelper.add_element(slab, 'SystemIdentifier')
      XMLHelper.add_attribute(sys_id, 'id', @id)
      XMLHelper.add_element(slab, 'InteriorAdjacentTo', @interior_adjacent_to) unless @interior_adjacent_to.nil?
      XMLHelper.add_element(slab, 'Area', to_float(@area)) unless @area.nil?
      XMLHelper.add_element(slab, 'Thickness', to_float(@thickness)) unless @thickness.nil?
      XMLHelper.add_element(slab, 'ExposedPerimeter', to_float(@exposed_perimeter)) unless @exposed_perimeter.nil?
      XMLHelper.add_element(slab, 'PerimeterInsulationDepth', to_float(@perimeter_insulation_depth)) unless @perimeter_insulation_depth.nil?
      XMLHelper.add_element(slab, 'UnderSlabInsulationWidth', to_float(@under_slab_insulation_width)) unless @under_slab_insulation_width.nil?
      XMLHelper.add_element(slab, 'UnderSlabInsulationSpansEntireSlab', to_boolean(@under_slab_insulation_spans_entire_slab)) unless @under_slab_insulation_spans_entire_slab.nil?
      XMLHelper.add_element(slab, 'DepthBelowGrade', to_float(@depth_below_grade)) unless @depth_below_grade.nil?
      insulation = XMLHelper.add_element(slab, 'PerimeterInsulation')
      sys_id = XMLHelper.add_element(insulation, 'SystemIdentifier')
      if not @perimeter_insulation_id.nil?
        XMLHelper.add_attribute(sys_id, 'id', @perimeter_insulation_id)
      else
        XMLHelper.add_attribute(sys_id, 'id', @id + 'PerimeterInsulation')
      end
      layer = XMLHelper.add_element(insulation, 'Layer')
      XMLHelper.add_element(layer, 'InstallationType', 'continuous')
      XMLHelper.add_element(layer, 'NominalRValue', to_float(@perimeter_insulation_r_value)) unless @perimeter_insulation_r_value.nil?
      insulation = XMLHelper.add_element(slab, 'UnderSlabInsulation')
      sys_id = XMLHelper.add_element(insulation, 'SystemIdentifier')
      if not @under_slab_insulation_id.nil?
        XMLHelper.add_attribute(sys_id, 'id', @under_slab_insulation_id)
      else
        XMLHelper.add_attribute(sys_id, 'id', @id + 'UnderSlabInsulation')
      end
      layer = XMLHelper.add_element(insulation, 'Layer')
      XMLHelper.add_element(layer, 'InstallationType', 'continuous')
      XMLHelper.add_element(layer, 'NominalRValue', to_float(@under_slab_insulation_r_value)) unless @under_slab_insulation_r_value.nil?
      XMLHelper.add_extension(slab, 'CarpetFraction', to_float(@carpet_fraction)) unless @carpet_fraction.nil?
      XMLHelper.add_extension(slab, 'CarpetRValue', to_float(@carpet_r_value)) unless @carpet_r_value.nil?
    end

    def from_oga(slab)
      return if slab.nil?

      @id = HPXML::get_id(slab)
      @interior_adjacent_to = XMLHelper.get_value(slab, 'InteriorAdjacentTo')
      @area = to_float_or_nil(XMLHelper.get_value(slab, 'Area'))
      @thickness = to_float_or_nil(XMLHelper.get_value(slab, 'Thickness'))
      @exposed_perimeter = to_float_or_nil(XMLHelper.get_value(slab, 'ExposedPerimeter'))
      @perimeter_insulation_depth = to_float_or_nil(XMLHelper.get_value(slab, 'PerimeterInsulationDepth'))
      @under_slab_insulation_width = to_float_or_nil(XMLHelper.get_value(slab, 'UnderSlabInsulationWidth'))
      @under_slab_insulation_spans_entire_slab = to_boolean_or_nil(XMLHelper.get_value(slab, 'UnderSlabInsulationSpansEntireSlab'))
      @depth_below_grade = to_float_or_nil(XMLHelper.get_value(slab, 'DepthBelowGrade'))
      @carpet_fraction = to_float_or_nil(XMLHelper.get_value(slab, 'extension/CarpetFraction'))
      @carpet_r_value = to_float_or_nil(XMLHelper.get_value(slab, 'extension/CarpetRValue'))
      perimeter_insulation = XMLHelper.get_element(slab, 'PerimeterInsulation')
      if not perimeter_insulation.nil?
        @perimeter_insulation_id = HPXML::get_id(perimeter_insulation)
        @perimeter_insulation_r_value = to_float_or_nil(XMLHelper.get_value(perimeter_insulation, "Layer[InstallationType='continuous']/NominalRValue"))
      end
      under_slab_insulation = XMLHelper.get_element(slab, 'UnderSlabInsulation')
      if not under_slab_insulation.nil?
        @under_slab_insulation_id = HPXML::get_id(under_slab_insulation)
        @under_slab_insulation_r_value = to_float_or_nil(XMLHelper.get_value(under_slab_insulation, "Layer[InstallationType='continuous']/NominalRValue"))
      end
    end
  end

  class Windows < BaseArrayElement
    def add(**kwargs)
      self << Window.new(@hpxml_object, **kwargs)
    end

    def from_oga(hpxml)
      return if hpxml.nil?

      XMLHelper.get_elements(hpxml, 'Building/BuildingDetails/Enclosure/Windows/Window').each do |window|
        self << Window.new(@hpxml_object, window)
      end
    end
  end

  class Window < BaseElement
    ATTRS = [:id, :area, :azimuth, :orientation, :frame_type, :aluminum_thermal_break, :glass_layers,
             :glass_type, :gas_fill, :ufactor, :shgc, :interior_shading_factor_summer,
             :interior_shading_factor_winter, :exterior_shading, :overhangs_depth,
             :overhangs_distance_to_top_of_window, :overhangs_distance_to_bottom_of_window,
             :fraction_operable, :wall_idref]
    attr_accessor(*ATTRS)

    def wall
      return if @wall_idref.nil?

      (@hpxml_object.walls + @hpxml_object.foundation_walls).each do |wall|
        next unless wall.id == @wall_idref

        return wall
      end
      fail "Attached wall '#{@wall_idref}' not found for window '#{@id}'."
    end

    def is_exterior
      return wall.is_exterior
    end

    def is_interior
      return !is_exterior
    end

    def is_thermal_boundary
      return HPXML::is_thermal_boundary(wall)
    end

    def is_exterior_thermal_boundary
      return (is_exterior && is_thermal_boundary)
    end

    def delete
      @hpxml_object.windows.delete(self)
    end

    def check_for_errors
      errors = []
      begin; wall; rescue StandardError => e; errors << e.message; end
      if (not @overhangs_distance_to_top_of_window.nil?) && (not @overhangs_distance_to_bottom_of_window.nil?)
        if @overhangs_distance_to_bottom_of_window <= @overhangs_distance_to_top_of_window
          fail "For Window '#{@id}', overhangs distance to bottom (#{@overhangs_distance_to_bottom_of_window}) must be greater than distance to top (#{@overhangs_distance_to_top_of_window})."
        end
      end
      # TODO: Remove this error when we can support it w/ EnergyPlus
      if (not @interior_shading_factor_summer.nil?) && (not @interior_shading_factor_winter.nil?)
        if @interior_shading_factor_summer > @interior_shading_factor_winter
          fail "SummerShadingCoefficient (#{interior_shading_factor_summer}) must be less than or equal to WinterShadingCoefficient (#{interior_shading_factor_winter}) for window '#{@id}'."
        end
      end

      return errors
    end

    def to_oga(doc)
      return if nil?

      windows = XMLHelper.create_elements_as_needed(doc, ['HPXML', 'Building', 'BuildingDetails', 'Enclosure', 'Windows'])
      window = XMLHelper.add_element(windows, 'Window')
      sys_id = XMLHelper.add_element(window, 'SystemIdentifier')
      XMLHelper.add_attribute(sys_id, 'id', @id)
      XMLHelper.add_element(window, 'Area', to_float(@area)) unless @area.nil?
      XMLHelper.add_element(window, 'Azimuth', to_integer(@azimuth)) unless @azimuth.nil?
      XMLHelper.add_element(window, 'UFactor', to_float(@ufactor)) unless @ufactor.nil?
      XMLHelper.add_element(window, 'SHGC', to_float(@shgc)) unless @shgc.nil?
      if (not @interior_shading_factor_summer.nil?) || (not @interior_shading_factor_winter.nil?)
        interior_shading = XMLHelper.add_element(window, 'InteriorShading')
        sys_id = XMLHelper.add_element(interior_shading, 'SystemIdentifier')
        XMLHelper.add_attribute(sys_id, 'id', "#{id}InteriorShading")
        XMLHelper.add_element(interior_shading, 'SummerShadingCoefficient', to_float(@interior_shading_factor_summer)) unless @interior_shading_factor_summer.nil?
        XMLHelper.add_element(interior_shading, 'WinterShadingCoefficient', to_float(@interior_shading_factor_winter)) unless @interior_shading_factor_winter.nil?
      end
      if (not @overhangs_depth.nil?) || (not @overhangs_distance_to_top_of_window.nil?) || (not @overhangs_distance_to_bottom_of_window.nil?)
        overhangs = XMLHelper.add_element(window, 'Overhangs')
        XMLHelper.add_element(overhangs, 'Depth', to_float(@overhangs_depth)) unless @overhangs_depth.nil?
        XMLHelper.add_element(overhangs, 'DistanceToTopOfWindow', to_float(@overhangs_distance_to_top_of_window)) unless @overhangs_distance_to_top_of_window.nil?
        XMLHelper.add_element(overhangs, 'DistanceToBottomOfWindow', to_float(@overhangs_distance_to_bottom_of_window)) unless @overhangs_distance_to_bottom_of_window.nil?
      end
      XMLHelper.add_element(window, 'FractionOperable', to_float(@fraction_operable)) unless @fraction_operable.nil?
      if not @wall_idref.nil?
        attached_to_wall = XMLHelper.add_element(window, 'AttachedToWall')
        XMLHelper.add_attribute(attached_to_wall, 'idref', @wall_idref)
      end
    end

    def from_oga(window)
      return if window.nil?

      @id = HPXML::get_id(window)
      @area = to_float_or_nil(XMLHelper.get_value(window, 'Area'))
      @azimuth = to_integer_or_nil(XMLHelper.get_value(window, 'Azimuth'))
      @orientation = XMLHelper.get_value(window, 'Orientation')
      @frame_type = XMLHelper.get_child_name(window, 'FrameType')
      @aluminum_thermal_break = to_boolean_or_nil(XMLHelper.get_value(window, 'FrameType/Aluminum/ThermalBreak'))
      @glass_layers = XMLHelper.get_value(window, 'GlassLayers')
      @glass_type = XMLHelper.get_value(window, 'GlassType')
      @gas_fill = XMLHelper.get_value(window, 'GasFill')
      @ufactor = to_float_or_nil(XMLHelper.get_value(window, 'UFactor'))
      @shgc = to_float_or_nil(XMLHelper.get_value(window, 'SHGC'))
      @interior_shading_factor_summer = to_float_or_nil(XMLHelper.get_value(window, 'InteriorShading/SummerShadingCoefficient'))
      @interior_shading_factor_winter = to_float_or_nil(XMLHelper.get_value(window, 'InteriorShading/WinterShadingCoefficient'))
      @exterior_shading = XMLHelper.get_value(window, 'ExteriorShading/Type')
      @overhangs_depth = to_float_or_nil(XMLHelper.get_value(window, 'Overhangs/Depth'))
      @overhangs_distance_to_top_of_window = to_float_or_nil(XMLHelper.get_value(window, 'Overhangs/DistanceToTopOfWindow'))
      @overhangs_distance_to_bottom_of_window = to_float_or_nil(XMLHelper.get_value(window, 'Overhangs/DistanceToBottomOfWindow'))
      @fraction_operable = to_float_or_nil(XMLHelper.get_value(window, 'FractionOperable'))
      @wall_idref = HPXML::get_idref(XMLHelper.get_element(window, 'AttachedToWall'))
    end
  end

  class Skylights < BaseArrayElement
    def add(**kwargs)
      self << Skylight.new(@hpxml_object, **kwargs)
    end

    def from_oga(hpxml)
      return if hpxml.nil?

      XMLHelper.get_elements(hpxml, 'Building/BuildingDetails/Enclosure/Skylights/Skylight').each do |skylight|
        self << Skylight.new(@hpxml_object, skylight)
      end
    end
  end

  class Skylight < BaseElement
    ATTRS = [:id, :area, :azimuth, :orientation, :frame_type, :aluminum_thermal_break, :glass_layers,
             :glass_type, :gas_fill, :ufactor, :shgc, :interior_shading_factor_summer,
             :interior_shading_factor_winter, :exterior_shading, :roof_idref]
    attr_accessor(*ATTRS)

    def roof
      return if @roof_idref.nil?

      @hpxml_object.roofs.each do |roof|
        next unless roof.id == @roof_idref

        return roof
      end
      fail "Attached roof '#{@roof_idref}' not found for skylight '#{@id}'."
    end

    def is_exterior
      return roof.is_exterior
    end

    def is_interior
      return !is_exterior
    end

    def is_thermal_boundary
      return HPXML::is_thermal_boundary(roof)
    end

    def is_exterior_thermal_boundary
      return (is_exterior && is_thermal_boundary)
    end

    def delete
      @hpxml_object.skylights.delete(self)
    end

    def check_for_errors
      errors = []
      begin; roof; rescue StandardError => e; errors << e.message; end
      return errors
    end

    def to_oga(doc)
      return if nil?

      skylights = XMLHelper.create_elements_as_needed(doc, ['HPXML', 'Building', 'BuildingDetails', 'Enclosure', 'Skylights'])
      skylight = XMLHelper.add_element(skylights, 'Skylight')
      sys_id = XMLHelper.add_element(skylight, 'SystemIdentifier')
      XMLHelper.add_attribute(sys_id, 'id', @id)
      XMLHelper.add_element(skylight, 'Area', to_float(@area)) unless @area.nil?
      XMLHelper.add_element(skylight, 'Azimuth', to_integer(@azimuth)) unless @azimuth.nil?
      XMLHelper.add_element(skylight, 'UFactor', to_float(@ufactor)) unless @ufactor.nil?
      XMLHelper.add_element(skylight, 'SHGC', to_float(@shgc)) unless @shgc.nil?
      if (not @interior_shading_factor_summer.nil?) || (not @interior_shading_factor_winter.nil?)
        interior_shading = XMLHelper.add_element(skylight, 'InteriorShading')
        sys_id = XMLHelper.add_element(interior_shading, 'SystemIdentifier')
        XMLHelper.add_attribute(sys_id, 'id', "#{id}InteriorShading")
        XMLHelper.add_element(interior_shading, 'SummerShadingCoefficient', to_float(@interior_shading_factor_summer)) unless @interior_shading_factor_summer.nil?
        XMLHelper.add_element(interior_shading, 'WinterShadingCoefficient', to_float(@interior_shading_factor_winter)) unless @interior_shading_factor_winter.nil?
      end
      if not @roof_idref.nil?
        attached_to_roof = XMLHelper.add_element(skylight, 'AttachedToRoof')
        XMLHelper.add_attribute(attached_to_roof, 'idref', @roof_idref)
      end
    end

    def from_oga(skylight)
      return if skylight.nil?

      @id = HPXML::get_id(skylight)
      @area = to_float_or_nil(XMLHelper.get_value(skylight, 'Area'))
      @azimuth = to_integer_or_nil(XMLHelper.get_value(skylight, 'Azimuth'))
      @orientation = XMLHelper.get_value(skylight, 'Orientation')
      @frame_type = XMLHelper.get_child_name(skylight, 'FrameType')
      @aluminum_thermal_break = to_boolean_or_nil(XMLHelper.get_value(skylight, 'FrameType/Aluminum/ThermalBreak'))
      @glass_layers = XMLHelper.get_value(skylight, 'GlassLayers')
      @glass_type = XMLHelper.get_value(skylight, 'GlassType')
      @gas_fill = XMLHelper.get_value(skylight, 'GasFill')
      @ufactor = to_float_or_nil(XMLHelper.get_value(skylight, 'UFactor'))
      @shgc = to_float_or_nil(XMLHelper.get_value(skylight, 'SHGC'))
      @interior_shading_factor_summer = to_float_or_nil(XMLHelper.get_value(skylight, 'InteriorShading/SummerShadingCoefficient'))
      @interior_shading_factor_winter = to_float_or_nil(XMLHelper.get_value(skylight, 'InteriorShading/WinterShadingCoefficient'))
      @exterior_shading = XMLHelper.get_value(skylight, 'ExteriorShading/Type')
      @roof_idref = HPXML::get_idref(XMLHelper.get_element(skylight, 'AttachedToRoof'))
    end
  end

  class Doors < BaseArrayElement
    def add(**kwargs)
      self << Door.new(@hpxml_object, **kwargs)
    end

    def from_oga(hpxml)
      return if hpxml.nil?

      XMLHelper.get_elements(hpxml, 'Building/BuildingDetails/Enclosure/Doors/Door').each do |door|
        self << Door.new(@hpxml_object, door)
      end
    end
  end

  class Door < BaseElement
    ATTRS = [:id, :wall_idref, :area, :azimuth, :r_value]
    attr_accessor(*ATTRS)

    def wall
      return if @wall_idref.nil?

      (@hpxml_object.walls + @hpxml_object.foundation_walls).each do |wall|
        next unless wall.id == @wall_idref

        return wall
      end
      fail "Attached wall '#{@wall_idref}' not found for door '#{@id}'."
    end

    def is_exterior
      return wall.is_exterior
    end

    def is_interior
      return !is_exterior
    end

    def is_thermal_boundary
      return HPXML::is_thermal_boundary(wall)
    end

    def is_exterior_thermal_boundary
      return (is_exterior && is_thermal_boundary)
    end

    def delete
      @hpxml_object.doors.delete(self)
    end

    def check_for_errors
      errors = []
      begin; wall; rescue StandardError => e; errors << e.message; end
      return errors
    end

    def to_oga(doc)
      return if nil?

      doors = XMLHelper.create_elements_as_needed(doc, ['HPXML', 'Building', 'BuildingDetails', 'Enclosure', 'Doors'])
      door = XMLHelper.add_element(doors, 'Door')
      sys_id = XMLHelper.add_element(door, 'SystemIdentifier')
      XMLHelper.add_attribute(sys_id, 'id', @id)
      if not @wall_idref.nil?
        attached_to_wall = XMLHelper.add_element(door, 'AttachedToWall')
        XMLHelper.add_attribute(attached_to_wall, 'idref', @wall_idref)
      end
      XMLHelper.add_element(door, 'Area', to_float(@area)) unless @area.nil?
      XMLHelper.add_element(door, 'Azimuth', to_integer(@azimuth)) unless @azimuth.nil?
      XMLHelper.add_element(door, 'RValue', to_float(@r_value)) unless @r_value.nil?
    end

    def from_oga(door)
      return if door.nil?

      @id = HPXML::get_id(door)
      @wall_idref = HPXML::get_idref(XMLHelper.get_element(door, 'AttachedToWall'))
      @area = to_float_or_nil(XMLHelper.get_value(door, 'Area'))
      @azimuth = to_integer_or_nil(XMLHelper.get_value(door, 'Azimuth'))
      @r_value = to_float_or_nil(XMLHelper.get_value(door, 'RValue'))
    end
  end

  class HeatingSystems < BaseArrayElement
    def add(**kwargs)
      self << HeatingSystem.new(@hpxml_object, **kwargs)
    end

    def from_oga(hpxml)
      return if hpxml.nil?

      XMLHelper.get_elements(hpxml, 'Building/BuildingDetails/Systems/HVAC/HVACPlant/HeatingSystem').each do |heating_system|
        self << HeatingSystem.new(@hpxml_object, heating_system)
      end
    end

    def total_fraction_heat_load_served
      map { |htg_sys| htg_sys.fraction_heat_load_served.to_f }.sum(0.0)
    end
  end

  class HeatingSystem < BaseElement
    ATTRS = [:id, :distribution_system_idref, :year_installed, :heating_system_type,
             :heating_system_fuel, :heating_capacity, :heating_efficiency_afue,
             :heating_efficiency_percent, :fraction_heat_load_served, :electric_auxiliary_energy,
             :heating_cfm, :energy_star, :seed_id, :is_shared_system, :number_of_units_served,
             :shared_loop_watts, :fan_coil_watts, :wlhp_heating_efficiency_cop]
    attr_accessor(*ATTRS)

    def distribution_system
      return if @distribution_system_idref.nil?

      @hpxml_object.hvac_distributions.each do |hvac_distribution|
        next unless hvac_distribution.id == @distribution_system_idref

        return hvac_distribution
      end
      fail "Attached HVAC distribution system '#{@distribution_system_idref}' not found for HVAC system '#{@id}'."
    end

    def attached_cooling_system
      return if distribution_system.nil?

      distribution_system.hvac_systems.each do |hvac_system|
        next if hvac_system.id == @id

        return hvac_system
      end
      return
    end

    def delete
      @hpxml_object.heating_systems.delete(self)
      @hpxml_object.water_heating_systems.each do |water_heating_system|
        next unless water_heating_system.related_hvac_idref == @id

        water_heating_system.related_hvac_idref = nil
      end
    end

    def check_for_errors
      errors = []
      begin; distribution_system; rescue StandardError => e; errors << e.message; end
      return errors
    end

    def to_oga(doc)
      return if nil?

      hvac_plant = XMLHelper.create_elements_as_needed(doc, ['HPXML', 'Building', 'BuildingDetails', 'Systems', 'HVAC', 'HVACPlant'])
      heating_system = XMLHelper.add_element(hvac_plant, 'HeatingSystem')
      sys_id = XMLHelper.add_element(heating_system, 'SystemIdentifier')
      XMLHelper.add_attribute(sys_id, 'id', @id)
      if not @distribution_system_idref.nil?
        distribution_system = XMLHelper.add_element(heating_system, 'DistributionSystem')
        XMLHelper.add_attribute(distribution_system, 'idref', @distribution_system_idref)
      end
      XMLHelper.add_element(heating_system, 'IsSharedSystem', to_boolean(@is_shared_system)) unless @is_shared_system.nil?
      XMLHelper.add_element(heating_system, 'NumberofUnitsServed', to_integer(@number_of_units_served)) unless @number_of_units_served.nil?
      if not @heating_system_type.nil?
        heating_system_type_e = XMLHelper.add_element(heating_system, 'HeatingSystemType')
        XMLHelper.add_element(heating_system_type_e, @heating_system_type)
      end
      XMLHelper.add_element(heating_system, 'HeatingSystemFuel', @heating_system_fuel) unless @heating_system_fuel.nil?
      XMLHelper.add_element(heating_system, 'HeatingCapacity', to_float(@heating_capacity)) unless @heating_capacity.nil?

      efficiency_units = nil
      efficiency_value = nil
      if [HVACTypeFurnace, HVACTypeWallFurnace, HVACTypeFloorFurnace, HVACTypeBoiler].include? @heating_system_type
        efficiency_units = UnitsAFUE
        efficiency_value = @heating_efficiency_afue
      elsif [HVACTypeElectricResistance, HVACTypeStove, HVACTypePortableHeater, HVACTypeFixedHeater, HVACTypeFireplace].include? @heating_system_type
        efficiency_units = UnitsPercent
        efficiency_value = @heating_efficiency_percent
      end
      if not efficiency_value.nil?
        annual_efficiency = XMLHelper.add_element(heating_system, 'AnnualHeatingEfficiency')
        XMLHelper.add_element(annual_efficiency, 'Units', efficiency_units)
        XMLHelper.add_element(annual_efficiency, 'Value', to_float(efficiency_value))
      end
      XMLHelper.add_element(heating_system, 'FractionHeatLoadServed', to_float(@fraction_heat_load_served)) unless @fraction_heat_load_served.nil?
      XMLHelper.add_element(heating_system, 'ElectricAuxiliaryEnergy', to_float(@electric_auxiliary_energy)) unless @electric_auxiliary_energy.nil?
      XMLHelper.add_extension(heating_system, 'HeatingFlowRate', to_float(@heating_cfm)) unless @heating_cfm.nil?
      XMLHelper.add_extension(heating_system, 'SharedLoopWatts', to_float(@shared_loop_watts)) unless @shared_loop_watts.nil?
      XMLHelper.add_extension(heating_system, 'FanCoilWatts', to_float(@fan_coil_watts)) unless @fan_coil_watts.nil?
      XMLHelper.add_extension(heating_system, 'SeedId', @seed_id) unless @seed_id.nil?
      if not @wlhp_heating_efficiency_cop.nil?
        wlhp = XMLHelper.create_elements_as_needed(heating_system, ['extension', 'WaterLoopHeatPump'])
        annual_efficiency = XMLHelper.add_element(wlhp, 'AnnualHeatingEfficiency')
        XMLHelper.add_element(annual_efficiency, 'Units', UnitsCOP)
        XMLHelper.add_element(annual_efficiency, 'Value', to_float(@wlhp_heating_efficiency_cop))
      end
    end

    def from_oga(heating_system)
      return if heating_system.nil?

      @id = HPXML::get_id(heating_system)
      @distribution_system_idref = HPXML::get_idref(XMLHelper.get_element(heating_system, 'DistributionSystem'))
      @year_installed = to_integer_or_nil(XMLHelper.get_value(heating_system, 'YearInstalled'))
      @is_shared_system = to_boolean_or_nil(XMLHelper.get_value(heating_system, 'IsSharedSystem'))
      @number_of_units_served = to_integer_or_nil(XMLHelper.get_value(heating_system, 'NumberofUnitsServed'))
      @heating_system_type = XMLHelper.get_child_name(heating_system, 'HeatingSystemType')
      @heating_system_fuel = XMLHelper.get_value(heating_system, 'HeatingSystemFuel')
      @heating_capacity = to_float_or_nil(XMLHelper.get_value(heating_system, 'HeatingCapacity'))
      if [HVACTypeFurnace, HVACTypeWallFurnace, HVACTypeFloorFurnace, HVACTypeBoiler].include? @heating_system_type
        @heating_efficiency_afue = to_float_or_nil(XMLHelper.get_value(heating_system, "AnnualHeatingEfficiency[Units='#{UnitsAFUE}']/Value"))
      elsif [HVACTypeElectricResistance, HVACTypeStove, HVACTypePortableHeater, HVACTypeFixedHeater, HVACTypeFireplace].include? @heating_system_type
        @heating_efficiency_percent = to_float_or_nil(XMLHelper.get_value(heating_system, "AnnualHeatingEfficiency[Units='Percent']/Value"))
      end
      @fraction_heat_load_served = to_float_or_nil(XMLHelper.get_value(heating_system, 'FractionHeatLoadServed'))
      @electric_auxiliary_energy = to_float_or_nil(XMLHelper.get_value(heating_system, 'ElectricAuxiliaryEnergy'))
      @heating_cfm = to_float_or_nil(XMLHelper.get_value(heating_system, 'extension/HeatingFlowRate'))
      @energy_star = XMLHelper.get_values(heating_system, 'ThirdPartyCertification').include?('Energy Star')
      @seed_id = XMLHelper.get_value(heating_system, 'extension/SeedId')
      @shared_loop_watts = to_float_or_nil(XMLHelper.get_value(heating_system, 'extension/SharedLoopWatts'))
      @fan_coil_watts = to_float_or_nil(XMLHelper.get_value(heating_system, 'extension/FanCoilWatts'))
      @wlhp_heating_efficiency_cop = to_float_or_nil(XMLHelper.get_value(heating_system, "extension/WaterLoopHeatPump/AnnualHeatingEfficiency[Units='#{UnitsCOP}']/Value"))
    end
  end

  class CoolingSystems < BaseArrayElement
    def add(**kwargs)
      self << CoolingSystem.new(@hpxml_object, **kwargs)
    end

    def from_oga(hpxml)
      return if hpxml.nil?

      XMLHelper.get_elements(hpxml, 'Building/BuildingDetails/Systems/HVAC/HVACPlant/CoolingSystem').each do |cooling_system|
        self << CoolingSystem.new(@hpxml_object, cooling_system)
      end
    end

    def total_fraction_cool_load_served
      map { |clg_sys| clg_sys.fraction_cool_load_served.to_f }.sum(0.0)
    end
  end

  class CoolingSystem < BaseElement
    ATTRS = [:id, :distribution_system_idref, :year_installed, :cooling_system_type,
             :cooling_system_fuel, :cooling_capacity, :compressor_type, :fraction_cool_load_served,
             :cooling_efficiency_seer, :cooling_efficiency_eer, :cooling_efficiency_kw_per_ton,
             :cooling_shr, :cooling_cfm, :energy_star, :seed_id, :is_shared_system,
             :number_of_units_served, :shared_loop_watts, :fan_coil_watts, :wlhp_cooling_capacity,
             :wlhp_cooling_efficiency_eer]
    attr_accessor(*ATTRS)

    def distribution_system
      return if @distribution_system_idref.nil?

      @hpxml_object.hvac_distributions.each do |hvac_distribution|
        next unless hvac_distribution.id == @distribution_system_idref

        return hvac_distribution
      end
      fail "Attached HVAC distribution system '#{@distribution_system_idref}' not found for HVAC system '#{@id}'."
    end

    def attached_heating_system
      return if distribution_system.nil?

      distribution_system.hvac_systems.each do |hvac_system|
        next if hvac_system.id == @id

        return hvac_system
      end
      return
    end

    def delete
      @hpxml_object.cooling_systems.delete(self)
      @hpxml_object.water_heating_systems.each do |water_heating_system|
        next unless water_heating_system.related_hvac_idref == @id

        water_heating_system.related_hvac_idref = nil
      end
    end

    def check_for_errors
      errors = []
      begin; distribution_system; rescue StandardError => e; errors << e.message; end
      return errors
    end

    def to_oga(doc)
      return if nil?

      hvac_plant = XMLHelper.create_elements_as_needed(doc, ['HPXML', 'Building', 'BuildingDetails', 'Systems', 'HVAC', 'HVACPlant'])
      cooling_system = XMLHelper.add_element(hvac_plant, 'CoolingSystem')
      sys_id = XMLHelper.add_element(cooling_system, 'SystemIdentifier')
      XMLHelper.add_attribute(sys_id, 'id', @id)
      if not @distribution_system_idref.nil?
        distribution_system = XMLHelper.add_element(cooling_system, 'DistributionSystem')
        XMLHelper.add_attribute(distribution_system, 'idref', @distribution_system_idref)
      end
      XMLHelper.add_element(cooling_system, 'IsSharedSystem', to_boolean(@is_shared_system)) unless @is_shared_system.nil?
      XMLHelper.add_element(cooling_system, 'NumberofUnitsServed', to_integer(@number_of_units_served)) unless @number_of_units_served.nil?
      XMLHelper.add_element(cooling_system, 'CoolingSystemType', @cooling_system_type) unless @cooling_system_type.nil?
      XMLHelper.add_element(cooling_system, 'CoolingSystemFuel', @cooling_system_fuel) unless @cooling_system_fuel.nil?
      XMLHelper.add_element(cooling_system, 'CoolingCapacity', to_float(@cooling_capacity)) unless @cooling_capacity.nil?
      XMLHelper.add_element(cooling_system, 'CompressorType', @compressor_type) unless @compressor_type.nil?
      XMLHelper.add_element(cooling_system, 'FractionCoolLoadServed', to_float(@fraction_cool_load_served)) unless @fraction_cool_load_served.nil?

      efficiency_units = nil
      efficiency_value = nil
      if [HVACTypeCentralAirConditioner, HVACTypeMiniSplitAirConditioner].include? @cooling_system_type
        efficiency_units = UnitsSEER
        efficiency_value = @cooling_efficiency_seer
      elsif [HVACTypeRoomAirConditioner].include? @cooling_system_type
        efficiency_units = UnitsEER
        efficiency_value = @cooling_efficiency_eer
      elsif [HVACTypeChiller].include? @cooling_system_type
        efficiency_units = UnitsKwPerTon
        efficiency_value = @cooling_efficiency_kw_per_ton
      end
      if not efficiency_value.nil?
        annual_efficiency = XMLHelper.add_element(cooling_system, 'AnnualCoolingEfficiency')
        XMLHelper.add_element(annual_efficiency, 'Units', efficiency_units)
        XMLHelper.add_element(annual_efficiency, 'Value', to_float(efficiency_value))
      end
      XMLHelper.add_element(cooling_system, 'SensibleHeatFraction', to_float(@cooling_shr)) unless @cooling_shr.nil?
      XMLHelper.add_extension(cooling_system, 'CoolingFlowRate', to_float(@cooling_cfm)) unless @cooling_cfm.nil?
      XMLHelper.add_extension(cooling_system, 'SharedLoopWatts', to_float(@shared_loop_watts)) unless @shared_loop_watts.nil?
      XMLHelper.add_extension(cooling_system, 'FanCoilWatts', to_float(@fan_coil_watts)) unless @fan_coil_watts.nil?
      XMLHelper.add_extension(cooling_system, 'SeedId', @seed_id) unless @seed_id.nil?
      if (not @wlhp_cooling_capacity.nil?) || (not @wlhp_cooling_efficiency_eer.nil?)
        wlhp = XMLHelper.create_elements_as_needed(cooling_system, ['extension', 'WaterLoopHeatPump'])
        XMLHelper.add_element(wlhp, 'CoolingCapacity', to_float(@wlhp_cooling_capacity)) unless @wlhp_cooling_capacity.nil?
        if not @wlhp_cooling_efficiency_eer.nil?
          annual_efficiency = XMLHelper.add_element(wlhp, 'AnnualCoolingEfficiency')
          XMLHelper.add_element(annual_efficiency, 'Units', UnitsEER)
          XMLHelper.add_element(annual_efficiency, 'Value', to_float(@wlhp_cooling_efficiency_eer))
        end
      end
    end

    def from_oga(cooling_system)
      return if cooling_system.nil?

      @id = HPXML::get_id(cooling_system)
      @distribution_system_idref = HPXML::get_idref(XMLHelper.get_element(cooling_system, 'DistributionSystem'))
      @year_installed = to_integer_or_nil(XMLHelper.get_value(cooling_system, 'YearInstalled'))
      @is_shared_system = to_boolean_or_nil(XMLHelper.get_value(cooling_system, 'IsSharedSystem'))
      @number_of_units_served = to_integer_or_nil(XMLHelper.get_value(cooling_system, 'NumberofUnitsServed'))
      @cooling_system_type = XMLHelper.get_value(cooling_system, 'CoolingSystemType')
      @cooling_system_fuel = XMLHelper.get_value(cooling_system, 'CoolingSystemFuel')
      @cooling_capacity = to_float_or_nil(XMLHelper.get_value(cooling_system, 'CoolingCapacity'))
      @compressor_type = XMLHelper.get_value(cooling_system, 'CompressorType')
      @fraction_cool_load_served = to_float_or_nil(XMLHelper.get_value(cooling_system, 'FractionCoolLoadServed'))
      if [HVACTypeCentralAirConditioner, HVACTypeMiniSplitAirConditioner].include? @cooling_system_type
        @cooling_efficiency_seer = to_float_or_nil(XMLHelper.get_value(cooling_system, "AnnualCoolingEfficiency[Units='#{UnitsSEER}']/Value"))
      elsif [HVACTypeRoomAirConditioner].include? @cooling_system_type
        @cooling_efficiency_eer = to_float_or_nil(XMLHelper.get_value(cooling_system, "AnnualCoolingEfficiency[Units='#{UnitsEER}']/Value"))
      elsif [HVACTypeChiller].include? @cooling_system_type
        @cooling_efficiency_kw_per_ton = to_float_or_nil(XMLHelper.get_value(cooling_system, "AnnualCoolingEfficiency[Units='#{UnitsKwPerTon}']/Value"))
      end
      @cooling_shr = to_float_or_nil(XMLHelper.get_value(cooling_system, 'SensibleHeatFraction'))
      @cooling_cfm = to_float_or_nil(XMLHelper.get_value(cooling_system, 'extension/CoolingFlowRate'))
      @energy_star = XMLHelper.get_values(cooling_system, 'ThirdPartyCertification').include?('Energy Star')
      @seed_id = XMLHelper.get_value(cooling_system, 'extension/SeedId')
      @shared_loop_watts = to_float_or_nil(XMLHelper.get_value(cooling_system, 'extension/SharedLoopWatts'))
      @fan_coil_watts = to_float_or_nil(XMLHelper.get_value(cooling_system, 'extension/FanCoilWatts'))
      @wlhp_cooling_capacity = to_float_or_nil(XMLHelper.get_value(cooling_system, 'extension/WaterLoopHeatPump/CoolingCapacity'))
      @wlhp_cooling_efficiency_eer = to_float_or_nil(XMLHelper.get_value(cooling_system, "extension/WaterLoopHeatPump/AnnualCoolingEfficiency[Units='#{UnitsEER}']/Value"))
    end
  end

  class HeatPumps < BaseArrayElement
    def add(**kwargs)
      self << HeatPump.new(@hpxml_object, **kwargs)
    end

    def from_oga(hpxml)
      return if hpxml.nil?

      XMLHelper.get_elements(hpxml, 'Building/BuildingDetails/Systems/HVAC/HVACPlant/HeatPump').each do |heat_pump|
        self << HeatPump.new(@hpxml_object, heat_pump)
      end
    end

    def total_fraction_heat_load_served
      map { |hp| hp.fraction_heat_load_served.to_f }.sum(0.0)
    end

    def total_fraction_cool_load_served
      map { |hp| hp.fraction_cool_load_served.to_f }.sum(0.0)
    end
  end

  class HeatPump < BaseElement
    ATTRS = [:id, :distribution_system_idref, :year_installed, :heat_pump_type, :heat_pump_fuel,
             :heating_capacity, :heating_capacity_17F, :cooling_capacity, :compressor_type,
             :cooling_shr, :backup_heating_fuel, :backup_heating_capacity,
             :backup_heating_efficiency_percent, :backup_heating_efficiency_afue,
             :backup_heating_switchover_temp, :fraction_heat_load_served, :fraction_cool_load_served,
             :cooling_efficiency_seer, :cooling_efficiency_eer, :heating_efficiency_hspf,
             :heating_efficiency_cop, :energy_star, :seed_id, :pump_watts_per_ton, :fan_watts_per_cfm,
             :is_shared_system, :number_of_units_served, :shared_loop_watts]
    attr_accessor(*ATTRS)

    def distribution_system
      return if @distribution_system_idref.nil?

      @hpxml_object.hvac_distributions.each do |hvac_distribution|
        next unless hvac_distribution.id == @distribution_system_idref

        return hvac_distribution
      end
      fail "Attached HVAC distribution system '#{@distribution_system_idref}' not found for HVAC system '#{@id}'."
    end

    def delete
      @hpxml_object.heat_pumps.delete(self)
      @hpxml_object.water_heating_systems.each do |water_heating_system|
        next unless water_heating_system.related_hvac_idref == @id

        water_heating_system.related_hvac_idref = nil
      end
    end

    def check_for_errors
      errors = []
      begin; distribution_system; rescue StandardError => e; errors << e.message; end
      return errors
    end

    def to_oga(doc)
      return if nil?

      hvac_plant = XMLHelper.create_elements_as_needed(doc, ['HPXML', 'Building', 'BuildingDetails', 'Systems', 'HVAC', 'HVACPlant'])
      heat_pump = XMLHelper.add_element(hvac_plant, 'HeatPump')
      sys_id = XMLHelper.add_element(heat_pump, 'SystemIdentifier')
      XMLHelper.add_attribute(sys_id, 'id', @id)
      if not @distribution_system_idref.nil?
        distribution_system = XMLHelper.add_element(heat_pump, 'DistributionSystem')
        XMLHelper.add_attribute(distribution_system, 'idref', @distribution_system_idref)
      end
      XMLHelper.add_element(heat_pump, 'IsSharedSystem', to_boolean(@is_shared_system)) unless @is_shared_system.nil?
      XMLHelper.add_element(heat_pump, 'NumberofUnitsServed', to_integer(@number_of_units_served)) unless @number_of_units_served.nil?
      XMLHelper.add_element(heat_pump, 'HeatPumpType', @heat_pump_type) unless @heat_pump_type.nil?
      XMLHelper.add_element(heat_pump, 'HeatPumpFuel', @heat_pump_fuel) unless @heat_pump_fuel.nil?
      XMLHelper.add_element(heat_pump, 'HeatingCapacity', to_float(@heating_capacity)) unless @heating_capacity.nil?
      XMLHelper.add_element(heat_pump, 'HeatingCapacity17F', to_float(@heating_capacity_17F)) unless @heating_capacity_17F.nil?
      XMLHelper.add_element(heat_pump, 'CoolingCapacity', to_float(@cooling_capacity)) unless @cooling_capacity.nil?
      XMLHelper.add_element(heat_pump, 'CompressorType', @compressor_type) unless @compressor_type.nil?
      XMLHelper.add_element(heat_pump, 'CoolingSensibleHeatFraction', to_float(@cooling_shr)) unless @cooling_shr.nil?
      if not @backup_heating_fuel.nil?
        XMLHelper.add_element(heat_pump, 'BackupSystemFuel', @backup_heating_fuel)
        efficiencies = { 'Percent' => @backup_heating_efficiency_percent,
                         UnitsAFUE => @backup_heating_efficiency_afue }
        efficiencies.each do |units, value|
          next if value.nil?

          backup_eff = XMLHelper.add_element(heat_pump, 'BackupAnnualHeatingEfficiency')
          XMLHelper.add_element(backup_eff, 'Units', units)
          XMLHelper.add_element(backup_eff, 'Value', to_float(value))
        end
        XMLHelper.add_element(heat_pump, 'BackupHeatingCapacity', to_float(@backup_heating_capacity)) unless @backup_heating_capacity.nil?
        XMLHelper.add_element(heat_pump, 'BackupHeatingSwitchoverTemperature', to_float(@backup_heating_switchover_temp)) unless @backup_heating_switchover_temp.nil?
      end
      XMLHelper.add_element(heat_pump, 'FractionHeatLoadServed', to_float(@fraction_heat_load_served)) unless @fraction_heat_load_served.nil?
      XMLHelper.add_element(heat_pump, 'FractionCoolLoadServed', to_float(@fraction_cool_load_served)) unless @fraction_cool_load_served.nil?

      clg_efficiency_units = nil
      clg_efficiency_value = nil
      htg_efficiency_units = nil
      htg_efficiency_value = nil
      if [HVACTypeHeatPumpAirToAir, HVACTypeHeatPumpMiniSplit].include? @heat_pump_type
        clg_efficiency_units = UnitsSEER
        clg_efficiency_value = @cooling_efficiency_seer
        htg_efficiency_units = UnitsHSPF
        htg_efficiency_value = @heating_efficiency_hspf
      elsif [HVACTypeHeatPumpGroundToAir].include? @heat_pump_type
        clg_efficiency_units = UnitsEER
        clg_efficiency_value = @cooling_efficiency_eer
        htg_efficiency_units = UnitsCOP
        htg_efficiency_value = @heating_efficiency_cop
      end
      if not clg_efficiency_value.nil?
        annual_efficiency = XMLHelper.add_element(heat_pump, 'AnnualCoolingEfficiency')
        XMLHelper.add_element(annual_efficiency, 'Units', clg_efficiency_units)
        XMLHelper.add_element(annual_efficiency, 'Value', to_float(clg_efficiency_value))
      end
      if not htg_efficiency_value.nil?
        annual_efficiency = XMLHelper.add_element(heat_pump, 'AnnualHeatingEfficiency')
        XMLHelper.add_element(annual_efficiency, 'Units', htg_efficiency_units)
        XMLHelper.add_element(annual_efficiency, 'Value', to_float(htg_efficiency_value))
      end
      XMLHelper.add_extension(heat_pump, 'PumpPowerWattsPerTon', to_float(@pump_watts_per_ton)) unless @pump_watts_per_ton.nil?
      XMLHelper.add_extension(heat_pump, 'FanPowerWattsPerCFM', to_float(@fan_watts_per_cfm)) unless @fan_watts_per_cfm.nil?
      XMLHelper.add_extension(heat_pump, 'SharedLoopWatts', to_float(@shared_loop_watts)) unless @shared_loop_watts.nil?
      XMLHelper.add_extension(heat_pump, 'SeedId', @seed_id) unless @seed_id.nil?
    end

    def from_oga(heat_pump)
      return if heat_pump.nil?

      @id = HPXML::get_id(heat_pump)
      @distribution_system_idref = HPXML::get_idref(XMLHelper.get_element(heat_pump, 'DistributionSystem'))
      @year_installed = to_integer_or_nil(XMLHelper.get_value(heat_pump, 'YearInstalled'))
      @is_shared_system = to_boolean_or_nil(XMLHelper.get_value(heat_pump, 'IsSharedSystem'))
      @number_of_units_served = to_integer_or_nil(XMLHelper.get_value(heat_pump, 'NumberofUnitsServed'))
      @heat_pump_type = XMLHelper.get_value(heat_pump, 'HeatPumpType')
      @heat_pump_fuel = XMLHelper.get_value(heat_pump, 'HeatPumpFuel')
      @heating_capacity = to_float_or_nil(XMLHelper.get_value(heat_pump, 'HeatingCapacity'))
      @heating_capacity_17F = to_float_or_nil(XMLHelper.get_value(heat_pump, 'HeatingCapacity17F'))
      @cooling_capacity = to_float_or_nil(XMLHelper.get_value(heat_pump, 'CoolingCapacity'))
      @compressor_type = XMLHelper.get_value(heat_pump, 'CompressorType')
      @cooling_shr = to_float_or_nil(XMLHelper.get_value(heat_pump, 'CoolingSensibleHeatFraction'))
      @backup_heating_fuel = XMLHelper.get_value(heat_pump, 'BackupSystemFuel')
      @backup_heating_capacity = to_float_or_nil(XMLHelper.get_value(heat_pump, 'BackupHeatingCapacity'))
      @backup_heating_efficiency_percent = to_float_or_nil(XMLHelper.get_value(heat_pump, "BackupAnnualHeatingEfficiency[Units='Percent']/Value"))
      @backup_heating_efficiency_afue = to_float_or_nil(XMLHelper.get_value(heat_pump, "BackupAnnualHeatingEfficiency[Units='#{UnitsAFUE}']/Value"))
      @backup_heating_switchover_temp = to_float_or_nil(XMLHelper.get_value(heat_pump, 'BackupHeatingSwitchoverTemperature'))
      @fraction_heat_load_served = to_float_or_nil(XMLHelper.get_value(heat_pump, 'FractionHeatLoadServed'))
      @fraction_cool_load_served = to_float_or_nil(XMLHelper.get_value(heat_pump, 'FractionCoolLoadServed'))
      if [HVACTypeHeatPumpAirToAir, HVACTypeHeatPumpMiniSplit].include? @heat_pump_type
        @cooling_efficiency_seer = to_float_or_nil(XMLHelper.get_value(heat_pump, "AnnualCoolingEfficiency[Units='#{UnitsSEER}']/Value"))
      elsif [HVACTypeHeatPumpGroundToAir].include? @heat_pump_type
        @cooling_efficiency_eer = to_float_or_nil(XMLHelper.get_value(heat_pump, "AnnualCoolingEfficiency[Units='#{UnitsEER}']/Value"))
      end
      if [HVACTypeHeatPumpAirToAir, HVACTypeHeatPumpMiniSplit].include? @heat_pump_type
        @heating_efficiency_hspf = to_float_or_nil(XMLHelper.get_value(heat_pump, "AnnualHeatingEfficiency[Units='#{UnitsHSPF}']/Value"))
      elsif [HVACTypeHeatPumpGroundToAir].include? @heat_pump_type
        @heating_efficiency_cop = to_float_or_nil(XMLHelper.get_value(heat_pump, "AnnualHeatingEfficiency[Units='#{UnitsCOP}']/Value"))
      end
      @energy_star = XMLHelper.get_values(heat_pump, 'ThirdPartyCertification').include?('Energy Star')
      @pump_watts_per_ton = to_float_or_nil(XMLHelper.get_value(heat_pump, 'extension/PumpPowerWattsPerTon'))
      @fan_watts_per_cfm = to_float_or_nil(XMLHelper.get_value(heat_pump, 'extension/FanPowerWattsPerCFM'))
      @seed_id = XMLHelper.get_value(heat_pump, 'extension/SeedId')
      @shared_loop_watts = to_float_or_nil(XMLHelper.get_value(heat_pump, 'extension/SharedLoopWatts'))
    end
  end

  class HVACControls < BaseArrayElement
    def add(**kwargs)
      self << HVACControl.new(@hpxml_object, **kwargs)
    end

    def from_oga(hpxml)
      return if hpxml.nil?

      XMLHelper.get_elements(hpxml, 'Building/BuildingDetails/Systems/HVAC/HVACControl').each do |hvac_control|
        self << HVACControl.new(@hpxml_object, hvac_control)
      end
    end
  end

  class HVACControl < BaseElement
    ATTRS = [:id, :control_type, :heating_setpoint_temp, :heating_setback_temp,
             :heating_setback_hours_per_week, :heating_setback_start_hour, :cooling_setpoint_temp,
             :cooling_setup_temp, :cooling_setup_hours_per_week, :cooling_setup_start_hour,
             :ceiling_fan_cooling_setpoint_temp_offset]
    attr_accessor(*ATTRS)

    def delete
      @hpxml_object.hvac_controls.delete(self)
    end

    def check_for_errors
      errors = []
      return errors
    end

    def to_oga(doc)
      return if nil?

      hvac = XMLHelper.create_elements_as_needed(doc, ['HPXML', 'Building', 'BuildingDetails', 'Systems', 'HVAC'])
      hvac_control = XMLHelper.add_element(hvac, 'HVACControl')
      sys_id = XMLHelper.add_element(hvac_control, 'SystemIdentifier')
      XMLHelper.add_attribute(sys_id, 'id', @id)
      XMLHelper.add_element(hvac_control, 'ControlType', @control_type) unless @control_type.nil?
      XMLHelper.add_element(hvac_control, 'SetpointTempHeatingSeason', to_float(@heating_setpoint_temp)) unless @heating_setpoint_temp.nil?
      XMLHelper.add_element(hvac_control, 'SetbackTempHeatingSeason', to_float(@heating_setback_temp)) unless @heating_setback_temp.nil?
      XMLHelper.add_element(hvac_control, 'TotalSetbackHoursperWeekHeating', to_integer(@heating_setback_hours_per_week)) unless @heating_setback_hours_per_week.nil?
      XMLHelper.add_element(hvac_control, 'SetupTempCoolingSeason', to_float(@cooling_setup_temp)) unless @cooling_setup_temp.nil?
      XMLHelper.add_element(hvac_control, 'SetpointTempCoolingSeason', to_float(@cooling_setpoint_temp)) unless @cooling_setpoint_temp.nil?
      XMLHelper.add_element(hvac_control, 'TotalSetupHoursperWeekCooling', to_integer(@cooling_setup_hours_per_week)) unless @cooling_setup_hours_per_week.nil?
      XMLHelper.add_extension(hvac_control, 'SetbackStartHourHeating', to_integer(@heating_setback_start_hour)) unless @heating_setback_start_hour.nil?
      XMLHelper.add_extension(hvac_control, 'SetupStartHourCooling', to_integer(@cooling_setup_start_hour)) unless @cooling_setup_start_hour.nil?
      XMLHelper.add_extension(hvac_control, 'CeilingFanSetpointTempCoolingSeasonOffset', to_float(@ceiling_fan_cooling_setpoint_temp_offset)) unless @ceiling_fan_cooling_setpoint_temp_offset.nil?
    end

    def from_oga(hvac_control)
      return if hvac_control.nil?

      @id = HPXML::get_id(hvac_control)
      @control_type = XMLHelper.get_value(hvac_control, 'ControlType')
      @heating_setpoint_temp = to_float_or_nil(XMLHelper.get_value(hvac_control, 'SetpointTempHeatingSeason'))
      @heating_setback_temp = to_float_or_nil(XMLHelper.get_value(hvac_control, 'SetbackTempHeatingSeason'))
      @heating_setback_hours_per_week = to_integer_or_nil(XMLHelper.get_value(hvac_control, 'TotalSetbackHoursperWeekHeating'))
      @heating_setback_start_hour = to_integer_or_nil(XMLHelper.get_value(hvac_control, 'extension/SetbackStartHourHeating'))
      @cooling_setpoint_temp = to_float_or_nil(XMLHelper.get_value(hvac_control, 'SetpointTempCoolingSeason'))
      @cooling_setup_temp = to_float_or_nil(XMLHelper.get_value(hvac_control, 'SetupTempCoolingSeason'))
      @cooling_setup_hours_per_week = to_integer_or_nil(XMLHelper.get_value(hvac_control, 'TotalSetupHoursperWeekCooling'))
      @cooling_setup_start_hour = to_integer_or_nil(XMLHelper.get_value(hvac_control, 'extension/SetupStartHourCooling'))
      @ceiling_fan_cooling_setpoint_temp_offset = to_float_or_nil(XMLHelper.get_value(hvac_control, 'extension/CeilingFanSetpointTempCoolingSeasonOffset'))
    end
  end

  class HVACDistributions < BaseArrayElement
    def add(**kwargs)
      self << HVACDistribution.new(@hpxml_object, **kwargs)
    end

    def from_oga(hpxml)
      return if hpxml.nil?

      XMLHelper.get_elements(hpxml, 'Building/BuildingDetails/Systems/HVAC/HVACDistribution').each do |hvac_distribution|
        self << HVACDistribution.new(@hpxml_object, hvac_distribution)
      end
    end
  end

  class HVACDistribution < BaseElement
    def initialize(hpxml_object, *args)
      @duct_leakage_measurements = DuctLeakageMeasurements.new(hpxml_object)
      @ducts = Ducts.new(hpxml_object)
      super(hpxml_object, *args)
    end
    ATTRS = [:id, :distribution_system_type, :annual_heating_dse, :annual_cooling_dse,
             :duct_system_sealed, :duct_leakage_to_outside_testing_exemption, :conditioned_floor_area_served,
             :number_of_return_registers, :hydronic_type, :hydronic_and_air_type]
    attr_accessor(*ATTRS)
    attr_reader(:duct_leakage_measurements, :ducts)

    def hvac_systems
      list = []
      (@hpxml_object.heating_systems + @hpxml_object.cooling_systems + @hpxml_object.heat_pumps).each do |hvac_system|
        next if hvac_system.distribution_system_idref.nil?
        next unless hvac_system.distribution_system_idref == @id

        list << hvac_system
      end

      if list.size == 0
        fail "Distribution system '#{@id}' found but no HVAC system attached to it."
      end

      num_htg = 0
      num_clg = 0
      list.each do |obj|
        if obj.respond_to? :fraction_heat_load_served
          num_htg += 1 if obj.fraction_heat_load_served.to_f > 0
        end
        if obj.respond_to? :fraction_cool_load_served
          num_clg += 1 if obj.fraction_cool_load_served.to_f > 0
        end
      end

      if num_clg > 1
        fail "Multiple cooling systems found attached to distribution system '#{@id}'."
      end
      if num_htg > 1
        fail "Multiple heating systems found attached to distribution system '#{@id}'."
      end

      return list
    end

    def delete
      @hpxml_object.hvac_distributions.delete(self)
      (@hpxml_object.heating_systems + @hpxml_object.cooling_systems + @hpxml_object.heat_pumps).each do |hvac_system|
        next if hvac_system.distribution_system_idref.nil?
        next unless hvac_system.distribution_system_idref == @id

        hvac_system.distribution_system_idref = nil
      end
      @hpxml_object.ventilation_fans.each do |ventilation_fan|
        next unless ventilation_fan.distribution_system_idref == @id

        ventilation_fan.distribution_system_idref = nil
      end
    end

    def check_for_errors
      errors = []
      begin; hvac_systems; rescue StandardError => e; errors << e.message; end
      errors += @duct_leakage_measurements.check_for_errors
      errors += @ducts.check_for_errors
      return errors
    end

    def to_oga(doc)
      return if nil?

      hvac = XMLHelper.create_elements_as_needed(doc, ['HPXML', 'Building', 'BuildingDetails', 'Systems', 'HVAC'])
      hvac_distribution = XMLHelper.add_element(hvac, 'HVACDistribution')
      sys_id = XMLHelper.add_element(hvac_distribution, 'SystemIdentifier')
      XMLHelper.add_attribute(sys_id, 'id', @id)
      distribution_system_type_e = XMLHelper.add_element(hvac_distribution, 'DistributionSystemType')
      if [HVACDistributionTypeAir, HVACDistributionTypeHydronic, HVACDistributionTypeHydronicAndAir].include? @distribution_system_type
        XMLHelper.add_element(distribution_system_type_e, @distribution_system_type)
        XMLHelper.add_element(hvac_distribution, 'ConditionedFloorAreaServed', Float(@conditioned_floor_area_served)) unless @conditioned_floor_area_served.nil?
      elsif [HVACDistributionTypeDSE].include? @distribution_system_type
        XMLHelper.add_element(distribution_system_type_e, 'Other', @distribution_system_type)
        XMLHelper.add_element(hvac_distribution, 'AnnualHeatingDistributionSystemEfficiency', to_float(@annual_heating_dse)) unless @annual_heating_dse.nil?
        XMLHelper.add_element(hvac_distribution, 'AnnualCoolingDistributionSystemEfficiency', to_float(@annual_cooling_dse)) unless @annual_cooling_dse.nil?
      else
        fail "Unexpected distribution_system_type '#{@distribution_system_type}'."
      end

      if [HPXML::HVACDistributionTypeHydronic].include? @distribution_system_type
        distribution = XMLHelper.get_element(hvac_distribution, 'DistributionSystemType/HydronicDistribution')
        XMLHelper.add_element(distribution, 'HydronicDistributionType', @hydronic_type) unless @hydronic_type.nil?
      end
      if [HPXML::HVACDistributionTypeHydronicAndAir].include? @distribution_system_type
        distribution = XMLHelper.get_element(hvac_distribution, 'DistributionSystemType/HydronicAndAirDistribution')
        XMLHelper.add_element(distribution, 'HydronicAndAirDistributionType', @hydronic_and_air_type) unless @hydronic_and_air_type.nil?
      end
      if [HPXML::HVACDistributionTypeAir, HPXML::HVACDistributionTypeHydronicAndAir].include? @distribution_system_type
        if @distribution_system_type == HPXML::HVACDistributionTypeAir
          distribution = XMLHelper.get_element(hvac_distribution, 'DistributionSystemType/AirDistribution')
        elsif @distribution_system_type == HPXML::HVACDistributionTypeHydronicAndAir
          distribution = XMLHelper.get_element(hvac_distribution, 'DistributionSystemType/HydronicAndAirDistribution')
        end
        @duct_leakage_measurements.to_oga(distribution)
        @ducts.to_oga(distribution)
        XMLHelper.add_element(distribution, 'NumberofReturnRegisters', Integer(@number_of_return_registers)) unless @number_of_return_registers.nil?
        XMLHelper.add_extension(distribution, 'DuctLeakageToOutsideTestingExemption', to_boolean(@duct_leakage_to_outside_testing_exemption)) unless @duct_leakage_to_outside_testing_exemption.nil?
      end
    end

    def from_oga(hvac_distribution)
      return if hvac_distribution.nil?

      @id = HPXML::get_id(hvac_distribution)
      @distribution_system_type = XMLHelper.get_child_name(hvac_distribution, 'DistributionSystemType')
      if @distribution_system_type == 'Other'
        @distribution_system_type = XMLHelper.get_value(XMLHelper.get_element(hvac_distribution, 'DistributionSystemType'), 'Other')
      end
      @annual_heating_dse = to_float_or_nil(XMLHelper.get_value(hvac_distribution, 'AnnualHeatingDistributionSystemEfficiency'))
      @annual_cooling_dse = to_float_or_nil(XMLHelper.get_value(hvac_distribution, 'AnnualCoolingDistributionSystemEfficiency'))
      @duct_system_sealed = to_boolean_or_nil(XMLHelper.get_value(hvac_distribution, 'HVACDistributionImprovement/DuctSystemSealed'))
      @conditioned_floor_area_served = to_float_or_nil(XMLHelper.get_value(hvac_distribution, 'ConditionedFloorAreaServed'))

      air_distribution = XMLHelper.get_element(hvac_distribution, 'DistributionSystemType/AirDistribution')
      hydronic_distribution = XMLHelper.get_element(hvac_distribution, 'DistributionSystemType/HydronicDistribution')
      hydronic_and_air_distribution = XMLHelper.get_element(hvac_distribution, 'DistributionSystemType/HydronicAndAirDistribution')

      if not hydronic_distribution.nil?
        @hydronic_type = XMLHelper.get_value(hydronic_distribution, 'HydronicDistributionType')
      end
      if not hydronic_and_air_distribution.nil?
        @hydronic_and_air_type = XMLHelper.get_value(hydronic_and_air_distribution, 'HydronicAndAirDistributionType')
      end
      if (not air_distribution.nil?) || (not hydronic_and_air_distribution.nil?)
        distribution = air_distribution
        distribution = hydronic_and_air_distribution if distribution.nil?
        @number_of_return_registers = to_integer_or_nil(XMLHelper.get_value(distribution, 'NumberofReturnRegisters'))
        @duct_leakage_to_outside_testing_exemption = to_boolean_or_nil(XMLHelper.get_value(distribution, 'extension/DuctLeakageToOutsideTestingExemption'))
        @duct_leakage_measurements.from_oga(distribution)
        @ducts.from_oga(distribution)
      end
    end
  end

  class DuctLeakageMeasurements < BaseArrayElement
    def add(**kwargs)
      self << DuctLeakageMeasurement.new(@hpxml_object, **kwargs)
    end

    def from_oga(hvac_distribution)
      return if hvac_distribution.nil?

      XMLHelper.get_elements(hvac_distribution, 'DuctLeakageMeasurement').each do |duct_leakage_measurement|
        self << DuctLeakageMeasurement.new(@hpxml_object, duct_leakage_measurement)
      end
    end
  end

  class DuctLeakageMeasurement < BaseElement
    ATTRS = [:duct_type, :duct_leakage_test_method, :duct_leakage_units, :duct_leakage_value,
             :duct_leakage_total_or_to_outside]
    attr_accessor(*ATTRS)

    def delete
      @hpxml_object.hvac_distributions.each do |hvac_distribution|
        next unless hvac_distribution.duct_leakage_measurements.include? self

        hvac_distribution.duct_leakage_measurements.delete(self)
      end
    end

    def check_for_errors
      errors = []
      return errors
    end

    def to_oga(air_distribution)
      duct_leakage_measurement_el = XMLHelper.add_element(air_distribution, 'DuctLeakageMeasurement')
      XMLHelper.add_element(duct_leakage_measurement_el, 'DuctType', @duct_type) unless @duct_type.nil?
      if not @duct_leakage_value.nil?
        duct_leakage_el = XMLHelper.add_element(duct_leakage_measurement_el, 'DuctLeakage')
        XMLHelper.add_element(duct_leakage_el, 'Units', @duct_leakage_units) unless @duct_leakage_units.nil?
        XMLHelper.add_element(duct_leakage_el, 'Value', to_float(@duct_leakage_value))
        XMLHelper.add_element(duct_leakage_el, 'TotalOrToOutside', @duct_leakage_total_or_to_outside) unless @duct_leakage_total_or_to_outside.nil?
      end
    end

    def from_oga(duct_leakage_measurement)
      return if duct_leakage_measurement.nil?

      @duct_type = XMLHelper.get_value(duct_leakage_measurement, 'DuctType')
      @duct_leakage_test_method = XMLHelper.get_value(duct_leakage_measurement, 'DuctLeakageTestMethod')
      @duct_leakage_units = XMLHelper.get_value(duct_leakage_measurement, 'DuctLeakage/Units')
      @duct_leakage_value = to_float_or_nil(XMLHelper.get_value(duct_leakage_measurement, 'DuctLeakage/Value'))
      @duct_leakage_total_or_to_outside = XMLHelper.get_value(duct_leakage_measurement, 'DuctLeakage/TotalOrToOutside')
    end
  end

  class Ducts < BaseArrayElement
    def add(**kwargs)
      self << Duct.new(@hpxml_object, **kwargs)
    end

    def from_oga(hvac_distribution)
      return if hvac_distribution.nil?

      XMLHelper.get_elements(hvac_distribution, 'Ducts').each do |duct|
        self << Duct.new(@hpxml_object, duct)
      end
    end
  end

  class Duct < BaseElement
    ATTRS = [:duct_type, :duct_insulation_r_value, :duct_insulation_material, :duct_location,
             :duct_fraction_area, :duct_surface_area]
    attr_accessor(*ATTRS)

    def delete
      @hpxml_object.hvac_distributions.each do |hvac_distribution|
        next unless hvac_distribution.ducts.include? self

        hvac_distribution.ducts.delete(self)
      end
    end

    def check_for_errors
      errors = []
      return errors
    end

    def to_oga(air_distribution)
      ducts_el = XMLHelper.add_element(air_distribution, 'Ducts')
      XMLHelper.add_element(ducts_el, 'DuctType', @duct_type) unless @duct_type.nil?
      XMLHelper.add_element(ducts_el, 'DuctInsulationRValue', to_float(@duct_insulation_r_value)) unless @duct_insulation_r_value.nil?
      XMLHelper.add_element(ducts_el, 'DuctLocation', @duct_location) unless @duct_location.nil?
      XMLHelper.add_element(ducts_el, 'DuctSurfaceArea', to_float(@duct_surface_area)) unless @duct_surface_area.nil?
    end

    def from_oga(duct)
      return if duct.nil?

      @duct_type = XMLHelper.get_value(duct, 'DuctType')
      @duct_insulation_r_value = to_float_or_nil(XMLHelper.get_value(duct, 'DuctInsulationRValue'))
      @duct_insulation_material = XMLHelper.get_child_name(duct, 'DuctInsulationMaterial')
      @duct_location = XMLHelper.get_value(duct, 'DuctLocation')
      @duct_fraction_area = to_float_or_nil(XMLHelper.get_value(duct, 'FractionDuctArea'))
      @duct_surface_area = to_float_or_nil(XMLHelper.get_value(duct, 'DuctSurfaceArea'))
    end
  end

  class VentilationFans < BaseArrayElement
    def add(**kwargs)
      self << VentilationFan.new(@hpxml_object, **kwargs)
    end

    def from_oga(hpxml)
      return if hpxml.nil?

      XMLHelper.get_elements(hpxml, 'Building/BuildingDetails/Systems/MechanicalVentilation/VentilationFans/VentilationFan').each do |ventilation_fan|
        self << VentilationFan.new(@hpxml_object, ventilation_fan)
      end
    end
  end

  class VentilationFan < BaseElement
    ATTRS = [:id, :fan_type, :rated_flow_rate, :tested_flow_rate, :hours_in_operation, :flow_rate_not_tested,
             :used_for_whole_building_ventilation, :used_for_seasonal_cooling_load_reduction,
             :used_for_local_ventilation, :total_recovery_efficiency, :total_recovery_efficiency_adjusted,
             :sensible_recovery_efficiency, :sensible_recovery_efficiency_adjusted,
             :fan_power, :fan_power_defaulted, :quantity, :fan_location, :distribution_system_idref, :start_hour,
             :is_shared_system, :in_unit_flow_rate, :fraction_recirculation,
             :preheating_fuel, :preheating_efficiency_cop, :preheating_fraction_load_served, :precooling_fuel,
             :precooling_efficiency_cop, :precooling_fraction_load_served,]
    attr_accessor(*ATTRS)

    def distribution_system
      return if @distribution_system_idref.nil?
      return unless @fan_type == MechVentTypeCFIS

      @hpxml_object.hvac_distributions.each do |hvac_distribution|
        next unless hvac_distribution.id == @distribution_system_idref

        if hvac_distribution.distribution_system_type == HVACDistributionTypeHydronic
          fail "Attached HVAC distribution system '#{@distribution_system_idref}' cannot be hydronic for ventilation fan '#{@id}'."
        end

        return hvac_distribution
      end
      fail "Attached HVAC distribution system '#{@distribution_system_idref}' not found for ventilation fan '#{@id}'."
    end

    def total_unit_flow_rate
      if not @is_shared_system
        if not @tested_flow_rate.nil?
          return @tested_flow_rate
        else
          return @rated_flow_rate
        end
      else
        return @in_unit_flow_rate
      end
    end

    def oa_unit_flow_rate
      return if total_unit_flow_rate.nil?
      if not @is_shared_system
        return total_unit_flow_rate
      else
        if @fan_type == HPXML::MechVentTypeExhaust && @fraction_recirculation > 0.0
          fail "Exhaust fan '#{@id}' must have the fraction recirculation set to zero."
        else
          return total_unit_flow_rate * (1 - @fraction_recirculation)
        end
      end
    end

    def average_oa_unit_flow_rate
      # Daily-average outdoor air (cfm) associated with the unit
      return if oa_unit_flow_rate.nil?
      return if @hours_in_operation.nil?
      return oa_unit_flow_rate * (@hours_in_operation / 24.0)
    end

    def average_total_unit_flow_rate
      # Daily-average total air (cfm) associated with the unit
      return if total_unit_flow_rate.nil?
      return if @hours_in_operation.nil?
      return total_unit_flow_rate * (@hours_in_operation / 24.0)
    end

    def unit_flow_rate_ratio
      return 1.0 unless @is_shared_system
      return if @in_unit_flow_rate.nil?
      if not @tested_flow_rate.nil?
        ratio = @in_unit_flow_rate / @tested_flow_rate
      elsif not @rated_flow_rate.nil?
        ratio = @in_unit_flow_rate / @rated_flow_rate
      end
      return if ratio.nil?
      if ratio >= 1.0
        fail "The in-unit flow rate of shared fan '#{@id}' must be less than the system flow rate."
      end
      return ratio
    end

    def unit_fan_power
      return if @fan_power.nil?
      if @is_shared_system
        return if unit_flow_rate_ratio.nil?
        return @fan_power * unit_flow_rate_ratio
      else
        return @fan_power
      end
    end

    def average_unit_fan_power
      return if unit_fan_power.nil?
      return if @hours_in_operation.nil?
      return unit_fan_power * (@hours_in_operation / 24.0)
    end

    def includes_supply_air?
      if [MechVentTypeSupply, MechVentTypeCFIS, MechVentTypeBalanced, MechVentTypeERV, MechVentTypeHRV].include? @fan_type
        return true
      end

      return false
    end

    def includes_exhaust_air?
      if [MechVentTypeExhaust, MechVentTypeBalanced, MechVentTypeERV, MechVentTypeHRV].include? @fan_type
        return true
      end

      return false
    end

    def is_balanced?
      if includes_supply_air? && includes_exhaust_air?
        return true
      end

      return false
    end

    def delete
      @hpxml_object.ventilation_fans.delete(self)
    end

    def check_for_errors
      errors = []
      begin; distribution_system; rescue StandardError => e; errors << e.message; end
      begin; oa_unit_flow_rate; rescue StandardError => e; errors << e.message; end
      begin; unit_flow_rate_ratio; rescue StandardError => e; errors << e.message; end
      return errors
    end

    def to_oga(doc)
      return if nil?

      ventilation_fans = XMLHelper.create_elements_as_needed(doc, ['HPXML', 'Building', 'BuildingDetails', 'Systems', 'MechanicalVentilation', 'VentilationFans'])
      ventilation_fan = XMLHelper.add_element(ventilation_fans, 'VentilationFan')
      sys_id = XMLHelper.add_element(ventilation_fan, 'SystemIdentifier')
      XMLHelper.add_attribute(sys_id, 'id', @id)
      XMLHelper.add_element(ventilation_fan, 'Quantity', to_integer(@quantity)) unless @quantity.nil?
      XMLHelper.add_element(ventilation_fan, 'FanType', @fan_type) unless @fan_type.nil?
      XMLHelper.add_element(ventilation_fan, 'RatedFlowRate', to_float(@rated_flow_rate)) unless @rated_flow_rate.nil?
      XMLHelper.add_element(ventilation_fan, 'TestedFlowRate', to_float(@tested_flow_rate)) unless @tested_flow_rate.nil?
      XMLHelper.add_element(ventilation_fan, 'HoursInOperation', to_float(@hours_in_operation)) unless @hours_in_operation.nil?
      XMLHelper.add_element(ventilation_fan, 'FanLocation', @fan_location) unless @fan_location.nil?
      XMLHelper.add_element(ventilation_fan, 'UsedForLocalVentilation', to_boolean(@used_for_local_ventilation)) unless @used_for_local_ventilation.nil?
      XMLHelper.add_element(ventilation_fan, 'UsedForWholeBuildingVentilation', to_boolean(@used_for_whole_building_ventilation)) unless @used_for_whole_building_ventilation.nil?
      XMLHelper.add_element(ventilation_fan, 'UsedForSeasonalCoolingLoadReduction', to_boolean(@used_for_seasonal_cooling_load_reduction)) unless @used_for_seasonal_cooling_load_reduction.nil?
      XMLHelper.add_element(ventilation_fan, 'IsSharedSystem', to_boolean(@is_shared_system)) unless @is_shared_system.nil?
      if @is_shared_system
        XMLHelper.add_element(ventilation_fan, 'FractionRecirculation', to_float(@fraction_recirculation)) unless @fraction_recirculation.nil?
      end
      XMLHelper.add_element(ventilation_fan, 'TotalRecoveryEfficiency', to_float(@total_recovery_efficiency)) unless @total_recovery_efficiency.nil?
      XMLHelper.add_element(ventilation_fan, 'SensibleRecoveryEfficiency', to_float(@sensible_recovery_efficiency)) unless @sensible_recovery_efficiency.nil?
      XMLHelper.add_element(ventilation_fan, 'AdjustedTotalRecoveryEfficiency', to_float(@total_recovery_efficiency_adjusted)) unless @total_recovery_efficiency_adjusted.nil?
      XMLHelper.add_element(ventilation_fan, 'AdjustedSensibleRecoveryEfficiency', to_float(@sensible_recovery_efficiency_adjusted)) unless @sensible_recovery_efficiency_adjusted.nil?
      XMLHelper.add_element(ventilation_fan, 'FanPower', to_float(@fan_power)) unless @fan_power.nil?
      if not @distribution_system_idref.nil?
        attached_to_hvac_distribution_system = XMLHelper.add_element(ventilation_fan, 'AttachedToHVACDistributionSystem')
        XMLHelper.add_attribute(attached_to_hvac_distribution_system, 'idref', @distribution_system_idref)
      end
      XMLHelper.add_extension(ventilation_fan, 'StartHour', to_integer(@start_hour)) unless @start_hour.nil?
      if @is_shared_system
        XMLHelper.add_extension(ventilation_fan, 'InUnitFlowRate', to_float(@in_unit_flow_rate)) unless @in_unit_flow_rate.nil?
        if (not @preheating_fuel.nil?) && (not @preheating_efficiency_cop.nil?)
          precond_htg = XMLHelper.create_elements_as_needed(ventilation_fan, ['extension', 'PreHeating'])
          XMLHelper.add_element(precond_htg, 'Fuel', @preheating_fuel) unless @preheating_fuel.nil?
          eff = XMLHelper.add_element(precond_htg, 'AnnualHeatingEfficiency') unless @preheating_efficiency_cop.nil?
          XMLHelper.add_element(eff, 'Value', to_float(@preheating_efficiency_cop)) unless eff.nil?
          XMLHelper.add_element(eff, 'Units', UnitsCOP) unless eff.nil?
          XMLHelper.add_element(precond_htg, 'FractionVentilationHeatLoadServed', to_float(@preheating_fraction_load_served)) unless @preheating_fraction_load_served.nil?
        end
        if (not @precooling_fuel.nil?) && (not @precooling_efficiency_cop.nil?)
          precond_clg = XMLHelper.create_elements_as_needed(ventilation_fan, ['extension', 'PreCooling'])
          XMLHelper.add_element(precond_clg, 'Fuel', @precooling_fuel) unless @precooling_fuel.nil?
          eff = XMLHelper.add_element(precond_clg, 'AnnualCoolingEfficiency') unless @precooling_efficiency_cop.nil?
          XMLHelper.add_element(eff, 'Value', to_float(@precooling_efficiency_cop)) unless eff.nil?
          XMLHelper.add_element(eff, 'Units', UnitsCOP) unless eff.nil?
          XMLHelper.add_element(precond_clg, 'FractionVentilationCoolLoadServed', to_float(@precooling_fraction_load_served)) unless @precooling_fraction_load_served.nil?
        end
      end
      XMLHelper.add_extension(ventilation_fan, 'FlowRateNotTested', @flow_rate_not_tested) unless @flow_rate_not_tested.nil?
      XMLHelper.add_extension(ventilation_fan, 'FanPowerDefaulted', @fan_power_defaulted) unless @fan_power_defaulted.nil?
    end

    def from_oga(ventilation_fan)
      return if ventilation_fan.nil?

      @id = HPXML::get_id(ventilation_fan)
      @quantity = to_integer_or_nil(XMLHelper.get_value(ventilation_fan, 'Quantity'))
      @fan_type = XMLHelper.get_value(ventilation_fan, 'FanType')
      @is_shared_system = to_boolean_or_nil(XMLHelper.get_value(ventilation_fan, 'IsSharedSystem'))
      @rated_flow_rate = to_float_or_nil(XMLHelper.get_value(ventilation_fan, 'RatedFlowRate'))
      @tested_flow_rate = to_float_or_nil(XMLHelper.get_value(ventilation_fan, 'TestedFlowRate'))
      @flow_rate_not_tested = to_boolean_or_nil(XMLHelper.get_value(ventilation_fan, 'extension/FlowRateNotTested'))
      @fan_power = to_float_or_nil(XMLHelper.get_value(ventilation_fan, 'FanPower'))
      @fan_power_defaulted = to_boolean_or_nil(XMLHelper.get_value(ventilation_fan, 'extension/FanPowerDefaulted'))
      if @is_shared_system
        @fraction_recirculation = to_float_or_nil(XMLHelper.get_value(ventilation_fan, 'FractionRecirculation'))
        @in_unit_flow_rate = to_float_or_nil(XMLHelper.get_value(ventilation_fan, 'extension/InUnitFlowRate'))
        @preheating_fuel = XMLHelper.get_value(ventilation_fan, 'extension/PreHeating/Fuel')
        @preheating_efficiency_cop = to_float_or_nil(XMLHelper.get_value(ventilation_fan, "extension/PreHeating/AnnualHeatingEfficiency[Units='#{UnitsCOP}']/Value"))
        @preheating_fraction_load_served = to_float_or_nil(XMLHelper.get_value(ventilation_fan, 'extension/PreHeating/FractionVentilationHeatLoadServed'))
        @precooling_fuel = XMLHelper.get_value(ventilation_fan, 'extension/PreCooling/Fuel')
        @precooling_efficiency_cop = to_float_or_nil(XMLHelper.get_value(ventilation_fan, "extension/PreCooling/AnnualCoolingEfficiency[Units='#{UnitsCOP}']/Value"))
        @precooling_fraction_load_served = to_float_or_nil(XMLHelper.get_value(ventilation_fan, 'extension/PreCooling/FractionVentilationCoolLoadServed'))
      end
      @hours_in_operation = to_float_or_nil(XMLHelper.get_value(ventilation_fan, 'HoursInOperation'))
      @fan_location = XMLHelper.get_value(ventilation_fan, 'FanLocation')
      @used_for_local_ventilation = to_boolean_or_nil(XMLHelper.get_value(ventilation_fan, 'UsedForLocalVentilation'))
      @used_for_whole_building_ventilation = to_boolean_or_nil(XMLHelper.get_value(ventilation_fan, 'UsedForWholeBuildingVentilation'))
      @used_for_seasonal_cooling_load_reduction = to_boolean_or_nil(XMLHelper.get_value(ventilation_fan, 'UsedForSeasonalCoolingLoadReduction'))
      @total_recovery_efficiency = to_float_or_nil(XMLHelper.get_value(ventilation_fan, 'TotalRecoveryEfficiency'))
      @total_recovery_efficiency_adjusted = to_float_or_nil(XMLHelper.get_value(ventilation_fan, 'AdjustedTotalRecoveryEfficiency'))
      @sensible_recovery_efficiency = to_float_or_nil(XMLHelper.get_value(ventilation_fan, 'SensibleRecoveryEfficiency'))
      @sensible_recovery_efficiency_adjusted = to_float_or_nil(XMLHelper.get_value(ventilation_fan, 'AdjustedSensibleRecoveryEfficiency'))
      @distribution_system_idref = HPXML::get_idref(XMLHelper.get_element(ventilation_fan, 'AttachedToHVACDistributionSystem'))
      @start_hour = to_integer_or_nil(XMLHelper.get_value(ventilation_fan, 'extension/StartHour'))
    end
  end

  class WaterHeatingSystems < BaseArrayElement
    def add(**kwargs)
      self << WaterHeatingSystem.new(@hpxml_object, **kwargs)
    end

    def from_oga(hpxml)
      return if hpxml.nil?

      XMLHelper.get_elements(hpxml, 'Building/BuildingDetails/Systems/WaterHeating/WaterHeatingSystem').each do |water_heating_system|
        self << WaterHeatingSystem.new(@hpxml_object, water_heating_system)
      end
    end
  end

  class WaterHeatingSystem < BaseElement
    ATTRS = [:id, :year_installed, :fuel_type, :water_heater_type, :location, :performance_adjustment,
             :tank_volume, :fraction_dhw_load_served, :heating_capacity, :energy_factor,
             :uniform_energy_factor, :recovery_efficiency, :uses_desuperheater, :jacket_r_value,
             :related_hvac_idref, :energy_star, :standby_loss, :temperature, :is_shared_system,
             :number_of_units_served]
    attr_accessor(*ATTRS)

    def related_hvac_system
      return if @related_hvac_idref.nil?

      (@hpxml_object.heating_systems + @hpxml_object.cooling_systems + @hpxml_object.heat_pumps).each do |hvac_system|
        next unless hvac_system.id == @related_hvac_idref

        return hvac_system
      end
      fail "RelatedHVACSystem '#{@related_hvac_idref}' not found for water heating system '#{@id}'."
    end

    def delete
      @hpxml_object.water_heating_systems.delete(self)
      @hpxml_object.solar_thermal_systems.each do |solar_thermal_system|
        next unless solar_thermal_system.water_heating_system_idref == @id

        solar_thermal_system.water_heating_system_idref = nil
      end
    end

    def check_for_errors
      errors = []
      begin; related_hvac_system; rescue StandardError => e; errors << e.message; end
      return errors
    end

    def to_oga(doc)
      return if nil?

      water_heating = XMLHelper.create_elements_as_needed(doc, ['HPXML', 'Building', 'BuildingDetails', 'Systems', 'WaterHeating'])
      water_heating_system = XMLHelper.add_element(water_heating, 'WaterHeatingSystem')
      sys_id = XMLHelper.add_element(water_heating_system, 'SystemIdentifier')
      XMLHelper.add_attribute(sys_id, 'id', @id)
      XMLHelper.add_element(water_heating_system, 'FuelType', @fuel_type) unless @fuel_type.nil?
      XMLHelper.add_element(water_heating_system, 'WaterHeaterType', @water_heater_type) unless @water_heater_type.nil?
      XMLHelper.add_element(water_heating_system, 'Location', @location) unless @location.nil?
      XMLHelper.add_element(water_heating_system, 'IsSharedSystem', to_boolean(@is_shared_system)) unless @is_shared_system.nil?
      XMLHelper.add_element(water_heating_system, 'NumberofUnitsServed', to_integer(@number_of_units_served)) unless @number_of_units_served.nil?
      XMLHelper.add_element(water_heating_system, 'PerformanceAdjustment', to_float(@performance_adjustment)) unless @performance_adjustment.nil?
      XMLHelper.add_element(water_heating_system, 'TankVolume', to_float(@tank_volume)) unless @tank_volume.nil?
      XMLHelper.add_element(water_heating_system, 'FractionDHWLoadServed', to_float(@fraction_dhw_load_served)) unless @fraction_dhw_load_served.nil?
      XMLHelper.add_element(water_heating_system, 'HeatingCapacity', to_float(@heating_capacity)) unless @heating_capacity.nil?
      XMLHelper.add_element(water_heating_system, 'EnergyFactor', to_float(@energy_factor)) unless @energy_factor.nil?
      XMLHelper.add_element(water_heating_system, 'UniformEnergyFactor', to_float(@uniform_energy_factor)) unless @uniform_energy_factor.nil?
      XMLHelper.add_element(water_heating_system, 'RecoveryEfficiency', to_float(@recovery_efficiency)) unless @recovery_efficiency.nil?
      if not @jacket_r_value.nil?
        water_heater_insulation = XMLHelper.add_element(water_heating_system, 'WaterHeaterInsulation')
        jacket = XMLHelper.add_element(water_heater_insulation, 'Jacket')
        XMLHelper.add_element(jacket, 'JacketRValue', @jacket_r_value)
      end
      XMLHelper.add_element(water_heating_system, 'StandbyLoss', to_float(@standby_loss)) unless @standby_loss.nil?
      XMLHelper.add_element(water_heating_system, 'HotWaterTemperature', to_float(@temperature)) unless @temperature.nil?
      XMLHelper.add_element(water_heating_system, 'UsesDesuperheater', to_boolean(@uses_desuperheater)) unless @uses_desuperheater.nil?
      if not @related_hvac_idref.nil?
        related_hvac_idref_el = XMLHelper.add_element(water_heating_system, 'RelatedHVACSystem')
        XMLHelper.add_attribute(related_hvac_idref_el, 'idref', @related_hvac_idref)
      end
    end

    def from_oga(water_heating_system)
      return if water_heating_system.nil?

      @id = HPXML::get_id(water_heating_system)
      @year_installed = to_integer_or_nil(XMLHelper.get_value(water_heating_system, 'YearInstalled'))
      @fuel_type = XMLHelper.get_value(water_heating_system, 'FuelType')
      @water_heater_type = XMLHelper.get_value(water_heating_system, 'WaterHeaterType')
      @location = XMLHelper.get_value(water_heating_system, 'Location')
      @is_shared_system = to_boolean_or_nil(XMLHelper.get_value(water_heating_system, 'IsSharedSystem'))
      @number_of_units_served = to_integer_or_nil(XMLHelper.get_value(water_heating_system, 'NumberofUnitsServed'))
      @performance_adjustment = to_float_or_nil(XMLHelper.get_value(water_heating_system, 'PerformanceAdjustment'))
      @tank_volume = to_float_or_nil(XMLHelper.get_value(water_heating_system, 'TankVolume'))
      @fraction_dhw_load_served = to_float_or_nil(XMLHelper.get_value(water_heating_system, 'FractionDHWLoadServed'))
      @heating_capacity = to_float_or_nil(XMLHelper.get_value(water_heating_system, 'HeatingCapacity'))
      @energy_factor = to_float_or_nil(XMLHelper.get_value(water_heating_system, 'EnergyFactor'))
      @uniform_energy_factor = to_float_or_nil(XMLHelper.get_value(water_heating_system, 'UniformEnergyFactor'))
      @recovery_efficiency = to_float_or_nil(XMLHelper.get_value(water_heating_system, 'RecoveryEfficiency'))
      @uses_desuperheater = to_boolean_or_nil(XMLHelper.get_value(water_heating_system, 'UsesDesuperheater'))
      @jacket_r_value = to_float_or_nil(XMLHelper.get_value(water_heating_system, 'WaterHeaterInsulation/Jacket/JacketRValue'))
      @related_hvac_idref = HPXML::get_idref(XMLHelper.get_element(water_heating_system, 'RelatedHVACSystem'))
      @energy_star = XMLHelper.get_values(water_heating_system, 'ThirdPartyCertification').include?('Energy Star')
      @standby_loss = to_float_or_nil(XMLHelper.get_value(water_heating_system, 'StandbyLoss'))
      @temperature = to_float_or_nil(XMLHelper.get_value(water_heating_system, 'HotWaterTemperature'))
    end
  end

  class HotWaterDistributions < BaseArrayElement
    def add(**kwargs)
      self << HotWaterDistribution.new(@hpxml_object, **kwargs)
    end

    def from_oga(hpxml)
      return if hpxml.nil?

      XMLHelper.get_elements(hpxml, 'Building/BuildingDetails/Systems/WaterHeating/HotWaterDistribution').each do |hot_water_distribution|
        self << HotWaterDistribution.new(@hpxml_object, hot_water_distribution)
      end
    end
  end

  class HotWaterDistribution < BaseElement
    ATTRS = [:id, :system_type, :pipe_r_value, :standard_piping_length, :recirculation_control_type,
             :recirculation_piping_length, :recirculation_branch_piping_length,
             :recirculation_pump_power, :dwhr_facilities_connected, :dwhr_equal_flow,
             :dwhr_efficiency, :has_shared_recirculation, :shared_recirculation_number_of_units_served,
             :shared_recirculation_pump_power, :shared_recirculation_control_type]
    attr_accessor(*ATTRS)

    def delete
      @hpxml_object.hot_water_distributions.delete(self)
    end

    def check_for_errors
      errors = []
      return errors
    end

    def to_oga(doc)
      return if nil?

      water_heating = XMLHelper.create_elements_as_needed(doc, ['HPXML', 'Building', 'BuildingDetails', 'Systems', 'WaterHeating'])
      hot_water_distribution = XMLHelper.add_element(water_heating, 'HotWaterDistribution')
      sys_id = XMLHelper.add_element(hot_water_distribution, 'SystemIdentifier')
      XMLHelper.add_attribute(sys_id, 'id', @id)
      if not @system_type.nil?
        system_type_e = XMLHelper.add_element(hot_water_distribution, 'SystemType')
        if @system_type == DHWDistTypeStandard
          standard = XMLHelper.add_element(system_type_e, @system_type)
          XMLHelper.add_element(standard, 'PipingLength', to_float(@standard_piping_length)) unless @standard_piping_length.nil?
        elsif system_type == DHWDistTypeRecirc
          recirculation = XMLHelper.add_element(system_type_e, @system_type)
          XMLHelper.add_element(recirculation, 'ControlType', @recirculation_control_type) unless @recirculation_control_type.nil?
          XMLHelper.add_element(recirculation, 'RecirculationPipingLoopLength', to_float(@recirculation_piping_length)) unless @recirculation_piping_length.nil?
          XMLHelper.add_element(recirculation, 'BranchPipingLoopLength', to_float(@recirculation_branch_piping_length)) unless @recirculation_branch_piping_length.nil?
          XMLHelper.add_element(recirculation, 'PumpPower', to_float(@recirculation_pump_power)) unless @recirculation_pump_power.nil?
        else
          fail "Unhandled hot water distribution type '#{@system_type}'."
        end
      end
      if not @pipe_r_value.nil?
        pipe_insulation = XMLHelper.add_element(hot_water_distribution, 'PipeInsulation')
        XMLHelper.add_element(pipe_insulation, 'PipeRValue', to_float(@pipe_r_value))
      end
      if (not @dwhr_facilities_connected.nil?) || (not @dwhr_equal_flow.nil?) || (not @dwhr_efficiency.nil?)
        drain_water_heat_recovery = XMLHelper.add_element(hot_water_distribution, 'DrainWaterHeatRecovery')
        XMLHelper.add_element(drain_water_heat_recovery, 'FacilitiesConnected', @dwhr_facilities_connected) unless @dwhr_facilities_connected.nil?
        XMLHelper.add_element(drain_water_heat_recovery, 'EqualFlow', to_boolean(@dwhr_equal_flow)) unless @dwhr_equal_flow.nil?
        XMLHelper.add_element(drain_water_heat_recovery, 'Efficiency', to_float(@dwhr_efficiency)) unless @dwhr_efficiency.nil?
      end
      if @has_shared_recirculation
        extension = XMLHelper.create_elements_as_needed(hot_water_distribution, ['extension'])
        shared_recirculation = XMLHelper.add_element(extension, 'SharedRecirculation')
        XMLHelper.add_element(shared_recirculation, 'NumberofUnitsServed', to_integer(@shared_recirculation_number_of_units_served)) unless @shared_recirculation_number_of_units_served.nil?
        XMLHelper.add_element(shared_recirculation, 'PumpPower', to_float(@shared_recirculation_pump_power)) unless @shared_recirculation_pump_power.nil?
        XMLHelper.add_element(shared_recirculation, 'ControlType', @shared_recirculation_control_type) unless @shared_recirculation_control_type.nil?
      end
    end

    def from_oga(hot_water_distribution)
      return if hot_water_distribution.nil?

      @id = HPXML::get_id(hot_water_distribution)
      @system_type = XMLHelper.get_child_name(hot_water_distribution, 'SystemType')
      @pipe_r_value = to_float_or_nil(XMLHelper.get_value(hot_water_distribution, 'PipeInsulation/PipeRValue'))
      if @system_type == 'Standard'
        @standard_piping_length = to_float_or_nil(XMLHelper.get_value(hot_water_distribution, 'SystemType/Standard/PipingLength'))
      elsif @system_type == 'Recirculation'
        @recirculation_control_type = XMLHelper.get_value(hot_water_distribution, 'SystemType/Recirculation/ControlType')
        @recirculation_piping_length = to_float_or_nil(XMLHelper.get_value(hot_water_distribution, 'SystemType/Recirculation/RecirculationPipingLoopLength'))
        @recirculation_branch_piping_length = to_float_or_nil(XMLHelper.get_value(hot_water_distribution, 'SystemType/Recirculation/BranchPipingLoopLength'))
        @recirculation_pump_power = to_float_or_nil(XMLHelper.get_value(hot_water_distribution, 'SystemType/Recirculation/PumpPower'))
      end
      @dwhr_facilities_connected = XMLHelper.get_value(hot_water_distribution, 'DrainWaterHeatRecovery/FacilitiesConnected')
      @dwhr_equal_flow = to_boolean_or_nil(XMLHelper.get_value(hot_water_distribution, 'DrainWaterHeatRecovery/EqualFlow'))
      @dwhr_efficiency = to_float_or_nil(XMLHelper.get_value(hot_water_distribution, 'DrainWaterHeatRecovery/Efficiency'))
      @has_shared_recirculation = XMLHelper.has_element(hot_water_distribution, 'extension/SharedRecirculation')
      if @has_shared_recirculation
        @shared_recirculation_number_of_units_served = to_integer_or_nil(XMLHelper.get_value(hot_water_distribution, 'extension/SharedRecirculation/NumberofUnitsServed'))
        @shared_recirculation_pump_power = to_float_or_nil(XMLHelper.get_value(hot_water_distribution, 'extension/SharedRecirculation/PumpPower'))
        @shared_recirculation_control_type = XMLHelper.get_value(hot_water_distribution, 'extension/SharedRecirculation/ControlType')
      end
    end
  end

  class WaterFixtures < BaseArrayElement
    def add(**kwargs)
      self << WaterFixture.new(@hpxml_object, **kwargs)
    end

    def from_oga(hpxml)
      return if hpxml.nil?

      XMLHelper.get_elements(hpxml, 'Building/BuildingDetails/Systems/WaterHeating/WaterFixture').each do |water_fixture|
        self << WaterFixture.new(@hpxml_object, water_fixture)
      end
    end
  end

  class WaterFixture < BaseElement
    ATTRS = [:id, :water_fixture_type, :low_flow]
    attr_accessor(*ATTRS)

    def delete
      @hpxml_object.water_fixtures.delete(self)
    end

    def check_for_errors
      errors = []
      return errors
    end

    def to_oga(doc)
      return if nil?

      water_heating = XMLHelper.create_elements_as_needed(doc, ['HPXML', 'Building', 'BuildingDetails', 'Systems', 'WaterHeating'])
      water_fixture = XMLHelper.add_element(water_heating, 'WaterFixture')
      sys_id = XMLHelper.add_element(water_fixture, 'SystemIdentifier')
      XMLHelper.add_attribute(sys_id, 'id', @id)
      XMLHelper.add_element(water_fixture, 'WaterFixtureType', @water_fixture_type) unless @water_fixture_type.nil?
      XMLHelper.add_element(water_fixture, 'LowFlow', to_boolean(@low_flow)) unless @low_flow.nil?
    end

    def from_oga(water_fixture)
      return if water_fixture.nil?

      @id = HPXML::get_id(water_fixture)
      @water_fixture_type = XMLHelper.get_value(water_fixture, 'WaterFixtureType')
      @low_flow = to_boolean_or_nil(XMLHelper.get_value(water_fixture, 'LowFlow'))
    end
  end

  class WaterHeating < BaseElement
    ATTRS = [:water_fixtures_usage_multiplier]
    attr_accessor(*ATTRS)

    def check_for_errors
      errors = []
      return errors
    end

    def to_oga(doc)
      return if nil?

      water_heating = XMLHelper.create_elements_as_needed(doc, ['HPXML', 'Building', 'BuildingDetails', 'Systems', 'WaterHeating'])
      XMLHelper.add_extension(water_heating, 'WaterFixturesUsageMultiplier', to_float(@water_fixtures_usage_multiplier)) unless @water_fixtures_usage_multiplier.nil?
    end

    def from_oga(hpxml)
      return if hpxml.nil?

      water_heating = XMLHelper.get_element(hpxml, 'Building/BuildingDetails/Systems/WaterHeating')
      return if water_heating.nil?

      @water_fixtures_usage_multiplier = to_float_or_nil(XMLHelper.get_value(water_heating, 'extension/WaterFixturesUsageMultiplier'))
    end
  end

  class SolarThermalSystems < BaseArrayElement
    def add(**kwargs)
      self << SolarThermalSystem.new(@hpxml_object, **kwargs)
    end

    def from_oga(hpxml)
      return if hpxml.nil?

      XMLHelper.get_elements(hpxml, 'Building/BuildingDetails/Systems/SolarThermal/SolarThermalSystem').each do |solar_thermal_system|
        self << SolarThermalSystem.new(@hpxml_object, solar_thermal_system)
      end
    end
  end

  class SolarThermalSystem < BaseElement
    ATTRS = [:id, :system_type, :collector_area, :collector_loop_type, :collector_azimuth,
             :collector_type, :collector_tilt, :collector_frta, :collector_frul, :storage_volume,
             :water_heating_system_idref, :solar_fraction]
    attr_accessor(*ATTRS)

    def water_heating_system
      return if @water_heating_system_idref.nil?

      @hpxml_object.water_heating_systems.each do |water_heater|
        next unless water_heater.id == @water_heating_system_idref

        return water_heater
      end
      fail "Attached water heating system '#{@water_heating_system_idref}' not found for solar thermal system '#{@id}'."
    end

    def delete
      @hpxml_object.solar_thermal_systems.delete(self)
    end

    def check_for_errors
      errors = []
      begin; water_heating_system; rescue StandardError => e; errors << e.message; end
      return errors
    end

    def to_oga(doc)
      return if nil?

      solar_thermal = XMLHelper.create_elements_as_needed(doc, ['HPXML', 'Building', 'BuildingDetails', 'Systems', 'SolarThermal'])
      solar_thermal_system = XMLHelper.add_element(solar_thermal, 'SolarThermalSystem')
      sys_id = XMLHelper.add_element(solar_thermal_system, 'SystemIdentifier')
      XMLHelper.add_attribute(sys_id, 'id', @id)
      XMLHelper.add_element(solar_thermal_system, 'SystemType', @system_type) unless @system_type.nil?
      XMLHelper.add_element(solar_thermal_system, 'CollectorArea', to_float(@collector_area)) unless @collector_area.nil?
      XMLHelper.add_element(solar_thermal_system, 'CollectorLoopType', @collector_loop_type) unless @collector_loop_type.nil?
      XMLHelper.add_element(solar_thermal_system, 'CollectorType', @collector_type) unless @collector_type.nil?
      XMLHelper.add_element(solar_thermal_system, 'CollectorAzimuth', to_integer(@collector_azimuth)) unless @collector_azimuth.nil?
      XMLHelper.add_element(solar_thermal_system, 'CollectorTilt', to_float(@collector_tilt)) unless @collector_tilt.nil?
      XMLHelper.add_element(solar_thermal_system, 'CollectorRatedOpticalEfficiency', to_float(@collector_frta)) unless @collector_frta.nil?
      XMLHelper.add_element(solar_thermal_system, 'CollectorRatedThermalLosses', to_float(@collector_frul)) unless @collector_frul.nil?
      XMLHelper.add_element(solar_thermal_system, 'StorageVolume', to_float(@storage_volume)) unless @storage_volume.nil?
      if not @water_heating_system_idref.nil?
        connected_to = XMLHelper.add_element(solar_thermal_system, 'ConnectedTo')
        XMLHelper.add_attribute(connected_to, 'idref', @water_heating_system_idref)
      end
      XMLHelper.add_element(solar_thermal_system, 'SolarFraction', to_float(@solar_fraction)) unless @solar_fraction.nil?
    end

    def from_oga(solar_thermal_system)
      return if solar_thermal_system.nil?

      @id = HPXML::get_id(solar_thermal_system)
      @system_type = XMLHelper.get_value(solar_thermal_system, 'SystemType')
      @collector_area = to_float_or_nil(XMLHelper.get_value(solar_thermal_system, 'CollectorArea'))
      @collector_loop_type = XMLHelper.get_value(solar_thermal_system, 'CollectorLoopType')
      @collector_azimuth = to_integer_or_nil(XMLHelper.get_value(solar_thermal_system, 'CollectorAzimuth'))
      @collector_type = XMLHelper.get_value(solar_thermal_system, 'CollectorType')
      @collector_tilt = to_float_or_nil(XMLHelper.get_value(solar_thermal_system, 'CollectorTilt'))
      @collector_frta = to_float_or_nil(XMLHelper.get_value(solar_thermal_system, 'CollectorRatedOpticalEfficiency'))
      @collector_frul = to_float_or_nil(XMLHelper.get_value(solar_thermal_system, 'CollectorRatedThermalLosses'))
      @storage_volume = to_float_or_nil(XMLHelper.get_value(solar_thermal_system, 'StorageVolume'))
      @water_heating_system_idref = HPXML::get_idref(XMLHelper.get_element(solar_thermal_system, 'ConnectedTo'))
      @solar_fraction = to_float_or_nil(XMLHelper.get_value(solar_thermal_system, 'SolarFraction'))
    end
  end

  class PVSystems < BaseArrayElement
    def add(**kwargs)
      self << PVSystem.new(@hpxml_object, **kwargs)
    end

    def from_oga(hpxml)
      return if hpxml.nil?

      XMLHelper.get_elements(hpxml, 'Building/BuildingDetails/Systems/Photovoltaics/PVSystem').each do |pv_system|
        self << PVSystem.new(@hpxml_object, pv_system)
      end
    end
  end

  class PVSystem < BaseElement
    ATTRS = [:id, :location, :module_type, :tracking, :array_orientation, :array_azimuth, :array_tilt,
             :max_power_output, :inverter_efficiency, :system_losses_fraction, :number_of_panels,
             :year_modules_manufactured, :is_shared_system, :number_of_bedrooms_served]
    attr_accessor(*ATTRS)

    def delete
      @hpxml_object.pv_systems.delete(self)
    end

    def check_for_errors
      errors = []
      return errors
    end

    def to_oga(doc)
      return if nil?

      photovoltaics = XMLHelper.create_elements_as_needed(doc, ['HPXML', 'Building', 'BuildingDetails', 'Systems', 'Photovoltaics'])
      pv_system = XMLHelper.add_element(photovoltaics, 'PVSystem')
      sys_id = XMLHelper.add_element(pv_system, 'SystemIdentifier')
      XMLHelper.add_attribute(sys_id, 'id', @id)
      XMLHelper.add_element(pv_system, 'IsSharedSystem', to_boolean(@is_shared_system)) unless @is_shared_system.nil?
      XMLHelper.add_element(pv_system, 'Location', @location) unless @location.nil?
      XMLHelper.add_element(pv_system, 'ModuleType', @module_type) unless @module_type.nil?
      XMLHelper.add_element(pv_system, 'Tracking', @tracking) unless @tracking.nil?
      XMLHelper.add_element(pv_system, 'ArrayAzimuth', to_integer(@array_azimuth)) unless @array_azimuth.nil?
      XMLHelper.add_element(pv_system, 'ArrayTilt', to_float(@array_tilt)) unless @array_tilt.nil?
      XMLHelper.add_element(pv_system, 'MaxPowerOutput', to_float(@max_power_output)) unless @max_power_output.nil?
      XMLHelper.add_element(pv_system, 'InverterEfficiency', to_float(@inverter_efficiency)) unless @inverter_efficiency.nil?
      XMLHelper.add_element(pv_system, 'SystemLossesFraction', to_float(@system_losses_fraction)) unless @system_losses_fraction.nil?
      XMLHelper.add_element(pv_system, 'YearModulesManufactured', to_integer(@year_modules_manufactured)) unless @year_modules_manufactured.nil?
      XMLHelper.add_extension(pv_system, 'NumberofBedroomsServed', to_integer(@number_of_bedrooms_served)) unless @number_of_bedrooms_served.nil?
    end

    def from_oga(pv_system)
      return if pv_system.nil?

      @id = HPXML::get_id(pv_system)
      @is_shared_system = to_boolean_or_nil(XMLHelper.get_value(pv_system, 'IsSharedSystem'))
      @location = XMLHelper.get_value(pv_system, 'Location')
      @module_type = XMLHelper.get_value(pv_system, 'ModuleType')
      @tracking = XMLHelper.get_value(pv_system, 'Tracking')
      @array_orientation = XMLHelper.get_value(pv_system, 'ArrayOrientation')
      @array_azimuth = to_integer_or_nil(XMLHelper.get_value(pv_system, 'ArrayAzimuth'))
      @array_tilt = to_float_or_nil(XMLHelper.get_value(pv_system, 'ArrayTilt'))
      @max_power_output = to_float_or_nil(XMLHelper.get_value(pv_system, 'MaxPowerOutput'))
      @inverter_efficiency = to_float_or_nil(XMLHelper.get_value(pv_system, 'InverterEfficiency'))
      @system_losses_fraction = to_float_or_nil(XMLHelper.get_value(pv_system, 'SystemLossesFraction'))
      @number_of_panels = to_integer_or_nil(XMLHelper.get_value(pv_system, 'NumberOfPanels'))
      @year_modules_manufactured = to_integer_or_nil(XMLHelper.get_value(pv_system, 'YearModulesManufactured'))
      @number_of_bedrooms_served = to_integer_or_nil(XMLHelper.get_value(pv_system, 'extension/NumberofBedroomsServed'))
    end
  end

  class ClothesWashers < BaseArrayElement
    def add(**kwargs)
      self << ClothesWasher.new(@hpxml_object, **kwargs)
    end

    def from_oga(hpxml)
      return if hpxml.nil?

      XMLHelper.get_elements(hpxml, 'Building/BuildingDetails/Appliances/ClothesWasher').each do |clothes_washer|
        self << ClothesWasher.new(@hpxml_object, clothes_washer)
      end
    end
  end

  class ClothesWasher < BaseElement
    ATTRS = [:id, :location, :modified_energy_factor, :integrated_modified_energy_factor,
             :rated_annual_kwh, :label_electric_rate, :label_gas_rate, :label_annual_gas_cost,
             :capacity, :label_usage, :usage_multiplier, :is_shared_appliance,
             :number_of_units, :number_of_units_served, :water_heating_system_idref]
    attr_accessor(*ATTRS)

    def water_heating_system
      return if @water_heating_system_idref.nil?

      @hpxml_object.water_heating_systems.each do |water_heater|
        next unless water_heater.id == @water_heating_system_idref

        return water_heater
      end
      fail "Attached water heating system '#{@water_heating_system_idref}' not found for clothes washer '#{@id}'."
    end

    def delete
      @hpxml_object.clothes_washers.delete(self)
    end

    def check_for_errors
      errors = []
      begin; water_heating_system; rescue StandardError => e; errors << e.message; end
      return errors
    end

    def to_oga(doc)
      return if nil?

      appliances = XMLHelper.create_elements_as_needed(doc, ['HPXML', 'Building', 'BuildingDetails', 'Appliances'])
      clothes_washer = XMLHelper.add_element(appliances, 'ClothesWasher')
      sys_id = XMLHelper.add_element(clothes_washer, 'SystemIdentifier')
      XMLHelper.add_attribute(sys_id, 'id', @id)
      XMLHelper.add_element(clothes_washer, 'NumberofUnits', to_integer(@number_of_units)) unless @number_of_units.nil?
      XMLHelper.add_element(clothes_washer, 'IsSharedAppliance', to_boolean(@is_shared_appliance)) unless @is_shared_appliance.nil?
      XMLHelper.add_element(clothes_washer, 'NumberofUnitsServed', to_integer(@number_of_units_served)) unless @number_of_units_served.nil?
      if not @water_heating_system_idref.nil?
        attached_water_heater = XMLHelper.add_element(clothes_washer, 'AttachedToWaterHeatingSystem')
        XMLHelper.add_attribute(attached_water_heater, 'idref', @water_heating_system_idref)
      end
      XMLHelper.add_element(clothes_washer, 'Location', @location) unless @location.nil?
      XMLHelper.add_element(clothes_washer, 'ModifiedEnergyFactor', to_float(@modified_energy_factor)) unless @modified_energy_factor.nil?
      XMLHelper.add_element(clothes_washer, 'IntegratedModifiedEnergyFactor', to_float(@integrated_modified_energy_factor)) unless @integrated_modified_energy_factor.nil?
      XMLHelper.add_element(clothes_washer, 'RatedAnnualkWh', to_float(@rated_annual_kwh)) unless @rated_annual_kwh.nil?
      XMLHelper.add_element(clothes_washer, 'LabelElectricRate', to_float(@label_electric_rate)) unless @label_electric_rate.nil?
      XMLHelper.add_element(clothes_washer, 'LabelGasRate', to_float(@label_gas_rate)) unless @label_gas_rate.nil?
      XMLHelper.add_element(clothes_washer, 'LabelAnnualGasCost', to_float(@label_annual_gas_cost)) unless @label_annual_gas_cost.nil?
      XMLHelper.add_element(clothes_washer, 'LabelUsage', to_float(@label_usage)) unless @label_usage.nil?
      XMLHelper.add_element(clothes_washer, 'Capacity', to_float(@capacity)) unless @capacity.nil?
      XMLHelper.add_extension(clothes_washer, 'UsageMultiplier', to_float(@usage_multiplier)) unless @usage_multiplier.nil?
    end

    def from_oga(clothes_washer)
      return if clothes_washer.nil?

      @id = HPXML::get_id(clothes_washer)
      @number_of_units = to_integer_or_nil(XMLHelper.get_value(clothes_washer, 'NumberofUnits'))
      @is_shared_appliance = to_boolean_or_nil(XMLHelper.get_value(clothes_washer, 'IsSharedAppliance'))
      @number_of_units_served = to_integer_or_nil(XMLHelper.get_value(clothes_washer, 'NumberofUnitsServed'))
      @location = XMLHelper.get_value(clothes_washer, 'Location')
      @modified_energy_factor = to_float_or_nil(XMLHelper.get_value(clothes_washer, 'ModifiedEnergyFactor'))
      @integrated_modified_energy_factor = to_float_or_nil(XMLHelper.get_value(clothes_washer, 'IntegratedModifiedEnergyFactor'))
      @rated_annual_kwh = to_float_or_nil(XMLHelper.get_value(clothes_washer, 'RatedAnnualkWh'))
      @label_electric_rate = to_float_or_nil(XMLHelper.get_value(clothes_washer, 'LabelElectricRate'))
      @label_gas_rate = to_float_or_nil(XMLHelper.get_value(clothes_washer, 'LabelGasRate'))
      @label_annual_gas_cost = to_float_or_nil(XMLHelper.get_value(clothes_washer, 'LabelAnnualGasCost'))
      @label_usage = to_float_or_nil(XMLHelper.get_value(clothes_washer, 'LabelUsage'))
      @capacity = to_float_or_nil(XMLHelper.get_value(clothes_washer, 'Capacity'))
      @usage_multiplier = to_float_or_nil(XMLHelper.get_value(clothes_washer, 'extension/UsageMultiplier'))
      @water_heating_system_idref = HPXML::get_idref(XMLHelper.get_element(clothes_washer, 'AttachedToWaterHeatingSystem'))
    end
  end

  class ClothesDryers < BaseArrayElement
    def add(**kwargs)
      self << ClothesDryer.new(@hpxml_object, **kwargs)
    end

    def from_oga(hpxml)
      return if hpxml.nil?

      XMLHelper.get_elements(hpxml, 'Building/BuildingDetails/Appliances/ClothesDryer').each do |clothes_dryer|
        self << ClothesDryer.new(@hpxml_object, clothes_dryer)
      end
    end
  end

  class ClothesDryer < BaseElement
    ATTRS = [:id, :location, :fuel_type, :energy_factor, :combined_energy_factor, :control_type,
             :usage_multiplier, :is_shared_appliance, :number_of_units, :number_of_units_served]
    attr_accessor(*ATTRS)

    def delete
      @hpxml_object.clothes_dryers.delete(self)
    end

    def check_for_errors
      errors = []
      return errors
    end

    def to_oga(doc)
      return if nil?

      appliances = XMLHelper.create_elements_as_needed(doc, ['HPXML', 'Building', 'BuildingDetails', 'Appliances'])
      clothes_dryer = XMLHelper.add_element(appliances, 'ClothesDryer')
      sys_id = XMLHelper.add_element(clothes_dryer, 'SystemIdentifier')
      XMLHelper.add_attribute(sys_id, 'id', @id)
      XMLHelper.add_element(clothes_dryer, 'NumberofUnits', to_integer(@number_of_units)) unless @number_of_units.nil?
      XMLHelper.add_element(clothes_dryer, 'IsSharedAppliance', to_boolean(@is_shared_appliance)) unless @is_shared_appliance.nil?
      XMLHelper.add_element(clothes_dryer, 'NumberofUnitsServed', to_integer(@number_of_units_served)) unless @number_of_units_served.nil?
      XMLHelper.add_element(clothes_dryer, 'Location', @location) unless @location.nil?
      XMLHelper.add_element(clothes_dryer, 'FuelType', @fuel_type) unless @fuel_type.nil?
      XMLHelper.add_element(clothes_dryer, 'EnergyFactor', to_float(@energy_factor)) unless @energy_factor.nil?
      XMLHelper.add_element(clothes_dryer, 'CombinedEnergyFactor', to_float(@combined_energy_factor)) unless @combined_energy_factor.nil?
      XMLHelper.add_element(clothes_dryer, 'ControlType', @control_type) unless @control_type.nil?
      XMLHelper.add_extension(clothes_dryer, 'UsageMultiplier', to_float(@usage_multiplier)) unless @usage_multiplier.nil?
    end

    def from_oga(clothes_dryer)
      return if clothes_dryer.nil?

      @id = HPXML::get_id(clothes_dryer)
      @number_of_units = to_integer_or_nil(XMLHelper.get_value(clothes_dryer, 'NumberofUnits'))
      @is_shared_appliance = to_boolean_or_nil(XMLHelper.get_value(clothes_dryer, 'IsSharedAppliance'))
      @number_of_units_served = to_integer_or_nil(XMLHelper.get_value(clothes_dryer, 'NumberofUnitsServed'))
      @location = XMLHelper.get_value(clothes_dryer, 'Location')
      @fuel_type = XMLHelper.get_value(clothes_dryer, 'FuelType')
      @energy_factor = to_float_or_nil(XMLHelper.get_value(clothes_dryer, 'EnergyFactor'))
      @combined_energy_factor = to_float_or_nil(XMLHelper.get_value(clothes_dryer, 'CombinedEnergyFactor'))
      @control_type = XMLHelper.get_value(clothes_dryer, 'ControlType')
      @usage_multiplier = to_float_or_nil(XMLHelper.get_value(clothes_dryer, 'extension/UsageMultiplier'))
    end
  end

  class Dishwashers < BaseArrayElement
    def add(**kwargs)
      self << Dishwasher.new(@hpxml_object, **kwargs)
    end

    def from_oga(hpxml)
      return if hpxml.nil?

      XMLHelper.get_elements(hpxml, 'Building/BuildingDetails/Appliances/Dishwasher').each do |dishwasher|
        self << Dishwasher.new(@hpxml_object, dishwasher)
      end
    end
  end

  class Dishwasher < BaseElement
    ATTRS = [:id, :location, :energy_factor, :rated_annual_kwh, :place_setting_capacity,
             :label_electric_rate, :label_gas_rate, :label_annual_gas_cost,
             :label_usage, :usage_multiplier, :is_shared_appliance, :water_heating_system_idref]
    attr_accessor(*ATTRS)

    def water_heating_system
      return if @water_heating_system_idref.nil?

      @hpxml_object.water_heating_systems.each do |water_heater|
        next unless water_heater.id == @water_heating_system_idref

        return water_heater
      end
      fail "Attached water heating system '#{@water_heating_system_idref}' not found for dishwasher '#{@id}'."
    end

    def delete
      @hpxml_object.dishwashers.delete(self)
    end

    def check_for_errors
      errors = []
      begin; water_heating_system; rescue StandardError => e; errors << e.message; end
      return errors
    end

    def to_oga(doc)
      return if nil?

      appliances = XMLHelper.create_elements_as_needed(doc, ['HPXML', 'Building', 'BuildingDetails', 'Appliances'])
      dishwasher = XMLHelper.add_element(appliances, 'Dishwasher')
      sys_id = XMLHelper.add_element(dishwasher, 'SystemIdentifier')
      XMLHelper.add_attribute(sys_id, 'id', @id)
      XMLHelper.add_element(dishwasher, 'IsSharedAppliance', to_boolean(@is_shared_appliance)) unless @is_shared_appliance.nil?
      if not @water_heating_system_idref.nil?
        attached_water_heater = XMLHelper.add_element(dishwasher, 'AttachedToWaterHeatingSystem')
        XMLHelper.add_attribute(attached_water_heater, 'idref', @water_heating_system_idref)
      end
      XMLHelper.add_element(dishwasher, 'Location', @location) unless @location.nil?
      XMLHelper.add_element(dishwasher, 'RatedAnnualkWh', to_float(@rated_annual_kwh)) unless @rated_annual_kwh.nil?
      XMLHelper.add_element(dishwasher, 'EnergyFactor', to_float(@energy_factor)) unless @energy_factor.nil?
      XMLHelper.add_element(dishwasher, 'PlaceSettingCapacity', to_integer(@place_setting_capacity)) unless @place_setting_capacity.nil?
      XMLHelper.add_element(dishwasher, 'LabelElectricRate', to_float(@label_electric_rate)) unless @label_electric_rate.nil?
      XMLHelper.add_element(dishwasher, 'LabelGasRate', to_float(@label_gas_rate)) unless @label_gas_rate.nil?
      XMLHelper.add_element(dishwasher, 'LabelAnnualGasCost', to_float(@label_annual_gas_cost)) unless @label_annual_gas_cost.nil?
      XMLHelper.add_element(dishwasher, 'LabelUsage', to_float(@label_usage)) unless @label_usage.nil?
      XMLHelper.add_extension(dishwasher, 'UsageMultiplier', to_float(@usage_multiplier)) unless @usage_multiplier.nil?
    end

    def from_oga(dishwasher)
      return if dishwasher.nil?

      @id = HPXML::get_id(dishwasher)
      @is_shared_appliance = to_boolean_or_nil(XMLHelper.get_value(dishwasher, 'IsSharedAppliance'))
      @location = XMLHelper.get_value(dishwasher, 'Location')
      @rated_annual_kwh = to_float_or_nil(XMLHelper.get_value(dishwasher, 'RatedAnnualkWh'))
      @energy_factor = to_float_or_nil(XMLHelper.get_value(dishwasher, 'EnergyFactor'))
      @place_setting_capacity = to_integer_or_nil(XMLHelper.get_value(dishwasher, 'PlaceSettingCapacity'))
      @label_electric_rate = to_float_or_nil(XMLHelper.get_value(dishwasher, 'LabelElectricRate'))
      @label_gas_rate = to_float_or_nil(XMLHelper.get_value(dishwasher, 'LabelGasRate'))
      @label_annual_gas_cost = to_float_or_nil(XMLHelper.get_value(dishwasher, 'LabelAnnualGasCost'))
      @label_usage = to_float_or_nil(XMLHelper.get_value(dishwasher, 'LabelUsage'))
      @usage_multiplier = to_float_or_nil(XMLHelper.get_value(dishwasher, 'extension/UsageMultiplier'))
      @water_heating_system_idref = HPXML::get_idref(XMLHelper.get_element(dishwasher, 'AttachedToWaterHeatingSystem'))
    end
  end

  class Refrigerators < BaseArrayElement
    def add(**kwargs)
      self << Refrigerator.new(@hpxml_object, **kwargs)
    end

    def from_oga(hpxml)
      return if hpxml.nil?

      XMLHelper.get_elements(hpxml, 'Building/BuildingDetails/Appliances/Refrigerator').each do |refrigerator|
        self << Refrigerator.new(@hpxml_object, refrigerator)
      end
    end
  end

  class Refrigerator < BaseElement
    ATTRS = [:id, :location, :rated_annual_kwh, :adjusted_annual_kwh, :usage_multiplier, :primary_indicator,
             :weekday_fractions, :weekend_fractions, :monthly_multipliers]
    attr_accessor(*ATTRS)

    def delete
      @hpxml_object.refrigerators.delete(self)
    end

    def check_for_errors
      errors = []

      if @hpxml_object.refrigerators.size > 1
        primary_indicator = false
        @hpxml_object.refrigerators.each do |refrigerator|
          next unless not refrigerator.primary_indicator.nil?
          fail 'More than one refrigerator designated as the primary.' if refrigerator.primary_indicator && primary_indicator

          primary_indicator = true if refrigerator.primary_indicator
        end
        fail 'Could not find a primary refrigerator.' if not primary_indicator
      end

      return errors
    end

    def to_oga(doc)
      return if nil?

      appliances = XMLHelper.create_elements_as_needed(doc, ['HPXML', 'Building', 'BuildingDetails', 'Appliances'])
      refrigerator = XMLHelper.add_element(appliances, 'Refrigerator')
      sys_id = XMLHelper.add_element(refrigerator, 'SystemIdentifier')
      XMLHelper.add_attribute(sys_id, 'id', @id)
      XMLHelper.add_element(refrigerator, 'Location', @location) unless @location.nil?
      XMLHelper.add_element(refrigerator, 'RatedAnnualkWh', to_float(@rated_annual_kwh)) unless @rated_annual_kwh.nil?
      XMLHelper.add_element(refrigerator, 'PrimaryIndicator', to_boolean(@primary_indicator)) unless @primary_indicator.nil?
      XMLHelper.add_extension(refrigerator, 'AdjustedAnnualkWh', to_float(@adjusted_annual_kwh)) unless @adjusted_annual_kwh.nil?
      XMLHelper.add_extension(refrigerator, 'UsageMultiplier', to_float(@usage_multiplier)) unless @usage_multiplier.nil?
      XMLHelper.add_extension(refrigerator, 'WeekdayScheduleFractions', @weekday_fractions) unless @weekday_fractions.nil?
      XMLHelper.add_extension(refrigerator, 'WeekendScheduleFractions', @weekend_fractions) unless @weekend_fractions.nil?
      XMLHelper.add_extension(refrigerator, 'MonthlyScheduleMultipliers', @monthly_multipliers) unless @monthly_multipliers.nil?
    end

    def from_oga(refrigerator)
      return if refrigerator.nil?

      @id = HPXML::get_id(refrigerator)
      @location = XMLHelper.get_value(refrigerator, 'Location')
      @rated_annual_kwh = to_float_or_nil(XMLHelper.get_value(refrigerator, 'RatedAnnualkWh'))
      @primary_indicator = to_boolean_or_nil(XMLHelper.get_value(refrigerator, 'PrimaryIndicator'))
      @adjusted_annual_kwh = to_float_or_nil(XMLHelper.get_value(refrigerator, 'extension/AdjustedAnnualkWh'))
      @usage_multiplier = to_float_or_nil(XMLHelper.get_value(refrigerator, 'extension/UsageMultiplier'))
      @weekday_fractions = XMLHelper.get_value(refrigerator, 'extension/WeekdayScheduleFractions')
      @weekend_fractions = XMLHelper.get_value(refrigerator, 'extension/WeekendScheduleFractions')
      @monthly_multipliers = XMLHelper.get_value(refrigerator, 'extension/MonthlyScheduleMultipliers')
    end
  end

  class Freezers < BaseArrayElement
    def add(**kwargs)
      self << Freezer.new(@hpxml_object, **kwargs)
    end

    def from_oga(hpxml)
      return if hpxml.nil?

      XMLHelper.get_elements(hpxml, 'Building/BuildingDetails/Appliances/Freezer').each do |freezer|
        self << Freezer.new(@hpxml_object, freezer)
      end
    end
  end

  class Freezer < BaseElement
    ATTRS = [:id, :location, :rated_annual_kwh, :adjusted_annual_kwh, :usage_multiplier,
             :weekday_fractions, :weekend_fractions, :monthly_multipliers]
    attr_accessor(*ATTRS)

    def delete
      @hpxml_object.freezers.delete(self)
    end

    def check_for_errors
      errors = []
      return errors
    end

    def to_oga(doc)
      return if nil?

      appliances = XMLHelper.create_elements_as_needed(doc, ['HPXML', 'Building', 'BuildingDetails', 'Appliances'])
      freezer = XMLHelper.add_element(appliances, 'Freezer')
      sys_id = XMLHelper.add_element(freezer, 'SystemIdentifier')
      XMLHelper.add_attribute(sys_id, 'id', @id)
      XMLHelper.add_element(freezer, 'Location', @location) unless @location.nil?
      XMLHelper.add_element(freezer, 'RatedAnnualkWh', to_float(@rated_annual_kwh)) unless @rated_annual_kwh.nil?
      XMLHelper.add_extension(freezer, 'AdjustedAnnualkWh', to_float(@adjusted_annual_kwh)) unless @adjusted_annual_kwh.nil?
      XMLHelper.add_extension(freezer, 'UsageMultiplier', to_float(@usage_multiplier)) unless @usage_multiplier.nil?
      XMLHelper.add_extension(freezer, 'WeekdayScheduleFractions', @weekday_fractions) unless @weekday_fractions.nil?
      XMLHelper.add_extension(freezer, 'WeekendScheduleFractions', @weekend_fractions) unless @weekend_fractions.nil?
      XMLHelper.add_extension(freezer, 'MonthlyScheduleMultipliers', @monthly_multipliers) unless @monthly_multipliers.nil?
    end

    def from_oga(freezer)
      return if freezer.nil?

      @id = HPXML::get_id(freezer)
      @location = XMLHelper.get_value(freezer, 'Location')
      @rated_annual_kwh = to_float_or_nil(XMLHelper.get_value(freezer, 'RatedAnnualkWh'))
      @adjusted_annual_kwh = to_float_or_nil(XMLHelper.get_value(freezer, 'extension/AdjustedAnnualkWh'))
      @usage_multiplier = to_float_or_nil(XMLHelper.get_value(freezer, 'extension/UsageMultiplier'))
      @weekday_fractions = XMLHelper.get_value(freezer, 'extension/WeekdayScheduleFractions')
      @weekend_fractions = XMLHelper.get_value(freezer, 'extension/WeekendScheduleFractions')
      @monthly_multipliers = XMLHelper.get_value(freezer, 'extension/MonthlyScheduleMultipliers')
    end
  end

  class Dehumidifiers < BaseArrayElement
    def add(**kwargs)
      self << Dehumidifier.new(@hpxml_object, **kwargs)
    end

    def from_oga(hpxml)
      return if hpxml.nil?

      XMLHelper.get_elements(hpxml, 'Building/BuildingDetails/Appliances/Dehumidifier').each do |dehumidifier|
        self << Dehumidifier.new(@hpxml_object, dehumidifier)
      end
    end
  end

  class Dehumidifier < BaseElement
    ATTRS = [:id, :capacity, :energy_factor, :integrated_energy_factor, :rh_setpoint, :fraction_served]
    attr_accessor(*ATTRS)

    def delete
      @hpxml_object.dehumidifiers.delete(self)
    end

    def check_for_errors
      errors = []
      return errors
    end

    def to_oga(doc)
      return if nil?

      appliances = XMLHelper.create_elements_as_needed(doc, ['HPXML', 'Building', 'BuildingDetails', 'Appliances'])
      dehumidifier = XMLHelper.add_element(appliances, 'Dehumidifier')
      sys_id = XMLHelper.add_element(dehumidifier, 'SystemIdentifier')
      XMLHelper.add_attribute(sys_id, 'id', @id)
      XMLHelper.add_element(dehumidifier, 'Capacity', to_float(@capacity)) unless @capacity.nil?
      XMLHelper.add_element(dehumidifier, 'EnergyFactor', to_float(@energy_factor)) unless @energy_factor.nil?
      XMLHelper.add_element(dehumidifier, 'IntegratedEnergyFactor', to_float(@integrated_energy_factor)) unless @integrated_energy_factor.nil?
      XMLHelper.add_element(dehumidifier, 'DehumidistatSetpoint', to_float(@rh_setpoint)) unless @rh_setpoint.nil?
      XMLHelper.add_element(dehumidifier, 'FractionDehumidificationLoadServed', to_float(@fraction_served)) unless @fraction_served.nil?
    end

    def from_oga(dehumidifier)
      return if dehumidifier.nil?

      @id = HPXML::get_id(dehumidifier)
      @capacity = to_float_or_nil(XMLHelper.get_value(dehumidifier, 'Capacity'))
      @energy_factor = to_float_or_nil(XMLHelper.get_value(dehumidifier, 'EnergyFactor'))
      @integrated_energy_factor = to_float_or_nil(XMLHelper.get_value(dehumidifier, 'IntegratedEnergyFactor'))
      @rh_setpoint = to_float_or_nil(XMLHelper.get_value(dehumidifier, 'DehumidistatSetpoint'))
      @fraction_served = to_float_or_nil(XMLHelper.get_value(dehumidifier, 'FractionDehumidificationLoadServed'))
    end
  end

  class CookingRanges < BaseArrayElement
    def add(**kwargs)
      self << CookingRange.new(@hpxml_object, **kwargs)
    end

    def from_oga(hpxml)
      return if hpxml.nil?

      XMLHelper.get_elements(hpxml, 'Building/BuildingDetails/Appliances/CookingRange').each do |cooking_range|
        self << CookingRange.new(@hpxml_object, cooking_range)
      end
    end
  end

  class CookingRange < BaseElement
    ATTRS = [:id, :location, :fuel_type, :is_induction, :usage_multiplier,
             :weekday_fractions, :weekend_fractions, :monthly_multipliers]
    attr_accessor(*ATTRS)

    def delete
      @hpxml_object.cooking_ranges.delete(self)
    end

    def check_for_errors
      errors = []
      return errors
    end

    def to_oga(doc)
      return if nil?

      appliances = XMLHelper.create_elements_as_needed(doc, ['HPXML', 'Building', 'BuildingDetails', 'Appliances'])
      cooking_range = XMLHelper.add_element(appliances, 'CookingRange')
      sys_id = XMLHelper.add_element(cooking_range, 'SystemIdentifier')
      XMLHelper.add_attribute(sys_id, 'id', @id)
      XMLHelper.add_element(cooking_range, 'Location', @location) unless @location.nil?
      XMLHelper.add_element(cooking_range, 'FuelType', @fuel_type) unless @fuel_type.nil?
      XMLHelper.add_element(cooking_range, 'IsInduction', to_boolean(@is_induction)) unless @is_induction.nil?
      XMLHelper.add_extension(cooking_range, 'UsageMultiplier', to_float(@usage_multiplier)) unless @usage_multiplier.nil?
      XMLHelper.add_extension(cooking_range, 'WeekdayScheduleFractions', @weekday_fractions) unless @weekday_fractions.nil?
      XMLHelper.add_extension(cooking_range, 'WeekendScheduleFractions', @weekend_fractions) unless @weekend_fractions.nil?
      XMLHelper.add_extension(cooking_range, 'MonthlyScheduleMultipliers', @monthly_multipliers) unless @monthly_multipliers.nil?
    end

    def from_oga(cooking_range)
      return if cooking_range.nil?

      @id = HPXML::get_id(cooking_range)
      @location = XMLHelper.get_value(cooking_range, 'Location')
      @fuel_type = XMLHelper.get_value(cooking_range, 'FuelType')
      @is_induction = to_boolean_or_nil(XMLHelper.get_value(cooking_range, 'IsInduction'))
      @usage_multiplier = to_float_or_nil(XMLHelper.get_value(cooking_range, 'extension/UsageMultiplier'))
      @weekday_fractions = XMLHelper.get_value(cooking_range, 'extension/WeekdayScheduleFractions')
      @weekend_fractions = XMLHelper.get_value(cooking_range, 'extension/WeekendScheduleFractions')
      @monthly_multipliers = XMLHelper.get_value(cooking_range, 'extension/MonthlyScheduleMultipliers')
    end
  end

  class Ovens < BaseArrayElement
    def add(**kwargs)
      self << Oven.new(@hpxml_object, **kwargs)
    end

    def from_oga(hpxml)
      return if hpxml.nil?

      XMLHelper.get_elements(hpxml, 'Building/BuildingDetails/Appliances/Oven').each do |oven|
        self << Oven.new(@hpxml_object, oven)
      end
    end
  end

  class Oven < BaseElement
    ATTRS = [:id, :is_convection]
    attr_accessor(*ATTRS)

    def delete
      @hpxml_object.ovens.delete(self)
    end

    def check_for_errors
      errors = []
      return errors
    end

    def to_oga(doc)
      return if nil?

      appliances = XMLHelper.create_elements_as_needed(doc, ['HPXML', 'Building', 'BuildingDetails', 'Appliances'])
      oven = XMLHelper.add_element(appliances, 'Oven')
      sys_id = XMLHelper.add_element(oven, 'SystemIdentifier')
      XMLHelper.add_attribute(sys_id, 'id', @id)
      XMLHelper.add_element(oven, 'IsConvection', to_boolean(@is_convection)) unless @is_convection.nil?
    end

    def from_oga(oven)
      return if oven.nil?

      @id = HPXML::get_id(oven)
      @is_convection = to_boolean_or_nil(XMLHelper.get_value(oven, 'IsConvection'))
    end
  end

  class LightingGroups < BaseArrayElement
    def add(**kwargs)
      self << LightingGroup.new(@hpxml_object, **kwargs)
    end

    def from_oga(hpxml)
      return if hpxml.nil?

      XMLHelper.get_elements(hpxml, 'Building/BuildingDetails/Lighting/LightingGroup').each do |lighting_group|
        self << LightingGroup.new(@hpxml_object, lighting_group)
      end
    end
  end

  class LightingGroup < BaseElement
    ATTRS = [:id, :location, :fraction_of_units_in_location, :lighting_type]
    attr_accessor(*ATTRS)

    def delete
      @hpxml_object.lighting_groups.delete(self)
    end

    def check_for_errors
      errors = []
      return errors
    end

    def to_oga(doc)
      return if nil?

      lighting = XMLHelper.create_elements_as_needed(doc, ['HPXML', 'Building', 'BuildingDetails', 'Lighting'])
      lighting_group = XMLHelper.add_element(lighting, 'LightingGroup')
      sys_id = XMLHelper.add_element(lighting_group, 'SystemIdentifier')
      XMLHelper.add_attribute(sys_id, 'id', @id)
      XMLHelper.add_element(lighting_group, 'Location', @location) unless @location.nil?
      XMLHelper.add_element(lighting_group, 'FractionofUnitsInLocation', to_float(@fraction_of_units_in_location)) unless @fraction_of_units_in_location.nil?
      if not @lighting_type.nil?
        lighting_type = XMLHelper.add_element(lighting_group, 'LightingType')
        XMLHelper.add_element(lighting_type, @lighting_type)
      end
    end

    def from_oga(lighting_group)
      return if lighting_group.nil?

      @id = HPXML::get_id(lighting_group)
      @location = XMLHelper.get_value(lighting_group, 'Location')
      @fraction_of_units_in_location = to_float_or_nil(XMLHelper.get_value(lighting_group, 'FractionofUnitsInLocation'))
      @lighting_type = XMLHelper.get_child_name(lighting_group, 'LightingType')
    end
  end

  class Lighting < BaseElement
    ATTRS = [:interior_usage_multiplier, :garage_usage_multiplier, :exterior_usage_multiplier,
             :interior_weekday_fractions, :interior_weekend_fractions, :interior_monthly_multipliers,
             :garage_weekday_fractions, :garage_weekend_fractions, :garage_monthly_multipliers,
             :exterior_weekday_fractions, :exterior_weekend_fractions, :exterior_monthly_multipliers,
             :holiday_exists, :holiday_kwh_per_day, :holiday_period_begin_month, :holiday_period_begin_day_of_month,
             :holiday_period_end_month, :holiday_period_end_day_of_month, :holiday_weekday_fractions, :holiday_weekend_fractions]
    attr_accessor(*ATTRS)

    def check_for_errors
      errors = []
      return errors
    end

    def to_oga(doc)
      return if nil?

      lighting = XMLHelper.create_elements_as_needed(doc, ['HPXML', 'Building', 'BuildingDetails', 'Lighting'])
      XMLHelper.add_extension(lighting, 'InteriorUsageMultiplier', to_float(@interior_usage_multiplier)) unless @interior_usage_multiplier.nil?
      XMLHelper.add_extension(lighting, 'GarageUsageMultiplier', to_float(@garage_usage_multiplier)) unless @garage_usage_multiplier.nil?
      XMLHelper.add_extension(lighting, 'ExteriorUsageMultiplier', to_float(@exterior_usage_multiplier)) unless @exterior_usage_multiplier.nil?
      XMLHelper.add_extension(lighting, 'InteriorWeekdayScheduleFractions', @interior_weekday_fractions) unless @interior_weekday_fractions.nil?
      XMLHelper.add_extension(lighting, 'InteriorWeekendScheduleFractions', @interior_weekend_fractions) unless @interior_weekend_fractions.nil?
      XMLHelper.add_extension(lighting, 'InteriorMonthlyScheduleMultipliers', @interior_monthly_multipliers) unless @interior_monthly_multipliers.nil?
      XMLHelper.add_extension(lighting, 'GarageWeekdayScheduleFractions', @garage_weekday_fractions) unless @garage_weekday_fractions.nil?
      XMLHelper.add_extension(lighting, 'GarageWeekendScheduleFractions', @garage_weekend_fractions) unless @garage_weekend_fractions.nil?
      XMLHelper.add_extension(lighting, 'GarageMonthlyScheduleMultipliers', @garage_monthly_multipliers) unless @garage_monthly_multipliers.nil?
      XMLHelper.add_extension(lighting, 'ExteriorWeekdayScheduleFractions', @exterior_weekday_fractions) unless @exterior_weekday_fractions.nil?
      XMLHelper.add_extension(lighting, 'ExteriorWeekendScheduleFractions', @exterior_weekend_fractions) unless @exterior_weekend_fractions.nil?
      XMLHelper.add_extension(lighting, 'ExteriorMonthlyScheduleMultipliers', @exterior_monthly_multipliers) unless @exterior_monthly_multipliers.nil?
      if @holiday_exists
        exterior_holiday_lighting = XMLHelper.create_elements_as_needed(doc, ['HPXML', 'Building', 'BuildingDetails', 'Lighting', 'extension', 'ExteriorHolidayLighting'])
        if not @holiday_kwh_per_day.nil?
          holiday_lighting_load = XMLHelper.add_element(exterior_holiday_lighting, 'Load')
          XMLHelper.add_element(holiday_lighting_load, 'Units', 'kWh/day')
          XMLHelper.add_element(holiday_lighting_load, 'Value', to_float(@holiday_kwh_per_day))
        end
        XMLHelper.add_element(exterior_holiday_lighting, 'PeriodBeginMonth', to_integer(@holiday_period_begin_month)) unless @holiday_period_begin_month.nil?
        XMLHelper.add_element(exterior_holiday_lighting, 'PeriodBeginDayOfMonth', to_integer(@holiday_period_begin_day_of_month)) unless @holiday_period_begin_day_of_month.nil?
        XMLHelper.add_element(exterior_holiday_lighting, 'PeriodEndMonth', to_integer(@holiday_period_end_month)) unless @holiday_period_end_month.nil?
        XMLHelper.add_element(exterior_holiday_lighting, 'PeriodEndDayOfMonth', to_integer(@holiday_period_end_day_of_month)) unless @holiday_period_end_day_of_month.nil?
        XMLHelper.add_element(exterior_holiday_lighting, 'WeekdayScheduleFractions', @holiday_weekday_fractions) unless @holiday_weekday_fractions.nil?
        XMLHelper.add_element(exterior_holiday_lighting, 'WeekendScheduleFractions', @holiday_weekend_fractions) unless @holiday_weekend_fractions.nil?
      end
    end

    def from_oga(hpxml)
      return if hpxml.nil?

      lighting = XMLHelper.get_element(hpxml, 'Building/BuildingDetails/Lighting')
      return if lighting.nil?

      @interior_usage_multiplier = to_float_or_nil(XMLHelper.get_value(lighting, 'extension/InteriorUsageMultiplier'))
      @garage_usage_multiplier = to_float_or_nil(XMLHelper.get_value(lighting, 'extension/GarageUsageMultiplier'))
      @exterior_usage_multiplier = to_float_or_nil(XMLHelper.get_value(lighting, 'extension/ExteriorUsageMultiplier'))
      @interior_weekday_fractions = XMLHelper.get_value(lighting, 'extension/InteriorWeekdayScheduleFractions')
      @interior_weekend_fractions = XMLHelper.get_value(lighting, 'extension/InteriorWeekendScheduleFractions')
      @interior_monthly_multipliers = XMLHelper.get_value(lighting, 'extension/InteriorMonthlyScheduleMultipliers')
      @garage_weekday_fractions = XMLHelper.get_value(lighting, 'extension/GarageWeekdayScheduleFractions')
      @garage_weekend_fractions = XMLHelper.get_value(lighting, 'extension/GarageWeekendScheduleFractions')
      @garage_monthly_multipliers = XMLHelper.get_value(lighting, 'extension/GarageMonthlyScheduleMultipliers')
      @exterior_weekday_fractions = XMLHelper.get_value(lighting, 'extension/ExteriorWeekdayScheduleFractions')
      @exterior_weekend_fractions = XMLHelper.get_value(lighting, 'extension/ExteriorWeekendScheduleFractions')
      @exterior_monthly_multipliers = XMLHelper.get_value(lighting, 'extension/ExteriorMonthlyScheduleMultipliers')
      if not XMLHelper.get_element(hpxml, 'Building/BuildingDetails/Lighting/extension/ExteriorHolidayLighting').nil?
        @holiday_exists = true
        @holiday_kwh_per_day = to_float_or_nil(XMLHelper.get_value(lighting, 'extension/ExteriorHolidayLighting/Load[Units="kWh/day"]/Value'))
        @holiday_period_begin_month = to_integer_or_nil(XMLHelper.get_value(lighting, 'extension/ExteriorHolidayLighting/PeriodBeginMonth'))
        @holiday_period_begin_day_of_month = to_integer_or_nil(XMLHelper.get_value(lighting, 'extension/ExteriorHolidayLighting/PeriodBeginDayOfMonth'))
        @holiday_period_end_month = to_integer_or_nil(XMLHelper.get_value(lighting, 'extension/ExteriorHolidayLighting/PeriodEndMonth'))
        @holiday_period_end_day_of_month = to_integer_or_nil(XMLHelper.get_value(lighting, 'extension/ExteriorHolidayLighting/PeriodEndDayOfMonth'))
        @holiday_weekday_fractions = XMLHelper.get_value(lighting, 'extension/ExteriorHolidayLighting/WeekdayScheduleFractions')
        @holiday_weekend_fractions = XMLHelper.get_value(lighting, 'extension/ExteriorHolidayLighting/WeekendScheduleFractions')
      else
        @holiday_exists = false
      end
    end
  end

  class CeilingFans < BaseArrayElement
    def add(**kwargs)
      self << CeilingFan.new(@hpxml_object, **kwargs)
    end

    def from_oga(hpxml)
      return if hpxml.nil?

      XMLHelper.get_elements(hpxml, 'Building/BuildingDetails/Lighting/CeilingFan').each do |ceiling_fan|
        self << CeilingFan.new(@hpxml_object, ceiling_fan)
      end
    end
  end

  class CeilingFan < BaseElement
    ATTRS = [:id, :efficiency, :quantity]
    attr_accessor(*ATTRS)

    def delete
      @hpxml_object.ceiling_fans.delete(self)
    end

    def check_for_errors
      errors = []
      return errors
    end

    def to_oga(doc)
      return if nil?

      lighting = XMLHelper.create_elements_as_needed(doc, ['HPXML', 'Building', 'BuildingDetails', 'Lighting'])
      ceiling_fan = XMLHelper.add_element(lighting, 'CeilingFan')
      sys_id = XMLHelper.add_element(ceiling_fan, 'SystemIdentifier')
      XMLHelper.add_attribute(sys_id, 'id', @id)
      if not @efficiency.nil?
        airflow = XMLHelper.add_element(ceiling_fan, 'Airflow')
        XMLHelper.add_element(airflow, 'FanSpeed', 'medium')
        XMLHelper.add_element(airflow, 'Efficiency', to_float(@efficiency))
      end
      XMLHelper.add_element(ceiling_fan, 'Quantity', to_integer(@quantity)) unless @quantity.nil?
    end

    def from_oga(ceiling_fan)
      @id = HPXML::get_id(ceiling_fan)
      @efficiency = to_float_or_nil(XMLHelper.get_value(ceiling_fan, "Airflow[FanSpeed='medium']/Efficiency"))
      @quantity = to_integer_or_nil(XMLHelper.get_value(ceiling_fan, 'Quantity'))
    end
  end

  class Pools < BaseArrayElement
    def add(**kwargs)
      self << Pool.new(@hpxml_object, **kwargs)
    end

    def from_oga(hpxml)
      return if hpxml.nil?

      XMLHelper.get_elements(hpxml, 'Building/BuildingDetails/Pools/Pool').each do |pool|
        self << Pool.new(@hpxml_object, pool)
      end
    end
  end

  class Pool < BaseElement
    ATTRS = [:id, :heater_id, :heater_type, :heater_load_units, :heater_load_value, :heater_usage_multiplier,
             :pump_id, :pump_kwh_per_year, :pump_usage_multiplier,
             :heater_weekday_fractions, :heater_weekend_fractions, :heater_monthly_multipliers,
             :pump_weekday_fractions, :pump_weekend_fractions, :pump_monthly_multipliers]
    attr_accessor(*ATTRS)

    def delete
      @hpxml_object.pools.delete(self)
    end

    def check_for_errors
      errors = []
      return errors
    end

    def to_oga(doc)
      return if nil?

      pools = XMLHelper.create_elements_as_needed(doc, ['HPXML', 'Building', 'BuildingDetails', 'Pools'])
      pool = XMLHelper.add_element(pools, 'Pool')
      sys_id = XMLHelper.add_element(pool, 'SystemIdentifier')
      XMLHelper.add_attribute(sys_id, 'id', @id)
      pumps = XMLHelper.add_element(pool, 'PoolPumps')
      pool_pump = XMLHelper.add_element(pumps, 'PoolPump')
      sys_id = XMLHelper.add_element(pool_pump, 'SystemIdentifier')
      if not @pump_id.nil?
        XMLHelper.add_attribute(sys_id, 'id', @pump_id)
      else
        XMLHelper.add_attribute(sys_id, 'id', @id + 'Pump')
      end
      if not @pump_kwh_per_year.nil?
        load = XMLHelper.add_element(pool_pump, 'Load')
        XMLHelper.add_element(load, 'Units', UnitsKwhPerYear)
        XMLHelper.add_element(load, 'Value', to_float(@pump_kwh_per_year))
        XMLHelper.add_extension(pool_pump, 'UsageMultiplier', to_float(@pump_usage_multiplier)) unless @pump_usage_multiplier.nil?
        XMLHelper.add_extension(pool_pump, 'WeekdayScheduleFractions', @pump_weekday_fractions) unless @pump_weekday_fractions.nil?
        XMLHelper.add_extension(pool_pump, 'WeekendScheduleFractions', @pump_weekend_fractions) unless @pump_weekend_fractions.nil?
        XMLHelper.add_extension(pool_pump, 'MonthlyScheduleMultipliers', @pump_monthly_multipliers) unless @pump_monthly_multipliers.nil?
      end
      if not @heater_type.nil?
        heater = XMLHelper.add_element(pool, 'Heater')
        sys_id = XMLHelper.add_element(heater, 'SystemIdentifier')
        if not @heater_id.nil?
          XMLHelper.add_attribute(sys_id, 'id', @heater_id)
        else
          XMLHelper.add_attribute(sys_id, 'id', @id + 'Heater')
        end
        XMLHelper.add_element(heater, 'Type', @heater_type)
        if (not @heater_load_units.nil?) && (not @heater_load_value.nil?)
          load = XMLHelper.add_element(heater, 'Load')
          XMLHelper.add_element(load, 'Units', @heater_load_units)
          XMLHelper.add_element(load, 'Value', to_float(@heater_load_value))
        end
        XMLHelper.add_extension(heater, 'UsageMultiplier', to_float(@heater_usage_multiplier)) unless @heater_usage_multiplier.nil?
        XMLHelper.add_extension(heater, 'WeekdayScheduleFractions', @heater_weekday_fractions) unless @heater_weekday_fractions.nil?
        XMLHelper.add_extension(heater, 'WeekendScheduleFractions', @heater_weekend_fractions) unless @heater_weekend_fractions.nil?
        XMLHelper.add_extension(heater, 'MonthlyScheduleMultipliers', @heater_monthly_multipliers) unless @heater_monthly_multipliers.nil?
      end
    end

    def from_oga(pool)
      @id = HPXML::get_id(pool)
      pool_pump = XMLHelper.get_element(pool, 'PoolPumps/PoolPump')
      @pump_id = HPXML::get_id(pool_pump)
      @pump_kwh_per_year = to_float_or_nil(XMLHelper.get_value(pool_pump, "Load[Units='#{UnitsKwhPerYear}']/Value"))
      @pump_usage_multiplier = to_float_or_nil(XMLHelper.get_value(pool_pump, 'extension/UsageMultiplier'))
      @pump_weekday_fractions = XMLHelper.get_value(pool_pump, 'extension/WeekdayScheduleFractions')
      @pump_weekend_fractions = XMLHelper.get_value(pool_pump, 'extension/WeekendScheduleFractions')
      @pump_monthly_multipliers = XMLHelper.get_value(pool_pump, 'extension/MonthlyScheduleMultipliers')
      heater = XMLHelper.get_element(pool, 'Heater')
      if not heater.nil?
        @heater_id = HPXML::get_id(heater)
        @heater_type = XMLHelper.get_value(heater, 'Type')
        @heater_load_units = XMLHelper.get_value(heater, 'Load/Units')
        @heater_load_value = to_float_or_nil(XMLHelper.get_value(heater, 'Load/Value'))
        @heater_usage_multiplier = to_float_or_nil(XMLHelper.get_value(heater, 'extension/UsageMultiplier'))
        @heater_weekday_fractions = XMLHelper.get_value(heater, 'extension/WeekdayScheduleFractions')
        @heater_weekend_fractions = XMLHelper.get_value(heater, 'extension/WeekendScheduleFractions')
        @heater_monthly_multipliers = XMLHelper.get_value(heater, 'extension/MonthlyScheduleMultipliers')
      end
    end
  end

  class HotTubs < BaseArrayElement
    def add(**kwargs)
      self << HotTub.new(@hpxml_object, **kwargs)
    end

    def from_oga(hpxml)
      return if hpxml.nil?

      XMLHelper.get_elements(hpxml, 'Building/BuildingDetails/HotTubs/HotTub').each do |hot_tub|
        self << HotTub.new(@hpxml_object, hot_tub)
      end
    end
  end

  class HotTub < BaseElement
    ATTRS = [:id, :heater_id, :heater_type, :heater_load_units, :heater_load_value, :heater_usage_multiplier,
             :pump_id, :pump_kwh_per_year, :pump_usage_multiplier,
             :heater_weekday_fractions, :heater_weekend_fractions, :heater_monthly_multipliers,
             :pump_weekday_fractions, :pump_weekend_fractions, :pump_monthly_multipliers]
    attr_accessor(*ATTRS)

    def delete
      @hpxml_object.hot_tubs.delete(self)
    end

    def check_for_errors
      errors = []
      return errors
    end

    def to_oga(doc)
      return if nil?

      hot_tubs = XMLHelper.create_elements_as_needed(doc, ['HPXML', 'Building', 'BuildingDetails', 'HotTubs'])
      hot_tub = XMLHelper.add_element(hot_tubs, 'HotTub')
      sys_id = XMLHelper.add_element(hot_tub, 'SystemIdentifier')
      XMLHelper.add_attribute(sys_id, 'id', @id)
      pumps = XMLHelper.add_element(hot_tub, 'HotTubPumps')
      hot_tub_pump = XMLHelper.add_element(pumps, 'HotTubPump')
      sys_id = XMLHelper.add_element(hot_tub_pump, 'SystemIdentifier')
      if not @pump_id.nil?
        XMLHelper.add_attribute(sys_id, 'id', @pump_id)
      else
        XMLHelper.add_attribute(sys_id, 'id', @id + 'Pump')
      end
      if not @pump_kwh_per_year.nil?
        load = XMLHelper.add_element(hot_tub_pump, 'Load')
        XMLHelper.add_element(load, 'Units', UnitsKwhPerYear)
        XMLHelper.add_element(load, 'Value', to_float(@pump_kwh_per_year))
        XMLHelper.add_extension(hot_tub_pump, 'UsageMultiplier', to_float(@pump_usage_multiplier)) unless @pump_usage_multiplier.nil?
        XMLHelper.add_extension(hot_tub_pump, 'WeekdayScheduleFractions', @pump_weekday_fractions) unless @pump_weekday_fractions.nil?
        XMLHelper.add_extension(hot_tub_pump, 'WeekendScheduleFractions', @pump_weekend_fractions) unless @pump_weekend_fractions.nil?
        XMLHelper.add_extension(hot_tub_pump, 'MonthlyScheduleMultipliers', @pump_monthly_multipliers) unless @pump_monthly_multipliers.nil?
      end
      if not @heater_type.nil?
        heater = XMLHelper.add_element(hot_tub, 'Heater')
        sys_id = XMLHelper.add_element(heater, 'SystemIdentifier')
        if not @heater_id.nil?
          XMLHelper.add_attribute(sys_id, 'id', @heater_id)
        else
          XMLHelper.add_attribute(sys_id, 'id', @id + 'Heater')
        end
        XMLHelper.add_element(heater, 'Type', @heater_type)
        if (not @heater_load_units.nil?) && (not @heater_load_value.nil?)
          load = XMLHelper.add_element(heater, 'Load')
          XMLHelper.add_element(load, 'Units', @heater_load_units)
          XMLHelper.add_element(load, 'Value', to_float(@heater_load_value))
        end
        XMLHelper.add_extension(heater, 'UsageMultiplier', to_float(@heater_usage_multiplier)) unless @heater_usage_multiplier.nil?
        XMLHelper.add_extension(heater, 'WeekdayScheduleFractions', @heater_weekday_fractions) unless @heater_weekday_fractions.nil?
        XMLHelper.add_extension(heater, 'WeekendScheduleFractions', @heater_weekend_fractions) unless @heater_weekend_fractions.nil?
        XMLHelper.add_extension(heater, 'MonthlyScheduleMultipliers', @heater_monthly_multipliers) unless @heater_monthly_multipliers.nil?
      end
    end

    def from_oga(hot_tub)
      @id = HPXML::get_id(hot_tub)
      hot_tub_pump = XMLHelper.get_element(hot_tub, 'HotTubPumps/HotTubPump')
      @pump_id = HPXML::get_id(hot_tub_pump)
      @pump_kwh_per_year = to_float_or_nil(XMLHelper.get_value(hot_tub_pump, "Load[Units='#{UnitsKwhPerYear}']/Value"))
      @pump_usage_multiplier = to_float_or_nil(XMLHelper.get_value(hot_tub_pump, 'extension/UsageMultiplier'))
      @pump_weekday_fractions = XMLHelper.get_value(hot_tub_pump, 'extension/WeekdayScheduleFractions')
      @pump_weekend_fractions = XMLHelper.get_value(hot_tub_pump, 'extension/WeekendScheduleFractions')
      @pump_monthly_multipliers = XMLHelper.get_value(hot_tub_pump, 'extension/MonthlyScheduleMultipliers')
      heater = XMLHelper.get_element(hot_tub, 'Heater')
      if not heater.nil?
        @heater_id = HPXML::get_id(heater)
        @heater_type = XMLHelper.get_value(heater, 'Type')
        @heater_load_units = XMLHelper.get_value(heater, 'Load/Units')
        @heater_load_value = to_float_or_nil(XMLHelper.get_value(heater, 'Load/Value'))
        @heater_usage_multiplier = to_float_or_nil(XMLHelper.get_value(heater, 'extension/UsageMultiplier'))
        @heater_weekday_fractions = XMLHelper.get_value(heater, 'extension/WeekdayScheduleFractions')
        @heater_weekend_fractions = XMLHelper.get_value(heater, 'extension/WeekendScheduleFractions')
        @heater_monthly_multipliers = XMLHelper.get_value(heater, 'extension/MonthlyScheduleMultipliers')
      end
    end
  end

  class PlugLoads < BaseArrayElement
    def add(**kwargs)
      self << PlugLoad.new(@hpxml_object, **kwargs)
    end

    def from_oga(hpxml)
      return if hpxml.nil?

      XMLHelper.get_elements(hpxml, 'Building/BuildingDetails/MiscLoads/PlugLoad').each do |plug_load|
        self << PlugLoad.new(@hpxml_object, plug_load)
      end
    end
  end

  class PlugLoad < BaseElement
    ATTRS = [:id, :plug_load_type, :kWh_per_year, :frac_sensible, :frac_latent, :usage_multiplier,
             :weekday_fractions, :weekend_fractions, :monthly_multipliers, :location]
    attr_accessor(*ATTRS)

    def delete
      @hpxml_object.plug_loads.delete(self)
    end

    def check_for_errors
      errors = []
      return errors
    end

    def to_oga(doc)
      return if nil?

      misc_loads = XMLHelper.create_elements_as_needed(doc, ['HPXML', 'Building', 'BuildingDetails', 'MiscLoads'])
      plug_load = XMLHelper.add_element(misc_loads, 'PlugLoad')
      sys_id = XMLHelper.add_element(plug_load, 'SystemIdentifier')
      XMLHelper.add_attribute(sys_id, 'id', @id)
      XMLHelper.add_element(plug_load, 'PlugLoadType', @plug_load_type) unless @plug_load_type.nil?
      XMLHelper.add_element(plug_load, 'Location', @location) unless @location.nil?
      if not @kWh_per_year.nil?
        load = XMLHelper.add_element(plug_load, 'Load')
        XMLHelper.add_element(load, 'Units', UnitsKwhPerYear)
        XMLHelper.add_element(load, 'Value', to_float(@kWh_per_year))
      end
      XMLHelper.add_extension(plug_load, 'FracSensible', to_float(@frac_sensible)) unless @frac_sensible.nil?
      XMLHelper.add_extension(plug_load, 'FracLatent', to_float(@frac_latent)) unless @frac_latent.nil?
      XMLHelper.add_extension(plug_load, 'UsageMultiplier', to_float(@usage_multiplier)) unless @usage_multiplier.nil?
      XMLHelper.add_extension(plug_load, 'WeekdayScheduleFractions', @weekday_fractions) unless @weekday_fractions.nil?
      XMLHelper.add_extension(plug_load, 'WeekendScheduleFractions', @weekend_fractions) unless @weekend_fractions.nil?
      XMLHelper.add_extension(plug_load, 'MonthlyScheduleMultipliers', @monthly_multipliers) unless @monthly_multipliers.nil?
    end

    def from_oga(plug_load)
      @id = HPXML::get_id(plug_load)
      @plug_load_type = XMLHelper.get_value(plug_load, 'PlugLoadType')
      @location = XMLHelper.get_value(plug_load, 'Location')
      @kWh_per_year = to_float_or_nil(XMLHelper.get_value(plug_load, "Load[Units='#{UnitsKwhPerYear}']/Value"))
      @frac_sensible = to_float_or_nil(XMLHelper.get_value(plug_load, 'extension/FracSensible'))
      @frac_latent = to_float_or_nil(XMLHelper.get_value(plug_load, 'extension/FracLatent'))
      @usage_multiplier = to_float_or_nil(XMLHelper.get_value(plug_load, 'extension/UsageMultiplier'))
      @weekday_fractions = XMLHelper.get_value(plug_load, 'extension/WeekdayScheduleFractions')
      @weekend_fractions = XMLHelper.get_value(plug_load, 'extension/WeekendScheduleFractions')
      @monthly_multipliers = XMLHelper.get_value(plug_load, 'extension/MonthlyScheduleMultipliers')
    end
  end

  class FuelLoads < BaseArrayElement
    def add(**kwargs)
      self << FuelLoad.new(@hpxml_object, **kwargs)
    end

    def from_oga(hpxml)
      return if hpxml.nil?

      XMLHelper.get_elements(hpxml, 'Building/BuildingDetails/MiscLoads/FuelLoad').each do |fuel_load|
        self << FuelLoad.new(@hpxml_object, fuel_load)
      end
    end
  end

  class FuelLoad < BaseElement
    ATTRS = [:id, :fuel_load_type, :fuel_type, :therm_per_year, :frac_sensible, :frac_latent, :usage_multiplier,
             :weekday_fractions, :weekend_fractions, :monthly_multipliers, :location]
    attr_accessor(*ATTRS)

    def delete
      @hpxml_object.fuel_loads.delete(self)
    end

    def check_for_errors
      errors = []
      return errors
    end

    def to_oga(doc)
      return if nil?

      misc_loads = XMLHelper.create_elements_as_needed(doc, ['HPXML', 'Building', 'BuildingDetails', 'MiscLoads'])
      fuel_load = XMLHelper.add_element(misc_loads, 'FuelLoad')
      sys_id = XMLHelper.add_element(fuel_load, 'SystemIdentifier')
      XMLHelper.add_attribute(sys_id, 'id', @id)
      XMLHelper.add_element(fuel_load, 'FuelLoadType', @fuel_load_type) unless @fuel_load_type.nil?
      XMLHelper.add_element(fuel_load, 'Location', @location) unless @location.nil?
      if not @therm_per_year.nil?
        load = XMLHelper.add_element(fuel_load, 'Load')
        XMLHelper.add_element(load, 'Units', UnitsThermPerYear)
        XMLHelper.add_element(load, 'Value', to_float(@therm_per_year))
      end
      XMLHelper.add_element(fuel_load, 'FuelType', @fuel_type) unless @fuel_type.nil?
      XMLHelper.add_extension(fuel_load, 'FracSensible', to_float(@frac_sensible)) unless @frac_sensible.nil?
      XMLHelper.add_extension(fuel_load, 'FracLatent', to_float(@frac_latent)) unless @frac_latent.nil?
      XMLHelper.add_extension(fuel_load, 'UsageMultiplier', to_float(@usage_multiplier)) unless @usage_multiplier.nil?
      XMLHelper.add_extension(fuel_load, 'WeekdayScheduleFractions', @weekday_fractions) unless @weekday_fractions.nil?
      XMLHelper.add_extension(fuel_load, 'WeekendScheduleFractions', @weekend_fractions) unless @weekend_fractions.nil?
      XMLHelper.add_extension(fuel_load, 'MonthlyScheduleMultipliers', @monthly_multipliers) unless @monthly_multipliers.nil?
    end

    def from_oga(fuel_load)
      @id = HPXML::get_id(fuel_load)
      @fuel_load_type = XMLHelper.get_value(fuel_load, 'FuelLoadType')
      @location = XMLHelper.get_value(fuel_load, 'Location')
      @therm_per_year = to_float_or_nil(XMLHelper.get_value(fuel_load, "Load[Units='#{UnitsThermPerYear}']/Value"))
      @fuel_type = XMLHelper.get_value(fuel_load, 'FuelType')
      @frac_sensible = to_float_or_nil(XMLHelper.get_value(fuel_load, 'extension/FracSensible'))
      @frac_latent = to_float_or_nil(XMLHelper.get_value(fuel_load, 'extension/FracLatent'))
      @usage_multiplier = to_float_or_nil(XMLHelper.get_value(fuel_load, 'extension/UsageMultiplier'))
      @weekday_fractions = XMLHelper.get_value(fuel_load, 'extension/WeekdayScheduleFractions')
      @weekend_fractions = XMLHelper.get_value(fuel_load, 'extension/WeekendScheduleFractions')
      @monthly_multipliers = XMLHelper.get_value(fuel_load, 'extension/MonthlyScheduleMultipliers')
    end
  end

  def _create_oga_document()
    doc = XMLHelper.create_doc(version = '1.0', encoding = 'UTF-8')
    hpxml = XMLHelper.add_element(doc, 'HPXML')
    XMLHelper.add_attribute(hpxml, 'xmlns', 'http://hpxmlonline.com/2019/10')
    XMLHelper.add_attribute(hpxml, 'xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance')
    XMLHelper.add_attribute(hpxml, 'xsi:schemaLocation', 'http://hpxmlonline.com/2019/10')
    XMLHelper.add_attribute(hpxml, 'schemaVersion', '3.0')
    return doc
  end

  def collapse_enclosure_surfaces()
    # Collapses like surfaces into a single surface with, e.g., aggregate surface area.
    # This can significantly speed up performance for HPXML files with lots of individual
    # surfaces (e.g., windows).

    surf_types = { roofs: @roofs,
                   walls: @walls,
                   rim_joists: @rim_joists,
                   foundation_walls: @foundation_walls,
                   frame_floors: @frame_floors,
                   slabs: @slabs,
                   windows: @windows,
                   skylights: @skylights,
                   doors: @doors }

    attrs_to_ignore = [:id,
                       :insulation_id,
                       :perimeter_insulation_id,
                       :under_slab_insulation_id,
                       :area,
                       :exposed_perimeter]

    # Look for pairs of surfaces that can be collapsed
    surf_types.each do |surf_type, surfaces|
      for i in 0..surfaces.size - 1
        surf = surfaces[i]
        next if surf.nil?

        for j in (surfaces.size - 1).downto(i + 1)
          surf2 = surfaces[j]
          next if surf2.nil?

          match = true
          surf.class::ATTRS.each do |attribute|
            next if attrs_to_ignore.include? attribute
            next if (surf_type == :foundation_walls) && (attribute == :azimuth) # Azimuth of foundation walls is irrelevant
            next if surf.send(attribute) == surf2.send(attribute)

            match = false
          end
          next unless match

          # Update values
          if (not surf.area.nil?) && (not surf2.area.nil?)
            surf.area += surf2.area
          end
          if (surf_type == :slabs) && (not surf.exposed_perimeter.nil?) && (not surf2.exposed_perimeter.nil?)
            surf.exposed_perimeter += surf2.exposed_perimeter
          end

          # Update subsurface idrefs as appropriate
          (@windows + @doors).each do |subsurf|
            next unless subsurf.wall_idref == surf2.id

            subsurf.wall_idref = surf.id
          end
          @skylights.each do |subsurf|
            next unless subsurf.roof_idref == surf2.id

            subsurf.roof_idref = surf.id
          end

          # Remove old surface
          surfaces[j].delete
        end
      end
    end
  end

  def delete_tiny_surfaces()
    (@rim_joists + @walls + @foundation_walls + @frame_floors + @roofs + @windows + @skylights + @doors + @slabs).reverse_each do |surface|
      next if surface.area.nil? || (surface.area > 0.1)

      surface.delete
    end
  end

  def delete_adiabatic_subsurfaces()
    @doors.reverse_each do |door|
      next if door.wall.exterior_adjacent_to != HPXML::LocationOtherHousingUnit

      door.delete
    end
    @windows.reverse_each do |window|
      next if window.wall.exterior_adjacent_to != HPXML::LocationOtherHousingUnit

      window.delete
    end
  end

  def check_for_errors()
    errors = []

    # ------------------------------- #
    # Check for errors across objects #
    # ------------------------------- #

    # Check for globally unique SystemIdentifier IDs
    sys_ids = {}
    self.class::HPXML_ATTRS.each do |attribute|
      hpxml_obj = send(attribute)
      next unless hpxml_obj.is_a? HPXML::BaseArrayElement

      hpxml_obj.each do |obj|
        next unless obj.respond_to? :id

        sys_ids[obj.id] = 0 if sys_ids[obj.id].nil?
        sys_ids[obj.id] += 1
      end
    end
    sys_ids.each do |sys_id, cnt|
      errors << "Duplicate SystemIdentifier IDs detected for '#{sys_id}'." if cnt > 1
    end

    # Check sum of HVAC FractionCoolLoadServeds <= 1
    if total_fraction_cool_load_served > 1.01 # Use 1.01 in case of rounding
      errors << "Expected FractionCoolLoadServed to sum to <= 1, but calculated sum is #{total_fraction_cool_load_served.round(2)}."
    end

    # Check sum of HVAC FractionHeatLoadServeds <= 1
    if total_fraction_heat_load_served > 1.01 # Use 1.01 in case of rounding
      errors << "Expected FractionHeatLoadServed to sum to <= 1, but calculated sum is #{total_fraction_heat_load_served.round(2)}."
    end

    # Check sum of HVAC FractionDHWLoadServed == 1
    frac_dhw_load = @water_heating_systems.map { |dhw| dhw.fraction_dhw_load_served.to_f }.sum(0.0)
    if (frac_dhw_load > 0) && ((frac_dhw_load < 0.99) || (frac_dhw_load > 1.01)) # Use 0.99/1.01 in case of rounding
      errors << "Expected FractionDHWLoadServed to sum to 1, but calculated sum is #{frac_dhw_load.round(2)}."
    end

    # Check sum of lighting fractions in a location <= 1
    ltg_fracs = {}
    @lighting_groups.each do |lighting_group|
      next if lighting_group.location.nil? || lighting_group.fraction_of_units_in_location.nil?

      ltg_fracs[lighting_group.location] = 0 if ltg_fracs[lighting_group.location].nil?
      ltg_fracs[lighting_group.location] += lighting_group.fraction_of_units_in_location
    end
    ltg_fracs.each do |location, sum|
      next if sum <= 1

      fail "Sum of fractions of #{location} lighting (#{sum}) is greater than 1."
    end

    # Check for HVAC systems referenced by multiple water heating systems
    (@heating_systems + @cooling_systems + @heat_pumps).each do |hvac_system|
      num_attached = 0
      @water_heating_systems.each do |water_heating_system|
        next if water_heating_system.related_hvac_idref.nil?
        next unless hvac_system.id == water_heating_system.related_hvac_idref

        num_attached += 1
      end
      next if num_attached <= 1

      errors << "RelatedHVACSystem '#{hvac_system.id}' is attached to multiple water heating systems."
    end

    # Check for the sum of CFA served by distribution systems <= CFA
    air_distributions = @hvac_distributions.select { |dist| dist if [HPXML::HVACDistributionTypeAir, HPXML::HVACDistributionTypeHydronicAndAir].include? dist.distribution_system_type }
    heating_dist = []
    cooling_dist = []
    air_distributions.each do |dist|
      heating_systems = dist.hvac_systems.select { |sys| sys if (sys.respond_to? :fraction_heat_load_served) && (sys.fraction_heat_load_served.to_f > 0) }
      cooling_systems = dist.hvac_systems.select { |sys| sys if (sys.respond_to? :fraction_cool_load_served) && (sys.fraction_cool_load_served.to_f > 0) }
      if heating_systems.size > 0
        heating_dist << dist
      end
      if cooling_systems.size > 0
        cooling_dist << dist
      end
    end
    heating_total_dist_cfa_served = heating_dist.map { |htg_dist| htg_dist.conditioned_floor_area_served.to_f }.sum(0.0)
    cooling_total_dist_cfa_served = cooling_dist.map { |clg_dist| clg_dist.conditioned_floor_area_served.to_f }.sum(0.0)
    if (heating_total_dist_cfa_served > @building_construction.conditioned_floor_area.to_f)
      errors << 'The total conditioned floor area served by the HVAC distribution system(s) for heating is larger than the conditioned floor area of the building.'
    end
    if (cooling_total_dist_cfa_served > @building_construction.conditioned_floor_area.to_f)
      errors << 'The total conditioned floor area served by the HVAC distribution system(s) for cooling is larger than the conditioned floor area of the building.'
    end

    # Check for objects referencing SFA/MF spaces where the building type is not SFA/MF
    if [ResidentialTypeSFD, ResidentialTypeManufactured].include? @building_construction.residential_facility_type
      mf_spaces = [LocationOtherHeatedSpace, LocationOtherHousingUnit, LocationOtherMultifamilyBufferSpace, LocationOtherNonFreezingSpace]
      (@roofs + @rim_joists + @walls + @foundation_walls + @frame_floors + @slabs).each do |surface|
        if mf_spaces.include? surface.interior_adjacent_to
          errors << "The building is of type '#{@building_construction.residential_facility_type}' but the surface '#{surface.id}' is adjacent to Attached/Multifamily space '#{surface.interior_adjacent_to}'."
        end
        if mf_spaces.include? surface.exterior_adjacent_to
          errors << "The building is of type '#{@building_construction.residential_facility_type}' but the surface '#{surface.id}' is adjacent to Attached/Multifamily space '#{surface.exterior_adjacent_to}'."
        end
      end
      (@water_heating_systems + @clothes_washers + @clothes_dryers + @dishwashers + @refrigerators + @cooking_ranges).each do |object|
        if mf_spaces.include? object.location
          errors << "The building is of type '#{@building_construction.residential_facility_type}' but the object '#{object.id}' is located in Attached/Multifamily space '#{object.location}'."
        end
      end
      @hvac_distributions.each do |hvac_distribution|
        hvac_distribution.ducts.each do |duct|
          if mf_spaces.include? duct.duct_location
            errors << "The building is of type '#{@building_construction.residential_facility_type}' but the HVAC distribution '#{hvac_distribution.id}' has a duct located in Attached/Multifamily space '#{duct.duct_location}'."
          end
        end
      end
    end

    # ------------------------------- #
    # Check for errors within objects #
    # ------------------------------- #

    # Ask objects to check for errors
    self.class::HPXML_ATTRS.each do |attribute|
      hpxml_obj = send(attribute)
      if not hpxml_obj.respond_to? :check_for_errors
        fail "Need to add 'check_for_errors' method to #{hpxml_obj.class} class."
      end

      errors += hpxml_obj.check_for_errors
    end

    return errors
  end

  def self.conditioned_locations
    return [HPXML::LocationLivingSpace,
            HPXML::LocationBasementConditioned,
            HPXML::LocationOtherHousingUnit]
  end

  def self.is_adiabatic(surface)
    if surface.exterior_adjacent_to == surface.interior_adjacent_to
      # E.g., wall between unit crawlspace and neighboring unit crawlspace
      return true
    elsif conditioned_locations.include?(surface.interior_adjacent_to) &&
          conditioned_locations.include?(surface.exterior_adjacent_to)
      # E.g., frame floor between living space and conditioned basement, or
      # wall between living space and "other housing unit"
      return true
    end
    return false
  end

  def self.is_thermal_boundary(surface)
    # Returns true if the surface is between conditioned space and outside/ground/unconditioned space.
    # Note: The location of insulation is not considered here, so an insulated foundation wall of an
    # unconditioned basement, for example, returns false.
    interior_conditioned = conditioned_locations.include? surface.interior_adjacent_to
    exterior_conditioned = conditioned_locations.include? surface.exterior_adjacent_to
    return (interior_conditioned != exterior_conditioned)
  end

  def self.get_id(parent, element_name = 'SystemIdentifier')
    return XMLHelper.get_attribute_value(XMLHelper.get_element(parent, element_name), 'id')
  end

  def self.get_idref(element)
    return XMLHelper.get_attribute_value(element, 'idref')
  end
end

def to_float(value)
  begin
    return Float(value)
  rescue
    fail "Cannot convert '#{value}' to float."
  end
end

def to_integer(value)
  begin
    value = Float(value)
  rescue
    fail "Cannot convert '#{value}' to integer."
  end
  if value % 1 == 0
    return Integer(value)
  else
    fail "Cannot convert '#{value}' to integer."
  end
end

def to_boolean(value)
  if value.is_a? TrueClass
    return true
  elsif value.is_a? FalseClass
    return false
  elsif (value.downcase.to_s == 'true') || (value == '1') || (value == 1)
    return true
  elsif (value.downcase.to_s == 'false') || (value == '0') || (value == 0)
    return false
  end

  fail "Cannot convert '#{value}' to boolean."
end

def to_float_or_nil(value)
  return if value.nil?

  return to_float(value)
end

def to_integer_or_nil(value)
  return if value.nil?

  return to_integer(value)
end

def to_boolean_or_nil(value)
  return if value.nil?

  return to_boolean(value)
end