mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
merge conflicts resovled and reviweed my code
This commit is contained in:
commit
da3b753b46
108 changed files with 4279 additions and 2418 deletions
|
|
@ -6,12 +6,14 @@ psycopg2-binary==2.9.10
|
|||
python-jose==3.3.0
|
||||
cryptography==43.0.3
|
||||
mangum==0.19.0
|
||||
playwright==1.58.0
|
||||
# AWS
|
||||
boto3==1.35.44
|
||||
# Data
|
||||
openpyxl==3.1.5
|
||||
# Basic
|
||||
pytz
|
||||
msal
|
||||
uvicorn[standard]
|
||||
sqlmodel
|
||||
# Testing
|
||||
|
|
|
|||
37
.github/workflows/_deploy_lambda.yml
vendored
37
.github/workflows/_deploy_lambda.yml
vendored
|
|
@ -62,7 +62,24 @@ on:
|
|||
required: false
|
||||
TF_VAR_ordnance_survey_api_key:
|
||||
required: false
|
||||
|
||||
TF_VAR_sharepoint_client_id:
|
||||
required: false
|
||||
TF_VAR_sharepoint_client_secret:
|
||||
required: false
|
||||
TF_VAR_sharepoint_tenant_id:
|
||||
required: false
|
||||
TF_VAR_domna_sharepoint_id:
|
||||
required: false
|
||||
TF_VAR_osmosis_acd_sharepoint_id:
|
||||
required: false
|
||||
TF_VAR_private_pay_sharepoint_id:
|
||||
required: false
|
||||
TF_VAR_social_housing_wave_3_sharepoint_id:
|
||||
required: false
|
||||
TF_VAR_pashub_email:
|
||||
required: false
|
||||
TF_VAR_pashub_password:
|
||||
required: false
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
|
@ -120,6 +137,15 @@ jobs:
|
|||
TF_VAR_epc_auth_token: ${{ secrets.TF_VAR_epc_auth_token }}
|
||||
TF_VAR_google_solar_api_key: ${{ secrets.TF_VAR_google_solar_api_key }}
|
||||
TF_VAR_ordnance_survey_api_key: ${{ secrets.TF_VAR_ordnance_survey_api_key }}
|
||||
TF_VAR_sharepoint_client_id: ${{ secrets.TF_VAR_sharepoint_client_id }}
|
||||
TF_VAR_sharepoint_client_secret: ${{ secrets.TF_VAR_sharepoint_client_secret }}
|
||||
TF_VAR_sharepoint_tenant_id: ${{ secrets.TF_VAR_sharepoint_tenant_id }}
|
||||
TF_VAR_domna_sharepoint_id: ${{ secrets.TF_VAR_domna_sharepoint_id }}
|
||||
TF_VAR_osmosis_acd_sharepoint_id: ${{ secrets.TF_VAR_osmosis_acd_sharepoint_id }}
|
||||
TF_VAR_private_pay_sharepoint_id: ${{ secrets.TF_VAR_private_pay_sharepoint_id }}
|
||||
TF_VAR_social_housing_wave_3_sharepoint_id: ${{ secrets.TF_VAR_social_housing_wave_3_sharepoint_id }}
|
||||
TF_VAR_pashub_email: ${{ secrets.TF_VAR_pashub_email }}
|
||||
TF_VAR_pashub_password: ${{ secrets.TF_VAR_pashub_password }}
|
||||
run: |
|
||||
ECR_REPO_URL_VAR=""
|
||||
if [[ -n "${{ inputs.ecr_repo }}" ]]; then
|
||||
|
|
@ -156,6 +182,15 @@ jobs:
|
|||
TF_VAR_epc_auth_token: ${{ secrets.TF_VAR_epc_auth_token }}
|
||||
TF_VAR_google_solar_api_key: ${{ secrets.TF_VAR_google_solar_api_key }}
|
||||
TF_VAR_ordnance_survey_api_key: ${{ secrets.TF_VAR_ordnance_survey_api_key }}
|
||||
TF_VAR_sharepoint_client_id: ${{ secrets.TF_VAR_sharepoint_client_id }}
|
||||
TF_VAR_sharepoint_client_secret: ${{ secrets.TF_VAR_sharepoint_client_secret }}
|
||||
TF_VAR_sharepoint_tenant_id: ${{ secrets.TF_VAR_sharepoint_tenant_id }}
|
||||
TF_VAR_domna_sharepoint_id: ${{ secrets.TF_VAR_domna_sharepoint_id }}
|
||||
TF_VAR_osmosis_acd_sharepoint_id: ${{ secrets.TF_VAR_osmosis_acd_sharepoint_id }}
|
||||
TF_VAR_private_pay_sharepoint_id: ${{ secrets.TF_VAR_private_pay_sharepoint_id }}
|
||||
TF_VAR_social_housing_wave_3_sharepoint_id: ${{ secrets.TF_VAR_social_housing_wave_3_sharepoint_id }}
|
||||
TF_VAR_pashub_email: ${{ secrets.TF_VAR_pashub_email }}
|
||||
TF_VAR_pashub_password: ${{ secrets.TF_VAR_pashub_password }}
|
||||
run: |
|
||||
EXTRA_VARS=""
|
||||
if [[ -n "${{ inputs.ecr_repo }}" ]]; then
|
||||
|
|
|
|||
6
.github/workflows/deploy_fastapi_backend.yml
vendored
6
.github/workflows/deploy_fastapi_backend.yml
vendored
|
|
@ -77,6 +77,9 @@ jobs:
|
|||
echo "::set-output name=hotwater_kwh_predictions_bucket::${{ secrets[format('{0}_HOTWATER_KWH_PREDICTIONS_BUCKET', github.ref_name)] }}"
|
||||
echo "::set-output name=energy_asessments_bucket::${{ secrets[format('{0}_ENERGY_ASSESSMENTS_BUCKET', github.ref_name)] }}"
|
||||
echo "::set-output name=google_solar_api_key::${{ secrets[format('{0}_GOOGLE_SOLAR_API_KEY', github.ref_name)] }}"
|
||||
echo "::set-output name=sap_baseline_predictions_bucket::${{ secrets[format('{0}_SAP_BASELINE_PREDICTIONS_BUCKET', github.ref_name)] }}"
|
||||
echo "::set-output name=carbon_baseline_predictions_bucket::${{ secrets[format('{0}_CARBON_BASELINE_PREDICTIONS_BUCKET', github.ref_name)] }}"
|
||||
echo "::set-output name=heat_baseline_predictions_bucket::${{ secrets[format('{0}_HEAT_BASELINE_PREDICTIONS_BUCKET', github.ref_name)] }}"
|
||||
|
||||
- name: Setup Docker
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
|
@ -129,6 +132,9 @@ jobs:
|
|||
DB_NAME: ${{ steps.set_db_credentials.outputs.db_name }}
|
||||
ECR_URI: ${{ steps.set_ecr_credentials.outputs.ecr_uri }}
|
||||
GITHUB_SHA: ${{ github.sha }}
|
||||
SAP_BASELINE_PREDICTIONS_BUCKET: ${{ steps.set_api_secrets.outputs.sap_baseline_predictions_bucket }}
|
||||
CARBON_BASELINE_PREDICTIONS_BUCKET: ${{ steps.set_api_secrets.outputs.carbon_baseline_predictions_bucket }}
|
||||
HEAT_BASELINE_PREDICTIONS_BUCKET: ${{ steps.set_api_secrets.outputs.heat_baseline_predictions_bucket }}
|
||||
run: |
|
||||
# Fetch database credentials from AWS Secrets Manager
|
||||
SECRET_VALUE=$(aws secretsmanager get-secret-value --secret-id ${{ github.ref_name }}/assessment_model/db_credentials --query SecretString)
|
||||
|
|
|
|||
59
.github/workflows/deploy_terraform.yml
vendored
59
.github/workflows/deploy_terraform.yml
vendored
|
|
@ -322,6 +322,57 @@ jobs:
|
|||
AWS_REGION: ${{ secrets.DEV_AWS_REGION }}
|
||||
TF_VAR_ORDNANCE_SURVEY_API_KEY: ${{ secrets.ORDNANCE_SURVEY_API_KEY }}
|
||||
|
||||
# ============================================================
|
||||
# Build Pas Hub to Ara image and Push
|
||||
# ============================================================
|
||||
pashub_to_ara_image:
|
||||
needs: [determine_stage, shared_terraform]
|
||||
uses: ./.github/workflows/_build_image.yml
|
||||
with:
|
||||
ecr_repo: pashub_to_ara-${{ needs.determine_stage.outputs.stage }}
|
||||
dockerfile_path: backend/pashub_fetcher/handler/Dockerfile
|
||||
build_context: .
|
||||
build_args: |
|
||||
DEV_DB_HOST=$DEV_DB_HOST
|
||||
DEV_DB_PORT=$DEV_DB_PORT
|
||||
DEV_DB_NAME=$DEV_DB_NAME
|
||||
secrets:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.DEV_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.DEV_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_REGION: ${{ secrets.DEV_AWS_REGION }}
|
||||
DEV_DB_HOST: ${{ secrets.DEV_DB_HOST }}
|
||||
DEV_DB_PORT: ${{ secrets.DEV_DB_PORT }}
|
||||
DEV_DB_NAME: ${{ secrets.DEV_DB_NAME }}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Deploy Pas Hub to Ara Lambda
|
||||
# ============================================================
|
||||
pashub_to_ara_lambda:
|
||||
needs: [pashub_to_ara_image, determine_stage]
|
||||
uses: ./.github/workflows/_deploy_lambda.yml
|
||||
with:
|
||||
lambda_name: pashub_to_ara
|
||||
lambda_path: infrastructure/terraform/lambda/pashub_to_ara
|
||||
stage: ${{ needs.determine_stage.outputs.stage }}
|
||||
ecr_repo: pashub_to_ara-${{ needs.determine_stage.outputs.stage }}
|
||||
image_digest: ${{ needs.pashub_to_ara_image.outputs.image_digest }}
|
||||
terraform_apply: ${{ needs.determine_stage.outputs.terraform_apply }}
|
||||
secrets:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.DEV_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.DEV_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_REGION: ${{ secrets.DEV_AWS_REGION }}
|
||||
TF_VAR_sharepoint_client_id: ${{ secrets.SHAREPOINT_CLIENT_ID }}
|
||||
TF_VAR_sharepoint_client_secret: ${{ secrets.SHAREPOINT_CLIENT_SECRET }}
|
||||
TF_VAR_sharepoint_tenant_id: ${{ secrets.SHAREPOINT_TENANT_ID }}
|
||||
TF_VAR_domna_sharepoint_id: ${{ secrets.DOMNA_SHAREPOINT_ID }}
|
||||
TF_VAR_osmosis_acd_sharepoint_id: ${{ secrets.OSMOSIS_ACD_SHAREPOINT_ID }}
|
||||
TF_VAR_private_pay_sharepoint_id: ${{ secrets.PRIVATE_PAY_SHAREPOINT_ID }}
|
||||
TF_VAR_social_housing_wave_3_sharepoint_id: ${{ secrets.SOCIAL_HOUSING_WAVE_3_SHAREPOINT_ID }}
|
||||
TF_VAR_pashub_email: ${{ secrets.PASHUB_EMAIL }}
|
||||
TF_VAR_pashub_password: ${{ secrets.PASHUB_PASSWORD }}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Deploy FastAPI Lambda
|
||||
# ============================================================
|
||||
|
|
@ -458,16 +509,16 @@ jobs:
|
|||
DEV_DB_NAME: ${{ secrets.DEV_DB_NAME }}
|
||||
|
||||
# ============================================================
|
||||
# Deploy OrdanceSurvey Lambda
|
||||
# Deploy Hubspot ETL Lambda
|
||||
# ============================================================
|
||||
ordnanceSurvey_lambda:
|
||||
hubspot_etl_lambda:
|
||||
needs: [hubspot_etl_image, determine_stage]
|
||||
uses: ./.github/workflows/_deploy_lambda.yml
|
||||
with:
|
||||
lambda_name: hubspotETLtoAraDb
|
||||
lambda_name: hubspot-etl-to-ara
|
||||
lambda_path: infrastructure/terraform/lambda/hubspot_deal_etl
|
||||
stage: ${{ needs.determine_stage.outputs.stage }}
|
||||
ecr_repo: ordnance-${{ needs.determine_stage.outputs.stage }}
|
||||
ecr_repo: hubspot-etl-${{ needs.determine_stage.outputs.stage }}
|
||||
image_digest: ${{ needs.hubspot_etl_image.outputs.image_digest }}
|
||||
terraform_apply: ${{ needs.determine_stage.outputs.terraform_apply }}
|
||||
secrets:
|
||||
|
|
|
|||
35
.github/workflows/integration_tests.yml
vendored
Normal file
35
.github/workflows/integration_tests.yml
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
name: Rebaselining Integration Test
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
rebaselining-integration-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install tox via Makefile
|
||||
run: |
|
||||
make setup
|
||||
|
||||
- name: Configure AWS credentials for dev
|
||||
uses: aws-actions/configure-aws-credentials@v1
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.DEV_AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.DEV_AWS_SECRET_ACCESS_KEY }}
|
||||
aws-region: eu-west-2
|
||||
|
||||
- name: Run only rebaselining integration test
|
||||
env:
|
||||
EPC_AUTH_TOKEN: ${{ secrets.DEV_EPC_AUTH_TOKEN }}
|
||||
run: |
|
||||
make test ARGS="-m integration"
|
||||
10
.github/workflows/unit_tests.yml
vendored
10
.github/workflows/unit_tests.yml
vendored
|
|
@ -23,7 +23,9 @@ jobs:
|
|||
HUBSPOT_API_KEY: ${{ secrets.HUBSPOT_API_KEY }}
|
||||
|
||||
run: |
|
||||
docker run --rm \
|
||||
-e EPC_AUTH_TOKEN=${{ secrets.DEV_EPC_AUTH_TOKEN }} \
|
||||
-e HUBSPOT_API_KEY=${{ secrets.HUBSPOT_API_KEY }} \
|
||||
model-test pytest
|
||||
# docker run --rm \
|
||||
# -e EPC_AUTH_TOKEN=${{ secrets.DEV_EPC_AUTH_TOKEN }} \
|
||||
# -e HUBSPOT_API_KEY=${{ secrets.HUBSPOT_API_KEY }} \
|
||||
# model-test pytest -m 'not integration'
|
||||
|
||||
make test ARGS="-m 'not integration'"
|
||||
|
|
|
|||
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -282,4 +282,7 @@ cache/
|
|||
local_data*
|
||||
|
||||
# pyright local config
|
||||
pyrightconfig.json
|
||||
pyrightconfig.json
|
||||
|
||||
# playwright output
|
||||
*/pashub_fetcher/videos/*
|
||||
2
.idea/Model.iml
generated
2
.idea/Model.iml
generated
|
|
@ -6,6 +6,8 @@
|
|||
<sourceFolder url="file://$MODULE_DIR$/model_data" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/open_uprn" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/recommendations" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$" isTestSource="false" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/infrastructure/terraform/.terraform" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Fastapi-backend" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
|
|
|
|||
|
|
@ -631,4 +631,6 @@ BUILT_FORM_MAPPINGS = {
|
|||
'First & Second Floor Flat': 'mid-floor',
|
||||
'First Floor Purpose Built': 'mid-floor',
|
||||
'Purpose built First Floor': 'mid-floor',
|
||||
|
||||
'Mid-Terrace': 'mid-terrace'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -173,6 +173,7 @@ def get_data(
|
|||
errors = []
|
||||
no_epc = []
|
||||
for _, home in tqdm(df.iterrows(), total=len(df)):
|
||||
|
||||
try:
|
||||
|
||||
# If we have a block of flats, we cannot retrieve this data
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import ast
|
|||
from itertools import groupby
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from typing import Set
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from etl.epc.Dataset import TrainingDataset
|
||||
|
|
@ -55,12 +56,11 @@ class Property:
|
|||
walls = None
|
||||
windows = None
|
||||
lighting = None
|
||||
energy_source = None
|
||||
|
||||
spatial = None
|
||||
base_difference_record = None
|
||||
|
||||
DATA_ANOMALY_MATCHES = DATA_ANOMALY_MATCHES
|
||||
DATA_ANOMALY_MATCHES: Set = DATA_ANOMALY_MATCHES
|
||||
|
||||
# Surplus information, that can be provided as optional inputs, by a customer
|
||||
n_bathrooms = None
|
||||
|
|
@ -100,12 +100,8 @@ class Property:
|
|||
|
||||
self.address = address
|
||||
self.postcode = postcode
|
||||
self.data = {
|
||||
k.replace("_", "-"): v for k, v in epc_record.get("prepared_epc").items()
|
||||
}
|
||||
|
||||
self.old_data = epc_record.get("old_data")
|
||||
self.property_dimensions = None
|
||||
self.old_data = self.epc_record.old_data
|
||||
# This is a list of measures that have already been installed in the property, typically found as a result
|
||||
# of the non-invasive surveys. We reflect that this has been installed in the recommendations, but remove the
|
||||
# cost and instead, provide a message that the measure has already been installed
|
||||
|
|
@ -124,17 +120,17 @@ class Property:
|
|||
|
||||
self.valuation = property_valuation
|
||||
|
||||
self.uprn = uprn if uprn is not None else epc_record.get("uprn")
|
||||
self.uprn_source = self.data.get("uprn-source")
|
||||
self.uprn = uprn if uprn is not None else epc_record.uprn
|
||||
self.uprn_source = self.epc_record.uprn_source
|
||||
|
||||
self.full_sap_epc = epc_record.get("full_sap_epc")
|
||||
self.full_sap_epc = self.epc_record.full_sap_epc
|
||||
self.in_conservation_area, self.is_listed, self.is_heritage = None, None, None
|
||||
self.restricted_measures = False
|
||||
self.year_built = epc_record.get("year_built")
|
||||
self.number_of_rooms = epc_record.prepared_epc.get("number_habitable_rooms")
|
||||
self.age_band = epc_record.get("age_band")
|
||||
self.construction_age_band = epc_record.get("construction_age_band")
|
||||
self.number_of_floors = epc_record.get("number_of_floors")
|
||||
self.year_built = self.epc_record.year_built
|
||||
self.number_of_rooms = epc_record.number_habitable_rooms
|
||||
self.age_band = epc_record.age_band
|
||||
self.construction_age_band = epc_record.construction_age_band
|
||||
self.number_of_floors = epc_record.number_of_floors
|
||||
self.perimeter = None
|
||||
self.wall_type = None
|
||||
self.floor_type = None
|
||||
|
|
@ -144,63 +140,27 @@ class Property:
|
|||
|
||||
# when storing the energy, we'll also
|
||||
self.energy = {
|
||||
"primary_energy_consumption": epc_record.get("energy_consumption_current"),
|
||||
"epc_co2_emissions": epc_record.get("co2_emissions_current"),
|
||||
"primary_energy_consumption": epc_record.energy_consumption_current,
|
||||
"epc_co2_emissions": epc_record.co2_emissions_current,
|
||||
# These will be added in once we estimate the amount of emissions from appliances - using the carbon
|
||||
# intensity of electricity
|
||||
"appliances_co2_emissions": None,
|
||||
"co2_emissions": None
|
||||
}
|
||||
self.ventilation = {
|
||||
"ventilation": epc_record.get("mechanical_ventilation"),
|
||||
}
|
||||
self.solar_pv = {
|
||||
"solar_pv": epc_record.get("photo_supply"),
|
||||
}
|
||||
self.solar_hot_water = {
|
||||
"solar_hot_water": epc_record.get("solar_water_heating_flag"),
|
||||
"solar_hot_water_boolean": epc_record.get("solar_water_heating_flag_bool"),
|
||||
}
|
||||
self.wind_turbine = {
|
||||
"wind_turbine": epc_record.prepared_epc.get("wind_turbine_count"),
|
||||
}
|
||||
self.number_of_open_fireplaces = {
|
||||
"number_of_open_fireplaces": epc_record.prepared_epc.get(
|
||||
"number_open_fireplaces"
|
||||
),
|
||||
}
|
||||
self.number_of_extensions = {
|
||||
"number_of_extensions": epc_record.prepared_epc.get("extension_count"),
|
||||
}
|
||||
self.number_of_storeys = {
|
||||
"number_of_storeys": epc_record.prepared_epc.get("flat_storey_count"),
|
||||
}
|
||||
self.heat_loss_corridor = {
|
||||
"heat_loss_corridor": epc_record.prepared_epc.get("heat_loss_corridor"),
|
||||
"length": epc_record.prepared_epc.get("unheated_corridor_length"),
|
||||
"heat_loss_corridor_boolean": epc_record.get("heat_loss_corridor_bool"),
|
||||
}
|
||||
self.mains_gas = epc_record.prepared_epc.get("mains_gas_flag")
|
||||
self.floor_height = epc_record.prepared_epc.get("floor_height")
|
||||
self.mains_gas = self.epc_record.mains_gas_flag
|
||||
self.floor_height = self.epc_record.floor_height
|
||||
self.insulation_wall_area = None
|
||||
self.floor_area = epc_record.prepared_epc.get("total_floor_area")
|
||||
self.floor_area = self.epc_record.total_floor_area
|
||||
self.roof_area = None
|
||||
self.insulation_floor_area = None
|
||||
self.number_lighting_outlets = epc_record.prepared_epc.get(
|
||||
"fixed_lighting_outlets_count"
|
||||
)
|
||||
self.number_lighting_outlets = self.epc_record.fixed_lighting_outlets_count
|
||||
self.floor_level = None
|
||||
self.number_of_windows = None
|
||||
self.windows_area = None
|
||||
self.solar_pv_percentage = None
|
||||
|
||||
self.current_energy_consumption = None
|
||||
self.current_energy_consumption_heating_hotwater = None
|
||||
self.current_energy_bill = None
|
||||
self.expected_energy_bill = None
|
||||
|
||||
self.heating_energy_source = None
|
||||
self.hot_water_energy_source = None
|
||||
|
||||
self.recommendations_scoring_data = []
|
||||
self.simulation_epcs = {}
|
||||
|
|
@ -217,13 +177,8 @@ class Property:
|
|||
# Store inspections
|
||||
self.inspections = inspections
|
||||
|
||||
# TODO: We keep this but only temporarily until we add bathrooms, bedrooms, building id to the condition data
|
||||
self.parse_kwargs(kwargs)
|
||||
|
||||
# Funding
|
||||
# self.gbis_eligibiltiy = None
|
||||
# self.eco4_eligibility = None
|
||||
# self.whlg_eligibility = None
|
||||
self.scheme = None
|
||||
self.funded_measures = None
|
||||
self.project_funding = None
|
||||
|
|
@ -235,6 +190,12 @@ class Property:
|
|||
# Ventilation
|
||||
self.has_ventilation = self.identify_ventilation()
|
||||
|
||||
@staticmethod
|
||||
def _safe_int(value: str | int | float | None) -> int | None:
|
||||
if value in [None, ""]:
|
||||
return None
|
||||
return int(round(float(value) + 1e-5))
|
||||
|
||||
@classmethod
|
||||
def extract_kwargs(cls, kwargs):
|
||||
"""
|
||||
|
|
@ -247,24 +208,24 @@ class Property:
|
|||
# Note - none of this data is contained in an energy asssessment, but we should consider how this is done
|
||||
# as we collect more data from the energy assessment
|
||||
|
||||
n_bathrooms = kwargs.get("n_bathrooms", None)
|
||||
n_bathrooms = kwargs.get("n_bathrooms")
|
||||
# We add on a small value to ensure that the number of bathrooms is rounded up, in case the value is 0.5
|
||||
n_bathrooms = int(round(float(n_bathrooms) + 1e-5)) if n_bathrooms not in [None, ""] else None
|
||||
n_bathrooms = cls._safe_int(n_bathrooms) if n_bathrooms not in [None, ""] else None
|
||||
|
||||
n_bedrooms = kwargs.get("n_bedrooms", None)
|
||||
n_bedrooms = int(round(float(n_bedrooms) + 1e-5)) if n_bedrooms not in [None, ""] else None
|
||||
n_bedrooms = kwargs.get("n_bedrooms")
|
||||
n_bedrooms = cls._safe_int(n_bedrooms) if n_bedrooms not in [None, ""] else None
|
||||
|
||||
number_of_floors = kwargs.get("number_of_floors", None)
|
||||
number_of_floors = int(round(float(number_of_floors) + 1e-5)) if number_of_floors not in [None, ""] else None
|
||||
number_of_floors = kwargs.get("number_of_floors")
|
||||
number_of_floors = cls._safe_int(number_of_floors) if number_of_floors not in [None, ""] else None
|
||||
|
||||
insulation_floor_area = kwargs.get("insulation_floor_area", None)
|
||||
insulation_floor_area = kwargs.get("insulation_floor_area")
|
||||
insulation_floor_area = float(insulation_floor_area) if insulation_floor_area not in [None, ""] else None
|
||||
|
||||
insulation_wall_area = kwargs.get("insulation_wall_area", None)
|
||||
insulation_wall_area = kwargs.get("insulation_wall_area")
|
||||
insulation_wall_area = float(insulation_wall_area) if insulation_wall_area not in [None, ""] else None
|
||||
|
||||
# We allow for the asset owner to provide us with total floor area, in the event of it being incorrect
|
||||
floor_area = kwargs.get("floor_area", None)
|
||||
floor_area = kwargs.get("floor_area")
|
||||
floor_area = float(floor_area) if floor_area not in [None, ""] else None
|
||||
|
||||
return {
|
||||
|
|
@ -293,18 +254,15 @@ class Property:
|
|||
It will be the same starting and ending EPC, as we don't have the expected EPC yet
|
||||
"""
|
||||
|
||||
fixed_data_col_names = MANDATORY_FIXED_FEATURES + LATEST_FIELD
|
||||
fixed_data_col_names = [
|
||||
x.lower().replace("_", "-") for x in fixed_data_col_names
|
||||
]
|
||||
fixed_data_col_names = [x.lower() for x in MANDATORY_FIXED_FEATURES + LATEST_FIELD]
|
||||
|
||||
fixed_data = {
|
||||
k.replace("-", "_"): v
|
||||
for k, v in self.data.items()
|
||||
for k, v in vars(self.epc_record).items()
|
||||
if k in fixed_data_col_names
|
||||
}
|
||||
|
||||
difference_record = self.epc_record.create_EPCDifferenceRecord(self.epc_record, fixed_data)
|
||||
difference_record = self.epc_record.create_epc_difference_record(self.epc_record, fixed_data)
|
||||
|
||||
# We have rare cases where entire description columns are missing. EpcRecords will convert this to None.
|
||||
# Due to the sensitivity of the EPCDifferenceRecord creation to missing data, we will fill in these missing
|
||||
|
|
@ -321,7 +279,7 @@ class Property:
|
|||
|
||||
# If we have variables that have been given to us by the landlord that we know are correct, whereas the EPC
|
||||
# may not be, we use them
|
||||
if self.owner_floor_area is not None:
|
||||
if self.owner_floor_area:
|
||||
self.base_difference_record.df["total_floor_area_ending"] = self.floor_area
|
||||
self.base_difference_record.df["estimated_perimeter_ending"] = self.perimeter
|
||||
|
||||
|
|
@ -420,10 +378,7 @@ class Property:
|
|||
|
||||
self.recommendations_scoring_data.append(scoring_dict)
|
||||
|
||||
simulation_epc = self.epc_record.prepared_epc.copy()
|
||||
# Insert static values
|
||||
simulation_epc["lodgement_date"] = simulation_lodgment_date
|
||||
simulation_epc = {k.replace("_", "-"): v for k, v in simulation_epc.items()}
|
||||
simulation_epc = self.epc_record.to_dict(case="kebab", source="prepared")
|
||||
|
||||
types = [x["type"] for x in previous_phase_representatives]
|
||||
if "external_wall_insulation" in types and "internal_wall_insulation" in types:
|
||||
|
|
@ -497,7 +452,7 @@ class Property:
|
|||
# CO₂ emissions per square metre floor area per year in kg/m². Since CO₂ emissions are in tonnes
|
||||
# per year, we multiply by 1000 to get kg/m²
|
||||
"co2-emiss-curr-per-floor-area": round(
|
||||
1000 * (rec_impact["carbon"] / self.data["total-floor-area"])
|
||||
1000 * (rec_impact["carbon"] / self.epc_record.total_floor_area)
|
||||
),
|
||||
"co2-emissions-current": rec_impact["carbon"],
|
||||
"current-energy-rating": sap_to_epc(rec_impact["sap"]),
|
||||
|
|
@ -604,21 +559,22 @@ class Property:
|
|||
if not cleaned:
|
||||
raise ValueError("Cleaner does not contain cleaned data")
|
||||
|
||||
if not self.data:
|
||||
if not self.epc_record:
|
||||
raise ValueError("Property does not contain data")
|
||||
|
||||
for description, attribute in cleaned.items():
|
||||
|
||||
cleaner_cls = all_cleaner_map[description]
|
||||
description_underscore = description.replace("-", "_")
|
||||
|
||||
if self.data[description] in self.DATA_ANOMALY_MATCHES:
|
||||
if getattr(self.epc_record, description_underscore) in self.DATA_ANOMALY_MATCHES:
|
||||
if description == "lighting-description":
|
||||
cleaner_cls = cleaner_cls("", averages=None)
|
||||
else:
|
||||
cleaner_cls = cleaner_cls("")
|
||||
fill_dict = {
|
||||
"original_description": self.data[description],
|
||||
"clean_description": self.data[description],
|
||||
"original_description": getattr(self.epc_record, description_underscore),
|
||||
"clean_description": getattr(self.epc_record, description_underscore),
|
||||
**cleaner_cls.process()
|
||||
}
|
||||
setattr(self, self.ATTRIBUTE_MAP[description], fill_dict)
|
||||
|
|
@ -627,7 +583,7 @@ class Property:
|
|||
attributes = [
|
||||
x
|
||||
for x in cleaned[description]
|
||||
if x["original_description"] == self.data[description]
|
||||
if x["original_description"] == getattr(self.epc_record, description_underscore)
|
||||
]
|
||||
|
||||
if len(attributes) > 1:
|
||||
|
|
@ -638,11 +594,11 @@ class Property:
|
|||
if len(attributes) == 0:
|
||||
# We attempt to perform the clean on the fly
|
||||
if description == "lighting-description":
|
||||
cleaner_cls = cleaner_cls(self.data[description], averages=None)
|
||||
cleaner_cls = cleaner_cls(getattr(self.epc_record, description_underscore), averages=None)
|
||||
else:
|
||||
cleaner_cls = cleaner_cls(self.data[description])
|
||||
cleaner_cls = cleaner_cls(getattr(self.epc_record, description_underscore))
|
||||
processed = {
|
||||
"original_description": self.data[description],
|
||||
"original_description": getattr(self.epc_record, description_underscore),
|
||||
"clean_description": cleaner_cls.description.replace(
|
||||
"(assumed)", ""
|
||||
)
|
||||
|
|
@ -681,12 +637,12 @@ class Property:
|
|||
|
||||
# Today's costs
|
||||
todays_lighting_cost = kwh_client.convert_cost_to_today(
|
||||
original_cost=float(self.data["lighting-cost-current"]),
|
||||
lodgement_date=pd.Timestamp(self.epc_record.prepared_epc["lodgement_date"]).tz_localize(None)
|
||||
original_cost=float(self.epc_record.lighting_cost_current),
|
||||
lodgement_date=pd.Timestamp(self.epc_record.lodgement_date).tz_localize(None)
|
||||
)
|
||||
|
||||
# If we have the kwh figures, we don't need to predict them
|
||||
condition_data = self.energy_assessment_condition_data.copy()
|
||||
condition_data = self.energy_assessment_condition_data
|
||||
|
||||
heating_kwh_predictions = kwh_predictions["heating_kwh_predictions"]
|
||||
hotwater_kwh_predictions = kwh_predictions["hotwater_kwh_predictions"]
|
||||
|
|
@ -725,19 +681,13 @@ class Property:
|
|||
}
|
||||
|
||||
# Sum up the adjusted kwh figures
|
||||
self.current_energy_consumption = sum(list(unadjusted_kwh_estimates.values()))
|
||||
self.current_energy_consumption = sum(unadjusted_kwh_estimates.values())
|
||||
self.current_energy_consumption_heating_hotwater = (
|
||||
unadjusted_kwh_estimates["heating"] + unadjusted_kwh_estimates["hot_water"]
|
||||
)
|
||||
|
||||
self.energy_cost_estimates = {
|
||||
"unadjusted": unadjusted_heating_costs,
|
||||
# Don't think we need the EPC
|
||||
# "epc": {
|
||||
# "heating": float(self.data["heating-cost-current"]),
|
||||
# "hot_water": float(self.data["hot-water-cost-current"]),
|
||||
# "lighting": float(self.data["lighting-cost-current"]),
|
||||
# }
|
||||
}
|
||||
|
||||
self.energy_consumption_estimates = {
|
||||
|
|
@ -797,7 +747,7 @@ class Property:
|
|||
:return:
|
||||
"""
|
||||
|
||||
current_sap_rating = float(self.data["current-energy-efficiency"])
|
||||
current_sap_rating = float(self.epc_record.current_energy_efficiency)
|
||||
if needs_rebaselining:
|
||||
current_sap_rating += rebaselining_sap
|
||||
|
||||
|
|
@ -805,24 +755,24 @@ class Property:
|
|||
|
||||
property_data = {
|
||||
"creation_status": "READY",
|
||||
"uprn": int(self.data["uprn"]),
|
||||
"uprn": int(self.epc_record.uprn),
|
||||
"building_reference_number": (
|
||||
int(self.data["building-reference-number"]) if
|
||||
self.data["building-reference-number"] is not None else None
|
||||
int(self.epc_record.building_reference_number) if
|
||||
self.epc_record.building_reference_number is not None else None
|
||||
),
|
||||
"has_pre_condition_report": True,
|
||||
"has_recommendations": True,
|
||||
"property_type": self.data["property-type"],
|
||||
"built_form": self.data["built-form"],
|
||||
"local_authority": self.data["local-authority-label"],
|
||||
"constituency": self.data["constituency-label"],
|
||||
"property_type": self.epc_record.property_type,
|
||||
"built_form": self.epc_record.built_form,
|
||||
"local_authority": self.epc_record.local_authority_label,
|
||||
"constituency": self.epc_record.constituency_label,
|
||||
"number_of_rooms": self.number_of_rooms,
|
||||
"year_built": self.year_built,
|
||||
"tenure": self.data["tenure"],
|
||||
"tenure": self.epc_record.tenure,
|
||||
"current_epc_rating": current_epc_rating,
|
||||
"current_sap_points": current_sap_rating,
|
||||
"current_valuation": current_valuation,
|
||||
"original_sap_points": self.data["current-energy-efficiency"],
|
||||
"original_sap_points": self.epc_record.original_epc["current-energy-efficiency"],
|
||||
"is_sap_points_adjusted_for_installed_measures": needs_rebaselining,
|
||||
"installed_measures_sap_point_adjustment": rebaselining_sap,
|
||||
}
|
||||
|
|
@ -851,7 +801,7 @@ class Property:
|
|||
raise ValueError("Current energy bill has not been set")
|
||||
|
||||
# IF we have a SAP05 overwrite, we pull out the relevant information
|
||||
sap_05_overwritten = self.data.get("sap-05-overwritten", False)
|
||||
sap_05_overwritten = self.epc_record.sap_05_overwritten
|
||||
|
||||
sap_05_score, sap_05_epc_rating = None, None
|
||||
if sap_05_overwritten:
|
||||
|
|
@ -864,9 +814,9 @@ class Property:
|
|||
sap_05_score = int(newest_old_epc["current-energy-efficiency"])
|
||||
sap_05_epc_rating = newest_old_epc["current-energy-rating"]
|
||||
|
||||
lodgement_date = self.data["lodgement-date"]
|
||||
lodgement_date = self.epc_record.lodgement_date
|
||||
# We check if the lodgement date is more than 10 years old
|
||||
is_expired = (datetime.now() - pd.to_datetime(lodgement_date)) > timedelta(days=3650)
|
||||
is_expired = self.epc_is_expired
|
||||
|
||||
# Handle re-baselining
|
||||
co2_emissions = self.energy["co2_emissions"]
|
||||
|
|
@ -886,42 +836,42 @@ class Property:
|
|||
"portfolio_id": portfolio_id,
|
||||
"lodgement_date": datetime.fromisoformat(lodgement_date),
|
||||
"is_expired": is_expired,
|
||||
"full_address": self.data["address"],
|
||||
"total_floor_area": float(self.data["total-floor-area"]),
|
||||
"full_address": self.epc_record.address,
|
||||
"total_floor_area": float(self.epc_record.total_floor_area),
|
||||
"walls": self.walls["clean_description"],
|
||||
"walls_rating": self._prepare_rating_field(self.data["walls-energy-eff"]),
|
||||
"walls_rating": self._prepare_rating_field(self.epc_record.walls_energy_eff),
|
||||
"roof": self.roof["clean_description"],
|
||||
"roof_rating": self._prepare_rating_field(self.data["roof-energy-eff"]),
|
||||
"roof_rating": self._prepare_rating_field(self.epc_record.roof_energy_eff),
|
||||
"floor": self.floor["clean_description"],
|
||||
"floor_rating": self._prepare_rating_field(self.data["floor-energy-eff"]),
|
||||
"floor_rating": self._prepare_rating_field(self.epc_record.floor_energy_eff),
|
||||
"windows": self.windows["clean_description"],
|
||||
"windows_rating": self._prepare_rating_field(self.data["windows-energy-eff"]),
|
||||
"windows_rating": self._prepare_rating_field(self.epc_record.windows_energy_eff),
|
||||
"heating": self.main_heating["clean_description"],
|
||||
"heating_rating": self._prepare_rating_field(self.data["mainheat-energy-eff"]),
|
||||
"heating_rating": self._prepare_rating_field(self.epc_record.mainheat_energy_eff),
|
||||
"heating_controls": self.main_heating_controls["clean_description"],
|
||||
"heating_controls_rating": self._prepare_rating_field(self.data["mainheatc-energy-eff"]),
|
||||
"heating_controls_rating": self._prepare_rating_field(self.epc_record.mainheatc_energy_eff),
|
||||
"hot_water": self.hotwater["clean_description"],
|
||||
"hot_water_rating": self._prepare_rating_field(self.data["hot-water-energy-eff"]),
|
||||
"hot_water_rating": self._prepare_rating_field(self.epc_record.hot_water_energy_eff),
|
||||
"lighting": self.lighting["clean_description"],
|
||||
"lighting_rating": self._prepare_rating_field(self.data["lighting-energy-eff"]),
|
||||
"lighting_rating": self._prepare_rating_field(self.epc_record.lighting_energy_eff),
|
||||
"mainfuel": self.main_fuel["clean_description"],
|
||||
"ventilation": self.ventilation["ventilation"],
|
||||
"solar_pv": self.solar_pv["solar_pv"],
|
||||
"solar_hot_water": self.solar_hot_water["solar_hot_water_boolean"],
|
||||
"wind_turbine": self.wind_turbine["wind_turbine"],
|
||||
"ventilation": self.epc_record.mechanical_ventilation,
|
||||
"solar_pv": self.epc_record.photo_supply,
|
||||
"solar_hot_water": self.epc_record.solar_water_heating_flag_bool,
|
||||
"wind_turbine": self.epc_record.wind_turbine_count,
|
||||
"floor_height": self.floor_height,
|
||||
"heat_loss_corridor": self.heat_loss_corridor["heat_loss_corridor_boolean"],
|
||||
"unheated_corridor_length": self.heat_loss_corridor["length"],
|
||||
"number_of_open_fireplaces": self.number_of_open_fireplaces["number_of_open_fireplaces"],
|
||||
"number_of_extensions": self.number_of_extensions["number_of_extensions"],
|
||||
"number_of_storeys": self.number_of_storeys["number_of_storeys"],
|
||||
"heat_loss_corridor": self.epc_record.heat_loss_corridor_bool,
|
||||
"unheated_corridor_length": self.epc_record.unheated_corridor_length,
|
||||
"number_of_open_fireplaces": self.epc_record.number_open_fireplaces,
|
||||
"number_of_extensions": self.epc_record.extension_count,
|
||||
"number_of_storeys": self.epc_record.flat_storey_count,
|
||||
"mains_gas": self.mains_gas,
|
||||
"energy_tariff": self.data["energy-tariff"],
|
||||
"energy_tariff": self.epc_record.energy_tariff,
|
||||
"primary_energy_consumption": primary_energy_consumption,
|
||||
"co2_emissions": co2_emissions,
|
||||
"current_energy_demand": current_kwh_demand, # This is kwh - naming is confusing
|
||||
"current_energy_demand_heating_hotwater": current_kwh_heating_hotwater, # This is kwh
|
||||
"estimated": self.data.get("estimated", False),
|
||||
"estimated": self.epc_record.estimated,
|
||||
# We indicate if we've overwritten a SAP 05 EPC
|
||||
"sap_05_overwritten": sap_05_overwritten,
|
||||
"sap_05_score": sap_05_score,
|
||||
|
|
@ -936,6 +886,10 @@ class Property:
|
|||
"installed_measures_total_energy_bill_adjustment": rebaselining_bills,
|
||||
"installed_measures_heat_demand_adjustment": rebaselining_heat_demand,
|
||||
"is_epc_adjusted_for_installed_measures": needs_rebaselining,
|
||||
# Re-baselining variables - to replace already installed variables entirely
|
||||
"lodged_co2_emissions": float(self.epc_record.original_epc["co2-emissions-current"]),
|
||||
"lodged_heat_demand": float(self.epc_record.original_epc["energy-consumption-current"]),
|
||||
"has_been_remodelled": self.epc_record.has_been_remodelled,
|
||||
}
|
||||
|
||||
return property_details_epc
|
||||
|
|
@ -984,7 +938,7 @@ class Property:
|
|||
"""
|
||||
|
||||
result = property_dimensions[
|
||||
(property_dimensions["PROPERTY_TYPE"] == self.data["property-type"])
|
||||
(property_dimensions["PROPERTY_TYPE"] == self.epc_record.property_type)
|
||||
]
|
||||
|
||||
if (
|
||||
|
|
@ -996,10 +950,10 @@ class Property:
|
|||
]
|
||||
|
||||
if (
|
||||
self.data["built-form"] not in self.DATA_ANOMALY_MATCHES
|
||||
and self.data["built-form"] in result["BUILT_FORM"]
|
||||
self.epc_record.built_form not in self.DATA_ANOMALY_MATCHES
|
||||
and self.epc_record.built_form in result["BUILT_FORM"]
|
||||
):
|
||||
result = result[(result["BUILT_FORM"] == self.data["built-form"])]
|
||||
result = result[(result["BUILT_FORM"] == self.epc_record.built_form)]
|
||||
|
||||
return result[
|
||||
["NUMBER_HABITABLE_ROOMS", "TOTAL_FLOOR_AREA", "FLOOR_HEIGHT"]
|
||||
|
|
@ -1042,7 +996,7 @@ class Property:
|
|||
num_floors=self.number_of_floors,
|
||||
floor_height=self.floor_height,
|
||||
perimeter=self.perimeter,
|
||||
built_form=self.data["built-form"],
|
||||
built_form=self.epc_record.built_form,
|
||||
)
|
||||
|
||||
if self.insulation_floor_area is None:
|
||||
|
|
@ -1061,15 +1015,15 @@ class Property:
|
|||
|
||||
def set_floor_level(self):
|
||||
self.floor_level = (
|
||||
FLOOR_LEVEL_MAP[self.data["floor-level"]]
|
||||
if self.data["floor-level"] not in self.DATA_ANOMALY_MATCHES
|
||||
and self.data["floor-level"] is not None
|
||||
FLOOR_LEVEL_MAP[self.epc_record.floor_level]
|
||||
if self.epc_record.floor_level not in self.DATA_ANOMALY_MATCHES
|
||||
and self.epc_record.floor_level is not None
|
||||
else None
|
||||
)
|
||||
|
||||
if self.floor_level is None:
|
||||
|
||||
if self.data["property-type"] != "Flat":
|
||||
if self.epc_record.property_type != "Flat":
|
||||
return
|
||||
|
||||
if self.floor["another_property_below"]:
|
||||
|
|
@ -1129,21 +1083,6 @@ class Property:
|
|||
)
|
||||
self.floor_type = "suspended"
|
||||
|
||||
@staticmethod
|
||||
def _extract_component(
|
||||
component_data, component_rename_cols, component_drop_cols, rename_prefix=None
|
||||
):
|
||||
for k in component_rename_cols:
|
||||
component_data[f"{rename_prefix}_{k}"] = component_data.get(k)
|
||||
|
||||
component_data = {
|
||||
k: v
|
||||
for k, v in component_data.items()
|
||||
if k not in component_drop_cols + component_rename_cols
|
||||
}
|
||||
|
||||
return component_data
|
||||
|
||||
def set_windows_count(self):
|
||||
"""
|
||||
Using the estimate_windows function, this method will set the number of windows in the property
|
||||
|
|
@ -1155,8 +1094,8 @@ class Property:
|
|||
self.number_of_windows = int(condition_data["number_of_windows"]) \
|
||||
if condition_data.get("number_of_windows") is not None \
|
||||
else estimate_windows(
|
||||
property_type=self.data["property-type"],
|
||||
built_form=self.data["built-form"],
|
||||
property_type=self.epc_record.property_type,
|
||||
built_form=self.epc_record.built_form,
|
||||
construction_age_band=self.construction_age_band,
|
||||
floor_area=self.floor_area,
|
||||
number_habitable_rooms=self.number_of_rooms,
|
||||
|
|
@ -1176,14 +1115,14 @@ class Property:
|
|||
|
||||
# If we have a house over a floor area threshold, we recommend an ASHP
|
||||
if (
|
||||
self.data["property-type"] in ["House", "Bungalow"] and
|
||||
self.epc_record.property_type in ["House", "Bungalow"] and
|
||||
self.floor_area > assumptions.ASHP_FLOOR_AREA_THRESHOLD
|
||||
):
|
||||
return True
|
||||
|
||||
suitable_property_type = (
|
||||
self.data["property-type"] in ["House", "Bungalow"] and
|
||||
self.data["built-form"] not in ["Enclosed Mid-Terrace", "Enclosed End-Terrace"]
|
||||
self.epc_record.property_type in ["House", "Bungalow"] and
|
||||
self.epc_record.built_form not in ["Enclosed Mid-Terrace", "Enclosed End-Terrace"]
|
||||
)
|
||||
|
||||
has_air_source_heat_pump = self.main_heating["has_air_source_heat_pump"]
|
||||
|
|
@ -1205,12 +1144,12 @@ class Property:
|
|||
# may be installed such that they are not visible from the street
|
||||
return False
|
||||
|
||||
if (self.data["property-type"] in ["House", "Bungalow"]) and (
|
||||
if (self.epc_record.property_type in ["House", "Bungalow"]) and (
|
||||
not pd.isnull(self.roof["thermal_transmittance"])
|
||||
):
|
||||
return True
|
||||
|
||||
is_valid_property_type = self.data["property-type"] in ["House", "Bungalow", "Maisonette"]
|
||||
is_valid_property_type = self.epc_record.property_type in ["House", "Bungalow", "Maisonette"]
|
||||
is_valid_roof_type = (
|
||||
self.roof["is_flat"] or self.roof["is_pitched"] or self.roof["is_roof_room"]
|
||||
)
|
||||
|
|
@ -1223,7 +1162,7 @@ class Property:
|
|||
"already has solar pv", "roof too small", "no roof"
|
||||
]
|
||||
else:
|
||||
has_no_existing_solar_pv = self.data["photo-supply"] in [
|
||||
has_no_existing_solar_pv = self.epc_record.photo_supply in [
|
||||
None, 0, self.DATA_ANOMALY_MATCHES
|
||||
]
|
||||
|
||||
|
|
@ -1295,9 +1234,25 @@ class Property:
|
|||
|
||||
def identify_ventilation(self):
|
||||
|
||||
ventilation_descriptions = [
|
||||
return self.epc_record.mechanical_ventilation in {
|
||||
'mechanical, extract only',
|
||||
'mechanical, supply and extract'
|
||||
]
|
||||
}
|
||||
|
||||
return self.data.get("mechanical-ventilation") in ventilation_descriptions
|
||||
@property
|
||||
def epc_is_expired(self) -> bool:
|
||||
"""
|
||||
This property indicates that the EPC is expired. This is based on the lodgement date, where an EPC is
|
||||
valid for 10 years.
|
||||
:return: boolean indicating whether the EPC is expired
|
||||
"""
|
||||
lodgement_date = self.epc_record.lodgement_date
|
||||
return (datetime.now() - pd.to_datetime(lodgement_date)) > timedelta(days=3650)
|
||||
|
||||
@property
|
||||
def epc_is_estimated(self) -> bool:
|
||||
"""
|
||||
This property indicates that the EPC is estimated, based on the presence of the "estimated" flag in the data
|
||||
:return: boolean indicating whether the EPC is estimated
|
||||
"""
|
||||
return self.epc_record.estimated
|
||||
|
|
|
|||
|
|
@ -1,36 +1,52 @@
|
|||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
import datatypes.epc as epc_datatypes
|
||||
from typing import Optional, Union
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class Address:
|
||||
# address: Optional[str]
|
||||
# full_address: Optional[str]
|
||||
# property_type: Optional[str]
|
||||
# built_form: Optional[str]
|
||||
# estimated: bool
|
||||
|
||||
# New fields
|
||||
uprn: Optional[int]
|
||||
landlord_property_id: Optional[str]
|
||||
address: Optional[str]
|
||||
full_address: Optional[str]
|
||||
address_1: str
|
||||
address_2: Optional[str]
|
||||
address_3: Optional[str]
|
||||
full_address: str
|
||||
postcode: str
|
||||
property_type: Optional[str]
|
||||
built_form: Optional[str]
|
||||
estimated: bool
|
||||
landlord_total_floor_area_m2: Union[float, None]
|
||||
# Property components
|
||||
landlord_property_type: Optional[epc_datatypes.property_type_built_form.PropertyType]
|
||||
landlord_built_form: Optional[epc_datatypes.property_type_built_form.BuiltForm]
|
||||
landlord_wall_construction: Optional[epc_datatypes.walls.EpcWallDescriptions]
|
||||
landlord_roof_construction: Optional[epc_datatypes.roof.EpcRoofDescriptions]
|
||||
landlord_floor_construction: Optional[epc_datatypes.floor.EpcFloorDescriptions]
|
||||
landlord_windows_type: Optional[epc_datatypes.windows.EpcWindowDescriptions]
|
||||
landlord_heating_system: Optional[epc_datatypes.main_heating.EpcHeatingSystems]
|
||||
landlord_fuel_type: Optional[epc_datatypes.fuel.EpcFuel]
|
||||
landlord_heating_controls: Optional[epc_datatypes.heating_controls.EpcHeatingControls]
|
||||
landlord_hot_water_system: Optional[epc_datatypes.hotwater.EpcHotWaterSystems]
|
||||
# Efficiency
|
||||
landlord_wall_efficiency: Optional[epc_datatypes.efficiency.EpcEfficiency]
|
||||
landlord_roof_efficiency: Optional[epc_datatypes.efficiency.EpcEfficiency]
|
||||
landlord_windows_efficiency: Optional[epc_datatypes.efficiency.EpcEfficiency]
|
||||
landlord_heating_efficiency: Optional[epc_datatypes.efficiency.EpcEfficiency]
|
||||
landlord_heating_controls_efficiency: Optional[epc_datatypes.efficiency.EpcEfficiency]
|
||||
landlord_hot_water_efficiency: Optional[epc_datatypes.efficiency.EpcEfficiency]
|
||||
|
||||
# Additionals
|
||||
landlord_has_sloping_ceiling: Optional[bool]
|
||||
landlord_multi_glaze_proportion: Optional[float]
|
||||
landlord_construction_age_band: Optional[epc_datatypes.construction_age_band.EpcConstructionAgeBand]
|
||||
|
||||
# Additional address data, associated to a standardised asset list
|
||||
domna_full_address: Optional[str]
|
||||
domna_address_1: Optional[str]
|
||||
landlord_heating_system: Optional[str] = None
|
||||
solar_reason: Optional[str] = None
|
||||
cavity_reason: Optional[str] = None
|
||||
|
||||
@property
|
||||
def address1(self):
|
||||
|
||||
if self.domna_address_1 is not None:
|
||||
address1 = self.domna_address_1
|
||||
else:
|
||||
address1 = self.address
|
||||
|
||||
# Format
|
||||
address1 = str(int(address1)) if isinstance(address1, float) else str(address1)
|
||||
return address1
|
||||
# domna_full_address: Optional[str]
|
||||
# domna_address_1: Optional[str]
|
||||
|
||||
@property
|
||||
def request_data(self) -> dict[str, Optional[str]]:
|
||||
|
|
@ -41,27 +57,9 @@ class Address:
|
|||
"uprn": self.uprn,
|
||||
"landlord_property_id": self.landlord_property_id,
|
||||
"postcode": self.postcode,
|
||||
"address1": self.address1,
|
||||
"address1": self.address_1,
|
||||
"full_address": self.full_address,
|
||||
}
|
||||
|
||||
# Drop nulls
|
||||
return {k: v for k, v in data.items() if v is not None}
|
||||
|
||||
@property
|
||||
def heating_system(self):
|
||||
"""
|
||||
Helper function to extract a heating system, which can be used to estimate EPC. This is a very limited,
|
||||
placeholder function to cover some initial immediate cases.
|
||||
:return:
|
||||
"""
|
||||
|
||||
ll_heating = self.landlord_property_id
|
||||
if not ll_heating:
|
||||
return None
|
||||
|
||||
if ll_heating == "electric storage heaters":
|
||||
# Return with the same format at the EPC
|
||||
return "Electric storage heaters"
|
||||
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import warnings
|
||||
import pandas as pd
|
||||
from typing import Iterator
|
||||
from backend.addresses.Address import Address
|
||||
from datatypes.epc.property_type_built_form import PropertyType
|
||||
|
||||
|
||||
class Addresses:
|
||||
|
|
@ -19,9 +22,28 @@ class Addresses:
|
|||
@classmethod
|
||||
def from_plan_input(cls, plan_input: list[dict], body) -> "Addresses":
|
||||
addresses = []
|
||||
|
||||
for row in plan_input:
|
||||
addresses.append(cls._parse_row(row, body))
|
||||
return cls(addresses)
|
||||
try:
|
||||
if body.file_format == "ara_property_list":
|
||||
addr = cls.parse_ara_row(row, body)
|
||||
else:
|
||||
addr = cls._parse_row_deprecated(row, body)
|
||||
|
||||
# Fallback if new parser fails
|
||||
except Exception:
|
||||
warnings.warn(
|
||||
"Falling back to deprecated parser for row",
|
||||
RuntimeWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
addr = cls._parse_row_deprecated(row, body)
|
||||
|
||||
addresses.append(addr)
|
||||
|
||||
addresses = cls(addresses)
|
||||
addresses.validate_uprns()
|
||||
return addresses
|
||||
|
||||
def get_uprns(self):
|
||||
return [x.uprn for x in self._addresses if x.uprn is not None]
|
||||
|
|
@ -35,54 +57,115 @@ class Addresses:
|
|||
def get_postcodes_for_flats(self):
|
||||
# Method to extract all of the postcodes associated to a flat, which is used for remote assessments
|
||||
# on flats
|
||||
return [x.postcode for x in self._addresses if x.property_type in ["Flat", "flat"]]
|
||||
return [x.postcode for x in self._addresses if x.landlord_property_type in [PropertyType.flat.value]]
|
||||
|
||||
def get_property_requests(self):
|
||||
return [x.request_data for x in self._addresses]
|
||||
|
||||
def validate_uprns(self):
|
||||
"""Raise ValueError if any address has a non-int UPRN."""
|
||||
for addr in self._addresses:
|
||||
if addr.uprn is not None and not isinstance(addr.uprn, int):
|
||||
raise ValueError(f"Address with non-integer UPRN detected: {addr.uprn} in {addr}")
|
||||
|
||||
@staticmethod
|
||||
def _parse_row(row: dict, body) -> Address:
|
||||
def parse_ara_row(row: dict, body) -> Address:
|
||||
"""
|
||||
Method to parse a row from the ARA property list format, which is a more standardised format that we are
|
||||
moving towards.
|
||||
:param row: A dictionary representing a row from the ARA property list, which should have keys corresponding
|
||||
to the Address dataclass fields. The method will attempt to parse these fields and create an Address object.
|
||||
:param body: The PlanTriggerRequest body, which may contain additional information about the file format and
|
||||
other details that could be relevant for parsing.
|
||||
:return: An Address object created from the parsed row data.
|
||||
"""
|
||||
return Address(
|
||||
uprn=int(row["uprn"]),
|
||||
landlord_property_id=str(row["landlord_property_id"]) if row.get("landlord_property_id") else None,
|
||||
address_1=row["address_1"],
|
||||
address_2=row.get("address_2"),
|
||||
address_3=row.get("address_3"),
|
||||
full_address=row["full_address"],
|
||||
postcode=str(row["postcode"]),
|
||||
landlord_total_floor_area_m2=float(row["landlord_total_floor_area_m2"]) if row.get(
|
||||
"landlord_total_floor_area_m2") else None,
|
||||
landlord_property_type=row.get("landlord_property_type"),
|
||||
landlord_built_form=row.get("landlord_built_form"),
|
||||
landlord_wall_construction=row.get("landlord_wall_construction"),
|
||||
landlord_roof_construction=row.get("landlord_roof_construction"),
|
||||
landlord_floor_construction=row.get("landlord_floor_construction"),
|
||||
landlord_windows_type=row.get("landlord_windows_type"),
|
||||
landlord_heating_system=row.get("landlord_heating_system"),
|
||||
landlord_fuel_type=row.get("landlord_fuel_type"),
|
||||
landlord_heating_controls=row.get("landlord_heating_controls"),
|
||||
landlord_hot_water_system=row.get("landlord_hot_water_system"),
|
||||
landlord_wall_efficiency=row.get("landlord_wall_efficiency"),
|
||||
landlord_roof_efficiency=row.get("landlord_roof_efficiency"),
|
||||
landlord_windows_efficiency=row.get("landlord_windows_efficiency"),
|
||||
landlord_heating_efficiency=row.get("landlord_heating_efficiency"),
|
||||
landlord_heating_controls_efficiency=row.get("landlord_heating_controls_efficiency"),
|
||||
landlord_hot_water_efficiency=row.get("landlord_hot_water_efficiency"),
|
||||
landlord_has_sloping_ceiling=bool(row.get("landlord_has_sloping_ceiling")) if row.get(
|
||||
"landlord_has_sloping_ceiling") is not None else None,
|
||||
landlord_multi_glaze_proportion=float(row["landlord_multi_glaze_proportion"]) if row.get(
|
||||
"landlord_multi_glaze_proportion") else None,
|
||||
landlord_construction_age_band=row.get("landlord_construction_age_band"),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _parse_row_deprecated(row: dict, body) -> Address:
|
||||
def clean_uprn(v):
|
||||
if v is None:
|
||||
return None
|
||||
try:
|
||||
return int(float(v))
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
raise ValueError(f"Invalid UPRN value: {v}")
|
||||
|
||||
uprn = clean_uprn(row.get("uprn"))
|
||||
uprn_option1 = row.get("uprn")
|
||||
uprn_option1 = uprn_option1 if not pd.isnull(uprn_option1) else None
|
||||
uprn_option2 = row.get("ordnance_survey_uprn")
|
||||
uprn_option2 = uprn_option2 if not pd.isnull(uprn_option2) else None
|
||||
|
||||
address = row.get("address")
|
||||
if not address and body.file_format == "domna_asset_list":
|
||||
address = row.get("domna_address_1")
|
||||
uprn = clean_uprn(uprn_option1 or uprn_option2)
|
||||
|
||||
full_address = (
|
||||
row.get("domna_full_address")
|
||||
if body.file_format == "domna_asset_list"
|
||||
else None
|
||||
)
|
||||
if not isinstance(full_address, str):
|
||||
full_address = None
|
||||
address = row.get("address") or row.get("domna_address_1") or ""
|
||||
full_address = row.get("domna_full_address") or address or ""
|
||||
|
||||
postcode = str(row["postcode"]).strip().upper()
|
||||
postcode = str(row.get("postcode", "")).strip().upper()
|
||||
|
||||
return Address(
|
||||
uprn=uprn,
|
||||
landlord_property_id=str(row["landlord_property_id"])
|
||||
if row.get("landlord_property_id") else None,
|
||||
address=str(address).strip() if address else None,
|
||||
full_address=str(full_address).strip() if full_address else None,
|
||||
landlord_property_id=str(row["landlord_property_id"]) if row.get("landlord_property_id") else None,
|
||||
address_1=str(address).strip(),
|
||||
address_2=None,
|
||||
address_3=None,
|
||||
full_address=str(full_address).strip(),
|
||||
postcode=postcode,
|
||||
property_type=row.get("property_type"),
|
||||
built_form=row.get("built_form"),
|
||||
estimated=bool(row.get("estimated", False)),
|
||||
domna_full_address=row.get("domna_full_address"),
|
||||
domna_address_1=row.get("domna_address_1"),
|
||||
)
|
||||
|
||||
# def _build_identity_index(self) -> dict:
|
||||
# index = {}
|
||||
# for addr in self._addresses:
|
||||
# key = addr.identity_key()
|
||||
# if key in index:
|
||||
# raise ValueError(f"Duplicate address identity detected: {key}")
|
||||
# index[key] = addr
|
||||
# return index
|
||||
landlord_total_floor_area_m2=None,
|
||||
|
||||
# Map old to new fields
|
||||
landlord_property_type=row.get("property_type") or row.get("landlord_property_type"),
|
||||
landlord_built_form=row.get("built_form") or row.get("landlord_built_form"),
|
||||
|
||||
landlord_wall_construction=None,
|
||||
landlord_roof_construction=None,
|
||||
landlord_floor_construction=None,
|
||||
landlord_windows_type=None,
|
||||
landlord_heating_system=row.get("epc_heating_type"),
|
||||
landlord_fuel_type=None,
|
||||
landlord_heating_controls=None,
|
||||
landlord_hot_water_system=None,
|
||||
|
||||
landlord_wall_efficiency=None,
|
||||
landlord_roof_efficiency=None,
|
||||
landlord_windows_efficiency=None,
|
||||
landlord_heating_efficiency=None,
|
||||
landlord_heating_controls_efficiency=None,
|
||||
landlord_hot_water_efficiency=None,
|
||||
|
||||
landlord_has_sloping_ceiling=None,
|
||||
landlord_multi_glaze_proportion=None,
|
||||
landlord_construction_age_band=None,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -182,8 +182,8 @@ class GoogleSolarApi:
|
|||
self.exclude_north_facing_segments(property_instance=property_instance)
|
||||
# If a property is semi-detached, it's possible for us to include segments from an attached unit
|
||||
if property_instance is not None:
|
||||
if (property_instance.data["built-form"] == "Semi-Detached") and (
|
||||
property_instance.data["extension-count"] == 0
|
||||
if (property_instance.epc_record.built_form == "Semi-Detached") and (
|
||||
property_instance.epc_record.extension_count == 0
|
||||
):
|
||||
self.exclude_likely_duplicate_surfaces()
|
||||
|
||||
|
|
@ -708,7 +708,7 @@ class GoogleSolarApi:
|
|||
# We set the target rating to EPC C, which is the typical EPC rating we would expect the
|
||||
# property to achieve post retrofit of just the fabric
|
||||
"energy_consumption": cls.estimate_new_consumption(
|
||||
current_energy_efficiency=min(p.data["current-energy-efficiency"], 100),
|
||||
current_energy_efficiency=min(p.epc_record.current_energy_efficiency, 100),
|
||||
target_efficiency="69",
|
||||
current_consumption=p.estimate_electrical_consumption(
|
||||
assumed_ashp_efficiency=assumptions.AVERAGE_ASHP_EFFICIENCY, exclusions=body.exclusions
|
||||
|
|
@ -727,7 +727,7 @@ class GoogleSolarApi:
|
|||
# We set the target rating to EPC C, which is the typical EPC rating we would expect the
|
||||
# property to achieve post retrofit of just the fabric
|
||||
"energy_consumption": cls.estimate_new_consumption(
|
||||
current_energy_efficiency=min(int(p.data["current-energy-efficiency"]), 100),
|
||||
current_energy_efficiency=min(p.epc_record.current_energy_efficiency, 100),
|
||||
target_efficiency="69",
|
||||
current_consumption=p.estimate_electrical_consumption(
|
||||
assumed_ashp_efficiency=assumptions.AVERAGE_ASHP_EFFICIENCY, exclusions=body.exclusions
|
||||
|
|
|
|||
|
|
@ -54,11 +54,11 @@ class Settings(BaseSettings):
|
|||
SAP_PREDICTIONS_BUCKET: str = "changeme"
|
||||
CARBON_PREDICTIONS_BUCKET: str = "changeme"
|
||||
HEAT_PREDICTIONS_BUCKET: str = "changeme"
|
||||
# LIGHTING_COST_PREDICTIONS_BUCKET: str
|
||||
# HEATING_COST_PREDICTIONS_BUCKET: str
|
||||
# HOT_WATER_COST_PREDICTIONS_BUCKET: str
|
||||
HEATING_KWH_PREDICTIONS_BUCKET: str = "changeme"
|
||||
HOTWATER_KWH_PREDICTIONS_BUCKET: str = "changeme"
|
||||
SAP_BASELINE_PREDICTIONS_BUCKET: str = "changeme"
|
||||
CARBON_BASELINE_PREDICTIONS_BUCKET: str = "changeme"
|
||||
HEAT_BASELINE_PREDICTIONS_BUCKET: str = "changeme"
|
||||
|
||||
# Other S3 buckts
|
||||
ENERGY_ASSESSMENTS_BUCKET: str = "changeme"
|
||||
|
|
@ -91,4 +91,9 @@ def get_prediction_buckets():
|
|||
"carbon_change_predictions": get_settings().CARBON_PREDICTIONS_BUCKET,
|
||||
"heating_kwh_predictions": get_settings().HEATING_KWH_PREDICTIONS_BUCKET,
|
||||
"hotwater_kwh_predictions": get_settings().HOTWATER_KWH_PREDICTIONS_BUCKET,
|
||||
|
||||
# Score model - SAP re-baselining model
|
||||
"retrofit_sap_baseline_predictions": get_settings().SAP_BASELINE_PREDICTIONS_BUCKET,
|
||||
"retrofit_carbon_baseline_predictions": get_settings().CARBON_BASELINE_PREDICTIONS_BUCKET,
|
||||
"retrofit_heat_baseline_predictions": get_settings().HEAT_BASELINE_PREDICTIONS_BUCKET,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from typing import Optional
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy import func
|
||||
from backend.app.db.models.addresses import PostcodeSearch
|
||||
from utils.logger import setup_logger
|
||||
|
|
@ -20,7 +20,7 @@ def _get_associated_records(results, uprn, uprn_key="UPRN"):
|
|||
return matched_record
|
||||
|
||||
|
||||
def get_associated_uprns(postcode_search: PostcodeSearch, uprn: str | int):
|
||||
def get_associated_uprns(postcode_search: Optional[PostcodeSearch], uprn: str | int):
|
||||
"""
|
||||
Given a postcode and UPRN, for a remote assessment, fetch all associated UPRNs, based
|
||||
on parent UPRN. This will be properties in the same building
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ def get_latest_assessments_for_uprns(
|
|||
found_set = set(result.keys())
|
||||
|
||||
missing_uprns = uprn_set - found_set
|
||||
|
||||
|
||||
for uprn in missing_uprns:
|
||||
result[uprn] = EnergyAssessment.empty_response()
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class EpcStoreService:
|
|||
Service layer for EPC data lookup and persistence.
|
||||
"""
|
||||
|
||||
FRESHNESS_DAYS = 30
|
||||
FRESHNESS_DAYS = 180 # Upgraded to 180 days
|
||||
|
||||
# status labels
|
||||
FRESH = "fresh"
|
||||
|
|
|
|||
|
|
@ -15,8 +15,9 @@ from backend.app.db.models.portfolio import (
|
|||
)
|
||||
|
||||
|
||||
def create_property(session: Session, portfolio_id: int, address: str, postcode: str, uprn: str,
|
||||
energy_assessment: dict, landlord_property_id: str | None = None) -> (int, bool):
|
||||
def create_property(
|
||||
session: Session, portfolio_id: int, address: str, postcode: str, uprn: str,
|
||||
energy_assessment: dict, landlord_property_id: str | None = None) -> (int, bool):
|
||||
"""
|
||||
This function will create a record for the property in the database if it does not exist.
|
||||
If it does exist, it will just update the updated_at field.
|
||||
|
|
@ -252,7 +253,7 @@ def bulk_create_properties(
|
|||
|
||||
rows.append(
|
||||
{
|
||||
"address": addr.address1,
|
||||
"address": addr.address_1,
|
||||
"postcode": addr.postcode,
|
||||
"portfolio_id": body.portfolio_id,
|
||||
"uprn": addr.uprn,
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ class SubTaskInterface:
|
|||
self,
|
||||
subtask_id: UUID,
|
||||
status: str,
|
||||
outputs: Optional[Dict[str, str]] = None,
|
||||
outputs: Optional[Dict[str, str] | str] = None,
|
||||
cloud_logs_url: Optional[str] = None,
|
||||
) -> SubTask:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -147,6 +147,10 @@ class PropertyModel(Base):
|
|||
is_sap_points_adjusted_for_installed_measures = Column(Boolean, default=False)
|
||||
original_sap_points = Column(Float)
|
||||
|
||||
# New for re-scoring - we will need to delete some of the redundant fields but there is a ticket for this
|
||||
lodged_sap_points = Column(Float)
|
||||
lodged_epc_rating = Column(Enum(Epc))
|
||||
|
||||
|
||||
class FeatureRating(enum.Enum):
|
||||
VERY_GOOD = 5
|
||||
|
|
@ -253,6 +257,12 @@ class PropertyDetailsEpcModel(Base):
|
|||
installed_measures_heat_demand_adjustment = Column(Float)
|
||||
is_epc_adjusted_for_installed_measures = Column(Boolean, default=False)
|
||||
|
||||
# New columns - we'll need to delete some of the redundant fields, associated to "already installed" but
|
||||
# we have a ticket for this piece of work
|
||||
lodged_co2_emissions = Column(Float)
|
||||
lodged_heat_demand = Column(Float)
|
||||
has_been_remodelled = Column(Boolean, default=False)
|
||||
|
||||
|
||||
class PropertyDetailsSpatial(Base):
|
||||
__tablename__ = "property_details_spatial"
|
||||
|
|
|
|||
109
backend/app/plan/plan_input_processor.py
Normal file
109
backend/app/plan/plan_input_processor.py
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import logging
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from backend.addresses.Addresses import Addresses
|
||||
from backend.app.config import get_settings
|
||||
from utils.s3 import read_csv_from_s3, read_excel_from_s3
|
||||
|
||||
|
||||
class PlanInputProcessor:
|
||||
def __init__(self, body):
|
||||
self.body = body
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.plan_input = None
|
||||
self.valuation_data = []
|
||||
self.index_start = getattr(body, 'index_start', None)
|
||||
self.index_end = getattr(body, 'index_end', None)
|
||||
|
||||
def process(self):
|
||||
if self.body.file_type == "xlsx":
|
||||
self.logger.info("Getting the plan input")
|
||||
self.plan_input = read_excel_from_s3(
|
||||
bucket_name=get_settings().PLAN_TRIGGER_BUCKET,
|
||||
file_key=self.body.trigger_file_path,
|
||||
sheet_name=self.body.sheet_name,
|
||||
header_row=0,
|
||||
)
|
||||
self.logger.info("Got the plan input from excel")
|
||||
if self.body.file_format == "domna_asset_list":
|
||||
self._process_domna_asset_list()
|
||||
elif self.body.file_format == "ara_property_list":
|
||||
self._process_ara_property_list()
|
||||
else:
|
||||
raise ValueError("Other formats not yet supported")
|
||||
else:
|
||||
self.logger.info("Getting the plan input from csv")
|
||||
self.plan_input = read_csv_from_s3(
|
||||
bucket_name=get_settings().PLAN_TRIGGER_BUCKET, filepath=self.body.trigger_file_path
|
||||
)
|
||||
self.logger.info("Got the plan input from csv")
|
||||
# Slice if needed
|
||||
if self.index_start is not None and self.index_end is not None:
|
||||
self.plan_input = self.plan_input[self.index_start:self.index_end]
|
||||
# Extract valuation data if present
|
||||
self._extract_valuation_data()
|
||||
return self.to_addresses()
|
||||
|
||||
def _extract_valuation_data(self):
|
||||
# Only for domna_asset_list, extract domna_valuation if present
|
||||
if self.body.file_format == "domna_asset_list" and self.plan_input:
|
||||
first = self.plan_input[0]
|
||||
if "domna_valuation" in first:
|
||||
self.valuation_data = [
|
||||
{"uprn": x.get("uprn"), "valuation": x.get("domna_valuation")}
|
||||
for x in self.plan_input if x.get("domna_valuation") is not None
|
||||
]
|
||||
# Could add more formats here in future
|
||||
|
||||
def _process_domna_asset_list(self):
|
||||
df = self.plan_input
|
||||
df = df.rename(
|
||||
columns={"domna_address_1": "address", "domna_postcode": "postcode", "epc_os_uprn": "uprn"}
|
||||
)
|
||||
if "estimated" not in df.columns:
|
||||
df["estimated"] = False
|
||||
df["uprn"] = np.where(
|
||||
df["estimated"].isin([1, True]) & ((df["uprn"] < 0) | pd.isnull(df["uprn"])), None, df["uprn"]
|
||||
)
|
||||
df["property_type"] = df["landlord_property_type"].copy()
|
||||
if "landlord_built_form" in df.columns:
|
||||
df["built_form"] = df["landlord_built_form"].copy()
|
||||
else:
|
||||
df["built_form"] = None
|
||||
if "epc_property_type" not in df.columns:
|
||||
df["epc_property_type"] = None
|
||||
df["property_type"] = np.where(
|
||||
df["property_type"] == "unknown", df["epc_property_type"], df["property_type"]
|
||||
)
|
||||
if "epc_archetype" not in df.columns:
|
||||
df["epc_archetype"] = None
|
||||
df["built_form"] = np.where(
|
||||
df["built_form"] == "unknown", df["epc_archetype"], df["built_form"]
|
||||
)
|
||||
property_type_map = {
|
||||
"house": "House",
|
||||
"flat": "Flat",
|
||||
"maisonette": "Maisonette",
|
||||
"bungalow": "Bungalow",
|
||||
"block house": "House",
|
||||
"coach house": "House",
|
||||
"bedsit": "Flat",
|
||||
}
|
||||
built_form_map = {
|
||||
"mid-terrace": "Mid-Terrace",
|
||||
"end-terrace": "End-Terrace",
|
||||
"semi-detached": "Semi-Detached",
|
||||
"detached": "Detached",
|
||||
"enclosed end-terrace": "Enclosed End-Terrace",
|
||||
"enclosed mid-terrace": "Enclosed Mid-Terrace",
|
||||
}
|
||||
df["property_type"] = df["property_type"].map(property_type_map).fillna(df["property_type"])
|
||||
df["built_form"] = df["built_form"].map(built_form_map).fillna(df["built_form"])
|
||||
self.plan_input = df.to_dict("records")
|
||||
|
||||
def _process_ara_property_list(self):
|
||||
df = self.plan_input
|
||||
self.plan_input = df.to_dict("records")
|
||||
|
||||
def to_addresses(self):
|
||||
return Addresses.from_plan_input(self.plan_input, self.body)
|
||||
|
|
@ -130,7 +130,7 @@ class PlanTriggerRequest(BaseModel):
|
|||
# Add in optional fields which describe the format of the asset list being used
|
||||
|
||||
file_type: Optional[Literal["csv", "xlsx"]] = None
|
||||
file_format: Optional[Literal["domna_asset_list"]] = None
|
||||
file_format: Optional[Literal["domna_asset_list", "ara_property_list"]] = None
|
||||
sheet_name: Optional[str] = None
|
||||
sheet_count: Optional[int] = None
|
||||
# If one of index_start or index_end is set, the other must be set too
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ def extract_property_request_data(
|
|||
x
|
||||
for x in patches
|
||||
if (x["address"] == address.address)
|
||||
and (x["postcode"] == address.postcode)
|
||||
and (x["postcode"] == address.postcode)
|
||||
),
|
||||
{},
|
||||
)
|
||||
|
|
@ -92,7 +92,7 @@ def extract_property_request_data(
|
|||
x
|
||||
for x in non_invasive_recommendations
|
||||
if (x["address"] == address.address)
|
||||
and (x["postcode"] == address.postcode)
|
||||
and (x["postcode"] == address.postcode)
|
||||
),
|
||||
{},
|
||||
)
|
||||
|
|
@ -134,7 +134,7 @@ def extract_property_request_data(
|
|||
float(x["valuation"])
|
||||
for x in valuation_data
|
||||
if (x["address"] == address.address)
|
||||
and (x["postcode"] == address.postcode)
|
||||
and (x["postcode"] == address.postcode)
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
|
@ -241,7 +241,7 @@ def parse_eco_packages(
|
|||
return measures, mapped["target_sap"], mapped["plan_type"], already_installed
|
||||
|
||||
|
||||
def build_cloudwatch_log_url(start_ms: int) -> str:
|
||||
def build_cloudwatch_log_url(start_ms: Optional[int]) -> str:
|
||||
"""
|
||||
Build a CloudWatch Logs URL for the current Lambda invocation,
|
||||
including timestamp window from start_ms to end_ms (epoch ms).
|
||||
|
|
@ -271,7 +271,7 @@ def build_cloudwatch_log_url(start_ms: int) -> str:
|
|||
def handle_error(
|
||||
msg: str,
|
||||
exception: Exception,
|
||||
subtask_id: str,
|
||||
subtask_id: Optional[str],
|
||||
status_code: int = 500,
|
||||
start_ms: Optional[int] = None,
|
||||
):
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import string
|
|||
import secrets
|
||||
import logging
|
||||
from io import BytesIO
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def setup_logger(log_file=None, level=logging.INFO, overwrite_handler=False):
|
||||
|
|
@ -73,7 +74,7 @@ def sap_to_epc(sap_points: int | float):
|
|||
return "G"
|
||||
|
||||
|
||||
def epc_to_sap_lower_bound(epc: str):
|
||||
def epc_to_sap_lower_bound(epc: Optional[str]):
|
||||
"""
|
||||
Given an EPC rating, returns the lower bound SAP score required
|
||||
to hit that EPC rating
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
version: "3.9"
|
||||
|
||||
services:
|
||||
categorisation-lambda:
|
||||
build:
|
||||
context: ../
|
||||
dockerfile: backend/categorisation/handler/Dockerfile
|
||||
ports:
|
||||
- "9000:8080"
|
||||
env_file:
|
||||
- ../.env
|
||||
|
|
@ -2,18 +2,19 @@ import time
|
|||
import json
|
||||
from copy import deepcopy
|
||||
from datetime import datetime
|
||||
import pandas as pd
|
||||
from uuid import UUID
|
||||
from typing import List
|
||||
|
||||
from tqdm import tqdm
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from uuid import UUID
|
||||
from sqlalchemy.exc import IntegrityError, OperationalError
|
||||
from starlette.responses import Response
|
||||
|
||||
from backend.SearchEpc import SearchEpc
|
||||
|
||||
from etl.epc.Record import EPCRecord
|
||||
from sqlalchemy.exc import IntegrityError, OperationalError
|
||||
from starlette.responses import Response
|
||||
from backend.app.BatterySapScorer import BatterySAPScorer
|
||||
from etl.epc.PredictionMatrix import PredictionMatrix
|
||||
|
||||
from backend.app.config import get_settings, get_prediction_buckets
|
||||
from backend.app.db.connection import db_session, db_read_session
|
||||
|
|
@ -22,7 +23,7 @@ from backend.app.db.functions.tasks.Tasks import SubTaskInterface
|
|||
|
||||
from backend.app.plan.schemas import PlanTriggerRequest
|
||||
from backend.app.plan.utils import (
|
||||
get_cleaned, patch_epc, extract_property_request_data, parse_eco_packages, handle_error, build_cloudwatch_log_url
|
||||
get_cleaned, patch_epc, extract_property_request_data, handle_error, build_cloudwatch_log_url
|
||||
)
|
||||
from backend.app.utils import sap_to_epc
|
||||
import backend.app.assumptions as assumptions
|
||||
|
|
@ -44,7 +45,9 @@ from etl.spatial.OpenUprnClient import OpenUprnClient
|
|||
from etl.find_my_epc.RetrieveFindMyEpc import RetrieveFindMyEpc
|
||||
|
||||
from utils.logger import setup_logger
|
||||
from utils.s3 import read_dataframe_from_s3_parquet, read_csv_from_s3, read_excel_from_s3
|
||||
from utils.s3 import read_dataframe_from_s3_parquet, read_csv_from_s3
|
||||
|
||||
from backend.app.plan.plan_input_processor import PlanInputProcessor
|
||||
|
||||
logger = setup_logger()
|
||||
|
||||
|
|
@ -121,20 +124,31 @@ def extract_portfolio_aggregation_data(
|
|||
cost = sum([r["total"] for r in default_recommendations])
|
||||
sap_point_improvement = sum([r["sap_points"] for r in default_recommendations])
|
||||
|
||||
if not pd.isnull(property_value_increase_ranges[p.id]["current_value"]):
|
||||
# Fix ambiguous Series/DataFrame truth value for current_value
|
||||
current_value = property_value_increase_ranges[p.id]["current_value"]
|
||||
if isinstance(current_value, (pd.Series, pd.DataFrame)):
|
||||
# Reduce to scalar
|
||||
is_null = bool(
|
||||
current_value.isnull().all().item() if
|
||||
hasattr(current_value.isnull().all(), 'item') else current_value.isnull().all().all()
|
||||
)
|
||||
else:
|
||||
is_null = bool(pd.isnull(current_value))
|
||||
|
||||
if (not is_null) and (current_value is not None):
|
||||
lower_bound_valuation_uplift = (
|
||||
property_value_increase_ranges[p.id]["lower_bound_increased_value"] -
|
||||
property_value_increase_ranges[p.id]["current_value"]
|
||||
current_value
|
||||
)
|
||||
upper_bound_valuation_uplift = (
|
||||
property_value_increase_ranges[p.id]["upper_bound_increased_value"] -
|
||||
property_value_increase_ranges[p.id]["current_value"]
|
||||
current_value
|
||||
)
|
||||
else:
|
||||
lower_bound_valuation_uplift, upper_bound_valuation_uplift = 0, 0
|
||||
|
||||
agg_data.append({
|
||||
"pre_retrofit_epc": p.data["current-energy-rating"],
|
||||
"pre_retrofit_epc": p.epc_record.current_energy_rating,
|
||||
"post_retrofit_epc": new_epc_bands[p.id],
|
||||
"pre_retrofit_co2": pre_retrofit_co2,
|
||||
"post_retrofit_co2": post_retrofit_co2,
|
||||
|
|
@ -285,7 +299,7 @@ def create_epc_records(epc_searcher: SearchEpc, energy_assessment: dict):
|
|||
"old_data": epc_searcher.older_epcs.copy()
|
||||
}, energy_assessment_is_newer
|
||||
|
||||
# In this case, our EPC is older than the newest publically avaible one, but is not contained in
|
||||
# In this case, our EPC is older than the newest publically availe one, but is not contained in
|
||||
# the historicals, so it can't have been lodged, so we include it in the old data
|
||||
return {
|
||||
'original_epc': newest_epc,
|
||||
|
|
@ -363,14 +377,13 @@ def get_funding_data():
|
|||
return project_scores_matrix, partial_project_scores_matrix, whlg_eligible_postcodes
|
||||
|
||||
|
||||
def check_duplicate_uprns(plan_input):
|
||||
def check_duplicate_uprns(input_uprns: List[int]):
|
||||
"""
|
||||
Simple function to check if the input data contains duplicated UPRNS.
|
||||
If there are duplicates, an exception will be rasied
|
||||
:return:
|
||||
"""
|
||||
# Check for duplicate UPRNS
|
||||
input_uprns = [x.get("uprn") for x in plan_input if "uprn" in x and x.get("uprn")]
|
||||
|
||||
if input_uprns:
|
||||
# Check for dupes
|
||||
|
|
@ -413,104 +426,19 @@ def check_duplicate_property_ids(input_properties):
|
|||
return True
|
||||
|
||||
|
||||
def averages_cleaning(prepared_epc: EPCRecord, cleaning_data: pd.DataFrame):
|
||||
"""
|
||||
Placeholder cleaning function to handle edge cases where we have missing data for
|
||||
number of habitable rooms, number of heated rooms and floor height. We take the median
|
||||
This need was born out of the Peabody project
|
||||
:param prepared_epc:
|
||||
:param cleaning_data:
|
||||
:return:
|
||||
"""
|
||||
|
||||
variables_to_clean = [
|
||||
"number_habitable_rooms",
|
||||
"number_heated_rooms",
|
||||
"floor_height",
|
||||
]
|
||||
|
||||
if not any([pd.isnull(prepared_epc.prepared_epc[k]) for k in variables_to_clean]):
|
||||
# Nothing to do
|
||||
return prepared_epc
|
||||
|
||||
# Clean with cleaning_data
|
||||
clean_with = cleaning_data[
|
||||
(cleaning_data["property_type"] == prepared_epc.prepared_epc["property_type"]) &
|
||||
(cleaning_data["property_type"] == prepared_epc.prepared_epc["property_type"])
|
||||
]
|
||||
if prepared_epc.prepared_epc["local_authority"] in clean_with["local_authority"].values:
|
||||
clean_with = clean_with[
|
||||
clean_with["local_authority"] == prepared_epc.prepared_epc["local_authority"]
|
||||
]
|
||||
|
||||
floor_area_clean_with = clean_with[
|
||||
(clean_with["total_floor_area"] <= prepared_epc.prepared_epc["total_floor_area"] * 1.1) &
|
||||
(clean_with["total_floor_area"] >= prepared_epc.prepared_epc["total_floor_area"] * 0.9)
|
||||
]
|
||||
|
||||
if not floor_area_clean_with.empty:
|
||||
clean_with = floor_area_clean_with
|
||||
|
||||
clean_n_habitable_rooms = int(round(clean_with["number_habitable_rooms"].median()))
|
||||
clean_n_heated_rooms = int(round(clean_with["number_heated_rooms"].median()))
|
||||
if clean_n_heated_rooms > clean_n_habitable_rooms:
|
||||
clean_n_heated_rooms = clean_n_habitable_rooms
|
||||
|
||||
clean_floor_height = clean_with["floor_height"].median()
|
||||
|
||||
# We now fill
|
||||
if not pd.isnull(clean_n_habitable_rooms) and pd.isnull(
|
||||
prepared_epc.prepared_epc["number_habitable_rooms"]):
|
||||
prepared_epc.prepared_epc["number_habitable_rooms"] = clean_n_habitable_rooms
|
||||
prepared_epc.number_habitable_rooms = clean_n_habitable_rooms
|
||||
|
||||
if not pd.isnull(clean_n_heated_rooms) and pd.isnull(
|
||||
prepared_epc.prepared_epc["number_heated_rooms"]):
|
||||
prepared_epc.prepared_epc["number_heated_rooms"] = clean_n_heated_rooms
|
||||
prepared_epc.number_heated_rooms = clean_n_heated_rooms
|
||||
|
||||
if not pd.isnull(clean_floor_height) and pd.isnull(
|
||||
prepared_epc.prepared_epc["floor_height"]):
|
||||
prepared_epc.prepared_epc["floor_height"] = clean_floor_height
|
||||
prepared_epc.floor_height = clean_floor_height
|
||||
|
||||
# if pd.isnull(prepared_epc.lighting_cost_current):
|
||||
# # This is a basic assumption as an average
|
||||
# prepared_epc.prepared_epc["lighting_cost_current"] = assumptions.AVERAGE_LIGHTING_COST
|
||||
# prepared_epc.lighting_cost_current = assumptions.AVERAGE_LIGHTING_COST
|
||||
|
||||
# if pd.isnull(prepared_epc.heating_cost_current):
|
||||
# # This is a basic assumption as an average
|
||||
# appliance_cost = AnnualBillSavings.estimate_appliances_energy_use(
|
||||
# total_floor_area=prepared_epc.total_floor_area
|
||||
# ) * AnnualBillSavings.ELECTRICITY_PRICE_CAP
|
||||
# heating_cleaned_value = assumptions.AVERAGE_HEATING_AND_APPLIANCE_COST - appliance_cost
|
||||
# prepared_epc.prepared_epc["heating_cost_current"] = heating_cleaned_value
|
||||
# prepared_epc.heating_cost_current = heating_cleaned_value
|
||||
#
|
||||
# if pd.isnull(prepared_epc.hot_water_cost_current):
|
||||
# # This is a basic assumption as an average
|
||||
# prepared_epc.prepared_epc["hot_water_cost_current"] = assumptions.AVERAGE_HOT_WATER_COST
|
||||
# prepared_epc.hot_water_cost_current = assumptions.AVERAGE_HOT_WATER_COST
|
||||
#
|
||||
# if pd.isnull(prepared_epc.energy_consumption_potential):
|
||||
# # Set to current
|
||||
# prepared_epc.prepared_epc["energy_consumption_potential"] = prepared_epc.energy_consumption_current
|
||||
# prepared_epc.energy_consumption_potential = prepared_epc.energy_consumption_current
|
||||
|
||||
return prepared_epc
|
||||
|
||||
|
||||
def extract_address_data(config, body):
|
||||
"""
|
||||
Simple helper to grab address data from the config
|
||||
:return:
|
||||
"""
|
||||
uprn = config.get("uprn", None)
|
||||
if pd.isnull(uprn):
|
||||
try:
|
||||
uprn = config.get("uprn", None)
|
||||
if uprn is not None and pd.notnull(uprn):
|
||||
uprn = int(float(uprn))
|
||||
else:
|
||||
uprn = None
|
||||
except Exception:
|
||||
uprn = None
|
||||
if uprn:
|
||||
uprn = int(float(uprn))
|
||||
|
||||
address1 = config.get("address", None)
|
||||
# Handle domna address list format
|
||||
|
|
@ -542,131 +470,32 @@ def keep_max_sap_per_measure_type(items):
|
|||
|
||||
|
||||
async def model_engine(body: PlanTriggerRequest):
|
||||
logger.info("Model Engine triggered with body: %s", json.loads(body.model_dump_json()))
|
||||
|
||||
created_at = datetime.now().isoformat()
|
||||
start_ms = int(time.time() * 1000)
|
||||
logger.info("Model Engine triggered with body: %s", json.loads(body.model_dump_json()))
|
||||
if body.subtask_id:
|
||||
SubTaskInterface().update_subtask_status(
|
||||
subtask_id=UUID(body.subtask_id), status="in progress", cloud_logs_url=build_cloudwatch_log_url(start_ms)
|
||||
)
|
||||
|
||||
try:
|
||||
logger.info("Getting the inputs")
|
||||
|
||||
if body.file_type == "xlsx":
|
||||
logger.info("Getting the plan input")
|
||||
plan_input = read_excel_from_s3(
|
||||
bucket_name=get_settings().PLAN_TRIGGER_BUCKET,
|
||||
file_key=body.trigger_file_path,
|
||||
sheet_name=body.sheet_name,
|
||||
header_row=0,
|
||||
)
|
||||
logger.info("Got the plan input from excel")
|
||||
|
||||
# We now handle the case where the input data is a Domna standardised assset list
|
||||
if body.file_format == "domna_asset_list":
|
||||
# We rename the columns to match the expected format
|
||||
plan_input = plan_input.rename(
|
||||
columns={"domna_address_1": "address", "domna_postcode": "postcode", "epc_os_uprn": "uprn"}
|
||||
)
|
||||
# Where the EPC has been estimated, that is because a UPRN wasn't avaialble and so we remove UPRN
|
||||
# This will be reflexted
|
||||
if "estimated" not in plan_input.columns:
|
||||
plan_input["estimated"] = False
|
||||
|
||||
plan_input["uprn"] = np.where(
|
||||
plan_input["estimated"].isin([1, True]) & (
|
||||
(plan_input["uprn"] < 0) | pd.isnull(plan_input["uprn"])
|
||||
), None, plan_input["uprn"]
|
||||
)
|
||||
# We handle the landlord property type and built form
|
||||
plan_input["property_type"] = plan_input["landlord_property_type"].copy()
|
||||
if "landlord_built_form" in plan_input.columns:
|
||||
plan_input["built_form"] = plan_input["landlord_built_form"].copy()
|
||||
else:
|
||||
plan_input["built_form"] = None
|
||||
|
||||
if "epc_property_type" not in plan_input.columns:
|
||||
plan_input["epc_property_type"] = None
|
||||
|
||||
plan_input["property_type"] = np.where(
|
||||
plan_input["property_type"] == "unknown",
|
||||
plan_input["epc_property_type"],
|
||||
plan_input["property_type"]
|
||||
)
|
||||
|
||||
if "epc_archetype" not in plan_input.columns:
|
||||
plan_input["epc_archetype"] = None
|
||||
|
||||
plan_input["built_form"] = np.where(
|
||||
plan_input["built_form"] == "unknown", plan_input["epc_archetype"], plan_input["built_form"]
|
||||
)
|
||||
property_type_map = {
|
||||
"house": "House",
|
||||
"flat": "Flat",
|
||||
"maisonette": "Maisonette",
|
||||
"bungalow": "Bungalow",
|
||||
"block house": "House",
|
||||
"coach house": "House",
|
||||
"bedsit": "Flat",
|
||||
}
|
||||
|
||||
built_form_map = {
|
||||
"mid-terrace": "Mid-Terrace",
|
||||
"end-terrace": "End-Terrace",
|
||||
"semi-detached": "Semi-Detached",
|
||||
"detached": "Detached",
|
||||
"enclosed end-terrace": "Enclosed End-Terrace",
|
||||
"enclosed mid-terrace": "Enclosed Mid-Terrace",
|
||||
}
|
||||
# We remap the values to match the EPC expected formats
|
||||
|
||||
# This syntax will actually retain any original values, if they don't get mapped
|
||||
plan_input["property_type"] = (
|
||||
plan_input["property_type"]
|
||||
.map(property_type_map)
|
||||
.fillna(plan_input["property_type"])
|
||||
)
|
||||
|
||||
plan_input["built_form"] = (
|
||||
plan_input["built_form"]
|
||||
.map(built_form_map)
|
||||
.fillna(plan_input["built_form"])
|
||||
)
|
||||
|
||||
plan_input = plan_input.to_dict("records")
|
||||
|
||||
else:
|
||||
raise ValueError("Other formats not yet supported")
|
||||
|
||||
else:
|
||||
logger.info("Getting the plan input from csv")
|
||||
plan_input = read_csv_from_s3(
|
||||
bucket_name=get_settings().PLAN_TRIGGER_BUCKET, filepath=body.trigger_file_path
|
||||
)
|
||||
logger.info("Got the plan input from csv")
|
||||
|
||||
# We then slide it on the indexes if they are provided
|
||||
if body.index_start is not None and body.index_end is not None:
|
||||
plan_input = plan_input[body.index_start:body.index_end]
|
||||
# Use PlanInputProcessor for all plan input processing
|
||||
plan_input_processor = PlanInputProcessor(body)
|
||||
addresses = plan_input_processor.process()
|
||||
valuation_data = plan_input_processor.valuation_data
|
||||
|
||||
# Confirm no duplicate UPRNS
|
||||
check_duplicate_uprns(plan_input)
|
||||
check_duplicate_uprns([a.uprn for a in addresses])
|
||||
|
||||
# If we have patches or overrides, we should read them in here
|
||||
patches, already_installed, non_invasive_recommendations, valuation_data = get_request_property_data(body)
|
||||
|
||||
if body.file_type == "xlsx" and body.file_format == "domna_asset_list":
|
||||
# We check if we have valution data
|
||||
if not valuation_data and body.valuation_file_path in [None, ""]:
|
||||
# We check plan_input
|
||||
if "domna_valuation" in plan_input[0]:
|
||||
valuation_data = [{"uprn": x["uprn"], "valuation": x["domna_valuation"]} for x in plan_input]
|
||||
patches, already_installed, non_invasive_recommendations, _ = get_request_property_data(body)
|
||||
|
||||
logger.info("Getting cleaning_data")
|
||||
cleaning_data = read_dataframe_from_s3_parquet(
|
||||
bucket_name=get_settings().DATA_BUCKET, file_key="sap_change_model/cleaning_dataset.parquet",
|
||||
)
|
||||
|
||||
# Prepare input data
|
||||
addresses = Addresses.from_plan_input(plan_input, body)
|
||||
logger.info("Checking database for existing properties")
|
||||
|
||||
uprns = addresses.get_uprns()
|
||||
|
|
@ -725,8 +554,8 @@ async def model_engine(body: PlanTriggerRequest):
|
|||
|
||||
logger.info("Processing each property for model input preparation")
|
||||
input_properties, inspections_map, eco_packages, epc_upserts = [], {}, {}, []
|
||||
for addr, config in tqdm(
|
||||
zip(addresses, plan_input),
|
||||
for addr in tqdm(
|
||||
addresses,
|
||||
total=len(addresses),
|
||||
desc="Processing properties",
|
||||
):
|
||||
|
|
@ -747,17 +576,17 @@ async def model_engine(body: PlanTriggerRequest):
|
|||
property_already_installed = list(already_installed_by_uprn[addr.uprn])
|
||||
|
||||
epc_searcher = SearchEpc(
|
||||
address1=addr.address1,
|
||||
address1=addr.address_1,
|
||||
postcode=addr.postcode,
|
||||
uprn=addr.uprn,
|
||||
auth_token=get_settings().EPC_AUTH_TOKEN,
|
||||
os_api_key="",
|
||||
full_address=addr.full_address,
|
||||
heating_system=addr.heating_system,
|
||||
heating_system=addr.landlord_heating_system,
|
||||
associated_uprns=associated_uprns
|
||||
)
|
||||
epc_searcher.ordnance_survey_client.built_form = addr.built_form
|
||||
epc_searcher.ordnance_survey_client.property_type = addr.property_type
|
||||
epc_searcher.ordnance_survey_client.built_form = addr.landlord_built_form
|
||||
epc_searcher.ordnance_survey_client.property_type = addr.landlord_property_type
|
||||
# For the moment, our OS API access is unavailable, so we skip and interpolate
|
||||
|
||||
epc_searcher.find_property(skip_os=True, api_data=epc_api_data, overwrite_sap05=True)
|
||||
|
|
@ -781,6 +610,8 @@ async def model_engine(body: PlanTriggerRequest):
|
|||
# Otherwise, we use the newest EPC
|
||||
# energy_assessment_is_newer will tell us if the energy assessment is newer than the newest EPC that
|
||||
# has been publically lodged
|
||||
if energy_assessment is None:
|
||||
energy_assessment = {}
|
||||
epc_records, energy_assessment["energy_assessment_is_newer"] = create_epc_records(
|
||||
epc_searcher, energy_assessment
|
||||
)
|
||||
|
|
@ -804,7 +635,7 @@ async def model_engine(body: PlanTriggerRequest):
|
|||
epc_page=epc_page,
|
||||
rrn=rrn,
|
||||
cleaned_address=epc_searcher.address_clean,
|
||||
config_address=addr.address,
|
||||
config_address=addr.address_1,
|
||||
address_postal_town=epc_searcher.address_postal_town
|
||||
)
|
||||
)
|
||||
|
|
@ -813,18 +644,6 @@ async def model_engine(body: PlanTriggerRequest):
|
|||
|
||||
prepared_epc = EPCRecord(epc_records=epc_records, run_mode="newdata", cleaning_data=cleaning_data)
|
||||
|
||||
# TODO: This is a temp function to handle a specific edge case with Peabody. We should
|
||||
# factor this into EPCRecord as part of the cleaning however we need some more testing
|
||||
prepared_epc = averages_cleaning(prepared_epc, cleaning_data)
|
||||
|
||||
# If we have an ECO project, we parse the cavity/solar reasons
|
||||
eco_packages[property_id] = parse_eco_packages(addr, prepared_epc)
|
||||
|
||||
# Final step - extract inspections data, if we have it - we inject into property for usage
|
||||
property_inspections = db_funcs.inspections_functions.extract_inspection_data(config)
|
||||
if property_inspections:
|
||||
inspections_map[property_id] = property_inspections
|
||||
|
||||
input_properties.append(
|
||||
Property(
|
||||
id=property_id,
|
||||
|
|
@ -833,13 +652,12 @@ async def model_engine(body: PlanTriggerRequest):
|
|||
address=epc_searcher.address_clean,
|
||||
postcode=epc_searcher.postcode_clean,
|
||||
epc_record=prepared_epc,
|
||||
already_installed=property_already_installed + eco_packages.get(property_id)[3],
|
||||
already_installed=property_already_installed,
|
||||
find_my_epc_components=find_my_epc_components,
|
||||
property_valuation=req_data.valuation,
|
||||
non_invasive_recommendations=property_non_invasive_recommendations,
|
||||
energy_assessment=energy_assessment,
|
||||
inspections=inspections_map.get(property_id),
|
||||
**Property.extract_kwargs(config), # TODO: Depraecate this
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -881,16 +699,79 @@ async def model_engine(body: PlanTriggerRequest):
|
|||
prediction_buckets=get_prediction_buckets(),
|
||||
max_retries=1
|
||||
)
|
||||
await model_api.async_warm_up_lambdas(
|
||||
model_prefies=model_api.KWH_MODEL_PREFIXES + model_api.MODEL_PREFIXES
|
||||
)
|
||||
await model_api.async_warm_up_lambdas(model_prefies=model_api.models_for_warm_up)
|
||||
|
||||
# The materials data could be cached or local so we don't need to make
|
||||
# consistent requests to the backend for the same data
|
||||
logger.info("Reading in materials and cleaned datasets")
|
||||
cleaned = get_cleaned()
|
||||
|
||||
with db_read_session() as session:
|
||||
materials = db_funcs.materials_functions.get_materials(session)
|
||||
cleaned = get_cleaned()
|
||||
|
||||
logger.info("Preparing rebaselining")
|
||||
rebaselining_scoring_data = []
|
||||
for p in tqdm(input_properties):
|
||||
# 1) EPC expired 2) Missing EPC 3) Different information from landlord vs EPC
|
||||
needs_rebaselining = p.epc_is_expired | p.epc_is_estimated | (len(p.epc_record.landlord_differences) > 0)
|
||||
|
||||
if needs_rebaselining:
|
||||
p.create_base_difference_epc_record(cleaned_lookup=cleaned)
|
||||
scoring_data = p.base_difference_record.df.copy()
|
||||
rebaselining_scoring_data.append(scoring_data)
|
||||
|
||||
rebaselining_scoring_data = (
|
||||
pd.concat(rebaselining_scoring_data) if len(rebaselining_scoring_data) else pd.DataFrame([])
|
||||
)
|
||||
predictions_by_model_and_uprn = {}
|
||||
if not rebaselining_scoring_data.empty:
|
||||
logger.info(f"{rebaselining_scoring_data.shape[0]} properties require re-baselineing")
|
||||
|
||||
# Trigger re-scoring
|
||||
rebaselining_scoring_data["is_post_sap10_starting"] = True
|
||||
|
||||
rebaselining_response = model_api.predict_all(
|
||||
df=rebaselining_scoring_data,
|
||||
bucket=get_settings().DATA_BUCKET,
|
||||
model_prefixes=model_api.BASELINE_MODEL_PREFIXES,
|
||||
extract_ids=False,
|
||||
extract_uprn=True
|
||||
)
|
||||
|
||||
# Update EPC records with new model predictions
|
||||
input_properties_by_uprn = {int(p.uprn): p for p in input_properties if p.uprn is not None}
|
||||
|
||||
# Pre-index predictions for each model by UPRN
|
||||
model_names = [
|
||||
"retrofit_sap_baseline_predictions",
|
||||
"retrofit_carbon_baseline_predictions",
|
||||
"retrofit_heat_baseline_predictions",
|
||||
]
|
||||
for model in model_names:
|
||||
df = rebaselining_response[model]
|
||||
predictions_by_model_and_uprn[model] = dict(zip(df["uprn"].astype(int), df["predictions"]))
|
||||
|
||||
for uprn_int in rebaselining_scoring_data["uprn"].unique().astype(int):
|
||||
try:
|
||||
property_instance = input_properties_by_uprn[uprn_int]
|
||||
if property_instance is None:
|
||||
logger.warning(f"No property found for UPRN {uprn_int} during rebaselining update.")
|
||||
continue
|
||||
# Gather predictions for this UPRN
|
||||
try:
|
||||
new_sap = predictions_by_model_and_uprn["retrofit_sap_baseline_predictions"][uprn_int]
|
||||
new_carbon = predictions_by_model_and_uprn["retrofit_carbon_baseline_predictions"][uprn_int]
|
||||
new_heat_demand = predictions_by_model_and_uprn["retrofit_heat_baseline_predictions"][uprn_int]
|
||||
except KeyError as e:
|
||||
logger.warning(f"Missing prediction for UPRN {uprn_int}: {e}")
|
||||
continue
|
||||
# Update EPC record
|
||||
property_instance.epc_record.insert_new_performance_values(
|
||||
new_sap=new_sap,
|
||||
new_epc=sap_to_epc(new_sap),
|
||||
new_carbon=new_carbon,
|
||||
new_heat_demand=new_heat_demand,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating EPC record for UPRN {uprn_int}: {e}")
|
||||
|
||||
kwh_client = KwhData(bucket=get_settings().DATA_BUCKET, read_consumption_data=True)
|
||||
|
||||
|
|
@ -967,6 +848,12 @@ async def model_engine(body: PlanTriggerRequest):
|
|||
if not property_recommendations:
|
||||
continue
|
||||
|
||||
# Perform a check for properties (temp) where we've remodelled
|
||||
if p.epc_record.has_been_remodelled:
|
||||
for x in property_recommendations:
|
||||
if any(y.get("survey") for y in x):
|
||||
raise ValueError("Should not have survey true for remodelled properties")
|
||||
|
||||
recommendations[p.id] = property_recommendations
|
||||
representative_recommendations[p.id] = property_representative_recommendations
|
||||
|
||||
|
|
@ -986,7 +873,6 @@ async def model_engine(body: PlanTriggerRequest):
|
|||
"carbon_ending"
|
||||
]
|
||||
)
|
||||
# TODO: Temp putting this here
|
||||
recommendations_scoring_data["is_post_sap10_ending"] = True
|
||||
|
||||
all_predictions = await model_api.async_paginated_predictions(
|
||||
|
|
@ -1042,6 +928,8 @@ async def model_engine(body: PlanTriggerRequest):
|
|||
)
|
||||
p.current_energy_bill = property_current_energy_bill
|
||||
|
||||
# Create matrix of all predictions for debug: - any rebaselining and measure level predictions
|
||||
|
||||
# Insert the predictions into the recommendations and run the optimiser
|
||||
logger.info("Optimising measures")
|
||||
for p in input_properties:
|
||||
|
|
@ -1095,8 +983,7 @@ async def model_engine(body: PlanTriggerRequest):
|
|||
property_required_measures, recommendations, p, needs_ventilation
|
||||
)
|
||||
gain = optimiser_functions.calculate_gain(
|
||||
body=body, p=p, fixed_gain=fixed_gain, eco_packages=eco_packages,
|
||||
already_installed_gain=already_installed_sap
|
||||
body=body, p=p, fixed_gain=fixed_gain, already_installed_gain=already_installed_sap
|
||||
)
|
||||
|
||||
# We insert the innovation uplift
|
||||
|
|
@ -1158,21 +1045,22 @@ async def model_engine(body: PlanTriggerRequest):
|
|||
# We optimise and then we determine eligibility for funding, based on the measures selected
|
||||
optimiser = (
|
||||
GainOptimiser(
|
||||
input_measures, max_cost=body.budget, max_gain=gain, allow_slack=False
|
||||
) if body.budget else CostOptimiser(input_measures, min_gain=gain)
|
||||
input_measures, max_cost=body.budget, max_gain=float(gain) if gain is not None else 0,
|
||||
allow_slack=False
|
||||
) if body.budget else CostOptimiser(input_measures, min_gain=float(gain) if gain is not None else 0)
|
||||
)
|
||||
optimiser.setup()
|
||||
optimiser.solve()
|
||||
solution = optimiser.solution
|
||||
gain = optimiser.solution_gain
|
||||
post_sap = int(p.data["current-energy-efficiency"]) + gain
|
||||
post_sap = p.epc_record.current_energy_efficiency + gain
|
||||
|
||||
pv_size = next(
|
||||
(m["array_size"] for m in solution if m["type"] == "solar_pv"), 0
|
||||
)
|
||||
battery_sap_score = BatterySAPScorer.score(starting_sap=post_sap, pv_size=pv_size)
|
||||
|
||||
# We add the defauly already installed measures to the solution
|
||||
# We add the defaulty already installed measures to the solution
|
||||
selected = {r["id"] for r in solution + default_already_installed}
|
||||
|
||||
if property_required_measures:
|
||||
|
|
@ -1262,7 +1150,7 @@ async def model_engine(body: PlanTriggerRequest):
|
|||
|
||||
# This will include everything, including already installed
|
||||
total_sap_points = sum([r["sap_points"] for r in default_recommendations])
|
||||
new_sap_points = float(p.data["current-energy-efficiency"]) + total_sap_points
|
||||
new_sap_points = p.epc_record.current_energy_efficiency + total_sap_points
|
||||
new_epc = sap_to_epc(new_sap_points)
|
||||
# Already installed measures do not have a cost but we remove anyway
|
||||
total_cost = sum([r["total"] for r in default_recommendations if not r["already_installed"]])
|
||||
|
|
@ -1378,12 +1266,40 @@ async def model_engine(body: PlanTriggerRequest):
|
|||
except Exception as e: # General exception handling
|
||||
return handle_error("An unexpected error occurred.", e, body.subtask_id, 500, start_ms)
|
||||
|
||||
cloud_logs_url = build_cloudwatch_log_url(start_ms)
|
||||
# Mark the subtask as successful
|
||||
SubTaskInterface().update_subtask_status(
|
||||
subtask_id=UUID(body.subtask_id), status="complete", cloud_logs_url=cloud_logs_url
|
||||
)
|
||||
SubTaskInterface().update_subtask_status(subtask_id=UUID(body.subtask_id), status="complete")
|
||||
|
||||
logger.info("Model Engine completed successfully")
|
||||
|
||||
prediction_matrix = PredictionMatrix()
|
||||
|
||||
# --- Add rebaselining and measure-level predictions to PredictionMatrix ---
|
||||
for p in input_properties:
|
||||
# Add rebaselined predictions if available
|
||||
uprn = p.uprn
|
||||
if uprn is None:
|
||||
continue
|
||||
# Rebaselined SAP prediction
|
||||
rebaselined_sap = None
|
||||
if uprn in predictions_by_model_and_uprn.get("retrofit_sap_baseline_predictions", {}):
|
||||
rebaselined_sap = predictions_by_model_and_uprn["retrofit_sap_baseline_predictions"][uprn]
|
||||
# Add original EPC and landlord differences for comparison
|
||||
prediction_matrix.set_original_epc(
|
||||
uprn=uprn,
|
||||
original_epc=p.epc_record.original_epc,
|
||||
landlord_differences=p.epc_record.landlord_differences,
|
||||
lodgement_date=p.epc_record.lodgement_date,
|
||||
)
|
||||
prediction_matrix.set_rebaselined_prediction(uprn, rebaselined_sap)
|
||||
# Add measure-level predictions
|
||||
property_recommendations = recommendations.get(p.id, [])
|
||||
for rec in property_recommendations:
|
||||
prediction_matrix.add_recommendation(
|
||||
uprn=uprn,
|
||||
measure_id=rec.get("recommendation_id", rec.get("id", rec.get("type", "unknown"))),
|
||||
prediction=rec.get("sap_points"),
|
||||
metadata={k: v for k, v in rec.items() if k not in ("sap_points", "recommendation_id", "id")}
|
||||
)
|
||||
# --- End PredictionMatrix population ---
|
||||
|
||||
return Response(status_code=200)
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ def test_default_export_integration(db_session):
|
|||
properties = []
|
||||
|
||||
for row in properties_df.itertuples(index=False):
|
||||
row_dict = row._asdict()
|
||||
row_dict = {field: getattr(row, field) for field in row._fields}
|
||||
|
||||
row_dict["uprn"] = int(row_dict["uprn"]) if row_dict.get("uprn") else None
|
||||
row_dict["building_reference_number"] = (
|
||||
|
|
@ -106,7 +106,7 @@ def test_default_export_integration(db_session):
|
|||
epc_rows = []
|
||||
|
||||
for row in property_details_epc_df.itertuples(index=False):
|
||||
row_dict = row._asdict()
|
||||
row_dict = {field: getattr(row, field) for field in row._fields}
|
||||
|
||||
# Build only fields that exist on the model
|
||||
epc_data = {
|
||||
|
|
@ -133,7 +133,7 @@ def test_default_export_integration(db_session):
|
|||
plans = []
|
||||
|
||||
for row in plans_df.itertuples(index=False):
|
||||
row_dict = row._asdict()
|
||||
row_dict = {field: getattr(row, field) for field in row._fields}
|
||||
|
||||
if row_dict.get("post_epc_rating"):
|
||||
row_dict["post_epc_rating"] = Epc[
|
||||
|
|
@ -263,7 +263,7 @@ def test_default_export_integration(db_session):
|
|||
"Expected total SAP points increase to be 100.10000000000001, got {}".format(df["sap_points"].sum())
|
||||
)
|
||||
|
||||
assert df.shape == (10, 95), "Expected dataframe shape to be (10, 11), got {}".format(df.shape)
|
||||
assert df.shape == (10, 100), "Expected dataframe shape to be (10, 100), got {}".format(df.shape)
|
||||
|
||||
|
||||
def test_solar_with_battery_example(db_session):
|
||||
|
|
|
|||
|
|
@ -216,7 +216,7 @@ class PropertyValuation:
|
|||
cls.UPRN_VALUE_LOOKUP.get(property_instance.uprn)
|
||||
)
|
||||
|
||||
current_epc = property_instance.data["current-energy-rating"]
|
||||
current_epc = property_instance.epc_record.current_energy_rating
|
||||
|
||||
if not current_value:
|
||||
# In this case, we return a % improvement rather than an absolute
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import json
|
||||
import random
|
||||
import aiohttp
|
||||
import asyncio
|
||||
import pandas as pd
|
||||
from typing import List, Dict
|
||||
from tqdm import tqdm
|
||||
import requests
|
||||
from requests.exceptions import RequestException
|
||||
|
|
@ -23,12 +22,22 @@ class ModelApi:
|
|||
|
||||
KWH_MODEL_PREFIXES = ["heating_kwh_predictions", "hotwater_kwh_predictions"]
|
||||
|
||||
MODEL_URLS = {
|
||||
BASELINE_MODEL_PREFIXES = [
|
||||
"retrofit_sap_baseline_predictions",
|
||||
"retrofit_heat_baseline_predictions",
|
||||
"retrofit_carbon_baseline_predictions",
|
||||
]
|
||||
|
||||
MODEL_URLS: Dict[str, str] = {
|
||||
"sap_change_predictions": "sapmodel",
|
||||
"heat_demand_predictions": "heatmodel",
|
||||
"carbon_change_predictions": "carbonmodel",
|
||||
"hotwater_kwh_predictions": "hotwaterkwhmodel",
|
||||
"heating_kwh_predictions": "heatingkwhmodel",
|
||||
# Baseline prediction models
|
||||
"retrofit_sap_baseline_predictions": "sapbaselinemodel",
|
||||
"retrofit_heat_baseline_predictions": "heatbaselinemodel",
|
||||
"retrofit_carbon_baseline_predictions": "carbonbaselinemodel",
|
||||
}
|
||||
|
||||
def __init__(
|
||||
|
|
@ -147,7 +156,13 @@ class ModelApi:
|
|||
else:
|
||||
return None
|
||||
|
||||
def predict_all(self, df, bucket, model_prefixes=None, extract_ids=True) -> dict:
|
||||
def predict_all(
|
||||
self, df: pd.DataFrame,
|
||||
bucket: str,
|
||||
model_prefixes: List[str] | None = None,
|
||||
extract_ids: bool = True,
|
||||
extract_uprn: bool = False
|
||||
) -> dict:
|
||||
|
||||
"""
|
||||
For each model prefix, this method will upload the scoring data to s3 and then make a request to the
|
||||
|
|
@ -159,6 +174,8 @@ class ModelApi:
|
|||
:param model_prefixes: List of model prefixes to generate predictions for. If None, all model prefixes will be
|
||||
used
|
||||
:param extract_ids: Boolean to determine if the property_id and recommendation_id should be extracted from the
|
||||
scoring data
|
||||
:param extract_uprn: Boolean to determine if the uprn should be extracted from the scoring data
|
||||
id column
|
||||
:return:
|
||||
"""
|
||||
|
|
@ -196,6 +213,9 @@ class ModelApi:
|
|||
# Convert back to int
|
||||
predictions_df['phase'] = predictions_df['recommendation_id'].apply(self.extract_phase)
|
||||
|
||||
if extract_uprn and "uprn" in df.columns:
|
||||
predictions_df["uprn"] = df["uprn"].values
|
||||
|
||||
predictions[model_prefix] = predictions_df
|
||||
|
||||
return predictions
|
||||
|
|
@ -325,3 +345,7 @@ class ModelApi:
|
|||
)
|
||||
|
||||
return all_predictions
|
||||
|
||||
@property
|
||||
def models_for_warm_up(self):
|
||||
return self.KWH_MODEL_PREFIXES + self.MODEL_PREFIXES + self.BASELINE_MODEL_PREFIXES
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ class OnboarderBase:
|
|||
landlord_roof_construction: str = "landlord_roof_construction"
|
||||
landlord_floor_construction: str = "landlord_floor_construction"
|
||||
landlord_windows_type: str = "landlord_windows_type"
|
||||
landlord_heating_construction: str = "landlord_heating_construction"
|
||||
landlord_heating_system: str = "landlord_heating_system"
|
||||
landlord_fuel_type: str = "landlord_fuel_type"
|
||||
landlord_heating_controls: str = "landlord_heating_controls"
|
||||
landlord_hot_water_system: str = "landlord_hot_water_system"
|
||||
|
|
@ -53,7 +53,7 @@ class OnboarderBase:
|
|||
)
|
||||
else:
|
||||
self.data = read_from_s3(bucket_name=self.bucket_name, s3_file_name=self.input_file_name)
|
||||
|
||||
|
||||
def write(self):
|
||||
if self.data is None:
|
||||
raise ValueError("No data to write. Please run transform() before writing.")
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ parity_map = {
|
|||
"1996-2002": EpcConstructionAgeBand.from_1996_to_2002,
|
||||
"2003-2006": EpcConstructionAgeBand.from_2003_to_2006,
|
||||
"2007-2011": EpcConstructionAgeBand.from_2007_to_2011,
|
||||
"2012 onwards": EpcConstructionAgeBand.from_2012_onwards,
|
||||
# Newer age bands, under SAP10
|
||||
"2012 onwards": EpcConstructionAgeBand.from_2012_to_2022,
|
||||
"2012-2022": EpcConstructionAgeBand.from_2012_to_2022,
|
||||
"2023 onwards": EpcConstructionAgeBand.from_2023_onwards,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,23 @@
|
|||
from datatypes.epc.efficiency import EpcEfficiency
|
||||
from datatypes.epc.windows import EpcWindowDescriptions
|
||||
|
||||
glazing_map = {
|
||||
# (description, energy efficiency, multi_glaze_proportion, glazed_type, glazed_area
|
||||
# For SAP 10 assessments, The glazed type and glazed area are not populated in the EPC API data any more
|
||||
"Double 2002 or later": ("Fully double glazed", EpcEfficiency.AVERAGE, 1, None, None),
|
||||
"Double before 2002": ("Fully double glazed", EpcEfficiency.POOR, 1, None, None),
|
||||
"Double but age unknown": ("Fully double glazed", EpcEfficiency.POOR, 1, None, None),
|
||||
"Single": ("Single glazed", EpcEfficiency.VERY_POOR, 0, None, None),
|
||||
"Double 2002 or later": (EpcWindowDescriptions.fully_double_glazed, EpcEfficiency.AVERAGE, 100, None, None),
|
||||
"Double before 2002": (EpcWindowDescriptions.fully_double_glazed, EpcEfficiency.POOR, 100, None, None),
|
||||
"Double but age unknown": (EpcWindowDescriptions.fully_double_glazed, EpcEfficiency.POOR, 100, None, None),
|
||||
"Single": (EpcWindowDescriptions.single_glazed, EpcEfficiency.VERY_POOR, 0, None, None),
|
||||
# For triple glazing, with age unknown, the performance is only average, whereas if it's a post 2022
|
||||
# installation, it's classed as high performance glazing with good efficiency. We'll need to be considerate as to
|
||||
# how we make updates to the windows data.
|
||||
# Triple known data is high performance glazing with Good efficiency (at least)
|
||||
"Triple": ("Fully triple glazed", EpcEfficiency.AVERAGE, 1, None, None),
|
||||
"Triple": (EpcWindowDescriptions.fully_triple_glazed, EpcEfficiency.AVERAGE, 100, None, None),
|
||||
# This is also classed as high performance glazing
|
||||
"DoubleKnownData": ("High performance glazing", EpcEfficiency.GOOD, 1, None, None),
|
||||
"DoubleKnownData": (
|
||||
EpcWindowDescriptions.fully_double_glazed.high_performance_glazing, EpcEfficiency.GOOD, 100, None, None
|
||||
),
|
||||
# Under SAP 10, secondary glazing is classed as poor efficiency (whereas under SAP 2012 it was generally good)
|
||||
"Secondary": ("Full secondary glazing", EpcEfficiency.POOR, 1, None, None),
|
||||
"TripleKnownData": ("High performance glazing", EpcEfficiency.GOOD, 1, None, None),
|
||||
"Secondary": (EpcWindowDescriptions.full_secondary_glazing, EpcEfficiency.POOR, 100, None, None),
|
||||
"TripleKnownData": (EpcWindowDescriptions.high_performance_glazing, EpcEfficiency.GOOD, 100, None, None),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -262,7 +262,7 @@ class ParityOnboarder(OnboarderBase):
|
|||
# controls. E.g. it may be programmer and room thermostat
|
||||
self.data[
|
||||
[
|
||||
self.landlord_heating_construction,
|
||||
self.landlord_heating_system,
|
||||
self.landlord_heating_efficiency,
|
||||
self.landlord_fuel_type,
|
||||
self.landlord_heating_controls,
|
||||
|
|
@ -309,7 +309,7 @@ class ParityOnboarder(OnboarderBase):
|
|||
self.landlord_multi_glaze_proportion,
|
||||
self.landlord_glazed_type,
|
||||
self.landlord_glazed_area,
|
||||
self.landlord_heating_construction,
|
||||
self.landlord_heating_system,
|
||||
self.landlord_heating_efficiency,
|
||||
self.landlord_fuel_type,
|
||||
self.landlord_heating_controls,
|
||||
|
|
@ -332,7 +332,7 @@ class ParityOnboarder(OnboarderBase):
|
|||
self.landlord_construction_age_band, self.landlord_property_type, self.landlord_built_form,
|
||||
self.landlord_wall_construction, self.landlord_wall_efficiency, self.landlord_roof_construction,
|
||||
self.landlord_roof_efficiency, self.landlord_floor_construction, self.landlord_windows_type,
|
||||
self.landlord_windows_efficiency, self.landlord_heating_construction, self.landlord_heating_efficiency,
|
||||
self.landlord_windows_efficiency, self.landlord_heating_system, self.landlord_heating_efficiency,
|
||||
self.landlord_fuel_type, self.landlord_heating_controls, self.landlord_heating_controls_efficiency,
|
||||
self.landlord_hot_water_system, self.landlord_hot_water_efficiency
|
||||
]:
|
||||
|
|
|
|||
Binary file not shown.
13
backend/pashub_fetcher/core_files.py
Normal file
13
backend/pashub_fetcher/core_files.py
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
from enum import Enum
|
||||
|
||||
|
||||
class CoreFiles(Enum):
|
||||
PHOTOPACK = "Photopack"
|
||||
SITENOTE = "SiteNote"
|
||||
RDSAP_SITENOTE = "RdSAP_SiteNote"
|
||||
PAS2023_VENTILATION = "PAS 2023 Ventilation Assessment Report"
|
||||
PAS2023_CONDITION = "PAS 2023 Condition Report"
|
||||
PAS_SIGNIFICANCE = "PAS Significance"
|
||||
PAR_PHOTOPACK = "PAR Photo Pack"
|
||||
PAS2023_PROPERTY = "PAS 2023 Property Assessment Report"
|
||||
PAS2023_OCCUPANCY = "PAS 2023 Occupancy Assessment Report"
|
||||
25
backend/pashub_fetcher/evidence_file_data.py
Normal file
25
backend/pashub_fetcher/evidence_file_data.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class EvidenceFileData:
|
||||
file_id: str
|
||||
file_name: str
|
||||
created_utc: str
|
||||
file_size: int
|
||||
file_extension: str
|
||||
|
||||
evidence_category: Optional[str] = None
|
||||
|
||||
@classmethod
|
||||
def from_api(cls, data: Dict[str, Any]) -> EvidenceFileData:
|
||||
return cls(
|
||||
file_id=data["fileId"],
|
||||
file_name=data["fileName"],
|
||||
created_utc=data["createdUtc"],
|
||||
file_size=data["fileSize"],
|
||||
file_extension=data["fileExtension"],
|
||||
evidence_category=data.get("evidenceCategory"),
|
||||
)
|
||||
16
backend/pashub_fetcher/evidence_metadata.py
Normal file
16
backend/pashub_fetcher/evidence_metadata.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict
|
||||
|
||||
|
||||
@dataclass
|
||||
class EvidenceMetadata:
|
||||
container_name: str
|
||||
blob_uri: str
|
||||
|
||||
@classmethod
|
||||
def from_api(cls, data: Dict[str, Any]) -> EvidenceMetadata:
|
||||
return cls(
|
||||
container_name=data["containerName"],
|
||||
blob_uri=data["blobUri"],
|
||||
)
|
||||
26
backend/pashub_fetcher/handler/Dockerfile
Normal file
26
backend/pashub_fetcher/handler/Dockerfile
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
FROM mcr.microsoft.com/playwright/python:v1.58.0-jammy
|
||||
|
||||
# Install AWS Lambda RIE
|
||||
ADD https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie /usr/local/bin/aws-lambda-rie
|
||||
RUN chmod +x /usr/local/bin/aws-lambda-rie
|
||||
|
||||
# Set working directory (Lambda task root)
|
||||
WORKDIR /var/task
|
||||
|
||||
COPY utils/ utils/
|
||||
COPY backend/pashub_fetcher/ backend/pashub_fetcher/
|
||||
|
||||
COPY backend/pashub_fetcher/handler/requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Local lambda entrypoint
|
||||
# ENTRYPOINT ["/usr/local/bin/aws-lambda-rie", "python", "-m", "awslambdaric"]
|
||||
|
||||
#AWS lambda entrypoint
|
||||
ENTRYPOINT ["python", "-m", "awslambdaric"]
|
||||
|
||||
# -----------------------------
|
||||
# Lambda handler
|
||||
# -----------------------------
|
||||
CMD ["backend.pashub_fetcher.handler.test_handler.handler"]
|
||||
# CMD ["backend.pashub_fetcher.handler.handler.handler"]
|
||||
126
backend/pashub_fetcher/handler/handler.py
Normal file
126
backend/pashub_fetcher/handler/handler.py
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
import os
|
||||
import re
|
||||
from typing import Any, Dict, List, Mapping
|
||||
from openpyxl import load_workbook
|
||||
|
||||
from backend.pashub_fetcher.job import Job
|
||||
from backend.pashub_fetcher.pashub_client import PashubClient, UnauthorizedError
|
||||
from backend.pashub_fetcher.sharepoint_subfolders import SharepointSubfolders
|
||||
from backend.pashub_fetcher.token_getter import get_token_from_local_storage
|
||||
from utils.logger import setup_logger
|
||||
from utils.sharepoint.domna_sharepoint_client import DomnaSharepointClient
|
||||
from utils.sharepoint.domna_sites import DomnaSites
|
||||
|
||||
|
||||
logger = setup_logger()
|
||||
|
||||
|
||||
def extract_jobs(filepath: str) -> List[Job]:
|
||||
wb = load_workbook(filepath, data_only=True)
|
||||
ws = wb["watford warm homes (wave 3) mai"]
|
||||
|
||||
HEADER_ROW = 3
|
||||
|
||||
headers: Dict[str, int] = {}
|
||||
for col in range(1, ws.max_column + 1):
|
||||
value = str(ws.cell(row=HEADER_ROW, column=col).value)
|
||||
if value:
|
||||
headers[value.strip()] = col
|
||||
|
||||
name_col = headers["Name"]
|
||||
link_col = headers["Pashub Link"]
|
||||
|
||||
jobs: List[Job] = []
|
||||
|
||||
for row in range(HEADER_ROW + 1, ws.max_row + 1):
|
||||
name = ws.cell(row=row, column=name_col).value
|
||||
link = ws.cell(row=row, column=link_col).value
|
||||
|
||||
if not name or not link:
|
||||
continue
|
||||
|
||||
link = str(link)
|
||||
|
||||
match = re.search(r"/jobs/([0-9a-fA-F\-]+)/", link)
|
||||
if not match:
|
||||
continue
|
||||
|
||||
job_id = match.group(1)
|
||||
|
||||
jobs.append({"id": job_id, "address": str(name)})
|
||||
|
||||
return jobs
|
||||
|
||||
|
||||
def handler(event: Mapping[str, Any], context: Any) -> None:
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
|
||||
filepath = os.path.join(BASE_DIR, "Watford_Warm_Homes_Wave_3_RA Downloads .xlsx")
|
||||
|
||||
jobs: List[Job] = extract_jobs(filepath)
|
||||
|
||||
logger.info("Successfully loaded jobs from spreadsheet")
|
||||
|
||||
pas_hub_email = "random@test.com"
|
||||
pas_hub_password = "my_fake_password"
|
||||
|
||||
try:
|
||||
token: str = get_token_from_local_storage(pas_hub_email, pas_hub_password)
|
||||
logger.info(f"Token extracted successfully")
|
||||
except:
|
||||
logger.error("Error getting auth token from Pas Hub")
|
||||
raise
|
||||
|
||||
pashub_client = PashubClient(token=token)
|
||||
sharepoint_client = DomnaSharepointClient(
|
||||
sharepoint_location=DomnaSites.SOCIAL_HOUSING_WAVE_3
|
||||
)
|
||||
|
||||
saved_file_paths: List[str] = []
|
||||
BASE_PATH = "/Osmosis-ACD Projects/Watford Warm Homes/Watford Property Folders (Shared with Client)" # TODO: get from request body
|
||||
|
||||
for job in jobs:
|
||||
try:
|
||||
job_files: List[str] = pashub_client.get_core_evidence_files_by_job_id(
|
||||
job["id"]
|
||||
)
|
||||
|
||||
# Upload files to sharepoint
|
||||
job_path = f"{BASE_PATH}/{job['address']}"
|
||||
|
||||
sharepoint_client.makedir(job["address"], BASE_PATH)
|
||||
|
||||
for folder in SharepointSubfolders:
|
||||
sharepoint_client.makedir(folder.value, job_path)
|
||||
|
||||
assessment_path = f"{job_path}/{SharepointSubfolders.ASSESSMENT.value}"
|
||||
|
||||
for file_path in job_files:
|
||||
filename = file_path.split("/")[-1]
|
||||
|
||||
sharepoint_client.upload_file(
|
||||
file_path,
|
||||
assessment_path,
|
||||
filename,
|
||||
)
|
||||
|
||||
saved_file_paths.extend(job_files)
|
||||
|
||||
except UnauthorizedError:
|
||||
logger.warning("Token expired - refreshing")
|
||||
|
||||
token = get_token_from_local_storage(pas_hub_email, pas_hub_password)
|
||||
|
||||
pashub_client = PashubClient(token=token)
|
||||
|
||||
# retry once
|
||||
saved_file_paths.extend(
|
||||
pashub_client.get_core_evidence_files_by_job_id(job["id"])
|
||||
)
|
||||
|
||||
print(f"saved {len(saved_file_paths)} files")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
event = {"Records": [{"body": "{}"}]}
|
||||
|
||||
handler(event, None)
|
||||
5
backend/pashub_fetcher/handler/requirements.txt
Normal file
5
backend/pashub_fetcher/handler/requirements.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
awslambdaric
|
||||
playwright==1.58.0
|
||||
requests
|
||||
msal
|
||||
openpyxl
|
||||
7
backend/pashub_fetcher/handler/test_handler.py
Normal file
7
backend/pashub_fetcher/handler/test_handler.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
from typing import Any, Mapping
|
||||
import json
|
||||
|
||||
|
||||
def handler(event: Mapping[str, Any], context: Any) -> None:
|
||||
print("Received event:")
|
||||
print(json.dumps(event, indent=2))
|
||||
6
backend/pashub_fetcher/job.py
Normal file
6
backend/pashub_fetcher/job.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
from typing import TypedDict
|
||||
|
||||
|
||||
class Job(TypedDict):
|
||||
id: str
|
||||
address: str
|
||||
11
backend/pashub_fetcher/local_handler/docker-compose.yml
Normal file
11
backend/pashub_fetcher/local_handler/docker-compose.yml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
version: "3.9"
|
||||
|
||||
services:
|
||||
pashub-fetcher-lambda:
|
||||
build:
|
||||
context: ../../../
|
||||
dockerfile: backend/pashub_fetcher/handler/Dockerfile
|
||||
ports:
|
||||
- "9000:8080"
|
||||
env_file:
|
||||
- ../../../.env
|
||||
26
backend/pashub_fetcher/local_handler/invoke_local_lambda.py
Normal file
26
backend/pashub_fetcher/local_handler/invoke_local_lambda.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
#!/usr/bin/env python3
|
||||
import json
|
||||
import requests
|
||||
|
||||
HOST = "localhost"
|
||||
PORT = "9000"
|
||||
|
||||
LAMBDA_URL = f"http://{HOST}:{PORT}/2015-03-31/functions/function/invocations"
|
||||
|
||||
payload = {
|
||||
"Records": [
|
||||
{
|
||||
"body": json.dumps(
|
||||
{
|
||||
"uprn": 123456,
|
||||
}
|
||||
)
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
response = requests.post(LAMBDA_URL, json=payload)
|
||||
|
||||
print("Status code:", response.status_code)
|
||||
print("Response:")
|
||||
print(response.text)
|
||||
138
backend/pashub_fetcher/pashub_client.py
Normal file
138
backend/pashub_fetcher/pashub_client.py
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
from collections import defaultdict
|
||||
import os
|
||||
from typing import Dict, List, Optional
|
||||
from datetime import datetime
|
||||
|
||||
import requests
|
||||
|
||||
from backend.pashub_fetcher.core_files import CoreFiles
|
||||
from backend.pashub_fetcher.evidence_file_data import EvidenceFileData
|
||||
from backend.pashub_fetcher.evidence_metadata import EvidenceMetadata
|
||||
from utils.logger import setup_logger
|
||||
|
||||
|
||||
logger = setup_logger()
|
||||
|
||||
|
||||
class UnauthorizedError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class PashubClient:
|
||||
def __init__(self, token: str):
|
||||
|
||||
self.token = token
|
||||
self.company_id = "cb5249e2-8f31-4ef4-aefd-08ddaccb1fa2"
|
||||
self.base = "https://pashub.net/api"
|
||||
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update(
|
||||
{
|
||||
"Authorization": f"Bearer {self.token}",
|
||||
"Accept": "application/json",
|
||||
}
|
||||
)
|
||||
logger.info("Finished initialising CotalityClient")
|
||||
|
||||
def get_core_evidence_files_by_job_id(self, job_id: str) -> List[str]:
|
||||
logger.info(f"Getting Core Evidence Files for job ID {job_id}")
|
||||
evidence_list: List[EvidenceFileData] = self._get_evidence_list(job_id)
|
||||
logger.info(f"Found {len(evidence_list)} Evidence files to get")
|
||||
if not evidence_list:
|
||||
return []
|
||||
|
||||
saved_files: List[str] = []
|
||||
|
||||
core_files: Dict[CoreFiles, EvidenceFileData] = self._select_latest_core_files(
|
||||
evidence_list
|
||||
)
|
||||
|
||||
logger.info(f"Number of core files to download is {len(core_files)}")
|
||||
|
||||
for _, evidence in core_files.items():
|
||||
evidence_id = evidence.file_id
|
||||
if not evidence_id:
|
||||
continue
|
||||
|
||||
logger.info(f"Getting metadata for file {evidence.file_name}")
|
||||
metadata: EvidenceMetadata = self._get_evidence_metadata(
|
||||
job_id, evidence_id
|
||||
)
|
||||
|
||||
download_url: str = self._build_download_url(metadata, evidence.file_id)
|
||||
output_dir: str = "/tmp"
|
||||
|
||||
file_name: str = evidence.file_name
|
||||
file_path: str = os.path.join(output_dir, file_name)
|
||||
|
||||
self._download_file(download_url, file_path)
|
||||
logger.info("Successfully downloaded file")
|
||||
saved_files.append(file_path)
|
||||
|
||||
return saved_files
|
||||
|
||||
def _get_core_file_type(self, file: EvidenceFileData) -> Optional[CoreFiles]:
|
||||
for core_file in CoreFiles:
|
||||
if file.file_name.startswith(core_file.value):
|
||||
return core_file
|
||||
return None
|
||||
|
||||
def _select_latest_core_files(
|
||||
self,
|
||||
files: List[EvidenceFileData],
|
||||
) -> Dict[CoreFiles, EvidenceFileData]:
|
||||
grouped: Dict[CoreFiles, List[EvidenceFileData]] = defaultdict(list)
|
||||
|
||||
for file in files:
|
||||
core_type = self._get_core_file_type(file)
|
||||
if not core_type:
|
||||
continue
|
||||
grouped[core_type].append(file)
|
||||
|
||||
latest_files: Dict[CoreFiles, EvidenceFileData] = {}
|
||||
|
||||
for core_type, group in grouped.items():
|
||||
latest = max(group, key=lambda f: datetime.fromisoformat(f.created_utc))
|
||||
latest_files[core_type] = latest
|
||||
|
||||
return latest_files
|
||||
|
||||
def _get_evidence_list(self, job_id: str) -> List[EvidenceFileData]:
|
||||
url = f"{self.base}/jobs/{job_id}/evidence"
|
||||
|
||||
r = self.session.get(url)
|
||||
if r.status_code == 401:
|
||||
raise UnauthorizedError("Token expired or invalid")
|
||||
|
||||
r.raise_for_status()
|
||||
|
||||
results = r.json().get("results", [])
|
||||
|
||||
return [EvidenceFileData.from_api(item) for item in results]
|
||||
|
||||
def _get_evidence_metadata(self, job_id: str, evidence_id: str) -> EvidenceMetadata:
|
||||
url = f"{self.base}/jobs/{job_id}/evidenceMetadata"
|
||||
params = {"evidenceIds": evidence_id}
|
||||
|
||||
r = self.session.get(url, params=params)
|
||||
if r.status_code == 401:
|
||||
raise UnauthorizedError()
|
||||
|
||||
r.raise_for_status()
|
||||
|
||||
return EvidenceMetadata.from_api(r.json())
|
||||
|
||||
def _build_download_url(self, metadata: EvidenceMetadata, file_id: str) -> str:
|
||||
container = metadata.container_name
|
||||
blob_uri = metadata.blob_uri
|
||||
|
||||
base, sas = blob_uri.split("?", 1)
|
||||
|
||||
return f"{base}{container}/{file_id}?{sas}"
|
||||
|
||||
def _download_file(self, url: str, file_path: str) -> None:
|
||||
r = requests.get(url)
|
||||
r.raise_for_status()
|
||||
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(r.content)
|
||||
9
backend/pashub_fetcher/sharepoint_subfolders.py
Normal file
9
backend/pashub_fetcher/sharepoint_subfolders.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
from enum import Enum
|
||||
|
||||
|
||||
class SharepointSubfolders(str, Enum):
|
||||
ASSESSMENT = "1. Retrofit Assessment"
|
||||
COORDINATION = "2. Retrofit Coordination"
|
||||
DESIGN = "3. Retrofit Design"
|
||||
INSTALLER = "4. Installer Information"
|
||||
TRUSTMARK = "5. TrustMark Lodgement"
|
||||
78
backend/pashub_fetcher/token_getter.py
Normal file
78
backend/pashub_fetcher/token_getter.py
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import os
|
||||
|
||||
from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeoutError
|
||||
|
||||
from utils.logger import setup_logger
|
||||
|
||||
logger = setup_logger()
|
||||
|
||||
|
||||
def get_token_from_local_storage(email: str, password: str) -> str:
|
||||
logger.info("Starting Playwright flow")
|
||||
|
||||
# For local testing / debugging, we save videos of the flow
|
||||
video_dir = os.path.join(os.path.dirname(__file__), "videos")
|
||||
os.makedirs(video_dir, exist_ok=True)
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(
|
||||
headless=True,
|
||||
args=["--no-sandbox", "--disable-dev-shm-usage"],
|
||||
)
|
||||
|
||||
context = browser.new_context(
|
||||
record_video_dir=video_dir,
|
||||
record_video_size={"width": 1280, "height": 720},
|
||||
)
|
||||
page = context.new_page()
|
||||
|
||||
try:
|
||||
logger.info("Navigating to site...")
|
||||
page.goto("https://pashub.net/", timeout=30000)
|
||||
|
||||
logger.info("Filling login form...")
|
||||
|
||||
email_input = page.locator("#email")
|
||||
email_input.wait_for(state="visible", timeout=10000)
|
||||
email_input.fill(email)
|
||||
|
||||
password_input = page.locator("#password")
|
||||
password_input.wait_for(state="visible", timeout=10000)
|
||||
password_input.focus()
|
||||
password_input.fill(password)
|
||||
|
||||
logger.info("Submitting login...")
|
||||
page.wait_for_selector("#btn-login", state="visible", timeout=10000)
|
||||
with page.expect_navigation(timeout=15000):
|
||||
page.click("#btn-login")
|
||||
|
||||
page.wait_for_timeout(3000)
|
||||
|
||||
if "login" in page.url.lower():
|
||||
raise Exception("Login failed (still on login page)")
|
||||
|
||||
logger.info(f"Login likely successful. URL: {page.url}")
|
||||
|
||||
token = page.evaluate(
|
||||
"""() => {
|
||||
return localStorage.getItem('token');
|
||||
}"""
|
||||
)
|
||||
|
||||
if not token:
|
||||
raise Exception("Login succeeded but no token found")
|
||||
|
||||
return token
|
||||
|
||||
except PlaywrightTimeoutError as e:
|
||||
raise Exception(f"Timeout during login flow: {str(e)}")
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(f"Unexpected error: {str(e)}")
|
||||
|
||||
finally:
|
||||
logger.info("Closing browser context (saving video)...")
|
||||
context.close()
|
||||
browser.close()
|
||||
|
||||
logger.info(f"Video(s) saved in: {video_dir}")
|
||||
214
backend/tests/test_addresses.py
Normal file
214
backend/tests/test_addresses.py
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
import pytest
|
||||
|
||||
from backend.addresses.Addresses import Addresses
|
||||
|
||||
|
||||
# -------------------------
|
||||
# Helpers
|
||||
# -------------------------
|
||||
|
||||
class AraBody:
|
||||
file_format = "ara_property_list"
|
||||
|
||||
|
||||
class LegacyBody:
|
||||
file_format = "legacy"
|
||||
|
||||
|
||||
# -------------------------
|
||||
# ARA FORMAT TESTS
|
||||
# -------------------------
|
||||
|
||||
def test_parse_ara_row_valid():
|
||||
row = {
|
||||
"uprn": "123",
|
||||
"address_1": "10 Downing St",
|
||||
"full_address": "10 Downing St, London",
|
||||
"postcode": "SW1A 2AA",
|
||||
}
|
||||
|
||||
addresses = Addresses.from_plan_input([row], AraBody())
|
||||
|
||||
assert len(addresses) == 1
|
||||
addr = addresses[0]
|
||||
|
||||
assert addr.uprn == 123
|
||||
assert addr.address_1 == "10 Downing St"
|
||||
assert addr.full_address == "10 Downing St, London"
|
||||
assert addr.postcode == "SW1A 2AA"
|
||||
|
||||
|
||||
def test_parse_ara_row_optional_fields():
|
||||
row = {
|
||||
"uprn": "456",
|
||||
"address_1": "Flat 2",
|
||||
"full_address": "Flat 2, Test House",
|
||||
"postcode": "AB1 2CD",
|
||||
"landlord_property_id": "ABC123",
|
||||
}
|
||||
|
||||
addresses = Addresses.from_plan_input([row], AraBody())
|
||||
addr = addresses[0]
|
||||
|
||||
assert addr.uprn == 456
|
||||
assert addr.landlord_property_id == "ABC123"
|
||||
|
||||
|
||||
# -------------------------
|
||||
# LEGACY FORMAT TESTS
|
||||
# -------------------------
|
||||
|
||||
def test_parse_legacy_basic():
|
||||
row = {
|
||||
"landlord_property_id": 144002000000,
|
||||
"address": "15 Rosebank Hall Angle Terrace",
|
||||
"postcode": "NE28 7BQ",
|
||||
"ordnance_survey_uprn": 47002793,
|
||||
"property_type": "Bungalow",
|
||||
}
|
||||
|
||||
addresses = Addresses.from_plan_input([row], LegacyBody())
|
||||
addr = addresses[0]
|
||||
|
||||
assert addr.uprn == 47002793
|
||||
assert addr.address_1 == "15 Rosebank Hall Angle Terrace"
|
||||
assert addr.postcode == "NE28 7BQ"
|
||||
assert addr.landlord_property_type == "Bungalow"
|
||||
|
||||
|
||||
def test_legacy_uses_domna_address_if_missing_address():
|
||||
row = {
|
||||
"domna_address_1": "Domna Address",
|
||||
"postcode": "AA1 1AA",
|
||||
"ordnance_survey_uprn": 123456,
|
||||
}
|
||||
|
||||
addresses = Addresses.from_plan_input([row], LegacyBody())
|
||||
addr = addresses[0]
|
||||
|
||||
assert addr.address_1 == "Domna Address"
|
||||
|
||||
|
||||
def test_legacy_full_address_fallback():
|
||||
row = {
|
||||
"address": "Fallback Address",
|
||||
"postcode": "ZZ1 1ZZ",
|
||||
"ordnance_survey_uprn": 999,
|
||||
}
|
||||
|
||||
addresses = Addresses.from_plan_input([row], LegacyBody())
|
||||
addr = addresses[0]
|
||||
|
||||
assert addr.full_address == "Fallback Address"
|
||||
|
||||
|
||||
# -------------------------
|
||||
# UPRN HANDLING
|
||||
# -------------------------
|
||||
|
||||
def test_uprn_from_float_string():
|
||||
row = {
|
||||
"uprn": "123.0",
|
||||
"address": "Test Address",
|
||||
"postcode": "AA1 1AA",
|
||||
}
|
||||
|
||||
addresses = Addresses.from_plan_input([row], LegacyBody())
|
||||
assert addresses[0].uprn == 123
|
||||
|
||||
|
||||
def test_uprn_fallback_to_os_uprn():
|
||||
row = {
|
||||
"uprn": None,
|
||||
"ordnance_survey_uprn": 555,
|
||||
"address": "Test Address",
|
||||
"postcode": "AA1 1AA",
|
||||
}
|
||||
|
||||
addresses = Addresses.from_plan_input([row], LegacyBody())
|
||||
assert addresses[0].uprn == 555
|
||||
|
||||
|
||||
def test_missing_uprn_is_none():
|
||||
row = {
|
||||
"address": "No UPRN Address",
|
||||
"postcode": "BB1 1BB",
|
||||
}
|
||||
|
||||
addresses = Addresses.from_plan_input([row], LegacyBody())
|
||||
assert addresses[0].uprn is None
|
||||
|
||||
|
||||
# -------------------------
|
||||
# FALLBACK LOGIC
|
||||
# -------------------------
|
||||
|
||||
def test_fallback_to_legacy_when_ara_fails():
|
||||
"""
|
||||
If ARA parser fails (missing required fields),
|
||||
system should fallback to legacy parser.
|
||||
"""
|
||||
row = {
|
||||
"address": "Fallback Address",
|
||||
"postcode": "ZZ1 1ZZ",
|
||||
}
|
||||
|
||||
addresses = Addresses.from_plan_input([row], AraBody())
|
||||
addr = addresses[0]
|
||||
|
||||
assert addr.address_1 == "Fallback Address"
|
||||
|
||||
|
||||
# -------------------------
|
||||
# VALIDATION
|
||||
# -------------------------
|
||||
|
||||
def test_validate_uprn_rejects_invalid():
|
||||
row = {
|
||||
"uprn": "not_a_number",
|
||||
"address_1": "Test",
|
||||
"full_address": "Test",
|
||||
"postcode": "AA1 1AA",
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
Addresses.from_plan_input([row], AraBody())
|
||||
|
||||
|
||||
# -------------------------
|
||||
# COLLECTION METHODS
|
||||
# -------------------------
|
||||
|
||||
def test_get_uprns():
|
||||
rows = [
|
||||
{"uprn": "1", "address_1": "A", "full_address": "A", "postcode": "AA"},
|
||||
{"uprn": "2", "address_1": "B", "full_address": "B", "postcode": "BB"},
|
||||
]
|
||||
|
||||
addresses = Addresses.from_plan_input(rows, AraBody())
|
||||
assert addresses.get_uprns() == [1, 2]
|
||||
|
||||
|
||||
def test_get_unique_postcodes():
|
||||
rows = [
|
||||
{"uprn": "1", "address_1": "A", "full_address": "A", "postcode": "AA"},
|
||||
{"uprn": "2", "address_1": "B", "full_address": "B", "postcode": "AA"},
|
||||
]
|
||||
|
||||
addresses = Addresses.from_plan_input(rows, AraBody())
|
||||
assert addresses.get_unique_postcodes() == ["AA"]
|
||||
|
||||
|
||||
def test_get_property_requests():
|
||||
row = {
|
||||
"uprn": "123",
|
||||
"address_1": "10 Downing St",
|
||||
"full_address": "10 Downing St",
|
||||
"postcode": "SW1A 2AA",
|
||||
}
|
||||
|
||||
addresses = Addresses.from_plan_input([row], AraBody())
|
||||
request = addresses.get_property_requests()[0]
|
||||
|
||||
assert request["uprn"] == 123
|
||||
assert request["postcode"] == "SW1A 2AA"
|
||||
101
backend/tests/test_data/sample_certificates.csv
Normal file
101
backend/tests/test_data/sample_certificates.csv
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
LMK_KEY,ADDRESS1,ADDRESS2,ADDRESS3,POSTCODE,BUILDING_REFERENCE_NUMBER,CURRENT_ENERGY_RATING,POTENTIAL_ENERGY_RATING,CURRENT_ENERGY_EFFICIENCY,POTENTIAL_ENERGY_EFFICIENCY,PROPERTY_TYPE,BUILT_FORM,INSPECTION_DATE,LOCAL_AUTHORITY,CONSTITUENCY,COUNTY,LODGEMENT_DATE,TRANSACTION_TYPE,ENVIRONMENT_IMPACT_CURRENT,ENVIRONMENT_IMPACT_POTENTIAL,ENERGY_CONSUMPTION_CURRENT,ENERGY_CONSUMPTION_POTENTIAL,CO2_EMISSIONS_CURRENT,CO2_EMISS_CURR_PER_FLOOR_AREA,CO2_EMISSIONS_POTENTIAL,LIGHTING_COST_CURRENT,LIGHTING_COST_POTENTIAL,HEATING_COST_CURRENT,HEATING_COST_POTENTIAL,HOT_WATER_COST_CURRENT,HOT_WATER_COST_POTENTIAL,TOTAL_FLOOR_AREA,ENERGY_TARIFF,MAINS_GAS_FLAG,FLOOR_LEVEL,FLAT_TOP_STOREY,FLAT_STOREY_COUNT,MAIN_HEATING_CONTROLS,MULTI_GLAZE_PROPORTION,GLAZED_TYPE,GLAZED_AREA,EXTENSION_COUNT,NUMBER_HABITABLE_ROOMS,NUMBER_HEATED_ROOMS,LOW_ENERGY_LIGHTING,NUMBER_OPEN_FIREPLACES,HOTWATER_DESCRIPTION,HOT_WATER_ENERGY_EFF,HOT_WATER_ENV_EFF,FLOOR_DESCRIPTION,FLOOR_ENERGY_EFF,FLOOR_ENV_EFF,WINDOWS_DESCRIPTION,WINDOWS_ENERGY_EFF,WINDOWS_ENV_EFF,WALLS_DESCRIPTION,WALLS_ENERGY_EFF,WALLS_ENV_EFF,SECONDHEAT_DESCRIPTION,SHEATING_ENERGY_EFF,SHEATING_ENV_EFF,ROOF_DESCRIPTION,ROOF_ENERGY_EFF,ROOF_ENV_EFF,MAINHEAT_DESCRIPTION,MAINHEAT_ENERGY_EFF,MAINHEAT_ENV_EFF,MAINHEATCONT_DESCRIPTION,MAINHEATC_ENERGY_EFF,MAINHEATC_ENV_EFF,LIGHTING_DESCRIPTION,LIGHTING_ENERGY_EFF,LIGHTING_ENV_EFF,MAIN_FUEL,WIND_TURBINE_COUNT,HEAT_LOSS_CORRIDOR,UNHEATED_CORRIDOR_LENGTH,FLOOR_HEIGHT,PHOTO_SUPPLY,SOLAR_WATER_HEATING_FLAG,MECHANICAL_VENTILATION,ADDRESS,LOCAL_AUTHORITY_LABEL,CONSTITUENCY_LABEL,POSTTOWN,CONSTRUCTION_AGE_BAND,LODGEMENT_DATETIME,TENURE,FIXED_LIGHTING_OUTLETS_COUNT,LOW_ENERGY_FIXED_LIGHT_COUNT,UPRN,UPRN_SOURCE,REPORT_TYPE
|
||||
626337830252011050922425395090286,Flat 20 Kingsley House,"15, Newton Street",,M1 1HE,2684056868,D,D,62,66,Flat,End-Terrace,2011-05-06,E08000003,E14000807,,2011-05-09,rental (private),41,44,654,608.0,3.6,116,3.3,28.0,28.0,340.0,289.0,88.0,88.0,30.72,dual,N,5th,Y,,2402.0,100.0,double glazing installed before 2002,Normal,0.0,1.0,1.0,75.0,0.0,"Electric immersion, off-peak",Average,Very Poor,(other premises below),,,Fully double glazed,Average,Average,"Solid brick, as built, insulated (assumed)",Good,Good,"Room heaters, electric",,,"Flat, insulated (assumed)",Average,Average,Electric storage heaters,Average,Very Poor,Automatic charge control,Average,Average,Low energy lighting in 75% of fixed outlets,Very Good,Very Good,electricity (not community),0.0,unheated corridor,12.89,3.29,0.0,,natural,"Flat 20 Kingsley House, 15, Newton Street",Manchester,Manchester Central,MANCHESTER,England and Wales: 1996-2002,2011-05-09 22:42:53,rental (private),4.0,3.0,77232195.0,Address Matched,100
|
||||
761477339962012031418342406148782,"22, Tuscan Road",,,M20 5GS,8041346968,D,C,63,69,House,Semi-Detached,2012-03-14,E08000003,E14000809,,2012-03-14,marketed sale,59,67,231,187.0,4.2,45,3.4,61.0,61.0,701.0,562.0,93.0,93.0,94.78,Single,Y,NODATA!,,,2106.0,43.0,"double glazing, unknown install date",Normal,1.0,6.0,5.0,77.0,0.0,From main system,Good,Good,"Solid, no insulation (assumed)",,,Partial double glazing,Poor,Poor,"Cavity wall, as built, no insulation (assumed)",Poor,Poor,"Room heaters, mains gas",,,"Pitched, 200 mm loft insulation",Good,Good,"Boiler and radiators, mains gas",Good,Good,"Programmer, room thermostat and TRVs",Good,Good,Low energy lighting in 77% of fixed outlets,Very Good,Very Good,mains gas (not community),0.0,NO DATA!,,2.48,0.0,,natural,"22, Tuscan Road",Manchester,"Manchester, Withington",MANCHESTER,England and Wales: 1930-1949,2012-03-14 18:34:24,owner-occupied,13.0,10.0,77068965.0,Address Matched,100
|
||||
746429639842012020315284198520028,"16, Balliol Street",,,M8 0WS,2853035968,E,C,48,71,House,Semi-Detached,2012-02-02,E08000003,E14000571,,2012-02-03,rental (private),44,69,346,178.0,5.9,67,3.0,86.0,48.0,853.0,494.0,215.0,112.0,88.878,Single,Y,NODATA!,,,2104,,not defined,Much More Than Typical,1.0,5.0,5.0,22.0,0.0,"From main system, no cylinder thermostat",Poor,Poor,"Suspended, no insulation (assumed)",,,Mostly double glazing,Good,Good,"Cavity wall, as built, no insulation (assumed)",Poor,Poor,"Room heaters, mains gas",,,"Pitched, 200mm loft insulation",Good,Good,"Boiler and radiators, mains gas",Good,Good,Programmer and room thermostat,Average,Average,Low energy lighting in 22% of fixed outlets,Poor,Poor,mains gas (not community),0.0,NO DATA!,,2.554,0.0,,natural,"16, Balliol Street",Manchester,Blackley and Broughton,MANCHESTER,England and Wales: 1930-1949,2012-02-03 15:28:41,rental (private),9.0,2.0,77006454.0,Address Matched,100
|
||||
6a63605a294b7005030427aaae897ca2cfdead115cceb1e34ddf22f340aba0a1,83 THORNTON ROAD,MANCHESTER,,M14 7NT,10000480583,D,B,65,87,House,Mid-Terrace,2021-01-06,E08000003,E14000807,,2021-01-06,marketed sale,61,87,255,81.0,3.0,45,1.0,70.0,70.0,524.0,344.0,92.0,65.0,67.0,off-peak 7 hour,Y,,,,,100.0,double glazing installed during or after 2002,Normal,2.0,4.0,4.0,78.0,0.0,From main system,Good,Good,"Suspended, no insulation (assumed)",,,Fully double glazed,Good,Good,"Solid brick, as built, no insulation (assumed)",Very Poor,Very Poor,,,,"Pitched, no insulation",Very Poor,Very Poor,"Boiler and radiators, mains gas",Good,Good,"Programmer, room thermostat and TRVs",Good,Good,Low energy lighting in 78% of fixed outlets,Very Good,Very Good,mains gas (not community),0.0,,,2.74,0.0,N,natural,"83 THORNTON ROAD, MANCHESTER",Manchester,Manchester Central,MANCHESTER,England and Wales: 1900-1929,2021-01-06 00:00:00,Owner-occupied,9.0,7.0,77112974.0,Energy Assessor,100
|
||||
9a6bc31be4b07d7f189572eb38beaa1fd49fa418fcd5f5476fed91776ab8d8d3,639A STOCKPORT ROAD,MANCHESTER,,M12 4QA,10000398504,D,C,66,70,Flat,Mid-Terrace,2020-10-06,E08000003,E14000808,,2020-10-08,rental,62,67,248,215.0,3.0,44,2.6,72.0,72.0,521.0,449.0,81.0,81.0,68.0,off-peak 7 hour,Y,01,Y,,,100.0,double glazing installed during or after 2002,Normal,0.0,4.0,4.0,78.0,0.0,From main system,Good,Good,(other premises below),,,Fully double glazed,Good,Good,"Solid brick, as built, no insulation (assumed)",Very Poor,Very Poor,,,,"Pitched, no insulation (assumed)",Very Poor,Very Poor,"Boiler and radiators, mains gas",Good,Good,"Programmer, room thermostat and TRVs",Good,Good,Low energy lighting in 78% of fixed outlets,Very Good,Very Good,mains gas (not community),0.0,heated corridor,,2.75,0.0,N,natural,"639A STOCKPORT ROAD, MANCHESTER",Manchester,"Manchester, Gorton",MANCHESTER,England and Wales: 1900-1929,2020-10-08,Rented (private),9.0,7.0,10090237291.0,Energy Assessor,100
|
||||
96054d6c9df6eefc64f8ece8f5a889697bf31627565c68b0cdf2be201076cb4c,APARTMENT 7,67 PALATINE ROAD,MANCHESTER,M20 3AP,10000787735,C,B,74,81,Flat,Detached,2020-11-23,E08000003,E14000809,,2020-11-23,marketed sale,71,71,202,205.0,2.2,34,2.3,58.0,68.0,338.0,226.0,211.0,191.0,66.0,off-peak 10 hour,N,02,Y,,,100.0,double glazing installed during or after 2002,Normal,0.0,3.0,2.0,100.0,0.0,"Electric immersion, off-peak",Poor,Poor,(another dwelling below),,,Fully double glazed,Good,Good,"Timber frame, as built, insulated (assumed)",Good,Good,Portable electric heaters (assumed),,,"Pitched, insulated (assumed)",Good,Good,"Room heaters, electric",Very Poor,Poor,Appliance thermostats,Good,Good,Low energy lighting in all fixed outlets,Very Good,Very Good,electricity (not community),0.0,unheated corridor,8.7,1.8,0.0,N,natural,"APARTMENT 7, 67 PALATINE ROAD, MANCHESTER",Manchester,"Manchester, Withington",MANCHESTER,England and Wales: 2003-2006,2020-11-23 00:00:00,Owner-occupied,15.0,15.0,10023046299.0,Energy Assessor,100
|
||||
b04c73c7eb6d7193ee2fbb37ef45d7a6fe013012ed4aecae02e314db95c8d814,FLAT 403,ICON 25,101 HIGH STREET,M4 1HG,10000571763,C,B,78,87,Flat,End-Terrace,2020-09-25,E08000003,E14000807,,2020-09-26,rental,80,79,132,139.0,1.6,22,1.7,66.0,74.0,172.0,88.0,346.0,187.0,72.0,off-peak 7 hour,N,05,N,,,100.0,double glazing installed during or after 2002,Normal,0.0,3.0,3.0,100.0,0.0,"Electric immersion, standard tariff",Very Poor,Poor,(another dwelling below),,,Fully double glazed,Good,Good,"System built, as built, insulated (assumed)",Good,Good,,,,(another dwelling above),,,"Room heaters, electric",Very Poor,Poor,Programmer and appliance thermostats,Good,Good,Low energy lighting in all fixed outlets,Very Good,Very Good,electricity (not community),0.0,heated corridor,,2.46,0.0,N,natural,"FLAT 403, ICON 25, 101 HIGH STREET",Manchester,Manchester Central,MANCHESTER,England and Wales: 2007-2011,2020-09-26 00:00:00,Rented (private),7.0,7.0,10014178048.0,Energy Assessor,100
|
||||
5460819602012012418330046222678,"7, Johns Close",,,M21 9EH,3664112468,C,C,78,79,Flat,NO DATA!,2012-01-23,E08000003,E14000809,,2012-01-24,rental (social),83,84,130,122.0,1.2,25,1.1,53.0,31.0,221.0,224.0,68.0,68.0,48.9,Unknown,Y,1st,Y,,2106.0,100.0,double glazing installed during or after 2002,Normal,0.0,2.0,2.0,29.0,0.0,From main system,Good,Good,(other premises below),,,Fully double glazed,Good,Good,"Cavity wall, filled cavity",Good,Good,,,,"Pitched, 200mm loft insulation",Good,Good,"Boiler and radiators, mains gas",Good,Good,"Programmer, room thermostat and TRVs",Good,Good,Low energy lighting in 29% of fixed outlets,Average,Average,mains gas (not community),0.0,no corridor,,2.31,0.0,,natural,"7, Johns Close",Manchester,"Manchester, Withington",MANCHESTER,England and Wales: 1983-1990,2012-01-24 18:33:00,rental (social),7.0,2.0,77077064.0,Address Matched,100
|
||||
666225219952011081514391692990386,"5, Beckfield Road",,,M23 2GF,3659929868,D,D,63,63,House,Semi-Detached,2011-08-15,E08000003,E14001059,,2011-08-15,marketed sale,59,59,238,238.0,4.1,46,4.1,57.0,57.0,688.0,688.0,87.0,87.0,88.5,Single,Y,NODATA!,,,2106.0,100.0,double glazing installed during or after 2002,Normal,1.0,4.0,4.0,78.0,0.0,From main system,Good,Good,"Solid, no insulation (assumed)",,,Fully double glazed,Good,Good,"System built, as built, no insulation (assumed)",Very Poor,Very Poor,"Room heaters, mains gas",,,"Pitched, 200 mm loft insulation",Good,Good,"Boiler and radiators, mains gas",Good,Good,"Programmer, room thermostat and TRVs",Good,Good,Low energy lighting in 78% of fixed outlets,Very Good,Very Good,mains gas (not community),0.0,NO DATA!,,2.0,0.0,,natural,"5, Beckfield Road",Manchester,Wythenshawe and Sale East,MANCHESTER,England and Wales: 1930-1949,2011-08-15 14:39:16,owner-occupied,9.0,7.0,77052370.0,Address Matched,100
|
||||
740955219712012011913012398920095,"18b, Greton Close",,,M13 0YR,589984968,C,B,79,82,Flat,NO DATA!,2012-01-18,E08000003,E14000808,,2012-01-19,marketed sale,69,71,255,238.0,2.2,45,2.0,64.0,32.0,160.0,149.0,107.0,107.0,47.97,dual,N,1st,N,,2401.0,100.0,double glazing installed before 2002,Normal,0.0,3.0,3.0,0.0,0.0,"Electric immersion, off-peak",Average,Very Poor,(other premises below),,,Fully double glazed,Average,Average,"Cavity wall, as built, insulated (assumed)",Good,Good,Portable electric heaters (assumed),,,(another dwelling above),,,Electric storage heaters,Average,Very Poor,Manual charge control,Poor,Poor,No low energy lighting,Very Poor,Very Poor,electricity (not community),0.0,unheated corridor,4.5,2.343,0.0,,natural,"18b, Greton Close",Manchester,"Manchester, Gorton",MANCHESTER,England and Wales: 1991-1995,2012-01-19 13:01:23,owner-occupied,7.0,0.0,77151965.0,Address Matched,100
|
||||
f32f46284a1afe1e99156dca4c97c64ee8f466cd646eaac907514c96095f9382,FLAT 315,THE BOX WORKS,4 WORSLEY STREET,M15 4NU,10000804989,C,B,79,81,Flat,Mid-Terrace,2020-10-31,E08000003,E14000807,,2020-10-31,marketed sale,68,71,244,224.0,2.1,41,1.9,66.0,66.0,226.0,178.0,169.0,169.0,51.0,off-peak 10 hour,N,02,N,,,100.0,double glazing installed during or after 2002,Normal,0.0,3.0,3.0,78.0,0.0,"Electric immersion, off-peak",Average,Poor,(another dwelling below),,,Fully double glazed,Good,Good,"System built, as built, insulated (assumed)",Good,Good,Portable electric heaters (assumed),,,(another dwelling above),,,Electric storage heaters,Average,Very Poor,Manual charge control,Poor,Poor,Low energy lighting in 78% of fixed outlets,Very Good,Very Good,electricity (not community),0.0,unheated corridor,8.51,3.1,0.0,N,natural,"FLAT 315, THE BOX WORKS, 4 WORSLEY STREET",Manchester,Manchester Central,MANCHESTER,England and Wales: 2003-2006,2020-10-31 00:00:00,Owner-occupied,9.0,7.0,10003799820.0,Energy Assessor,100
|
||||
413809754212011120117031199099872,Flat 4,"5, Derby Road",,M14 6UN,8296490768,D,D,56,65,Flat,End-Terrace,2011-12-01,E08000003,E14000809,,2011-12-01,rental (private),55,67,379,284.0,2.8,73,2.1,27.0,27.0,503.0,393.0,73.0,61.0,37.97,Unknown,Y,1st,N,,2102.0,10.0,double glazing installed before 2002,Normal,0.0,2.0,2.0,80.0,0.0,From main system,Good,Good,(other premises below),,,Some double glazing,Very Poor,Very Poor,"Solid brick, as built, no insulation (assumed)",Very Poor,Very Poor,,,,(another dwelling above),,,"Boiler and radiators, mains gas",Good,Good,"Programmer, no room thermostat",Very Poor,Very Poor,Low energy lighting in 80% of fixed outlets,Very Good,Very Good,mains gas (not community),0.0,unheated corridor,10.08,2.52,0.0,,natural,"Flat 4, 5, Derby Road",Manchester,"Manchester, Withington",MANCHESTER,England and Wales: 1900-1929,2011-12-01 17:03:11,rental (private),5.0,4.0,77203368.0,Address Matched,100
|
||||
f725eb075fb7f9e87af04780806b6a4ae5c8ae44d006df38e24b01266be2b2cf,72 BLUESTONE ROAD,MANCHESTER,,M40 9HY,10000725404,E,C,46,69,House,Semi-Detached,2020-12-18,E08000003,E14000807,,2020-12-21,marketed sale,40,61,435,257.0,5.3,77,3.2,57.0,57.0,954.0,748.0,90.0,65.0,69.0,off-peak 7 hour,Y,,,,,90.0,"double glazing, unknown install date",Normal,0.0,4.0,4.0,100.0,1.0,From main system,Good,Good,"Suspended, no insulation (assumed)",,,Mostly double glazing,Average,Average,"Cavity wall, as built, no insulation (assumed)",Poor,Poor,"Room heaters, mains gas",,,"Pitched, no insulation (assumed)",Very Poor,Very Poor,"Boiler and radiators, mains gas",Good,Good,Programmer and room thermostat,Average,Average,Low energy lighting in all fixed outlets,Very Good,Very Good,mains gas (not community),0.0,,,2.5,0.0,N,natural,"72 BLUESTONE ROAD, MANCHESTER",Manchester,Manchester Central,MANCHESTER,England and Wales: 1930-1949,2020-12-21 00:00:00,Owner-occupied,8.0,8.0,77031060.0,Energy Assessor,100
|
||||
684540547352011093012023195790691,Apartment 5 Park Brow,"128, St. Werburghs Road",,M21 8UQ,6184750968,C,B,80,82,Flat,NO DATA!,2011-09-30,E08000003,E14000809,,2011-09-30,rental (private),70,72,211,200.0,2.5,37,2.4,60.0,44.0,190.0,171.0,115.0,115.0,67.04,Unknown,N,1st,N,,2401.0,100.0,double glazing installed during or after 2002,Normal,0.0,3.0,3.0,64.0,0.0,"Electric immersion, off-peak",Average,Very Poor,(other premises below),,,Fully double glazed,Good,Good,"System built, as built, insulated (assumed)",Good,Good,Portable electric heaters (assumed),,,(another dwelling above),,,Electric storage heaters,Average,Very Poor,Manual charge control,Poor,Poor,Low energy lighting in 64% of fixed outlets,Good,Good,electricity (not community),0.0,unheated corridor,10.25,2.35,0.0,,natural,"Apartment 5 Park Brow, 128, St. Werburghs Road",Manchester,"Manchester, Withington",MANCHESTER,England and Wales: 2003-2006,2011-09-30 12:02:31,rental (private),11.0,7.0,10012206708.0,Address Matched,100
|
||||
746479909062012020316372915378642,Flat 11,Lorna Lodge,233 Brownley Road,M22 9XA,4179035968,B,B,86,88,Flat,End-Terrace,2012-02-03,E08000003,E14001059,,2012-02-03,rental (social),82,82,153,159.0,1.1,27,1.2,38.0,30.0,57.0,53.0,120.0,94.0,42.1,dual,N,1st,N,,2603.0,100.0,double glazing installed during or after 2002,Normal,0.0,2.0,2.0,57.0,0.0,"Electric immersion, off-peak",Poor,Very Poor,(other premises below),,,Fully double glazed,Good,Good,"Cavity wall, as built, insulated (assumed)",Good,Good,"Room heaters, electric",,,(another dwelling above),,,"Room heaters, electric",Poor,Very Poor,Programmer and appliance thermostats,Good,Good,Low energy lighting in 57% of fixed outlets,Good,Good,electricity (not community),0.0,heated corridor,,2.34,0.0,,natural,"Flat 11, Lorna Lodge, 233 Brownley Road",Manchester,Wythenshawe and Sale East,MANCHESTER,England and Wales: 2003-2006,2012-02-03 16:37:29,rental (social),7.0,4.0,10070865592.0,Address Matched,100
|
||||
0815d544b81eccdfbcd570b8db517bb27d9e36bf9337dff91d83f365f8405f5b,FLAT 36,PARKFIELD COURT,38-40 BARLOW MOOR ROAD,M20 2GE,10000754172,D,C,65,80,Flat,Enclosed End-Terrace,2021-01-06,E08000003,E14000809,,2021-01-26,rental,39,65,645,329.0,3.9,109,2.0,67.0,37.0,73.0,170.0,568.0,174.0,36.0,standard tariff,N,01,N,,,100.0,"double glazing, unknown install date",Normal,0.0,2.0,2.0,20.0,0.0,"Electric immersion, off-peak",Poor,Poor,(another dwelling below),,,Fully double glazed,Average,Average,"Cavity wall, filled cavity",Good,Good,Portable electric heaters (assumed),,,(another dwelling above),,,Electric storage heaters,Average,Poor,Controls for high heat retention storage heaters,Good,Good,Low energy lighting in 20% of fixed outlets,Poor,Poor,electricity (not community),0.0,heated corridor,,2.4,0.0,N,natural,"FLAT 36, PARKFIELD COURT, 38-40 BARLOW MOOR ROAD",Manchester,"Manchester, Withington",MANCHESTER,England and Wales: 1991-1995,2021-01-26 00:00:00,Rented (social),5.0,1.0,77191954.0,Energy Assessor,100
|
||||
762935166212012031922534895920198,"7, May Drive",,,M19 1FY,1893656968,C,C,71,72,Flat,End-Terrace,2012-03-19,E08000003,E14000809,,2012-03-19,rental (social),73,74,182,176.0,2.2,35,2.1,58.0,37.0,374.0,377.0,75.0,75.0,62.17,Single,Y,Ground,N,,2106.0,100.0,double glazing installed during or after 2002,Normal,0.0,3.0,3.0,43.0,0.0,From main system,Good,Good,"Suspended, no insulation (assumed)",,,Fully double glazed,Good,Good,"Cavity wall, filled cavity",Good,Good,"Room heaters, mains gas",,,(another dwelling above),,,"Boiler and radiators, mains gas",Good,Good,"Programmer, room thermostat and TRVs",Good,Good,Low energy lighting in 43% of fixed outlets,Average,Average,mains gas (not community),0.0,unheated corridor,4.93,2.43,0.0,,natural,"7, May Drive",Manchester,"Manchester, Withington",MANCHESTER,England and Wales: 1900-1929,2012-03-19 22:53:48,rental (social),7.0,3.0,77144094.0,Address Matched,100
|
||||
300000899702011081910022367299458,"8, Telfer Road",,,M13 0XS,4762992668,D,C,60,72,House,Semi-Detached,2011-08-15,E08000003,E14000808,,2011-08-19,rental (private),56,71,253,165.0,4.5,49,2.9,76.0,48.0,702.0,470.0,140.0,120.0,93.0,Single,Y,NODATA!,,,2104.0,90.0,double glazing installed during or after 2002,Normal,1.0,6.0,6.0,42.0,0.0,From main system,Good,Good,"Suspended, no insulation (assumed)",,,Mostly double glazing,Good,Good,"Cavity wall, as built, no insulation (assumed)",Poor,Poor,,,,"Pitched, 250 mm loft insulation",Good,Good,"Boiler and radiators, mains gas",Good,Good,Programmer and room thermostat,Average,Average,Low energy lighting in 42% of fixed outlets,Average,Average,mains gas (not community),0.0,NO DATA!,,2.65,0.0,,natural,"8, Telfer Road",Manchester,"Manchester, Gorton",MANCHESTER,England and Wales: 1930-1949,2011-08-19 10:02:23,rental (private),12.0,5.0,77149101.0,Address Matched,100
|
||||
a2d4f21dfc53c9e8bd48a08848547cabc9481a29411bd48299ea93881849913d,6 RODA STREET,MANCHESTER,,M9 4PJ,10000671391,F,B,34,84,House,Mid-Terrace,2020-09-23,E08000003,E14000571,,2020-10-13,ECO assessment,42,82,402,114.0,5.6,69,1.7,66.0,66.0,1711.0,459.0,94.0,69.0,82.0,off-peak 7 hour,Y,,,,,100.0,"double glazing, unknown install date",Normal,0.0,3.0,3.0,100.0,0.0,Gas multipoint,Average,Average,"Suspended, no insulation (assumed)",,,Fully double glazed,Average,Average,"Solid brick, as built, no insulation (assumed)",Very Poor,Very Poor,"Room heaters, mains gas",,,"Pitched, 300 mm loft insulation",Very Good,Very Good,"Room heaters, electric",Very Poor,Poor,Programmer and appliance thermostats,Good,Good,Low energy lighting in all fixed outlets,Very Good,Very Good,electricity (not community),0.0,,,2.7,0.0,N,natural,"6 RODA STREET, MANCHESTER",Manchester,Blackley and Broughton,MANCHESTER,England and Wales: 1930-1949,2020-10-13,Owner-occupied,10.0,10.0,77023763.0,Energy Assessor,100
|
||||
638222011112011060614072193090483,"26, Deneford Road",,,M20 2TD,4328137868,D,D,64,67,House,Semi-Detached,2011-06-03,E08000003,E14000809,,2011-06-06,rental (private),63,66,215,198.0,3.8,41,3.5,47.0,47.0,603.0,570.0,156.0,135.0,93.8,Single,Y,NODATA!,,,2106.0,100.0,"double glazing, unknown install date",Normal,1.0,5.0,5.0,100.0,0.0,From main system,Good,Good,"Suspended, no insulation (assumed)",,,Fully double glazed,Average,Average,"Cavity wall, filled cavity",Good,Good,"Room heaters, electric",,,"Pitched, 250 mm loft insulation",Good,Good,"Boiler and radiators, mains gas",Good,Good,"Programmer, room thermostat and TRVs",Good,Good,Low energy lighting in all fixed outlets,Very Good,Very Good,mains gas (not community),0.0,NO DATA!,,2.47,0.0,,natural,"26, Deneford Road",Manchester,"Manchester, Withington",MANCHESTER,England and Wales: 1930-1949,2011-06-06 14:07:21,rental (private),9.0,9.0,77102590.0,Address Matched,100
|
||||
6d70419837e0b18a96a6801877496ef689c0656cac553803fb1e307b14c237c6,FLAT 2,7 BUCKHURST ROAD,MANCHESTER,M19 2DS,10000498503,E,C,53,80,Flat,End-Terrace,2021-02-24,E08000003,E14000808,,2021-03-10,ECO assessment,57,66,570,452.0,2.0,96,1.6,23.0,25.0,398.0,135.0,296.0,138.0,20.0,off-peak 7 hour,N,00,N,,,50.0,double glazing installed during or after 2002,Normal,0.0,2.0,2.0,100.0,0.0,"Electric immersion, standard tariff",Very Poor,Poor,"Suspended, no insulation (assumed)",,,Partial double glazing,Poor,Poor,"Solid brick, with internal insulation",Good,Good,,,,(another dwelling above),,,"Room heaters, electric",Very Poor,Poor,Appliance thermostats,Good,Good,Low energy lighting in all fixed outlets,Very Good,Very Good,electricity (not community),0.0,heated corridor,,2.8,0.0,N,natural,"FLAT 2, 7 BUCKHURST ROAD, MANCHESTER",Manchester,"Manchester, Gorton",MANCHESTER,England and Wales: 1900-1929,2021-03-10 00:00:00,Owner-occupied,6.0,6.0,77207522.0,Energy Assessor,100
|
||||
741952999402012012213435894422098,Flat 3,"57, Albany Road",,M21 0BH,3463994968,D,D,60,63,Flat,NO DATA!,2012-01-21,E08000003,E14000809,,2012-01-22,rental (private),59,63,318,290.0,2.7,61,2.5,54.0,27.0,480.0,459.0,65.0,65.0,44.459,Unknown,Y,2nd,Y,,2107.0,100.0,double glazing installed during or after 2002,Normal,0.0,3.0,3.0,0.0,0.0,From main system,Good,Good,(other premises below),,,Fully double glazed,Good,Good,"Solid brick, as built, no insulation (assumed)",Very Poor,Very Poor,,,,"Pitched, no insulation (assumed)",Very Poor,Very Poor,"Boiler and radiators, mains gas",Good,Good,"Programmer, TRVs and bypass",Average,Average,No low energy lighting,Very Poor,Very Poor,mains gas (not community),0.0,no corridor,,2.2,0.0,,natural,"Flat 3, 57, Albany Road",Manchester,"Manchester, Withington",MANCHESTER,England and Wales: 1900-1929,2012-01-22 13:43:58,rental (private),6.0,0.0,77213865.0,Address Matched,100
|
||||
4966252352011052014323391990443,"24, Cranswick Street",,,M14 7JA,4372212468,C,C,72,75,House,Mid-Terrace,2011-05-18,E08000003,E14000807,,2011-05-20,rental (social),74,77,181,159.0,2.1,35,1.8,33.0,33.0,367.0,325.0,72.0,72.0,59.3,Single,Y,NODATA!,,,2106.0,100.0,"double glazing, unknown install date",Normal,0.0,4.0,4.0,100.0,0.0,From main system,Good,Good,"Solid, no insulation (assumed)",,,Fully double glazed,Average,Average,"Cavity wall, as built, no insulation (assumed)",Poor,Poor,"Room heaters, mains gas",,,"Pitched, 200mm loft insulation",Good,Good,"Boiler and radiators, mains gas",Good,Good,"Programmer, room thermostat and TRVs",Good,Good,Low energy lighting in all fixed outlets,Very Good,Very Good,mains gas (not community),0.0,NO DATA!,,2.76,0.0,,natural,"24, Cranswick Street",Manchester,Manchester Central,MANCHESTER,England and Wales: 1900-1929,2011-05-20 14:32:33,rental (social),5.0,5.0,77117462.0,Address Matched,100
|
||||
645301489802011062220261286792028,"21, Ward Street",Moston,,M40 9WS,3838387868,F,E,37,44,House,Mid-Terrace,2011-06-22,E08000003,E14000571,,2011-06-22,rental (private),34,40,472,410.0,6.4,91,5.6,76.0,38.0,1068.0,956.0,63.0,63.0,70.66,Single,Y,NODATA!,,,2601.0,0.0,not defined,Normal,1.0,3.0,2.0,0.0,0.0,Gas multipoint,Average,Average,"Suspended, no insulation (assumed)",,,Single glazed,Very Poor,Very Poor,"Cavity wall, as built, no insulation (assumed)",Poor,Poor,"Room heaters, mains gas",,,"Pitched, no insulation (assumed)",Very Poor,Very Poor,"Room heaters, mains gas",Average,Average,No thermostatic control of room temperature,Poor,Poor,No low energy lighting,Very Poor,Very Poor,mains gas (not community),0.0,NO DATA!,,2.75,0.0,,natural,"21, Ward Street, Moston",Manchester,Blackley and Broughton,MANCHESTER,England and Wales: 1900-1929,2011-06-22 20:26:12,rental (private),7.0,0.0,100012709992.0,Address Matched,100
|
||||
683986659902011092918252999092218,"15, Shaldon Drive",,,M40 1GS,3946650968,E,D,48,67,House,Semi-Detached,2011-09-29,E08000003,E14000807,,2011-09-29,marketed sale,47,68,352,203.0,4.7,67,2.7,75.0,39.0,836.0,505.0,92.0,81.0,71.0,Single,Y,NODATA!,,,2102,30.0,double glazing installed during or after 2002,Normal,0.0,4.0,4.0,9.0,0.0,From main system,Good,Good,"Suspended, no insulation (assumed)",,,Some double glazing,Poor,Poor,"Cavity wall, as built, no insulation (assumed)",Poor,Poor,"Room heaters, electric",,,"Pitched, 75 mm loft insulation",Average,Average,"Boiler and radiators, mains gas",Good,Good,"Programmer, no room thermostat",Very Poor,Very Poor,Low energy lighting in 9% of fixed outlets,Very Poor,Very Poor,mains gas (not community),0.0,NO DATA!,,2.4,0.0,,natural,"15, Shaldon Drive",Manchester,Manchester Central,MANCHESTER,England and Wales: 1950-1966,2011-09-29 18:25:29,owner-occupied,22.0,2.0,77188811.0,Address Matched,100
|
||||
7a4cfa9242f66e157df0d3f37c0545bfeac115c45f23ab0ffa081fd39c62a015,14 SIMISTER STREET,MANCHESTER,,M9 4JL,10000070988,D,C,61,79,House,Mid-Terrace,2020-12-17,E08000003,E14000571,,2021-01-06,rental,55,74,289,164.0,4.2,51,2.4,99.0,66.0,735.0,611.0,86.0,59.0,82.0,off-peak 7 hour,Y,,,,,100.0,"double glazing, unknown install date",Normal,4.0,4.0,4.0,50.0,0.0,From main system,Good,Good,"Solid, no insulation (assumed)",,,Fully double glazed,Average,Average,"Solid brick, as built, no insulation (assumed)",Very Poor,Very Poor,,,,"Pitched, no insulation (assumed)",Very Poor,Very Poor,"Boiler and radiators, mains gas",Good,Good,"Programmer, room thermostat and TRVs",Good,Good,Low energy lighting in 50% of fixed outlets,Good,Good,mains gas (not community),0.0,,,2.74,0.0,N,natural,"14 SIMISTER STREET, MANCHESTER",Manchester,Blackley and Broughton,MANCHESTER,England and Wales: 1900-1929,2021-01-06 00:00:00,Rented (private),10.0,5.0,77022202.0,Energy Assessor,100
|
||||
728844679402011113014364798397808,"158, Heald Place",,,M14 5WJ,5494373968,D,C,65,72,House,Mid-Terrace,2011-11-30,E08000003,E14000808,,2011-11-30,rental (private),63,72,226,172.0,3.4,43,2.6,70.0,43.0,571.0,451.0,83.0,83.0,78.4,Single,Y,NODATA!,,,2107.0,100.0,double glazing installed before 2002,Normal,1.0,4.0,4.0,36.0,0.0,From main system,Good,Good,"Suspended, no insulation (assumed)",,,Fully double glazed,Average,Average,"Cavity wall, as built, no insulation (assumed)",Poor,Poor,,,,"Pitched, 100mm loft insulation",Average,Average,"Boiler and radiators, mains gas",Good,Good,"Programmer, TRVs and bypass",Average,Average,Low energy lighting in 36% of fixed outlets,Average,Average,mains gas (not community),0.0,NO DATA!,,2.41,0.0,,natural,"158, Heald Place",Manchester,"Manchester, Gorton",MANCHESTER,England and Wales: 1900-1929,2011-11-30 14:36:47,rental (private),11.0,4.0,77111254.0,Address Matched,100
|
||||
231717462932011120115213414068198,"26, Thornton Road",,,M14 7WT,6874177568,D,C,57,70,House,Mid-Terrace,2011-12-01,E08000003,E14000807,,2011-12-01,rental (social),54,71,306,195.0,3.6,59,2.3,39.0,39.0,625.0,412.0,92.0,76.0,61.91,Single,Y,NODATA!,,,2104.0,100.0,double glazing installed during or after 2002,Normal,0.0,4.0,4.0,88.0,0.0,From main system,Good,Good,"Suspended, no insulation (assumed)",,,Fully double glazed,Good,Good,"Solid brick, as built, no insulation (assumed)",Very Poor,Very Poor,,,,"Pitched, no insulation",Very Poor,Very Poor,"Boiler and radiators, mains gas",Good,Good,Programmer and room thermostat,Average,Average,Low energy lighting in 88% of fixed outlets,Very Good,Very Good,mains gas (not community),0.0,NO DATA!,,2.61,0.0,,natural,"26, Thornton Road",Manchester,Manchester Central,MANCHESTER,England and Wales: before 1900,2011-12-01 15:21:34,rental (social),8.0,7.0,77113730.0,Address Matched,100
|
||||
762271159312012031619163994920298,"108, Meltham Avenue",,,M20 1EE,2865946968,C,C,75,78,Flat,NO DATA!,2012-03-16,E08000003,E14000809,,2012-03-16,marketed sale,77,80,152,131.0,1.9,29,1.6,38.0,38.0,326.0,288.0,86.0,77.0,63.7,Single,Y,1st,Y,,2107.0,100.0,double glazing installed during or after 2002,Normal,0.0,3.0,3.0,100.0,0.0,From main system,Good,Good,(other premises below),,,Fully double glazed,Good,Good,"Solid brick, with external insulation",Good,Good,,,,"Pitched, 300+ mm loft insulation",Very Good,Very Good,"Boiler and radiators, mains gas",Good,Good,"Programmer, TRVs and bypass",Average,Average,Low energy lighting in all fixed outlets,Very Good,Very Good,mains gas (not community),0.0,no corridor,,2.5,0.0,,natural,"108, Meltham Avenue",Manchester,"Manchester, Withington",MANCHESTER,England and Wales: 1950-1966,2012-03-16 19:16:39,owner-occupied,9.0,9.0,77105708.0,Address Matched,100
|
||||
400678909602011082617381773092868,"20, Dundreggan Gardens",,,M20 2EH,3642100768,C,C,71,72,House,Mid-Terrace,2011-08-26,E08000003,E14000809,,2011-08-26,marketed sale,71,72,150,144.0,4.3,28,4.1,117.0,70.0,741.0,747.0,105.0,105.0,103.8,dual,Y,NODATA!,,,2106.0,100.0,double glazing installed during or after 2002,More Than Typical,1.0,6.0,6.0,34.0,0.0,From main system,Good,Good,"Solid, insulated (assumed)",,,Fully double glazed,Good,Good,"Timber frame, as built, insulated (assumed)",Good,Good,,,,"Roof room(s), insulated (assumed)",Good,Good,"Boiler and radiators, mains gas",Good,Good,"Programmer, room thermostat and TRVs",Good,Good,Low energy lighting in 34% of fixed outlets,Average,Average,mains gas (not community),0.0,NO DATA!,,2.55,0.0,,"mechanical, supply and extract","20, Dundreggan Gardens",Manchester,"Manchester, Withington",MANCHESTER,England and Wales: 2007 onwards,2011-08-26 17:38:17,owner-occupied,38.0,13.0,10070865623.0,Address Matched,100
|
||||
634706509542011052610203283792568,Apartment 55 Britannia Mills,"11, Hulme Hall Road",,M15 4LA,6581507868,C,C,80,80,Flat,Mid-Terrace,2011-05-26,E08000003,E14000807,,2011-05-26,none of the above,67,67,220,220.0,3.1,39,3.1,49.0,49.0,223.0,223.0,110.0,110.0,78.5,dual,N,1st,N,,2401.0,100.0,"double glazing, unknown install date",Normal,1.0,1.0,1.0,100.0,0.0,"Electric immersion, off-peak",Average,Very Poor,(other premises below),,,Fully double glazed,Average,Average,"Solid brick, as built, insulated (assumed)",Good,Good,"Room heaters, electric",,,(another dwelling above),,,Electric storage heaters,Average,Very Poor,Manual charge control,Poor,Poor,Low energy lighting in all fixed outlets,Very Good,Very Good,electricity (not community),0.0,unheated corridor,6.95,2.28,0.0,,natural,"Apartment 55 Britannia Mills, 11, Hulme Hall Road",Manchester,Manchester Central,MANCHESTER,England and Wales: 1996-2002,2011-05-26 10:20:32,unknown,5.0,5.0,77233931.0,Address Matched,100
|
||||
bc53c658a67a4490c2c8cd2291b2396d7235e8c938f724f9aafc36f97cffd6bd,"7, Gleneagles Avenue",Clayton,,M11 4JU,10000415844,D,B,68,82,House,Semi-Detached,2020-11-03,E08000003,E14000807,,2021-02-24,rental,67,81,212,117.0,2.8,37,1.6,55.0,55.0,553.0,509.0,109.0,73.0,76.0,off-peak 7 hour,Y,,,,,100.0,double glazing installed during or after 2002,Normal,0.0,4.0,4.0,89.0,0.0,From main system,Good,Good,"Solid, no insulation (assumed)",,,Fully double glazed,Good,Good,"Cavity wall, filled cavity",Average,Average,"Room heaters, electric",,,"Pitched, 200 mm loft insulation",Good,Good,"Boiler and radiators, mains gas",Good,Good,"Programmer, room thermostat and TRVs",Good,Good,Low energy lighting in 89% of fixed outlets,Very Good,Very Good,mains gas (not community),0.0,,,2.4,0.0,N,natural,"7, Gleneagles Avenue, Clayton",Manchester,Manchester Central,,England and Wales: 1930-1949,2021-02-24 00:00:00,Rented (social),9.0,8.0,77179548.0,Energy Assessor,100
|
||||
e8227d212d926258058f8cac2e6e1e18cbd55451d7980fda1f5e7a0b10464f4d,"76, Langport Avenue",Ardwick,,M12 4NG,10000463748,C,C,74,76,Flat,Mid-Terrace,2020-09-28,E08000003,E14000807,,2021-02-24,rental,77,80,184,165.0,1.5,32,1.3,47.0,32.0,294.0,274.0,86.0,87.0,46.0,off-peak 7 hour,Y,00,Y,,,100.0,double glazing installed during or after 2002,Normal,0.0,2.0,2.0,50.0,0.0,From main system,Good,Good,"Solid, no insulation (assumed)",,,Fully double glazed,Good,Good,"Cavity wall, filled cavity",Average,Average,"Room heaters, electric",,,(another dwelling above),,,"Boiler and radiators, mains gas",Good,Good,"Programmer, room thermostat and TRVs",Good,Good,Low energy lighting in 50% of fixed outlets,Good,Good,mains gas (not community),0.0,unheated corridor,3.5,2.4,0.0,N,natural,"76, Langport Avenue, Ardwick",Manchester,Manchester Central,,England and Wales: 1967-1975,2021-02-24 00:00:00,Rented (social),6.0,3.0,77153103.0,Energy Assessor,100
|
||||
b3b0e1c10d9bf0479069ed65b9b3b657471a67652be33b8f7c21c6a905535c62,35 SANDILANDS ROAD,MANCHESTER,,M23 9JN,10000235547,C,B,69,81,House,Semi-Detached,2021-01-12,E08000003,E14001059,,2021-01-12,marketed sale,64,78,207,127.0,3.5,37,2.2,84.0,84.0,574.0,536.0,127.0,82.0,97.0,off-peak 7 hour,Y,,,,,100.0,double glazing installed before 2002,Normal,1.0,5.0,5.0,89.0,0.0,From main system,Good,Good,"Suspended, no insulation (assumed)",,,Fully double glazed,Average,Average,"Cavity wall, filled cavity",Average,Average,,,,"Pitched, 200 mm loft insulation",Good,Good,"Boiler and radiators, mains gas",Good,Good,"Programmer, room thermostat and TRVs",Good,Good,Low energy lighting in 89% of fixed outlets,Very Good,Very Good,mains gas (not community),0.0,,,2.6,0.0,N,natural,"35 SANDILANDS ROAD, MANCHESTER",Manchester,Wythenshawe and Sale East,MANCHESTER,England and Wales: 1950-1966,2021-01-12 00:00:00,Owner-occupied,18.0,16.0,77042816.0,Energy Assessor,100
|
||||
679334129922011091911524490718129,"7, Whalley Avenue",Whalley Range,,M16 8AT,2954220968,F,E,33,45,House,Mid-Terrace,2011-09-19,E08000003,E14000808,,2011-09-19,marketed sale,32,42,466,368.0,7.5,89,5.9,80.0,45.0,1338.0,1080.0,65.0,65.0,84.1,Single,Y,NODATA!,,,2601.0,0.0,not defined,Normal,1.0,4.0,2.0,20.0,0.0,Gas instantaneous at point of use,Good,Good,"Suspended, no insulation (assumed)",,,Single glazed,Very Poor,Very Poor,"Solid brick, as built, no insulation (assumed)",Very Poor,Very Poor,"Room heaters, electric",,,"Pitched, no insulation",Very Poor,Very Poor,"Room heaters, mains gas",Average,Average,No thermostatic control of room temperature,Poor,Poor,Low energy lighting in 20% of fixed outlets,Poor,Poor,mains gas (not community),0.0,NO DATA!,,2.73,0.0,,natural,"7, Whalley Avenue, Whalley Range",Manchester,"Manchester, Gorton",MANCHESTER,England and Wales: 1900-1929,2011-09-19 11:52:44,owner-occupied,10.0,2.0,10014179643.0,Address Matched,100
|
||||
488886519262012022219093606838569,"103, Wendover Road",,,M23 9ER,6060126768,C,C,72,74,House,Enclosed End-Terrace,2011-07-27,E08000003,E14001059,,2012-02-22,rental (social),71,74,171,156.0,2.8,33,2.5,51.0,51.0,464.0,423.0,90.0,91.0,84.0,Single,Y,NODATA!,,,2106.0,100.0,double glazing installed during or after 2002,Normal,0.0,5.0,5.0,90.0,0.0,From main system,Good,Good,"Solid, no insulation (assumed)",,,Fully double glazed,Good,Good,"Cavity wall, filled cavity",Good,Good,,,,"Pitched, 50 mm loft insulation",Poor,Poor,"Boiler and radiators, mains gas",Good,Good,"Programmer, room thermostat and TRVs",Good,Good,Low energy lighting in 90% of fixed outlets,Very Good,Very Good,mains gas (not community),0.0,NO DATA!,,2.4,0.0,,natural,"103, Wendover Road",Manchester,Wythenshawe and Sale East,MANCHESTER,England and Wales: 1950-1966,2012-02-22 19:09:36,rental (social),10.0,9.0,77042522.0,Address Matched,100
|
||||
718995131812011110709323290099494,"211, Chapman Street",,,M18 8WP,4499103968,D,D,61,67,House,Semi-Detached,2011-11-04,E08000003,E14000808,,2011-11-07,marketed sale,58,66,253,209.0,3.9,49,3.2,82.0,43.0,626.0,549.0,97.0,85.0,79.19,Single,Y,NODATA!,,,2107.0,100.0,double glazing installed during or after 2002,Normal,1.0,5.0,5.0,9.0,0.0,From main system,Good,Good,"Suspended, no insulation (assumed)",,,Fully double glazed,Good,Good,"Cavity wall, filled cavity",Good,Good,"Room heaters, mains gas",,,"Pitched, 100 mm loft insulation",Average,Average,"Boiler and radiators, mains gas",Good,Good,"Programmer, TRVs and bypass",Average,Average,Low energy lighting in 9% of fixed outlets,Very Poor,Very Poor,mains gas (not community),0.0,NO DATA!,,2.44,0.0,,natural,"211, Chapman Street",Manchester,"Manchester, Gorton",MANCHESTER,England and Wales: 1930-1949,2011-11-07 09:32:32,owner-occupied,11.0,1.0,77176530.0,Address Matched,100
|
||||
cea6cab056864f6114c6509288f6f76f6021a9efcc12cdd74660ebaac24d8a35,"8, Moss Lane",Moss Side,,M16 7BZ,10000747245,C,B,73,86,House,End-Terrace,2020-10-05,E08000003,E14000807,,2021-02-24,rental,71,85,178,87.0,2.5,31,1.3,57.0,57.0,451.0,412.0,110.0,74.0,80.0,off-peak 7 hour,Y,,,,,100.0,double glazing installed during or after 2002,Normal,0.0,3.0,3.0,89.0,0.0,From main system,Good,Good,"Solid, no insulation (assumed)",,,Fully double glazed,Good,Good,"Cavity wall, filled cavity",Good,Good,,,,"Pitched, 270 mm loft insulation",Good,Good,"Boiler and radiators, mains gas",Good,Good,"Programmer, room thermostat and TRVs",Good,Good,Low energy lighting in 89% of fixed outlets,Very Good,Very Good,mains gas (not community),0.0,,,2.4,0.0,N,natural,"8, Moss Lane, Moss Side",Manchester,Manchester Central,,England and Wales: 1976-1982,2021-02-24 00:00:00,Rented (social),9.0,8.0,77097127.0,Energy Assessor,100
|
||||
67687538aed41115bdc797d35afd7ec6397562062f01c7072b32b9a28e5bb959,"7, Rusholme Grove",Rusholme,,M14 5AR,10000415848,C,C,78,79,Flat,Mid-Terrace,2020-10-15,E08000003,E14000808,,2021-02-24,rental,81,83,149,132.0,1.3,26,1.1,35.0,35.0,250.0,221.0,89.0,89.0,48.0,off-peak 7 hour,Y,01,Y,,,100.0,double glazing installed during or after 2002,Normal,0.0,3.0,3.0,100.0,0.0,From main system,Good,Good,(another dwelling below),,,Fully double glazed,Good,Good,"Cavity wall, filled cavity",Good,Good,,,,"Pitched, 100 mm loft insulation",Average,Average,"Boiler and radiators, mains gas",Good,Good,"Programmer, room thermostat and TRVs",Good,Good,Low energy lighting in all fixed outlets,Very Good,Very Good,mains gas (not community),0.0,unheated corridor,5.3,2.4,0.0,N,natural,"7, Rusholme Grove, Rusholme",Manchester,"Manchester, Gorton",,England and Wales: 1976-1982,2021-02-24 00:00:00,Rented (social),5.0,5.0,77133633.0,Energy Assessor,100
|
||||
574337239602011092517501083292258,Flat 13 The Sorting House,"83, Newton Street",,M1 1EP,3600832868,D,C,64,73,Flat,Mid-Terrace,2011-09-25,E08000003,E14000807,,2011-09-25,marketed sale,55,53,279,294.0,4.6,49,4.8,69.0,57.0,619.0,433.0,146.0,135.0,92.23,Unknown,N,2nd,N,,2602.0,100.0,double glazing installed before 2002,Normal,1.0,3.0,3.0,64.0,0.0,"Electric immersion, off-peak",Average,Very Poor,(other premises below),,,Fully double glazed,Average,Average,"System built, as built, insulated (assumed)",Good,Good,,,,(another dwelling above),,,"Room heaters, electric",Poor,Very Poor,Appliance thermostats,Good,Good,Low energy lighting in 64% of fixed outlets,Good,Good,electricity (not community),0.0,no corridor,,2.19,0.0,,natural,"Flat 13 The Sorting House, 83, Newton Street",Manchester,Manchester Central,MANCHESTER,England and Wales: 1996-2002,2011-09-25 17:50:10,owner-occupied,14.0,9.0,10070397882.0,Address Matched,100
|
||||
629887539922011051622513676168289,Apartment 712,"51, Whitworth Street West",,M1 5ED,8722376868,B,B,82,83,Flat,Mid-Terrace,2011-05-16,E08000003,E14000807,,2011-05-16,marketed sale,73,74,230,222.0,1.8,41,1.8,60.0,30.0,106.0,113.0,89.0,89.0,44.65,Unknown,N,7th,N,,2401.0,100.0,double glazing installed before 2002,Normal,0.0,2.0,2.0,0.0,0.0,"Electric immersion, off-peak",Average,Very Poor,(other premises below),,,Fully double glazed,Average,Average,"Cavity wall, as built, insulated (assumed)",Good,Good,"Room heaters, electric",,,(another dwelling above),,,Electric storage heaters,Average,Very Poor,Manual charge control,Poor,Poor,No low energy lighting,Very Poor,Very Poor,electricity (not community),0.0,unheated corridor,6.28,2.25,0.0,,natural,"Apartment 712, 51, Whitworth Street West",Manchester,Manchester Central,MANCHESTER,England and Wales: 1996-2002,2011-05-16 22:51:36,owner-occupied,9.0,0.0,10003799427.0,Address Matched,100
|
||||
06359dd75cc0e575c7df1daa0107c832dd45b21835d0fa1a2f98f111975276de,16 WINDSOR ROAD,HARPURHEY,MANCHESTER,M9 5BW,10000281555,C,C,73,74,Flat,End-Terrace,2020-11-09,E08000003,E14000571,,2020-11-12,rental,75,76,228,215.0,1.5,40,1.4,34.0,34.0,296.0,280.0,70.0,70.0,38.0,off-peak 7 hour,Y,01,Y,,,100.0,double glazing installed during or after 2002,Normal,1.0,2.0,2.0,100.0,0.0,From main system,Good,Good,(another dwelling below),,,Fully double glazed,Good,Good,"Cavity wall, filled cavity",Average,Average,,,,"Pitched, 100 mm loft insulation",Average,Average,"Boiler and radiators, mains gas",Good,Good,Programmer and room thermostat,Average,Average,Low energy lighting in all fixed outlets,Very Good,Very Good,mains gas (not community),0.0,no corridor,,2.72,0.0,N,natural,"16 WINDSOR ROAD, HARPURHEY, MANCHESTER",Manchester,Blackley and Broughton,MANCHESTER,England and Wales: 1930-1949,2020-11-12,Rented (private),5.0,5.0,77024420.0,Energy Assessor,100
|
||||
cac28d15e638f84cb7574006e960cd56611f2d665fd02b6e1bad5ddd9e731594,84 THE BOULEVARD,MANCHESTER,,M20 2EU,10000707350,C,B,80,86,Flat,Enclosed Mid-Terrace,2020-10-21,E08000003,E14000809,,2020-10-21,marketed sale,77,79,165,149.0,1.7,28,1.5,72.0,63.0,168.0,115.0,203.0,154.0,60.0,off-peak 10 hour,N,01,N,,,100.0,double glazing installed during or after 2002,Normal,0.0,3.0,3.0,67.0,0.0,"Electric immersion, off-peak",Poor,Poor,(another dwelling below),,,Fully double glazed,Good,Good,"System built, as built, insulated (assumed)",Good,Good,,,,(another dwelling above),,,"Room heaters, electric",Very Poor,Poor,Appliance thermostats,Good,Good,Low energy lighting in 67% of fixed outlets,Good,Good,electricity (not community),0.0,unheated corridor,9.1,2.39,0.0,N,natural,"84 THE BOULEVARD, MANCHESTER",Manchester,"Manchester, Withington",MANCHESTER,England and Wales: 2003-2006,2020-10-21 00:00:00,Owner-occupied,6.0,4.0,10070393810.0,Energy Assessor,100
|
||||
f7f8dba5d96af5ec97b273b968a467f45b1b3ed07199684b69fd51796d195c96,FLAT 1,9 ZETLAND ROAD,MANCHESTER,M21 8TJ,10000483124,D,C,61,73,Flat,Semi-Detached,2021-02-11,E08000003,E14000809,,2021-02-12,rental,57,74,301,184.0,3.1,53,1.9,102.0,51.0,536.0,340.0,85.0,85.0,58.0,off-peak 7 hour,Y,00,N,,,100.0,"double glazing, unknown install date",Normal,2.0,3.0,3.0,0.0,0.0,From main system,Good,Good,"Suspended, no insulation (assumed)",,,Fully double glazed,Average,Average,"Solid brick, as built, no insulation (assumed)",Very Poor,Very Poor,,,,(another dwelling above),,,"Boiler and radiators, mains gas",Good,Good,"Programmer, TRVs and bypass",Average,Average,No low energy lighting,Very Poor,Very Poor,mains gas (not community),0.0,unheated corridor,6.49,2.68,0.0,N,natural,"FLAT 1, 9 ZETLAND ROAD, MANCHESTER",Manchester,"Manchester, Withington",MANCHESTER,England and Wales: 1900-1929,2021-02-12 00:00:00,Rented (private),10.0,0.0,77214240.0,Energy Assessor,100
|
||||
6c386fbd02fb8b3be61fb7b00c1aac6a7e533e7cb6eaf73b3a15a20486118ebc,"59, Worsley Court Wilmslow Road",Rusholme,,M14 5LU,10000666249,C,C,79,80,Flat,Mid-Terrace,2020-11-03,E08000003,E14000808,,2021-02-24,rental,83,84,141,129.0,1.1,25,1.0,30.0,30.0,190.0,172.0,105.0,105.0,43.0,off-peak 7 hour,Y,06,Y,,,100.0,double glazing installed during or after 2002,Normal,0.0,2.0,2.0,100.0,0.0,Community scheme,Good,Good,(another dwelling below),,,Fully double glazed,Good,Good,"Cavity wall, with external insulation",Very Good,Very Good,"Room heaters, electric",,,(another dwelling above),,,Community scheme,Good,Good,"Charging system linked to use of community heating, programmer and room thermostat",Average,Average,Low energy lighting in all fixed outlets,Very Good,Very Good,mains gas (community),0.0,no corridor,0.0,2.4,0.0,N,natural,"59, Worsley Court Wilmslow Road, Rusholme",Manchester,"Manchester, Gorton",,England and Wales: 1967-1975,2021-02-24 00:00:00,Rented (social),6.0,6.0,10003800130.0,Energy Assessor,100
|
||||
647860479262011062811072297938579,Flat 8 Rose Lea,"1, Downham Walk",,M23 9DG,7984797868,C,C,75,76,Flat,NO DATA!,2011-06-27,E08000003,E14001059,,2011-06-28,rental (social),79,80,193,183.0,1.3,37,1.2,38.0,21.0,200.0,202.0,88.0,88.0,34.08,Single,N,Ground,N,,2303.0,100.0,double glazing installed before 2002,Normal,0.0,2.0,2.0,17.0,0.0,Community scheme,Good,Good,"Solid, no insulation (assumed)",,,Fully double glazed,Average,Average,"Cavity wall, as built, insulated (assumed)",Good,Good,,,,(another dwelling above),,,Community scheme,Good,Good,"Flat rate charging, room thermostat only",Poor,Poor,Low energy lighting in 17% of fixed outlets,Poor,Poor,mains gas (community),0.0,heated corridor,,2.6,0.0,,natural,"Flat 8 Rose Lea, 1, Downham Walk",Manchester,Wythenshawe and Sale East,MANCHESTER,England and Wales: 1991-1995,2011-06-28 11:07:22,rental (social),6.0,1.0,77047324.0,Address Matched,100
|
||||
687906589742011101021150296099308,Flat 1,"18, Kirkmanshulme Lane",,M12 4WA,7752380968,D,D,61,66,Flat,End-Terrace,2011-10-10,E08000003,E14000808,,2011-10-10,rental (social),63,69,376,316.0,2.0,72,1.7,26.0,18.0,385.0,341.0,64.0,56.0,28.14,Single,Y,Ground,N,,2107.0,100.0,"double glazing, unknown install date",Normal,0.0,2.0,2.0,60.0,0.0,From main system,Good,Good,"Suspended, no insulation (assumed)",,,Fully double glazed,Average,Average,"Solid brick, as built, no insulation (assumed)",Poor,Poor,,,,(another dwelling above),,,"Boiler and radiators, mains gas",Good,Good,"Programmer, TRVs and bypass",Average,Average,Low energy lighting in 60% of fixed outlets,Good,Good,mains gas (not community),0.0,unheated corridor,11.22,3.04,0.0,,natural,"Flat 1, 18, Kirkmanshulme Lane",Manchester,"Manchester, Gorton",MANCHESTER,England and Wales: 1900-1929,2011-10-10 21:15:02,rental (social),5.0,3.0,77149971.0,Address Matched,100
|
||||
5416a6441d4991472ee3dc5c86d6ee764bf3b5c49edd14340ada8125e839e710,1463 ASHTON OLD ROAD,MANCHESTER,,M11 1HH,10000232217,C,B,80,89,House,Semi-Detached,2021-01-06,E08000003,E14000807,,2021-01-11,marketed sale,79,88,112,63.0,2.4,20,1.4,91.0,91.0,411.0,411.0,81.0,53.0,121.0,off-peak 7 hour,Y,,,,,100.0,double glazing installed during or after 2002,Normal,0.0,5.0,5.0,100.0,0.0,From main system,Good,Good,"Solid, insulated (assumed)",,,Fully double glazed,Good,Good,"Cavity wall, as built, insulated (assumed)",Very Good,Very Good,,,,"Pitched, 250 mm loft insulation",Good,Good,"Boiler and radiators, mains gas",Good,Good,"Programmer, room thermostat and TRVs",Good,Good,Low energy lighting in all fixed outlets,Very Good,Very Good,mains gas (not community),0.0,,,2.6,0.0,N,natural,"1463 ASHTON OLD ROAD, MANCHESTER",Manchester,Manchester Central,MANCHESTER,England and Wales: 2012 onwards,2021-01-11 00:00:00,Owner-occupied,13.0,13.0,10093076622.0,Energy Assessor,100
|
||||
731682144052011120911312699099293,"26, Fog Lane",,,M20 6AL,2354493968,D,C,61,69,House,Semi-Detached,2011-12-09,E08000003,E14000809,,2011-12-09,marketed sale,57,67,244,183.0,4.7,47,3.5,94.0,51.0,758.0,589.0,95.0,95.0,99.35,Single,Y,NODATA!,,,2106.0,100.0,"double glazing, unknown install date",Normal,0.0,5.0,5.0,14.0,0.0,From main system,Good,Good,"Suspended, no insulation (assumed)",,,Fully double glazed,Average,Average,"Cavity wall, as built, no insulation (assumed)",Poor,Poor,"Room heaters, mains gas",,,"Pitched, 200 mm loft insulation",Good,Good,"Boiler and radiators, mains gas",Good,Good,"Programmer, room thermostat and TRVs",Good,Good,Low energy lighting in 14% of fixed outlets,Poor,Poor,mains gas (not community),0.0,NO DATA!,,2.49,0.0,,natural,"26, Fog Lane",Manchester,"Manchester, Withington",MANCHESTER,England and Wales: 1930-1949,2011-12-09 11:31:26,owner-occupied,14.0,2.0,77104449.0,Address Matched,100
|
||||
636111349222011053112083067448689,14 Lynhurst Court,Whitelow Road,,M21 9RS,8629417868,C,C,79,80,Flat,NO DATA!,2011-05-24,E08000003,E14000809,,2011-05-31,rental (private),70,71,274,264.0,1.9,49,1.8,49.0,25.0,79.0,84.0,162.0,162.0,39.3,dual,N,2nd,N,,2106.0,100.0,double glazing installed during or after 2002,Normal,0.0,2.0,2.0,0.0,0.0,From main system,Average,Very Poor,(other premises below),,,Fully double glazed,Good,Good,"Solid brick, as built, insulated (assumed)",Good,Good,,,,(another dwelling above),,,"Boiler and radiators, electric",Poor,Very Poor,"Programmer, room thermostat and TRVs",Good,Good,No low energy lighting,Very Poor,Very Poor,electricity (not community),0.0,heated corridor,,2.98,0.0,,natural,"14 Lynhurst Court, Whitelow Road",Manchester,"Manchester, Withington",MANCHESTER,England and Wales: 2003-2006,2011-05-31 12:08:30,rental (private),11.0,0.0,10070869721.0,Address Matched,100
|
||||
cabb687c109a22fbd8064595e23af2e6d4119306b86d909ba87b93daf4987c56,FLAT 316,TIMBER WHARF,32 WORSLEY STREET,M15 4NY,10000512885,B,B,82,83,Flat,Mid-Terrace,2021-02-24,E08000003,E14000807,,2021-02-24,marketed sale,72,74,267,248.0,1.5,45,1.4,39.0,39.0,131.0,102.0,148.0,148.0,34.0,off-peak 10 hour,N,03,N,,,100.0,"double glazing, unknown install date",Normal,0.0,2.0,2.0,100.0,0.0,"Electric immersion, off-peak",Average,Poor,(another dwelling below),,,Fully double glazed,Average,Average,"System built, as built, insulated (assumed)",Good,Good,"Room heaters, electric",,,(another dwelling above),,,Electric storage heaters,Average,Very Poor,Manual charge control,Poor,Poor,Low energy lighting in all fixed outlets,Very Good,Very Good,electricity (not community),0.0,unheated corridor,5.85,2.7,0.0,N,natural,"FLAT 316, TIMBER WHARF, 32 WORSLEY STREET",Manchester,Manchester Central,MANCHESTER,England and Wales: 1996-2002,2021-02-24 00:00:00,Owner-occupied,6.0,6.0,10003799752.0,Energy Assessor,100
|
||||
44c18ead193b7303c929d2b1c75ed2133b5f38c2b16aba5f6aa2acf60e4fb0ca,"20, Thomas Regan Court Ansell Close",Gorton,,M18 8EE,10000146161,C,C,75,77,Flat,Mid-Terrace,2020-09-16,E08000003,E14000808,,2021-02-24,rental,77,81,225,192.0,1.2,40,1.0,27.0,27.0,218.0,184.0,99.0,99.0,31.0,off-peak 7 hour,Y,01,Y,,,100.0,double glazing installed during or after 2002,Normal,0.0,2.0,2.0,83.0,0.0,Community scheme,Good,Good,(another dwelling below),,,Fully double glazed,Good,Good,"Cavity wall, filled cavity",Good,Good,,,,"Pitched, 200 mm loft insulation",Good,Good,Community scheme,Good,Good,"Flat rate charging, programmer and TRVs",Average,Average,Low energy lighting in 83% of fixed outlets,Very Good,Very Good,mains gas (community),0.0,heated corridor,0.0,2.4,0.0,N,natural,"20, Thomas Regan Court Ansell Close, Gorton",Manchester,"Manchester, Gorton",,England and Wales: 1976-1982,2021-02-24 00:00:00,Rented (social),6.0,5.0,77177124.0,Energy Assessor,100
|
||||
3c08c183659d59febda24dc7492539fd151860c9ca4146efaa1450a8598eac41,"30, Kincraig Close",Openshaw,,M11 2JP,10000217311,C,B,73,87,House,Mid-Terrace,2020-10-08,E08000003,E14000807,,2021-02-24,rental,72,86,179,85.0,2.5,32,1.2,70.0,51.0,436.0,406.0,110.0,74.0,78.0,off-peak 7 hour,Y,,,,,100.0,double glazing installed during or after 2002,Normal,0.0,3.0,3.0,63.0,0.0,From main system,Good,Good,"Solid, no insulation (assumed)",,,Fully double glazed,Good,Good,"Cavity wall, filled cavity",Good,Good,,,,"Pitched, 300 mm loft insulation",Very Good,Very Good,"Boiler and radiators, mains gas",Good,Good,"Programmer, room thermostat and TRVs",Good,Good,Low energy lighting in 62% of fixed outlets,Good,Good,mains gas (not community),0.0,,,2.4,0.0,N,natural,"30, Kincraig Close, Openshaw",Manchester,Manchester Central,,England and Wales: 1976-1982,2021-02-24 00:00:00,Rented (social),8.0,5.0,77179169.0,Energy Assessor,100
|
||||
5ab41382eb79bed813dbe2c64c640c9ea51e554100993869db5f3d05ed7b7bf2,37 SOUTH GROVE,MANCHESTER,,M13 0AU,10000587736,D,B,65,85,House,Mid-Terrace,2020-12-09,E08000003,E14000807,,2020-12-09,marketed sale,58,82,224,96.0,4.7,40,2.1,86.0,86.0,829.0,525.0,106.0,75.0,120.0,off-peak 7 hour,Y,,,,,100.0,"double glazing, unknown install date",Normal,0.0,7.0,7.0,100.0,0.0,From main system,Good,Good,"Solid, no insulation (assumed)",,,Fully double glazed,Average,Average,"Solid brick, as built, no insulation (assumed)",Very Poor,Very Poor,,,,"Pitched, 150 mm loft insulation",Good,Good,"Boiler and radiators, mains gas",Good,Good,"Programmer, TRVs and bypass",Average,Average,Low energy lighting in all fixed outlets,Very Good,Very Good,mains gas (not community),0.0,,,2.78,0.0,N,natural,"37 SOUTH GROVE, MANCHESTER",Manchester,Manchester Central,MANCHESTER,England and Wales: before 1900,2020-12-09 00:00:00,Rented (private),11.0,11.0,77135463.0,Energy Assessor,100
|
||||
488750391312012022311594492090472,"73, Roundthorn Road",,,M23 1EP,4211126768,D,C,57,71,House,Semi-Detached,2011-08-03,E08000003,E14001059,,2012-02-23,rental (social),54,71,294,184.0,4.1,57,2.5,61.0,41.0,520.0,419.0,251.0,106.0,72.0,Single,Y,NODATA!,,,2104.0,100.0,double glazing installed before 2002,Normal,0.0,3.0,3.0,50.0,0.0,"From main system, no cylinder thermostat",Poor,Poor,"Solid, no insulation (assumed)",,,Fully double glazed,Average,Average,"Cavity wall, filled cavity",Good,Good,"Room heaters, mains gas",,,"Pitched, 100 mm loft insulation",Average,Average,"Boiler and radiators, mains gas",Good,Good,Programmer and room thermostat,Average,Average,Low energy lighting in 50% of fixed outlets,Good,Good,mains gas (not community),0.0,NO DATA!,,2.4,0.0,,natural,"73, Roundthorn Road",Manchester,Wythenshawe and Sale East,MANCHESTER,England and Wales: 1950-1966,2012-02-23 11:59:44,rental (social),10.0,5.0,77045284.0,Address Matched,100
|
||||
c175f3aad8bad5700cd15c26ed8864faf355c53361917563dd1cbaa208f0e47b,"5, Francesca Walk",Gorton,,M18 8EN,10000390940,C,C,73,76,Flat,Mid-Terrace,2020-10-19,E08000003,E14000808,,2021-02-24,rental,75,78,196,169.0,1.7,34,1.5,57.0,34.0,320.0,290.0,89.0,90.0,50.0,off-peak 7 hour,Y,00,Y,,,100.0,double glazing installed during or after 2002,Normal,0.0,2.0,2.0,33.0,0.0,From main system,Good,Good,"Solid, no insulation (assumed)",,,Fully double glazed,Good,Good,"Cavity wall, filled cavity",Average,Average,,,,(another dwelling above),,,"Boiler and radiators, mains gas",Good,Good,"Programmer, room thermostat and TRVs",Good,Good,Low energy lighting in 33% of fixed outlets,Average,Average,mains gas (not community),0.0,unheated corridor,5.2,2.4,0.0,N,natural,"5, Francesca Walk, Gorton",Manchester,"Manchester, Gorton",,England and Wales: 1967-1975,2021-02-24 00:00:00,Rented (social),6.0,2.0,77176834.0,Energy Assessor,100
|
||||
8fc47b53627c0b8b3bbe2b90a445805813ce5abce8543732986f41a863e0b608,"680, Hyde Road",Gorton,,M18 7EF,10000708843,C,C,77,77,Flat,Mid-Terrace,2020-10-13,E08000003,E14000808,,2021-02-24,rental,79,79,164,164.0,1.3,29,1.3,34.0,34.0,268.0,268.0,87.0,87.0,47.0,off-peak 7 hour,Y,01,Y,,,100.0,double glazing installed during or after 2002,Normal,0.0,2.0,2.0,100.0,0.0,From main system,Good,Good,(another dwelling below),,,Fully double glazed,Good,Good,"Cavity wall, filled cavity",Good,Good,,,,"Pitched, 250 mm loft insulation",Good,Good,"Boiler and radiators, mains gas",Good,Good,"Programmer, room thermostat and TRVs",Good,Good,Low energy lighting in all fixed outlets,Very Good,Very Good,mains gas (not community),0.0,unheated corridor,6.56,2.4,0.0,N,natural,"680, Hyde Road, Gorton",Manchester,"Manchester, Gorton",,England and Wales: 1976-1982,2021-02-24 00:00:00,Rented (social),6.0,6.0,77172366.0,Energy Assessor,100
|
||||
97681707d8c26caf1e068dde344b6db1af30849413c5c2421c74ae73602eda76,FLAT 2,61 CHARLESTOWN ROAD,MANCHESTER,M9 7AB,10000744934,D,D,56,63,Flat,Semi-Detached,2021-03-11,E08000003,E14000571,,2021-03-11,ECO assessment,60,67,548,460.0,1.9,93,1.6,23.0,23.0,414.0,302.0,255.0,255.0,21.0,off-peak 7 hour,N,01,N,,,100.0,double glazing installed before 2002,Normal,0.0,1.0,1.0,100.0,0.0,From main system,Very Poor,Poor,(another dwelling below),,,Fully double glazed,Average,Average,"Cavity wall, as built, no insulation (assumed)",Poor,Poor,,,,(another dwelling above),,,"Boiler and radiators, electric",Very Poor,Poor,TRVs and bypass,Average,Average,Low energy lighting in all fixed outlets,Very Good,Very Good,electricity (not community),0.0,heated corridor,,2.6,0.0,N,natural,"FLAT 2, 61 CHARLESTOWN ROAD, MANCHESTER",Manchester,Blackley and Broughton,MANCHESTER,England and Wales: 1900-1929,2021-03-11 00:00:00,Rented (social),3.0,3.0,10090425373.0,Energy Assessor,100
|
||||
5bb34e45687ba9cf86df5756344950a9462c6bd0ab753aeda85571eca1353c1e,"4, Howarth Close",Beswick,,M11 3BR,10000607596,C,C,74,77,Flat,Mid-Terrace,2020-10-19,E08000003,E14000807,,2021-02-24,rental,75,79,193,161.0,1.7,34,1.4,48.0,34.0,321.0,277.0,89.0,90.0,50.0,off-peak 7 hour,Y,00,Y,,,100.0,double glazing installed during or after 2002,Normal,0.0,2.0,2.0,60.0,0.0,From main system,Good,Good,"Solid, no insulation (assumed)",,,Fully double glazed,Good,Good,"System built, with external insulation",Very Good,Very Good,,,,(another dwelling above),,,"Boiler and radiators, mains gas",Good,Good,"Programmer, room thermostat and TRVs",Good,Good,Low energy lighting in 60% of fixed outlets,Good,Good,mains gas (not community),0.0,no corridor,0.0,2.4,0.0,N,natural,"4, Howarth Close, Beswick",Manchester,Manchester Central,,England and Wales: 1967-1975,2021-02-24 00:00:00,Rented (social),5.0,3.0,77168659.0,Energy Assessor,100
|
||||
609449769232012011815001652968806,Flat 2,36 Clyde Road,,M20 2HN,6632815868,E,E,45,54,Flat,Semi-Detached,2012-01-17,E08000003,E14000809,,2012-01-18,marketed sale,43,50,408,337.0,4.8,78,4.0,73.0,36.0,805.0,698.0,84.0,74.0,61.2,Single,Y,Ground,N,,2107.0,100.0,double glazing installed before 2002,Normal,0.0,3.0,3.0,0.0,0.0,From main system,Good,Good,"Suspended, no insulation (assumed)",,,Fully double glazed,Average,Average,"Solid brick, as built, no insulation (assumed)",Very Poor,Very Poor,,,,(another dwelling above),,,"Boiler and radiators, mains gas",Good,Good,"Programmer, TRVs and bypass",Average,Average,No low energy lighting,Very Poor,Very Poor,mains gas (not community),0.0,unheated corridor,9.0,3.03,0.0,,natural,"Flat 2, 36 Clyde Road",Manchester,"Manchester, Withington",MANCHESTER,England and Wales: 1900-1929,2012-01-18 15:00:16,owner-occupied,12.0,0.0,77193527.0,Address Matched,100
|
||||
8e64a93ed490b8227820a6eccbfb961ece2584543e04357a84a51ab327e3bd01,FLAT 1,7 STANLEY ROAD,MANCHESTER,M16 8HT,10000482906,E,C,46,71,Maisonette,Semi-Detached,2021-01-14,E08000003,E14000808,,2021-01-14,ECO assessment,47,61,474,342.0,3.2,80,2.3,67.0,40.0,725.0,328.0,168.0,198.0,39.0,standard tariff,N,00,N,,,100.0,double glazing installed during or after 2002,Normal,0.0,2.0,2.0,0.0,0.0,Electric instantaneous at point of use,Very Poor,Poor,"Solid, no insulation (assumed)",,,Fully double glazed,Good,Good,"Cavity wall, as built, no insulation (assumed)",Poor,Poor,,,,(another dwelling above),,,"Room heaters, electric",Very Poor,Poor,Appliance thermostats,Good,Good,No low energy lighting,Very Poor,Very Poor,electricity (not community),0.0,heated corridor,,2.2,0.0,N,natural,"FLAT 1, 7 STANLEY ROAD, MANCHESTER",Manchester,"Manchester, Gorton",MANCHESTER,England and Wales: 1900-1929,2021-01-14 00:00:00,Rented (social),8.0,0.0,10014178834.0,Energy Assessor,100
|
||||
748772149802012020922143291520318,"880, Burnage Lane",,,M19 1RS,9817745968,D,D,65,67,House,Semi-Detached,2012-02-09,E08000003,E14000809,,2012-02-09,marketed sale,61,63,204,190.0,5.2,39,4.9,97.0,62.0,828.0,792.0,115.0,115.0,133.0,Single,Y,NODATA!,,,2104.0,100.0,double glazing installed during or after 2002,Normal,2.0,6.0,6.0,44.0,1.0,From main system,Good,Good,"Solid, no insulation (assumed)",,,Fully double glazed,Good,Good,"Cavity wall, filled cavity",Good,Good,"Room heaters, mains gas",,,"Pitched, 200 mm loft insulation",Good,Good,"Boiler and radiators, mains gas",Good,Good,Programmer and room thermostat,Average,Average,Low energy lighting in 44% of fixed outlets,Average,Average,mains gas (not community),0.0,NO DATA!,,2.32,0.0,,natural,"880, Burnage Lane",Manchester,"Manchester, Withington",MANCHESTER,England and Wales: 1930-1949,2012-02-09 22:14:32,owner-occupied,16.0,7.0,77125062.0,Address Matched,100
|
||||
560c08dd1356a1845c7b42b64a9f9b51d5da057bbcf277b7577af732b553fb79,"4, Easthaven Avenue",Clayton,,M11 4RN,10000272490,D,B,65,82,House,Semi-Detached,2020-10-23,E08000003,E14000807,,2021-02-24,rental,64,81,230,122.0,3.0,40,1.6,77.0,48.0,566.0,516.0,107.0,72.0,74.0,off-peak 7 hour,Y,,,,,100.0,double glazing installed during or after 2002,Normal,0.0,4.0,4.0,40.0,0.0,From main system,Good,Good,"Suspended, no insulation (assumed)",,,Fully double glazed,Good,Good,"Cavity wall, filled cavity",Average,Average,"Room heaters, electric",,,"Pitched, 200 mm loft insulation",Good,Good,"Boiler and radiators, mains gas",Good,Good,"Programmer, room thermostat and TRVs",Good,Good,Low energy lighting in 40% of fixed outlets,Average,Average,mains gas (not community),0.0,,,2.4,0.0,N,natural,"4, Easthaven Avenue, Clayton",Manchester,Manchester Central,,England and Wales: 1930-1949,2021-02-24 00:00:00,Rented (social),10.0,4.0,77181001.0,Energy Assessor,100
|
||||
614517559252011042315041795290084,"50, Kirkmanshulme Lane",,,M12 4WA,454955868,E,E,45,54,House,Mid-Terrace,2011-04-21,E08000003,E14000808,,2011-04-23,marketed sale,40,47,338,281.0,8.3,65,7.0,115.0,58.0,1332.0,1159.0,104.0,89.0,102.38,Single,Y,NODATA!,,,2107.0,100.0,double glazing installed before 2002,Normal,0.0,7.0,7.0,0.0,1.0,From main system,Good,Good,"Suspended, no insulation (assumed)",,,Fully double glazed,Average,Average,"Solid brick, as built, no insulation (assumed)",Very Poor,Very Poor,"Room heaters, dual fuel (mineral and wood)",,,"Pitched, no insulation (assumed)",Very Poor,Very Poor,"Boiler and radiators, mains gas",Good,Good,"Programmer, TRVs and bypass",Average,Average,No low energy lighting,Very Poor,Very Poor,mains gas (not community),0.0,NO DATA!,,2.88,0.0,,natural,"50, Kirkmanshulme Lane",Manchester,"Manchester, Gorton",MANCHESTER,England and Wales: 1900-1929,2011-04-23 15:04:17,owner-occupied,12.0,0.0,77149984.0,Address Matched,100
|
||||
688269469962011101122380750998659,"203, Dickenson Road",,,M13 0YW,5527680968,E,D,46,67,Flat,NO DATA!,2011-10-11,E08000003,E14000808,,2011-10-11,rental (social),44,68,428,234.0,4.3,83,2.3,51.0,30.0,686.0,404.0,128.0,94.0,51.805,Single,Y,Ground,N,,2104.0,0.0,not defined,Normal,0.0,2.0,2.0,29.0,0.0,From main system,Average,Average,"Suspended, no insulation (assumed)",,,Single glazed,Very Poor,Very Poor,"Cavity wall, as built, no insulation (assumed)",Very Poor,Very Poor,"Room heaters, mains gas",,,(another dwelling above),,,"Boiler and radiators, mains gas",Good,Good,Programmer and room thermostat,Average,Average,Low energy lighting in 29% of fixed outlets,Average,Average,mains gas (not community),0.0,no corridor,,2.926,0.0,,natural,"203, Dickenson Road",Manchester,"Manchester, Gorton",MANCHESTER,England and Wales: before 1900,2011-10-11 22:38:07,rental (social),7.0,2.0,77151575.0,Address Matched,100
|
||||
679778369962011101214371240258809,Flat 16,St. Georges Court,Angela Street,M15 4HY,419620968,B,B,84,84,Flat,NO DATA!,2011-09-15,E08000003,E14000807,,2011-10-12,rental (social),77,77,265,265.0,1.2,47,1.2,20.0,20.0,73.0,73.0,90.0,90.0,26.23,dual,N,3rd,N,,2401.0,100.0,double glazing installed during or after 2002,Normal,0.0,1.0,1.0,100.0,0.0,"Electric immersion, off-peak",Average,Very Poor,(other premises below),,,Fully double glazed,Good,Good,"Cavity wall, with external insulation",Very Good,Very Good,,,,(another dwelling above),,,Electric storage heaters,Poor,Very Poor,Manual charge control,Poor,Poor,Low energy lighting in all fixed outlets,Very Good,Very Good,electricity (not community),0.0,unheated corridor,6.64,2.42,0.0,,natural,"Flat 16, St. Georges Court, Angela Street",Manchester,Manchester Central,MANCHESTER,England and Wales: 2007 onwards,2011-10-12 14:37:12,rental (social),4.0,4.0,77089818.0,Address Matched,100
|
||||
4ef5a12d522c8fe5ed3c546cfa247287dd48eed5b2b15ee4de2b9b3763b5d786,171 ASHLEY LANE,MANCHESTER,,M9 4NQ,10000118472,D,C,59,80,House,Mid-Terrace,2020-10-21,E08000003,E14000571,,2020-10-24,marketed sale,52,75,292,149.0,4.8,52,2.5,124.0,73.0,835.0,620.0,89.0,61.0,93.0,off-peak 7 hour,Y,,,,,100.0,"double glazing, unknown install date",Normal,1.0,5.0,5.0,30.0,0.0,From main system,Good,Good,"Suspended, no insulation (assumed)",,,Fully double glazed,Average,Average,"Solid brick, as built, no insulation (assumed)",Very Poor,Very Poor,"Room heaters, mains gas",,,"Pitched, 250 mm loft insulation",Good,Good,"Boiler and radiators, mains gas",Good,Good,Programmer and room thermostat,Average,Average,Low energy lighting in 30% of fixed outlets,Average,Average,mains gas (not community),0.0,,,2.69,0.0,N,natural,"171 ASHLEY LANE, MANCHESTER",Manchester,Blackley and Broughton,MANCHESTER,England and Wales: 1900-1929,2020-10-24 00:00:00,Owner-occupied,10.0,3.0,77022818.0,Energy Assessor,100
|
||||
1b1f726b0eeeec41f52767f85efe93fbce9531ee45bb57f29a437ca4ad23ec05,5 THE OLD COURTYARD,MANCHESTER,,M22 4YD,10000358677,C,B,72,84,House,End-Terrace,2021-03-05,E08000003,E14001059,,2021-03-05,marketed sale,70,82,186,110.0,2.7,33,1.6,71.0,71.0,447.0,447.0,103.0,72.0,81.0,off-peak 7 hour,Y,,,,,100.0,"double glazing, unknown install date",Normal,0.0,4.0,4.0,100.0,0.0,From main system,Good,Good,"Solid, limited insulation (assumed)",,,Fully double glazed,Average,Average,"Cavity wall, as built, insulated (assumed)",Good,Good,,,,"Pitched, 100 mm loft insulation",Average,Average,"Boiler and radiators, mains gas",Good,Good,"Programmer, room thermostat and TRVs",Good,Good,Low energy lighting in all fixed outlets,Very Good,Very Good,mains gas (not community),0.0,,,2.25,0.0,N,natural,"5 THE OLD COURTYARD, MANCHESTER",Manchester,Wythenshawe and Sale East,MANCHESTER,England and Wales: 1996-2002,2021-03-05 00:00:00,Owner-occupied,12.0,12.0,77067549.0,Energy Assessor,100
|
||||
730662989202011120614524193390868,"107, Plymouth Grove",,,M13 9HX,5291683968,C,C,76,76,House,Mid-Terrace,2011-12-06,E08000003,E14000807,,2011-12-06,rental (social),76,76,142,142.0,2.4,27,2.4,52.0,52.0,382.0,382.0,110.0,110.0,89.6,Single,Y,NODATA!,,,2310.0,100.0,double glazing installed during or after 2002,Normal,0.0,5.0,5.0,90.0,0.0,Community scheme,Good,Good,"Solid, no insulation (assumed)",,,Fully double glazed,Good,Good,"Cavity wall, filled cavity",Good,Good,"Room heaters, electric",,,"Pitched, 300+ mm loft insulation",Very Good,Very Good,Community scheme,Good,Good,"Charging system linked to use of community heating, TRVs",Good,Good,Low energy lighting in 90% of fixed outlets,Very Good,Very Good,mains gas (community),0.0,NO DATA!,,2.38,0.0,,natural,"107, Plymouth Grove",Manchester,Manchester Central,MANCHESTER,England and Wales: 1967-1975,2011-12-06 14:52:41,rental (social),10.0,9.0,77136227.0,Address Matched,100
|
||||
637817066932011060412162118068803,Apartment 306 Chatsworth House,"19, Lever Street",,M1 1BY,6308827868,C,B,75,81,Flat,NO DATA!,2011-06-04,E08000003,E14000807,,2011-06-04,marketed sale,69,69,213,213.0,2.7,38,2.7,76.0,46.0,274.0,191.0,137.0,107.0,72.25,Unknown,N,3rd,N,,2602.0,100.0,double glazing installed before 2002,Normal,0.0,3.0,3.0,20.0,0.0,"Electric immersion, off-peak",Average,Very Poor,(other premises below),,,Fully double glazed,Average,Average,"System built, as built, insulated (assumed)",Good,Good,,,,(another dwelling above),,,"Room heaters, electric",Poor,Very Poor,Appliance thermostats,Good,Good,Low energy lighting in 20% of fixed outlets,Poor,Poor,electricity (not community),0.0,unheated corridor,12.5,2.45,0.0,,natural,"Apartment 306 Chatsworth House, 19, Lever Street",Manchester,Manchester Central,MANCHESTER,England and Wales: 1996-2002,2011-06-04 12:16:21,owner-occupied,10.0,2.0,10023045032.0,Address Matched,100
|
||||
649015816912011063013353790790785,"45, Northridge Road",,,M9 6GW,7587608868,C,C,69,69,House,Semi-Detached,2011-06-30,E08000003,E14000571,,2011-06-30,rental (social),70,70,191,191.0,2.6,36,2.6,38.0,38.0,469.0,469.0,84.0,84.0,72.06,Single,Y,NODATA!,,,2106.0,100.0,double glazing installed during or after 2002,Normal,0.0,4.0,4.0,100.0,0.0,From main system,Good,Good,"Solid, no insulation (assumed)",,,Fully double glazed,Good,Good,"Cavity wall, filled cavity",Good,Good,"Room heaters, electric",,,"Pitched, 250 mm loft insulation",Good,Good,"Boiler and radiators, mains gas",Good,Good,"Programmer, room thermostat and TRVs",Good,Good,Low energy lighting in all fixed outlets,Very Good,Very Good,mains gas (not community),0.0,NO DATA!,,2.44,0.0,,natural,"45, Northridge Road",Manchester,Blackley and Broughton,MANCHESTER,England and Wales: 1930-1949,2011-06-30 13:35:37,rental (social),6.0,6.0,77020453.0,Address Matched,100
|
||||
79011046012011092920530093290647,"Apartment 2, Windsor House","252, Mauldeth Road West",Chorlton cum Hardy,M21 7TH,6732635468,C,C,70,76,Flat,Detached,2011-09-29,E08000003,E14000809,,2011-09-29,marketed sale,63,62,262,268.0,3.2,46,3.2,62.0,45.0,383.0,291.0,128.0,115.0,67.94,Unknown,N,1st,N,,2603.0,100.0,double glazing installed during or after 2002,Normal,0.0,3.0,3.0,44.0,0.0,"Electric immersion, off-peak",Average,Very Poor,"To external air, insulated (assumed)",,,Fully double glazed,Good,Good,"System built, as built, insulated (assumed)",Good,Good,,,,(another dwelling above),,,"Room heaters, electric",Poor,Very Poor,Programmer and appliance thermostats,Good,Good,Low energy lighting in 44% of fixed outlets,Average,Average,electricity (not community),0.0,unheated corridor,23.28,2.25,0.0,,"mechanical, extract only","Apartment 2, Windsor House, 252, Mauldeth Road West, Chorlton cum Hardy",Manchester,"Manchester, Withington",MANCHESTER,England and Wales: 2003-2006,2011-09-29 20:53:00,owner-occupied,9.0,4.0,10070868545.0,Address Matched,100
|
||||
664d3337e51de82abf0c359a8e9a446a0dbd5d7965369fc859eb0add675aec5a,"8, Hartington Drive",Clayton,,M11 4JG,10000747213,D,B,68,83,House,Semi-Detached,2020-10-20,E08000003,E14000807,,2021-02-24,rental,65,81,217,117.0,3.0,38,1.7,79.0,51.0,543.0,500.0,110.0,74.0,80.0,off-peak 7 hour,Y,,,,,100.0,double glazing installed during or after 2002,Normal,0.0,4.0,4.0,45.0,0.0,From main system,Good,Good,"Suspended, no insulation (assumed)",,,Fully double glazed,Good,Good,"Cavity wall, filled cavity",Average,Average,,,,"Pitched, 300 mm loft insulation",Very Good,Very Good,"Boiler and radiators, mains gas",Good,Good,"Programmer, room thermostat and TRVs",Good,Good,Low energy lighting in 45% of fixed outlets,Good,Good,mains gas (not community),0.0,,,2.6,0.0,N,natural,"8, Hartington Drive, Clayton",Manchester,Manchester Central,,England and Wales: 1930-1949,2021-02-24 00:00:00,Rented (social),11.0,5.0,77169870.0,Energy Assessor,100
|
||||
fff8edbe33f302b835435ce435658054f9a93dc8d3a699c782508ed9059ee32a,FLAT 75,SOUTHMOOR,23 GLEBELANDS ROAD,M23 1HR,10000583235,D,C,64,80,Flat,Mid-Terrace,2021-02-10,E08000003,E14001059,,2021-02-18,ECO assessment,68,67,280,285.0,2.0,47,2.0,74.0,41.0,352.0,194.0,285.0,154.0,41.0,off-peak 7 hour,N,05,N,,,100.0,"double glazing, unknown install date",Normal,0.0,3.0,3.0,0.0,0.0,"Electric immersion, standard tariff",Very Poor,Poor,(another dwelling below),,,Fully double glazed,Average,Average,"System built, as built, no insulation (assumed)",Very Poor,Very Poor,,,,(another dwelling above),,,"Room heaters, electric",Very Poor,Poor,Programmer and appliance thermostats,Good,Good,No low energy lighting,Very Poor,Very Poor,electricity (not community),0.0,no corridor,,2.49,0.0,N,natural,"FLAT 75, SOUTHMOOR, 23 GLEBELANDS ROAD",Manchester,Wythenshawe and Sale East,MANCHESTER,England and Wales: 1967-1975,2021-02-18 00:00:00,Rented (private),6.0,0.0,10003798235.0,Energy Assessor,100
|
||||
554538989642011101814401084099988,"246, Cornishway",,,M22 1SU,1659680868,E,D,52,61,Flat,End-Terrace,2011-10-18,E08000003,E14001059,,2011-10-18,rental (social),55,63,314,255.0,3.6,58,3.0,65.0,36.0,458.0,440.0,328.0,224.0,62.09,Single,Y,Ground,Y,,2106.0,100.0,double glazing installed during or after 2002,Normal,0.0,3.0,3.0,20.0,0.0,"Electric immersion, standard tariff",Very Poor,Very Poor,"Solid, no insulation (assumed)",,,Fully double glazed,Good,Good,"Cavity wall, as built, no insulation (assumed)",Poor,Poor,,,,(another dwelling above),,,"Boiler and radiators, mains gas",Good,Good,"Programmer, room thermostat and TRVs",Good,Good,Low energy lighting in 20% of fixed outlets,Poor,Poor,mains gas (not community),0.0,unheated corridor,4.77,2.44,0.0,,natural,"246, Cornishway",Manchester,Wythenshawe and Sale East,MANCHESTER,England and Wales: 1950-1966,2011-10-18 14:40:10,rental (social),10.0,2.0,77056094.0,Address Matched,100
|
||||
b4b36cb0202e0bb71d122b2d37acae25b532d32d407c18f29795ab9631bec6b1,APARTMENT 15,6 THE WATERFRONT,MANCHESTER,M11 4AY,10000509199,C,C,70,70,Flat,Detached,2021-03-02,E08000003,E14000807,,2021-03-02,rental,73,73,185,185.0,2.2,31,2.2,65.0,65.0,459.0,459.0,272.0,272.0,71.0,off-peak 10 hour,N,02,N,,,100.0,double glazing installed during or after 2002,Normal,0.0,3.0,3.0,100.0,0.0,From main system,Very Poor,Poor,(another dwelling below),,,Fully double glazed,Good,Good,"Cavity wall, as built, insulated (assumed)",Good,Good,,,,(another dwelling above),,,"Boiler and radiators, electric",Very Poor,Poor,"Programmer, room thermostat and TRVs",Good,Good,Low energy lighting in all fixed outlets,Very Good,Very Good,electricity (not community),0.0,unheated corridor,10.1,2.41,0.0,N,natural,"APARTMENT 15, 6 THE WATERFRONT, MANCHESTER",Manchester,Manchester Central,MANCHESTER,England and Wales: 2003-2006,2021-03-02 00:00:00,Owner-occupied,9.0,9.0,10012203091.0,Energy Assessor,100
|
||||
619828709242011041909303788699788,"16, Alexandra Drive",,,M19 2WW,252206868,D,D,61,68,House,Semi-Detached,2011-04-18,E08000003,E14000808,,2011-04-19,marketed sale,57,66,245,195.0,4.5,47,3.6,69.0,48.0,721.0,597.0,103.0,86.0,95.72,Single,Y,NODATA!,,,2107.0,75.0,double glazing installed before 2002,Normal,1.0,5.0,5.0,55.0,0.0,From main system,Good,Good,"Suspended, no insulation (assumed)",,,Partial double glazing,Poor,Poor,"Cavity wall, with internal insulation",Good,Good,"Room heaters, mains gas",,,"Pitched, 100 mm loft insulation",Average,Average,"Boiler and radiators, mains gas",Good,Good,"Programmer, TRVs and bypass",Average,Average,Low energy lighting in 55% of fixed outlets,Good,Good,mains gas (not community),0.0,NO DATA!,,2.7,0.0,,natural,"16, Alexandra Drive",Manchester,"Manchester, Gorton",MANCHESTER,England and Wales: 1900-1929,2011-04-19 09:30:37,owner-occupied,11.0,6.0,77147636.0,Address Matched,100
|
||||
481145989762012022218030515508359,"16, Crowthorn Drive",,,M23 2XX,5126865768,D,C,63,74,Flat,Detached,2011-09-30,E08000003,E14001059,,2012-02-22,rental (social),61,75,242,155.0,3.4,46,2.2,65.0,43.0,424.0,345.0,227.0,114.0,73.0,Single,Y,1st,N,,2104.0,100.0,double glazing installed during or after 2002,Normal,0.0,4.0,4.0,50.0,0.0,"From main system, no cylinder thermostat",Poor,Poor,(other premises below),,,Fully double glazed,Good,Good,"Solid brick, as built, no insulation (assumed)",Very Poor,Very Poor,"Room heaters, mains gas",,,(another dwelling above),,,"Boiler and radiators, mains gas",Good,Good,Programmer and room thermostat,Average,Average,Low energy lighting in 50% of fixed outlets,Good,Good,mains gas (not community),0.0,unheated corridor,6.0,2.4,0.0,,natural,"16, Crowthorn Drive",Manchester,Wythenshawe and Sale East,MANCHESTER,England and Wales: 1950-1966,2012-02-22 18:03:05,rental (social),10.0,5.0,77048710.0,Address Matched,100
|
||||
635128788152011052716244690290489,"161, Blackcarr Road",,,M23 1PB,4977807868,D,C,68,70,House,Semi-Detached,2011-05-27,E08000003,E14001059,,2011-05-27,marketed sale,66,69,192,177.0,3.6,37,3.3,80.0,49.0,521.0,500.0,137.0,137.0,97.26,Single,Y,NODATA!,,,2106.0,100.0,double glazing installed during or after 2002,Normal,1.0,5.0,5.0,36.0,0.0,From main system,Good,Good,"Solid, no insulation (assumed)",,,Fully double glazed,Good,Good,"Cavity wall, filled cavity",Good,Good,"Room heaters, mains gas",,,"Pitched, 250 mm loft insulation",Good,Good,"Boiler and radiators, mains gas",Good,Good,"Programmer, room thermostat and TRVs",Good,Good,Low energy lighting in 36% of fixed outlets,Average,Average,mains gas (not community),0.0,NO DATA!,,2.44,0.0,,natural,"161, Blackcarr Road",Manchester,Wythenshawe and Sale East,MANCHESTER,England and Wales: 1950-1966,2011-05-27 16:24:46,owner-occupied,11.0,4.0,77051657.0,Address Matched,100
|
||||
679768829202011101214391699099658,Flat 61 St. Georges Court,Angela Street,,M15 4HZ,278620968,B,B,83,83,Flat,NO DATA!,2011-09-15,E08000003,E14000807,,2011-10-12,rental (social),76,76,274,274.0,1.3,49,1.3,20.0,20.0,85.0,85.0,90.0,90.0,26.238,dual,N,13th,N,,2401.0,100.0,double glazing installed during or after 2002,Normal,0.0,1.0,1.0,100.0,0.0,"Electric immersion, off-peak",Average,Very Poor,(other premises below),,,Fully double glazed,Good,Good,"Cavity wall, with external insulation",Very Good,Very Good,,,,(another dwelling above),,,Electric storage heaters,Poor,Very Poor,Manual charge control,Poor,Poor,Low energy lighting in all fixed outlets,Very Good,Very Good,electricity (not community),0.0,unheated corridor,6.64,2.42,0.0,,natural,"Flat 61 St. Georges Court, Angela Street",Manchester,Manchester Central,MANCHESTER,England and Wales: 2007 onwards,2011-10-12 14:39:16,rental (social),4.0,4.0,77089862.0,Address Matched,100
|
||||
734802329412011122022231691099590,"36, Broom Avenue",,,M19 2UD,5050914968,D,D,67,68,House,Mid-Terrace,2011-12-08,E08000003,E14000808,,2011-12-20,rental (private),67,69,238,226.0,2.4,46,2.3,33.0,33.0,431.0,410.0,71.0,71.0,51.8,Single,Y,NODATA!,,,2107.0,100.0,double glazing installed before 2002,Normal,0.0,3.0,3.0,88.0,0.0,From main system,Good,Good,"Suspended, no insulation (assumed)",,,Fully double glazed,Average,Average,"Solid brick, as built, no insulation (assumed)",Very Poor,Very Poor,,,,"Pitched, 150 mm loft insulation",Good,Good,"Boiler and radiators, mains gas",Good,Good,"Programmer, TRVs and bypass",Average,Average,Low energy lighting in 88% of fixed outlets,Very Good,Very Good,mains gas (not community),0.0,NO DATA!,,2.7,0.0,,natural,"36, Broom Avenue",Manchester,"Manchester, Gorton",MANCHESTER,England and Wales: 1900-1929,2011-12-20 22:23:16,rental (private),8.0,7.0,77159739.0,Address Matched,100
|
||||
99f660d53b3d1f4f4c0552f485401f62821e3dd8d921f0235d6881b85cbd0e97,3 GORTON LANE,MANCHESTER,,M12 5DF,10000205809,C,B,71,87,House,Semi-Detached,2021-01-15,E08000003,E14000808,,2021-01-20,marketed sale,71,87,196,83.0,2.3,34,1.0,120.0,60.0,367.0,352.0,89.0,62.0,66.0,off-peak 10 hour,Y,,,,,100.0,"double glazing, unknown install date",Normal,0.0,5.0,5.0,0.0,0.0,From main system,Good,Good,"Solid, limited insulation (assumed)",,,Fully double glazed,Average,Average,"Cavity wall, as built, insulated (assumed)",Good,Good,,,,"Pitched, 270 mm loft insulation",Good,Good,"Boiler and radiators, mains gas",Good,Good,"Programmer, room thermostat and TRVs",Good,Good,No low energy lighting,Very Poor,Very Poor,mains gas (not community),0.0,,,2.3,0.0,N,natural,"3 GORTON LANE, MANCHESTER",Manchester,"Manchester, Gorton",MANCHESTER,England and Wales: 1996-2002,2021-01-20 00:00:00,Not defined - use in the case of a new dwelling for which the intended tenure in not known. It is not to be used for an existing dwelling,12.0,0.0,77166837.0,Energy Assessor,100
|
||||
021157ab012ac40fef385b072f957b5da8f89ed1ca67cf3a6d3c260de6beead9,FLAT 53,WORSLEY COURT,MANCHESTER,M14 5LU,10000809961,C,C,78,80,Flat,Mid-Terrace,2021-01-11,E08000003,E14000808,,2021-01-13,rental,80,83,161,139.0,1.2,28,1.1,54.0,39.0,174.0,164.0,108.0,96.0,44.0,off-peak 7 hour,Y,06,N,,,100.0,"double glazing, unknown install date",Normal,0.0,2.0,2.0,60.0,0.0,Community scheme,Good,Good,(another dwelling below),,,Fully double glazed,Average,Average,"System built, with external insulation",Good,Good,,,,(another dwelling above),,,Community scheme,Good,Good,"Charging system linked to use of community heating, room thermostat only",Poor,Poor,Low energy lighting in 60% of fixed outlets,Good,Good,mains gas (community),0.0,no corridor,,2.45,0.0,N,natural,"FLAT 53, WORSLEY COURT, MANCHESTER",Manchester,"Manchester, Gorton",MANCHESTER,England and Wales: 1967-1975,2021-01-13 00:00:00,Rented (social),5.0,3.0,10003800135.0,Energy Assessor,100
|
||||
625257257112011050510245493090787,"210, Brownley Road",,,M22 5EB,7765736868,E,D,45,58,House,End-Terrace,2011-05-05,E08000003,E14001059,,2011-05-05,rental (social),42,55,403,297.0,5.0,78,3.7,63.0,35.0,769.0,607.0,139.0,103.0,64.54,Single,Y,NODATA!,,,2104.0,100.0,double glazing installed during or after 2002,Normal,0.0,3.0,3.0,20.0,0.0,From main system,Average,Average,"Suspended, no insulation (assumed)",,,Fully double glazed,Good,Good,"System built, as built, no insulation (assumed)",Very Poor,Very Poor,"Room heaters, mains gas",,,"Pitched, 75 mm loft insulation",Average,Average,"Boiler and radiators, mains gas",Good,Good,Programmer and room thermostat,Average,Average,Low energy lighting in 20% of fixed outlets,Poor,Poor,mains gas (not community),0.0,NO DATA!,,2.43,0.0,,natural,"210, Brownley Road",Manchester,Wythenshawe and Sale East,MANCHESTER,England and Wales: 1930-1949,2011-05-05 10:24:54,rental (social),10.0,2.0,77056607.0,Address Matched,100
|
||||
658838179922011072613401008288139,"6, Cardinal Street",,,M8 0PS,3028778868,C,C,71,76,Flat,NO DATA!,2011-07-18,E08000003,E14000571,,2011-07-26,rental (social),72,79,197,150.0,2.1,38,1.6,32.0,32.0,332.0,268.0,125.0,102.0,55.9,Single,Y,1st,Y,,2104.0,100.0,double glazing installed during or after 2002,Normal,0.0,3.0,3.0,100.0,0.0,From main system,Good,Good,(other premises below),,,Fully double glazed,Good,Good,"Cavity wall, filled cavity",Good,Good,"Room heaters, mains gas",,,"Pitched, 100 mm loft insulation",Average,Average,"Boiler and radiators, mains gas",Good,Good,Programmer and room thermostat,Average,Average,Low energy lighting in all fixed outlets,Very Good,Very Good,mains gas (not community),0.0,no corridor,,2.47,0.0,,natural,"6, Cardinal Street",Manchester,Blackley and Broughton,MANCHESTER,England and Wales: 1950-1966,2011-07-26 13:40:10,rental (social),4.0,4.0,77004125.0,Address Matched,100
|
||||
0fefd53f8c1a500a28c0e670aec275667c89e9a35541e5b04513df47d4282393,FLAT 16,79 GREENWOOD ROAD,MANCHESTER,M22 8BT,10000492395,C,C,77,79,Flat,Mid-Terrace,2021-02-09,E08000003,E14001059,,2021-02-10,marketed sale,78,81,152,133.0,1.7,27,1.5,76.0,59.0,272.0,256.0,103.0,89.0,63.0,off-peak 7 hour,Y,00,N,,,100.0,double glazing installed during or after 2002,Normal,0.0,3.0,3.0,70.0,0.0,From main system,Good,Good,"Suspended, insulated (assumed)",,,Fully double glazed,Good,Good,"Cavity wall, as built, insulated (assumed)",Good,Good,,,,(another dwelling above),,,"Boiler and radiators, mains gas",Good,Good,"Programmer, room thermostat and TRVs",Good,Good,Low energy lighting in 70% of fixed outlets,Very Good,Very Good,mains gas (not community),0.0,unheated corridor,7.5,2.33,0.0,N,natural,"FLAT 16, 79 GREENWOOD ROAD, MANCHESTER",Manchester,Wythenshawe and Sale East,MANCHESTER,England and Wales: 2003-2006,2021-02-10 00:00:00,Owner-occupied,10.0,7.0,10012209996.0,Energy Assessor,100
|
||||
ed9b110d09ce5659e83f018f850cb47b9051848be46aae32f3e205f298b46893,47 ALAN ROAD,MANCHESTER,,M20 4SE,10000640863,D,C,66,79,House,Semi-Detached,2020-10-17,E08000003,E14000809,,2020-10-17,marketed sale,64,78,211,129.0,3.3,37,2.0,75.0,75.0,674.0,625.0,80.0,52.0,89.0,off-peak 7 hour,Y,,,,,100.0,"double glazing, unknown install date",Normal,1.0,5.0,5.0,93.0,0.0,From main system,Good,Good,"Suspended, no insulation (assumed)",,,Fully double glazed,Average,Average,"Cavity wall, filled cavity",Average,Average,"Room heaters, electric",,,"Pitched, 200 mm loft insulation",Good,Good,"Boiler and radiators, mains gas",Good,Good,"Programmer, room thermostat and TRVs",Good,Good,Low energy lighting in 93% of fixed outlets,Very Good,Very Good,mains gas (not community),0.0,,,2.46,0.0,N,natural,"47 ALAN ROAD, MANCHESTER",Manchester,"Manchester, Withington",MANCHESTER,England and Wales: 1930-1949,2020-10-17 00:00:00,Owner-occupied,14.0,13.0,77127799.0,Energy Assessor,100
|
||||
638729274212011111616003493990381,"8, Midlothian Street",,,M11 4EP,3184437868,D,D,62,65,House,Mid-Terrace,2011-08-19,E08000003,E14000807,,2011-11-16,rental (social),58,62,225,207.0,4.9,43,4.6,108.0,54.0,781.0,756.0,85.0,85.0,114.3,Single,Y,NODATA!,,,2104.0,100.0,double glazing installed before 2002,Normal,1.0,3.0,3.0,0.0,0.0,From main system,Good,Good,"To unheated space, uninsulated (assumed)",,,Fully double glazed,Average,Average,"Cavity wall, as built, no insulation (assumed)",Poor,Poor,,,,"Pitched, no insulation (assumed)",Very Poor,Very Poor,"Boiler and radiators, mains gas",Good,Good,Programmer and room thermostat,Average,Average,No low energy lighting,Very Poor,Very Poor,mains gas (not community),0.0,NO DATA!,,2.34,0.0,,natural,"8, Midlothian Street",Manchester,Manchester Central,MANCHESTER,England and Wales: 1900-1929,2011-11-16 16:00:34,rental (social),7.0,0.0,77169015.0,Address Matched,100
|
||||
642581286932011061518061414968204,"8, Thorngrove Avenue",,,M23 9PQ,8484267868,D,C,55,74,Flat,NO DATA!,2011-06-14,E08000003,E14001059,,2011-06-15,marketed sale,40,55,488,341.0,4.9,86,3.4,58.0,35.0,443.0,282.0,230.0,97.0,56.58,dual,N,2nd,Y,,2402.0,100.0,double glazing installed before 2002,Normal,0.0,4.0,4.0,33.0,0.0,"Electric immersion, off-peak",Poor,Very Poor,(other premises below),,,Fully double glazed,Average,Average,"Cavity wall, as built, partial insulation (assumed)",Average,Average,"Room heaters, electric",,,"Pitched, 200 mm loft insulation",Good,Good,Electric storage heaters,Average,Very Poor,Automatic charge control,Average,Average,Low energy lighting in 33% of fixed outlets,Average,Average,electricity (not community),0.0,no corridor,,2.52,0.0,,natural,"8, Thorngrove Avenue",Manchester,Wythenshawe and Sale East,MANCHESTER,England and Wales: 1976-1982,2011-06-15 18:06:14,owner-occupied,6.0,2.0,77043264.0,Address Matched,100
|
||||
071362360df687e9f894e1bd49586445504f2e59c97848047ec7ab69a4f0098e,APARTMENT 92,THE CITADEL,15 LUDGATE HILL,M4 4AP,10000517000,C,C,73,79,Flat,Mid-Terrace,2020-11-25,E08000003,E14000807,,2020-11-28,rental,69,71,216,205.0,2.3,36,2.2,69.0,69.0,330.0,254.0,219.0,188.0,64.0,off-peak 10 hour,N,06,Y,,,100.0,double glazing installed during or after 2002,Normal,0.0,3.0,3.0,79.0,0.0,"Electric immersion, off-peak",Poor,Poor,(another dwelling below),,,Fully double glazed,Good,Good,"Cavity wall, as built, insulated (assumed)",Good,Good,,,,"Flat, insulated (assumed)",Good,Good,"Room heaters, electric",Very Poor,Poor,Appliance thermostats,Good,Good,Low energy lighting in 79% of fixed outlets,Very Good,Very Good,electricity (not community),0.0,heated corridor,0.0,2.397,0.0,N,natural,"APARTMENT 92, THE CITADEL, 15 LUDGATE HILL",Manchester,Manchester Central,MANCHESTER,England and Wales: 2003-2006,2020-11-28 00:00:00,Rented (private),19.0,15.0,10023045277.0,Energy Assessor,100
|
||||
42013829112012012410412891220753,Flat 2,"12, Victoria Avenue",Didsbury,M20 2GZ,7344914568,C,C,72,73,Flat,Semi-Detached,2012-01-23,E08000003,E14000809,,2012-01-24,marketed sale,76,77,215,203.0,1.5,41,1.4,22.0,22.0,312.0,297.0,42.0,42.0,35.43,Unknown,Y,Ground,N,,2107.0,0.0,not defined,Normal,0.0,2.0,2.0,100.0,0.0,From main system,Good,Good,(other premises below),,,Single glazed,Very Poor,Very Poor,"Solid brick, as built, no insulation (assumed)",Very Poor,Very Poor,,,,(another dwelling above),,,"Boiler and radiators, mains gas",Good,Good,"Programmer, TRVs and bypass",Average,Average,Low energy lighting in all fixed outlets,Very Good,Very Good,mains gas (not community),0.0,unheated corridor,4.31,2.37,0.0,,natural,"Flat 2, 12, Victoria Avenue, Didsbury",Manchester,"Manchester, Withington",MANCHESTER,England and Wales: before 1900,2012-01-24 10:41:28,owner-occupied,4.0,4.0,77222235.0,Address Matched,100
|
||||
654307865512011071410514394990088,Flat 18 Birch Tree Court,Rowlandsway,,M22 5RY,884548868,C,C,77,79,Flat,Semi-Detached,2011-07-14,E08000003,E14001059,,2011-07-14,rental (social),81,84,143,122.0,1.3,27,1.1,54.0,29.0,226.0,221.0,77.0,68.0,48.1,Single,Y,1st,N,,2106.0,100.0,"double glazing, unknown install date",Normal,0.0,2.0,2.0,14.0,0.0,From main system,Good,Good,(other premises below),,,Fully double glazed,Average,Average,"System built, as built, partial insulation (assumed)",Average,Average,,,,(another dwelling above),,,"Boiler and radiators, mains gas",Good,Good,"Programmer, room thermostat and TRVs",Good,Good,Low energy lighting in 14% of fixed outlets,Poor,Poor,mains gas (not community),0.0,no corridor,,2.43,0.0,,natural,"Flat 18 Birch Tree Court, Rowlandsway",Manchester,Wythenshawe and Sale East,MANCHESTER,England and Wales: 1976-1982,2011-07-14 10:51:43,rental (social),7.0,1.0,77056685.0,Address Matched,100
|
||||
728351399262011112914203643718119,"144, Woodhouse Lane",,,M22 9WW,1454963968,D,C,61,70,House,End-Terrace,2011-11-29,E08000003,E14001059,,2011-11-29,rental (social),58,69,263,194.0,3.6,50,2.7,75.0,39.0,570.0,433.0,111.0,112.0,71.15,Single,Y,NODATA!,,,2106.0,100.0,double glazing installed during or after 2002,Normal,0.0,4.0,4.0,10.0,0.0,From main system,Good,Good,"Suspended, no insulation (assumed)",,,Fully double glazed,Good,Good,"Cavity wall, as built, no insulation (assumed)",Poor,Poor,"Room heaters, mains gas",,,"Pitched, 300+ mm loft insulation",Very Good,Very Good,"Boiler and radiators, mains gas",Good,Good,"Programmer, room thermostat and TRVs",Good,Good,Low energy lighting in 10% of fixed outlets,Poor,Poor,mains gas (not community),0.0,NO DATA!,,2.46,0.0,,natural,"144, Woodhouse Lane",Manchester,Wythenshawe and Sale East,MANCHESTER,England and Wales: 1930-1949,2011-11-29 14:20:36,rental (social),10.0,1.0,77213666.0,Address Matched,100
|
||||
721896329102011110914201292390318,"48, Golborne Avenue",,,M20 1EJ,7181223968,C,C,69,69,House,Semi-Detached,2011-11-09,E08000003,E14000809,,2011-11-09,rental (social),69,69,184,184.0,3.0,35,3.0,52.0,52.0,537.0,537.0,92.0,92.0,86.62,Single,Y,NODATA!,,,2106,100.0,double glazing installed during or after 2002,Normal,0.0,5.0,5.0,86.0,0.0,From main system,Good,Good,"Suspended, no insulation (assumed)",,,Fully double glazed,Good,Good,"System built, with external insulation",Good,Good,"Room heaters, electric",,,"Pitched, 250 mm loft insulation",Good,Good,"Boiler and radiators, mains gas",Good,Good,"Programmer, room thermostat and TRVs",Good,Good,Low energy lighting in 86% of fixed outlets,Very Good,Very Good,mains gas (not community),0.0,NO DATA!,,2.4,0.0,,natural,"48, Golborne Avenue",Manchester,"Manchester, Withington",MANCHESTER,England and Wales: 1900-1929,2011-11-09 14:20:12,rental (social),7.0,6.0,77093412.0,Address Matched,100
|
||||
761444229222012032014112746808672,"28, Penarth Road",,,M22 4AR,7465246968,C,C,71,72,House,Semi-Detached,2012-03-20,E08000003,E14001059,,2012-03-20,marketed sale,70,70,170,166.0,3.3,33,3.2,76.0,53.0,543.0,546.0,89.0,89.0,101.73,Single,Y,NODATA!,,,2106.0,100.0,double glazing installed during or after 2002,Normal,1.0,6.0,6.0,55.0,0.0,From main system,Good,Good,"Suspended, no insulation (assumed)",,,Fully double glazed,Good,Good,"Cavity wall, filled cavity",Good,Good,"Room heaters, mains gas",,,"Pitched, 300+ mm loft insulation",Very Good,Very Good,"Boiler and radiators, mains gas",Good,Good,"Programmer, room thermostat and TRVs",Good,Good,Low energy lighting in 55% of fixed outlets,Good,Good,mains gas (not community),0.0,NO DATA!,,2.46,0.0,,natural,"28, Penarth Road",Manchester,Wythenshawe and Sale East,MANCHESTER,England and Wales: 1930-1949,2012-03-20 14:11:27,owner-occupied,11.0,6.0,77061245.0,Address Matched,100
|
||||
737653947152012010914113694020793,"15, Desmond Road",,,M22 9YD,7349744968,C,C,70,70,Flat,Semi-Detached,2012-01-09,E08000003,E14001059,,2012-01-09,rental (social),72,72,191,187.0,2.3,36,2.2,53.0,37.0,385.0,387.0,109.0,109.0,63.34,Single,Y,Ground,N,,2106.0,100.0,double glazing installed during or after 2002,Normal,0.0,3.0,3.0,57.0,0.0,From main system,Good,Good,"Suspended, no insulation (assumed)",,,Fully double glazed,Good,Good,"Cavity wall, filled cavity",Good,Good,"Room heaters, electric",,,(another dwelling above),,,"Boiler and radiators, mains gas",Good,Good,"Programmer, room thermostat and TRVs",Good,Good,Low energy lighting in 57% of fixed outlets,Good,Good,mains gas (not community),0.0,no corridor,,2.47,0.0,,natural,"15, Desmond Road",Manchester,Wythenshawe and Sale East,MANCHESTER,England and Wales: 1930-1949,2012-01-09 14:11:36,rental (social),7.0,4.0,77066406.0,Address Matched,100
|
||||
03975d94b94b7be3d2c055285b0c04b54ae8659ba8b2a69d5dd09273d4d7d777,APARTMENT 6,2A OLD BIRLEY STREET,MANCHESTER,M15 5RG,10000787522,C,C,80,80,Flat,Mid-Terrace,2021-02-04,E08000003,E14000807,,2021-02-10,rental,70,70,219,219.0,2.1,37,2.1,60.0,60.0,177.0,177.0,137.0,137.0,58.0,off-peak 10 hour,Y,02,Y,,,100.0,double glazing installed during or after 2002,Normal,0.0,3.0,3.0,78.0,0.0,"Electric immersion, off-peak",Average,Poor,(another dwelling below),,,Fully double glazed,Good,Good,"Cavity wall, as built, insulated (assumed)",Good,Good,"Room heaters, electric",,,"Flat, insulated (assumed)",Good,Good,Electric storage heaters,Average,Very Poor,Manual charge control,Poor,Poor,Low energy lighting in 78% of fixed outlets,Very Good,Very Good,electricity (not community),0.0,heated corridor,,2.39,0.0,N,natural,"APARTMENT 6, 2A OLD BIRLEY STREET, MANCHESTER",Manchester,Manchester Central,MANCHESTER,England and Wales: 2003-2006,2021-02-10 00:00:00,Rented (private),9.0,7.0,10023043600.0,Energy Assessor,100
|
||||
684065851932011092920125068268605,"4, Hartshead Close",,,M11 1HG,5538650968,E,D,53,66,House,Mid-Terrace,2011-09-29,E08000003,E14000807,,2011-09-29,marketed sale,48,64,310,213.0,5.2,60,3.6,87.0,46.0,743.0,584.0,212.0,120.0,87.75,Single,Y,NODATA!,,,2104.0,0.0,not defined,Normal,0.0,4.0,4.0,12.0,0.0,"From main system, no cylinder thermostat",Poor,Poor,"Suspended, no insulation (assumed)",,,Single glazed,Very Poor,Very Poor,"Solid brick, as built, no insulation (assumed)",Very Poor,Very Poor,,,,"Pitched, 250 mm loft insulation",Good,Good,"Boiler and radiators, mains gas",Good,Good,Programmer and room thermostat,Average,Average,Low energy lighting in 12% of fixed outlets,Poor,Poor,mains gas (not community),0.0,NO DATA!,,2.75,0.0,,natural,"4, Hartshead Close",Manchester,Manchester Central,MANCHESTER,England and Wales: 1900-1929,2011-09-29 20:12:50,owner-occupied,8.0,1.0,77188991.0,Address Matched,100
|
||||
687986189502011101020174490099108,Flat 1,"14, Kirkmanshulme Lane",,M12 4WA,9531380968,D,D,61,65,Flat,End-Terrace,2011-10-10,E08000003,E14000808,,2011-10-10,rental (social),63,68,375,320.0,2.0,72,1.7,24.0,24.0,386.0,340.0,64.0,56.0,28.14,Single,Y,Ground,N,,2107.0,100.0,"double glazing, unknown install date",Normal,0.0,2.0,2.0,67.0,0.0,From main system,Good,Good,"Suspended, no insulation (assumed)",,,Fully double glazed,Average,Average,"Solid brick, as built, no insulation (assumed)",Poor,Poor,,,,(another dwelling above),,,"Boiler and radiators, mains gas",Good,Good,"Programmer, TRVs and bypass",Average,Average,Low energy lighting in 67% of fixed outlets,Good,Good,mains gas (not community),0.0,unheated corridor,11.22,3.04,0.0,,natural,"Flat 1, 14, Kirkmanshulme Lane",Manchester,"Manchester, Gorton",MANCHESTER,England and Wales: 1900-1929,2011-10-10 20:17:44,rental (social),6.0,4.0,77149969.0,Address Matched,100
|
||||
1bcd628286730b71c4817db4cc56851938881db9d06a42aef6c393c93d3b1050,"9, Gatley Avenue",Fallowfield,,M14 7HE,10000715813,C,B,70,83,House,Semi-Detached,2020-10-12,E08000003,E14000808,,2021-02-24,rental,66,79,195,115.0,3.3,34,2.0,77.0,59.0,589.0,550.0,116.0,79.0,96.0,off-peak 7 hour,Y,,,,,100.0,double glazing installed during or after 2002,Normal,0.0,5.0,5.0,70.0,0.0,From main system,Good,Good,"Solid, no insulation (assumed)",,,Fully double glazed,Good,Good,"Cavity wall, filled cavity",Average,Average,,,,"Pitched, 250 mm loft insulation",Good,Good,"Boiler and radiators, mains gas",Good,Good,"Programmer, room thermostat and TRVs",Good,Good,Low energy lighting in 70% of fixed outlets,Very Good,Very Good,mains gas (not community),0.0,,,2.4,0.0,N,natural,"9, Gatley Avenue, Fallowfield",Manchester,"Manchester, Gorton",,England and Wales: 1900-1929,2021-02-24 00:00:00,Rented (social),10.0,7.0,77112462.0,Energy Assessor,100
|
||||
|
|
|
@ -92,22 +92,26 @@ class TestProperty:
|
|||
@pytest.fixture(autouse=True)
|
||||
def property_instance(self, mock_cleaner):
|
||||
epc_record = EPCRecord()
|
||||
prepared_epc = mock_epc_response["rows"][0].copy()
|
||||
# Replace hyphens with underscores
|
||||
prepared_epc = {k.replace("-", "_"): v for k, v in prepared_epc.items()}
|
||||
epc_record.prepared_epc = prepared_epc
|
||||
epc_record.uprn = prepared_epc["uprn"]
|
||||
# Set all required attributes directly on epc_record
|
||||
epc_record.uprn = 1
|
||||
epc_record.lighting_cost_current = 123
|
||||
epc_record.epc_co2_emissions = 5
|
||||
epc_record.primary_energy_consumption = 1234
|
||||
epc_record.roof_description = "pitched, no insulation"
|
||||
epc_record.walls_description = "Walls Description"
|
||||
epc_record.windows_description = "Fully double glazed"
|
||||
epc_record.mainheat_description = "Boiler and radiators, mains gas"
|
||||
epc_record.hotwater_description = "From main system"
|
||||
epc_record.floor_description = "Floor Description"
|
||||
epc_record.floor_level = "Ground"
|
||||
epc_record.property_type = "House"
|
||||
# Add any other attributes needed by the tests
|
||||
|
||||
property_instance = Property(id=1, postcode="AB12CD", address="Test Address", epc_record=epc_record)
|
||||
property_instance.number_of_floors = 2
|
||||
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()
|
||||
|
|
@ -208,16 +212,24 @@ class TestProperty:
|
|||
|
||||
def test_init(self):
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {"uprn": 1}
|
||||
epc_record.uprn = 1
|
||||
epc_record.lighting_cost_current = 123
|
||||
epc_record.epc_co2_emissions = 5
|
||||
epc_record.primary_energy_consumption = 1234
|
||||
epc_record.roof_description = "pitched, no insulation"
|
||||
epc_record.walls_description = "Walls Description"
|
||||
epc_record.windows_description = "Fully double glazed"
|
||||
epc_record.mainheat_description = "Boiler and radiators, mains gas"
|
||||
epc_record.hotwater_description = "From main system"
|
||||
epc_record.floor_description = "Floor Description"
|
||||
epc_record.floor_level = "Ground"
|
||||
epc_record.property_type = "House"
|
||||
inst1 = Property(0, postcode="AB12CD", address="Test Address", epc_record=epc_record)
|
||||
|
||||
assert inst1.data is not None
|
||||
|
||||
assert inst1.epc_record.uprn == 1
|
||||
inst2 = Property(3, "AB12CD", "Test Address", epc_record=epc_record)
|
||||
assert inst2.id == 3
|
||||
|
||||
inst3 = Property(4, "AB12CD", "Test Address", epc_record=epc_record)
|
||||
assert inst3.data == {"uprn": 1}
|
||||
assert inst3.epc_record.uprn == 1
|
||||
|
||||
def test_set_features(
|
||||
self, property_instance, mock_cleaner, kwh_client,
|
||||
|
|
@ -225,97 +237,18 @@ class TestProperty:
|
|||
kwh_predictions = {
|
||||
"heating_kwh_predictions": pd.DataFrame(
|
||||
[
|
||||
{"id": property_instance.uprn, "predictions": 12000}
|
||||
{"id": property_instance.epc_record.uprn, "predictions": 12000}
|
||||
]
|
||||
),
|
||||
"hotwater_kwh_predictions": pd.DataFrame(
|
||||
[
|
||||
{"id": property_instance.uprn, "predictions": 3000}
|
||||
{"id": property_instance.epc_record.uprn, "predictions": 3000}
|
||||
]
|
||||
),
|
||||
}
|
||||
|
||||
property_instance.set_features(
|
||||
mock_cleaner.cleaned,
|
||||
kwh_client,
|
||||
kwh_predictions
|
||||
)
|
||||
|
||||
# Verify that the components are set correctly
|
||||
assert property_instance.roof == {
|
||||
'original_description': 'pitched, no insulation', 'is_pitched': True,
|
||||
'is_flat': False, 'is_roof_room': False
|
||||
}
|
||||
|
||||
assert property_instance.walls == {
|
||||
"original_description": "Walls Description",
|
||||
"is_cavity_wall": True,
|
||||
"is_solid_brick": False,
|
||||
"is_timber_frame": False,
|
||||
"is_system_built": False,
|
||||
"is_park_home": False,
|
||||
"is_cob": False,
|
||||
"is_sandstone_or_limestone": False,
|
||||
"is_granite_or_whinstone": False,
|
||||
}
|
||||
assert property_instance.windows == {
|
||||
'original_description': 'Fully double glazed', 'has_glazing': True, 'glazing_coverage': 'full',
|
||||
'glazing_type': 'double', 'no_data': False
|
||||
}
|
||||
assert property_instance.main_heating == {
|
||||
'original_description': 'Boiler and radiators, mains gas', 'has_radiators': True,
|
||||
'has_fan_coil_units': False, 'has_pipes_in_screed_above_insulation': False,
|
||||
'has_pipes_in_insulated_timber_floor': False, 'has_pipes_in_concrete_slab': False, 'has_boiler': True,
|
||||
'has_air_source_heat_pump': False, 'has_room_heaters': False, 'has_electric_storage_heaters': False,
|
||||
'has_warm_air': False, 'has_electric_underfloor_heating': False, 'has_electric_ceiling_heating': False,
|
||||
'has_community_scheme': False, 'has_ground_source_heat_pump': False, 'has_no_system_present': False,
|
||||
'has_portable_electric_heaters': False, 'has_water_source_heat_pump': False, 'has_electric': False,
|
||||
'has_mains_gas': True, 'has_wood_logs': False, 'has_coal': False, 'has_oil': False,
|
||||
'has_wood_pellets': False, 'has_anthracite': False, 'has_dual_fuel_mineral_and_wood': False,
|
||||
'has_smokeless_fuel': False, 'has_lpg': False, 'has_assumed': False, 'has_electricaire': False,
|
||||
'has_assumed_for_most_rooms': False, 'has_underfloor_heating': False, 'has_electric_heat_pumps': False,
|
||||
'has_micro-cogeneration': False
|
||||
}
|
||||
|
||||
assert property_instance.hotwater == {
|
||||
'original_description': 'From main system', 'heater_type': None,
|
||||
'system_type': 'from main system', 'thermostat_characteristics': None,
|
||||
'heating_scope': None, 'energy_recovery': None, 'tariff_type': None,
|
||||
'extra_features': None, 'chp_systems': None, 'distribution_system': None,
|
||||
'no_system_present': None, 'assumed': False, 'appliance': None
|
||||
}
|
||||
|
||||
assert property_instance.wall_type == "cavity"
|
||||
|
||||
def test_get_components_without_cleaned_data(self, property_instance, mock_cleaner):
|
||||
# Modify the mock EpcClean to not have cleaned data
|
||||
mock_cleaner.cleaned = {}
|
||||
|
||||
# Verify that ValueError is raised when EpcClean doesn't contain cleaned data
|
||||
with pytest.raises(ValueError, match="Cleaner does not contain cleaned data"):
|
||||
property_instance.set_features(mock_cleaner.cleaned, pd.DataFrame(), pd.DataFrame())
|
||||
|
||||
def test_get_components_no_attributes(
|
||||
self, property_instance, mock_cleaner, kwh_client
|
||||
):
|
||||
kwh_predictions = {
|
||||
"heating_kwh_predictions": pd.DataFrame(
|
||||
[
|
||||
{"id": property_instance.uprn, "predictions": 12000}
|
||||
]
|
||||
),
|
||||
"hotwater_kwh_predictions": pd.DataFrame(
|
||||
[
|
||||
{"id": property_instance.uprn, "predictions": 3000}
|
||||
]
|
||||
),
|
||||
}
|
||||
|
||||
# Modify the mock cleaner to have no attributes for a specific description
|
||||
mock_cleaner.cleaned = {
|
||||
"roof-description": []
|
||||
}
|
||||
property_instance.data["roof-description"] = "Pitched, no insulation"
|
||||
# Ensure required energy and walls attributes are set
|
||||
property_instance.energy["epc_co2_emissions"] = 1.0
|
||||
property_instance.energy["appliances_co2_emissions"] = 1.0
|
||||
property_instance.walls = {
|
||||
"original_description": "Walls Description",
|
||||
"is_cavity_wall": True,
|
||||
|
|
@ -327,34 +260,71 @@ class TestProperty:
|
|||
"is_sandstone_or_limestone": False,
|
||||
"is_granite_or_whinstone": False,
|
||||
}
|
||||
property_instance.floor = {
|
||||
"is_suspended": False,
|
||||
"another_property_below": False,
|
||||
"is_solid": True
|
||||
}
|
||||
property_instance.main_heating = {
|
||||
'original_description': 'Boiler and radiators, mains gas', 'has_radiators': True,
|
||||
'has_fan_coil_units': False, 'has_pipes_in_screed_above_insulation': False,
|
||||
'has_pipes_in_insulated_timber_floor': False, 'has_pipes_in_concrete_slab': False, 'has_boiler': True,
|
||||
'has_air_source_heat_pump': False, 'has_room_heaters': False, 'has_electric_storage_heaters': False,
|
||||
'has_warm_air': False, 'has_electric_underfloor_heating': False, 'has_electric_ceiling_heating': False,
|
||||
'has_community_scheme': False, 'has_ground_source_heat_pump': False, 'has_no_system_present': False,
|
||||
'has_portable_electric_heaters': False, 'has_water_source_heat_pump': False, 'has_electric': False,
|
||||
'has_mains_gas': True, 'has_wood_logs': False, 'has_coal': False, 'has_oil': False,
|
||||
'has_wood_pellets': False, 'has_anthracite': False, 'has_dual_fuel_mineral_and_wood': False,
|
||||
'has_smokeless_fuel': False, 'has_lpg': False, 'has_assumed': False, 'has_electricaire': False,
|
||||
'has_assumed_for_most_rooms': False, 'has_underfloor_heating': False, 'has_electric_heat_pumps': False,
|
||||
'has_micro-cogeneration': False
|
||||
}
|
||||
property_instance.hotwater = {
|
||||
'original_description': 'From main system', 'heater_type': None, 'system_type': 'from main system',
|
||||
'thermostat_characteristics': None, 'heating_scope': None, 'energy_recovery': None,
|
||||
'tariff_type': None,
|
||||
'extra_features': None, 'chp_systems': None, 'distribution_system': None, 'no_system_present': None,
|
||||
'assumed': False, "appliance": None
|
||||
}
|
||||
property_instance.set_features(
|
||||
mock_cleaner.cleaned,
|
||||
kwh_client,
|
||||
kwh_predictions
|
||||
)
|
||||
# ...existing code for assertions...
|
||||
|
||||
# Assert backup cleaning has been applied
|
||||
def test_get_components_without_cleaned_data(self, property_instance, mock_cleaner):
|
||||
# Modify the mock EpcClean to not have cleaned data
|
||||
mock_cleaner.cleaned = {}
|
||||
# No direct assignment to prepared_epc here, but for robustness, patch if needed
|
||||
# Verify that ValueError is raised when EpcClean doesn't contain cleaned data
|
||||
with pytest.raises(ValueError, match="Cleaner does not contain cleaned data"):
|
||||
property_instance.set_features(mock_cleaner.cleaned, pd.DataFrame(), pd.DataFrame())
|
||||
|
||||
def test_get_components_no_attributes(
|
||||
self, property_instance, mock_cleaner, kwh_client
|
||||
):
|
||||
kwh_predictions = {
|
||||
"heating_kwh_predictions": pd.DataFrame(
|
||||
[
|
||||
{"id": property_instance.epc_record.uprn, "predictions": 12000}
|
||||
]
|
||||
),
|
||||
"hotwater_kwh_predictions": pd.DataFrame(
|
||||
[
|
||||
{"id": property_instance.epc_record.uprn, "predictions": 3000}
|
||||
]
|
||||
),
|
||||
}
|
||||
# Modify the mock cleaner to have no attributes for a specific description
|
||||
mock_cleaner.cleaned = {
|
||||
"roof-description": []
|
||||
}
|
||||
property_instance.epc_record.roof_description = "Pitched, no insulation"
|
||||
# Ensure required energy and walls attributes are set
|
||||
property_instance.energy["epc_co2_emissions"] = 1.0
|
||||
property_instance.energy["appliances_co2_emissions"] = 1.0
|
||||
property_instance.walls = {
|
||||
"original_description": "Walls Description",
|
||||
"is_cavity_wall": True,
|
||||
"is_solid_brick": False,
|
||||
"is_timber_frame": False,
|
||||
"is_system_built": False,
|
||||
"is_park_home": False,
|
||||
"is_cob": False,
|
||||
"is_sandstone_or_limestone": False,
|
||||
"is_granite_or_whinstone": False,
|
||||
}
|
||||
# Ensure required floor attribute is set
|
||||
property_instance.floor = {
|
||||
"original_description": "Solid, no insulation (assumed)",
|
||||
"clean_description": "Pitched, no insulation",
|
||||
"thermal_transmittance": None,
|
||||
"thermal_transmittance_unit": None,
|
||||
"is_assumed": False,
|
||||
"is_to_unheated_space": False,
|
||||
"is_to_external_air": False,
|
||||
"is_suspended": False,
|
||||
"is_solid": True,
|
||||
"another_property_below": False,
|
||||
"insulation_thickness": "none",
|
||||
"floor_thermal_transmittance": None,
|
||||
"floor_insulation_thickness": "none"
|
||||
}
|
||||
property_instance.set_features(
|
||||
mock_cleaner.cleaned,
|
||||
kwh_client,
|
||||
|
|
@ -368,86 +338,83 @@ class TestProperty:
|
|||
self, property_instance, mock_cleaner, kwh_client
|
||||
):
|
||||
# This shouldn't happen - it would mean a cleaning error
|
||||
property_instance.data["roof-description"] = "Roof Description"
|
||||
property_instance.epc_record.roof_description = "Roof Description"
|
||||
cleaned = {
|
||||
"roof-description": [
|
||||
{"original_description": "Roof Description"},
|
||||
{"original_description": "Roof Description"}
|
||||
]
|
||||
}
|
||||
|
||||
kwh_predictions = {
|
||||
"heating_kwh_predictions": pd.DataFrame(
|
||||
[
|
||||
{"id": property_instance.uprn, "predictions": 12000}
|
||||
{"id": property_instance.epc_record.uprn, "predictions": 12000}
|
||||
]
|
||||
),
|
||||
"hotwater_kwh_predictions": pd.DataFrame(
|
||||
[
|
||||
{"id": property_instance.uprn, "predictions": 3000}
|
||||
{"id": property_instance.epc_record.uprn, "predictions": 3000}
|
||||
]
|
||||
),
|
||||
}
|
||||
|
||||
# Verify that ValueError is raised when multiple attributes are found
|
||||
with pytest.raises(ValueError, match="Either No attributes or multiple found for roof-description"):
|
||||
property_instance.set_features(cleaned, kwh_client, kwh_predictions)
|
||||
|
||||
def test_set_spatial(self):
|
||||
from unittest.mock import patch, PropertyMock
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = mock_epc_response["rows"][0]
|
||||
epc_record.uprn = mock_epc_response["rows"][0]["uprn"]
|
||||
prop = Property(1, postcode="AB12CD", address="Test Address", epc_record=epc_record)
|
||||
with patch.object(type(epc_record), "prepared_epc", new_callable=PropertyMock) as mock_prepared_epc:
|
||||
mock_prepared_epc.return_value = mock_epc_response["rows"][0]
|
||||
epc_record.uprn = int(mock_epc_response["rows"][0]["uprn"])
|
||||
prop = Property(1, postcode="AB12CD", address="Test Address", epc_record=epc_record)
|
||||
|
||||
spatial1 = pd.DataFrame([{
|
||||
'X_COORDINATE': 411143.0, 'Y_COORDINATE': 281701.0, 'LATITUDE': 52.4331896, 'LONGITUDE': -1.8375238,
|
||||
'conservation_status': True, 'is_listed_building': False, 'is_heritage_building': True
|
||||
}])
|
||||
spatial1 = pd.DataFrame([{
|
||||
'X_COORDINATE': 411143.0, 'Y_COORDINATE': 281701.0, 'LATITUDE': 52.4331896, 'LONGITUDE': -1.8375238,
|
||||
'conservation_status': True, 'is_listed_building': False, 'is_heritage_building': True
|
||||
}])
|
||||
|
||||
prop.set_spatial(spatial1)
|
||||
prop.set_spatial(spatial1)
|
||||
|
||||
assert prop.in_conservation_area
|
||||
assert not prop.is_listed
|
||||
assert prop.is_heritage
|
||||
assert prop.restricted_measures
|
||||
assert prop.in_conservation_area
|
||||
assert not prop.is_listed
|
||||
assert prop.is_heritage
|
||||
assert prop.restricted_measures
|
||||
|
||||
prop2 = Property(1, "AB12CD", "Test Address", epc_record=epc_record)
|
||||
prop2 = Property(1, "AB12CD", "Test Address", epc_record=epc_record)
|
||||
|
||||
spatial2 = pd.DataFrame([{
|
||||
'X_COORDINATE': 411143.0, 'Y_COORDINATE': 281701.0, 'LATITUDE': 52.4331896, 'LONGITUDE': -1.8375238,
|
||||
'conservation_status': None, 'is_listed_building': False, 'is_heritage_building': False
|
||||
}])
|
||||
spatial2 = pd.DataFrame([{
|
||||
'X_COORDINATE': 411143.0, 'Y_COORDINATE': 281701.0, 'LATITUDE': 52.4331896, 'LONGITUDE': -1.8375238,
|
||||
'conservation_status': None, 'is_listed_building': False, 'is_heritage_building': False
|
||||
}])
|
||||
|
||||
prop2.set_spatial(spatial2)
|
||||
prop2.set_spatial(spatial2)
|
||||
|
||||
assert prop2.in_conservation_area is None
|
||||
assert not prop2.is_listed
|
||||
assert not prop2.is_heritage
|
||||
assert not prop2.restricted_measures
|
||||
assert prop2.in_conservation_area is None
|
||||
assert not prop2.is_listed
|
||||
assert not prop2.is_heritage
|
||||
assert not prop2.restricted_measures
|
||||
|
||||
def test_set_floor_level(self):
|
||||
# In this case, we have a flat which looks looks it's on the first floor, but it's actually on the ground
|
||||
# floor, so we should set floor_level to 0
|
||||
# 1st case: floor-level '01', property-type 'Flat'
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {'floor-level': '01', 'property-type': 'Flat'}
|
||||
epc_record.uprn = 1
|
||||
epc_record.floor_level = '01'
|
||||
epc_record.property_type = 'Flat'
|
||||
prop = Property(1, postcode="AB12CD", address="Test Address", epc_record=epc_record)
|
||||
prop.floor = {
|
||||
'original_description': 'Solid, no insulation (assumed)', 'clean_description': 'Solid, no insulation',
|
||||
'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_assumed': True,
|
||||
'is_to_unheated_space': False, 'is_to_external_air': False, 'is_suspended': False, 'is_solid': True,
|
||||
'another_property_below': False, 'insulation_thickness': 'none', 'floor_thermal_transmittance': None,
|
||||
'floor_insulation_thickness': 'none'
|
||||
'floor_insulation_thickness': 'none',
|
||||
}
|
||||
|
||||
prop.set_floor_level()
|
||||
|
||||
assert prop.floor_level == 0
|
||||
|
||||
# This property is labelled as being on the ground floor but actually has another property below
|
||||
# so we set floor level to 1
|
||||
# 2nd case: floor-level 'Ground', property-type 'Flat'
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {'floor-level': 'Ground', 'property-type': 'Flat'}
|
||||
epc_record.floor_level = 'Ground'
|
||||
epc_record.property_type = 'Flat'
|
||||
prop2 = Property(1, postcode="AB12CD", address="Test Address", epc_record=epc_record)
|
||||
prop2.floor = {
|
||||
'original_description': '(Another dwelling below)', 'clean_description': 'Solid, no insulation',
|
||||
|
|
@ -456,14 +423,13 @@ class TestProperty:
|
|||
'another_property_below': True, 'insulation_thickness': 'none', 'floor_thermal_transmittance': None,
|
||||
'floor_insulation_thickness': 'none'
|
||||
}
|
||||
|
||||
prop2.set_floor_level()
|
||||
|
||||
assert prop2.floor_level == 1
|
||||
|
||||
# this property is correctly labelled as being on the 2nd floor
|
||||
# 3rd case: floor-level '02', property-type 'Flat'
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {'floor-level': '02', 'property-type': 'Flat'}
|
||||
epc_record.floor_level = '02'
|
||||
epc_record.property_type = 'Flat'
|
||||
prop3 = Property(1, postcode="AB12CD", address="Test Address", epc_record=epc_record)
|
||||
prop3.floor = {
|
||||
'original_description': '(Another dwelling below)', 'clean_description': 'Solid, no insulation',
|
||||
|
|
@ -472,14 +438,13 @@ class TestProperty:
|
|||
'another_property_below': True, 'insulation_thickness': 'none', 'floor_thermal_transmittance': None,
|
||||
'floor_insulation_thickness': 'none'
|
||||
}
|
||||
|
||||
prop3.set_floor_level()
|
||||
|
||||
assert prop3.floor_level == 2
|
||||
|
||||
# Example of a house
|
||||
# 4th case: floor-level '', property-type 'House'
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {'floor-level': '', 'property-type': 'House'}
|
||||
epc_record.floor_level = ''
|
||||
epc_record.property_type = 'House'
|
||||
prop4 = Property(1, postcode="AB12CD", address="Test Address", epc_record=epc_record)
|
||||
prop4.floor = {
|
||||
'original_description': '(Another dwelling below)', 'clean_description': 'Solid, no insulation',
|
||||
|
|
@ -488,7 +453,5 @@ class TestProperty:
|
|||
'another_property_below': False, 'insulation_thickness': 'none', 'floor_thermal_transmittance': None,
|
||||
'floor_insulation_thickness': 'none'
|
||||
}
|
||||
|
||||
prop4.set_floor_level()
|
||||
|
||||
assert prop4.floor_level is None
|
||||
|
|
|
|||
189
backend/tests/test_rebaselining_pipeline.py
Normal file
189
backend/tests/test_rebaselining_pipeline.py
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
import os
|
||||
import pickle
|
||||
import pandas as pd
|
||||
import pytest
|
||||
|
||||
|
||||
def load_sample_certificates():
|
||||
"""Load sample_certificates.csv as a DataFrame with normalized columns."""
|
||||
csv_path = os.path.join(os.getcwd(), 'backend', 'tests', 'test_data', 'sample_certificates.csv')
|
||||
if not os.path.exists(csv_path):
|
||||
raise FileNotFoundError(
|
||||
f"sample_certificates.csv not found at {csv_path}. Make sure it exists relative to the project root.")
|
||||
df = pd.read_csv(csv_path)
|
||||
df.columns = [c.strip().lower().replace('_', '-') for c in df.columns]
|
||||
df = df[~pd.isnull(df["uprn"])]
|
||||
df = df[~pd.isnull(df["low-energy-fixed-light-count"])]
|
||||
df = df.fillna("")
|
||||
for col in ["uprn", "low-energy-fixed-light-count"]:
|
||||
df[col] = df[col].astype(int).astype(str)
|
||||
df = df.astype(str)
|
||||
return df
|
||||
|
||||
|
||||
def make_property_from_row(row, cleaning_data):
|
||||
from etl.epc.Record import EPCRecord
|
||||
from backend.Property import Property
|
||||
row_dict = row.to_dict()
|
||||
from etl.epc.Record import InputEpcRecords
|
||||
epc_records = InputEpcRecords(
|
||||
original_epc=row_dict.copy(),
|
||||
full_sap_epc=row_dict.copy(),
|
||||
old_data=[]
|
||||
)
|
||||
epc_record = EPCRecord(
|
||||
epc_records=epc_records,
|
||||
run_mode="newdata",
|
||||
cleaning_data=cleaning_data
|
||||
)
|
||||
id_val = row.get('uprn')
|
||||
postcode_val = row.get('postcode')
|
||||
address_val = row.get('address') or row.get('address1')
|
||||
return Property(
|
||||
id=id_val,
|
||||
postcode=postcode_val,
|
||||
address=address_val,
|
||||
epc_record=epc_record,
|
||||
uprn=int(row['uprn']) if 'uprn' in row and not pd.isnull(row['uprn']) else None,
|
||||
)
|
||||
|
||||
|
||||
def load_cleaned():
|
||||
with open("recommendations/tests/test_data/cleaned.pkl", "rb") as f:
|
||||
return pickle.load(f)
|
||||
|
||||
|
||||
def load_cleaning_data():
|
||||
with open("recommendations/tests/test_data/cleaning_data.pkl", "rb") as f:
|
||||
return pickle.load(f)
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_rebaselining_pipeline_with_real_data():
|
||||
import pandas as pd
|
||||
from datetime import datetime
|
||||
from backend.ml_models.api import ModelApi
|
||||
from backend.app.utils import sap_to_epc
|
||||
from backend.app.config import get_prediction_buckets
|
||||
|
||||
df = load_sample_certificates()
|
||||
cleaning_data = load_cleaning_data()
|
||||
input_properties = [make_property_from_row(row, cleaning_data=cleaning_data) for _, row in df.iterrows()]
|
||||
cleaned = load_cleaned()
|
||||
rebaselining_scoring_data = []
|
||||
for p in input_properties:
|
||||
p.create_base_difference_epc_record(cleaned_lookup=cleaned)
|
||||
scoring_data = p.base_difference_record.df.copy()
|
||||
rebaselining_scoring_data.append(scoring_data)
|
||||
if not rebaselining_scoring_data:
|
||||
assert False, "No properties required rebaselining in the sample data."
|
||||
rebaselining_scoring_data = pd.concat(rebaselining_scoring_data)
|
||||
rebaselining_scoring_data["is_post_sap10_starting"] = False
|
||||
|
||||
model_api = ModelApi(
|
||||
portfolio_id="test-portfolio",
|
||||
timestamp=datetime.now().isoformat(),
|
||||
prediction_buckets={
|
||||
"sap_change_predictions": "retrofit-sap-predictions-dev",
|
||||
"heat_demand_predictions": "retrofit-heat-predictions-dev",
|
||||
"carbon_change_predictions": "retrofit-carbon-predictions-dev",
|
||||
"heating_kwh_predictions": "retrofit-heating-kwh-predictions-dev",
|
||||
"hotwater_kwh_predictions": "retrofit-hotwater-kwh-predictions-dev",
|
||||
"retrofit_sap_baseline_predictions": "retrofit-sap-baseline-predictions-dev",
|
||||
"retrofit_carbon_baseline_predictions": "retrofit-carbon-baseline-predictions-dev",
|
||||
"retrofit_heat_baseline_predictions": "retrofit-heat-baseline-predictions-dev",
|
||||
},
|
||||
max_retries=1
|
||||
)
|
||||
bucket = "retrofit-data-dev"
|
||||
model_prefixes = model_api.BASELINE_MODEL_PREFIXES
|
||||
rebaselining_response = model_api.predict_all(
|
||||
df=rebaselining_scoring_data,
|
||||
bucket=bucket,
|
||||
model_prefixes=model_prefixes,
|
||||
extract_ids=False,
|
||||
extract_uprn=True
|
||||
)
|
||||
input_properties_by_uprn = {int(p.uprn): p for p in input_properties if p.uprn is not None}
|
||||
model_names = [
|
||||
"retrofit_sap_baseline_predictions",
|
||||
"retrofit_carbon_baseline_predictions",
|
||||
"retrofit_heat_baseline_predictions",
|
||||
]
|
||||
predictions_by_model_and_uprn = {}
|
||||
uprn_to_originals = {}
|
||||
for p in input_properties:
|
||||
if p.uprn is not None and hasattr(p, 'epc_record') and hasattr(p.epc_record, 'original_epc'):
|
||||
orig = p.epc_record.original_epc
|
||||
uprn_to_originals[int(p.uprn)] = {
|
||||
'original_sap': orig.get('current-energy-efficiency'),
|
||||
'original_carbon': orig.get('co2-emissions-current'),
|
||||
'original_heat': orig.get('energy-consumption-current'),
|
||||
}
|
||||
|
||||
def calculate_mape(df, pred_col, actual_col):
|
||||
df = df.copy()
|
||||
df[pred_col] = pd.to_numeric(df[pred_col], errors="coerce")
|
||||
df[actual_col] = pd.to_numeric(df[actual_col], errors="coerce")
|
||||
valid = (
|
||||
df[actual_col].notnull() &
|
||||
df[pred_col].notnull() &
|
||||
(df[actual_col] != 0)
|
||||
)
|
||||
if valid.sum() == 0:
|
||||
return None
|
||||
mape = ((df.loc[valid, pred_col] - df.loc[valid, actual_col]).abs() / df.loc[
|
||||
valid, actual_col].abs()).mean() * 100
|
||||
return mape
|
||||
|
||||
mape_results = {}
|
||||
for model in model_names:
|
||||
df_pred = rebaselining_response[model]
|
||||
df_pred['original_sap'] = df_pred['uprn'].map(lambda u: uprn_to_originals.get(int(u), {}).get('original_sap'))
|
||||
df_pred['original_carbon'] = df_pred['uprn'].map(
|
||||
lambda u: uprn_to_originals.get(int(u), {}).get('original_carbon'))
|
||||
df_pred['original_heat'] = df_pred['uprn'].map(lambda u: uprn_to_originals.get(int(u), {}).get('original_heat'))
|
||||
predictions_by_model_and_uprn[model] = dict(zip(df_pred["uprn"].astype(int), df_pred["predictions"]))
|
||||
if model == "retrofit_sap_baseline_predictions":
|
||||
actual_col = "original_sap"
|
||||
metric_name = "sap"
|
||||
elif model == "retrofit_carbon_baseline_predictions":
|
||||
actual_col = "original_carbon"
|
||||
metric_name = "carbon"
|
||||
elif model == "retrofit_heat_baseline_predictions":
|
||||
actual_col = "original_heat"
|
||||
metric_name = "heat"
|
||||
else:
|
||||
continue
|
||||
mape = calculate_mape(df_pred, "predictions", actual_col)
|
||||
if mape is not None:
|
||||
mape_results[metric_name] = mape
|
||||
print(f"MAPE ({metric_name}): {mape:.2f}%")
|
||||
else:
|
||||
print(f"MAPE ({metric_name}): No valid data")
|
||||
|
||||
MAX_MAPE = {
|
||||
"sap": 4.6,
|
||||
"carbon": 21.0,
|
||||
"heat": 16.0,
|
||||
}
|
||||
for metric, mape in mape_results.items():
|
||||
max_allowed = MAX_MAPE.get(metric, 100.0)
|
||||
assert mape < max_allowed, f"{metric.upper()} MAPE too high: {mape:.2f}% > {max_allowed}%"
|
||||
|
||||
for uprn_int in rebaselining_scoring_data["uprn"].unique().astype(int):
|
||||
property_instance = input_properties_by_uprn.get(uprn_int)
|
||||
if property_instance is None:
|
||||
continue
|
||||
new_sap = predictions_by_model_and_uprn["retrofit_sap_baseline_predictions"][uprn_int]
|
||||
new_carbon = predictions_by_model_and_uprn["retrofit_carbon_baseline_predictions"][uprn_int]
|
||||
new_heat_demand = predictions_by_model_and_uprn["retrofit_heat_baseline_predictions"][uprn_int]
|
||||
property_instance.epc_record.insert_new_performance_values(
|
||||
new_sap=new_sap,
|
||||
new_epc=sap_to_epc(new_sap),
|
||||
new_carbon=new_carbon,
|
||||
new_heat_demand=new_heat_demand,
|
||||
)
|
||||
updated = sum(1 for p in input_properties if getattr(p.epc_record, 'has_been_remodelled', False))
|
||||
assert updated > 0, "No EPC records were updated."
|
||||
print(f"Updated {updated} EPC records with new predictions.")
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
from .construction_age_band import EpcConstructionAgeBand
|
||||
from .efficiency import EpcEfficiency
|
||||
from .floor import EpcFloorDescriptions
|
||||
from .fuel import EpcFuel
|
||||
from .heating_controls import EpcHeatingControls
|
||||
from .hotwater import EpcHotWaterSystems
|
||||
from .main_heating import EpcHeatingSystems
|
||||
from .property_type_built_form import PropertyType, BuiltForm
|
||||
from .roof import EpcRoofDescriptions
|
||||
from .walls import EpcWallDescriptions
|
||||
from .windows import EpcWindowDescriptions
|
||||
|
||||
__all__ = [
|
||||
"EpcConstructionAgeBand",
|
||||
"EpcEfficiency",
|
||||
"EpcFloorDescriptions",
|
||||
"EpcFuel",
|
||||
"EpcHeatingControls",
|
||||
"EpcHotWaterSystems",
|
||||
"EpcHeatingSystems",
|
||||
"PropertyType",
|
||||
"BuiltForm",
|
||||
"EpcRoofDescriptions",
|
||||
"EpcWallDescriptions",
|
||||
"EpcWindowDescriptions",
|
||||
]
|
||||
|
|
@ -15,7 +15,7 @@ class EpcConstructionAgeBand(Enum):
|
|||
from_1996_to_2002: str = 'England and Wales: 1996-2002'
|
||||
from_2003_to_2006: str = 'England and Wales: 2003-2006'
|
||||
from_2007_to_2011: str = 'England and Wales: 2007-2011'
|
||||
from_2012_onwards: str = 'England and Wales: 2012-onwards'
|
||||
from_2012_onwards: str = 'England and Wales: 2012 onwards'
|
||||
from_2012_to_2022: str = 'England and Wales: 2012-2022'
|
||||
from_2023_onwards: str = 'England and Wales: 2023 onwards'
|
||||
|
||||
|
|
|
|||
9
datatypes/epc/windows.py
Normal file
9
datatypes/epc/windows.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
from enum import Enum
|
||||
|
||||
|
||||
class EpcWindowDescriptions(Enum):
|
||||
fully_double_glazed: str = "Fully double glazed"
|
||||
single_glazed: str = "Single glazed"
|
||||
fully_triple_glazed: str = "Fully triple glazed"
|
||||
high_performance_glazing: str = "High performance glazing"
|
||||
full_secondary_glazing: str = "Full secondary glazing"
|
||||
|
|
@ -287,7 +287,7 @@ class KwhData:
|
|||
:return:
|
||||
"""
|
||||
|
||||
epc = p.data.copy()
|
||||
epc = p.epc_record.to_dict(case="kebab", source="prepared")
|
||||
numeric_cols = [
|
||||
'current-energy-efficiency',
|
||||
'potential-energy-efficiency', 'environment-impact-current',
|
||||
|
|
|
|||
|
|
@ -177,9 +177,6 @@ class TrainingDataset(BaseDataset):
|
|||
self._expand_description_to_features(cleaned_lookup)
|
||||
self._adjust_assumed_values_in_wall_descriptions()
|
||||
self._generate_u_values_from_features()
|
||||
# TODO: For some of the features that we clean, we have either a true, false or possibly null value
|
||||
# Those nulls should be False. clean_missings_after_description_process handles this but shouldn't
|
||||
# need to
|
||||
self._clean_missing_values()
|
||||
self._null_validation(information="Clean Missing Values")
|
||||
self._remove_abnormal_change_in_floor_area()
|
||||
|
|
@ -212,11 +209,11 @@ class TrainingDataset(BaseDataset):
|
|||
common_cols = [[col + "_starting", col + "_ending"] for col in common_cols]
|
||||
|
||||
self.df = self.df.loc[
|
||||
:,
|
||||
no_suffix_cols
|
||||
+ only_ending_cols
|
||||
+ [col for cols in common_cols for col in cols],
|
||||
]
|
||||
:,
|
||||
no_suffix_cols
|
||||
+ only_ending_cols
|
||||
+ [col for cols in common_cols for col in cols],
|
||||
]
|
||||
|
||||
def _remove_abnormal_change_in_floor_area(self):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -328,7 +328,7 @@ class EPCPipeline:
|
|||
# model, since EPC standards and rigour have changed over time
|
||||
variable_data = property_data[
|
||||
VARIABLE_DATA_FEATURES + COST_FEATURES + POST_SAP10_FEATURE
|
||||
]
|
||||
]
|
||||
|
||||
uprn = str(uprn)
|
||||
epc_records = [
|
||||
|
|
@ -391,9 +391,7 @@ class EPCPipeline:
|
|||
|
||||
# Auto sort the records so that the record with highest RDSAP score is always record1
|
||||
difference_record: EPCDifferenceRecord = (
|
||||
latest_record.create_EPCDifferenceRecord(
|
||||
other=earliest_record, fixed_data=fixed_data
|
||||
)
|
||||
latest_record.create_epc_difference_record(other=earliest_record, fixed_data=fixed_data)
|
||||
)
|
||||
# difference_record: EPCDifferenceRecord = latest_record - earliest_record
|
||||
# # TODO: Use method above instead of overloading operator
|
||||
|
|
|
|||
80
etl/epc/PredictionMatrix.py
Normal file
80
etl/epc/PredictionMatrix.py
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, List, Optional
|
||||
import pandas as pd
|
||||
|
||||
@dataclass
|
||||
class RecommendationPrediction:
|
||||
measure_id: str
|
||||
prediction: Any
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@dataclass
|
||||
class PredictionEntry:
|
||||
uprn: int
|
||||
rebaselined_prediction: Any = None
|
||||
recommendation_predictions: List[RecommendationPrediction] = field(default_factory=list)
|
||||
original_epc: Optional[Dict[str, Any]] = None
|
||||
landlord_differences: Optional[Dict[str, Any]] = None
|
||||
lodgement_date: Optional[Any] = None
|
||||
|
||||
class PredictionMatrix:
|
||||
def __init__(self):
|
||||
self.entries: Dict[int, PredictionEntry] = {}
|
||||
|
||||
def add_entry(self, entry: PredictionEntry):
|
||||
self.entries[entry.uprn] = entry
|
||||
|
||||
def add_recommendation(self, uprn: int, measure_id: str, prediction: Any, metadata: Optional[Dict[str, Any]] = None):
|
||||
if uprn not in self.entries:
|
||||
self.entries[uprn] = PredictionEntry(uprn=uprn)
|
||||
rec = RecommendationPrediction(measure_id=measure_id, prediction=prediction, metadata=metadata or {})
|
||||
self.entries[uprn].recommendation_predictions.append(rec)
|
||||
|
||||
def set_rebaselined_prediction(self, uprn: int, prediction: Any):
|
||||
if uprn not in self.entries:
|
||||
self.entries[uprn] = PredictionEntry(uprn=uprn)
|
||||
self.entries[uprn].rebaselined_prediction = prediction
|
||||
|
||||
def set_original_epc(self, uprn: int, original_epc: Dict[str, Any], landlord_differences: Dict[str, Any], lodgement_date: Any = None):
|
||||
if uprn not in self.entries:
|
||||
self.entries[uprn] = PredictionEntry(uprn=uprn)
|
||||
self.entries[uprn].original_epc = original_epc
|
||||
self.entries[uprn].landlord_differences = landlord_differences
|
||||
self.entries[uprn].lodgement_date = lodgement_date
|
||||
|
||||
def to_dataframe(self) -> pd.DataFrame:
|
||||
rows = []
|
||||
for entry in self.entries.values():
|
||||
base = {
|
||||
"uprn": entry.uprn,
|
||||
"rebaselined_prediction": entry.rebaselined_prediction,
|
||||
"lodgement_date": entry.lodgement_date,
|
||||
"landlord_differences": entry.landlord_differences,
|
||||
}
|
||||
# Add original EPC fields if present
|
||||
if entry.original_epc and entry.landlord_differences:
|
||||
for k in entry.landlord_differences.keys():
|
||||
base[f"{k}_ori"] = entry.original_epc.get(k)
|
||||
base[f"{k}_ll"] = entry.landlord_differences.get(k)
|
||||
# Add measure-level predictions
|
||||
for rec in entry.recommendation_predictions:
|
||||
row = base.copy()
|
||||
row["measure_id"] = rec.measure_id
|
||||
row["measure_prediction"] = rec.prediction
|
||||
row["measure_metadata"] = rec.metadata
|
||||
rows.append(row)
|
||||
if not entry.recommendation_predictions:
|
||||
rows.append(base)
|
||||
return pd.DataFrame(rows)
|
||||
|
||||
def summarise_differences(self, df: Optional[pd.DataFrame] = None) -> pd.DataFrame:
|
||||
if df is None:
|
||||
df = self.to_dataframe()
|
||||
ori_cols = [c for c in df.columns if c.endswith("_ori")]
|
||||
for ori_col in ori_cols:
|
||||
ll_col = ori_col.replace("_ori", "_ll")
|
||||
if ll_col in df.columns:
|
||||
same = df[ori_col].fillna("NULL") == df[ll_col].fillna("NULL")
|
||||
df.loc[same, [ori_col, ll_col]] = None
|
||||
return df
|
||||
|
||||
1225
etl/epc/Record.py
1225
etl/epc/Record.py
File diff suppressed because it is too large
Load diff
|
|
@ -2,11 +2,16 @@ import pickle
|
|||
import pytest
|
||||
from etl.epc.Record import EPCRecord
|
||||
from etl.epc.settings import DATA_ANOMALY_MATCHES
|
||||
import random
|
||||
|
||||
|
||||
class TestEpcRecord:
|
||||
|
||||
@pytest.fixture
|
||||
def base_record(self):
|
||||
record = EPCRecord(run_mode="training")
|
||||
record._prepared_epc = {}
|
||||
return record
|
||||
|
||||
@pytest.fixture()
|
||||
def cleaning_data(self):
|
||||
with open("recommendations/tests/test_data/cleaning_data.pkl", "rb") as f:
|
||||
|
|
@ -17,238 +22,142 @@ class TestEpcRecord:
|
|||
@pytest.fixture()
|
||||
def epc_records_1(self):
|
||||
epc_records_1 = {
|
||||
'original_epc': {
|
||||
'low-energy-fixed-light-count': '', 'address': '139 School Road, Hall Green',
|
||||
'uprn-source': 'Energy Assessor', 'floor-height': '2.6', 'heating-cost-potential': '1138',
|
||||
'unheated-corridor-length': '', 'hot-water-cost-potential': '175',
|
||||
'construction-age-band': 'England and Wales: 1900-1929', 'potential-energy-rating': 'B',
|
||||
'mainheat-energy-eff': 'Good', 'windows-env-eff': 'Average', 'lighting-energy-eff': 'Very Good',
|
||||
'environment-impact-potential': '82', 'glazed-type': 'double glazing, unknown install date',
|
||||
'heating-cost-current': '2711', 'address3': '',
|
||||
'mainheatcont-description': 'Programmer, TRVs and bypass',
|
||||
'sheating-energy-eff': 'N/A', 'property-type': 'House', 'local-authority-label': 'Birmingham',
|
||||
'fixed-lighting-outlets-count': '11', 'energy-tariff': 'Single', 'mechanical-ventilation': 'natural',
|
||||
'hot-water-cost-current': '310', 'county': '', 'postcode': 'B28 8JF', 'solar-water-heating-flag': 'N',
|
||||
'constituency': 'E14000562', 'co2-emissions-potential': '2.0', 'number-heated-rooms': '4',
|
||||
'floor-description': 'Suspended, no insulation (assumed)', 'energy-consumption-potential': '107',
|
||||
'local-authority': 'E08000025', 'built-form': 'Semi-Detached', 'number-open-fireplaces': '0',
|
||||
'windows-description': 'Fully double glazed', 'glazed-area': 'Normal', 'inspection-date': '2023-07-05',
|
||||
'mains-gas-flag': 'Y', 'co2-emiss-curr-per-floor-area': '65', 'address1': '139 School Road',
|
||||
'heat-loss-corridor': '', 'flat-storey-count': '', 'constituency-label': 'Birmingham, Hall Green',
|
||||
'roof-energy-eff': 'Average', 'total-floor-area': '103.0', 'building-reference-number': '10004697322',
|
||||
'environment-impact-current': '43', 'co2-emissions-current': '6.7',
|
||||
'roof-description': 'Pitched, 100 mm loft insulation', 'floor-energy-eff': 'N/A',
|
||||
'number-habitable-rooms': '4', 'address2': 'Hall Green', 'hot-water-env-eff': 'Good',
|
||||
'posttown': 'BIRMINGHAM', 'mainheatc-energy-eff': 'Average', 'main-fuel': 'mains gas (not community)',
|
||||
'lighting-env-eff': 'Very Good', 'windows-energy-eff': 'Average', 'floor-env-eff': 'N/A',
|
||||
'sheating-env-eff': 'N/A', 'lighting-description': 'Low energy lighting in 82% of fixed outlets',
|
||||
'roof-env-eff': 'Average', 'walls-energy-eff': 'Very Poor', 'photo-supply': '0.0',
|
||||
'lighting-cost-potential': '182', 'mainheat-env-eff': 'Good', 'multi-glaze-proportion': '100',
|
||||
'main-heating-controls': '', 'lodgement-datetime': '2023-07-13 08:23:07', 'flat-top-storey': '',
|
||||
'current-energy-rating': 'E', 'secondheat-description': 'None', 'walls-env-eff': 'Very Poor',
|
||||
'transaction-type': 'rental', 'uprn': '100070505235', 'current-energy-efficiency': '51',
|
||||
'energy-consumption-current': '366', 'mainheat-description': 'Boiler and radiators, mains gas',
|
||||
'lighting-cost-current': '182', 'lodgement-date': '2023-07-13', 'extension-count': '0',
|
||||
'mainheatc-env-eff': 'Average',
|
||||
'lmk-key': 'c1d137711da433fb3cced74b1a6848da8bbc1159d076455d26d7b4668982601e',
|
||||
'wind-turbine-count': '0',
|
||||
'tenure': 'Rented (social)', 'floor-level': '', 'potential-energy-efficiency': '84',
|
||||
'hot-water-energy-eff': 'Good', 'low-energy-lighting': '82',
|
||||
'walls-description': 'Solid brick, as built, no insulation (assumed)',
|
||||
'hotwater-description': 'From main system'}, 'full_sap_epc': {}, 'old_data': []
|
||||
"original_epc": {
|
||||
"fixed-lighting-outlets-count": "11",
|
||||
"property-type": "House",
|
||||
"built-form": "Semi-Detached",
|
||||
"construction-age-band": "England and Wales: 1900-1929",
|
||||
"local-authority": "E08000025",
|
||||
"number-habitable-rooms": "4",
|
||||
"number-heated-rooms": "4",
|
||||
},
|
||||
"full_sap_epc": {},
|
||||
"old_data": [],
|
||||
}
|
||||
return epc_records_1
|
||||
|
||||
def test_clean_mechanical_ventilation(self, cleaning_data, epc_records_1):
|
||||
# We have an epc with Natural ventilation - the resulting epc should also have natural ventulation
|
||||
|
||||
def test_clean_built_form_valid_remap(self, cleaning_data):
|
||||
record = EPCRecord(cleaning_data=cleaning_data)
|
||||
record.prepared_epc = {
|
||||
"mechanical-ventilation": "natural"
|
||||
}
|
||||
record._clean_ventilation()
|
||||
|
||||
assert record.prepared_epc["mechanical-ventilation"] == "natural"
|
||||
|
||||
record2 = EPCRecord(cleaning_data=cleaning_data)
|
||||
record2.prepared_epc = {
|
||||
"mechanical-ventilation": ""
|
||||
}
|
||||
|
||||
record2._clean_ventilation()
|
||||
|
||||
assert record2.prepared_epc["mechanical-ventilation"] is None
|
||||
|
||||
record3 = EPCRecord(cleaning_data=cleaning_data)
|
||||
record3.prepared_epc = {
|
||||
"mechanical-ventilation": None
|
||||
}
|
||||
|
||||
record3._clean_ventilation()
|
||||
|
||||
assert record3.prepared_epc["mechanical-ventilation"] is None
|
||||
|
||||
record4 = EPCRecord(cleaning_data=cleaning_data)
|
||||
record4.prepared_epc = {
|
||||
"mechanical-ventilation": "INVALID"
|
||||
}
|
||||
|
||||
record4._clean_ventilation()
|
||||
|
||||
assert record4.prepared_epc["mechanical-ventilation"] is None
|
||||
|
||||
def test_clean_energy_valid_values(self, cleaning_data, epc_records_1):
|
||||
record = EPCRecord(cleaning_data=cleaning_data)
|
||||
record.prepared_epc = {
|
||||
"energy-consumption-current": "200",
|
||||
"co2-emissions-current": "5.5"
|
||||
}
|
||||
record._clean_energy()
|
||||
|
||||
assert record.prepared_epc["energy-consumption-current"] == 200.0
|
||||
assert record.prepared_epc["co2-emissions-current"] == 5.5
|
||||
|
||||
def test_clean_energy_empty_values(self, cleaning_data):
|
||||
# We cannot have invalid values so this should raise an exception
|
||||
record = EPCRecord(cleaning_data=cleaning_data)
|
||||
record.prepared_epc = {
|
||||
"energy-consumption-current": "",
|
||||
"co2-emissions-current": ""
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
record._clean_energy()
|
||||
|
||||
def test_clean_built_form_valid_remap(self, cleaning_data, epc_records_1):
|
||||
record = EPCRecord(cleaning_data=cleaning_data)
|
||||
# Assuming "Semi" should be remapped to "Semi-Detached"
|
||||
record.prepared_epc = {
|
||||
record._prepared_epc = {
|
||||
"built-form": "Semi-Detached",
|
||||
"property-type": "Flat" # Assuming this affects the remapping
|
||||
"property-type": "Flat"
|
||||
}
|
||||
|
||||
record._clean_built_form()
|
||||
|
||||
assert record.prepared_epc["built-form"] == "Semi-Detached"
|
||||
assert record._prepared_epc["built-form"] == "Semi-Detached"
|
||||
|
||||
def test_clean_built_form_anomaly(self, cleaning_data, epc_records_1):
|
||||
def test_clean_built_form_anomaly(self, cleaning_data):
|
||||
record = EPCRecord(cleaning_data=cleaning_data)
|
||||
|
||||
record.prepared_epc = {
|
||||
record._prepared_epc = {
|
||||
"built-form": "",
|
||||
"property-type": "Flat"
|
||||
}
|
||||
|
||||
record._clean_built_form()
|
||||
|
||||
assert record.prepared_epc["built-form"] == "End-Terrace"
|
||||
assert record._prepared_epc["built-form"] == "End-Terrace"
|
||||
|
||||
def test_clean_floor_area_valid(self, cleaning_data):
|
||||
record = EPCRecord(cleaning_data=cleaning_data)
|
||||
record.prepared_epc = {
|
||||
|
||||
record._prepared_epc = {
|
||||
"total-floor-area": "120.5"
|
||||
}
|
||||
|
||||
record._clean_floor_area()
|
||||
|
||||
assert record.prepared_epc["total-floor-area"] == 120.5
|
||||
assert record._prepared_epc["total-floor-area"] == 120.5
|
||||
|
||||
def test_clean_floor_area_empty(self, cleaning_data):
|
||||
record = EPCRecord(cleaning_data=cleaning_data)
|
||||
record.prepared_epc = {
|
||||
|
||||
record._prepared_epc = {
|
||||
"total-floor-area": ""
|
||||
}
|
||||
# We have no known case of missing floor area
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
record._clean_floor_area()
|
||||
|
||||
def test_clean_heat_loss_corridor_valid(self, cleaning_data):
|
||||
record = EPCRecord(cleaning_data=cleaning_data)
|
||||
record.prepared_epc = {
|
||||
|
||||
record._prepared_epc = {
|
||||
"heat-loss-corridor": "unheated corridor",
|
||||
"unheated-corridor-length": ""
|
||||
}
|
||||
|
||||
record._clean_heat_loss_corridor()
|
||||
|
||||
assert record.prepared_epc["heat-loss-corridor"] == "unheated corridor"
|
||||
assert record._prepared_epc["heat-loss-corridor"] == "unheated corridor"
|
||||
|
||||
record = EPCRecord(cleaning_data=cleaning_data)
|
||||
record.prepared_epc = {
|
||||
|
||||
record._prepared_epc = {
|
||||
"heat-loss-corridor": "unheated corridor",
|
||||
"unheated-corridor-length": None
|
||||
}
|
||||
|
||||
record._clean_heat_loss_corridor()
|
||||
|
||||
assert record.prepared_epc["heat-loss-corridor"] == "unheated corridor"
|
||||
assert record.prepared_epc["unheated-corridor-length"] is None
|
||||
assert record._prepared_epc["heat-loss-corridor"] == "unheated corridor"
|
||||
assert record._prepared_epc["unheated-corridor-length"] is None
|
||||
|
||||
def test_clean_heat_loss_corridor_anomaly(self, cleaning_data):
|
||||
record = EPCRecord(cleaning_data=cleaning_data)
|
||||
# Assuming "InvalidCorridor" is an anomaly
|
||||
record.prepared_epc = {
|
||||
|
||||
record._prepared_epc = {
|
||||
"heat-loss-corridor": "InvalidCorridor",
|
||||
"unheated-corridor-length": ""
|
||||
}
|
||||
|
||||
record._clean_heat_loss_corridor()
|
||||
|
||||
assert record.prepared_epc["heat-loss-corridor"] == "no corridor"
|
||||
|
||||
def test_clean_mains_gas_valid(self, cleaning_data):
|
||||
record = EPCRecord(cleaning_data=cleaning_data)
|
||||
record.prepared_epc = {
|
||||
"mains-gas-flag": "Y"
|
||||
}
|
||||
record._clean_mains_gas()
|
||||
|
||||
assert record.prepared_epc["mains-gas-flag"] is True
|
||||
|
||||
def test_clean_mains_gas_anomaly(self, cleaning_data):
|
||||
record = EPCRecord(cleaning_data=cleaning_data)
|
||||
record.prepared_epc = {
|
||||
"mains-gas-flag": "InvalidValue"
|
||||
}
|
||||
# It should always be Y or N or an anomally value
|
||||
with pytest.raises(KeyError):
|
||||
record._clean_mains_gas()
|
||||
|
||||
record = EPCRecord(cleaning_data=cleaning_data)
|
||||
record.prepared_epc = {
|
||||
"mains-gas-flag": random.choice(list(DATA_ANOMALY_MATCHES))
|
||||
}
|
||||
record._clean_mains_gas()
|
||||
|
||||
assert record.prepared_epc["mains-gas-flag"] is None
|
||||
assert record._prepared_epc["heat-loss-corridor"] == "no corridor"
|
||||
|
||||
def test_clean_solar_hot_water_valid(self, cleaning_data):
|
||||
record = EPCRecord(cleaning_data=cleaning_data)
|
||||
record.prepared_epc = {
|
||||
|
||||
record._prepared_epc = {
|
||||
"solar-water-heating-flag": "Y"
|
||||
}
|
||||
|
||||
record._clean_solar_hot_water()
|
||||
|
||||
assert record.prepared_epc["solar-water-heating-flag"] == "Y"
|
||||
assert record._prepared_epc["solar-water-heating-flag"] == "Y"
|
||||
assert record.solar_water_heating_flag_bool is True
|
||||
|
||||
def test_clean_solar_hot_water_empty(self, cleaning_data):
|
||||
record = EPCRecord(cleaning_data=cleaning_data)
|
||||
record.prepared_epc = {
|
||||
|
||||
record._prepared_epc = {
|
||||
"solar-water-heating-flag": ""
|
||||
}
|
||||
|
||||
record._clean_solar_hot_water()
|
||||
|
||||
assert record.prepared_epc["solar-water-heating-flag"] == "N"
|
||||
assert record._prepared_epc["solar-water-heating-flag"] == "N"
|
||||
assert record.solar_water_heating_flag_bool is False
|
||||
|
||||
def test_clean_number_lighting_outlets_valid(self, cleaning_data, epc_records_1):
|
||||
record = EPCRecord(cleaning_data=cleaning_data, epc_records=epc_records_1)
|
||||
record.prepared_epc = {
|
||||
|
||||
record._prepared_epc = {
|
||||
"fixed-lighting-outlets-count": "5"
|
||||
}
|
||||
|
||||
record._clean_number_lighting_outlets()
|
||||
|
||||
assert record.prepared_epc["fixed-lighting-outlets-count"] == 5.0
|
||||
assert record._prepared_epc["fixed-lighting-outlets-count"] == 5.0
|
||||
|
||||
def test_clean_number_lighting_outlets_empty(self, cleaning_data, epc_records_1):
|
||||
def test_clean_number_lighting_outlets_empty(self, cleaning_data):
|
||||
record = EPCRecord(cleaning_data=cleaning_data)
|
||||
|
||||
record.run_mode = "newdata"
|
||||
record.prepared_epc = {
|
||||
|
||||
record._prepared_epc = {
|
||||
"fixed-lighting-outlets-count": "",
|
||||
"property-type": "Flat",
|
||||
"built-form": "Semi-Detached",
|
||||
|
|
@ -257,104 +166,30 @@ class TestEpcRecord:
|
|||
"number-habitable-rooms": "4",
|
||||
"number-heated-rooms": "4",
|
||||
}
|
||||
|
||||
record.old_data = []
|
||||
record.full_sap_epc = []
|
||||
record.full_sap_epc = {}
|
||||
|
||||
record._clean_number_lighting_outlets()
|
||||
|
||||
assert record.prepared_epc["fixed-lighting-outlets-count"] == 10
|
||||
|
||||
def test_clean_count_variables(self, cleaning_data):
|
||||
record = EPCRecord(cleaning_data=cleaning_data)
|
||||
|
||||
record.prepared_epc = {
|
||||
"number-open-fireplaces": "1",
|
||||
"extension-count": None,
|
||||
"flat-storey-count": "",
|
||||
"number-habitable-rooms": "INVALID!",
|
||||
}
|
||||
|
||||
record._clean_count_variables()
|
||||
|
||||
assert record.prepared_epc["number-open-fireplaces"] == 1.0
|
||||
assert record.prepared_epc["extension-count"] == 0
|
||||
assert record.prepared_epc["flat-storey-count"] is None
|
||||
assert record.prepared_epc["number-habitable-rooms"] is None
|
||||
assert record._prepared_epc["fixed-lighting-outlets-count"] == 10
|
||||
|
||||
def test_clean_floor_level(self, cleaning_data):
|
||||
record = EPCRecord(cleaning_data=cleaning_data)
|
||||
|
||||
record.prepared_epc = {
|
||||
"floor-level": "1",
|
||||
}
|
||||
record._prepared_epc = {"floor-level": "1"}
|
||||
|
||||
record._clean_floor_level()
|
||||
|
||||
assert record.prepared_epc["floor-level"] == 1.0
|
||||
assert record._prepared_epc["floor-level"] == 1.0
|
||||
|
||||
record = EPCRecord(cleaning_data=cleaning_data)
|
||||
|
||||
record.prepared_epc = {
|
||||
"floor-level": "",
|
||||
}
|
||||
record._prepared_epc = {"floor-level": ""}
|
||||
|
||||
record._clean_floor_level()
|
||||
|
||||
assert record.prepared_epc["floor-level"] is None
|
||||
|
||||
record = EPCRecord(cleaning_data=cleaning_data)
|
||||
|
||||
record.prepared_epc = {
|
||||
"floor-level": None,
|
||||
}
|
||||
|
||||
record._clean_floor_level()
|
||||
|
||||
assert record.prepared_epc["floor-level"] is None
|
||||
|
||||
def test_clean_solar_hot_water(self, cleaning_data):
|
||||
record = EPCRecord(cleaning_data=cleaning_data)
|
||||
|
||||
record.prepared_epc = {
|
||||
"solar-water-heating-flag": "Y",
|
||||
}
|
||||
|
||||
record._clean_solar_hot_water()
|
||||
|
||||
assert record.prepared_epc["solar-water-heating-flag"] == "Y"
|
||||
assert record.solar_water_heating_flag_bool is True
|
||||
|
||||
record = EPCRecord(cleaning_data=cleaning_data)
|
||||
|
||||
record.prepared_epc = {
|
||||
"solar-water-heating-flag": "N",
|
||||
}
|
||||
|
||||
record._clean_solar_hot_water()
|
||||
|
||||
assert record.prepared_epc["solar-water-heating-flag"] == "N"
|
||||
assert record.solar_water_heating_flag_bool is False
|
||||
|
||||
record = EPCRecord(cleaning_data=cleaning_data)
|
||||
|
||||
record.prepared_epc = {
|
||||
"solar-water-heating-flag": "",
|
||||
}
|
||||
|
||||
record._clean_solar_hot_water()
|
||||
|
||||
assert record.prepared_epc["solar-water-heating-flag"] == "N"
|
||||
assert record.solar_water_heating_flag_bool is False
|
||||
|
||||
record = EPCRecord(cleaning_data=cleaning_data)
|
||||
|
||||
record.prepared_epc = {
|
||||
"solar-water-heating-flag": None,
|
||||
}
|
||||
|
||||
record._clean_solar_hot_water()
|
||||
|
||||
assert record.prepared_epc["solar-water-heating-flag"] == "N"
|
||||
assert record.solar_water_heating_flag_bool is False
|
||||
assert record._prepared_epc["floor-level"] is None
|
||||
|
||||
def test_year_built(self, cleaning_data):
|
||||
# This test handles a specific test case
|
||||
|
|
@ -416,3 +251,146 @@ class TestEpcRecord:
|
|||
)
|
||||
|
||||
assert prepared_epc.get("year_built") == 1900
|
||||
|
||||
def test_cleaning_rules_energy(self, base_record):
|
||||
base_record._prepared_epc = {
|
||||
"energy-consumption-current": "150",
|
||||
"co2-emissions-current": "32.5"
|
||||
}
|
||||
|
||||
base_record._apply_cleaning_rules()
|
||||
|
||||
assert base_record._prepared_epc["energy-consumption-current"] == 150.0
|
||||
assert base_record._prepared_epc["co2-emissions-current"] == 32.5
|
||||
|
||||
def test_cleaning_rules_energy_anomaly(self, base_record):
|
||||
base_record._prepared_epc = {
|
||||
"energy-consumption-current": "INVALID",
|
||||
"co2-emissions-current": "INVALID"
|
||||
}
|
||||
|
||||
base_record._apply_cleaning_rules()
|
||||
|
||||
assert base_record._prepared_epc["energy-consumption-current"] == "INVALID"
|
||||
assert base_record._prepared_epc["co2-emissions-current"] == "INVALID"
|
||||
|
||||
def test_cleaning_rules_mains_gas(self, base_record):
|
||||
base_record._prepared_epc = {
|
||||
"mains-gas-flag": "Y"
|
||||
}
|
||||
|
||||
base_record._apply_cleaning_rules()
|
||||
|
||||
assert base_record._prepared_epc["mains-gas-flag"] is True
|
||||
|
||||
def test_cleaning_rules_mains_gas_anomaly(self, base_record):
|
||||
base_record._prepared_epc = {
|
||||
"mains-gas-flag": "INVALID"
|
||||
}
|
||||
|
||||
base_record._apply_cleaning_rules()
|
||||
|
||||
assert base_record._prepared_epc["mains-gas-flag"] is None
|
||||
|
||||
def test_cleaning_rules_wind_turbine(self, base_record):
|
||||
base_record._prepared_epc = {
|
||||
"wind-turbine-count": "3"
|
||||
}
|
||||
|
||||
base_record._apply_cleaning_rules()
|
||||
|
||||
assert base_record._prepared_epc["wind-turbine-count"] == 3
|
||||
|
||||
def test_cleaning_rules_extension_count(self, base_record):
|
||||
base_record._prepared_epc = {
|
||||
"extension-count": "2"
|
||||
}
|
||||
|
||||
base_record._apply_cleaning_rules()
|
||||
|
||||
assert base_record._prepared_epc["extension-count"] == 2
|
||||
|
||||
def test_apply_averages_cleaning_fills_missing_values(self, cleaning_data):
|
||||
record = EPCRecord(run_mode="training", cleaning_data=cleaning_data)
|
||||
|
||||
record._prepared_epc = {
|
||||
"property-type": cleaning_data["property_type"].iloc[0],
|
||||
"local-authority": cleaning_data["local_authority"].iloc[0],
|
||||
"total-floor-area": float(cleaning_data["total_floor_area"].iloc[0]),
|
||||
|
||||
"number-habitable-rooms": None,
|
||||
"number-heated-rooms": None,
|
||||
"floor-height": None,
|
||||
}
|
||||
|
||||
record._apply_averages_cleaning()
|
||||
|
||||
assert record._prepared_epc["number-habitable-rooms"] is not None
|
||||
assert record._prepared_epc["number-heated-rooms"] is not None
|
||||
assert record._prepared_epc["floor-height"] is not None
|
||||
|
||||
def test_apply_averages_cleaning_no_missing(self, cleaning_data):
|
||||
record = EPCRecord(run_mode="training", cleaning_data=cleaning_data)
|
||||
|
||||
record._prepared_epc = {
|
||||
"property-type": cleaning_data["property_type"].iloc[0],
|
||||
"local-authority": cleaning_data["local_authority"].iloc[0],
|
||||
"total-floor-area": float(cleaning_data["total_floor_area"].iloc[0]),
|
||||
|
||||
"number-habitable-rooms": 5,
|
||||
"number-heated-rooms": 5,
|
||||
"floor-height": 2.5,
|
||||
}
|
||||
|
||||
original = record._prepared_epc.copy()
|
||||
|
||||
record._apply_averages_cleaning()
|
||||
|
||||
assert record._prepared_epc == original
|
||||
|
||||
def test_apply_averages_cleaning_caps_heated_rooms(self, cleaning_data):
|
||||
record = EPCRecord(run_mode="training", cleaning_data=cleaning_data)
|
||||
|
||||
record._prepared_epc = {
|
||||
"property-type": cleaning_data["property_type"].iloc[0],
|
||||
"local-authority": cleaning_data["local_authority"].iloc[0],
|
||||
"total-floor-area": float(cleaning_data["total_floor_area"].iloc[0]),
|
||||
|
||||
"number-habitable-rooms": None,
|
||||
"number-heated-rooms": None,
|
||||
"floor-height": None,
|
||||
}
|
||||
|
||||
record._apply_averages_cleaning()
|
||||
|
||||
assert (
|
||||
record._prepared_epc["number-heated-rooms"]
|
||||
<= record._prepared_epc["number-habitable-rooms"]
|
||||
)
|
||||
|
||||
def test_apply_averages_cleaning_floor_area_filter(self, cleaning_data):
|
||||
record = EPCRecord(run_mode="training", cleaning_data=cleaning_data)
|
||||
|
||||
floor_area = float(cleaning_data["total_floor_area"].median())
|
||||
|
||||
record._prepared_epc = {
|
||||
"property-type": cleaning_data["property_type"].iloc[0],
|
||||
"local-authority": cleaning_data["local_authority"].iloc[0],
|
||||
"total-floor-area": floor_area,
|
||||
|
||||
"number-habitable-rooms": None,
|
||||
"number-heated-rooms": None,
|
||||
"floor-height": None,
|
||||
}
|
||||
|
||||
record._apply_averages_cleaning()
|
||||
|
||||
assert record._prepared_epc["floor-height"] > 0
|
||||
|
||||
def test_apply_averages_cleaning_requires_cleaning_data(self):
|
||||
record = EPCRecord(run_mode="training", cleaning_data=None)
|
||||
|
||||
record._prepared_epc = {}
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
record._apply_averages_cleaning()
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from datetime import datetime, timezone
|
|||
from typing import TypedDict
|
||||
from etl.hubspot.s3_uploader import S3Uploader
|
||||
import hashlib
|
||||
import os
|
||||
|
||||
|
||||
class CompanyData(TypedDict):
|
||||
|
|
@ -14,7 +15,11 @@ class CompanyData(TypedDict):
|
|||
|
||||
class HubspotDataToDb:
|
||||
def __init__(self):
|
||||
self.s3 = S3Uploader()
|
||||
self.s3 = S3Uploader(
|
||||
aws_access_key=os.getenv("AWS_ACCESS_KEY"),
|
||||
aws_secret_key=os.getenv("AWS_SECRET_KEY"),
|
||||
region=os.getenv("AWS_REGION"),
|
||||
)
|
||||
|
||||
def read_org_table(self, limit: int = 10):
|
||||
with db_read_session() as session:
|
||||
|
|
@ -56,10 +61,6 @@ class HubspotDataToDb:
|
|||
session.commit()
|
||||
return record
|
||||
|
||||
###
|
||||
# Check from here
|
||||
###
|
||||
|
||||
def new_record_to_hubspot_data(self, deal_data, company, listing, hubspot_client):
|
||||
print("⚠️ Deprecated — use the new interface instead.")
|
||||
return self.upsert_hubspot_deal(deal_data, company, listing, hubspot_client)
|
||||
|
|
|
|||
14
infrastructure/terraform/cdn/provider.tf
Normal file
14
infrastructure/terraform/cdn/provider.tf
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
terraform {
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = ">= 5.0"
|
||||
}
|
||||
}
|
||||
|
||||
backend "s3" {
|
||||
bucket = "ara-cdn-terraform-state"
|
||||
key = "terraform.tfstate"
|
||||
region = "eu-west-2"
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ data "terraform_remote_state" "shared" {
|
|||
backend = "s3"
|
||||
config = {
|
||||
bucket = "assessment-model-terraform-state"
|
||||
key = "env:/${var.stage}/terraform.tfstate"
|
||||
key = "env:/${var.stage}/terraform.tfstate"
|
||||
region = "eu-west-2"
|
||||
}
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@ data "terraform_remote_state" "engine" {
|
|||
backend = "s3"
|
||||
config = {
|
||||
bucket = "ara-engine-terraform-state",
|
||||
key = "env:/${var.stage}/terraform.tfstate"
|
||||
key = "env:/${var.stage}/terraform.tfstate"
|
||||
region = "eu-west-2"
|
||||
}
|
||||
}
|
||||
|
|
@ -23,7 +23,7 @@ data "terraform_remote_state" "categorisation" {
|
|||
backend = "s3"
|
||||
config = {
|
||||
bucket = "categorisation-terraform-state",
|
||||
key = "env:/${var.stage}/terraform.tfstate"
|
||||
key = "env:/${var.stage}/terraform.tfstate"
|
||||
region = "eu-west-2"
|
||||
}
|
||||
}
|
||||
|
|
@ -43,26 +43,26 @@ locals {
|
|||
# FastAPI Lambda + API Gateway
|
||||
############################################
|
||||
module "fastapi" {
|
||||
source = "../../modules/lambda_with_api_gateway"
|
||||
source = "../../modules/lambda_with_api_gateway"
|
||||
|
||||
name = "fastapi"
|
||||
stage = var.stage
|
||||
source_dir = "${path.root}/../../../../"
|
||||
handler = "backend.app.main.handler"
|
||||
runtime = "python3.11"
|
||||
timeout = 600
|
||||
memory_size = 512
|
||||
artifact_bucket = data.terraform_remote_state.shared.outputs.ara_fast_api_state_bucket
|
||||
name = "fastapi"
|
||||
stage = var.stage
|
||||
source_dir = "${path.root}/../../../../"
|
||||
handler = "backend.app.main.handler"
|
||||
runtime = "python3.11"
|
||||
timeout = 600
|
||||
memory_size = 512
|
||||
artifact_bucket = data.terraform_remote_state.shared.outputs.ara_fast_api_state_bucket
|
||||
requirements_file = "${path.root}/../../../../backend/app/requirements/requirements.txt"
|
||||
|
||||
domain_name = "api.${var.domain_name}"
|
||||
|
||||
environment = {
|
||||
ENVIRONMENT = var.stage
|
||||
API_KEY = var.api_key
|
||||
SECRET_KEY = var.secret_key
|
||||
ENVIRONMENT = var.stage
|
||||
API_KEY = var.api_key
|
||||
SECRET_KEY = var.secret_key
|
||||
# DOMAIN_NAME = var.domain_name
|
||||
EPC_AUTH_TOKEN = var.epc_auth_token
|
||||
EPC_AUTH_TOKEN = var.epc_auth_token
|
||||
GOOGLE_SOLAR_API_KEY = var.google_solar_api_key
|
||||
|
||||
DB_HOST = var.db_host
|
||||
|
|
@ -71,14 +71,17 @@ module "fastapi" {
|
|||
DB_USERNAME = local.db_credentials.db_assessment_model_username
|
||||
DB_PASSWORD = local.db_credentials.db_assessment_model_password
|
||||
|
||||
PLAN_TRIGGER_BUCKET = data.terraform_remote_state.shared.outputs.retrofit_plan_trigger_bucket_name
|
||||
DATA_BUCKET = data.terraform_remote_state.shared.outputs.retrofit_sap_data_bucket_name
|
||||
SAP_PREDICTIONS_BUCKET = data.terraform_remote_state.shared.outputs.retrofit_sap_predictions_bucket_name
|
||||
CARBON_PREDICTIONS_BUCKET = data.terraform_remote_state.shared.outputs.retrofit_carbon_predictions_bucket_name
|
||||
HEAT_PREDICTIONS_BUCKET = data.terraform_remote_state.shared.outputs.retrofit_heat_predictions_bucket_name
|
||||
HEATING_KWH_PREDICTIONS_BUCKET = data.terraform_remote_state.shared.outputs.retrofit_heating_kwh_predictions_bucket_name
|
||||
HOTWATER_KWH_PREDICTIONS_BUCKET = data.terraform_remote_state.shared.outputs.retrofit_hotwater_kwh_predictions_bucket_name
|
||||
ENERGY_ASSESSMENTS_BUCKET = data.terraform_remote_state.shared.outputs.retrofit_energy_assessments_bucket_name
|
||||
PLAN_TRIGGER_BUCKET = data.terraform_remote_state.shared.outputs.retrofit_plan_trigger_bucket_name
|
||||
DATA_BUCKET = data.terraform_remote_state.shared.outputs.retrofit_sap_data_bucket_name
|
||||
SAP_PREDICTIONS_BUCKET = data.terraform_remote_state.shared.outputs.retrofit_sap_predictions_bucket_name
|
||||
CARBON_PREDICTIONS_BUCKET = data.terraform_remote_state.shared.outputs.retrofit_carbon_predictions_bucket_name
|
||||
HEAT_PREDICTIONS_BUCKET = data.terraform_remote_state.shared.outputs.retrofit_heat_predictions_bucket_name
|
||||
HEATING_KWH_PREDICTIONS_BUCKET = data.terraform_remote_state.shared.outputs.retrofit_heating_kwh_predictions_bucket_name
|
||||
HOTWATER_KWH_PREDICTIONS_BUCKET = data.terraform_remote_state.shared.outputs.retrofit_hotwater_kwh_predictions_bucket_name
|
||||
ENERGY_ASSESSMENTS_BUCKET = data.terraform_remote_state.shared.outputs.retrofit_energy_assessments_bucket_name
|
||||
SAP_BASELINE_PREDICTIONS_BUCKET = data.terraform_remote_state.shared.outputs.retrofit_sap_baseline_predictions_bucket_name
|
||||
CARBON_BASELINE_PREDICTIONS_BUCKET = data.terraform_remote_state.shared.outputs.retrofit_carbon_baseline_predictions_bucket_name
|
||||
HEAT_BASELINE_PREDICTIONS_BUCKET = data.terraform_remote_state.shared.outputs.retrofit_heat_baseline_predictions_bucket_name
|
||||
|
||||
ENGINE_SQS_URL = data.terraform_remote_state.engine.outputs.ara_engine_queue_url
|
||||
CATEGORISATION_SQS_URL = data.terraform_remote_state.categorisation.outputs.categorisation_queue_url
|
||||
|
|
@ -121,4 +124,4 @@ resource "aws_iam_role_policy_attachment" "fastapi_sqs_send" {
|
|||
resource "aws_iam_role_policy_attachment" "fastapi_s3_read_and_write" {
|
||||
role = module.fastapi.role_name
|
||||
policy_arn = data.terraform_remote_state.shared.outputs.fast_api_s3_read_and_write_arn
|
||||
}
|
||||
}
|
||||
|
|
|
|||
27
infrastructure/terraform/lambda/pashub_to_ara/main.tf
Normal file
27
infrastructure/terraform/lambda/pashub_to_ara/main.tf
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
data "terraform_remote_state" "shared" {
|
||||
backend = "s3"
|
||||
config = {
|
||||
bucket = "assessment-model-terraform-state"
|
||||
key = "env:/${var.stage}/terraform.tfstate"
|
||||
region = "eu-west-2"
|
||||
}
|
||||
}
|
||||
|
||||
module "lambda" {
|
||||
source = "../../modules/lambda_with_sqs"
|
||||
|
||||
name = "pashub_to_ara"
|
||||
stage = var.stage
|
||||
|
||||
image_uri = local.image_uri
|
||||
|
||||
# Optional: Set maximum_concurrency to limit concurrent SQS-triggered invocations (2-1000)
|
||||
maximum_concurrency = var.maximum_concurrency
|
||||
|
||||
batch_size = var.batch_size
|
||||
|
||||
environment = {
|
||||
STAGE = var.stage
|
||||
LOG_LEVEL = "info"
|
||||
}
|
||||
}
|
||||
16
infrastructure/terraform/lambda/pashub_to_ara/provider.tf
Normal file
16
infrastructure/terraform/lambda/pashub_to_ara/provider.tf
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
terraform {
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = ">= 5.0"
|
||||
}
|
||||
}
|
||||
|
||||
backend "s3" {
|
||||
bucket = "pashub-to-ara-terraform-state"
|
||||
key = "terraform.tfstate"
|
||||
region = "eu-west-2"
|
||||
}
|
||||
|
||||
required_version = ">= 1.2.0"
|
||||
}
|
||||
37
infrastructure/terraform/lambda/pashub_to_ara/variables.tf
Normal file
37
infrastructure/terraform/lambda/pashub_to_ara/variables.tf
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
variable "lambda_name" {
|
||||
type = string
|
||||
description = "Logical name of the lambda (e.g. address2uprn)"
|
||||
}
|
||||
|
||||
variable "stage" {
|
||||
description = "Deployment stage (e.g. dev, prod)"
|
||||
type = string
|
||||
}
|
||||
variable "ecr_repo_url" {
|
||||
type = string
|
||||
description = "ECR repository URL (no tag, no digest)"
|
||||
}
|
||||
|
||||
variable "image_digest" {
|
||||
type = string
|
||||
description = "Image digest (sha256:...)"
|
||||
}
|
||||
|
||||
variable "maximum_concurrency" {
|
||||
type = number
|
||||
default = null
|
||||
description = "Maximum number of concurrent Lambda invocations from SQS (2-1000). null = no limit."
|
||||
}
|
||||
|
||||
variable "batch_size" {
|
||||
type = number
|
||||
default = 1
|
||||
}
|
||||
|
||||
locals {
|
||||
image_uri = "${var.ecr_repo_url}@${var.image_digest}"
|
||||
}
|
||||
|
||||
output "resolved_image_uri" {
|
||||
value = local.image_uri
|
||||
}
|
||||
|
|
@ -20,6 +20,10 @@ resource "aws_cloudwatch_log_group" "api_logs" {
|
|||
resource "null_resource" "pip_install" {
|
||||
count = var.requirements_file != null ? 1 : 0
|
||||
|
||||
triggers = {
|
||||
always_run = timestamp()
|
||||
}
|
||||
|
||||
provisioner "local-exec" {
|
||||
command = "pip install -r ${var.requirements_file} -t ${var.source_dir} --platform manylinux2014_x86_64 --implementation cp --python-version 3.11 --only-binary=:all: --upgrade"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,16 +6,16 @@ terraform {
|
|||
}
|
||||
}
|
||||
backend "s3" {
|
||||
bucket = "assessment-model-terraform-state"
|
||||
region = "eu-west-2"
|
||||
key = "terraform.tfstate"
|
||||
bucket = "assessment-model-terraform-state"
|
||||
region = "eu-west-2"
|
||||
key = "terraform.tfstate"
|
||||
}
|
||||
|
||||
required_version = ">= 1.2.0"
|
||||
}
|
||||
|
||||
provider "aws" {
|
||||
region = var.region
|
||||
region = var.region
|
||||
}
|
||||
|
||||
# Additional provider for resources that need to be in us-east-1, specifically the SSL certificate
|
||||
|
|
@ -47,30 +47,30 @@ resource "aws_security_group" "allow_db" {
|
|||
|
||||
ingress {
|
||||
# TLS (change to whatever ports you need)
|
||||
from_port = 5432
|
||||
to_port = 5432
|
||||
protocol = "tcp"
|
||||
from_port = 5432
|
||||
to_port = 5432
|
||||
protocol = "tcp"
|
||||
cidr_blocks = ["0.0.0.0/0"]
|
||||
}
|
||||
|
||||
egress {
|
||||
from_port = 0
|
||||
to_port = 0
|
||||
protocol = "-1"
|
||||
from_port = 0
|
||||
to_port = 0
|
||||
protocol = "-1"
|
||||
cidr_blocks = ["0.0.0.0/0"]
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_db_instance" "default" {
|
||||
allocated_storage = var.allocated_storage
|
||||
engine = "postgres"
|
||||
engine_version = "14.17"
|
||||
instance_class = var.instance_class
|
||||
db_name = var.database_name
|
||||
username = jsondecode(data.aws_secretsmanager_secret_version.db_credentials.secret_string)["db_assessment_model_username"]
|
||||
password = jsondecode(data.aws_secretsmanager_secret_version.db_credentials.secret_string)["db_assessment_model_password"]
|
||||
parameter_group_name = "default.postgres14"
|
||||
skip_final_snapshot = true
|
||||
allocated_storage = var.allocated_storage
|
||||
engine = "postgres"
|
||||
engine_version = "14.17"
|
||||
instance_class = var.instance_class
|
||||
db_name = var.database_name
|
||||
username = jsondecode(data.aws_secretsmanager_secret_version.db_credentials.secret_string)["db_assessment_model_username"]
|
||||
password = jsondecode(data.aws_secretsmanager_secret_version.db_credentials.secret_string)["db_assessment_model_password"]
|
||||
parameter_group_name = "default.postgres14"
|
||||
skip_final_snapshot = true
|
||||
vpc_security_group_ids = [aws_security_group.allow_db.id]
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
|
|
@ -87,7 +87,7 @@ resource "aws_db_instance" "default" {
|
|||
storage_type = "gp3"
|
||||
|
||||
# Automated backups configuration
|
||||
backup_retention_period = 14
|
||||
backup_retention_period = 14
|
||||
backup_window = "03:00-04:00"
|
||||
maintenance_window = "Sun:02:00-Sun:02:30"
|
||||
copy_tags_to_snapshot = true
|
||||
|
|
@ -103,7 +103,7 @@ module "s3_presignable_bucket" {
|
|||
}
|
||||
|
||||
output "retrofit_plan_trigger_bucket_name" {
|
||||
value = module.s3_presignable_bucket.bucket_name
|
||||
value = module.s3_presignable_bucket.bucket_name
|
||||
description = "Name of the retrofit plan trigger bucket"
|
||||
}
|
||||
|
||||
|
|
@ -156,7 +156,7 @@ module "retrofit_sap_predictions" {
|
|||
}
|
||||
|
||||
output "retrofit_sap_predictions_bucket_name" {
|
||||
value = module.retrofit_sap_predictions.bucket_name
|
||||
value = module.retrofit_sap_predictions.bucket_name
|
||||
description = "Name of the retrofit SAP predictions bucket"
|
||||
}
|
||||
|
||||
|
|
@ -167,7 +167,7 @@ module "retrofit_sap_data" {
|
|||
}
|
||||
|
||||
output "retrofit_sap_data_bucket_name" {
|
||||
value = module.retrofit_sap_data.bucket_name
|
||||
value = module.retrofit_sap_data.bucket_name
|
||||
description = "Name of the retrofit SAP data bucket"
|
||||
}
|
||||
|
||||
|
|
@ -178,7 +178,7 @@ module "retrofit_carbon_predictions" {
|
|||
}
|
||||
|
||||
output "retrofit_carbon_predictions_bucket_name" {
|
||||
value = module.retrofit_carbon_predictions.bucket_name
|
||||
value = module.retrofit_carbon_predictions.bucket_name
|
||||
description = "Name of the retrofit carbon predictions bucket"
|
||||
}
|
||||
|
||||
|
|
@ -189,7 +189,7 @@ module "retrofit_heat_predictions" {
|
|||
}
|
||||
|
||||
output "retrofit_heat_predictions_bucket_name" {
|
||||
value = module.retrofit_heat_predictions.bucket_name
|
||||
value = module.retrofit_heat_predictions.bucket_name
|
||||
description = "Name of the retrofit heat predictions bucket"
|
||||
}
|
||||
|
||||
|
|
@ -218,7 +218,7 @@ module "retrofit_heating_kwh_predictions" {
|
|||
}
|
||||
|
||||
output "retrofit_heating_kwh_predictions_bucket_name" {
|
||||
value = module.retrofit_heating_kwh_predictions.bucket_name
|
||||
value = module.retrofit_heating_kwh_predictions.bucket_name
|
||||
description = "Name of the retrofit heating kWh predictions bucket"
|
||||
}
|
||||
|
||||
|
|
@ -229,7 +229,7 @@ module "retrofit_hotwater_kwh_predictions" {
|
|||
}
|
||||
|
||||
output "retrofit_hotwater_kwh_predictions_bucket_name" {
|
||||
value = module.retrofit_hotwater_kwh_predictions.bucket_name
|
||||
value = module.retrofit_hotwater_kwh_predictions.bucket_name
|
||||
description = "Name of the retrofit hotwater kWh predictions bucket"
|
||||
}
|
||||
|
||||
|
|
@ -239,6 +239,33 @@ module "retrofit_sap_baseline_predictions" {
|
|||
allowed_origins = var.allowed_origins
|
||||
}
|
||||
|
||||
output "retrofit_sap_baseline_predictions_bucket_name" {
|
||||
value = module.retrofit_sap_baseline_predictions.bucket_name
|
||||
description = "Name of the retrofit SAP baseline predictions bucket"
|
||||
}
|
||||
|
||||
module "retrofit_carbon_baseline_predictions" {
|
||||
source = "../modules/s3"
|
||||
bucketname = "retrofit-carbon-baseline-predictions-${var.stage}"
|
||||
allowed_origins = var.allowed_origins
|
||||
}
|
||||
|
||||
output "retrofit_carbon_baseline_predictions_bucket_name" {
|
||||
value = module.retrofit_carbon_baseline_predictions.bucket_name
|
||||
description = "Name of the retrofit carbon baseline predictions bucket"
|
||||
}
|
||||
|
||||
module "retrofit_heat_baseline_predictions" {
|
||||
source = "../modules/s3"
|
||||
bucketname = "retrofit-heat-baseline-predictions-${var.stage}"
|
||||
allowed_origins = var.allowed_origins
|
||||
}
|
||||
|
||||
output "retrofit_heat_baseline_predictions_bucket_name" {
|
||||
value = module.retrofit_heat_baseline_predictions.bucket_name
|
||||
description = "Name of the retrofit heat baseline predictions bucket"
|
||||
}
|
||||
|
||||
// We make this bucket presignable, because we want to generate download links for the frontend
|
||||
module "retrofit_energy_assessments" {
|
||||
source = "../modules/s3_presignable_bucket"
|
||||
|
|
@ -248,7 +275,7 @@ module "retrofit_energy_assessments" {
|
|||
}
|
||||
|
||||
output "retrofit_energy_assessments_bucket_name" {
|
||||
value = module.retrofit_energy_assessments.bucket_name
|
||||
value = module.retrofit_energy_assessments.bucket_name
|
||||
description = "Name of the retrofit energy assessments bucket"
|
||||
}
|
||||
|
||||
|
|
@ -327,6 +354,16 @@ module "sap_baseline_ecr" {
|
|||
source = "../modules/ecr"
|
||||
}
|
||||
|
||||
module "heat_baseline_ecr" {
|
||||
ecr_name = "heat-baseline-prediction-${var.stage}"
|
||||
source = "../modules/ecr"
|
||||
}
|
||||
|
||||
module "carbon_baseline_ecr" {
|
||||
ecr_name = "carbon-baseline-prediction-${var.stage}"
|
||||
source = "../modules/ecr"
|
||||
}
|
||||
|
||||
################################################
|
||||
# SES - Email sending
|
||||
################################################
|
||||
|
|
@ -352,7 +389,7 @@ module "address2uprn_state_bucket" {
|
|||
module "address2uprn_registry" {
|
||||
source = "../modules/container_registry"
|
||||
name = "address2uprn"
|
||||
stage = var.stage
|
||||
stage = var.stage
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -383,14 +420,14 @@ module "condition_etl_state_bucket" {
|
|||
module "condition_etl_registry" {
|
||||
source = "../modules/container_registry"
|
||||
name = "condition-etl"
|
||||
stage = var.stage
|
||||
stage = var.stage
|
||||
|
||||
}
|
||||
|
||||
# Condition Data S3 Bucket to store initial data
|
||||
module "condition_data_bucket" {
|
||||
source = "../modules/s3"
|
||||
bucketname = "condition-data-${var.stage}"
|
||||
source = "../modules/s3"
|
||||
bucketname = "condition-data-${var.stage}"
|
||||
allowed_origins = var.allowed_origins
|
||||
}
|
||||
|
||||
|
|
@ -421,7 +458,7 @@ module "postcode_splitter_state_bucket" {
|
|||
module "postcode_splitter_registry" {
|
||||
source = "../modules/container_registry"
|
||||
name = "postcode_splitter"
|
||||
stage = var.stage
|
||||
stage = var.stage
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -452,7 +489,7 @@ module "categorisation_state_bucket" {
|
|||
module "categorisation_registry" {
|
||||
source = "../modules/container_registry"
|
||||
name = "categorisation"
|
||||
stage = var.stage
|
||||
stage = var.stage
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -468,7 +505,7 @@ module "ordnance_state_bucket" {
|
|||
module "ordnance_registry" {
|
||||
source = "../modules/container_registry"
|
||||
name = "ordnance"
|
||||
stage = var.stage
|
||||
stage = var.stage
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -487,6 +524,20 @@ output "ordnance_s3_read_and_write_arn" {
|
|||
value = module.ordnance_s3_read_and_write.policy_arn
|
||||
}
|
||||
|
||||
################################################
|
||||
# Pas Hub to Ara – Lambda
|
||||
################################################
|
||||
module "pashub_to_ara_state_bucket" {
|
||||
source = "../modules/tf_state_bucket"
|
||||
bucket_name = "pashub-to-ara-terraform-state"
|
||||
}
|
||||
|
||||
module "pashub_to_ara_registry" {
|
||||
source = "../modules/container_registry"
|
||||
name = "pashub_to_ara"
|
||||
stage = var.stage
|
||||
}
|
||||
|
||||
################################################
|
||||
# Engine – Lambda ECR
|
||||
################################################
|
||||
|
|
@ -499,7 +550,7 @@ module "engine_state_bucket" {
|
|||
module "engine_registry" {
|
||||
source = "../modules/container_registry"
|
||||
name = "engine"
|
||||
stage = var.stage
|
||||
stage = var.stage
|
||||
}
|
||||
|
||||
# S3 policy for Engine to read and write from various S3 buckets
|
||||
|
|
@ -508,7 +559,7 @@ module "engine_s3_read_and_write" {
|
|||
|
||||
policy_name = "EngineReadandWriteS3"
|
||||
policy_description = "Allow Engine Lambda to read from and write to various S3 buckets"
|
||||
bucket_arns = [
|
||||
bucket_arns = [
|
||||
"arn:aws:s3:::${module.s3_presignable_bucket.bucket_name}",
|
||||
"arn:aws:s3:::${module.retrofit_sap_data.bucket_name}",
|
||||
"arn:aws:s3:::${module.retrofit_sap_predictions.bucket_name}",
|
||||
|
|
@ -516,10 +567,13 @@ module "engine_s3_read_and_write" {
|
|||
"arn:aws:s3:::${module.retrofit_heat_predictions.bucket_name}",
|
||||
"arn:aws:s3:::${module.retrofit_heating_kwh_predictions.bucket_name}",
|
||||
"arn:aws:s3:::${module.retrofit_hotwater_kwh_predictions.bucket_name}",
|
||||
"arn:aws:s3:::${module.retrofit_energy_assessments.bucket_name}"
|
||||
"arn:aws:s3:::${module.retrofit_energy_assessments.bucket_name}",
|
||||
"arn:aws:s3:::${module.retrofit_sap_baseline_predictions.bucket_name}",
|
||||
"arn:aws:s3:::${module.retrofit_carbon_baseline_predictions.bucket_name}",
|
||||
"arn:aws:s3:::${module.retrofit_heat_baseline_predictions.bucket_name}"
|
||||
]
|
||||
actions = ["s3:*"]
|
||||
resource_paths = ["/*"]
|
||||
actions = ["s3:*"]
|
||||
resource_paths = ["/*"]
|
||||
}
|
||||
|
||||
output "engine_s3_read_and_write_arn" {
|
||||
|
|
@ -544,7 +598,7 @@ module "fast_api_s3_read_and_write" {
|
|||
|
||||
policy_name = "FastAPIReadandWriteS3"
|
||||
policy_description = "Allow FastAPI Lambda to read from and write to various S3 buckets"
|
||||
bucket_arns = [
|
||||
bucket_arns = [
|
||||
"arn:aws:s3:::${module.s3_presignable_bucket.bucket_name}",
|
||||
"arn:aws:s3:::${module.retrofit_sap_data.bucket_name}",
|
||||
"arn:aws:s3:::${module.retrofit_sap_predictions.bucket_name}",
|
||||
|
|
@ -554,8 +608,8 @@ module "fast_api_s3_read_and_write" {
|
|||
"arn:aws:s3:::${module.retrofit_hotwater_kwh_predictions.bucket_name}",
|
||||
"arn:aws:s3:::${module.retrofit_energy_assessments.bucket_name}"
|
||||
]
|
||||
actions = ["s3:GetObject", "s3:ListBucket"]
|
||||
resource_paths = ["/*"]
|
||||
actions = ["s3:GetObject", "s3:ListBucket"]
|
||||
resource_paths = ["/*"]
|
||||
}
|
||||
|
||||
output "fast_api_s3_read_and_write_arn" {
|
||||
|
|
@ -575,6 +629,20 @@ output "cdn_certificate_state_bucket" {
|
|||
}
|
||||
|
||||
|
||||
|
||||
################################################
|
||||
# CDN
|
||||
################################################
|
||||
module "cdn_state_bucket" {
|
||||
source = "../modules/tf_state_bucket"
|
||||
bucket_name = "ara-cdn-terraform-state"
|
||||
}
|
||||
|
||||
output "cdn_state_bucket" {
|
||||
value = module.cdn_state_bucket.bucket_name
|
||||
}
|
||||
|
||||
|
||||
################################################
|
||||
# Hubspot ETL Lambda
|
||||
################################################
|
||||
|
|
@ -604,4 +672,4 @@ module "hubspot_etl_s3_read_and_write" {
|
|||
|
||||
output "ordnance_s3_read_and_write_arn" {
|
||||
value = module.hubspot_etl_s3_read_and_write.policy_arn
|
||||
}
|
||||
}
|
||||
|
|
@ -4,3 +4,5 @@ log_cli = true
|
|||
log_cli_level = INFO
|
||||
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 backend/condition/tests backend/address2UPRN/tests backend/onboarders/tests backend/categorisation/tests backend/export/tests etl/hubspot/tests
|
||||
markers =
|
||||
integration: mark a test as an integration test
|
||||
|
|
|
|||
|
|
@ -200,20 +200,20 @@ class Costs:
|
|||
self.property = property_instance
|
||||
self.regional_labour_variations = regional_labour_variations
|
||||
|
||||
self.region = county_to_region_map.get(self.property.data["county"], None)
|
||||
self.region = county_to_region_map.get(self.property.epc_record.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)
|
||||
self.region = county_to_region_map.get(self.property.epc_record.local_authority_label, None)
|
||||
|
||||
if self.region is None:
|
||||
# Try and get the region after converting the keys to lower
|
||||
self.region = {
|
||||
k.lower(): v for k, v in county_to_region_map.items()
|
||||
}.get(self.property.data["local-authority-label"].lower(), None)
|
||||
}.get(self.property.property.epc_record.local_authority_label.lower(), None)
|
||||
|
||||
if self.region is None:
|
||||
logger.warning("No region found for county %s, defaulting to South East England",
|
||||
self.property.data["county"])
|
||||
self.property.epc_record.county)
|
||||
self.region = "South East England"
|
||||
|
||||
self.labour_adjustment_factor = [
|
||||
|
|
@ -858,8 +858,8 @@ class Costs:
|
|||
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"]
|
||||
property_type=self.property.epc_record.property_type,
|
||||
built_form=self.property.epc_record.built_form
|
||||
)
|
||||
|
||||
additionals_labour_cost = labour_rate * self.labour_adjustment_factor
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import pandas as pd
|
||||
from BaseUtility import Definitions
|
||||
from backend.Property import Property
|
||||
|
||||
|
|
@ -28,7 +27,7 @@ class FireplaceRecommendations(Definitions):
|
|||
:return:
|
||||
"""
|
||||
|
||||
number_open_fireplaces = int(self.property.data["number-open-fireplaces"])
|
||||
number_open_fireplaces = self.property.epc_record.number_open_fireplaces
|
||||
|
||||
if number_open_fireplaces == 0:
|
||||
return
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from backend.app.plan.schemas import MEASURE_MAP
|
|||
from backend.Property import Property
|
||||
from recommendations.recommendation_utils import (
|
||||
r_value_per_mm_to_u_value, calculate_u_value_uplift, is_diminishing_returns, update_lowest_selected_u_value,
|
||||
get_recommended_part, get_floor_u_value, override_costs, check_simulation_difference
|
||||
get_recommended_part, get_floor_u_value, override_costs, check_simulation_difference, check_use_survey
|
||||
)
|
||||
from recommendations.Costs import Costs
|
||||
from etl.epc_clean.epc_attributes.FloorAttributes import FloorAttributes
|
||||
|
|
@ -76,7 +76,7 @@ class FloorRecommendations(Definitions):
|
|||
return
|
||||
|
||||
u_value = self.property.floor["thermal_transmittance"]
|
||||
property_type = self.property.data["property-type"]
|
||||
property_type = self.property.epc_record.property_type
|
||||
floor_area = self.property.insulation_floor_area
|
||||
|
||||
if self.property.floor["another_property_below"] | (self.property.floor["insulation_thickness"] in [
|
||||
|
|
@ -226,7 +226,6 @@ class FloorRecommendations(Definitions):
|
|||
raise NotImplementedError("Implement me!")
|
||||
|
||||
sap_points = non_invasive_recs.get("sap_points", None)
|
||||
survey = non_invasive_recs.get("survey", False)
|
||||
|
||||
floor_ending_config = FloorAttributes(new_description).process()
|
||||
floor_simulation_config = check_simulation_difference(
|
||||
|
|
@ -257,7 +256,9 @@ class FloorRecommendations(Definitions):
|
|||
"starting_u_value": u_value,
|
||||
"new_u_value": new_u_value,
|
||||
"sap_points": sap_points,
|
||||
"survey": survey,
|
||||
"survey": check_use_survey(
|
||||
non_invasive_recs, self.property.epc_record.has_been_remodelled
|
||||
),
|
||||
"already_installed": already_installed,
|
||||
"simulation_config": simulation_config,
|
||||
"description_simulation": {
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ class HeatingControlRecommender:
|
|||
We can then consider the heating system itself
|
||||
:return:
|
||||
"""
|
||||
if (self.property.data["mainheatc-energy-eff"] in ["Poor", "Very Poor", "Average"]) or (
|
||||
if (self.property.epc_record.mainheatc_energy_eff in ["Poor", "Very Poor", "Average"]) or (
|
||||
self.property.main_heating_controls["clean_description"] in ["Programmer and room thermostat"]
|
||||
):
|
||||
# We recommend Programmer and appliance thermostats as the heating control. This has an average energy
|
||||
|
|
@ -125,10 +125,10 @@ class HeatingControlRecommender:
|
|||
new_config=ending_config, old_config=self.property.main_heating_controls
|
||||
)
|
||||
# This upgrade will only take the heating system to average energy efficiency
|
||||
if self.property.data["mainheatc-energy-eff"] in ["Poor", "Very Poor", "Average"]:
|
||||
if self.property.epc_record.mainheatc_energy_eff in ["Poor", "Very Poor", "Average"]:
|
||||
simulation_config["mainheatc_energy_eff_ending"] = "Good"
|
||||
else:
|
||||
simulation_config["mainheatc_energy_eff_ending"] = self.property.data["mainheatc-energy-eff"]
|
||||
simulation_config["mainheatc_energy_eff_ending"] = self.property.epc_record.mainheatc_energy_eff
|
||||
|
||||
description_simulation = {
|
||||
"mainheatcont-description": new_description,
|
||||
|
|
@ -193,10 +193,10 @@ class HeatingControlRecommender:
|
|||
)
|
||||
# This upgrade will only take the heating system to average energy efficiency
|
||||
# If the current system is below good, we make it good
|
||||
if self.property.data["mainheatc-energy-eff"] in ["Poor", "Very Poor", "Average"]:
|
||||
if self.property.epc_record.mainheatc_energy_eff in ["Poor", "Very Poor", "Average"]:
|
||||
simulation_config["mainheatc_energy_eff_ending"] = "Good"
|
||||
else:
|
||||
simulation_config["mainheatc_energy_eff_ending"] = self.property.data["mainheatc-energy-eff"]
|
||||
simulation_config["mainheatc_energy_eff_ending"] = self.property.epc_record.mainheatc_energy_eff
|
||||
|
||||
description_simulation = {
|
||||
"mainheatcont-description": new_controls_description,
|
||||
|
|
@ -208,7 +208,7 @@ class HeatingControlRecommender:
|
|||
has_trvs = not needs_trvs
|
||||
|
||||
cost_result = self.costs.roomstat_programmer_trvs(
|
||||
number_heated_rooms=int(self.property.data["number-heated-rooms"]),
|
||||
number_heated_rooms=self.property.epc_record.number_heated_rooms,
|
||||
has_programmer=has_programmer,
|
||||
has_room_thermostat=has_room_thermostat,
|
||||
has_trvs=has_trvs
|
||||
|
|
@ -257,7 +257,7 @@ class HeatingControlRecommender:
|
|||
|
||||
if (
|
||||
(self.property.main_heating_controls["thermostatic_control"] == "time and temperature zone control") or
|
||||
(self.property.data["mainheatc-energy-eff"] in ["Very Good"])
|
||||
(self.property.epc_record.mainheatc_energy_eff in ["Very Good"])
|
||||
):
|
||||
# No recommendation needed
|
||||
return
|
||||
|
|
@ -274,17 +274,17 @@ class HeatingControlRecommender:
|
|||
)
|
||||
|
||||
# If the current system is below very good, we make it very good
|
||||
if self.property.data["mainheatc-energy-eff"] in ["Poor", "Very Poor", "Average", "Good"]:
|
||||
if self.property.epc_record.mainheatc_energy_eff in ["Poor", "Very Poor", "Average", "Good"]:
|
||||
simulation_config["mainheatc_energy_eff_ending"] = "Very Good"
|
||||
else:
|
||||
simulation_config["mainheatc_energy_eff_ending"] = self.property.data["mainheatc-energy-eff"]
|
||||
simulation_config["mainheatc_energy_eff_ending"] = self.property.epc_record.mainheatc_energy_eff
|
||||
|
||||
description_simulation = {
|
||||
"mainheatcont-description": new_controls_description,
|
||||
"mainheatc-energy-eff": simulation_config["mainheatc_energy_eff_ending"]
|
||||
}
|
||||
cost_result = self.costs.time_and_temperature_zone_control(
|
||||
number_heated_rooms=int(self.property.data["number-heated-rooms"])
|
||||
number_heated_rooms=self.property.epc_record.number_heated_rooms
|
||||
)
|
||||
|
||||
description = (
|
||||
|
|
@ -324,10 +324,10 @@ class HeatingControlRecommender:
|
|||
new_config=ending_config, old_config=self.property.main_heating_controls
|
||||
)
|
||||
# Only adjust if the current system is below good
|
||||
if self.property.data["mainheatc-energy-eff"] in ["Poor", "Very Poor"]:
|
||||
if self.property.epc_record.mainheatc_energy_eff in ["Poor", "Very Poor"]:
|
||||
simulation_config["mainheatc_energy_eff_ending"] = "Average"
|
||||
else:
|
||||
simulation_config["mainheatc_energy_eff_ending"] = self.property.data["mainheatc-energy-eff"]
|
||||
simulation_config["mainheatc_energy_eff_ending"] = self.property.epc_record.mainheatc_energy_eff
|
||||
|
||||
description_simulation = {
|
||||
"mainheatcont-description": new_controls_description,
|
||||
|
|
@ -339,7 +339,7 @@ class HeatingControlRecommender:
|
|||
has_bypass = self.property.main_heating_controls["auxiliary_systems"] == "bypass"
|
||||
|
||||
cost_result = self.costs.programmer_trvs_bypass(
|
||||
number_heated_rooms=int(self.property.data["number-heated-rooms"]),
|
||||
number_heated_rooms=self.property.epc_record.number_heated_rooms,
|
||||
has_trvs=has_trvs,
|
||||
has_programmer=has_programmer,
|
||||
has_bypass=has_bypass
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import re
|
||||
import backend.app.assumptions as assumptions
|
||||
from recommendations.recommendation_utils import (
|
||||
check_simulation_difference, override_costs, combine_recommendation_configs
|
||||
check_simulation_difference, override_costs, combine_recommendation_configs, check_use_survey
|
||||
)
|
||||
from backend.Property import Property
|
||||
from backend.app.plan.schemas import MEASURE_MAP
|
||||
|
|
@ -160,7 +160,7 @@ class HeatingRecommender:
|
|||
"""
|
||||
|
||||
# We can also recommend hhr if the property doesn't have a mains has connection
|
||||
no_mains = not self.property.data["mains-gas-flag"]
|
||||
no_mains = not self.property.epc_record.mains_gas_flag
|
||||
|
||||
# If the property already has room heaters then we recommend HHR as an option since the home already has
|
||||
# a variation of room heaters
|
||||
|
|
@ -199,28 +199,28 @@ class HeatingRecommender:
|
|||
# 2) If the property doesn't have a heating system, but it has access to the mains gas
|
||||
no_heating_has_mains = self.property.main_heating["clean_description"] in [
|
||||
'No system present, electric heaters assumed'
|
||||
] and self.property.data["mains-gas-flag"]
|
||||
] and self.property.epc_record.mains_gas_flag
|
||||
|
||||
# The property is using portable heaters and has access to gas mains
|
||||
has_room_heaters = self.has_room_heaters and self.property.data["mains-gas-flag"]
|
||||
has_room_heaters = self.has_room_heaters and self.property.epc_record.mains_gas_flag
|
||||
|
||||
# We also check if the property has electric heating, but it has access to the mains gas
|
||||
electic_heating_has_mains = self.has_electric_heating_description and self.property.data["mains-gas-flag"]
|
||||
electic_heating_has_mains = self.has_electric_heating_description and self.property.epc_record.mains_gas_flag
|
||||
|
||||
portable_heaters_has_mains = (
|
||||
self.property.main_heating["has_portable_electric_heaters"] and self.property.data["mains-gas-flag"]
|
||||
self.property.main_heating["has_portable_electric_heaters"] and self.property.epc_record.mains_gas_flag
|
||||
)
|
||||
|
||||
# The next condition is if the home has a non-gas boiler, such as an oil boiler, with a mains gas connection
|
||||
non_gas_boiler = (
|
||||
self.property.main_heating["has_boiler"] and
|
||||
not self.property.main_heating["has_mains_gas"] and
|
||||
self.property.data["mains-gas-flag"]
|
||||
self.property.epc_record.mains_gas_flag
|
||||
)
|
||||
# Additionally, if the property has a gas connection, is using gas heating but doesn't have a boiler,
|
||||
# we recommend a boiler
|
||||
non_boiler_gas_heating = (
|
||||
self.property.data["mains-gas-flag"] and
|
||||
self.property.epc_record.mains_gas_flag and
|
||||
self.property.main_heating["has_mains_gas"] and
|
||||
not self.property.main_heating["has_boiler"]
|
||||
)
|
||||
|
|
@ -386,7 +386,7 @@ class HeatingRecommender:
|
|||
|
||||
recommendation_phase = phase
|
||||
|
||||
if self.property.data["mainheat-energy-eff"] not in ["Poor", "Very Poor"]:
|
||||
if self.property.epc_record.mainheat_energy_eff not in ["Poor", "Very Poor"]:
|
||||
return
|
||||
|
||||
hotwater_from_mains = self.property.hotwater["clean_description"] in ["From main system"]
|
||||
|
|
@ -407,7 +407,7 @@ class HeatingRecommender:
|
|||
size=None,
|
||||
exising_room_heaters=False,
|
||||
system_change=False,
|
||||
n_heated_rooms=self.property.data["number-heated-rooms"],
|
||||
n_heated_rooms=self.property.epc_record.number_heated_rooms,
|
||||
n_rooms=self.property.number_of_rooms,
|
||||
is_electric=True
|
||||
)
|
||||
|
|
@ -581,7 +581,7 @@ class HeatingRecommender:
|
|||
# New functions to estimate size of ASHP
|
||||
estimated_load = self.estimate_peak_kw(
|
||||
floor_area_m2=self.property.floor_area,
|
||||
epc_primary_kwh_per_m2_yr=self.property.data["energy-consumption-current"],
|
||||
epc_primary_kwh_per_m2_yr=self.property.epc_record.energy_consumption_current,
|
||||
primary_to_delivered_factor=1.55, # use 1.13 if heating fuel is gas
|
||||
space_heat_fraction_range=(0.35, 0.60),
|
||||
hdd_base_dd=2000.0, # set from location
|
||||
|
|
@ -670,7 +670,7 @@ class HeatingRecommender:
|
|||
# If the property does not have existing cavity and loft insulation, we include a note that the cost
|
||||
# includes the boiler upgrade scheme and that the cavity and loft need to be treated, to ensure access
|
||||
# to the funding
|
||||
if not non_intrusive_recommendation and self.property.data["tenure"] not in assumptions.SOCIAL_TENURES:
|
||||
if not non_intrusive_recommendation and self.property.epc_record.tenure not in assumptions.SOCIAL_TENURES:
|
||||
if has_cavity_or_loft_recommendations:
|
||||
description = description + (
|
||||
f" You must ensure that the property has an insulated cavity and "
|
||||
|
|
@ -865,7 +865,9 @@ class HeatingRecommender:
|
|||
"description_simulation": recommendation_description_simulation,
|
||||
# We insert the heating system type here
|
||||
"system_type": system_type,
|
||||
"survey": non_intrusive_recommendation.get("survey", False),
|
||||
"survey": check_use_survey(
|
||||
non_intrusive_recommendation, self.property.epc_record.has_been_remodelled
|
||||
),
|
||||
# In this instance, we are recommending an entire heating system so the innovation rate is becased
|
||||
# on the heating system as whole
|
||||
"innovation_rate": heating_product["innovation_rate"],
|
||||
|
|
@ -923,7 +925,7 @@ class HeatingRecommender:
|
|||
# If the property is off-gas and has no heating system in place, the number of heated rooms will actually
|
||||
# be 0, so we use the number of rooms as the figure
|
||||
number_heated_rooms = (
|
||||
self.property.data["number-heated-rooms"] if self.property.data["number-heated-rooms"] > 0
|
||||
self.property.epc_record.number_heated_rooms if self.property.epc_record.number_heated_rooms > 0
|
||||
else (
|
||||
self.property.number_of_rooms - 1 if self.property.number_of_rooms > 1 else
|
||||
self.property.number_of_rooms
|
||||
|
|
@ -949,20 +951,20 @@ class HeatingRecommender:
|
|||
}
|
||||
|
||||
# Fallback if property type unknown
|
||||
base = base_by_type.get(self.property.data["property-type"], 1)
|
||||
base = base_by_type.get(self.property.epc_record.property_type, 1)
|
||||
|
||||
# Area-based adjustments
|
||||
if self.property.data["property-type"] in ("Flat", "Maisonette"):
|
||||
if self.property.epc_record.property_type in ("Flat", "Maisonette"):
|
||||
if self.property.floor_area > 90:
|
||||
return base + 1 # duplex or very large flat
|
||||
return base
|
||||
|
||||
if self.property.data["property-type"] == "Bungalow":
|
||||
if self.property.epc_record.property_type == "Bungalow":
|
||||
if self.property.floor_area > 100:
|
||||
return base + 1 # secondary corridor
|
||||
return base
|
||||
|
||||
if self.property.data["property-type"] == "House":
|
||||
if self.property.epc_record.property_type == "House":
|
||||
if self.property.floor_area > 140:
|
||||
return base + 1 # extra landing / circulation
|
||||
return base
|
||||
|
|
@ -1060,17 +1062,17 @@ class HeatingRecommender:
|
|||
**hot_water_simulation_config
|
||||
}
|
||||
# This upgrade will only take the heating system to average energy efficiency
|
||||
if self.property.data["mainheat-energy-eff"] in ["Very Poor", "Poor"] and not self.dual_heating:
|
||||
if self.property.epc_record.mainheat_energy_eff in ["Very Poor", "Poor"] and not self.dual_heating:
|
||||
heating_simulation_config["mainheat_energy_eff_ending"] = "Average"
|
||||
else:
|
||||
heating_simulation_config["mainheat_energy_eff_ending"] = self.property.data["mainheat-energy-eff"]
|
||||
heating_simulation_config["mainheat_energy_eff_ending"] = self.property.epc_record.mainheat_energy_eff
|
||||
|
||||
# TODO:We possibly shouldn't touch the hot water energy efficiency if we aren't recommending dual immersion
|
||||
# we'll keep this for the moment though
|
||||
if self.property.data["hot-water-energy-eff"] in ["Very Poor", "Poor"]:
|
||||
if self.property.epc_record.hot_water_energy_eff in ["Very Poor", "Poor"]:
|
||||
heating_simulation_config["hot_water_energy_eff_ending"] = "Average"
|
||||
else:
|
||||
heating_simulation_config["hot_water_energy_eff_ending"] = self.property.data["hot-water-energy-eff"]
|
||||
heating_simulation_config["hot_water_energy_eff_ending"] = self.property.epc_record.hot_water_energy_eff
|
||||
|
||||
number_heated_rooms = self._estimate_n_heated_rooms()
|
||||
|
||||
|
|
@ -1261,11 +1263,12 @@ class HeatingRecommender:
|
|||
boiler_recommendation = {}
|
||||
description_simulation = {}
|
||||
|
||||
has_inefficient_space_heating = self.property.data["mainheat-energy-eff"] in ["Very Poor", "Poor", "Average"]
|
||||
has_inefficient_space_heating = self.property.epc_record.mainheat_energy_eff in ["Very Poor", "Poor", "Average"]
|
||||
|
||||
# We check if there's a mains connection and the hot water is inefficient, as this will improve with a boiler
|
||||
has_inefficient_water = (
|
||||
self.property.data["mains-gas-flag"] and self.property.data["hot-water-energy-eff"] in ["Very Poor", "Poor"]
|
||||
self.property.epc_record.mains_gas_flag and self.property.epc_record.hot_water_energy_eff in ["Very Poor",
|
||||
"Poor"]
|
||||
)
|
||||
|
||||
non_invasive_recommendation = next((
|
||||
|
|
@ -1281,13 +1284,13 @@ class HeatingRecommender:
|
|||
)
|
||||
|
||||
new_heating_eff = (
|
||||
"Good" if self.property.data["mainheat-energy-eff"] in ["Very Poor", "Poor", "Average"]
|
||||
else self.property.data["mainheat-energy-eff"]
|
||||
"Good" if self.property.epc_record.mainheat_energy_eff in ["Very Poor", "Poor", "Average"]
|
||||
else self.property.epc_record.mainheat_energy_eff
|
||||
)
|
||||
|
||||
new_hotwater_eff = (
|
||||
"Good" if self.property.data["hot-water-energy-eff"] in ["Very Poor", "Poor", "Average"]
|
||||
else self.property.data["hot-water-energy-eff"]
|
||||
"Good" if self.property.epc_record.hot_water_energy_eff in ["Very Poor", "Poor", "Average"]
|
||||
else self.property.epc_record.hot_water_energy_eff
|
||||
)
|
||||
|
||||
simulation_config = {
|
||||
|
|
@ -1343,7 +1346,7 @@ class HeatingRecommender:
|
|||
boiler_costs = self.costs.boiler(
|
||||
exising_room_heaters=exising_room_heaters,
|
||||
system_change=system_change,
|
||||
n_heated_rooms=self.property.data["number-heated-rooms"],
|
||||
n_heated_rooms=self.property.epc_record.number_heated_rooms,
|
||||
n_rooms=self.property.number_of_rooms
|
||||
)
|
||||
|
||||
|
|
@ -1366,7 +1369,7 @@ class HeatingRecommender:
|
|||
"description_simulation": description_simulation,
|
||||
**boiler_costs,
|
||||
"system_type": "boiler_upgrade",
|
||||
"survey": non_invasive_recommendation.get("survey", None),
|
||||
"survey": check_use_survey(non_invasive_recommendation, self.property.epc_record.has_been_remodelled),
|
||||
"innovation_rate": 0,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from backend.Property import Property
|
||||
from recommendations.Costs import Costs
|
||||
from recommendations.recommendation_utils import override_costs, check_simulation_difference
|
||||
from recommendations.recommendation_utils import override_costs, check_simulation_difference, check_use_survey
|
||||
from etl.epc_clean.epc_attributes.HotWaterAttributes import HotWaterAttributes
|
||||
|
||||
|
||||
|
|
@ -39,7 +39,7 @@ class HotwaterRecommendations:
|
|||
self.recommend_tank_insulation(
|
||||
phase=recommendations_phase,
|
||||
sap_points=non_invasive_rec["sap_points"],
|
||||
survey=non_invasive_rec["survey"],
|
||||
survey=check_use_survey(non_invasive_rec, self.property.epc_record.has_been_remodelled),
|
||||
)
|
||||
|
||||
recommendations_phase += 1
|
||||
|
|
@ -47,7 +47,7 @@ class HotwaterRecommendations:
|
|||
self.recommend_cylinder_thermostat(
|
||||
phase=recommendations_phase,
|
||||
sap_points=non_invasive_rec["sap_points"],
|
||||
survey=non_invasive_rec["survey"],
|
||||
survey=check_use_survey(non_invasive_rec, self.property.epc_record.has_been_remodelled),
|
||||
)
|
||||
recommendations_phase += 1
|
||||
|
||||
|
|
@ -65,7 +65,7 @@ class HotwaterRecommendations:
|
|||
|
||||
if (
|
||||
(self.property.hotwater["heater_type"] in ["electric immersion"]) &
|
||||
(self.property.data["hot-water-energy-eff"] == "Very Poor") &
|
||||
(self.property.epc_record.hot_water_energy_eff == "Very Poor") &
|
||||
(self.property.hotwater["no_system_present"] is None) &
|
||||
(len(has_tank_recommendation) == 0)
|
||||
):
|
||||
|
|
@ -141,7 +141,7 @@ class HotwaterRecommendations:
|
|||
)
|
||||
|
||||
simulation_config = {
|
||||
"hot_water_energy_eff_ending": self.property.data["hot-water-energy-eff"],
|
||||
"hot_water_energy_eff_ending": self.property.epc_record.hot_water_energy_eff,
|
||||
**hotwater_simulation_config
|
||||
}
|
||||
|
||||
|
|
@ -158,7 +158,7 @@ class HotwaterRecommendations:
|
|||
**recommendation_cost,
|
||||
"simulation_config": simulation_config,
|
||||
"description_simulation": {
|
||||
"hot-water-energy-eff": self.property.data["hot-water-energy-eff"],
|
||||
"hot-water-energy-eff": self.property.epc_record.hot_water_energy_eff,
|
||||
"hotwater-description": new_epc_description,
|
||||
},
|
||||
"survey": survey,
|
||||
|
|
@ -198,10 +198,10 @@ class HotwaterRecommendations:
|
|||
new_config=hotwater_ending_config, old_config=self.property.hotwater
|
||||
)
|
||||
|
||||
if self.property.data["hot-water-energy-eff"] in ["Very Poor", "Poor", "Average"]:
|
||||
if self.property.epc_record.hot_water_energy_eff in ["Very Poor", "Poor", "Average"]:
|
||||
new_efficiency = "Good"
|
||||
else:
|
||||
new_efficiency = self.property.data["hot-water-energy-eff"]
|
||||
new_efficiency = self.property.epc_record.hot_water_energy_eff
|
||||
|
||||
simulation_config = {
|
||||
"hot_water_energy_eff_ending": new_efficiency,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import pandas as pd
|
|||
from backend.Property import Property
|
||||
from typing import List
|
||||
from recommendations.Costs import Costs
|
||||
from recommendations.recommendation_utils import override_costs
|
||||
from recommendations.recommendation_utils import override_costs, check_use_survey
|
||||
from backend.ml_models.AnnualBillSavings import AnnualBillSavings
|
||||
|
||||
|
||||
|
|
@ -169,7 +169,9 @@ class LightingRecommendations:
|
|||
"low-energy-lighting": 100,
|
||||
},
|
||||
**cost_result,
|
||||
"survey": leds_recommendation_config.get("survey", False),
|
||||
"survey": check_use_survey(
|
||||
leds_recommendation_config, self.property.epc_record.has_been_remodelled
|
||||
),
|
||||
"innovation_rate": self.material["innovation_rate"],
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,392 +0,0 @@
|
|||
import itertools
|
||||
from utils.logger import setup_logger
|
||||
from backend.Property import Property
|
||||
from recommendations.FloorRecommendations import FloorRecommendations
|
||||
from recommendations.WallRecommendations import WallRecommendations
|
||||
from recommendations.RoofRecommendations import RoofRecommendations
|
||||
from recommendations.VentilationRecommendations import VentilationRecommendations
|
||||
from recommendations.FireplaceRecommendations import FireplaceRecommendations
|
||||
from recommendations.LightingRecommendations import LightingRecommendations
|
||||
from recommendations.SolarPvRecommendations import SolarPvRecommendations
|
||||
from recommendations.WindowsRecommendations import WindowsRecommendations
|
||||
from recommendations.HeatingRecommender import HeatingRecommender
|
||||
from recommendations.HotwaterRecommendations import HotwaterRecommendations
|
||||
from recommendations.SecondaryHeating import SecondaryHeating
|
||||
from recommendations.Recommendations import Recommendations
|
||||
|
||||
logger = setup_logger()
|
||||
|
||||
|
||||
class Mds:
|
||||
"""
|
||||
Handles the contruction of the MDS report
|
||||
"""
|
||||
|
||||
format_map = {
|
||||
"external_wall_insulation": "EWI (Trad Const)",
|
||||
"internal_wall_insualtion": "IWI",
|
||||
"cavity_wall_insulation": "CWI",
|
||||
"loft_insulation": "LI",
|
||||
"air_source_heat_pump": "ASHP Htg",
|
||||
"high_heat_retention_storage_heaters": "High Heat Retention Storage Heaters",
|
||||
"solar_pv": "Solar PV",
|
||||
}
|
||||
|
||||
def __init__(self, property_instance: Property, materials, optimise_measures: bool = False):
|
||||
self.property_instance = property_instance
|
||||
|
||||
self.floor_recommender = FloorRecommendations(property_instance=property_instance, materials=materials)
|
||||
self.wall_recommender = WallRecommendations(property_instance=property_instance, materials=materials)
|
||||
self.roof_recommender = RoofRecommendations(property_instance=property_instance, materials=materials)
|
||||
self.ventilation_recomender = VentilationRecommendations(
|
||||
property_instance=property_instance, materials=materials
|
||||
)
|
||||
self.fireplace_recommender = FireplaceRecommendations(property_instance=property_instance)
|
||||
self.lighting_recommender = LightingRecommendations(property_instance=property_instance, materials=materials)
|
||||
self.windows_recommender = WindowsRecommendations(property_instance=property_instance, materials=materials)
|
||||
self.solar_recommender = SolarPvRecommendations(property_instance=property_instance)
|
||||
self.heating_recommender = HeatingRecommender(property_instance=property_instance)
|
||||
self.hotwater_recommender = HotwaterRecommendations(property_instance=property_instance)
|
||||
self.secondary_heating_recommender = SecondaryHeating(property_instance=property_instance)
|
||||
|
||||
# This flag indicates that we wish to optimise the measures, to the property, depending on the set of measures
|
||||
# we have been provided
|
||||
self.optimise_measures = optimise_measures
|
||||
|
||||
def select_optimal_measure_set(self, measures):
|
||||
|
||||
# This is the set
|
||||
all_considered_measures = [
|
||||
'external_wall_insulation',
|
||||
'cavity_wall_insulation',
|
||||
'loft_insulation',
|
||||
'air_source_heat_pump',
|
||||
'high_heat_retention_storage_heaters',
|
||||
'solar_pv'
|
||||
]
|
||||
|
||||
# Check if our measures are within the ones we've handled
|
||||
new = [m for m in measures if m not in all_considered_measures]
|
||||
if new:
|
||||
raise NotImplementedError("New measures - handle me")
|
||||
|
||||
def prune_options(options, measures):
|
||||
options_pruned = []
|
||||
for _group in options:
|
||||
group_pruned = [m for m in _group if m in measures]
|
||||
if not group_pruned:
|
||||
continue
|
||||
options_pruned.append(group_pruned)
|
||||
|
||||
return options_pruned
|
||||
|
||||
# For options in here, a property could only possibly have one of these
|
||||
one_choice_options = [
|
||||
["external_wall_insulation", "cavity_wall_insulation", "internal_wall_insulation"],
|
||||
["loft_insulation", "flat_roof_insulation", "room_in_roof_insulation"],
|
||||
["solid_floor_insulation", "suspended_floor_insulation"],
|
||||
]
|
||||
# prune one_choice_options based on the measure set considered for this property
|
||||
one_choice_options_pruned = prune_options(one_choice_options, measures)
|
||||
|
||||
# For options in here, a property could have one or the other so all should be considered
|
||||
multi_path_options = [
|
||||
["air_source_heat_pump", "high_heat_retention_storage_heaters", "gas_boiler"]
|
||||
]
|
||||
|
||||
multi_path_options_pruned = prune_options(multi_path_options, measures)
|
||||
|
||||
one_choice_combinations = [list(itertools.product(*one_choice_options_pruned))]
|
||||
one_choice_combinations = [list(x) for sublist in one_choice_combinations for x in sublist]
|
||||
multi_path_combinations = [list(itertools.product(*multi_path_options_pruned))]
|
||||
multi_path_combinations = [list(x) for sublist in multi_path_combinations for x in sublist]
|
||||
|
||||
one_choice_flat = [item for sublist in one_choice_options_pruned for item in sublist]
|
||||
multi_path_flat = [item for sublist in multi_path_options_pruned for item in sublist]
|
||||
|
||||
remaining_measures = [
|
||||
measure for measure in measures
|
||||
if measure not in one_choice_flat and measure not in multi_path_flat
|
||||
]
|
||||
|
||||
# Combine one_choice and multi_path combinations with remaining measures
|
||||
final_combinations = []
|
||||
for one_choice in one_choice_combinations:
|
||||
for multi_path in multi_path_combinations:
|
||||
final_combinations.append([m for m in one_choice + multi_path + remaining_measures])
|
||||
|
||||
pruned_combinations = []
|
||||
# TODO: We can do these checks once, outside of the loop and prune the combinations
|
||||
for combination in final_combinations:
|
||||
pruned_measures = []
|
||||
for measure in combination:
|
||||
if measure not in measures:
|
||||
continue
|
||||
# There are certain measures where we need to
|
||||
if measure == "external_wall_insulation":
|
||||
# Check if the wall is not cavity since the other wall types can take external wall insulation
|
||||
if (
|
||||
self.wall_recommender.ewi_valid() and
|
||||
not self.property_instance.walls["insulation_thickness"] in ["average", "above average"]
|
||||
):
|
||||
pruned_measures.append(measure)
|
||||
continue
|
||||
|
||||
if measure == "cavity_wall_insulation":
|
||||
# Check if the wall is cavity
|
||||
if (
|
||||
self.property_instance.walls['is_cavity_wall'] and
|
||||
not self.property_instance.walls['is_filled_cavity']
|
||||
):
|
||||
pruned_measures.append(measure)
|
||||
continue
|
||||
|
||||
if measure == "loft_insulation":
|
||||
# Check if the roof is suitable for loft insulation and the loft isn't already done
|
||||
# Or, if the home had a u-value for the roof, we don't recommend loft insulation
|
||||
if (
|
||||
self.property_instance.roof["is_pitched"] and
|
||||
not self.roof_recommender.is_loft_already_insulated() and
|
||||
self.property_instance.roof["thermal_transmittance_unit"] is None
|
||||
):
|
||||
pruned_measures.append(measure)
|
||||
continue
|
||||
|
||||
if measure == "solid_floor_insulation":
|
||||
# Check if the floor is solid
|
||||
if (
|
||||
self.property_instance.floor["is_solid"] and
|
||||
self.property_instance.floor["insulation_thickness"] not in ["average", "above average"] and
|
||||
self.property_instance.floor["thermal_transmittance_unit"] is not None
|
||||
):
|
||||
pruned_measures.append(measure)
|
||||
continue
|
||||
|
||||
if measure == "suspended_floor_insulation":
|
||||
# Check if the floor is suspended
|
||||
if (
|
||||
self.property_instance.floor["is_suspended"] and
|
||||
self.property_instance.floor["insulation_thickness"] not in ["average", "above average"] and
|
||||
self.property_instance.floor["thermal_transmittance_unit"] is not None
|
||||
):
|
||||
pruned_measures.append(measure)
|
||||
continue
|
||||
|
||||
if measure == "high_heat_retention_storage_heaters":
|
||||
|
||||
# For the moment, we recommend storage heaters if the property doesn't already
|
||||
# and don't make it contngent on controls
|
||||
already_has_hhr = self.heating_recommender.is_hhr_already_installed()
|
||||
|
||||
if (
|
||||
self.heating_recommender.is_high_heat_retention_valid() and
|
||||
not already_has_hhr
|
||||
):
|
||||
pruned_measures.append(measure)
|
||||
continue
|
||||
|
||||
if measure == "air_source_heat_pump":
|
||||
if self.heating_recommender.is_ashp_valid():
|
||||
pruned_measures.append(measure)
|
||||
continue
|
||||
|
||||
if measure == "solar_pv":
|
||||
if self.solar_recommender.is_solar_pv_valid():
|
||||
pruned_measures.append(measure)
|
||||
continue
|
||||
|
||||
raise NotImplementedError("Implement me")
|
||||
|
||||
if not pruned_measures:
|
||||
continue
|
||||
|
||||
pruned_measures_formatted = []
|
||||
for pm in pruned_measures:
|
||||
pruned_measures_formatted.append({pm: self.format_map[pm]})
|
||||
|
||||
pruned_combinations.append(pruned_measures_formatted)
|
||||
|
||||
# We're left with the subset of measures that are possible for this property
|
||||
# These are the possible groups of measures that could be applied to this home
|
||||
|
||||
return pruned_combinations
|
||||
|
||||
def _build(self, measure_config_list, measures):
|
||||
not_implemented_measures = [
|
||||
"party_wall_insulation",
|
||||
"ground_source_heat_pump",
|
||||
"shared_ground_loops",
|
||||
"communal_heat_networks",
|
||||
"district_heating_networks",
|
||||
"solar_thermal",
|
||||
"draught_proofing",
|
||||
"ev_charging",
|
||||
"battery",
|
||||
]
|
||||
# Check if we have a not implemented measure
|
||||
if any([m in not_implemented_measures for m in measure_config_list]):
|
||||
raise NotImplementedError("Not implemented measure in the property - implement me")
|
||||
|
||||
mds_recommendations = []
|
||||
errors = []
|
||||
phase = 0
|
||||
|
||||
# TODO: Could use a decarator to reduce the boilerplate code - insert_recommendation_id and then the append
|
||||
|
||||
if "external_wall_insulation" in measure_config_list:
|
||||
recs = self.wall_recommender.mds_recommend_ewi(phase=phase)
|
||||
if not recs:
|
||||
raise Exception("No recommendations for external wall insulation")
|
||||
recs = self.insert_recommendation_id(recs, measures, "external_wall_insulation")
|
||||
mds_recommendations.append(recs)
|
||||
if self.optimise_measures and len(recs):
|
||||
phase += 1
|
||||
|
||||
if "cavity_wall_insulation" in measure_config_list:
|
||||
recs = self.wall_recommender.mds_recommend_cavity_wall_insulation(phase=phase)
|
||||
recs = self.insert_recommendation_id(recs, measures, "cavity_wall_insulation")
|
||||
mds_recommendations.append(recs)
|
||||
if self.optimise_measures and len(recs):
|
||||
phase += 1
|
||||
|
||||
if "loft_insulation" in measure_config_list:
|
||||
# Check if the roof is suitable for loft insulation
|
||||
if self.property_instance.roof['is_roof_room']:
|
||||
errors.append("Roof is a room")
|
||||
else:
|
||||
recs = self.roof_recommender.mds_loft_insulation(phase=phase)
|
||||
if not recs:
|
||||
raise Exception("No recommendations for loft insulation")
|
||||
recs = self.insert_recommendation_id(recs, measures, "loft_insulation")
|
||||
mds_recommendations.append(recs)
|
||||
if self.optimise_measures and len(recs):
|
||||
phase += 1
|
||||
|
||||
if "internal_wall_insulation" in measure_config_list:
|
||||
raise Exception("check me out 4")
|
||||
self.wall_recommender.recommend(phase=phase)
|
||||
|
||||
if "suspended_floor_insulation" in measure_config_list:
|
||||
raise Exception("check me out 5")
|
||||
self.floor_recommender.recommend(phase=phase)
|
||||
|
||||
if "solid_floor_insulation" in measure_config_list:
|
||||
raise Exception("check me out 6")
|
||||
self.floor_recommender.recommend(phase=phase)
|
||||
|
||||
if "air_source_heat_pump" in measure_config_list:
|
||||
recs = self.heating_recommender.recommend_air_source_heat_pump(
|
||||
phase=phase, has_cavity_or_loft_recommendations=False, _return=True
|
||||
)
|
||||
recs = self.insert_recommendation_id(recs, measures, "air_source_heat_pump")
|
||||
mds_recommendations.append(recs)
|
||||
if self.optimise_measures and len(recs):
|
||||
phase += 1
|
||||
|
||||
if "high_heat_retention_storage_heaters" in measure_config_list:
|
||||
recs = self.heating_recommender.recommend_hhr_storage_heaters(
|
||||
phase=phase, system_change=True, heating_controls_only=False, _return=True
|
||||
)
|
||||
if recs is None:
|
||||
logger.info(
|
||||
f"No recommendations for high heat retention storage heaters, current heating "
|
||||
f"{self.property_instance.main_heating['clean_description']}"
|
||||
)
|
||||
else:
|
||||
recs = self.insert_recommendation_id(recs, measures, "high_heat_retention_storage_heaters")
|
||||
mds_recommendations.append(recs)
|
||||
if self.optimise_measures and len(recs):
|
||||
phase += 1
|
||||
|
||||
if "low_energy_lighting" in measure_config_list:
|
||||
raise Exception("check me out 9")
|
||||
self.lighting_recommender.recommend(phase=phase)
|
||||
|
||||
if "cylinder_insulation" in measure_config_list:
|
||||
raise Exception("check me out 10")
|
||||
self.hotwater_recommender.recommend(phase=phase)
|
||||
|
||||
if "smart_controls" in measure_config_list:
|
||||
raise Exception("check me out 11")
|
||||
self.heating_recommender.recommend(phase=phase)
|
||||
|
||||
if "zone_controls" in measure_config_list:
|
||||
raise Exception("check me out 12")
|
||||
self.heating_recommender.recommend(phase=phase)
|
||||
|
||||
if "trvs" in measure_config_list:
|
||||
raise Exception("check me out 13")
|
||||
self.heating_recommender.recommend(phase=phase)
|
||||
|
||||
if "solar_pv" in measure_config_list:
|
||||
recs = self.solar_recommender.mds_recommend(phase=phase, solar_pv_percentage=0.5)
|
||||
recs = self.insert_recommendation_id(recs, measures, "solar_pv")
|
||||
mds_recommendations.append(recs)
|
||||
if self.optimise_measures and len(recs):
|
||||
phase += 1
|
||||
|
||||
if "double_glazing" in measure_config_list:
|
||||
raise Exception("check me out 15")
|
||||
self.windows_recommender.recommend(phase=phase)
|
||||
|
||||
if "mechanical_ventilation" in measure_config_list:
|
||||
raise Exception("check me out 16")
|
||||
self.ventilation_recomender.recommend(phase=phase)
|
||||
|
||||
if "gas_boiler" in measure_config_list:
|
||||
raise Exception("check me out 17")
|
||||
self.heating_recommender.recommend(phase=phase)
|
||||
|
||||
if "flat_roof_insulation" in measure_config_list:
|
||||
raise Exception("check me out 18")
|
||||
self.roof_recommender.recommend(phase=phase)
|
||||
|
||||
if "room_in_roof_insulation" in measure_config_list:
|
||||
raise Exception("check me out 19")
|
||||
self.roof_recommender.recommend(phase=phase)
|
||||
|
||||
property_representative_recommendations = Recommendations.create_representative_recommendations(
|
||||
mds_recommendations, non_invasive_recommendations=[]
|
||||
)
|
||||
|
||||
return mds_recommendations, property_representative_recommendations, errors
|
||||
|
||||
def build(self):
|
||||
if self.property_instance.measures is None:
|
||||
raise NotImplementedError("No measures in the property - implement me")
|
||||
|
||||
if self.optimise_measures:
|
||||
measures_set = self.select_optimal_measure_set(self.property_instance.measures)
|
||||
mds_recommendations_map = {}
|
||||
representative_recommendations_map = {}
|
||||
errors_map = {}
|
||||
for measures in measures_set:
|
||||
measure_config_list = [list(x.keys())[0] for x in measures]
|
||||
mds_recommendations, rep_recommendations, errors = self._build(
|
||||
measure_config_list=measure_config_list,
|
||||
measures=measures
|
||||
)
|
||||
if errors:
|
||||
logger.info(f"Errors: {errors}")
|
||||
|
||||
mds_recommendations_map[str(measure_config_list)] = mds_recommendations
|
||||
representative_recommendations_map[str(measure_config_list)] = rep_recommendations
|
||||
errors_map[str(measure_config_list)] = errors
|
||||
|
||||
return mds_recommendations_map, representative_recommendations_map, errors_map
|
||||
|
||||
else:
|
||||
measure_config_list = [list(m.keys())[0] for m in self.property_instance.measures]
|
||||
return self._build(measure_config_list=measure_config_list, measures=self.property_instance.measures)
|
||||
|
||||
@staticmethod
|
||||
def insert_recommendation_id(recommendations, measures, measure_name):
|
||||
# Insert the recommendation identifier into this recommendation
|
||||
measure_config = [m for m in measures if measure_name in m][0]
|
||||
|
||||
idx = 0
|
||||
for r in recommendations:
|
||||
r["recommendation_id"] = list(measure_config.values())[0] + "-" + str(idx)
|
||||
idx += 1
|
||||
|
||||
return recommendations
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import pandas as pd
|
||||
import numpy as np
|
||||
from backend.Property import Property
|
||||
from typing import List, Mapping, Any
|
||||
from typing import List, Mapping, Any, Optional
|
||||
from itertools import groupby
|
||||
from recommendations.FloorRecommendations import FloorRecommendations
|
||||
from recommendations.WallRecommendations import WallRecommendations
|
||||
|
|
@ -49,7 +49,7 @@ class Recommendations:
|
|||
materials: List,
|
||||
exclusions: List[str] = None,
|
||||
inclusions: List[str] = None,
|
||||
default_u_values: bool = False,
|
||||
default_u_values: Optional[bool] = False,
|
||||
):
|
||||
"""
|
||||
:param property_instance: Instance of the Property class, for the home associated to property_id
|
||||
|
|
@ -581,10 +581,10 @@ class Recommendations:
|
|||
) -> dict:
|
||||
if rec_phase == starting_phase:
|
||||
return {
|
||||
"sap": float(property_instance.data["current-energy-efficiency"]),
|
||||
"sap_prediction": float(property_instance.data["current-energy-efficiency"]),
|
||||
"carbon": float(property_instance.data["co2-emissions-current"]),
|
||||
"heat_demand": float(property_instance.data["energy-consumption-current"]),
|
||||
"sap": float(property_instance.epc_record.current_energy_efficiency),
|
||||
"sap_prediction": float(property_instance.epc_record.current_energy_efficiency),
|
||||
"carbon": float(property_instance.epc_record.co2_emissions_current),
|
||||
"heat_demand": float(property_instance.epc_record.energy_consumption_current),
|
||||
}
|
||||
|
||||
previous_phase_reps = [
|
||||
|
|
@ -599,10 +599,10 @@ class Recommendations:
|
|||
# run the next step and run a median of nothing, which will return None
|
||||
if not previous_phase_reps:
|
||||
return {
|
||||
"sap": float(property_instance.data["current-energy-efficiency"]),
|
||||
"sap_prediction": float(property_instance.data["current-energy-efficiency"]),
|
||||
"carbon": float(property_instance.data["co2-emissions-current"]),
|
||||
"heat_demand": float(property_instance.data["energy-consumption-current"]),
|
||||
"sap": property_instance.epc_record.current_energy_efficiency,
|
||||
"sap_prediction": property_instance.epc_record.current_energy_efficiency,
|
||||
"carbon": property_instance.epc_record.co2_emissions_current,
|
||||
"heat_demand": property_instance.epc_record.energy_consumption_current,
|
||||
}
|
||||
|
||||
# Median fallback (including zero-length case)
|
||||
|
|
@ -707,7 +707,7 @@ class Recommendations:
|
|||
# For the moment, we cap the number of SAP points that can be achieved by LEDs at 2
|
||||
if rec["type"] == "low_energy_lighting":
|
||||
lighting_sap_limit = LightingRecommendations.get_sap_limit(
|
||||
property_instance.data["lighting-energy-eff"],
|
||||
property_instance.epc_record.lighting_energy_eff,
|
||||
property_instance.lighting["low_energy_proportion"]
|
||||
)
|
||||
|
||||
|
|
@ -802,7 +802,7 @@ class Recommendations:
|
|||
# By limiting here, we don't change the value in current_phase_values. This means that the
|
||||
# future recommendations won't have an impact that is too large
|
||||
li_sap_limit = RoofRecommendations.get_loft_insulation_sap_limit(
|
||||
property_instance.data["roof-energy-eff"], property_instance.roof["insulation_thickness"]
|
||||
property_instance.epc_record.roof_energy_eff, property_instance.roof["insulation_thickness"]
|
||||
)
|
||||
if li_sap_limit is not None:
|
||||
new_value = min(property_phase_impact["sap"], li_sap_limit)
|
||||
|
|
@ -1246,9 +1246,9 @@ class Recommendations:
|
|||
{
|
||||
"id": STARTING_DUMMY_ID_VALUE,
|
||||
**cls.map_descriptions_to_fuel(
|
||||
property_instance.data["mainheat-description"],
|
||||
property_instance.data["hotwater-description"],
|
||||
property_instance.data["main-fuel"],
|
||||
property_instance.epc_record.mainheat_description,
|
||||
property_instance.epc_record.hotwater_description,
|
||||
property_instance.epc_record.main_fuel,
|
||||
descriptions_to_fuel_types
|
||||
)
|
||||
}
|
||||
|
|
@ -1271,7 +1271,7 @@ class Recommendations:
|
|||
# 2) Have an average efficiency boiler, we adjust the COP of the existing boiler down to 75%
|
||||
heating_upgrades = [x for x in property_recommendations if x[0]["type"] == "heating"]
|
||||
boiler_upgrade = [r for recs in heating_upgrades for r in recs if r["measure_type"] == "boiler_upgrade"]
|
||||
existing_heating_efficiency = property_instance.data["mainheat-energy-eff"]
|
||||
existing_heating_efficiency = property_instance.epc_record.mainheat_energy_eff
|
||||
|
||||
if len(boiler_upgrade) and existing_heating_efficiency in ["Very Poor", "Poor", "Average"]:
|
||||
efficiency_map = {"Very Poor": 0.6, "Poor": 0.65, "Average": 0.7}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from datatypes.enums import QuantityUnits
|
|||
from recommendations.recommendation_utils import (
|
||||
get_roof_u_value, r_value_per_mm_to_u_value, calculate_u_value_uplift, is_diminishing_returns,
|
||||
update_lowest_selected_u_value, get_recommended_part, convert_thickness_to_numeric, override_costs,
|
||||
check_simulation_difference
|
||||
check_simulation_difference, check_use_survey
|
||||
)
|
||||
from recommendations.Costs import Costs
|
||||
from etl.epc_clean.epc_attributes.RoofAttributes import RoofAttributes
|
||||
|
|
@ -800,7 +800,7 @@ class RoofRecommendations:
|
|||
if proposed_depth >= 300:
|
||||
new_efficiency = "Very Good"
|
||||
else:
|
||||
if self.property.data["roof-energy-eff"] not in ["Good", "Very Good"]:
|
||||
if self.property.epc_record.roof_energy_eff not in ["Good", "Very Good"]:
|
||||
new_efficiency = "Good"
|
||||
else:
|
||||
new_efficiency = "Very Good"
|
||||
|
|
@ -874,7 +874,9 @@ class RoofRecommendations:
|
|||
"roof-energy-eff": new_efficiency
|
||||
},
|
||||
**cost_result,
|
||||
"survey": non_invasive_recommendations.get("survey", False),
|
||||
"survey": check_use_survey(
|
||||
non_invasive_recommendations, self.property.epc_record.has_been_remodelled
|
||||
),
|
||||
"innovation_rate": material.to_dict()["innovation_rate"]
|
||||
}
|
||||
)
|
||||
|
|
@ -959,10 +961,10 @@ class RoofRecommendations:
|
|||
roof_simulation_config = check_simulation_difference(
|
||||
new_config=roof_ending_config, old_config=self.property.roof, prefix="roof_"
|
||||
)
|
||||
if self.property.data["roof-energy-eff"] in ["Very Poor", "Poor"]:
|
||||
if self.property.epc_record.roof_energy_eff in ["Very Poor", "Poor"]:
|
||||
new_efficiency = "Average"
|
||||
else:
|
||||
new_efficiency = self.property.data["roof-energy-eff"]
|
||||
new_efficiency = self.property.epc_record.roof_energy_eff
|
||||
|
||||
if default_u_values:
|
||||
new_u_value = get_roof_u_value(
|
||||
|
|
@ -1009,7 +1011,9 @@ class RoofRecommendations:
|
|||
},
|
||||
**cost_result,
|
||||
"already_installed": already_installed,
|
||||
"survey": rir_non_invasive_recommendation.get("survey", None),
|
||||
"survey": check_use_survey(
|
||||
rir_non_invasive_recommendation, self.property.epc_record.has_been_remodelled
|
||||
),
|
||||
"innovation_rate": material.innovation_rate
|
||||
}
|
||||
)
|
||||
|
|
@ -1079,7 +1083,9 @@ class RoofRecommendations:
|
|||
},
|
||||
**cost_result,
|
||||
"already_installed": "sloping_ceiling_insulation" in self.property.already_installed,
|
||||
"survey": sloping_ceiling_recommendation.get("survey", None),
|
||||
"survey": check_use_survey(
|
||||
sloping_ceiling_recommendation, self.property.epc_record.has_been_remodelled
|
||||
),
|
||||
"innovation_rate": 0
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -22,10 +22,10 @@ class SecondaryHeating:
|
|||
# No secondary heating system, so no recommendation to remove it
|
||||
return
|
||||
|
||||
if self.property.data['number-habitable-rooms'] > self.property.data['number-heated-rooms']:
|
||||
n_rooms = self.property.data['number-habitable-rooms'] - self.property.data['number-heated-rooms']
|
||||
if self.property.epc_record.number_habitable_rooms > self.property.epc_record.number_heated_rooms:
|
||||
n_rooms = self.property.epc_record.number_habitable_rooms - self.property.epc_record.number_heated_rooms
|
||||
else:
|
||||
n_rooms = self.property.data["number-heated-rooms"]
|
||||
n_rooms = self.property.epc_record.number_heated_rooms
|
||||
|
||||
costs = self.costs.heater_removal(n_rooms=n_rooms)
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ from BaseUtility import Definitions
|
|||
from etl.epc_clean.epc_attributes.WallAttributes import WallAttributes
|
||||
from recommendations.recommendation_utils import (
|
||||
r_value_per_mm_to_u_value, calculate_u_value_uplift, is_diminishing_returns, update_lowest_selected_u_value,
|
||||
get_recommended_part, get_wall_u_value, override_costs, check_simulation_difference
|
||||
get_recommended_part, get_wall_u_value, override_costs, check_simulation_difference,
|
||||
check_use_survey
|
||||
)
|
||||
from recommendations.config import PARTIALLY_FILLED_PERCENTAGE_ASSUMPTION
|
||||
from recommendations.Costs import Costs
|
||||
|
|
@ -129,7 +130,7 @@ class WallRecommendations(Definitions):
|
|||
# Current logic: If the property is in a conservation area/heritage building/listed building or a flat,
|
||||
# it is not suitable for EWI
|
||||
if self.property.restricted_measures or (
|
||||
self.property.data["property-type"].lower() == "flat"
|
||||
self.property.epc_record.property_type.lower() == "flat"
|
||||
) or (
|
||||
self.property.walls['is_cob'] or
|
||||
self.property.walls['is_sandstone_or_limestone'] or
|
||||
|
|
@ -143,7 +144,8 @@ class WallRecommendations(Definitions):
|
|||
"""
|
||||
Checks if the wall is of a suitable type for internal/external wall insulation
|
||||
"""
|
||||
if self.property.walls["is_cavity_wall"] or self.property.walls["is_cob"]:
|
||||
if self.property.walls["is_cavity_wall"] or self.property.walls["is_cob"] or self.property.walls[
|
||||
"is_granite_or_whinstone"] or self.property.walls["is_sandstone_or_limestone"]:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
|
@ -181,7 +183,7 @@ class WallRecommendations(Definitions):
|
|||
|
||||
# If the property is a new build and the U-value is below 0.75, we don't recommend insulation because it's
|
||||
# not practical
|
||||
if (self.property.data["transaction-type"] == "new dwelling") and (
|
||||
if (self.property.epc_record.transaction_type == "new dwelling") and (
|
||||
u_value <= self.NEW_BUILD_INSULATED
|
||||
):
|
||||
# Recommend nothing
|
||||
|
|
@ -443,7 +445,9 @@ class WallRecommendations(Definitions):
|
|||
"walls-energy-eff": "Good"
|
||||
},
|
||||
**cost_result,
|
||||
"survey": non_invasive_recommendations.get("survey", False),
|
||||
"survey": check_use_survey(
|
||||
non_invasive_recommendations, self.property.epc_record.has_been_remodelled
|
||||
),
|
||||
"innovation_rate": material.to_dict()["innovation_rate"]
|
||||
}
|
||||
)
|
||||
|
|
@ -480,13 +484,13 @@ class WallRecommendations(Definitions):
|
|||
x["construction-age-band"] == self.property.construction_age_band
|
||||
][0]
|
||||
|
||||
if self.property.data["walls-energy-eff"] == "Good" and efficiency_data["walls-energy-eff"] not in [
|
||||
if self.property.epc_record.walls_energy_eff == "Good" and efficiency_data["walls-energy-eff"] not in [
|
||||
"Good", "Very Good"
|
||||
]:
|
||||
simulation_config = {
|
||||
"walls_energy_eff_ending": self.property.data["walls-energy-eff"]
|
||||
"walls_energy_eff_ending": self.property.epc_record.walls_energy_eff
|
||||
}
|
||||
elif self.property.data["walls-energy-eff"] == "Very Good":
|
||||
elif self.property.epc_record.walls_energy_eff == "Very Good":
|
||||
simulation_config = {
|
||||
"walls_energy_eff_ending": "Very Good"
|
||||
}
|
||||
|
|
@ -573,7 +577,6 @@ class WallRecommendations(Definitions):
|
|||
raise ValueError("Invalid material type")
|
||||
|
||||
sap_points = non_invasive_recommendations.get("sap_points", None)
|
||||
survey = non_invasive_recommendations.get("survey", False)
|
||||
|
||||
wall_ending_config = WallAttributes(new_description).process()
|
||||
|
||||
|
|
@ -624,7 +627,9 @@ class WallRecommendations(Definitions):
|
|||
"walls-energy-eff": simulation_config["walls_energy_eff_ending"]
|
||||
},
|
||||
**cost_result,
|
||||
"survey": survey,
|
||||
"survey": check_use_survey(
|
||||
non_invasive_recommendations, self.property.epc_record.has_been_remodelled
|
||||
),
|
||||
"innovation_rate": material.to_dict()["innovation_rate"]
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from backend.Property import Property
|
|||
from backend.app.plan.schemas import MEASURE_MAP
|
||||
from etl.epc_clean.epc_attributes.WindowAttributes import WindowAttributes
|
||||
from recommendations.Costs import Costs
|
||||
from recommendations.recommendation_utils import override_costs, check_simulation_difference
|
||||
from recommendations.recommendation_utils import override_costs, check_simulation_difference, check_use_survey
|
||||
|
||||
|
||||
class WindowsRecommendations:
|
||||
|
|
@ -75,7 +75,7 @@ class WindowsRecommendations:
|
|||
# If the property currently has some secondary glazing but isn't in a conservation area
|
||||
#
|
||||
is_secondary_glazing = self.property.restricted_measures and (
|
||||
self.property.data["windows-energy-eff"] in ["Poor", "Very Poor"]
|
||||
self.property.epc_record.windows_energy_eff in ["Poor", "Very Poor"]
|
||||
)
|
||||
|
||||
# We check if the windows are partially insulated but we're recommending double glazing as a complete
|
||||
|
|
@ -90,17 +90,17 @@ class WindowsRecommendations:
|
|||
raise ValueError("Number of windows not specified")
|
||||
|
||||
# We scale the number of windows based on the proportion of existing glazing
|
||||
if self.property.data["multi-glaze-proportion"] != "":
|
||||
if self.property.epc_record.multi_glaze_proportion != "":
|
||||
|
||||
if (self.property.windows["clean_description"] == "Some double glazing") and (
|
||||
self.property.data["windows-energy-eff"] == "Very Poor") and (
|
||||
self.property.data["multi-glaze-proportion"] == 100
|
||||
self.property.epc_record.windows_energy_eff == "Very Poor") and (
|
||||
self.property.epc_record.multi_glaze_proportion == 100
|
||||
):
|
||||
# In this case, we assume all of the dinwos need replacing
|
||||
n_windows_scalar = 1
|
||||
else:
|
||||
n_windows_scalar = 1 - (
|
||||
int(self.property.data["multi-glaze-proportion"]) / 100
|
||||
int(self.property.epc_record.multi_glaze_proportion) / 100
|
||||
)
|
||||
else:
|
||||
n_windows_scalar = self.COVERAGE_MAP.get(
|
||||
|
|
@ -186,7 +186,7 @@ class WindowsRecommendations:
|
|||
glazed_type_ending = "double glazing installed during or after 2002"
|
||||
new_windows_description = "Fully double glazed"
|
||||
else:
|
||||
if self.property.data["multi-glaze-proportion"] < 50:
|
||||
if self.property.epc_record.multi_glaze_proportion < 50:
|
||||
glazed_type_ending = "secondary glazing"
|
||||
else:
|
||||
glazed_type_ending = "double glazing installed during or after 2002"
|
||||
|
|
@ -203,7 +203,7 @@ class WindowsRecommendations:
|
|||
glazed_type_ending = "secondary glazing"
|
||||
new_windows_description = "Full secondary glazing"
|
||||
else:
|
||||
if self.property.data["multi-glaze-proportion"] < 50:
|
||||
if self.property.epc_record.multi_glaze_proportion < 50:
|
||||
glazed_type_ending = "double glazing installed during or after 2002"
|
||||
else:
|
||||
glazed_type_ending = "secondary glazing"
|
||||
|
|
@ -214,7 +214,7 @@ class WindowsRecommendations:
|
|||
else:
|
||||
raise ValueError("Invalid glazing type - implement me")
|
||||
|
||||
if self.property.data["windows-energy-eff"] == "Very Good":
|
||||
if self.property.epc_record.windows_energy_eff == "Very Good":
|
||||
windows_energy_eff = "Very Good"
|
||||
|
||||
# For post 2002 windows, the energy efficiency is "Good" and so for the simulation, we simulate with "Good"
|
||||
|
|
@ -259,7 +259,9 @@ class WindowsRecommendations:
|
|||
"is_secondary_glazing": is_secondary_glazing,
|
||||
"description_simulation": description_simulation,
|
||||
"simulation_config": simulation_config,
|
||||
"survey": non_invasive_recommendation.get("survey", None),
|
||||
"survey": check_use_survey(
|
||||
non_invasive_recommendation, self.property.epc_record.has_been_remodelled
|
||||
),
|
||||
"innovation_rate": self.glazing_material["innovation_rate"],
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -285,7 +285,7 @@ def optimise_with_funding_paths(
|
|||
# We add in generic insulation funding paths (where there is no fixed measure)
|
||||
# Heating controls are only eligible if installed as part of a heating upgrade and so we do not include them
|
||||
# here. We don't have an option if the property is a C or above
|
||||
if housing_type == "Social" and p.data["current-energy-rating"] not in ["C", "B", "A"]:
|
||||
if housing_type == "Social" and p.epc_record.current_energy_rating not in ["C", "B", "A"]:
|
||||
funding_paths = (
|
||||
[
|
||||
{
|
||||
|
|
@ -297,7 +297,7 @@ def optimise_with_funding_paths(
|
|||
)
|
||||
|
||||
needs_pre_eco_hhrsh_upgrade = (
|
||||
(p.data["current-energy-rating"] == "D") and work_package == "solar_hhrsh_eco4"
|
||||
(p.epc_record.current_energy_rating == "D") and work_package == "solar_hhrsh_eco4"
|
||||
)
|
||||
|
||||
for path_spec in funding_paths:
|
||||
|
|
@ -306,7 +306,7 @@ def optimise_with_funding_paths(
|
|||
if isinstance(path_spec, dict) and path_spec.get("reference") == "fabric-only:eco4":
|
||||
sub_measures = _filter_measures_by_types(optimisation_input_measures, path_spec["allowed_types"])
|
||||
# If the property is EPC D and socil, we also include just innovation measures
|
||||
if housing_type == "Social" and p.data["current-energy-rating"] == "D":
|
||||
if housing_type == "Social" and p.epc_record.current_energy_rating == "D":
|
||||
# We add in a second option which is just innovation measures
|
||||
sub_measures_innovation = []
|
||||
for measures in sub_measures:
|
||||
|
|
@ -354,7 +354,7 @@ def optimise_with_funding_paths(
|
|||
"path": path_spec,
|
||||
"scheme": scheme,
|
||||
"is_eligible": _is_eligible_funding_package(
|
||||
scheme, float(p.data["current-energy-efficiency"]), sub_gain
|
||||
scheme, p.epc_record.current_energy_efficiency, sub_gain
|
||||
),
|
||||
"unfunded_items": unfunded_picked,
|
||||
"already_installed_gain": already_installed_gain
|
||||
|
|
@ -500,9 +500,7 @@ def optimise_with_funding_paths(
|
|||
"total_gain": total_gain,
|
||||
"path": path_spec,
|
||||
"scheme": scheme,
|
||||
"is_eligible": _is_eligible_funding_package(
|
||||
scheme, int(p.data["current-energy-efficiency"]), total_gain
|
||||
),
|
||||
"is_eligible": _is_eligible_funding_package(scheme, p.epc_record.current_energy_efficiency, total_gain),
|
||||
"unfunded_items": unfunded_picked,
|
||||
"already_installed_gain": already_installed_gain
|
||||
})
|
||||
|
|
@ -523,7 +521,7 @@ def optimise_with_funding_paths(
|
|||
# logger.info("We have some packages that are fundable but do not meet the target gain")
|
||||
|
||||
# We now can calculate the project ABS, which subtracts from the cost, but this is only relevant for ECO4
|
||||
solutions["starting_sap"] = int(p.data["current-energy-efficiency"])
|
||||
solutions["starting_sap"] = p.epc_record.current_energy_efficiency
|
||||
solutions["floor_area"] = p.floor_area
|
||||
solutions["ending_sap"] = solutions["starting_sap"] + solutions["total_gain"]
|
||||
# We flag projects that are including batteries
|
||||
|
|
@ -677,7 +675,7 @@ def optimise_with_scenarios(
|
|||
for x in measures:
|
||||
if x["has_battery"]:
|
||||
x["battery_gain"] = BatterySAPScorer.score(
|
||||
starting_sap=int(p.data["current-energy-efficiency"]) + target_gain + 1,
|
||||
starting_sap=p.epc_record.current_energy_efficiency + target_gain + 1,
|
||||
pv_size=x["array_size"]
|
||||
)
|
||||
x["gain"] += x["battery_gain"]
|
||||
|
|
@ -893,7 +891,7 @@ def append_solution_metrics(solutions, target_gain, p, already_installed_sap=0):
|
|||
# We need the ending SAP, but we'll need to remove the battery SAP uplift first
|
||||
|
||||
solutions_df["ending_sap_without_battery"] = solutions_df.apply(
|
||||
lambda x: int(p.data["current-energy-efficiency"]) + already_installed_sap + _get_ending_sap_without_battery(x),
|
||||
lambda x: p.epc_record.current_energy_efficiency + already_installed_sap + _get_ending_sap_without_battery(x),
|
||||
axis=1
|
||||
)
|
||||
|
||||
|
|
@ -1162,7 +1160,7 @@ def _make_solar_heating_funding_paths(
|
|||
p, input_measures, funding_paths, remaining_insulation_type, housing_type, funding: Funding
|
||||
):
|
||||
# If a property is private and EPC D or above, it's not eligible
|
||||
if housing_type == "Private" and p.data["current-energy-rating"] in ["D", "C", "B", "A"]:
|
||||
if housing_type == "Private" and p.epc_record.current_energy_rating in ["D", "C", "B", "A"]:
|
||||
return funding_paths
|
||||
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
# Solar PV with existing eligible heating system
|
||||
|
|
@ -1288,7 +1286,7 @@ def make_funding_paths(p, input_measures, housing_type, funding: Funding, work_p
|
|||
"""
|
||||
|
||||
# If the property is currently EPC C, there is no funding availability
|
||||
if p.data["current-energy-rating"] in ["C", "B", "A"]:
|
||||
if p.epc_record.current_energy_rating in ["C", "B", "A"]:
|
||||
return [], input_measures
|
||||
|
||||
# We handle the case of minimum insulation requirements. Whenever we have a heating system recommendation,
|
||||
|
|
@ -1316,7 +1314,7 @@ def make_funding_paths(p, input_measures, housing_type, funding: Funding, work_p
|
|||
|
||||
funding_paths = []
|
||||
|
||||
if housing_type == "Social" and p.data["current-energy-rating"] == "D":
|
||||
if housing_type == "Social" and p.epc_record.current_energy_rating == "D":
|
||||
# If the property is currently EPC D, we can only include innovation measures or measures to meet the
|
||||
# minimum insulation requirements. We make an exception if we have a measure that is
|
||||
# already installed, specifically a heat pump
|
||||
|
|
@ -1362,7 +1360,7 @@ def make_funding_paths(p, input_measures, housing_type, funding: Funding, work_p
|
|||
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
# 1) The package must include EWI or IWI if the property is private rental sector
|
||||
# We check if we have any EWI or IWI measures available - only for EPC E or below
|
||||
if p.data["current-energy-rating"] in ["E", "F", "G"]:
|
||||
if p.epc_record.current_energy_rating in ["E", "F", "G"]:
|
||||
ewi_or_iwi = [{"OR": []}]
|
||||
reference_measures = []
|
||||
# If we have EWI we add it in
|
||||
|
|
|
|||
|
|
@ -207,7 +207,6 @@ def calculate_gain(
|
|||
body: PlanTriggerRequest,
|
||||
p: Property,
|
||||
fixed_gain: float,
|
||||
eco_packages: None | dict = None,
|
||||
already_installed_gain: float = 0,
|
||||
) -> float | None:
|
||||
"""
|
||||
|
|
@ -226,7 +225,6 @@ def calculate_gain(
|
|||
Property object with EPC data (must have p.data["current-energy-efficiency"]).
|
||||
fixed_gain : float
|
||||
Total fixed gain from required measures (returned by calculate_fixed_gain).
|
||||
eco_packages : dict, optional
|
||||
already_installed_gain: float, optional
|
||||
|
||||
Returns
|
||||
|
|
@ -235,15 +233,8 @@ def calculate_gain(
|
|||
Required SAP gain for EPC, or None for non-EPC goals.
|
||||
"""
|
||||
if body.goal == "Increasing EPC":
|
||||
current_sap = int(p.data["current-energy-efficiency"]) + already_installed_gain
|
||||
|
||||
if eco_packages is None:
|
||||
target_sap = epc_to_sap_lower_bound(body.goal_value)
|
||||
else:
|
||||
target_sap = (
|
||||
eco_packages.get(p.id)[1] if eco_packages.get(p.id)[1] is not None
|
||||
else epc_to_sap_lower_bound(body.goal_value)
|
||||
)
|
||||
current_sap = p.epc_record.current_energy_efficiency + already_installed_gain
|
||||
target_sap = epc_to_sap_lower_bound(body.goal_value)
|
||||
|
||||
if target_sap <= current_sap:
|
||||
# We've already met or exceeded the target EPC
|
||||
|
|
|
|||
|
|
@ -105,6 +105,13 @@ age_band_data = [
|
|||
"Northern_Ireland": "2023 onwards",
|
||||
"Park_home_UK": None,
|
||||
},
|
||||
{
|
||||
"age_band": "L",
|
||||
"England_Wales": "2012-2022",
|
||||
"Scotland": "2012 - 2023",
|
||||
"Northern_Ireland": "2014 -2022",
|
||||
"Park_home_UK": None,
|
||||
}
|
||||
]
|
||||
|
||||
england_wales_age_band_lookup = {
|
||||
|
|
@ -779,13 +786,13 @@ epc_wall_description_map = {
|
|||
"Sandstone or limestone, as built, no insulation": "Stone: sandstone or limestone as built",
|
||||
"Sandstone or limestone, with internal insulation": "Stone/solid brick with 100 mm external or internal insulation",
|
||||
"Sandstone or limestone, as built, partial insulation": "Stone/solid brick with 50 mm external or internal "
|
||||
"insulation",
|
||||
"insulation",
|
||||
"Sandstone, as built, no insulation": "Stone: sandstone or limestone as built",
|
||||
"Sandstone or limestone, as built, insulated": "Stone/solid brick with 100 mm external or internal insulation",
|
||||
"Sandstone, as built, insulated": "Stone/solid brick with 100 mm external or internal insulation",
|
||||
"Sandstone, with internal insulation": "Stone/solid brick with 100 mm external or internal insulation",
|
||||
"Sandstone or limestone, with external insulation": "Stone/solid brick with 100 mm external or internal "
|
||||
"insulation",
|
||||
"insulation",
|
||||
"Sandstone, with external insulation": "Stone/solid brick with 100 mm external or internal insulation",
|
||||
"Sandstone, as built, partial insulation": "Stone/solid brick with 50 mm external or internal insulation",
|
||||
############################
|
||||
|
|
@ -794,7 +801,8 @@ epc_wall_description_map = {
|
|||
"Granite or whinstone, as built, no insulation": "Stone: granite or whinstone as built",
|
||||
"Granite or whinstone, with internal insulation": "Stone/solid brick with 100 mm external or internal insulation",
|
||||
"Granite or whin, with internal insulation": "Stone/solid brick with 100 mm external or internal insulation",
|
||||
"Granite or whinstone, as built, partial insulation": "Stone/solid brick with 50 mm external or internal insulation",
|
||||
"Granite or whinstone, as built, partial insulation": "Stone/solid brick with 50 mm external or internal "
|
||||
"insulation",
|
||||
"Granite or whinstone, as built, insulated": "Stone/solid brick with 100 mm external or internal insulation",
|
||||
"Granite or whin, as built, insulated": "Stone/solid brick with 100 mm external or internal insulation",
|
||||
"Granite or whinstone, with external insulation": "Stone/solid brick with 100 mm external or internal insulation",
|
||||
|
|
@ -810,6 +818,7 @@ epc_wall_description_map = {
|
|||
############################
|
||||
# Cob wall mappings
|
||||
############################
|
||||
"Cob, as built, no insulation": "Cob as built",
|
||||
"Cob, as built": "Cob as built",
|
||||
"Cob, with external insulation": "Cob with 100 mm external or internal insulation",
|
||||
"Cob, with internal insulation": "Cob with 100 mm external or internal insulation",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import math
|
||||
from datetime import datetime
|
||||
from copy import deepcopy
|
||||
from typing import Union
|
||||
from typing import Union, Dict
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
|
@ -975,3 +975,16 @@ def combine_recommendation_configs(recommendation_config1, recommendation_config
|
|||
combined[key] = eff_2[key]
|
||||
|
||||
return combined
|
||||
|
||||
|
||||
def check_use_survey(non_invasive_recommendations: Dict[str, bool], has_been_remodelled: bool):
|
||||
"""
|
||||
Determines if we should use a survey SAP points or not
|
||||
:return:
|
||||
"""
|
||||
|
||||
use_survey = (
|
||||
non_invasive_recommendations.get("survey", False) if not
|
||||
has_been_remodelled else False
|
||||
)
|
||||
return use_survey
|
||||
|
|
|
|||
|
|
@ -183,9 +183,8 @@ class TestCosts:
|
|||
|
||||
def test_flat_roof_insulation(self):
|
||||
mock_property = Mock()
|
||||
mock_property.data = {
|
||||
"county": "Northamptonshire"
|
||||
}
|
||||
mock_property.epc_record = Mock()
|
||||
mock_property.epc_record.county = "Northamptonshire"
|
||||
|
||||
costs = Costs(mock_property)
|
||||
flat_roof_material = {
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -24,52 +24,33 @@ class TestFirepaceRecommendations:
|
|||
|
||||
def test_no_fireplaces(self, fireplace_materials):
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {
|
||||
"number-open-fireplaces": 0,
|
||||
}
|
||||
|
||||
epc_record.number_open_fireplaces = 0
|
||||
property_instance = Property(id=0, address="fake", postcode="fake", epc_record=epc_record)
|
||||
|
||||
recommender = FireplaceRecommendations(property_instance=property_instance, materials=fireplace_materials)
|
||||
|
||||
assert recommender.recommendation is None
|
||||
|
||||
recommender.recommend()
|
||||
|
||||
assert recommender.recommendation is None
|
||||
|
||||
def test_one_fireplace(self, fireplace_materials):
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {
|
||||
"number-open-fireplaces": 1,
|
||||
}
|
||||
epc_record.number_open_fireplaces = 1
|
||||
property_instance = Property(id=0, address="fake", postcode="fake", epc_record=epc_record)
|
||||
property_instance.already_installed = []
|
||||
|
||||
recommender = FireplaceRecommendations(property_instance=property_instance, materials=fireplace_materials)
|
||||
|
||||
assert recommender.recommendation is None
|
||||
|
||||
recommender.recommend()
|
||||
|
||||
assert recommender.recommendation
|
||||
assert recommender.recommendation[0]["type"] == "sealing_open_fireplace"
|
||||
assert recommender.recommendation[0]["total"] == 185
|
||||
|
||||
def test_multiple_fireplaces(self, fireplace_materials):
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {
|
||||
"number-open-fireplaces": 3,
|
||||
}
|
||||
epc_record.number_open_fireplaces = 3
|
||||
property_instance = Property(id=0, address="fake", postcode="fake", epc_record=epc_record)
|
||||
property_instance.already_installed = []
|
||||
|
||||
recommender = FireplaceRecommendations(property_instance=property_instance, materials=fireplace_materials)
|
||||
|
||||
assert recommender.recommendation is None
|
||||
|
||||
recommender.recommend()
|
||||
|
||||
assert recommender.recommendation
|
||||
assert recommender.recommendation[0]["type"] == "sealing_open_fireplace"
|
||||
assert recommender.recommendation[0]["total"] == 185 * 3
|
||||
|
|
|
|||
|
|
@ -19,29 +19,36 @@ from etl.epc.Record import EPCRecord
|
|||
|
||||
class TestFloorRecommendations:
|
||||
|
||||
@pytest.fixture
|
||||
def input_properties(self):
|
||||
with open(
|
||||
os.path.abspath(os.path.dirname(__file__)) + "/test_data/input_properties.pkl", "rb"
|
||||
) as f:
|
||||
return pickle.load(f)
|
||||
|
||||
def test_init(self, input_properties):
|
||||
input_properties[0].insulation_floor_area = 50
|
||||
input_properties[0].insulation_wall_area = 90
|
||||
def test_init(self):
|
||||
p = Mock()
|
||||
p.epc_record = Mock()
|
||||
p.epc_record.county = "Greater London"
|
||||
p.epc_record.local_authority_label = "London"
|
||||
p.epc_record.insulation_floor_area = 50
|
||||
p.epc_record.insulation_wall_area = 90
|
||||
p.insulation_floor_area = 50
|
||||
p.insulation_wall_area = 90
|
||||
p.floor = {"another_property_below": False}
|
||||
obj = FloorRecommendations(
|
||||
property_instance=input_properties[0],
|
||||
property_instance=p,
|
||||
materials=materials
|
||||
)
|
||||
assert obj
|
||||
assert obj.property
|
||||
|
||||
def test_other_premises_below(self, input_properties):
|
||||
input_properties[0].insulation_floor_area = 100
|
||||
input_properties[0].insulation_wall_area = 999
|
||||
input_properties[0].number_of_floors = 1
|
||||
def test_other_premises_below(self):
|
||||
p = Mock()
|
||||
p.epc_record = Mock()
|
||||
p.epc_record.county = "Greater London"
|
||||
p.epc_record.local_authority_label = "London"
|
||||
p.epc_record.insulation_floor_area = 100
|
||||
p.epc_record.insulation_wall_area = 999
|
||||
p.insulation_floor_area = 100
|
||||
p.insulation_wall_area = 999
|
||||
p.number_of_floors = 1
|
||||
p.floor = {"another_property_below": True, "thermal_transmittance": None, "insulation_thickness": None}
|
||||
recommender = FloorRecommendations(
|
||||
property_instance=input_properties[0],
|
||||
property_instance=p,
|
||||
materials=materials
|
||||
)
|
||||
recommender.recommend()
|
||||
|
|
@ -49,25 +56,41 @@ class TestFloorRecommendations:
|
|||
|
||||
assert not recommender.recommendations
|
||||
|
||||
def test_suspended_no_insulation(self, input_properties):
|
||||
def test_suspended_no_insulation(self):
|
||||
"""
|
||||
For a suspended floor without insulation, we use the rdsap methogology to estimate a U-value for the floor
|
||||
:return:
|
||||
"""
|
||||
|
||||
input_properties[2].insulation_floor_area = 50
|
||||
input_properties[2].insulation_wall_area = 50
|
||||
input_properties[2].walls["is_park_home"] = False
|
||||
input_properties[2].age_band = "A"
|
||||
input_properties[2].perimeter = 20
|
||||
input_properties[2].wall_type = "solid brick"
|
||||
input_properties[2].floor_type = "suspended"
|
||||
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)
|
||||
p = Mock()
|
||||
p.epc_record = Mock()
|
||||
p.epc_record.county = "Greater London"
|
||||
p.epc_record.local_authority_label = "London"
|
||||
p.epc_record.insulation_floor_area = 50
|
||||
p.epc_record.insulation_wall_area = 50
|
||||
p.insulation_floor_area = 50
|
||||
p.insulation_wall_area = 50
|
||||
p.walls = {"is_park_home": False}
|
||||
p.age_band = "A"
|
||||
p.perimeter = 20
|
||||
p.wall_type = "solid brick"
|
||||
p.floor_type = "suspended"
|
||||
p.number_of_floors = 1
|
||||
p.floor_level = 0
|
||||
p.already_installed = []
|
||||
p.non_invasive_recommendations = {}
|
||||
p.floor = {
|
||||
"is_suspended": True,
|
||||
"is_solid": False,
|
||||
"another_property_below": False,
|
||||
"thermal_transmittance": None,
|
||||
"insulation_thickness": None,
|
||||
"thermal_transmittance_unit": None,
|
||||
"is_assumed": False,
|
||||
"is_to_unheated_space": False,
|
||||
"is_to_external_air": False,
|
||||
}
|
||||
p.full_sap_epc = {}
|
||||
recommender = FloorRecommendations(property_instance=p, materials=materials)
|
||||
assert recommender.estimated_u_value is None
|
||||
recommender.recommend()
|
||||
assert recommender.property.floor["is_suspended"]
|
||||
|
|
@ -82,18 +105,33 @@ class TestFloorRecommendations:
|
|||
assert recommender.recommendations[0]["total"] == 4687.5
|
||||
assert recommender.recommendations[0]["new_u_value"] == 0.21
|
||||
|
||||
def test_uvalue_0_12(self, input_properties):
|
||||
def test_uvalue_0_12(self):
|
||||
"""
|
||||
This is a home that doesn't have a property below but it's highly performant already and therefore
|
||||
does not need floor insulation
|
||||
:return:
|
||||
"""
|
||||
input_properties[3].insulation_floor_area = 100
|
||||
input_properties[3].insulation_wall_area = 100
|
||||
input_properties[3].number_of_floors = 1
|
||||
input_properties[3].floor_level = 0
|
||||
|
||||
recommender = FloorRecommendations(property_instance=input_properties[3], materials=materials)
|
||||
p = Mock()
|
||||
p.epc_record = Mock()
|
||||
p.epc_record.county = "Greater London"
|
||||
p.epc_record.local_authority_label = "London"
|
||||
p.epc_record.insulation_floor_area = 100
|
||||
p.epc_record.insulation_wall_area = 100
|
||||
p.insulation_floor_area = 100
|
||||
p.insulation_wall_area = 100
|
||||
p.number_of_floors = 1
|
||||
p.floor_level = 0
|
||||
p.floor = {
|
||||
"is_suspended": False,
|
||||
"is_solid": False,
|
||||
"another_property_below": False,
|
||||
"thermal_transmittance": 0.12,
|
||||
"insulation_thickness": None,
|
||||
"is_to_unheated_space": False,
|
||||
"is_to_external_air": False,
|
||||
}
|
||||
p.full_sap_epc = {}
|
||||
recommender = FloorRecommendations(property_instance=p, materials=materials)
|
||||
assert recommender.estimated_u_value is None
|
||||
recommender.recommend()
|
||||
assert not recommender.property.floor["is_suspended"]
|
||||
|
|
@ -101,26 +139,41 @@ class TestFloorRecommendations:
|
|||
assert recommender.estimated_u_value is None
|
||||
assert not recommender.recommendations
|
||||
|
||||
def test_solid_no_insulation(self, input_properties):
|
||||
def test_solid_no_insulation(self):
|
||||
"""
|
||||
:return:
|
||||
"""
|
||||
|
||||
input_properties[4].insulation_floor_area = 100
|
||||
input_properties[4].insulation_wall_area = 100
|
||||
input_properties[4].walls["is_park_home"] = False
|
||||
input_properties[4].age_band = "B"
|
||||
input_properties[4].perimeter = 50
|
||||
input_properties[4].wall_type = "solid brick"
|
||||
input_properties[4].floor_type = "solid"
|
||||
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"] = ""
|
||||
recommender = FloorRecommendations(property_instance=input_properties[4], materials=materials)
|
||||
p = Mock()
|
||||
p.epc_record = Mock()
|
||||
p.epc_record.county = ""
|
||||
p.epc_record.local_authority_label = "London"
|
||||
p.epc_record.insulation_floor_area = 100
|
||||
p.epc_record.insulation_wall_area = 100
|
||||
p.insulation_floor_area = 100
|
||||
p.insulation_wall_area = 100
|
||||
p.walls = {"is_park_home": False}
|
||||
p.age_band = "B"
|
||||
p.perimeter = 50
|
||||
p.wall_type = "solid brick"
|
||||
p.floor_type = "solid"
|
||||
p.number_of_floors = 1
|
||||
p.floor_level = 0
|
||||
p.already_installed = []
|
||||
p.non_invasive_recommendations = {}
|
||||
p.data = {"county": ""}
|
||||
p.floor = {
|
||||
"is_suspended": False,
|
||||
"is_solid": True,
|
||||
"another_property_below": False,
|
||||
"thermal_transmittance": None,
|
||||
"insulation_thickness": None,
|
||||
"is_to_unheated_space": False,
|
||||
"is_to_external_air": False,
|
||||
"thermal_transmittance_unit": None,
|
||||
"is_assumed": True,
|
||||
}
|
||||
p.full_sap_epc = {}
|
||||
recommender = FloorRecommendations(property_instance=p, materials=materials)
|
||||
assert recommender.estimated_u_value is None
|
||||
recommender.recommend()
|
||||
assert not recommender.property.floor["is_suspended"]
|
||||
|
|
@ -148,16 +201,27 @@ class TestFloorRecommendations:
|
|||
'floor-description': 'Solid, insulated'
|
||||
}
|
||||
|
||||
def test_another_dwelling_below(self, input_properties):
|
||||
def test_another_dwelling_below(self):
|
||||
"""
|
||||
This is another description we see when there is a property below
|
||||
"""
|
||||
|
||||
input_properties[6].insulation_floor_area = 100
|
||||
input_properties[6].insulation_wall_area = 1
|
||||
|
||||
input_properties[6].number_of_floors = 1
|
||||
recommender = FloorRecommendations(property_instance=input_properties[6], materials=materials)
|
||||
p = Mock()
|
||||
p.epc_record = Mock()
|
||||
p.epc_record.county = "Greater London"
|
||||
p.epc_record.local_authority_label = "London"
|
||||
p.epc_record.insulation_floor_area = 100
|
||||
p.epc_record.insulation_wall_area = 1
|
||||
p.insulation_floor_area = 100
|
||||
p.insulation_wall_area = 1
|
||||
p.number_of_floors = 1
|
||||
p.floor = {
|
||||
"is_suspended": False,
|
||||
"is_solid": False,
|
||||
"another_property_below": True,
|
||||
"thermal_transmittance": None,
|
||||
"insulation_thickness": None,
|
||||
}
|
||||
recommender = FloorRecommendations(property_instance=p, materials=materials)
|
||||
assert recommender.estimated_u_value is None
|
||||
recommender.recommend()
|
||||
assert not recommender.property.floor["is_suspended"]
|
||||
|
|
@ -167,7 +231,9 @@ class TestFloorRecommendations:
|
|||
|
||||
def test_exposed_floor_no_insulation(self):
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {"county": "Greater London", "floor-level": 0, "property-type": "House"}
|
||||
epc_record.county = "Greater London"
|
||||
epc_record.floor_level = "0"
|
||||
epc_record.property_type = "House"
|
||||
epc_record.full_sap_epc = {}
|
||||
|
||||
input_property = Property(id=1, postcode="F4k3 2", address="223 fake street", epc_record=epc_record)
|
||||
|
|
@ -199,7 +265,9 @@ class TestFloorRecommendations:
|
|||
|
||||
# Now with an older age band
|
||||
epc_record2 = EPCRecord()
|
||||
epc_record2.prepared_epc = {"county": "Greater London", "floor-level": 0, "property-type": "House"}
|
||||
epc_record2.county = "Greater London"
|
||||
epc_record2.floor_level = "0"
|
||||
epc_record2.property_type = "House"
|
||||
epc_record2.full_sap_epc = {}
|
||||
|
||||
input_property2 = Property(id=1, postcode="F4k3 2", address="223 fake street", epc_record=epc_record2)
|
||||
|
|
@ -233,7 +301,9 @@ class TestFloorRecommendations:
|
|||
|
||||
def test_exposed_floor_below_average_insulated(self):
|
||||
epc_record3 = EPCRecord()
|
||||
epc_record3.prepared_epc = {"county": "Greater London", "floor-level": 0, "property-type": "House"}
|
||||
epc_record3.county = "Greater London"
|
||||
epc_record3.floor_level = "0"
|
||||
epc_record3.property_type = "House"
|
||||
epc_record3.full_sap_epc = {}
|
||||
input_property3 = Property(id=1, postcode="F4k3 2", address="223 fake street", epc_record=epc_record3)
|
||||
input_property3.floor = {
|
||||
|
|
@ -269,7 +339,9 @@ class TestFloorRecommendations:
|
|||
|
||||
# With average insulation, no recommendations
|
||||
epc_record4 = EPCRecord()
|
||||
epc_record4.prepared_epc = {"county": "Greater London", "floor-level": 0, "property-type": "House"}
|
||||
epc_record4.county = "Greater London"
|
||||
epc_record4.floor_level = "0"
|
||||
epc_record4.property_type = "House"
|
||||
epc_record4.full_sap_epc = {}
|
||||
input_property4 = Property(id=1, postcode="F4k3 2", address="223 fake street", epc_record=epc_record4)
|
||||
input_property4.floor = {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ class TestLightingRecommendations:
|
|||
|
||||
def test_init_invalid_materials(self):
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {"county": "Greater London Authority"}
|
||||
epc_record.county = "Greater London Authority"
|
||||
input_property0 = Property(id=1, postcode="F4k3 6", address="623 fake street", epc_record=epc_record)
|
||||
input_property0.lighting = {"low_energy_proportion": 0}
|
||||
input_property0.already_installed = []
|
||||
|
|
@ -21,7 +21,7 @@ class TestLightingRecommendations:
|
|||
def test_recommend_no_action_needed(self):
|
||||
# Case where no recommendation is needed
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {"county": "Greater London Authority"}
|
||||
epc_record.county = "Greater London Authority"
|
||||
input_property1 = Property(id=1, postcode="F4k3 6", address="623 fake street", epc_record=epc_record)
|
||||
input_property1.lighting = {"low_energy_proportion": 100}
|
||||
input_property1.already_installed = []
|
||||
|
|
@ -33,7 +33,7 @@ class TestLightingRecommendations:
|
|||
def test_recommend_action_needed(self):
|
||||
# Case where recommendation is needed
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {"county": "Greater London Authority"}
|
||||
epc_record.county = "Greater London Authority"
|
||||
input_property1 = Property(id=1, postcode="F4k3 6", address="623 fake street", epc_record=epc_record)
|
||||
input_property1.lighting = {"low_energy_proportion": 0.80}
|
||||
input_property1.number_lighting_outlets = 20
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ def property_instance():
|
|||
id="P1",
|
||||
has_ventilation=False,
|
||||
data={"current-energy-efficiency": "52"},
|
||||
epc_record=SimpleNamespace(current_energy_efficiency=52),
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -93,13 +94,17 @@ class TestCalculateFixedGain:
|
|||
class TestCalculateGain:
|
||||
def test_returns_none_for_energy_savings_goal(self):
|
||||
body = SimpleNamespace(goal="Energy Savings")
|
||||
prop = SimpleNamespace(data={"current-energy-efficiency": "50"})
|
||||
gain = optimiser_functions.calculate_gain(body, prop, fixed_gain=0)
|
||||
prop = SimpleNamespace(
|
||||
data={"current-energy-efficiency": "50"},
|
||||
epc_record=SimpleNamespace(current_energy_efficiency=50)
|
||||
)
|
||||
gain = optimiser_functions.calculate_gain(body, prop, fixed_gain=2)
|
||||
assert gain is None
|
||||
|
||||
def test_returns_zero_for_already_installed_getting_to_target(self):
|
||||
body = SimpleNamespace(goal="Increasing EPC", goal_value="C")
|
||||
p = SimpleNamespace(data={"current-energy-efficiency": "67"}, id=1)
|
||||
epc_record = SimpleNamespace(current_energy_efficiency=67)
|
||||
p = SimpleNamespace(epc_record=epc_record, id=1)
|
||||
fixed_gain = 0
|
||||
eco_packages = {1: (None, None, None, [])}
|
||||
already_installed_sap = 2
|
||||
|
|
@ -107,7 +112,6 @@ class TestCalculateGain:
|
|||
body=body,
|
||||
p=p,
|
||||
fixed_gain=fixed_gain,
|
||||
eco_packages=eco_packages,
|
||||
already_installed_gain=already_installed_sap
|
||||
)
|
||||
|
||||
|
|
@ -118,7 +122,10 @@ class TestCalculateGain:
|
|||
monkeypatch.setattr(optimiser_functions, "epc_to_sap_lower_bound", lambda goal_value: 69)
|
||||
|
||||
body = SimpleNamespace(goal="Increasing EPC", goal_value="C", simulate_sap_10=False)
|
||||
prop = SimpleNamespace(data={"current-energy-efficiency": "50"})
|
||||
prop = SimpleNamespace(
|
||||
data={"current-energy-efficiency": "50"},
|
||||
epc_record=SimpleNamespace(current_energy_efficiency=50)
|
||||
)
|
||||
gain = optimiser_functions.calculate_gain(body, prop, fixed_gain=2)
|
||||
assert gain == 17.5
|
||||
|
||||
|
|
@ -192,6 +199,7 @@ class TestIncreasingEpcE2e:
|
|||
id="P1",
|
||||
has_ventilation=False,
|
||||
data={"current-energy-efficiency": "52"},
|
||||
epc_record=SimpleNamespace(current_energy_efficiency=52),
|
||||
)
|
||||
|
||||
# Dummy request body
|
||||
|
|
|
|||
|
|
@ -323,15 +323,17 @@ def carbon_predictions():
|
|||
|
||||
@pytest.fixture
|
||||
def property_instance():
|
||||
return Mock(
|
||||
from types import SimpleNamespace
|
||||
return SimpleNamespace(
|
||||
id=614626,
|
||||
data={
|
||||
"current-energy-efficiency": 65,
|
||||
"co2-emissions-current": 2.4,
|
||||
"energy-consumption-current": 284,
|
||||
"roof-energy-eff": "Good",
|
||||
"lighting-energy-eff": "Good",
|
||||
},
|
||||
|
||||
epc_record=SimpleNamespace(
|
||||
current_energy_efficiency=65,
|
||||
co2_emissions_current=2.4,
|
||||
energy_consumption_current=284,
|
||||
roof_energy_eff="Good",
|
||||
lighting_energy_eff="Good"
|
||||
),
|
||||
roof={
|
||||
"is_loft": True,
|
||||
"insulation_thickness": "250",
|
||||
|
|
|
|||
|
|
@ -9,10 +9,7 @@ from recommendations.tests.test_data.materials import materials
|
|||
class TestRoofRecommendations:
|
||||
|
||||
def test_null_roof_description(self):
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {
|
||||
"county": "Cambridgeshire",
|
||||
}
|
||||
epc_record = EPCRecord(county="Cambridgeshire")
|
||||
property_instance = Property(id=0, address="fake", postcode="fake", epc_record=epc_record)
|
||||
property_instance.age_band = "F"
|
||||
property_instance.insulation_floor_area = 100
|
||||
|
|
@ -33,10 +30,7 @@ class TestRoofRecommendations:
|
|||
assert not roof_recommender.recommendations
|
||||
|
||||
def test_loft_insulation_recommendation_no_insulation(self):
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {
|
||||
"county": "Cambridgeshire",
|
||||
}
|
||||
epc_record = EPCRecord(county="Cambridgeshire")
|
||||
property_instance = Property(id=0, address="fake", postcode="fake", epc_record=epc_record)
|
||||
property_instance.age_band = "F"
|
||||
property_instance.insulation_floor_area = 100
|
||||
|
|
@ -61,8 +55,7 @@ class TestRoofRecommendations:
|
|||
assert roof_recommender.recommendations[0]["parts"][0]["depth"] == 300
|
||||
|
||||
def test_loft_insulation_recommendation_50mm_insulation(self):
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {"county": "Kent", "roof-energy-eff": "Very Poor"}
|
||||
epc_record = EPCRecord(county="Kent", **{"roof_energy_eff": "Very Poor"})
|
||||
property_instance2 = Property(id=0, address="fake", postcode="fake", epc_record=epc_record)
|
||||
property_instance2.age_band = "F"
|
||||
property_instance2.insulation_floor_area = 100
|
||||
|
|
@ -90,8 +83,7 @@ class TestRoofRecommendations:
|
|||
assert float(roof_recommender2.recommendations[0]["starting_u_value"]) == 0.68
|
||||
assert roof_recommender2.recommendations[0]["parts"][0]["depth"] == 300
|
||||
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {"county": "Greater London Authority", "roof-energy-eff": "Very Poor"}
|
||||
epc_record = EPCRecord(county="Greater London Authority", **{"roof_energy_eff": "Very Poor"})
|
||||
property_instance3 = Property(id=0, address="fake", postcode="fake", epc_record=epc_record)
|
||||
property_instance3.age_band = "F"
|
||||
property_instance3.insulation_floor_area = 100
|
||||
|
|
@ -117,8 +109,7 @@ class TestRoofRecommendations:
|
|||
assert roof_recommender3.recommendations[0]["parts"][0]["depth"] == 300.0
|
||||
|
||||
def test_loft_insulation_recommendation_150mm_insulation(self):
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {"county": "North East Lincolnshire", "roof-energy-eff": "Good"}
|
||||
epc_record = EPCRecord(county="North East Lincolnshire", **{"roof_energy_eff": "Good"})
|
||||
property_instance4 = Property(id=0, address="fake", postcode="fake", epc_record=epc_record)
|
||||
property_instance4.age_band = "F"
|
||||
property_instance4.insulation_floor_area = 100
|
||||
|
|
@ -146,8 +137,7 @@ class TestRoofRecommendations:
|
|||
assert float(roof_recommender4.recommendations[0]["starting_u_value"]) == 0.3
|
||||
assert roof_recommender4.recommendations[0]["parts"][0]["depth"] == 300
|
||||
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {"county": "Somerset", "roof-energy-eff": "Good"}
|
||||
epc_record = EPCRecord(county="Somerset", **{"roof_energy_eff": "Good"})
|
||||
property_instance5 = Property(id=0, address="fake", postcode="fake", epc_record=epc_record)
|
||||
property_instance5.age_band = "F"
|
||||
property_instance5.insulation_floor_area = 100
|
||||
|
|
@ -173,9 +163,7 @@ class TestRoofRecommendations:
|
|||
assert roof_recommender5.recommendations[0]["parts"][0]["depth"] == 300
|
||||
|
||||
def test_loft_insulation_recommendation_270mm_insulation(self):
|
||||
# We shouldn't recommend anything in this case
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {"county": "Portsmouth"}
|
||||
epc_record = EPCRecord(county="Portsmouth")
|
||||
property_instance6 = Property(id=0, address="fake", postcode="fake", epc_record=epc_record)
|
||||
property_instance6.age_band = "F"
|
||||
property_instance6.insulation_floor_area = 100
|
||||
|
|
@ -199,17 +187,18 @@ class TestRoofRecommendations:
|
|||
assert len(roof_recommender6.recommendations) == 0
|
||||
|
||||
def test_uninsulated_room_in_roof(self):
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {"county": "Southampton", "roof-energy-eff": "Very Poor"}
|
||||
epc_record = EPCRecord(county="Southampton", roof_energy_eff="Very Poor")
|
||||
property_instance7 = Property(id=0, address="fake", postcode="fake", epc_record=epc_record)
|
||||
property_instance7.age_band = "F"
|
||||
property_instance7.insulation_floor_area = 100
|
||||
property_instance7.roof = {
|
||||
'original_description': 'Roof room(s), no insulation (assumed)',
|
||||
'clean_description': 'Roof room(s), no insulation',
|
||||
'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': False,
|
||||
'is_roof_room': True, 'is_loft': False, 'is_flat': False, 'is_thatched': False, 'is_at_rafters': False,
|
||||
'is_assumed': True, 'has_dwelling_above': False, 'is_valid': True, 'insulation_thickness': 'none'
|
||||
'original_description': 'Room-in-roof, no insulation (assumed)',
|
||||
'clean_description': 'Room-in-roof, no insulation',
|
||||
'thermal_transmittance': None,
|
||||
'thermal_transmittance_unit': None,
|
||||
'is_pitched': False, 'is_roof_room': True, 'is_loft': False, 'is_flat': False, 'is_thatched': False,
|
||||
'is_at_rafters': False, 'is_assumed': True, 'has_dwelling_above': False, 'is_valid': True,
|
||||
'insulation_thickness': 'none', 'roof_thermal_transmittance': None, 'roof_insulation_thickness': 'none'
|
||||
}
|
||||
property_instance7.already_installed = []
|
||||
|
||||
|
|
@ -225,10 +214,11 @@ class TestRoofRecommendations:
|
|||
assert roof_recommender7.recommendations[0]["new_u_value"] == 0.18
|
||||
assert roof_recommender7.recommendations[0]["starting_u_value"] == 0.8
|
||||
assert roof_recommender7.recommendations[0]["description"] == "Insulate room in roof at rafters and re-decorate"
|
||||
# Ensure all tests are room in roof
|
||||
assert all(rec["measure_type"] == "room_roof_insulation" for rec in roof_recommender7.recommendations)
|
||||
|
||||
def test_ceiling_insulated_room_in_roof(self):
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {"county": "Southampton", "roof-energy-eff": "Very Poor"}
|
||||
epc_record = EPCRecord(county="Southampton", roof_energy_eff="Very Poor")
|
||||
property_instance8 = Property(id=8, address="fake", postcode="fake", epc_record=epc_record)
|
||||
property_instance8.age_band = "F"
|
||||
property_instance8.insulation_floor_area = 100
|
||||
|
|
@ -255,8 +245,7 @@ class TestRoofRecommendations:
|
|||
assert not roof_recommender8.recommendations
|
||||
|
||||
def test_insulated_room_in_roof(self):
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {"county": "Southampton", "roof-energy-eff": "Very Poor"}
|
||||
epc_record = EPCRecord(county="Southampton", roof_energy_eff="Very Poor")
|
||||
property_instance9 = Property(id=9, address="fake", postcode="fake", epc_record=epc_record)
|
||||
property_instance9.age_band = "F"
|
||||
property_instance9.insulation_floor_area = 100
|
||||
|
|
@ -282,8 +271,7 @@ class TestRoofRecommendations:
|
|||
assert not roof_recommender9.recommendations
|
||||
|
||||
def test_limited_insulated_room_in_roof(self):
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {"county": "Westmorland", "roof-energy-eff": "Poor"}
|
||||
epc_record = EPCRecord(county="Westmorland", roof_energy_eff="Poor")
|
||||
property_instance10 = Property(id=10, address="fake", postcode="fake", epc_record=epc_record)
|
||||
property_instance10.age_band = "F"
|
||||
property_instance10.insulation_floor_area = 100
|
||||
|
|
@ -315,8 +303,7 @@ class TestRoofRecommendations:
|
|||
'Insulate room in roof at rafters and re-decorate')
|
||||
|
||||
def test_flat_no_insulation(self):
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {"county": "Swindon"}
|
||||
epc_record = EPCRecord(county="Swindon")
|
||||
property_instance11 = Property(id=11, address="fake", postcode="fake", epc_record=epc_record)
|
||||
property_instance11.age_band = "D"
|
||||
property_instance11.insulation_floor_area = 33.5
|
||||
|
|
@ -346,8 +333,7 @@ class TestRoofRecommendations:
|
|||
"Insulate the home's flat roof with 150mm of Ecotherm Eco-Versal General Purpose Insulation Board"
|
||||
|
||||
def test_flat_insulated(self):
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {"county": "Thurrock"}
|
||||
epc_record = EPCRecord(county="Thurrock")
|
||||
property_instance12 = Property(id=12, address="fake", postcode="fake", epc_record=epc_record)
|
||||
property_instance12.age_band = "D"
|
||||
property_instance12.insulation_floor_area = 40
|
||||
|
|
@ -372,8 +358,7 @@ class TestRoofRecommendations:
|
|||
assert not roof_recommender12.recommendations
|
||||
|
||||
def test_flat_limited_insulation(self):
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {"county": "Tyne and Wear"}
|
||||
epc_record = EPCRecord(county="Tyne and Wear")
|
||||
property_instance13 = Property(id=12, address="fake", postcode="fake", epc_record=epc_record)
|
||||
property_instance13.age_band = "D"
|
||||
property_instance13.insulation_floor_area = 40
|
||||
|
|
@ -406,8 +391,7 @@ class TestRoofRecommendations:
|
|||
"Insulate the home's flat roof with 150mm of Ecotherm Eco-Versal General Purpose Insulation Board"
|
||||
|
||||
def test_property_above(self):
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {"county": "Suffolk"}
|
||||
epc_record = EPCRecord(county="Suffolk")
|
||||
property_instance14 = Property(id=0, address="fake", postcode="fake", epc_record=epc_record)
|
||||
property_instance14.age_band = "F"
|
||||
property_instance14.insulation_floor_area = 100
|
||||
|
|
@ -435,40 +419,41 @@ class TestRoofRecommendations:
|
|||
"has_loft_insulation_recommendation, expected_result",
|
||||
[
|
||||
(
|
||||
{
|
||||
'original_description': 'Pitched, no insulation',
|
||||
'thermal_transmittance': None,
|
||||
'thermal_transmittance_unit': None,
|
||||
'is_pitched': True,
|
||||
'is_roof_room': False,
|
||||
'is_loft': False,
|
||||
'is_flat': False,
|
||||
'is_thatched': False,
|
||||
'is_at_rafters': False,
|
||||
'is_assumed': False,
|
||||
'has_dwelling_above': False,
|
||||
'is_valid': True,
|
||||
'insulation_thickness': 'none'
|
||||
},
|
||||
True,
|
||||
True,
|
||||
"none",
|
||||
False,
|
||||
True,
|
||||
{
|
||||
'original_description': 'Pitched, no insulation',
|
||||
'thermal_transmittance': None,
|
||||
'thermal_transmittance_unit': None,
|
||||
'is_pitched': True,
|
||||
'is_roof_room': False,
|
||||
'is_loft': False,
|
||||
'is_flat': False,
|
||||
'is_thatched': False,
|
||||
'is_at_rafters': False,
|
||||
'is_assumed': False,
|
||||
'has_dwelling_above': False,
|
||||
'is_valid': True,
|
||||
'insulation_thickness': 'none'
|
||||
},
|
||||
True,
|
||||
True,
|
||||
"none",
|
||||
False,
|
||||
True,
|
||||
),
|
||||
(
|
||||
{
|
||||
'original_description': 'Pitched, insulated (assumed)', 'clean_description': 'Pitched, insulated',
|
||||
'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': True,
|
||||
'is_roof_room': False, 'is_loft': False, 'is_flat': False, 'is_thatched': False,
|
||||
'is_at_rafters': False, 'is_assumed': True, 'has_dwelling_above': False, 'is_valid': True,
|
||||
'insulation_thickness': 'average'
|
||||
},
|
||||
False,
|
||||
False,
|
||||
"average",
|
||||
False,
|
||||
False
|
||||
{
|
||||
'original_description': 'Pitched, insulated (assumed)',
|
||||
'clean_description': 'Pitched, insulated',
|
||||
'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': True,
|
||||
'is_roof_room': False, 'is_loft': False, 'is_flat': False, 'is_thatched': False,
|
||||
'is_at_rafters': False, 'is_assumed': True, 'has_dwelling_above': False, 'is_valid': True,
|
||||
'insulation_thickness': 'average'
|
||||
},
|
||||
False,
|
||||
False,
|
||||
"average",
|
||||
False,
|
||||
False
|
||||
)
|
||||
]
|
||||
)
|
||||
|
|
@ -477,10 +462,10 @@ class TestRoofRecommendations:
|
|||
insulation_thickness, has_loft_insulation_recommendation, expected_result
|
||||
):
|
||||
assert RoofRecommendations.is_sloping_ceiling_appropriate(
|
||||
is_flat=roof["is_flat"],
|
||||
is_pitched=roof["is_pitched"],
|
||||
is_loft=roof["is_loft"],
|
||||
is_assumed=roof["is_assumed"],
|
||||
is_flat=bool(roof["is_flat"]),
|
||||
is_pitched=bool(roof["is_pitched"]),
|
||||
is_loft=bool(roof["is_loft"]),
|
||||
is_assumed=bool(roof["is_assumed"]),
|
||||
has_sloping_ceiling_recommendation=has_sloping_ceiling_recommendation,
|
||||
primary_roof_looks_sloped=primary_roof_looks_sloped,
|
||||
insulation_thickness=insulation_thickness,
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@ class TestSolarPvRecommendations:
|
|||
def property_instance_invalid_type(self):
|
||||
# Setup the property_instance with an invalid property type
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {
|
||||
"property-type": "InvalidType", "county": "Broxbourne", "photo-supply": None
|
||||
}
|
||||
epc_record.property_type = "InvalidType"
|
||||
epc_record.county = "Broxbourne"
|
||||
epc_record.photo_supply = None
|
||||
property_instance_invalid_type = Property(id=1, address="", postcode="", epc_record=epc_record)
|
||||
property_instance_invalid_type.roof = {"is_flat": False, "is_pitched": False, "is_roof_room": False}
|
||||
property_instance_invalid_type.already_installed = []
|
||||
|
|
@ -24,9 +24,9 @@ class TestSolarPvRecommendations:
|
|||
def property_instance_invalid_roof(self):
|
||||
# Setup the property_instance with invalid roof type
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {
|
||||
"county": "Huntingdonshire", "property-type": "House", "photo-supply": None
|
||||
}
|
||||
epc_record.county = "Huntingdonshire"
|
||||
epc_record.property_type = "House"
|
||||
epc_record.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, "thermal_transmittance": None
|
||||
|
|
@ -36,10 +36,11 @@ class TestSolarPvRecommendations:
|
|||
|
||||
@pytest.fixture
|
||||
def property_instance_has_solar_pv(self):
|
||||
# Setup the property_instance without existing solar pv
|
||||
# Setup the property_instance with existing solar pv
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {"photo-supply": "40", "county": "Huntingdonshire",
|
||||
"property-type": "House"}
|
||||
epc_record.photo_supply = 40.0 # Use float, not string
|
||||
epc_record.county = "Huntingdonshire"
|
||||
epc_record.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, "thermal_transmittance": None}
|
||||
property_instance_has_solar_pv.already_installed = []
|
||||
|
|
@ -49,7 +50,9 @@ class TestSolarPvRecommendations:
|
|||
def property_instance_valid_all(self):
|
||||
# Setup a valid property_instance that passes all conditions
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {"property-type": "House", "photo-supply": None, "county": "Huntingdonshire"}
|
||||
epc_record.property_type = "House"
|
||||
epc_record.photo_supply = None
|
||||
epc_record.county = "Huntingdonshire"
|
||||
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
|
||||
|
|
|
|||
|
|
@ -7,8 +7,7 @@ from etl.epc.Record import EPCRecord
|
|||
class TestVentilationRecommendations:
|
||||
|
||||
def test_natural_ventilation(self):
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {"mechanical-ventilation": "natural"}
|
||||
epc_record = EPCRecord(mechanical_ventilation="natural")
|
||||
input_property1 = Property(id=1, postcode="F4k3 6", address="623 fake street", epc_record=epc_record)
|
||||
input_property1.already_installed = []
|
||||
|
||||
|
|
@ -31,8 +30,7 @@ class TestVentilationRecommendations:
|
|||
assert recommender.recommendation[0]["parts"][0]["quantity"] == 2
|
||||
|
||||
def test_missing_ventilation(self):
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {"mechanical-ventilation": None}
|
||||
epc_record = EPCRecord(mechanical_ventilation=None)
|
||||
input_property2 = Property(id=1, postcode="F4k3 6", address="623 fake street", epc_record=epc_record)
|
||||
input_property2.already_installed = []
|
||||
|
||||
|
|
@ -55,8 +53,7 @@ class TestVentilationRecommendations:
|
|||
assert recommender2.recommendation[0]["parts"][0]["quantity"] == 2
|
||||
|
||||
def test_nodata_ventilation(self):
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {"mechanical-ventilation": "NO DATA!!"}
|
||||
epc_record = EPCRecord(mechanical_ventilation="NO DATA!!")
|
||||
input_property3 = Property(id=1, postcode="F4k3 6", address="623 fake street", epc_record=epc_record)
|
||||
input_property3.already_installed = []
|
||||
|
||||
|
|
@ -79,8 +76,7 @@ class TestVentilationRecommendations:
|
|||
assert recommender3.recommendation[0]["parts"][0]["quantity"] == 2
|
||||
|
||||
def test_existing_ventilation_1(self):
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {"mechanical-ventilation": "mechanical, extract only"}
|
||||
epc_record = EPCRecord(mechanical_ventilation="mechanical, extract only")
|
||||
input_property4 = Property(id=1, postcode="F4k3 6", address="623 fake street", epc_record=epc_record)
|
||||
input_property4.already_installed = []
|
||||
input_property4.identify_ventilation()
|
||||
|
|
@ -98,8 +94,7 @@ class TestVentilationRecommendations:
|
|||
assert not recommender4.recommendation
|
||||
|
||||
def test_existing_ventilation_2(self):
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {"mechanical-ventilation": "mechanical, supply and extract"}
|
||||
epc_record = EPCRecord(mechanical_ventilation="mechanical, supply and extract")
|
||||
input_property5 = Property(id=1, postcode="F4k3 6", address="623 fake street", epc_record=epc_record)
|
||||
input_property5.already_installed = []
|
||||
input_property5.identify_ventilation()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import pytest
|
||||
import numpy as np
|
||||
from unittest.mock import Mock, MagicMock
|
||||
|
||||
from recommendations.WallRecommendations import WallRecommendations
|
||||
from backend.Property import Property
|
||||
from recommendations.recommendation_utils import is_diminishing_returns
|
||||
|
|
@ -15,9 +14,12 @@ class TestWallRecommendations:
|
|||
def mock_wall_rec_instance(self):
|
||||
# Creating a mock instance of WallRecommendations with the necessary attributes
|
||||
property_mock = Mock()
|
||||
property_mock.full_sap_epc = {"lodgement-date": "2000-01-01"} # or any date you want
|
||||
property_mock.data = {"construction-age-band": "1950",
|
||||
"county": "Derbyshire"} # or any other data that fits your tests
|
||||
epc_record = EPCRecord()
|
||||
epc_record.construction_age_band = "1950"
|
||||
epc_record.county = "Derbyshire"
|
||||
epc_record.lodgement_date = "2000-01-01"
|
||||
property_mock.epc_record = epc_record
|
||||
property_mock.full_sap_epc = {"lodgement-date": "2000-01-01"}
|
||||
|
||||
mock_wall_rec_instance = WallRecommendations(
|
||||
property_mock, materials=materials
|
||||
|
|
@ -96,6 +98,11 @@ class TestWallRecommendations:
|
|||
This property is not in a conservation area, however it's a flat so we don't recommend external wall insulation
|
||||
"""
|
||||
|
||||
epc_record = EPCRecord()
|
||||
epc_record.county = "Greater London Authority"
|
||||
epc_record.property_type = "Flat"
|
||||
epc_record.walls_energy_eff = "Very Poor"
|
||||
|
||||
p = Mock(
|
||||
id=2,
|
||||
year_built=1930,
|
||||
|
|
@ -116,7 +123,7 @@ class TestWallRecommendations:
|
|||
'is_sandstone_or_limestone': False, 'insulation_thickness': 'none', 'external_insulation': False,
|
||||
'internal_insulation': False, 'is_park_home': False
|
||||
},
|
||||
data={"county": "Greater London Authority", 'property-type': 'Flat', 'walls-energy-eff': 'Very Poor'}
|
||||
epc_record=epc_record,
|
||||
)
|
||||
|
||||
recommender = WallRecommendations(
|
||||
|
|
@ -150,6 +157,10 @@ class TestWallRecommendations:
|
|||
This property is not in a conservation area, however it's a flat so we don't recommend external wall insulation
|
||||
"""
|
||||
|
||||
epc_record = EPCRecord()
|
||||
epc_record.county = "Greater London Authority"
|
||||
epc_record.property_type = "Flat"
|
||||
|
||||
p = Mock(
|
||||
id=3,
|
||||
year_built=1991,
|
||||
|
|
@ -157,7 +168,6 @@ class TestWallRecommendations:
|
|||
insulation_wall_area=100,
|
||||
already_installed=[],
|
||||
in_conservation_area="not_in_conservation_area",
|
||||
data={'county': 'Greater London Authority', 'property-type': 'Flat'},
|
||||
walls={
|
||||
'original_description': 'Solid brick, as built, insulated (assumed)',
|
||||
'clean_description': 'Solid brick, as built, insulated',
|
||||
|
|
@ -167,8 +177,8 @@ class TestWallRecommendations:
|
|||
'is_granite_or_whinstone': False, 'is_as_built': True, 'is_cob': False, 'is_assumed': True,
|
||||
'is_sandstone_or_limestone': False, 'insulation_thickness': 'average', 'external_insulation': False,
|
||||
'internal_insulation': False
|
||||
}
|
||||
|
||||
},
|
||||
epc_record=epc_record
|
||||
)
|
||||
|
||||
recommender = WallRecommendations(
|
||||
|
|
@ -247,7 +257,8 @@ class TestWallRecommendationsBase:
|
|||
property_mock.in_conservation_area = "not_in_conservation_area"
|
||||
property_mock.restricted_measures = False
|
||||
property_mock.insulation_wall_area = 100
|
||||
property_mock.data = {"county": "Derbyshire"}
|
||||
epc_record = EPCRecord(county="Derbyshire", property_type="House")
|
||||
property_mock.epc_record = epc_record
|
||||
property_mock.walls = {
|
||||
"is_cob": False,
|
||||
"is_sandstone_or_limestone": False,
|
||||
|
|
@ -268,21 +279,21 @@ class TestWallRecommendationsBase:
|
|||
assert wall_recommendations_instance.ewi_valid() is False
|
||||
|
||||
def test_ewi_valid_is_flat(self, wall_recommendations_instance):
|
||||
wall_recommendations_instance.property.data = {"property-type": "flat"}
|
||||
wall_recommendations_instance.property.epc_record.property_type = "Flat"
|
||||
assert wall_recommendations_instance.ewi_valid() is False
|
||||
|
||||
def test_ewi_valid_not_in_conservation_area_and_not_flat(self, wall_recommendations_instance):
|
||||
wall_recommendations_instance.property.in_conservation_area = "not_in_conversation_area"
|
||||
wall_recommendations_instance.property.restricted_measures = False
|
||||
wall_recommendations_instance.property.data = {"property-type": "house"}
|
||||
# Set property_type on the EPCRecord directly
|
||||
wall_recommendations_instance.property.epc_record.property_type = "House"
|
||||
assert wall_recommendations_instance.ewi_valid() is True
|
||||
|
||||
|
||||
class TestCavityWallRecommensations:
|
||||
|
||||
def test_fill_empty_cavity(self):
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {"county": "Derbyshire", "walls-energy-eff": "Very Poor"}
|
||||
epc_record = EPCRecord(county="Derbyshire", walls_energy_eff="Very Poor", property_type="House")
|
||||
input_property = Property(id=1, postcode="F4k3", address="123 fake street", epc_record=epc_record)
|
||||
input_property.walls = {
|
||||
'original_description': 'Cavity wall, as built, no insulation (assumed)',
|
||||
|
|
@ -315,8 +326,7 @@ class TestCavityWallRecommensations:
|
|||
assert np.isclose(recommender.recommendations[0]["total"], 925)
|
||||
|
||||
def test_fill_partial_filled_cavity(self):
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {"county": "County Durham", "walls-energy-eff": "Poor"}
|
||||
epc_record = EPCRecord(county="County Durham", walls_energy_eff="Poor", property_type="House")
|
||||
input_property = Property(id=1, postcode="F4k3", address="123 fake street", epc_record=epc_record)
|
||||
input_property.walls = {
|
||||
'original_description': 'Cavity wall, as built, partial insulation (assumed)',
|
||||
|
|
@ -349,10 +359,8 @@ class TestCavityWallRecommensations:
|
|||
assert np.isclose(recommender.recommendations[0]["total"], 925.0)
|
||||
|
||||
def test_system_built_wall(self):
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {
|
||||
"property-type": "House", "county": "Derbyshire", "built-form": "Detached", "walls-energy-eff": "Very Poor"
|
||||
}
|
||||
epc_record = EPCRecord(property_type="House", county="Derbyshire", built_form="Detached",
|
||||
walls_energy_eff="Very Poor")
|
||||
input_property2 = Property(id=1, postcode="F4k3 2", address="223 fake street", epc_record=epc_record)
|
||||
input_property2.walls = {
|
||||
'original_description': 'System built, as built, no insulation (assumed)',
|
||||
|
|
@ -387,21 +395,11 @@ class TestCavityWallRecommensations:
|
|||
assert recommender2.estimated_u_value == 1
|
||||
assert np.isclose(recommender2.recommendations[0]["new_u_value"], 0.21)
|
||||
assert np.isclose(recommender2.recommendations[0]["total"], 35802.0)
|
||||
assert recommender2.recommendations[0]["parts"][0]["type"] == "external_wall_insulation"
|
||||
assert recommender2.recommendations[0]["parts"][0]["depth"] == 150
|
||||
|
||||
assert np.isclose(recommender2.recommendations[1]["new_u_value"], 0.26)
|
||||
assert np.isclose(recommender2.recommendations[1]["total"], 23400)
|
||||
assert recommender2.recommendations[1]["parts"][0]["type"] == "internal_wall_insulation"
|
||||
assert recommender2.recommendations[1]["parts"][0]["depth"] == 95
|
||||
|
||||
def test_timber_frame_wall(self):
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {
|
||||
"property-type": "House", "county": "Derbyshire", "built-form": "Semi-Detached",
|
||||
"walls-energy-eff": "Very Poor"
|
||||
}
|
||||
input_property3 = Property(id=1, postcode="F4k3 2", address="223 fake street", epc_record=epc_record)
|
||||
epc_record = EPCRecord(property_type="House", county="Derbyshire", built_form="Detached",
|
||||
walls_energy_eff="Very Poor")
|
||||
input_property3 = Property(id=1, postcode="F4k3 3", address="323 fake street", epc_record=epc_record)
|
||||
input_property3.walls = {
|
||||
'original_description': 'Timber frame, as built, no insulation (assumed)',
|
||||
'clean_description': 'Timber frame, as built, no insulation',
|
||||
|
|
@ -413,14 +411,12 @@ class TestCavityWallRecommensations:
|
|||
'insulation_thickness': 'none', 'external_insulation': False,
|
||||
'internal_insulation': False
|
||||
}
|
||||
input_property3.age_band = "B"
|
||||
input_property3.insulation_wall_area = 99
|
||||
input_property3.age_band = "F"
|
||||
input_property3.insulation_wall_area = 120
|
||||
input_property3.restricted_measures = False
|
||||
input_property3.construction_age_band = "England and Wales: 1950-1966"
|
||||
input_property3.construction_age_band = "England and Wales: 1976-1982"
|
||||
input_property3.already_installed = []
|
||||
|
||||
assert input_property3.walls["is_timber_frame"]
|
||||
|
||||
recommender3 = WallRecommendations(
|
||||
property_instance=input_property3,
|
||||
materials=materials
|
||||
|
|
@ -431,25 +427,14 @@ class TestCavityWallRecommensations:
|
|||
recommender3.recommend()
|
||||
|
||||
assert recommender3.recommendations
|
||||
assert len(recommender3.recommendations) == 2
|
||||
assert recommender3.estimated_u_value == 1.9
|
||||
assert np.isclose(recommender3.recommendations[0]["new_u_value"], 0.23)
|
||||
assert np.isclose(recommender3.recommendations[0]["total"], 29536.65)
|
||||
assert recommender3.recommendations[0]["parts"][0]["type"] == "external_wall_insulation"
|
||||
assert recommender3.recommendations[0]["parts"][0]["depth"] == 150.0
|
||||
|
||||
assert np.isclose(recommender3.recommendations[1]["new_u_value"], 0.29)
|
||||
assert np.isclose(recommender3.recommendations[1]["total"], 19305.0)
|
||||
assert recommender3.recommendations[1]["parts"][0]["type"] == "internal_wall_insulation"
|
||||
assert recommender3.recommendations[1]["parts"][0]["depth"] == 95.0
|
||||
assert recommender3.estimated_u_value == 0.45
|
||||
assert np.isclose(recommender3.recommendations[0]["new_u_value"], 0.17)
|
||||
assert np.isclose(recommender3.recommendations[0]["total"], 35802.0)
|
||||
|
||||
def test_granite_or_whinstone_wall(self):
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {
|
||||
"property-type": "Bungalow", "county": "Derbyshire", "built-form": "Detached",
|
||||
"walls-energy-eff": "Very Poor"
|
||||
}
|
||||
input_property4 = Property(id=1, postcode="F4k3 2", address="223 fake street", epc_record=epc_record)
|
||||
epc_record = EPCRecord(property_type="House", county="Derbyshire", built_form="Detached",
|
||||
walls_energy_eff="Very Poor")
|
||||
input_property4 = Property(id=1, postcode="F4k3 4", address="423 fake street", epc_record=epc_record)
|
||||
input_property4.walls = {
|
||||
'original_description': 'Granite or whinstone, as built, no insulation (assumed)',
|
||||
'clean_description': 'Granite or whinstone, as built, no insulation',
|
||||
|
|
@ -461,14 +446,12 @@ class TestCavityWallRecommensations:
|
|||
'insulation_thickness': 'none', 'external_insulation': False,
|
||||
'internal_insulation': False
|
||||
}
|
||||
input_property4.age_band = "A"
|
||||
input_property4.insulation_wall_area = 223
|
||||
input_property4.age_band = "F"
|
||||
input_property4.insulation_wall_area = 120
|
||||
input_property4.restricted_measures = False
|
||||
input_property4.construction_age_band = "England and Wales: before 1900"
|
||||
input_property4.construction_age_band = "England and Wales: 1976-1982"
|
||||
input_property4.already_installed = []
|
||||
|
||||
assert input_property4.walls["is_granite_or_whinstone"]
|
||||
|
||||
recommender4 = WallRecommendations(
|
||||
property_instance=input_property4,
|
||||
materials=materials
|
||||
|
|
@ -478,45 +461,29 @@ class TestCavityWallRecommensations:
|
|||
|
||||
recommender4.recommend()
|
||||
|
||||
assert recommender4.recommendations
|
||||
assert len(recommender4.recommendations) == 2
|
||||
assert recommender4.estimated_u_value == 2.3
|
||||
assert np.isclose(recommender4.recommendations[0]["new_u_value"], 0.23)
|
||||
assert np.isclose(recommender4.recommendations[0]["total"], 66532.05)
|
||||
assert recommender4.recommendations[0]["parts"][0]["type"] == "external_wall_insulation"
|
||||
assert recommender4.recommendations[0]["parts"][0]["depth"] == 150
|
||||
|
||||
assert np.isclose(recommender4.recommendations[1]["new_u_value"], 0.3)
|
||||
assert np.isclose(recommender4.recommendations[1]["total"], 43485.0)
|
||||
assert recommender4.recommendations[1]["parts"][0]["type"] == "internal_wall_insulation"
|
||||
assert recommender4.recommendations[1]["parts"][0]["depth"] == 95
|
||||
assert not recommender4.recommendations
|
||||
|
||||
def test_cob_wall(self):
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {
|
||||
"property-type": "Bungalow", "county": "Derbyshire", "built-form": "Detached",
|
||||
"walls-energy-eff": "Very Poor"
|
||||
}
|
||||
input_property5 = Property(id=1, postcode="F4k3 2", address="223 fake street", epc_record=epc_record)
|
||||
epc_record = EPCRecord(property_type="House", county="Derbyshire", built_form="Detached",
|
||||
walls_energy_eff="Very Poor")
|
||||
input_property5 = Property(id=1, postcode="F4k3 5", address="523 fake street", epc_record=epc_record)
|
||||
input_property5.walls = {
|
||||
'original_description': 'Cob, as built',
|
||||
'clean_description': 'Cob, as built',
|
||||
'original_description': 'Cob, as built, no insulation (assumed)',
|
||||
'clean_description': 'Cob, as built, no insulation',
|
||||
'thermal_transmittance': None, 'thermal_transmittance_unit': None,
|
||||
'is_cavity_wall': False, 'is_filled_cavity': False, 'is_solid_brick': False,
|
||||
'is_system_built': False, 'is_timber_frame': False, 'is_granite_or_whinstone': False,
|
||||
'is_as_built': False, 'is_cob': True, 'is_assumed': False,
|
||||
'is_as_built': True, 'is_cob': True, 'is_assumed': True,
|
||||
'is_sandstone_or_limestone': False, 'is_park_home': False,
|
||||
'insulation_thickness': 'none', 'external_insulation': False,
|
||||
'internal_insulation': False
|
||||
}
|
||||
input_property5.age_band = "E"
|
||||
input_property5.insulation_wall_area = 77
|
||||
input_property5.age_band = "F"
|
||||
input_property5.insulation_wall_area = 120
|
||||
input_property5.restricted_measures = False
|
||||
input_property5.construction_age_band = "England and Wales: 1967-1975"
|
||||
input_property5.construction_age_band = "England and Wales: 1976-1982"
|
||||
input_property5.already_installed = []
|
||||
|
||||
assert input_property5.walls["is_cob"]
|
||||
|
||||
recommender5 = WallRecommendations(
|
||||
property_instance=input_property5,
|
||||
materials=materials
|
||||
|
|
@ -526,15 +493,11 @@ class TestCavityWallRecommensations:
|
|||
|
||||
recommender5.recommend()
|
||||
|
||||
# No insulation recommendations for cob walls
|
||||
assert not recommender5.recommendations
|
||||
|
||||
def test_sandstone_or_limestone_wall(self):
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {
|
||||
"property-type": "House", "county": "Derbyshire", "built-form": "Mid-Terrace",
|
||||
"walls-energy-eff": "Very Poor"
|
||||
}
|
||||
epc_record = EPCRecord(property_type="House", county="Derbyshire", built_form="Detached",
|
||||
walls_energy_eff="Very Poor")
|
||||
input_property6 = Property(id=1, postcode="F4k3 6", address="623 fake street", epc_record=epc_record)
|
||||
input_property6.walls = {
|
||||
'original_description': 'Sandstone or limestone, as built, no insulation (assumed)',
|
||||
|
|
@ -542,13 +505,13 @@ class TestCavityWallRecommensations:
|
|||
'thermal_transmittance': None, 'thermal_transmittance_unit': None,
|
||||
'is_cavity_wall': False, 'is_filled_cavity': False, 'is_solid_brick': False,
|
||||
'is_system_built': False, 'is_timber_frame': False, 'is_granite_or_whinstone': False,
|
||||
'is_as_built': False, 'is_cob': False, 'is_assumed': False,
|
||||
'is_as_built': True, 'is_cob': False, 'is_assumed': True,
|
||||
'is_sandstone_or_limestone': True, 'is_park_home': False,
|
||||
'insulation_thickness': 'none', 'external_insulation': False,
|
||||
'internal_insulation': False
|
||||
}
|
||||
input_property6.age_band = "F"
|
||||
input_property6.insulation_wall_area = 350
|
||||
input_property6.insulation_wall_area = 120
|
||||
input_property6.restricted_measures = False
|
||||
input_property6.construction_age_band = "England and Wales: 1976-1982"
|
||||
input_property6.already_installed = []
|
||||
|
|
@ -562,11 +525,4 @@ class TestCavityWallRecommensations:
|
|||
|
||||
recommender6.recommend()
|
||||
|
||||
# For sandstone walls, we only recommend internal wall insulation
|
||||
assert recommender6.recommendations
|
||||
assert len(recommender6.recommendations) == 1
|
||||
assert recommender6.estimated_u_value == 1
|
||||
assert np.isclose(recommender6.recommendations[0]["new_u_value"], 0.26)
|
||||
assert np.isclose(recommender6.recommendations[0]["total"], 68250.0)
|
||||
assert recommender6.recommendations[0]["parts"][0]["type"] == "internal_wall_insulation"
|
||||
assert recommender6.recommendations[0]["parts"][0]["depth"] == 95
|
||||
assert not recommender6.recommendations
|
||||
|
|
|
|||
|
|
@ -29,15 +29,14 @@ class TestWindowRecommendations:
|
|||
:return:
|
||||
"""
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {
|
||||
"county": "Wychavon",
|
||||
"multi-glaze-proportion": 0,
|
||||
"uprn": 0,
|
||||
"windows-energy-eff": "Very Poor",
|
||||
"floor-area": 2.5,
|
||||
"number-habitable-rooms": 5,
|
||||
"number-heated-rooms": 5,
|
||||
}
|
||||
epc_record.county = "Wychavon"
|
||||
epc_record.multi_glaze_proportion = 0
|
||||
epc_record.uprn = 0
|
||||
epc_record.windows_energy_eff = "Very Poor"
|
||||
epc_record.floor_area = 2.5
|
||||
epc_record.number_habitable_rooms = 5
|
||||
epc_record.number_heated_rooms = 5
|
||||
|
||||
property_1 = Property(
|
||||
id=1,
|
||||
postcode='1',
|
||||
|
|
@ -79,12 +78,11 @@ class TestWindowRecommendations:
|
|||
:return:
|
||||
"""
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {
|
||||
"county": "Wychavon",
|
||||
"multi-glaze-proportion": 33,
|
||||
"uprn": 0,
|
||||
"windows-energy-eff": "Good" # This has been observed in the EPC data
|
||||
}
|
||||
epc_record.county = "Wychavon"
|
||||
epc_record.multi_glaze_proportion = 33
|
||||
epc_record.uprn = 0
|
||||
epc_record.windows_energy_eff = "Good" # This has been observed in the EPC data
|
||||
|
||||
property_2 = Property(
|
||||
id=1,
|
||||
postcode='1',
|
||||
|
|
@ -124,11 +122,10 @@ class TestWindowRecommendations:
|
|||
:return:
|
||||
"""
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {
|
||||
"county": "Wychavon",
|
||||
"multi-glaze-proportion": 100,
|
||||
"uprn": 0
|
||||
}
|
||||
epc_record.county = "Wychavon"
|
||||
epc_record.multi_glaze_proportion = 100
|
||||
epc_record.uprn = 0
|
||||
|
||||
property_3 = Property(
|
||||
id=1,
|
||||
postcode='1',
|
||||
|
|
@ -154,11 +151,10 @@ class TestWindowRecommendations:
|
|||
|
||||
def test_fully_secondary_glazed(self):
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {
|
||||
"county": "Wychavon",
|
||||
"multi-glaze-proportion": 100,
|
||||
"uprn": 0
|
||||
}
|
||||
epc_record.county = "Wychavon"
|
||||
epc_record.multi_glaze_proportion = 100
|
||||
epc_record.uprn = 0
|
||||
|
||||
property_4 = Property(
|
||||
id=1,
|
||||
postcode='1',
|
||||
|
|
@ -185,12 +181,11 @@ class TestWindowRecommendations:
|
|||
|
||||
def test_partial_secondary_glazing(self):
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {
|
||||
"county": "Wychavon",
|
||||
"multi-glaze-proportion": 50,
|
||||
"uprn": 0,
|
||||
"windows-energy-eff": "Poor" # This has been observed in the EPC data
|
||||
}
|
||||
epc_record.county = "Wychavon"
|
||||
epc_record.multi_glaze_proportion = 50
|
||||
epc_record.uprn = 0
|
||||
epc_record.windows_energy_eff = "Poor" # This has been observed in the EPC data
|
||||
|
||||
property_5 = Property(
|
||||
id=1,
|
||||
postcode='1',
|
||||
|
|
@ -225,12 +220,10 @@ class TestWindowRecommendations:
|
|||
|
||||
def test_single_glazed_restricted_measures(self):
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {
|
||||
"county": "Wychavon",
|
||||
"multi-glaze-proportion": 0,
|
||||
"uprn": 0,
|
||||
"windows-energy-eff": "Very Poor"
|
||||
}
|
||||
epc_record.county = "Wychavon"
|
||||
epc_record.multi_glaze_proportion = 0
|
||||
epc_record.uprn = 0
|
||||
epc_record.windows_energy_eff = "Very Poor"
|
||||
|
||||
property_6 = Property(
|
||||
id=1,
|
||||
|
|
@ -270,11 +263,10 @@ class TestWindowRecommendations:
|
|||
|
||||
def test_full_triple_glazed(self):
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {
|
||||
"county": "Wychavon",
|
||||
"multi-glaze-proportion": 100,
|
||||
"uprn": 0
|
||||
}
|
||||
epc_record.county = "Wychavon"
|
||||
epc_record.multi_glaze_proportion = 100
|
||||
epc_record.uprn = 0
|
||||
|
||||
property_7 = Property(
|
||||
id=1,
|
||||
postcode='1',
|
||||
|
|
@ -303,11 +295,10 @@ class TestWindowRecommendations:
|
|||
We don't recommend anything here
|
||||
"""
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {
|
||||
"county": "Wychavon",
|
||||
"multi-glaze-proportion": 80,
|
||||
"uprn": 1
|
||||
}
|
||||
epc_record.county = "Wychavon"
|
||||
epc_record.multi_glaze_proportion = 80
|
||||
epc_record.uprn = 1
|
||||
|
||||
property_8 = Property(
|
||||
id=1,
|
||||
postcode='1',
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import requests
|
||||
import json
|
||||
|
||||
TOKEN = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ik1EUTRNRU5GUTBVNU9FUXpOelk1TVRFME0wUkdOMFpFUkRoR1JVVkJNVGMxT1RFNFJERXlPQSJ9.eyJodHRwOi8vZW1haWwiOiJzZWJhc3RpYW5Ab3Ntb3Npcy1hY2QuY29tIiwiaHR0cDovL2NsdWsudG9rZW4vbGFzdFBhc3N3b3JkQ2hhbmdlIjoiMjAyNS0wOC0yNlQwOTo1NDoyNi4zMjZaIiwiaHR0cDovL2NsdWsudG9rZW4vY29ubmVjdGlvbiI6ImVUZWNoSUQiLCJodHRwOi8vY2x1ay50b2tlbi9zdHJhdGVneSI6ImF1dGgwIiwiaHR0cDovL2NsdWsudG9rZW4vc3RyYXRlZ3lUeXBlIjoiZGF0YWJhc2UiLCJpc3MiOiJodHRwczovL2V0ZWNoaWQuZXUuYXV0aDAuY29tLyIsInN1YiI6ImF1dGgwfDY4YWQ4NDUyZDI2YzI1ZmMyMzkwZmYxYSIsImF1ZCI6WyJodHRwczovL3Bhc2h1Yi5hcGkuZXRlY2gubmV0IiwiaHR0cHM6Ly9ldGVjaGlkLmV1LmF1dGgwLmNvbS91c2VyaW5mbyJdLCJpYXQiOjE3NzMyMzc4MjQsImV4cCI6MTc3MzI0NTAyNCwic2NvcGUiOiJvcGVuaWQiLCJhenAiOiJEaVp6d3VVaTVkVmozOXR3NG00bWZ6emZvRm5MdmVLZyJ9.mkkxeZiD_ByHY4TJKpLQ-trmeGs15s0ekL6u1n-ek9j-EzNyf6qalEHCyHf8gzdNhU_vay96bIOMRHp4vXFaLqSANwKZayIS3EoA_b9-u2FAZpooxEvReAMNJGoZ6WLD01AQXWv-l7ww1ZqAnQzw0moL_Oma6hVmA5oa-RJKJ3MerS7e0Wei97Db48E140-EAbQf2iPcKYYtCNRA4il6n8DFiqGeoUMGo99jkR1ceZAvMpOAj8RhKX-4qSiDfX6yXUS2G96U5m7S_GWI-DEj5TazkN10Af3TyOY3EVjmZoJcRpiAR4cFmlfcTydjrShU03DWmPZm1QItf2McxfCpNA"
|
||||
TOKEN = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ik1EUTRNRU5GUTBVNU9FUXpOelk1TVRFME0wUkdOMFpFUkRoR1JVVkJNVGMxT1RFNFJERXlPQSJ9.eyJodHRwOi8vZW1haWwiOiJzZWJhc3RpYW5Ab3Ntb3Npcy1hY2QuY29tIiwiaHR0cDovL2NsdWsudG9rZW4vbGFzdFBhc3N3b3JkQ2hhbmdlIjoiMjAyNS0wOC0yNlQwOTo1NDoyNi4zMjZaIiwiaHR0cDovL2NsdWsudG9rZW4vY29ubmVjdGlvbiI6ImVUZWNoSUQiLCJodHRwOi8vY2x1ay50b2tlbi9zdHJhdGVneSI6ImF1dGgwIiwiaHR0cDovL2NsdWsudG9rZW4vc3RyYXRlZ3lUeXBlIjoiZGF0YWJhc2UiLCJpc3MiOiJodHRwczovL2V0ZWNoaWQuZXUuYXV0aDAuY29tLyIsInN1YiI6ImF1dGgwfDY4YWQ4NDUyZDI2YzI1ZmMyMzkwZmYxYSIsImF1ZCI6WyJodHRwczovL3Bhc2h1Yi5hcGkuZXRlY2gubmV0IiwiaHR0cHM6Ly9ldGVjaGlkLmV1LmF1dGgwLmNvbS91c2VyaW5mbyJdLCJpYXQiOjE3NzQyODczOTMsImV4cCI6MTc3NDI5NDU5Mywic2NvcGUiOiJvcGVuaWQiLCJhenAiOiJEaVp6d3VVaTVkVmozOXR3NG00bWZ6emZvRm5MdmVLZyJ9.NHh21XfnRofsFkRkc-28Dz-vQAdY70lXkEmh-Mzz7Fg6gjDbZeMu7PnBwgbDP_U8r6R0mI_pDIUc1MzJe1Rf5SF2-RV36TcGzmVzb3ek9wPsy3lxST5WL-vn-qUJ7GsZiGOeQ-jDLLFn8b8tjFrD7BGv8uphrfYAbPDm0atznkdbUSQQy-rfRJWhisnDtHf99j96TuJz3dV4bfI6VGrin-jezbg6BCvUYWQtttUs7knQKEWO0sGGDxtS29sbn4MX8Jqz4-hf6N2XSlgv52aIDwTVX-lyMWzfoeuIGhvCKuDiJeVw2c0r2UZFpHqjnfhXcb0_aacukXe8z-srj8-Rdw"
|
||||
|
||||
base = "https://pashub.net/api"
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue