mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
commit
25c95fb749
12 changed files with 488 additions and 16 deletions
|
|
@ -61,7 +61,8 @@ class Property:
|
||||||
n_bedrooms = None
|
n_bedrooms = None
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, id, postcode, address, epc_record, already_installed=None, **kwargs
|
self, id, postcode, address, epc_record, already_installed=None, non_invasive_recommendations=None,
|
||||||
|
**kwargs
|
||||||
):
|
):
|
||||||
|
|
||||||
self.epc_record = epc_record
|
self.epc_record = epc_record
|
||||||
|
|
@ -80,6 +81,10 @@ class Property:
|
||||||
# cost and instead, provide a message that the measure has already been installed
|
# cost and instead, provide a message that the measure has already been installed
|
||||||
|
|
||||||
self.already_installed = ast.literal_eval(already_installed['already_installed']) if already_installed else []
|
self.already_installed = ast.literal_eval(already_installed['already_installed']) if already_installed else []
|
||||||
|
self.non_invasive_recommendations = (
|
||||||
|
ast.literal_eval(non_invasive_recommendations['recommendations']) if
|
||||||
|
non_invasive_recommendations else []
|
||||||
|
)
|
||||||
|
|
||||||
self.uprn = epc_record.get("uprn")
|
self.uprn = epc_record.get("uprn")
|
||||||
self.full_sap_epc = epc_record.get("full_sap_epc")
|
self.full_sap_epc = epc_record.get("full_sap_epc")
|
||||||
|
|
@ -142,6 +147,8 @@ class Property:
|
||||||
|
|
||||||
self.current_adjusted_energy = None
|
self.current_adjusted_energy = None
|
||||||
self.expected_adjusted_energy = None
|
self.expected_adjusted_energy = None
|
||||||
|
self.current_energy_bill = None
|
||||||
|
self.expected_energy_bill = None
|
||||||
|
|
||||||
self.recommendations_scoring_data = []
|
self.recommendations_scoring_data = []
|
||||||
|
|
||||||
|
|
@ -277,6 +284,7 @@ class Property:
|
||||||
recommendation_record=recommendation_record,
|
recommendation_record=recommendation_record,
|
||||||
recommendations=previous_phase_representatives + [rec],
|
recommendations=previous_phase_representatives + [rec],
|
||||||
primary_recommendation_id=rec["recommendation_id"],
|
primary_recommendation_id=rec["recommendation_id"],
|
||||||
|
non_invasive_recommendations=self.non_invasive_recommendations,
|
||||||
)
|
)
|
||||||
self.recommendations_scoring_data.append(scoring_dict)
|
self.recommendations_scoring_data.append(scoring_dict)
|
||||||
|
|
||||||
|
|
@ -286,6 +294,7 @@ class Property:
|
||||||
recommendation_record,
|
recommendation_record,
|
||||||
recommendations: list,
|
recommendations: list,
|
||||||
primary_recommendation_id: int,
|
primary_recommendation_id: int,
|
||||||
|
non_invasive_recommendations: list = None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
This function will iterate through a list of recommendations and apply a simulation for each recommendation
|
This function will iterate through a list of recommendations and apply a simulation for each recommendation
|
||||||
|
|
@ -294,10 +303,12 @@ class Property:
|
||||||
:param recommendation_record: The record of the property, which will be updated
|
:param recommendation_record: The record of the property, which will be updated
|
||||||
:param recommendations: The list of recommendations to apply
|
:param recommendations: The list of recommendations to apply
|
||||||
:param primary_recommendation_id: The id of the primary recommendation, which is used to identify the record
|
:param primary_recommendation_id: The id of the primary recommendation, which is used to identify the record
|
||||||
|
:param non_invasive_recommendations: The list of non-invasive recommendations
|
||||||
:return: The updated recommendation record
|
:return: The updated recommendation record
|
||||||
"""
|
"""
|
||||||
|
|
||||||
output = recommendation_record.copy()
|
output = recommendation_record.copy()
|
||||||
|
non_invasive_recommendations = [] if non_invasive_recommendations is None else non_invasive_recommendations
|
||||||
|
|
||||||
for col in [
|
for col in [
|
||||||
"walls_insulation_thickness",
|
"walls_insulation_thickness",
|
||||||
|
|
@ -316,6 +327,13 @@ class Property:
|
||||||
"external_wall_insulation",
|
"external_wall_insulation",
|
||||||
"cavity_wall_insulation",
|
"cavity_wall_insulation",
|
||||||
]:
|
]:
|
||||||
|
|
||||||
|
# # If we have a non-incasive recommendation that the cavity wall is partially filled, we skip the
|
||||||
|
# # cavity wall insulation recommendation (since on the EPC, the property will look like how it did
|
||||||
|
# # before any works)
|
||||||
|
# if "cavity_surveyed_as_filled_is_partial" in non_invasive_recommendations:
|
||||||
|
# continue
|
||||||
|
|
||||||
# The upgrade made here is to the u-value of the walls and the description of the
|
# The upgrade made here is to the u-value of the walls and the description of the
|
||||||
# insulation thickness
|
# insulation thickness
|
||||||
output["walls_thermal_transmittance_ending"] = recommendation[
|
output["walls_thermal_transmittance_ending"] = recommendation[
|
||||||
|
|
@ -892,12 +910,16 @@ class Property:
|
||||||
|
|
||||||
return component_data
|
return component_data
|
||||||
|
|
||||||
def set_adjusted_energy(self, current_adjusted_energy, expected_adjusted_energy):
|
def set_adjusted_energy(
|
||||||
|
self, current_adjusted_energy, expected_adjusted_energy, current_energy_bill, expected_energy_bill
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Stores these values for usage later
|
Stores these values for usage later
|
||||||
"""
|
"""
|
||||||
self.current_adjusted_energy = current_adjusted_energy
|
self.current_adjusted_energy = current_adjusted_energy
|
||||||
self.expected_adjusted_energy = expected_adjusted_energy
|
self.expected_adjusted_energy = expected_adjusted_energy
|
||||||
|
self.current_energy_bill = current_energy_bill
|
||||||
|
self.expected_energy_bill = expected_energy_bill
|
||||||
|
|
||||||
def set_windows_count(self):
|
def set_windows_count(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ from backend.app.db.models.portfolio import Portfolio
|
||||||
|
|
||||||
|
|
||||||
def aggregate_portfolio_recommendations(
|
def aggregate_portfolio_recommendations(
|
||||||
session, portfolio_id: int, total_valuation_increase: float, labour_days: float
|
session, portfolio_id: int, total_valuation_increase: float, labour_days: float, aggregated_data: dict
|
||||||
):
|
):
|
||||||
# Aggregate multiple fields
|
# Aggregate multiple fields
|
||||||
aggregates = (
|
aggregates = (
|
||||||
|
|
@ -27,6 +27,7 @@ def aggregate_portfolio_recommendations(
|
||||||
"energy_savings": aggregates.energy_savings or 0,
|
"energy_savings": aggregates.energy_savings or 0,
|
||||||
"co2_equivalent_savings": aggregates.co2_equivalent_savings or 0,
|
"co2_equivalent_savings": aggregates.co2_equivalent_savings or 0,
|
||||||
"energy_cost_savings": aggregates.energy_cost_savings or 0,
|
"energy_cost_savings": aggregates.energy_cost_savings or 0,
|
||||||
|
**aggregated_data
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get the portfolio and update the fields
|
# Get the portfolio and update the fields
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,21 @@ class Portfolio(Base):
|
||||||
labour_days = Column(Float)
|
labour_days = Column(Float)
|
||||||
created_at = Column(DateTime, nullable=False, default=datetime.datetime.now(pytz.utc))
|
created_at = Column(DateTime, nullable=False, default=datetime.datetime.now(pytz.utc))
|
||||||
updated_at = Column(DateTime, nullable=False, default=datetime.datetime.now(pytz.utc))
|
updated_at = Column(DateTime, nullable=False, default=datetime.datetime.now(pytz.utc))
|
||||||
|
# Aggregations for summary
|
||||||
|
epc_breakdown_pre_retrofit = Column(Text)
|
||||||
|
epc_breakdown_post_retrofit = Column(Text)
|
||||||
|
n_units_to_retrofit = Column(Integer)
|
||||||
|
co2_per_unit_pre_retrofit = Column(Text)
|
||||||
|
co2_per_unit_post_retrofit = Column(Text)
|
||||||
|
energy_bill_per_unit_pre_retrofit = Column(Text)
|
||||||
|
energy_bill_per_unit_post_retrofit = Column(Text)
|
||||||
|
energy_consumption_per_unit_pre_retrofit = Column(Text)
|
||||||
|
energy_consumption_per_unit_post_retrofit = Column(Text)
|
||||||
|
valuation_improvement_per_unit = Column(Text)
|
||||||
|
cost_per_unit = Column(Text)
|
||||||
|
cost_per_co2_saved = Column(Text)
|
||||||
|
cost_per_sap_point = Column(Text)
|
||||||
|
valuation_return_on_investment = Column(Text)
|
||||||
|
|
||||||
|
|
||||||
class PropertyCreationStatus(enum.Enum):
|
class PropertyCreationStatus(enum.Enum):
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import json
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
|
|
@ -51,12 +52,166 @@ def patch_epc(patch, epc_records):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
for patch_variable, patch_value in patch.items():
|
for patch_variable, patch_value in patch.items():
|
||||||
|
if patch_value == "":
|
||||||
|
continue
|
||||||
if patch_variable in epc_records["original_epc"]:
|
if patch_variable in epc_records["original_epc"]:
|
||||||
epc_records["original_epc"][patch_variable] = patch_value
|
epc_records["original_epc"][patch_variable] = patch_value
|
||||||
|
|
||||||
return epc_records
|
return epc_records
|
||||||
|
|
||||||
|
|
||||||
|
def extract_portfolio_aggregation_data(
|
||||||
|
input_properties, total_valuation_increase, recommendations, new_epc_bands, property_value_increase_ranges
|
||||||
|
):
|
||||||
|
# We aggregate a number of metrics for the portfolio:
|
||||||
|
# 1) A breakdown of the number of properties in each EPC band
|
||||||
|
# a) before retrofit
|
||||||
|
# b) after retrofit
|
||||||
|
# 2) Number of units
|
||||||
|
# 3) Co2/unit
|
||||||
|
# a) before retrofit
|
||||||
|
# b) after retrofit
|
||||||
|
# 4) Energy bill/unit
|
||||||
|
# a) before retrofit
|
||||||
|
# b) after retrofit
|
||||||
|
# 5) Average valuation improvement/unit
|
||||||
|
# 6) Total cost
|
||||||
|
# 7) Cost per unit
|
||||||
|
# 8) £ per CO2 saved
|
||||||
|
# 9) £ per SAP point
|
||||||
|
|
||||||
|
# We need to construct the underlyind data for this
|
||||||
|
|
||||||
|
# Helper function to reformat the EPC data
|
||||||
|
def reformat_epc_data(epc_counts):
|
||||||
|
# Define all possible EPC bands in the required order
|
||||||
|
epc_bands = ["G", "F", "E", "D", "C", "B", "A"]
|
||||||
|
|
||||||
|
# Create the formatted data list by checking each band in the order
|
||||||
|
formatted_data = []
|
||||||
|
for band in epc_bands:
|
||||||
|
# Get the count from the dictionary, defaulting to 0 if not present
|
||||||
|
count = epc_counts.get(band, 0)
|
||||||
|
# Append the formatted dictionary to the list
|
||||||
|
formatted_data.append({"name": band, band: count})
|
||||||
|
|
||||||
|
return formatted_data
|
||||||
|
|
||||||
|
n_units = len(input_properties)
|
||||||
|
|
||||||
|
agg_data = []
|
||||||
|
for p in input_properties:
|
||||||
|
# Get the recommendations for the property - we include all properties, even ones without recommendations
|
||||||
|
property_recommendations = recommendations.get(p.id, [])
|
||||||
|
|
||||||
|
# Get just the default recommendations
|
||||||
|
default_recommendations = [r for r in property_recommendations if r["default"]]
|
||||||
|
|
||||||
|
has_recommendations = len(default_recommendations) > 0
|
||||||
|
|
||||||
|
# We can now calculate multiple outputs based on default recommendations
|
||||||
|
carbon_savings = sum([r["co2_equivalent_savings"] for r in default_recommendations])
|
||||||
|
|
||||||
|
pre_retrofit_co2 = p.data["co2-emissions-current"]
|
||||||
|
post_retrofit_co2 = pre_retrofit_co2 - carbon_savings
|
||||||
|
|
||||||
|
pre_retrofit_energy_bill = p.current_energy_bill
|
||||||
|
post_retrofit_energy_bill = p.current_energy_bill - sum(
|
||||||
|
[r["energy_cost_savings"] for r in default_recommendations]
|
||||||
|
)
|
||||||
|
|
||||||
|
pre_retrofit_energy_consumption = p.current_adjusted_energy
|
||||||
|
post_retrofit_energy_consumption = p.current_adjusted_energy - sum(
|
||||||
|
[r["adjusted_heat_demand"] for r in default_recommendations]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add up energy savings
|
||||||
|
cost = sum([r["total"] for r in default_recommendations])
|
||||||
|
sap_point_improvement = sum([r["sap_points"] for r in default_recommendations])
|
||||||
|
|
||||||
|
lower_bound_valuation_uplift = (
|
||||||
|
property_value_increase_ranges[p.id]["lower_bound_increased_value"] -
|
||||||
|
property_value_increase_ranges[p.id]["current_value"]
|
||||||
|
)
|
||||||
|
upper_bound_valuation_uplift = (
|
||||||
|
property_value_increase_ranges[p.id]["upper_bound_increased_value"] -
|
||||||
|
property_value_increase_ranges[p.id]["current_value"]
|
||||||
|
)
|
||||||
|
|
||||||
|
agg_data.append({
|
||||||
|
"pre_retrofit_epc": p.data["current-energy-rating"],
|
||||||
|
"post_retrofit_epc": new_epc_bands[p.id],
|
||||||
|
"pre_retrofit_co2": pre_retrofit_co2,
|
||||||
|
"post_retrofit_co2": post_retrofit_co2,
|
||||||
|
"pre_retrofit_energy_bill": pre_retrofit_energy_bill,
|
||||||
|
"post_retrofit_energy_bill": post_retrofit_energy_bill,
|
||||||
|
"pre_retrofit_energy_consumption": pre_retrofit_energy_consumption,
|
||||||
|
"post_retrofit_energy_consumption": post_retrofit_energy_consumption,
|
||||||
|
"cost": cost,
|
||||||
|
"sap_point_improvement": sap_point_improvement,
|
||||||
|
"lower_bound_valuation_uplift": lower_bound_valuation_uplift,
|
||||||
|
"upper_bound_valuation_uplift": upper_bound_valuation_uplift,
|
||||||
|
"has_recommendations": has_recommendations
|
||||||
|
})
|
||||||
|
|
||||||
|
agg_data = pd.DataFrame(agg_data)
|
||||||
|
|
||||||
|
n_units_to_retrofit = agg_data["has_recommendations"].sum()
|
||||||
|
|
||||||
|
valuation_improvement_lower_bound_per_unit = (
|
||||||
|
agg_data["lower_bound_valuation_uplift"].mean()
|
||||||
|
)
|
||||||
|
valuation_improvement_upper_bound_per_unit = (
|
||||||
|
agg_data["upper_bound_valuation_uplift"].mean()
|
||||||
|
)
|
||||||
|
|
||||||
|
total_carbon_saved = agg_data["pre_retrofit_co2"].sum() - agg_data["post_retrofit_co2"].sum()
|
||||||
|
total_sap_points = agg_data["sap_point_improvement"].sum()
|
||||||
|
|
||||||
|
def format_money(amount):
|
||||||
|
return f"£{amount:,.0f}"
|
||||||
|
|
||||||
|
valuation_improvment_per_unit = str(
|
||||||
|
format_money(
|
||||||
|
total_valuation_increase / n_units) + (f" ({format_money(valuation_improvement_lower_bound_per_unit)} - "
|
||||||
|
f"{format_money(valuation_improvement_upper_bound_per_unit)})")
|
||||||
|
)
|
||||||
|
|
||||||
|
valuation_return_on_investment = str(
|
||||||
|
str(round(total_valuation_increase / agg_data["cost"].sum(), 2)) +
|
||||||
|
f" ("
|
||||||
|
f"{agg_data['lower_bound_valuation_uplift'].sum() / agg_data['cost'].sum():,.2f} - "
|
||||||
|
f"{agg_data['upper_bound_valuation_uplift'].sum() / agg_data['cost'].sum():,.2f})"
|
||||||
|
)
|
||||||
|
|
||||||
|
aggregation_data = {
|
||||||
|
"epc_breakdown_pre_retrofit": json.dumps(
|
||||||
|
reformat_epc_data(agg_data["pre_retrofit_epc"].value_counts().to_dict())
|
||||||
|
),
|
||||||
|
"epc_breakdown_post_retrofit": json.dumps(
|
||||||
|
reformat_epc_data(agg_data["post_retrofit_epc"].value_counts().to_dict())
|
||||||
|
),
|
||||||
|
"number_of_properties": int(n_units),
|
||||||
|
"n_units_to_retrofit": int(n_units_to_retrofit),
|
||||||
|
"co2_per_unit_pre_retrofit": str(round(agg_data["pre_retrofit_co2"].mean(), 2)) + "t",
|
||||||
|
"co2_per_unit_post_retrofit": str(round(agg_data["post_retrofit_co2"].mean(), 2)) + "t",
|
||||||
|
"energy_bill_per_unit_pre_retrofit": format_money(agg_data["pre_retrofit_energy_bill"].mean()),
|
||||||
|
"energy_bill_per_unit_post_retrofit": format_money(agg_data["post_retrofit_energy_bill"].mean()),
|
||||||
|
"energy_consumption_per_unit_pre_retrofit": str(
|
||||||
|
round(agg_data["pre_retrofit_energy_consumption"].mean())) + "kWh",
|
||||||
|
"energy_consumption_per_unit_post_retrofit": str(
|
||||||
|
round(agg_data["post_retrofit_energy_consumption"].mean())) + "kWh",
|
||||||
|
"valuation_improvement_per_unit": valuation_improvment_per_unit,
|
||||||
|
"cost_per_unit": format_money(agg_data["cost"].mean()),
|
||||||
|
"cost_per_co2_saved": format_money(agg_data["cost"].sum() / total_carbon_saved),
|
||||||
|
"cost_per_sap_point": format_money(agg_data["cost"].sum() / total_sap_points),
|
||||||
|
"valuation_return_on_investment": valuation_return_on_investment,
|
||||||
|
# TODO: Could we add 10yr carbon credits value?
|
||||||
|
}
|
||||||
|
|
||||||
|
return aggregation_data
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter(
|
router = APIRouter(
|
||||||
prefix="/plan",
|
prefix="/plan",
|
||||||
tags=["plan"],
|
tags=["plan"],
|
||||||
|
|
@ -91,6 +246,12 @@ async def trigger_plan(body: PlanTriggerRequest):
|
||||||
bucket_name=get_settings().PLAN_TRIGGER_BUCKET, filepath=body.already_installed_file_path
|
bucket_name=get_settings().PLAN_TRIGGER_BUCKET, filepath=body.already_installed_file_path
|
||||||
)
|
)
|
||||||
|
|
||||||
|
non_invasive_recommendations = []
|
||||||
|
if body.non_invasive_recommendations_file_path:
|
||||||
|
non_invasive_recommendations = read_csv_from_s3(
|
||||||
|
bucket_name=get_settings().PLAN_TRIGGER_BUCKET, filepath=body.non_invasive_recommendations_file_path
|
||||||
|
)
|
||||||
|
|
||||||
cleaning_data = read_dataframe_from_s3_parquet(
|
cleaning_data = read_dataframe_from_s3_parquet(
|
||||||
bucket_name=get_settings().DATA_BUCKET, file_key="sap_change_model/cleaning_dataset.parquet",
|
bucket_name=get_settings().DATA_BUCKET, file_key="sap_change_model/cleaning_dataset.parquet",
|
||||||
)
|
)
|
||||||
|
|
@ -146,6 +307,12 @@ async def trigger_plan(body: PlanTriggerRequest):
|
||||||
x for x in already_installed if
|
x for x in already_installed if
|
||||||
(x["address"] == config["address"]) and (x["postcode"] == config["postcode"])
|
(x["address"] == config["address"]) and (x["postcode"] == config["postcode"])
|
||||||
), {})
|
), {})
|
||||||
|
|
||||||
|
property_non_invasive_recommendations = next((
|
||||||
|
x for x in non_invasive_recommendations if
|
||||||
|
(x["address"] == config["address"]) and (x["postcode"] == config["postcode"])
|
||||||
|
), {})
|
||||||
|
|
||||||
input_properties.append(
|
input_properties.append(
|
||||||
Property(
|
Property(
|
||||||
id=property_id,
|
id=property_id,
|
||||||
|
|
@ -153,6 +320,7 @@ async def trigger_plan(body: PlanTriggerRequest):
|
||||||
postcode=epc_searcher.postcode_clean,
|
postcode=epc_searcher.postcode_clean,
|
||||||
epc_record=prepared_epc,
|
epc_record=prepared_epc,
|
||||||
already_installed=property_already_installed,
|
already_installed=property_already_installed,
|
||||||
|
non_invasive_recommendations=property_non_invasive_recommendations,
|
||||||
**Property.extract_kwargs(config)
|
**Property.extract_kwargs(config)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -243,7 +411,13 @@ async def trigger_plan(body: PlanTriggerRequest):
|
||||||
|
|
||||||
property_instance = [p for p in input_properties if p.id == property_id][0]
|
property_instance = [p for p in input_properties if p.id == property_id][0]
|
||||||
|
|
||||||
recommendations_with_impact, current_adjusted_energy, expected_adjusted_energy = (
|
(
|
||||||
|
recommendations_with_impact,
|
||||||
|
current_adjusted_energy,
|
||||||
|
expected_adjusted_energy,
|
||||||
|
current_energy_bill,
|
||||||
|
expected_energy_bill
|
||||||
|
) = (
|
||||||
Recommendations.calculate_recommendation_impact(
|
Recommendations.calculate_recommendation_impact(
|
||||||
property_instance=property_instance,
|
property_instance=property_instance,
|
||||||
all_predictions=all_predictions,
|
all_predictions=all_predictions,
|
||||||
|
|
@ -254,7 +428,9 @@ async def trigger_plan(body: PlanTriggerRequest):
|
||||||
# Store the resulting adjusted energy in the property instance
|
# Store the resulting adjusted energy in the property instance
|
||||||
property_instance.set_adjusted_energy(
|
property_instance.set_adjusted_energy(
|
||||||
current_adjusted_energy=current_adjusted_energy,
|
current_adjusted_energy=current_adjusted_energy,
|
||||||
expected_adjusted_energy=expected_adjusted_energy
|
expected_adjusted_energy=expected_adjusted_energy,
|
||||||
|
current_energy_bill=current_energy_bill,
|
||||||
|
expected_energy_bill=expected_energy_bill
|
||||||
)
|
)
|
||||||
|
|
||||||
input_measures = prepare_input_measures(recommendations_with_impact, body.goal)
|
input_measures = prepare_input_measures(recommendations_with_impact, body.goal)
|
||||||
|
|
@ -316,6 +492,8 @@ async def trigger_plan(body: PlanTriggerRequest):
|
||||||
logger.info("Uploading recommendations to the database")
|
logger.info("Uploading recommendations to the database")
|
||||||
property_valuation_increases = []
|
property_valuation_increases = []
|
||||||
session.commit()
|
session.commit()
|
||||||
|
new_epc_bands = {}
|
||||||
|
property_value_increase_ranges = {}
|
||||||
for i in range(0, len(input_properties), BATCH_SIZE):
|
for i in range(0, len(input_properties), BATCH_SIZE):
|
||||||
try:
|
try:
|
||||||
# Take a slice of the input_properties list to make a batch
|
# Take a slice of the input_properties list to make a batch
|
||||||
|
|
@ -327,8 +505,10 @@ async def trigger_plan(body: PlanTriggerRequest):
|
||||||
total_sap_points = sum([r["sap_points"] for r in default_recommendations])
|
total_sap_points = sum([r["sap_points"] for r in default_recommendations])
|
||||||
new_sap_points = float(p.data["current-energy-efficiency"]) + total_sap_points
|
new_sap_points = float(p.data["current-energy-efficiency"]) + total_sap_points
|
||||||
new_epc = sap_to_epc(new_sap_points)
|
new_epc = sap_to_epc(new_sap_points)
|
||||||
|
new_epc_bands[p.id] = new_epc
|
||||||
|
|
||||||
valuations = PropertyValuation.estimate(property_instance=p, target_epc=new_epc)
|
valuations = PropertyValuation.estimate(property_instance=p, target_epc=new_epc)
|
||||||
|
property_value_increase_ranges[p.id] = valuations
|
||||||
|
|
||||||
# Your existing operations
|
# Your existing operations
|
||||||
property_details_epc = p.get_property_details_epc(
|
property_details_epc = p.get_property_details_epc(
|
||||||
|
|
@ -392,11 +572,20 @@ async def trigger_plan(body: PlanTriggerRequest):
|
||||||
[sum(r["labour_days"] for r in rec_group if r["default"]) for p_id, rec_group in recommendations.items()]
|
[sum(r["labour_days"] for r in rec_group if r["default"]) for p_id, rec_group in recommendations.items()]
|
||||||
))
|
))
|
||||||
|
|
||||||
|
aggregated_data = extract_portfolio_aggregation_data(
|
||||||
|
input_properties=input_properties,
|
||||||
|
total_valuation_increase=total_valuation_increase,
|
||||||
|
recommendations=recommendations,
|
||||||
|
new_epc_bands=new_epc_bands,
|
||||||
|
property_value_increase_ranges=property_value_increase_ranges
|
||||||
|
)
|
||||||
|
|
||||||
aggregate_portfolio_recommendations(
|
aggregate_portfolio_recommendations(
|
||||||
session,
|
session,
|
||||||
portfolio_id=body.portfolio_id,
|
portfolio_id=body.portfolio_id,
|
||||||
total_valuation_increase=total_valuation_increase,
|
total_valuation_increase=total_valuation_increase,
|
||||||
labour_days=labour_days
|
labour_days=labour_days,
|
||||||
|
aggregated_data=aggregated_data
|
||||||
)
|
)
|
||||||
|
|
||||||
# Commit final changes
|
# Commit final changes
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ class PlanTriggerRequest(BaseModel):
|
||||||
trigger_file_path: str
|
trigger_file_path: str
|
||||||
already_installed_file_path: Optional[str] = None
|
already_installed_file_path: Optional[str] = None
|
||||||
patches_file_path: Optional[str] = None
|
patches_file_path: Optional[str] = None
|
||||||
|
non_invasive_recommendations_file_path: Optional[str] = None
|
||||||
exclusions: Optional[conlist(str, min_items=1)] = None
|
exclusions: Optional[conlist(str, min_items=1)] = None
|
||||||
|
|
||||||
# Pre-defined list of possibilities for exclusions
|
# Pre-defined list of possibilities for exclusions
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ council_tax_bands = pd.DataFrame(council_tax_bands)
|
||||||
|
|
||||||
# This is information we need to override on the EPC itself, for instance if a new survey has been conducted and
|
# This is information we need to override on the EPC itself, for instance if a new survey has been conducted and
|
||||||
# that has not reached the API
|
# that has not reached the API
|
||||||
|
# For 53 Bromley, the non-invasives found the walls to be partially filled
|
||||||
patches = [
|
patches = [
|
||||||
{
|
{
|
||||||
'address': '6 Beech Road', 'postcode': 'DY1 4BP',
|
'address': '6 Beech Road', 'postcode': 'DY1 4BP',
|
||||||
|
|
@ -42,7 +43,11 @@ patches = [
|
||||||
'energy-consumption-current': '491',
|
'energy-consumption-current': '491',
|
||||||
'co2-emissions-current': '5.0',
|
'co2-emissions-current': '5.0',
|
||||||
'potential-energy-efficiency': '87'
|
'potential-energy-efficiency': '87'
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
'address': '53 Bromley', 'postcode': 'DY5 4PJ',
|
||||||
|
'walls-description': 'Cavity wall, partial insulation (assumed)',
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
# This is information that is found as a result of the non-invasives, that mean that certain measures
|
# This is information that is found as a result of the non-invasives, that mean that certain measures
|
||||||
|
|
@ -56,6 +61,19 @@ already_installed = [
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
non_invasive_recommendations = [
|
||||||
|
{'address': '8 Corporation Road', 'postcode': 'DY2 7PX', 'recommendations': []},
|
||||||
|
{'address': '21 Wells Road', 'postcode': 'DY5 3TB', 'recommendations': ['cavity_extract_and_refill']},
|
||||||
|
{'address': '27 Milton Road', 'postcode': 'WV14 8HZ', 'recommendations': ['cavity_extract_and_refill']},
|
||||||
|
{'address': '195 Ashenhurst Road', 'postcode': 'DY1 2JB', 'recommendations': ['cavity_extract_and_refill']},
|
||||||
|
{'address': '53 Bromley', 'postcode': 'DY5 4PJ', 'recommendations': ['cavity_surveyed_as_filled_is_partial']},
|
||||||
|
{'address': '91 Osprey Drive', 'postcode': 'DY1 2JS', 'recommendations': ['cavity_extract_and_refill']},
|
||||||
|
{'address': '47 Fairfield Road', 'postcode': 'DY8 5UJ', 'recommendations': ['cavity_extract_and_refill']},
|
||||||
|
{'address': '150 Huntingtree Road', 'postcode': 'B63 4HP', 'recommendations': ['cavity_extract_and_refill']},
|
||||||
|
{'address': '6 Beech Road', 'postcode': 'DY1 4BP', 'recommendations': []},
|
||||||
|
{'address': '5 Oaklands', 'postcode': 'B62 0JA', 'recommendations': ['cavity_extract_and_refill']},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def app():
|
def app():
|
||||||
raw_asset_list = read_excel_from_s3(
|
raw_asset_list = read_excel_from_s3(
|
||||||
|
|
@ -102,6 +120,14 @@ def app():
|
||||||
file_name=patches_filename
|
file_name=patches_filename
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Store non-invasive recommendations in S3
|
||||||
|
non_invasive_recommendations_filename = f"{USER_ID}/{PORTFOLIO_ID}/non_invasive_recommendations.json"
|
||||||
|
save_csv_to_s3(
|
||||||
|
dataframe=pd.DataFrame(non_invasive_recommendations),
|
||||||
|
bucket_name="retrofit-plan-inputs-dev",
|
||||||
|
file_name=non_invasive_recommendations_filename
|
||||||
|
)
|
||||||
|
|
||||||
# EPC C portoflio
|
# EPC C portoflio
|
||||||
body = {
|
body = {
|
||||||
"portfolio_id": str(PORTFOLIO_ID),
|
"portfolio_id": str(PORTFOLIO_ID),
|
||||||
|
|
@ -111,6 +137,7 @@ def app():
|
||||||
"trigger_file_path": filename,
|
"trigger_file_path": filename,
|
||||||
"already_installed_file_path": already_installed_filename,
|
"already_installed_file_path": already_installed_filename,
|
||||||
"patches_file_path": patches_filename,
|
"patches_file_path": patches_filename,
|
||||||
|
"non_invasive_recommendations_file_path": non_invasive_recommendations_filename,
|
||||||
"budget": None,
|
"budget": None,
|
||||||
}
|
}
|
||||||
print(body)
|
print(body)
|
||||||
|
|
@ -124,6 +151,7 @@ def app():
|
||||||
"trigger_file_path": filename,
|
"trigger_file_path": filename,
|
||||||
"already_installed_file_path": already_installed_filename,
|
"already_installed_file_path": already_installed_filename,
|
||||||
"patches_file_path": patches_filename,
|
"patches_file_path": patches_filename,
|
||||||
|
"non_invasive_recommendations_file_path": non_invasive_recommendations_filename,
|
||||||
"budget": None,
|
"budget": None,
|
||||||
}
|
}
|
||||||
print(body)
|
print(body)
|
||||||
|
|
|
||||||
19
etl/non_intrusive_surveys/photos/README.md
Normal file
19
etl/non_intrusive_surveys/photos/README.md
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
# Non Intrusive Surveys - photo upload
|
||||||
|
|
||||||
|
This folder contains photos taken during non-intrusive surveys. Photos are stored in folders named after the survey ID.
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
Install the required packages by running the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
The main application is found in the app.py file. To run the application, use the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python app.py
|
||||||
|
```
|
||||||
149
etl/non_intrusive_surveys/photos/app.py
Normal file
149
etl/non_intrusive_surveys/photos/app.py
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
import boto3
|
||||||
|
import os
|
||||||
|
from PIL import Image
|
||||||
|
from pathlib import Path
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# Inputs
|
||||||
|
ENV_FILEPATH = "etl/non_intrusive_surveys/photos/.env"
|
||||||
|
PHOTO_DIRECTORY = "/Users/khalimconn-kowlessar/Downloads/IMMO - Dudley Pilot - non-invasive raw data"
|
||||||
|
FOLDER_UPRN_LOOKUP = {
|
||||||
|
"91 Osprey Drive DY1 2JS": 90048026,
|
||||||
|
"195 Ashenhurst Rd DY1 2JB": 90051858,
|
||||||
|
"6 Beech Rd DY1 4BP": 90055152,
|
||||||
|
"53 Bromley DY5 4PJ": 90060989,
|
||||||
|
"5 Oaklands B62 0JA": 90028499,
|
||||||
|
"47 Fairfield Rd DY8 5UJ": 90077535,
|
||||||
|
"150 Huntingtree Rd B63 4HP": 90093693,
|
||||||
|
"27 Milton Rd DY1 2JB": 90106884,
|
||||||
|
"21 Wells Rd DY5 3TB": 90022227,
|
||||||
|
"8 Corporation Rd DY2 7PX": 90070461
|
||||||
|
}
|
||||||
|
|
||||||
|
load_dotenv(ENV_FILEPATH)
|
||||||
|
CLOUDFRONT_DISTRIBUTION_DOMAIN_NAME = os.getenv("CLOUDFRONT_DISTRIBUTION_DOMAIN_NAME", None)
|
||||||
|
CDN_BUCKET_NAME = os.getenv("CDN_BUCKET_NAME", None)
|
||||||
|
|
||||||
|
|
||||||
|
def list_subdirectories(directory_path):
|
||||||
|
"""
|
||||||
|
List all subdirectories within a given directory.
|
||||||
|
|
||||||
|
:param directory_path: Path to the directory.
|
||||||
|
:return: A list of paths to the subdirectories.
|
||||||
|
"""
|
||||||
|
directory = Path(directory_path)
|
||||||
|
subdirectories = [subdir for subdir in directory.iterdir() if subdir.is_dir()]
|
||||||
|
return subdirectories
|
||||||
|
|
||||||
|
|
||||||
|
def list_files_in_directory(directory_path, file_extension=".jpg"):
|
||||||
|
"""
|
||||||
|
List all files with a specific extension within a given directory and its subdirectories.
|
||||||
|
|
||||||
|
:param directory_path: Path to the directory to scan.
|
||||||
|
:param file_extension: File extension to filter by.
|
||||||
|
:return: A list of paths to the files.
|
||||||
|
"""
|
||||||
|
# Convert the directory path to a Path object if it's not already one
|
||||||
|
directory = Path(directory_path) if not isinstance(directory_path, Path) else directory_path
|
||||||
|
|
||||||
|
# List all files of the specified type in the directory and subdirectories
|
||||||
|
file_list = [file for file in directory.rglob(f'*{file_extension}')]
|
||||||
|
|
||||||
|
return file_list
|
||||||
|
|
||||||
|
|
||||||
|
def create_images(input_path, uprn):
|
||||||
|
# Define the base directory path
|
||||||
|
base_directory = f"non_intrusive_photos/{uprn}"
|
||||||
|
print(f"Creating directory: {base_directory}") # Debug: print the directory to be created
|
||||||
|
|
||||||
|
# Need to create local directory if it doesn't exist
|
||||||
|
os.makedirs(base_directory, exist_ok=True)
|
||||||
|
|
||||||
|
# Define output paths
|
||||||
|
thumbnail_path = os.path.join(base_directory, "thumbnail.jpg")
|
||||||
|
full_hd_path = os.path.join(base_directory, "1080p.jpg")
|
||||||
|
webp_path = os.path.join(base_directory, "webp.webp") # Save as WebP format
|
||||||
|
|
||||||
|
# Load the image
|
||||||
|
with Image.open(input_path) as img:
|
||||||
|
# Create a thumbnail
|
||||||
|
thumbnail = img.copy()
|
||||||
|
thumbnail.thumbnail((128, 128), Image.Resampling.LANCZOS)
|
||||||
|
thumbnail.save(thumbnail_path, 'JPEG', quality=85)
|
||||||
|
|
||||||
|
# Create a 1080p version
|
||||||
|
full_hd = img.copy()
|
||||||
|
full_hd.thumbnail((1920, 1080), Image.Resampling.LANCZOS)
|
||||||
|
full_hd.save(full_hd_path, 'JPEG', quality=90)
|
||||||
|
|
||||||
|
# Convert to WebP for better compression
|
||||||
|
webp = img.copy()
|
||||||
|
webp.save(webp_path, 'WEBP', quality=90)
|
||||||
|
|
||||||
|
# Return paths to the processed images
|
||||||
|
return thumbnail_path, full_hd_path, webp_path
|
||||||
|
|
||||||
|
|
||||||
|
def upload_to_s3(bucket_name, file_path, object_name):
|
||||||
|
s3_client = boto3.client('s3')
|
||||||
|
s3_client.upload_file(file_path, bucket_name, object_name)
|
||||||
|
print(f"Uploaded {object_name} to S3 bucket {bucket_name}")
|
||||||
|
|
||||||
|
|
||||||
|
def upload_photos_to_s3(bucket_name, photo_paths):
|
||||||
|
# Upload each photo
|
||||||
|
for path in photo_paths:
|
||||||
|
object_name = path.split('/')[-1] # Assuming the path format is folder/filename
|
||||||
|
upload_to_s3(bucket_name, path, object_name)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_cdn_url(distribution_domain, object_name):
|
||||||
|
return f"https://{distribution_domain}/{object_name}"
|
||||||
|
|
||||||
|
|
||||||
|
def process_and_upload_images(uprn, input_image_path, bucket_name, distribution_domain):
|
||||||
|
# Create images
|
||||||
|
thumbnail, full_hd, original = create_images(input_image_path, uprn=str(uprn))
|
||||||
|
|
||||||
|
# Upload images
|
||||||
|
upload_photos_to_s3(bucket_name, photo_paths=[thumbnail, full_hd, original])
|
||||||
|
|
||||||
|
# Generate CDN links
|
||||||
|
cdn_links = [generate_cdn_url(distribution_domain, path.split('/')[-1]) for path in [thumbnail, full_hd, original]]
|
||||||
|
|
||||||
|
# Delete local files
|
||||||
|
for path in [thumbnail, full_hd, original]:
|
||||||
|
os.remove(path)
|
||||||
|
|
||||||
|
return cdn_links
|
||||||
|
|
||||||
|
|
||||||
|
def app():
|
||||||
|
"""
|
||||||
|
This application is tasked with uploading the photos, recorded during the non-invasive surveys, to s3 and the
|
||||||
|
database.
|
||||||
|
To begin with, this app will simply read the files from the local machine, however we will come up with a more
|
||||||
|
efficient way to do this in the future.
|
||||||
|
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
|
||||||
|
# List all files in the directory using pathlib
|
||||||
|
property_directories = list_subdirectories(PHOTO_DIRECTORY)
|
||||||
|
|
||||||
|
# For each property, we want to list all of the photos in the directory
|
||||||
|
for property_dir in property_directories:
|
||||||
|
photo_files = list_files_in_directory(property_dir)
|
||||||
|
uprn = FOLDER_UPRN_LOOKUP[property_dir.name]
|
||||||
|
|
||||||
|
# We now want to convert each file, and upload it to s3
|
||||||
|
for photo_filepath in photo_files:
|
||||||
|
process_and_upload_images(
|
||||||
|
uprn=uprn,
|
||||||
|
input_image_path=photo_filepath,
|
||||||
|
bucket_name=CDN_BUCKET_NAME,
|
||||||
|
distribution_domain=CLOUDFRONT_DISTRIBUTION_DOMAIN_NAME
|
||||||
|
)
|
||||||
3
etl/non_intrusive_surveys/photos/requirements.txt
Normal file
3
etl/non_intrusive_surveys/photos/requirements.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
Pillow
|
||||||
|
boto3
|
||||||
|
python-dotenv
|
||||||
|
|
@ -91,6 +91,10 @@ DOUBLE_RADIATOR_COST = 300
|
||||||
FLUE_COST = 600
|
FLUE_COST = 600
|
||||||
PIPEWORK_COST = 750 # Min cost is £500
|
PIPEWORK_COST = 750 # Min cost is £500
|
||||||
|
|
||||||
|
# This is the cost per meter squared for cavity extraction
|
||||||
|
# https://www.checkatrade.com/blog/cost-guides/cavity-wall-insulation-removal-cost/
|
||||||
|
CAVITY_EXTRACTION_COST = 21.5
|
||||||
|
|
||||||
|
|
||||||
class Costs:
|
class Costs:
|
||||||
"""
|
"""
|
||||||
|
|
@ -173,7 +177,7 @@ class Costs:
|
||||||
if not self.labour_adjustment_factor:
|
if not self.labour_adjustment_factor:
|
||||||
raise ValueError("Labour adjustment factor not found")
|
raise ValueError("Labour adjustment factor not found")
|
||||||
|
|
||||||
def cavity_wall_insulation(self, wall_area, material):
|
def cavity_wall_insulation(self, wall_area, material, is_extraction_and_refill=False):
|
||||||
"""
|
"""
|
||||||
Calculates the total cost for cavity wall insulation based on material and labor costs,
|
Calculates the total cost for cavity wall insulation based on material and labor costs,
|
||||||
including contingency, preliminaries, profit, and VAT.
|
including contingency, preliminaries, profit, and VAT.
|
||||||
|
|
@ -208,6 +212,13 @@ class Costs:
|
||||||
# Assume a team of 2
|
# Assume a team of 2
|
||||||
labour_days = (labour_hours / 8) / 2
|
labour_days = (labour_hours / 8) / 2
|
||||||
|
|
||||||
|
if is_extraction_and_refill:
|
||||||
|
# bump up the cost of the work
|
||||||
|
total_cost = total_cost + CAVITY_EXTRACTION_COST * wall_area
|
||||||
|
# Additional 2 days work
|
||||||
|
labour_hours = labour_hours + (2 * 8)
|
||||||
|
labour_days = labour_days + 2
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"total": total_cost,
|
"total": total_cost,
|
||||||
"subtotal": subtotal_before_vat,
|
"subtotal": subtotal_before_vat,
|
||||||
|
|
|
||||||
|
|
@ -149,12 +149,14 @@ class Recommendations:
|
||||||
property_recommendations = self.insert_temp_recommendation_id(property_recommendations)
|
property_recommendations = self.insert_temp_recommendation_id(property_recommendations)
|
||||||
|
|
||||||
# We also need to create the representative recommendations for each recommendation type
|
# We also need to create the representative recommendations for each recommendation type
|
||||||
property_representative_recommendations = self.create_representative_recommendations(property_recommendations)
|
property_representative_recommendations = self.create_representative_recommendations(
|
||||||
|
property_recommendations, non_invasive_recommendations=self.property_instance.non_invasive_recommendations
|
||||||
|
)
|
||||||
|
|
||||||
return property_recommendations, property_representative_recommendations
|
return property_recommendations, property_representative_recommendations
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_representative_recommendations(property_recommendations):
|
def create_representative_recommendations(property_recommendations, non_invasive_recommendations):
|
||||||
"""
|
"""
|
||||||
This method will create a representative recommendation for each recommendation type
|
This method will create a representative recommendation for each recommendation type
|
||||||
In order to create a representative recommendation, we choose the recommendation that has:
|
In order to create a representative recommendation, we choose the recommendation that has:
|
||||||
|
|
@ -169,6 +171,13 @@ class Recommendations:
|
||||||
|
|
||||||
for recommendations_by_type in property_recommendations:
|
for recommendations_by_type in property_recommendations:
|
||||||
|
|
||||||
|
# If the property was initially surveyed as filled, but the cavity was only partially filled, we don't
|
||||||
|
# want to include the cavity wall insulation recommendation in the defaults
|
||||||
|
# if (recommendations_by_type[0].get("type") == "cavity_wall_insulation") and (
|
||||||
|
# "cavity_surveyed_as_filled_is_partial" in non_invasive_recommendations
|
||||||
|
# ):
|
||||||
|
# continue
|
||||||
|
|
||||||
if recommendations_by_type[0].get("type") == "mechanical_ventilation":
|
if recommendations_by_type[0].get("type") == "mechanical_ventilation":
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
@ -238,13 +247,13 @@ class Recommendations:
|
||||||
|
|
||||||
property_sap_predictions = all_predictions["sap_change_predictions"][
|
property_sap_predictions = all_predictions["sap_change_predictions"][
|
||||||
all_predictions["sap_change_predictions"]["property_id"] == str(property_instance.id)
|
all_predictions["sap_change_predictions"]["property_id"] == str(property_instance.id)
|
||||||
]
|
].copy()
|
||||||
property_heat_predictions = all_predictions["heat_demand_predictions"][
|
property_heat_predictions = all_predictions["heat_demand_predictions"][
|
||||||
all_predictions["heat_demand_predictions"]["property_id"] == str(property_instance.id)
|
all_predictions["heat_demand_predictions"]["property_id"] == str(property_instance.id)
|
||||||
]
|
].copy()
|
||||||
property_carbon_predictions = all_predictions["carbon_change_predictions"][
|
property_carbon_predictions = all_predictions["carbon_change_predictions"][
|
||||||
all_predictions["carbon_change_predictions"]["property_id"] == str(property_instance.id)
|
all_predictions["carbon_change_predictions"]["property_id"] == str(property_instance.id)
|
||||||
]
|
].copy()
|
||||||
|
|
||||||
property_recommendations = recommendations[property_instance.id].copy()
|
property_recommendations = recommendations[property_instance.id].copy()
|
||||||
|
|
||||||
|
|
@ -272,6 +281,8 @@ class Recommendations:
|
||||||
current_epc_rating=property_instance.data["current-energy-rating"],
|
current_epc_rating=property_instance.data["current-energy-rating"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# TODO: This isn't quite right as this is based on EVERY possible measure, not just the ones that are
|
||||||
|
# actually implemented
|
||||||
expected_adjusted_energy = AnnualBillSavings.adjust_energy_to_metered(
|
expected_adjusted_energy = AnnualBillSavings.adjust_energy_to_metered(
|
||||||
epc_energy_consumption=expected_heat_demand,
|
epc_energy_consumption=expected_heat_demand,
|
||||||
current_epc_rating=property_instance.data["current-energy-rating"],
|
current_epc_rating=property_instance.data["current-energy-rating"],
|
||||||
|
|
@ -281,6 +292,10 @@ class Recommendations:
|
||||||
current_adjusted_energy - expected_adjusted_energy
|
current_adjusted_energy - expected_adjusted_energy
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# TODO: We should determine if the home is gas & electricity or just electricity
|
||||||
|
current_energy_bill = AnnualBillSavings.calculate_annual_bill(current_adjusted_energy)
|
||||||
|
expected_energy_bill = AnnualBillSavings.calculate_annual_bill(expected_adjusted_energy)
|
||||||
|
|
||||||
for recommendations_by_type in property_recommendations:
|
for recommendations_by_type in property_recommendations:
|
||||||
for rec in recommendations_by_type:
|
for rec in recommendations_by_type:
|
||||||
|
|
||||||
|
|
@ -355,4 +370,10 @@ class Recommendations:
|
||||||
rec["heat_demand"] is None) or (rec["energy_cost_savings"] is None):
|
rec["heat_demand"] is None) or (rec["energy_cost_savings"] is None):
|
||||||
raise ValueError("sap points, co2 or heat demand is missing")
|
raise ValueError("sap points, co2 or heat demand is missing")
|
||||||
|
|
||||||
return property_recommendations, current_adjusted_energy, expected_adjusted_energy
|
return (
|
||||||
|
property_recommendations,
|
||||||
|
current_adjusted_energy,
|
||||||
|
expected_adjusted_energy,
|
||||||
|
current_energy_bill,
|
||||||
|
expected_energy_bill
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,9 @@ class WallRecommendations(Definitions):
|
||||||
insulation_thickness = self.property.walls["insulation_thickness"]
|
insulation_thickness = self.property.walls["insulation_thickness"]
|
||||||
|
|
||||||
# We check if the wall is already insulated and if so, we exit
|
# We check if the wall is already insulated and if so, we exit
|
||||||
if (insulation_thickness in ["average", "above average"]) or self.property.walls["is_filled_cavity"]:
|
if ((insulation_thickness in ["average", "above average"]) or self.property.walls["is_filled_cavity"]) and (
|
||||||
|
"cavity_extract_and_refill" not in self.property.non_invasive_recommendations
|
||||||
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
if u_value:
|
if u_value:
|
||||||
|
|
@ -216,15 +218,26 @@ class WallRecommendations(Definitions):
|
||||||
if new_u_value <= self.BUILDING_REGULATIONS_PART_L_CAVITY_WALL_MAX_U_VALUE:
|
if new_u_value <= self.BUILDING_REGULATIONS_PART_L_CAVITY_WALL_MAX_U_VALUE:
|
||||||
lowest_selected_u_value = update_lowest_selected_u_value(lowest_selected_u_value, new_u_value)
|
lowest_selected_u_value = update_lowest_selected_u_value(lowest_selected_u_value, new_u_value)
|
||||||
|
|
||||||
|
is_extraction_and_refill = "cavity_extract_and_refill" in self.property.non_invasive_recommendations
|
||||||
|
|
||||||
cost_result = self.costs.cavity_wall_insulation(
|
cost_result = self.costs.cavity_wall_insulation(
|
||||||
wall_area=self.property.insulation_wall_area,
|
wall_area=self.property.insulation_wall_area,
|
||||||
material=material.to_dict(),
|
material=material.to_dict(),
|
||||||
|
is_extraction_and_refill=is_extraction_and_refill
|
||||||
)
|
)
|
||||||
|
|
||||||
already_installed = "cavity_wall_insulation" in self.property.already_installed
|
already_installed = "cavity_wall_insulation" in self.property.already_installed
|
||||||
if already_installed:
|
if already_installed:
|
||||||
cost_result = override_costs(cost_result)
|
cost_result = override_costs(cost_result)
|
||||||
|
|
||||||
|
if is_extraction_and_refill:
|
||||||
|
description = f"Extract and refill cavity wall insulation with {material['description']}"
|
||||||
|
else:
|
||||||
|
description = self._make_description(material)
|
||||||
|
|
||||||
|
# updated the new u-value with the best possible our installers have
|
||||||
|
new_u_value = max(0.31, new_u_value)
|
||||||
|
|
||||||
recommendations.append(
|
recommendations.append(
|
||||||
{
|
{
|
||||||
"phase": phase,
|
"phase": phase,
|
||||||
|
|
@ -237,7 +250,7 @@ class WallRecommendations(Definitions):
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
"type": "cavity_wall_insulation",
|
"type": "cavity_wall_insulation",
|
||||||
"description": self._make_description(material),
|
"description": description,
|
||||||
"starting_u_value": u_value,
|
"starting_u_value": u_value,
|
||||||
"new_u_value": new_u_value,
|
"new_u_value": new_u_value,
|
||||||
"sap_points": None,
|
"sap_points": None,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue