Merge pull request #428 from Hestia-Homes/remote-assessment-api

Remote assessment api
This commit is contained in:
KhalimCK 2025-05-15 16:27:00 +01:00 committed by GitHub
commit 5f9ac2fced
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 521 additions and 255 deletions

View file

@ -36,8 +36,3 @@ jobs:
run: |
pip install -r model_data/requirements/dev.txt
pytest
# - name: Upload coverage to Codecov
# uses: codecov/codecov-action@v2
# with:
# token: ${{ secrets.CODECOV_TOKEN }}
# fail_ci_if_error: true

7
.idea/Model.iml generated
View file

@ -10,11 +10,4 @@
<orderEntry type="jdk" jdkName="Fastapi-backend" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyNamespacePackagesService">
<option name="namespacePackageFolders">
<list>
<option value="$MODULE_DIR$/local_data" />
</list>
</option>
</component>
</module>

30
Makefile Normal file
View file

@ -0,0 +1,30 @@
# Project Makefile
PYTHON = python
.PHONY: setup test lint typecheck check clean
# Install dev dependencies + tox
setup:
$(PYTHON) -m pip install --upgrade pip
$(PYTHON) -m pip install tox black ruff mypy
# Run tests (pass ARGS="..." for specific tests)
test:
tox -- $(ARGS)
# Code formatting check + linting
lint:
ruff .
black --check .
# Static type checks
typecheck:
mypy .
# Full quality check (all checks + tests)
check: lint typecheck test
# Clean up tox environments
clean:
rm -rf .tox

View file

