handling ambiguous cases for sloping ceiling vs loft insulation

This commit is contained in:
Khalim Conn-Kowlessar 2026-01-27 19:04:37 +00:00
parent f123a7ab89
commit 79ef0805c3
5 changed files with 477 additions and 47 deletions

View file

@ -84,6 +84,7 @@ class Property:
uprn=None, # Pass as an optional input
property_valuation=None,
already_installed=None,
find_my_epc_components=None,
non_invasive_recommendations=None,
measures=None,
energy_assessment=None,
@ -114,6 +115,7 @@ class Property:
non_invasive_recommendations['recommendations'] if
non_invasive_recommendations else []
)
self.find_my_epc_components = find_my_epc_components # Store the find my epc components
# This is a list of measures that have been recommended for the property
if isinstance(measures, list):
self.measures = measures

View file

@ -796,9 +796,9 @@ async def model_engine(body: PlanTriggerRequest):
property_non_invasive_recommendations, patch = req_data.non_invasive_recommendations, req_data.patch
# if we have a remote assment data type, we pull the additional data and include it
epc_page_source = {}
epc_page_source, find_my_epc_components = {}, []
if (body.event_type == "remote_assessment") and not (epc_searcher.newest_epc.get("estimated")):
property_non_invasive_recommendations, patch, epc_page_source = (
property_non_invasive_recommendations, patch, epc_page_source, find_my_epc_components = (
RetrieveFindMyEpc.get_from_epc_with_fallback(
epc=epc_searcher.newest_epc,
epc_page=epc_page,
@ -834,6 +834,7 @@ async def model_engine(body: PlanTriggerRequest):
postcode=epc_searcher.postcode_clean,
epc_record=prepared_epc,
already_installed=property_already_installed + eco_packages.get(property_id)[3],
find_my_epc_components=find_my_epc_components,
property_valuation=req_data.valuation,
non_invasive_recommendations=property_non_invasive_recommendations,
energy_assessment=energy_assessment,

View file

@ -36,6 +36,9 @@ class RetrieveFindMyEpc:
self.rrn = rrn
self.address_cleaned = self.address.replace(",", "").replace(" ", "").lower()
# Containers for the extracted components
self.property_components = []
self.walls = []
self.address_postal_town = address_postal_town
@ -256,12 +259,10 @@ class RetrieveFindMyEpc:
property_features_table = soup.find("tbody", class_="govuk-table__body")
property_features_table = property_features_table.find_all("tr")
# Extract wall types
self.walls = []
for row in property_features_table:
cells = row.find_all("td")
if row.find("th").text.strip() == "Wall":
self.walls.append(cells[0].text.strip())
self.extract_property_components(property_features_table)
# Extract walls
self.walls = [x["description"] for x in self.property_components if x["component_name"] == "Wall"]
# Finally, we format the recommendations
recommendations = self.format_recommendations(recommendations, assessment_data, sap_2012_date)
@ -424,6 +425,37 @@ class RetrieveFindMyEpc:
return chosen_epc, epc_certificate
@staticmethod
def extract_property_components(property_features_table: list):
"""
Function to pull out a table for property components, marking their appearance index
:param property_features_table: The table of property features, as extracted by BeautifulSoup
:return: List of property components with appearance index
"""
property_components = []
for row in property_features_table:
cells = row.find_all("td")
component_name = row.find("th").text.strip()
property_components.append(
{
"component_name": component_name,
"description": cells[0].text.strip(),
"efficiency": cells[1].text.strip(),
}
)
# Add an appearance index, which will indicate if the component appears multiple times, so this
# becomes a reference for the building part the component is associated to (main, extensions, etc)
# We want to inject this appearance index into the component dictionaries
component_count = {}
for component in property_components:
name = component['component_name']
if name not in component_count:
component_count[name] = 0
component['appearance_index'] = component_count[name]
component_count[name] += 1
return property_components
def retrieve_newest_find_my_epc_data(
self, sap_2012_date=None, return_page=False, epc_page_source=None, rrn=None
):
@ -577,12 +609,10 @@ class RetrieveFindMyEpc:
property_features_table = address_res.find("tbody", class_="govuk-table__body")
property_features_table = property_features_table.find_all("tr")
# Extract wall types
self.walls = []
for row in property_features_table:
cells = row.find_all("td")
if row.find("th").text.strip() == "Wall":
self.walls.append(cells[0].text.strip())
property_components = self.extract_property_components(property_features_table)
# Extract walls
self.walls = [x["description"] for x in self.property_components if x["component_name"] == "Wall"]
# Finally, we format the recommendations
recommendations = self.format_recommendations(recommendations, assessment_data, sap_2012_date)
@ -615,6 +645,7 @@ class RetrieveFindMyEpc:
"heating_text": heating_text,
"hot_water_text": hot_water_text,
"recommendations": recommendations,
"property_components": property_components,
"epc_data": epc_data,
**assessment_data,
**low_carbon_energy_sources,
@ -804,7 +835,9 @@ class RetrieveFindMyEpc:
"page_source": find_epc_data.get("page_source")
}
return non_invasive_recommendations, patch, page_source
property_components = find_epc_data.get("property_components", [])
return non_invasive_recommendations, patch, page_source, property_components
@classmethod
def get_from_epc_with_fallback(

View file

@ -11,6 +11,7 @@ from recommendations.recommendation_utils import (
)
from recommendations.Costs import Costs
from etl.epc_clean.epc_attributes.RoofAttributes import RoofAttributes
from backend.app.plan.schemas import ROOF_INSULATION_MEASURES
class RoofRecommendations:
@ -122,7 +123,7 @@ class RoofRecommendations:
@staticmethod
def is_sloping_ceiling_appropriate(
is_pitched: bool, is_loft: bool, is_assumed: bool, has_sloping_ceiling_recommendation: bool,
primary_roof_is_sloped: bool
primary_roof_is_sloped: bool, insulation_thickness: str
) -> bool:
"""
@ -133,6 +134,7 @@ class RoofRecommendations:
recommendation
:param primary_roof_is_sloped: Boolean - indicates if the primary room is described a sloped (as opposed to
an extension)
:param insulation_thickness: String - insulation thickness of the roof
:return:
"""
# We need to check:
@ -141,11 +143,27 @@ class RoofRecommendations:
# 3) Is the insulation status NOT assumed
# 4) Is there a sloping ceiling recommendation (this may relate to the primary or secondary roof)
# If we have a loft primary roof and sloping cei
# If we have a loft primary roof and sloping ceiling
has_suitable_features = (
is_pitched and not is_loft and not is_assumed and primary_roof_is_sloped
)
# Check if it needs a recommendation
needs_recommendation_condition1 = has_sloping_ceiling_recommendation | (
insulation_thickness in ["below average"]
)
needs_recommendation_condition2 = has_sloping_ceiling_recommendation & (
insulation_thickness in ["none"]
)
# If the insulation thickness is 'none' this isn't alone conclusive for us to determine if it's
# a sloped ceiling
needs_recommendation = needs_recommendation_condition1 | needs_recommendation_condition2
# The property is pitched, not a loft, not assumed and has a sloping ceiling rec
if (is_pitched and not is_loft and not is_assumed and has_sloping_ceiling_recommendation and
primary_roof_is_sloped):
if has_suitable_features and needs_recommendation:
return True
return False
@ -157,17 +175,18 @@ class RoofRecommendations:
is_at_rafters: bool,
rir_over_loft: bool,
is_assumed: bool,
insulation_thickness: str,
has_loft_insulation_recommendation: bool,
has_sloping_ceiling_recommendation: bool
) -> bool:
"""
Determine if loft insulation is appropriate
:param non_invasive_recommendations: List - list of non-invasive recommendations
:param measures: List - list of measures
:param is_pitched: Boolean - indicates whether or not the roof is pitched
:param is_at_rafters: Boolean - indicates whether or not the loft insulation is at rafters
:param rir_over_loft: Boolean - indicates whether or not there we should be doing RIR insulation
:param is_assumed: Boolean - indicates whether or not the roof insulation status is assumed
:param insulation_thickness: String - insulation thickness of the roof
:param has_loft_insulation_recommendation: Boolean - indicates whether or not there
is a loft insulation non-invasive recommendation
:param has_sloping_ceiling_recommendation: Boolean - indicates whether or not there
@ -183,6 +202,15 @@ class RoofRecommendations:
if is_pitched and not is_assumed and has_sloping_ceiling_recommendation:
return False
# We check the insulation thickness. If it's one of the "average", "below average", "none" values,
if (
is_assumed and is_pitched and insulation_thickness in ["average", "below average", "above average"]
and not has_sloping_ceiling_recommendation and not has_loft_insulation_recommendation
):
# This is a pitched roof, without access to the loft, with unknown insulation status
return True
return has_loft_insulation_recommendation or (
is_pitched and has_li_in_measures and not is_at_rafters
) and not rir_over_loft
@ -283,6 +311,146 @@ class RoofRecommendations:
return False
@staticmethod
def _deduce_primary_roof(component_needs: dict) -> str:
"""
Helper function for deducing the primary roof type used by _handle_multi_roof_types
"""
# Can a non-primary part satisfy loft insulation?
primary_needs_loft = component_needs[1]["needs_loft_insulation"]
secondary_needs_loft = any(
p['needs_loft_insulation'] for idx, p in component_needs.items() if idx != 1
)
if primary_needs_loft and not secondary_needs_loft:
# Only option is loft
return "loft"
primary_needs_sloping = component_needs[1]["needs_sloping_ceiling"]
secondary_needs_sloping = any(
p['needs_sloping_ceiling'] for idx, p in component_needs.items() if idx != 1
)
if primary_needs_sloping and not secondary_needs_sloping:
# Only option is sloping ceiling
return "sloping_ceiling"
return "loft_insulation" # Defer to the cheaper option
def _handle_multi_roof_types(
self,
measures: List,
find_my_epc_components: List[Mapping[str, Any]],
non_invasive_recommendations: List[Mapping[str, Any]],
has_sloping_ceiling_recommendation: bool,
has_loft_insulation_recommendation: bool,
rir_over_loft: bool
) -> tuple[bool, bool]:
"""
This is a rough function to handle some edge cases, where we have two roof descriptions where
both look like they could be sloping ceilings or lofts. In this case, we need to deduce
which roof is the primary roof, and therefore whether or not we should recommend sloping ceiling insulation
:param measures: List - list of measures
:param find_my_epc_components: List - list of components from find my epc
:param non_invasive_recommendations: List - list of non-invasive recommendations
:param has_sloping_ceiling_recommendation: Boolean - indicates whether or not there is a sloping ceiling
recommendation
:param has_loft_insulation_recommendation: Boolean - indicates whether or not there is a loft insulation
recommendation
:param rir_over_loft: Boolean - indicates whether or not there we should be doing RIR insulation
:return: tuple[bool, bool] - (needs_sloping_ceiling, needs_loft_insulation)
"""
# We utilise the find my EPC data to solve cases where the primary roof and secondary roof
# being loft and sloped ceiling is ambiguous
# We need to:
# 1) Check if we have two roof types
# 2) check if both could be considered sloped
# 3) Check if we have two non-invasive recommendations for both roof types
# 4) Determine which roof is the primary roof
# We check a specific condition - which will imply loft insulation isn't appropriate but room in roof
# insulation is
# 1) We have an uninsulated loft (assumed)
# 2) We have a non-intrusive recommendation for room in roof insulation
# We only use this when we have sloping ceiling and loft insulation recommendations
# Components are indexed from 0
roof_count = max(
x["appearance_index"] for x in find_my_epc_components if x["component_name"] == "Roof"
) + 1
roof_non_invasive_recommendations = [
x["type"] for x in non_invasive_recommendations if x['type'] in ROOF_INSULATION_MEASURES
]
has_both_recommendations = (
"loft_insulation" in roof_non_invasive_recommendations and \
"sloping_ceiling_insulation" in roof_non_invasive_recommendations
)
if (roof_count <= 1) or not has_both_recommendations:
if roof_count > 1:
if "loft_insulation" in roof_non_invasive_recommendations:
return False, True
if "sloping_ceiling_insulation" in roof_non_invasive_recommendations:
return True, False
return True, False # Indicates that the property needs sloping ceiling as we only run this in that case
extracted_roof_descriptions = {
idx: {
"description": component["description"],
**RoofAttributes(component["description"]).process()
} for idx, component in enumerate(find_my_epc_components) if component["component_name"] == "Roof"
}
component_needs = {}
for component_idx, mapped in extracted_roof_descriptions.items():
is_pitched = mapped["is_pitched"]
is_loft = mapped["is_loft"]
is_assumed = mapped["is_assumed"]
insulation_thickness = mapped["insulation_thickness"]
is_at_rafters = mapped["is_at_rafters"]
needs_sloping_ceiling = self.is_sloping_ceiling_appropriate(
is_pitched=is_pitched,
is_loft=is_loft,
is_assumed=is_assumed,
has_sloping_ceiling_recommendation=has_sloping_ceiling_recommendation,
primary_roof_is_sloped=True,
insulation_thickness=insulation_thickness
)
# If the roof has some form of insulation already but isn't a loft, it's
# not a loft. E.g. "pitched, limited insulation" is for sloping ceiling, not loft
needs_loft_insulation = self.is_loft_insulation_appropriate(
measures=measures,
is_pitched=is_pitched,
is_at_rafters=is_at_rafters,
rir_over_loft=rir_over_loft,
insulation_thickness=insulation_thickness,
has_loft_insulation_recommendation=has_loft_insulation_recommendation,
is_assumed=is_assumed,
has_sloping_ceiling_recommendation=False
)
component_needs[component_idx] = {
"needs_sloping_ceiling": needs_sloping_ceiling,
"needs_loft_insulation": needs_loft_insulation
}
# Given the results we determine if the primary roof is sloped. The situation we may be in is
# one where the only otion is to assign one of the primary or secondary roof as a loft or sloped ceiling
# forcing our hand on whether the primary roof is sloped
primary_roof_type = self._deduce_primary_roof(component_needs)
if primary_roof_type in ["ambiguous", "sloping_ceiling"]:
return True, False # Set sloping ceiling to true, loft to false
return False, True # Set sloping ceiling to false, loft to true
def recommend(self, phase: int, measures: List | None = None, default_u_values: bool = False):
"""
Main method to recommend roof insulation measures
@ -322,17 +490,13 @@ class RoofRecommendations:
non_invasive_recommendations = self.property.non_invasive_recommendations
# We check a specific condition - which will imply loft insulation isn't appropriate but room in roof
# insulation is
# 1) We have an uninsulated loft (assumed)
# 2) We have a non-intrusive recommendation for room in roof insulation
is_pitched = self.property.roof["is_pitched"]
is_loft = self.property.roof["is_loft"]
is_assumed = self.property.roof["is_assumed"]
is_at_rafters = self.property.roof["is_at_rafters"]
is_flat = self.property.roof["is_flat"]
is_room_roof = self.property.roof["is_roof_room"]
insulation_thickness = self.property.roof["insulation_thickness"]
has_sloping_ceiling_recommendation = any(
x["type"] == "sloping_ceiling_insulation" for x in non_invasive_recommendations
@ -343,24 +507,32 @@ class RoofRecommendations:
primary_roof_is_sloped = self._is_primary_roof_sloped(
is_pitched=is_pitched, is_loft=is_loft, is_assumed=is_assumed
)
rir_over_loft = (
is_pitched and self.property.roof["insulation_thickness"] == "none" and
"room_in_roof_insulation" in [x["type"] for x in non_invasive_recommendations]
)
needs_sloping_ceiling = self.is_sloping_ceiling_appropriate(
is_pitched=is_pitched, is_loft=is_loft, is_assumed=is_assumed,
is_pitched=is_pitched,
is_loft=is_loft,
is_assumed=is_assumed,
has_sloping_ceiling_recommendation=has_sloping_ceiling_recommendation,
primary_roof_is_sloped=primary_roof_is_sloped
primary_roof_is_sloped=primary_roof_is_sloped,
insulation_thickness=insulation_thickness
)
needs_loft_insulation = self.is_loft_insulation_appropriate(
measures=measures, is_pitched=is_pitched, is_at_rafters=is_at_rafters,
rir_over_loft=rir_over_loft, has_loft_insulation_recommendation=has_loft_insulation_recommendation,
is_assumed=is_assumed, has_sloping_ceiling_recommendation=has_sloping_ceiling_recommendation
measures=measures,
is_pitched=is_pitched,
is_at_rafters=is_at_rafters,
rir_over_loft=rir_over_loft,
insulation_thickness=insulation_thickness,
has_loft_insulation_recommendation=has_loft_insulation_recommendation,
is_assumed=is_assumed,
has_sloping_ceiling_recommendation=has_sloping_ceiling_recommendation
)
needs_flat_roof_insulation = self.is_flat_roof_insulation_appropriate(
is_flat=is_flat, measures=measures,
is_flat=is_flat,
measures=measures,
has_flat_roof_recommendation=has_flat_roof_recommendation,
primary_roof_is_sloped=primary_roof_is_sloped
)
@ -371,6 +543,17 @@ class RoofRecommendations:
has_room_roof_recommendation=has_room_roof_recommendation
)
# We handle possible multi roof types
if needs_sloping_ceiling:
needs_sloping_ceiling, needs_loft_insulation = self._handle_multi_roof_types(
measures=measures,
find_my_epc_components=self.property.find_my_epc_components,
non_invasive_recommendations=non_invasive_recommendations,
has_sloping_ceiling_recommendation=has_sloping_ceiling_recommendation,
has_loft_insulation_recommendation=has_loft_insulation_recommendation,
rir_over_loft=rir_over_loft
)
################################################################
# ~~~~~ Loft Insulation Recommendation Logic ~~~~~
################################################################

View file

@ -1,6 +1,7 @@
import pytest
from unittest.mock import Mock
from backend.Property import Property
from etl.customers.immo.pilot.asset_list import non_invasive_recommendations
from etl.epc.Record import EPCRecord
from recommendations.RoofRecommendations import RoofRecommendations
from recommendations.tests.test_data.materials import materials
@ -407,7 +408,7 @@ class TestRoofRecommendations:
# ~~~~~~~~~~~~ Sloping Ceiling Insulation ~~~~~~~~~~~~
@pytest.mark.parametrize(
"roof, has_sloping_ceiling_recommendation, primary_roof_is_sloped, expected_result",
"roof, has_sloping_ceiling_recommendation, primary_roof_is_sloped, insulation_thickness, expected_result",
[
(
{
@ -427,6 +428,7 @@ class TestRoofRecommendations:
},
True,
True,
"none",
True,
),
(
@ -439,19 +441,22 @@ class TestRoofRecommendations:
},
False,
False,
"average",
False
)
]
)
def test_is_sloping_ceiling_appropriate(
self, roof, has_sloping_ceiling_recommendation, primary_roof_is_sloped, expected_result
self, roof, has_sloping_ceiling_recommendation, primary_roof_is_sloped,
insulation_thickness, expected_result
):
assert RoofRecommendations.is_sloping_ceiling_appropriate(
is_pitched=roof["is_pitched"],
is_loft=roof["is_loft"],
is_assumed=roof["is_assumed"],
has_sloping_ceiling_recommendation=has_sloping_ceiling_recommendation,
primary_roof_is_sloped=primary_roof_is_sloped
primary_roof_is_sloped=primary_roof_is_sloped,
insulation_thickness=insulation_thickness
) == expected_result
def test_sloping_ceiling_pitched_no_insulation(self):
@ -465,19 +470,42 @@ class TestRoofRecommendations:
'insulation_thickness': 'none'
},
roof_area=64.085,
data={"county": None, "local-authority-label": "Manchester"}
data={"county": None, "local-authority-label": "Manchester"},
age_band="D",
already_installed=[],
non_invasive_recommendations=[
{'type': 'flat_roof_insulation', 'sap_points': 9, 'survey': True},
{'type': 'sloping_ceiling_insulation', 'sap_points': 9, 'survey': True},
{'type': 'cavity_wall_insulation', 'sap_points': 6, 'survey': True},
{'type': 'suspended_floor_insulation', 'sap_points': 2, 'survey': True},
{'type': 'roomstat_programmer_trvs', 'sap_points': 3, 'survey': True},
{'type': 'time_temperature_zone_control', 'sap_points': 3, 'survey': True},
{'type': 'solar_pv', 'sap_points': 5, 'survey': True, 'suitable': True}
],
find_my_epc_components=[
{'component_name': 'Wall', 'description': 'Solid brick, as built, no insulation (assumed)',
'efficiency': 'Very poor', 'appearance_index': 0},
{'component_name': 'Roof', 'description': 'Pitched, no insulation', 'efficiency': 'Very poor',
'appearance_index': 0},
{'component_name': 'Roof', 'description': 'Pitched, limited insulation', 'efficiency': 'Very poor',
'appearance_index': 1},
{'component_name': 'Window', 'description': 'Some multiple glazing', 'efficiency': 'Very poor',
'appearance_index': 0},
{'component_name': 'Main heating', 'description': 'Boiler and radiators, mains gas',
'efficiency': 'Good', 'appearance_index': 0},
{'component_name': 'Main heating control', 'description': 'Programmer, room thermostat and TRVs',
'efficiency': 'Good', 'appearance_index': 0},
{'component_name': 'Hot water', 'description': 'From main system', 'efficiency': 'Good',
'appearance_index': 0},
{'component_name': 'Lighting', 'description': 'Low energy lighting in 28% of fixed outlets',
'efficiency': 'Average', 'appearance_index': 0},
{'component_name': 'Floor', 'description': 'Solid, no insulation (assumed)', 'efficiency': 'N/A',
'appearance_index': 0},
{'component_name': 'Secondary heating', 'description': 'None', 'efficiency': 'N/A',
'appearance_index': 0}
]
)
property_instance.age_band = "D"
property_instance.already_installed = []
property_instance.non_invasive_recommendations = [
{'type': 'flat_roof_insulation', 'sap_points': 9, 'survey': True},
{'type': 'sloping_ceiling_insulation', 'sap_points': 9, 'survey': True},
{'type': 'cavity_wall_insulation', 'sap_points': 6, 'survey': True},
{'type': 'suspended_floor_insulation', 'sap_points': 2, 'survey': True},
{'type': 'roomstat_programmer_trvs', 'sap_points': 3, 'survey': True},
{'type': 'time_temperature_zone_control', 'sap_points': 3, 'survey': True},
{'type': 'solar_pv', 'sap_points': 5, 'survey': True, 'suitable': True}
]
roof_recommender = RoofRecommendations(property_instance=property_instance, materials=[])
assert not roof_recommender.recommendations
@ -500,3 +528,186 @@ class TestRoofRecommendations:
assert roof_recommender.recommendations[0]["description_simulation"] == {
'roof-description': 'Pitched, insulated', 'roof-energy-eff': 'Average'
}
def test_ambiguous_sloping_ceiling_or_loft(self):
# In this case, we actually expect loft insulation to be recommended
property_instance = Mock(
id=0,
roof={
# Roof looks like it could be a sloping ceiling but it's actually a loft
'original_description': 'Pitched, no insulation', 'clean_description': 'Pitched, no insulation',
'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': True,
'is_roof_room': False, 'is_loft': False, 'is_flat': False, 'is_thatched': False,
'is_at_rafters': False, 'is_assumed': False, 'has_dwelling_above': False, 'is_valid': True,
'insulation_thickness': 'none'
},
roof_area=197.748,
data={"county": None, "local-authority-label": "Manchester"},
already_installed=[],
find_my_epc_components=[
{'component_name': 'Wall', 'description': 'Solid brick, as built, no insulation (assumed)',
'efficiency': 'Very poor', 'appearance_index': 0},
{'component_name': 'Roof', 'description': 'Pitched, no insulation', 'efficiency': 'Very poor',
'appearance_index': 0},
{'component_name': 'Roof', 'description': 'Pitched, limited insulation', 'efficiency': 'Very poor',
'appearance_index': 1},
{'component_name': 'Window', 'description': 'Some multiple glazing', 'efficiency': 'Very poor',
'appearance_index': 0},
{'component_name': 'Main heating', 'description': 'Boiler and radiators, mains gas',
'efficiency': 'Good', 'appearance_index': 0},
{'component_name': 'Main heating control', 'description': 'Programmer, room thermostat and TRVs',
'efficiency': 'Good', 'appearance_index': 0},
{'component_name': 'Hot water', 'description': 'From main system', 'efficiency': 'Good',
'appearance_index': 0},
{'component_name': 'Lighting', 'description': 'Low energy lighting in 28% of fixed outlets',
'efficiency': 'Average', 'appearance_index': 0},
{'component_name': 'Floor', 'description': 'Solid, no insulation (assumed)', 'efficiency': 'N/A',
'appearance_index': 0},
{'component_name': 'Secondary heating', 'description': 'None', 'efficiency': 'N/A',
'appearance_index': 0}
],
age_band="B",
non_invasive_recommendations=[
{'type': 'loft_insulation', 'sap_points': 3, 'survey': True},
{'type': 'flat_roof_insulation', 'sap_points': 2, 'survey': True},
{'type': 'sloping_ceiling_insulation', 'sap_points': 2, 'survey': True},
{'type': 'internal_wall_insulation', 'sap_points': 9, 'survey': True},
{'type': 'draught_proofing', 'sap_points': 1, 'survey': True},
{'type': 'low_energy_lighting', 'sap_points': 1, 'survey': True},
{'type': 'solar_water_heating', 'sap_points': 1, 'survey': True},
{'type': 'double_glazing', 'sap_points': 3, 'survey': True},
{'type': 'solar_pv', 'sap_points': 4, 'survey': True, 'suitable': True}
],
insulation_floor_area=162
)
roof_recommender = RoofRecommendations(property_instance=property_instance, materials=materials)
assert not roof_recommender.recommendations
roof_recommender.recommend(phase=0)
assert len(roof_recommender.recommendations) == 3
# Should all be loft insulation recommendations
assert all(
rec["type"] == "loft_insulation" for rec in roof_recommender.recommendations
)
def test_no_access_pitched_roof_assumed(self):
"""
In this case, the roof will have been surveyed as pitched, but the surveyor won't
have gotten access to the property to check the insulation. Therefore, we
recommend loft insulation. We assume that the roof is a locked off loft
:return:
"""
property_instance = Mock(
id=0,
roof={
'original_description': 'Pitched, limited insulation (assumed)',
'clean_description': 'Pitched, limited insulation', 'thermal_transmittance': None,
'thermal_transmittance_unit': None, 'is_pitched': True, 'is_roof_room': False, 'is_loft': False,
'is_flat': False, 'is_thatched': False, 'is_at_rafters': False, 'is_assumed': True,
'has_dwelling_above': False, 'is_valid': True, 'insulation_thickness': 'below average'
},
roof_area=73.24,
data={"county": None, "local-authority-label": "Manchester"},
already_installed=[],
find_my_epc_components=[
{'component_name': 'Wall', 'description': 'Solid brick, as built, no insulation (assumed)',
'efficiency': 'Very poor', 'appearance_index': 0},
{'component_name': 'Wall', 'description': 'System built, as built, no insulation (assumed)',
'efficiency': 'Poor', 'appearance_index': 1},
{'component_name': 'Wall', 'description': 'Cavity wall, filled cavity', 'efficiency': 'Average',
'appearance_index': 2},
{'component_name': 'Roof', 'description': 'Pitched, limited insulation (assumed)',
'efficiency': 'Very poor', 'appearance_index': 0},
{'component_name': 'Window', 'description': 'Fully double glazed', 'efficiency': 'Average',
'appearance_index': 0},
{'component_name': 'Main heating', 'description': 'Boiler and radiators, mains gas',
'efficiency': 'Good', 'appearance_index': 0},
{'component_name': 'Main heating control', 'description': 'Programmer and room thermostat',
'efficiency': 'Average', 'appearance_index': 0},
{'component_name': 'Hot water', 'description': 'From main system', 'efficiency': 'Good',
'appearance_index': 0},
{'component_name': 'Lighting', 'description': 'Low energy lighting in 75% of fixed outlets',
'efficiency': 'Very good', 'appearance_index': 0},
{'component_name': 'Roof', 'description': '(another dwelling above)', 'efficiency': 'N/A',
'appearance_index': 1},
{'component_name': 'Floor', 'description': 'Suspended, no insulation (assumed)', 'efficiency': 'N/A',
'appearance_index': 0},
{'component_name': 'Floor', 'description': 'Solid, no insulation (assumed)', 'efficiency': 'N/A',
'appearance_index': 1},
{'component_name': 'Secondary heating', 'description': 'None', 'efficiency': 'N/A',
'appearance_index': 0}
],
age_band="B",
non_invasive_recommendations=[
{'type': 'internal_wall_insulation', 'sap_points': 2, 'survey': True},
{'type': 'suspended_floor_insulation', 'sap_points': 2, 'survey': True},
{'type': 'solid_floor_insulation', 'sap_points': 1, 'survey': True},
{'type': 'low_energy_lighting', 'sap_points': 0, 'survey': True}
],
insulation_floor_area=60
)
roof_recommender = RoofRecommendations(property_instance=property_instance, materials=materials)
assert not roof_recommender.recommendations
roof_recommender.recommend(phase=0)
assert len(roof_recommender.recommendations) == 3
# Should all be loft insulation recommendations
assert all(
rec["type"] == "loft_insulation" for rec in roof_recommender.recommendations
)
def test_traditional_loft_insulation(self):
property_instance = Mock(
id=0,
roof={
'original_description': 'Pitched, no insulation', 'clean_description': 'Pitched, no insulation',
'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': True,
'is_roof_room': False, 'is_loft': False, 'is_flat': False, 'is_thatched': False,
'is_at_rafters': False, 'is_assumed': False, 'has_dwelling_above': False, 'is_valid': True,
'insulation_thickness': 'none'
},
roof_area=48.82666666666667,
data={"county": None, "local-authority-label": "Manchester"},
already_installed=[],
find_my_epc_components=[
{'component_name': 'Wall', 'description': 'Cavity wall, filled cavity', 'efficiency': 'Good',
'appearance_index': 0},
{'component_name': 'Roof', 'description': 'Pitched, no insulation', 'efficiency': 'Very poor',
'appearance_index': 0},
{'component_name': 'Window', 'description': 'Fully double glazed', 'efficiency': 'Good',
'appearance_index': 0},
{'component_name': 'Main heating', 'description': 'Boiler and radiators, mains gas',
'efficiency': 'Good', 'appearance_index': 0},
{'component_name': 'Main heating control', 'description': 'TRVs and bypass', 'efficiency': 'Average',
'appearance_index': 0},
{'component_name': 'Hot water', 'description': 'From main system', 'efficiency': 'Good',
'appearance_index': 0},
{'component_name': 'Lighting', 'description': 'Low energy lighting in all fixed outlets',
'efficiency': 'Very good', 'appearance_index': 0},
{'component_name': 'Floor', 'description': 'Solid, no insulation (assumed)', 'efficiency': 'N/A',
'appearance_index': 0},
{'component_name': 'Secondary heating', 'description': 'Room heaters, electric', 'efficiency': 'N/A',
'appearance_index': 0}
],
age_band="F",
non_invasive_recommendations=[
{'type': 'loft_insulation', 'sap_points': 9, 'survey': True},
{'type': 'solid_floor_insulation', 'sap_points': 2, 'survey': True},
{'type': 'solar_water_heating', 'sap_points': 1, 'survey': True},
{'type': 'solar_pv', 'sap_points': 11, 'survey': True, 'suitable': True}
],
insulation_floor_area=40.0
)
roof_recommender = RoofRecommendations(property_instance=property_instance, materials=materials)
assert not roof_recommender.recommendations
roof_recommender.recommend(0)
assert len(roof_recommender.recommendations) == 3
# should all be loft insulation recommendations
assert all(rec["type"] == "loft_insulation" for rec in roof_recommender.recommendations)