Model/backend/app/plan/router.py
2023-08-01 14:45:29 +01:00

289 lines
13 KiB
Python

import datetime
from fastapi import APIRouter, Depends
from backend.app.dependencies import validate_token
from backend.app.plan.schemas import PlanTriggerRequest
from backend.app.utils import read_csv_from_s3
from backend.app.config import get_settings
from backend.Property import Property
from epc_api.client import EpcClient
from utils.logger import setup_logger
from recommendations.FloorRecommendations import FloorRecommendations
from recommendations.WallRecommendations import WallRecommendations
from utils.uvalue_estimates import classify_decile_newvalues
from model_data.EpcClean import EpcClean
# database interaction functions
from backend.app.db.functions.property_functions import create_property, create_property_targets
# TODO: This is placeholder until data is stored in DB
from backend.app.plan.uvalue_estimates_walls import uvalue_estimates_walls
from backend.app.plan.uvalue_estimates_floors import uvalue_estimates_floors
logger = setup_logger()
router = APIRouter(
prefix="/plan",
tags=["plan"],
dependencies=[Depends(validate_token)],
responses={404: {"description": "Not found"}}
)
# TODO: Load this data from db
open_uprn_data = [
{'UPRN': 6032920, 'X_COORDINATE': 535110.0, 'Y_COORDINATE': 181819.0, 'LATITUDE': 51.5191407,
'LONGITUDE': -0.0540506},
{'UPRN': 6038625, 'X_COORDINATE': 535374.0, 'Y_COORDINATE': 182784.0, 'LATITUDE': 51.5277492,
'LONGITUDE': -0.0498772},
{'UPRN': 34153991, 'X_COORDINATE': 523238.74, 'Y_COORDINATE': 178003.02, 'LATITUDE': 51.4875579,
'LONGITUDE': -0.226392},
{'UPRN': 10008299676, 'X_COORDINATE': 533285.0, 'Y_COORDINATE': 184711.0, 'LATITUDE': 51.5455629,
'LONGITUDE': -0.0792445},
{'UPRN': 10008299677, 'X_COORDINATE': 533285.0, 'Y_COORDINATE': 184711.0, 'LATITUDE': 51.5455629,
'LONGITUDE': -0.0792445},
{'UPRN': 100021039066, 'X_COORDINATE': 535506.0, 'Y_COORDINATE': 185624.0, 'LATITUDE': 51.5532385,
'LONGITUDE': -0.0468833},
{'UPRN': 100021226060, 'X_COORDINATE': 529247.0, 'Y_COORDINATE': 187959.0, 'LATITUDE': 51.5756908,
'LONGITUDE': -0.1362513},
{'UPRN': 200003489276, 'X_COORDINATE': 533210.0, 'Y_COORDINATE': 179442.0, 'LATITUDE': 51.4982309,
'LONGITUDE': -0.0823165}
]
in_conservation_area_data = [
{'uprn': 6032920, 'is_in_conservation_area': 'not_in_conservation_area'},
{'uprn': 6038625, 'is_in_conservation_area': 'not_in_conservation_area'},
{'uprn': 34153991, 'is_in_conservation_area': 'unknown'},
{'uprn': 10008299676, 'is_in_conservation_area': 'in_conservation_area'},
{'uprn': 10008299677, 'is_in_conservation_area': 'in_conservation_area'},
{'uprn': 100021039066, 'is_in_conservation_area': 'not_in_conservation_area'},
{'uprn': 100021226060, 'is_in_conservation_area': 'in_conservation_area'},
{'uprn': 200003489276, 'is_in_conservation_area': 'in_conservation_area'}
]
# TODO: db
floors_decile_data = {
'decile_labels': ['Decile 1', 'Decile 2', 'Decile 3', 'Decile 4', 'Decile 5', 'Decile 6', 'Decile 7', 'Decile 8',
'Decile 9', 'Decile 10'], 'decile_boundaries': [6., 50., 56., 69., 77.6, 87., 98., 112.,
127., 150., 2279.]}
walls_decile_data = {
'decile_labels': ['Decile 1', 'Decile 2', 'Decile 3', 'Decile 4', 'Decile 5', 'Decile 6', 'Decile 7', 'Decile 8',
'Decile 9', 'Decile 10'], 'decile_boundaries': [6., 49., 51., 55., 64., 71., 76., 83., 96.,
120., 2279.]}
lighting_averages = [
{'lighting-description': 'good lighting efficiency', 'low-energy-lighting': 99.26666666666667},
{'lighting-description': 'excellent lighting efficiency', 'low-energy-lighting': 100.0},
{'lighting-description': 'below average lighting efficiency', 'low-energy-lighting': 0.0}
]
@router.post("/trigger")
async def trigger_plan(body: PlanTriggerRequest):
logger.info("Getting the inputs")
# Read in the trigger file from s3
bucket_name = get_settings().PLAN_TRIGGER_BUCKET
epc_client = EpcClient(auth_token=get_settings().EPC_AUTH_TOKEN)
plan_input = read_csv_from_s3(bucket_name=bucket_name, filepath=body.trigger_file_path)
input_properties = []
for config in plan_input:
# We validate each record in the file. If the record is NOT valid, we need to handle this accordingly
# TODO: implment validation
# Create a record in db
property_id, is_new = create_property(
portfolio_id=body.portfolio_id, address=config['address'], postcode=config['postcode']
)
# if a new record was not created, we don't produduce recommendations
if not is_new:
continue
# TODO: Need to add heat demand target
create_property_targets(
property_id=property_id,
portfolio_id=body.portfolio_id,
epc_target=body.goal_value,
)
input_properties.append(
Property(
postcode=config['postcode'],
address1=config['address'],
epc_client=epc_client,
id=property_id
)
)
logger.info("Getting EPC data")
for p in input_properties:
p.search_address_epc()
p.set_year_built()
logger.info("Getting coordinates")
# This is placeholder, until the full dataset is loaded into the database
for p in input_properties:
coordinate_data = [x for x in open_uprn_data if x['UPRN'] == int(p.data['uprn'])][0]
p.set_coordinates(coordinate_data)
logger.info("Check if property is in conservation area")
for p in input_properties:
in_conservation_area = [x for x in in_conservation_area_data if x['uprn'] == int(p.data['uprn'])][0].get(
"is_in_conservation_area"
)
p.set_is_in_conservation_area(in_conservation_area)
# TODO: This won't work perfectly as we need the table of lighting averages by constituency
cleaner = EpcClean(data=[x.data for x in input_properties])
cleaner.clean()
logger.info("Getting components and properties recommendations")
recommendations = []
for property_id, p in enumerate(input_properties):
# For each property, classiy floor area decide
total_floor_area_group_decile = classify_decile_newvalues(
decile_boundaries=floors_decile_data["decile_boundaries"],
decile_labels=floors_decile_data["decile_labels"],
new_values=[float(p.data["total-floor-area"])],
)[0]
# Property recommendations
p.get_components(cleaner.cleaned)
# This is placeholder, until the full dataset is loaded into the database and we just make a read to the
# database
floors_u_value_estimate = [
x for x in uvalue_estimates_floors
if (x['local-authority'] == p.data["local-authority"]) &
(x['property-type'] == p.data["property-type"]) &
(x['built-form'] == p.data["built-form"]) &
(x['floor-energy-eff'] == p.data["floor-energy-eff"] if p.data["floor-energy-eff"] != 'N/A' else True) &
(x['floor-env-eff'] == p.data["floor-env-eff"] if p.data["floor-env-eff"] != 'N/A' else True)
]
# Floor recommendations
floor_recommender = FloorRecommendations(
property_instance=p, uvalue_estimates=floors_u_value_estimate,
total_floor_area_group_decile=total_floor_area_group_decile
)
floor_recommender.recommend()
# insert property id
for rec in floor_recommender.recommendations:
rec["property_id"] = property_id
recommendations.extend(floor_recommender.recommendations)
# Wall recommendations
# We would make this u-value query directly to the database
total_floor_area_group_decile = classify_decile_newvalues(
decile_boundaries=walls_decile_data["decile_boundaries"],
decile_labels=walls_decile_data["decile_labels"],
new_values=[float(p.data["total-floor-area"])],
)[0]
# This is placeholder, until the full dataset is loaded into the database and we just make a read to the
# database
walls_u_value_estimate = [
x for x in uvalue_estimates_walls
if (x['local-authority'] == p.data["local-authority"]) &
(x['property-type'] == p.data["property-type"]) &
(x['built-form'] == p.data["built-form"]) &
(x['walls-energy-eff'] == p.data["walls-energy-eff"] if p.data["walls-energy-eff"] != 'N/A' else True) &
(x['walls-env-eff'] == p.data["walls-env-eff"] if p.data["walls-env-eff"] != 'N/A' else True)
]
wall_recomendations = WallRecommendations(
property_instance=p,
uvalue_estimates=walls_u_value_estimate,
total_floor_area_group_decile=total_floor_area_group_decile
)
wall_recomendations.recommend()
# insert property id
for rec in wall_recomendations.recommendations:
rec["property_id"] = property_id
recommendations.extend(wall_recomendations.recommendations)
# Once we're done, we'll store:
# 1) the property data
# 2) the property details (epc)
# 3) the recommendations
# Upload property data
for p in input_properties:
property_data = {
"creation_status": "COMPLETE",
"uprn": int(p.data["uprn"]),
"has_pre_condition_report": True,
"has_recommendations": True,
"property_type": p.data["property-type"],
"built_form": p.data["built-form"],
"local_authority": p.data["local-authority-label"],
"constituency": p.data["constituency-label"],
"number_of_rooms": p.data["number-habitable-rooms"],
"year_built": p.year_built,
"tenure": p.data["tenure"],
"current_epc_rating": p.data["current-energy-rating"],
"current_sap_points": p.data["current-energy-efficiency"]
}
def clean_upload_data(to_update, to_clean_values):
for k, v in to_update.items():
if v in to_clean_values:
to_update[k] = None
return to_update
property_data = clean_upload_data(property_data, to_clean_values=p.DATA_ANOMALY_MATCHES)
def prepare_rating(field):
rating_lookup = {
"Very Good": 5,
"Good": 4,
"Average": 3,
"Poor": 2,
"Very Poor": 1,
"N/A": None,
}
return rating_lookup[field] if field not in p.DATA_ANOMALY_MATCHES else None
property_details_epc = {
"property_id": p.id,
"portfolio_id": body.portfolio_id,
"full_address": p.data["address"],
"total_floor_area": float(p.data["total-floor-area"]),
"walls": p.walls["clean_description"],
"walls_rating": prepare_rating(p.data["walls-energy-eff"]),
"roof": p.roof["clean_description"],
"roof_rating": prepare_rating(p.data["roof-energy-eff"]),
"floor": p.floor["clean_description"],
"floor_rating": prepare_rating(p.data["floor-energy-eff"]),
"windows": p.windows["clean_description"],
"windows_rating": prepare_rating(p.data["windows-energy-eff"]),
"heating": p.main_heating["clean_description"],
"heating_rating": prepare_rating(p.data["mainheat-energy-eff"]),
"heating_controls": p.main_heating_controls["clean_description"],
"heating_controls_rating": prepare_rating(p.data["mainheatc-energy-eff"]),
"hot_water": p.hotwater["clean_description"],
"hot_water_rating": prepare_rating(p.data["hot-water-energy-eff"]),
"lighting": p.lighting["clean_description"],
"lighting_rating": prepare_rating(p.data["lighting-energy-eff"]),
"mainfuel": p.main_fuel["clean_description"],
"ventilation": p.ventilation["ventilation"],
"solar_pv": p.solar_pv["solar_pv"],
"solar_hot_water": p.solar_hot_water["solar_hot_water"],
"wind_turbine": p.wind_turbine["wind_turbine"],
"floor_height": p.data["floor-height"],
"heat_loss_corridor": p.data["heat-loss-corridor"],
"unheated_corridor_length": p.data["unheated-corridor-length"],
"number_of_open_fireplaces": p.number_of_open_fireplaces,
"number_of_extensions": p.number_of_extensions,
"number_of_storeys": p.number_of_storeys,
"mains_gas": p.data["mains-gas-flag"],
"energy_tarrif": p.data["energy-tariff"],
"primary_energy_consumption": p.energy["primary_energy_consumption"],
"co2_emissions": p.energy["co2_emissions"],
}
return {"recommendations": recommendations}