mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
1285 lines
60 KiB
Python
1285 lines
60 KiB
Python
import numpy as np
|
|
from recommendations.county_to_region import county_to_region_map
|
|
|
|
# This data comes from SPONs 2023
|
|
regional_labour_variations = [
|
|
{"Region": "Outer London", "Adjustment_Factor": 1.00},
|
|
{"Region": "Inner London", "Adjustment_Factor": 1.05},
|
|
{"Region": "South East England", "Adjustment_Factor": 0.96},
|
|
{"Region": "South West England", "Adjustment_Factor": 0.90},
|
|
{"Region": "East of England", "Adjustment_Factor": 0.93},
|
|
{"Region": "East Midlands", "Adjustment_Factor": 0.88},
|
|
{"Region": "West Midlands", "Adjustment_Factor": 0.87},
|
|
{"Region": "North East England", "Adjustment_Factor": 0.83},
|
|
{"Region": "North West England", "Adjustment_Factor": 0.88},
|
|
{"Region": "Yorkshire and the Humber", "Adjustment_Factor": 0.86},
|
|
{"Region": "Wales", "Adjustment_Factor": 0.88},
|
|
{"Region": "Scotland", "Adjustment_Factor": 0.88},
|
|
{"Region": "Northern Ireland", "Adjustment_Factor": 0.76}
|
|
]
|
|
|
|
# This data is based on the MCS database
|
|
MCS_SOLAR_PV_COST_DATA = {
|
|
"last_updated": "2024-06-10",
|
|
"average_cost_per_kwh": 1750,
|
|
"average_cost_per_kwh-Outer London": 1776,
|
|
"average_cost_per_kwh-Inner London": 1776,
|
|
"average_cost_per_kwh-South East England": 1672,
|
|
"average_cost_per_kwh-South West England": 1732,
|
|
"average_cost_per_kwh-East of England": 1721,
|
|
"average_cost_per_kwh-East Midlands": 1730,
|
|
"average_cost_per_kwh-West Midlands": 1761,
|
|
"average_cost_per_kwh-North East England": 1669,
|
|
"average_cost_per_kwh-North West England": 1764,
|
|
"average_cost_per_kwh-Yorkshire and the Humber": 1705,
|
|
"average_cost_per_kwh-Wales": 1896,
|
|
"average_cost_per_kwh-Scotland": 1767,
|
|
"average_cost_per_kwh-Northern Ireland": 1767,
|
|
}
|
|
|
|
# This data is based on the MCS database, We use the larger figure between the 2023 and 2024 average,
|
|
# to be conservative
|
|
MCS_AIR_SOURCE_HEAT_PUMP_COST_DATA = {
|
|
"Outer London": 13220,
|
|
"Inner London": 13220,
|
|
"South East England": 13547,
|
|
"South West England": 12776,
|
|
"East of England": 12585,
|
|
"East Midlands": 12239,
|
|
"West Midlands": 13182,
|
|
"North East England": 11829,
|
|
"North West England": 11714,
|
|
"Yorkshire and the Humber": 11919,
|
|
"Wales": 13701,
|
|
"Scotland": 12586,
|
|
"Northern Ireland": 12000, # There are hardly any air source heat pump installs going on in Northern Ireland
|
|
}
|
|
BOILER_UPGRADE_SCHEME_ASHP_VALUE = 7500
|
|
|
|
# This is based on quotes from installers
|
|
BATTERY_COST = 3500
|
|
|
|
# This is based on https://www.checkatrade.com/blog/cost-guides/cost-smart-thermostat/
|
|
SMART_APPLIANCE_THERMOSTAT_COST = 400
|
|
PROGRAMMER_COST = 120
|
|
ROOM_THERMOSTAT_COST = 150
|
|
TRVS_COST = 35
|
|
|
|
# Cost for TTZC
|
|
# Smart thermostat based on checkatrade https://www.checkatrade.com/blog/cost-guides/cost-smart-thermostat/
|
|
# Based on the Nest system
|
|
TTZC_SMART_THERMOSTAT_COST = 205
|
|
TTZC_SMART_THERMOSTAT_LABOUR_HOURS = 2
|
|
TTZC_ELECTRICIAN_HOURLY_RATE = 45
|
|
# Based on cost of a Nest temperature sensor
|
|
TTZC_ROOM_TEMPERATURE_SENSOR_COST = 50
|
|
TTZC_ROOM_TEMPERATURE_SENSOR_LABOUR_HOURS = 0.17 # (Assume ~ 10 mins install per sensor)
|
|
# Basedon an average cost of smart radiator values
|
|
TTZC_SMART_RADIATOR_VALUES = 50
|
|
TTZC_SMART_RADIATOR_VALUES_LABOUR_HOURS = 0.37 # (Assume ~ 15-30 mins install per valve)
|
|
|
|
# Low carbon combi boiler - median value based on £2200 - £3000 range
|
|
LOW_CARBON_COMBI_BOILER = 2200
|
|
|
|
# boiler prices based on
|
|
# https://www.greenmatch.co.uk/boilers/30kw-boiler
|
|
# https://www.greenmatch.co.uk/boilers/35kw-boiler
|
|
# https://www.greenmatch.co.uk/boilers/40kw-boiler
|
|
# These are exclusive of installation costs
|
|
CONDENSING_BOILER_COSTS = {
|
|
"30kw": 1550,
|
|
"35kw": 1610,
|
|
"40kw": 1625
|
|
}
|
|
|
|
# Assumes 3 hours to remove each heater (including re-decorating)
|
|
ROOM_HEATER_REMOVAL_COST = 120
|
|
ROOM_HEATER_REMOVAL_LABOUR_HOURS = 3
|
|
|
|
# This is a cost quoted by Jim for a system flush - existig system will run more efficiently
|
|
SYSTEM_FLUSH_COST = 250
|
|
|
|
SINGLE_RADIATOR_COST = 150
|
|
DOUBLE_RADIATOR_COST = 300
|
|
FLUE_COST = 600
|
|
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:
|
|
"""
|
|
A class to calculate the costs associated with construction works,
|
|
specifically focusing on cavity wall insulation.
|
|
It includes contingency, preliminaries, profit margin, and VAT calculations.
|
|
|
|
As a sense check, there is a useful article from checkatrade on retrofitting and expected costs:
|
|
https://www.checkatrade.com/blog/cost-guides/retrofit-insulation-cost/
|
|
|
|
Another useful article for benchmarking the cost of floor insulation:
|
|
https://www.checkatrade.com/blog/cost-guides/floor-insulation-cost/
|
|
"""
|
|
|
|
# Contingency is a percentage of the total cost of the work and covers unforseen expenses
|
|
# We assume a conservative 10% contingency for all works which is a rate defined by SPONs
|
|
CONTINGENCY = 0.1
|
|
|
|
# For flat roof, we assume it's a high risk project as it's very weather dependent and also is heavily
|
|
# dependent on the quality of the existing roof
|
|
FLAT_ROOF_CONTINGENCY = 0.15
|
|
|
|
# We use a higher contingency rate for internal wall insulation because of the potential for issues with moving
|
|
# fittings and trimming doors, as well as scope for damage to the existing wall during preparation.
|
|
IWI_CONTINGENCY = 0.2
|
|
|
|
# Where there is more uncertainty, a higher contingency rate is used
|
|
HIGH_RISK_CONTINGENCY = 0.2
|
|
# When there is less uncertainty, a lower contingency rate is used
|
|
LOW_RISK_CONTINGENCY = 0.05
|
|
|
|
# Preliminaries are a percentage of the total cost of the work and covers the cost of site-specific costs
|
|
# such as site preparation, safety measures and project management. This rate can vary but we'll assume a 10%
|
|
# rate, on the total cost before VAT, as recommended by SPONs
|
|
PRELIMINARIES = 0.1
|
|
|
|
# For higher risk projects, a higher preliminaries rate is used. SPONs indicates that a higher risk project might
|
|
# have a preliminaries of 12-14% so we use 12% as the median for the preliminaries rate.
|
|
# For External wall insulation (EWI), we use 15% as the preliminaries rate if we think the property might
|
|
# need scaffolding, otherwise we use 12%. This is to account for any site preparation that might be required
|
|
EWI_NO_SCAFFOLDING_PRELIMINARIES = 0.2
|
|
EWI_SCAFFOLDING_PRELIMINARIES = 0.25
|
|
|
|
VAT_RATE = 0.2
|
|
PROFIT_MARGIN = 0.2
|
|
|
|
# Based on this greenmatch article, on average, a Sash window is around 50% more expensive than a casement window.
|
|
# Therefore, for a conservative cost estimate, and allowance for a more premium window type, we inflate the material
|
|
# cost of the windows to allow for a sash window type
|
|
# https://www.greenmatch.co.uk/windows/double-glazing/cost
|
|
SASH_WINDOW_INFLATION_FACTOR = 1.5
|
|
|
|
# Typically, secondary glazing can be installed for 25% of the cost of double glazed windows - to be conservative,
|
|
# we scale the cost by half
|
|
SECONDARY_GLAZING_SCALING_FACTOR = 0.5
|
|
|
|
def __init__(self, property_instance):
|
|
"""
|
|
Initializes the Costs class with a property instance.
|
|
|
|
:param property_instance: Instance of a Property class containing relevant details like wall area.
|
|
"""
|
|
if not hasattr(property_instance, 'insulation_wall_area'):
|
|
raise ValueError("Property instance must have an 'insulation_wall_area' attribute")
|
|
self.property = property_instance
|
|
self.regional_labour_variations = regional_labour_variations
|
|
|
|
self.region = county_to_region_map.get(self.property.data["county"], None)
|
|
if self.region is None:
|
|
# Try and grab using the local-authority-label
|
|
self.region = county_to_region_map.get(self.property.data["local-authority-label"], None)
|
|
if self.region is None:
|
|
raise ValueError("Region not found in county map")
|
|
|
|
self.labour_adjustment_factor = [
|
|
x["Adjustment_Factor"] for x in self.regional_labour_variations if
|
|
x["Region"] == self.region
|
|
][0]
|
|
|
|
if not self.labour_adjustment_factor:
|
|
raise ValueError("Labour adjustment factor not found")
|
|
|
|
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,
|
|
including contingency, preliminaries, profit, and VAT.
|
|
|
|
Because of some limitations in the SPONs data, there are no materials that can be blown through a wall,
|
|
therefore we have adapted similar materials, basing our estimates on 75mm cavity slabs, and have halved the
|
|
labour time required. That is why we still price based on wall area despite volume actually being the correct
|
|
metric.
|
|
|
|
:return: A dictionary containing detailed cost breakdown.
|
|
"""
|
|
|
|
material_cost_per_m2 = material["material_cost"]
|
|
|
|
base_material_cost = material_cost_per_m2 * wall_area
|
|
labour_cost = material["labour_cost"] * wall_area * self.labour_adjustment_factor
|
|
|
|
subtotal_before_profit = base_material_cost + labour_cost
|
|
|
|
contingency_cost = subtotal_before_profit * self.CONTINGENCY
|
|
preliminaries_cost = subtotal_before_profit * self.PRELIMINARIES
|
|
profit_cost = subtotal_before_profit * self.PROFIT_MARGIN
|
|
|
|
subtotal_before_vat = subtotal_before_profit + contingency_cost + preliminaries_cost + profit_cost
|
|
|
|
vat_cost = subtotal_before_vat * self.VAT_RATE
|
|
|
|
total_cost = subtotal_before_vat + vat_cost
|
|
|
|
labour_hours = material["labour_hours_per_unit"] * wall_area
|
|
|
|
# Assume a team of 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 {
|
|
"total": total_cost,
|
|
"subtotal": subtotal_before_vat,
|
|
"vat": vat_cost,
|
|
"contingency": contingency_cost,
|
|
"preliminaries": preliminaries_cost,
|
|
"material": base_material_cost,
|
|
"profit": profit_cost,
|
|
"labour_hours": labour_hours,
|
|
"labour_cost": labour_cost,
|
|
"labour_days": labour_days
|
|
}
|
|
|
|
def loft_insulation(self, floor_area, material):
|
|
"""
|
|
Calculates the total cost for cavity wall insulation based on material and labor costs,
|
|
including contingency, preliminaries, profit, and VAT.
|
|
|
|
:return: A dictionary containing detailed cost breakdown.
|
|
"""
|
|
material_cost_per_m2 = material["material_cost"]
|
|
|
|
# We inflate material costs due to recent price increases
|
|
material_cost_per_m2 = material_cost_per_m2 * 1.5
|
|
|
|
base_material_cost = material_cost_per_m2 * floor_area
|
|
labour_cost = material["labour_cost"] * floor_area * self.labour_adjustment_factor
|
|
|
|
subtotal_before_profit = base_material_cost + labour_cost
|
|
|
|
# We use high risk contingency because of the possibility of access issues and clearing existing insulation
|
|
contingency_cost = subtotal_before_profit * self.HIGH_RISK_CONTINGENCY
|
|
preliminaries_cost = subtotal_before_profit * self.PRELIMINARIES
|
|
profit_cost = subtotal_before_profit * self.PROFIT_MARGIN
|
|
|
|
subtotal_before_vat = subtotal_before_profit + contingency_cost + preliminaries_cost + profit_cost
|
|
|
|
vat_cost = subtotal_before_vat * self.VAT_RATE
|
|
|
|
total_cost = subtotal_before_vat + vat_cost
|
|
|
|
labour_hours = material["labour_hours_per_unit"] * floor_area
|
|
|
|
# Assume a team of 1 person
|
|
labour_days = labour_hours / 8
|
|
|
|
return {
|
|
"total": total_cost,
|
|
"subtotal": subtotal_before_vat,
|
|
"vat": vat_cost,
|
|
"contingency": contingency_cost,
|
|
"preliminaries": preliminaries_cost,
|
|
"material": base_material_cost,
|
|
"profit": profit_cost,
|
|
"labour_hours": labour_hours,
|
|
"labour_cost": labour_cost,
|
|
"labour_days": labour_days
|
|
}
|
|
|
|
def internal_wall_insulation(self, wall_area, material, non_insulation_materials):
|
|
"""
|
|
Broadly speaking, the high level steps to an internal wall insulation job are the following:
|
|
|
|
1) Demolition: This involves removing existing wall linings, fittings, and any other obstacles.
|
|
It's important to factor in the disposal of debris and the potential need for additional protective
|
|
measures to ensure the safety of the work area.
|
|
|
|
2) Insulation Installation: This is the core part of the process where the chosen insulation material is
|
|
applied. The choice of insulation material will depend on several factors including thermal performance,
|
|
wall construction, and space constraints.
|
|
|
|
3) Vapour Barrier Installation: This is crucial for preventing moisture from penetrating the insulation,
|
|
which can compromise its effectiveness and lead to mold growth.
|
|
|
|
4) Re-decoration: This involves applying plaster to the wall and then painting.
|
|
The quality of finish here is important for both aesthetic and functional reasons.
|
|
|
|
5) Trim and Finishing Work: Post-insulation, tasks such as re-installing skirting boards, door frames,
|
|
or window sills might be necessary.
|
|
:return:
|
|
"""
|
|
|
|
# Extract and check the different types of data we'll need
|
|
demolition_data = [x for x in non_insulation_materials if x["type"] == "iwi_wall_demolition"]
|
|
vapour_barrier_data = [x for x in non_insulation_materials if x["type"] == "iwi_vapour_barrier"]
|
|
redecoration_data = [x for x in non_insulation_materials if x["type"] == "iwi_redecoration"]
|
|
if not demolition_data:
|
|
raise ValueError("No data found for iwi_wall_demolition")
|
|
|
|
if (len(vapour_barrier_data) != 1) or (len(redecoration_data) != 3):
|
|
raise ValueError("Incorrect number of data entries for non-insulation materials")
|
|
|
|
# Break out the individual material costs
|
|
# Since we don't know the exact wall construction, we take an average for demolition costs, since
|
|
# the cost will depend on the type of wall construction
|
|
demolition_material_costs = np.mean([x["material_cost"] * wall_area for x in demolition_data])
|
|
insulation_material_costs = material["material_cost"] * wall_area
|
|
vapour_barrier_material_costs = vapour_barrier_data[0]["material_cost"] * wall_area
|
|
redecoration_material_costs = sum([x["material_cost"] * wall_area for x in redecoration_data])
|
|
|
|
demolition_plant_costs = np.mean([x["plant_cost"] * wall_area for x in demolition_data])
|
|
|
|
# Again for demolition, we average since we aren't sure which demolition process will be used
|
|
demolition_labour_costs = np.mean([x["labour_cost"] * wall_area for x in demolition_data])
|
|
insulation_labour_costs = material["labour_cost"] * wall_area
|
|
vapour_barrier_labour_costs = vapour_barrier_data[0]["labour_cost"] * wall_area
|
|
redecoration_labour_costs = sum([x["labour_cost"] * wall_area for x in redecoration_data])
|
|
|
|
labour_costs = (demolition_labour_costs + insulation_labour_costs + vapour_barrier_labour_costs +
|
|
redecoration_labour_costs)
|
|
|
|
labour_costs = labour_costs * self.labour_adjustment_factor
|
|
|
|
materials_costs = (demolition_material_costs + insulation_material_costs + vapour_barrier_material_costs +
|
|
redecoration_material_costs)
|
|
|
|
subtotal_before_profit = labour_costs + materials_costs + demolition_plant_costs
|
|
|
|
contingency_cost = subtotal_before_profit * self.IWI_CONTINGENCY
|
|
preliminaries_cost = subtotal_before_profit * self.PRELIMINARIES
|
|
profit_cost = subtotal_before_profit * self.PROFIT_MARGIN
|
|
|
|
subtotal_before_vat = subtotal_before_profit + contingency_cost + preliminaries_cost + profit_cost
|
|
|
|
vat_cost = subtotal_before_vat * self.VAT_RATE
|
|
|
|
total_cost = subtotal_before_vat + vat_cost
|
|
|
|
demolition_labour_hours = np.mean([x["labour_hours_per_unit"] * wall_area for x in demolition_data])
|
|
insulation_labour_hours = material["labour_hours_per_unit"] * wall_area
|
|
vapour_barrier_labour_hours = vapour_barrier_data[0]["labour_hours_per_unit"] * wall_area
|
|
redecoration_labour_hours = sum([x["labour_hours_per_unit"] * wall_area for x in redecoration_data])
|
|
|
|
labour_hours = (demolition_labour_hours + insulation_labour_hours + vapour_barrier_labour_hours +
|
|
redecoration_labour_hours)
|
|
|
|
# To install internal wall insulation, a small to medium size project might be conducted by a team of 3-5 people
|
|
labour_days = (labour_hours / 8) / 4
|
|
|
|
return {
|
|
"total": total_cost,
|
|
"subtotal": subtotal_before_vat,
|
|
"vat": vat_cost,
|
|
"contingency": contingency_cost,
|
|
"preliminaries": preliminaries_cost,
|
|
"material": materials_costs,
|
|
"profit": profit_cost,
|
|
"labour_hours": labour_hours,
|
|
"labour_days": labour_days,
|
|
"labour_cost": labour_costs
|
|
}
|
|
|
|
def suspended_floor_insulation(self, insulation_floor_area, material, non_insulation_materials):
|
|
"""
|
|
We characterise the steps for suspended floor insulation as the following tasks:
|
|
|
|
1) Removal of Carpet and Underfelt: Where necessary, remove existing floor coverings to access the floorboards.
|
|
2) Removal of Floor Boarding: Carefully remove floorboards to access the space beneath for insulation.
|
|
3) Installation of Vapour Barrier: Install a vapour barrier to prevent moisture from affecting
|
|
the insulation and floor structure.
|
|
4) Installation of Insulation: Fit the chosen insulation material between the joists in the floor void.
|
|
5) Refixing Floorboards: Replace and secure the floorboards after insulation installation.
|
|
6) Re-carpeting: Lay down the carpet or other floor coverings once the insulation and floorboards are in place.
|
|
:return:
|
|
"""
|
|
|
|
demolition_data = [x for x in non_insulation_materials if x["type"] == "suspended_floor_demolition"]
|
|
vapour_barrier_data = [x for x in non_insulation_materials if x["type"] == "suspended_floor_vapour_barrier"]
|
|
redecoration_data = [x for x in non_insulation_materials if x["type"] == "suspended_floor_redecoration"]
|
|
|
|
if (len(demolition_data) != 2) or (len(vapour_barrier_data) != 1) or (len(redecoration_data) != 2):
|
|
raise ValueError("Incorrect number of data entries for non-insulation materials")
|
|
|
|
# Break out the individual material costs
|
|
demolition_material_costs = sum([x["material_cost"] * insulation_floor_area for x in demolition_data])
|
|
insulation_material_costs = material["material_cost"] * insulation_floor_area
|
|
vapour_barrier_material_costs = vapour_barrier_data[0]["material_cost"] * insulation_floor_area
|
|
redecoration_material_costs = sum([x["material_cost"] * insulation_floor_area for x in redecoration_data])
|
|
|
|
demolition_labour_costs = sum([x["labour_cost"] * insulation_floor_area for x in demolition_data])
|
|
insulation_labour_costs = material["labour_cost"] * insulation_floor_area
|
|
vapour_barrier_labour_costs = vapour_barrier_data[0]["labour_cost"] * insulation_floor_area
|
|
redecoration_labour_costs = sum([x["labour_cost"] * insulation_floor_area for x in redecoration_data])
|
|
|
|
labour_costs = (demolition_labour_costs + insulation_labour_costs + vapour_barrier_labour_costs +
|
|
redecoration_labour_costs)
|
|
|
|
labour_costs = labour_costs * self.labour_adjustment_factor
|
|
|
|
materials_costs = (demolition_material_costs + insulation_material_costs + vapour_barrier_material_costs +
|
|
redecoration_material_costs)
|
|
|
|
subtotal_before_profit = labour_costs + materials_costs
|
|
|
|
# Because of the possiblity of damage to the existing floor, or difficulties associated to moving fittings,
|
|
# we use a higher contingency rate
|
|
contingency_cost = subtotal_before_profit * self.HIGH_RISK_CONTINGENCY
|
|
preliminaries_cost = subtotal_before_profit * self.PRELIMINARIES
|
|
profit_cost = subtotal_before_profit * self.PROFIT_MARGIN
|
|
|
|
subtotal_before_vat = subtotal_before_profit + contingency_cost + preliminaries_cost + profit_cost
|
|
|
|
vat_cost = subtotal_before_vat * self.VAT_RATE
|
|
|
|
total_cost = subtotal_before_vat + vat_cost
|
|
|
|
demolition_labour_hours = sum([x["labour_hours_per_unit"] * insulation_floor_area for x in demolition_data])
|
|
insulation_labour_hours = material["labour_hours_per_unit"] * insulation_floor_area
|
|
vapour_barrier_labour_hours = vapour_barrier_data[0]["labour_hours_per_unit"] * insulation_floor_area
|
|
redecoration_labour_hours = sum([x["labour_hours_per_unit"] * insulation_floor_area for x in redecoration_data])
|
|
|
|
labour_hours = (demolition_labour_hours + insulation_labour_hours + vapour_barrier_labour_hours +
|
|
redecoration_labour_hours)
|
|
|
|
# Assume a team of 3 people for a small to medium size project
|
|
labour_days = (labour_hours / 8) / 3
|
|
|
|
return {
|
|
"total": total_cost,
|
|
"subtotal": subtotal_before_vat,
|
|
"vat": vat_cost,
|
|
"contingency": contingency_cost,
|
|
"preliminaries": preliminaries_cost,
|
|
"material": materials_costs,
|
|
"profit": profit_cost,
|
|
"labour_hours": labour_hours,
|
|
"labour_days": labour_days,
|
|
"labour_cost": labour_costs
|
|
}
|
|
|
|
def solid_floor_insulation(self, insulation_floor_area, material, non_insulation_materials):
|
|
"""
|
|
We characterise the steps for solid floor insulation as the following tasks:
|
|
|
|
1) Removal of Carpet and Underfelt: This is the initial stage where any existing floor coverings, like carpets,
|
|
tiles, or linoleum, are carefully removed. This exposes the solid floor beneath, which is typically concrete.
|
|
|
|
2) Preparation of Flooring: This step is critical. It involves:
|
|
- Cleaning the existing floor surface thoroughly to remove debris and ensure a flat surface.
|
|
- Assessing and repairing any damage to the concrete floor. This might include filling cracks or leveling
|
|
uneven areas.
|
|
|
|
3) Installation of a Damp Proof Membrane (DPM): Before installing insulation, a DPM is often laid down to
|
|
prevent moisture from rising into the insulation and the interior space. This step is crucial in areas prone to
|
|
dampness.
|
|
|
|
4) Install Insulation: The insulation, often in the form of rigid foam boards, is laid over the DPM.
|
|
The choice of insulation material will depend on the desired thermal properties and the available floor height.
|
|
Care is taken to minimize thermal bridges and ensure a snug fit between insulation boards.
|
|
|
|
5) Laying a New Subfloor: Over the insulation, a new subfloor is often installed. This could be a layer of
|
|
screed (a type of concrete) or wooden boarding, depending on the specific requirements and preferences.
|
|
|
|
6) Re-decoration and Finishing Touches: Once the subfloor is in place and has set or dried (if necessary),
|
|
the final floor finish can be applied. This might involve:
|
|
- Laying new tiles, wooden flooring, or other chosen materials.
|
|
- If you're planning to re-carpet, this would be the stage to do it.
|
|
- Skirting boards may need to be refitted or replaced.
|
|
|
|
7) Considerations for Doors and Fixtures: It's important to note that raising the floor level can affect door
|
|
thresholds and other fixtures. Doors may need to be trimmed, and fixtures might need adjustments.
|
|
|
|
:param insulation_floor_area: Area of the floor to be insulated
|
|
:param material: Selected insulation material
|
|
:param non_insulation_materials: Non-insulation materials required for the job
|
|
:return:
|
|
"""
|
|
|
|
demolition_data = [x for x in non_insulation_materials if x["type"] == "solid_floor_demolition"]
|
|
preparation_data = [x for x in non_insulation_materials if x["type"] == "solid_floor_preparation"]
|
|
vapour_barrier_data = [x for x in non_insulation_materials if x["type"] == "solid_floor_vapour_barrier"]
|
|
redecoration_data = [x for x in non_insulation_materials if x["type"] == "solid_floor_redecoration"]
|
|
|
|
if ((len(demolition_data) != 1) or (len(preparation_data) != 2) or (len(vapour_barrier_data) != 1) or
|
|
(len(redecoration_data) != 3)):
|
|
raise ValueError("Incorrect number of data entries for non-insulation materials")
|
|
|
|
# Break out the individual material costs
|
|
preparation_material_costs = sum([x["material_cost"] * insulation_floor_area for x in preparation_data])
|
|
insulation_material_costs = material["material_cost"] * insulation_floor_area
|
|
vapour_barrier_material_costs = vapour_barrier_data[0]["material_cost"] * insulation_floor_area
|
|
redecoration_material_costs = sum([x["material_cost"] * insulation_floor_area for x in redecoration_data])
|
|
|
|
demolition_labour_costs = sum([x["labour_cost"] * insulation_floor_area for x in demolition_data])
|
|
preparation_labour_costs = sum([x["labour_cost"] * insulation_floor_area for x in preparation_data])
|
|
insulation_labour_costs = material["labour_cost"] * insulation_floor_area
|
|
vapour_barrier_labour_costs = vapour_barrier_data[0]["labour_cost"] * insulation_floor_area
|
|
redecoration_labour_costs = sum([x["labour_cost"] * insulation_floor_area for x in redecoration_data])
|
|
|
|
preparation_plant_costs = sum([x["plant_cost"] * insulation_floor_area for x in preparation_data])
|
|
|
|
labour_costs = (demolition_labour_costs + insulation_labour_costs + vapour_barrier_labour_costs +
|
|
redecoration_labour_costs + preparation_labour_costs)
|
|
|
|
labour_costs = labour_costs * self.labour_adjustment_factor
|
|
|
|
materials_cost = (preparation_material_costs + insulation_material_costs + vapour_barrier_material_costs +
|
|
redecoration_material_costs)
|
|
|
|
subtotal_before_profit = labour_costs + materials_cost + preparation_plant_costs
|
|
|
|
# We use HIGH_RISH_CONTINGENCY because of the potential for issues with moving fittings and trimming doors,
|
|
# as well as scope for damage to the existing floor during preparation.
|
|
contingency_cost = subtotal_before_profit * self.HIGH_RISK_CONTINGENCY
|
|
preliminaries_cost = subtotal_before_profit * self.PRELIMINARIES
|
|
profit_cost = subtotal_before_profit * self.PROFIT_MARGIN
|
|
|
|
subtotal_before_vat = subtotal_before_profit + contingency_cost + preliminaries_cost + profit_cost
|
|
vat_cost = subtotal_before_vat * self.VAT_RATE
|
|
total_cost = subtotal_before_vat + vat_cost
|
|
|
|
demolition_labour_hours = sum([x["labour_hours_per_unit"] * insulation_floor_area for x in demolition_data])
|
|
preparation_labour_hours = sum([x["labour_hours_per_unit"] * insulation_floor_area for x in preparation_data])
|
|
insulation_labour_hours = material["labour_hours_per_unit"] * insulation_floor_area
|
|
vapour_barrier_labour_hours = vapour_barrier_data[0]["labour_hours_per_unit"] * insulation_floor_area
|
|
redecoration_labour_hours = sum([x["labour_hours_per_unit"] * insulation_floor_area for x in redecoration_data])
|
|
|
|
labour_hours = (demolition_labour_hours + insulation_labour_hours + vapour_barrier_labour_hours +
|
|
redecoration_labour_hours + preparation_labour_hours)
|
|
|
|
# Assume a team of 3 people for a small to medium size project
|
|
labour_days = (labour_hours / 8) / 3
|
|
|
|
return {
|
|
"total": total_cost,
|
|
"subtotal": subtotal_before_vat,
|
|
"vat": vat_cost,
|
|
"contingency": contingency_cost,
|
|
"preliminaries": preliminaries_cost,
|
|
"material": materials_cost,
|
|
"profit": profit_cost,
|
|
"labour_hours": labour_hours,
|
|
"labour_days": labour_days,
|
|
"labour_cost": labour_costs
|
|
}
|
|
|
|
def external_wall_insulation(self, wall_area, material, non_insulation_materials):
|
|
"""
|
|
We characterise external wall insulation as the following steps:
|
|
|
|
1) Preparation of the Area: Tidying up the surroundings, trimming back foliage, and laying down protective
|
|
sheets to protect the flooring and landscaping around the work area.
|
|
|
|
2) Scaffolding Setup (if needed): Erecting scaffolding for safe access to the walls of semi-detached or
|
|
detached houses. For terraced houses or lower-level work, scaffolding might not be necessary.
|
|
|
|
3) Wall Surface Preparation: Cleaning the wall surface, removing any loose or flaking material,
|
|
and possibly applying a primer. If the existing wall is weak or damaged, partial or full replacement
|
|
of the top surface may be necessary.
|
|
|
|
4) Applying Primer: If the existing wall is suitable, applying a primer to improve adhesion of the insulation
|
|
boards and stabilize the wall surface, especially if it's old or weathered.
|
|
|
|
5) Insulation Application: Attaching insulation boards to the primed wall using adhesive, mechanical fixings,
|
|
or a combination of both.
|
|
|
|
6) Basecoat and Mesh Application: Applying a basecoat embedded with a reinforcing mesh over the insulation.
|
|
This layer provides strength and helps prevent cracking.
|
|
|
|
7) Decorative Finish: Applying a decorative finish, such as render or cladding, which protects the insulation
|
|
and provides an aesthetic look.
|
|
|
|
8) Reinstalling Fixtures: Reattaching any fixtures like downpipes, satellite dishes, or lighting fixtures that
|
|
were removed during preparation. Extensions or adjustments may be required due to the increased wall thickness.
|
|
|
|
9) Inspection and Cleanup: Conducting a thorough inspection to ensure quality and integrity of the EWI system,
|
|
followed by cleaning up the site to remove all debris and materials.
|
|
|
|
In the actual materials data, at this point, we have costing for:
|
|
- wall preparation, hacking off existing wall finishes, linings, etc (ewi_wall_demolition)
|
|
- wall surface cleaning and priming (ewi_wall_preparation)
|
|
- insulation (external_wall_insulation)
|
|
- basecoat and mesh with decorative render topcoat finish (ewi_basecoat_and_mesh)
|
|
|
|
All of this data comes from SPONS, however there are some clear features missing. Because we could not find
|
|
suitable cost records in SPONS for steps like cleaning the area, setting up small scale scaffolding,
|
|
re-attaching any fitings and cleaning up the area afterwards, instead we have accounted for these steps by
|
|
increasing the preliminaries rate. It is acknowldeged though, that this is not ideal and that the cost of these
|
|
steps should be included in the materials data. We will look to improve this in the future, with data from
|
|
installers
|
|
|
|
:param wall_area:
|
|
:param material:
|
|
:param non_insulation_materials:
|
|
:return:
|
|
"""
|
|
|
|
# For semi detatched and detatched houses, as well as maisonettes, we price for scaffolding
|
|
|
|
if self.property.data["property-type"] == "House":
|
|
if self.property.data["built-form"] in ['Semi-Detached', 'Detached', "End-Terrace"]:
|
|
preliminaries_rate = self.EWI_SCAFFOLDING_PRELIMINARIES
|
|
else:
|
|
preliminaries_rate = self.EWI_NO_SCAFFOLDING_PRELIMINARIES
|
|
elif self.property.data["property-type"] in ["Maisonette", "Flat"]:
|
|
preliminaries_rate = self.EWI_SCAFFOLDING_PRELIMINARIES
|
|
elif self.property.data["property-type"] == "Bungalow":
|
|
preliminaries_rate = self.EWI_NO_SCAFFOLDING_PRELIMINARIES
|
|
|
|
demolition_data = [x for x in non_insulation_materials if x["type"] == "ewi_wall_demolition"]
|
|
preparation_data = [x for x in non_insulation_materials if x["type"] == "ewi_wall_preparation"]
|
|
redecoration_data = [x for x in non_insulation_materials if x["type"] == "ewi_wall_redecoration"]
|
|
|
|
if (len(demolition_data) != 3) or (len(preparation_data) != 1) or (len(redecoration_data) != 1):
|
|
raise ValueError("Incorrect number of data entries for non-insulation materials")
|
|
|
|
# Break out the individual material costs
|
|
# Since we don't know the exact wall construction, we take an average for demolition costs, since
|
|
# the cost will depend on the type of wall construction
|
|
demolition_material_costs = np.mean([x["material_cost"] * wall_area for x in demolition_data])
|
|
insulation_material_costs = material["material_cost"] * wall_area
|
|
preparation_material_costs = preparation_data[0]["material_cost"] * wall_area
|
|
redecoration_material_costs = redecoration_data[0]["material_cost"] * wall_area
|
|
|
|
demolition_plant_costs = np.mean([x["plant_cost"] * wall_area for x in demolition_data])
|
|
|
|
demolition_labour_costs = np.mean([x["labour_cost"] * wall_area for x in demolition_data])
|
|
insulation_labour_costs = material["labour_cost"] * wall_area
|
|
preparation_labour_costs = preparation_data[0]["labour_cost"] * wall_area
|
|
redecoration_labour_costs = redecoration_data[0]["labour_cost"] * wall_area
|
|
|
|
labour_costs = (demolition_labour_costs + insulation_labour_costs + redecoration_labour_costs +
|
|
preparation_labour_costs)
|
|
|
|
labour_costs = labour_costs * self.labour_adjustment_factor
|
|
|
|
materials_costs = (demolition_material_costs + insulation_material_costs + preparation_material_costs +
|
|
redecoration_material_costs)
|
|
|
|
subtotal_before_profit = labour_costs + materials_costs + demolition_plant_costs
|
|
|
|
contingency_cost = subtotal_before_profit * self.CONTINGENCY
|
|
preliminaries_cost = subtotal_before_profit * preliminaries_rate
|
|
profit_cost = subtotal_before_profit * self.PROFIT_MARGIN
|
|
|
|
subtotal_before_vat = subtotal_before_profit + contingency_cost + preliminaries_cost + profit_cost
|
|
vat_cost = subtotal_before_vat * self.VAT_RATE
|
|
total_cost = subtotal_before_vat + vat_cost
|
|
|
|
demolition_labour_hours = np.mean([x["labour_hours_per_unit"] * wall_area for x in demolition_data])
|
|
insulation_labour_hours = material["labour_hours_per_unit"] * wall_area
|
|
preparation_labour_hours = preparation_data[0]["labour_hours_per_unit"] * wall_area
|
|
redecoration_labour_hours = redecoration_data[0]["labour_hours_per_unit"] * wall_area
|
|
|
|
labour_hours = (demolition_labour_hours + insulation_labour_hours + redecoration_labour_hours +
|
|
preparation_labour_hours)
|
|
|
|
# Assume a team of 3-5 people for a small to medium size project
|
|
labour_days = (labour_hours / 8) / 4
|
|
|
|
return {
|
|
"total": total_cost,
|
|
"subtotal": subtotal_before_vat,
|
|
"vat": vat_cost,
|
|
"contingency": contingency_cost,
|
|
"preliminaries": preliminaries_cost,
|
|
"material": materials_costs,
|
|
"profit": profit_cost,
|
|
"labour_hours": labour_hours,
|
|
"labour_days": labour_days,
|
|
"labour_cost": labour_costs
|
|
}
|
|
|
|
def low_energy_lighting(self, number_of_lights, number_current_lel_lights, material):
|
|
|
|
"""
|
|
Calculates the total cost for low energy lighting based on material and labor costs,
|
|
including contingency, preliminaries, profit, and VAT.
|
|
|
|
:param number_of_lights: Int, number of light
|
|
:param number_current_lel_lights: Int, number of low energy lights currently installed in the home
|
|
:material: Dict, material data containing costs of fittings
|
|
"""
|
|
|
|
# If there are no lights fitted in the property, we increase the contingency in case there are potential wiring
|
|
# blockers
|
|
if number_current_lel_lights == 0:
|
|
contingency = self.HIGH_RISK_CONTINGENCY
|
|
else:
|
|
contingency = self.CONTINGENCY
|
|
|
|
material_cost = material["material_cost"] * number_of_lights
|
|
labour_cost = material["labour_cost"] * number_of_lights * self.labour_adjustment_factor
|
|
|
|
subtotal_before_profit = material_cost + labour_cost
|
|
|
|
contingency_cost = subtotal_before_profit * contingency
|
|
preliminaries_cost = subtotal_before_profit * self.PRELIMINARIES
|
|
profit_cost = subtotal_before_profit * self.PROFIT_MARGIN
|
|
|
|
subtotal_before_vat = subtotal_before_profit + contingency_cost + preliminaries_cost + profit_cost
|
|
vat_cost = subtotal_before_vat * self.VAT_RATE
|
|
total_cost = subtotal_before_vat + vat_cost
|
|
|
|
labour_hours = material["labour_hours_per_unit"] * number_of_lights
|
|
# Assume a single electrician installing
|
|
labour_days = (labour_hours / 8)
|
|
|
|
return {
|
|
"total": total_cost,
|
|
"subtotal": subtotal_before_vat,
|
|
"vat": vat_cost,
|
|
"contingency": contingency_cost,
|
|
"preliminaries": preliminaries_cost,
|
|
"material": material_cost,
|
|
"profit": profit_cost,
|
|
"labour_hours": labour_hours,
|
|
"labour_days": labour_days,
|
|
"labour_cost": labour_cost
|
|
}
|
|
|
|
def flat_roof_insulation(self, floor_area, material, non_insulation_materials):
|
|
"""
|
|
A model of a warm, flat roof construction can be seen in this video:
|
|
https://www.youtube.com/watch?v=WZ6Ng6YI9OA
|
|
Warm, flat roof insulation will normally be 100-125mm in depth
|
|
|
|
We break this measure down into the following jobs to be done
|
|
1) Preparation of the room. This involves cleaning the existing roof surface, removing any debris and repairing
|
|
any damage. Additionally, an edge barrier will likely need to be installed, to protect the sides of the
|
|
roof from water ingress.
|
|
2) Primer Application. A layer of primer is applied to the clean roof surface to enhance the adhestia of
|
|
subsequent layers, and seal the existing roof surface.
|
|
3) Vapour Proof Layer Installation. Lay a vapour control layer to prevent moisture ingress from inside the
|
|
building, which is essential in warm roof construction.
|
|
4) Insulation Layer Application. Place and securely fix insulation boards over the roof. These could be rigid
|
|
boards like PIR (Polyisocyanurate).
|
|
5) Waterproofing Membrane Installation: Cover the insulation (and timber layer, if used) with a
|
|
waterproofing membrane, like EPDM, PVC, or bituminous felt. Carefully seal all joints, edges, and around any
|
|
roof penetrations to ensure water tightness
|
|
|
|
:param floor_area: Area of the flat roof to be insulated, based on the area of the floor
|
|
:param material: Selected insulation material
|
|
:param non_insulation_materials: Non-insulation materials required for the job
|
|
:return:
|
|
"""
|
|
|
|
preparation_data_m2 = [
|
|
x for x in non_insulation_materials if
|
|
(x["type"] == "flat_roof_preparation") and (x["cost_unit"] == "gbp_per_m2")
|
|
]
|
|
vapour_barrier_data = [x for x in non_insulation_materials if x["type"] == "flat_roof_vapour_barrier"]
|
|
waterproofing_data = [x for x in non_insulation_materials if x["type"] == "flat_roof_waterproofing"]
|
|
|
|
if (len(preparation_data_m2) != 2) or (len(vapour_barrier_data) != 1) or (
|
|
len(waterproofing_data) != 1):
|
|
raise ValueError("Incorrect number of data entries for non-insulation materials")
|
|
|
|
# Break out the individual material costs
|
|
preparation_m2_material_costs = sum([x["material_cost"] * floor_area for x in preparation_data_m2])
|
|
vapour_barrier_material_costs = vapour_barrier_data[0]["material_cost"] * floor_area
|
|
insulation_material_costs = material["material_cost"] * floor_area
|
|
|
|
preparation_m2_labour_costs = sum([x["labour_cost"] * floor_area for x in preparation_data_m2])
|
|
vapour_barrier_labour_costs = vapour_barrier_data[0]["labour_cost"] * floor_area
|
|
|
|
# For waterproofing and upstand, we only have a total cost
|
|
waterproofing_total_costs = waterproofing_data[0]["total_cost"] * floor_area
|
|
|
|
labour_costs = preparation_m2_labour_costs + vapour_barrier_labour_costs
|
|
labour_costs = labour_costs * self.labour_adjustment_factor
|
|
|
|
materials_costs = preparation_m2_material_costs + vapour_barrier_material_costs + insulation_material_costs
|
|
|
|
subtotal_before_profit = labour_costs + materials_costs + waterproofing_total_costs
|
|
|
|
contingency_cost = subtotal_before_profit * self.FLAT_ROOF_CONTINGENCY
|
|
preliminaries_cost = subtotal_before_profit * self.PRELIMINARIES
|
|
profit_cost = subtotal_before_profit * self.PROFIT_MARGIN
|
|
|
|
subtotal_before_vat = subtotal_before_profit + contingency_cost + preliminaries_cost + profit_cost
|
|
vat_cost = subtotal_before_vat * self.VAT_RATE
|
|
total_cost = subtotal_before_vat + vat_cost
|
|
|
|
preparation_m2_labour_hours = sum([x["labour_hours_per_unit"] * floor_area for x in preparation_data_m2])
|
|
vapour_barrier_labour_hours = vapour_barrier_data[0]["labour_hours_per_unit"] * floor_area
|
|
waterproofing_labour_hours = waterproofing_data[0]["labour_hours_per_unit"] * floor_area
|
|
|
|
labour_hours = preparation_m2_labour_hours + vapour_barrier_labour_hours + waterproofing_labour_hours
|
|
|
|
# To install flat roof insulation, assume a small/medium project might be conducted by a team of 2-4.
|
|
# We'll assume a team of 2 since a lot of the roofs will be on the smaller side and will review this later
|
|
labour_days = (labour_hours / 8) / 2
|
|
|
|
return {
|
|
"total": total_cost,
|
|
"subtotal": subtotal_before_vat,
|
|
"vat": vat_cost,
|
|
"contingency": contingency_cost,
|
|
"preliminaries": preliminaries_cost,
|
|
"material": materials_costs,
|
|
"profit": profit_cost,
|
|
"labour_hours": labour_hours,
|
|
"labour_days": labour_days,
|
|
"labour_cost": labour_costs
|
|
}
|
|
|
|
def window_glazing(self, number_of_windows, material, is_secondary_glazing=False):
|
|
"""
|
|
We characterise the jobs to be done for window glazing as the following:
|
|
1) Initial Assessment and Measurements: Before removing the existing window, it's essential to assess the
|
|
condition of the window frame and opening. Precise measurements are taken to ensure the new double glazed
|
|
windows fit perfectly.
|
|
|
|
2) Remove the Existing Window: This involves carefully dismantling and removing the old single glazed window. It
|
|
requires skill to avoid damaging the surrounding wall and the window frame (if it's to be reused).
|
|
|
|
3) Dispose of the Existing Window: The old window, especially if it's a single glazed unit, needs to be
|
|
disposed of responsibly. Glass and other materials should be recycled where possible.
|
|
|
|
4) Surface Preparation: The window opening might need some preparation, especially if there's damage or if
|
|
adjustments are needed to accommodate the new window. This can include repairing or replacing parts of the
|
|
window frame, sealing gaps, and ensuring the opening is level and square.
|
|
|
|
5) Install the Window Frame (if new frames are used): In many cases, double glazed windows come with their
|
|
frames. These need to be installed securely into the window opening. This process involves aligning, leveling,
|
|
and fixing the frame in place.
|
|
|
|
6) Install the Window Sill: If a new window sill is required, it is installed at this stage. It needs to be
|
|
correctly aligned with the frame and securely attached.
|
|
|
|
7) Install the Double Glazed Glass Units: The glass units are carefully inserted into the frame. This step
|
|
requires precision to ensure a snug fit without causing stress on the glass, which could lead to cracking or
|
|
breaking.
|
|
|
|
8) Sealing and Weatherproofing: After the glass units are in place, it's crucial to seal around the frame and
|
|
between the glass and frame to ensure there are no drafts and that the installation is weather-tight. This
|
|
typically involves applying silicone sealant or other appropriate sealing materials.
|
|
|
|
9) Finishing Touches: This includes any cosmetic work, such as trimming, painting, or staining the frame and
|
|
sill to match the rest of the property. It might also involve cleaning up any mess created during the
|
|
installation.
|
|
|
|
10) Inspection and Testing: Finally, the new windows should be inspected to ensure they open, close, and lock
|
|
correctly. This is also a good time to check for any gaps or issues with the sealing.
|
|
|
|
For this cost estimation process, we factor in initial assement into the preliminaries
|
|
|
|
"""
|
|
|
|
material_cost = material["material_cost"] * number_of_windows
|
|
|
|
labour_cost = (
|
|
material["labour_cost"] * number_of_windows * self.labour_adjustment_factor
|
|
)
|
|
multiplier = self.SECONDARY_GLAZING_SCALING_FACTOR if is_secondary_glazing else (
|
|
self.SASH_WINDOW_INFLATION_FACTOR)
|
|
|
|
subtotal = (material_cost + labour_cost) * multiplier
|
|
|
|
contingency_cost = subtotal * self.CONTINGENCY
|
|
preliminaries_cost = subtotal * self.PRELIMINARIES
|
|
profit_cost = subtotal * self.PROFIT_MARGIN
|
|
|
|
subtotal_before_vat = subtotal + contingency_cost + preliminaries_cost + profit_cost
|
|
|
|
vat_cost = subtotal_before_vat * self.VAT_RATE
|
|
|
|
total_cost = subtotal_before_vat + vat_cost
|
|
|
|
labour_hours = material["labour_hours_per_unit"] * number_of_windows
|
|
labour_hours = labour_hours * self.SECONDARY_GLAZING_SCALING_FACTOR if is_secondary_glazing else labour_hours
|
|
|
|
# Assume a team of 2
|
|
labour_days = (labour_hours / 8) / 2
|
|
|
|
return {
|
|
"total": total_cost,
|
|
"subtotal": subtotal_before_vat,
|
|
"vat": vat_cost,
|
|
"contingency": contingency_cost,
|
|
"preliminaries": preliminaries_cost,
|
|
"material": material_cost,
|
|
"profit": profit_cost,
|
|
"labour_hours": labour_hours,
|
|
"labour_cost": labour_cost,
|
|
"labour_days": labour_days
|
|
}
|
|
|
|
def solar_pv(self, wattage: float, has_battery: bool = False):
|
|
|
|
"""
|
|
Calculates the total cost for solar PV based data provided by the MCS dashboard, which contains
|
|
costing data for installations of renewable and clean energy measures.
|
|
|
|
The data in the dashboard is filtered on domestic building installations and then the data across the
|
|
various regions is manually collected. There is currently no automated way to get the data from the MCS
|
|
dashboard
|
|
|
|
Price can also be benchmarked against this checkatrade article:
|
|
https://www.checkatrade.com/blog/cost-guides/cost-of-solar-panel-installation/
|
|
:param wattage: Peak wattage of the solar PV system]
|
|
:param has_battery: Bool, whether the system includes a battery
|
|
"""
|
|
|
|
# Get the cost data relevant to the region
|
|
regional_cost = MCS_SOLAR_PV_COST_DATA["-".join(["average_cost_per_kwh", self.region])]
|
|
|
|
kw = wattage / 1000
|
|
total_cost = kw * regional_cost
|
|
|
|
if has_battery:
|
|
# The battery cost is based on the £3500 quote, recieved from installers
|
|
total_cost += BATTERY_COST
|
|
|
|
subtotal_before_vat = total_cost / (1 + self.VAT_RATE)
|
|
|
|
vat = total_cost - subtotal_before_vat
|
|
|
|
# Labour hours are based on estimates from online research but an average team seems to consist of 3 people
|
|
# and most jobs take around 2 days. Assuming an 8 hour day for 3 people across 2 days, gives us 72 hours of
|
|
# labour
|
|
return {
|
|
"total": total_cost,
|
|
"subtotal": subtotal_before_vat,
|
|
"vat": vat,
|
|
"labour_hours": 72,
|
|
"labour_days": 2,
|
|
}
|
|
|
|
def programmer_and_appliance_thermostat(self, has_programmer):
|
|
"""
|
|
Calculate the total cost of installing a programmer and appliance thermostat
|
|
If the property already has a programmer, then the only thing we need to calculate the cost for is the
|
|
appliance thermostat
|
|
"""
|
|
|
|
if has_programmer:
|
|
labour_hours = 2
|
|
total_cost = SMART_APPLIANCE_THERMOSTAT_COST
|
|
else:
|
|
labour_hours = 4
|
|
total_cost = SMART_APPLIANCE_THERMOSTAT_COST + PROGRAMMER_COST
|
|
|
|
subtotal_before_vat = total_cost / (1 + self.VAT_RATE)
|
|
vat = total_cost - subtotal_before_vat
|
|
|
|
# We estimate the cost of an appliance thermostat at £400, which is the upper end of the range
|
|
return {
|
|
"total": total_cost,
|
|
"subtotal": subtotal_before_vat,
|
|
"vat": vat,
|
|
"labour_hours": labour_hours,
|
|
"labour_days": 1,
|
|
}
|
|
|
|
def electric_room_heaters(self, number_heated_rooms):
|
|
"""
|
|
We base the estimates for the cost of electric room heaters on the cost per room as estimated by the
|
|
following article:
|
|
https://www.bestelectricradiators.co.uk/blog/cost-to-install-a-new-heating-system-uk/
|
|
|
|
:param number_heated_rooms: int, number of rooms to be heated
|
|
:return:
|
|
"""
|
|
|
|
total_cost = 500 * number_heated_rooms
|
|
subtotal_before_vat = total_cost / (1 + self.VAT_RATE)
|
|
vat = total_cost - subtotal_before_vat
|
|
|
|
# TODO: Rough estimate to be reviewed
|
|
labour_hours = 1 * number_heated_rooms
|
|
labour_days = np.ceil(labour_hours / 8)
|
|
|
|
return {
|
|
"total": total_cost,
|
|
"subtotal": subtotal_before_vat,
|
|
"vat": vat,
|
|
"labour_hours": labour_hours,
|
|
"labour_days": labour_days,
|
|
}
|
|
|
|
def high_heat_electric_storage_heaters(self, number_heated_rooms):
|
|
|
|
"""
|
|
We base the estimates for the cost of electric storage heaters on the cost per room as estimated by the
|
|
energy saving trust
|
|
https://energysavingtrust.org.uk/advice/electric-heating/
|
|
|
|
The cost is based on the number of heated rooms
|
|
:param number_heated_rooms: int, number of rooms to be heated
|
|
"""
|
|
|
|
total_cost = 1500 * number_heated_rooms
|
|
subtotal_before_vat = total_cost / (1 + self.VAT_RATE)
|
|
vat = total_cost - subtotal_before_vat
|
|
|
|
# TODO: Rough estimate to be reviewed
|
|
labour_hours = 3 * number_heated_rooms
|
|
labour_days = np.ceil(labour_hours / 8)
|
|
|
|
return {
|
|
"total": total_cost,
|
|
"subtotal": subtotal_before_vat,
|
|
"vat": vat,
|
|
"labour_hours": labour_hours,
|
|
"labour_days": labour_days,
|
|
}
|
|
|
|
def celect_type_controls(self):
|
|
"""
|
|
Calculate the cost of installing Celect type controls
|
|
"""
|
|
|
|
# The £50 cost is a rough estimate based on internet research
|
|
total_cost = 50
|
|
subtotal_before_vat = total_cost / (1 + self.VAT_RATE)
|
|
vat = total_cost - subtotal_before_vat
|
|
|
|
# We estimate the labour hours to be 4
|
|
return {
|
|
"total": total_cost,
|
|
"subtotal": subtotal_before_vat,
|
|
"vat": vat,
|
|
"labour_hours": 4,
|
|
"labour_days": 1,
|
|
}
|
|
|
|
def hot_water_tank_insulation(self):
|
|
"""
|
|
Calculate the cost of installing hot water tank insulation
|
|
"""
|
|
|
|
# The £50 cost is a rough estimate based on internet research
|
|
total_cost = 50
|
|
subtotal_before_vat = total_cost / (1 + self.VAT_RATE)
|
|
vat = total_cost - subtotal_before_vat
|
|
|
|
return {
|
|
"total": total_cost,
|
|
"subtotal": subtotal_before_vat,
|
|
"vat": vat,
|
|
"labour_hours": 0,
|
|
"labour_days": 0,
|
|
}
|
|
|
|
def roomstat_programmer_trvs(
|
|
self, number_heated_rooms, has_programmer, has_trvs, has_room_thermostat
|
|
):
|
|
"""
|
|
|
|
:return:
|
|
"""
|
|
|
|
total_cost = 0
|
|
labour_hours = 0
|
|
|
|
if not has_programmer:
|
|
total_cost += PROGRAMMER_COST
|
|
labour_hours += 1
|
|
|
|
if not has_trvs:
|
|
total_cost += TRVS_COST * number_heated_rooms
|
|
labour_hours += 0.25 * number_heated_rooms
|
|
|
|
if not has_room_thermostat:
|
|
total_cost += ROOM_THERMOSTAT_COST
|
|
labour_hours += 0.5
|
|
|
|
subtotal_before_vat = total_cost / (1 + self.VAT_RATE)
|
|
vat = total_cost - subtotal_before_vat
|
|
|
|
return {
|
|
"total": total_cost,
|
|
"subtotal": subtotal_before_vat,
|
|
"vat": vat,
|
|
"labour_hours": labour_hours,
|
|
"labour_days": 1,
|
|
}
|
|
|
|
def time_and_temperature_zone_control(self, number_heated_rooms):
|
|
|
|
# The product costs are inclusive of VAT
|
|
product_costs = (
|
|
TTZC_SMART_THERMOSTAT_COST +
|
|
TTZC_ROOM_TEMPERATURE_SENSOR_COST * number_heated_rooms +
|
|
TTZC_SMART_RADIATOR_VALUES * number_heated_rooms
|
|
)
|
|
labour_hours = (
|
|
TTZC_SMART_THERMOSTAT_LABOUR_HOURS +
|
|
TTZC_ROOM_TEMPERATURE_SENSOR_LABOUR_HOURS * number_heated_rooms +
|
|
TTZC_SMART_RADIATOR_VALUES_LABOUR_HOURS * number_heated_rooms
|
|
)
|
|
labour_costs = TTZC_ELECTRICIAN_HOURLY_RATE * labour_hours
|
|
# Add continency and preliminaries to the labour to account for the complexity of the job
|
|
labour_costs = labour_costs * (1 + self.CONTINGENCY + self.PRELIMINARIES)
|
|
|
|
vat = labour_costs * self.VAT_RATE
|
|
|
|
subtotal_before_vat = product_costs + labour_costs
|
|
total_cost = subtotal_before_vat + vat
|
|
|
|
labour_days = np.ceil(labour_hours / 8)
|
|
|
|
return {
|
|
"total": total_cost,
|
|
"subtotal": subtotal_before_vat,
|
|
"vat": vat,
|
|
"labour_hours": labour_hours,
|
|
"labour_days": labour_days,
|
|
}
|
|
|
|
def heater_removal(self, n_rooms):
|
|
"""
|
|
Estimates the costs of removal of heaters, including the redecoration costs of the space behind the heater
|
|
:return:
|
|
"""
|
|
|
|
removal_cost = ROOM_HEATER_REMOVAL_COST * n_rooms
|
|
removal_labour_hours = ROOM_HEATER_REMOVAL_LABOUR_HOURS * n_rooms
|
|
|
|
vat = removal_cost * self.VAT_RATE
|
|
|
|
subtotal_before_vat = removal_cost
|
|
total_cost = subtotal_before_vat + vat
|
|
|
|
return {
|
|
"total": total_cost,
|
|
"subtotal": subtotal_before_vat,
|
|
"vat": vat,
|
|
"labour_hours": removal_labour_hours,
|
|
"labour_days": np.ceil(removal_labour_hours / 8),
|
|
}
|
|
|
|
@staticmethod
|
|
def _estimate_n_radiators(number_habitable_rooms, total_floor_area, property_type, built_form):
|
|
# Base number of radiators: one per habitable room
|
|
base_radiators = number_habitable_rooms
|
|
|
|
# Additional radiators for non-habitable essential areas (e.g., kitchens, hallways)
|
|
additional_radiators = 3 # Initial assumption
|
|
|
|
# Adjust additional radiators based on property type
|
|
if property_type == 'Flat':
|
|
additional_radiators -= 1 # Flats may need fewer radiators due to less exposure
|
|
elif property_type in ['House', 'Bungalow', 'Maisonette']:
|
|
# Multiple floors in Maisonette may require additional heating points
|
|
additional_radiators += 2 # Houses and bungalows might need more due to greater exposure
|
|
else:
|
|
raise Exception("Invalid property type")
|
|
|
|
# Adjust total radiator needs based on built form
|
|
form_factor = {
|
|
'Mid-Terrace': 0.95,
|
|
'Semi-Detached': 1.05,
|
|
'Detached': 1.25,
|
|
'End-Terrace': 1.05
|
|
}
|
|
|
|
# Calculate total heating power needed and number of radiators based on standard output
|
|
total_heating_power_required = total_floor_area * 80 # Watts per square meter
|
|
radiator_output = 1000 # Average wattage per radiator
|
|
total_radiators_based_on_power = (total_heating_power_required / radiator_output) * form_factor[built_form]
|
|
|
|
# Final estimation taking the higher of calculated needs or base room count
|
|
estimated_radiators = max(total_radiators_based_on_power, base_radiators + additional_radiators)
|
|
return round(estimated_radiators)
|
|
|
|
def boiler(self, size, exising_room_heaters, system_change, n_heated_rooms, n_rooms):
|
|
"""
|
|
Based on a basic estimate of median value £2600 to install a low carbon combi boiler
|
|
First time central heating vosts can als be found here:
|
|
https://www.checkatrade.com/blog/cost-guides/central-heating-installation-cost/
|
|
:return:
|
|
"""
|
|
|
|
unit_cost = CONDENSING_BOILER_COSTS[size]
|
|
# The unit cost is the cost without VAT
|
|
# We now need to estimate the cost of the works
|
|
labour_days = 2
|
|
labour_hours = labour_days * 8
|
|
labour_rate = 300
|
|
|
|
# Average cost of installation is 1 (maybe 2days) at £300 per day
|
|
# https://www.checkatrade.com/blog/cost-guides/new-boiler-cost/
|
|
# To be pessimistic, assume 2 days work
|
|
labour_cost = labour_rate * self.labour_adjustment_factor * labour_days
|
|
# Add contingency and preliminaries
|
|
labour_cost = labour_cost * (1 + self.CONTINGENCY + self.PRELIMINARIES)
|
|
|
|
# labour_days = labour_days + (removal_labour_hours / 8)
|
|
|
|
vat = labour_cost * self.VAT_RATE
|
|
|
|
subtotal_before_vat = unit_cost + labour_cost
|
|
total_cost = subtotal_before_vat + vat
|
|
|
|
# if there are existing room heaters, we need to add the cost of removing them
|
|
if exising_room_heaters:
|
|
removal_costing = self.heater_removal(n_rooms=n_heated_rooms)
|
|
# Add the totals to the existing totals
|
|
total_cost += removal_costing["total"]
|
|
subtotal_before_vat += removal_costing["subtotal"]
|
|
labour_hours += removal_costing["labour_hours"]
|
|
labour_days += removal_costing["labour_days"]
|
|
vat += removal_costing["vat"]
|
|
|
|
if system_change:
|
|
# We need the cost of radiators
|
|
n_radiators = self._estimate_n_radiators(
|
|
number_habitable_rooms=n_rooms,
|
|
total_floor_area=self.property.floor_area,
|
|
property_type=self.property.data["property-type"],
|
|
built_form=self.property.data["built-form"]
|
|
)
|
|
|
|
additionals_labour_cost = labour_rate * self.labour_adjustment_factor
|
|
radiator_cost = DOUBLE_RADIATOR_COST * n_radiators
|
|
system_change_cost = radiator_cost + FLUE_COST + PIPEWORK_COST + additionals_labour_cost
|
|
system_change_cost_before_vat = system_change_cost / (1 + self.VAT_RATE)
|
|
system_change_vat = system_change_cost - system_change_cost_before_vat
|
|
# We add an extra labour day for the system change
|
|
labour_days += 1
|
|
labour_hours += 8
|
|
total_cost += system_change_cost
|
|
subtotal_before_vat += system_change_cost_before_vat
|
|
vat += system_change_vat
|
|
|
|
return {
|
|
"total": total_cost,
|
|
"subtotal": subtotal_before_vat,
|
|
"vat": vat,
|
|
"labour_hours": labour_hours,
|
|
"labour_days": labour_days,
|
|
}
|
|
|
|
def air_source_heat_pump(self):
|
|
"""
|
|
Based on the region and type of property, this function will produce a cost estimation for an air source heat
|
|
pump. This cost will include the boiler upgrade scheme grant
|
|
|
|
"""
|
|
|
|
# This is the average cost of a project, we'll add some additional contingency
|
|
regional_cost = MCS_AIR_SOURCE_HEAT_PUMP_COST_DATA[self.region]
|
|
|
|
total_cost = regional_cost * (1 + self.CONTINGENCY) - BOILER_UPGRADE_SCHEME_ASHP_VALUE
|
|
subtotal_before_vat = total_cost / (1 + self.VAT_RATE)
|
|
vat = total_cost - subtotal_before_vat
|
|
|
|
# We assume 3 days installation
|
|
labour_days = 3
|
|
labour_hours = labour_days * 8
|
|
|
|
return {
|
|
"total": total_cost,
|
|
"subtotal": subtotal_before_vat,
|
|
"vat": vat,
|
|
"labour_hours": labour_hours,
|
|
"labour_days": labour_days,
|
|
}
|