mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
Merge pull request #341 from Hestia-Homes/retrofit-assessmet-api
Retrofit assessmet api
This commit is contained in:
commit
213a9cf533
9 changed files with 161 additions and 44 deletions
|
|
@ -71,6 +71,10 @@ def get_latest_assessment_by_uprn(session: Session, uprn: int) -> Optional[Energ
|
||||||
:param uprn: The unique property reference number
|
:param uprn: The unique property reference number
|
||||||
:return: The latest EnergyAssessment object or None if not found
|
:return: The latest EnergyAssessment object or None if not found
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if not uprn:
|
||||||
|
return EnergyAssessment.empty_response()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Query the EnergyAssessment model, filter by uprn, order by inspection_date in descending order
|
# Query the EnergyAssessment model, filter by uprn, order by inspection_date in descending order
|
||||||
latest_assessment = session.query(EnergyAssessment).filter_by(uprn=uprn).order_by(
|
latest_assessment = session.query(EnergyAssessment).filter_by(uprn=uprn).order_by(
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,8 @@ from backend.app.db.models.portfolio import (
|
||||||
from sqlalchemy.orm.exc import NoResultFound
|
from sqlalchemy.orm.exc import NoResultFound
|
||||||
|
|
||||||
|
|
||||||
def create_property(session: Session, portfolio_id: int, address: str, postcode: str, uprn: str) -> (int, bool):
|
def create_property(session: Session, portfolio_id: int, address: str, postcode: str, uprn: str,
|
||||||
|
energy_assessment: dict) -> (int, bool):
|
||||||
"""
|
"""
|
||||||
This function will create a record for the property in the database if it does not exist.
|
This function will create a record for the property in the database if it does not exist.
|
||||||
If it does exist, it will just update the updated_at field.
|
If it does exist, it will just update the updated_at field.
|
||||||
|
|
@ -39,13 +40,17 @@ def create_property(session: Session, portfolio_id: int, address: str, postcode:
|
||||||
|
|
||||||
except NoResultFound:
|
except NoResultFound:
|
||||||
# Property doesn't exist, create a new one
|
# Property doesn't exist, create a new one
|
||||||
|
|
||||||
|
status = PortfolioStatus.ASSESSMENT.value if len(energy_assessment["epc"]) == 0 \
|
||||||
|
else PortfolioStatus.SURVEY.value
|
||||||
|
|
||||||
new_property = PropertyModel(
|
new_property = PropertyModel(
|
||||||
address=address,
|
address=address,
|
||||||
postcode=postcode,
|
postcode=postcode,
|
||||||
portfolio_id=portfolio_id,
|
portfolio_id=portfolio_id,
|
||||||
uprn=uprn,
|
uprn=uprn,
|
||||||
creation_status=PropertyCreationStatus.LOADING,
|
creation_status=PropertyCreationStatus.LOADING,
|
||||||
status=PortfolioStatus.ASSESSMENT.value,
|
status=status,
|
||||||
has_pre_condition_report=False,
|
has_pre_condition_report=False,
|
||||||
has_recommendations=False
|
has_recommendations=False
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ Base = declarative_base()
|
||||||
class PortfolioStatus(enum.Enum):
|
class PortfolioStatus(enum.Enum):
|
||||||
SCOPING = "scoping"
|
SCOPING = "scoping"
|
||||||
ASSESSMENT = "assessment"
|
ASSESSMENT = "assessment"
|
||||||
|
SURVEY = "survey"
|
||||||
TENDERING = "tendering"
|
TENDERING = "tendering"
|
||||||
PROJECT_UNDERWAY = "project underway"
|
PROJECT_UNDERWAY = "project underway"
|
||||||
COMPLETION_ON_TRACK = "completion; status: on track"
|
COMPLETION_ON_TRACK = "completion; status: on track"
|
||||||
|
|
|
||||||
|
|
@ -235,10 +235,14 @@ def create_epc_records(epc_searcher: SearchEpc, energy_assessment: dict):
|
||||||
EnergyAssessment.empty_response() method
|
EnergyAssessment.empty_response() method
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
newest_epc = epc_searcher.newest_epc.copy()
|
||||||
|
if newest_epc["uprn"] == "" and epc_searcher.uprn:
|
||||||
|
newest_epc["uprn"] = epc_searcher.uprn
|
||||||
|
|
||||||
if not energy_assessment["epc"]:
|
if not energy_assessment["epc"]:
|
||||||
energy_assessment_is_newer = False
|
energy_assessment_is_newer = False
|
||||||
return {
|
return {
|
||||||
'original_epc': epc_searcher.newest_epc.copy(),
|
'original_epc': newest_epc,
|
||||||
'full_sap_epc': epc_searcher.full_sap_epc.copy(),
|
'full_sap_epc': epc_searcher.full_sap_epc.copy(),
|
||||||
'old_data': epc_searcher.older_epcs.copy(),
|
'old_data': epc_searcher.older_epcs.copy(),
|
||||||
}, energy_assessment_is_newer
|
}, energy_assessment_is_newer
|
||||||
|
|
@ -249,22 +253,22 @@ def create_epc_records(epc_searcher: SearchEpc, energy_assessment: dict):
|
||||||
# We insert county into the epc, since right now this isn't something that we pull out from the energy
|
# We insert county into the epc, since right now this isn't something that we pull out from the energy
|
||||||
# assessment
|
# assessment
|
||||||
for col in ["county", "constituency", "constituency-label", "local-authority", "local-authority-label"]:
|
for col in ["county", "constituency", "constituency-label", "local-authority", "local-authority-label"]:
|
||||||
epc[col] = epc_searcher.newest_epc[col]
|
epc[col] = newest_epc[col]
|
||||||
|
|
||||||
# We check if the energy assessment is newer than the newest EPC
|
# We check if the energy assessment is newer than the newest EPC
|
||||||
if pd.to_datetime(energy_assessment_date) > pd.to_datetime(epc_searcher.newest_epc["inspection-date"]):
|
if pd.to_datetime(energy_assessment_date) > pd.to_datetime(newest_epc["inspection-date"]):
|
||||||
# In this case, our energy assessment is newer than the EPCs available for this property
|
# In this case, our energy assessment is newer than the EPCs available for this property
|
||||||
energy_assessment_is_newer = True
|
energy_assessment_is_newer = True
|
||||||
return {
|
return {
|
||||||
"original_epc": epc,
|
"original_epc": epc,
|
||||||
"full_sap_epc": epc_searcher.full_sap_epc.copy(),
|
"full_sap_epc": epc_searcher.full_sap_epc.copy(),
|
||||||
"old_data": epc_searcher.older_epcs.copy() + [epc_searcher.newest_epc.copy()]
|
"old_data": epc_searcher.older_epcs.copy() + [newest_epc]
|
||||||
}, energy_assessment_is_newer
|
}, energy_assessment_is_newer
|
||||||
|
|
||||||
# We check if the EPC we have produced is contained in the set of EPCs done for the property
|
# We check if the EPC we have produced is contained in the set of EPCs done for the property
|
||||||
# We do this based on inspection-date and SAP
|
# We do this based on inspection-date and SAP
|
||||||
epc_in_historicals = [
|
epc_in_historicals = [
|
||||||
x for x in epc_searcher.older_epcs + [epc_searcher.newest_epc]
|
x for x in epc_searcher.older_epcs + [newest_epc]
|
||||||
if x["inspection-date"] == energy_assessment_date and
|
if x["inspection-date"] == energy_assessment_date and
|
||||||
x["current-energy-efficiency"] == epc["current-energy-efficiency"]
|
x["current-energy-efficiency"] == epc["current-energy-efficiency"]
|
||||||
]
|
]
|
||||||
|
|
@ -273,7 +277,7 @@ def create_epc_records(epc_searcher: SearchEpc, energy_assessment: dict):
|
||||||
if epc_in_historicals:
|
if epc_in_historicals:
|
||||||
# Then the EPC we have produced is already in the set of EPCs, and our EPC is older than the newest
|
# Then the EPC we have produced is already in the set of EPCs, and our EPC is older than the newest
|
||||||
return {
|
return {
|
||||||
"original_epc": epc_searcher.newest_epc.copy(),
|
"original_epc": newest_epc,
|
||||||
"full_sap_epc": epc_searcher.full_sap_epc.copy(),
|
"full_sap_epc": epc_searcher.full_sap_epc.copy(),
|
||||||
"old_data": epc_searcher.older_epcs.copy()
|
"old_data": epc_searcher.older_epcs.copy()
|
||||||
}, energy_assessment_is_newer
|
}, energy_assessment_is_newer
|
||||||
|
|
@ -281,7 +285,7 @@ def create_epc_records(epc_searcher: SearchEpc, energy_assessment: dict):
|
||||||
# In this case, our EPC is older than the newest publically avaible one, but is not contained in
|
# In this case, our EPC is older than the newest publically avaible one, but is not contained in
|
||||||
# the historicals, so it can't have been lodged, so we include it in the old data
|
# the historicals, so it can't have been lodged, so we include it in the old data
|
||||||
return {
|
return {
|
||||||
'original_epc': epc_searcher.newest_epc.copy(),
|
'original_epc': newest_epc,
|
||||||
'full_sap_epc': epc_searcher.full_sap_epc.copy(),
|
'full_sap_epc': epc_searcher.full_sap_epc.copy(),
|
||||||
'old_data': epc_searcher.older_epcs.copy() + [epc],
|
'old_data': epc_searcher.older_epcs.copy() + [epc],
|
||||||
}, energy_assessment_is_newer
|
}, energy_assessment_is_newer
|
||||||
|
|
@ -412,7 +416,8 @@ async def trigger_plan(body: PlanTriggerRequest):
|
||||||
|
|
||||||
# Create a record in db
|
# Create a record in db
|
||||||
property_id, is_new = create_property(
|
property_id, is_new = create_property(
|
||||||
session, body.portfolio_id, epc_searcher.address_clean, epc_searcher.postcode_clean, epc_searcher.uprn
|
session, body.portfolio_id, epc_searcher.address_clean, epc_searcher.postcode_clean, epc_searcher.uprn,
|
||||||
|
energy_assessment
|
||||||
)
|
)
|
||||||
if not is_new and not body.multi_plan:
|
if not is_new and not body.multi_plan:
|
||||||
continue
|
continue
|
||||||
|
|
@ -799,6 +804,15 @@ async def trigger_plan(body: PlanTriggerRequest):
|
||||||
if ventilation_rec:
|
if ventilation_rec:
|
||||||
selected_recommendations.add(ventilation_rec["recommendation_id"])
|
selected_recommendations.add(ventilation_rec["recommendation_id"])
|
||||||
|
|
||||||
|
# If we have a trickle vents recommendation, we also switch it on. We don't just check the solution
|
||||||
|
trickle_vents_rec = next(
|
||||||
|
(r[0] for r in recommendations[p.id] if r[0]["type"] == "trickle_vents"),
|
||||||
|
None
|
||||||
|
)
|
||||||
|
# If a matching recommendation was found, add its ID to the selected recommendations
|
||||||
|
if trickle_vents_rec:
|
||||||
|
selected_recommendations.add(trickle_vents_rec["recommendation_id"])
|
||||||
|
|
||||||
# We'll use the set of selected recommendations to filter the recommendations to upload
|
# We'll use the set of selected recommendations to filter the recommendations to upload
|
||||||
final_recommendations = [
|
final_recommendations = [
|
||||||
[
|
[
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ def app():
|
||||||
"surveyor": "JAFFERSONS ENERGY CONSULTANTS",
|
"surveyor": "JAFFERSONS ENERGY CONSULTANTS",
|
||||||
"project_code": "VEC001",
|
"project_code": "VEC001",
|
||||||
}
|
}
|
||||||
|
# 5 Grove Mansions
|
||||||
# These are the recommendations based on the on-site survey of the property.
|
# These are the recommendations based on the on-site survey of the property.
|
||||||
non_intrusive_recommendations = [
|
non_intrusive_recommendations = [
|
||||||
{
|
{
|
||||||
|
|
@ -22,17 +22,17 @@ def app():
|
||||||
"recommendations": [
|
"recommendations": [
|
||||||
{
|
{
|
||||||
"type": "draught_proofing",
|
"type": "draught_proofing",
|
||||||
"cost": 123,
|
"cost": 100,
|
||||||
"survey": True,
|
"survey": True,
|
||||||
"sap_points": 1
|
"sap_points": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "mixed_glazing", "cost": 12345, "survey": True,
|
"type": "mixed_glazing", "cost": 14632, "survey": True,
|
||||||
"description": "Install double glazing to north facing windows and secondary glazing to the "
|
"description": "Install double glazing to north facing windows and secondary glazing to the "
|
||||||
"remaining windows at the front of the building",
|
"remaining windows at the front of the building",
|
||||||
"sap_points": 3
|
"sap_points": 3
|
||||||
},
|
},
|
||||||
{"type": "trickle_vents", "cost": 500, "survey": True},
|
{"type": "trickle_vents", "cost": 1000, "survey": True},
|
||||||
{"type": "suspended_floor_insulation", "cost": None, "survey": True, "sap_points": 2},
|
{"type": "suspended_floor_insulation", "cost": None, "survey": True, "sap_points": 2},
|
||||||
{"type": "internal_wall_insulation", "cost": None, "survey": True, "sap_points": 5},
|
{"type": "internal_wall_insulation", "cost": None, "survey": True, "sap_points": 5},
|
||||||
]
|
]
|
||||||
|
|
@ -41,14 +41,14 @@ def app():
|
||||||
# 8 Grove Mansions
|
# 8 Grove Mansions
|
||||||
"uprn": 10024087855,
|
"uprn": 10024087855,
|
||||||
"recommendations": [
|
"recommendations": [
|
||||||
{"type": "draught_proofing", "cost": 123, "survey": True, "sap_points": 2},
|
{"type": "draught_proofing", "cost": 100, "survey": True, "sap_points": 2},
|
||||||
{
|
{
|
||||||
"type": "mixed_glazing", "cost": 12345, "survey": True,
|
"type": "mixed_glazing", "cost": 7814, "survey": True,
|
||||||
"description": "Install double glazing to north facing windows and secondary glazing to the "
|
"description": "Install double glazing to north facing windows and secondary glazing to the "
|
||||||
"remaining windows at the front of the building",
|
"remaining windows at the front of the building",
|
||||||
"sap_points": 4
|
"sap_points": 4
|
||||||
},
|
},
|
||||||
{"type": "trickle_vents", "cost": 500, "survey": True},
|
{"type": "trickle_vents", "cost": 700, "survey": True},
|
||||||
{"type": "low_energy_lighting", "cost": None, "survey": True, "sap_points": 0},
|
{"type": "low_energy_lighting", "cost": None, "survey": True, "sap_points": 0},
|
||||||
{"type": "internal_wall_insulation", "cost": None, "survey": True, 'sap_points': 5},
|
{"type": "internal_wall_insulation", "cost": None, "survey": True, 'sap_points': 5},
|
||||||
]
|
]
|
||||||
|
|
@ -57,14 +57,14 @@ def app():
|
||||||
# 9 Grove Mansions
|
# 9 Grove Mansions
|
||||||
"uprn": 121016128,
|
"uprn": 121016128,
|
||||||
"recommendations": [
|
"recommendations": [
|
||||||
{"type": "draught_proofing", "cost": 123, "survey": True, "sap_points": 1},
|
{"type": "draught_proofing", "cost": 100, "survey": True, "sap_points": 1},
|
||||||
{
|
{
|
||||||
"type": "mixed_glazing", "cost": 12345, "survey": True,
|
"type": "mixed_glazing", "cost": 9740, "survey": True,
|
||||||
"description": "Install double glazing to north facing windows and secondary glazing to the "
|
"description": "Install double glazing to north facing windows and secondary glazing to the "
|
||||||
"remaining windows at the front of the building",
|
"remaining windows at the front of the building",
|
||||||
"sap_points": 3
|
"sap_points": 3
|
||||||
},
|
},
|
||||||
{"type": "trickle_vents", "cost": 500, "survey": True},
|
{"type": "trickle_vents", "cost": 1000, "survey": True},
|
||||||
{"type": "low_energy_lighting", "cost": None, "survey": True, "sap_points": 1},
|
{"type": "low_energy_lighting", "cost": None, "survey": True, "sap_points": 1},
|
||||||
{"type": "suspended_floor_insulation", "cost": None, "sap_points": 1},
|
{"type": "suspended_floor_insulation", "cost": None, "sap_points": 1},
|
||||||
{"type": "internal_wall_insulation", "cost": None, "survey": True, "sap_points": 6},
|
{"type": "internal_wall_insulation", "cost": None, "survey": True, "sap_points": 6},
|
||||||
|
|
@ -75,12 +75,12 @@ def app():
|
||||||
"uprn": 121016124,
|
"uprn": 121016124,
|
||||||
"recommendations": [
|
"recommendations": [
|
||||||
{
|
{
|
||||||
"type": "mixed_glazing", "cost": 12345, "survey": True,
|
"type": "mixed_glazing", "cost": 12662, "survey": True,
|
||||||
"description": "Install double glazing to north facing windows and secondary glazing to the "
|
"description": "Install double glazing to north facing windows and secondary glazing to the "
|
||||||
"remaining windows at the front of the building",
|
"remaining windows at the front of the building",
|
||||||
"sap_points": 5
|
"sap_points": 5
|
||||||
},
|
},
|
||||||
{"type": "trickle_vents", "cost": 500, "survey": True},
|
{"type": "trickle_vents", "cost": 1300, "survey": True},
|
||||||
{"type": "low_energy_lighting", "cost": None, "survey": True, "sap_points": 2},
|
{"type": "low_energy_lighting", "cost": None, "survey": True, "sap_points": 2},
|
||||||
{"type": "internal_wall_insulation", "cost": None, "survey": True, "sap_points": 8},
|
{"type": "internal_wall_insulation", "cost": None, "survey": True, "sap_points": 8},
|
||||||
]
|
]
|
||||||
|
|
@ -89,14 +89,14 @@ def app():
|
||||||
# 14 Grove Mansions
|
# 14 Grove Mansions
|
||||||
"uprn": 121016117,
|
"uprn": 121016117,
|
||||||
"recommendations": [
|
"recommendations": [
|
||||||
{"type": "draught_proofing", "cost": 123, "survey": True, "sap_points": 1},
|
{"type": "draught_proofing", "cost": 100, "survey": True, "sap_points": 1},
|
||||||
{
|
{
|
||||||
"type": "mixed_glazing", "cost": 12345, "survey": True,
|
"type": "mixed_glazing", "cost": 10736, "survey": True,
|
||||||
"description": "Install double glazing to north facing windows and secondary glazing to the "
|
"description": "Install double glazing to north facing windows and secondary glazing to the "
|
||||||
"remaining windows at the front of the building",
|
"remaining windows at the front of the building",
|
||||||
"sap_points": 4
|
"sap_points": 4
|
||||||
},
|
},
|
||||||
{"type": "trickle_vents", "cost": 500, "survey": True},
|
{"type": "trickle_vents", "cost": 1000, "survey": True},
|
||||||
{"type": "low_energy_lighting", "cost": None, "survey": True, "sap_points": 1},
|
{"type": "low_energy_lighting", "cost": None, "survey": True, "sap_points": 1},
|
||||||
{"type": "internal_wall_insulation", "cost": None, "survey": True, "sap_points": 6},
|
{"type": "internal_wall_insulation", "cost": None, "survey": True, "sap_points": 6},
|
||||||
]
|
]
|
||||||
|
|
@ -113,6 +113,7 @@ def app():
|
||||||
]
|
]
|
||||||
|
|
||||||
asset_list = [
|
asset_list = [
|
||||||
|
# These are properties where we've done a survey
|
||||||
{
|
{
|
||||||
"uprn": 121016121, "address": "", "postcode": ""
|
"uprn": 121016121, "address": "", "postcode": ""
|
||||||
},
|
},
|
||||||
|
|
@ -131,6 +132,63 @@ def app():
|
||||||
{
|
{
|
||||||
"uprn": 10024087902, "address": "", "postcode": ""
|
"uprn": 10024087902, "address": "", "postcode": ""
|
||||||
},
|
},
|
||||||
|
# These properties we just model with default data
|
||||||
|
# Flat 1
|
||||||
|
{
|
||||||
|
"uprn": 121016113, "address": "", "postcode": ""
|
||||||
|
},
|
||||||
|
# Flat 10
|
||||||
|
{
|
||||||
|
"uprn": 121016114, "address": "", "postcode": ""
|
||||||
|
},
|
||||||
|
# Flat 11
|
||||||
|
{
|
||||||
|
"uprn": 121016115, "address": "", "postcode": ""
|
||||||
|
},
|
||||||
|
# Flat 12
|
||||||
|
{
|
||||||
|
"uprn": 121016116, "address": "", "postcode": ""
|
||||||
|
},
|
||||||
|
# Flat 15
|
||||||
|
{
|
||||||
|
"uprn": 121016118, "address": "", "postcode": ""
|
||||||
|
},
|
||||||
|
# Flat 16
|
||||||
|
{
|
||||||
|
"uprn": 121016119, "address": "", "postcode": ""
|
||||||
|
},
|
||||||
|
# Flat 17
|
||||||
|
{
|
||||||
|
"address": "Flat 17 Grove Mansions", "postcode": "SW4 9SL"
|
||||||
|
},
|
||||||
|
# Flat 18
|
||||||
|
{
|
||||||
|
"uprn": 10024087901, "address": "", "postcode": ""
|
||||||
|
},
|
||||||
|
# Flat 3
|
||||||
|
{
|
||||||
|
"uprn": 121016122, "address": "", "postcode": ""
|
||||||
|
},
|
||||||
|
# Flat 4
|
||||||
|
{
|
||||||
|
"uprn": 121016123, "address": "", "postcode": ""
|
||||||
|
},
|
||||||
|
# Flat 6
|
||||||
|
{
|
||||||
|
"uprn": 121016125, "address": "", "postcode": ""
|
||||||
|
},
|
||||||
|
# Flat 7
|
||||||
|
{
|
||||||
|
"uprn": 10024087854, "address": "", "postcode": ""
|
||||||
|
},
|
||||||
|
# Flat 7A
|
||||||
|
{
|
||||||
|
"uprn": 10024087840, "address": "", "postcode": ""
|
||||||
|
},
|
||||||
|
# Flat 8A
|
||||||
|
{
|
||||||
|
"uprn": 10024087841, "address": "", "postcode": ""
|
||||||
|
},
|
||||||
]
|
]
|
||||||
asset_list = pd.DataFrame(asset_list)
|
asset_list = pd.DataFrame(asset_list)
|
||||||
|
|
||||||
|
|
@ -162,7 +220,7 @@ def app():
|
||||||
"patches_file_path": "",
|
"patches_file_path": "",
|
||||||
"non_invasive_recommendations_file_path": non_invasive_recommendations_filename,
|
"non_invasive_recommendations_file_path": non_invasive_recommendations_filename,
|
||||||
"inclusions": [
|
"inclusions": [
|
||||||
"draught_proofing", "mixed_glazing", "trickle_vents", "low_energy_lighting",
|
"draught_proofing", "mixed_glazing", "trickle_vents", "low_energy_lighting", "windows"
|
||||||
],
|
],
|
||||||
"budget": None,
|
"budget": None,
|
||||||
"scenario_name": "Quick wins - do now while tenanted",
|
"scenario_name": "Quick wins - do now while tenanted",
|
||||||
|
|
@ -185,7 +243,9 @@ def app():
|
||||||
"trickle_vents",
|
"trickle_vents",
|
||||||
"low_energy_lighting",
|
"low_energy_lighting",
|
||||||
"suspended_floor_insulation",
|
"suspended_floor_insulation",
|
||||||
"internal_wall_insulation"
|
"internal_wall_insulation",
|
||||||
|
"room_roof_insulation",
|
||||||
|
"windows"
|
||||||
],
|
],
|
||||||
"budget": None,
|
"budget": None,
|
||||||
"scenario_name": "Do when void",
|
"scenario_name": "Do when void",
|
||||||
|
|
|
||||||
|
|
@ -168,9 +168,8 @@ class Costs:
|
||||||
# https://www.greenmatch.co.uk/windows/double-glazing/cost
|
# https://www.greenmatch.co.uk/windows/double-glazing/cost
|
||||||
SASH_WINDOW_INFLATION_FACTOR = 1.5
|
SASH_WINDOW_INFLATION_FACTOR = 1.5
|
||||||
|
|
||||||
# Typically, secondary glazing can be installed for 25% of the cost of double glazed windows - to be conservative,
|
# Based on relative costs from SCIS
|
||||||
# we scale the cost by half
|
SECONDARY_GLAZING_SCALING_FACTOR = 0.85
|
||||||
SECONDARY_GLAZING_SCALING_FACTOR = 0.5
|
|
||||||
|
|
||||||
def __init__(self, property_instance):
|
def __init__(self, property_instance):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,9 @@ class LightingRecommendations:
|
||||||
# worth more than 2 points, but this is unlikely in the context of other upgrades that can be made to the property
|
# worth more than 2 points, but this is unlikely in the context of other upgrades that can be made to the property
|
||||||
SAP_LIMIT = 2
|
SAP_LIMIT = 2
|
||||||
|
|
||||||
|
# If more than 50% of the lighting is LEDs already, the limit is 1 SAP point
|
||||||
|
SAP_LOWER_LIMIT = 1
|
||||||
|
|
||||||
def __init__(self, property_instance: Property, materials: List):
|
def __init__(self, property_instance: Property, materials: List):
|
||||||
"""
|
"""
|
||||||
:param property_instance: Instance of the Property class, for the home associated to property_id
|
:param property_instance: Instance of the Property class, for the home associated to property_id
|
||||||
|
|
@ -128,6 +131,7 @@ class LightingRecommendations:
|
||||||
"description_simulation": {
|
"description_simulation": {
|
||||||
"lighting-energy-eff": "Very Good",
|
"lighting-energy-eff": "Very Good",
|
||||||
"lighting-description": "Low energy lighting in all fixed outlets",
|
"lighting-description": "Low energy lighting in all fixed outlets",
|
||||||
|
"low-energy-lighting": 100,
|
||||||
},
|
},
|
||||||
**cost_result,
|
**cost_result,
|
||||||
"survey": leds_recommendation_config.get("survey", False)
|
"survey": leds_recommendation_config.get("survey", False)
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,7 @@ class Recommendations:
|
||||||
property_recommendations = []
|
property_recommendations = []
|
||||||
phase = 0
|
phase = 0
|
||||||
measures = self.find_included_measures()
|
measures = self.find_included_measures()
|
||||||
|
non_invasive_recommendation_types = [r["type"] for r in self.property_instance.non_invasive_recommendations]
|
||||||
|
|
||||||
# Building Fabric
|
# Building Fabric
|
||||||
self.wall_recomender.recommend(phase=phase, measures=measures)
|
self.wall_recomender.recommend(phase=phase, measures=measures)
|
||||||
|
|
@ -149,7 +150,8 @@ class Recommendations:
|
||||||
property_recommendations.append(self.floor_recommender.recommendations)
|
property_recommendations.append(self.floor_recommender.recommendations)
|
||||||
phase += 1
|
phase += 1
|
||||||
|
|
||||||
if "windows" in measures:
|
if "windows" in measures and "mixed_glazing" not in non_invasive_recommendation_types:
|
||||||
|
# If we have a mixed glazing recommendation, we prioritise this over the windows recommendation
|
||||||
self.windows_recommender.recommend(phase=phase)
|
self.windows_recommender.recommend(phase=phase)
|
||||||
if self.windows_recommender.recommendation:
|
if self.windows_recommender.recommendation:
|
||||||
property_recommendations.append(self.windows_recommender.recommendation)
|
property_recommendations.append(self.windows_recommender.recommendation)
|
||||||
|
|
@ -529,7 +531,12 @@ class Recommendations:
|
||||||
|
|
||||||
# For the moment, we cap the number of SAP points that can be achieved by LEDs at 2
|
# For the moment, we cap the number of SAP points that can be achieved by LEDs at 2
|
||||||
if rec["type"] == "low_energy_lighting":
|
if rec["type"] == "low_energy_lighting":
|
||||||
property_phase_impact["sap"] = min(property_phase_impact["sap"], LightingRecommendations.SAP_LIMIT)
|
|
||||||
|
if property_instance.data["low-energy-lighting"] < 50:
|
||||||
|
lighting_sap_limit = LightingRecommendations.SAP_LIMIT
|
||||||
|
else:
|
||||||
|
lighting_sap_limit = LightingRecommendations.SAP_LOWER_LIMIT
|
||||||
|
property_phase_impact["sap"] = min(property_phase_impact["sap"], lighting_sap_limit)
|
||||||
property_phase_impact["carbon"] = min(
|
property_phase_impact["carbon"] = min(
|
||||||
property_phase_impact["carbon"], rec["co2_equivalent_savings"]
|
property_phase_impact["carbon"], rec["co2_equivalent_savings"]
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -155,23 +155,44 @@ class RoofRecommendations:
|
||||||
)
|
)
|
||||||
|
|
||||||
self.estimated_u_value = u_value
|
self.estimated_u_value = u_value
|
||||||
if (u_value <= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE) or (
|
if (u_value <= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE) or all(
|
||||||
"loft_insulation" not in measures
|
m not in measures for m in MEASURE_MAP["roof_insulation"]
|
||||||
):
|
):
|
||||||
# The Roof is already compliant
|
# The Roof is already compliant
|
||||||
return
|
return
|
||||||
|
|
||||||
if (self.property.roof["is_pitched"] and "loft_insulation" in measures) or (
|
non_invasive_recommendations = self.property.non_invasive_recommendations
|
||||||
self.property.roof["is_flat"] and "flat_roof_insulation" in measures
|
|
||||||
|
# We firstly handle non-intrusive recommendations, which may override the normal roof insulation recommendations
|
||||||
|
if ("loft_insulation" in [x["type"] for x in non_invasive_recommendations]) or (
|
||||||
|
self.property.roof["is_pitched"] and "loft_insulation" in measures
|
||||||
):
|
):
|
||||||
insulation_thickness = 0 if "loft_insulation" not in measures else self.insulation_thickness
|
self.recommend_roof_insulation(
|
||||||
self.recommend_roof_insulation(u_value, insulation_thickness, self.property.roof, phase)
|
u_value=u_value,
|
||||||
|
insulation_thickness=self.insulation_thickness,
|
||||||
|
phase=phase,
|
||||||
|
is_flat=False,
|
||||||
|
is_pitched=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if (
|
||||||
|
(self.property.roof["is_flat"] and "flat_roof_insulation" in measures) or
|
||||||
|
"flat_roof_insulation" in [x["type"] for x in non_invasive_recommendations]
|
||||||
|
):
|
||||||
|
self.recommend_roof_insulation(
|
||||||
|
u_value=u_value,
|
||||||
|
insulation_thickness=0,
|
||||||
|
phase=phase,
|
||||||
|
is_flat=True,
|
||||||
|
is_pitched=False
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# There are cases where the property might have a room roof as the second roof, but we have a recommendation for
|
# There are cases where the property might have a room roof as the second roof, but we have a recommendation for
|
||||||
# it, so we allow this override
|
# it, so we allow this override
|
||||||
if self.property.roof["is_roof_room"] and ("room_roof_insulation" in measures) or (
|
if self.property.roof["is_roof_room"] and ("room_roof_insulation" in measures) or (
|
||||||
"room_roof_insulation" in [x["type"] for x in self.property.non_invasive_recommendations]
|
"room_roof_insulation" in [x["type"] for x in non_invasive_recommendations]
|
||||||
):
|
):
|
||||||
self.recommend_room_roof_insulation(u_value, phase)
|
self.recommend_room_roof_insulation(u_value, phase)
|
||||||
return
|
return
|
||||||
|
|
@ -195,7 +216,7 @@ class RoofRecommendations:
|
||||||
raise ValueError("Invalid material type")
|
raise ValueError("Invalid material type")
|
||||||
|
|
||||||
def recommend_roof_insulation(
|
def recommend_roof_insulation(
|
||||||
self, u_value, insulation_thickness, roof, phase
|
self, u_value, insulation_thickness, phase, is_pitched, is_flat
|
||||||
):
|
):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
@ -218,7 +239,9 @@ class RoofRecommendations:
|
||||||
|
|
||||||
:param u_value: U-value of the roof before any retrofit measures have been installed
|
:param u_value: U-value of the roof before any retrofit measures have been installed
|
||||||
:param insulation_thickness: Existing Insulation thickness of the loft
|
:param insulation_thickness: Existing Insulation thickness of the loft
|
||||||
:param roof: dictionary describing the make-up of the roof
|
:param phase: Phase of the recommendation
|
||||||
|
:param is_pitched: Is the roof pitched
|
||||||
|
:param is_flat: Is the roof flat
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
@ -226,10 +249,10 @@ class RoofRecommendations:
|
||||||
# Therefore the price is 100mm + whatever thickness is rolled on top, rolled at a 90 degree angle
|
# Therefore the price is 100mm + whatever thickness is rolled on top, rolled at a 90 degree angle
|
||||||
# from the base layer
|
# from the base layer
|
||||||
|
|
||||||
if roof["is_pitched"]:
|
if is_pitched:
|
||||||
insulation_materials = self.loft_insulation_materials
|
insulation_materials = self.loft_insulation_materials
|
||||||
non_insulation_materials = self.loft_non_insulation_materials
|
non_insulation_materials = self.loft_non_insulation_materials
|
||||||
elif roof["is_flat"]:
|
elif is_flat:
|
||||||
insulation_materials = self.flat_roof_insulation_materials
|
insulation_materials = self.flat_roof_insulation_materials
|
||||||
non_insulation_materials = self.flat_roof_non_insulation_materials
|
non_insulation_materials = self.flat_roof_non_insulation_materials
|
||||||
else:
|
else:
|
||||||
|
|
@ -251,7 +274,7 @@ class RoofRecommendations:
|
||||||
# Note: This requirement is only for loft insulation
|
# Note: This requirement is only for loft insulation
|
||||||
if (
|
if (
|
||||||
(material["depth"] + insulation_thickness) < self.MINIMUM_RECOMMENDED_LOFT_INSULATION
|
(material["depth"] + insulation_thickness) < self.MINIMUM_RECOMMENDED_LOFT_INSULATION
|
||||||
) and roof["is_pitched"]:
|
) and is_pitched:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
part_u_value = r_value_per_mm_to_u_value(material["depth"], material["r_value_per_mm"])
|
part_u_value = r_value_per_mm_to_u_value(material["depth"], material["r_value_per_mm"])
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue