예제 #1
0
파일: code.py 프로젝트: NRCan/energuide_api
    def from_data(cls, wall_code: element.Element) -> 'WallCode':
        structure_type_english = wall_code.findtext(
            'Layers/StructureType/English')
        structure_type_french = wall_code.findtext(
            'Layers/StructureType/French')

        component_type_size_english = wall_code.findtext(
            'Layers/ComponentTypeSize/English')
        component_type_size_french = wall_code.findtext(
            'Layers/ComponentTypeSize/French')

        try:
            return WallCode(identifier=wall_code.get('@id', str),
                            label=wall_code.get_text('Label'),
                            tags={
                                WallCodeTag.STRUCTURE_TYPE:
                                bilingual.Bilingual(
                                    english=structure_type_english,
                                    french=structure_type_french,
                                ) if structure_type_english
                                and structure_type_french else None,
                                WallCodeTag.COMPONENT_TYPE_SIZE:
                                bilingual.Bilingual(
                                    english=component_type_size_english,
                                    french=component_type_size_french,
                                ) if component_type_size_english
                                and component_type_size_french else None,
                            })
        except ElementGetValueError as exc:
            raise InvalidEmbeddedDataTypeError(
                WallCode, 'Unable to get identifier attributes') from exc
예제 #2
0
def wall_code() -> code.WallCode:
    return code.WallCode(
        identifier='Code 1',
        label='1201101121',
        tags={
            code.WallCodeTag.STRUCTURE_TYPE: bilingual.Bilingual(
                english='Wood frame',
                french='Ossature de bois',
            ),
            code.WallCodeTag.COMPONENT_TYPE_SIZE: bilingual.Bilingual(
                english='38x89 mm (2x4 in)',
                french='38x89 (2x4)',
            )
        },
    )
예제 #3
0
def sample() -> door.Door:
    return door.Door(
        label='Front door',
        door_type=bilingual.Bilingual(english='Solid wood', french='Bois massif'),
        door_insulation=insulation.Insulation(0.39),
        height=distance.Distance(1.9799),
        width=distance.Distance(0.8499),
    )
예제 #4
0
def sample() -> ceiling.Ceiling:
    return ceiling.Ceiling(
        label='Main attic',
        ceiling_type=bilingual.Bilingual(english='Attic/gable', french='Combles/pignon'),
        nominal_insulation=insulation.Insulation(2.864),
        effective_insulation=insulation.Insulation(2.9463),
        ceiling_area=area.Area(46.4515),
        ceiling_length=distance.Distance(23.875),
    )
예제 #5
0
def sample() -> heating.Heating:
    return heating.Heating(
        heating_type=heating.HeatingType.FURNACE,
        energy_source=heating.EnergySource.NATURAL_GAS,
        equipment_type=bilingual.Bilingual(english='English Type', french='French Type'),
        label='Heating/Cooling System',
        output_size=26.5,
        efficiency=95.0,
        steady_state='AFUE'
    )
예제 #6
0
def sample_window_code() -> typing.Dict[str, code.WindowCode]:
    return {
        'Code 11':
        code.WindowCode(identifier='Code 11',
                        label='202002',
                        tags={
                            code.WindowCodeTag.GLAZING_TYPE:
                            bilingual.Bilingual(
                                english='Double/double with 1 coat',
                                french='Double/double, 1 couche',
                            ),
                            code.WindowCodeTag.COATING_TINTS:
                            bilingual.Bilingual(english='Clear',
                                                french='Transparent'),
                            code.WindowCodeTag.FILL_TYPE:
                            bilingual.Bilingual(english='6 mm Air',
                                                french="6 mm d'air"),
                            code.WindowCodeTag.SPACER_TYPE:
                            bilingual.Bilingual(english='Metal',
                                                french='Métal'),
                            code.WindowCodeTag.CODE_TYPE:
                            bilingual.Bilingual(english='Picture',
                                                french='Fixe'),
                            code.WindowCodeTag.FRAME_MATERIAL:
                            bilingual.Bilingual(english='Wood', french='Bois'),
                        })
    }
예제 #7
0
파일: door.py 프로젝트: NRCan/energuide_api
 def from_data(cls, door: element.Element) -> 'Door':
     try:
         return Door(
             label=door.get_text('Label'),
             door_type=bilingual.Bilingual(
                 english=door.get_text('Construction/Type/English'),
                 french=door.get_text('Construction/Type/French'),
             ),
             door_insulation=insulation.Insulation(
                 door.get('Construction/Type/@value', float)),
             height=distance.Distance(
                 door.get('Measurements/@height', float)),
             width=distance.Distance(door.get('Measurements/@width',
                                              float)),
         )
     except (ElementGetValueError) as exc:
         raise InvalidEmbeddedDataTypeError(Door) from exc
예제 #8
0
 def from_data(cls, ceiling: element.Element) -> 'Ceiling':
     try:
         return Ceiling(
             label=ceiling.get_text('Label'),
             ceiling_type=bilingual.Bilingual(
                 english=ceiling.get_text('Construction/Type/English'),
                 french=ceiling.get_text('Construction/Type/French'),
             ),
             nominal_insulation=insulation.Insulation(
                 ceiling.get('Construction/CeilingType/@nominalInsulation',
                             float)),
             effective_insulation=insulation.Insulation(
                 ceiling.get('Construction/CeilingType/@rValue', float)),
             ceiling_area=area.Area(ceiling.get('Measurements/@area',
                                                float)),
             ceiling_length=distance.Distance(
                 ceiling.get('Measurements/@length', float)),
         )
     except (ElementGetValueError) as exc:
         raise InvalidEmbeddedDataTypeError(Ceiling) from exc
예제 #9
0
class BasementFloor(_BasementFloor):

    _FLOOR_TYPE_TRANSLATION = {
        FloorType.SLAB: bilingual.Bilingual(
            english='Slab',
            french='Dalle',
        ),
        FloorType.FLOOR_ABOVE_CRAWLSPACE: bilingual.Bilingual(
            english='Floor above crawl space',
            french='Plancher au-dessus du vide sanitaire',
        ),
    }

    @classmethod
    def _empty_floor(cls, floor_type: FloorType) -> 'BasementFloor':
        return BasementFloor(
            floor_type=floor_type,
            rectangular=False,
            nominal_insulation=None,
            effective_insulation=None,
            length=None,
            width=None,
            perimeter=distance.Distance(0.0),
            floor_area=area.Area(0.0),
        )

    @classmethod
    def _from_data(cls,
                   floor: element.Element,
                   construction_type: str,
                   floor_type: FloorType) -> 'BasementFloor':

        length: typing.Optional[float] = None
        width: typing.Optional[float] = None

        try:
            rectangular = floor.get('Measurements/@isRectangular', str) == 'true'
            if rectangular:
                length = floor.get('Measurements/@length', float)
                width = floor.get('Measurements/@width', float)
                perimeter = (2 * length) + (2 * width)
                floor_area = length * width
            else:
                floor_area = floor.get('Measurements/@area', float)
                perimeter = floor.get('Measurements/@perimeter', float)

            nominal_insulation_node = floor.xpath(f'Construction/{construction_type}/@nominalInsulation')
            effective_insulation_node = floor.xpath(f'Construction/{construction_type}/@rValue')

            nominal_insulation = float(nominal_insulation_node[0]) if nominal_insulation_node else None

            effective_insulation = float(effective_insulation_node[0]) if effective_insulation_node else None
        except ValueError as exc:
            raise InvalidEmbeddedDataTypeError(BasementFloor, 'Invalid insulation attribute values') from exc
        except ElementGetValueError as exc:
            raise InvalidEmbeddedDataTypeError(BasementFloor, 'Invalid attributes') from exc

        return BasementFloor(
            floor_type=floor_type,
            rectangular=rectangular,
            nominal_insulation=insulation.Insulation(nominal_insulation)
            if nominal_insulation is not None else None,

            effective_insulation=insulation.Insulation(effective_insulation)
            if effective_insulation is not None else None,

            length=distance.Distance(length) if length is not None else None,
            width=distance.Distance(width) if width is not None else None,
            perimeter=distance.Distance(perimeter),
            floor_area=area.Area(floor_area),
        )


    @classmethod
    def from_basement(cls, floor: typing.Optional[element.Element]) -> typing.List['BasementFloor']:
        return [
            cls._from_data(floor, 'AddedToSlab', FloorType.SLAB)
            if floor is not None else cls._empty_floor(FloorType.SLAB)
        ]

    @classmethod
    def from_crawlspace(cls, floor: typing.Optional[element.Element]) -> typing.List['BasementFloor']:
        if floor is None:
            return [cls._empty_floor(FloorType.SLAB), cls._empty_floor(FloorType.FLOOR_ABOVE_CRAWLSPACE)]

        return [
            cls._from_data(floor, 'AddedToSlab', FloorType.SLAB),
            cls._from_data(floor, 'FloorsAbove', FloorType.FLOOR_ABOVE_CRAWLSPACE),
        ]

    @classmethod
    def from_slab(cls, floor: typing.Optional[element.Element]) -> typing.List['BasementFloor']:
        return [
            cls._from_data(floor, 'AddedToSlab', FloorType.SLAB)
            if floor is not None else cls._empty_floor(FloorType.SLAB)
        ]

    def to_dict(self) -> typing.Dict[str, typing.Any]:
        floor_type = self._FLOOR_TYPE_TRANSLATION.get(self.floor_type)

        return {
            'floorTypeEnglish': floor_type.english if floor_type is not None else None,
            'floorTypeFrench': floor_type.french if floor_type is not None else None,
            'insulationNominalRsi': self.nominal_insulation.rsi if self.nominal_insulation is not None else None,
            'insulationNominalR': self.nominal_insulation.r_value if self.nominal_insulation is not None else None,
            'insulationEffectiveRsi': self.effective_insulation.rsi if self.effective_insulation is not None else None,
            'insulationEffectiveR': self.effective_insulation.r_value
                                    if self.effective_insulation is not None else None,
            'areaMetres': self.floor_area.square_metres,
            'areaFeet': self.floor_area.square_feet,
            'perimeterMetres': self.perimeter.metres,
            'perimeterFeet': self.perimeter.feet,
            'widthMetres': self.width.metres if self.width is not None else None,
            'widthFeet': self.width.feet if self.width is not None else None,
            'lengthMetres': self.length.metres if self.length is not None else None,
            'lengthFeet': self.length.feet if self.length is not None else None,
        }
예제 #10
0
class Heating(_Heating):

    _KWH_TO_BTU = 3412.142

    _HEATING_TYPE_NODE_NAMES = {
        'Baseboards': HeatingType.BASEBOARD,
        'Furnace': HeatingType.FURNACE,
        'Boiler': HeatingType.BOILER,
        'ComboHeatDhw': HeatingType.COMBO_HEAT_DHW
    }

    _HEATING_TYPE_TRANSLATIONS = {
        HeatingType.BASEBOARD:
        bilingual.Bilingual(english='Electric baseboard',
                            french='Plinthe électrique'),
        HeatingType.FURNACE:
        bilingual.Bilingual(english='Furnace', french='Fournaise'),
        HeatingType.BOILER:
        bilingual.Bilingual(english='Boiler', french='Chaudière'),
        HeatingType.COMBO_HEAT_DHW:
        bilingual.Bilingual(
            english='Combo Heat/DHW',
            french='Combinaison chaleur/ Eau Chaude Domestique'),
        HeatingType.P911:
        bilingual.Bilingual(
            english='Certified combo system, space and domestic water heating',
            french='Système combiné certifié pour le chauffage '
            'des locaux et de l’eau'),
    }

    _ENERGY_SOURCE_CODES = {
        1: EnergySource.ELECTRIC,
        2: EnergySource.NATURAL_GAS,
        3: EnergySource.OIL,
        4: EnergySource.PROPANE,
        5: EnergySource.WOOD,
        6: EnergySource.WOOD,
        7: EnergySource.WOOD,
        8: EnergySource.WOOD,
    }

    _ENERGY_SOURCE_TRANSLATIONS = {
        EnergySource.ELECTRIC:
        bilingual.Bilingual(english='Electric Space Heating',
                            french='Chauffage électrique'),
        EnergySource.NATURAL_GAS:
        bilingual.Bilingual(english='Natural Gas',
                            french='Chauffage au gaz naturel'),
        EnergySource.OIL:
        bilingual.Bilingual(english='Oil Space Heating',
                            french='Chauffage au mazout'),
        EnergySource.PROPANE:
        bilingual.Bilingual(english='Propane Space Heating',
                            french='Chauffage au propane'),
        EnergySource.WOOD:
        bilingual.Bilingual(
            english='Wood Space Heating (Mixed Wood, Hardwood, '
            'Soft Wood or Wood Pellets)',
            french='Chauffage au bois(Bois mélangé, Bois dur, Bois mou, '
            'Granules de bois)'),
    }

    @classmethod
    def _get_output_size(cls, node: element.Element) -> float:
        capacity_node = node.find('Type1/*/Specifications/OutputCapacity')
        assert capacity_node is not None

        try:
            units = capacity_node.get('@uiUnits', str)
            capacity_value = capacity_node.get('@value', float)
        except ElementGetValueError as exc:
            raise InvalidEmbeddedDataTypeError(
                Heating, 'Invalid/missing attribute values') from exc

        capacity: typing.Optional[float]
        if units == 'kW':
            capacity = capacity_value
        elif units == 'btu/hr' or units == 'btu/h':
            capacity = capacity_value / cls._KWH_TO_BTU
        else:
            raise InvalidEmbeddedDataTypeError(
                Heating, f'Unknown capacity units: {units}')
        assert capacity is not None
        return capacity

    @classmethod
    def _get_heating_type(cls, node: element.Element) -> HeatingType:
        candidates = [candidate.tag for candidate in node.xpath('Type1/*')]
        heating_type: typing.Optional[HeatingType] = None
        for candidate in candidates:
            if candidate in cls._HEATING_TYPE_NODE_NAMES:
                heating_type = cls._HEATING_TYPE_NODE_NAMES[candidate]
                break
        if heating_type is None:
            raise InvalidEmbeddedDataTypeError(
                Heating,
                f'Could not identify HeatingType, candidate node names = {[candidates]}'
            )
        return heating_type

    @classmethod
    def _get_energy_source(cls, node: element.Element) -> EnergySource:
        try:
            code = node.get('Type1/*/Equipment/EnergySource/@code', int)
        except ElementGetValueError as exc:
            raise InvalidEmbeddedDataTypeError(
                Heating, 'No EnergySource heating code') from exc

        energy_source = cls._ENERGY_SOURCE_CODES.get(code)
        if energy_source is None:
            raise InvalidEmbeddedDataTypeError(
                Heating, f'Unknown energy source: {energy_source}')
        return energy_source

    @classmethod
    def _get_equipment_type(cls, node: element.Element) -> bilingual.Bilingual:
        english_text = node.get_text('Type1/*/Equipment/EquipmentType/English')
        french_text = node.get_text('Type1/*/Equipment/EquipmentType/French')
        return bilingual.Bilingual(english=english_text, french=french_text)

    @staticmethod
    def _get_steady_state(node: element.Element) -> str:
        try:
            steady_state_value = node.get(
                'Type1/*/Specifications/@isSteadyState', str)
        except ElementGetValueError as exc:
            raise InvalidEmbeddedDataTypeError(
                Heating, 'No isSteadyState property value') from exc

        return 'Steady State' if steady_state_value == 'true' else 'AFUE'

    @classmethod
    def from_data(cls, node: element.Element) -> 'Heating':
        try:
            label = node.get_text('Label')
            efficiency = node.get('Type1/*/Specifications/@efficiency', float)
        except ElementGetValueError as exc:
            raise InvalidEmbeddedDataTypeError(
                Heating, 'Invalid/missing Heating values') from exc

        heating_type = cls._get_heating_type(node)
        output_size = cls._get_output_size(node)

        if heating_type is HeatingType.BASEBOARD:
            steady_state = 'Steady State'
            energy_source = EnergySource.ELECTRIC
            equipment_type = cls._HEATING_TYPE_TRANSLATIONS[
                HeatingType.BASEBOARD]
        else:
            steady_state = cls._get_steady_state(node)
            energy_source = cls._get_energy_source(node)
            equipment_type = cls._get_equipment_type(node)

        return Heating(
            label=label,
            output_size=output_size,
            efficiency=efficiency,
            steady_state=steady_state,
            heating_type=heating_type,
            energy_source=energy_source,
            equipment_type=equipment_type,
        )

    def to_dict(self) -> typing.Dict[str, typing.Any]:
        return {
            'label':
            self.label,
            'heatingTypeEnglish':
            self._HEATING_TYPE_TRANSLATIONS[self.heating_type].english,
            'heatingTypeFrench':
            self._HEATING_TYPE_TRANSLATIONS[self.heating_type].french,
            'energySourceEnglish':
            self._ENERGY_SOURCE_TRANSLATIONS[self.energy_source].english,
            'energySourceFrench':
            self._ENERGY_SOURCE_TRANSLATIONS[self.energy_source].french,
            'equipmentTypeEnglish':
            self.equipment_type.english,
            'equipmentTypeFrench':
            self.equipment_type.french,
            'outputSizeKW':
            self.output_size,
            'outputSizeBtu':
            self.output_size * self._KWH_TO_BTU,
            'efficiency':
            self.efficiency,
            'steadyState':
            self.steady_state,
        }
예제 #11
0
 def _get_equipment_type(cls, node: element.Element) -> bilingual.Bilingual:
     english_text = node.get_text('Type1/*/Equipment/EquipmentType/English')
     french_text = node.get_text('Type1/*/Equipment/EquipmentType/French')
     return bilingual.Bilingual(english=english_text, french=french_text)
예제 #12
0
파일: code.py 프로젝트: NRCan/energuide_api
    def from_data(cls, window_code: element.Element) -> 'WindowCode':

        glazing_type_english = window_code.findtext(
            'Layers/GlazingTypes/English')
        glazing_type_french = window_code.findtext(
            'Layers/GlazingTypes/French')

        coating_tint_english = window_code.findtext(
            'Layers/CoatingsTints/English')
        coating_tint_french = window_code.findtext(
            'Layers/CoatingsTints/French')

        fill_type_english = window_code.findtext('Layers/FillType/English')
        fill_type_french = window_code.findtext('Layers/FillType/French')

        spacer_type_english = window_code.findtext('Layers/SpacerType/English')
        spacer_type_french = window_code.findtext('Layers/SpacerType/French')

        window_code_type_english = window_code.findtext('Layers/Type/English')
        window_code_type_french = window_code.findtext('Layers/Type/French')

        frame_material_english = window_code.findtext(
            'Layers/FrameMaterial/English')
        frame_material_french = window_code.findtext(
            'Layers/FrameMaterial/French')

        try:
            return WindowCode(
                identifier=window_code.get('@id', str),
                label=window_code.get_text('Label'),
                tags={
                    WindowCodeTag.GLAZING_TYPE:
                    bilingual.Bilingual(
                        english=glazing_type_english,
                        french=glazing_type_french,
                    )
                    if glazing_type_english and glazing_type_french else None,
                    WindowCodeTag.COATING_TINTS:
                    bilingual.Bilingual(
                        english=coating_tint_english,
                        french=coating_tint_french,
                    )
                    if coating_tint_english and coating_tint_french else None,
                    WindowCodeTag.FILL_TYPE:
                    bilingual.Bilingual(
                        english=fill_type_english,
                        french=fill_type_french,
                    ) if fill_type_english and fill_type_french else None,
                    WindowCodeTag.SPACER_TYPE:
                    bilingual.Bilingual(
                        english=spacer_type_english,
                        french=spacer_type_french,
                    ) if spacer_type_english and spacer_type_french else None,
                    WindowCodeTag.CODE_TYPE:
                    bilingual.Bilingual(
                        english=window_code_type_english,
                        french=window_code_type_french,
                    ) if window_code_type_english and window_code_type_french
                    else None,
                    WindowCodeTag.FRAME_MATERIAL:
                    bilingual.Bilingual(
                        english=frame_material_english,
                        french=frame_material_french,
                    ) if frame_material_english and frame_material_french else
                    None,
                })
        except ElementGetValueError as exc:
            raise InvalidEmbeddedDataTypeError(
                WindowCode, 'Unable to get identifier attributes') from exc
예제 #13
0
class Ventilation(_Ventilation):

    _CFM_MULTIPLIER = 2.11888

    _VENTILATION_TRANSLATIONS = {
        VentilationType.NOT_APPLICABLE:
        bilingual.Bilingual(english='N/A', french='N/A'),
        VentilationType.ENERGY_STAR_INSTITUTE_CERTIFIED:
        bilingual.Bilingual(
            english=
            'Home Ventilating Institute listed ENERGY STAR certified heat recovery ventilator',
            french='Ventilateur-récupérateur de chaleur répertorié par le '
            'Home Ventilating Institute et certifié ENERGY STAR',
        ),
        VentilationType.ENERGY_STAR_NOT_INSTITUTE_CERTIFIED:
        bilingual.Bilingual(
            english='ENERGY STAR certified heat recovery ventilator',
            french='Ventilateur-récupérateur de chaleur certifié ENERGY STAR',
        ),
        VentilationType.NOT_ENERGY_STAR_INSTITUTE_CERTIFIED:
        bilingual.Bilingual(
            english=
            'Heat recovery ventilator certified by the Home Ventilating Institute',
            french=
            'Ventilateur-récupérateur de chaleur certifié par le Home Ventilating Institute',
        ),
        VentilationType.NOT_ENERGY_STAR_NOT_INSTITUTE_CERTIFIED:
        bilingual.Bilingual(
            english='Heat recovery ventilator',
            french='Ventilateur-récupérateur de chaleur',
        ),
    }

    @staticmethod
    def _derive_ventilation_type(total_supply_flow: float, energy_star: bool,
                                 institute_certified: bool) -> VentilationType:
        if total_supply_flow == 0:
            return VentilationType.NOT_APPLICABLE
        elif energy_star and institute_certified:
            return VentilationType.ENERGY_STAR_INSTITUTE_CERTIFIED
        elif energy_star and not institute_certified:
            return VentilationType.ENERGY_STAR_NOT_INSTITUTE_CERTIFIED
        elif not energy_star and institute_certified:
            return VentilationType.NOT_ENERGY_STAR_INSTITUTE_CERTIFIED
        return VentilationType.NOT_ENERGY_STAR_NOT_INSTITUTE_CERTIFIED

    @classmethod
    def from_data(cls, ventilation: element.Element) -> 'Ventilation':
        try:
            energy_star = ventilation.attrib['isEnergyStar'] == 'true'
            institute_certified = ventilation.attrib[
                'isHomeVentilatingInstituteCertified'] == 'true'
            total_supply_flow = float(ventilation.attrib['supplyFlowrate'])

            ventilation_type = cls._derive_ventilation_type(
                total_supply_flow, energy_star, institute_certified)

            return Ventilation(
                ventilation_type=ventilation_type,
                air_flow_rate=total_supply_flow,
                efficiency=float(ventilation.attrib['efficiency1']),
            )
        except (KeyError, ValueError) as exc:
            raise InvalidEmbeddedDataTypeError(Ventilation) from exc

    @property
    def air_flow_rate_cmf(self):
        return self.air_flow_rate * self._CFM_MULTIPLIER

    def to_dict(self) -> typing.Dict[str, typing.Union[str, float]]:
        ventilation_translation = self._VENTILATION_TRANSLATIONS[
            self.ventilation_type]
        return {
            'typeEnglish': ventilation_translation.english,
            'typeFrench': ventilation_translation.french,
            'airFlowRateLps': self.air_flow_rate,
            'airFlowRateCfm': self.air_flow_rate_cmf,
            'efficiency': self.efficiency,
        }
예제 #14
0
    def test_from_row(self, sample_input_d: typing.Dict[str,
                                                        typing.Any]) -> None:
        output = dwelling.ParsedDwellingDataRow.from_row(sample_input_d)

        wall_code = code.WallCode(
            identifier='Code 1',
            label='1201101121',
            tags={
                code.WallCodeTag.STRUCTURE_TYPE:
                bilingual.Bilingual(
                    english='Wood frame',
                    french='Ossature de bois',
                ),
                code.WallCodeTag.COMPONENT_TYPE_SIZE:
                bilingual.Bilingual(
                    english='38x89 mm (2x4 in)',
                    french='38x89 (2x4)',
                )
            },
        )

        window_code = code.WindowCode(
            identifier='Code 12',
            label='234002',
            tags={
                code.WindowCodeTag.GLAZING_TYPE:
                bilingual.Bilingual(
                    english='Double/double with 1 coat',
                    french='Double/double, 1 couche',
                ),
                code.WindowCodeTag.COATING_TINTS:
                bilingual.Bilingual(english='Low-E .20 (hard1)',
                                    french='Faible E .20 (Dur 1)'),
                code.WindowCodeTag.FILL_TYPE:
                bilingual.Bilingual(english='9 mm Argon',
                                    french="9 mm d'argon"),
                code.WindowCodeTag.SPACER_TYPE:
                bilingual.Bilingual(english='Metal', french='Métal'),
                code.WindowCodeTag.CODE_TYPE:
                bilingual.Bilingual(english='Picture', french='Fixe'),
                code.WindowCodeTag.FRAME_MATERIAL:
                bilingual.Bilingual(english='Wood', french='Bois'),
            })

        assert output == dwelling.ParsedDwellingDataRow(
            eval_id=123,
            eval_type=dwelling.EvaluationType.PRE_RETROFIT,
            entry_date=datetime.date(2018, 1, 1),
            creation_date=datetime.datetime(2018, 1, 8, 9),
            modification_date=datetime.datetime(2018, 6, 1, 9),
            year_built=2000,
            city='Ottawa',
            region=dwelling.Region.ONTARIO,
            forward_sortation_area='K1P',
            ers_rating=567,
            file_id='4K13D01404',
            ceilings=[
                ceiling.Ceiling(
                    label='Main attic',
                    ceiling_type=bilingual.Bilingual(english='Attic/gable',
                                                     french='Combles/pignon'),
                    nominal_insulation=insulation.Insulation(2.864),
                    effective_insulation=insulation.Insulation(2.9463),
                    ceiling_area=area.Area(46.4515),
                    ceiling_length=distance.Distance(23.875),
                )
            ],
            floors=[
                floor.Floor(
                    label='Rm over garage',
                    nominal_insulation=insulation.Insulation(2.46),
                    effective_insulation=insulation.Insulation(2.9181),
                    floor_area=area.Area(9.2903),
                    floor_length=distance.Distance(3.048),
                )
            ],
            walls=[
                wall.Wall(
                    label='Second level',
                    wall_code=wall_code,
                    nominal_insulation=insulation.Insulation(1.432),
                    effective_insulation=insulation.Insulation(1.8016),
                    perimeter=distance.Distance(42.9768),
                    height=distance.Distance(2.4384),
                )
            ],
            doors=[
                door.Door(
                    label='Front door',
                    door_type=bilingual.Bilingual(english='Solid wood',
                                                  french='Bois massif'),
                    door_insulation=insulation.Insulation(0.39),
                    height=distance.Distance(1.9799),
                    width=distance.Distance(0.8499),
                )
            ],
            windows=[
                window.Window(
                    label='East0001',
                    window_code=window_code,
                    window_insulation=insulation.Insulation(0.4779),
                    width=distance.Distance(1.967738),
                    height=distance.Distance(1.3220699),
                )
            ],
            heated_floor=heated_floor_area.HeatedFloorArea(
                area_above_grade=area.Area(92.9),
                area_below_grade=area.Area(185.8),
            ),
            ventilations=[
                ventilation.Ventilation(
                    ventilation_type=ventilation.VentilationType.
                    NOT_ENERGY_STAR_NOT_INSTITUTE_CERTIFIED,
                    air_flow_rate=220.0,
                    efficiency=55.0,
                )
            ],
            water_heatings=[
                water_heating.WaterHeating(
                    water_heater_type=water_heating.WaterHeaterType.
                    ELECTRICITY_CONVENTIONAL_TANK,
                    tank_volume=189.3001,
                    efficiency_ef=0.8217,
                    efficiency_percentage=None,
                    drain_water_heat_recovery_efficiency_percentage=None,
                )
            ],
            heating_system=heating.Heating(
                heating_type=heating.HeatingType.FURNACE,
                energy_source=heating.EnergySource.NATURAL_GAS,
                equipment_type=bilingual.Bilingual(
                    english='Furnace w/ continuous pilot',
                    french='Fournaise avec veilleuse permanente'),
                label='Heating/Cooling System',
                output_size=0.009671344275824395,
                efficiency=78.0,
                steady_state='Steady State',
            ),
            foundations=[
                basement.Basement(
                    foundation_type=basement.FoundationType.BASEMENT,
                    label='Basement',
                    configuration_type='BCCB',
                    walls=[
                        basement.BasementWall(
                            wall_type=basement.WallType.INTERIOR,
                            nominal_insulation=insulation.Insulation(
                                rsi=1.432),
                            effective_insulation=insulation.Insulation(
                                rsi=1.4603),
                            composite_percentage=100.0,
                            wall_area=area.Area(97.36458048))
                    ],
                    floors=[
                        basement.BasementFloor(
                            floor_type=basement.FloorType.SLAB,
                            rectangular=False,
                            nominal_insulation=insulation.Insulation(rsi=0.0),
                            effective_insulation=insulation.Insulation(
                                rsi=0.0),
                            length=None,
                            width=None,
                            perimeter=distance.Distance(
                                distance_metres=39.9297),
                            floor_area=area.Area(92.903))
                    ],
                    header=basement.BasementHeader(
                        nominal_insulation=insulation.Insulation(rsi=3.87),
                        effective_insulation=insulation.Insulation(rsi=4.0777),
                        height=distance.Distance(distance_metres=0.23),
                        perimeter=distance.Distance(distance_metres=39.9288)),
                ),
                basement.Basement(
                    foundation_type=basement.FoundationType.CRAWLSPACE,
                    label='Crawl',
                    configuration_type='SCN',
                    walls=[
                        basement.BasementWall(
                            wall_type=basement.WallType.NOT_APPLICABLE,
                            nominal_insulation=insulation.Insulation(
                                rsi=1.432),
                            effective_insulation=insulation.Insulation(
                                rsi=1.7968),
                            composite_percentage=100.0,
                            wall_area=area.Area(21.333012959999998))
                    ],
                    floors=[
                        basement.BasementFloor(
                            floor_type=basement.FloorType.SLAB,
                            rectangular=True,
                            nominal_insulation=None,
                            effective_insulation=None,
                            width=distance.Distance(distance_metres=4.9987),
                            length=distance.Distance(distance_metres=4.9999),
                            perimeter=distance.Distance(
                                distance_metres=19.9972),
                            floor_area=area.Area(24.993000130000002)),
                        basement.BasementFloor(
                            floor_type=basement.FloorType.
                            FLOOR_ABOVE_CRAWLSPACE,
                            rectangular=True,
                            nominal_insulation=insulation.Insulation(rsi=0.0),
                            effective_insulation=insulation.Insulation(
                                rsi=0.468),
                            width=distance.Distance(distance_metres=4.9987),
                            length=distance.Distance(distance_metres=4.9999),
                            perimeter=distance.Distance(
                                distance_metres=19.9972),
                            floor_area=area.Area(24.993000130000002))
                    ],
                    header=None,
                ),
                basement.Basement(
                    foundation_type=basement.FoundationType.SLAB,
                    label='Slab',
                    configuration_type='SCN',
                    walls=[],
                    floors=[
                        basement.BasementFloor(
                            floor_type=basement.FloorType.SLAB,
                            rectangular=True,
                            nominal_insulation=None,
                            effective_insulation=None,
                            width=distance.Distance(distance_metres=3.048),
                            length=distance.Distance(distance_metres=6.096),
                            perimeter=distance.Distance(
                                distance_metres=18.288),
                            floor_area=area.Area(18.580608))
                    ],
                    header=None,
                ),
            ],
            energy_upgrades=[
                upgrade.Upgrade(
                    upgrade_type='Ceilings',
                    cost=0,
                    priority=12,
                ),
                upgrade.Upgrade(
                    upgrade_type='MainWalls',
                    cost=1,
                    priority=2,
                ),
                upgrade.Upgrade(
                    upgrade_type='Foundation',
                    cost=2,
                    priority=3,
                ),
            ])
예제 #15
0
def test_translation() -> None:
    output = bilingual.Bilingual(english='english text', french='french text')
    assert output.english == 'english text'
    assert output.french == 'french text'
예제 #16
0
class BasementWall(_BasementWall):

    _WALL_TYPE_TRANSLATION = {
        WallType.INTERIOR: bilingual.Bilingual(
            english='Interior',
            french='Intérieur',
        ),
        WallType.EXTERIOR: bilingual.Bilingual(
            english='Exterior',
            french='Extérieur',
        ),
        WallType.PONY: bilingual.Bilingual(
            english='Pony Wall',
            french='Murs bas',
        ),
        WallType.NOT_APPLICABLE: bilingual.Bilingual(
            english='Wall',
            french='Mur',
        ),
    }

    @classmethod
    def _from_data(cls,
                   wall: element.Element,
                   wall_perimeter: float,
                   wall_height: float,
                   tag: WallType,
                   backup_percentage: float) -> 'BasementWall':

        maybe_percentage = wall.attrib.get('percentage')
        percentage = float(maybe_percentage) if maybe_percentage else backup_percentage

        try:
            nominal_insulation = wall.get('@nominalRsi', float)
            effective_insulation = wall.get('@rsi', float)
        except ElementGetValueError as exc:
            raise InvalidEmbeddedDataTypeError(BasementWall, 'Invalid insulation attributes') from exc

        return BasementWall(
            wall_type=tag,
            nominal_insulation=insulation.Insulation(nominal_insulation),
            effective_insulation=insulation.Insulation(effective_insulation),
            composite_percentage=percentage,
            wall_area=area.Area(wall_perimeter * wall_height * (percentage / 100))
        )


    @classmethod
    def from_basement(cls, wall: element.Element, wall_perimeter: float) -> typing.List['BasementWall']:
        interior_wall_sections = wall.xpath('Construction/InteriorAddedInsulation/Composite/Section')
        exterior_wall_sections = wall.xpath('Construction/ExteriorAddedInsulation/Composite/Section')
        pony_wall_sections = wall.xpath('Construction/PonyWallType/Composite/Section')

        try:
            wall_height = wall.get('Measurements/@height', float)
            pony_height = wall.get('Measurements/@ponyWallHeight', float)
        except ElementGetValueError as exc:
            raise InvalidEmbeddedDataTypeError(BasementWall, 'Missing/invalid basement wall height') from exc

        walls = []
        sections = (interior_wall_sections, exterior_wall_sections, pony_wall_sections)

        parsers = (
            lambda section, percentage: BasementWall._from_data(
                section,
                wall_perimeter,
                wall_height,
                WallType.INTERIOR,
                percentage
            ),
            lambda section, percentage: BasementWall._from_data(
                section,
                wall_perimeter,
                wall_height,
                WallType.EXTERIOR,
                percentage
            ),
            lambda section, percentage: BasementWall._from_data(
                section,
                wall_perimeter,
                pony_height,
                WallType.PONY,
                percentage
            )
        )

        for parser, wall_sections in zip(parsers, sections):
            percentages = [wall.attrib.get('percentage') for wall in wall_sections]
            accounted_for = sum(float(percentage) for percentage in percentages if percentage is not None)

            walls.extend([parser(wall, 100-accounted_for) for wall in wall_sections])

        return walls

    @classmethod
    def from_crawlspace(cls, wall: element.Element, wall_perimeter: float) -> typing.List['BasementWall']:
        wall_sections = wall.xpath('Construction/Type/Composite/Section')

        try:
            wall_height = wall.get('Measurements/@height', float)
        except ElementGetValueError as exc:
            raise InvalidEmbeddedDataTypeError(BasementWall, 'Missing/invalid wall height') from exc

        percentages = [wall.attrib.get('percentage') for wall in wall_sections]
        accounted_for = sum(float(percentage) for percentage in percentages if percentage is not None)

        return [
            BasementWall._from_data(
                wall_section,
                wall_perimeter,
                wall_height,
                WallType.NOT_APPLICABLE,
                100-accounted_for
            )
            for wall_section in wall_sections
        ]

    def to_dict(self) -> typing.Dict[str, typing.Any]:
        wall_type = self._WALL_TYPE_TRANSLATION.get(self.wall_type)
        return {
            'wallTypeEnglish': wall_type.english if wall_type is not None else None,
            'wallTypeFrench': wall_type.french if wall_type is not None else None,
            'insulationNominalRsi': self.nominal_insulation.rsi,
            'insulationNominalR': self.nominal_insulation.r_value,
            'insulationEffectiveRsi': self.effective_insulation.rsi,
            'insulationEffectiveR': self.effective_insulation.r_value,
            'percentage': self.composite_percentage,
            'areaMetres': self.wall_area.square_metres,
            'areaFeet': self.wall_area.square_feet,
        }
예제 #17
0
class Basement(_Basement):

    _MATERIAL_TRANSLATIONS = {
        MaterialType.UNKNOWN: bilingual.Bilingual(english='', french=''),
        MaterialType.WOOD: bilingual.Bilingual(
            english='wood',
            french='bois',
        ),
        MaterialType.CONCRETE: bilingual.Bilingual(
            english='concrete',
            french='béton',
        ),
        MaterialType.CONCRETE_AND_WOOD: bilingual.Bilingual(
            english='concrete and wood',
            french='béton et bois',
        ),
    }

    _FOUNDATION_TRANSLATIONS = {
        FoundationType.BASEMENT: bilingual.Bilingual(
            english='Basement',
            french='Sous-sol',
        ),
        FoundationType.CRAWLSPACE: bilingual.Bilingual(
            english='Crawlspace',
            french='Vide Sanitaire',
        ),
        FoundationType.SLAB: bilingual.Bilingual(
            english='Slab',
            french='Dalle',
        ),
    }

    @classmethod
    def from_data(cls, basement: element.Element) -> 'Basement':
        foundation_type = cls._derive_foundation_type(basement.tag)
        if foundation_type is FoundationType.UNKNOWN:
            raise InvalidEmbeddedDataTypeError(Basement, f'Invalid foundation type: {basement.tag}')

        if foundation_type is FoundationType.BASEMENT:
            floor_from_data = BasementFloor.from_basement
            wall_from_data = BasementWall.from_basement
            header_from_data = BasementHeader.from_data
        elif foundation_type is FoundationType.CRAWLSPACE:
            floor_from_data = BasementFloor.from_crawlspace
            wall_from_data = BasementWall.from_crawlspace
            header_from_data = BasementHeader.from_data
        else:
            floor_from_data = BasementFloor.from_slab
            wall_from_data = lambda *args: []
            header_from_data = lambda *args: None

        floor_nodes = basement.xpath('Floor')
        header_nodes = basement.xpath('Components/FloorHeader')
        wall_nodes = basement.xpath('Wall')

        floors = floor_from_data(floor_nodes[0] if floor_nodes else None)
        walls = wall_from_data(wall_nodes[0], floors[0].perimeter.metres) if wall_nodes else []
        header = header_from_data(header_nodes[0]) if header_nodes else None

        try:
            configuration_type = basement.get('Configuration/@type', str)
            label = basement.get_text('Label')
        except ElementGetValueError as exc:
            raise InvalidEmbeddedDataTypeError(Basement, 'Missing/invalid foundation attributes') from exc

        if not configuration_type:
            raise InvalidEmbeddedDataTypeError(Basement, 'Empty configuration type')

        return Basement(
            foundation_type=foundation_type,
            label=label,
            configuration_type=configuration_type,
            walls=walls,
            floors=floors,
            header=header,
        )

    @staticmethod
    def _derive_foundation_type(tag: str) -> FoundationType:
        if tag == 'Basement':
            return FoundationType.BASEMENT
        elif tag == 'Crawlspace':
            return FoundationType.CRAWLSPACE
        elif tag == 'Slab':
            return FoundationType.SLAB
        return FoundationType.UNKNOWN

    @staticmethod
    def _derive_material(configuration_type: str) -> MaterialType:
        material = configuration_type[1]
        if material == 'W':
            return MaterialType.WOOD
        elif material == 'C':
            return MaterialType.CONCRETE
        elif material == 'B':
            return MaterialType.CONCRETE_AND_WOOD
        return MaterialType.UNKNOWN

    @property
    def material(self) -> MaterialType:
        return Basement._derive_material(self.configuration_type)

    def to_dict(self) -> typing.Dict[str, typing.Any]:
        material = self._MATERIAL_TRANSLATIONS.get(self.material)
        foundation = self._FOUNDATION_TRANSLATIONS.get(self.foundation_type)
        return {
            'foundationTypeEnglish': foundation.english if foundation else None,
            'foundationTypeFrench': foundation.french if foundation else None,
            'label': self.label,
            'configurationType': self.configuration_type,
            'materialEnglish': material.english if material else None,
            'materialFrench': material.french if material else None,
            'walls': [wall.to_dict() for wall in self.walls],
            'floors': [floor.to_dict() for floor in self.floors],
            'header': self.header.to_dict() if self.header is not None else None,
        }