@ -37,4 +37,5 @@ To run tests in a specific service, e.g. inside of model_data, simply run
pytest --cov-config=model_data/.coveragerc --cov=model_data
```
This will produce the test results and coverage reports
This will produce the test results and coverage reports

View file

@ -10,7 +10,7 @@ from openai import OpenAI
import numpy as np
import pandas as pd
from tqdm import tqdm
from fuzzywuzzy import process
from thefuzz import process
from utils.logger import setup_logger
from backend.SearchEpc import SearchEpc
from BaseUtility import Definitions
@ -1235,11 +1235,11 @@ class AssetList:
elif self.old_format_non_intrusives_present:
non_intrusives_wall_filter = (
self.standardised_asset_list['non-intrusives: WFT Findings'].str.lower().str.strip().isin(
["empty cavity", "partial fill", "empty", "EMPTY CAVITY 70MM", "partial"]
["empty cavity", "partial fill", "empty", "EMPTY CAVITY 70MM", "partial", "empty cav"]
) | (
(
self.standardised_asset_list['non-intrusives: WFT Findings']
.str.lower().str.strip().str.contains("empty cavity|partial fill|empty|partial") &
.str.lower().str.strip().str.contains("empty cavity|partial fill") &
~self.standardised_asset_list['non-intrusives: WFT Findings']
.astype(str).str.lower().str.strip().str.contains("major access issues")
)
@ -1368,8 +1368,16 @@ class AssetList:
print("Review these categories!!!!")
extraction_wall_filter = (
self.standardised_asset_list['non-intrusives: WFT Findings'].str.lower().str.strip().isin(
["retro drilled", "retro filled", "fibre from build", "polybead", "retro drilled and filled",
"retro drilled & filled", "blown in white wool", "blown in yellow wool"]
[
'blown in yellow wool', 'retro drilled & filled', 'white fibre from build',
'foam filled from build', 'retro drilled gas in block', 'block in rock wool', 'rdf / tilehung',
'fibre from build', 'blown in rock wool', 'rdf / tile hung', 'retro drilled',
'rock wool from build', 'part rendered retro drilled', 'white fibtr from build.',
'retro drilled and filled', 'blown in white wool', 'blown in yellow fibre from build', 'rdf',
'polybead', 'foam filled', 'blown in white bead from build', 'blown in yellow fibre',
'retro drilled det', 'blown in rockwool', 'retro drilled det empty cav', 'retro drilled end',
'retro filled extension', 'retro filled', 'foam'
]
)
)
@ -1433,7 +1441,18 @@ class AssetList:
self.standardised_asset_list["solar_epc_data_indicates_correct_heating_system"] &
self.standardised_asset_list["solar_epc_data_indicates_requires_heating_upgrade"]
).sum():
raise ValueError("Both heating system checks are true - this should not be possible")
logger.info("We have an example of both heating system checks being true - checking known cases")
known_edge_cases = ['Ground source heat pump, radiators, electric, Electric storage heaters']
error_cases = self.standardised_asset_list[
(
self.standardised_asset_list["solar_epc_data_indicates_correct_heating_system"] &
self.standardised_asset_list["solar_epc_data_indicates_requires_heating_upgrade"]
)
]
if all(error_cases[self.EPC_API_DATA_NAMES["mainheat-description"]].isin(known_edge_cases)):
logger.info("Within known edge cases")
else:
raise ValueError("Both heating system checks are true - this should not be possible")
# Check 3: Does the property meet the fabric condition
# Solar PV installs are subject to the minimum insulation requirements which means:

View file

@ -64,7 +64,7 @@ def app():
# Thurrock
data_folder = "/Users/khalimconn-kowlessar/Documents/hestia/Customers/Thurrock"
data_filename = "THURROCK COUNCIL.xlsx"
data_filename = "THURROCK COUNCIL - For analysis.xlsx"
sheet_name = "Assets"
postcode_column = 'Postcode'
fulladdress_column = "Full Address"
@ -182,6 +182,34 @@ def app():
# master_filepaths = []
# master_to_asset_list_filepath = None
data_folder = "/Users/khalimconn-kowlessar/Documents/hestia/Customers/Places For People/North-West"
data_filename = "Places for People NORTH WEST - INSPECTIONS MASTER - UPDATE.xlsx"
sheet_name = "CHECKED"
postcode_column = 'Postcode'
fulladdress_column = None
address1_column = "AddressLine1"
address1_method = None
address_cols_to_concat = ["AddressLine1", "AddressLine2", "AddressLine3"]
missing_postcodes_method = None
landlord_year_built = None
landlord_os_uprn = None
landlord_property_type = "Archetype (PFP)"
landlord_built_form = "Archetype (PFP)"
landlord_wall_construction = None
landlord_roof_construction = None
landlord_heating_system = None
landlord_existing_pv = None
landlord_property_id = "Uprn"
outcomes_filename = None
outcomes_sheetname = None
outcomes_postcode = None
outcomes_houseno = None
outcomes_id = None
master_filepaths = []
master_to_asset_list_filepath = None
landlord_sap = None
phase = None
# Maps addresses to uprn in problematic cases
manual_uprn_map = {}

View file

@ -3,7 +3,7 @@ pandas
usaddress
pydantic-settings==2.6.0
epc-api-python==1.0.2
fuzzywuzzy
thefuzz
boto3
openpyxl
openai

View file

@ -14,7 +14,7 @@ from etl.epc_clean.epc_attributes.RoofAttributes import RoofAttributes
from BaseUtility import Definitions
from utils.logger import setup_logger
from typing import List
from fuzzywuzzy import process
from thefuzz import process
from backend.app.utils import sap_to_epc
logger = setup_logger()

View file

@ -10,8 +10,8 @@ boto3==1.35.44
# ML, Data Science
usaddress==0.5.11
epc-api-python==1.0.2
fuzzywuzzy==0.18.0
python-Levenshtein==0.26.0
thefuzz
python-Levenshtein>=0.24.0,!=0.26.0
textblob==0.18.0.post0
msgpack==1.1.0
scikit-learn==1.5.2

View file

@ -103,6 +103,11 @@ class TestProperty:
property_instance.number_of_rooms = 5
property_instance.floor_area = 100
property_instance.floor_height = 2.5
# Fill these values that come from the epc_record
property_instance.energy["primary_energy_consumption"] = 1234
property_instance.energy["epc_co2_emissions"] = 5
return property_instance
@pytest.fixture

View file

@ -9,7 +9,7 @@ EPC_AUTH_TOKEN = os.getenv("EPC_AUTH_TOKEN")
class TestSearchEpcIntegration:
@pytest.mark.parametrize(
"address, postcode, uprn, skip_os, expected_partial_address",
"address, postcode, uprn, skip_os, lmk_key, n_old_epcs",
[
# Test case 1: Valid address and postcode, skipping OS
# In this case, the property is an individual flat but the uprn associated to the
@ -21,7 +21,11 @@ class TestSearchEpcIntegration:
# In this case, the newest EPC, does not have a uprn associated to it. If we did a search by
# uprn, we would get an old EPC
("Flat 8, Hainton House", "DN32 9AQ", 10090082018, True,
"bd1149a20a73397184f07a9955f872424826e70f4870c058d71be887766ee1f8", 3),
"bd1149a20a73397184f07a9955f872424826e70f4870c058d71be887766ee1f8", 2),
# Test case 3: When we make a request to the API for this property, we get back results for
# flats 1, 2 and 3. We have some logic to handle the response so that we get back flat 1
("Flat 1, 1 Tottenham Street, London", "W1T 2AE", 5167411, True,
"3e6414d7f15f4cf7a69dc20c469bcf043d31a49239b183f1bd0c0e1aafa23c93", 0),
],
)

View file

@ -0,0 +1,104 @@
"""
This script marks which properties have been reviewed by the Medway.
"""
import pandas as pd
import numpy as np
from tqdm import tqdm
import os
asset_list = pd.read_excel(
"/Users/khalimconn-kowlessar/Documents/hestia/Customers/Medway/MEDWAY Asset List - Standardised.xlsx",
sheet_name="Standardised Asset List",
)
flat_data = pd.read_excel(
"/Users/khalimconn-kowlessar/Documents/hestia/Customers/Medway/MEDWAY Asset List - Standardised.xlsx",
sheet_name="Flat Data",
)
reviewed_assets = pd.read_excel(
"/Users/khalimconn-kowlessar/Documents/hestia/Customers/Medway/Programme Final Check.xlsx",
)
exclude_from_programme = reviewed_assets.copy() # [reviewed_assets["Khalim - include in programme"] == "No"].copy()
exclude_from_programme["exclusion_reason"] = None
exclude_from_programme["exclusion_reason"] = np.where(
(exclude_from_programme["UPRN"] == "SOLD"),
"Sold",
exclude_from_programme["exclusion_reason"],
)
exclude_from_programme["exclusion_reason"] = np.where(
(exclude_from_programme["Include in SHDF Bid?"] == "Definite"),
"Included in SHDF Bid",
exclude_from_programme["exclusion_reason"],
)
exclude_from_programme["exclusion_reason"] = np.where(
(exclude_from_programme['Move Forward'] == "EPC C"),
"Excluded from Programme",
exclude_from_programme["exclusion_reason"],
)
# exclude_from_programme = exclude_from_programme[~pd.isnull(exclude_from_programme["exclusion_reason"])]
exclude_from_programme = exclude_from_programme.reset_index(drop=True)
exclude_from_programme["row_id"] = exclude_from_programme.index
# Match to asset list
matched = []
for _, x in tqdm(exclude_from_programme.iterrows(), total=len(exclude_from_programme)):
if x["No."] == 218 and x["Postcode"] == "ME8 6QB":
pc = "ME8 6QP"
elif x["No."] == 198 and x["Postcode"] == "ME8 6HL":
pc = "ME8 6LU"
elif x["No."] == "39a" and x["Postcode"] == "ME7 2BU":
pc = "ME7 2BU"
else:
pc = x["Postcode"]
hn = x["No."]
m = asset_list[
(asset_list["domna_address_1"] == str(hn)) &
(asset_list["domna_postcode"] == str(pc))
]
if m.empty:
m = asset_list[
(asset_list["domna_full_address"].str.replace(",", "").str.lower().str.contains(
x["Address"].lower().strip()))
]
if m.shape[0] == 1:
matched.append(
{
"full_address": m["domna_full_address"].values[0],
"postcode": m["domna_postcode"].values[0],
"review_no": x["No."],
"review_address": x["Address"],
"review_postcode": x["Postcode"],
"exclusion_reason": x["exclusion_reason"],
"landlord_property_id": m["landlord_property_id"].values[0],
"ciga_guarantee": x["Unnamed: 21"]
}
)
continue
raise NotImplementedError("FIX ME")
matched = pd.DataFrame(matched)
matched = matched.rename(
columns={"review_address": "ciga_check_address"}
)
asset_list = asset_list.merge(
matched[["landlord_property_id", "exclusion_reason", "ciga_guarantee", "ciga_check_address"]],
how="left", on="landlord_property_id"
)
# Store as an excel
filename = "/Users/khalimconn-kowlessar/Documents/hestia/Customers/Medway/Reviewed Standardised Programme.xlsx"
# Store the data in two tabs. One for the asset list with the EPC data and the second with the flat data
with pd.ExcelWriter(filename) as writer:
asset_list.to_excel(writer, sheet_name="Standardised Asset List", index=False)
flat_data.to_excel(writer, sheet_name="Flat Data", index=False)

View file

@ -0,0 +1,143 @@
"""
Having produced the 4 standardsied asset lists for PFP, this script performs a final review
on those assets, reconciling against a list of properties that they sent us that indicates the
properties that they have retained, acquired and then the list will also include some properties that we
have never seen before and so might require additional inspections
"""
import pandas as pd
import numpy as np
import os
from tqdm import tqdm
def match_to_list(pfp_reconciliation_list, asset_list):
lookup = []
for _, asset in tqdm(pfp_reconciliation_list.iterrows(), total=pfp_reconciliation_list.shape[0]):
_id = str(asset['PRO PROPREF'])
# Match to the asset list - we check the bas ID and then we test removing leading zeros
matched = asset_list[asset_list["landlord_property_id"] == _id]
if matched.empty:
_id_stripped = _id.lstrip("0")
matched = asset_list[asset_list["landlord_property_id"] == _id_stripped]
if not matched.empty:
lookup.append(
{
"reconciliation_id": _id,
"landlord_property_id": matched["landlord_property_id"].values[0],
}
)
lookup = pd.DataFrame(lookup)
asset_list["reconciliation"] = np.where(
asset_list["landlord_property_id"].isin(
lookup["landlord_property_id"].values
),
"Property still owned by PFP",
"Property not owned by PFP"
)
return asset_list, lookup
data_folder = "/Users/khalimconn-kowlessar/Documents/hestia/Customers/Places For People/Finalise Programme"
pfp_reconciliation_list = pd.read_excel(
os.path.join(data_folder, "PFP properties w repair responsibility.xlsx"),
)
# London
pfp_london = pd.read_excel(
os.path.join(data_folder, "Standardised Asset Lists/PFP - areas surrounding London - Standardised.xlsx"),
sheet_name="Standardised Asset List"
)
pfp_london["landlord_property_id"] = pfp_london["landlord_property_id"].astype(str)
# North-East
pfp_ne = pd.read_excel(
os.path.join(data_folder, "Standardised Asset Lists/PFP - North East - Standardised.xlsx"),
sheet_name="Standardised Asset List"
)
pfp_ne["landlord_property_id"] = pfp_ne["landlord_property_id"].astype(str)
# North-West
pfp_nw = pd.read_excel(
os.path.join(
data_folder,
"Standardised Asset Lists/Places for People NORTH WEST - INSPECTIONS MASTER - UPDATE - "
"Standardised.xlsx"
),
sheet_name="Standardised Asset List"
)
pfp_nw["landlord_property_id"] = pfp_nw["landlord_property_id"].astype(str)
# East
pfp_east = pd.read_excel(
os.path.join(data_folder, "Standardised Asset Lists/PFP - East - Standardised.xlsx"),
sheet_name="Standardised Asset List"
)
pfp_east["landlord_property_id"] = pfp_east["landlord_property_id"].astype(str)
pfp_london, lookup_london = match_to_list(pfp_reconciliation_list, pfp_london)
pfp_ne, lookup_ne = match_to_list(pfp_reconciliation_list, pfp_ne)
pfp_nw, lookup_nw = match_to_list(pfp_reconciliation_list, pfp_nw)
pfp_east, lookup_east = match_to_list(pfp_reconciliation_list, pfp_east)
pfp_london["reconciliation"].value_counts()
pfp_ne["reconciliation"].value_counts()
pfp_nw["reconciliation"].value_counts()
pfp_east["reconciliation"].value_counts()
# We store the reconciled datasets
pfp_london.to_csv(
os.path.join(data_folder, "Reconciled Programme/PFP - areas surrounding London - reconciled.csv"),
index=False
)
pfp_ne.to_csv(
os.path.join(data_folder, "Reconciled Programme/PFP - North East - reconciled.csv"),
index=False
)
pfp_nw.to_csv(
os.path.join(data_folder, "Reconciled Programme/PFP - North West - reconciled.csv"),
index=False
)
pfp_east.to_csv(
os.path.join(data_folder, "Reconciled Programme/PFP - East - reconciled.csv"),
index=False
)
pd.set_option('display.max_columns', None)
pd.set_option('display.width', 1000)
# We look at what was on the reconciled list, that was NOT on the original list
all_ids = lookup_london["reconciliation_id"].tolist() + \
lookup_ne["reconciliation_id"].tolist() + \
lookup_nw["reconciliation_id"].tolist() + \
lookup_east["reconciliation_id"].tolist()
missed_inspections = pd.read_excel(
os.path.join(
data_folder,
"/Users/khalimconn-kowlessar/Documents/hestia/Customers/Places For People/North-West/Places for People NORTH "
"WEST - INSPECTIONS MASTER - UPDATE.xlsx"
),
sheet_name="MISSING STILL"
)
missed_inspections.columns = ["landlord_id", "address"]
not_seen = pfp_reconciliation_list[
~pfp_reconciliation_list["PRO PROPREF"].astype(str).isin(all_ids)
].copy()
not_seen["Note"] = None
not_seen["Note"] = np.where(
not_seen["PRO PROPREF"].astype(str).isin(missed_inspections["landlord_id"].astype(str).values) |
not_seen["PRO PROPREF"].astype(str).str.lstrip("0").isin(missed_inspections["landlord_id"].astype(str).values),
"Property not inspected",
not_seen["Note"]
)
not_seen["Note"] = not_seen["Note"].fillna("Property not in original lists")
# Store
not_seen = os.path.join(
data_folder, "Reconciled Programme/Properties not inspected by Domna.xlsx"
)

View file

@ -356,3 +356,64 @@ class TestEpcRecord:
assert record.prepared_epc["solar-water-heating-flag"] == "N"
assert record.solar_water_heating_flag_bool is False
def test_year_built(self, cleaning_data):
# This test handles a specific test case
# Mock the property object
epc_records = {
"original_epc": {
'low-energy-fixed-light-count': '', 'address': '19 Waterloo Road, Shoeburyness',
'uprn-source': 'Energy Assessor', 'floor-height': '2.65', 'heating-cost-potential': '436',
'unheated-corridor-length': '', 'hot-water-cost-potential': '100',
'construction-age-band': 'England and Wales: 1900-1929', 'potential-energy-rating': 'B',
'mainheat-energy-eff': 'Good', 'windows-env-eff': 'Good', 'lighting-energy-eff': 'Very Good',
'environment-impact-potential': '89', 'glazed-type': 'double glazing installed during or after 2002',
'heating-cost-current': '888', 'address3': '',
'mainheatcont-description': 'Programmer and room thermostat',
'sheating-energy-eff': 'N/A', 'report-type': '100', 'property-type': 'House',
'local-authority-label': 'Southend-on-Sea', 'fixed-lighting-outlets-count': '9',
'energy-tariff': 'Single',
'mechanical-ventilation': 'natural', 'hot-water-cost-current': '386', 'county': '',
'postcode': 'SS3 9EQ',
'solar-water-heating-flag': 'N', 'constituency': 'E14001501', 'co2-emissions-potential': '0.7',
'number-heated-rooms': '4', 'floor-description': 'Suspended, no insulation (assumed)',
'energy-consumption-potential': '49', 'local-authority': 'E06000033', 'built-form': 'Mid-Terrace',
'number-open-fireplaces': '0', 'windows-description': 'Fully double glazed', 'glazed-area': 'Normal',
'inspection-date': '2025-03-17', 'mains-gas-flag': 'Y', 'co2-emiss-curr-per-floor-area': '58',
'address1': '19 Waterloo Road', 'heat-loss-corridor': '', 'flat-storey-count': '',
'constituency-label': '',
'roof-energy-eff': 'Average', 'total-floor-area': '78.0', 'building-reference-number': '10007286268',
'environment-impact-current': '48', 'co2-emissions-current': '4.5',
'roof-description': 'Pitched, 100 mm loft insulation', 'floor-energy-eff': 'N/A',
'number-habitable-rooms': '4', 'address2': 'Shoeburyness', 'hot-water-env-eff': 'Average',
'posttown': 'SOUTHEND-ON-SEA', 'mainheatc-energy-eff': 'Average',
'main-fuel': 'mains gas (not community)',
'lighting-env-eff': 'Very Good', 'windows-energy-eff': 'Good', 'floor-env-eff': 'N/A',
'sheating-env-eff': 'N/A', 'lighting-description': 'Low energy lighting in 78% of fixed outlets',
'roof-env-eff': 'Average', 'walls-energy-eff': 'Very Poor', 'photo-supply': '0.0',
'lighting-cost-potential': '101', 'mainheat-env-eff': 'Good', 'multi-glaze-proportion': '100',
'main-heating-controls': '', 'lodgement-datetime': '2025-03-25 16:59:15', 'flat-top-storey': '',
'current-energy-rating': 'D', 'secondheat-description': 'None', 'walls-env-eff': 'Very Poor',
'transaction-type': 'marketed sale', 'uprn': 100090702270, 'current-energy-efficiency': '56',
'energy-consumption-current': '329', 'mainheat-description': 'Boiler and radiators, mains gas',
'lighting-cost-current': '101', 'lodgement-date': '2025-03-25', 'extension-count': '1',
'mainheatc-env-eff': 'Average',
'lmk-key': 'ff00a1e150063f7bbcac1644be57fdcf05b6c9c60053f80c5d218bf2863fea93',
'wind-turbine-count': '0',
'tenure': 'Owner-occupied', 'floor-level': '', 'potential-energy-efficiency': '89',
'hot-water-energy-eff': 'Average', 'low-energy-lighting': '78',
'walls-description': 'Solid brick, as built, no insulation (assumed)',
'hotwater-description': 'From main system'
},
"full_sap_epc": {},
"old_data": []
}
prepared_epc = EPCRecord(
epc_records=epc_records,
run_mode="newdata",
cleaning_data=cleaning_data
)
assert prepared_epc.get("year_built") == 1900

View file

@ -1,40 +0,0 @@
import pytest
from etl.spatial.BoreholeClient import BoreholeClient
@pytest.fixture
def mock_borehole_data(monkeypatch):
# Define the mock data to be returned by the read() method
mock_data = [
{
'X': 464343.0,
'Y': 415553.0
},
{
'X': 464341.0,
'Y': 415556.0
},
# Add more mock data entries here...
]
# Monkeypatch the read() method to return the mock data
def mock_read(self):
return mock_data
# Apply the monkeypatch to the BoreholeClient class
monkeypatch.setattr(BoreholeClient, 'read', mock_read)
@pytest.mark.usefixtures('mock_borehole_data')
def test_distance_between_bng_coords():
# Create an instance of BoreholeClient
borehole_client = BoreholeClient("path/to/dbf/file")
# Test the distance calculation
distance_m, distance_km = borehole_client.distance_between_bng_coords(
x1_bng=123456, y1_bng=789012, x2_bng=464343.0, y2_bng=415553.0
)
# Perform assertions to verify the results
assert distance_m == 505643.71987596166
assert distance_km == 505.64371987596166

View file

@ -1,4 +1,4 @@
[pytest]
pythonpath = .
addopts = --cov-report term-missing --cov=etl --cov=recommendations --cov=backend
testpaths = etl/*/tests recommendations/tests backend/tests
addopts = --cov-report term-missing --cov=etl/epc --cov=recommendations --cov=backend --cov=etl/epc_clean --cov=etl/spatial
testpaths = recommendations/tests backend/tests etl/epc/tests etl/epc_clean/tests etl/spatial/tests

View file

@ -185,17 +185,24 @@ class HeatingRecommender:
self.property.main_heating["clean_description"]
]["dual"]["types"]
recommendation_system_types = list(set([x["system_type"] for x in self.heating_recommendations]))
heating_measures = [
m for m in self.heating_recommendations if
m["measure_type"] not in ["time_temperature_zone_control", "roomstat_programmer_trvs"]
]
recommendation_system_types = list(
set([x["system_type"] for x in heating_measures])
)
# 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]
x for x in heating_measures 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]
x for x in heating_measures if x["system_type"] == dual_heating_description[1]
]
# we combine the two recommendations
combined_recommendations = []

View file

@ -1,6 +1,5 @@
from recommendations.Costs import Costs
from unittest.mock import Mock
import datetime
import pytest
@ -298,10 +297,10 @@ class TestCosts:
# Test for different wattages
@pytest.mark.parametrize("n_panels, expected_cost", [
(7, 4055.0),
(10, 4540.0),
(12, 4863.0),
(15, 5707.0),
(7, 5458.727999999999),
(10, 6013.139999999999),
(12, 6386.447999999999),
(15, 7594.451999999999),
])
def test_solar_pv_different_wattages(self, n_panels, expected_cost):
mock_property = Mock()

View file

@ -39,8 +39,7 @@ testing_examples = [
'fixed-lighting-outlets-count': 10.0, 'low-energy-fixed-light-count': 7.0, 'uprn': 100110195416.0,
'uprn-source': 'Address Matched'
},
"heating_measure_types": ["air_source_heat_pump"],
"heating_controls_measure_types": ["time_temperature_zone_control"],
"heating_measure_types": ["air_source_heat_pump", "time_temperature_zone_control"],
"notes": "This property has a boiler, radiators & mains gas with good efficiency so the only recommendation"
"we expect here is for an air source heat pump. The heating controls are a programmer, room thermostat"
"and TRVs and so we should expect a TTZC recommendation"
@ -89,7 +88,6 @@ testing_examples = [
"heating_measure_types": [
"high_heat_retention_storage_heater",
],
"heating_controls_measure_types": [],
"notes": "This property has electric room heaters and is off gas so a boiler recommendation is not appropriate."
"We would expect a high heat retention storage recommendation. The property is a flat and therefore"
"we don't expect an air source heat pump recommendation. We also wouldn't expect a specific heating"
@ -137,7 +135,6 @@ testing_examples = [
'uprn': 100090311351.0, 'uprn-source': 'Address Matched', 'property-type_y': None, 'built-form_y': None,
},
"heating_measure_types": ['high_heat_retention_storage_heater', 'air_source_heat_pump'],
"heating_controls_measure_types": [],
"notes": "This test has electric storage heaters with automatic charge control - we recommend hhr storage"
"heaters in this case, but because there are already electic storage heaters in place, we "
"note, in the description of the recommendation, that this upgrade may be possible by retrofitting"
@ -181,14 +178,11 @@ testing_examples = [
'tenure': 'owner-occupied', 'fixed-lighting-outlets-count': None, 'low-energy-fixed-light-count': None,
'uprn': 100021560521.0, 'uprn-source': 'Address Matched',
},
"heating_measure_types": ['boiler_upgrade'],
"heating_controls_measure_types": [
"heating_measure_types": [
'roomstat_programmer_trvs',
'time_temperature_zone_control',
],
"notes": "Because of this property is a maisonette, which already has a boiler (but an inefficient one due to "
"the current water heating efficiency) the only recommendation we expect is for "
"a boiler upgrade. The heating controls are programmer and thermostat, so we can also recommend"
"notes": "The heating controls are programmer and thermostat, so we can also recommend"
"better heating controls"
},
{
@ -231,10 +225,9 @@ testing_examples = [
'tenure': 'owner-occupied', 'fixed-lighting-outlets-count': 9.0, 'low-energy-fixed-light-count': 5.0,
'uprn': 100021936225.0, 'uprn-source': 'Address Matched',
},
"heating_measure_types": [],
"heating_controls_measure_types": [
"heating_measure_types": [
'roomstat_programmer_trvs',
'time_temperature_zone_control',
'time_temperature_zone_control'
],
"notes": "Because this property already has a boiler, we don't recommend HHR. We don't recommend an ashp "
"because the home is mid-terraced. Because the heating controls are "
@ -284,7 +277,6 @@ testing_examples = [
"heating_measure_types": [
'high_heat_retention_storage_heater',
],
"heating_controls_measure_types": [],
"notes": "This property is a flat so we don't have an ASHP recommendation. It also doesn't have access to the "
"mains and so it can't have a gas boiler. We don't expect any controls recommendations"
},
@ -326,8 +318,8 @@ testing_examples = [
'tenure': 'owner-occupied', 'fixed-lighting-outlets-count': None, 'low-energy-fixed-light-count': None,
'uprn': 100080513604.0, 'uprn-source': 'Address Matched'
},
"heating_measure_types": ['air_source_heat_pump'],
"heating_controls_measure_types": [
"heating_measure_types": [
'air_source_heat_pump',
'roomstat_programmer_trvs',
'time_temperature_zone_control',
],
@ -381,7 +373,6 @@ testing_examples = [
'high_heat_retention_storage_heater',
'boiler_upgrade'
],
"heating_controls_measure_types": [],
"notes": "This property has assumed electric heating and is mid-terrace house. It has a mains gas connection."
"We can recommend a boiler upgrade and high heat retention storage heaters"
},
@ -427,7 +418,6 @@ testing_examples = [
'air_source_heat_pump',
'high_heat_retention_storage_heater',
],
"heating_controls_measure_types": [],
"notes": "This property has an oil boiler and doesn't have a mains gas connection so we can only recommend"
"an air source heat pump and HHR (since if the home has a non-gas boiler, we recommend HHR)"
},
@ -477,7 +467,6 @@ testing_examples = [
'air_source_heat_pump',
'boiler_upgrade' # TTZs
],
"heating_controls_measure_types": [],
"notes": "This property has room heaters, from the mains gas supply. We recommend a boiler upgrade as"
"well as an air source heat pump and HHR (since the home has a room heater set up)"
},
@ -525,7 +514,6 @@ testing_examples = [
'boiler_upgrade',
'high_heat_retention_storage_heater',
],
"heating_controls_measure_types": [],
"notes": "This property has assumed electric heaters. Boiler upgrade, HHR are recommended. We don't recommend"
"an ASHP off of the bat because it's mid-terrace."
},
@ -572,7 +560,6 @@ testing_examples = [
'high_heat_retention_storage_heater',
'boiler_upgrade'
],
"heating_controls_measure_types": [],
"notes": "This has a form of assumed electric heating and has a mains connection so we recommend HHR, boiler"
"upgrade and ASHP"
},
@ -620,7 +607,6 @@ testing_examples = [
'boiler_upgrade',
'high_heat_retention_storage_heater',
],
"heating_controls_measure_types": [],
"notes": "This property already has storage heaters with manual charge control. The home is mid terrace so"
"the ashp is not suitable"
},
@ -668,7 +654,6 @@ testing_examples = [
'high_heat_retention_storage_heater',
'air_source_heat_pump',
],
"heating_controls_measure_types": [],
"notes": "This property has an LFG boiler but it doesn't have a mains gas connection so we can only recommend"
"an air source heat pump and hhr storage"
},
@ -714,7 +699,6 @@ testing_examples = [
'high_heat_retention_storage_heater',
'air_source_heat_pump',
],
"heating_controls_measure_types": [],
"notes": "This property has electric boilers in place, but does not have a mains connection so we don't "
"recommend a boiler upgrade. We recommend HHR and ASHP"
},
@ -762,7 +746,6 @@ testing_examples = [
'air_source_heat_pump',
'high_heat_retention_storage_heater'
],
"heating_controls_measure_types": [],
"notes": "This property has a dual fuel boiler and no mains gas connection. We recommend ASHP and HHR, but"
"no gas condensing boiler"
},
@ -807,7 +790,6 @@ testing_examples = [
'air_source_heat_pump',
'high_heat_retention_storage_heater',
],
"heating_controls_measure_types": [],
"notes": "This property has a coal boiler and no mains gas connection. We recommend ASHP and HHR, but"
"no gas condensing boiler"
},
@ -855,7 +837,6 @@ testing_examples = [
'air_source_heat_pump',
'high_heat_retention_storage_heater',
],
"heating_controls_measure_types": [],
"notes": "This property has a smokeless fuel boiler and no mains gas connection. We recommend ASHP and HHR, but"
"no gas condensing boiler"
},
@ -901,7 +882,6 @@ testing_examples = [
'air_source_heat_pump',
'high_heat_retention_storage_heater',
],
"heating_controls_measure_types": [],
"notes": "This property has a wood pellets boiler and no mains gas connection. We recommend ASHP and HHR, but"
"no gas condensing boiler"
},
@ -948,7 +928,6 @@ testing_examples = [
'high_heat_retention_storage_heater',
'air_source_heat_pump',
],
"heating_controls_measure_types": [],
"notes": "This is an end-terrace house, without mains gas connection, so we recommend is HHR & ASHP"
},
{
@ -990,7 +969,6 @@ testing_examples = [
'uprn-source': 'Address Matched', 'sheating-energy-eff': None, 'sheating-env-eff': None
},
"heating_measure_types": [],
"heating_controls_measure_types": [],
"notes": "This property already has an ashp. We don't recommend any heating upgrades"
},
{
@ -1032,9 +1010,7 @@ testing_examples = [
},
"heating_measure_types": [
'air_source_heat_pump',
'high_heat_retention_storage_heater'
],
"heating_controls_measure_types": [
'high_heat_retention_storage_heater',
'time_temperature_zone_control',
],
"notes": "This property has dual heating. A boiler and electric storage heaters. The heating is efficient so"
@ -1081,9 +1057,7 @@ testing_examples = [
'air_source_heat_pump',
'boiler_upgrade',
'boiler_upgrade+high_heat_retention_storage_heater',
'high_heat_retention_storage_heater'
],
"heating_controls_measure_types": [
'high_heat_retention_storage_heater',
'time_temperature_zone_control'
],
"notes": "This property is a modified version of the previous dual heating property, where we lower the"
@ -1132,7 +1106,6 @@ testing_examples = [
'air_source_heat_pump',
'high_heat_retention_storage_heater'
],
"heating_controls_measure_types": [],
"notes": "This property has anthracite heating without mains. "
"We recommend ASHP and HHR, but no gas condensing boiler"
},
@ -1180,7 +1153,6 @@ testing_examples = [
'boiler_upgrade',
'high_heat_retention_storage_heater'
],
"heating_controls_measure_types": [],
"notes": "This property has room heaters with two different fuel sources, so we recommend HHR, ASHP, and a "
"boiler upgrade"
},
@ -1224,7 +1196,6 @@ testing_examples = [
"heating_measure_types": [
'high_heat_retention_storage_heater'
],
"heating_controls_measure_types": [],
"notes": "This property is a flag, without mains gas connection. Currently has underfloor electric heating"
"so we recommend HHR"
},
@ -1272,7 +1243,6 @@ testing_examples = [
'boiler_upgrade',
'high_heat_retention_storage_heater'
],
"heating_controls_measure_types": [],
"notes": "The property has warm air electricaire heating, so we recommend ASHP and HHR. It also has a mains"
"connection so we recommend a gas condensing boiler"
},
@ -1320,89 +1290,6 @@ testing_examples = [
'boiler_upgrade',
'boiler_upgrade',
],
"heating_controls_measure_types": [],
"notes": "This property has warm air mains gas heating, so we recommend a gas condensing boiler"
}
]
# import random
# from pathlib import Path
# import inspect
# import pandas as pd
#
# # this can be used to get example data to build the test cases
# src_file_path = inspect.getfile(lambda: None)
# EPC_DIRECTORY = Path(src_file_path).parent / "local_data" / "all-domestic-certificates"
# epc_directories = [entry for entry in EPC_DIRECTORY.iterdir() if entry.is_dir()]
# directory = random.sample(epc_directories, 1)[0]
# data = pd.read_csv(directory / "certificates.csv", low_memory=False)
# # Rename the columns to the same format as the api returns
# data.columns = [c.replace("_", "-").lower() for c in data.columns]
# data["floor-height"] = data["floor-height"].fillna(2.45)
#
# used_examples = pd.DataFrame(
# [
# {
# "mainheat-description": x["epc"]["mainheat-description"],
# "mainheat-energy-eff": x["epc"]["mainheat-energy-eff"],
# "property-type": x["epc"]["property-type"],
# "built-form": x["epc"]["built-form"],
# "used": True
# } for x in testing_examples
# ]
# )
#
# data = data.merge(
# used_examples, how="left", on=["mainheat-description", "mainheat-energy-eff", "built-form", "property-type"]
# )
# data = data[pd.isnull(data["used"])].drop(columns=["used"])
#
# eg = data.sample(1).to_dict("records")[0]
# print(eg["mainheat-description"])
# print(eg["mainheat-energy-eff"])
# print(eg["property-type"])
# print(eg["built-form"])
# print(eg["mainheatcont-description"])
#
# ### We also use the Midlands EPC F/G portfolio to get examples to create tests
#
# completed_descriptions = [
# "Portable electric heaters assumed for most rooms",
# "Boiler and radiators, oil",
# "Boiler and radiators, mains gas",
# "Room heaters, mains gas",
# "No system present: electric heaters assumed",
# "Room heaters, electric",
# "Electric storage heaters",
# "Boiler and radiators, LPG",
# "Boiler and radiators, electric",
# "Boiler and radiators, dual fuel (mineral and wood)",
# "Boiler and radiators, coal",
# "Boiler and radiators, smokeless fuel",
# "Boiler and radiators, wood pellets",
# "Room heaters, dual fuel (mineral and wood)",
# "Air source heat pump, radiators, electric",
# "Portable electric heaters assumed for most rooms, Room heaters, electric",
# "Boiler and radiators, mains gas, Electric storage heaters",
# "Room heaters, anthracite",
# "Room heaters, mains gas, Room heaters, dual fuel (mineral and wood)",
# "Electric underfloor heating",
# "Warm air, Electricaire"
# ]
#
# portfolio = pd.read_excel(
# "/Users/khalimconn-kowlessar/Documents/hestia/Customers/sfr/20240820 portfolio_epc_data.xlsx"
# )
# portfolio.columns = [c.replace("_", "-").lower() for c in portfolio.columns]
# portfolio = portfolio[~portfolio["mainheat-description"].isin(completed_descriptions)]
# portfolio['sheating-energy-eff'] = None
# portfolio['sheating-env-eff'] = None
# portfolio["lodgement-datetime"] = portfolio["lodgement-datetime"].astype(str)
#
# print(portfolio["mainheat-description"].value_counts())
#
# eg = portfolio[
# (portfolio["mainheat-description"] == "Warm air, mains gas")
# ].sample(1)
# eg = eg.squeeze().to_dict()
# print(eg)

View file

@ -1,6 +1,30 @@
import datetime
materials = [
{
'id': 2484,
'type': 'room_roof_insulation',
'description': 'Room in roof insulation',
'depth': 100,
'depth_unit': 'mm',
'cost_unit': 'gbp_per_m2',
'r_value_per_mm': 0.038,
'r_value_unit': 'square_meter_kelvin_per_watt',
'thermal_conductivity': 0.022,
'thermal_conductivity_unit': 'watt_per_meter_kelvin',
'link': 'SCIS',
'created_at': '2025-03-16 15:26:22.379',
'cost': None,
'is_active': True,
'prime_material_cost': None,
'material_cost': 0.0,
'labour_cost': 0.0,
'labour_hours_per_unit': 0.0,
'plant_cost': 0.0,
'total_cost': 210.0,
'notes': None,
'is_installer_quote': True
},
{'id': 1997, 'type': 'cavity_wall_insulation', 'description': 'Imperial Bead cavity wall insulation', 'depth': 75.0,
'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.030303031,
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.033,

View file

@ -65,6 +65,7 @@ class TestFloorRecommendations:
input_properties[2].number_of_floors = 1
input_properties[2].floor_level = 0
input_properties[2].already_installed = []
input_properties[2].non_invasive_recommendations = {}
recommender = FloorRecommendations(property_instance=input_properties[2], materials=materials)
assert recommender.estimated_u_value is None
@ -115,6 +116,7 @@ class TestFloorRecommendations:
input_properties[4].number_of_floors = 1
input_properties[4].floor_level = 0
input_properties[4].already_installed = []
input_properties[4].non_invasive_recommendations = {}
# In this case, we have no county, so in this case, it should yse the local-authority-label if possible
input_properties[4].data["county"] = ""

View file

@ -92,23 +92,13 @@ class TestHeatingRecommendations:
recommender = HeatingRecommender(property_instance=p)
# Check they're empty
assert not recommender.heating_recommendations
assert not recommender.heating_control_recommendations
recommender.recommend(has_cavity_or_loft_recommendations=False)
assert len(recommender.heating_recommendations) == len(test_case["heating_measure_types"])
assert (
len(recommender.heating_control_recommendations) ==
len(test_case["heating_controls_measure_types"])
)
# Check the exact measure types
assert (
{x["measure_type"] for x in recommender.heating_recommendations} ==
set(test_case["heating_measure_types"])
)
assert (
{x["measure_type"] for x in recommender.heating_control_recommendations} ==
set(test_case["heating_controls_measure_types"])
)

View file

@ -41,18 +41,18 @@ class TestLightingRecommendations:
assert len(lr.recommendation) == 1
assert lr.recommendation == [
{
'phase': 0, 'parts': [], 'type': 'low_energy_lighting', 'measure_type': 'low_energy_lighting',
'description': 'Install low energy lighting in 4 outlets', 'starting_u_value': None,
'new_u_value': None,
'already_installed': False, 'sap_points': 0.4, 'kwh_savings': 219.0, 'co2_equivalent_savings': 0.035478,
'description_simulation': {
'lighting-energy-eff': 'Very Good',
'lighting-description': 'Low energy lighting in all fixed outlets',
'low-energy-lighting': 100
},
'total': 240.24, 'subtotal': 200.20000000000002,
'vat': 40.040000000000006, 'contingency': 14.3, 'preliminaries': 14.3, 'material': 80.0, 'profit': 28.6,
'labour_hours': 3.2, 'labour_days': 0.4, 'labour_cost': 63.0, 'survey': False
}
{'phase': 0, 'parts': [], 'type': 'low_energy_lighting', 'measure_type': 'low_energy_lighting',
'description': 'Install low energy lighting in 4 outlets', 'starting_u_value': None, 'new_u_value': None,
'already_installed': False, 'sap_points': 0.4, 'kwh_savings': 219.0, 'energy_cost_savings': 54.4434,
'co2_equivalent_savings': 0.035478, 'description_simulation': {
'lighting-energy-eff': 'Very Good',
'lighting-description': 'Low energy '
'lighting in all '
'fixed outlets',
'low-energy-lighting': 100
},
'total': 240.24,
'subtotal': 200.20000000000002, 'vat': 40.040000000000006, 'contingency': 14.3, 'preliminaries': 14.3,
'material': 80.0, 'profit': 28.6, 'labour_hours': 3.2, 'labour_days': 0.4, 'labour_cost': 63.0,
'survey': False}
]

View file

@ -3,6 +3,7 @@ from recommendations.SolarPvRecommendations import SolarPvRecommendations
from backend.Property import Property
from etl.epc.Record import EPCRecord
import pandas as pd
import numpy as np
class TestSolarPvRecommendations:
@ -25,7 +26,9 @@ class TestSolarPvRecommendations:
"county": "Huntingdonshire", "property-type": "House", "photo-supply": None
}
property_instance_invalid_roof = Property(id=1, address="", postcode="", epc_record=epc_record)
property_instance_invalid_roof.roof = {"is_flat": False, "is_pitched": False, "is_roof_room": False}
property_instance_invalid_roof.roof = {
"is_flat": False, "is_pitched": False, "is_roof_room": False, "thermal_transmittance": None
}
return property_instance_invalid_roof
@pytest.fixture
@ -35,7 +38,7 @@ class TestSolarPvRecommendations:
epc_record.prepared_epc = {"photo-supply": "40", "county": "Huntingdonshire",
"property-type": "House"}
property_instance_has_solar_pv = Property(id=1, address="", postcode="", epc_record=epc_record)
property_instance_has_solar_pv.roof = {"is_flat": True}
property_instance_has_solar_pv.roof = {"is_flat": True, "thermal_transmittance": None}
return property_instance_has_solar_pv
@pytest.fixture
@ -46,7 +49,7 @@ class TestSolarPvRecommendations:
property_instance_valid_all = Property(id=1, address="", postcode="", epc_record=epc_record)
property_instance_valid_all.roof_area = 40
property_instance_valid_all.number_of_floors = 2
property_instance_valid_all.roof = {"is_flat": True}
property_instance_valid_all.roof = {"is_flat": True, "thermal_transmittance": None}
property_instance_valid_all.solar_panel_configuration = {
"panel_performance": pd.DataFrame(
[
@ -82,23 +85,16 @@ class TestSolarPvRecommendations:
solar_pv.recommend(phase=0)
assert len(solar_pv.recommendation) == 2
assert solar_pv.recommendation == [
{
'phase': 0, 'parts': [], 'type': 'solar_pv', 'measure_type': 'solar_pv',
'description': 'Install a 4.0 kilowatt-peak (kWp) solar photovoltaic (PV) panel system on 50% the '
'roof.',
'starting_u_value': None, 'new_u_value': None, 'sap_points': None, 'already_installed': False,
'total': 4850.0, 'subtotal': 4041.666666666667, 'vat': 808.333333333333, 'labour_hours': 48,
'labour_days': 2, 'photo_supply': 50.0, 'has_battery': False, 'initial_ac_kwh_per_year': 3800,
'description_simulation': {'photo-supply': 50.0}
},
{
'phase': 0, 'parts': [], 'type': 'solar_pv', 'measure_type': 'solar_pv',
'description': 'Install a 4.0 kilowatt-peak (kWp) solar photovoltaic (PV) panel system on 50% the '
'roof, '
'with a battery storage system.',
'starting_u_value': None, 'new_u_value': None, 'sap_points': None, 'already_installed': False,
'total': 7550.0, 'subtotal': 6291.666666666667, 'vat': 1258.333333333333, 'labour_hours': 48,
'labour_days': 2, 'photo_supply': 50.0, 'has_battery': True, 'initial_ac_kwh_per_year': 3800,
'description_simulation': {'photo-supply': 50.0}
}
{'phase': 0, 'parts': [], 'type': 'solar_pv', 'measure_type': 'solar_pv',
'description': 'Install a 4.0 kilowatt-peak (kWp) solar panel system.', 'starting_u_value': None,
'new_u_value': None, 'sap_points': np.float64(10.0), 'already_installed': False,
'total': 6013.139999999999, 'subtotal': 5010.95, 'vat': 0, 'labour_hours': 48, 'labour_days': 2,
'photo_supply': np.float64(50.0), 'has_battery': False, 'initial_ac_kwh_per_year': np.int64(3800),
'description_simulation': {'photo-supply': np.float64(50.0)}},
{'phase': 0, 'parts': [], 'type': 'solar_pv', 'measure_type': 'solar_pv',
'description': 'Install a 4.0 kilowatt-peak (kWp) solar panel system, with a battery.',
'starting_u_value': None, 'new_u_value': None, 'sap_points': np.float64(10.0), 'already_installed': False,
'total': 10537.008, 'subtotal': 8780.84, 'vat': 0, 'labour_hours': 48, 'labour_days': 2,
'photo_supply': np.float64(50.0), 'has_battery': True, 'initial_ac_kwh_per_year': np.int64(3800),
'description_simulation': {'photo-supply': np.float64(50.0)}}
]

View file

@ -18,7 +18,7 @@ class TestVentilationRecommendations:
assert not recommender.recommendation
recommender.recommend()
recommender.recommend(phase=None)
assert len(recommender.recommendation) == 1
@ -40,7 +40,7 @@ class TestVentilationRecommendations:
assert not recommender2.recommendation
recommender2.recommend()
recommender2.recommend(phase=None)
assert len(recommender2.recommendation) == 1
@ -62,7 +62,7 @@ class TestVentilationRecommendations:
assert not recommender3.recommendation
recommender3.recommend()
recommender3.recommend(phase=None)
assert len(recommender3.recommendation) == 1
@ -84,7 +84,7 @@ class TestVentilationRecommendations:
assert not recommender4.recommendation
recommender4.recommend()
recommender4.recommend(phase=None)
assert not recommender4.recommendation
assert recommender4.has_ventilaion
@ -101,7 +101,7 @@ class TestVentilationRecommendations:
assert not recommender5.recommendation
recommender5.recommend()
recommender5.recommend(phase=None)
assert not recommender5.recommendation
assert recommender5.has_ventilaion

View file

@ -56,7 +56,8 @@ class TestWindowRecommendations:
'has_glazing_ending': True, 'glazing_type_ending': 'double',
'multi_glaze_proportion_ending': 100, 'windows_energy_eff_ending': 'Good',
'glazed_type_ending': 'double glazing installed during or after 2002'
}
},
"survey": None
}
]
@ -106,7 +107,8 @@ class TestWindowRecommendations:
'glazing_coverage_ending': 'full', 'multi_glaze_proportion_ending': 100,
'windows_energy_eff_ending': 'Good', 'glazing_type_ending': 'double',
'glazed_type_ending': 'double glazing installed during or after 2002'
}
},
"survey": None
}
]
@ -206,7 +208,8 @@ class TestWindowRecommendations:
'glazing_coverage_ending': 'full', 'multi_glaze_proportion_ending': 100,
'windows_energy_eff_ending': 'Good', 'glazing_type_ending': 'secondary',
'glazed_type_ending': 'secondary glazing'
}
},
"survey": None
}
]
@ -254,8 +257,9 @@ class TestWindowRecommendations:
'has_glazing_ending': True, 'glazing_coverage_ending': 'full',
'glazing_type_ending': 'secondary', 'multi_glaze_proportion_ending': 100,
'windows_energy_eff_ending': 'Good', 'glazed_type_ending': 'secondary glazing'
}
}
},
"survey": None
},
]
def test_full_triple_glazed(self):
@ -383,6 +387,7 @@ class TestWindowRecommendations:
'glazing_type': 'single',
'no_data': False
}
property_9.perimeter = 23.430749027719962
property_9.number_of_windows = 7
property_9.restricted_measures = False
@ -410,7 +415,8 @@ class TestWindowRecommendations:
'glazing_type_ending': 'double', 'multi_glaze_proportion_ending': 100,
'windows_energy_eff_ending': 'Good',
'glazed_type_ending': 'double glazing installed during or after 2002'
}
},
"survey": None
}
]

View file

@ -2,3 +2,4 @@ pytest
mock
pytest-cov
pytest-mock
dotenv

11
tox.ini Normal file
View file

@ -0,0 +1,11 @@
[tox]
envlist = py311
skipsdist = True
[testenv]
description = Install dependencies and run tests
deps =
-rbackend/engine/requirements.txt
-rtest.requirements.txt
commands = pytest