mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
1146 lines
52 KiB
Python
1146 lines
52 KiB
Python
import re
|
|
import backend.app.assumptions as assumptions
|
|
from recommendations.Costs import Costs, BOILER_UPGRADE_SCHEME_ASHP_VALUE
|
|
from recommendations.recommendation_utils import (
|
|
check_simulation_difference, override_costs, combine_recommendation_configs
|
|
)
|
|
from backend.Property import Property
|
|
from backend.app.plan.schemas import MEASURE_MAP
|
|
from etl.epc_clean.epc_attributes.MainheatAttributes import MainHeatAttributes
|
|
from etl.epc_clean.epc_attributes.HotWaterAttributes import HotWaterAttributes
|
|
from etl.epc_clean.epc_attributes.MainFuelAttributes import MainFuelAttributes
|
|
from recommendations.HeatingControlRecommender import HeatingControlRecommender
|
|
|
|
|
|
class HeatingRecommender:
|
|
high_heat_retention_contols_desc = "Controls for high heat retention storage heaters"
|
|
|
|
DUAL_HEATING_DESCRIPTIONS = {
|
|
"Boiler and radiators, mains gas, electric storage heaters": {
|
|
"hhr": {
|
|
"mainheating_description": "Boiler and radiators, mains gas, Electric storage heaters",
|
|
"recommendation_description": "Install high heat retention electric storage heaters alongside the "
|
|
"boiler. The current electric heaters may be retrofit with high heat "
|
|
"retention storage controls"
|
|
" however this is dependent on the existing system and may not be "
|
|
"possible.",
|
|
"controls_prefix": "current_controls"
|
|
},
|
|
"boiler": {
|
|
"mainheating_description": "Boiler and radiators, mains gas, electric storage heaters",
|
|
"recommendation_description": "Upgrade the existing boiler to a new, more efficient condensing "
|
|
"boiler. ",
|
|
"controls_suffix": "Manual charge controls"
|
|
},
|
|
# These are the heating types we need to produce a dual heating recommendation
|
|
"dual": {
|
|
"recommendation_description": "Upgrade both the existing boiler to a new condensing boiler and"
|
|
" upgrade storage heaters to high heat retention storage heaters.",
|
|
"types": [
|
|
# type 1
|
|
"boiler_upgrade",
|
|
# type 2
|
|
"high_heat_retention_storage_heater",
|
|
]
|
|
}
|
|
},
|
|
"Portable electric heaters assumed for most rooms, room heaters, electric": {
|
|
"hhr": {
|
|
"mainheating_description": "Electric storage heaters, radiators",
|
|
"recommendation_description": "Install high heat retention electric storage heaters.",
|
|
"controls_prefix": ""
|
|
},
|
|
"boiler": {
|
|
"mainheating_description": "Boiler and radiators, mains gas",
|
|
"recommendation_description": "Upgrade to a new condensing boiler.",
|
|
"controls_suffix": ""
|
|
},
|
|
# These are the heating types we need to produce a dual heating recommendation
|
|
"dual": None
|
|
}
|
|
}
|
|
|
|
def __init__(self, property_instance: Property):
|
|
self.property = property_instance
|
|
self.costs = Costs(self.property)
|
|
|
|
self.heating_recommendations = []
|
|
self.heating_control_recommendations = []
|
|
|
|
self.has_electric_heating_description = (
|
|
self.property.main_heating["has_electric"] or self.property.main_heating["has_electricaire"]
|
|
)
|
|
self.has_ashp = self.property.main_heating["has_air_source_heat_pump"]
|
|
self.has_room_heaters = (
|
|
self.property.main_heating["has_room_heaters"] or
|
|
self.property.main_heating["has_portable_electric_heaters"]
|
|
)
|
|
self.has_boiler = self.property.main_heating["has_boiler"]
|
|
|
|
self.dual_heating = self.identify_dual_heating()
|
|
|
|
def identify_dual_heating(self):
|
|
# All heat systems are in here so we identify whether two of these are true
|
|
# MainHeatAttributes.HEAT_SYSTEMS
|
|
|
|
n_trues = 0
|
|
for heat_system in MainHeatAttributes.HEAT_SYSTEMS:
|
|
if self.property.main_heating[f"has_{heat_system.replace(' ', '_')}"]:
|
|
n_trues += 1
|
|
|
|
if n_trues > 2 or n_trues == 0:
|
|
raise Exception("Implement me")
|
|
if n_trues == 1:
|
|
return False
|
|
|
|
return True
|
|
|
|
def is_high_heat_retention_valid(self, ashp_only_heating_recommendation, measures):
|
|
"""
|
|
Check conditions if high heat retention storage is valid
|
|
If there's already an ASHP in place, we don't recommend HHR
|
|
:return:
|
|
"""
|
|
|
|
# We can also recommend hhr if the property doesn't have a mains has connection
|
|
no_mains = not self.property.data["mains-gas-flag"]
|
|
|
|
# If the property already has room heaters then we recommend HHR as an option since the home already has
|
|
# a variation of room heaters
|
|
|
|
hhr_suitable = no_mains or self.has_electric_heating_description or self.has_room_heaters
|
|
|
|
return (
|
|
hhr_suitable and (not ashp_only_heating_recommendation) and not self.has_ashp and
|
|
("high_heat_retention_storage_heater" in measures)
|
|
)
|
|
|
|
def is_boiler_upgrade_suitable(self, measures, ashp_only_heating_recommendation):
|
|
"""
|
|
These are the conditions we apply to recommend a boiler installation
|
|
:return:
|
|
"""
|
|
|
|
# 1) if the property has mains heating with boiler and radiators, we recommend optimal heating controls
|
|
# If it's NOT a gas boiler, we'll potentially recommend a boiler
|
|
has_gas_boiler = self.has_boiler and self.property.main_heating["has_mains_gas"]
|
|
|
|
# 2) If the property doesn't have a heating system, but it has access to the mains gas
|
|
no_heating_has_mains = self.property.main_heating["clean_description"] in [
|
|
'No system present, electric heaters assumed'
|
|
] and self.property.data["mains-gas-flag"]
|
|
|
|
# The property is using portable heaters and has access to gas mains
|
|
has_room_heaters = self.has_room_heaters and self.property.data["mains-gas-flag"]
|
|
|
|
# We also check if the property has electric heating, but it has access to the mains gas
|
|
electic_heating_has_mains = self.has_electric_heating_description and self.property.data["mains-gas-flag"]
|
|
|
|
portable_heaters_has_mains = (
|
|
self.property.main_heating["has_portable_electric_heaters"] and self.property.data["mains-gas-flag"]
|
|
)
|
|
|
|
# The next condition is if the home has a non-gas boiler, such as an oil boiler, with a mains gas connection
|
|
non_gas_boiler = (
|
|
self.property.main_heating["has_boiler"] and
|
|
not self.property.main_heating["has_mains_gas"] and
|
|
self.property.data["mains-gas-flag"]
|
|
)
|
|
# Additionally, if the property has a gas connection, is using gas heating but doesn't have a boiler,
|
|
# we recommend a boiler
|
|
non_boiler_gas_heating = (
|
|
self.property.data["mains-gas-flag"] and
|
|
self.property.main_heating["has_mains_gas"] and
|
|
not self.property.main_heating["has_boiler"]
|
|
)
|
|
|
|
is_valid = (
|
|
(
|
|
has_gas_boiler or
|
|
no_heating_has_mains or
|
|
electic_heating_has_mains or
|
|
has_room_heaters or
|
|
portable_heaters_has_mains or
|
|
non_gas_boiler or
|
|
non_boiler_gas_heating
|
|
) and
|
|
(not ashp_only_heating_recommendation) and
|
|
("boiler_upgrade" in measures) and
|
|
(not self.has_ashp)
|
|
)
|
|
|
|
return is_valid, has_gas_boiler
|
|
|
|
def recommend_dual_heating(self):
|
|
|
|
if self.property.main_heating["clean_description"] not in self.DUAL_HEATING_DESCRIPTIONS:
|
|
return
|
|
|
|
# if we have set dual to None, we do not produce a dual heating recommendation
|
|
if self.DUAL_HEATING_DESCRIPTIONS[
|
|
self.property.main_heating["clean_description"]
|
|
]["dual"] is None:
|
|
return
|
|
|
|
dual_heating_description = self.DUAL_HEATING_DESCRIPTIONS[
|
|
self.property.main_heating["clean_description"]
|
|
]["dual"]["types"]
|
|
|
|
recommendation_system_types = list(set([x["system_type"] for x in self.heating_recommendations]))
|
|
|
|
# We check if we have the required type
|
|
if not any([x in recommendation_system_types for x in dual_heating_description]):
|
|
return
|
|
|
|
type_1_recommendations = [
|
|
x for x in self.heating_recommendations if x["system_type"] == dual_heating_description[0]
|
|
]
|
|
type_2_recommendations = [
|
|
x for x in self.heating_recommendations if x["system_type"] == dual_heating_description[1]
|
|
]
|
|
# we combine the two recommendations
|
|
combined_recommendations = []
|
|
for rec in type_1_recommendations:
|
|
for rec2 in type_2_recommendations:
|
|
combined_rec = rec.copy()
|
|
# Update the description
|
|
combined_rec["description"] = self.DUAL_HEATING_DESCRIPTIONS[
|
|
self.property.main_heating["clean_description"]
|
|
]["dual"]["recommendation_description"]
|
|
|
|
# Combine simulation_config
|
|
# Make sure we end up with the best efficiecy values
|
|
combined_rec["simulation_config"] = combine_recommendation_configs(
|
|
rec["simulation_config"], rec2["simulation_config"]
|
|
)
|
|
# Combine description_simulation
|
|
combined_rec["description_simulation"] = combine_recommendation_configs(
|
|
rec["description_simulation"], rec2["description_simulation"]
|
|
)
|
|
|
|
# Combine costs
|
|
for k in ["total", "subtotal", "vat", "labour_hours", "labour_days"]:
|
|
combined_rec[k] = rec[k] + rec2[k]
|
|
|
|
combined_rec["measure_type"] = "+".join([rec["measure_type"], rec2["measure_type"]])
|
|
|
|
combined_recommendations.append(combined_rec)
|
|
|
|
self.heating_recommendations.extend(combined_recommendations)
|
|
|
|
def recommend(self, has_cavity_or_loft_recommendations, phase=0, measures=None):
|
|
"""
|
|
Produces heating recommendations
|
|
|
|
:param has_cavity_or_loft_recommendations: boolean indicating if we have produced a cavity or loft insulation
|
|
recommendation. If there are cavity or loft recommendations, the property would need to complete those measures
|
|
before being able to get the boiler upgrade scheme benefits. The messaging in the front end would be to
|
|
:param phase: indicates the phase of the retrofit programme
|
|
:param measures: A list of measures for the recommendations
|
|
"""
|
|
|
|
measures = MEASURE_MAP["heating"] if measures is None else measures
|
|
|
|
# TODO: We could have a system flush recommendation for an existing boiler, where there is no need to replace
|
|
# the boiler, but instead flushing the system will make it run more efficiently. There is a cost for this
|
|
# in the Costs class, stored as SYSTEM_FLUSH_COST
|
|
|
|
# TODO: Right now, we don't have recommendations for electric boilers - we should probably have one
|
|
|
|
# if we have a non-invasive ashp recommendation, we get the configuration directly from the property instance
|
|
non_invasive_ashp_recommendation = next(
|
|
(r for r in self.property.non_invasive_recommendations if r["type"] == "air_source_heat_pump"),
|
|
{"suitable": True}
|
|
)
|
|
# We allow for the non-invasive recommendation to be that ASHP is not suitable
|
|
|
|
# This option will prevent other heating recommendations from being specified, other than an ASHP
|
|
ashp_only_heating_recommendation = non_invasive_ashp_recommendation.get(
|
|
"ashp_only_heating_recommendation", False
|
|
)
|
|
self.heating_recommendations = []
|
|
self.heating_control_recommendations = []
|
|
# This first iteration of the recommender will provide very basic recommendation
|
|
# We recommend heating controls based on the main heating system
|
|
|
|
hhr_valid = self.is_high_heat_retention_valid(ashp_only_heating_recommendation, measures)
|
|
|
|
if hhr_valid:
|
|
# Recommend high heat retention storage heaters
|
|
self.recommend_hhr_storage_heaters(phase=phase, system_change=True, heating_controls_only=False)
|
|
|
|
gas_boiler_suitable, has_gas_boiler = self.is_boiler_upgrade_suitable(
|
|
measures=measures, ashp_only_heating_recommendation=ashp_only_heating_recommendation
|
|
)
|
|
|
|
if gas_boiler_suitable:
|
|
# This indicates that the home previously did not have a boiler in place and so would require
|
|
# an overhaul to the system - right now, this is all reasons, apart from if there is an existing boiler
|
|
system_change = not has_gas_boiler
|
|
exising_room_heaters = self.property.main_heating["has_room_heaters"]
|
|
|
|
self.recommend_boiler_upgrades(
|
|
phase=phase, system_change=system_change, exising_room_heaters=exising_room_heaters
|
|
)
|
|
|
|
# If we have dual heating and we allow for a combined recommendation, to upgrade both systems
|
|
if self.dual_heating:
|
|
self.recommend_dual_heating()
|
|
|
|
# We recommend air source heat pumps
|
|
# Heat pumps are suitable for all property types:
|
|
# https://energysavingtrust.org.uk/from-flats-to-terraced-houses-heat-pumps-are-suitable-for-all-property-types/
|
|
# Just seems least probable for flats, so we'll allow houses and bungalows
|
|
# In the future, we'll allow overrides, so that non-intrusive surveys can contradict these conditions
|
|
# and either allow or prevent the recommendation of an air source heat pump
|
|
|
|
if (
|
|
self.property.is_ashp_valid(measures=measures) and
|
|
non_invasive_ashp_recommendation["suitable"] and
|
|
not self.has_ashp
|
|
):
|
|
self.recommend_air_source_heat_pump(
|
|
phase=phase,
|
|
has_cavity_or_loft_recommendations=has_cavity_or_loft_recommendations,
|
|
|
|
)
|
|
|
|
return
|
|
|
|
def recommend_electric_boiler_upgrade(self, phase):
|
|
|
|
# Small initial scope, just handles the case of properties that have electric boilers where the efficiency
|
|
# is poor or very poor
|
|
# We recommend upgrading to a new electric boiler
|
|
|
|
recommendation_phase = phase
|
|
|
|
if self.property.data["mainheat-energy-eff"] not in ["Poor", "Very Poor"]:
|
|
return
|
|
|
|
hotwater_from_mains = self.property.hotwater["clean_description"] in ["From main system"]
|
|
hotwater_from_cylinder = self.property.hotwater["clean_description"] in [
|
|
"From main system, no cylinder thermostat"
|
|
]
|
|
# if the hotwater is from the mains, we probably have a combi boiler so we recommend a new electric boiler
|
|
|
|
if hotwater_from_mains:
|
|
description = f"Upgrade to a higher efficiency electric boiler"
|
|
|
|
simulation_config = {
|
|
"mainheat_energy_eff_ending": "Average",
|
|
"hot_water_energy_eff_ending": "Average"
|
|
}
|
|
|
|
boiler_costs = self.costs.boiler(
|
|
size=None,
|
|
exising_room_heaters=False,
|
|
system_change=False,
|
|
n_heated_rooms=self.property.data["number-heated-rooms"],
|
|
n_rooms=self.property.number_of_rooms,
|
|
is_electric=True
|
|
)
|
|
|
|
already_installed = "heating" in self.property.already_installed
|
|
if already_installed:
|
|
boiler_costs = override_costs(boiler_costs)
|
|
description = "Heating system has already been upgraded, no further action needed."
|
|
|
|
boiler_recommendation = {
|
|
"phase": recommendation_phase,
|
|
"parts": [],
|
|
"type": "heating",
|
|
"description": description,
|
|
"starting_u_value": None,
|
|
"new_u_value": None,
|
|
"sap_points": None,
|
|
"already_installed": already_installed,
|
|
"simulation_config": simulation_config,
|
|
**boiler_costs
|
|
}
|
|
|
|
controls_recommender = HeatingControlRecommender(self.property)
|
|
controls_recommender.recommend(heating_description="Boiler and radiators, electric")
|
|
|
|
self.heating_recommendations.extend([boiler_recommendation] + controls_recommender.recommendation)
|
|
return
|
|
|
|
if hotwater_from_cylinder:
|
|
# We recommend a change from a system boiler, with a cylinder to a combi boiler
|
|
description = ("Replace the existing boiler and cylinder without a thermostat with a new electric combi "
|
|
"boiler")
|
|
|
|
def size_heat_pump(self):
|
|
"""
|
|
Given the methodology by installers (SCIS) this function will perform a basic heat loss calculation and
|
|
produce a recommendation for the size of the heat pump
|
|
:return:
|
|
"""
|
|
|
|
floor_area = self.property.floor_area
|
|
|
|
# We use the default heat loss W/m2 values are specified by the insaller, depending on the property type
|
|
|
|
def remap_to_heat_loss(construction_age_band):
|
|
if "before 1900" in construction_age_band:
|
|
return "Pre 1900 (solid stone)"
|
|
elif "1900-1929" in construction_age_band:
|
|
return "Early 1900s (solid brick)"
|
|
elif re.search(r'1930|1949|1950|1966|1967|1975', construction_age_band):
|
|
return "1950-1980 (cavity void)"
|
|
elif re.search(r'1976|1982|1983|1990', construction_age_band):
|
|
return "Post 1980 (cavity wall construction)"
|
|
elif re.search(r'1991|1995|1996|2002|2003|2011', construction_age_band):
|
|
return "2000-2018"
|
|
elif "2012 onwards" in construction_age_band:
|
|
return "New build (2018+)"
|
|
else:
|
|
return None
|
|
|
|
def select_heatpump_size(heat_loss_calculation):
|
|
"""
|
|
This function calculates the size of the heat pump based on the heat loss calculation, mapping
|
|
the heat loss calculation to the size of the heat pump in KW
|
|
:param heat_loss_calculation: This is calcualted as the floor area multipled by the heat loss constant,
|
|
divided by 1000
|
|
"""
|
|
if heat_loss_calculation < 5:
|
|
return 5
|
|
elif 5 <= heat_loss_calculation < 6:
|
|
return 6
|
|
elif 6 <= heat_loss_calculation < 8.5:
|
|
return 8.5
|
|
elif 8.5 <= heat_loss_calculation < 11.2:
|
|
return 11.2
|
|
elif 11.2 <= heat_loss_calculation < 14:
|
|
return 14
|
|
elif 14 <= heat_loss_calculation < 17:
|
|
return 17
|
|
elif 17 <= heat_loss_calculation < 20:
|
|
return 20
|
|
else:
|
|
return None
|
|
|
|
heat_loss_constants = {
|
|
"New build (2018+)": 35,
|
|
"2000-2018": 50,
|
|
"Post 1980 (cavity wall construction)": 60,
|
|
"1950-1980 (cavity void)": 70,
|
|
"Early 1900s (solid brick)": 80,
|
|
"Pre 1900 (solid stone)": 90
|
|
}
|
|
|
|
heat_loss_group = remap_to_heat_loss(self.property.construction_age_band)
|
|
heat_loss_constant = heat_loss_constants[heat_loss_group]
|
|
|
|
heat_loss_calculation = floor_area * heat_loss_constant / 1000
|
|
|
|
heat_pump_size = select_heatpump_size(heat_loss_calculation)
|
|
|
|
return heat_pump_size
|
|
|
|
def recommend_air_source_heat_pump(self, phase, has_cavity_or_loft_recommendations, _return=False):
|
|
"""
|
|
This method will implement the recommendation for an air source heat pump
|
|
This is ultimately an overhaul to the heating system and so is recommended as an alternative to other
|
|
heating system recommendations
|
|
:return:
|
|
"""
|
|
|
|
# Look for a non-intrusive recommendation
|
|
non_intrusive_recommendation = next((
|
|
r for r in self.property.non_invasive_recommendations if r["type"] == "air_source_heat_pump"
|
|
), {})
|
|
|
|
controls_recommender = HeatingControlRecommender(self.property)
|
|
controls_recommender.recommend(heating_description="Air source heat pump, radiators, electric")
|
|
ashp_size = self.size_heat_pump()
|
|
|
|
ashp_costs = self.costs.air_source_heat_pump(ashp_size)
|
|
if non_intrusive_recommendation:
|
|
# Update with non-intrusive recommendation
|
|
if non_intrusive_recommendation.get("cost"):
|
|
ashp_costs.update(
|
|
{"total": non_intrusive_recommendation["cost"], "subtotal": None, "vat": None}
|
|
)
|
|
|
|
already_installed = "air_source_heat_pump" in self.property.already_installed
|
|
|
|
controls_recommendations = controls_recommender.recommendation
|
|
if already_installed or not controls_recommendations:
|
|
# We set an empty object, so we just produce one recommendation
|
|
controls_recommendations = [None]
|
|
|
|
if already_installed:
|
|
ashp_costs = override_costs(ashp_costs)
|
|
|
|
if non_intrusive_recommendation and not all([x is None for x in controls_recommendations]):
|
|
# We just use the ttzc control
|
|
controls_recommendations = [
|
|
x for x in controls_recommendations if (
|
|
x["description_simulation"]["mainheatcont-description"] == "Time and temperature zone control"
|
|
)
|
|
]
|
|
|
|
# This is a map from the heating controls description to the description of the air source heat pump set up
|
|
ashp_descriptions = {
|
|
"Time and temperature zone control": (
|
|
f"Install a {ashp_size}KW air source heat pump, and upgrade heating controls to Smart Thermostats, "
|
|
"room sensors and smart radiator valves (time & temperature zone control). Ensure you have an 18 or "
|
|
"24 hour tariff"
|
|
),
|
|
"Programmer, TRVs and bypass": (
|
|
f"Install a {ashp_size}KW air source heat pump, with programmer, TRVs and a Bypass valve. Ensure you "
|
|
"have an 18 or 24 hour tariff"
|
|
),
|
|
}
|
|
|
|
new_heating_description = "Air source heat pump, radiators, electric"
|
|
new_hot_water_description = "From main system"
|
|
ashp_recommendations = []
|
|
for controls_rec in controls_recommendations:
|
|
|
|
ashp_costs_with_controls = ashp_costs.copy()
|
|
|
|
if controls_rec:
|
|
for key in ashp_costs_with_controls:
|
|
if ashp_costs_with_controls[key] is not None:
|
|
ashp_costs_with_controls[key] += controls_rec[key]
|
|
|
|
if controls_rec is None:
|
|
description = f"Install a {ashp_size}KW Air source heat pump. Ensure you have an 18 or 24 hour tariff"
|
|
elif already_installed:
|
|
description = "The property already has an air source heat pump, no further action needed."
|
|
else:
|
|
description = ashp_descriptions[controls_rec["description_simulation"]["mainheatcont-description"]]
|
|
|
|
# If the property does not have existing cavity and loft insulation, we include a note that the cost
|
|
# includes the boiler upgrade scheme and that the cavity and loft need to be treated, to ensure access
|
|
# to the funding
|
|
if not non_intrusive_recommendation and self.property.data["tenure"] not in assumptions.SOCIAL_TENURES:
|
|
if has_cavity_or_loft_recommendations:
|
|
description = description + (
|
|
f" You must ensure that the property has an insulated cavity and "
|
|
f"270mm+ loft insulation to qualify for the grant, to claim £"
|
|
f"{BOILER_UPGRADE_SCHEME_ASHP_VALUE} of funding from the boiler upgrade scheme grant. "
|
|
)
|
|
else:
|
|
description = description + (
|
|
f" £{BOILER_UPGRADE_SCHEME_ASHP_VALUE} of funding can be claimed from the boiler upgrade scheme"
|
|
)
|
|
|
|
simulation_config = {
|
|
"mainheat_energy_eff_ending": "Very Good",
|
|
"hot_water_energy_eff_ending": "Very Good"
|
|
}
|
|
description_simulation = {
|
|
"mainheat-description": new_heating_description,
|
|
"mainheat-energy-eff": simulation_config["mainheat_energy_eff_ending"],
|
|
"hot-water-energy-eff": simulation_config["hot_water_energy_eff_ending"],
|
|
"hotwater-description": new_hot_water_description,
|
|
}
|
|
# Installation of a boiler improves the hot water system so we need to reflect this in
|
|
# the outcome of the recommendation
|
|
heating_ending_config = MainHeatAttributes(new_heating_description).process()
|
|
hotwater_ending_config = HotWaterAttributes(new_hot_water_description).process()
|
|
|
|
# If the property does not currently have electric main fuel, we'll simulate the change
|
|
fuel_ending_config = {}
|
|
if self.property.main_fuel["fuel_type"] != "electricity":
|
|
new_fuel_description = "electricity (not community)"
|
|
fuel_ending_config = MainFuelAttributes(new_fuel_description).process()
|
|
description_simulation = {
|
|
**description_simulation,
|
|
"main-fuel": new_fuel_description
|
|
}
|
|
|
|
# Check the simulation differences
|
|
heating_simulation_config = check_simulation_difference(
|
|
new_config=heating_ending_config, old_config=self.property.main_heating
|
|
)
|
|
hotwater_simulation_config = check_simulation_difference(
|
|
new_config=hotwater_ending_config, old_config=self.property.hotwater
|
|
)
|
|
fuel_simulation_config = check_simulation_difference(
|
|
new_config=fuel_ending_config, old_config=self.property.main_fuel
|
|
)
|
|
|
|
simulation_config = {
|
|
**simulation_config,
|
|
**heating_simulation_config,
|
|
**hotwater_simulation_config,
|
|
**fuel_simulation_config,
|
|
}
|
|
|
|
if controls_rec is not None:
|
|
# We should have just the single recommendation for heat controls, which is time
|
|
# and temperature zone controls
|
|
simulation_config = {
|
|
**simulation_config,
|
|
**controls_rec["simulation_config"]
|
|
}
|
|
|
|
description_simulation = {
|
|
**description_simulation,
|
|
**controls_rec["description_simulation"]
|
|
}
|
|
|
|
ashp_recommendation = {
|
|
"phase": phase,
|
|
"parts": [],
|
|
"type": "heating",
|
|
"measure_type": "air_source_heat_pump",
|
|
"description": description,
|
|
"starting_u_value": None,
|
|
"new_u_value": None,
|
|
"sap_points": None,
|
|
"already_installed": already_installed,
|
|
"simulation_config": simulation_config,
|
|
"description_simulation": description_simulation,
|
|
**ashp_costs_with_controls
|
|
}
|
|
|
|
ashp_recommendations.append(ashp_recommendation)
|
|
|
|
if _return:
|
|
return [ashp_recommendations]
|
|
|
|
self.heating_recommendations.extend(ashp_recommendations)
|
|
|
|
@staticmethod
|
|
def check_simulation_difference(old_config, new_config):
|
|
"""
|
|
Given two dictionaries, that describe the heating control configurations, this method will compare the two
|
|
and pick out the differences. These differences will be things that have been added and things that have been
|
|
removed. This will be used to determine how we should be updating the configuration in the simulation
|
|
:return:
|
|
"""
|
|
|
|
differences = {key + "_ending": new_config[key] for key in new_config if old_config[key] != new_config[key]}
|
|
|
|
return differences
|
|
|
|
def combine_heating_and_controls(
|
|
self,
|
|
controls_recommendations,
|
|
heating_simulation_config,
|
|
heating_description_simulation,
|
|
costs,
|
|
description,
|
|
phase,
|
|
heating_controls_only,
|
|
system_change,
|
|
system_type,
|
|
measure_type
|
|
):
|
|
"""
|
|
Given a recommendation for heating controls, and a recommendation for the heating system, we combine the two
|
|
into a single recommendation
|
|
:param controls_recommendations: The heating controls recommendations
|
|
:param heating_simulation_config: The simulation configuration for the heating system
|
|
:param heating_description_simulation: The simulation configuration for the heating description
|
|
:param costs: The costs of the heating system
|
|
:param description: The description of the recommendation
|
|
:param phase: The phase of the recommendation
|
|
:param heating_controls_only: If True, we will also add a recommendation for heating controls only
|
|
:param system_change: Indicates if we are recommending a different type of heating system, compared to the
|
|
current system. If we have a system change and we have a heat control recommendation, we only recommend
|
|
both heating and controls together
|
|
:param system_type: The type of heating system we are recommending
|
|
:param measure_type: The type of measure we are recommending - more granular than the "type" field, allowing us
|
|
to distinguish between different types of heating recommendations
|
|
"""
|
|
|
|
# We produce recommendations with & without heating controls
|
|
# We will also produce a recommendation for heating controls only
|
|
heating_controls_switch = [True, False] if controls_recommendations else [False]
|
|
if not heating_simulation_config:
|
|
heating_controls_switch = []
|
|
|
|
if system_change and len(controls_recommendations):
|
|
heating_controls_switch = [True]
|
|
|
|
output = []
|
|
for controls_switch in heating_controls_switch:
|
|
total_costs = costs.copy()
|
|
recommendation_simulation_config = heating_simulation_config.copy()
|
|
recommendation_description_simulation = heating_description_simulation.copy()
|
|
recommendation_description = description
|
|
if controls_switch:
|
|
# We add the costs of the heating controls, onto each key in the costs dictionary
|
|
for key in total_costs:
|
|
total_costs[key] += controls_recommendations[0][key]
|
|
|
|
recommendation_simulation_config = {
|
|
**recommendation_simulation_config,
|
|
**controls_recommendations[0]["simulation_config"]
|
|
}
|
|
|
|
recommendation_description_simulation = {
|
|
**recommendation_description_simulation,
|
|
**controls_recommendations[0]["description_simulation"]
|
|
}
|
|
|
|
controls_description = controls_recommendations[0]['description']
|
|
|
|
recommendation_description = f"{description} {controls_description}"
|
|
|
|
already_installed = "heating_controls" in self.property.already_installed
|
|
if already_installed:
|
|
total_costs = override_costs(total_costs)
|
|
recommendation_description = "Heating system has already been upgraded, no further action needed."
|
|
|
|
recommendation = {
|
|
"phase": phase,
|
|
"parts": [],
|
|
"type": "heating",
|
|
"measure_type": measure_type,
|
|
"description": recommendation_description,
|
|
"starting_u_value": None,
|
|
"new_u_value": None,
|
|
"sap_points": None,
|
|
"already_installed": already_installed,
|
|
**total_costs,
|
|
"simulation_config": recommendation_simulation_config,
|
|
"description_simulation": recommendation_description_simulation,
|
|
# We insert the heating system type here
|
|
"system_type": system_type
|
|
}
|
|
|
|
output.append(recommendation)
|
|
|
|
if heating_controls_only and len(controls_recommendations):
|
|
# Also add on a recommendation for heating controls only
|
|
heating_control_recommendation = controls_recommendations[0].copy()
|
|
# Capitalize the first letter of the description
|
|
heating_control_recommendation["description"] = (
|
|
heating_control_recommendation["description"][0].upper() +
|
|
heating_control_recommendation["description"][1:]
|
|
)
|
|
|
|
output.append(
|
|
{
|
|
"phase": phase,
|
|
"parts": [
|
|
# TODO
|
|
],
|
|
"type": "heating",
|
|
"starting_u_value": None,
|
|
"new_u_value": None,
|
|
"sap_points": None,
|
|
**heating_control_recommendation
|
|
}
|
|
)
|
|
|
|
return output
|
|
|
|
def is_hhr_already_installed(self):
|
|
"""
|
|
Check if the property already has high heat retention storage heaters
|
|
:return:
|
|
"""
|
|
|
|
already_has_hhr = "Electric storage heaters" in self.property.main_heating["clean_description"]
|
|
|
|
# Some electric storage heaters will show that the controls are "Manual charge controls" which are indicative
|
|
# of the old model of electric storage heaters, originating from 1961.
|
|
# Newer HHR storage heaters will charge up over night but will retain the heat durin the day for when warmth
|
|
# is actually needed, unlike traditional storage heaters that charge up at night and release heat during the day
|
|
# which isn't always ideal for the occupants.
|
|
already_has_hhr_contols = (
|
|
self.property.main_heating_controls[
|
|
"clean_description"
|
|
].lower() == self.high_heat_retention_contols_desc.lower()
|
|
)
|
|
|
|
return already_has_hhr and already_has_hhr_contols
|
|
|
|
def recommend_hhr_storage_heaters(self, phase, system_change, heating_controls_only, _return=False):
|
|
"""
|
|
We will recommend upgrading to a high heat retention storage system, if the current system is not already
|
|
high heat retention storage
|
|
|
|
If the property currently has electric storage heaters, with automatic charge control, we allow for a high
|
|
heat retention stoarage heaters recommendation. This is because the automatic charge control is not the same
|
|
as the high heat retention storage heaters. HHR storage heaters aren't guaranteed to be more efficient but
|
|
we can at least present the option to the end user and they can decide if they want to go ahead with the
|
|
recommendation or not. There's a useful guide by quidos, describing the differences between some of the
|
|
different storage heater options:
|
|
https://www.quidos.co.uk/wp-content/uploads/2017/04/Technical-Bulletin-010417-Storage-Heatersv2.pdf
|
|
|
|
:param phase: The phase of the recommendation
|
|
:param system_change: Indicates if we are recommending a different type of heating system, compared to the
|
|
current system
|
|
:param heating_controls_only: Indicates if we should include a recommendation for just heating controls
|
|
:param _return: Indicates if we should return the recommendations, rather than appending them to the
|
|
recommendations list
|
|
:return:
|
|
"""
|
|
|
|
controls_recommender = HeatingControlRecommender(self.property)
|
|
# The heating controls we're recommending for are based on the recommended heating system
|
|
|
|
# We only recommend Celect-type controls if the current heating system is not Celect-type controls
|
|
if self.property.main_heating_controls["clean_description"] != self.high_heat_retention_contols_desc:
|
|
if self.dual_heating:
|
|
|
|
controls_prefix = self.DUAL_HEATING_DESCRIPTIONS[
|
|
self.property.main_heating["clean_description"]
|
|
]["hhr"]["controls_prefix"]
|
|
|
|
if controls_prefix == "current_controls":
|
|
description_prefix = self.property.main_heating_controls["clean_description"]
|
|
elif controls_prefix == "":
|
|
description_prefix = ""
|
|
else:
|
|
raise NotImplementedError("Implement me")
|
|
else:
|
|
description_prefix = ""
|
|
|
|
controls_recommender.recommend(
|
|
heating_description="Electric storage heaters", description_prefix=description_prefix
|
|
)
|
|
|
|
has_hhr = self.is_hhr_already_installed()
|
|
# Conditions for not recommending electric storage heaters
|
|
if has_hhr:
|
|
# No recommendation needed
|
|
return
|
|
|
|
# We check if the property has dual heating in place with a boiler and storage heaters
|
|
if self.dual_heating:
|
|
new_heating_description = self.DUAL_HEATING_DESCRIPTIONS[
|
|
self.property.main_heating["clean_description"]
|
|
]["hhr"]["mainheating_description"]
|
|
new_hot_water_description = self.property.hotwater["clean_description"] # We keep the hot water system
|
|
else:
|
|
new_heating_description = "Electric storage heaters"
|
|
new_hot_water_description = "Electric immersion, off-peak"
|
|
|
|
# Set up artefacts, suitable for the simulation and regardless of controls
|
|
heating_ending_config = MainHeatAttributes(new_heating_description).process()
|
|
heating_simulation_config = check_simulation_difference(
|
|
new_config=heating_ending_config, old_config=self.property.main_heating
|
|
)
|
|
|
|
hot_water_end_config = HotWaterAttributes(new_hot_water_description).process()
|
|
hot_water_simulation_config = check_simulation_difference(
|
|
new_config=hot_water_end_config, old_config=self.property.hotwater
|
|
)
|
|
|
|
heating_simulation_config = {
|
|
**heating_simulation_config,
|
|
**hot_water_simulation_config
|
|
}
|
|
# This upgrade will only take the heating system to average energy efficiency
|
|
if self.property.data["mainheat-energy-eff"] in ["Very Poor", "Poor"] and not self.dual_heating:
|
|
heating_simulation_config["mainheat_energy_eff_ending"] = "Average"
|
|
else:
|
|
heating_simulation_config["mainheat_energy_eff_ending"] = self.property.data["mainheat-energy-eff"]
|
|
|
|
if self.property.data["hot-water-energy-eff"] in ["Very Poor", "Poor"]:
|
|
heating_simulation_config["hot_water_energy_eff_ending"] = "Average"
|
|
else:
|
|
heating_simulation_config["hot_water_energy_eff_ending"] = self.property.data["hot-water-energy-eff"]
|
|
|
|
# If the property is off-gas and has no heating system in place, the number of heated rooms will actually
|
|
# be 0, so we use the number of rooms as the figure
|
|
number_heated_rooms = (
|
|
self.property.data["number-heated-rooms"] if self.property.data["number-heated-rooms"] > 0
|
|
else (
|
|
self.property.number_of_rooms - 1 if self.property.number_of_rooms > 1 else
|
|
self.property.number_of_rooms
|
|
)
|
|
)
|
|
# To be conservative, we adjust if we still have 1 room
|
|
if (number_heated_rooms == 1) and (self.property.number_of_rooms > 2):
|
|
number_heated_rooms = self.property.number_of_rooms - 1
|
|
|
|
# Upgrade to electric storage heaters
|
|
costs = self.costs.high_heat_electric_storage_heaters(
|
|
number_heated_rooms=number_heated_rooms,
|
|
needs_cylinder=self.property.hotwater["system_type"] == "from main system"
|
|
)
|
|
if self.dual_heating:
|
|
description = self.DUAL_HEATING_DESCRIPTIONS[
|
|
self.property.main_heating["clean_description"]
|
|
]["hhr"]["recommendation_description"]
|
|
|
|
else:
|
|
description = "Install high heat retention electric storage heaters with an appropriate off-peak tariff."
|
|
|
|
# We check the existing heating system and controls
|
|
if (
|
|
self.property.main_heating["has_electric_storage_heaters"] and
|
|
self.property.main_heating_controls["charging_system"] in
|
|
["automatic charge control", "manual charge control"]
|
|
):
|
|
description += (" The current electric heaters may be retrofit with high heat retention storage controls"
|
|
" however this is dependent on the existing system and may not be possible.")
|
|
|
|
heating_description_simulation = {
|
|
"mainheat-description": new_heating_description,
|
|
"mainheat-energy-eff": heating_simulation_config["mainheat_energy_eff_ending"],
|
|
"hotwater-description": new_hot_water_description,
|
|
"hot-water-energy-eff": heating_simulation_config["hot_water_energy_eff_ending"]
|
|
}
|
|
|
|
recommendations = self.combine_heating_and_controls(
|
|
controls_recommendations=controls_recommender.recommendation,
|
|
heating_simulation_config=heating_simulation_config,
|
|
heating_description_simulation=heating_description_simulation,
|
|
costs=costs,
|
|
description=description,
|
|
phase=phase,
|
|
heating_controls_only=heating_controls_only,
|
|
system_change=system_change,
|
|
system_type="high_heat_retention_storage_heater",
|
|
measure_type="high_heat_retention_storage_heater"
|
|
)
|
|
if _return:
|
|
return recommendations
|
|
|
|
self.heating_recommendations.extend(recommendations)
|
|
|
|
@staticmethod
|
|
def estimate_boiler_size(property_type, built_form, floor_area, floor_height, num_heated_rooms):
|
|
# Step 1: Base size estimation based on property type (as a starting point)
|
|
base_size = {
|
|
'Flat': 25,
|
|
'House': 30,
|
|
'Maisonette': 28,
|
|
'Bungalow': 27
|
|
}
|
|
|
|
# Step 2: Calculate the volume of the property
|
|
volume = floor_area * floor_height
|
|
|
|
# Step 3: Adjust base size for built form (to account for heat retention)
|
|
form_adjustment = {
|
|
'Mid-Terrace': 0,
|
|
'End-Terrace': 2,
|
|
'Semi-Detached': 4,
|
|
'Detached': 6
|
|
}
|
|
|
|
# Step 4: Further adjust for the total volume and number of heated rooms
|
|
volume_adjustment = (volume / 100) # Simplified adjustment factor for volume
|
|
rooms_adjustment = (num_heated_rooms - 5) * 0.5 # Assuming base case of 5 rooms
|
|
|
|
# Calculate the estimated boiler size
|
|
estimated_size = base_size[property_type] + form_adjustment[built_form] + volume_adjustment + rooms_adjustment
|
|
|
|
# Step 5: Align with available boiler sizes and ensure it does not exceed 35kW, as it's rare to need more
|
|
available_sizes = [30, 35, 40, 45, 50]
|
|
estimated_size = min(max(estimated_size, 30), 40) # Ensure within 30kW to 35kW range
|
|
|
|
# Find the closest available size (in this case, either rounding up or down to align with 30 or 35)
|
|
closest_size = min(available_sizes, key=lambda x: abs(x - estimated_size))
|
|
|
|
return closest_size
|
|
|
|
@staticmethod
|
|
def estimate_electric_boiler_size(num_heated_rooms):
|
|
"""
|
|
We use the approach similar to as defined in
|
|
https://www.greenmatch.co.uk/boilers/combi-boilers/electric-combi-boilers
|
|
Instead of radiators as a proxy, we do the number of heated rooms
|
|
|
|
:param num_heated_rooms: The number of heated rooms in the property
|
|
:return:
|
|
"""
|
|
|
|
return max(num_heated_rooms * 1.5, 6)
|
|
|
|
def recommend_boiler_upgrades(self, phase, system_change, exising_room_heaters):
|
|
"""
|
|
This boiler recommendation will only recommend a like-for-like upgrade, since changing the system
|
|
is generally more expensive
|
|
:param phase:
|
|
:param system_change: Indicates if the property would be undergoing a heating system change. This could be true
|
|
if the home didn't have a heating system in place, or if the home had electric heating
|
|
previously
|
|
:param exising_room_heaters: Indicates if the property had room heaters previously - if so, a boiler
|
|
recommendation will need to be accompanied by removal of the room heaters
|
|
:return:
|
|
"""
|
|
|
|
recommendation_phase = phase
|
|
|
|
# We now recommend boiler upgrades, if applicable
|
|
simulation_config = {}
|
|
boiler_costs = {}
|
|
boiler_recommendation = {}
|
|
description_simulation = {}
|
|
|
|
has_inefficient_space_heating = self.property.data["mainheat-energy-eff"] in ["Very Poor", "Poor", "Average"]
|
|
|
|
# We check if there's a mains connection and the hot water is inefficient, as this will improve with a boiler
|
|
has_inefficient_water = (
|
|
self.property.data["mains-gas-flag"] and
|
|
self.property.data["hot-water-energy-eff"] in ["Very Poor", "Poor", "Average"]
|
|
)
|
|
|
|
if has_inefficient_space_heating or has_inefficient_water:
|
|
boiler_size = self.estimate_boiler_size(
|
|
property_type=self.property.data["property-type"],
|
|
built_form=self.property.data["built-form"],
|
|
floor_area=self.property.floor_area,
|
|
floor_height=self.property.floor_height,
|
|
num_heated_rooms=self.property.data["number-heated-rooms"],
|
|
)
|
|
|
|
if self.dual_heating:
|
|
description = self.DUAL_HEATING_DESCRIPTIONS[
|
|
self.property.main_heating["clean_description"]
|
|
]["boiler"]["recommendation_description"]
|
|
else:
|
|
description = "Upgrade to a new condensing boiler."
|
|
|
|
new_heating_eff = (
|
|
"Good" if self.property.data["mainheat-energy-eff"] in ["Very Poor", "Poor", "Average"]
|
|
else self.property.data["mainheat-energy-eff"]
|
|
)
|
|
|
|
new_hotwater_eff = (
|
|
"Good" if self.property.data["hot-water-energy-eff"] in ["Very Poor", "Poor", "Average"]
|
|
else self.property.data["hot-water-energy-eff"]
|
|
)
|
|
|
|
simulation_config = {
|
|
"mainheat_energy_eff_ending": new_heating_eff,
|
|
"hot_water_energy_eff_ending": new_hotwater_eff
|
|
}
|
|
|
|
description_simulation = {
|
|
"mainheat-energy-eff": simulation_config["mainheat_energy_eff_ending"],
|
|
"hot-water-energy-eff": simulation_config["hot_water_energy_eff_ending"],
|
|
}
|
|
|
|
if system_change:
|
|
# Installation of a boiler improves the hot water system so we need to reflect this in
|
|
# the outcome of the recommendation
|
|
if self.dual_heating:
|
|
new_heating_description = self.DUAL_HEATING_DESCRIPTIONS[
|
|
self.property.main_heating["clean_description"]
|
|
]["boiler"]["mainheating_description"]
|
|
else:
|
|
new_heating_description = "Boiler and radiators, mains gas"
|
|
|
|
new_hotwater_description = "From main system"
|
|
new_fuel_description = "mains gas (not community)"
|
|
|
|
heating_ending_config = MainHeatAttributes(new_heating_description).process()
|
|
hotwater_ending_config = HotWaterAttributes(new_hotwater_description).process()
|
|
fuel_ending_config = MainFuelAttributes(new_fuel_description).process()
|
|
|
|
heating_simulation_config = check_simulation_difference(
|
|
new_config=heating_ending_config, old_config=self.property.main_heating
|
|
)
|
|
hotwater_simulation_config = check_simulation_difference(
|
|
new_config=hotwater_ending_config, old_config=self.property.hotwater
|
|
)
|
|
fuel_simulation_config = check_simulation_difference(
|
|
new_config=fuel_ending_config, old_config=self.property.main_fuel
|
|
)
|
|
|
|
simulation_config = {
|
|
**simulation_config,
|
|
**heating_simulation_config,
|
|
**hotwater_simulation_config,
|
|
**fuel_simulation_config,
|
|
}
|
|
|
|
description_simulation = {
|
|
**description_simulation,
|
|
"mainheat-description": new_heating_description,
|
|
"hotwater-description": new_hotwater_description,
|
|
"main-fuel": new_fuel_description
|
|
}
|
|
|
|
boiler_costs = self.costs.boiler(
|
|
size=f"{boiler_size}kw",
|
|
exising_room_heaters=exising_room_heaters,
|
|
system_change=system_change,
|
|
n_heated_rooms=self.property.data["number-heated-rooms"],
|
|
n_rooms=self.property.number_of_rooms
|
|
)
|
|
|
|
already_installed = "heating" in self.property.already_installed
|
|
if already_installed:
|
|
boiler_costs = override_costs(boiler_costs)
|
|
description = "Heating system has already been upgraded, no further action needed."
|
|
|
|
boiler_recommendation = {
|
|
"phase": recommendation_phase,
|
|
"parts": [],
|
|
"type": "heating",
|
|
"measure_type": "boiler_upgrade",
|
|
"description": description,
|
|
"starting_u_value": None,
|
|
"new_u_value": None,
|
|
"sap_points": None,
|
|
"already_installed": already_installed,
|
|
"simulation_config": simulation_config,
|
|
"description_simulation": description_simulation,
|
|
**boiler_costs,
|
|
"system_type": "boiler_upgrade",
|
|
}
|
|
|
|
# We recommend the heating controls
|
|
# If the property did not previously have a boiler, we combine
|
|
controls_recommender = HeatingControlRecommender(self.property)
|
|
if self.dual_heating:
|
|
description_suffix = self.DUAL_HEATING_DESCRIPTIONS[
|
|
self.property.main_heating["clean_description"]
|
|
]["boiler"]["controls_suffix"]
|
|
else:
|
|
description_suffix = ""
|
|
controls_recommender.recommend(
|
|
heating_description="Boiler and radiators, mains gas",
|
|
description_suffix=description_suffix
|
|
)
|
|
# We may have 2 recommendations from the heating controls
|
|
|
|
if not controls_recommender.recommendation and not boiler_recommendation:
|
|
return
|
|
|
|
if not system_change and len(boiler_recommendation):
|
|
# If there is not a system change, we add the boiler recommendation at point.
|
|
self.heating_recommendations.extend([boiler_recommendation])
|
|
|
|
if system_change:
|
|
# We combine the heating and controls recommendations, in the case of a system change
|
|
combined_recommendations = []
|
|
for controls_recommendation in controls_recommender.recommendation:
|
|
combined_recommendation = self.combine_heating_and_controls(
|
|
controls_recommendations=[controls_recommendation],
|
|
heating_simulation_config=simulation_config,
|
|
heating_description_simulation=description_simulation,
|
|
costs=boiler_costs,
|
|
description=boiler_recommendation["description"],
|
|
phase=recommendation_phase,
|
|
heating_controls_only=False,
|
|
system_change=True,
|
|
system_type="boiler_upgrade",
|
|
measure_type="boiler_upgrade",
|
|
)
|
|
combined_recommendations.extend(combined_recommendation)
|
|
|
|
# Overwrite the existing boiler recommendation
|
|
self.heating_recommendations.extend(combined_recommendations)
|
|
else:
|
|
# We consider a heating control upgrade as a measure which occures in the same phase as a boiler upgrade
|
|
# Namely, we have the following options within this phase
|
|
# 1) Boiler + heating controls
|
|
# 2) Boiler only
|
|
# 3) Heating controls only
|
|
# But they are options that are not mutually exclusive
|
|
# So, we actually set heating controls as a heating recommendation
|
|
for recommendation in controls_recommender.recommendation:
|
|
recommendation["phase"] = recommendation_phase
|
|
# recommendation["type"] = "heating"
|
|
|
|
self.heating_control_recommendations.extend(controls_recommender.recommendation)
|
|
|
|
return
|