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
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)', ) }, )
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), )
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), )
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' )
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'), }) }
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
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
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, }
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, }
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)
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
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, }
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, ), ])
def test_translation() -> None: output = bilingual.Bilingual(english='english text', french='french text') assert output.english == 'english text' assert output.french == 'french text'
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, }
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, }
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, }