예제 #18
0
class WaterHeating(_WaterHeating):

    _LITRE_TO_GALLON = 0.264172

    _TYPE_MAP = {
        ("not applicable", "not applicable"):
        WaterHeaterType.NOT_APPLICABLE,
        ("electricity", "not applicable"):
        WaterHeaterType.NOT_APPLICABLE,
        ("electricity", "conventional tank"):
        WaterHeaterType.ELECTRICITY_CONVENTIONAL_TANK,
        ("electricity", "conserver tank"):
        WaterHeaterType.ELECTRICITY_CONSERVER_TANK,
        ("electricity", "instantaneous"):
        WaterHeaterType.ELECTRICITY_INSTANTANEOUS,
        ("electricity", "tankless heat pump"):
        WaterHeaterType.ELECTRICITY_TANKLESS_HEAT_PUMP,
        ("electricity", "heat pump"):
        WaterHeaterType.ELECTRICITY_HEAT_PUMP,
        ("electricity", "add-on heat pump"):
        WaterHeaterType.ELECTRICITY_ADDON_HEAT_PUMP,
        ("electricity", "integrated heat pump"):
        WaterHeaterType.ELECTRICITY_ADDON_HEAT_PUMP,
        ("natural gas", "not applicable"):
        WaterHeaterType.NOT_APPLICABLE,
        ("natural gas", "conventional tank"):
        WaterHeaterType.NATURAL_GAS_CONVENTIONAL_TANK,
        ("natural gas", "conventional tank (pilot)"):
        WaterHeaterType.NATURAL_GAS_CONVENTIONAL_TANK_PILOT,
        ("natural gas", "tankless coil"):
        WaterHeaterType.NATURAL_GAS_TANKLESS_COIL,
        ("natural gas", "instantaneous"):
        WaterHeaterType.NATURAL_GAS_INSTANTANEOUS,
        ("natural gas", "instantaneous (condensing)"):
        WaterHeaterType.NATURAL_GAS_INSTANTANEOUS_CONDENSING,
        ("natural gas", "instantaneous (pilot)"):
        WaterHeaterType.NATURAL_GAS_INSTANTANEOUS_PILOT,
        ("natural gas", "induced draft fan"):
        WaterHeaterType.NATURAL_GAS_INDUCED_DRAFT_FAN,
        ("natural gas", "induced draft fan (pilot)"):
        WaterHeaterType.NATURAL_GAS_INDUCED_DRAFT_FAN_PILOT,
        ("natural gas", "direct vent (sealed)"):
        WaterHeaterType.NATURAL_GAS_DIRECT_VENT_SEALED,
        ("natural gas", "direct vent (sealed, pilot)"):
        WaterHeaterType.NATURAL_GAS_DIRECT_VENT_SEALED_PILOT,
        ("natural gas", "condensing"):
        WaterHeaterType.NATURAL_GAS_CONDENSING,
        ("oil", "not applicable"):
        WaterHeaterType.NOT_APPLICABLE,
        ("oil", "conventional tank"):
        WaterHeaterType.OIL_CONVENTIONAL_TANK,
        ("oil", "tankless coil"):
        WaterHeaterType.OIL_TANKLESS_COIL,
        ("propane", "not applicable"):
        WaterHeaterType.NOT_APPLICABLE,
        ("propane", "conventional tank"):
        WaterHeaterType.PROPANE_CONVENTIONAL_TANK,
        ("propane", "conventional tank (pilot)"):
        WaterHeaterType.PROPANE_CONVENTIONAL_TANK_PILOT,
        ("propane", "tankless coil"):
        WaterHeaterType.PROPANE_TANKLESS_COIL,
        ("propane", "instantaneous"):
        WaterHeaterType.PROPANE_INSTANTANEOUS,
        ("propane", "instantaneous (condensing)"):
        WaterHeaterType.PROPANE_INSTANTANEOUS_CONDENSING,
        ("propane", "instantaneous (pilot)"):
        WaterHeaterType.PROPANE_INSTANTANEOUS_PILOT,
        ("propane", "induced draft fan"):
        WaterHeaterType.PROPANE_INDUCED_DRAFT_FAN,
        ("propane", "induced draft fan (pilot)"):
        WaterHeaterType.PROPANE_INDUCED_DRAFT_FAN_PILOT,
        ("propane", "direct vent (sealed)"):
        WaterHeaterType.PROPANE_DIRECT_VENT_SEALED,
        ("propane", "direct vent (sealed, pilot)"):
        WaterHeaterType.PROPANE_DIRECT_VENT_SEALED_PILOT,
        ("propane", "condensing"):
        WaterHeaterType.PROPANE_CONDENSING,
        ("mixed wood", "not applicable"):
        WaterHeaterType.NOT_APPLICABLE,
        ("mixed wood", "fireplace"):
        WaterHeaterType.WOOD_SPACE_HEATING_FIREPLACE,
        ("mixed wood", "wood stove water coil"):
        WaterHeaterType.WOOD_SPACE_HEATING_WOOD_STOVE_WATER_COIL,
        ("mixed wood", "indoor wood boiler"):
        WaterHeaterType.WOOD_SPACE_HEATING_INDOOR_WOOD_BOILER,
        ("mixed wood", "outdoor wood boiler"):
        WaterHeaterType.WOOD_SPACE_HEATING_OUTDOOR_WOOD_BOILER,
        ("mixed wood", "wood hot water tank"):
        WaterHeaterType.WOOD_SPACE_HEATING_WOOD_HOT_WATER_TANK,
        ("hardwood", "not applicable"):
        WaterHeaterType.NOT_APPLICABLE,
        ("hardwood", "fireplace"):
        WaterHeaterType.WOOD_SPACE_HEATING_FIREPLACE,
        ("hardwood", "wood stove water coil"):
        WaterHeaterType.WOOD_SPACE_HEATING_WOOD_STOVE_WATER_COIL,
        ("hardwood", "indoor wood boiler"):
        WaterHeaterType.WOOD_SPACE_HEATING_INDOOR_WOOD_BOILER,
        ("hardwood", "outdoor wood boiler"):
        WaterHeaterType.WOOD_SPACE_HEATING_OUTDOOR_WOOD_BOILER,
        ("hardwood", "wood hot water tank"):
        WaterHeaterType.WOOD_SPACE_HEATING_WOOD_HOT_WATER_TANK,
        ("soft wood", "not applicable"):
        WaterHeaterType.NOT_APPLICABLE,
        ("soft wood", "fireplace"):
        WaterHeaterType.WOOD_SPACE_HEATING_FIREPLACE,
        ("soft wood", "wood stove water coil"):
        WaterHeaterType.WOOD_SPACE_HEATING_WOOD_STOVE_WATER_COIL,
        ("soft wood", "indoor wood boiler"):
        WaterHeaterType.WOOD_SPACE_HEATING_INDOOR_WOOD_BOILER,
        ("soft wood", "outdoor wood boiler"):
        WaterHeaterType.WOOD_SPACE_HEATING_OUTDOOR_WOOD_BOILER,
        ("soft wood", "wood hot water tank"):
        WaterHeaterType.WOOD_SPACE_HEATING_WOOD_HOT_WATER_TANK,
        ("wood pellets", "not applicable"):
        WaterHeaterType.NOT_APPLICABLE,
        ("wood pellets", "fireplace"):
        WaterHeaterType.WOOD_SPACE_HEATING_FIREPLACE,
        ("wood pellets", "wood stove water coil"):
        WaterHeaterType.WOOD_SPACE_HEATING_WOOD_STOVE_WATER_COIL,
        ("wood pellets", "indoor wood boiler"):
        WaterHeaterType.WOOD_SPACE_HEATING_INDOOR_WOOD_BOILER,
        ("wood pellets", "outdoor wood boiler"):
        WaterHeaterType.WOOD_SPACE_HEATING_OUTDOOR_WOOD_BOILER,
        ("wood pellets", "wood hot water tank"):
        WaterHeaterType.WOOD_SPACE_HEATING_WOOD_HOT_WATER_TANK,
        ("solar", "solar collector system"):
        WaterHeaterType.SOLAR_COLLECTOR_SYSTEM,
        ("csa p9-11 tested combo heat/dhw", "csa p9-11 tested combo heat/dhw"):
        WaterHeaterType.CSA_DHW,
    }

    _WATER_HEATER_TYPE_TRANSLATION = {
        WaterHeaterType.NOT_APPLICABLE:
        bilingual.Bilingual(
            english="",
            french="",
        ),
        WaterHeaterType.ELECTRICITY_CONVENTIONAL_TANK:
        bilingual.Bilingual(
            english="Electric storage tank",
            french="Réservoir électrique",
        ),
        WaterHeaterType.ELECTRICITY_CONSERVER_TANK:
        bilingual.Bilingual(
            english="Electric storage tank",
            french="Réservoir électrique",
        ),
        WaterHeaterType.ELECTRICITY_INSTANTANEOUS:
        bilingual.Bilingual(
            english="Electric tankless water heater",
            french="Chauffe-eau électrique sans réservoir",
        ),
        WaterHeaterType.ELECTRICITY_TANKLESS_HEAT_PUMP:
        bilingual.Bilingual(
            english="Electric tankless heat pump",
            french="Thermopompe électrique sans réservoir",
        ),
        WaterHeaterType.ELECTRICITY_HEAT_PUMP:
        bilingual.Bilingual(
            english="Electric heat pump",
            french="Thermopompe électrique",
        ),
        WaterHeaterType.ELECTRICITY_ADDON_HEAT_PUMP:
        bilingual.Bilingual(
            english="Integrated heat pump",
            french="Thermopompe intégrée",
        ),
        WaterHeaterType.NATURAL_GAS_CONVENTIONAL_TANK:
        bilingual.Bilingual(
            english="Natural gas storage tank",
            french="Réservoir au gaz naturel",
        ),
        WaterHeaterType.NATURAL_GAS_CONVENTIONAL_TANK_PILOT:
        bilingual.Bilingual(
            english="Natural gas storage tank with pilot",
            french="Réservoir au gaz naturel avec veilleuse",
        ),
        WaterHeaterType.NATURAL_GAS_TANKLESS_COIL:
        bilingual.Bilingual(
            english="Natural gas tankless coil",
            french="Serpentin sans réservoir au gaz naturel",
        ),
        WaterHeaterType.NATURAL_GAS_INSTANTANEOUS:
        bilingual.Bilingual(
            english="Natural gas tankless",
            french="Chauffe-eau instantané au gaz naturel",
        ),
        WaterHeaterType.NATURAL_GAS_INSTANTANEOUS_CONDENSING:
        bilingual.Bilingual(
            english="Natural gas tankless",
            french="Chauffe-eau instantané au gaz naturel",
        ),
        WaterHeaterType.NATURAL_GAS_INSTANTANEOUS_PILOT:
        bilingual.Bilingual(
            english="Natural gas tankless with pilot",
            french="Chauffe-eau instantané au gaz naturel avec veilleuse",
        ),
        WaterHeaterType.NATURAL_GAS_INDUCED_DRAFT_FAN:
        bilingual.Bilingual(
            english="Natural gas power vented storage tank",
            french="Réservoir au gaz naturel à évacuation forcée",
        ),
        WaterHeaterType.NATURAL_GAS_INDUCED_DRAFT_FAN_PILOT:
        bilingual.Bilingual(
            english="Natural gas power vented storage tank with pilot",
            french=
            "Réservoir au gaz naturel à évacuation forcée avec veilleuse",
        ),
        WaterHeaterType.NATURAL_GAS_DIRECT_VENT_SEALED:
        bilingual.Bilingual(
            english="Natural gas direct vented storage tank",
            french="Réservoir au gaz naturel à évacuation directe",
        ),
        WaterHeaterType.NATURAL_GAS_DIRECT_VENT_SEALED_PILOT:
        bilingual.Bilingual(
            english="Natural gas direct vented storage tank with pilot",
            french=
            "Réservoir au gaz naturel à évacuation directe avec veilleuse",
        ),
        WaterHeaterType.NATURAL_GAS_CONDENSING:
        bilingual.Bilingual(
            english="Natural gas condensing storage tank",
            french="Réservoir au gaz naturel à condensation",
        ),
        WaterHeaterType.OIL_CONVENTIONAL_TANK:
        bilingual.Bilingual(
            english="Oil-fired storage tank",
            french="Réservoir au mazout",
        ),
        WaterHeaterType.OIL_TANKLESS_COIL:
        bilingual.Bilingual(
            english="Oil-type tankless coil",
            french="Serpentin sans réservoir au mazout",
        ),
        WaterHeaterType.PROPANE_CONVENTIONAL_TANK:
        bilingual.Bilingual(
            english="Propane storage tank",
            french="Réservoir au propane",
        ),
        WaterHeaterType.PROPANE_CONVENTIONAL_TANK_PILOT:
        bilingual.Bilingual(
            english="Propane storage tank with pilot",
            french="Réservoir au propane avec veilleuse",
        ),
        WaterHeaterType.PROPANE_TANKLESS_COIL:
        bilingual.Bilingual(
            english="Propane tankless coil",
            french="Serpentin sans réservoir au propane",
        ),
        WaterHeaterType.PROPANE_INSTANTANEOUS:
        bilingual.Bilingual(
            english="Propane tankless",
            french="Chauffe-eau instantané au propane",
        ),
        WaterHeaterType.PROPANE_INSTANTANEOUS_CONDENSING:
        bilingual.Bilingual(
            english="Propane condensing tankless",
            french="Chauffe-eau instantané au propane à condensation",
        ),
        WaterHeaterType.PROPANE_INSTANTANEOUS_PILOT:
        bilingual.Bilingual(
            english="Propane tankless with pilot",
            french="Chauffe-eau instantané au propane avec veilleuse",
        ),
        WaterHeaterType.PROPANE_INDUCED_DRAFT_FAN:
        bilingual.Bilingual(
            english="Propane power vented storage tank",
            french="Réservoir au propane à évacuation forcée",
        ),
        WaterHeaterType.PROPANE_INDUCED_DRAFT_FAN_PILOT:
        bilingual.Bilingual(
            english="Propane power vented storage tank with pilot",
            french="Réservoir au propane à évacuation forcée avec veilleuse",
        ),
        WaterHeaterType.PROPANE_DIRECT_VENT_SEALED:
        bilingual.Bilingual(
            english="Propane power vented storage tank",
            french="Réservoir au propane à évacuation directe",
        ),
        WaterHeaterType.PROPANE_DIRECT_VENT_SEALED_PILOT:
        bilingual.Bilingual(
            english="Propane power vented storage tank with pilot",
            french="Réservoir au propane à évacuation directe avec veilleuse",
        ),
        WaterHeaterType.PROPANE_CONDENSING:
        bilingual.Bilingual(
            english="Propane condensing storage tank",
            french="Réservoir au propane à condensation",
        ),
        WaterHeaterType.WOOD_SPACE_HEATING_FIREPLACE:
        bilingual.Bilingual(
            english="Fireplace",
            french="Foyer",
        ),
        WaterHeaterType.WOOD_SPACE_HEATING_WOOD_STOVE_WATER_COIL:
        bilingual.Bilingual(
            english="Wood stove water coil",
            french="Poêle à bois avec serpentin à l'eau",
        ),
        WaterHeaterType.WOOD_SPACE_HEATING_INDOOR_WOOD_BOILER:
        bilingual.Bilingual(
            english="Indoor wood boiler",
            french="Chaudière intérieure au bois",
        ),
        WaterHeaterType.WOOD_SPACE_HEATING_OUTDOOR_WOOD_BOILER:
        bilingual.Bilingual(
            english="Outdoor wood boiler",
            french="Chaudière extérieure au bois",
        ),
        WaterHeaterType.WOOD_SPACE_HEATING_WOOD_HOT_WATER_TANK:
        bilingual.Bilingual(
            english="Wood-fired water storage tank",
            french="Réservoir à eau chaude au bois",
        ),
        WaterHeaterType.SOLAR_COLLECTOR_SYSTEM:
        bilingual.Bilingual(
            english="Solar domestic water heater",
            french="Chauffe-eau solaire domestique",
        ),
        WaterHeaterType.CSA_DHW:
        bilingual.Bilingual(
            english="Certified combo system, space and domestic water heating",
            french=
            "Système combiné certifié pour le chauffage des locaux et de l’eau",
        ),
    }

    @classmethod
    def _from_data(cls, water_heating: element.Element) -> 'WaterHeating':
        drain_water_efficiency: typing.Optional[float] = None
        if water_heating.get('@hasDrainWaterHeatRecovery', str) == 'true':
            drain_water_efficiency = water_heating.get(
                'DrainWaterHeatRecovery/@effectivenessAt9.5', float)

        try:
            energy_type = water_heating.get_text('EnergySource/English')
            tank_type = water_heating.get_text('TankType/English')

            water_heater_type = cls._TYPE_MAP[(energy_type.lower(),
                                               tank_type.lower())]
            volume = water_heating.get('TankVolume/@value', float)
        except ElementGetValueError as exc:
            raise InvalidEmbeddedDataTypeError(
                WaterHeating, 'Missing/invalid attribue or text') from exc
        except KeyError as exc:
            raise InvalidEmbeddedDataTypeError(
                WaterHeating,
                'Invlaid energy and tank type combination') from exc

        efficiency_ef_node = water_heating.xpath('EnergyFactor/@value')
        efficiency_percent_node = water_heating.xpath(
            'EnergyFactor/@thermalEfficiency')

        if not efficiency_ef_node and not efficiency_percent_node:
            raise InvalidEmbeddedDataTypeError(WaterHeating,
                                               'No efficiency values')

        return WaterHeating(
            water_heater_type=water_heater_type,
            tank_volume=volume,
            efficiency_ef=float(efficiency_ef_node[0])
            if efficiency_ef_node else None,
            efficiency_percentage=float(efficiency_percent_node[0])
            if efficiency_percent_node else None,
            drain_water_heat_recovery_efficiency_percentage=
            drain_water_efficiency,
        )

    @classmethod
    def from_data(
            cls,
            water_heating: element.Element) -> typing.List['WaterHeating']:
        water_heatings = water_heating.xpath(
            "*[self::Primary or self::Secondary]")

        return [cls._from_data(heater) for heater in water_heatings]

    @property
    def tank_volume_gallon(self) -> float:
        return self.tank_volume * self._LITRE_TO_GALLON

    def to_dict(self) -> typing.Dict[str, typing.Union[str, float, None]]:
        translation = self._WATER_HEATER_TYPE_TRANSLATION[
            self.water_heater_type]

        return {
            'typeEnglish':
            translation.english,
            'typeFrench':
            translation.french,
            'tankVolumeLitres':
            self.tank_volume,
            'tankVolumeGallon':
            self.tank_volume_gallon,
            'efficiencyEf':
            self.efficiency_ef,
            'efficiencyPercentage':
            self.efficiency_percentage,
            'drainWaterHeatRecoveryEfficiencyPercentage':
            self.drain_water_heat_recovery_efficiency_percentage,
        }