mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
handling ambiguous cases for sloping ceiling vs loft insulation
This commit is contained in:
parent
f123a7ab89
commit
79ef0805c3
5 changed files with 477 additions and 47 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 ~~~~~
|
||||
################################################################
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue