From 942c2923daf5a18d528ef47847bc2b2b2b8d512e Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Wed, 8 Apr 2026 10:55:48 +0000 Subject: [PATCH 01/47] initial setup --- .../hubspot_trigger_orchestrator/handler.py | 61 +++++++++++++++++++ .../hubspot_deal_differ.py | 21 +++++++ ...ot_trigger_orchestrator_trigger_request.py | 5 ++ etl/hubspot/hubspotDataTodB.py | 6 +- etl/hubspot/scripts/scraper/main.py | 10 +-- 5 files changed, 96 insertions(+), 7 deletions(-) create mode 100644 backend/hubspot_trigger_orchestrator/handler.py create mode 100644 backend/hubspot_trigger_orchestrator/hubspot_deal_differ.py create mode 100644 backend/hubspot_trigger_orchestrator/hubspot_trigger_orchestrator_trigger_request.py diff --git a/backend/hubspot_trigger_orchestrator/handler.py b/backend/hubspot_trigger_orchestrator/handler.py new file mode 100644 index 00000000..1f83ed80 --- /dev/null +++ b/backend/hubspot_trigger_orchestrator/handler.py @@ -0,0 +1,61 @@ +import json +from typing import Any, Dict, Mapping, Optional + +from backend.app.db.models.organisation import HubspotDealData +from backend.hubspot_trigger_orchestrator.hubspot_deal_differ import HubspotDealDiffer +from backend.hubspot_trigger_orchestrator.hubspot_trigger_orchestrator_trigger_request import ( + HubspotTriggerOrchestratorTriggerRequest, +) +from backend.utils.subtasks import task_handler +from etl.hubspot.hubspotClient import HubspotClient +from etl.hubspot.hubspotDataTodB import HubspotDataToDb +from utils.logger import setup_logger + +logger = setup_logger() + + +@task_handler() +def handler(event: Mapping[str, Any], context: Any) -> None: + + db_client = HubspotDataToDb() + hubspot_client = HubspotClient() + + for record in event.get("Records", []): + body_dict = json.loads(record["body"]) + + logger.debug("Validating request body") + payload = HubspotTriggerOrchestratorTriggerRequest.model_validate(body_dict) + logger.debug("Successfully validated request body") + + hubspot_deal_id: str = payload.hubspot_deal_id + + db_deal: Optional[HubspotDealData] = db_client.find_deal_with_deal_id( + hubspot_deal_id + ) + if not db_deal: + # new hubspot deal, no diffing to do + # TODO: trigger hubspot to db ETL + return + + hubspot_deal: Dict[str, str] + company: Optional[str] + listing: Optional[dict[str, str]] + + hubspot_deal, company, listing = hubspot_client.get_deal_info_for_db( + hubspot_deal_id + ) + + if HubspotDealDiffer.check_for_pashub_trigger( + new_deal=hubspot_deal, old_deal=db_deal + ): + # TODO: trigger pashub file fetcher + return + + if HubspotDealDiffer.check_for_db_update_trigger( + new_deal=hubspot_deal, + new_company=company, + new_listing=listing, + old_deal=db_deal, + ): + # TODO: trigger db upsert + return diff --git a/backend/hubspot_trigger_orchestrator/hubspot_deal_differ.py b/backend/hubspot_trigger_orchestrator/hubspot_deal_differ.py new file mode 100644 index 00000000..9d66c637 --- /dev/null +++ b/backend/hubspot_trigger_orchestrator/hubspot_deal_differ.py @@ -0,0 +1,21 @@ +from typing import Dict, Optional + +from backend.app.db.models.organisation import HubspotDealData + + +class HubspotDealDiffer: + + @staticmethod + def check_for_pashub_trigger( + new_deal: Dict[str, str], old_deal: HubspotDealData + ) -> bool: + raise NotImplementedError + + @staticmethod + def check_for_db_update_trigger( + new_deal: Dict[str, str], + new_company: Optional[str], + new_listing: Optional[Dict[str, str]], + old_deal: HubspotDealData, + ) -> bool: + raise NotImplementedError diff --git a/backend/hubspot_trigger_orchestrator/hubspot_trigger_orchestrator_trigger_request.py b/backend/hubspot_trigger_orchestrator/hubspot_trigger_orchestrator_trigger_request.py new file mode 100644 index 00000000..1adfa07c --- /dev/null +++ b/backend/hubspot_trigger_orchestrator/hubspot_trigger_orchestrator_trigger_request.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel + + +class HubspotTriggerOrchestratorTriggerRequest(BaseModel): + hubspot_deal_id: str diff --git a/etl/hubspot/hubspotDataTodB.py b/etl/hubspot/hubspotDataTodB.py index 6325efc2..36167bf0 100644 --- a/etl/hubspot/hubspotDataTodB.py +++ b/etl/hubspot/hubspotDataTodB.py @@ -74,7 +74,7 @@ class HubspotDataToDb: .all() ) - def find_deal_with_deal_id(self, deal_id): + def find_deal_with_deal_id(self, deal_id: str) -> Optional[HubspotDealData]: with db_read_session() as session: return ( session.query(HubspotDealData) @@ -477,7 +477,9 @@ class HubspotDataToDb: dealname=deal_data.get("dealname"), dealstage=deal_data.get("dealstage"), listing_id=listing.get("listing_id", None) if listing else None, - landlord_property_id=listing.get("owner_property_id") if listing else None, + landlord_property_id=( + listing.get("owner_property_id") if listing else None + ), uprn=listing.get("national_uprn") if listing else None, outcome=deal_data.get("outcome"), outcome_notes=deal_data.get("outcome_notes"), diff --git a/etl/hubspot/scripts/scraper/main.py b/etl/hubspot/scripts/scraper/main.py index 4f71c6d0..a003ad28 100644 --- a/etl/hubspot/scripts/scraper/main.py +++ b/etl/hubspot/scripts/scraper/main.py @@ -16,9 +16,9 @@ def handler(body: dict[str, Any], context: Any) -> None: hubspot: HubspotClient = HubspotClient() dbloader: HubspotDataToDb = HubspotDataToDb() - deal = dbloader.find_deal_with_deal_id(hubspot_deal_id) - if deal: - dbloader.update_deal_with_checks(deal, hubspot) + db_deal = dbloader.find_deal_with_deal_id(hubspot_deal_id) + if db_deal: + dbloader.update_deal_with_checks(db_deal, hubspot) else: - deal, company, listing = hubspot.get_deal_info_for_db(hubspot_deal_id) - dbloader.upsert_deal(deal, company, listing, hubspot) + hubspot_deal, company, listing = hubspot.get_deal_info_for_db(hubspot_deal_id) + dbloader.upsert_deal(hubspot_deal, company, listing, hubspot) From d730a90246e4cb856c37999274cf58e1723739ca Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Wed, 8 Apr 2026 14:47:31 +0000 Subject: [PATCH 02/47] added coordination comments --- backend/address2UPRN/README.md | 6 +++--- backend/app/db/models/organisation.py | 1 + etl/hubspot/hubspotClient.py | 1 + etl/hubspot/hubspotDataTodB.py | 6 ++++++ sfr/principal_pitch/2_export_data.py | 8 ++++---- 5 files changed, 15 insertions(+), 7 deletions(-) diff --git a/backend/address2UPRN/README.md b/backend/address2UPRN/README.md index b17b36d1..6b0fc753 100644 --- a/backend/address2UPRN/README.md +++ b/backend/address2UPRN/README.md @@ -52,9 +52,9 @@ I uploaded the missing uprn here: s3://retrofit-data-dev/ara_raw_inputs/calico/m ordnance_survey sqs is => https://eu-west-2.console.aws.amazon.com/sqs/v3/home?region=eu-west-2#/queues/https%3A%2F%2Fsqs.eu-west-2.amazonaws.com%2F337213553626%2FordnanceSurvey-queue-dev { - "s3_uri": "s3://retrofit-data-dev/ara_raw_inputs/calico/missinguprn.csv", - "task_id": "a7b70a02-4df4-45b5-a50b-196e095910bb", - "sub_task_id": "567cf73b-1210-4909-9ecc-36ae7e23420e" + "s3_uri": "s3://retrofit-data-dev/ara_raw_inputs/eon/beyond_housing/Book(Sheet1).csv", + "task_id": "ccdec0d1-ebf3-484f-b2ae-397200dd25da", + "sub_task_id": "569d41f6-45cd-4e64-a586-eb8c2097375d" } diff --git a/backend/app/db/models/organisation.py b/backend/app/db/models/organisation.py index cc3ef2bc..784cc4ad 100644 --- a/backend/app/db/models/organisation.py +++ b/backend/app/db/models/organisation.py @@ -38,6 +38,7 @@ class HubspotDealData(SQLModel, table=True): major_condition_issue_evidence_s3_url: Optional[str] = Field(default=None) coordination_status: Optional[str] = Field(default=None) + coordination_comments: Optional[str] = Field(default=None) design_status: Optional[str] = Field(default=None) listing_id: Optional[str] = Field(default=None) diff --git a/etl/hubspot/hubspotClient.py b/etl/hubspot/hubspotClient.py index a9ea535d..dc2fc7fe 100644 --- a/etl/hubspot/hubspotClient.py +++ b/etl/hubspot/hubspotClient.py @@ -246,6 +246,7 @@ class HubspotClient: "major_condition_issue_description", "major_condition_issue_photos", "coordination_status__stage_1_", + "coordination_comments", "retrofit_design_status", "pashub_link", "sharepoint_link", diff --git a/etl/hubspot/hubspotDataTodB.py b/etl/hubspot/hubspotDataTodB.py index 6325efc2..d7b47ef3 100644 --- a/etl/hubspot/hubspotDataTodB.py +++ b/etl/hubspot/hubspotDataTodB.py @@ -165,6 +165,10 @@ class HubspotDataToDb: == hs_deal.get("coordination_status__stage_1_"), "coordination stage 1 status mismatch", ), + soft_assert( + deal_in_db.coordination_comments == hs_deal.get("coordination_comments"), + "coordination_comments mismatch", + ), soft_assert( deal_in_db.design_status == hs_deal.get("retrofit_design_status"), "retrofit design mismatch", @@ -383,6 +387,7 @@ class HubspotDataToDb: "coordination_status": deal_data.get( "coordination_status__stage_1_" ), + "coordination_comments": deal_data.get("coordination_comments"), "design_status": deal_data.get("retrofit_design_status"), "pashub_link": deal_data.get("pashub_link"), "sharepoint_link": deal_data.get("sharepoint_link"), @@ -490,6 +495,7 @@ class HubspotDataToDb: "major_condition_issue_photos" ), coordination_status=deal_data.get("coordination_status__stage_1_"), + coordination_comments=deal_data.get("coordination_comments"), design_status=deal_data.get("retrofit_design_status"), pashub_link=deal_data.get("pashub_link"), sharepoint_link=deal_data.get("sharepoint_link"), diff --git a/sfr/principal_pitch/2_export_data.py b/sfr/principal_pitch/2_export_data.py index 3baa7a44..7c80f4dc 100644 --- a/sfr/principal_pitch/2_export_data.py +++ b/sfr/principal_pitch/2_export_data.py @@ -26,13 +26,13 @@ from backend.app.db.functions.materials_functions import get_materials from collections import defaultdict from sqlalchemy import func -PORTFOLIO_ID = 656 -SCENARIOS = [1177] +PORTFOLIO_ID = 632 +SCENARIOS = [1144] scenario_names = { - 1177: "EPC C; Proposed Measures", + 1144: "EPC C", } -project_name = "Walsall Council | WH:LG" +project_name = "Calico Project" def get_data(portfolio_id, scenario_ids): From d6bfef59aff595c9aa3baa9a2aaccd4581d69b4d Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Wed, 8 Apr 2026 14:48:17 +0000 Subject: [PATCH 03/47] remove db update from hubspot client get method --- .../hubspot_trigger_orchestrator/handler.py | 2 +- etl/hubspot/hubspotClient.py | 10 ++---- etl/hubspot/hubspotDataTodB.py | 23 ++++++++------ etl/hubspot/scripts/scraper/main.py | 31 ++++++++++++++----- .../tests/test_hubspot_client_integration.py | 2 +- 5 files changed, 41 insertions(+), 27 deletions(-) diff --git a/backend/hubspot_trigger_orchestrator/handler.py b/backend/hubspot_trigger_orchestrator/handler.py index 1f83ed80..c79fe2b9 100644 --- a/backend/hubspot_trigger_orchestrator/handler.py +++ b/backend/hubspot_trigger_orchestrator/handler.py @@ -41,7 +41,7 @@ def handler(event: Mapping[str, Any], context: Any) -> None: company: Optional[str] listing: Optional[dict[str, str]] - hubspot_deal, company, listing = hubspot_client.get_deal_info_for_db( + hubspot_deal, company, listing = hubspot_client.get_deal_company_listing( hubspot_deal_id ) diff --git a/etl/hubspot/hubspotClient.py b/etl/hubspot/hubspotClient.py index a9ea535d..777ad482 100644 --- a/etl/hubspot/hubspotClient.py +++ b/etl/hubspot/hubspotClient.py @@ -26,10 +26,10 @@ from hubspot.crm.associations.v4.models import ( # type: ignore[reportMissingTy ForwardPaging as AssociationsPaging, NextPage as AssociationsPagingNext, ) -from etl.hubspot.hubspotDataTodB import CompanyData, HubspotDataToDb from backend.app.config import get_settings +from etl.hubspot.company_data import CompanyData from utils.logger import setup_logger import mimetypes @@ -279,18 +279,12 @@ class HubspotClient: deal_info: dict[str, str] = cast(dict[str, str], deal.properties) # type: ignore[reportUnknownMemberType] return deal_info - def get_deal_info_for_db( + def get_deal_company_listing( self, deal_id: str ) -> tuple[dict[str, str], Optional[str], Optional[dict[str, str]]]: deal: dict[str, str] = self.from_deal_id_get_info(deal_id) company: Optional[str] = self.from_deal_id_get_associated_company_id(deal_id) - - if company: - company_data: CompanyData = self.get_company_information(company) - dbloader: HubspotDataToDb = HubspotDataToDb() - dbloader.upsert_company(company_data) - listing: Optional[dict[str, str]] = self.from_deal_id_get_associated_listing( deal_id ) diff --git a/etl/hubspot/hubspotDataTodB.py b/etl/hubspot/hubspotDataTodB.py index 36167bf0..49dd1685 100644 --- a/etl/hubspot/hubspotDataTodB.py +++ b/etl/hubspot/hubspotDataTodB.py @@ -2,17 +2,14 @@ from backend.app.db.connection import db_read_session from backend.app.db.models.organisation import Organisation, HubspotDealData from sqlmodel import select from datetime import datetime, timezone -from typing import TypedDict, Optional +from typing import Dict, Optional +from etl.hubspot.company_data import CompanyData +from etl.hubspot.hubspotClient import HubspotClient from etl.hubspot.s3_uploader import S3Uploader import hashlib import os -class CompanyData(TypedDict): - hs_object_id: str - name: str - - class HubspotDataToDb: def __init__(self): self.s3 = S3Uploader( @@ -98,7 +95,9 @@ class HubspotDataToDb: sha256.update(chunk) return sha256.hexdigest() - def update_deal_with_checks(self, deal_in_db, hubspot_client) -> bool: + def update_deal_with_checks( + self, deal_in_db: HubspotDealData, hubspot_client: HubspotClient + ) -> bool: """ Checks if a deal needs updating and syncs it with HubSpot. Also handles major_condition_issue_photos file upload to S3 with integrity check. @@ -112,7 +111,7 @@ class HubspotDataToDb: print(f"🔍 Checking if deal needs updating (deal_id={deal_in_db.deal_id})") - hs_deal, hs_company_id, hs_listing = hubspot_client.get_deal_info_for_db( + hs_deal, hs_company_id, hs_listing = hubspot_client.get_deal_company_listing( deal_in_db.deal_id ) @@ -346,7 +345,13 @@ class HubspotDataToDb: return True - def upsert_deal(self, deal_data, company, listing, hubspot_client): + def upsert_deal( + self, + deal_data: Dict[str, str], + company: Optional[str], + listing: Optional[dict[str, str]], + hubspot_client: HubspotClient, + ): """ Inserts or updates a deal record. Also uploads photos if present and adds S3 URL. diff --git a/etl/hubspot/scripts/scraper/main.py b/etl/hubspot/scripts/scraper/main.py index a003ad28..e5658a20 100644 --- a/etl/hubspot/scripts/scraper/main.py +++ b/etl/hubspot/scripts/scraper/main.py @@ -1,7 +1,8 @@ +from backend.app.db.models.organisation import HubspotDealData from etl.hubspot.hubspotClient import HubspotClient -from etl.hubspot.hubspotDataTodB import HubspotDataToDb +from etl.hubspot.hubspotDataTodB import CompanyData, HubspotDataToDb from backend.utils.subtasks import task_handler -from typing import Any +from typing import Any, Dict, Optional @task_handler() @@ -14,11 +15,25 @@ def handler(body: dict[str, Any], context: Any) -> None: ) hubspot_deal_id = "327170793707" - hubspot: HubspotClient = HubspotClient() - dbloader: HubspotDataToDb = HubspotDataToDb() - db_deal = dbloader.find_deal_with_deal_id(hubspot_deal_id) + hubspot_client = HubspotClient() + db_client = HubspotDataToDb() + db_deal: Optional[HubspotDealData] = db_client.find_deal_with_deal_id( + hubspot_deal_id + ) if db_deal: - dbloader.update_deal_with_checks(db_deal, hubspot) + db_client.update_deal_with_checks(db_deal, hubspot_client) else: - hubspot_deal, company, listing = hubspot.get_deal_info_for_db(hubspot_deal_id) - dbloader.upsert_deal(hubspot_deal, company, listing, hubspot) + hubspot_deal: Dict[str, str] + company: Optional[str] + listing: Optional[dict[str, str]] + + hubspot_deal, company, listing = hubspot_client.get_deal_company_listing( + hubspot_deal_id + ) + + if company: + company_data: CompanyData = hubspot_client.get_company_information(company) + db_client: HubspotDataToDb = HubspotDataToDb() + db_client.upsert_company(company_data) + + db_client.upsert_deal(hubspot_deal, company, listing, hubspot_client) diff --git a/etl/hubspot/tests/test_hubspot_client_integration.py b/etl/hubspot/tests/test_hubspot_client_integration.py index a3d8ae54..d0dd818a 100644 --- a/etl/hubspot/tests/test_hubspot_client_integration.py +++ b/etl/hubspot/tests/test_hubspot_client_integration.py @@ -71,7 +71,7 @@ class TestHubspotClientIntegration: def test_get_deal_info_for_db(self, client: HubspotClient): deal_id: str = "263490768079" - deal, company, listing = client.get_deal_info_for_db(deal_id) + deal, company, listing = client.get_deal_company_listing(deal_id) assert "dealname" in deal assert "dealstage" in deal From b968fbab448c39aaabd21251c406f5ca7c0a8f83 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Wed, 8 Apr 2026 14:48:29 +0000 Subject: [PATCH 04/47] include missing file --- etl/hubspot/company_data.py | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 etl/hubspot/company_data.py diff --git a/etl/hubspot/company_data.py b/etl/hubspot/company_data.py new file mode 100644 index 00000000..13b2ee88 --- /dev/null +++ b/etl/hubspot/company_data.py @@ -0,0 +1,6 @@ +from typing import TypedDict + + +class CompanyData(TypedDict): + hs_object_id: str + name: str From 540054e12f83514aa5baf2861235514e4450bae1 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Wed, 8 Apr 2026 14:53:11 +0000 Subject: [PATCH 05/47] rename method --- backend/hubspot_trigger_orchestrator/handler.py | 4 ++-- etl/hubspot/hubspotClient.py | 2 +- etl/hubspot/hubspotDataTodB.py | 4 ++-- etl/hubspot/scripts/scraper/main.py | 4 ++-- etl/hubspot/tests/test_hubspot_client_integration.py | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/backend/hubspot_trigger_orchestrator/handler.py b/backend/hubspot_trigger_orchestrator/handler.py index c79fe2b9..38724812 100644 --- a/backend/hubspot_trigger_orchestrator/handler.py +++ b/backend/hubspot_trigger_orchestrator/handler.py @@ -41,8 +41,8 @@ def handler(event: Mapping[str, Any], context: Any) -> None: company: Optional[str] listing: Optional[dict[str, str]] - hubspot_deal, company, listing = hubspot_client.get_deal_company_listing( - hubspot_deal_id + hubspot_deal, company, listing = ( + hubspot_client.get_deal_and_company_and_listing(hubspot_deal_id) ) if HubspotDealDiffer.check_for_pashub_trigger( diff --git a/etl/hubspot/hubspotClient.py b/etl/hubspot/hubspotClient.py index 777ad482..cedaa7f3 100644 --- a/etl/hubspot/hubspotClient.py +++ b/etl/hubspot/hubspotClient.py @@ -279,7 +279,7 @@ class HubspotClient: deal_info: dict[str, str] = cast(dict[str, str], deal.properties) # type: ignore[reportUnknownMemberType] return deal_info - def get_deal_company_listing( + def get_deal_and_company_and_listing( self, deal_id: str ) -> tuple[dict[str, str], Optional[str], Optional[dict[str, str]]]: diff --git a/etl/hubspot/hubspotDataTodB.py b/etl/hubspot/hubspotDataTodB.py index 49dd1685..e7008618 100644 --- a/etl/hubspot/hubspotDataTodB.py +++ b/etl/hubspot/hubspotDataTodB.py @@ -111,8 +111,8 @@ class HubspotDataToDb: print(f"🔍 Checking if deal needs updating (deal_id={deal_in_db.deal_id})") - hs_deal, hs_company_id, hs_listing = hubspot_client.get_deal_company_listing( - deal_in_db.deal_id + hs_deal, hs_company_id, hs_listing = ( + hubspot_client.get_deal_and_company_and_listing(deal_in_db.deal_id) ) # Soft compare key fields diff --git a/etl/hubspot/scripts/scraper/main.py b/etl/hubspot/scripts/scraper/main.py index e5658a20..d8d4a357 100644 --- a/etl/hubspot/scripts/scraper/main.py +++ b/etl/hubspot/scripts/scraper/main.py @@ -27,8 +27,8 @@ def handler(body: dict[str, Any], context: Any) -> None: company: Optional[str] listing: Optional[dict[str, str]] - hubspot_deal, company, listing = hubspot_client.get_deal_company_listing( - hubspot_deal_id + hubspot_deal, company, listing = ( + hubspot_client.get_deal_and_company_and_listing(hubspot_deal_id) ) if company: diff --git a/etl/hubspot/tests/test_hubspot_client_integration.py b/etl/hubspot/tests/test_hubspot_client_integration.py index d0dd818a..0f4b425c 100644 --- a/etl/hubspot/tests/test_hubspot_client_integration.py +++ b/etl/hubspot/tests/test_hubspot_client_integration.py @@ -71,7 +71,7 @@ class TestHubspotClientIntegration: def test_get_deal_info_for_db(self, client: HubspotClient): deal_id: str = "263490768079" - deal, company, listing = client.get_deal_company_listing(deal_id) + deal, company, listing = client.get_deal_and_company_and_listing(deal_id) assert "dealname" in deal assert "dealstage" in deal From 21ca0d7649bc2ff9be36dd236d80b3abed391894 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Wed, 8 Apr 2026 15:25:50 +0000 Subject: [PATCH 06/47] =?UTF-8?q?diff=20checker=20for=20pashub=20trigger?= =?UTF-8?q?=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pytest.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest.ini b/pytest.ini index db7afaf5..792b27e0 100644 --- a/pytest.ini +++ b/pytest.ini @@ -3,6 +3,6 @@ pythonpath = . 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 +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 backend/hubspot_trigger_orchestrator/tests markers = integration: mark a test as an integration test From 39f37f1668907db733ffa602e1a2b84c9e766fd0 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Wed, 8 Apr 2026 15:26:32 +0000 Subject: [PATCH 07/47] =?UTF-8?q?diff=20checker=20for=20pashub=20trigger?= =?UTF-8?q?=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tests/test_hubspot_deal_differ.py | 424 ++++++++++++++++++ 1 file changed, 424 insertions(+) create mode 100644 backend/hubspot_trigger_orchestrator/tests/test_hubspot_deal_differ.py diff --git a/backend/hubspot_trigger_orchestrator/tests/test_hubspot_deal_differ.py b/backend/hubspot_trigger_orchestrator/tests/test_hubspot_deal_differ.py new file mode 100644 index 00000000..ddca766a --- /dev/null +++ b/backend/hubspot_trigger_orchestrator/tests/test_hubspot_deal_differ.py @@ -0,0 +1,424 @@ +from datetime import datetime +from typing import Dict +import uuid + +from backend.app.db.models.organisation import HubspotDealData +from backend.hubspot_trigger_orchestrator.hubspot_deal_differ import HubspotDealDiffer + + +def test_pashub_trigger__outcome_note_added__returns_false() -> None: + # arrange + deal_id = uuid.uuid4() + + old_deal = HubspotDealData( + id=deal_id, + deal_id="1", + created_at=datetime(2025, 12, 1, 12, 0, 0), + updated_at=datetime(2025, 12, 1, 12, 0, 0), + ) + new_deal: Dict[str, str] = { + "id": str(deal_id), + "deal_id": "1", + "outcome_notes": "test note", + "created_at": datetime(2025, 12, 1, 12, 0, 0).isoformat(), + "updated_at": datetime(2025, 12, 1, 12, 30, 0).isoformat(), + } + + expected_output = False + + # act + actual_output: bool = HubspotDealDiffer.check_for_pashub_trigger( + new_deal=new_deal, old_deal=old_deal + ) + + # assert + assert actual_output == expected_output + + +def test_pashub_trigger__pashub_link_changed__returns_true() -> None: + # arrange + deal_id = uuid.uuid4() + + old_deal = HubspotDealData( + id=deal_id, + deal_id="1", + pashub_link="www.google.co.uk", + created_at=datetime(2025, 12, 1, 12, 0, 0), + updated_at=datetime(2025, 12, 1, 12, 0, 0), + ) + new_deal: Dict[str, str] = { + "id": str(deal_id), + "deal_id": "1", + "pashub_link": "www.bbc.co.uk", + "created_at": datetime(2025, 12, 1, 12, 0, 0).isoformat(), + "updated_at": datetime(2025, 12, 1, 12, 30, 0).isoformat(), + } + + expected_output = True + + # act + actual_output: bool = HubspotDealDiffer.check_for_pashub_trigger( + new_deal=new_deal, old_deal=old_deal + ) + + # assert + assert actual_output == expected_output + + +def test_pashub_trigger__coordination_completed_and_pashub_link_set__returns_true() -> ( + None +): + # arrange + deal_id = uuid.uuid4() + + old_deal = HubspotDealData( + id=deal_id, + deal_id="1", + pashub_link="www.google.co.uk", + coordination_status="random", + created_at=datetime(2025, 12, 1, 12, 0, 0), + updated_at=datetime(2025, 12, 1, 12, 0, 0), + ) + new_deal: Dict[str, str] = { + "id": str(deal_id), + "deal_id": "1", + "coordination_status": "v1 ioe/mtp complete", + "pashub_link": "www.google.co.uk", + "created_at": datetime(2025, 12, 1, 12, 0, 0).isoformat(), + "updated_at": datetime(2025, 12, 1, 12, 30, 0).isoformat(), + } + + expected_output = True + + # act + actual_output: bool = HubspotDealDiffer.check_for_pashub_trigger( + new_deal=new_deal, old_deal=old_deal + ) + + # assert + assert actual_output == expected_output + + +def test_pashub_trigger__coordination_completed_and_pashub_link_set__returns_true_2() -> ( + None +): + # arrange + deal_id = uuid.uuid4() + + old_deal = HubspotDealData( + id=deal_id, + deal_id="1", + pashub_link="www.google.co.uk", + coordination_status="random", + created_at=datetime(2025, 12, 1, 12, 0, 0), + updated_at=datetime(2025, 12, 1, 12, 0, 0), + ) + new_deal: Dict[str, str] = { + "id": str(deal_id), + "deal_id": "1", + "coordination_status": "v2 ioe/mtp complete", + "pashub_link": "www.google.co.uk", + "created_at": datetime(2025, 12, 1, 12, 0, 0).isoformat(), + "updated_at": datetime(2025, 12, 1, 12, 30, 0).isoformat(), + } + + expected_output = True + + # act + actual_output: bool = HubspotDealDiffer.check_for_pashub_trigger( + new_deal=new_deal, old_deal=old_deal + ) + + # assert + assert actual_output == expected_output + + +def test_pashub_trigger__coordination_completed_and_pashub_link_not_set__returns_false() -> ( + None +): + # arrange + deal_id = uuid.uuid4() + + old_deal = HubspotDealData( + id=deal_id, + deal_id="1", + coordination_status="random", + created_at=datetime(2025, 12, 1, 12, 0, 0), + updated_at=datetime(2025, 12, 1, 12, 0, 0), + ) + new_deal: Dict[str, str] = { + "id": str(deal_id), + "deal_id": "1", + "coordination_status": "v2 ioe/mtp complete", + "created_at": datetime(2025, 12, 1, 12, 0, 0).isoformat(), + "updated_at": datetime(2025, 12, 1, 12, 30, 0).isoformat(), + } + + expected_output = False + + # act + actual_output: bool = HubspotDealDiffer.check_for_pashub_trigger( + new_deal=new_deal, old_deal=old_deal + ) + + # assert + assert actual_output == expected_output + + +def test_pashub_trigger__coordination_status_not_completed_and_pashub_link_set__returns_false() -> ( + None +): + # arrange + deal_id = uuid.uuid4() + + old_deal = HubspotDealData( + id=deal_id, + deal_id="1", + pashub_link="www.google.co.uk", + coordination_status="random", + created_at=datetime(2025, 12, 1, 12, 0, 0), + updated_at=datetime(2025, 12, 1, 12, 0, 0), + ) + new_deal: Dict[str, str] = { + "id": str(deal_id), + "deal_id": "1", + "coordination_status": "not complete", + "pashub_link": "www.google.co.uk", + "created_at": datetime(2025, 12, 1, 12, 0, 0).isoformat(), + "updated_at": datetime(2025, 12, 1, 12, 30, 0).isoformat(), + } + + expected_output = False + + # act + actual_output: bool = HubspotDealDiffer.check_for_pashub_trigger( + new_deal=new_deal, old_deal=old_deal + ) + + # assert + assert actual_output == expected_output + + +def test_pashub_trigger__design_completed_and_pashub_link_set__returns_true() -> None: + # arrange + deal_id = uuid.uuid4() + + old_deal = HubspotDealData( + id=deal_id, + deal_id="1", + pashub_link="www.google.co.uk", + created_at=datetime(2025, 12, 1, 12, 0, 0), + updated_at=datetime(2025, 12, 1, 12, 0, 0), + ) + new_deal: Dict[str, str] = { + "id": str(deal_id), + "deal_id": "1", + "pashub_link": "www.google.co.uk", + "design_status": "uploaded", + "created_at": datetime(2025, 12, 1, 12, 0, 0).isoformat(), + "updated_at": datetime(2025, 12, 1, 12, 30, 0).isoformat(), + } + + expected_output = True + + # act + actual_output: bool = HubspotDealDiffer.check_for_pashub_trigger( + new_deal=new_deal, old_deal=old_deal + ) + + # assert + assert actual_output == expected_output + + +def test_pashub_trigger__design_completed_and_pashub_link_not_set__returns_false() -> ( + None +): + # arrange + deal_id = uuid.uuid4() + + old_deal = HubspotDealData( + id=deal_id, + deal_id="1", + created_at=datetime(2025, 12, 1, 12, 0, 0), + updated_at=datetime(2025, 12, 1, 12, 0, 0), + ) + new_deal: Dict[str, str] = { + "id": str(deal_id), + "deal_id": "1", + "design_status": "uploaded", + "created_at": datetime(2025, 12, 1, 12, 0, 0).isoformat(), + "updated_at": datetime(2025, 12, 1, 12, 30, 0).isoformat(), + } + + expected_output = False + + # act + actual_output: bool = HubspotDealDiffer.check_for_pashub_trigger( + new_deal=new_deal, old_deal=old_deal + ) + + # assert + assert actual_output == expected_output + + +def test_pashub_trigger__design_not_completed_and_pashub_link_set__returns_false() -> ( + None +): + # arrange + deal_id = uuid.uuid4() + + old_deal = HubspotDealData( + id=deal_id, + deal_id="1", + pashub_link="www.google.co.uk", + created_at=datetime(2025, 12, 1, 12, 0, 0), + updated_at=datetime(2025, 12, 1, 12, 0, 0), + ) + new_deal: Dict[str, str] = { + "id": str(deal_id), + "deal_id": "1", + "pashub_link": "www.google.co.uk", + "design_status": "not uploaded", + "created_at": datetime(2025, 12, 1, 12, 0, 0).isoformat(), + "updated_at": datetime(2025, 12, 1, 12, 30, 0).isoformat(), + } + + expected_output = False + + # act + actual_output: bool = HubspotDealDiffer.check_for_pashub_trigger( + new_deal=new_deal, old_deal=old_deal + ) + + # assert + assert actual_output == expected_output + + +def test_pashub_trigger__lodgement_completed_and_pashub_link_set__returns_true() -> ( + None +): + # arrange + deal_id = uuid.uuid4() + + old_deal = HubspotDealData( + id=deal_id, + deal_id="1", + pashub_link="www.google.co.uk", + created_at=datetime(2025, 12, 1, 12, 0, 0), + updated_at=datetime(2025, 12, 1, 12, 0, 0), + ) + new_deal: Dict[str, str] = { + "id": str(deal_id), + "deal_id": "1", + "pashub_link": "www.google.co.uk", + "lodgement_status": "lodgement complete", + "created_at": datetime(2025, 12, 1, 12, 0, 0).isoformat(), + "updated_at": datetime(2025, 12, 1, 12, 30, 0).isoformat(), + } + + expected_output = True + + # act + actual_output: bool = HubspotDealDiffer.check_for_pashub_trigger( + new_deal=new_deal, old_deal=old_deal + ) + + # assert + assert actual_output == expected_output + + +def test_pashub_trigger__lodgement_completed_and_pashub_link_set__returns_true_2() -> ( + None +): + # arrange + deal_id = uuid.uuid4() + + old_deal = HubspotDealData( + id=deal_id, + deal_id="1", + pashub_link="www.google.co.uk", + created_at=datetime(2025, 12, 1, 12, 0, 0), + updated_at=datetime(2025, 12, 1, 12, 0, 0), + ) + new_deal: Dict[str, str] = { + "id": str(deal_id), + "deal_id": "1", + "pashub_link": "www.google.co.uk", + "lodgement_status": "measures lodged", + "created_at": datetime(2025, 12, 1, 12, 0, 0).isoformat(), + "updated_at": datetime(2025, 12, 1, 12, 30, 0).isoformat(), + } + + expected_output = True + + # act + actual_output: bool = HubspotDealDiffer.check_for_pashub_trigger( + new_deal=new_deal, old_deal=old_deal + ) + + # assert + assert actual_output == expected_output + + +def test_pashub_trigger__lodgement_completed_and_pashub_link_not_set__returns_false() -> ( + None +): + # arrange + deal_id = uuid.uuid4() + + old_deal = HubspotDealData( + id=deal_id, + deal_id="1", + created_at=datetime(2025, 12, 1, 12, 0, 0), + updated_at=datetime(2025, 12, 1, 12, 0, 0), + ) + new_deal: Dict[str, str] = { + "id": str(deal_id), + "deal_id": "1", + "design_status": "lodgement complete", + "created_at": datetime(2025, 12, 1, 12, 0, 0).isoformat(), + "updated_at": datetime(2025, 12, 1, 12, 30, 0).isoformat(), + } + + expected_output = False + + # act + actual_output: bool = HubspotDealDiffer.check_for_pashub_trigger( + new_deal=new_deal, old_deal=old_deal + ) + + # assert + assert actual_output == expected_output + + +def test_pashub_trigger__lodgement_not_completed_and_pashub_link_set__returns_false() -> ( + None +): + # arrange + deal_id = uuid.uuid4() + + old_deal = HubspotDealData( + id=deal_id, + deal_id="1", + pashub_link="www.google.co.uk", + created_at=datetime(2025, 12, 1, 12, 0, 0), + updated_at=datetime(2025, 12, 1, 12, 0, 0), + ) + new_deal: Dict[str, str] = { + "id": str(deal_id), + "deal_id": "1", + "pashub_link": "www.google.co.uk", + "lodgement_status": "lodgement not complete", + "created_at": datetime(2025, 12, 1, 12, 0, 0).isoformat(), + "updated_at": datetime(2025, 12, 1, 12, 30, 0).isoformat(), + } + + expected_output = False + + # act + actual_output: bool = HubspotDealDiffer.check_for_pashub_trigger( + new_deal=new_deal, old_deal=old_deal + ) + + # assert + assert actual_output == expected_output From 9f7448ac438cbfd6ece4f91d556fa58f2798abce Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Wed, 8 Apr 2026 15:35:42 +0000 Subject: [PATCH 08/47] =?UTF-8?q?pashub=20trigger=20true=20if=20pashub=20l?= =?UTF-8?q?ink=20is=20changed=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hubspot_deal_differ.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/backend/hubspot_trigger_orchestrator/hubspot_deal_differ.py b/backend/hubspot_trigger_orchestrator/hubspot_deal_differ.py index 9d66c637..50f3af04 100644 --- a/backend/hubspot_trigger_orchestrator/hubspot_deal_differ.py +++ b/backend/hubspot_trigger_orchestrator/hubspot_deal_differ.py @@ -9,6 +9,23 @@ class HubspotDealDiffer: def check_for_pashub_trigger( new_deal: Dict[str, str], old_deal: HubspotDealData ) -> bool: + """ + Case 1: PasHub Link is updated + Case 2: Coordination is completed (and PasHub Link is populated) + Case 3: Design is completed (and PasHub Link is populated) + Case 4: Lodgement is completed (and PasHub Link is populated) + """ + new_pashub_link: Optional[str] = new_deal["pashub_link"] + # Case 1 + if not new_pashub_link: + return False + + if not old_deal.pashub_link: + return True + + if old_deal.pashub_link != new_pashub_link: + return True + raise NotImplementedError @staticmethod From ad2c979b155840f36a14b239aa1cccaa20e361ca Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Wed, 8 Apr 2026 15:35:52 +0000 Subject: [PATCH 09/47] =?UTF-8?q?pashub=20trigger=20false=20if=20pashub=20?= =?UTF-8?q?link=20not=20set=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/hubspot_trigger_orchestrator/hubspot_deal_differ.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/hubspot_trigger_orchestrator/hubspot_deal_differ.py b/backend/hubspot_trigger_orchestrator/hubspot_deal_differ.py index 50f3af04..ab2b667e 100644 --- a/backend/hubspot_trigger_orchestrator/hubspot_deal_differ.py +++ b/backend/hubspot_trigger_orchestrator/hubspot_deal_differ.py @@ -15,7 +15,7 @@ class HubspotDealDiffer: Case 3: Design is completed (and PasHub Link is populated) Case 4: Lodgement is completed (and PasHub Link is populated) """ - new_pashub_link: Optional[str] = new_deal["pashub_link"] + new_pashub_link: Optional[str] = new_deal.get("pashub_link", "") # Case 1 if not new_pashub_link: return False From 832bcd96e457a71453e1d4d2aa97a73dcba1b243 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Wed, 8 Apr 2026 15:42:29 +0000 Subject: [PATCH 10/47] =?UTF-8?q?pashub=20trigger=20true=20if=20coordinati?= =?UTF-8?q?on=20complete=20and=20pashub=20link=20set=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hubspot_deal_differ.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/backend/hubspot_trigger_orchestrator/hubspot_deal_differ.py b/backend/hubspot_trigger_orchestrator/hubspot_deal_differ.py index ab2b667e..77208432 100644 --- a/backend/hubspot_trigger_orchestrator/hubspot_deal_differ.py +++ b/backend/hubspot_trigger_orchestrator/hubspot_deal_differ.py @@ -1,4 +1,4 @@ -from typing import Dict, Optional +from typing import Dict, List, Optional from backend.app.db.models.organisation import HubspotDealData @@ -15,7 +15,11 @@ class HubspotDealDiffer: Case 3: Design is completed (and PasHub Link is populated) Case 4: Lodgement is completed (and PasHub Link is populated) """ - new_pashub_link: Optional[str] = new_deal.get("pashub_link", "") + new_pashub_link: str = new_deal.get("pashub_link", "") + COORDINATION_COMPLETE: List[str] = [ + "v1 ioe/mtp complete", + "v2 ioe/mtp complete", + ] # Case 1 if not new_pashub_link: return False @@ -26,6 +30,16 @@ class HubspotDealDiffer: if old_deal.pashub_link != new_pashub_link: return True + # Case 2 + new_coordination_status: str = new_deal.get("coordination_status", "") + + if ( + new_coordination_status + and new_coordination_status in COORDINATION_COMPLETE + and new_coordination_status != old_deal.coordination_status + ): + return True + raise NotImplementedError @staticmethod From 0dfd3f5238e969d4d233135c857f2084461b84c5 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Wed, 8 Apr 2026 15:45:41 +0000 Subject: [PATCH 11/47] =?UTF-8?q?pashub=20trigger=20true=20if=20design=20c?= =?UTF-8?q?omplete=20and=20pashub=20link=20set=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hubspot_deal_differ.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/backend/hubspot_trigger_orchestrator/hubspot_deal_differ.py b/backend/hubspot_trigger_orchestrator/hubspot_deal_differ.py index 77208432..ad20aca7 100644 --- a/backend/hubspot_trigger_orchestrator/hubspot_deal_differ.py +++ b/backend/hubspot_trigger_orchestrator/hubspot_deal_differ.py @@ -15,11 +15,14 @@ class HubspotDealDiffer: Case 3: Design is completed (and PasHub Link is populated) Case 4: Lodgement is completed (and PasHub Link is populated) """ - new_pashub_link: str = new_deal.get("pashub_link", "") COORDINATION_COMPLETE: List[str] = [ "v1 ioe/mtp complete", "v2 ioe/mtp complete", ] + RETROFIT_DESIGN_COMPLETE = "uploaded" + + new_pashub_link: str = new_deal.get("pashub_link", "") + # Case 1 if not new_pashub_link: return False @@ -40,6 +43,16 @@ class HubspotDealDiffer: ): return True + # Case 3 + new_design_status: str = new_deal.get("design_status", "") + + if ( + new_design_status + and new_design_status == RETROFIT_DESIGN_COMPLETE + and new_design_status != old_deal.design_status + ): + return True + raise NotImplementedError @staticmethod From 9da0cabb0ffcbb7338d5dd0c9796234202acbb0a Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Wed, 8 Apr 2026 15:52:41 +0000 Subject: [PATCH 12/47] =?UTF-8?q?pashub=20trigger=20true=20if=20lodgement?= =?UTF-8?q?=20complete=20and=20pashub=20link=20set=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hubspot_deal_differ.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/backend/hubspot_trigger_orchestrator/hubspot_deal_differ.py b/backend/hubspot_trigger_orchestrator/hubspot_deal_differ.py index ad20aca7..8f96ce73 100644 --- a/backend/hubspot_trigger_orchestrator/hubspot_deal_differ.py +++ b/backend/hubspot_trigger_orchestrator/hubspot_deal_differ.py @@ -20,6 +20,7 @@ class HubspotDealDiffer: "v2 ioe/mtp complete", ] RETROFIT_DESIGN_COMPLETE = "uploaded" + LODGEMENT_COMPLETE: List[str] = ["lodgement complete", "measures lodged"] new_pashub_link: str = new_deal.get("pashub_link", "") @@ -53,7 +54,17 @@ class HubspotDealDiffer: ): return True - raise NotImplementedError + # Case 4 + new_lodgement_status: str = new_deal.get("lodgement_status", "") + + if ( + new_lodgement_status + and new_lodgement_status in LODGEMENT_COMPLETE + and new_lodgement_status != old_deal.lodgement_status + ): + return True + + return False @staticmethod def check_for_db_update_trigger( From 2d0bc67731239d045c6f7106ba87e6a7b640b2ff Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Wed, 8 Apr 2026 15:59:29 +0000 Subject: [PATCH 13/47] =?UTF-8?q?diff=20checker=20for=20pashub=20trigger?= =?UTF-8?q?=20=F0=9F=9F=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hubspot_deal_differ.py | 133 ++++++++++-------- 1 file changed, 72 insertions(+), 61 deletions(-) diff --git a/backend/hubspot_trigger_orchestrator/hubspot_deal_differ.py b/backend/hubspot_trigger_orchestrator/hubspot_deal_differ.py index 8f96ce73..1dd4ed51 100644 --- a/backend/hubspot_trigger_orchestrator/hubspot_deal_differ.py +++ b/backend/hubspot_trigger_orchestrator/hubspot_deal_differ.py @@ -4,67 +4,12 @@ from backend.app.db.models.organisation import HubspotDealData class HubspotDealDiffer: - - @staticmethod - def check_for_pashub_trigger( - new_deal: Dict[str, str], old_deal: HubspotDealData - ) -> bool: - """ - Case 1: PasHub Link is updated - Case 2: Coordination is completed (and PasHub Link is populated) - Case 3: Design is completed (and PasHub Link is populated) - Case 4: Lodgement is completed (and PasHub Link is populated) - """ - COORDINATION_COMPLETE: List[str] = [ - "v1 ioe/mtp complete", - "v2 ioe/mtp complete", - ] - RETROFIT_DESIGN_COMPLETE = "uploaded" - LODGEMENT_COMPLETE: List[str] = ["lodgement complete", "measures lodged"] - - new_pashub_link: str = new_deal.get("pashub_link", "") - - # Case 1 - if not new_pashub_link: - return False - - if not old_deal.pashub_link: - return True - - if old_deal.pashub_link != new_pashub_link: - return True - - # Case 2 - new_coordination_status: str = new_deal.get("coordination_status", "") - - if ( - new_coordination_status - and new_coordination_status in COORDINATION_COMPLETE - and new_coordination_status != old_deal.coordination_status - ): - return True - - # Case 3 - new_design_status: str = new_deal.get("design_status", "") - - if ( - new_design_status - and new_design_status == RETROFIT_DESIGN_COMPLETE - and new_design_status != old_deal.design_status - ): - return True - - # Case 4 - new_lodgement_status: str = new_deal.get("lodgement_status", "") - - if ( - new_lodgement_status - and new_lodgement_status in LODGEMENT_COMPLETE - and new_lodgement_status != old_deal.lodgement_status - ): - return True - - return False + COORDINATION_COMPLETE: List[str] = [ + "v1 ioe/mtp complete", + "v2 ioe/mtp complete", + ] + RETROFIT_DESIGN_COMPLETE = "uploaded" + LODGEMENT_COMPLETE: List[str] = ["lodgement complete", "measures lodged"] @staticmethod def check_for_db_update_trigger( @@ -74,3 +19,69 @@ class HubspotDealDiffer: old_deal: HubspotDealData, ) -> bool: raise NotImplementedError + + @staticmethod + def check_for_pashub_trigger( + new_deal: Dict[str, str], old_deal: HubspotDealData + ) -> bool: + new_pashub_link: str = new_deal.get("pashub_link", "") + + if not HubspotDealDiffer._has_valid_pashub_link(new_pashub_link): + return False + + if HubspotDealDiffer._new_or_updated_pashub_link(new_pashub_link, old_deal): + return True + + if HubspotDealDiffer._coordination_completed(new_deal, old_deal): + return True + + if HubspotDealDiffer._design_completed(new_deal, old_deal): + return True + + if HubspotDealDiffer._lodgement_completed(new_deal, old_deal): + return True + + return False + + @staticmethod + def _has_valid_pashub_link(new_pashub_link: str) -> bool: + return bool(new_pashub_link) + + @staticmethod + def _new_or_updated_pashub_link( + new_pashub_link: str, old_deal: HubspotDealData + ) -> bool: + if not old_deal.pashub_link: + return True + return old_deal.pashub_link != new_pashub_link + + @staticmethod + def _coordination_completed( + new_deal: Dict[str, str], old_deal: HubspotDealData + ) -> bool: + new_status: str = new_deal.get("coordination_status", "") + return ( + new_status != "" + and new_status in HubspotDealDiffer.COORDINATION_COMPLETE + and new_status != old_deal.coordination_status + ) + + @staticmethod + def _design_completed(new_deal: Dict[str, str], old_deal: HubspotDealData) -> bool: + new_status: str = new_deal.get("design_status", "") + return ( + new_status != "" + and new_status == HubspotDealDiffer.RETROFIT_DESIGN_COMPLETE + and new_status != old_deal.design_status + ) + + @staticmethod + def _lodgement_completed( + new_deal: Dict[str, str], old_deal: HubspotDealData + ) -> bool: + new_status: str = new_deal.get("lodgement_status", "") + return ( + new_status != "" + and new_status in HubspotDealDiffer.LODGEMENT_COMPLETE + and new_status != old_deal.lodgement_status + ) From f719149c03fe1827499fbc66df1facc0ab325676 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Wed, 8 Apr 2026 16:04:30 +0000 Subject: [PATCH 14/47] =?UTF-8?q?replace=20incorrect=20tests=20?= =?UTF-8?q?=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tests/test_hubspot_deal_differ.py | 73 +------------------ 1 file changed, 4 insertions(+), 69 deletions(-) diff --git a/backend/hubspot_trigger_orchestrator/tests/test_hubspot_deal_differ.py b/backend/hubspot_trigger_orchestrator/tests/test_hubspot_deal_differ.py index ddca766a..ba6b80e4 100644 --- a/backend/hubspot_trigger_orchestrator/tests/test_hubspot_deal_differ.py +++ b/backend/hubspot_trigger_orchestrator/tests/test_hubspot_deal_differ.py @@ -165,40 +165,6 @@ def test_pashub_trigger__coordination_completed_and_pashub_link_not_set__returns assert actual_output == expected_output -def test_pashub_trigger__coordination_status_not_completed_and_pashub_link_set__returns_false() -> ( - None -): - # arrange - deal_id = uuid.uuid4() - - old_deal = HubspotDealData( - id=deal_id, - deal_id="1", - pashub_link="www.google.co.uk", - coordination_status="random", - created_at=datetime(2025, 12, 1, 12, 0, 0), - updated_at=datetime(2025, 12, 1, 12, 0, 0), - ) - new_deal: Dict[str, str] = { - "id": str(deal_id), - "deal_id": "1", - "coordination_status": "not complete", - "pashub_link": "www.google.co.uk", - "created_at": datetime(2025, 12, 1, 12, 0, 0).isoformat(), - "updated_at": datetime(2025, 12, 1, 12, 30, 0).isoformat(), - } - - expected_output = False - - # act - actual_output: bool = HubspotDealDiffer.check_for_pashub_trigger( - new_deal=new_deal, old_deal=old_deal - ) - - # assert - assert actual_output == expected_output - - def test_pashub_trigger__design_completed_and_pashub_link_set__returns_true() -> None: # arrange deal_id = uuid.uuid4() @@ -261,39 +227,6 @@ def test_pashub_trigger__design_completed_and_pashub_link_not_set__returns_false assert actual_output == expected_output -def test_pashub_trigger__design_not_completed_and_pashub_link_set__returns_false() -> ( - None -): - # arrange - deal_id = uuid.uuid4() - - old_deal = HubspotDealData( - id=deal_id, - deal_id="1", - pashub_link="www.google.co.uk", - created_at=datetime(2025, 12, 1, 12, 0, 0), - updated_at=datetime(2025, 12, 1, 12, 0, 0), - ) - new_deal: Dict[str, str] = { - "id": str(deal_id), - "deal_id": "1", - "pashub_link": "www.google.co.uk", - "design_status": "not uploaded", - "created_at": datetime(2025, 12, 1, 12, 0, 0).isoformat(), - "updated_at": datetime(2025, 12, 1, 12, 30, 0).isoformat(), - } - - expected_output = False - - # act - actual_output: bool = HubspotDealDiffer.check_for_pashub_trigger( - new_deal=new_deal, old_deal=old_deal - ) - - # assert - assert actual_output == expected_output - - def test_pashub_trigger__lodgement_completed_and_pashub_link_set__returns_true() -> ( None ): @@ -391,7 +324,7 @@ def test_pashub_trigger__lodgement_completed_and_pashub_link_not_set__returns_fa assert actual_output == expected_output -def test_pashub_trigger__lodgement_not_completed_and_pashub_link_set__returns_false() -> ( +def test_pashub_trigger__coordination_design_lodgement_not_completed_and_pashub_link_set__returns_false() -> ( None ): # arrange @@ -408,7 +341,9 @@ def test_pashub_trigger__lodgement_not_completed_and_pashub_link_set__returns_fa "id": str(deal_id), "deal_id": "1", "pashub_link": "www.google.co.uk", - "lodgement_status": "lodgement not complete", + "coordination_status": "not uploaded", + "design_status": "not uploaded", + "lodgement_status": "not uploaded", "created_at": datetime(2025, 12, 1, 12, 0, 0).isoformat(), "updated_at": datetime(2025, 12, 1, 12, 30, 0).isoformat(), } From dd0522713e85456b0b93fbb7a1d114795b1cb56d Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 9 Apr 2026 08:30:28 +0000 Subject: [PATCH 15/47] refactor upsert_deal by introducing helper methods --- etl/hubspot/hubspotDataTodB.py | 383 +++++++++++++++++---------------- 1 file changed, 198 insertions(+), 185 deletions(-) diff --git a/etl/hubspot/hubspotDataTodB.py b/etl/hubspot/hubspotDataTodB.py index e7008618..f0beeee8 100644 --- a/etl/hubspot/hubspotDataTodB.py +++ b/etl/hubspot/hubspotDataTodB.py @@ -366,109 +366,9 @@ class HubspotDataToDb: if existing: print(f"🔄 Updating existing deal (deal_id={deal_id})") + self._update_existing_deal(existing, deal_data, listing, company) - for attr, value in { - "dealname": deal_data.get("dealname"), - "dealstage": deal_data.get("dealstage"), - "listing_id": listing.get("listing_id", None) if listing else None, - "landlord_property_id": ( - listing.get("owner_property_id", None) if listing else None - ), - "uprn": listing.get("national_uprn", None) if listing else None, - "outcome": deal_data.get("outcome"), - "outcome_notes": deal_data.get("outcome_notes"), - "project_code": deal_data.get("project_code"), - "company_id": company, - "major_condition_issue_description": deal_data.get( - "major_condition_issue_description" - ), - "major_condition_issue_photos": deal_data.get( - "major_condition_issue_photos" - ), - "coordination_status": deal_data.get( - "coordination_status__stage_1_" - ), - "design_status": deal_data.get("retrofit_design_status"), - "pashub_link": deal_data.get("pashub_link"), - "sharepoint_link": deal_data.get("sharepoint_link"), - "dampmould_growth": deal_data.get("dampmould_growth"), - "damp_mould_and_repairs_comments": deal_data.get( - "damp_mould_and_repairs_comments" - ), - "pre_sap": deal_data.get("pre_sap"), - "coordinator": deal_data.get("coordinator"), - "mtp_completion_date": self._parse_hs_date( - deal_data.get("mtp_completion_date") - ), - "mtp_re_model_completion_date": self._parse_hs_date( - deal_data.get("mtp_re_model_completion_date") - ), - "ioe_v3_completion_date": self._parse_hs_date( - deal_data.get("ioe_v3_completion_date") - ), - "proposed_measures": deal_data.get("proposed_measures"), - "approved_package": deal_data.get("approved_package"), - "designer": deal_data.get("designer"), - "design_completion_date": self._parse_hs_date( - deal_data.get("design_completion_date") - ), - "actual_measures_installed": deal_data.get( - "actual_measures_installed" - ), - "installer": deal_data.get("installer"), - "installer_handover": deal_data.get("installer_handover"), - "lodgement_status": deal_data.get("lodgement_status"), - "measures_lodgement_date": self._parse_hs_date( - deal_data.get("measures_lodgement_date") - ), - "lodgement_date": self._parse_hs_date( - deal_data.get("lodgement_date") - ), - "expected_commencement_date": self._parse_hs_date( - deal_data.get("expected_commencement_date") - ), - "surveyor": deal_data.get("surveyor"), - "confirmed_survey_date": self._parse_hs_date( - deal_data.get("confirmed_survey_date") - ), - "confirmed_survey_time": deal_data.get("confirmed_survey_time"), - "surveyed_date": self._parse_hs_date( - deal_data.get("surveyed_date") - ), - "design_type": deal_data.get("design_type"), - }.items(): - setattr(existing, attr, value or getattr(existing, attr)) - - # Upload if photo exists but S3 link missing - if ( - existing.major_condition_issue_photos - and not existing.major_condition_issue_evidence_s3_url - ): - # Fetch fresh URL from HubSpot instead of using potentially expired stored URL - fresh_deal = hubspot_client.from_deal_id_get_info(existing.deal_id) - photo_url = fresh_deal.get("major_condition_issue_photos") - - if photo_url: - try: - local_file = hubspot_client.download_file_from_url( - photo_url - ) - s3_url = self.s3.upload_file( - local_file, - "retrofit-data-dev", - prefix="hubspot/awaabs_law_evidence/", - ) - existing.major_condition_issue_evidence_s3_url = s3_url - except Exception as e: - print( - f"⚠️ Failed to download photo for deal_id {existing.deal_id}: {e}" - ) - # Continue without the file — don't crash the update - finally: - if "local_file" in locals() and os.path.exists(local_file): - os.remove(local_file) - else: - print(f"⚠️ Photo URL missing for deal_id {existing.deal_id}") + self._handle_existing_photo_upload(existing, hubspot_client) session.add(existing) session.commit() @@ -477,94 +377,207 @@ class HubspotDataToDb: else: print(f"🆕 Inserting new deal (deal_id={deal_id})") - new_record = HubspotDealData( - deal_id=deal_id, - dealname=deal_data.get("dealname"), - dealstage=deal_data.get("dealstage"), - listing_id=listing.get("listing_id", None) if listing else None, - landlord_property_id=( - listing.get("owner_property_id") if listing else None - ), - uprn=listing.get("national_uprn") if listing else None, - outcome=deal_data.get("outcome"), - outcome_notes=deal_data.get("outcome_notes"), - project_code=deal_data.get("project_code"), - company_id=company, - major_condition_issue_description=deal_data.get( - "major_condition_issue_description" - ), - major_condition_issue_photos=deal_data.get( - "major_condition_issue_photos" - ), - coordination_status=deal_data.get("coordination_status__stage_1_"), - design_status=deal_data.get("retrofit_design_status"), - pashub_link=deal_data.get("pashub_link"), - sharepoint_link=deal_data.get("sharepoint_link"), - dampmould_growth=deal_data.get("dampmould_growth"), - damp_mould_and_repairs_comments=deal_data.get( - "damp_mould_and_repairs_comments" - ), - pre_sap=deal_data.get("pre_sap"), - coordinator=deal_data.get("coordinator"), - mtp_completion_date=self._parse_hs_date( - deal_data.get("mtp_completion_date") - ), - mtp_re_model_completion_date=self._parse_hs_date( - deal_data.get("mtp_re_model_completion_date") - ), - ioe_v3_completion_date=self._parse_hs_date( - deal_data.get("ioe_v3_completion_date") - ), - proposed_measures=deal_data.get("proposed_measures"), - approved_package=deal_data.get("approved_package"), - designer=deal_data.get("designer"), - design_completion_date=self._parse_hs_date( - deal_data.get("design_completion_date") - ), - actual_measures_installed=deal_data.get( - "actual_measures_installed" - ), - installer=deal_data.get("installer"), - installer_handover=deal_data.get("installer_handover"), - lodgement_status=deal_data.get("lodgement_status"), - measures_lodgement_date=self._parse_hs_date( - deal_data.get("measures_lodgement_date") - ), - lodgement_date=self._parse_hs_date(deal_data.get("lodgement_date")), - expected_commencement_date=self._parse_hs_date( - deal_data.get("expected_commencement_date") - ), - surveyor=deal_data.get("surveyor"), - confirmed_survey_date=self._parse_hs_date( - deal_data.get("confirmed_survey_date") - ), - confirmed_survey_time=deal_data.get("confirmed_survey_time"), - surveyed_date=self._parse_hs_date(deal_data.get("surveyed_date")), - design_type=deal_data.get("design_type"), + new_record: HubspotDealData = self._build_new_deal( + deal_id, deal_data, listing, company ) # Handle upload at insert time - if new_record.major_condition_issue_photos: - try: - local_file = hubspot_client.download_file_from_url( - new_record.major_condition_issue_photos - ) - s3_url = self.s3.upload_file( - local_file, - "retrofit-data-dev", - prefix="hubspot/awaabs_law_evidence/", - ) - new_record.major_condition_issue_evidence_s3_url = s3_url - except Exception as e: - print( - f"⚠️ Failed to download photo for deal_id {new_record.deal_id}: {e}" - ) - # Continue without the file — don't crash the insert - finally: - if "local_file" in locals() and os.path.exists(local_file): - os.remove(local_file) + self._handle_new_photo_upload(new_record, hubspot_client) session.add(new_record) session.commit() session.refresh(new_record) return new_record + + def _update_existing_deal( + self, + existing: HubspotDealData, + deal_data: Dict[str, str], + listing: Optional[dict[str, str]], + company: Optional[str], + ): + for attr, value in { + "dealname": deal_data.get("dealname"), + "dealstage": deal_data.get("dealstage"), + "listing_id": listing.get("listing_id", None) if listing else None, + "landlord_property_id": ( + listing.get("owner_property_id", None) if listing else None + ), + "uprn": listing.get("national_uprn", None) if listing else None, + "outcome": deal_data.get("outcome"), + "outcome_notes": deal_data.get("outcome_notes"), + "project_code": deal_data.get("project_code"), + "company_id": company, + "major_condition_issue_description": deal_data.get( + "major_condition_issue_description" + ), + "major_condition_issue_photos": deal_data.get( + "major_condition_issue_photos" + ), + "coordination_status": deal_data.get("coordination_status__stage_1_"), + "design_status": deal_data.get("retrofit_design_status"), + "pashub_link": deal_data.get("pashub_link"), + "sharepoint_link": deal_data.get("sharepoint_link"), + "dampmould_growth": deal_data.get("dampmould_growth"), + "damp_mould_and_repairs_comments": deal_data.get( + "damp_mould_and_repairs_comments" + ), + "pre_sap": deal_data.get("pre_sap"), + "coordinator": deal_data.get("coordinator"), + "mtp_completion_date": self._parse_hs_date( + deal_data.get("mtp_completion_date") + ), + "mtp_re_model_completion_date": self._parse_hs_date( + deal_data.get("mtp_re_model_completion_date") + ), + "ioe_v3_completion_date": self._parse_hs_date( + deal_data.get("ioe_v3_completion_date") + ), + "proposed_measures": deal_data.get("proposed_measures"), + "approved_package": deal_data.get("approved_package"), + "designer": deal_data.get("designer"), + "design_completion_date": self._parse_hs_date( + deal_data.get("design_completion_date") + ), + "actual_measures_installed": deal_data.get("actual_measures_installed"), + "installer": deal_data.get("installer"), + "installer_handover": deal_data.get("installer_handover"), + "lodgement_status": deal_data.get("lodgement_status"), + "measures_lodgement_date": self._parse_hs_date( + deal_data.get("measures_lodgement_date") + ), + "lodgement_date": self._parse_hs_date(deal_data.get("lodgement_date")), + "expected_commencement_date": self._parse_hs_date( + deal_data.get("expected_commencement_date") + ), + "surveyor": deal_data.get("surveyor"), + "confirmed_survey_date": self._parse_hs_date( + deal_data.get("confirmed_survey_date") + ), + "confirmed_survey_time": deal_data.get("confirmed_survey_time"), + "surveyed_date": self._parse_hs_date(deal_data.get("surveyed_date")), + "design_type": deal_data.get("design_type"), + }.items(): + setattr(existing, attr, value or getattr(existing, attr)) + + def _build_new_deal( + self, + deal_id: str, + deal_data: Dict[str, str], + listing: Optional[dict[str, str]], + company: Optional[str], + ) -> HubspotDealData: + return HubspotDealData( + deal_id=deal_id, + dealname=deal_data.get("dealname"), + dealstage=deal_data.get("dealstage"), + listing_id=listing.get("listing_id") if listing else None, + landlord_property_id=( + listing.get("owner_property_id") if listing else None + ), + uprn=listing.get("national_uprn") if listing else None, + outcome=deal_data.get("outcome"), + outcome_notes=deal_data.get("outcome_notes"), + project_code=deal_data.get("project_code"), + company_id=company, + major_condition_issue_description=deal_data.get( + "major_condition_issue_description" + ), + major_condition_issue_photos=deal_data.get("major_condition_issue_photos"), + coordination_status=deal_data.get("coordination_status__stage_1_"), + design_status=deal_data.get("retrofit_design_status"), + pashub_link=deal_data.get("pashub_link"), + sharepoint_link=deal_data.get("sharepoint_link"), + dampmould_growth=deal_data.get("dampmould_growth"), + damp_mould_and_repairs_comments=deal_data.get( + "damp_mould_and_repairs_comments" + ), + pre_sap=deal_data.get("pre_sap"), + coordinator=deal_data.get("coordinator"), + mtp_completion_date=self._parse_hs_date( + deal_data.get("mtp_completion_date") + ), + mtp_re_model_completion_date=self._parse_hs_date( + deal_data.get("mtp_re_model_completion_date") + ), + ioe_v3_completion_date=self._parse_hs_date( + deal_data.get("ioe_v3_completion_date") + ), + proposed_measures=deal_data.get("proposed_measures"), + approved_package=deal_data.get("approved_package"), + designer=deal_data.get("designer"), + design_completion_date=self._parse_hs_date( + deal_data.get("design_completion_date") + ), + actual_measures_installed=deal_data.get("actual_measures_installed"), + installer=deal_data.get("installer"), + installer_handover=deal_data.get("installer_handover"), + lodgement_status=deal_data.get("lodgement_status"), + measures_lodgement_date=self._parse_hs_date( + deal_data.get("measures_lodgement_date") + ), + lodgement_date=self._parse_hs_date(deal_data.get("lodgement_date")), + expected_commencement_date=self._parse_hs_date( + deal_data.get("expected_commencement_date") + ), + surveyor=deal_data.get("surveyor"), + confirmed_survey_date=self._parse_hs_date( + deal_data.get("confirmed_survey_date") + ), + confirmed_survey_time=deal_data.get("confirmed_survey_time"), + surveyed_date=self._parse_hs_date(deal_data.get("surveyed_date")), + design_type=deal_data.get("design_type"), + ) + + def _handle_existing_photo_upload( + self, + existing: HubspotDealData, + hubspot_client: HubspotClient, + ): + if ( + existing.major_condition_issue_photos + and not existing.major_condition_issue_evidence_s3_url + ): + fresh_deal = hubspot_client.from_deal_id_get_info(existing.deal_id) + photo_url = fresh_deal.get("major_condition_issue_photos") + + if not photo_url: + print(f"⚠️ Photo URL missing for deal_id {existing.deal_id}") + return + + self._upload_photo_to_s3(existing, photo_url, hubspot_client) + + def _handle_new_photo_upload( + self, + record: HubspotDealData, + hubspot_client: HubspotClient, + ): + if record.major_condition_issue_photos: + self._upload_photo_to_s3( + record, + record.major_condition_issue_photos, + hubspot_client, + ) + + def _upload_photo_to_s3( + self, + record: HubspotDealData, + photo_url: str, + hubspot_client: HubspotClient, + ): + try: + local_file = hubspot_client.download_file_from_url(photo_url) + + s3_url = self.s3.upload_file( + local_file, + "retrofit-data-dev", + prefix="hubspot/awaabs_law_evidence/", + ) + + record.major_condition_issue_evidence_s3_url = s3_url + + except Exception as e: + print(f"⚠️ Failed to upload photo for deal_id {record.deal_id}: {e}") + finally: + if "local_file" in locals() and os.path.exists(local_file): + os.remove(local_file) From 8ce76190442e5a7f8a9c1af038651521b7edb105 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 9 Apr 2026 09:14:20 +0000 Subject: [PATCH 16/47] refactor pashub trigger tests --- .../tests/test_hubspot_deal_differ.py | 470 ++++++++---------- etl/hubspot/hubspotDataTodB.py | 72 ++- 2 files changed, 233 insertions(+), 309 deletions(-) diff --git a/backend/hubspot_trigger_orchestrator/tests/test_hubspot_deal_differ.py b/backend/hubspot_trigger_orchestrator/tests/test_hubspot_deal_differ.py index ba6b80e4..75fa7927 100644 --- a/backend/hubspot_trigger_orchestrator/tests/test_hubspot_deal_differ.py +++ b/backend/hubspot_trigger_orchestrator/tests/test_hubspot_deal_differ.py @@ -1,359 +1,295 @@ from datetime import datetime -from typing import Dict +from typing import Any, Dict import uuid +import pytest + from backend.app.db.models.organisation import HubspotDealData from backend.hubspot_trigger_orchestrator.hubspot_deal_differ import HubspotDealDiffer -def test_pashub_trigger__outcome_note_added__returns_false() -> None: - # arrange - deal_id = uuid.uuid4() +BASE_TIME = datetime(2025, 12, 1, 12, 0, 0) - old_deal = HubspotDealData( - id=deal_id, + +def make_old_deal(**overrides: Any) -> HubspotDealData: + return HubspotDealData( + id=overrides.get("id", uuid.uuid4()), deal_id="1", - created_at=datetime(2025, 12, 1, 12, 0, 0), - updated_at=datetime(2025, 12, 1, 12, 0, 0), + created_at=BASE_TIME, + updated_at=BASE_TIME, + **{k: v for k, v in overrides.items() if k != "id"}, ) - new_deal: Dict[str, str] = { + + +def make_new_deal(deal_id: uuid.UUID, **overrides: Any) -> Dict[str, str]: + return { "id": str(deal_id), "deal_id": "1", - "outcome_notes": "test note", - "created_at": datetime(2025, 12, 1, 12, 0, 0).isoformat(), + "created_at": BASE_TIME.isoformat(), "updated_at": datetime(2025, 12, 1, 12, 30, 0).isoformat(), + **overrides, } - expected_output = False - # act - actual_output: bool = HubspotDealDiffer.check_for_pashub_trigger( - new_deal=new_deal, old_deal=old_deal +# ------------------------------------- +# Random change we aren't interested in +# ------------------------------------- + + +@pytest.mark.parametrize( + "new_overrides,expected", + [ + ({"outcome_notes": "test note"}, False), + ], +) +def test_pashub_trigger__outcome_note_added__returns_false( + new_overrides: Dict[str, str], + expected: bool, +) -> None: + deal_id = uuid.uuid4() + old_deal = make_old_deal(id=deal_id) + new_deal = make_new_deal(deal_id, **new_overrides) + + assert ( + HubspotDealDiffer.check_for_pashub_trigger( + new_deal=new_deal, + old_deal=old_deal, + ) + == expected ) - # assert - assert actual_output == expected_output + +# ------------------------- +# Pashub link changes +# ------------------------- -def test_pashub_trigger__pashub_link_changed__returns_true() -> None: - # arrange +@pytest.mark.parametrize( + "old_overrides,new_overrides,expected", + [ + ( + {"pashub_link": "www.google.co.uk"}, + {"pashub_link": "www.bbc.co.uk"}, + True, + ), + ], +) +def test_pashub_trigger__pashub_link_changed__returns_true( + old_overrides: Dict[str, str], + new_overrides: Dict[str, str], + expected: bool, +) -> None: + deal_id = uuid.uuid4() + old_deal = make_old_deal(id=deal_id, **old_overrides) + new_deal = make_new_deal(deal_id, **new_overrides) + + assert ( + HubspotDealDiffer.check_for_pashub_trigger( + new_deal=new_deal, + old_deal=old_deal, + ) + == expected + ) + + +# ------------------------- +# Coordination +# ------------------------- + + +@pytest.mark.parametrize( + "coordination_status,expected", + [ + ("v1 ioe/mtp complete", True), + ("v2 ioe/mtp complete", True), + ], +) +def test_pashub_trigger__coordination_completed_and_pashub_link_set__returns_true( + coordination_status: str, + expected: bool, +) -> None: deal_id = uuid.uuid4() - old_deal = HubspotDealData( + old_deal = make_old_deal( id=deal_id, - deal_id="1", - pashub_link="www.google.co.uk", - created_at=datetime(2025, 12, 1, 12, 0, 0), - updated_at=datetime(2025, 12, 1, 12, 0, 0), - ) - new_deal: Dict[str, str] = { - "id": str(deal_id), - "deal_id": "1", - "pashub_link": "www.bbc.co.uk", - "created_at": datetime(2025, 12, 1, 12, 0, 0).isoformat(), - "updated_at": datetime(2025, 12, 1, 12, 30, 0).isoformat(), - } - - expected_output = True - - # act - actual_output: bool = HubspotDealDiffer.check_for_pashub_trigger( - new_deal=new_deal, old_deal=old_deal - ) - - # assert - assert actual_output == expected_output - - -def test_pashub_trigger__coordination_completed_and_pashub_link_set__returns_true() -> ( - None -): - # arrange - deal_id = uuid.uuid4() - - old_deal = HubspotDealData( - id=deal_id, - deal_id="1", pashub_link="www.google.co.uk", coordination_status="random", - created_at=datetime(2025, 12, 1, 12, 0, 0), - updated_at=datetime(2025, 12, 1, 12, 0, 0), - ) - new_deal: Dict[str, str] = { - "id": str(deal_id), - "deal_id": "1", - "coordination_status": "v1 ioe/mtp complete", - "pashub_link": "www.google.co.uk", - "created_at": datetime(2025, 12, 1, 12, 0, 0).isoformat(), - "updated_at": datetime(2025, 12, 1, 12, 30, 0).isoformat(), - } - - expected_output = True - - # act - actual_output: bool = HubspotDealDiffer.check_for_pashub_trigger( - new_deal=new_deal, old_deal=old_deal ) - # assert - assert actual_output == expected_output - - -def test_pashub_trigger__coordination_completed_and_pashub_link_set__returns_true_2() -> ( - None -): - # arrange - deal_id = uuid.uuid4() - - old_deal = HubspotDealData( - id=deal_id, - deal_id="1", + new_deal = make_new_deal( + deal_id, pashub_link="www.google.co.uk", - coordination_status="random", - created_at=datetime(2025, 12, 1, 12, 0, 0), - updated_at=datetime(2025, 12, 1, 12, 0, 0), - ) - new_deal: Dict[str, str] = { - "id": str(deal_id), - "deal_id": "1", - "coordination_status": "v2 ioe/mtp complete", - "pashub_link": "www.google.co.uk", - "created_at": datetime(2025, 12, 1, 12, 0, 0).isoformat(), - "updated_at": datetime(2025, 12, 1, 12, 30, 0).isoformat(), - } - - expected_output = True - - # act - actual_output: bool = HubspotDealDiffer.check_for_pashub_trigger( - new_deal=new_deal, old_deal=old_deal + coordination_status=coordination_status, ) - # assert - assert actual_output == expected_output + assert ( + HubspotDealDiffer.check_for_pashub_trigger( + new_deal=new_deal, + old_deal=old_deal, + ) + == expected + ) def test_pashub_trigger__coordination_completed_and_pashub_link_not_set__returns_false() -> ( None ): - # arrange deal_id = uuid.uuid4() - old_deal = HubspotDealData( + old_deal = make_old_deal( id=deal_id, - deal_id="1", coordination_status="random", - created_at=datetime(2025, 12, 1, 12, 0, 0), - updated_at=datetime(2025, 12, 1, 12, 0, 0), - ) - new_deal: Dict[str, str] = { - "id": str(deal_id), - "deal_id": "1", - "coordination_status": "v2 ioe/mtp complete", - "created_at": datetime(2025, 12, 1, 12, 0, 0).isoformat(), - "updated_at": datetime(2025, 12, 1, 12, 30, 0).isoformat(), - } - - expected_output = False - - # act - actual_output: bool = HubspotDealDiffer.check_for_pashub_trigger( - new_deal=new_deal, old_deal=old_deal ) - # assert - assert actual_output == expected_output + new_deal = make_new_deal( + deal_id, + coordination_status="v2 ioe/mtp complete", + ) + + assert ( + HubspotDealDiffer.check_for_pashub_trigger( + new_deal=new_deal, + old_deal=old_deal, + ) + is False + ) + + +# ------------------------- +# Design +# ------------------------- def test_pashub_trigger__design_completed_and_pashub_link_set__returns_true() -> None: - # arrange deal_id = uuid.uuid4() - old_deal = HubspotDealData( + old_deal = make_old_deal( id=deal_id, - deal_id="1", pashub_link="www.google.co.uk", - created_at=datetime(2025, 12, 1, 12, 0, 0), - updated_at=datetime(2025, 12, 1, 12, 0, 0), - ) - new_deal: Dict[str, str] = { - "id": str(deal_id), - "deal_id": "1", - "pashub_link": "www.google.co.uk", - "design_status": "uploaded", - "created_at": datetime(2025, 12, 1, 12, 0, 0).isoformat(), - "updated_at": datetime(2025, 12, 1, 12, 30, 0).isoformat(), - } - - expected_output = True - - # act - actual_output: bool = HubspotDealDiffer.check_for_pashub_trigger( - new_deal=new_deal, old_deal=old_deal ) - # assert - assert actual_output == expected_output + new_deal = make_new_deal( + deal_id, + pashub_link="www.google.co.uk", + design_status="uploaded", + ) + + assert ( + HubspotDealDiffer.check_for_pashub_trigger( + new_deal=new_deal, + old_deal=old_deal, + ) + is True + ) def test_pashub_trigger__design_completed_and_pashub_link_not_set__returns_false() -> ( None ): - # arrange deal_id = uuid.uuid4() - old_deal = HubspotDealData( - id=deal_id, - deal_id="1", - created_at=datetime(2025, 12, 1, 12, 0, 0), - updated_at=datetime(2025, 12, 1, 12, 0, 0), - ) - new_deal: Dict[str, str] = { - "id": str(deal_id), - "deal_id": "1", - "design_status": "uploaded", - "created_at": datetime(2025, 12, 1, 12, 0, 0).isoformat(), - "updated_at": datetime(2025, 12, 1, 12, 30, 0).isoformat(), - } + old_deal = make_old_deal(id=deal_id) - expected_output = False - - # act - actual_output: bool = HubspotDealDiffer.check_for_pashub_trigger( - new_deal=new_deal, old_deal=old_deal + new_deal = make_new_deal( + deal_id, + design_status="uploaded", ) - # assert - assert actual_output == expected_output + assert ( + HubspotDealDiffer.check_for_pashub_trigger( + new_deal=new_deal, + old_deal=old_deal, + ) + is False + ) -def test_pashub_trigger__lodgement_completed_and_pashub_link_set__returns_true() -> ( - None -): - # arrange +# ------------------------- +# Lodgement +# ------------------------- + + +@pytest.mark.parametrize( + "lodgement_status,expected", + [ + ("lodgement complete", True), + ("measures lodged", True), + ], +) +def test_pashub_trigger__lodgement_completed_and_pashub_link_set__returns_true( + lodgement_status: str, + expected: bool, +) -> None: deal_id = uuid.uuid4() - old_deal = HubspotDealData( + old_deal = make_old_deal( id=deal_id, - deal_id="1", pashub_link="www.google.co.uk", - created_at=datetime(2025, 12, 1, 12, 0, 0), - updated_at=datetime(2025, 12, 1, 12, 0, 0), - ) - new_deal: Dict[str, str] = { - "id": str(deal_id), - "deal_id": "1", - "pashub_link": "www.google.co.uk", - "lodgement_status": "lodgement complete", - "created_at": datetime(2025, 12, 1, 12, 0, 0).isoformat(), - "updated_at": datetime(2025, 12, 1, 12, 30, 0).isoformat(), - } - - expected_output = True - - # act - actual_output: bool = HubspotDealDiffer.check_for_pashub_trigger( - new_deal=new_deal, old_deal=old_deal ) - # assert - assert actual_output == expected_output - - -def test_pashub_trigger__lodgement_completed_and_pashub_link_set__returns_true_2() -> ( - None -): - # arrange - deal_id = uuid.uuid4() - - old_deal = HubspotDealData( - id=deal_id, - deal_id="1", + new_deal = make_new_deal( + deal_id, pashub_link="www.google.co.uk", - created_at=datetime(2025, 12, 1, 12, 0, 0), - updated_at=datetime(2025, 12, 1, 12, 0, 0), - ) - new_deal: Dict[str, str] = { - "id": str(deal_id), - "deal_id": "1", - "pashub_link": "www.google.co.uk", - "lodgement_status": "measures lodged", - "created_at": datetime(2025, 12, 1, 12, 0, 0).isoformat(), - "updated_at": datetime(2025, 12, 1, 12, 30, 0).isoformat(), - } - - expected_output = True - - # act - actual_output: bool = HubspotDealDiffer.check_for_pashub_trigger( - new_deal=new_deal, old_deal=old_deal + lodgement_status=lodgement_status, ) - # assert - assert actual_output == expected_output + assert ( + HubspotDealDiffer.check_for_pashub_trigger( + new_deal=new_deal, + old_deal=old_deal, + ) + == expected + ) def test_pashub_trigger__lodgement_completed_and_pashub_link_not_set__returns_false() -> ( None ): - # arrange deal_id = uuid.uuid4() - old_deal = HubspotDealData( - id=deal_id, - deal_id="1", - created_at=datetime(2025, 12, 1, 12, 0, 0), - updated_at=datetime(2025, 12, 1, 12, 0, 0), - ) - new_deal: Dict[str, str] = { - "id": str(deal_id), - "deal_id": "1", - "design_status": "lodgement complete", - "created_at": datetime(2025, 12, 1, 12, 0, 0).isoformat(), - "updated_at": datetime(2025, 12, 1, 12, 30, 0).isoformat(), - } + old_deal = make_old_deal(id=deal_id) - expected_output = False - - # act - actual_output: bool = HubspotDealDiffer.check_for_pashub_trigger( - new_deal=new_deal, old_deal=old_deal + new_deal = make_new_deal( + deal_id, + design_status="lodgement complete", ) - # assert - assert actual_output == expected_output + assert ( + HubspotDealDiffer.check_for_pashub_trigger( + new_deal=new_deal, + old_deal=old_deal, + ) + is False + ) + + +# ------------------------- +# Negative case +# ------------------------- def test_pashub_trigger__coordination_design_lodgement_not_completed_and_pashub_link_set__returns_false() -> ( None ): - # arrange deal_id = uuid.uuid4() - old_deal = HubspotDealData( + old_deal = make_old_deal( id=deal_id, - deal_id="1", pashub_link="www.google.co.uk", - created_at=datetime(2025, 12, 1, 12, 0, 0), - updated_at=datetime(2025, 12, 1, 12, 0, 0), - ) - new_deal: Dict[str, str] = { - "id": str(deal_id), - "deal_id": "1", - "pashub_link": "www.google.co.uk", - "coordination_status": "not uploaded", - "design_status": "not uploaded", - "lodgement_status": "not uploaded", - "created_at": datetime(2025, 12, 1, 12, 0, 0).isoformat(), - "updated_at": datetime(2025, 12, 1, 12, 30, 0).isoformat(), - } - - expected_output = False - - # act - actual_output: bool = HubspotDealDiffer.check_for_pashub_trigger( - new_deal=new_deal, old_deal=old_deal ) - # assert - assert actual_output == expected_output + new_deal = make_new_deal( + deal_id, + pashub_link="www.google.co.uk", + coordination_status="not uploaded", + design_status="not uploaded", + lodgement_status="not uploaded", + ) + + assert ( + HubspotDealDiffer.check_for_pashub_trigger( + new_deal=new_deal, + old_deal=old_deal, + ) + is False + ) diff --git a/etl/hubspot/hubspotDataTodB.py b/etl/hubspot/hubspotDataTodB.py index f0beeee8..06cc3be9 100644 --- a/etl/hubspot/hubspotDataTodB.py +++ b/etl/hubspot/hubspotDataTodB.py @@ -291,54 +291,33 @@ class HubspotDataToDb: return False # Handle photo upload if it exists but S3 URL is missing - if ( - deal_in_db.major_condition_issue_photos - and not deal_in_db.major_condition_issue_evidence_s3_url - ): + if self._needs_photo_upload(deal_in_db): print( f"🖼️ Found photo for deal_id {deal_in_db.deal_id} — uploading to S3..." ) photo_url = hs_deal.get("major_condition_issue_photos") + if photo_url: - try: - # Download from HubSpot using fresh URL from hs_deal (not stale DB URL) - local_file = hubspot_client.download_file_from_url(photo_url) + self._upload_photo_to_s3( + deal_in_db, + photo_url, + hubspot_client, + verify=True, # 👈 key difference + ) - # Upload to S3 - bucket = "retrofit-data-dev" - s3_url = self.s3.upload_file( - local_file, bucket, prefix="hubspot/awaabs_law_evidence/" + # persist change + with db_read_session() as session: + db_record = session.get(HubspotDealData, deal_in_db.id) + db_record.major_condition_issue_evidence_s3_url = ( + deal_in_db.major_condition_issue_evidence_s3_url ) + session.add(db_record) + session.commit() - # Download again to verify integrity - downloaded = self.s3.download_from_url(s3_url) - if self._sha256(local_file) == self._sha256(downloaded): - print("✅ SHA256 match verified — upload successful.") - else: - print("❌ SHA256 mismatch — integrity check failed.") - raise ValueError("File integrity check failed after S3 upload.") - - # Update DB record with S3 URL - with db_read_session() as session: - db_record = session.get(HubspotDealData, deal_in_db.id) - db_record.major_condition_issue_evidence_s3_url = s3_url - session.add(db_record) - session.commit() - print( - f"✅ Updated DB with S3 URL for deal_id={deal_in_db.deal_id}" - ) - return False - except Exception as e: - print( - f"⚠️ Failed to download/upload photo for deal_id {deal_in_db.deal_id}: {e}" - ) - # Continue without the file — don't crash the entire update - finally: - if "local_file" in locals() and os.path.exists(local_file): - os.remove(local_file) + return False else: - print(f"⚠️ Photo URL missing for deal_id {deal_in_db.deal_id}") + print(f"⚠️ Photo URL missing for deal_id {deal_in_db.deal_id}") else: print(f"✅ No update or upload required for deal_id {deal_in_db.deal_id}.") @@ -534,10 +513,7 @@ class HubspotDataToDb: existing: HubspotDealData, hubspot_client: HubspotClient, ): - if ( - existing.major_condition_issue_photos - and not existing.major_condition_issue_evidence_s3_url - ): + if self._needs_photo_upload(existing): fresh_deal = hubspot_client.from_deal_id_get_info(existing.deal_id) photo_url = fresh_deal.get("major_condition_issue_photos") @@ -564,6 +540,7 @@ class HubspotDataToDb: record: HubspotDealData, photo_url: str, hubspot_client: HubspotClient, + verify: bool = False, ): try: local_file = hubspot_client.download_file_from_url(photo_url) @@ -574,6 +551,11 @@ class HubspotDataToDb: prefix="hubspot/awaabs_law_evidence/", ) + if verify: + downloaded = self.s3.download_from_url(s3_url) + if self._sha256(local_file) != self._sha256(downloaded): + raise ValueError("File integrity check failed after S3 upload.") + record.major_condition_issue_evidence_s3_url = s3_url except Exception as e: @@ -581,3 +563,9 @@ class HubspotDataToDb: finally: if "local_file" in locals() and os.path.exists(local_file): os.remove(local_file) + + def _needs_photo_upload(self, deal: HubspotDealData) -> bool: + return bool( + deal.major_condition_issue_photos + and not deal.major_condition_issue_evidence_s3_url + ) From c439d5f55794f8b1c8f9041d568f28d44f43d0fd Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 9 Apr 2026 09:41:43 +0000 Subject: [PATCH 17/47] move everything to etl/hubspot/ --- .../hubspot_trigger_orchestrator/handler.py | 61 -------------- .../hubspot}/hubspot_deal_differ.py | 0 ...ot_trigger_orchestrator_trigger_request.py | 0 etl/hubspot/scripts/scraper/main.py | 80 +++++++++++++------ .../tests/test_hubspot_deal_differ.py | 2 +- 5 files changed, 57 insertions(+), 86 deletions(-) delete mode 100644 backend/hubspot_trigger_orchestrator/handler.py rename {backend/hubspot_trigger_orchestrator => etl/hubspot}/hubspot_deal_differ.py (100%) rename {backend/hubspot_trigger_orchestrator => etl/hubspot}/hubspot_trigger_orchestrator_trigger_request.py (100%) rename {backend/hubspot_trigger_orchestrator => etl/hubspot}/tests/test_hubspot_deal_differ.py (98%) diff --git a/backend/hubspot_trigger_orchestrator/handler.py b/backend/hubspot_trigger_orchestrator/handler.py deleted file mode 100644 index 38724812..00000000 --- a/backend/hubspot_trigger_orchestrator/handler.py +++ /dev/null @@ -1,61 +0,0 @@ -import json -from typing import Any, Dict, Mapping, Optional - -from backend.app.db.models.organisation import HubspotDealData -from backend.hubspot_trigger_orchestrator.hubspot_deal_differ import HubspotDealDiffer -from backend.hubspot_trigger_orchestrator.hubspot_trigger_orchestrator_trigger_request import ( - HubspotTriggerOrchestratorTriggerRequest, -) -from backend.utils.subtasks import task_handler -from etl.hubspot.hubspotClient import HubspotClient -from etl.hubspot.hubspotDataTodB import HubspotDataToDb -from utils.logger import setup_logger - -logger = setup_logger() - - -@task_handler() -def handler(event: Mapping[str, Any], context: Any) -> None: - - db_client = HubspotDataToDb() - hubspot_client = HubspotClient() - - for record in event.get("Records", []): - body_dict = json.loads(record["body"]) - - logger.debug("Validating request body") - payload = HubspotTriggerOrchestratorTriggerRequest.model_validate(body_dict) - logger.debug("Successfully validated request body") - - hubspot_deal_id: str = payload.hubspot_deal_id - - db_deal: Optional[HubspotDealData] = db_client.find_deal_with_deal_id( - hubspot_deal_id - ) - if not db_deal: - # new hubspot deal, no diffing to do - # TODO: trigger hubspot to db ETL - return - - hubspot_deal: Dict[str, str] - company: Optional[str] - listing: Optional[dict[str, str]] - - hubspot_deal, company, listing = ( - hubspot_client.get_deal_and_company_and_listing(hubspot_deal_id) - ) - - if HubspotDealDiffer.check_for_pashub_trigger( - new_deal=hubspot_deal, old_deal=db_deal - ): - # TODO: trigger pashub file fetcher - return - - if HubspotDealDiffer.check_for_db_update_trigger( - new_deal=hubspot_deal, - new_company=company, - new_listing=listing, - old_deal=db_deal, - ): - # TODO: trigger db upsert - return diff --git a/backend/hubspot_trigger_orchestrator/hubspot_deal_differ.py b/etl/hubspot/hubspot_deal_differ.py similarity index 100% rename from backend/hubspot_trigger_orchestrator/hubspot_deal_differ.py rename to etl/hubspot/hubspot_deal_differ.py diff --git a/backend/hubspot_trigger_orchestrator/hubspot_trigger_orchestrator_trigger_request.py b/etl/hubspot/hubspot_trigger_orchestrator_trigger_request.py similarity index 100% rename from backend/hubspot_trigger_orchestrator/hubspot_trigger_orchestrator_trigger_request.py rename to etl/hubspot/hubspot_trigger_orchestrator_trigger_request.py diff --git a/etl/hubspot/scripts/scraper/main.py b/etl/hubspot/scripts/scraper/main.py index d8d4a357..8c4af1a7 100644 --- a/etl/hubspot/scripts/scraper/main.py +++ b/etl/hubspot/scripts/scraper/main.py @@ -1,39 +1,71 @@ from backend.app.db.models.organisation import HubspotDealData from etl.hubspot.hubspotClient import HubspotClient -from etl.hubspot.hubspotDataTodB import CompanyData, HubspotDataToDb + +# from etl.hubspot.hubspotDataTodB import CompanyData, HubspotDataToDb +from etl.hubspot.hubspotDataTodB import HubspotDataToDb from backend.utils.subtasks import task_handler from typing import Any, Dict, Optional +from etl.hubspot.hubspot_deal_differ import HubspotDealDiffer +from etl.hubspot.hubspot_trigger_orchestrator_trigger_request import ( + HubspotTriggerOrchestratorTriggerRequest, +) + @task_handler() def handler(body: dict[str, Any], context: Any) -> None: - hubspot_deal_id = body.get("hubspot_deal_id", "") - - if hubspot_deal_id == "": - raise RuntimeError( - "Missing Hubspot Deal ID in SQS body request, 'hubspot_deal_id'" - ) - hubspot_deal_id = "327170793707" - - hubspot_client = HubspotClient() db_client = HubspotDataToDb() + hubspot_client = HubspotClient() + + payload = HubspotTriggerOrchestratorTriggerRequest.model_validate(body) + hubspot_deal_id: str = payload.hubspot_deal_id + db_deal: Optional[HubspotDealData] = db_client.find_deal_with_deal_id( hubspot_deal_id ) - if db_deal: - db_client.update_deal_with_checks(db_deal, hubspot_client) - else: - hubspot_deal: Dict[str, str] - company: Optional[str] - listing: Optional[dict[str, str]] - hubspot_deal, company, listing = ( - hubspot_client.get_deal_and_company_and_listing(hubspot_deal_id) - ) + if not db_deal: + # New hubspot deal, no diffing to do + # TODO: Trigger hubspot to db ETL + return - if company: - company_data: CompanyData = hubspot_client.get_company_information(company) - db_client: HubspotDataToDb = HubspotDataToDb() - db_client.upsert_company(company_data) + hubspot_deal: Dict[str, str] + company: Optional[str] + listing: Optional[dict[str, str]] - db_client.upsert_deal(hubspot_deal, company, listing, hubspot_client) + hubspot_deal, company, listing = hubspot_client.get_deal_and_company_and_listing( + hubspot_deal_id + ) + + if HubspotDealDiffer.check_for_pashub_trigger( + new_deal=hubspot_deal, old_deal=db_deal + ): + # TODO: trigger pashub file fetcher + return + + if HubspotDealDiffer.check_for_db_update_trigger( + new_deal=hubspot_deal, + new_company=company, + new_listing=listing, + old_deal=db_deal, + ): + # TODO: trigger db upsert + return + + # if db_deal: + # db_client.update_deal_with_checks(db_deal, hubspot_client) + # else: + # hubspot_deal: Dict[str, str] + # company: Optional[str] + # listing: Optional[dict[str, str]] + + # hubspot_deal, company, listing = ( + # hubspot_client.get_deal_and_company_and_listing(hubspot_deal_id) + # ) + + # if company: + # company_data: CompanyData = hubspot_client.get_company_information(company) + # db_client: HubspotDataToDb = HubspotDataToDb() + # db_client.upsert_company(company_data) + + # db_client.upsert_deal(hubspot_deal, company, listing, hubspot_client) diff --git a/backend/hubspot_trigger_orchestrator/tests/test_hubspot_deal_differ.py b/etl/hubspot/tests/test_hubspot_deal_differ.py similarity index 98% rename from backend/hubspot_trigger_orchestrator/tests/test_hubspot_deal_differ.py rename to etl/hubspot/tests/test_hubspot_deal_differ.py index 75fa7927..12c5a288 100644 --- a/backend/hubspot_trigger_orchestrator/tests/test_hubspot_deal_differ.py +++ b/etl/hubspot/tests/test_hubspot_deal_differ.py @@ -5,7 +5,7 @@ import uuid import pytest from backend.app.db.models.organisation import HubspotDealData -from backend.hubspot_trigger_orchestrator.hubspot_deal_differ import HubspotDealDiffer +from etl.hubspot.hubspot_deal_differ import HubspotDealDiffer BASE_TIME = datetime(2025, 12, 1, 12, 0, 0) From 605652b30969156a445bc5aebf8cf083246aabc9 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 9 Apr 2026 09:56:58 +0000 Subject: [PATCH 18/47] =?UTF-8?q?diff=20checker=20for=20db=20load=20trigge?= =?UTF-8?q?r=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- etl/hubspot/hubspotDataTodB.py | 8 +- etl/hubspot/tests/test_hubspot_deal_differ.py | 138 ++++++++++++++---- 2 files changed, 116 insertions(+), 30 deletions(-) diff --git a/etl/hubspot/hubspotDataTodB.py b/etl/hubspot/hubspotDataTodB.py index 06cc3be9..4f43f1f7 100644 --- a/etl/hubspot/hubspotDataTodB.py +++ b/etl/hubspot/hubspotDataTodB.py @@ -2,7 +2,7 @@ from backend.app.db.connection import db_read_session from backend.app.db.models.organisation import Organisation, HubspotDealData from sqlmodel import select from datetime import datetime, timezone -from typing import Dict, Optional +from typing import Dict, Optional, Tuple from etl.hubspot.company_data import CompanyData from etl.hubspot.hubspotClient import HubspotClient from etl.hubspot.s3_uploader import S3Uploader @@ -103,7 +103,7 @@ class HubspotDataToDb: Also handles major_condition_issue_photos file upload to S3 with integrity check. """ - def soft_assert(condition, message="Assertion Failed"): + def soft_assert(condition: bool, message: str = "Assertion Failed"): if not condition: print(f"⚠️ Soft Assert Failed: {message}") return False @@ -111,6 +111,10 @@ class HubspotDataToDb: print(f"🔍 Checking if deal needs updating (deal_id={deal_in_db.deal_id})") + hs_deal: Dict[str, str] + hs_company_id: Optional[str] + hs_listing: Optional[Dict[str, str]] + hs_deal, hs_company_id, hs_listing = ( hubspot_client.get_deal_and_company_and_listing(deal_in_db.deal_id) ) diff --git a/etl/hubspot/tests/test_hubspot_deal_differ.py b/etl/hubspot/tests/test_hubspot_deal_differ.py index 12c5a288..876fcab9 100644 --- a/etl/hubspot/tests/test_hubspot_deal_differ.py +++ b/etl/hubspot/tests/test_hubspot_deal_differ.py @@ -31,9 +31,9 @@ def make_new_deal(deal_id: uuid.UUID, **overrides: Any) -> Dict[str, str]: } -# ------------------------------------- -# Random change we aren't interested in -# ------------------------------------- +# ==================== +# PASHUB TRIGGER TESTS +# ==================== @pytest.mark.parametrize( @@ -59,11 +59,6 @@ def test_pashub_trigger__outcome_note_added__returns_false( ) -# ------------------------- -# Pashub link changes -# ------------------------- - - @pytest.mark.parametrize( "old_overrides,new_overrides,expected", [ @@ -92,11 +87,6 @@ def test_pashub_trigger__pashub_link_changed__returns_true( ) -# ------------------------- -# Coordination -# ------------------------- - - @pytest.mark.parametrize( "coordination_status,expected", [ @@ -155,11 +145,6 @@ def test_pashub_trigger__coordination_completed_and_pashub_link_not_set__returns ) -# ------------------------- -# Design -# ------------------------- - - def test_pashub_trigger__design_completed_and_pashub_link_set__returns_true() -> None: deal_id = uuid.uuid4() @@ -204,11 +189,6 @@ def test_pashub_trigger__design_completed_and_pashub_link_not_set__returns_false ) -# ------------------------- -# Lodgement -# ------------------------- - - @pytest.mark.parametrize( "lodgement_status,expected", [ @@ -263,11 +243,6 @@ def test_pashub_trigger__lodgement_completed_and_pashub_link_not_set__returns_fa ) -# ------------------------- -# Negative case -# ------------------------- - - def test_pashub_trigger__coordination_design_lodgement_not_completed_and_pashub_link_set__returns_false() -> ( None ): @@ -293,3 +268,110 @@ def test_pashub_trigger__coordination_design_lodgement_not_completed_and_pashub_ ) is False ) + + +# ======================= +# DB UPDATE TRIGGER TESTS +# ======================= + + +def test_db_update_trigger__no_changes__returns_false() -> None: + deal_id = uuid.uuid4() + + old_deal = make_old_deal( + id=deal_id, + dealname="Test Deal", + dealstage="stage_1", + outcome="won", + ) + + new_deal = make_new_deal( + deal_id, + hs_object_id="1", + dealname="Test Deal", + dealstage="stage_1", + outcome="won", + ) + + result = HubspotDealDiffer.check_for_db_update_trigger( + new_deal=new_deal, + new_company=None, + new_listing=None, + old_deal=old_deal, + ) + + assert result is False + + +def test_db_update_trigger__dealname_changed__returns_true() -> None: + deal_id = uuid.uuid4() + + old_deal = make_old_deal( + id=deal_id, + dealname="Old Name", + ) + + new_deal = make_new_deal( + deal_id, + hs_object_id="1", + dealname="New Name", + ) + + result = HubspotDealDiffer.check_for_db_update_trigger( + new_deal=new_deal, + new_company=None, + new_listing=None, + old_deal=old_deal, + ) + + assert result is True + + +def test_db_update_trigger__company_changed__returns_true() -> None: + deal_id = uuid.uuid4() + + old_deal = make_old_deal( + id=deal_id, + company_id="old_company", + ) + + new_deal = make_new_deal( + deal_id, + hs_object_id="1", + ) + + new_company = "new_company" + + result = HubspotDealDiffer.check_for_db_update_trigger( + new_deal=new_deal, + new_company=new_company, + new_listing=None, + old_deal=old_deal, + ) + + assert result is True + + +def test_db_update_trigger__listing_changed__returns_true() -> None: + deal_id = uuid.uuid4() + + old_deal = make_old_deal( + id=deal_id, + listing_id="abc", + ) + + new_deal = make_new_deal( + deal_id, + hs_object_id="1", + ) + + new_listing = {"listing_id": "xyz"} + + result = HubspotDealDiffer.check_for_db_update_trigger( + new_deal=new_deal, + new_company=None, + new_listing=new_listing, + old_deal=old_deal, + ) + + assert result is True From 01636514aaa58f8749af194fac0ac31cd8a79284 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 9 Apr 2026 10:32:59 +0000 Subject: [PATCH 19/47] pull diffing logic out of loading method --- etl/hubspot/hubspotDataTodB.py | 181 ++++++++++++++-------------- etl/hubspot/scripts/scraper/main.py | 37 ++++-- 2 files changed, 118 insertions(+), 100 deletions(-) diff --git a/etl/hubspot/hubspotDataTodB.py b/etl/hubspot/hubspotDataTodB.py index 4f43f1f7..1c4b6b54 100644 --- a/etl/hubspot/hubspotDataTodB.py +++ b/etl/hubspot/hubspotDataTodB.py @@ -96,13 +96,103 @@ class HubspotDataToDb: return sha256.hexdigest() def update_deal_with_checks( - self, deal_in_db: HubspotDealData, hubspot_client: HubspotClient + self, + deal_in_db: HubspotDealData, + hubspot_client: HubspotClient, + hs_deal: Dict[str, str], + hs_company_id: Optional[str], + hs_listing: Optional[Dict[str, str]], ) -> bool: """ - Checks if a deal needs updating and syncs it with HubSpot. - Also handles major_condition_issue_photos file upload to S3 with integrity check. + Updates deal in database and handles major_condition_issue_photos file upload to S3 with integrity check. """ + self.upsert_deal(hs_deal, hs_company_id, hs_listing, hubspot_client) + # Handle photo upload if it exists but S3 URL is missing + if self._needs_photo_upload(deal_in_db): + print( + f"🖼️ Found photo for deal_id {deal_in_db.deal_id} — uploading to S3..." + ) + + photo_url = hs_deal.get("major_condition_issue_photos") + + if photo_url: + self._upload_photo_to_s3( + deal_in_db, + photo_url, + hubspot_client, + verify=True, + ) + + # persist change + with db_read_session() as session: + db_record = session.get(HubspotDealData, deal_in_db.id) + db_record.major_condition_issue_evidence_s3_url = ( + deal_in_db.major_condition_issue_evidence_s3_url + ) + session.add(db_record) + session.commit() + + return False + else: + print(f"⚠️ Photo URL missing for deal_id {deal_in_db.deal_id}") + + else: + print(f"✅ No update or upload required for deal_id {deal_in_db.deal_id}.") + + return True + + def upsert_deal( + self, + deal_data: Dict[str, str], + company: Optional[str], + listing: Optional[dict[str, str]], + hubspot_client: HubspotClient, + ): + """ + Inserts or updates a deal record. + Also uploads photos if present and adds S3 URL. + """ + with db_read_session() as session: + deal_id = deal_data.get("hs_object_id") + + statement = select(HubspotDealData).where( + HubspotDealData.deal_id == deal_id + ) + existing = session.exec(statement).first() + + if existing: + print(f"🔄 Updating existing deal (deal_id={deal_id})") + self._update_existing_deal(existing, deal_data, listing, company) + + self._handle_existing_photo_upload(existing, hubspot_client) + + session.add(existing) + session.commit() + session.refresh(existing) + return existing + + else: + print(f"🆕 Inserting new deal (deal_id={deal_id})") + new_record: HubspotDealData = self._build_new_deal( + deal_id, deal_data, listing, company + ) + + # Handle upload at insert time + self._handle_new_photo_upload(new_record, hubspot_client) + + session.add(new_record) + session.commit() + session.refresh(new_record) + return new_record + + def _deprecated_diff( + self, + deal_in_db: HubspotDealData, + hs_deal: Dict[str, str], + hs_company_id: Optional[str], + hs_listing: Optional[Dict[str, str]], + ): def soft_assert(condition: bool, message: str = "Assertion Failed"): if not condition: print(f"⚠️ Soft Assert Failed: {message}") @@ -111,14 +201,6 @@ class HubspotDataToDb: print(f"🔍 Checking if deal needs updating (deal_id={deal_in_db.deal_id})") - hs_deal: Dict[str, str] - hs_company_id: Optional[str] - hs_listing: Optional[Dict[str, str]] - - hs_deal, hs_company_id, hs_listing = ( - hubspot_client.get_deal_and_company_and_listing(deal_in_db.deal_id) - ) - # Soft compare key fields checks = [ soft_assert( @@ -291,87 +373,10 @@ class HubspotDataToDb: print( f"❗ Discrepancies found for deal_id {deal_in_db.deal_id} — syncing with HubSpot." ) - self.upsert_deal(hs_deal, hs_company_id, hs_listing, hubspot_client) return False - # Handle photo upload if it exists but S3 URL is missing - if self._needs_photo_upload(deal_in_db): - print( - f"🖼️ Found photo for deal_id {deal_in_db.deal_id} — uploading to S3..." - ) - - photo_url = hs_deal.get("major_condition_issue_photos") - - if photo_url: - self._upload_photo_to_s3( - deal_in_db, - photo_url, - hubspot_client, - verify=True, # 👈 key difference - ) - - # persist change - with db_read_session() as session: - db_record = session.get(HubspotDealData, deal_in_db.id) - db_record.major_condition_issue_evidence_s3_url = ( - deal_in_db.major_condition_issue_evidence_s3_url - ) - session.add(db_record) - session.commit() - - return False - else: - print(f"⚠️ Photo URL missing for deal_id {deal_in_db.deal_id}") - - else: - print(f"✅ No update or upload required for deal_id {deal_in_db.deal_id}.") - return True - def upsert_deal( - self, - deal_data: Dict[str, str], - company: Optional[str], - listing: Optional[dict[str, str]], - hubspot_client: HubspotClient, - ): - """ - Inserts or updates a deal record. - Also uploads photos if present and adds S3 URL. - """ - with db_read_session() as session: - deal_id = deal_data.get("hs_object_id") - - statement = select(HubspotDealData).where( - HubspotDealData.deal_id == deal_id - ) - existing = session.exec(statement).first() - - if existing: - print(f"🔄 Updating existing deal (deal_id={deal_id})") - self._update_existing_deal(existing, deal_data, listing, company) - - self._handle_existing_photo_upload(existing, hubspot_client) - - session.add(existing) - session.commit() - session.refresh(existing) - return existing - - else: - print(f"🆕 Inserting new deal (deal_id={deal_id})") - new_record: HubspotDealData = self._build_new_deal( - deal_id, deal_data, listing, company - ) - - # Handle upload at insert time - self._handle_new_photo_upload(new_record, hubspot_client) - - session.add(new_record) - session.commit() - session.refresh(new_record) - return new_record - def _update_existing_deal( self, existing: HubspotDealData, diff --git a/etl/hubspot/scripts/scraper/main.py b/etl/hubspot/scripts/scraper/main.py index 8c4af1a7..768a86eb 100644 --- a/etl/hubspot/scripts/scraper/main.py +++ b/etl/hubspot/scripts/scraper/main.py @@ -1,8 +1,7 @@ from backend.app.db.models.organisation import HubspotDealData from etl.hubspot.hubspotClient import HubspotClient -# from etl.hubspot.hubspotDataTodB import CompanyData, HubspotDataToDb -from etl.hubspot.hubspotDataTodB import HubspotDataToDb +from etl.hubspot.hubspotDataTodB import CompanyData, HubspotDataToDb from backend.utils.subtasks import task_handler from typing import Any, Dict, Optional @@ -24,11 +23,6 @@ def handler(body: dict[str, Any], context: Any) -> None: hubspot_deal_id ) - if not db_deal: - # New hubspot deal, no diffing to do - # TODO: Trigger hubspot to db ETL - return - hubspot_deal: Dict[str, str] company: Optional[str] listing: Optional[dict[str, str]] @@ -37,10 +31,14 @@ def handler(body: dict[str, Any], context: Any) -> None: hubspot_deal_id ) - if HubspotDealDiffer.check_for_pashub_trigger( - new_deal=hubspot_deal, old_deal=db_deal - ): - # TODO: trigger pashub file fetcher + if not db_deal: + # New hubspot deal, no diffing to do + if company: + company_data: CompanyData = hubspot_client.get_company_information(company) + db_client: HubspotDataToDb = HubspotDataToDb() + db_client.upsert_company(company_data) + + db_client.upsert_deal(hubspot_deal, company, listing, hubspot_client) return if HubspotDealDiffer.check_for_db_update_trigger( @@ -49,7 +47,22 @@ def handler(body: dict[str, Any], context: Any) -> None: new_listing=listing, old_deal=db_deal, ): - # TODO: trigger db upsert + db_client.update_deal_with_checks( + deal_in_db=db_deal, + hubspot_client=hubspot_client, + hs_deal=hubspot_deal, + hs_company_id=company, + hs_listing=listing, + ) + return + + # ============================== + # Orchestration of other lambdas + # ============================== + if HubspotDealDiffer.check_for_pashub_trigger( + new_deal=hubspot_deal, old_deal=db_deal + ): + # TODO: trigger pashub file fetcher return # if db_deal: From 125527baa9a56573c3f8120fbf9daca805d8b20e Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 9 Apr 2026 10:46:52 +0000 Subject: [PATCH 20/47] move HubspotDealData object to dedicated file --- backend/app/db/models/hubspot_deal_data.py | 77 +++++++++++++++++++ backend/app/db/models/organisation.py | 77 +------------------ etl/hubspot/hubspotDataTodB.py | 10 ++- etl/hubspot/hubspot_deal_differ.py | 2 +- etl/hubspot/scripts/scraper/main.py | 9 +-- etl/hubspot/tests/test_hubspot_deal_differ.py | 2 +- 6 files changed, 91 insertions(+), 86 deletions(-) create mode 100644 backend/app/db/models/hubspot_deal_data.py diff --git a/backend/app/db/models/hubspot_deal_data.py b/backend/app/db/models/hubspot_deal_data.py new file mode 100644 index 00000000..d5a51ace --- /dev/null +++ b/backend/app/db/models/hubspot_deal_data.py @@ -0,0 +1,77 @@ +import uuid +from sqlmodel import SQLModel, Field, Column, text +from datetime import datetime +from typing import Optional +from sqlalchemy import DateTime +from sqlalchemy.sql import func + + +class HubspotDealData(SQLModel, table=True): + __tablename__ = "hubspot_deal_data" + + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) + + # HubSpot Deal identifiers + deal_id: str = Field(index=True, nullable=False) + dealname: Optional[str] = Field(default=None) + dealstage: Optional[str] = Field(default=None) + company_id: Optional[str] = Field(default=None) + project_code: Optional[str] = Field(default=None) + + # HubSpot custom properties + landlord_property_id: Optional[str] = Field(default=None) + uprn: Optional[str] = Field(default=None) + outcome: Optional[str] = Field(default=None) + outcome_notes: Optional[str] = Field(default=None) + + major_condition_issue_description: Optional[str] = Field(default=None) + major_condition_issue_photos: Optional[str] = Field(default=None) + major_condition_issue_evidence_s3_url: Optional[str] = Field(default=None) + + coordination_status: Optional[str] = Field(default=None) + coordination_comments: Optional[str] = Field(default=None) + design_status: Optional[str] = Field(default=None) + + listing_id: Optional[str] = Field(default=None) + pashub_link: Optional[str] = Field(default=None) + sharepoint_link: Optional[str] = Field(default=None) + dampmould_growth: Optional[str] = Field(default=None) + damp_mould_and_repairs_comments: Optional[str] = Field(default=None) + pre_sap: Optional[str] = Field(default=None) + coordinator: Optional[str] = Field(default=None) + mtp_completion_date: Optional[datetime] = Field(default=None) + mtp_re_model_completion_date: Optional[datetime] = Field(default=None) + ioe_v3_completion_date: Optional[datetime] = Field(default=None) + proposed_measures: Optional[str] = Field(default=None) + approved_package: Optional[str] = Field(default=None) + designer: Optional[str] = Field(default=None) + design_completion_date: Optional[datetime] = Field(default=None) + actual_measures_installed: Optional[str] = Field(default=None) + installer: Optional[str] = Field(default=None) + installer_handover: Optional[str] = Field(default=None) + lodgement_status: Optional[str] = Field(default=None) + measures_lodgement_date: Optional[datetime] = Field(default=None) + lodgement_date: Optional[datetime] = Field(default=None) + expected_commencement_date: Optional[datetime] = Field(default=None) + surveyor: Optional[str] = Field(default=None) + confirmed_survey_date: Optional[datetime] = Field(default=None) + confirmed_survey_time: Optional[str] = Field(default=None) + surveyed_date: Optional[datetime] = Field(default=None) + design_type: Optional[str] = Field(default=None) + + created_at: datetime = Field( + sa_column=Column( + DateTime(timezone=True), + server_default=text("(NOW() AT TIME ZONE 'utc')"), + nullable=False, + ) + ) + + updated_at: datetime = Field( + sa_column=Column( + DateTime(timezone=True), + server_default=text("(NOW() AT TIME ZONE 'utc')"), + onupdate=func.now(), + nullable=False, + ) + ) diff --git a/backend/app/db/models/organisation.py b/backend/app/db/models/organisation.py index 784cc4ad..8afc5d63 100644 --- a/backend/app/db/models/organisation.py +++ b/backend/app/db/models/organisation.py @@ -1,9 +1,7 @@ -from sqlmodel import SQLModel, Field, Column, text +import uuid +from sqlmodel import SQLModel, Field from datetime import datetime, timezone from typing import Optional -from sqlalchemy import DateTime -from sqlalchemy.sql import func -import uuid class Organisation(SQLModel, table=True): @@ -13,74 +11,3 @@ class Organisation(SQLModel, table=True): updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) hubspot_company_id: Optional[str] = None name: Optional[str] = None - - -class HubspotDealData(SQLModel, table=True): - __tablename__ = "hubspot_deal_data" - - id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) - - # HubSpot Deal identifiers - deal_id: str = Field(index=True, nullable=False) - dealname: Optional[str] = Field(default=None) - dealstage: Optional[str] = Field(default=None) - company_id: Optional[str] = Field(default=None) - project_code: Optional[str] = Field(default=None) - - # HubSpot custom properties - landlord_property_id: Optional[str] = Field(default=None) - uprn: Optional[str] = Field(default=None) - outcome: Optional[str] = Field(default=None) - outcome_notes: Optional[str] = Field(default=None) - - major_condition_issue_description: Optional[str] = Field(default=None) - major_condition_issue_photos: Optional[str] = Field(default=None) - major_condition_issue_evidence_s3_url: Optional[str] = Field(default=None) - - coordination_status: Optional[str] = Field(default=None) - coordination_comments: Optional[str] = Field(default=None) - design_status: Optional[str] = Field(default=None) - - listing_id: Optional[str] = Field(default=None) - pashub_link: Optional[str] = Field(default=None) - sharepoint_link: Optional[str] = Field(default=None) - dampmould_growth: Optional[str] = Field(default=None) - damp_mould_and_repairs_comments: Optional[str] = Field(default=None) - pre_sap: Optional[str] = Field(default=None) - coordinator: Optional[str] = Field(default=None) - mtp_completion_date: Optional[datetime] = Field(default=None) - mtp_re_model_completion_date: Optional[datetime] = Field(default=None) - ioe_v3_completion_date: Optional[datetime] = Field(default=None) - proposed_measures: Optional[str] = Field(default=None) - approved_package: Optional[str] = Field(default=None) - designer: Optional[str] = Field(default=None) - design_completion_date: Optional[datetime] = Field(default=None) - actual_measures_installed: Optional[str] = Field(default=None) - installer: Optional[str] = Field(default=None) - installer_handover: Optional[str] = Field(default=None) - lodgement_status: Optional[str] = Field(default=None) - measures_lodgement_date: Optional[datetime] = Field(default=None) - lodgement_date: Optional[datetime] = Field(default=None) - expected_commencement_date: Optional[datetime] = Field(default=None) - surveyor: Optional[str] = Field(default=None) - confirmed_survey_date: Optional[datetime] = Field(default=None) - confirmed_survey_time: Optional[str] = Field(default=None) - surveyed_date: Optional[datetime] = Field(default=None) - design_type: Optional[str] = Field(default=None) - - created_at: datetime = Field( - sa_column=Column( - DateTime(timezone=True), - server_default=text("(NOW() AT TIME ZONE 'utc')"), - nullable=False, - ) - ) - - updated_at: datetime = Field( - sa_column=Column( - DateTime(timezone=True), - server_default=text("(NOW() AT TIME ZONE 'utc')"), - onupdate=func.now(), - nullable=False, - ) - ) diff --git a/etl/hubspot/hubspotDataTodB.py b/etl/hubspot/hubspotDataTodB.py index 3c017f0e..5ebc8c73 100644 --- a/etl/hubspot/hubspotDataTodB.py +++ b/etl/hubspot/hubspotDataTodB.py @@ -1,13 +1,15 @@ -from backend.app.db.connection import db_read_session -from backend.app.db.models.organisation import Organisation, HubspotDealData +import hashlib +import os from sqlmodel import select from datetime import datetime, timezone from typing import Dict, Optional + +from backend.app.db.models.hubspot_deal_data import HubspotDealData from etl.hubspot.company_data import CompanyData from etl.hubspot.hubspotClient import HubspotClient from etl.hubspot.s3_uploader import S3Uploader -import hashlib -import os +from backend.app.db.connection import db_read_session +from backend.app.db.models.organisation import Organisation class HubspotDataToDb: diff --git a/etl/hubspot/hubspot_deal_differ.py b/etl/hubspot/hubspot_deal_differ.py index 1dd4ed51..dd992243 100644 --- a/etl/hubspot/hubspot_deal_differ.py +++ b/etl/hubspot/hubspot_deal_differ.py @@ -1,6 +1,6 @@ from typing import Dict, List, Optional -from backend.app.db.models.organisation import HubspotDealData +from backend.app.db.models.hubspot_deal_data import HubspotDealData class HubspotDealDiffer: diff --git a/etl/hubspot/scripts/scraper/main.py b/etl/hubspot/scripts/scraper/main.py index 768a86eb..826d7e05 100644 --- a/etl/hubspot/scripts/scraper/main.py +++ b/etl/hubspot/scripts/scraper/main.py @@ -1,14 +1,13 @@ -from backend.app.db.models.organisation import HubspotDealData -from etl.hubspot.hubspotClient import HubspotClient - -from etl.hubspot.hubspotDataTodB import CompanyData, HubspotDataToDb -from backend.utils.subtasks import task_handler from typing import Any, Dict, Optional +from etl.hubspot.hubspotClient import HubspotClient +from etl.hubspot.hubspotDataTodB import CompanyData, HubspotDataToDb from etl.hubspot.hubspot_deal_differ import HubspotDealDiffer from etl.hubspot.hubspot_trigger_orchestrator_trigger_request import ( HubspotTriggerOrchestratorTriggerRequest, ) +from backend.utils.subtasks import task_handler +from backend.app.db.models.hubspot_deal_data import HubspotDealData @task_handler() diff --git a/etl/hubspot/tests/test_hubspot_deal_differ.py b/etl/hubspot/tests/test_hubspot_deal_differ.py index 876fcab9..74d3f057 100644 --- a/etl/hubspot/tests/test_hubspot_deal_differ.py +++ b/etl/hubspot/tests/test_hubspot_deal_differ.py @@ -4,7 +4,7 @@ import uuid import pytest -from backend.app.db.models.organisation import HubspotDealData +from backend.app.db.models.hubspot_deal_data import HubspotDealData from etl.hubspot.hubspot_deal_differ import HubspotDealDiffer From 36aaabb3cfa05d776484f2af8fa53973936dc5b5 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 9 Apr 2026 11:19:33 +0000 Subject: [PATCH 21/47] =?UTF-8?q?diff=20checker=20for=20db=20load=20trigge?= =?UTF-8?q?r=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/db/models/hubspot_deal_data.py | 10 +- etl/hubspot/hubspotDataTodB.py | 247 ++------------------- etl/hubspot/hubspot_deal_differ.py | 90 +++++++- etl/hubspot/scripts/scraper/main.py | 18 -- etl/hubspot/utils.py | 11 + 5 files changed, 127 insertions(+), 249 deletions(-) create mode 100644 etl/hubspot/utils.py diff --git a/backend/app/db/models/hubspot_deal_data.py b/backend/app/db/models/hubspot_deal_data.py index d5a51ace..1d7607e0 100644 --- a/backend/app/db/models/hubspot_deal_data.py +++ b/backend/app/db/models/hubspot_deal_data.py @@ -59,19 +59,21 @@ class HubspotDealData(SQLModel, table=True): surveyed_date: Optional[datetime] = Field(default=None) design_type: Optional[str] = Field(default=None) - created_at: datetime = Field( + created_at: Optional[datetime] = Field( sa_column=Column( DateTime(timezone=True), server_default=text("(NOW() AT TIME ZONE 'utc')"), nullable=False, - ) + ), + default=None, # Nullable in db but optional here as value is set on db save for new record ) - updated_at: datetime = Field( + updated_at: Optional[datetime] = Field( sa_column=Column( DateTime(timezone=True), server_default=text("(NOW() AT TIME ZONE 'utc')"), onupdate=func.now(), nullable=False, - ) + ), + default=None, # Nullable in db but optional here as value is set on db save for new record ) diff --git a/etl/hubspot/hubspotDataTodB.py b/etl/hubspot/hubspotDataTodB.py index 5ebc8c73..210c9593 100644 --- a/etl/hubspot/hubspotDataTodB.py +++ b/etl/hubspot/hubspotDataTodB.py @@ -10,6 +10,7 @@ from etl.hubspot.hubspotClient import HubspotClient from etl.hubspot.s3_uploader import S3Uploader from backend.app.db.connection import db_read_session from backend.app.db.models.organisation import Organisation +from etl.hubspot.utils import parse_hs_date class HubspotDataToDb: @@ -60,11 +61,7 @@ class HubspotDataToDb: session.commit() return record - def new_record_to_hubspot_data(self, deal_data, company, listing, hubspot_client): - print("⚠️ Deprecated — use the new interface instead.") - return self.upsert_deal(deal_data, company, listing, hubspot_client) - - def find_all_deals_with_company_id(self, company_id): + def find_all_deals_with_company_id(self, company_id: str): """Returns a list of deals for a given company_id.""" with db_read_session() as session: return ( @@ -137,7 +134,7 @@ class HubspotDataToDb: return False else: - print(f"⚠️ Photo URL missing for deal_id {deal_in_db.deal_id}") + print(f"⚠️ Photo URL missing for deal_id {deal_in_db.deal_id}") else: print(f"✅ No update or upload required for deal_id {deal_in_db.deal_id}.") @@ -188,202 +185,6 @@ class HubspotDataToDb: session.refresh(new_record) return new_record - def _deprecated_diff( - self, - deal_in_db: HubspotDealData, - hs_deal: Dict[str, str], - hs_company_id: Optional[str], - hs_listing: Optional[Dict[str, str]], - ): - def soft_assert(condition: bool, message: str = "Assertion Failed"): - if not condition: - print(f"⚠️ Soft Assert Failed: {message}") - return False - return True - - print(f"🔍 Checking if deal needs updating (deal_id={deal_in_db.deal_id})") - - # Soft compare key fields - checks = [ - soft_assert( - deal_in_db.deal_id == hs_deal.get("hs_object_id"), "deal_id mismatch" - ), - soft_assert(deal_in_db.company_id == hs_company_id, "company_id mismatch"), - soft_assert( - deal_in_db.listing_id == hs_listing.get("listing_id"), - "listing_id mismatch", - ), - soft_assert( - deal_in_db.landlord_property_id == hs_listing.get("owner_property_id"), - "landlord_property_id mismatch", - ), - soft_assert( - deal_in_db.outcome == hs_deal.get("outcome"), "outcome mismatch" - ), - soft_assert( - deal_in_db.dealstage == hs_deal.get("dealstage"), "dealstage mismatch" - ), - soft_assert( - deal_in_db.dealname == hs_deal.get("dealname"), "dealname mismatch" - ), - soft_assert( - deal_in_db.project_code == hs_deal.get("project_code"), - "project_code mismatch", - ), - soft_assert( - deal_in_db.uprn == hs_listing.get("national_uprn"), "uprn mismatch" - ), - soft_assert( - deal_in_db.outcome_notes == hs_deal.get("outcome_notes"), - "outcome_notes mismatch", - ), - soft_assert( - deal_in_db.major_condition_issue_description - == hs_deal.get("major_condition_issue_description"), - "major condition description mismatch", - ), - soft_assert( - deal_in_db.major_condition_issue_photos - == hs_deal.get("major_condition_issue_photos"), - "major condition issue photos mismatch", - ), - soft_assert( - deal_in_db.coordination_status - == hs_deal.get("coordination_status__stage_1_"), - "coordination stage 1 status mismatch", - ), - soft_assert( - deal_in_db.coordination_comments - == hs_deal.get("coordination_comments"), - "coordination_comments mismatch", - ), - soft_assert( - deal_in_db.design_status == hs_deal.get("retrofit_design_status"), - "retrofit design mismatch", - ), - soft_assert( - deal_in_db.pashub_link == hs_deal.get("pashub_link"), - "pashub_link mismatch", - ), - soft_assert( - deal_in_db.sharepoint_link == hs_deal.get("sharepoint_link"), - "sharepoint_link mismatch", - ), - soft_assert( - deal_in_db.dampmould_growth == hs_deal.get("dampmould_growth"), - "dampmould_growth mismatch", - ), - soft_assert( - deal_in_db.damp_mould_and_repairs_comments - == hs_deal.get("damp_mould_and_repairs_comments"), - "damp_mould_and_repairs_comments mismatch", - ), - soft_assert( - deal_in_db.pre_sap == hs_deal.get("pre_sap"), - "pre_sap mismatch", - ), - soft_assert( - deal_in_db.coordinator == hs_deal.get("coordinator"), - "coordinator mismatch", - ), - soft_assert( - deal_in_db.mtp_completion_date - == self._parse_hs_date(hs_deal.get("mtp_completion_date")), - "mtp_completion_date mismatch", - ), - soft_assert( - deal_in_db.mtp_re_model_completion_date - == self._parse_hs_date(hs_deal.get("mtp_re_model_completion_date")), - "mtp_re_model_completion_date mismatch", - ), - soft_assert( - deal_in_db.ioe_v3_completion_date - == self._parse_hs_date(hs_deal.get("ioe_v3_completion_date")), - "ioe_v3_completion_date mismatch", - ), - soft_assert( - deal_in_db.proposed_measures == hs_deal.get("proposed_measures"), - "proposed_measures mismatch", - ), - soft_assert( - deal_in_db.approved_package == hs_deal.get("approved_package"), - "approved_package mismatch", - ), - soft_assert( - deal_in_db.designer == hs_deal.get("designer"), - "designer mismatch", - ), - soft_assert( - deal_in_db.design_completion_date - == self._parse_hs_date(hs_deal.get("design_completion_date")), - "design_completion_date mismatch", - ), - soft_assert( - deal_in_db.actual_measures_installed - == hs_deal.get("actual_measures_installed"), - "actual_measures_installed mismatch", - ), - soft_assert( - deal_in_db.installer == hs_deal.get("installer"), - "installer mismatch", - ), - soft_assert( - deal_in_db.installer_handover == hs_deal.get("installer_handover"), - "installer_handover mismatch", - ), - soft_assert( - deal_in_db.lodgement_status == hs_deal.get("lodgement_status"), - "lodgement_status mismatch", - ), - soft_assert( - deal_in_db.measures_lodgement_date - == self._parse_hs_date(hs_deal.get("measures_lodgement_date")), - "measures_lodgement_date mismatch", - ), - soft_assert( - deal_in_db.lodgement_date - == self._parse_hs_date(hs_deal.get("lodgement_date")), - "lodgement_date mismatch", - ), - soft_assert( - deal_in_db.expected_commencement_date - == self._parse_hs_date(hs_deal.get("expected_commencement_date")), - "expected_commencement_date mismatch", - ), - soft_assert( - deal_in_db.surveyor == hs_deal.get("surveyor"), - "surveyor mismatch", - ), - soft_assert( - deal_in_db.confirmed_survey_date - == self._parse_hs_date(hs_deal.get("confirmed_survey_date")), - "confirmed_survey_date mismatch", - ), - soft_assert( - deal_in_db.confirmed_survey_time - == hs_deal.get("confirmed_survey_time"), - "confirmed_survey_time mismatch", - ), - soft_assert( - deal_in_db.surveyed_date - == self._parse_hs_date(hs_deal.get("surveyed_date")), - "surveyed_date mismatch", - ), - soft_assert( - deal_in_db.design_type == hs_deal.get("design_type"), - "design_type mismatch", - ), - ] - - # If discrepancies found, update from HubSpot - if not all(checks): - print( - f"❗ Discrepancies found for deal_id {deal_in_db.deal_id} — syncing with HubSpot." - ) - return False - - return True - def _update_existing_deal( self, existing: HubspotDealData, @@ -420,38 +221,36 @@ class HubspotDataToDb: ), "pre_sap": deal_data.get("pre_sap"), "coordinator": deal_data.get("coordinator"), - "mtp_completion_date": self._parse_hs_date( - deal_data.get("mtp_completion_date") - ), - "mtp_re_model_completion_date": self._parse_hs_date( + "mtp_completion_date": parse_hs_date(deal_data.get("mtp_completion_date")), + "mtp_re_model_completion_date": parse_hs_date( deal_data.get("mtp_re_model_completion_date") ), - "ioe_v3_completion_date": self._parse_hs_date( + "ioe_v3_completion_date": parse_hs_date( deal_data.get("ioe_v3_completion_date") ), "proposed_measures": deal_data.get("proposed_measures"), "approved_package": deal_data.get("approved_package"), "designer": deal_data.get("designer"), - "design_completion_date": self._parse_hs_date( + "design_completion_date": parse_hs_date( deal_data.get("design_completion_date") ), "actual_measures_installed": deal_data.get("actual_measures_installed"), "installer": deal_data.get("installer"), "installer_handover": deal_data.get("installer_handover"), "lodgement_status": deal_data.get("lodgement_status"), - "measures_lodgement_date": self._parse_hs_date( + "measures_lodgement_date": parse_hs_date( deal_data.get("measures_lodgement_date") ), - "lodgement_date": self._parse_hs_date(deal_data.get("lodgement_date")), - "expected_commencement_date": self._parse_hs_date( + "lodgement_date": parse_hs_date(deal_data.get("lodgement_date")), + "expected_commencement_date": parse_hs_date( deal_data.get("expected_commencement_date") ), "surveyor": deal_data.get("surveyor"), - "confirmed_survey_date": self._parse_hs_date( + "confirmed_survey_date": parse_hs_date( deal_data.get("confirmed_survey_date") ), "confirmed_survey_time": deal_data.get("confirmed_survey_time"), - "surveyed_date": self._parse_hs_date(deal_data.get("surveyed_date")), + "surveyed_date": parse_hs_date(deal_data.get("surveyed_date")), "design_type": deal_data.get("design_type"), }.items(): setattr(existing, attr, value or getattr(existing, attr)) @@ -491,38 +290,34 @@ class HubspotDataToDb: ), pre_sap=deal_data.get("pre_sap"), coordinator=deal_data.get("coordinator"), - mtp_completion_date=self._parse_hs_date( - deal_data.get("mtp_completion_date") - ), - mtp_re_model_completion_date=self._parse_hs_date( + mtp_completion_date=parse_hs_date(deal_data.get("mtp_completion_date")), + mtp_re_model_completion_date=parse_hs_date( deal_data.get("mtp_re_model_completion_date") ), - ioe_v3_completion_date=self._parse_hs_date( + ioe_v3_completion_date=parse_hs_date( deal_data.get("ioe_v3_completion_date") ), proposed_measures=deal_data.get("proposed_measures"), approved_package=deal_data.get("approved_package"), designer=deal_data.get("designer"), - design_completion_date=self._parse_hs_date( + design_completion_date=parse_hs_date( deal_data.get("design_completion_date") ), actual_measures_installed=deal_data.get("actual_measures_installed"), installer=deal_data.get("installer"), installer_handover=deal_data.get("installer_handover"), lodgement_status=deal_data.get("lodgement_status"), - measures_lodgement_date=self._parse_hs_date( + measures_lodgement_date=parse_hs_date( deal_data.get("measures_lodgement_date") ), - lodgement_date=self._parse_hs_date(deal_data.get("lodgement_date")), - expected_commencement_date=self._parse_hs_date( + lodgement_date=parse_hs_date(deal_data.get("lodgement_date")), + expected_commencement_date=parse_hs_date( deal_data.get("expected_commencement_date") ), surveyor=deal_data.get("surveyor"), - confirmed_survey_date=self._parse_hs_date( - deal_data.get("confirmed_survey_date") - ), + confirmed_survey_date=parse_hs_date(deal_data.get("confirmed_survey_date")), confirmed_survey_time=deal_data.get("confirmed_survey_time"), - surveyed_date=self._parse_hs_date(deal_data.get("surveyed_date")), + surveyed_date=parse_hs_date(deal_data.get("surveyed_date")), design_type=deal_data.get("design_type"), ) diff --git a/etl/hubspot/hubspot_deal_differ.py b/etl/hubspot/hubspot_deal_differ.py index dd992243..42def3b2 100644 --- a/etl/hubspot/hubspot_deal_differ.py +++ b/etl/hubspot/hubspot_deal_differ.py @@ -1,6 +1,7 @@ from typing import Dict, List, Optional from backend.app.db.models.hubspot_deal_data import HubspotDealData +from etl.hubspot.utils import parse_hs_date class HubspotDealDiffer: @@ -18,7 +19,94 @@ class HubspotDealDiffer: new_listing: Optional[Dict[str, str]], old_deal: HubspotDealData, ) -> bool: - raise NotImplementedError + """ + Returns True if ANY difference exists between HubSpot data and DB. + Returns False if everything matches (i.e. no update needed). + """ + + # --- Deal ID --- + if str(old_deal.deal_id) != str(new_deal.get("hs_object_id")): + return True + + # --- Company --- + if new_company is not None: + if old_deal.company_id != new_company: + return True + + # --- Listing --- + hs_listing = new_listing or {} + + if old_deal.listing_id != hs_listing.get("listing_id"): + return True + + if old_deal.landlord_property_id != hs_listing.get("owner_property_id"): + return True + + if old_deal.uprn != hs_listing.get("national_uprn"): + return True + + # --- Field mappings --- + FIELD_MAP = { + "outcome": "outcome", + "dealstage": "dealstage", + "dealname": "dealname", + "project_code": "project_code", + "outcome_notes": "outcome_notes", + "major_condition_issue_description": "major_condition_issue_description", + "major_condition_issue_photos": "major_condition_issue_photos", + "coordination_status__stage_1_": "coordination_status", + "coordination_comments": "coordination_comments", + "retrofit_design_status": "design_status", + "pashub_link": "pashub_link", + "sharepoint_link": "sharepoint_link", + "dampmould_growth": "dampmould_growth", + "damp_mould_and_repairs_comments": "damp_mould_and_repairs_comments", + "pre_sap": "pre_sap", + "coordinator": "coordinator", + "proposed_measures": "proposed_measures", + "approved_package": "approved_package", + "designer": "designer", + "actual_measures_installed": "actual_measures_installed", + "installer": "installer", + "installer_handover": "installer_handover", + "lodgement_status": "lodgement_status", + "design_type": "design_type", + "surveyor": "surveyor", + } + + for hs_field, db_field in FIELD_MAP.items(): + old_value = getattr(old_deal, db_field) + new_value = new_deal.get(hs_field) + + if old_value != new_value: + return True + + # --- Date fields --- + date_fields = [ + ("mtp_completion_date", "mtp_completion_date"), + ("mtp_re_model_completion_date", "mtp_re_model_completion_date"), + ("ioe_v3_completion_date", "ioe_v3_completion_date"), + ("design_completion_date", "design_completion_date"), + ("measures_lodgement_date", "measures_lodgement_date"), + ("lodgement_date", "lodgement_date"), + ("expected_commencement_date", "expected_commencement_date"), + ("confirmed_survey_date", "confirmed_survey_date"), + ("surveyed_date", "surveyed_date"), + ] + + for hs_field, db_field in date_fields: + old_value = getattr(old_deal, db_field) + new_value = parse_hs_date(new_deal.get(hs_field)) + + if old_value != new_value: + return True + + # --- Time field --- + if old_deal.confirmed_survey_time != new_deal.get("confirmed_survey_time"): + return True + + # No differences found + return False @staticmethod def check_for_pashub_trigger( diff --git a/etl/hubspot/scripts/scraper/main.py b/etl/hubspot/scripts/scraper/main.py index 826d7e05..5d5b2b26 100644 --- a/etl/hubspot/scripts/scraper/main.py +++ b/etl/hubspot/scripts/scraper/main.py @@ -63,21 +63,3 @@ def handler(body: dict[str, Any], context: Any) -> None: ): # TODO: trigger pashub file fetcher return - - # if db_deal: - # db_client.update_deal_with_checks(db_deal, hubspot_client) - # else: - # hubspot_deal: Dict[str, str] - # company: Optional[str] - # listing: Optional[dict[str, str]] - - # hubspot_deal, company, listing = ( - # hubspot_client.get_deal_and_company_and_listing(hubspot_deal_id) - # ) - - # if company: - # company_data: CompanyData = hubspot_client.get_company_information(company) - # db_client: HubspotDataToDb = HubspotDataToDb() - # db_client.upsert_company(company_data) - - # db_client.upsert_deal(hubspot_deal, company, listing, hubspot_client) diff --git a/etl/hubspot/utils.py b/etl/hubspot/utils.py new file mode 100644 index 00000000..9fbeae62 --- /dev/null +++ b/etl/hubspot/utils.py @@ -0,0 +1,11 @@ +from datetime import datetime +from typing import Optional + + +def parse_hs_date(value: Optional[str]) -> Optional[datetime]: + if not value: + return None + try: + return datetime.fromisoformat(value.replace("Z", "+00:00")) + except ValueError: + return None From f572dfd2b316c17636061c72d323803394f0343c Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 9 Apr 2026 12:53:48 +0000 Subject: [PATCH 22/47] trigger pashub to ara lambda if necessary --- etl/hubspot/scripts/scraper/main.py | 31 +++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/etl/hubspot/scripts/scraper/main.py b/etl/hubspot/scripts/scraper/main.py index 5d5b2b26..18e425a4 100644 --- a/etl/hubspot/scripts/scraper/main.py +++ b/etl/hubspot/scripts/scraper/main.py @@ -1,3 +1,5 @@ +import json +import boto3 from typing import Any, Dict, Optional from etl.hubspot.hubspotClient import HubspotClient @@ -8,6 +10,9 @@ from etl.hubspot.hubspot_trigger_orchestrator_trigger_request import ( ) from backend.utils.subtasks import task_handler from backend.app.db.models.hubspot_deal_data import HubspotDealData +from utils.logger import setup_logger + +logger = setup_logger() @task_handler() @@ -15,6 +20,9 @@ def handler(body: dict[str, Any], context: Any) -> None: db_client = HubspotDataToDb() hubspot_client = HubspotClient() + sqs_client = boto3.client("sqs") + PASHUB_TRIGGER_QUEUE_URL = "pashub_to_ara-queue-dev" # TODO: get from env var + payload = HubspotTriggerOrchestratorTriggerRequest.model_validate(body) hubspot_deal_id: str = payload.hubspot_deal_id @@ -40,6 +48,9 @@ def handler(body: dict[str, Any], context: Any) -> None: db_client.upsert_deal(hubspot_deal, company, listing, hubspot_client) return + deal_unchanged = True + + # Deal already in db, check whether anything has changed if HubspotDealDiffer.check_for_db_update_trigger( new_deal=hubspot_deal, new_company=company, @@ -53,6 +64,9 @@ def handler(body: dict[str, Any], context: Any) -> None: hs_company_id=company, hs_listing=listing, ) + deal_unchanged = False + + if deal_unchanged: return # ============================== @@ -62,4 +76,21 @@ def handler(body: dict[str, Any], context: Any) -> None: new_deal=hubspot_deal, old_deal=db_deal ): # TODO: trigger pashub file fetcher + message_body: Dict[str, Optional[str]] = { + "pashub_link": hubspot_deal["pashub_link"], + "address": None, # can we get this? + "sharepoint_link": hubspot_deal["sharepoint_link"], + "uprn": hubspot_deal["national_uprn"], + "landlord_property_id": hubspot_deal["owner_property_id"], + "deal_stage": hubspot_deal["deal_stage"], + } + + response = sqs_client.send_message( + QueueUrl=PASHUB_TRIGGER_QUEUE_URL, MessageBody=json.dumps(message_body) + ) + + logger.info( + f"Sent message to Pashub To Ara queue. MessageId: {response['MessageId']}" + ) + return From c718e36c1180a0fd2413a6a5d6cf93562eebc462 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 9 Apr 2026 12:54:01 +0000 Subject: [PATCH 23/47] trigger pashub to ara lambda if necessary --- etl/hubspot/scripts/scraper/main.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/etl/hubspot/scripts/scraper/main.py b/etl/hubspot/scripts/scraper/main.py index 18e425a4..0bc285a7 100644 --- a/etl/hubspot/scripts/scraper/main.py +++ b/etl/hubspot/scripts/scraper/main.py @@ -92,5 +92,3 @@ def handler(body: dict[str, Any], context: Any) -> None: logger.info( f"Sent message to Pashub To Ara queue. MessageId: {response['MessageId']}" ) - - return From 2b93e06629e146c8d536d019414946cf958bf405 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 9 Apr 2026 13:07:13 +0000 Subject: [PATCH 24/47] add todo --- etl/hubspot/hubspotClient.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/etl/hubspot/hubspotClient.py b/etl/hubspot/hubspotClient.py index 8053b41f..6bdf71ed 100644 --- a/etl/hubspot/hubspotClient.py +++ b/etl/hubspot/hubspotClient.py @@ -230,7 +230,9 @@ class HubspotClient: self.logger.info(f"Listing info for deal {deal_id}: {listing_info}") return listing_info - def from_deal_id_get_info(self, deal_id: str) -> dict[str, str]: + def from_deal_id_get_info( + self, deal_id: str + ) -> dict[str, str]: # TODO: add dataclass for this deals_api: DealsBasicApi = self.client.crm.deals.basic_api # type: ignore[reportUnknownMemberType] deal: HubspotObject = self._call_with_retry( From ff0027dbc46f04c2383a2a8630f476c0c9b071f8 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 9 Apr 2026 13:09:48 +0000 Subject: [PATCH 25/47] remove todo --- etl/hubspot/scripts/scraper/main.py | 1 - 1 file changed, 1 deletion(-) diff --git a/etl/hubspot/scripts/scraper/main.py b/etl/hubspot/scripts/scraper/main.py index 0bc285a7..cec03da8 100644 --- a/etl/hubspot/scripts/scraper/main.py +++ b/etl/hubspot/scripts/scraper/main.py @@ -75,7 +75,6 @@ def handler(body: dict[str, Any], context: Any) -> None: if HubspotDealDiffer.check_for_pashub_trigger( new_deal=hubspot_deal, old_deal=db_deal ): - # TODO: trigger pashub file fetcher message_body: Dict[str, Optional[str]] = { "pashub_link": hubspot_deal["pashub_link"], "address": None, # can we get this? From 96ac34f42209daaa0a33364ceaae61fcc7f65152 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Thu, 9 Apr 2026 14:47:54 +0100 Subject: [PATCH 26/47] adding hacky handling for matching on lmk key or uprn --- backend/SearchEpc.py | 26 +- backend/addresses/Address.py | 4 + backend/addresses/Addresses.py | 16 +- backend/engine/engine.py | 14 +- backend/epc_api/api_spec.md | 1 + .../json_samples/CEPC-7.0/cepc+rr.json | 278 +++ .../json_samples/CEPC-7.0/cepc-rr.json | 60 + .../epc_api/json_samples/CEPC-7.0/cepc.json | 285 +++ .../epc_api/json_samples/CEPC-7.0/dec+rr.json | 128 + .../epc_api/json_samples/CEPC-7.0/dec-rr.json | 76 + .../json_samples/CEPC-7.1/cepc+rr.json | 264 +++ .../json_samples/CEPC-7.1/cepc-rr.json | 60 + .../epc_api/json_samples/CEPC-7.1/cepc.json | 288 +++ .../epc_api/json_samples/CEPC-7.1/dec+rr.json | 118 + .../epc_api/json_samples/CEPC-7.1/dec-rr.json | 76 + .../json_samples/CEPC-8.0.0/cepc+rr.json | 468 ++++ .../json_samples/CEPC-8.0.0/cepc-rr.json | 60 + .../epc_api/json_samples/CEPC-8.0.0/cepc.json | 334 +++ .../json_samples/CEPC-8.0.0/dec+rr.json | 131 + .../json_samples/CEPC-8.0.0/dec-rr.json | 76 + .../epc_api/json_samples/CEPC-8.0.0/dec.json | 299 +++ .../json_samples/RdSAP-Schema-17.0/epc.json | 352 +++ .../json_samples/RdSAP-Schema-17.1/epc.json | 472 ++++ .../json_samples/RdSAP-Schema-18.0/epc.json | 413 ++++ .../json_samples/RdSAP-Schema-19.0/epc.json | 386 +++ .../json_samples/RdSAP-Schema-20.0.0/epc.json | 340 +++ .../json_samples/RdSAP-Schema-21.0.0/epc.json | 403 ++++ .../json_samples/RdSAP-Schema-21.0.1/epc.json | 446 ++++ .../json_samples/SAP-Schema-16.0/rdsap.json | 299 +++ .../json_samples/SAP-Schema-16.0/sap.json | 348 +++ .../json_samples/SAP-Schema-16.1/rdsap.json | 400 ++++ .../json_samples/SAP-Schema-16.1/sap.json | 365 +++ .../json_samples/SAP-Schema-16.2/rdsap.json | 931 ++++++++ .../json_samples/SAP-Schema-16.2/sap.json | 2112 +++++++++++++++++ .../json_samples/SAP-Schema-16.3/rdsap.json | 371 +++ .../json_samples/SAP-Schema-16.3/sap.json | 456 ++++ .../json_samples/SAP-Schema-17.0/epc.json | 375 +++ .../json_samples/SAP-Schema-17.1/epc.json | 562 +++++ .../json_samples/SAP-Schema-18.0.0/epc.json | 510 ++++ .../json_samples/SAP-Schema-19.0.0/epc.json | 610 +++++ .../json_samples/SAP-Schema-19.1.0/epc.json | 613 +++++ .../json_samples/SAP-Schema-19.2.0/epc.json | 612 +++++ etl/find_my_epc/RetrieveFindMyEpc.py | 11 +- 43 files changed, 14438 insertions(+), 11 deletions(-) create mode 100644 backend/epc_api/api_spec.md create mode 100644 backend/epc_api/json_samples/CEPC-7.0/cepc+rr.json create mode 100644 backend/epc_api/json_samples/CEPC-7.0/cepc-rr.json create mode 100644 backend/epc_api/json_samples/CEPC-7.0/cepc.json create mode 100644 backend/epc_api/json_samples/CEPC-7.0/dec+rr.json create mode 100644 backend/epc_api/json_samples/CEPC-7.0/dec-rr.json create mode 100644 backend/epc_api/json_samples/CEPC-7.1/cepc+rr.json create mode 100644 backend/epc_api/json_samples/CEPC-7.1/cepc-rr.json create mode 100644 backend/epc_api/json_samples/CEPC-7.1/cepc.json create mode 100644 backend/epc_api/json_samples/CEPC-7.1/dec+rr.json create mode 100644 backend/epc_api/json_samples/CEPC-7.1/dec-rr.json create mode 100644 backend/epc_api/json_samples/CEPC-8.0.0/cepc+rr.json create mode 100644 backend/epc_api/json_samples/CEPC-8.0.0/cepc-rr.json create mode 100644 backend/epc_api/json_samples/CEPC-8.0.0/cepc.json create mode 100644 backend/epc_api/json_samples/CEPC-8.0.0/dec+rr.json create mode 100644 backend/epc_api/json_samples/CEPC-8.0.0/dec-rr.json create mode 100644 backend/epc_api/json_samples/CEPC-8.0.0/dec.json create mode 100644 backend/epc_api/json_samples/RdSAP-Schema-17.0/epc.json create mode 100644 backend/epc_api/json_samples/RdSAP-Schema-17.1/epc.json create mode 100644 backend/epc_api/json_samples/RdSAP-Schema-18.0/epc.json create mode 100644 backend/epc_api/json_samples/RdSAP-Schema-19.0/epc.json create mode 100644 backend/epc_api/json_samples/RdSAP-Schema-20.0.0/epc.json create mode 100644 backend/epc_api/json_samples/RdSAP-Schema-21.0.0/epc.json create mode 100644 backend/epc_api/json_samples/RdSAP-Schema-21.0.1/epc.json create mode 100644 backend/epc_api/json_samples/SAP-Schema-16.0/rdsap.json create mode 100644 backend/epc_api/json_samples/SAP-Schema-16.0/sap.json create mode 100644 backend/epc_api/json_samples/SAP-Schema-16.1/rdsap.json create mode 100644 backend/epc_api/json_samples/SAP-Schema-16.1/sap.json create mode 100644 backend/epc_api/json_samples/SAP-Schema-16.2/rdsap.json create mode 100644 backend/epc_api/json_samples/SAP-Schema-16.2/sap.json create mode 100644 backend/epc_api/json_samples/SAP-Schema-16.3/rdsap.json create mode 100644 backend/epc_api/json_samples/SAP-Schema-16.3/sap.json create mode 100644 backend/epc_api/json_samples/SAP-Schema-17.0/epc.json create mode 100644 backend/epc_api/json_samples/SAP-Schema-17.1/epc.json create mode 100644 backend/epc_api/json_samples/SAP-Schema-18.0.0/epc.json create mode 100644 backend/epc_api/json_samples/SAP-Schema-19.0.0/epc.json create mode 100644 backend/epc_api/json_samples/SAP-Schema-19.1.0/epc.json create mode 100644 backend/epc_api/json_samples/SAP-Schema-19.2.0/epc.json diff --git a/backend/SearchEpc.py b/backend/SearchEpc.py index a633176e..7d3a8b3d 100644 --- a/backend/SearchEpc.py +++ b/backend/SearchEpc.py @@ -2,6 +2,7 @@ import os import time import re +from typing import Optional from urllib.parse import urlencode import usaddress import pandas as pd @@ -162,7 +163,8 @@ class SearchEpc: property_type=None, fast=False, heating_system: [str, None] = None, - associated_uprns: [List[int] | None] = None + associated_uprns: [List[int] | None] = None, + lmk_key: Optional[str] = None, ): """ Address lines 1 and postcode are mandatory fields. The other address lines are optional @@ -225,6 +227,7 @@ class SearchEpc: # Be default, this is set to false. This flag indicates whether we should take the existing EPC, but use # the estimated EPC to clean missings self.clean_missing_on_expired = False + self.lmk_key = lmk_key def set_strict_property_type_search(self): """ @@ -289,18 +292,22 @@ class SearchEpc: else: return None - def _get_epc(self, params, size): + def _get_epc(self, params, size, lmk_key=None): """ To be called by get_epc() - not for external usage """ - url = os.path.join(self.client.domestic.host, "search") - if size: - url += "?" + urlencode({k: v for k, v in {"size": size}.items() if v}) + if lmk_key is not None: + url = os.path.join(self.client.domestic.host, f"certificate/{lmk_key}") + else: + url = os.path.join(self.client.domestic.host, "search") + if size: + url += "?" + urlencode({k: v for k, v in {"size": size}.items() if v}) for retry in range(self.max_retries): try: response = self.client.domestic.call(method="get", url=url, params=params) + if response: self.data = response return { @@ -341,6 +348,15 @@ class SearchEpc: self.data = output["response"] return output["msg"] + if self.lmk_key: + data = {"rows": []} + api_response = self._get_epc(params={}, size=size, lmk_key=self.lmk_key) + data["rows"].extend(api_response["response"]["rows"]) + api_response["msg"] = self.SUCCESS + self.data = data + self.data["rows"][0]["uprn"] = str(self.uprn) + return api_response["msg"] + if not self.uprn and not self.address1 and not self.postcode: raise ValueError("No search parameters provided") diff --git a/backend/addresses/Address.py b/backend/addresses/Address.py index f348b141..0d957679 100644 --- a/backend/addresses/Address.py +++ b/backend/addresses/Address.py @@ -48,6 +48,10 @@ class Address: # domna_full_address: Optional[str] # domna_address_1: Optional[str] + # Identifiers to handle the case where we have erroneous matches based on UPRN due to probelmatic EPC data + lmk_key: Optional[str] + epc_certificate_number: Optional[str] + @property def request_data(self) -> dict[str, Optional[str]]: """ diff --git a/backend/addresses/Addresses.py b/backend/addresses/Addresses.py index c1624522..0496e95e 100644 --- a/backend/addresses/Addresses.py +++ b/backend/addresses/Addresses.py @@ -134,6 +134,16 @@ class Addresses: postcode = str(row.get("postcode", "")).strip().upper() + lmk_key = row.get("lmk_key", None) + # Handle NAN + if pd.isnull(lmk_key): + lmk_key = None + epc_certificate_number = row.get("certificate_number", None) + + landlord_heating_system = row.get("epc_heating_type", None) + if pd.isnull(landlord_heating_system): + landlord_heating_system = None + return Address( uprn=uprn, landlord_property_id=str(row["landlord_property_id"]) if row.get("landlord_property_id") else None, @@ -153,7 +163,7 @@ class Addresses: landlord_roof_construction=None, landlord_floor_construction=None, landlord_windows_type=None, - landlord_heating_system=row.get("epc_heating_type"), + landlord_heating_system=landlord_heating_system, landlord_fuel_type=None, landlord_heating_controls=None, landlord_hot_water_system=None, @@ -168,4 +178,8 @@ class Addresses: landlord_has_sloping_ceiling=None, landlord_multi_glaze_proportion=None, landlord_construction_age_band=None, + + # EPC identifiers which are helpful if UPRN is problematic + lmk_key=lmk_key, + epc_certificate_number=epc_certificate_number ) diff --git a/backend/engine/engine.py b/backend/engine/engine.py index b1b57529..67c9dd4e 100644 --- a/backend/engine/engine.py +++ b/backend/engine/engine.py @@ -582,7 +582,8 @@ async def model_engine(body: PlanTriggerRequest): os_api_key="", full_address=addr.full_address, heating_system=addr.landlord_heating_system, - associated_uprns=associated_uprns + associated_uprns=associated_uprns, + lmk_key=addr.lmk_key ) epc_searcher.ordnance_survey_client.built_form = addr.landlord_built_form epc_searcher.ordnance_survey_client.property_type = addr.landlord_property_type @@ -627,7 +628,12 @@ async def model_engine(body: PlanTriggerRequest): # if we have a remote assment data type, we pull the additional data and include it epc_page_source, find_my_epc_components = {}, [] - if (body.event_type == "remote_assessment") and not (epc_searcher.newest_epc.get("estimated")): + if ((body.event_type == "remote_assessment") and not ( + epc_searcher.newest_epc.get("estimated")) + ) or addr.epc_certificate_number: + + if addr.epc_certificate_number: + rrn = addr.epc_certificate_number property_non_invasive_recommendations, patch, epc_page_source, find_my_epc_components = ( RetrieveFindMyEpc.get_from_epc_with_fallback( epc=epc_searcher.newest_epc, @@ -641,6 +647,10 @@ async def model_engine(body: PlanTriggerRequest): epc_records = patch_epc(patch, epc_records) + # Hack - temp while we're planning to rebuild backend + if addr.epc_certificate_number is not None and epc_records["original_epc"].get("estimated"): + epc_records["original_epc"]["estimated"] = False + prepared_epc = EPCRecord( epc_records=epc_records, run_mode="newdata", cleaning_data=cleaning_data, address_metadata=addr ) diff --git a/backend/epc_api/api_spec.md b/backend/epc_api/api_spec.md new file mode 100644 index 00000000..5e1c309d --- /dev/null +++ b/backend/epc_api/api_spec.md @@ -0,0 +1 @@ +Hello World \ No newline at end of file diff --git a/backend/epc_api/json_samples/CEPC-7.0/cepc+rr.json b/backend/epc_api/json_samples/CEPC-7.0/cepc+rr.json new file mode 100644 index 00000000..876603e7 --- /dev/null +++ b/backend/epc_api/json_samples/CEPC-7.0/cepc+rr.json @@ -0,0 +1,278 @@ +{ + "ber": 158.9, + "ser": 59.26, + "ter": 39.95, + "tyr": 106.52, + "uprn": 12457, + "status": "entered", + "postcode": "PT42 7AD", + "post_town": "POSTTOWN", + "issue_date": "2013-08-16", + "methodology": "SBEM", + "report_type": 3, + "schema_type": "CEPC-7.0", + "uprn_source": "Energy Assessor", + "valid_until": "2023-08-15", + "asset_rating": 134, + "language_code": 1, + "output_engine": "EPCgen, v4.1.e.5", + "property_type": "A3/A4/A5 Restaurant and Cafes/Drinking Establishments and Hot Food takeaways", + "address_line_2": "Acme Coffee", + "address_line_3": "13 Old Street", + "assessment_type": "CEPC", + "inspection_date": "2013-08-10", + "inspection_type": "Physical", + "ac_questionnaire": { + "ac_present": "No", + "ac_rated_output": { + "ac_rating_unknown_flag": 1 + }, + "ac_inspection_commissioned": 4 + }, + "calculation_tool": "CLG, iSBEM, v4.1.e, SBEM, v4.1.e.5", + "is_heritage_site": "N", + "transaction_type": 1, + "registration_date": "2013-08-15", + "building_complexity": "Level 3", + "new_build_benchmark": 34, + "location_description": "Located in main shopping area in Posttown town centre", + "technical_information": { + "floor_area": 314, + "building_level": 3, + "main_heating_fuel": "Grid Supplied Electricity", + "building_environment": "Heating and Natural Ventilation" + }, + "summary_of_performance": { + "building_data": [ + { + "area": 313.8, + "weather": "BEL", + "activities": [ + { + "id": 1010, + "area": 142.6 + }, + { + "id": 1030, + "area": 66.9 + }, + { + "id": 1032, + "area": 58.4 + }, + { + "id": 1036, + "area": 4.8 + }, + { + "id": 1031, + "area": 41.1 + } + ], + "building_w_k": 1317.44, + "hvac_systems": [ + { + "area": 142.6, + "type": "No Heating or Cooling", + "fuel_type": "Oil", + "activities": [ + { + "id": 1010, + "area": 142.6 + } + ], + "heat_source": "LTHW boiler", + "cooling_sseer": 0, + "heating_sseff": 0, + "kwh_m2_cooling": 0, + "kwh_m2_heating": 0, + "cooling_gen_seer": 0, + "heating_gen_seff": 0, + "kwh_m2_auxiliary": 0, + "mj_m2_cooling_dem": 47.0757, + "mj_m2_heating_dem": 604.144 + }, + { + "area": 171.2, + "type": "Other local room heater - unfanned", + "fuel_type": "Grid Supplied Electricity", + "activities": [ + { + "id": 1030, + "area": 66.9 + }, + { + "id": 1032, + "area": 58.4 + }, + { + "id": 1036, + "area": 4.8 + }, + { + "id": 1031, + "area": 41.1 + } + ], + "heat_source": "Room heater", + "cooling_sseer": 0, + "heating_sseff": 0.8, + "kwh_m2_cooling": 0, + "kwh_m2_heating": 255.695, + "cooling_gen_seer": 0, + "heating_gen_seff": 1, + "kwh_m2_auxiliary": 1.95411, + "mj_m2_cooling_dem": 683.721, + "mj_m2_heating_dem": 736.402 + } + ], + "analysis_type": "ACTUAL", + "area_exterior": 873.7, + "building_alpha": 3.81869, + "building_w_m2k": 1.50789, + "q50_infiltration": 25, + "global_performance": { + "kwh_m2_chp": 0, + "kwh_m2_dhw": 27.3554, + "kwh_m2_lpg": 0, + "kwh_m2_oil": 0, + "kwh_m2_pvs": 0, + "kwh_m2_ses": 0, + "kwh_m2_coal": 0, + "kwh_m2_wind": 0, + "kwh_m2_biogas": 0, + "kwh_m2_biomass": 0, + "kwh_m2_cooling": 0, + "kwh_m2_heating": 139.499, + "kwh_m2_lighting": 139.423, + "kwh_m2_supplied": 307.344, + "kwh_m2_auxiliary": 1.06611, + "kwh_m2_displaced": 0, + "kwh_m2_dual_fuel": 0, + "kwh_m2_equipment": 66.704, + "kwh_m2_smokeless": 0, + "kwh_m2_anthracite": 0, + "kwh_m2_waste_heat": 0, + "kwh_m2_natural_gas": 0, + "kwh_m2_district_heating": 0 + } + }, + { + "area": 313.8, + "weather": "BEL", + "activities": [ + { + "id": 1010, + "area": 142.6 + }, + { + "id": 1030, + "area": 66.9 + }, + { + "id": 1032, + "area": 58.4 + }, + { + "id": 1036, + "area": 4.8 + }, + { + "id": 1031, + "area": 41.1 + } + ], + "building_w_k": 301.464, + "hvac_systems": [ + { + "area": 142.6, + "type": "No Heating or Cooling", + "fuel_type": "Oil", + "activities": [ + { + "id": 1010, + "area": 142.6 + } + ], + "heat_source": "LTHW boiler", + "cooling_sseer": 0, + "heating_sseff": 0, + "kwh_m2_cooling": 0, + "kwh_m2_heating": 0, + "cooling_gen_seer": 0, + "heating_gen_seff": 0, + "kwh_m2_auxiliary": 0, + "mj_m2_cooling_dem": 18.7903, + "mj_m2_heating_dem": 179.595 + }, + { + "area": 171.2, + "type": "Other local room heater - unfanned", + "fuel_type": "Oil", + "activities": [ + { + "id": 1030, + "area": 66.9 + }, + { + "id": 1032, + "area": 58.4 + }, + { + "id": 1036, + "area": 4.8 + }, + { + "id": 1031, + "area": 41.1 + } + ], + "heat_source": "Room heater", + "cooling_sseer": 0, + "heating_sseff": 0.792, + "kwh_m2_cooling": 0, + "kwh_m2_heating": 104.881, + "cooling_gen_seer": 0, + "heating_gen_seff": 0, + "kwh_m2_auxiliary": 0.521096, + "mj_m2_cooling_dem": 287.737, + "mj_m2_heating_dem": 299.036 + } + ], + "analysis_type": "NOTIONAL", + "area_exterior": 873.7, + "building_alpha": 42.6344, + "building_w_m2k": 0.345043, + "q50_infiltration": 6.0896, + "global_performance": { + "kwh_m2_chp": 0, + "kwh_m2_dhw": 32.702, + "kwh_m2_lpg": 0, + "kwh_m2_oil": 89.9222, + "kwh_m2_pvs": 0, + "kwh_m2_ses": 0, + "kwh_m2_coal": 0, + "kwh_m2_wind": 0, + "kwh_m2_biogas": 0, + "kwh_m2_biomass": 0, + "kwh_m2_cooling": 0, + "kwh_m2_heating": 57.2199, + "kwh_m2_lighting": 25.9805, + "kwh_m2_supplied": 26.2648, + "kwh_m2_auxiliary": 0.284295, + "kwh_m2_displaced": 0, + "kwh_m2_dual_fuel": 0, + "kwh_m2_equipment": 66.704, + "kwh_m2_smokeless": 0, + "kwh_m2_anthracite": 0, + "kwh_m2_waste_heat": 0, + "kwh_m2_natural_gas": 0, + "kwh_m2_district_heating": 0 + } + } + ] + }, + "existing_stock_benchmark": 90, + "related_certificate_number": "4444-5555-6666-7777-8889", + "current_energy_efficiency_band": "F" +} \ No newline at end of file diff --git a/backend/epc_api/json_samples/CEPC-7.0/cepc-rr.json b/backend/epc_api/json_samples/CEPC-7.0/cepc-rr.json new file mode 100644 index 00000000..9e594325 --- /dev/null +++ b/backend/epc_api/json_samples/CEPC-7.0/cepc-rr.json @@ -0,0 +1,60 @@ +{ + "uprn": 12457, + "status": "cancelled", + "postcode": "SW1A 2AA", + "post_town": "Fulchester", + "issue_date": "2020-05-14", + "report_type": 4, + "schema_type": "CEPC-7.0", + "uprn_source": "Energy Assessor", + "valid_until": "2021-05-03", + "long_payback": [ + { + "co2_impact": "HIGH", + "recommendation": "Consider installing an air source heat pump.", + "recommendation_code": "EPC-R5" + } + ], + "language_code": 1, + "other_payback": [ + { + "co2_impact": "HIGH", + "recommendation": "Consider installing PV.", + "recommendation_code": "EPC-R4" + } + ], + "property_type": "B1 Offices and Workshop businesses", + "short_payback": [ + { + "co2_impact": "HIGH", + "recommendation": "Consider replacing T8 lamps with retrofit T5 conversion kit.", + "recommendation_code": "ECP-L5" + }, + { + "co2_impact": "LOW", + "recommendation": "Introduce HF (high frequency) ballasts for fluorescent tubes: Reduced number of fittings required.", + "recommendation_code": "EPC-L7" + } + ], + "address_line_1": "Some Unit", + "address_line_2": "2 Lonely Street", + "address_line_3": "Some Area", + "address_line_4": "Some County", + "medium_payback": [ + { + "co2_impact": "MEDIUM", + "recommendation": "Add optimum start/stop to the heating system.", + "recommendation_code": "EPC-H7" + } + ], + "assessment_type": "CEPC-RR", + "inspection_date": "2020-05-04", + "calculation_tool": "CEPC Compute v0.2", + "formatted_report": "ZGVmYXVsdA==", + "registration_date": "2020-05-05", + "technical_information": { + "floor_area": 10, + "building_environment": "Natural Ventilation Only" + }, + "related_certificate_number": "0000-0000-0000-0000-0001" +} \ No newline at end of file diff --git a/backend/epc_api/json_samples/CEPC-7.0/cepc.json b/backend/epc_api/json_samples/CEPC-7.0/cepc.json new file mode 100644 index 00000000..d65c34bf --- /dev/null +++ b/backend/epc_api/json_samples/CEPC-7.0/cepc.json @@ -0,0 +1,285 @@ +{ + "ber": 67.09, + "ser": 42.07, + "ter": 23.2, + "tyr": 67.98, + "uprn": 12457, + "status": "entered", + "postcode": "SW1A 2AA", + "post_town": "Whitbury", + "issue_date": "2020-05-14", + "report_type": 3, + "schema_type": "CEPC-7.0", + "uprn_source": "Energy Assessor", + "valid_until": "2026-05-04", + "asset_rating": 80, + "language_code": 1, + "property_type": "B1 Offices and Workshop businesses", + "address_line_1": "Some Unit", + "address_line_2": "2 Lonely Street", + "address_line_3": "Some Area", + "address_line_4": "Some County", + "assessment_type": "CEPC", + "inspection_date": "2020-05-04", + "ac_questionnaire": { + "ac_present": "No", + "ac_rated_output": { + "ac_kw_rating": 100 + }, + "ac_estimated_output": 3, + "ac_inspection_commissioned": 1 + }, + "calculation_tool": "Casio fx-39", + "is_heritage_site": "N", + "or_building_data": { + "hvac_system": "Yes", + "other_hvac_system": "Yes", + "internal_environment": "Yes", + "assessment_period_alignment": "Yes" + }, + "transaction_type": 1, + "or_benchmark_data": { + "benchmarks": [ + { + "floor_area": 403, + "benchmark_id": 1 + } + ] + }, + "registration_date": "2020-05-04", + "building_complexity": "Level 3", + "new_build_benchmark": 28, + "or_usable_floor_area": { + "ufa_1": { + "floor_area": 100 + } + }, + "or_energy_consumption": { + "anthracite": { + "end_date": "2030-12-25", + "estimate": 3, + "start_date": "2019-12-25", + "consumption": 12.6 + } + }, + "technical_information": { + "floor_area": 403, + "building_level": 3, + "main_heating_fuel": "Natural Gas", + "renewable_sources": "Renewable sources test", + "special_energy_uses": "Test sp", + "building_environment": "Air Conditioning", + "or_availability_date": "2020-01-04", + "other_fuel_description": "Test" + }, + "or_assessment_end_date": "2030-05-14", + "summary_of_performance": { + "building_data": [ + { + "area": 402.6, + "weather": "LON", + "activities": [ + { + "id": 1005, + "area": 402.6 + } + ], + "building_w_k": 411.86, + "hvac_systems": [ + { + "area": 402.6, + "type": "Fan coil systems", + "fuel_type": "Natural Gas", + "activities": [ + { + "id": 1005, + "area": 402.6 + } + ], + "heat_source": "LTHW boiler", + "cooling_sseer": 1.963, + "heating_sseff": 0.827, + "kwh_m2_cooling": 25.3865, + "kwh_m2_heating": 41.0355, + "cooling_gen_seer": 2.5, + "heating_gen_seff": 0.89, + "kwh_m2_auxiliary": 39.0832, + "mj_m2_cooling_dem": 179.401, + "mj_m2_heating_dem": 122.171 + } + ], + "analysis_type": "ACTUAL", + "area_exterior": 1058.32, + "building_alpha": 18.3412, + "building_w_m2k": 0.389164, + "q50_infiltration": 10, + "global_performance": { + "kwh_m2_chp": 0, + "kwh_m2_dhw": 2.77834, + "kwh_m2_lpg": 0, + "kwh_m2_oil": 0, + "kwh_m2_pvs": 3.07665, + "kwh_m2_ses": 0.801605, + "kwh_m2_coal": 0, + "kwh_m2_wind": 3.02777, + "kwh_m2_biogas": 0, + "kwh_m2_biomass": 0, + "kwh_m2_cooling": 25.3865, + "kwh_m2_heating": 41.0355, + "kwh_m2_lighting": 54.0787, + "kwh_m2_supplied": 121.327, + "kwh_m2_auxiliary": 39.0832, + "kwh_m2_displaced": 6.10442, + "kwh_m2_dual_fuel": 0, + "kwh_m2_equipment": 42.185, + "kwh_m2_smokeless": 0, + "kwh_m2_anthracite": 0, + "kwh_m2_waste_heat": 0, + "kwh_m2_natural_gas": 41.0355, + "kwh_m2_district_heating": 0 + } + }, + { + "area": 402.6, + "weather": "LON", + "activities": [ + { + "id": 1005, + "area": 402.6 + } + ], + "building_w_k": 362.524, + "hvac_systems": [ + { + "area": 402.6, + "type": "Fan coil systems", + "fuel_type": "Natural Gas", + "activities": [ + { + "id": 1005, + "area": 402.6 + } + ], + "heat_source": "LTHW boiler", + "cooling_sseer": 3.6, + "heating_sseff": 0.819, + "kwh_m2_cooling": 7.46769, + "kwh_m2_heating": 18.1581, + "cooling_gen_seer": 0, + "heating_gen_seff": 0, + "kwh_m2_auxiliary": 14.0716, + "mj_m2_cooling_dem": 96.7814, + "mj_m2_heating_dem": 53.5375 + } + ], + "analysis_type": "NOTIONAL", + "area_exterior": 1058.32, + "building_alpha": 11.2489, + "building_w_m2k": 0.342547, + "q50_infiltration": 3, + "global_performance": { + "kwh_m2_chp": 0, + "kwh_m2_dhw": 3.33761, + "kwh_m2_lpg": 0, + "kwh_m2_oil": 3.33761, + "kwh_m2_pvs": 0, + "kwh_m2_ses": 0, + "kwh_m2_coal": 0, + "kwh_m2_wind": 0, + "kwh_m2_biogas": 0, + "kwh_m2_biomass": 0, + "kwh_m2_cooling": 7.46769, + "kwh_m2_heating": 18.1582, + "kwh_m2_lighting": 14.4489, + "kwh_m2_supplied": 35.9883, + "kwh_m2_auxiliary": 14.0716, + "kwh_m2_displaced": 0, + "kwh_m2_dual_fuel": 0, + "kwh_m2_equipment": 42.185, + "kwh_m2_smokeless": 0, + "kwh_m2_anthracite": 0, + "kwh_m2_waste_heat": 0, + "kwh_m2_natural_gas": 18.1582, + "kwh_m2_district_heating": 0 + } + }, + { + "area": 402.6, + "weather": "LON", + "activities": [ + { + "id": 1005, + "area": 402.6 + } + ], + "building_w_k": 718.761, + "hvac_systems": [ + { + "area": 402.6, + "type": "Fan coil systems", + "fuel_type": "Natural Gas", + "activities": [ + { + "id": 1005, + "area": 402.6 + } + ], + "heat_source": "LTHW boiler", + "cooling_sseer": 2.25, + "heating_sseff": 0.73, + "kwh_m2_cooling": 25.6538, + "kwh_m2_heating": 71.9516, + "cooling_gen_seer": 0, + "heating_gen_seff": 0, + "kwh_m2_auxiliary": 2.16062, + "mj_m2_cooling_dem": 207.795, + "mj_m2_heating_dem": 189.088 + } + ], + "analysis_type": "REFERENCE", + "area_exterior": 1058.32, + "building_alpha": 10, + "building_w_m2k": 0.679153, + "q50_infiltration": 10, + "global_performance": { + "kwh_m2_chp": 0, + "kwh_m2_dhw": 6.41192, + "kwh_m2_lpg": 0, + "kwh_m2_oil": 0, + "kwh_m2_pvs": 0, + "kwh_m2_ses": 0, + "kwh_m2_coal": 0, + "kwh_m2_wind": 0, + "kwh_m2_biogas": 0, + "kwh_m2_biomass": 0, + "kwh_m2_cooling": 25.6538, + "kwh_m2_heating": 71.9516, + "kwh_m2_lighting": 45.54, + "kwh_m2_supplied": 73.3544, + "kwh_m2_auxiliary": 2.16062, + "kwh_m2_displaced": 0, + "kwh_m2_dual_fuel": 0, + "kwh_m2_equipment": 42.185, + "kwh_m2_smokeless": 0, + "kwh_m2_anthracite": 0, + "kwh_m2_waste_heat": 0, + "kwh_m2_natural_gas": 78.3634, + "kwh_m2_district_heating": 0 + } + } + ] + }, + "renewable_energy_source": [ + { + "name": "Solar panel", + "end_date": "2030-12-25", + "generation": 20, + "start_date": "2019-12-25", + "energy_type": 2 + } + ], + "existing_stock_benchmark": 81, + "or_assessment_start_date": "2020-05-14", + "related_certificate_number": "4192-1535-8427-8844-6702", + "current_energy_efficiency_band": "D" +} \ No newline at end of file diff --git a/backend/epc_api/json_samples/CEPC-7.0/dec+rr.json b/backend/epc_api/json_samples/CEPC-7.0/dec+rr.json new file mode 100644 index 00000000..ed560a1b --- /dev/null +++ b/backend/epc_api/json_samples/CEPC-7.0/dec+rr.json @@ -0,0 +1,128 @@ +{ + "uprn": 12457, + "status": "entered", + "postcode": "PT4 1SK", + "post_town": "POSTTOWN", + "dec_status": 0, + "issue_date": "2016-02-23", + "methodology": "ORCalc", + "reason_type": 2, + "report_type": 1, + "schema_type": "CEPC-7.0", + "uprn_source": "Energy Assessor", + "valid_until": "2017-04-24", + "language_code": 1, + "output_engine": "ORGen v3.6.2", + "property_type": "Schools And Seasonal Public Buildings; Swimming Pool Centre", + "address_line_1": "Mr Blobby's Sports Academy", + "address_line_2": "Mr Blobby's Academy", + "address_line_3": "Blobby Custard Lane", + "assessment_type": "DEC", + "inspection_date": "2016-04-11", + "this_assessment": { + "heating_co2": 153, + "energy_rating": 77, + "nominated_date": "2016-02-23", + "renewables_co2": 0, + "electricity_co2": 163 + }, + "ac_questionnaire": { + "ac_present": "Yes", + "ac_rated_output": { + "ac_rating_unknown_flag": 1 + }, + "ac_estimated_output": 2, + "ac_inspection_commissioned": 4 + }, + "calculation_tool": "Property-Tectonics Ltd, Lifespan DEC, v3.6.1", + "or_building_data": { + "hvac_system": "Radiators", + "internal_environment": "Mixed-mode with Natural Ventilation", + "assessment_period_alignment": "End Of Main Heating Fuel Period" + }, + "or_previous_data": { + "asset_rating": 0, + "previous_rating_1": { + "or": 95, + "ormm": 11, + "oryyyy": 2014 + }, + "previous_rating_2": { + "or": 113, + "ormm": 11, + "oryyyy": 2013 + } + }, + "year1_assessment": { + "heating_co2": 176, + "energy_rating": 95, + "nominated_date": "2014-11-30", + "renewables_co2": 0, + "electricity_co2": 266 + }, + "year2_assessment": { + "heating_co2": 238, + "energy_rating": 113, + "nominated_date": "2013-11-30", + "renewables_co2": 0, + "electricity_co2": 333 + }, + "building_category": "S3; H6", + "or_benchmark_data": { + "benchmarks": [ + { + "name": "Secondary school", + "tufa": 4646.48, + "benchmark": "Schools And Seasonal Public Buildings", + "floor_area": 4646.48, + "area_metric": "Gross floor area measured as RICS Gross Internal Area (GIA)", + "benchmark_id": 1, + "occupancy_level": "Extended Occupancy" + }, + { + "name": "Swimming pool", + "tufa": 254.52, + "benchmark": "Swimming Pool Centre", + "floor_area": 254.52, + "area_metric": "Gross floor area measured as RICS Gross Internal Area (GIA)", + "benchmark_id": 2, + "occupancy_level": "Extended Occupancy" + } + ], + "main_benchmark": "Schools And Seasonal Public Buildings" + }, + "registration_date": "2016-04-25", + "or_energy_consumption": { + "gas": { + "end_date": "2016-01-01", + "estimate": 0, + "start_date": "2015-01-01", + "consumption": 786655 + }, + "electricity": { + "end_date": "2015-12-31", + "estimate": 0, + "start_date": "2015-01-01", + "consumption": 296820 + } + }, + "technical_information": { + "floor_area": 4901, + "main_heating_fuel": "Natural Gas", + "building_environment": "Mixed-mode with Natural Ventilation", + "separately_metered_electric_heating": 0 + }, + "or_assessment_end_date": "2016-01-01", + "or_assessment_start_date": "2015-01-01", + "dec_annual_energy_summary": { + "typical_thermal_use": 244, + "renewables_electrical": 0, + "typical_electrical_use": 67, + "renewables_fuel_thermal": 0, + "annual_energy_use_electrical": 61, + "annual_energy_use_fuel_thermal": 161 + }, + "related_certificate_number": "3333-4444-5555-6666-7778", + "dec_related_party_disclosure": 3, + "current_energy_efficiency_band": "D" +} \ No newline at end of file diff --git a/backend/epc_api/json_samples/CEPC-7.0/dec-rr.json b/backend/epc_api/json_samples/CEPC-7.0/dec-rr.json new file mode 100644 index 00000000..1974d031 --- /dev/null +++ b/backend/epc_api/json_samples/CEPC-7.0/dec-rr.json @@ -0,0 +1,76 @@ +{ + "uprn": 12457, + "status": "entered", + "postcode": "SW1A 2AA", + "post_town": "Fulchester", + "issue_date": "2020-05-04", + "report_type": 2, + "schema_type": "CEPC-7.0", + "uprn_source": "Energy Assessor", + "valid_until": "2028-05-03", + "long_payback": [ + { + "co2_impact": "LOW", + "recommendation": "Consider replacing or improving glazing", + "recommendation_code": "ECP-F4" + } + ], + "language_code": 1, + "other_payback": [ + { + "co2_impact": "HIGH", + "recommendation": "Add a big wind turbine", + "recommendation_code": "ECP-H2" + } + ], + "property_type": "University campus", + "short_payback": [ + { + "co2_impact": "MEDIUM", + "recommendation": "Consider thinking about maybe possibly getting a solar panel but only one.", + "recommendation_code": "ECP-L5" + }, + { + "co2_impact": "LOW", + "recommendation": "Consider introducing variable speed drives (VSD) for fans, pumps and compressors.", + "recommendation_code": "EPC-L7" + } + ], + "site_services": { + "service_1": { + "quantity": 751445, + "description": "Electricity" + }, + "service_2": { + "quantity": 72956, + "description": "Gas" + }, + "service_3": { + "quantity": 0, + "description": "Not used" + } + }, + "address_line_1": "Some Unit", + "address_line_2": "2 Lonely Street", + "address_line_3": "Some Area", + "address_line_4": "Some County", + "medium_payback": [ + { + "co2_impact": "LOW", + "recommendation": "Engage experts to propose specific measures to reduce hot waterwastage and plan to carry this out.", + "recommendation_code": "ECP-C1" + } + ], + "assessment_type": "DEC-RR", + "inspection_date": "2020-05-04", + "inspection_type": "Physical", + "calculation_tool": "DCLG, ORCalc, v3.6.2", + "formatted_report": "ZGVmYXVsdA==", + "registration_date": "2020-05-04", + "technical_information": { + "floor_area": 10, + "renewable_sources": "Renewable source", + "special_energy_uses": "Special discount", + "building_environment": "Air Conditioning" + } +} \ No newline at end of file diff --git a/backend/epc_api/json_samples/CEPC-7.1/cepc+rr.json b/backend/epc_api/json_samples/CEPC-7.1/cepc+rr.json new file mode 100644 index 00000000..a41ca736 --- /dev/null +++ b/backend/epc_api/json_samples/CEPC-7.1/cepc+rr.json @@ -0,0 +1,264 @@ +{ + "ber": 135.65, + "ser": 72.8, + "ter": 39.05, + "tyr": 114.44, + "uprn": 12457, + "status": "entered", + "postcode": "A1 1AA", + "post_town": "New Town", + "energy_use": { + "energy_consumption_current": 802.38 + }, + "issue_date": "2017-10-13", + "methodology": "SBEM", + "report_type": 3, + "schema_type": "CEPC-7.1", + "uprn_source": "Energy Assessor", + "valid_until": "2027-10-12", + "asset_rating": 93, + "language_code": 1, + "output_engine": "EPCgen, v5.3.a.0", + "property_type": "A1/A2 Retail and Financial/Professional services", + "address_line_1": "99a Address Street", + "address_line_2": "Place Location", + "assessment_type": "CEPC", + "inspection_date": "2017-10-10", + "inspection_type": "Physical", + "ac_questionnaire": { + "ac_present": "No", + "ac_rated_output": { + "ac_rating_unknown_flag": 1 + }, + "ac_inspection_commissioned": 4 + }, + "calculation_tool": "CLG, iSBEM, v5.3.a, SBEM, v5.3.a.0", + "is_heritage_site": "N", + "transaction_type": 6, + "registration_date": "2017-10-13", + "building_complexity": "Level 3", + "new_build_benchmark": 27, + "technical_information": { + "floor_area": 222, + "building_level": 3, + "main_heating_fuel": "Grid Supplied Electricity", + "building_environment": "Heating and Natural Ventilation" + }, + "summary_of_performance": { + "building_data": [ + { + "area": 221.66, + "weather": "NEW", + "activities": [ + { + "id": 1078, + "area": 6.84 + }, + { + "id": 1077, + "area": 3.42 + }, + { + "id": 1074, + "area": 43.89 + }, + { + "id": 1305, + "area": 167.51 + } + ], + "building_w_k": 756.843, + "hvac_systems": [ + { + "area": 54.15, + "type": "Other local room heater - fanned", + "fuel_type": "Grid Supplied Electricity", + "activities": [ + { + "id": 1078, + "area": 6.84 + }, + { + "id": 1077, + "area": 3.42 + }, + { + "id": 1074, + "area": 43.89 + } + ], + "heat_source": "Room heater", + "cooling_sseer": 0, + "heating_sseff": 0.8, + "kwh_m2_cooling": 0, + "kwh_m2_heating": 221.413, + "cooling_gen_seer": 0, + "heating_gen_seff": 1, + "kwh_m2_auxiliary": 8.58942, + "mj_m2_cooling_dem": 82.8076, + "mj_m2_heating_dem": 637.668 + }, + { + "area": 167.51, + "type": "Other local room heater - unfanned", + "fuel_type": "Grid Supplied Electricity", + "activities": [ + { + "id": 1305, + "area": 167.51 + } + ], + "heat_source": "Room heater", + "cooling_sseer": 0, + "heating_sseff": 0.8, + "kwh_m2_cooling": 0, + "kwh_m2_heating": 143.224, + "cooling_gen_seer": 0, + "heating_gen_seff": 1, + "kwh_m2_auxiliary": 0, + "mj_m2_cooling_dem": 277.439, + "mj_m2_heating_dem": 412.486 + } + ], + "analysis_type": "ACTUAL", + "area_exterior": 578.13, + "building_alpha": 5.48977, + "building_w_m2k": 1.30912, + "q50_infiltration": 25, + "global_performance": { + "kwh_m2_chp": 0, + "kwh_m2_dhw": 1.42751, + "kwh_m2_lpg": 0, + "kwh_m2_oil": 0, + "kwh_m2_pvs": 0, + "kwh_m2_ses": 0, + "kwh_m2_coal": 0, + "kwh_m2_wind": 0, + "kwh_m2_biogas": 0, + "kwh_m2_biomass": 0, + "kwh_m2_cooling": 0, + "kwh_m2_heating": 162.325, + "kwh_m2_lighting": 95.5093, + "kwh_m2_supplied": 261.361, + "kwh_m2_auxiliary": 2.09834, + "kwh_m2_displaced": 0, + "kwh_m2_dual_fuel": 0, + "kwh_m2_equipment": 17.0788, + "kwh_m2_smokeless": 0, + "kwh_m2_anthracite": 0, + "kwh_m2_waste_heat": 0, + "kwh_m2_natural_gas": 0, + "kwh_m2_district_heating": 0 + } + }, + { + "area": 221.66, + "weather": "NEW", + "activities": [ + { + "id": 1078, + "area": 6.84 + }, + { + "id": 1077, + "area": 3.42 + }, + { + "id": 1074, + "area": 43.89 + }, + { + "id": 1305, + "area": 167.51 + } + ], + "building_w_k": 141.605, + "hvac_systems": [ + { + "area": 54.15, + "type": "Other local room heater - fanned", + "fuel_type": "Oil", + "activities": [ + { + "id": 1078, + "area": 6.84 + }, + { + "id": 1077, + "area": 3.42 + }, + { + "id": 1074, + "area": 43.89 + } + ], + "heat_source": "Room heater", + "cooling_sseer": 0, + "heating_sseff": 0.819, + "kwh_m2_cooling": 0, + "kwh_m2_heating": 83.8955, + "cooling_gen_seer": 0, + "heating_gen_seff": 0, + "kwh_m2_auxiliary": 2.29051, + "mj_m2_cooling_dem": 32.988, + "mj_m2_heating_dem": 247.357 + }, + { + "area": 167.51, + "type": "Other local room heater - unfanned", + "fuel_type": "Oil", + "activities": [ + { + "id": 1305, + "area": 167.51 + } + ], + "heat_source": "Room heater", + "cooling_sseer": 0, + "heating_sseff": 0.819, + "kwh_m2_cooling": 0, + "kwh_m2_heating": 18.2439, + "cooling_gen_seer": 0, + "heating_gen_seff": 0, + "kwh_m2_auxiliary": 0, + "mj_m2_cooling_dem": 153.68, + "mj_m2_heating_dem": 53.7903 + } + ], + "analysis_type": "NOTIONAL", + "area_exterior": 578.13, + "building_alpha": 12.3616, + "building_w_m2k": 0.244936, + "q50_infiltration": 5, + "global_performance": { + "kwh_m2_chp": 0, + "kwh_m2_dhw": 1.64373, + "kwh_m2_lpg": 0, + "kwh_m2_oil": 35.9259, + "kwh_m2_pvs": 0, + "kwh_m2_ses": 0, + "kwh_m2_coal": 0, + "kwh_m2_wind": 0, + "kwh_m2_biogas": 0, + "kwh_m2_biomass": 0, + "kwh_m2_cooling": 0, + "kwh_m2_heating": 34.2821, + "kwh_m2_lighting": 53.9687, + "kwh_m2_supplied": 54.5286, + "kwh_m2_auxiliary": 0.559555, + "kwh_m2_displaced": 0, + "kwh_m2_dual_fuel": 0, + "kwh_m2_equipment": 17.0788, + "kwh_m2_smokeless": 0, + "kwh_m2_anthracite": 0, + "kwh_m2_waste_heat": 0, + "kwh_m2_natural_gas": 0, + "kwh_m2_district_heating": 0 + } + } + ] + }, + "existing_stock_benchmark": 79, + "related_certificate_number": "9999-9999-3333-9999-2222", + "current_energy_efficiency_band": "D" +} \ No newline at end of file diff --git a/backend/epc_api/json_samples/CEPC-7.1/cepc-rr.json b/backend/epc_api/json_samples/CEPC-7.1/cepc-rr.json new file mode 100644 index 00000000..be34f330 --- /dev/null +++ b/backend/epc_api/json_samples/CEPC-7.1/cepc-rr.json @@ -0,0 +1,60 @@ +{ + "uprn": 12457, + "status": "cancelled", + "postcode": "SW1A 2AA", + "post_town": "Fulchester", + "issue_date": "2020-05-14", + "report_type": 4, + "schema_type": "CEPC-7.1", + "uprn_source": "Energy Assessor", + "valid_until": "2021-05-03", + "long_payback": [ + { + "co2_impact": "HIGH", + "recommendation": "Consider installing an air source heat pump.", + "recommendation_code": "EPC-R5" + } + ], + "language_code": 1, + "other_payback": [ + { + "co2_impact": "HIGH", + "recommendation": "Consider installing PV.", + "recommendation_code": "EPC-R4" + } + ], + "property_type": "B1 Offices and Workshop businesses", + "short_payback": [ + { + "co2_impact": "HIGH", + "recommendation": "Consider replacing T8 lamps with retrofit T5 conversion kit.", + "recommendation_code": "ECP-L5" + }, + { + "co2_impact": "LOW", + "recommendation": "Introduce HF (high frequency) ballasts for fluorescent tubes: Reduced number of fittings required.", + "recommendation_code": "EPC-L7" + } + ], + "address_line_1": "Some Unit", + "address_line_2": "2 Lonely Street", + "address_line_3": "Some Area", + "address_line_4": "Some County", + "medium_payback": [ + { + "co2_impact": "MEDIUM", + "recommendation": "Add optimum start/stop to the heating system.", + "recommendation_code": "EPC-H7" + } + ], + "assessment_type": "CEPC-RR", + "inspection_date": "2020-05-04", + "calculation_tool": "CEPC Compute v0.2", + "formatted_report": "ZGVmYXVsdA==", + "registration_date": "2020-05-05", + "technical_information": { + "floor_area": 10, + "building_environment": "Natural Ventilation Only" + }, + "related_certificate_number": "0000-0000-0000-0000-0001" +} \ No newline at end of file diff --git a/backend/epc_api/json_samples/CEPC-7.1/cepc.json b/backend/epc_api/json_samples/CEPC-7.1/cepc.json new file mode 100644 index 00000000..f89954e6 --- /dev/null +++ b/backend/epc_api/json_samples/CEPC-7.1/cepc.json @@ -0,0 +1,288 @@ +{ + "ber": 67.09, + "ser": 42.07, + "ter": 23.2, + "tyr": 67.98, + "uprn": 12457, + "status": "entered", + "postcode": "BT4 3SR", + "post_town": "Whitbury", + "energy_use": { + "energy_consumption_current": 413.22 + }, + "issue_date": "2020-05-14", + "report_type": 3, + "schema_type": "CEPC-7.1", + "uprn_source": "Energy Assessor", + "valid_until": "2026-05-04", + "asset_rating": 80, + "language_code": 1, + "property_type": "B1 Offices and Workshop businesses", + "address_line_1": "Some Unit", + "address_line_2": "2 Lonely Street", + "address_line_3": "Some Area", + "address_line_4": "Some County", + "assessment_type": "CEPC", + "inspection_date": "2020-05-04", + "ac_questionnaire": { + "ac_present": "No", + "ac_rated_output": { + "ac_kw_rating": 100 + }, + "ac_estimated_output": 3, + "ac_inspection_commissioned": 1 + }, + "calculation_tool": "Casio fx-39", + "is_heritage_site": "N", + "or_building_data": { + "hvac_system": "Yes", + "other_hvac_system": "Yes", + "internal_environment": "Yes", + "assessment_period_alignment": "Yes" + }, + "transaction_type": 1, + "or_benchmark_data": { + "benchmarks": [ + { + "floor_area": 403, + "benchmark_id": 1 + } + ] + }, + "registration_date": "2020-05-04", + "building_complexity": "Level 3", + "new_build_benchmark": 28, + "or_usable_floor_area": { + "ufa_1": { + "floor_area": 100 + } + }, + "or_energy_consumption": { + "anthracite": { + "end_date": "2030-12-25", + "estimate": 3, + "start_date": "2019-12-25", + "consumption": 12.6 + } + }, + "technical_information": { + "floor_area": 403, + "building_level": 3, + "main_heating_fuel": "Natural Gas", + "renewable_sources": "Renewable sources test", + "special_energy_uses": "Test sp", + "building_environment": "Air Conditioning", + "or_availability_date": "2020-01-04", + "other_fuel_description": "Test" + }, + "or_assessment_end_date": "2030-05-14", + "summary_of_performance": { + "building_data": [ + { + "area": 402.6, + "weather": "LON", + "activities": [ + { + "id": 1005, + "area": 402.6 + } + ], + "building_w_k": 411.86, + "hvac_systems": [ + { + "area": 402.6, + "type": "Fan coil systems", + "fuel_type": "Natural Gas", + "activities": [ + { + "id": 1005, + "area": 402.6 + } + ], + "heat_source": "LTHW boiler", + "cooling_sseer": 1.963, + "heating_sseff": 0.827, + "kwh_m2_cooling": 25.3865, + "kwh_m2_heating": 41.0355, + "cooling_gen_seer": 2.5, + "heating_gen_seff": 0.89, + "kwh_m2_auxiliary": 39.0832, + "mj_m2_cooling_dem": 179.401, + "mj_m2_heating_dem": 122.171 + } + ], + "analysis_type": "ACTUAL", + "area_exterior": 1058.32, + "building_alpha": 18.3412, + "building_w_m2k": 0.389164, + "q50_infiltration": 10, + "global_performance": { + "kwh_m2_chp": 0, + "kwh_m2_dhw": 2.77834, + "kwh_m2_lpg": 0, + "kwh_m2_oil": 0, + "kwh_m2_pvs": 3.07665, + "kwh_m2_ses": 0.801605, + "kwh_m2_coal": 0, + "kwh_m2_wind": 3.02777, + "kwh_m2_biogas": 0, + "kwh_m2_biomass": 0, + "kwh_m2_cooling": 25.3865, + "kwh_m2_heating": 41.0355, + "kwh_m2_lighting": 54.0787, + "kwh_m2_supplied": 121.327, + "kwh_m2_auxiliary": 39.0832, + "kwh_m2_displaced": 6.10442, + "kwh_m2_dual_fuel": 0, + "kwh_m2_equipment": 42.185, + "kwh_m2_smokeless": 0, + "kwh_m2_anthracite": 0, + "kwh_m2_waste_heat": 0, + "kwh_m2_natural_gas": 41.0355, + "kwh_m2_district_heating": 0 + } + }, + { + "area": 402.6, + "weather": "LON", + "activities": [ + { + "id": 1005, + "area": 402.6 + } + ], + "building_w_k": 362.524, + "hvac_systems": [ + { + "area": 402.6, + "type": "Fan coil systems", + "fuel_type": "Natural Gas", + "activities": [ + { + "id": 1005, + "area": 402.6 + } + ], + "heat_source": "LTHW boiler", + "cooling_sseer": 3.6, + "heating_sseff": 0.819, + "kwh_m2_cooling": 7.46769, + "kwh_m2_heating": 18.1581, + "cooling_gen_seer": 0, + "heating_gen_seff": 0, + "kwh_m2_auxiliary": 14.0716, + "mj_m2_cooling_dem": 96.7814, + "mj_m2_heating_dem": 53.5375 + } + ], + "analysis_type": "NOTIONAL", + "area_exterior": 1058.32, + "building_alpha": 11.2489, + "building_w_m2k": 0.342547, + "q50_infiltration": 3, + "global_performance": { + "kwh_m2_chp": 0, + "kwh_m2_dhw": 3.33761, + "kwh_m2_lpg": 0, + "kwh_m2_oil": 3.33761, + "kwh_m2_pvs": 0, + "kwh_m2_ses": 0, + "kwh_m2_coal": 0, + "kwh_m2_wind": 0, + "kwh_m2_biogas": 0, + "kwh_m2_biomass": 0, + "kwh_m2_cooling": 7.46769, + "kwh_m2_heating": 18.1582, + "kwh_m2_lighting": 14.4489, + "kwh_m2_supplied": 35.9883, + "kwh_m2_auxiliary": 14.0716, + "kwh_m2_displaced": 0, + "kwh_m2_dual_fuel": 0, + "kwh_m2_equipment": 42.185, + "kwh_m2_smokeless": 0, + "kwh_m2_anthracite": 0, + "kwh_m2_waste_heat": 0, + "kwh_m2_natural_gas": 18.1582, + "kwh_m2_district_heating": 0 + } + }, + { + "area": 402.6, + "weather": "LON", + "activities": [ + { + "id": 1005, + "area": 402.6 + } + ], + "building_w_k": 718.761, + "hvac_systems": [ + { + "area": 402.6, + "type": "Fan coil systems", + "fuel_type": "Natural Gas", + "activities": [ + { + "id": 1005, + "area": 402.6 + } + ], + "heat_source": "LTHW boiler", + "cooling_sseer": 2.25, + "heating_sseff": 0.73, + "kwh_m2_cooling": 25.6538, + "kwh_m2_heating": 71.9516, + "cooling_gen_seer": 0, + "heating_gen_seff": 0, + "kwh_m2_auxiliary": 2.16062, + "mj_m2_cooling_dem": 207.795, + "mj_m2_heating_dem": 189.088 + } + ], + "analysis_type": "REFERENCE", + "area_exterior": 1058.32, + "building_alpha": 10, + "building_w_m2k": 0.679153, + "q50_infiltration": 10, + "global_performance": { + "kwh_m2_chp": 0, + "kwh_m2_dhw": 6.41192, + "kwh_m2_lpg": 0, + "kwh_m2_oil": 0, + "kwh_m2_pvs": 0, + "kwh_m2_ses": 0, + "kwh_m2_coal": 0, + "kwh_m2_wind": 0, + "kwh_m2_biogas": 0, + "kwh_m2_biomass": 0, + "kwh_m2_cooling": 25.6538, + "kwh_m2_heating": 71.9516, + "kwh_m2_lighting": 45.54, + "kwh_m2_supplied": 73.3544, + "kwh_m2_auxiliary": 2.16062, + "kwh_m2_displaced": 0, + "kwh_m2_dual_fuel": 0, + "kwh_m2_equipment": 42.185, + "kwh_m2_smokeless": 0, + "kwh_m2_anthracite": 0, + "kwh_m2_waste_heat": 0, + "kwh_m2_natural_gas": 78.3634, + "kwh_m2_district_heating": 0 + } + } + ] + }, + "renewable_energy_source": [ + { + "name": "Solar panel", + "end_date": "2030-12-25", + "generation": 20, + "start_date": "2019-12-25", + "energy_type": 2 + } + ], + "existing_stock_benchmark": 81, + "or_assessment_start_date": "2020-05-14", + "related_certificate_number": "4192-1535-8427-8844-6702", + "current_energy_efficiency_band": "D" +} \ No newline at end of file diff --git a/backend/epc_api/json_samples/CEPC-7.1/dec+rr.json b/backend/epc_api/json_samples/CEPC-7.1/dec+rr.json new file mode 100644 index 00000000..f03bd052 --- /dev/null +++ b/backend/epc_api/json_samples/CEPC-7.1/dec+rr.json @@ -0,0 +1,118 @@ +{ + "uprn": 12457, + "status": "entered", + "postcode": "A1 1AA", + "post_town": "Town", + "dec_status": 0, + "issue_date": "2015-12-14", + "methodology": "ORCalc", + "reason_type": 1, + "report_type": 1, + "schema_type": "CEPC-7.1", + "uprn_source": "Energy Assessor", + "valid_until": "2016-12-29", + "language_code": 1, + "output_engine": "ORGen v3.6.2", + "property_type": "Schools And Seasonal Public Buildings", + "address_line_2": "Place Early Years Centre", + "address_line_3": "Address Road", + "assessment_type": "DEC", + "inspection_date": "2015-12-09", + "this_assessment": { + "heating_co2": 28, + "energy_rating": 80, + "nominated_date": "2015-12-30", + "renewables_co2": 0, + "electricity_co2": 33 + }, + "ac_questionnaire": { + "ac_present": "No", + "ac_rated_output": { + "ac_rating_unknown_flag": 1 + }, + "ac_inspection_commissioned": 4 + }, + "calculation_tool": "DCLG, ORCalc, v3.6.2", + "or_building_data": { + "hvac_system": "Convectors", + "internal_environment": "Heating and Natural Ventilation", + "assessment_period_alignment": "End Of Main Heating Fuel Period" + }, + "or_previous_data": { + "previous_rating_1": { + "or": 75, + "ormm": 12, + "oryyyy": 2014 + }, + "previous_rating_2": { + "or": 75, + "ormm": 12, + "oryyyy": 2013 + } + }, + "year1_assessment": { + "heating_co2": 24, + "energy_rating": 75, + "nominated_date": "2014-12-01", + "renewables_co2": 0, + "electricity_co2": 30 + }, + "year2_assessment": { + "heating_co2": 29, + "energy_rating": 75, + "nominated_date": "2013-12-01", + "renewables_co2": 0, + "electricity_co2": 31 + }, + "building_category": "S3;", + "or_benchmark_data": { + "benchmarks": [ + { + "name": "Nursery or kindergarten", + "tufa": 1219.2, + "benchmark": "Schools And Seasonal Public Buildings", + "floor_area": 1219.2, + "area_metric": "Gross floor area measured as RICS Gross Internal Area (GIA)", + "benchmark_id": 1, + "occupancy_level": "Extended Occupancy", + "total_equivalent": 2437.5 + } + ], + "main_benchmark": "Schools And Seasonal Public Buildings" + }, + "registration_date": "2015-12-14", + "location_description": "Refurbished and extended nursery school building with flat and pitched roof areas, double glazed windows and cavity walls. The building is heated by gas fired boilers.", + "or_energy_consumption": { + "gas": { + "end_date": "2015-09-30", + "estimate": 0, + "start_date": "2014-09-30", + "consumption": 143533 + }, + "electricity": { + "end_date": "2015-10-01", + "estimate": 0, + "start_date": "2014-10-01", + "consumption": 59477 + } + }, + "technical_information": { + "floor_area": 1219.2, + "main_heating_fuel": "Natural Gas", + "building_environment": "Heating and Natural Ventilation", + "separately_metered_electric_heating": 0 + }, + "or_assessment_end_date": "2015-09-30", + "or_assessment_start_date": "2014-09-30", + "dec_annual_energy_summary": { + "typical_thermal_use": 176, + "renewables_electrical": 0, + "typical_electrical_use": 51, + "renewables_fuel_thermal": 0, + "annual_energy_use_electrical": 49, + "annual_energy_use_fuel_thermal": 118 + }, + "related_certificate_number": "0000-0000-0000-0000-0006", + "dec_related_party_disclosure": 4, + "current_energy_efficiency_band": "D" +} \ No newline at end of file diff --git a/backend/epc_api/json_samples/CEPC-7.1/dec-rr.json b/backend/epc_api/json_samples/CEPC-7.1/dec-rr.json new file mode 100644 index 00000000..95accca6 --- /dev/null +++ b/backend/epc_api/json_samples/CEPC-7.1/dec-rr.json @@ -0,0 +1,76 @@ +{ + "uprn": 12457, + "status": "entered", + "postcode": "SW1A 2AA", + "post_town": "Fulchester", + "issue_date": "2020-05-04", + "report_type": 2, + "schema_type": "CEPC-7.1", + "uprn_source": "Energy Assessor", + "valid_until": "2028-05-03", + "long_payback": [ + { + "co2_impact": "LOW", + "recommendation": "Consider replacing or improving glazing", + "recommendation_code": "ECP-F4" + } + ], + "language_code": 1, + "other_payback": [ + { + "co2_impact": "HIGH", + "recommendation": "Add a big wind turbine", + "recommendation_code": "ECP-H2" + } + ], + "property_type": "University campus", + "short_payback": [ + { + "co2_impact": "MEDIUM", + "recommendation": "Consider thinking about maybe possibly getting a solar panel but only one.", + "recommendation_code": "ECP-L5" + }, + { + "co2_impact": "LOW", + "recommendation": "Consider introducing variable speed drives (VSD) for fans, pumps and compressors.", + "recommendation_code": "EPC-L7" + } + ], + "site_services": { + "service_1": { + "quantity": 751445, + "description": "Electricity" + }, + "service_2": { + "quantity": 72956, + "description": "Gas" + }, + "service_3": { + "quantity": 0, + "description": "Not used" + } + }, + "address_line_1": "Some Unit", + "address_line_2": "2 Lonely Street", + "address_line_3": "Some Area", + "address_line_4": "Some County", + "medium_payback": [ + { + "co2_impact": "LOW", + "recommendation": "Engage experts to propose specific measures to reduce hot waterwastage and plan to carry this out.", + "recommendation_code": "ECP-C1" + } + ], + "assessment_type": "DEC-RR", + "inspection_date": "2020-05-04", + "inspection_type": "Physical", + "calculation_tool": "DCLG, ORCalc, v3.6.2", + "formatted_report": "ZGVmYXVsdA==", + "registration_date": "2020-05-04", + "technical_information": { + "floor_area": 10, + "renewable_sources": "Renewable source", + "special_energy_uses": "Special discount", + "building_environment": "Air Conditioning" + } +} \ No newline at end of file diff --git a/backend/epc_api/json_samples/CEPC-8.0.0/cepc+rr.json b/backend/epc_api/json_samples/CEPC-8.0.0/cepc+rr.json new file mode 100644 index 00000000..9ff6ca3a --- /dev/null +++ b/backend/epc_api/json_samples/CEPC-8.0.0/cepc+rr.json @@ -0,0 +1,468 @@ +{ + "ber": 76.29, + "ser": 45.61, + "ter": 31.05, + "tyr": 90.98, + "uprn": 12457, + "status": "entered", + "postcode": "NE0 0AA", + "post_town": "Big Rock", + "energy_use": { + "energy_consumption_current": 451.27 + }, + "issue_date": "2021-03-19", + "methodology": "SBEM", + "report_type": 3, + "schema_type": "CEPC-8.0.0", + "uprn_source": "Energy Assessor", + "valid_until": "2031-03-18", + "asset_rating": 84, + "language_code": 1, + "output_engine": "EPCgen, v5.6.b.0", + "property_type": "A1/A2 Retail and Financial/Professional services", + "address_line_1": "60 Maple Syrup Road", + "address_line_2": "Candy Mountain", + "assessment_type": "CEPC", + "inspection_date": "2021-03-19", + "inspection_type": "Physical", + "ac_questionnaire": { + "ac_present": "No", + "ac_rated_output": { + "ac_rating_unknown_flag": 1 + }, + "ac_inspection_commissioned": 4 + }, + "calculation_tool": "G-ISBEM Ltd, G-ISBEM, v24.0, SBEM, v5.6.b.0", + "is_heritage_site": "N", + "transaction_type": 1, + "registration_date": "2021-03-19", + "building_complexity": "Level 3", + "new_build_benchmark": 34, + "technical_information": { + "floor_area": 951, + "building_level": 3, + "main_heating_fuel": "Grid Supplied Electricity", + "building_environment": "Air Conditioning" + }, + "summary_of_performance": { + "building_data": [ + { + "area": 951.34, + "weather": "NEW", + "activities": [ + { + "id": 1067, + "area": 81.32 + }, + { + "id": 1070, + "area": 13.9 + }, + { + "id": 1074, + "area": 258.42 + }, + { + "id": 1078, + "area": 76.99 + }, + { + "id": 1071, + "area": 490.62 + }, + { + "id": 1077, + "area": 30.09 + } + ], + "building_w_k": 314.665, + "hvac_systems": [ + { + "area": 353.64, + "type": "No Heating or Cooling", + "fuel_type": "Oil", + "activities": [ + { + "id": 1067, + "area": 81.32 + }, + { + "id": 1070, + "area": 13.9 + }, + { + "id": 1074, + "area": 258.42 + } + ], + "heat_source": "LTHW boiler", + "cooling_sseer": 0, + "heating_sseff": 0, + "kwh_m2_cooling": 0, + "kwh_m2_heating": 0, + "cooling_gen_seer": 0, + "heating_gen_seff": 0, + "kwh_m2_auxiliary": 4.9793, + "mj_m2_cooling_dem": 44.0312, + "mj_m2_heating_dem": 79.1732 + }, + { + "area": 567.61, + "type": "Split or multi-split system", + "fuel_type": "Grid Supplied Electricity", + "activities": [ + { + "id": 1078, + "area": 76.99 + }, + { + "id": 1071, + "area": 490.62 + } + ], + "heat_source": "Heat pump: air source", + "cooling_sseer": 2.457, + "heating_sseff": 3.466, + "kwh_m2_cooling": 65.3618, + "kwh_m2_heating": 2.17225, + "cooling_gen_seer": 3.46, + "heating_gen_seff": 3.72, + "kwh_m2_auxiliary": 6.9318, + "mj_m2_cooling_dem": 578.138, + "mj_m2_heating_dem": 27.1043 + }, + { + "area": 30.09, + "type": "Other local room heater - unfanned", + "fuel_type": "Grid Supplied Electricity", + "activities": [ + { + "id": 1077, + "area": 30.09 + } + ], + "heat_source": "Direct or storage electric heater", + "cooling_sseer": 0, + "heating_sseff": 0.8, + "kwh_m2_cooling": 0, + "kwh_m2_heating": 101.261, + "cooling_gen_seer": 0, + "heating_gen_seff": 1, + "kwh_m2_auxiliary": 28.4481, + "mj_m2_cooling_dem": 72.0834, + "mj_m2_heating_dem": 291.63 + } + ], + "analysis_type": "ACTUAL", + "area_exterior": 509.17, + "building_alpha": 4.09978, + "building_w_m2k": 0.617996, + "q50_infiltration": 25, + "global_performance": { + "kwh_m2_chp": 0, + "kwh_m2_dhw": 1.61017, + "kwh_m2_lpg": 0, + "kwh_m2_oil": 0, + "kwh_m2_pvs": 0, + "kwh_m2_ses": 0, + "kwh_m2_coal": 0, + "kwh_m2_wind": 0, + "kwh_m2_biogas": 0, + "kwh_m2_biomass": 0, + "kwh_m2_cooling": 38.9976, + "kwh_m2_heating": 4.49883, + "kwh_m2_lighting": 94.9991, + "kwh_m2_supplied": 146.993, + "kwh_m2_auxiliary": 6.88654, + "kwh_m2_displaced": 0, + "kwh_m2_dual_fuel": 0, + "kwh_m2_equipment": 19.5261, + "kwh_m2_smokeless": 0, + "kwh_m2_anthracite": 0, + "kwh_m2_waste_heat": 0, + "kwh_m2_natural_gas": 0, + "kwh_m2_district_heating": 0 + } + }, + { + "area": 951.34, + "weather": "NEW", + "activities": [ + { + "id": 1067, + "area": 81.32 + }, + { + "id": 1070, + "area": 13.9 + }, + { + "id": 1074, + "area": 258.42 + }, + { + "id": 1078, + "area": 76.99 + }, + { + "id": 1071, + "area": 490.62 + }, + { + "id": 1077, + "area": 30.09 + } + ], + "building_w_k": 106.722, + "hvac_systems": [ + { + "area": 353.64, + "type": "No Heating or Cooling", + "fuel_type": "Oil", + "activities": [ + { + "id": 1067, + "area": 81.32 + }, + { + "id": 1070, + "area": 13.9 + }, + { + "id": 1074, + "area": 258.42 + } + ], + "heat_source": "LTHW boiler", + "cooling_sseer": 0, + "heating_sseff": 0, + "kwh_m2_cooling": 0, + "kwh_m2_heating": 0, + "cooling_gen_seer": 0, + "heating_gen_seff": 0, + "kwh_m2_auxiliary": 2.98759, + "mj_m2_cooling_dem": 22.3547, + "mj_m2_heating_dem": 24.8966 + }, + { + "area": 567.61, + "type": "Split or multi-split system", + "fuel_type": "Grid Supplied Electricity", + "activities": [ + { + "id": 1078, + "area": 76.99 + }, + { + "id": 1071, + "area": 490.62 + } + ], + "heat_source": "Heat pump: air source", + "cooling_sseer": 3.6, + "heating_sseff": 2.43, + "kwh_m2_cooling": 23.8278, + "kwh_m2_heating": 0.00251615, + "cooling_gen_seer": 0, + "heating_gen_seff": 0, + "kwh_m2_auxiliary": 3.77382, + "mj_m2_cooling_dem": 308.809, + "mj_m2_heating_dem": 0.0220112 + }, + { + "area": 30.09, + "type": "Other local room heater - unfanned", + "fuel_type": "Oil", + "activities": [ + { + "id": 1077, + "area": 30.09 + } + ], + "heat_source": "Direct or storage electric heater", + "cooling_sseer": 0, + "heating_sseff": 0.819, + "kwh_m2_cooling": 0, + "kwh_m2_heating": 35.9561, + "cooling_gen_seer": 0, + "heating_gen_seff": 0, + "kwh_m2_auxiliary": 7.58617, + "mj_m2_cooling_dem": 78.2393, + "mj_m2_heating_dem": 106.013 + } + ], + "analysis_type": "NOTIONAL", + "area_exterior": 509.17, + "building_alpha": 12.5855, + "building_w_m2k": 0.2096, + "q50_infiltration": 3, + "global_performance": { + "kwh_m2_chp": 0, + "kwh_m2_dhw": 1.45546, + "kwh_m2_lpg": 0, + "kwh_m2_oil": 2.59272, + "kwh_m2_pvs": 0, + "kwh_m2_ses": 0, + "kwh_m2_coal": 0, + "kwh_m2_wind": 0, + "kwh_m2_biogas": 0, + "kwh_m2_biomass": 0, + "kwh_m2_cooling": 14.2167, + "kwh_m2_heating": 1.13875, + "kwh_m2_lighting": 41.9026, + "kwh_m2_supplied": 59.7228, + "kwh_m2_auxiliary": 3.60214, + "kwh_m2_displaced": 0, + "kwh_m2_dual_fuel": 0, + "kwh_m2_equipment": 19.5261, + "kwh_m2_smokeless": 0, + "kwh_m2_anthracite": 0, + "kwh_m2_waste_heat": 0, + "kwh_m2_natural_gas": 0, + "kwh_m2_district_heating": 0 + } + }, + { + "area": 951.34, + "weather": "NEW", + "activities": [ + { + "id": 1067, + "area": 81.32 + }, + { + "id": 1070, + "area": 13.9 + }, + { + "id": 1074, + "area": 258.42 + }, + { + "id": 1078, + "area": 76.99 + }, + { + "id": 1071, + "area": 490.62 + }, + { + "id": 1077, + "area": 30.09 + } + ], + "building_w_k": 193.204, + "hvac_systems": [ + { + "area": 353.64, + "type": "No Heating or Cooling", + "fuel_type": "Natural Gas", + "activities": [ + { + "id": 1067, + "area": 81.32 + }, + { + "id": 1070, + "area": 13.9 + }, + { + "id": 1074, + "area": 258.42 + } + ], + "heat_source": "LTHW boiler", + "cooling_sseer": 0, + "heating_sseff": 0, + "kwh_m2_cooling": 0, + "kwh_m2_heating": 0, + "cooling_gen_seer": 0, + "heating_gen_seff": 0, + "kwh_m2_auxiliary": 0, + "mj_m2_cooling_dem": 13.1255, + "mj_m2_heating_dem": 71.1616 + }, + { + "area": 567.61, + "type": "Split or multi-split system", + "fuel_type": "Natural Gas", + "activities": [ + { + "id": 1078, + "area": 76.99 + }, + { + "id": 1071, + "area": 490.62 + } + ], + "heat_source": "LTHW boiler", + "cooling_sseer": 2.25, + "heating_sseff": 0.73, + "kwh_m2_cooling": 49.5997, + "kwh_m2_heating": 6.49072, + "cooling_gen_seer": 0, + "heating_gen_seff": 0, + "kwh_m2_auxiliary": 2.47934, + "mj_m2_cooling_dem": 401.756, + "mj_m2_heating_dem": 17.0576 + }, + { + "area": 30.09, + "type": "Other local room heater - unfanned", + "fuel_type": "Natural Gas", + "activities": [ + { + "id": 1077, + "area": 30.09 + } + ], + "heat_source": "LTHW boiler", + "cooling_sseer": 2.25, + "heating_sseff": 0.73, + "kwh_m2_cooling": 25.4056, + "kwh_m2_heating": 84.553, + "cooling_gen_seer": 0, + "heating_gen_seff": 0, + "kwh_m2_auxiliary": 12.1545, + "mj_m2_cooling_dem": 205.785, + "mj_m2_heating_dem": 222.205 + } + ], + "analysis_type": "REFERENCE", + "area_exterior": 509.17, + "building_alpha": 10, + "building_w_m2k": 0.379449, + "q50_infiltration": 10, + "global_performance": { + "kwh_m2_chp": 0, + "kwh_m2_dhw": 2.7961, + "kwh_m2_lpg": 0, + "kwh_m2_oil": 0, + "kwh_m2_pvs": 0, + "kwh_m2_ses": 0, + "kwh_m2_coal": 0, + "kwh_m2_wind": 0, + "kwh_m2_biogas": 0, + "kwh_m2_biomass": 0, + "kwh_m2_cooling": 30.3968, + "kwh_m2_heating": 6.54699, + "kwh_m2_lighting": 78.7393, + "kwh_m2_supplied": 110.999, + "kwh_m2_auxiliary": 1.86372, + "kwh_m2_displaced": 0, + "kwh_m2_dual_fuel": 0, + "kwh_m2_equipment": 19.5261, + "kwh_m2_smokeless": 0, + "kwh_m2_anthracite": 0, + "kwh_m2_waste_heat": 0, + "kwh_m2_natural_gas": 9.34308, + "kwh_m2_district_heating": 0 + } + } + ] + }, + "existing_stock_benchmark": 100, + "related_certificate_number": "0000-0000-0000-0000-0001", + "current_energy_efficiency_band": "D" +} \ No newline at end of file diff --git a/backend/epc_api/json_samples/CEPC-8.0.0/cepc-rr.json b/backend/epc_api/json_samples/CEPC-8.0.0/cepc-rr.json new file mode 100644 index 00000000..065f54bb --- /dev/null +++ b/backend/epc_api/json_samples/CEPC-8.0.0/cepc-rr.json @@ -0,0 +1,60 @@ +{ + "uprn": 12457, + "status": "cancelled", + "postcode": "SW1A 2AA", + "post_town": "Fulchester", + "issue_date": "2020-05-14", + "report_type": 4, + "schema_type": "CEPC-8.0.0", + "uprn_source": "Energy Assessor", + "valid_until": "2021-05-03", + "long_payback": [ + { + "co2_impact": "HIGH", + "recommendation": "Consider installing an air source heat pump.", + "recommendation_code": "EPC-R5" + } + ], + "language_code": 1, + "other_payback": [ + { + "co2_impact": "HIGH", + "recommendation": "Consider installing PV.", + "recommendation_code": "EPC-R4" + } + ], + "property_type": "B1 Offices and Workshop businesses", + "short_payback": [ + { + "co2_impact": "HIGH", + "recommendation": "Consider replacing T8 lamps with retrofit T5 conversion kit.", + "recommendation_code": "ECP-L5" + }, + { + "co2_impact": "LOW", + "recommendation": "Introduce HF (high frequency) ballasts for fluorescent tubes: Reduced number of fittings required.", + "recommendation_code": "EPC-L7" + } + ], + "address_line_1": "Some Unit", + "address_line_2": "2 Lonely Street", + "address_line_3": "Some Area", + "address_line_4": "Some County", + "medium_payback": [ + { + "co2_impact": "MEDIUM", + "recommendation": "Add optimum start/stop to the heating system.", + "recommendation_code": "EPC-H7" + } + ], + "assessment_type": "CEPC-RR", + "inspection_date": "2020-05-04", + "calculation_tool": "CEPC Compute v0.2", + "formatted_report": "ZGVmYXVsdA==", + "registration_date": "2020-05-05", + "technical_information": { + "floor_area": 10, + "building_environment": "Natural Ventilation Only" + }, + "related_certificate_number": "0000-0000-0000-0000-0001" +} \ No newline at end of file diff --git a/backend/epc_api/json_samples/CEPC-8.0.0/cepc.json b/backend/epc_api/json_samples/CEPC-8.0.0/cepc.json new file mode 100644 index 00000000..ec747ee7 --- /dev/null +++ b/backend/epc_api/json_samples/CEPC-8.0.0/cepc.json @@ -0,0 +1,334 @@ +{ + "ber": 76.29, + "ser": 45.61, + "ter": 31.05, + "tyr": 90.98, + "uprn": 12457, + "status": "entered", + "postcode": "NE0 0AA", + "post_town": "Big Rock", + "energy_use": { + "energy_consumption_current": 451.27 + }, + "issue_date": "2021-03-19", + "methodology": "SBEM", + "report_type": 3, + "schema_type": "CEPC-8.0.0", + "uprn_source": "Energy Assessor", + "valid_until": "2031-03-18", + "asset_rating": 84, + "language_code": 1, + "output_engine": "EPCgen, v5.6.b.0", + "property_type": "A1/A2 Retail and Financial/Professional services", + "address_line_1": "60 Maple Syrup Road", + "address_line_2": "Candy Mountain", + "assessment_type": "CEPC", + "inspection_date": "2021-03-19", + "inspection_type": "Physical", + "ac_questionnaire": { + "ac_present": "No", + "ac_rated_output": { + "ac_kw_rating": 100, + "ac_rating_unknown_flag": 0 + }, + "ac_estimated_output": 3, + "ac_inspection_commissioned": 4 + }, + "calculation_tool": "G-ISBEM Ltd, G-ISBEM, v24.0, SBEM, v5.6.b.0", + "is_heritage_site": "N", + "transaction_type": 1, + "registration_date": "2021-03-19", + "building_complexity": "Level 3", + "new_build_benchmark": 34, + "technical_information": { + "floor_area": 951, + "building_level": 3, + "main_heating_fuel": "Grid Supplied Electricity", + "renewable_sources": "Renewable sources test", + "special_energy_uses": "Test sp", + "building_environment": "Air Conditioning", + "other_fuel_description": "Other fuel test" + }, + "summary_of_performance": { + "building_data": [ + { + "area": 951.34, + "weather": "NEW", + "activities": [ + { + "id": 1067, + "area": 81.32 + }, + { + "id": 1070, + "area": 13.9 + }, + { + "id": 1074, + "area": 258.42 + }, + { + "id": 1078, + "area": 76.99 + }, + { + "id": 1071, + "area": 490.62 + }, + { + "id": 1077, + "area": 30.09 + } + ], + "building_w_k": 193.204, + "hvac_systems": [ + { + "area": 353.64, + "type": "No Heating or Cooling", + "fuel_type": "Natural Gas", + "activities": [ + { + "id": 1067, + "area": 81.32 + }, + { + "id": 1070, + "area": 13.9 + }, + { + "id": 1074, + "area": 258.42 + } + ], + "heat_source": "LTHW boiler", + "cooling_sseer": 0, + "heating_sseff": 0, + "kwh_m2_cooling": 0, + "kwh_m2_heating": 0, + "cooling_gen_seer": 0, + "heating_gen_seff": 0, + "kwh_m2_auxiliary": 0, + "mj_m2_cooling_dem": 13.1255, + "mj_m2_heating_dem": 71.1616 + }, + { + "area": 567.61, + "type": "Split or multi-split system", + "fuel_type": "Natural Gas", + "activities": [ + { + "id": 1078, + "area": 76.99 + }, + { + "id": 1071, + "area": 490.62 + } + ], + "heat_source": "LTHW boiler", + "cooling_sseer": 2.25, + "heating_sseff": 0.73, + "kwh_m2_cooling": 49.5997, + "kwh_m2_heating": 6.49072, + "cooling_gen_seer": 0, + "heating_gen_seff": 0, + "kwh_m2_auxiliary": 2.47934, + "mj_m2_cooling_dem": 401.756, + "mj_m2_heating_dem": 17.0576 + }, + { + "area": 30.09, + "type": "Other local room heater - unfanned", + "fuel_type": "Natural Gas", + "activities": [ + { + "id": 1077, + "area": 30.09 + } + ], + "heat_source": "LTHW boiler", + "cooling_sseer": 2.25, + "heating_sseff": 0.73, + "kwh_m2_cooling": 25.4056, + "kwh_m2_heating": 84.553, + "cooling_gen_seer": 0, + "heating_gen_seff": 0, + "kwh_m2_auxiliary": 12.1545, + "mj_m2_cooling_dem": 205.785, + "mj_m2_heating_dem": 222.205 + } + ], + "analysis_type": "REFERENCE", + "area_exterior": 509.17, + "building_alpha": 10, + "building_w_m2k": 0.379449, + "q50_infiltration": 10, + "global_performance": { + "kwh_m2_chp": 0, + "kwh_m2_dhw": 2.7961, + "kwh_m2_lpg": 0, + "kwh_m2_oil": 0, + "kwh_m2_pvs": 0, + "kwh_m2_ses": 0, + "kwh_m2_coal": 0, + "kwh_m2_wind": 0, + "kwh_m2_biogas": 0, + "kwh_m2_biomass": 0, + "kwh_m2_cooling": 30.3968, + "kwh_m2_heating": 6.54699, + "kwh_m2_lighting": 78.7393, + "kwh_m2_supplied": 110.999, + "kwh_m2_auxiliary": 1.86372, + "kwh_m2_displaced": 0, + "kwh_m2_dual_fuel": 0, + "kwh_m2_equipment": 19.5261, + "kwh_m2_smokeless": 0, + "kwh_m2_anthracite": 0, + "kwh_m2_waste_heat": 0, + "kwh_m2_natural_gas": 9.34308, + "kwh_m2_district_heating": 0 + } + }, + { + "area": 951.34, + "weather": "NEW", + "activities": [ + { + "id": 1067, + "area": 81.32 + }, + { + "id": 1070, + "area": 13.9 + }, + { + "id": 1074, + "area": 258.42 + }, + { + "id": 1078, + "area": 76.99 + }, + { + "id": 1071, + "area": 490.62 + }, + { + "id": 1077, + "area": 30.09 + } + ], + "building_w_k": 193.204, + "hvac_systems": [ + { + "area": 353.64, + "type": "No Heating or Cooling", + "fuel_type": "Natural Gas", + "activities": [ + { + "id": 1067, + "area": 81.32 + }, + { + "id": 1070, + "area": 13.9 + }, + { + "id": 1074, + "area": 258.42 + } + ], + "heat_source": "LTHW boiler", + "cooling_sseer": 0, + "heating_sseff": 0, + "kwh_m2_cooling": 0, + "kwh_m2_heating": 0, + "cooling_gen_seer": 0, + "heating_gen_seff": 0, + "kwh_m2_auxiliary": 0, + "mj_m2_cooling_dem": 13.1255, + "mj_m2_heating_dem": 71.1616 + }, + { + "area": 567.61, + "type": "Split or multi-split system", + "fuel_type": "Natural Gas", + "activities": [ + { + "id": 1078, + "area": 76.99 + }, + { + "id": 1071, + "area": 490.62 + } + ], + "heat_source": "LTHW boiler", + "cooling_sseer": 2.25, + "heating_sseff": 0.73, + "kwh_m2_cooling": 49.5997, + "kwh_m2_heating": 6.49072, + "cooling_gen_seer": 0, + "heating_gen_seff": 0, + "kwh_m2_auxiliary": 2.47934, + "mj_m2_cooling_dem": 401.756, + "mj_m2_heating_dem": 17.0576 + }, + { + "area": 30.09, + "type": "Other local room heater - unfanned", + "fuel_type": "Natural Gas", + "activities": [ + { + "id": 1077, + "area": 30.09 + } + ], + "heat_source": "LTHW boiler", + "cooling_sseer": 2.25, + "heating_sseff": 0.73, + "kwh_m2_cooling": 25.4056, + "kwh_m2_heating": 84.553, + "cooling_gen_seer": 0, + "heating_gen_seff": 0, + "kwh_m2_auxiliary": 12.1545, + "mj_m2_cooling_dem": 205.785, + "mj_m2_heating_dem": 222.205 + } + ], + "analysis_type": "ACTUAL", + "area_exterior": 509.17, + "building_alpha": 10, + "building_w_m2k": 0.379449, + "q50_infiltration": 10, + "global_performance": { + "kwh_m2_chp": 0, + "kwh_m2_dhw": 2.7961, + "kwh_m2_lpg": 0, + "kwh_m2_oil": 0, + "kwh_m2_pvs": 0, + "kwh_m2_ses": 0, + "kwh_m2_coal": 0, + "kwh_m2_wind": 0, + "kwh_m2_biogas": 0, + "kwh_m2_biomass": 0, + "kwh_m2_cooling": 30.3968, + "kwh_m2_heating": 6.54699, + "kwh_m2_lighting": 78.7393, + "kwh_m2_supplied": 110.999, + "kwh_m2_auxiliary": 1.86372, + "kwh_m2_displaced": 0, + "kwh_m2_dual_fuel": 0, + "kwh_m2_equipment": 19.5261, + "kwh_m2_smokeless": 0, + "kwh_m2_anthracite": 0, + "kwh_m2_waste_heat": 0, + "kwh_m2_natural_gas": 9.34308, + "kwh_m2_district_heating": 0 + } + } + ] + }, + "existing_stock_benchmark": 100, + "current_energy_efficiency_band": "D" +} \ No newline at end of file diff --git a/backend/epc_api/json_samples/CEPC-8.0.0/dec+rr.json b/backend/epc_api/json_samples/CEPC-8.0.0/dec+rr.json new file mode 100644 index 00000000..83cfac0c --- /dev/null +++ b/backend/epc_api/json_samples/CEPC-8.0.0/dec+rr.json @@ -0,0 +1,131 @@ +{ + "uprn": 12457, + "status": "entered", + "postcode": "A00 0AA", + "post_town": "Floatering", + "dec_status": 0, + "issue_date": "2021-10-12", + "methodology": "ORCalc", + "reason_type": 1, + "report_type": 1, + "schema_type": "CEPC-8.0.0", + "uprn_source": "Energy Assessor", + "valid_until": "2022-01-31", + "language_code": 1, + "output_engine": "ORGen v4.0.4", + "property_type": "Fitness And Health Centre; Swimming Pool Centre", + "address_line_1": "Swim & Fitness Centre", + "address_line_2": "Swimming Lane", + "assessment_type": "DEC", + "inspection_date": "2021-09-02", + "this_assessment": { + "heating_co2": 156, + "energy_rating": 42, + "nominated_date": "2021-09-01", + "renewables_co2": 13, + "electricity_co2": 70 + }, + "ac_questionnaire": { + "ac_present": "Yes", + "ac_rated_output": { + "ac_kw_rating": 30 + }, + "ac_inspection_commissioned": 1 + }, + "calculation_tool": "CLG, ORCalc, v4.0.4", + "or_building_data": { + "hvac_system": "Radiators", + "internal_environment": "Heating and Mechanical Ventilation", + "assessment_period_alignment": "End Of Main Heating Fuel Period" + }, + "or_previous_data": { + "asset_rating": 45 + }, + "building_category": "H7; H6;", + "or_benchmark_data": { + "benchmarks": [ + { + "name": "Gymnasium", + "tufa": 110.382, + "benchmark": "Fitness And Health Centre", + "floor_area": 110.382, + "area_metric": "Gross floor area measured as RICS Gross Internal Area (GIA)", + "benchmark_id": 1, + "occupancy_level": "Standard Occupancy" + }, + { + "name": "Swimming pool", + "tufa": 1358.936, + "benchmark": "Swimming Pool Centre", + "floor_area": 1358.936, + "area_metric": "Gross floor area measured as RICS Gross Internal Area (GIA)", + "benchmark_id": 2, + "occupancy_level": "Standard Occupancy" + } + ], + "main_benchmark": "Swimming Pool Centre" + }, + "registration_date": "2021-10-12", + "location_description": "Swimming pool with gumnasium.", + "or_usable_floor_area": { + "ufa_1": { + "name": "Basement Plant", + "floor_area": 118.16 + }, + "ufa_2": { + "name": "Basement void around pool", + "floor_area": 179.673 + }, + "ufa_3": { + "name": "Ground Plant", + "floor_area": 146.223 + }, + "ufa_4": { + "name": "First Floor Plant", + "floor_area": 49.807 + }, + "total_ufa": 493.863 + }, + "or_energy_consumption": { + "gas": { + "end_date": "2021-08-31", + "estimate": 0, + "start_date": "2020-09-01", + "consumption": 805167 + }, + "electricity": { + "end_date": "2021-08-31", + "estimate": 0, + "start_date": "2020-09-01", + "consumption": 126161 + } + }, + "technical_information": { + "floor_area": 1469.318, + "main_heating_fuel": "Natural Gas", + "building_environment": "Heating and Mechanical Ventilation", + "separately_metered_electric_heating": 0 + }, + "or_assessment_end_date": "2021-08-31", + "renewable_energy_source": [ + { + "name": "CHP", + "end_date": "2021-08-31", + "generation": 24589, + "start_date": "2020-09-01", + "energy_type": 0 + } + ], + "or_assessment_start_date": "2020-08-31", + "dec_annual_energy_summary": { + "typical_thermal_use": 1196.24, + "renewables_electrical": 16.3, + "typical_electrical_use": 238.61, + "renewables_fuel_thermal": 0, + "annual_energy_use_electrical": 86.1, + "annual_energy_use_fuel_thermal": 548.82 + }, + "related_certificate_number": "0000-0000-0000-0000-0001", + "dec_related_party_disclosure": 1, + "current_energy_efficiency_band": "B" +} \ No newline at end of file diff --git a/backend/epc_api/json_samples/CEPC-8.0.0/dec-rr.json b/backend/epc_api/json_samples/CEPC-8.0.0/dec-rr.json new file mode 100644 index 00000000..94d8b78a --- /dev/null +++ b/backend/epc_api/json_samples/CEPC-8.0.0/dec-rr.json @@ -0,0 +1,76 @@ +{ + "uprn": 12457, + "status": "entered", + "postcode": "SW1A 2AA", + "post_town": "Fulchester", + "issue_date": "2020-05-04", + "report_type": 2, + "schema_type": "CEPC-8.0.0", + "uprn_source": "Energy Assessor", + "valid_until": "2028-05-03", + "long_payback": [ + { + "co2_impact": "LOW", + "recommendation": "Consider replacing or improving glazing", + "recommendation_code": "ECP-F4" + } + ], + "language_code": 1, + "other_payback": [ + { + "co2_impact": "HIGH", + "recommendation": "Add a big wind turbine", + "recommendation_code": "ECP-H2" + } + ], + "property_type": "University campus", + "short_payback": [ + { + "co2_impact": "MEDIUM", + "recommendation": "Consider thinking about maybe possibly getting a solar panel but only one.", + "recommendation_code": "ECP-L5" + }, + { + "co2_impact": "LOW", + "recommendation": "Consider introducing variable speed drives (VSD) for fans, pumps and compressors.", + "recommendation_code": "EPC-L7" + } + ], + "site_services": { + "service_1": { + "quantity": 751445, + "description": "Electricity" + }, + "service_2": { + "quantity": 72956, + "description": "Gas" + }, + "service_3": { + "quantity": 0, + "description": "Not used" + } + }, + "address_line_1": "Some Unit", + "address_line_2": "2 Lonely Street", + "address_line_3": "Some Area", + "address_line_4": "Some County", + "medium_payback": [ + { + "co2_impact": "LOW", + "recommendation": "Engage experts to propose specific measures to reduce hot waterwastage and plan to carry this out.", + "recommendation_code": "ECP-C1" + } + ], + "assessment_type": "DEC-RR", + "inspection_date": "2020-05-04", + "inspection_type": "Physical", + "calculation_tool": "DCLG, ORCalc, v3.6.2", + "formatted_report": "ZGVmYXVsdA==", + "registration_date": "2020-05-04", + "technical_information": { + "floor_area": 10, + "renewable_sources": "Renewable source", + "special_energy_uses": "Special discount", + "building_environment": "Air Conditioning" + } +} \ No newline at end of file diff --git a/backend/epc_api/json_samples/CEPC-8.0.0/dec.json b/backend/epc_api/json_samples/CEPC-8.0.0/dec.json new file mode 100644 index 00000000..898e34cb --- /dev/null +++ b/backend/epc_api/json_samples/CEPC-8.0.0/dec.json @@ -0,0 +1,299 @@ +{ + "uprn": 12457, + "status": "entered", + "postcode": "SW1A 2AA", + "post_town": "Whitbury", + "dec_status": 1, + "issue_date": "2020-05-14", + "reason_type": 6, + "report_type": 1, + "schema_type": "CEPC-8.0.0", + "uprn_source": "Energy Assessor", + "valid_until": "2026-05-04", + "language_code": 1, + "output_engine": "MWW-91.1.1", + "property_type": "B1 Offices and Workshop businesses", + "address_line_1": "Some Unit", + "address_line_2": "2 Lonely Street", + "address_line_3": "Some Area", + "address_line_4": "Some County", + "assessment_type": "DEC", + "inspection_date": "2020-05-04", + "this_assessment": { + "heating_co2": 3, + "energy_rating": 1, + "nominated_date": "2020-01-01", + "renewables_co2": 0, + "electricity_co2": 7 + }, + "ac_questionnaire": { + "ac_present": "Yes", + "ac_rated_output": { + "ac_kw_rating": 1 + }, + "ac_estimated_output": 1, + "ac_inspection_commissioned": 1 + }, + "calculation_tool": "DCLG, ORCalc, v3.6.3", + "is_heritage_site": "N", + "or_previous_data": { + "asset_rating": 100 + }, + "year1_assessment": { + "heating_co2": 5, + "energy_rating": 24, + "nominated_date": "2019-01-01", + "renewables_co2": 1, + "electricity_co2": 10 + }, + "year2_assessment": { + "heating_co2": 10, + "energy_rating": 40, + "nominated_date": "2018-01-01", + "renewables_co2": 2, + "electricity_co2": 15 + }, + "building_category": "C1", + "or_benchmark_data": { + "benchmarks": [ + { + "name": "Library", + "tufa": 1840, + "floor_area": 15, + "benchmark_id": 1, + "occupancy_level": "level" + } + ] + }, + "registration_date": "2020-05-04", + "building_complexity": "Level 3", + "or_energy_consumption": { + "gas": { + "end_date": "2007-12-18", + "estimate": 0, + "start_date": "2007-01-18", + "consumption": 310400 + }, + "electricity": { + "end_date": "2008-01-31", + "estimate": 1, + "start_date": "2007-01-31", + "consumption": 422480 + } + }, + "technical_information": { + "floor_area": 99, + "main_heating_fuel": "Natural Gas", + "special_energy_uses": "special", + "building_environment": "Heating and Natural Ventilation", + "other_fuel_description": "other" + }, + "or_assessment_end_date": "2020-05-01", + "summary_of_performance": { + "building_data": [ + { + "area": 402.6, + "weather": "LON", + "activities": [ + { + "id": 1005, + "area": 402.6 + } + ], + "building_w_k": 411.86, + "hvac_systems": [ + { + "area": 402.6, + "type": "Fan coil systems", + "fuel_type": "Natural Gas", + "activities": [ + { + "id": 1005, + "area": 402.6 + } + ], + "heat_source": "LTHW boiler", + "cooling_sseer": 1.963, + "heating_sseff": 0.827, + "kwh_m2_cooling": 25.3865, + "kwh_m2_heating": 41.0355, + "cooling_gen_seer": 2.5, + "heating_gen_seff": 0.89, + "kwh_m2_auxiliary": 39.0832, + "mj_m2_cooling_dem": 179.401, + "mj_m2_heating_dem": 122.171 + } + ], + "analysis_type": "ACTUAL", + "area_exterior": 1058.32, + "building_alpha": 18.3412, + "building_w_m2k": 0.389164, + "q50_infiltration": 10, + "global_performance": { + "kwh_m2_chp": 0, + "kwh_m2_dhw": 2.77834, + "kwh_m2_lpg": 0, + "kwh_m2_oil": 0, + "kwh_m2_pvs": 3.07665, + "kwh_m2_ses": 0.801605, + "kwh_m2_coal": 0, + "kwh_m2_wind": 3.02777, + "kwh_m2_biogas": 0, + "kwh_m2_biomass": 0, + "kwh_m2_cooling": 25.3865, + "kwh_m2_heating": 41.0355, + "kwh_m2_lighting": 54.0787, + "kwh_m2_supplied": 121.327, + "kwh_m2_auxiliary": 39.0832, + "kwh_m2_displaced": 6.10442, + "kwh_m2_dual_fuel": 0, + "kwh_m2_equipment": 42.185, + "kwh_m2_smokeless": 0, + "kwh_m2_anthracite": 0, + "kwh_m2_waste_heat": 0, + "kwh_m2_natural_gas": 41.0355, + "kwh_m2_district_heating": 0 + } + }, + { + "area": 402.6, + "weather": "LON", + "activities": [ + { + "id": 1005, + "area": 402.6 + } + ], + "building_w_k": 362.524, + "hvac_systems": [ + { + "area": 402.6, + "type": "Fan coil systems", + "fuel_type": "Natural Gas", + "activities": [ + { + "id": 1005, + "area": 402.6 + } + ], + "heat_source": "LTHW boiler", + "cooling_sseer": 3.6, + "heating_sseff": 0.819, + "kwh_m2_cooling": 7.46769, + "kwh_m2_heating": 18.1581, + "cooling_gen_seer": 0, + "heating_gen_seff": 0, + "kwh_m2_auxiliary": 14.0716, + "mj_m2_cooling_dem": 96.7814, + "mj_m2_heating_dem": 53.5375 + } + ], + "analysis_type": "NOTIONAL", + "area_exterior": 1058.32, + "building_alpha": 11.2489, + "building_w_m2k": 0.342547, + "q50_infiltration": 3, + "global_performance": { + "kwh_m2_chp": 0, + "kwh_m2_dhw": 3.33761, + "kwh_m2_lpg": 0, + "kwh_m2_oil": 3.33761, + "kwh_m2_pvs": 0, + "kwh_m2_ses": 0, + "kwh_m2_coal": 0, + "kwh_m2_wind": 0, + "kwh_m2_biogas": 0, + "kwh_m2_biomass": 0, + "kwh_m2_cooling": 7.46769, + "kwh_m2_heating": 18.1582, + "kwh_m2_lighting": 14.4489, + "kwh_m2_supplied": 35.9883, + "kwh_m2_auxiliary": 14.0716, + "kwh_m2_displaced": 0, + "kwh_m2_dual_fuel": 0, + "kwh_m2_equipment": 42.185, + "kwh_m2_smokeless": 0, + "kwh_m2_anthracite": 0, + "kwh_m2_waste_heat": 0, + "kwh_m2_natural_gas": 18.1582, + "kwh_m2_district_heating": 0 + } + }, + { + "area": 402.6, + "weather": "LON", + "activities": [ + { + "id": 1005, + "area": 402.6 + } + ], + "building_w_k": 718.761, + "hvac_systems": [ + { + "area": 402.6, + "type": "Fan coil systems", + "fuel_type": "Natural Gas", + "activities": [ + { + "id": 1005, + "area": 402.6 + } + ], + "heat_source": "LTHW boiler", + "cooling_sseer": 2.25, + "heating_sseff": 0.73, + "kwh_m2_cooling": 25.6538, + "kwh_m2_heating": 71.9516, + "cooling_gen_seer": 0, + "heating_gen_seff": 0, + "kwh_m2_auxiliary": 2.16062, + "mj_m2_cooling_dem": 207.795, + "mj_m2_heating_dem": 189.088 + } + ], + "analysis_type": "REFERENCE", + "area_exterior": 1058.32, + "building_alpha": 10, + "building_w_m2k": 0.679153, + "q50_infiltration": 10, + "global_performance": { + "kwh_m2_chp": 0, + "kwh_m2_dhw": 6.41192, + "kwh_m2_lpg": 0, + "kwh_m2_oil": 0, + "kwh_m2_pvs": 0, + "kwh_m2_ses": 0, + "kwh_m2_coal": 0, + "kwh_m2_wind": 0, + "kwh_m2_biogas": 0, + "kwh_m2_biomass": 0, + "kwh_m2_cooling": 25.6538, + "kwh_m2_heating": 71.9516, + "kwh_m2_lighting": 45.54, + "kwh_m2_supplied": 73.3544, + "kwh_m2_auxiliary": 2.16062, + "kwh_m2_displaced": 0, + "kwh_m2_dual_fuel": 0, + "kwh_m2_equipment": 42.185, + "kwh_m2_smokeless": 0, + "kwh_m2_anthracite": 0, + "kwh_m2_waste_heat": 0, + "kwh_m2_natural_gas": 78.3634, + "kwh_m2_district_heating": 0 + } + } + ] + }, + "or_assessment_start_date": "2020-05-01", + "dec_annual_energy_summary": { + "typical_thermal_use": 1, + "renewables_electrical": 1, + "typical_electrical_use": 1, + "renewables_fuel_thermal": 1, + "annual_energy_use_electrical": 1, + "annual_energy_use_fuel_thermal": 1 + }, + "dec_related_party_disclosure": 4, + "current_energy_efficiency_band": "A" +} \ No newline at end of file diff --git a/backend/epc_api/json_samples/RdSAP-Schema-17.0/epc.json b/backend/epc_api/json_samples/RdSAP-Schema-17.0/epc.json new file mode 100644 index 00000000..e1d48ffc --- /dev/null +++ b/backend/epc_api/json_samples/RdSAP-Schema-17.0/epc.json @@ -0,0 +1,352 @@ +{ + "uprn": 12457, + "roofs": [ + { + "description": { + "value": "(another dwelling above)", + "language": "1" + }, + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + } + ], + "walls": [ + { + "description": { + "value": "System built, with internal insulation", + "language": "1" + }, + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "floors": [ + { + "description": { + "value": "(another dwelling below)", + "language": "1" + }, + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + } + ], + "status": "entered", + "tenure": 2, + "window": { + "description": { + "value": "Fully double glazed", + "language": "1" + }, + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 3 + }, + "lighting": { + "description": { + "value": "Low energy lighting in 57% of fixed outlets", + "language": "1" + }, + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + }, + "postcode": "PT5 4RZ", + "hot_water": { + "description": { + "value": "Electric immersion, off-peak", + "language": "1" + }, + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 2 + }, + "post_town": "POSTTOWN", + "built_form": 2, + "door_count": 2, + "glazed_area": 1, + "glazing_gap": "16+", + "region_code": 3, + "report_type": 2, + "sap_heating": { + "cylinder_size": 2, + "water_heating_code": 903, + "water_heating_fuel": 29, + "instantaneous_wwhrs": { + "rooms_with_bath_and_or_shower": 1, + "rooms_with_mixer_shower_no_bath": 0, + "rooms_with_bath_and_mixer_shower": 0 + }, + "main_heating_details": [ + { + "has_fghrs": "N", + "main_fuel_type": 29, + "heat_emitter_type": 0, + "emitter_temperature": "NA", + "main_heating_number": 1, + "main_heating_control": 2401, + "main_heating_category": 7, + "main_heating_fraction": 1, + "sap_main_heating_code": 402, + "main_heating_data_source": 2 + } + ], + "immersion_heating_type": 1, + "cylinder_insulation_type": 0, + "has_fixed_air_conditioning": "false" + }, + "sap_version": 9.92, + "schema_type": "RdSAP-Schema-17.0", + "uprn_source": "Energy Assessor", + "country_code": "EAW", + "main_heating": [ + { + "description": { + "value": "Electric storage heaters", + "language": "1" + }, + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 1 + } + ], + "dwelling_type": { + "value": "Mid-floor flat", + "language": "1" + }, + "language_code": 1, + "property_type": 2, + "address_line_1": "42, Moria Mines Lane", + "assessment_type": "RdSAP", + "completion_date": "2016-01-12", + "inspection_date": "2016-01-12", + "extensions_count": 0, + "measurement_type": 1, + "sap_flat_details": { + "level": 2, + "top_storey": "N", + "flat_location": 1, + "heat_loss_corridor": 0 + }, + "total_floor_area": 55, + "transaction_type": 8, + "conservatory_type": 1, + "heated_room_count": 1, + "pvc_window_frames": "true", + "registration_date": "2016-01-12", + "sap_energy_source": { + "mains_gas": "N", + "meter_type": 1, + "photovoltaic_supply": { + "none_or_no_details": { + "pv_connection": 0, + "percent_roof_area": 0 + } + }, + "wind_turbines_count": 0, + "wind_turbines_terrain_type": 2 + }, + "secondary_heating": { + "description": { + "value": "Portable electric heaters (assumed)", + "language": "1" + }, + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + }, + "lzc_energy_sources": [ + 11 + ], + "sap_building_parts": [ + { + "identifier": "Main Dwelling", + "wall_dry_lined": "N", + "wall_thickness": 240, + "floor_heat_loss": 6, + "roof_construction": 3, + "wall_construction": 8, + "building_part_number": 1, + "sap_floor_dimensions": [ + { + "floor": 0, + "room_height": { + "value": 2.4, + "quantity": "metres" + }, + "total_floor_area": { + "value": 54.6, + "quantity": "square metres" + }, + "party_wall_length": { + "value": 7.3, + "quantity": "metres" + }, + "heat_loss_perimeter": { + "value": 23.3, + "quantity": "metres" + } + } + ], + "wall_insulation_type": 3, + "construction_age_band": "D", + "party_wall_construction": 0, + "wall_thickness_measured": "Y", + "roof_insulation_location": "ND", + "roof_insulation_thickness": "ND", + "wall_insulation_thickness": "50mm" + } + ], + "low_energy_lighting": 57, + "solar_water_heating": "N", + "habitable_room_count": 3, + "heating_cost_current": { + "value": 214, + "currency": "GBP" + }, + "insulated_door_count": 0, + "co2_emissions_current": 3.9, + "energy_rating_average": 60, + "energy_rating_current": 66, + "lighting_cost_current": { + "value": 61, + "currency": "GBP" + }, + "main_heating_controls": [ + { + "description": { + "value": "Manual charge control", + "language": "1" + }, + "energy_efficiency_rating": 2, + "environmental_efficiency_rating": 2 + } + ], + "multiple_glazing_type": 3, + "open_fireplaces_count": 0, + "has_hot_water_cylinder": "true", + "heating_cost_potential": { + "value": 216, + "currency": "GBP" + }, + "hot_water_cost_current": { + "value": 396, + "currency": "GBP" + }, + "mechanical_ventilation": 0, + "percent_draughtproofed": 100, + "suggested_improvements": [ + { + "sequence": 1, + "typical_saving": { + "value": 158, + "currency": "GBP" + }, + "indicative_cost": "£15 - £30", + "improvement_type": "C", + "improvement_details": { + "improvement_number": 1 + }, + "improvement_category": 5, + "energy_performance_rating": 74, + "environmental_impact_rating": 60 + }, + { + "sequence": 2, + "typical_saving": { + "value": 14, + "currency": "GBP" + }, + "indicative_cost": "£15", + "improvement_type": "E", + "improvement_details": { + "improvement_number": 35 + }, + "improvement_category": 5, + "energy_performance_rating": 74, + "environmental_impact_rating": 60 + }, + { + "sequence": 3, + "typical_saving": { + "value": 64, + "currency": "GBP" + }, + "indicative_cost": "£1,200 - £1,800", + "improvement_type": "L2", + "improvement_details": { + "improvement_number": 60 + }, + "improvement_category": 5, + "energy_performance_rating": 78, + "environmental_impact_rating": 64 + }, + { + "sequence": 4, + "typical_saving": { + "value": 22, + "currency": "GBP" + }, + "indicative_cost": "£1,000", + "improvement_type": "X", + "improvement_details": { + "improvement_number": 48 + }, + "improvement_category": 5, + "energy_performance_rating": 79, + "environmental_impact_rating": 66 + } + ], + "co2_emissions_potential": 2.5, + "energy_rating_potential": 79, + "lighting_cost_potential": { + "value": 42, + "currency": "GBP" + }, + "schema_version_original": "LIG-17.0", + "alternative_improvements": [ + { + "sequence": 1, + "typical_saving": { + "value": 141, + "currency": "GBP" + }, + "improvement_type": "J2", + "improvement_details": { + "improvement_number": 54 + }, + "improvement_category": 6, + "energy_performance_rating": 81, + "environmental_impact_rating": 96 + }, + { + "sequence": 2, + "typical_saving": { + "value": 118, + "currency": "GBP" + }, + "improvement_type": "Z1", + "improvement_details": { + "improvement_number": 51 + }, + "improvement_category": 6, + "energy_performance_rating": 80, + "environmental_impact_rating": 83 + } + ], + "hot_water_cost_potential": { + "value": 154, + "currency": "GBP" + }, + "renewable_heat_incentive": { + "water_heating": 4818, + "space_heating_existing_dwelling": 2415 + }, + "energy_consumption_current": 427, + "has_fixed_air_conditioning": "false", + "multiple_glazed_proportion": 100, + "calculation_software_version": "9.0.0", + "energy_consumption_potential": 267, + "environmental_impact_current": 48, + "fixed_lighting_outlets_count": 7, + "current_energy_efficiency_band": "D", + "environmental_impact_potential": 66, + "has_heated_separate_conservatory": "false", + "potential_energy_efficiency_band": "C", + "co2_emissions_current_per_floor_area": 72, + "low_energy_fixed_lighting_outlets_count": 4 +} \ No newline at end of file diff --git a/backend/epc_api/json_samples/RdSAP-Schema-17.1/epc.json b/backend/epc_api/json_samples/RdSAP-Schema-17.1/epc.json new file mode 100644 index 00000000..35db05fd --- /dev/null +++ b/backend/epc_api/json_samples/RdSAP-Schema-17.1/epc.json @@ -0,0 +1,472 @@ +{ + "uprn": 12457, + "roofs": [ + { + "description": { + "value": "Pitched, 100 mm loft insulation", + "language": "1" + }, + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 3 + }, + { + "description": { + "value": "Pitched, insulated (assumed)", + "language": "1" + }, + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "walls": [ + { + "description": { + "value": "Cavity wall, filled cavity", + "language": "1" + }, + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + }, + { + "description": { + "value": "Cavity wall, as built, insulated (assumed)", + "language": "1" + }, + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "floors": [ + { + "description": { + "value": "Solid, no insulation (assumed)", + "language": "1" + }, + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + } + ], + "status": "entered", + "tenure": 1, + "window": { + "description": { + "value": "Fully double glazed", + "language": "1" + }, + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 3 + }, + "lighting": { + "description": { + "value": "Low energy lighting in 23% of fixed outlets", + "language": "1" + }, + "energy_efficiency_rating": 2, + "environmental_efficiency_rating": 2 + }, + "postcode": "PT42 5HL", + "hot_water": { + "description": { + "value": "From main system", + "language": "1" + }, + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 3 + }, + "post_town": "POSTTOWN", + "built_form": 1, + "door_count": 4, + "glazed_area": 1, + "glazing_gap": "16+", + "region_code": 17, + "report_type": 2, + "sap_heating": { + "cylinder_size": 2, + "water_heating_code": 901, + "water_heating_fuel": 28, + "cylinder_thermostat": "Y", + "instantaneous_wwhrs": { + "rooms_with_bath_and_or_shower": 1, + "rooms_with_mixer_shower_no_bath": 0, + "rooms_with_bath_and_mixer_shower": 1 + }, + "secondary_fuel_type": 29, + "main_heating_details": [ + { + "has_fghrs": "N", + "main_fuel_type": 28, + "boiler_flue_type": 1, + "fan_flue_present": "N", + "heat_emitter_type": 1, + "emitter_temperature": "NA", + "main_heating_number": 1, + "main_heating_control": 2104, + "main_heating_category": 2, + "main_heating_fraction": 1, + "mcs_installed_heat_pump": "false", + "main_heating_data_source": 1, + "main_heating_index_number": 9049 + } + ], + "immersion_heating_type": "NA", + "secondary_heating_type": 691, + "cylinder_insulation_type": 1, + "has_fixed_air_conditioning": "false", + "cylinder_insulation_thickness": 12 + }, + "sap_version": 9.92, + "schema_type": "RdSAP-Schema-17.1", + "uprn_source": "Energy Assessor", + "country_code": "EAW", + "main_heating": [ + { + "description": { + "value": "Boiler and radiators, oil", + "language": "1" + }, + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 3 + } + ], + "dwelling_type": { + "value": "Detached house", + "language": "1" + }, + "language_code": 1, + "property_type": 0, + "address_line_1": "15, Hedge Lane", + "address_line_2": "Lower Moria", + "assessment_type": "RdSAP", + "completion_date": "2018-05-29", + "inspection_date": "2018-05-29", + "extensions_count": 1, + "measurement_type": 2, + "total_floor_area": 101, + "transaction_type": 1, + "conservatory_type": 1, + "heated_room_count": 7, + "pvc_window_frames": "true", + "registration_date": "2018-05-27", + "sap_energy_source": { + "mains_gas": "N", + "meter_type": 2, + "photovoltaic_supply": { + "none_or_no_details": { + "pv_connection": 0, + "percent_roof_area": 0 + } + }, + "wind_turbines_count": 0, + "wind_turbines_terrain_type": 2 + }, + "secondary_heating": { + "description": { + "value": "Room heaters, electric", + "language": "1" + }, + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + }, + "lzc_energy_sources": [ + 11 + ], + "sap_building_parts": [ + { + "identifier": "Main Dwelling", + "wall_dry_lined": "N", + "wall_thickness": 270, + "floor_heat_loss": 7, + "roof_construction": 4, + "wall_construction": 4, + "building_part_number": 1, + "sap_floor_dimensions": [ + { + "floor": 0, + "room_height": { + "value": 2.4, + "quantity": "metres" + }, + "floor_insulation": 1, + "total_floor_area": { + "value": 48.45, + "quantity": "square metres" + }, + "party_wall_length": { + "value": 0, + "quantity": "metres" + }, + "floor_construction": 1, + "heat_loss_perimeter": { + "value": 22.3, + "quantity": "metres" + } + }, + { + "floor": 1, + "room_height": { + "value": 2.4, + "quantity": "metres" + }, + "total_floor_area": { + "value": 48.45, + "quantity": "square metres" + }, + "party_wall_length": { + "value": 0, + "quantity": "metres" + }, + "heat_loss_perimeter": { + "value": 28.4, + "quantity": "metres" + } + } + ], + "wall_insulation_type": 2, + "construction_age_band": "G", + "party_wall_construction": "NA", + "wall_thickness_measured": "Y", + "roof_insulation_location": 2, + "roof_insulation_thickness": "100mm" + }, + { + "identifier": "Extension 1", + "wall_dry_lined": "N", + "wall_thickness": 260, + "floor_heat_loss": 7, + "roof_construction": 5, + "wall_construction": 4, + "building_part_number": 2, + "sap_floor_dimensions": [ + { + "floor": 0, + "room_height": { + "value": 2.4, + "quantity": "metres" + }, + "floor_insulation": 1, + "total_floor_area": { + "value": 21.84, + "quantity": "square metres" + }, + "party_wall_length": 0, + "floor_construction": 1, + "heat_loss_perimeter": { + "value": 16.6, + "quantity": "metres" + } + } + ], + "wall_insulation_type": 4, + "construction_age_band": "H", + "party_wall_construction": "NA", + "wall_thickness_measured": "Y", + "roof_insulation_location": 4, + "roof_insulation_thickness": 0, + "wall_insulation_thickness": "NI" + } + ], + "low_energy_lighting": 23, + "solar_water_heating": "N", + "habitable_room_count": 7, + "heating_cost_current": { + "value": 659, + "currency": "GBP" + }, + "insulated_door_count": 0, + "co2_emissions_current": 5.8, + "energy_rating_average": 60, + "energy_rating_current": 53, + "lighting_cost_current": { + "value": 115, + "currency": "GBP" + }, + "main_heating_controls": [ + { + "description": { + "value": "Programmer and room thermostat", + "language": "1" + }, + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 3 + } + ], + "multiple_glazing_type": 3, + "open_fireplaces_count": 0, + "has_hot_water_cylinder": "true", + "heating_cost_potential": { + "value": 470, + "currency": "GBP" + }, + "hot_water_cost_current": { + "value": 161, + "currency": "GBP" + }, + "mechanical_ventilation": 0, + "percent_draughtproofed": 100, + "suggested_improvements": [ + { + "sequence": 1, + "typical_saving": { + "value": 25, + "currency": "GBP" + }, + "indicative_cost": "£100 - £350", + "improvement_type": "A", + "improvement_details": { + "improvement_number": 5 + }, + "improvement_category": 5, + "energy_performance_rating": 54, + "environmental_impact_rating": 47 + }, + { + "sequence": 2, + "typical_saving": { + "value": 87, + "currency": "GBP" + }, + "indicative_cost": "£4,000 - £6,000", + "improvement_type": "W2", + "improvement_details": { + "improvement_number": 58 + }, + "improvement_category": 5, + "energy_performance_rating": 58, + "environmental_impact_rating": 52 + }, + { + "sequence": 3, + "typical_saving": { + "value": 16, + "currency": "GBP" + }, + "indicative_cost": "£15 - £30", + "improvement_type": "C", + "improvement_details": { + "improvement_number": 3 + }, + "improvement_category": 5, + "energy_performance_rating": 59, + "environmental_impact_rating": 53 + }, + { + "sequence": 4, + "typical_saving": { + "value": 42, + "currency": "GBP" + }, + "indicative_cost": "£50", + "improvement_type": "E", + "improvement_details": { + "improvement_number": 35 + }, + "improvement_category": 5, + "energy_performance_rating": 61, + "environmental_impact_rating": 54 + }, + { + "sequence": 5, + "typical_saving": { + "value": 38, + "currency": "GBP" + }, + "indicative_cost": "£350 - £450", + "improvement_type": "G", + "improvement_details": { + "improvement_number": 13 + }, + "improvement_category": 5, + "energy_performance_rating": 62, + "environmental_impact_rating": 56 + }, + { + "sequence": 6, + "typical_saving": { + "value": 48, + "currency": "GBP" + }, + "indicative_cost": "£2,200 - £3,000", + "improvement_type": "I", + "improvement_details": { + "improvement_number": 20 + }, + "improvement_category": 5, + "energy_performance_rating": 65, + "environmental_impact_rating": 59 + }, + { + "sequence": 7, + "typical_saving": { + "value": 41, + "currency": "GBP" + }, + "indicative_cost": "£4,000 - £6,000", + "improvement_type": "N", + "improvement_details": { + "improvement_number": 19 + }, + "improvement_category": 5, + "energy_performance_rating": 68, + "environmental_impact_rating": 62 + }, + { + "sequence": 8, + "typical_saving": { + "value": 27, + "currency": "GBP" + }, + "indicative_cost": "£2,000", + "improvement_type": "X", + "improvement_details": { + "improvement_number": 48 + }, + "improvement_category": 5, + "energy_performance_rating": 69, + "environmental_impact_rating": 64 + }, + { + "sequence": 9, + "typical_saving": { + "value": 297, + "currency": "GBP" + }, + "indicative_cost": "£5,000 - £8,000", + "improvement_type": "U", + "improvement_details": { + "improvement_number": 34 + }, + "improvement_category": 5, + "energy_performance_rating": 78, + "environmental_impact_rating": 72 + } + ], + "co2_emissions_potential": 2.7, + "energy_rating_potential": 78, + "lighting_cost_potential": { + "value": 65, + "currency": "GBP" + }, + "schema_version_original": "LIG-17.1", + "hot_water_cost_potential": { + "value": 77, + "currency": "GBP" + }, + "renewable_heat_incentive": { + "water_heating": 3301, + "impact_of_loft_insulation": -565, + "space_heating_existing_dwelling": 11351 + }, + "energy_consumption_current": 234, + "has_fixed_air_conditioning": "false", + "multiple_glazed_proportion": 100, + "calculation_software_version": "v92.0.1.1", + "energy_consumption_potential": 95, + "environmental_impact_current": 46, + "fixed_lighting_outlets_count": 13, + "current_energy_efficiency_band": "E", + "environmental_impact_potential": 72, + "has_heated_separate_conservatory": "false", + "potential_energy_efficiency_band": "C", + "co2_emissions_current_per_floor_area": 57, + "low_energy_fixed_lighting_outlets_count": 3 +} \ No newline at end of file diff --git a/backend/epc_api/json_samples/RdSAP-Schema-18.0/epc.json b/backend/epc_api/json_samples/RdSAP-Schema-18.0/epc.json new file mode 100644 index 00000000..44761b86 --- /dev/null +++ b/backend/epc_api/json_samples/RdSAP-Schema-18.0/epc.json @@ -0,0 +1,413 @@ +{ + "uprn": 12457, + "roofs": [ + { + "description": { + "value": "Pitched, 100 mm loft insulation", + "language": "1" + }, + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 3 + }, + { + "description": { + "value": "Flat, insulated", + "language": "1" + }, + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 3 + }, + { + "description": { + "value": "Roof room(s), insulated (assumed)", + "language": "1" + }, + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "walls": [ + { + "description": { + "value": "Solid brick, as built, no insulation (assumed)", + "language": "1" + }, + "energy_efficiency_rating": 1, + "environmental_efficiency_rating": 1 + }, + { + "description": { + "value": "Cavity wall, as built, insulated (assumed)", + "language": "1" + }, + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "floors": [ + { + "description": { + "value": "Solid, no insulation (assumed)", + "language": "1" + }, + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + } + ], + "status": "entered", + "tenure": 1, + "window": { + "description": { + "value": "Fully double glazed", + "language": "1" + }, + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 3 + }, + "lighting": { + "description": { + "value": "Low energy lighting in 67% of fixed outlets", + "language": "1" + }, + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + }, + "postcode": "PT11 4RF", + "hot_water": { + "description": { + "value": "From main system", + "language": "1" + }, + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + }, + "post_town": "POSTTOWN", + "built_form": 4, + "door_count": 2, + "glazed_area": 1, + "glazing_gap": 12, + "region_code": 1, + "report_type": 2, + "sap_heating": { + "cylinder_size": 1, + "water_heating_code": 901, + "water_heating_fuel": 26, + "instantaneous_wwhrs": { + "rooms_with_bath_and_or_shower": 1, + "rooms_with_mixer_shower_no_bath": 0, + "rooms_with_bath_and_mixer_shower": 0 + }, + "main_heating_details": [ + { + "has_fghrs": "N", + "main_fuel_type": 26, + "boiler_flue_type": 2, + "heat_emitter_type": 1, + "emitter_temperature": 0, + "main_heating_number": 1, + "main_heating_control": 2106, + "main_heating_category": 2, + "main_heating_fraction": 1, + "central_heating_pump_age": 0, + "main_heating_data_source": 1, + "main_heating_index_number": 16137 + } + ], + "immersion_heating_type": "NA", + "has_fixed_air_conditioning": "false" + }, + "sap_version": 9.92, + "schema_type": "RdSAP-Schema-18.0", + "uprn_source": "Energy Assessor", + "country_code": "EAW", + "main_heating": [ + { + "description": { + "value": "Boiler and radiators, mains gas", + "language": "1" + }, + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "dwelling_type": { + "value": "Mid-terrace house", + "language": "1" + }, + "language_code": 1, + "property_type": 0, + "address_line_1": "1, Bagshot Lane", + "address_line_2": "Village", + "assessment_type": "RdSAP", + "completion_date": "2017-03-19", + "inspection_date": "2017-03-19", + "extensions_count": 1, + "measurement_type": 1, + "total_floor_area": 93, + "transaction_type": 5, + "conservatory_type": 1, + "heated_room_count": 5, + "pvc_window_frames": "true", + "registration_date": "2017-03-19", + "sap_energy_source": { + "mains_gas": "Y", + "meter_type": 1, + "photovoltaic_supply": { + "none_or_no_details": { + "pv_connection": 0, + "percent_roof_area": 0 + } + }, + "wind_turbines_count": 0, + "wind_turbines_terrain_type": 2 + }, + "secondary_heating": { + "description": { + "value": "None", + "language": "1" + }, + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + }, + "lzc_energy_sources": [ + 11 + ], + "sap_building_parts": [ + { + "identifier": "Main Dwelling", + "wall_dry_lined": "N", + "wall_thickness": 330, + "floor_heat_loss": 7, + "sap_room_in_roof": { + "floor_area": { + "value": 9, + "quantity": "square metres" + }, + "insulation": "AB", + "roof_room_connected": "N", + "construction_age_band": "G" + }, + "roof_construction": 4, + "wall_construction": 3, + "building_part_number": 1, + "sap_floor_dimensions": [ + { + "floor": 0, + "room_height": { + "value": 2.4, + "quantity": "metres" + }, + "floor_insulation": 1, + "total_floor_area": { + "value": 29.12, + "quantity": "square metres" + }, + "party_wall_length": { + "value": 11.2, + "quantity": "metres" + }, + "floor_construction": 1, + "heat_loss_perimeter": { + "value": 5.2, + "quantity": "metres" + } + }, + { + "floor": 1, + "room_height": { + "value": 2.4, + "quantity": "metres" + }, + "total_floor_area": { + "value": 29.12, + "quantity": "square metres" + }, + "party_wall_length": { + "value": 11.2, + "quantity": "metres" + }, + "heat_loss_perimeter": { + "value": 10.4, + "quantity": "metres" + } + } + ], + "wall_insulation_type": 4, + "construction_age_band": "C", + "party_wall_construction": 0, + "wall_thickness_measured": "Y", + "roof_insulation_location": 2, + "roof_insulation_thickness": "100mm", + "wall_insulation_thickness": "NI" + }, + { + "identifier": "Extension", + "wall_dry_lined": "N", + "wall_thickness": 290, + "floor_heat_loss": 7, + "roof_construction": 1, + "wall_construction": 4, + "building_part_number": 2, + "sap_floor_dimensions": [ + { + "floor": 0, + "room_height": { + "value": 2.4, + "quantity": "metres" + }, + "floor_insulation": 1, + "total_floor_area": { + "value": 15.6, + "quantity": "square metres" + }, + "party_wall_length": { + "value": 6, + "quantity": "metres" + }, + "floor_construction": 1, + "heat_loss_perimeter": { + "value": 5.2, + "quantity": "metres" + } + } + ], + "wall_insulation_type": 4, + "construction_age_band": "G", + "party_wall_construction": 0, + "wall_thickness_measured": "Y", + "roof_insulation_location": 6, + "wall_insulation_thickness": "NI", + "flat_roof_insulation_thickness": "NI" + } + ], + "low_energy_lighting": 67, + "solar_water_heating": "N", + "habitable_room_count": 5, + "heating_cost_current": { + "value": 619, + "currency": "GBP" + }, + "insulated_door_count": 0, + "co2_emissions_current": 3.4, + "energy_rating_average": 60, + "energy_rating_current": 69, + "lighting_cost_current": { + "value": 81, + "currency": "GBP" + }, + "main_heating_controls": [ + { + "description": { + "value": "Programmer, room thermostat and TRVs", + "language": "1" + }, + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "multiple_glazing_type": 3, + "open_fireplaces_count": 0, + "has_hot_water_cylinder": "false", + "heating_cost_potential": { + "value": 534, + "currency": "GBP" + }, + "hot_water_cost_current": { + "value": 100, + "currency": "GBP" + }, + "mechanical_ventilation": 0, + "percent_draughtproofed": 100, + "suggested_improvements": [ + { + "sequence": 1, + "typical_saving": { + "value": 88, + "currency": "GBP" + }, + "indicative_cost": "£4,000 - £14,000", + "improvement_type": "Q", + "improvement_details": { + "improvement_number": 7 + }, + "improvement_category": 5, + "energy_performance_rating": 72, + "environmental_impact_rating": 71 + }, + { + "sequence": 2, + "typical_saving": { + "value": 18, + "currency": "GBP" + }, + "indicative_cost": "£15", + "improvement_type": "E", + "improvement_details": { + "improvement_number": 35 + }, + "improvement_category": 5, + "energy_performance_rating": 73, + "environmental_impact_rating": 71 + }, + { + "sequence": 3, + "typical_saving": { + "value": 33, + "currency": "GBP" + }, + "indicative_cost": "£4,000 - £6,000", + "improvement_type": "N", + "improvement_details": { + "improvement_number": 19 + }, + "improvement_category": 5, + "energy_performance_rating": 74, + "environmental_impact_rating": 73 + }, + { + "sequence": 4, + "typical_saving": { + "value": 302, + "currency": "GBP" + }, + "indicative_cost": "£5,000 - £8,000", + "improvement_type": "U", + "improvement_details": { + "improvement_number": 34 + }, + "improvement_category": 5, + "energy_performance_rating": 85, + "environmental_impact_rating": 83 + } + ], + "co2_emissions_potential": 1.7, + "energy_rating_potential": 85, + "lighting_cost_potential": { + "value": 61, + "currency": "GBP" + }, + "schema_version_original": "LIG-17.0", + "hot_water_cost_potential": { + "value": 68, + "currency": "GBP" + }, + "renewable_heat_incentive": { + "water_heating": 2087, + "impact_of_loft_insulation": -214, + "impact_of_solid_wall_insulation": -1864, + "space_heating_existing_dwelling": 10483 + }, + "energy_consumption_current": 230, + "has_fixed_air_conditioning": "false", + "multiple_glazed_proportion": 100, + "calculation_software_version": "2.0.0.0", + "energy_consumption_potential": 115, + "environmental_impact_current": 66, + "fixed_lighting_outlets_count": 9, + "current_energy_efficiency_band": "C", + "environmental_impact_potential": 83, + "has_heated_separate_conservatory": "false", + "potential_energy_efficiency_band": "B", + "co2_emissions_current_per_floor_area": 41, + "low_energy_fixed_lighting_outlets_count": 6 +} \ No newline at end of file diff --git a/backend/epc_api/json_samples/RdSAP-Schema-19.0/epc.json b/backend/epc_api/json_samples/RdSAP-Schema-19.0/epc.json new file mode 100644 index 00000000..ce4e44b4 --- /dev/null +++ b/backend/epc_api/json_samples/RdSAP-Schema-19.0/epc.json @@ -0,0 +1,386 @@ +{ + "uprn": 12457, + "roofs": [ + { + "description": { + "value": "Pitched, 150 mm loft insulation", + "language": "1" + }, + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + }, + { + "description": { + "value": "Pitched, insulated (assumed)", + "language": "1" + }, + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 3 + } + ], + "walls": [ + { + "description": { + "value": "Cavity wall, filled cavity", + "language": "1" + }, + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 3 + }, + { + "description": { + "value": "Cavity wall, as built, insulated (assumed)", + "language": "1" + }, + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "floors": [ + { + "description": { + "value": "Solid, no insulation (assumed)", + "language": "1" + }, + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + } + ], + "status": "entered", + "tenure": 3, + "window": { + "description": { + "value": "Fully double glazed", + "language": "1" + }, + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 3 + }, + "lighting": { + "description": { + "value": "Low energy lighting in 87% of fixed outlets", + "language": "1" + }, + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + }, + "postcode": "A1 1AA", + "hot_water": { + "description": { + "value": "From main system", + "language": "1" + }, + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + }, + "post_town": "Town", + "built_form": 2, + "door_count": 1, + "glazed_area": 1, + "region_code": 19, + "report_type": 2, + "sap_heating": { + "cylinder_size": 1, + "water_heating_code": 901, + "water_heating_fuel": 26, + "instantaneous_wwhrs": { + "rooms_with_bath_and_or_shower": 1, + "rooms_with_mixer_shower_no_bath": 0, + "rooms_with_bath_and_mixer_shower": 0 + }, + "secondary_fuel_type": 9, + "main_heating_details": [ + { + "has_fghrs": "N", + "main_fuel_type": 26, + "boiler_flue_type": 2, + "heat_emitter_type": 1, + "emitter_temperature": 0, + "main_heating_number": 1, + "main_heating_control": 2106, + "main_heating_category": 2, + "main_heating_fraction": 1, + "central_heating_pump_age": 0, + "main_heating_data_source": 1, + "main_heating_index_number": 15274 + } + ], + "immersion_heating_type": "NA", + "secondary_heating_type": 631, + "has_fixed_air_conditioning": "false" + }, + "sap_version": 9.94, + "schema_type": "RdSAP-Schema-19.0", + "uprn_source": "Energy Assessor", + "country_code": "EAW", + "main_heating": [ + { + "description": { + "value": "Boiler and radiators, mains gas", + "language": "1" + }, + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "dwelling_type": { + "value": "Semi-detached house", + "language": "1" + }, + "language_code": 1, + "property_type": 0, + "address_line_1": "15, Address Lane", + "address_line_2": "New Town", + "assessment_type": "RdSAP", + "completion_date": "2020-06-04", + "inspection_date": "2020-06-03", + "extensions_count": 1, + "measurement_type": 1, + "total_floor_area": 94, + "transaction_type": 8, + "conservatory_type": 2, + "heated_room_count": 5, + "pvc_window_frames": "false", + "registration_date": "2020-06-04", + "sap_energy_source": { + "mains_gas": "Y", + "meter_type": 3, + "photovoltaic_supply": { + "none_or_no_details": { + "pv_connection": 0, + "percent_roof_area": 0 + } + }, + "wind_turbines_count": 0, + "wind_turbines_terrain_type": 2 + }, + "secondary_heating": { + "description": { + "value": "Room heaters, dual fuel (mineral and wood)", + "language": "1" + }, + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + }, + "lzc_energy_sources": [ + 11, + 9 + ], + "sap_building_parts": [ + { + "identifier": "Main Dwelling", + "wall_dry_lined": "N", + "wall_thickness": 300, + "floor_heat_loss": 7, + "roof_construction": 4, + "wall_construction": 4, + "building_part_number": 1, + "sap_floor_dimensions": [ + { + "floor": 0, + "room_height": { + "value": 2.47, + "quantity": "metres" + }, + "floor_insulation": 1, + "total_floor_area": { + "value": 39.91, + "quantity": "square metres" + }, + "party_wall_length": { + "value": 8.81, + "quantity": "metres" + }, + "floor_construction": 1, + "heat_loss_perimeter": { + "value": 13.65, + "quantity": "metres" + } + }, + { + "floor": 1, + "room_height": { + "value": 2.36, + "quantity": "metres" + }, + "total_floor_area": { + "value": 39.91, + "quantity": "square metres" + }, + "party_wall_length": { + "value": 8.81, + "quantity": "metres" + }, + "heat_loss_perimeter": { + "value": 17.87, + "quantity": "metres" + } + } + ], + "wall_insulation_type": 2, + "construction_age_band": "D", + "party_wall_construction": 1, + "wall_thickness_measured": "Y", + "roof_insulation_location": 2, + "roof_insulation_thickness": "150mm", + "wall_insulation_thickness": "NI", + "floor_insulation_thickness": "NI" + }, + { + "identifier": "Extension", + "wall_dry_lined": "N", + "wall_thickness": 300, + "floor_heat_loss": 7, + "roof_construction": 5, + "wall_construction": 4, + "building_part_number": 2, + "sap_floor_dimensions": [ + { + "floor": 0, + "room_height": { + "value": 2.34, + "quantity": "metres" + }, + "floor_insulation": 1, + "total_floor_area": { + "value": 13.88, + "quantity": "square metres" + }, + "party_wall_length": 0, + "floor_construction": 1, + "heat_loss_perimeter": { + "value": 10.8, + "quantity": "metres" + } + } + ], + "wall_insulation_type": 4, + "construction_age_band": "G", + "party_wall_construction": "NA", + "wall_thickness_measured": "Y", + "roof_insulation_location": 4, + "roof_insulation_thickness": "ND", + "wall_insulation_thickness": "NI", + "floor_insulation_thickness": "NI" + } + ], + "low_energy_lighting": 87, + "solar_water_heating": "N", + "habitable_room_count": 5, + "heating_cost_current": { + "value": 666, + "currency": "GBP" + }, + "insulated_door_count": 0, + "co2_emissions_current": 3.8, + "energy_rating_average": 60, + "energy_rating_current": 66, + "lighting_cost_current": { + "value": 81, + "currency": "GBP" + }, + "main_heating_controls": [ + { + "description": { + "value": "Programmer, room thermostat and TRVs", + "language": "1" + }, + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "multiple_glazing_type": 3, + "open_fireplaces_count": 0, + "has_hot_water_cylinder": "false", + "heating_cost_potential": { + "value": 615, + "currency": "GBP" + }, + "hot_water_cost_current": { + "value": 107, + "currency": "GBP" + }, + "mechanical_ventilation": 0, + "percent_draughtproofed": 100, + "suggested_improvements": [ + { + "sequence": 1, + "typical_saving": { + "value": 51, + "currency": "GBP" + }, + "indicative_cost": "£4,000 - £6,000", + "improvement_type": "W2", + "improvement_details": { + "improvement_number": 58 + }, + "improvement_category": 5, + "energy_performance_rating": 68, + "environmental_impact_rating": 65 + }, + { + "sequence": 2, + "typical_saving": { + "value": 33, + "currency": "GBP" + }, + "indicative_cost": "£4,000 - £6,000", + "improvement_type": "N", + "improvement_details": { + "improvement_number": 19 + }, + "improvement_category": 5, + "energy_performance_rating": 70, + "environmental_impact_rating": 67 + }, + { + "sequence": 3, + "typical_saving": { + "value": 316, + "currency": "GBP" + }, + "indicative_cost": "£3,500 - £5,500", + "improvement_type": "U", + "improvement_details": { + "improvement_number": 34 + }, + "improvement_category": 5, + "energy_performance_rating": 79, + "environmental_impact_rating": 75 + } + ], + "co2_emissions_potential": 2.4, + "energy_rating_potential": 79, + "lighting_cost_potential": { + "value": 81, + "currency": "GBP" + }, + "schema_version_original": "LIG-19.0", + "hot_water_cost_potential": { + "value": 74, + "currency": "GBP" + }, + "renewable_heat_incentive": { + "water_heating": 2207, + "impact_of_loft_insulation": -394, + "space_heating_existing_dwelling": 9825 + }, + "energy_consumption_current": 222, + "has_fixed_air_conditioning": "false", + "multiple_glazed_proportion": 100, + "calculation_software_version": "2.1.1.0", + "energy_consumption_potential": 137, + "environmental_impact_current": 62, + "fixed_lighting_outlets_count": 15, + "windows_transmission_details": { + "u_value": 3.1, + "data_source": 2, + "solar_transmittance": 0.76 + }, + "current_energy_efficiency_band": "D", + "environmental_impact_potential": 75, + "has_heated_separate_conservatory": "false", + "potential_energy_efficiency_band": "C", + "co2_emissions_current_per_floor_area": 41, + "low_energy_fixed_lighting_outlets_count": 13 +} \ No newline at end of file diff --git a/backend/epc_api/json_samples/RdSAP-Schema-20.0.0/epc.json b/backend/epc_api/json_samples/RdSAP-Schema-20.0.0/epc.json new file mode 100644 index 00000000..bcb3618f --- /dev/null +++ b/backend/epc_api/json_samples/RdSAP-Schema-20.0.0/epc.json @@ -0,0 +1,340 @@ +{ + "uprn": 12457, + "roofs": [ + { + "description": "Pitched, 25 mm loft insulation", + "energy_efficiency_rating": 2, + "environmental_efficiency_rating": 2 + }, + { + "description": "Pitched, 250 mm loft insulation", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "walls": [ + { + "description": "Solid brick, as built, no insulation (assumed)", + "energy_efficiency_rating": 1, + "environmental_efficiency_rating": 1 + }, + { + "description": "Cavity wall, as built, insulated (assumed)", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "floors": [ + { + "description": "Suspended, no insulation (assumed)", + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + }, + { + "description": "Solid, insulated (assumed)", + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + } + ], + "status": "entered", + "tenure": 1, + "window": { + "description": "Fully double glazed", + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 3 + }, + "addendum": { + "stone_walls": "true", + "system_build": "true", + "addendum_numbers": [ + 1, + 8 + ] + }, + "lighting": { + "description": "Low energy lighting in 50% of fixed outlets", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + }, + "postcode": "A0 0AA", + "hot_water": { + "description": "From main system", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + }, + "post_town": "Whitbury", + "built_form": 2, + "door_count": 2, + "glazed_area": 1, + "region_code": 1, + "report_type": 2, + "sap_heating": { + "cylinder_size": 1, + "water_heating_code": 901, + "water_heating_fuel": 26, + "instantaneous_wwhrs": { + "rooms_with_bath_and_or_shower": 1, + "rooms_with_mixer_shower_no_bath": 0, + "rooms_with_bath_and_mixer_shower": 0 + }, + "secondary_fuel_type": 25, + "main_heating_details": [ + { + "has_fghrs": "N", + "main_fuel_type": 26, + "boiler_flue_type": 2, + "fan_flue_present": "N", + "heat_emitter_type": 1, + "emitter_temperature": 0, + "main_heating_number": 1, + "main_heating_control": 2106, + "main_heating_category": 2, + "main_heating_fraction": 1, + "sap_main_heating_code": 101, + "central_heating_pump_age": 0, + "main_heating_data_source": 1, + "main_heating_index_number": 17507 + } + ], + "immersion_heating_type": "NA", + "has_fixed_air_conditioning": "false" + }, + "sap_version": 9.8, + "sap_windows": [ + { + "orientation": 1, + "window_area": 200.1, + "window_type": 2, + "glazing_type": 1, + "window_location": 0 + }, + { + "orientation": 2, + "window_area": 180.2, + "window_type": 1, + "glazing_type": 2, + "window_location": 1 + } + ], + "schema_type": "RdSAP-Schema-20.0.0", + "uprn_source": "Energy Assessor", + "country_code": "EAW", + "main_heating": [ + { + "description": "Boiler and radiators, anthracite", + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 1 + }, + { + "description": "Boiler and radiators, mains gas", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "dwelling_type": "Mid-terrace house", + "language_code": 1, + "property_type": 0, + "address_line_1": "1 Some Street", + "assessment_type": "RdSAP", + "completion_date": "2020-05-04", + "inspection_date": "2020-05-04", + "extensions_count": 0, + "measurement_type": 1, + "sap_flat_details": { + "level": 1, + "top_storey": "N", + "storey_count": 3, + "flat_location": 1, + "heat_loss_corridor": 2, + "unheated_corridor_length": 10 + }, + "total_floor_area": 55, + "transaction_type": 1, + "conservatory_type": 1, + "heated_room_count": 5, + "registration_date": "2020-05-04", + "sap_energy_source": { + "mains_gas": "Y", + "meter_type": 2, + "photovoltaic_supply": { + "none_or_no_details": { + "pv_connection": 0, + "percent_roof_area": 50 + } + }, + "wind_turbines_count": 0, + "wind_turbines_terrain_type": 2 + }, + "secondary_heating": { + "description": "Room heaters, electric", + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + }, + "lzc_energy_sources": [ + 11 + ], + "sap_building_parts": [ + { + "identifier": "Main Dwelling", + "wall_dry_lined": "N", + "floor_heat_loss": 7, + "sap_room_in_roof": { + "floor_area": 100, + "insulation": "AB", + "roof_room_connected": "N", + "construction_age_band": "B" + }, + "roof_construction": 4, + "wall_construction": 4, + "building_part_number": 1, + "sap_floor_dimensions": [ + { + "floor": 0, + "room_height": { + "value": 2.45, + "quantity": "metres" + }, + "floor_insulation": 1, + "total_floor_area": { + "value": 45.82, + "quantity": "square metres" + }, + "party_wall_length": { + "value": 7.9, + "quantity": "metres" + }, + "floor_construction": 1, + "heat_loss_perimeter": { + "value": 19.5, + "quantity": "metres" + } + }, + { + "floor": 1, + "room_height": { + "value": 2.59, + "quantity": "metres" + }, + "total_floor_area": { + "value": 45.82, + "quantity": "square metres" + }, + "party_wall_length": { + "value": 7.9, + "quantity": "metres" + }, + "heat_loss_perimeter": { + "value": 19.5, + "quantity": "metres" + } + } + ], + "wall_insulation_type": 2, + "construction_age_band": "K", + "party_wall_construction": 0, + "wall_thickness_measured": "N", + "roof_insulation_location": 2, + "roof_insulation_thickness": "200mm", + "wall_insulation_thickness": "NI", + "floor_insulation_thickness": "NI" + } + ], + "low_energy_lighting": 100, + "solar_water_heating": "N", + "habitable_room_count": 5, + "heating_cost_current": 365.98, + "insulated_door_count": 2, + "co2_emissions_current": 2.4, + "energy_rating_average": 60, + "energy_rating_current": 50, + "lighting_cost_current": 123.45, + "main_heating_controls": [ + { + "description": "Programmer, room thermostat and TRVs", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + }, + { + "description": "Time and temperature zone control", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + } + ], + "multiple_glazing_type": 2, + "open_fireplaces_count": 0, + "heating_cost_potential": 250.34, + "hot_water_cost_current": 200.4, + "insulated_door_u_value": 3, + "mechanical_ventilation": 0, + "percent_draughtproofed": 100, + "suggested_improvements": [ + { + "sequence": 1, + "typical_saving": 360, + "indicative_cost": "£100 - £350", + "improvement_type": "Z3", + "improvement_details": { + "improvement_number": 5 + }, + "improvement_category": 6, + "energy_performance_rating": 50, + "environmental_impact_rating": 50 + }, + { + "sequence": 2, + "typical_saving": 99, + "indicative_cost": 2000, + "improvement_type": "Z2", + "improvement_details": { + "improvement_number": 1 + }, + "improvement_category": 2, + "energy_performance_rating": 60, + "environmental_impact_rating": 64 + }, + { + "sequence": 3, + "typical_saving": 99, + "indicative_cost": 1000, + "improvement_type": "Z2", + "improvement_details": { + "improvement_texts": { + "improvement_summary": "An improvement summary", + "improvement_description": "An improvement desc" + } + }, + "improvement_category": 2, + "energy_performance_rating": 60, + "environmental_impact_rating": 64 + } + ], + "co2_emissions_potential": 1.4, + "energy_rating_potential": 72, + "lighting_cost_potential": 84.23, + "schema_version_original": "SAP-19.0", + "hot_water_cost_potential": 180.43, + "renewable_heat_incentive": { + "water_heating": 2285, + "impact_of_loft_insulation": -2114, + "impact_of_cavity_insulation": -122, + "impact_of_solid_wall_insulation": -3560, + "space_heating_existing_dwelling": 13120 + }, + "energy_consumption_current": 230, + "multiple_glazed_proportion": 100, + "calculation_software_version": "13.05r16", + "energy_consumption_potential": 88, + "environmental_impact_current": 52, + "fixed_lighting_outlets_count": 16, + "windows_transmission_details": { + "u_value": 2, + "data_source": 2, + "solar_transmittance": 0.72 + }, + "multiple_glazed_proportion_nr": "NR", + "current_energy_efficiency_band": "E", + "environmental_impact_potential": 74, + "potential_energy_efficiency_band": "C", + "co2_emissions_current_per_floor_area": 20, + "low_energy_fixed_lighting_outlets_count": 16 +} \ No newline at end of file diff --git a/backend/epc_api/json_samples/RdSAP-Schema-21.0.0/epc.json b/backend/epc_api/json_samples/RdSAP-Schema-21.0.0/epc.json new file mode 100644 index 00000000..3f9793b0 --- /dev/null +++ b/backend/epc_api/json_samples/RdSAP-Schema-21.0.0/epc.json @@ -0,0 +1,403 @@ +{ + "uprn": 12457, + "roofs": [ + { + "description": "Pitched, 25 mm loft insulation", + "energy_efficiency_rating": 2, + "environmental_efficiency_rating": 2 + }, + { + "description": "Pitched, 250 mm loft insulation", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "walls": [ + { + "description": "Solid brick, as built, no insulation (assumed)", + "energy_efficiency_rating": 1, + "environmental_efficiency_rating": 1 + }, + { + "description": "Cavity wall, as built, insulated (assumed)", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "floors": [ + { + "description": "Suspended, no insulation (assumed)", + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + }, + { + "description": "Solid, insulated (assumed)", + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + } + ], + "status": "entered", + "tenure": 1, + "window": { + "description": "Fully double glazed", + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 3 + }, + "addendum": { + "stone_walls": "true", + "system_build": "true", + "addendum_numbers": [ + 1, + 8 + ] + }, + "lighting": { + "description": "Low energy lighting in 50% of fixed outlets", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + }, + "postcode": "A0 0AA", + "hot_water": { + "description": "From main system", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + }, + "post_town": "Whitbury", + "built_form": 2, + "door_count": 3, + "region_code": 1, + "report_type": 2, + "sap_heating": { + "cylinder_size": 1, + "shower_outlets": { + "shower_outlet": { + "shower_wwhrs": 1, + "shower_outlet_type": 1 + } + }, + "water_heating_code": 901, + "water_heating_fuel": 26, + "instantaneous_wwhrs": { + "wwhrs_index_number1": 1, + "wwhrs_index_number2": 2 + }, + "secondary_fuel_type": 25, + "main_heating_details": [ + { + "has_fghrs": "N", + "main_fuel_type": 26, + "boiler_flue_type": 2, + "fan_flue_present": "N", + "heat_emitter_type": 1, + "emitter_temperature": 0, + "main_heating_number": 1, + "boiler_ignition_type": 1, + "main_heating_control": 2106, + "main_heating_category": 2, + "main_heating_fraction": 1, + "sap_main_heating_code": 101, + "central_heating_pump_age": 0, + "main_heating_data_source": 1, + "main_heating_index_number": 17507 + } + ], + "immersion_heating_type": "NA", + "has_fixed_air_conditioning": "false" + }, + "sap_version": 10.2, + "sap_windows": [ + { + "pvc_frame": "false", + "glazing_gap": 6, + "orientation": 1, + "window_type": 2, + "frame_factor": 1.0, + "glazing_type": 14, + "window_width": 1.2, + "window_height": 2.0, + "draught_proofed": "true", + "window_location": 0, + "window_wall_type": 6, + "permanent_shutters_present": "N", + "window_transmission_details": { + "u_value": 1.0, + "data_source": 2, + "solar_transmittance": 1.0 + }, + "permanent_shutters_insulated": "N" + }, + { + "pvc_frame": "false", + "glazing_gap": 6, + "orientation": 1, + "window_type": 2, + "frame_factor": 1.0, + "glazing_type": 1, + "window_width": 1.2, + "window_height": 2.0, + "draught_proofed": "true", + "window_location": 1, + "window_wall_type": 6, + "permanent_shutters_present": "N", + "window_transmission_details": { + "u_value": 1.0, + "data_source": 2, + "solar_transmittance": 1.0 + }, + "permanent_shutters_insulated": "N" + } + ], + "schema_type": "RdSAP-Schema-21.0.0", + "uprn_source": "Energy Assessor", + "country_code": "ENG", + "main_heating": [ + { + "description": "Boiler and radiators, anthracite", + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 1 + }, + { + "description": "Boiler and radiators, mains gas", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "dwelling_type": "Mid-terrace house", + "language_code": 1, + "pressure_test": 6, + "property_type": 0, + "address_line_1": "1 Some Street", + "assessment_type": "RdSAP", + "completion_date": "2023-12-01", + "inspection_date": "2023-12-01", + "wet_rooms_count": 0, + "extensions_count": 0, + "measurement_type": 1, + "sap_flat_details": { + "level": 1, + "top_storey": "N", + "storey_count": 3, + "flat_location": 1, + "heat_loss_corridor": 2, + "unheated_corridor_length": 10 + }, + "total_floor_area": 55, + "transaction_type": 16, + "conservatory_type": 1, + "heated_room_count": 5, + "registration_date": "2023-12-01", + "sap_energy_source": { + "mains_gas": "Y", + "meter_type": 2, + "pv_batteries": { + "pv_battery": { + "battery_capacity": 5 + } + }, + "pv_connection": 0, + "pv_battery_count": 1, + "photovoltaic_supply": { + "none_or_no_details": { + "percent_roof_area": 0 + } + }, + "wind_turbines_count": 0, + "wind_turbine_details": { + "hub_height": 0, + "rotor_diameter": 0 + }, + "gas_smart_meter_present": "false", + "is_dwelling_export_capable": "false", + "wind_turbines_terrain_type": 4, + "electricity_smart_meter_present": "true" + }, + "secondary_heating": { + "description": "Room heaters, electric", + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + }, + "sap_building_parts": [ + { + "identifier": "Main Dwelling", + "wall_dry_lined": "N", + "floor_heat_loss": 7, + "sap_room_in_roof": { + "floor_area": 100, + "construction_age_band": "B" + }, + "roof_construction": 4, + "wall_construction": 4, + "building_part_number": 1, + "sap_floor_dimensions": [ + { + "floor": 0, + "room_height": { + "value": 2.45, + "quantity": "metres" + }, + "floor_insulation": 1, + "total_floor_area": { + "value": 45.82, + "quantity": "square metres" + }, + "party_wall_length": { + "value": 7.9, + "quantity": "metres" + }, + "floor_construction": 1, + "heat_loss_perimeter": { + "value": 19.5, + "quantity": "metres" + } + }, + { + "floor": 1, + "room_height": { + "value": 2.59, + "quantity": "metres" + }, + "total_floor_area": { + "value": 45.82, + "quantity": "square metres" + }, + "party_wall_length": { + "value": 7.9, + "quantity": "metres" + }, + "heat_loss_perimeter": { + "value": 19.5, + "quantity": "metres" + } + } + ], + "wall_insulation_type": 2, + "construction_age_band": "M", + "sap_alternative_wall_1": { + "wall_area": 10.4, + "wall_dry_lined": "N", + "wall_construction": 4, + "wall_insulation_type": 2, + "wall_thickness_measured": "N" + }, + "sap_alternative_wall_2": { + "wall_area": 10.8, + "wall_dry_lined": "N", + "wall_construction": 4, + "wall_insulation_type": 2, + "wall_thickness_measured": "N" + }, + "party_wall_construction": 0, + "wall_thickness_measured": "N", + "roof_insulation_location": 2, + "roof_insulation_thickness": "200mm", + "wall_insulation_thickness": "NI", + "floor_insulation_thickness": "NI" + } + ], + "open_chimneys_count": 1, + "solar_water_heating": "N", + "habitable_room_count": 5, + "heating_cost_current": 365.98, + "insulated_door_count": 2, + "co2_emissions_current": 2.4, + "energy_rating_average": 60, + "energy_rating_current": 50, + "lighting_cost_current": 123.45, + "main_heating_controls": [ + { + "description": "Programmer, room thermostat and TRVs", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + }, + { + "description": "Time and temperature zone control", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + } + ], + "has_hot_water_cylinder": "true", + "heating_cost_potential": 250.34, + "hot_water_cost_current": 200.4, + "insulated_door_u_value": 3, + "mechanical_ventilation": 6, + "percent_draughtproofed": 100, + "suggested_improvements": [ + { + "sequence": 1, + "typical_saving": 360, + "indicative_cost": "£100 - £350", + "improvement_type": "Z3", + "improvement_details": { + "improvement_number": 5 + }, + "improvement_category": 6, + "energy_performance_rating": 50, + "environmental_impact_rating": 50 + }, + { + "sequence": 2, + "typical_saving": 99, + "indicative_cost": 2000, + "improvement_type": "Z2", + "improvement_details": { + "improvement_number": 1 + }, + "improvement_category": 2, + "energy_performance_rating": 60, + "environmental_impact_rating": 64 + }, + { + "sequence": 3, + "typical_saving": 99, + "indicative_cost": 1000, + "improvement_type": "Z2", + "improvement_details": { + "improvement_texts": { + "improvement_description": "Improvement desc" + } + }, + "improvement_category": 2, + "energy_performance_rating": 60, + "environmental_impact_rating": 64 + } + ], + "co2_emissions_potential": 1.4, + "energy_rating_potential": 72, + "lighting_cost_potential": 84.23, + "schema_version_original": "SAP-19.0", + "hot_water_cost_potential": 180.43, + "renewable_heat_incentive": { + "water_heating": 2285, + "impact_of_loft_insulation": -2114, + "impact_of_cavity_insulation": -122, + "impact_of_solid_wall_insulation": -3560, + "space_heating_existing_dwelling": 13120 + }, + "draughtproofed_door_count": 1, + "mechanical_vent_duct_type": 3, + "energy_consumption_current": 230, + "has_fixed_air_conditioning": "false", + "multiple_glazed_proportion": 100, + "calculation_software_version": "13.05r16", + "energy_consumption_potential": 88, + "environmental_impact_current": 52, + "windows_transmission_details": { + "u_value": 2, + "data_source": 2, + "solar_transmittance": 0.72 + }, + "cfl_fixed_lighting_bulbs_count": 5, + "current_energy_efficiency_band": "E", + "environmental_impact_potential": 74, + "led_fixed_lighting_bulbs_count": 10, + "mechanical_vent_duct_placement": 2, + "mechanical_vent_duct_insulation": 2, + "potential_energy_efficiency_band": "C", + "pressure_test_certificate_number": 0, + "mechanical_ventilation_index_number": 12, + "co2_emissions_current_per_floor_area": 20, + "low_energy_fixed_lighting_bulbs_count": 16, + "mechanical_vent_duct_insulation_level": 2, + "mechanical_vent_measured_installation": "false", + "incandescent_fixed_lighting_bulbs_count": 5 +} \ No newline at end of file diff --git a/backend/epc_api/json_samples/RdSAP-Schema-21.0.1/epc.json b/backend/epc_api/json_samples/RdSAP-Schema-21.0.1/epc.json new file mode 100644 index 00000000..166e60a0 --- /dev/null +++ b/backend/epc_api/json_samples/RdSAP-Schema-21.0.1/epc.json @@ -0,0 +1,446 @@ +{ + "uprn": 12457, + "roofs": [ + { + "description": { + "value": "Pitched, 25 mm loft insulation", + "language": "1" + }, + "energy_efficiency_rating": 2, + "environmental_efficiency_rating": 2 + }, + { + "description": { + "value": "Pitched, 250 mm loft insulation", + "language": "1" + }, + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "walls": [ + { + "description": { + "value": "Solid brick, as built, no insulation (assumed)", + "language": "1" + }, + "energy_efficiency_rating": 1, + "environmental_efficiency_rating": 1 + }, + { + "description": { + "value": "Cavity wall, as built, insulated (assumed)", + "language": "1" + }, + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "floors": [ + { + "description": { + "value": "Suspended, no insulation (assumed)", + "language": "1" + }, + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + }, + { + "description": { + "value": "Solid, insulated (assumed)", + "language": "1" + }, + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + } + ], + "status": "entered", + "tenure": 1, + "window": { + "description": { + "value": "Fully double glazed", + "language": "1" + }, + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 3 + }, + "addendum": { + "stone_walls": "true", + "system_build": "true", + "addendum_numbers": [ + 1, + 13 + ] + }, + "lighting": { + "description": { + "value": "Low energy lighting in 50% of fixed outlets", + "language": "1" + }, + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + }, + "postcode": "A0 0AA", + "hot_water": { + "description": { + "value": "From main system", + "language": "1" + }, + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + }, + "post_town": "Whitbury", + "built_form": 2, + "door_count": 3, + "region_code": 1, + "report_type": 2, + "sap_heating": { + "cylinder_size": 1, + "shower_outlets": { + "shower_outlet": { + "shower_wwhrs": 1, + "shower_outlet_type": 1 + } + }, + "water_heating_code": 901, + "water_heating_fuel": 26, + "instantaneous_wwhrs": { + "wwhrs_index_number1": 1, + "wwhrs_index_number2": 2 + }, + "secondary_fuel_type": 25, + "main_heating_details": [ + { + "has_fghrs": "N", + "main_fuel_type": 26, + "boiler_flue_type": 2, + "fan_flue_present": "N", + "heat_emitter_type": 1, + "emitter_temperature": 0, + "main_heating_number": 1, + "boiler_ignition_type": 1, + "main_heating_control": 2106, + "main_heating_category": 2, + "main_heating_fraction": 1, + "sap_main_heating_code": 101, + "central_heating_pump_age": 0, + "main_heating_data_source": 1, + "main_heating_index_number": 17507 + } + ], + "immersion_heating_type": "NA", + "has_fixed_air_conditioning": "false" + }, + "sap_version": 10.2, + "sap_windows": [ + { + "pvc_frame": "false", + "glazing_gap": 6, + "orientation": 1, + "window_type": 2, + "frame_factor": 1.0, + "glazing_type": 14, + "window_width": 1.2, + "window_height": 2.0, + "draught_proofed": "true", + "window_location": 0, + "window_wall_type": 6, + "permanent_shutters_present": "N", + "window_transmission_details": { + "u_value": 1.0, + "data_source": 2, + "solar_transmittance": 1.0 + }, + "permanent_shutters_insulated": "N" + }, + { + "pvc_frame": "false", + "glazing_gap": 6, + "orientation": 1, + "window_type": 2, + "frame_factor": 1.0, + "glazing_type": 1, + "window_width": 1.2, + "window_height": 2.0, + "draught_proofed": "true", + "window_location": 1, + "window_wall_type": 6, + "permanent_shutters_present": "N", + "window_transmission_details": { + "u_value": 1.0, + "data_source": 2, + "solar_transmittance": 1.0 + }, + "permanent_shutters_insulated": "N" + } + ], + "schema_type": "RdSAP-Schema-21.0.1", + "uprn_source": "Energy Assessor", + "country_code": "ENG", + "main_heating": [ + { + "description": { + "value": "Boiler and radiators, anthracite", + "language": "1" + }, + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 1 + }, + { + "description": { + "value": "Boiler and radiators, mains gas", + "language": "1" + }, + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "dwelling_type": "Mid-terrace house", + "language_code": 1, + "pressure_test": 6, + "property_type": 0, + "address_line_1": "1 Some Street", + "assessment_type": "RdSAP", + "completion_date": "2025-04-04", + "inspection_date": "2025-04-04", + "wet_rooms_count": 0, + "extensions_count": 0, + "measurement_type": 1, + "sap_flat_details": { + "level": 1, + "top_storey": "N", + "storey_count": 3, + "flat_location": 1, + "heat_loss_corridor": 2, + "unheated_corridor_length": { + "value": 10, + "quantity": "metres" + } + }, + "total_floor_area": 55, + "transaction_type": 16, + "conservatory_type": 1, + "heated_room_count": 5, + "registration_date": "2025-04-04", + "sap_energy_source": { + "mains_gas": "Y", + "meter_type": 2, + "pv_batteries": { + "pv_battery": { + "battery_capacity": 5 + } + }, + "pv_connection": 0, + "pv_battery_count": 1, + "photovoltaic_supply": { + "none_or_no_details": { + "percent_roof_area": 0 + } + }, + "wind_turbines_count": 0, + "wind_turbine_details": { + "hub_height": 0, + "rotor_diameter": 0 + }, + "gas_smart_meter_present": "false", + "is_dwelling_export_capable": "false", + "wind_turbines_terrain_type": 4, + "electricity_smart_meter_present": "true" + }, + "secondary_heating": { + "description": { + "value": "Room heaters, electric", + "language": "1" + }, + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + }, + "sap_building_parts": [ + { + "identifier": "Main Dwelling", + "wall_dry_lined": "N", + "floor_heat_loss": 7, + "sap_room_in_roof": { + "floor_area": 100, + "construction_age_band": "B" + }, + "roof_construction": 4, + "wall_construction": 4, + "building_part_number": 1, + "sap_floor_dimensions": [ + { + "floor": 0, + "room_height": { + "value": 2.45, + "quantity": "metres" + }, + "floor_insulation": 1, + "total_floor_area": { + "value": 45.82, + "quantity": "square metres" + }, + "party_wall_length": { + "value": 7.9, + "quantity": "metres" + }, + "floor_construction": 1, + "heat_loss_perimeter": { + "value": 19.5, + "quantity": "metres" + } + }, + { + "floor": 1, + "room_height": { + "value": 2.59, + "quantity": "metres" + }, + "total_floor_area": { + "value": 45.82, + "quantity": "square metres" + }, + "party_wall_length": { + "value": 7.9, + "quantity": "metres" + }, + "heat_loss_perimeter": { + "value": 19.5, + "quantity": "metres" + } + } + ], + "wall_insulation_type": 2, + "construction_age_band": "M", + "sap_alternative_wall_1": { + "wall_area": 10.4, + "wall_dry_lined": "N", + "wall_construction": 4, + "wall_insulation_type": 2, + "wall_thickness_measured": "N" + }, + "sap_alternative_wall_2": { + "wall_area": 10.8, + "wall_dry_lined": "N", + "wall_construction": 4, + "wall_insulation_type": 2, + "wall_thickness_measured": "N" + }, + "party_wall_construction": 0, + "wall_thickness_measured": "N", + "roof_insulation_location": 2, + "roof_insulation_thickness": "200mm", + "wall_insulation_thickness": "NI", + "floor_insulation_thickness": "NI" + } + ], + "open_chimneys_count": 1, + "solar_water_heating": "N", + "habitable_room_count": 5, + "heating_cost_current": 365.98, + "insulated_door_count": 2, + "co2_emissions_current": 2.4, + "energy_rating_average": 60, + "energy_rating_current": 50, + "lighting_cost_current": 123.45, + "main_heating_controls": [ + { + "description": { + "value": "Programmer, room thermostat and TRVs", + "language": "1" + }, + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + }, + { + "description": { + "value": "Time and temperature zone control", + "language": "1" + }, + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + } + ], + "has_hot_water_cylinder": "true", + "heating_cost_potential": 250.34, + "hot_water_cost_current": 200.4, + "insulated_door_u_value": 3, + "mechanical_ventilation": 6, + "percent_draughtproofed": 100, + "suggested_improvements": [ + { + "sequence": 1, + "typical_saving": 139, + "indicative_cost": "£220 - £250", + "improvement_type": "G", + "improvement_details": { + "improvement_number": 66 + }, + "improvement_category": 5, + "energy_performance_rating": 70, + "environmental_impact_rating": 70 + }, + { + "sequence": 2, + "typical_saving": 49, + "indicative_cost": "£4,000 - £7,000", + "improvement_type": "N", + "improvement_details": { + "improvement_number": 19 + }, + "improvement_category": 5, + "energy_performance_rating": 72, + "environmental_impact_rating": 73 + }, + { + "sequence": 3, + "typical_saving": 286, + "indicative_cost": "£8,000 - £10,000", + "improvement_type": "U", + "improvement_details": { + "improvement_number": 34 + }, + "improvement_category": 5, + "energy_performance_rating": 83, + "environmental_impact_rating": 75 + } + ], + "co2_emissions_potential": 1.4, + "energy_rating_potential": 72, + "lighting_cost_potential": 84.23, + "schema_version_original": "SAP-19.0", + "hot_water_cost_potential": 180.43, + "renewable_heat_incentive": { + "water_heating": 2285, + "impact_of_loft_insulation": -2114, + "impact_of_cavity_insulation": -122, + "impact_of_solid_wall_insulation": -3560, + "space_heating_existing_dwelling": 13120 + }, + "draughtproofed_door_count": 1, + "mechanical_vent_duct_type": 3, + "energy_consumption_current": 230, + "has_fixed_air_conditioning": "false", + "multiple_glazed_proportion": 100, + "calculation_software_version": "13.05r16", + "energy_consumption_potential": 88, + "environmental_impact_current": 52, + "windows_transmission_details": { + "u_value": 2, + "data_source": 2, + "solar_transmittance": 0.72 + }, + "cfl_fixed_lighting_bulbs_count": 5, + "current_energy_efficiency_band": "E", + "environmental_impact_potential": 74, + "led_fixed_lighting_bulbs_count": 10, + "mechanical_vent_duct_placement": 2, + "mechanical_vent_duct_insulation": 2, + "potential_energy_efficiency_band": "C", + "pressure_test_certificate_number": 0, + "mechanical_ventilation_index_number": 12, + "co2_emissions_current_per_floor_area": 20, + "low_energy_fixed_lighting_bulbs_count": 16, + "mechanical_vent_duct_insulation_level": 2, + "mechanical_vent_measured_installation": "false", + "incandescent_fixed_lighting_bulbs_count": 0 +} \ No newline at end of file diff --git a/backend/epc_api/json_samples/SAP-Schema-16.0/rdsap.json b/backend/epc_api/json_samples/SAP-Schema-16.0/rdsap.json new file mode 100644 index 00000000..f1caacf4 --- /dev/null +++ b/backend/epc_api/json_samples/SAP-Schema-16.0/rdsap.json @@ -0,0 +1,299 @@ +{ + "uprn": 12457, + "roofs": [ + { + "description": "Pitched, 300+ mm loft insulation", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + } + ], + "walls": [ + { + "description": "Cavity wall, filled cavity", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + }, + { + "description": "Cavity wall, as built, insulated (assumed)", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "floors": [ + { + "description": "Solid, no insulation (assumed)", + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + } + ], + "status": "entered", + "windows": [ + { + "description": "Fully double glazed", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "lighting": { + "description": "Low energy lighting in 67% of fixed outlets", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + }, + "postcode": "AA1 1AA", + "hot_water": { + "description": "From main system", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + }, + "post_town": "Town", + "built_form": 1, + "door_count": 2, + "glazed_area": 1, + "region_code": 12, + "report_type": 2, + "sap_heating": { + "wwhrs": { + "rooms_with_bath_and_or_shower": 2, + "rooms_with_mixer_shower_no_bath": 0, + "rooms_with_bath_and_mixer_shower": 0 + }, + "cylinder_size": 1, + "water_heating_code": 901, + "water_heating_fuel": 26, + "main_heating_details": [ + { + "has_fghrs": "N", + "main_fuel_type": 26, + "boiler_flue_type": 2, + "fan_flue_present": "Y", + "heat_emitter_type": 1, + "boiler_index_number": 10265, + "main_heating_number": 1, + "main_heating_control": 2106, + "main_heating_category": 2, + "main_heating_fraction": 1, + "main_heating_data_source": 1 + } + ], + "has_fixed_air_conditioning": "false" + }, + "sap_version": 9.91, + "schema_type": "SAP-Schema-16.0", + "uprn_source": "Energy Assessor", + "country_code": "EAW", + "main_heating": [ + { + "description": "Boiler and radiators, mains gas", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "dwelling_type": "Detached bungalow", + "language_code": 1, + "property_type": 1, + "address_line_1": "11, Street Road", + "schema_version": "LIG-16.0", + "assessment_type": "RdSAP", + "completion_date": "2012-09-30", + "inspection_date": "2012-09-27", + "extensions_count": 1, + "measurement_type": 1, + "total_floor_area": 107, + "transaction_type": 1, + "conservatory_type": 1, + "heated_room_count": 5, + "registration_date": "2012-09-30", + "restricted_access": 0, + "sap_energy_source": { + "main_gas": "Y", + "meter_type": 2, + "photovoltaic_supply": { + "percent_roof_area": 0 + }, + "wind_turbines_count": 0, + "wind_turbines_terrain_type": 2 + }, + "secondary_heating": { + "description": "None", + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + }, + "lzc_energy_sources": [ + 11 + ], + "sap_building_parts": [ + { + "identifier": "Main Dwelling", + "wall_dry_lined": "N", + "wall_thickness": 300, + "floor_heat_loss": 7, + "roof_construction": 4, + "wall_construction": 4, + "building_part_number": 1, + "sap_floor_dimensions": [ + { + "floor": 0, + "room_height": 2.33, + "floor_insulation": 1, + "total_floor_area": 97.96, + "floor_construction": 1, + "heat_loss_perimeter": 37 + } + ], + "wall_insulation_type": 2, + "construction_age_band": "E", + "wall_thickness_measured": "Y", + "roof_insulation_location": 2, + "roof_insulation_thickness": "300mm+" + }, + { + "identifier": "Extension", + "wall_dry_lined": "N", + "floor_heat_loss": 7, + "roof_construction": 5, + "wall_construction": 4, + "building_part_number": 2, + "sap_floor_dimensions": [ + { + "floor": 0, + "room_height": 3.08, + "floor_insulation": 1, + "total_floor_area": 8.63, + "floor_construction": 1, + "heat_loss_perimeter": 7.42 + } + ], + "wall_insulation_type": 4, + "construction_age_band": "K", + "wall_thickness_measured": "N", + "roof_insulation_location": 4, + "roof_insulation_thickness": "NI" + } + ], + "low_energy_lighting": 67, + "solar_water_heating": "N", + "bedf_revision_number": 329, + "habitable_room_count": 5, + "heating_cost_current": { + "value": 494, + "currency": "GBP" + }, + "insulated_door_count": 0, + "co2_emissions_current": 2.9, + "energy_rating_average": 60, + "energy_rating_current": 73, + "lighting_cost_current": { + "value": 73, + "currency": "GBP" + }, + "main_heating_controls": [ + { + "description": "Programmer, room thermostat and TRVs", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "multiple_glazing_type": 2, + "open_fireplaces_count": 0, + "has_hot_water_cylinder": "false", + "heating_cost_potential": { + "value": 428, + "currency": "GBP" + }, + "hot_water_cost_current": { + "value": 90, + "currency": "GBP" + }, + "mechanical_ventilation": 0, + "percent_draughtproofed": 100, + "suggested_improvements": [ + { + "sequence": 1, + "typical_saving": { + "value": 68, + "currency": "GBP" + }, + "indicative_cost": "£800 - £1,200", + "improvement_type": "W", + "improvement_details": { + "improvement_number": 47 + }, + "improvement_category": 5, + "energy_performance_rating": 76, + "environmental_impact_rating": 76 + }, + { + "sequence": 2, + "typical_saving": { + "value": 16, + "currency": "GBP" + }, + "indicative_cost": "£25", + "improvement_type": "E", + "improvement_details": { + "improvement_number": 35 + }, + "improvement_category": 5, + "energy_performance_rating": 76, + "environmental_impact_rating": 76 + }, + { + "sequence": 3, + "typical_saving": { + "value": 26, + "currency": "GBP" + }, + "indicative_cost": "£4,000 - £6,000", + "improvement_type": "N", + "improvement_details": { + "improvement_number": 19 + }, + "improvement_category": 5, + "energy_performance_rating": 77, + "environmental_impact_rating": 78 + }, + { + "sequence": 4, + "typical_saving": { + "value": 238, + "currency": "GBP" + }, + "indicative_cost": "£9,000 - £14,000", + "improvement_type": "U", + "improvement_details": { + "improvement_number": 34 + }, + "improvement_category": 5, + "energy_performance_rating": 86, + "environmental_impact_rating": 86 + } + ], + "co2_emissions_potential": 1.4, + "energy_rating_potential": 86, + "lighting_cost_potential": { + "value": 55, + "currency": "GBP" + }, + "hot_water_cost_potential": { + "value": 64, + "currency": "GBP" + }, + "renewable_heat_incentive": { + "water_heating": 2264, + "space_heating_existing_dwelling": 9324 + }, + "seller_commission_report": "Y", + "energy_consumption_current": 144, + "has_fixed_air_conditioning": "false", + "multiple_glazed_proportion": 100, + "calculation_software_version": 8.0, + "energy_consumption_potential": 64, + "environmental_impact_current": 72, + "fixed_lighting_outlets_count": 15, + "current_energy_efficiency_band": "C", + "environmental_impact_potential": 86, + "has_heated_separate_conservatory": "false", + "potential_energy_efficiency_band": "B", + "co2_emissions_current_per_floor_area": 28, + "low_energy_fixed_lighting_outlets_count": 10 +} \ No newline at end of file diff --git a/backend/epc_api/json_samples/SAP-Schema-16.0/sap.json b/backend/epc_api/json_samples/SAP-Schema-16.0/sap.json new file mode 100644 index 00000000..3fdf06bc --- /dev/null +++ b/backend/epc_api/json_samples/SAP-Schema-16.0/sap.json @@ -0,0 +1,348 @@ +{ + "uprn": 12457, + "roofs": [ + { + "description": "(other premises above)", + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + } + ], + "walls": [ + { + "description": "Average thermal transmittance 0.34 W/m²K", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "floors": [ + { + "description": "Average thermal transmittance 0.25 W/m²K", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "status": "entered", + "windows": { + "description": "Fully double glazed", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + }, + "lighting": { + "description": "Low energy lighting in 57% of fixed outlets", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + }, + "postcode": "AA1 1AA", + "data_type": 2, + "hot_water": { + "description": "From main system", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + }, + "post_town": "Town", + "built_form": 1, + "living_area": 31.53, + "orientation": 0, + "region_code": 17, + "report_type": 3, + "sap_heating": { + "thermal_store": 1, + "has_solar_panel": "false", + "water_fuel_type": 1, + "water_heating_code": 901, + "hot_water_store_size": 145, + "main_heating_details": [ + { + "main_fuel_type": 1, + "heat_emitter_type": 1, + "boiler_index_number": 9206, + "is_flue_fan_present": "true", + "main_heating_number": 1, + "main_heating_control": 2106, + "is_interlocked_system": "true", + "main_heating_category": 2, + "main_heating_fraction": 1, + "main_heating_data_source": 1, + "has_delayed_start_thermostat": "false", + "load_or_weather_compensation": 0, + "is_central_heating_pump_in_heated_space": "true" + } + ], + "has_hot_water_cylinder": "true", + "has_cylinder_thermostat": "true", + "has_fixed_air_conditioning": "false", + "secondary_heating_category": 1, + "is_cylinder_in_heated_space": "true", + "is_hot_water_separately_timed": "true", + "is_primary_pipework_insulated": "true", + "hot_water_store_insulation_type": 1, + "hot_water_store_heat_loss_source": 3, + "hot_water_store_insulation_thickness": 50 + }, + "sap_version": 9.9, + "schema_type": "SAP-Schema-16.0", + "uprn_source": "Energy Assessor", + "country_code": "EAW", + "main_heating": [ + { + "description": "Boiler and radiators, mains gas", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "air_tightness": { + "description": "Air permeability 2.7 m³/h.m² (as tested)", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + }, + "dwelling_type": "Mid-floor flat", + "language_code": 1, + "property_type": 2, + "address_line_1": "28, Place Drive", + "schema_version": "LIG-16.0", + "assessment_date": "2012-09-29", + "assessment_type": "SAP", + "completion_date": "2012-09-29", + "inspection_date": "2012-09-29", + "sap_ventilation": { + "psv_count": 0, + "pressure_test": 1, + "air_permeability": 2.72, + "open_flues_count": 0, + "ventilation_type": 1, + "extract_fans_count": 3, + "open_fireplaces_count": 0, + "sheltered_sides_count": 2, + "flueless_gas_fires_count": 0 + }, + "sap_data_version": 9.81, + "sap_flat_details": { + "level": 2 + }, + "total_floor_area": 80, + "transaction_type": 6, + "conservatory_type": 1, + "registration_date": "2012-09-29", + "restricted_access": 0, + "sap_energy_source": { + "electricity_tariff": 1, + "wind_turbines_count": 0, + "wind_turbine_terrain_type": 1, + "fixed_lighting_outlets_count": 7, + "low_energy_fixed_lighting_outlets_count": 4, + "low_energy_fixed_lighting_outlets_percentage": 57 + }, + "sap_opening_types": [ + { + "name": 1, + "type": 2, + "u_value": 2, + "data_source": 2, + "description": "Door", + "glazing_type": 6 + }, + { + "name": 2, + "type": 4, + "u_value": 1.8, + "data_source": 2, + "description": "Window", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + } + ], + "secondary_heating": { + "description": "None", + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + }, + "lzc_energy_sources": [ + 11 + ], + "sap_building_parts": [ + { + "sap_walls": [ + { + "name": "Wall 1", + "u_value": 0.35, + "wall_type": 2, + "description": "Wall 5 render", + "total_wall_area": 58.29, + "is_curtain_walling": "false" + }, + { + "name": "Wall 2", + "u_value": 0.27, + "wall_type": 2, + "description": "Wall 2 common area", + "total_wall_area": 9.28, + "is_curtain_walling": "false" + }, + { + "name": "Wall 3", + "u_value": 0.35, + "wall_type": 2, + "description": "Wall 3 lift shaft", + "total_wall_area": 8.9, + "is_curtain_walling": "false" + } + ], + "identifier": "Main Dwelling", + "overshading": 2, + "sap_openings": [ + { + "name": 1, + "type": 1, + "width": 0.91, + "height": 2.1, + "location": "Wall 2", + "orientation": 0 + }, + { + "name": 2, + "type": 2, + "width": 1.15, + "height": 1.4, + "location": "Wall 1", + "orientation": 6 + }, + { + "name": 3, + "type": 2, + "width": 0.9, + "height": 1.4, + "location": "Wall 1", + "orientation": 7 + }, + { + "name": 4, + "type": 2, + "width": 1.3, + "height": 1.4, + "location": "Wall 1", + "orientation": 6 + }, + { + "name": 5, + "type": 2, + "width": 1.3, + "height": 1.4, + "location": "Wall 1", + "orientation": 5 + }, + { + "name": 6, + "type": 2, + "width": 1.15, + "height": 1.4, + "location": "Wall 1", + "orientation": 6 + }, + { + "name": 7, + "type": 2, + "width": 0.55, + "height": 1.05, + "location": "Wall 1", + "orientation": 4 + }, + { + "name": 8, + "type": 2, + "width": 1.15, + "height": 1.4, + "location": "Wall 1", + "orientation": 4 + }, + { + "name": 9, + "type": 2, + "width": 1.15, + "height": 1.4, + "location": "Wall 1", + "orientation": 4 + }, + { + "name": 10, + "type": 2, + "width": 1.3, + "height": 1.05, + "location": "Wall 1", + "orientation": 4 + } + ], + "construction_year": 2011, + "sap_thermal_bridges": { + "thermal_bridge_code": 4, + "user_defined_y_value": 0.08, + "calculation_reference": "SAP 2005 record" + }, + "building_part_number": 1, + "sap_floor_dimensions": [ + { + "storey": 1, + "u_value": 0.25, + "floor_type": 4, + "description": "Floor 2", + "storey_height": 2.32, + "heat_loss_area": 5.19, + "total_floor_area": 79.9 + } + ], + "thermal_mass_parameter": 250 + } + ], + "bedf_revision_number": 329, + "heating_cost_current": 213, + "co2_emissions_current": 1.3, + "energy_rating_average": 60, + "energy_rating_current": 82, + "lighting_cost_current": 66, + "main_heating_controls": [ + { + "description": "Programmer, room thermostat and TRVs", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "has_hot_water_cylinder": "true", + "heating_cost_potential": 215, + "hot_water_cost_current": 94, + "suggested_improvements": [ + { + "sequence": 1, + "typical_saving": 18, + "indicative_cost": "£8", + "improvement_type": "E", + "improvement_details": { + "improvement_number": 35 + }, + "improvement_category": 1, + "energy_performance_rating": 83, + "environmental_impact_rating": 87 + } + ], + "co2_emissions_potential": 1.3, + "energy_rating_potential": 83, + "lighting_cost_potential": 46, + "hot_water_cost_potential": 94, + "is_in_smoke_control_area": "unknown", + "renewable_heat_incentive": { + "rhi_new_dwelling": { + "space_heating": 1780, + "water_heating": 2306 + } + }, + "seller_commission_report": "Y", + "energy_consumption_current": 87, + "has_fixed_air_conditioning": "false", + "calculation_software_version": 5.4, + "energy_consumption_potential": 82, + "environmental_impact_current": 86, + "current_energy_efficiency_band": "B", + "environmental_impact_potential": 87, + "has_heated_separate_conservatory": "false", + "potential_energy_efficiency_band": "B", + "co2_emissions_current_per_floor_area": 16 +} \ No newline at end of file diff --git a/backend/epc_api/json_samples/SAP-Schema-16.1/rdsap.json b/backend/epc_api/json_samples/SAP-Schema-16.1/rdsap.json new file mode 100644 index 00000000..e6a3bb0a --- /dev/null +++ b/backend/epc_api/json_samples/SAP-Schema-16.1/rdsap.json @@ -0,0 +1,400 @@ +{ + "uprn": 12457, + "roofs": [ + { + "description": "Pitched, 300+ mm loft insulation", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + } + ], + "walls": [ + { + "description": "Cavity wall, filled cavity", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + }, + { + "description": "System built, as built, no insulation (assumed)", + "energy_efficiency_rating": 1, + "environmental_efficiency_rating": 1 + }, + { + "description": "Solid brick, as built, no insulation (assumed)", + "energy_efficiency_rating": 1, + "environmental_efficiency_rating": 1 + } + ], + "floors": [ + { + "description": "Suspended, no insulation (assumed)", + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + } + ], + "status": "entered", + "tenure": 2, + "windows": [ + { + "description": "Fully double glazed", + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 3 + } + ], + "addendum": { + "system_build": "true" + }, + "lighting": { + "description": "Low energy lighting in 36% of fixed outlets", + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 3 + }, + "postcode": "AA1 1AA", + "hot_water": { + "description": "From main system", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + }, + "post_town": "Town", + "built_form": 4, + "door_count": 2, + "glazed_area": 1, + "region_code": 14, + "report_type": 2, + "sap_heating": { + "wwhrs": { + "rooms_with_bath_and_or_shower": 1, + "rooms_with_mixer_shower_no_bath": 0, + "rooms_with_bath_and_mixer_shower": 0 + }, + "cylinder_size": 2, + "water_heating_code": 901, + "water_heating_fuel": 26, + "cylinder_thermostat": "Y", + "main_heating_details": [ + { + "has_fghrs": "N", + "main_fuel_type": 26, + "boiler_flue_type": 2, + "fan_flue_present": "Y", + "heat_emitter_type": 1, + "main_heating_number": 1, + "main_heating_control": 2103, + "main_heating_category": 2, + "main_heating_fraction": 1, + "sap_main_heating_code": 101, + "main_heating_data_source": 2 + } + ], + "cylinder_insulation_type": 1, + "has_fixed_air_conditioning": "false", + "cylinder_insulation_thickness": 38 + }, + "sap_version": 9.91, + "schema_type": "SAP-Schema-16.1", + "uprn_source": "Energy Assessor", + "country_code": "EAW", + "main_heating": [ + { + "description": "Boiler and radiators, mains gas", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "dwelling_type": "Mid-terrace house", + "language_code": 1, + "property_type": 0, + "address_line_1": "15, What the Dickens Road", + "schema_version": "LIG-16.1", + "assessment_type": "RdSAP", + "completion_date": "2013-01-13", + "inspection_date": "2013-01-09", + "extensions_count": 1, + "measurement_type": 1, + "total_floor_area": 72, + "transaction_type": 8, + "conservatory_type": 1, + "heated_room_count": 5, + "registration_date": "2013-01-13", + "restricted_access": 0, + "sap_energy_source": { + "main_gas": "Y", + "meter_type": 2, + "photovoltaic_supply": { + "percent_roof_area": 0 + }, + "wind_turbines_count": 0, + "wind_turbines_terrain_type": 1 + }, + "secondary_heating": { + "description": "None", + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + }, + "lzc_energy_sources": [ + 9 + ], + "sap_building_parts": [ + { + "identifier": "Main Dwelling", + "wall_dry_lined": "N", + "wall_thickness": 300, + "floor_heat_loss": 7, + "roof_construction": 4, + "wall_construction": 4, + "building_part_number": 1, + "sap_alternative_wall": { + "wall_area": 18.9, + "wall_dry_lined": "N", + "wall_construction": 3, + "wall_insulation_type": 4, + "wall_thickness_measured": "N" + }, + "sap_floor_dimensions": [ + { + "floor": 0, + "room_height": 2.43, + "floor_insulation": 1, + "total_floor_area": 34.2, + "floor_construction": 2, + "heat_loss_perimeter": 14.9 + }, + { + "floor": 1, + "room_height": 2.47, + "total_floor_area": 34.2, + "heat_loss_perimeter": 9.84 + } + ], + "wall_insulation_type": 2, + "construction_age_band": "C", + "wall_thickness_measured": "Y", + "roof_insulation_location": 2, + "roof_insulation_thickness": "300mm+" + }, + { + "identifier": "Extension 1", + "wall_dry_lined": "N", + "wall_thickness": 200, + "floor_heat_loss": 7, + "roof_construction": 1, + "wall_construction": 8, + "building_part_number": 2, + "sap_floor_dimensions": [ + { + "floor": 0, + "room_height": 2.26, + "floor_insulation": 0, + "total_floor_area": 3.5, + "floor_construction": 0, + "heat_loss_perimeter": 5.7 + } + ], + "wall_insulation_type": 4, + "construction_age_band": "E", + "wall_thickness_measured": "Y", + "roof_insulation_location": 4, + "roof_insulation_thickness": "NI" + } + ], + "low_energy_lighting": 36, + "solar_water_heating": "N", + "bedf_revision_number": 333, + "habitable_room_count": 5, + "heating_cost_current": { + "value": 546, + "currency": "GBP" + }, + "insulated_door_count": 0, + "co2_emissions_current": 3.4, + "energy_rating_average": 60, + "energy_rating_current": 61, + "lighting_cost_current": { + "value": 69, + "currency": "GBP" + }, + "main_heating_controls": [ + { + "description": "Room thermostat only", + "energy_efficiency_rating": 2, + "environmental_efficiency_rating": 2 + } + ], + "multiple_glazing_type": 3, + "open_fireplaces_count": 0, + "has_hot_water_cylinder": "true", + "heating_cost_potential": { + "value": 378, + "currency": "GBP" + }, + "hot_water_cost_current": { + "value": 127, + "currency": "GBP" + }, + "mechanical_ventilation": 0, + "percent_draughtproofed": 100, + "suggested_improvements": [ + { + "sequence": 1, + "typical_saving": { + "value": 66, + "currency": "GBP" + }, + "indicative_cost": "£4,000 - £14,000", + "improvement_type": "Q", + "improvement_details": { + "improvement_number": 7 + }, + "improvement_category": 5, + "energy_performance_rating": 65, + "environmental_impact_rating": 63 + }, + { + "sequence": 2, + "typical_saving": { + "value": 36, + "currency": "GBP" + }, + "indicative_cost": "£800 - £1,200", + "improvement_type": "W", + "improvement_details": { + "improvement_number": 47 + }, + "improvement_category": 5, + "energy_performance_rating": 67, + "environmental_impact_rating": 66 + }, + { + "sequence": 3, + "typical_saving": { + "value": 23, + "currency": "GBP" + }, + "indicative_cost": "£35", + "improvement_type": "E", + "improvement_details": { + "improvement_number": 35 + }, + "improvement_category": 5, + "energy_performance_rating": 68, + "environmental_impact_rating": 67 + }, + { + "sequence": 4, + "typical_saving": { + "value": 21, + "currency": "GBP" + }, + "indicative_cost": "£350 - £450", + "improvement_type": "G", + "improvement_details": { + "improvement_number": 15 + }, + "improvement_category": 5, + "energy_performance_rating": 69, + "environmental_impact_rating": 68 + }, + { + "sequence": 5, + "typical_saving": { + "value": 75, + "currency": "GBP" + }, + "indicative_cost": "£2,200 - £3,000", + "improvement_type": "I", + "improvement_details": { + "improvement_number": 20 + }, + "improvement_category": 5, + "energy_performance_rating": 73, + "environmental_impact_rating": 73 + }, + { + "sequence": 6, + "typical_saving": { + "value": 34, + "currency": "GBP" + }, + "indicative_cost": "£4,000 - £6,000", + "improvement_type": "N", + "improvement_details": { + "improvement_number": 19 + }, + "improvement_category": 5, + "energy_performance_rating": 74, + "environmental_impact_rating": 76 + }, + { + "sequence": 7, + "typical_saving": { + "value": 247, + "currency": "GBP" + }, + "indicative_cost": "£9,000 - £14,000", + "improvement_type": "U", + "improvement_details": { + "improvement_number": 34 + }, + "improvement_category": 5, + "energy_performance_rating": 85, + "environmental_impact_rating": 86 + } + ], + "co2_emissions_potential": 1.0, + "energy_rating_potential": 85, + "lighting_cost_potential": { + "value": 42, + "currency": "GBP" + }, + "alternative_improvements": [ + { + "sequence": 1, + "typical_saving": { + "value": 57, + "currency": "GBP" + }, + "improvement_type": "Z1", + "improvement_details": { + "improvement_number": 51 + }, + "improvement_category": 6, + "energy_performance_rating": 72, + "environmental_impact_rating": 74 + }, + { + "sequence": 2, + "typical_saving": { + "value": 85, + "currency": "GBP" + }, + "improvement_type": "Z3", + "improvement_details": { + "improvement_number": 53 + }, + "improvement_category": 6, + "energy_performance_rating": 73, + "environmental_impact_rating": 72 + } + ], + "hot_water_cost_potential": { + "value": 68, + "currency": "GBP" + }, + "renewable_heat_incentive": { + "water_heating": 2530, + "space_heating_existing_dwelling": 8564 + }, + "seller_commission_report": "Y", + "energy_consumption_current": 244, + "has_fixed_air_conditioning": "false", + "multiple_glazed_proportion": 100, + "calculation_software_version": "6.2.0.6", + "energy_consumption_potential": 66, + "environmental_impact_current": 59, + "fixed_lighting_outlets_count": 11, + "current_energy_efficiency_band": "D", + "environmental_impact_potential": 86, + "has_heated_separate_conservatory": "false", + "potential_energy_efficiency_band": "B", + "co2_emissions_current_per_floor_area": 47, + "low_energy_fixed_lighting_outlets_count": 4 +} \ No newline at end of file diff --git a/backend/epc_api/json_samples/SAP-Schema-16.1/sap.json b/backend/epc_api/json_samples/SAP-Schema-16.1/sap.json new file mode 100644 index 00000000..ac083fe4 --- /dev/null +++ b/backend/epc_api/json_samples/SAP-Schema-16.1/sap.json @@ -0,0 +1,365 @@ +{ + "uprn": 12457, + "roofs": [ + { + "description": "Average thermal transmittance 0.15 W/m²K", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "walls": [ + { + "description": "Average thermal transmittance 0.30 W/m²K", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "floors": [ + { + "description": "Average thermal transmittance 0.18 W/m²K", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + } + ], + "status": "entered", + "windows": { + "description": "Fully double glazed", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + }, + "lighting": { + "description": "Low energy lighting in 33% of fixed outlets", + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 3 + }, + "postcode": "AA1 1AA", + "data_type": 2, + "hot_water": { + "description": "From main system", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + }, + "post_town": "Town", + "built_form": 4, + "living_area": 12.04, + "orientation": 0, + "region_code": 15, + "report_type": 3, + "sap_heating": { + "thermal_store": 1, + "has_solar_panel": "false", + "water_fuel_type": 1, + "water_heating_code": 901, + "main_heating_details": [ + { + "main_fuel_type": 1, + "heat_emitter_type": 1, + "boiler_index_number": 9901, + "is_flue_fan_present": "true", + "main_heating_number": 1, + "main_heating_control": 2106, + "is_interlocked_system": "true", + "main_heating_category": 2, + "main_heating_fraction": 1, + "main_heating_data_source": 1, + "has_delayed_start_thermostat": "false", + "load_or_weather_compensation": 0, + "is_central_heating_pump_in_heated_space": "true" + } + ], + "has_hot_water_cylinder": "false", + "has_fixed_air_conditioning": "false", + "secondary_heating_category": 1 + }, + "sap_version": 9.9, + "schema_type": "SAP-Schema-16.1", + "uprn_source": "Energy Assessor", + "country_code": "EAW", + "main_heating": [ + { + "description": "Boiler and radiators, mains gas", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "air_tightness": { + "description": "Air permeability 7.7 m³/h.m² (as tested)", + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 3 + }, + "dwelling_type": "Mid-terrace house", + "language_code": 1, + "property_type": 0, + "address_line_1": "28, Place Drive", + "schema_version": "LIG-16.0", + "assessment_date": "2013-01-12", + "assessment_type": "SAP", + "completion_date": "2013-01-12", + "inspection_date": "2013-01-12", + "sap_ventilation": { + "psv_count": 0, + "pressure_test": 1, + "air_permeability": 7.7, + "open_flues_count": 0, + "ventilation_type": 1, + "extract_fans_count": 3, + "open_fireplaces_count": 0, + "sheltered_sides_count": 2, + "flueless_gas_fires_count": 0 + }, + "total_floor_area": 59, + "transaction_type": 6, + "conservatory_type": 1, + "registration_date": "2013-01-12", + "restricted_access": 0, + "sap_energy_source": { + "electricity_tariff": 1, + "wind_turbines_count": 0, + "wind_turbine_terrain_type": 2, + "fixed_lighting_outlets_count": 9, + "low_energy_fixed_lighting_outlets_count": 3, + "low_energy_fixed_lighting_outlets_percentage": 33 + }, + "sap_opening_types": [ + { + "name": 1, + "type": 1, + "u_value": 1.57, + "data_source": 2, + "glazing_type": 1 + }, + { + "name": 2, + "type": 4, + "u_value": 1.9, + "frame_type": 2, + "data_source": 3, + "glazing_gap": 3, + "frame_factor": 0.7, + "glazing_type": 6, + "isargonfilled": "false", + "solar_transmittance": 0.63 + }, + { + "name": 4, + "type": 2, + "u_value": 1.9, + "data_source": 2, + "glazing_type": 6 + } + ], + "secondary_heating": { + "description": "None", + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + }, + "lzc_energy_sources": [ + 9 + ], + "sap_building_parts": [ + { + "sap_roofs": [ + { + "name": "Roof 1", + "u_value": 0.15, + "roof_type": 2, + "description": "Roof 1", + "total_roof_area": 29.25 + } + ], + "sap_walls": [ + { + "name": "Wall 1", + "u_value": 0.3, + "wall_type": 2, + "description": "Wall 1", + "total_wall_area": 38.69, + "is_curtain_walling": "false" + }, + { + "name": "Wall 2", + "u_value": 0.3, + "wall_type": 2, + "description": "High step", + "total_wall_area": 2.25, + "is_curtain_walling": "false" + }, + { + "name": "Wall 3", + "u_value": 0.3, + "wall_type": 2, + "description": "low step", + "total_wall_area": 2.25, + "is_curtain_walling": "false" + } + ], + "identifier": "Main Dwelling", + "overshading": 2, + "sap_openings": [ + { + "name": 1, + "type": 1, + "width": 0.93, + "height": 2.1, + "location": "Wall 1", + "orientation": 0 + }, + { + "name": 2, + "type": 2, + "width": 1.77, + "height": 1.35, + "location": "Wall 1", + "orientation": 1 + }, + { + "name": 3, + "type": 2, + "width": 1.2, + "height": 1.05, + "location": "Wall 1", + "orientation": 1 + }, + { + "name": 4, + "type": 4, + "width": 0.93, + "height": 2.1, + "location": "Wall 1", + "orientation": 0 + }, + { + "name": 5, + "type": 2, + "width": 1.2, + "height": 1.05, + "location": "Wall 1", + "orientation": 5 + }, + { + "name": 6, + "type": 2, + "width": 1.77, + "height": 1.05, + "location": "Wall 1", + "orientation": 5 + } + ], + "construction_year": 2012, + "sap_thermal_bridges": { + "thermal_bridge_code": 4, + "user_defined_y_value": 0.08, + "calculation_reference": "ACDs" + }, + "building_part_number": 1, + "sap_floor_dimensions": [ + { + "storey": 0, + "u_value": 0.18, + "floor_type": 2, + "description": "Floor 1", + "storey_height": 2.37, + "heat_loss_area": 29.25, + "total_floor_area": 29.25 + }, + { + "storey": 1, + "u_value": 0, + "floor_type": 3, + "storey_height": 2.59, + "heat_loss_area": 0, + "total_floor_area": 29.25 + } + ], + "thermal_mass_parameter": 250 + } + ], + "bedf_revision_number": 333, + "heating_cost_current": 236, + "co2_emissions_current": 1.3, + "energy_rating_average": 60, + "energy_rating_current": 79, + "lighting_cost_current": 66, + "main_heating_controls": [ + { + "description": "Programmer, room thermostat and TRVs", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "has_hot_water_cylinder": "false", + "heating_cost_potential": 240, + "hot_water_cost_current": 76, + "suggested_improvements": [ + { + "sequence": 1, + "typical_saving": 23, + "indicative_cost": "£15", + "improvement_type": "E", + "improvement_details": { + "improvement_number": 35 + }, + "improvement_category": 1, + "energy_performance_rating": 80, + "environmental_impact_rating": 84 + }, + { + "sequence": 2, + "typical_saving": 25, + "indicative_cost": "£4,000 - £6,000", + "improvement_type": "N", + "improvement_details": { + "improvement_number": 19 + }, + "improvement_category": 3, + "energy_performance_rating": 81, + "environmental_impact_rating": 86 + }, + { + "sequence": 3, + "typical_saving": 226, + "indicative_cost": "£11,000 - £20,000", + "improvement_type": "U", + "improvement_details": { + "improvement_number": 34 + }, + "improvement_category": 3, + "energy_performance_rating": 94, + "environmental_impact_rating": 98 + }, + { + "sequence": 4, + "typical_saving": 19, + "indicative_cost": "£1,500 - £4,000", + "improvement_type": "V", + "improvement_details": { + "improvement_number": 44 + }, + "improvement_category": 3, + "energy_performance_rating": 95, + "environmental_impact_rating": 99 + } + ], + "co2_emissions_potential": 1.2, + "energy_rating_potential": 80, + "lighting_cost_potential": 39, + "hot_water_cost_potential": 76, + "is_in_smoke_control_area": "unknown", + "renewable_heat_incentive": { + "rhi_new_dwelling": { + "space_heating": 1867, + "water_heating": 1839 + } + }, + "seller_commission_report": "Y", + "energy_consumption_current": 119, + "has_fixed_air_conditioning": "false", + "calculation_software_version": 5.4, + "energy_consumption_potential": 110, + "environmental_impact_current": 83, + "current_energy_efficiency_band": "C", + "environmental_impact_potential": 84, + "has_heated_separate_conservatory": "false", + "potential_energy_efficiency_band": "C", + "co2_emissions_current_per_floor_area": 22 +} \ No newline at end of file diff --git a/backend/epc_api/json_samples/SAP-Schema-16.2/rdsap.json b/backend/epc_api/json_samples/SAP-Schema-16.2/rdsap.json new file mode 100644 index 00000000..cea3d9c2 --- /dev/null +++ b/backend/epc_api/json_samples/SAP-Schema-16.2/rdsap.json @@ -0,0 +1,931 @@ +{ + "uprn": 12457, + "roofs": [ + { + "description": "Pitched, 75 mm loft insulation", + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 3 + } + ], + "walls": [ + { + "description": "Cavity wall, as built, no insulation (assumed)", + "energy_efficiency_rating": 2, + "environmental_efficiency_rating": 2 + }, + { + "description": "Cavity wall, as built, insulated (assumed)", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "floors": [ + { + "description": "Suspended, no insulation (assumed)", + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + }, + { + "description": "To unheated space, limited insulation (assumed)", + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + } + ], + "status": "entered", + "tenure": 1, + "windows": [ + { + "description": "Partial double glazing", + "energy_efficiency_rating": 2, + "environmental_efficiency_rating": 2 + } + ], + "addendum": { + "cavity_fill_recommended": "true" + }, + "lighting": { + "description": "Low energy lighting in 13% of fixed outlets", + "energy_efficiency_rating": 2, + "environmental_efficiency_rating": 2 + }, + "postcode": "AA1 1AA", + "hot_water": { + "description": "From main system", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + }, + "post_town": "Town", + "built_form": 1, + "door_count": 4, + "glazed_area": 4, + "region_code": 3, + "report_type": 2, + "sap_heating": { + "wwhrs": { + "rooms_with_bath_and_or_shower": 3, + "rooms_with_mixer_shower_no_bath": 0, + "rooms_with_bath_and_mixer_shower": 3 + }, + "cylinder_size": 4, + "water_heating_code": 901, + "water_heating_fuel": 26, + "cylinder_thermostat": "Y", + "secondary_fuel_type": 33, + "main_heating_details": [ + { + "has_fghrs": "N", + "main_fuel_type": 26, + "boiler_flue_type": 2, + "fan_flue_present": "N", + "heat_emitter_type": 1, + "main_heating_number": 1, + "main_heating_control": 2106, + "main_heating_category": 2, + "main_heating_fraction": 0.5, + "sap_main_heating_code": 101, + "main_heating_data_source": 2 + }, + { + "has_fghrs": "N", + "main_fuel_type": 26, + "boiler_flue_type": 2, + "fan_flue_present": "Y", + "heat_emitter_type": 1, + "main_heating_number": 2, + "main_heating_control": 2106, + "main_heating_category": 2, + "main_heating_fraction": 0.5, + "sap_main_heating_code": 101, + "main_heating_data_source": 2 + } + ], + "secondary_heating_type": 631, + "cylinder_insulation_type": 1, + "has_fixed_air_conditioning": "false", + "cylinder_insulation_thickness": 50 + }, + "sap_version": 9.91, + "sap_windows": [ + { + "u_value": 3.1, + "data_source": 2, + "orientation": 7, + "window_area": 1, + "window_type": 1, + "glazing_type": 3, + "window_location": 2, + "solar_transmittance": 0.76 + }, + { + "u_value": 3.1, + "data_source": 2, + "orientation": 7, + "window_area": 1, + "window_type": 1, + "glazing_type": 3, + "window_location": 2, + "solar_transmittance": 0.76 + }, + { + "u_value": 3.1, + "data_source": 2, + "orientation": 7, + "window_area": 1, + "window_type": 1, + "glazing_type": 3, + "window_location": 2, + "solar_transmittance": 0.76 + }, + { + "u_value": 3.1, + "data_source": 2, + "orientation": 7, + "window_area": 1, + "window_type": 1, + "glazing_type": 3, + "window_location": 2, + "solar_transmittance": 0.76 + }, + { + "u_value": 3.1, + "data_source": 2, + "orientation": 1, + "window_area": 1, + "window_type": 1, + "glazing_type": 3, + "window_location": 2, + "solar_transmittance": 0.76 + }, + { + "u_value": 3.1, + "data_source": 2, + "orientation": 1, + "window_area": 1, + "window_type": 1, + "glazing_type": 3, + "window_location": 0, + "solar_transmittance": 0.76 + }, + { + "u_value": 3.1, + "data_source": 2, + "orientation": 3, + "window_area": 1, + "window_type": 1, + "glazing_type": 3, + "window_location": 2, + "solar_transmittance": 0.76 + }, + { + "u_value": 3.1, + "data_source": 2, + "orientation": 3, + "window_area": 1, + "window_type": 1, + "glazing_type": 3, + "window_location": 2, + "solar_transmittance": 0.76 + }, + { + "u_value": 3.1, + "data_source": 2, + "orientation": 3, + "window_area": 1, + "window_type": 1, + "glazing_type": 3, + "window_location": 2, + "solar_transmittance": 0.76 + }, + { + "u_value": 3.1, + "data_source": 2, + "orientation": 3, + "window_area": 1, + "window_type": 1, + "glazing_type": 3, + "window_location": 2, + "solar_transmittance": 0.76 + }, + { + "u_value": 4.8, + "data_source": 2, + "orientation": 3, + "window_area": 2, + "window_type": 1, + "glazing_type": 5, + "window_location": 2, + "solar_transmittance": 0.85 + }, + { + "u_value": 4.8, + "data_source": 2, + "orientation": 3, + "window_area": 2, + "window_type": 1, + "glazing_type": 5, + "window_location": 2, + "solar_transmittance": 0.85 + }, + { + "u_value": 4.8, + "data_source": 2, + "orientation": 3, + "window_area": 2, + "window_type": 1, + "glazing_type": 5, + "window_location": 2, + "solar_transmittance": 0.85 + }, + { + "u_value": 4.8, + "data_source": 2, + "orientation": 3, + "window_area": 2, + "window_type": 1, + "glazing_type": 5, + "window_location": 2, + "solar_transmittance": 0.85 + }, + { + "u_value": 4.8, + "data_source": 2, + "orientation": 3, + "window_area": 1, + "window_type": 1, + "glazing_type": 5, + "window_location": 2, + "solar_transmittance": 0.85 + }, + { + "u_value": 4.8, + "data_source": 2, + "orientation": 7, + "window_area": 1, + "window_type": 1, + "glazing_type": 5, + "window_location": 0, + "solar_transmittance": 0.85 + }, + { + "u_value": 4.8, + "data_source": 2, + "orientation": 7, + "window_area": 1, + "window_type": 1, + "glazing_type": 5, + "window_location": 0, + "solar_transmittance": 0.85 + }, + { + "u_value": 4.8, + "data_source": 2, + "orientation": 7, + "window_area": 1, + "window_type": 1, + "glazing_type": 5, + "window_location": 0, + "solar_transmittance": 0.85 + }, + { + "u_value": 4.8, + "data_source": 2, + "orientation": 7, + "window_area": 1, + "window_type": 1, + "glazing_type": 5, + "window_location": 0, + "solar_transmittance": 0.85 + }, + { + "u_value": 4.8, + "data_source": 2, + "orientation": 7, + "window_area": 1, + "window_type": 1, + "glazing_type": 5, + "window_location": 0, + "solar_transmittance": 0.85 + }, + { + "u_value": 4.8, + "data_source": 2, + "orientation": 7, + "window_area": 1, + "window_type": 1, + "glazing_type": 5, + "window_location": 0, + "solar_transmittance": 0.85 + }, + { + "u_value": 4.8, + "data_source": 2, + "orientation": 7, + "window_area": 1, + "window_type": 1, + "glazing_type": 5, + "window_location": 0, + "solar_transmittance": 0.85 + }, + { + "u_value": 4.8, + "data_source": 2, + "orientation": 5, + "window_area": 1, + "window_type": 1, + "glazing_type": 5, + "window_location": 0, + "solar_transmittance": 0.85 + }, + { + "u_value": 4.8, + "data_source": 2, + "orientation": 5, + "window_area": 1, + "window_type": 1, + "glazing_type": 5, + "window_location": 0, + "solar_transmittance": 0.85 + }, + { + "u_value": 4.8, + "data_source": 2, + "orientation": 5, + "window_area": 4, + "window_type": 1, + "glazing_type": 5, + "window_location": 0, + "solar_transmittance": 0.85 + }, + { + "u_value": 4.8, + "data_source": 2, + "orientation": 5, + "window_area": 4, + "window_type": 1, + "glazing_type": 5, + "window_location": 0, + "solar_transmittance": 0.85 + }, + { + "u_value": 4.8, + "data_source": 2, + "orientation": 5, + "window_area": 4, + "window_type": 1, + "glazing_type": 5, + "window_location": 0, + "solar_transmittance": 0.85 + }, + { + "u_value": 4.8, + "data_source": 2, + "orientation": 5, + "window_area": 3, + "window_type": 1, + "glazing_type": 5, + "window_location": 0, + "solar_transmittance": 0.85 + }, + { + "u_value": 4.8, + "data_source": 2, + "orientation": 7, + "window_area": 1, + "window_type": 1, + "glazing_type": 5, + "window_location": 0, + "solar_transmittance": 0.85 + }, + { + "u_value": 4.8, + "data_source": 2, + "orientation": 7, + "window_area": 1, + "window_type": 1, + "glazing_type": 5, + "window_location": 0, + "solar_transmittance": 0.85 + }, + { + "u_value": 4.8, + "data_source": 2, + "orientation": 7, + "window_area": 1, + "window_type": 1, + "glazing_type": 5, + "window_location": 0, + "solar_transmittance": 0.85 + }, + { + "u_value": 4.8, + "data_source": 2, + "orientation": 7, + "window_area": 1, + "window_type": 1, + "glazing_type": 5, + "window_location": 0, + "solar_transmittance": 0.85 + }, + { + "u_value": 4.8, + "data_source": 2, + "orientation": 7, + "window_area": 1, + "window_type": 1, + "glazing_type": 5, + "window_location": 0, + "solar_transmittance": 0.85 + }, + { + "u_value": 4.8, + "data_source": 2, + "orientation": 7, + "window_area": 1, + "window_type": 1, + "glazing_type": 5, + "window_location": 0, + "solar_transmittance": 0.85 + }, + { + "u_value": 4.8, + "data_source": 2, + "orientation": 3, + "window_area": 1, + "window_type": 1, + "glazing_type": 5, + "window_location": 0, + "solar_transmittance": 0.85 + }, + { + "u_value": 4.8, + "data_source": 2, + "orientation": 3, + "window_area": 1, + "window_type": 1, + "glazing_type": 5, + "window_location": 0, + "solar_transmittance": 0.85 + }, + { + "u_value": 4.8, + "data_source": 2, + "orientation": 3, + "window_area": 5, + "window_type": 1, + "glazing_type": 5, + "window_location": 0, + "solar_transmittance": 0.85 + }, + { + "u_value": 4.8, + "data_source": 2, + "orientation": 3, + "window_area": 5, + "window_type": 1, + "glazing_type": 5, + "window_location": 0, + "solar_transmittance": 0.85 + }, + { + "u_value": 4.8, + "data_source": 2, + "orientation": 3, + "window_area": 1, + "window_type": 1, + "glazing_type": 5, + "window_location": 0, + "solar_transmittance": 0.85 + }, + { + "u_value": 3.1, + "data_source": 2, + "orientation": 5, + "window_area": 2, + "window_type": 1, + "glazing_type": 3, + "window_location": 0, + "solar_transmittance": 0.76 + }, + { + "u_value": 4.8, + "data_source": 2, + "orientation": 5, + "window_area": 3, + "window_type": 1, + "glazing_type": 5, + "window_location": 0, + "solar_transmittance": 0.85 + }, + { + "u_value": 4.8, + "data_source": 2, + "orientation": 5, + "window_area": 3, + "window_type": 1, + "glazing_type": 5, + "window_location": 0, + "solar_transmittance": 0.85 + }, + { + "u_value": 4.8, + "data_source": 2, + "orientation": 5, + "window_area": 3, + "window_type": 1, + "glazing_type": 5, + "window_location": 0, + "solar_transmittance": 0.85 + }, + { + "u_value": 4.8, + "data_source": 2, + "orientation": 5, + "window_area": 8, + "window_type": 1, + "glazing_type": 5, + "window_location": 0, + "solar_transmittance": 0.85 + }, + { + "u_value": 4.8, + "data_source": 2, + "orientation": 5, + "window_area": 3, + "window_type": 1, + "glazing_type": 5, + "window_location": 0, + "solar_transmittance": 0.85 + }, + { + "u_value": 2, + "data_source": 2, + "orientation": 3, + "window_area": 2, + "window_type": 1, + "glazing_type": 2, + "window_location": 0, + "solar_transmittance": 0.72 + }, + { + "u_value": 2, + "data_source": 2, + "orientation": 3, + "window_area": 3, + "window_type": 1, + "glazing_type": 2, + "window_location": 0, + "solar_transmittance": 0.72 + }, + { + "u_value": 2, + "data_source": 2, + "orientation": 3, + "window_area": 3, + "window_type": 1, + "glazing_type": 2, + "window_location": 0, + "solar_transmittance": 0.72 + }, + { + "u_value": 2, + "data_source": 2, + "orientation": 7, + "window_area": 6, + "window_type": 1, + "glazing_type": 2, + "window_location": 0, + "solar_transmittance": 0.72 + }, + { + "u_value": 2, + "data_source": 2, + "orientation": 7, + "window_area": 8, + "window_type": 1, + "glazing_type": 2, + "window_location": 0, + "solar_transmittance": 0.72 + }, + { + "u_value": 2, + "data_source": 2, + "orientation": 7, + "window_area": 8, + "window_type": 1, + "glazing_type": 2, + "window_location": 0, + "solar_transmittance": 0.72 + }, + { + "u_value": 2, + "data_source": 2, + "orientation": 5, + "window_area": 9, + "window_type": 1, + "glazing_type": 2, + "window_location": 0, + "solar_transmittance": 0.72 + } + ], + "schema_type": "SAP-Schema-16.2", + "uprn_source": "Energy Assessor", + "country_code": "EAW", + "main_heating": [ + { + "description": "Boiler and radiators, mains gas", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + }, + { + "description": "Boiler and radiators, mains gas", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "dwelling_type": "Detached house", + "language_code": 1, + "property_type": 0, + "address_line_1": "11, Some Close", + "address_line_2": "Long Itchington", + "schema_version": "LIG-16.1", + "assessment_type": "RdSAP", + "completion_date": "2013-07-12", + "inspection_date": "2013-06-27", + "extensions_count": 2, + "measurement_type": 1, + "total_floor_area": 1563, + "transaction_type": 5, + "conservatory_type": 2, + "heated_room_count": 15, + "registration_date": "2013-07-12", + "restricted_access": 0, + "sap_energy_source": { + "main_gas": "Y", + "meter_type": 1, + "photovoltaic_supply": { + "percent_roof_area": 0 + }, + "wind_turbines_count": 0, + "wind_turbines_terrain_type": 3 + }, + "secondary_heating": { + "description": "Room heaters, coal", + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + }, + "lzc_energy_sources": [ + 9 + ], + "sap_building_parts": [ + { + "identifier": "Main Dwelling", + "wall_dry_lined": "N", + "wall_thickness": 320, + "floor_heat_loss": 7, + "roof_construction": 4, + "wall_construction": 4, + "building_part_number": 1, + "sap_floor_dimensions": [ + { + "floor": 0, + "room_height": 2.52, + "floor_insulation": 1, + "total_floor_area": 668.41, + "floor_construction": 2, + "heat_loss_perimeter": 96.43 + }, + { + "floor": 1, + "room_height": 2.87, + "total_floor_area": 691.43, + "heat_loss_perimeter": 106.57 + } + ], + "wall_insulation_type": 4, + "construction_age_band": "C", + "wall_thickness_measured": "Y", + "roof_insulation_location": 2, + "roof_insulation_thickness": "75mm" + }, + { + "identifier": "Extension 1", + "wall_dry_lined": "N", + "wall_thickness": 500, + "floor_heat_loss": 7, + "roof_construction": 5, + "wall_construction": 4, + "building_part_number": 2, + "sap_floor_dimensions": [ + { + "floor": 0, + "room_height": 2.54, + "floor_insulation": 1, + "total_floor_area": 82.03, + "floor_construction": 1, + "heat_loss_perimeter": 34.27 + } + ], + "wall_insulation_type": 4, + "construction_age_band": "H", + "wall_thickness_measured": "Y", + "roof_insulation_location": 2, + "roof_insulation_thickness": "75mm" + }, + { + "identifier": "Extension 2", + "wall_dry_lined": "N", + "wall_thickness": 280, + "floor_heat_loss": 2, + "roof_construction": 4, + "wall_construction": 4, + "building_part_number": 3, + "sap_floor_dimensions": [ + { + "floor": 0, + "room_height": 3.21, + "floor_insulation": 1, + "total_floor_area": 121.31, + "floor_construction": 2, + "heat_loss_perimeter": 37.71 + } + ], + "wall_insulation_type": 4, + "construction_age_band": "H", + "wall_thickness_measured": "Y", + "roof_insulation_location": 2, + "roof_insulation_thickness": "75mm" + } + ], + "low_energy_lighting": 13, + "solar_water_heating": "N", + "bedf_revision_number": 340, + "habitable_room_count": 15, + "heating_cost_current": { + "value": 13144, + "currency": "GBP" + }, + "insulated_door_count": 0, + "co2_emissions_current": 78, + "energy_rating_average": 60, + "energy_rating_current": 52, + "lighting_cost_current": { + "value": 635, + "currency": "GBP" + }, + "main_heating_controls": [ + { + "description": "Programmer, room thermostat and TRVs", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "multiple_glazing_type": "ND", + "open_fireplaces_count": 0, + "has_hot_water_cylinder": "true", + "heating_cost_potential": { + "value": 8582, + "currency": "GBP" + }, + "hot_water_cost_current": { + "value": 188, + "currency": "GBP" + }, + "mechanical_ventilation": 2, + "percent_draughtproofed": 0, + "suggested_improvements": [ + { + "sequence": 1, + "typical_saving": { + "value": 637.67, + "currency": "GBP" + }, + "indicative_cost": "£100 - £350", + "improvement_type": "A", + "improvement_details": { + "improvement_number": 5 + }, + "improvement_category": 5, + "energy_performance_rating": 55, + "environmental_impact_rating": 42 + }, + { + "sequence": 2, + "typical_saving": { + "value": 1224.21, + "currency": "GBP" + }, + "indicative_cost": "£500 - £1,500", + "improvement_type": "B", + "improvement_details": { + "improvement_number": 6 + }, + "improvement_category": 5, + "energy_performance_rating": 59, + "environmental_impact_rating": 46 + }, + { + "sequence": 3, + "typical_saving": { + "value": 476.28, + "currency": "GBP" + }, + "indicative_cost": "£800 - £1,200", + "improvement_type": "W", + "improvement_details": { + "improvement_number": 47 + }, + "improvement_category": 5, + "energy_performance_rating": 60, + "environmental_impact_rating": 48 + }, + { + "sequence": 4, + "typical_saving": { + "value": 787.42, + "currency": "GBP" + }, + "indicative_cost": "£80 - £120", + "improvement_type": "D", + "improvement_details": { + "improvement_number": 10 + }, + "improvement_category": 5, + "energy_performance_rating": 63, + "environmental_impact_rating": 51 + }, + { + "sequence": 5, + "typical_saving": { + "value": 234.75, + "currency": "GBP" + }, + "indicative_cost": "£305", + "improvement_type": "E", + "improvement_details": { + "improvement_number": 35 + }, + "improvement_category": 5, + "energy_performance_rating": 64, + "environmental_impact_rating": 51 + }, + { + "sequence": 6, + "typical_saving": { + "value": 1219.36, + "currency": "GBP" + }, + "indicative_cost": "£2,200 - £3,000", + "improvement_type": "I", + "improvement_details": { + "improvement_number": 20 + }, + "improvement_category": 5, + "energy_performance_rating": 68, + "environmental_impact_rating": 56 + }, + { + "sequence": 7, + "typical_saving": { + "value": 310.6, + "currency": "GBP" + }, + "indicative_cost": "£3,300 - £6,500", + "improvement_type": "O", + "improvement_details": { + "improvement_number": 8 + }, + "improvement_category": 5, + "energy_performance_rating": 69, + "environmental_impact_rating": 58 + } + ], + "co2_emissions_potential": 51, + "energy_rating_potential": 69, + "lighting_cost_potential": { + "value": 344, + "currency": "GBP" + }, + "hot_water_cost_potential": { + "value": 151, + "currency": "GBP" + }, + "renewable_heat_incentive": { + "water_heating": 3640, + "impact_of_loft_insulation": -12585, + "impact_of_cavity_insulation": -21982, + "space_heating_existing_dwelling": 207769 + }, + "seller_commission_report": "Y", + "energy_consumption_current": 239, + "has_fixed_air_conditioning": "false", + "calculation_software_version": "1.4.1.0", + "energy_consumption_potential": 153, + "environmental_impact_current": 40, + "fixed_lighting_outlets_count": 70, + "current_energy_efficiency_band": "E", + "environmental_impact_potential": 58, + "has_heated_separate_conservatory": "false", + "potential_energy_efficiency_band": "C", + "co2_emissions_current_per_floor_area": 50, + "low_energy_fixed_lighting_outlets_count": 9 +} \ No newline at end of file diff --git a/backend/epc_api/json_samples/SAP-Schema-16.2/sap.json b/backend/epc_api/json_samples/SAP-Schema-16.2/sap.json new file mode 100644 index 00000000..b6db5a2b --- /dev/null +++ b/backend/epc_api/json_samples/SAP-Schema-16.2/sap.json @@ -0,0 +1,2112 @@ +{ + "uprn": 12457, + "roofs": [ + { + "description": "Average thermal transmittance 0.14 W/m²K", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + } + ], + "walls": [ + { + "description": "Average thermal transmittance 0.17 W/m²K", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + } + ], + "floors": [ + { + "description": "Average thermal transmittance 0.13 W/m²K", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + } + ], + "status": "entered", + "windows": { + "description": "High performance glazing", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + }, + "lighting": { + "description": "Low energy lighting in all fixed outlets", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + }, + "postcode": "AA1 1AA", + "data_type": 2, + "hot_water": { + "description": "From main system, plus solar", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 5 + }, + "post_town": "Town", + "built_form": 1, + "living_area": 44.4, + "orientation": 0, + "region_code": 16, + "report_type": 3, + "sap_heating": { + "has_solar_panel": "true", + "water_fuel_type": 39, + "solar_store_volume": 100, + "water_heating_code": 901, + "secondary_fuel_type": 20, + "hot_water_store_size": 300, + "main_heating_details": [ + { + "main_fuel_type": 39, + "heat_emitter_type": 2, + "main_heating_code": 201, + "main_heating_number": 1, + "main_heating_control": 2207, + "main_heating_category": 4, + "main_heating_fraction": 1, + "main_heating_data_source": 3, + "has_delayed_start_thermostat": "false", + "load_or_weather_compensation": 0, + "underfloor_heat_emitter_type": 2, + "is_main_heating_hetas_approved": "false", + "is_central_heating_pump_in_heated_space": "true" + } + ], + "has_hot_water_cylinder": "true", + "has_solar_powered_pump": "false", + "secondary_heating_code": 633, + "has_cylinder_thermostat": "true", + "solar_panel_aperture_area": 5.26, + "has_fixed_air_conditioning": "false", + "secondary_heating_category": 10, + "solar_panel_collector_type": 3, + "is_cylinder_in_heated_space": "true", + "solar_panel_collector_pitch": 3, + "is_hot_water_separately_timed": "true", + "is_primary_pipework_insulated": "true", + "secondary_heating_data_source": 3, + "hot_water_store_insulation_type": 1, + "hot_water_store_heat_loss_source": 3, + "is_solar_store_combined_cylinder": "true", + "solar_panel_collector_data_source": 2, + "solar_panel_collector_orientation": 3, + "solar_panel_collector_overshading": 1, + "is_heat_pump_assisted_by_immersion": "false", + "is_secondary_heating_hetas_approved": "false", + "hot_water_store_insulation_thickness": 80, + "solar_panel_collector_heat_loss_rate": 3, + "solar_panel_collector_zero_loss_efficiency": 60 + }, + "sap_version": 9.9, + "schema_type": "SAP-Schema-16.2", + "uprn_source": "Energy Assessor", + "country_code": "EAW", + "main_heating": [ + { + "description": "Ground source heat pump, underfloor, electric", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 5 + } + ], + "air_tightness": { + "description": "Air permeability 6.4 m³/h.m² (as tested)", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + }, + "dwelling_type": "Detached house", + "language_code": 1, + "property_type": 0, + "address_line_1": "17, Street Terrace", + "schema_version": "LIG-16.0", + "assessment_date": "2013-02-06", + "assessment_type": "SAP", + "completion_date": "2013-02-06", + "inspection_date": "2013-02-06", + "sap_ventilation": { + "psv_count": 0, + "pressure_test": 1, + "air_permeability": 6.373, + "open_flues_count": 0, + "ventilation_type": 1, + "extract_fans_count": 9, + "open_fireplaces_count": 0, + "sheltered_sides_count": 0, + "flueless_gas_fires_count": 0 + }, + "total_floor_area": 709, + "transaction_type": 6, + "conservatory_type": 1, + "registration_date": "2013-02-06", + "restricted_access": 0, + "sap_energy_source": { + "pv_arrays": [ + { + "pitch": 3, + "peak_power": 3.6, + "orientation": 5, + "overshading": 1 + } + ], + "electricity_tariff": 1, + "wind_turbines_count": 0, + "wind_turbine_terrain_type": 3, + "fixed_lighting_outlets_count": 40, + "low_energy_fixed_lighting_outlets_count": 40, + "low_energy_fixed_lighting_outlets_percentage": 100 + }, + "sap_opening_types": [ + { + "name": 1, + "type": 2, + "u_value": 1.4, + "data_source": 2, + "description": "D7", + "glazing_type": 6 + }, + { + "name": 2, + "type": 2, + "u_value": 1.4, + "data_source": 2, + "description": "D12", + "glazing_type": 6 + }, + { + "name": 3, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "W27", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 4, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "W28A", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 5, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "W29A", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 6, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "W30", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 7, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "W31", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 8, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "W32", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 9, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "W25", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 10, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "W26", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 11, + "type": 2, + "u_value": 1.4, + "data_source": 2, + "description": "D3", + "glazing_type": 6 + }, + { + "name": 12, + "type": 2, + "u_value": 1.4, + "data_source": 2, + "description": "D11", + "glazing_type": 6 + }, + { + "name": 13, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "W34", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 14, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "W33", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 15, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "W20", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 16, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "W21", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 17, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "W35", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 18, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "W36", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 19, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "W37", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 20, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "W22", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 21, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "W23", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 22, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "W24", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 23, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "W24A", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 24, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "W38", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 25, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "W39", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 26, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "W40", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 27, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "W41", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 28, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "W42", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 29, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "W1A", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 30, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "W2A", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 31, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "W1B", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 32, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "W2B", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 33, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "W18", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 34, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "W19", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 35, + "type": 2, + "u_value": 1.4, + "data_source": 2, + "description": "D4", + "glazing_type": 6 + }, + { + "name": 36, + "type": 2, + "u_value": 1.4, + "data_source": 2, + "description": "D6", + "glazing_type": 6 + }, + { + "name": 37, + "type": 2, + "u_value": 1.4, + "data_source": 2, + "description": "D5", + "glazing_type": 6 + }, + { + "name": 38, + "type": 2, + "u_value": 1.4, + "data_source": 2, + "description": "D9", + "glazing_type": 6 + }, + { + "name": 39, + "type": 2, + "u_value": 1.4, + "data_source": 2, + "description": "D10", + "glazing_type": 6 + }, + { + "name": 40, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "W17", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 41, + "type": 5, + "u_value": 1.4, + "data_source": 2, + "description": "W14", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 42, + "type": 5, + "u_value": 1.4, + "data_source": 2, + "description": "W16", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 43, + "type": 2, + "u_value": 1.4, + "data_source": 2, + "description": "D1", + "glazing_type": 6 + }, + { + "name": 44, + "type": 2, + "u_value": 1.4, + "data_source": 2, + "description": "D2", + "glazing_type": 6 + }, + { + "name": 45, + "type": 1, + "u_value": 1.4, + "data_source": 2, + "description": "D8", + "glazing_type": 1 + }, + { + "name": 46, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "W9", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 47, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "W29", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 48, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "W28", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 49, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "W13", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 50, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "W9A", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 51, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "W15", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 52, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "WF", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 53, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "WG", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 54, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "WH", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 55, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "WI", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 56, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "WJ", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 57, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "W30A", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 58, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "W31A", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 59, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "WLL", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 60, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "WMM", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 61, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "WAA", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 62, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "WBB", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 63, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "WCC", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 64, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "WDD", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 65, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "WO", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 66, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "WP", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 67, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "WQ", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 68, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "W1", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 69, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "W10", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 70, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "W2", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 71, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "W3", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 72, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "W4", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 73, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "W5", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 74, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "W6", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 75, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "W7", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 76, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "W11", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 77, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "W12", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 78, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "WA", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 79, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "WB", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 80, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "WC", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 81, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "WD", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 82, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "WE", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 83, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "WK", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 84, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "WL", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 85, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "WM", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 86, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "WN", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 87, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "WFF", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 88, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "WGG", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 89, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "WHH", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 90, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "WII", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 91, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "WJJ", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 92, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "WKK", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 93, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "WR", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 94, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "WS", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 95, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "WT", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 96, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "WU", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 97, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "WV", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 98, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "WW", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 99, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "WX", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 100, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "WY", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + }, + { + "name": 101, + "type": 4, + "u_value": 1.4, + "data_source": 2, + "description": "WZ", + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + } + ], + "secondary_heating": { + "description": "Room heaters, wood logs", + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + }, + "lzc_energy_sources": [ + 4, + 7, + 10, + 11 + ], + "sap_building_parts": [ + { + "sap_roofs": [ + { + "name": "Roof 1", + "u_value": 0.129, + "roof_type": 2, + "description": "Roof 1 ZINC FLAT", + "total_roof_area": 145 + }, + { + "name": "Roof 2", + "u_value": 0.139, + "roof_type": 2, + "description": "Roof 2 THATCH ROOF", + "total_roof_area": 93.03 + }, + { + "name": "Roof 3", + "u_value": 0.19, + "roof_type": 2, + "description": "Roof 3 DORMER", + "total_roof_area": 4.8 + }, + { + "name": "Roof 4", + "u_value": 0.151, + "roof_type": 2, + "description": "Roof 4 SLATE", + "total_roof_area": 157.54 + } + ], + "sap_walls": [ + { + "name": "Wall 1", + "u_value": 0.166, + "wall_type": 2, + "description": "Wall 1 BRICK & FLINT", + "total_wall_area": 407.31, + "is_curtain_walling": "false" + }, + { + "name": "Wall 2", + "u_value": 0.168, + "wall_type": 1, + "description": "Wall 2 CONCRETE", + "total_wall_area": 122.42, + "is_curtain_walling": "false" + }, + { + "name": "Wall 3", + "u_value": 0.181, + "wall_type": 2, + "description": "Wall 3 INT WALL", + "total_wall_area": 17.33, + "is_curtain_walling": "false" + }, + { + "name": "Wall 4", + "u_value": 0.158, + "wall_type": 2, + "description": "Wall 4 TF", + "total_wall_area": 236.58, + "is_curtain_walling": "false" + } + ], + "identifier": "Main Dwelling", + "overshading": 1, + "sap_openings": [ + { + "name": 1, + "type": 1, + "width": 0.91, + "height": 2.1, + "location": "Wall 1", + "orientation": 0 + }, + { + "name": 2, + "type": 2, + "width": 0.91, + "height": 2.1, + "location": "Wall 1", + "orientation": 0 + }, + { + "name": 3, + "type": 3, + "width": 3.99, + "height": 0.78, + "location": "Wall 4", + "orientation": 7 + }, + { + "name": 4, + "type": 4, + "width": 3.33, + "height": 0.97, + "location": "Wall 4", + "orientation": 6 + }, + { + "name": 5, + "type": 5, + "width": 3.17, + "height": 0.97, + "location": "Wall 4", + "orientation": 6 + }, + { + "name": 6, + "type": 6, + "width": 1.28, + "height": 0.97, + "location": "Wall 4", + "orientation": 6 + }, + { + "name": 7, + "type": 7, + "width": 2.31, + "height": 0.97, + "location": "Wall 4", + "orientation": 6 + }, + { + "name": 8, + "type": 8, + "width": 2.31, + "height": 0.97, + "location": "Wall 4", + "orientation": 6 + }, + { + "name": 9, + "type": 9, + "width": 5.86, + "height": 1.36, + "location": "Wall 4", + "orientation": 7 + }, + { + "name": 10, + "type": 10, + "width": 3.95, + "height": 1.36, + "location": "Wall 4", + "orientation": 7 + }, + { + "name": 11, + "type": 11, + "width": 1.68, + "height": 2.75, + "location": "Wall 1", + "orientation": 0 + }, + { + "name": 12, + "type": 12, + "width": 1.99, + "height": 2.2, + "location": "Wall 4", + "orientation": 0 + }, + { + "name": 13, + "type": 13, + "width": 1.24, + "height": 0.95, + "location": "Wall 1", + "orientation": 8 + }, + { + "name": 14, + "type": 14, + "width": 1.99, + "height": 0.95, + "location": "Wall 1", + "orientation": 2 + }, + { + "name": 15, + "type": 15, + "width": 2.92, + "height": 2.09, + "location": "Wall 4", + "orientation": 2 + }, + { + "name": 16, + "type": 16, + "width": 1.38, + "height": 2.09, + "location": "Wall 4", + "orientation": 4 + }, + { + "name": 17, + "type": 17, + "width": 1.36, + "height": 2.4, + "location": "Wall 4", + "orientation": 1 + }, + { + "name": 18, + "type": 18, + "width": 2.09, + "height": 2.4, + "location": "Wall 4", + "orientation": 3 + }, + { + "name": 19, + "type": 19, + "width": 1.88, + "height": 2.4, + "location": "Wall 4", + "orientation": 3 + }, + { + "name": 20, + "type": 20, + "width": 0.68, + "height": 0.99, + "location": "Wall 4", + "orientation": 6 + }, + { + "name": 21, + "type": 21, + "width": 0.68, + "height": 0.99, + "location": "Wall 4", + "orientation": 6 + }, + { + "name": 22, + "type": 22, + "width": 0.68, + "height": 0.99, + "location": "Wall 4", + "orientation": 6 + }, + { + "name": 23, + "type": 23, + "width": 0.68, + "height": 0.99, + "location": "Wall 4", + "orientation": 6 + }, + { + "name": 24, + "type": 24, + "width": 0.54, + "height": 0.99, + "location": "Wall 1", + "orientation": 5 + }, + { + "name": 25, + "type": 25, + "width": 0.54, + "height": 0.99, + "location": "Wall 1", + "orientation": 5 + }, + { + "name": 26, + "type": 26, + "width": 0.54, + "height": 0.99, + "location": "Wall 1", + "orientation": 7 + }, + { + "name": 27, + "type": 27, + "width": 0.54, + "height": 0.99, + "location": "Wall 1", + "orientation": 7 + }, + { + "name": 28, + "type": 28, + "width": 0.54, + "height": 0.99, + "location": "Wall 1", + "orientation": 7 + }, + { + "name": 29, + "type": 29, + "width": 1.45, + "height": 1.42, + "location": "Wall 4", + "orientation": 3 + }, + { + "name": 30, + "type": 30, + "width": 1.68, + "height": 1.42, + "location": "Wall 4", + "orientation": 3 + }, + { + "name": 31, + "type": 31, + "width": 1.45, + "height": 1.42, + "location": "Wall 4", + "orientation": 5 + }, + { + "name": 32, + "type": 32, + "width": 1.68, + "height": 1.42, + "location": "Wall 4", + "orientation": 1 + }, + { + "name": 33, + "type": 33, + "width": 1.35, + "height": 2.09, + "location": "Wall 4", + "orientation": 2 + }, + { + "name": 34, + "type": 34, + "width": 1.35, + "height": 2.09, + "location": "Wall 4", + "orientation": 2 + }, + { + "name": 35, + "type": 35, + "width": 3.84, + "height": 2.8, + "location": "Wall 1", + "orientation": 0 + }, + { + "name": 36, + "type": 36, + "width": 4.74, + "height": 2.8, + "location": "Wall 1", + "orientation": 0 + }, + { + "name": 37, + "type": 37, + "width": 2.81, + "height": 2.8, + "location": "Wall 4", + "orientation": 0 + }, + { + "name": 38, + "type": 38, + "width": 6.45, + "height": 2.4, + "location": "Wall 4", + "orientation": 0 + }, + { + "name": 39, + "type": 39, + "width": 3.84, + "height": 2.4, + "location": "Wall 4", + "orientation": 0 + }, + { + "name": 40, + "type": 40, + "width": 1.4, + "height": 2.76, + "location": "Wall 1", + "orientation": 2 + }, + { + "name": 41, + "type": 41, + "width": 0.6, + "height": 0.89, + "location": "Roof 2", + "orientation": 8 + }, + { + "name": 42, + "type": 42, + "width": 7.25, + "height": 0.89, + "location": "Roof 2", + "orientation": 4 + }, + { + "name": 43, + "type": 43, + "width": 1.81, + "height": 2.1, + "location": "Wall 1", + "orientation": 0 + }, + { + "name": 44, + "type": 44, + "width": 2.49, + "height": 2.1, + "location": "Wall 1", + "orientation": 0 + }, + { + "name": 45, + "type": 45, + "width": 3.08, + "height": 2.62, + "location": "Wall 1", + "orientation": 0 + }, + { + "name": 46, + "type": 46, + "width": 1.68, + "height": 1.36, + "location": "Wall 1", + "orientation": 2 + }, + { + "name": 47, + "type": 47, + "width": 1.18, + "height": 2.44, + "location": "Wall 4", + "orientation": 3 + }, + { + "name": 48, + "type": 48, + "width": 1.18, + "height": 1.23, + "location": "Wall 4", + "orientation": 3 + }, + { + "name": 49, + "type": 49, + "width": 4.27, + "height": 2.85, + "location": "Wall 1", + "orientation": 2 + }, + { + "name": 50, + "type": 50, + "width": 1.68, + "height": 4.15, + "location": "Wall 1", + "orientation": 2 + }, + { + "name": 51, + "type": 51, + "width": 1.74, + "height": 2.62, + "location": "Wall 4", + "orientation": 6 + }, + { + "name": 52, + "type": 52, + "width": 2.36, + "height": 0.38, + "location": "Wall 4", + "orientation": 6 + }, + { + "name": 53, + "type": 53, + "width": 1.5, + "height": 0.38, + "location": "Wall 4", + "orientation": 6 + }, + { + "name": 54, + "type": 54, + "width": 0.7, + "height": 0.38, + "location": "Wall 1", + "orientation": 6 + }, + { + "name": 55, + "type": 55, + "width": 0.92, + "height": 0.38, + "location": "Wall 1", + "orientation": 6 + }, + { + "name": 56, + "type": 56, + "width": 0.9, + "height": 0.38, + "location": "Wall 1", + "orientation": 6 + }, + { + "name": 57, + "type": 57, + "width": 1.18, + "height": 2.98, + "location": "Wall 1", + "orientation": 7 + }, + { + "name": 58, + "type": 58, + "width": 1.18, + "height": 2.98, + "location": "Wall 1", + "orientation": 3 + }, + { + "name": 59, + "type": 59, + "width": 0.62, + "height": 0.38, + "location": "Wall 4", + "orientation": 7 + }, + { + "name": 60, + "type": 60, + "width": 1.69, + "height": 0.38, + "location": "Wall 4", + "orientation": 7 + }, + { + "name": 61, + "type": 61, + "width": 2.38, + "height": 0.38, + "location": "Wall 1", + "orientation": 7 + }, + { + "name": 62, + "type": 62, + "width": 1.13, + "height": 0.38, + "location": "Wall 1", + "orientation": 7 + }, + { + "name": 63, + "type": 63, + "width": 0.73, + "height": 0.38, + "location": "Wall 1", + "orientation": 7 + }, + { + "name": 64, + "type": 64, + "width": 1.94, + "height": 0.38, + "location": "Wall 1", + "orientation": 7 + }, + { + "name": 65, + "type": 65, + "width": 1.39, + "height": 0.38, + "location": "Wall 1", + "orientation": 7 + }, + { + "name": 66, + "type": 66, + "width": 1.59, + "height": 0.38, + "location": "Wall 1", + "orientation": 7 + }, + { + "name": 67, + "type": 67, + "width": 1.65, + "height": 0.38, + "location": "Wall 1", + "orientation": 7 + }, + { + "name": 68, + "type": 68, + "width": 0.46, + "height": 0.54, + "location": "Wall 1", + "orientation": 6 + }, + { + "name": 69, + "type": 69, + "width": 0.46, + "height": 0.54, + "location": "Wall 1", + "orientation": 6 + }, + { + "name": 70, + "type": 70, + "width": 1.14, + "height": 1.06, + "location": "Wall 1", + "orientation": 8 + }, + { + "name": 71, + "type": 71, + "width": 1.14, + "height": 1.06, + "location": "Wall 1", + "orientation": 8 + }, + { + "name": 72, + "type": 72, + "width": 1.14, + "height": 1.06, + "location": "Wall 1", + "orientation": 8 + }, + { + "name": 73, + "type": 73, + "width": 1.14, + "height": 1.06, + "location": "Wall 1", + "orientation": 8 + }, + { + "name": 74, + "type": 74, + "width": 1.14, + "height": 1.06, + "location": "Wall 1", + "orientation": 4 + }, + { + "name": 75, + "type": 75, + "width": 1.14, + "height": 1.06, + "location": "Wall 1", + "orientation": 4 + }, + { + "name": 76, + "type": 76, + "width": 2, + "height": 1.09, + "location": "Wall 4", + "orientation": 8 + }, + { + "name": 77, + "type": 77, + "width": 2, + "height": 1.09, + "location": "Wall 4", + "orientation": 8 + }, + { + "name": 78, + "type": 78, + "width": 0.76, + "height": 0.38, + "location": "Wall 4", + "orientation": 6 + }, + { + "name": 79, + "type": 79, + "width": 0.76, + "height": 0.38, + "location": "Wall 4", + "orientation": 6 + }, + { + "name": 80, + "type": 80, + "width": 0.76, + "height": 0.38, + "location": "Wall 4", + "orientation": 6 + }, + { + "name": 81, + "type": 81, + "width": 2.57, + "height": 0.38, + "location": "Wall 4", + "orientation": 6 + }, + { + "name": 82, + "type": 82, + "width": 2.57, + "height": 0.38, + "location": "Wall 4", + "orientation": 6 + }, + { + "name": 83, + "type": 83, + "width": 0.88, + "height": 0.38, + "location": "Wall 1", + "orientation": 6 + }, + { + "name": 84, + "type": 84, + "width": 0.88, + "height": 0.38, + "location": "Wall 1", + "orientation": 6 + }, + { + "name": 85, + "type": 85, + "width": 0.88, + "height": 0.38, + "location": "Wall 1", + "orientation": 6 + }, + { + "name": 86, + "type": 86, + "width": 0.88, + "height": 0.38, + "location": "Wall 1", + "orientation": 6 + }, + { + "name": 87, + "type": 87, + "width": 1.28, + "height": 0.38, + "location": "Wall 4", + "orientation": 7 + }, + { + "name": 88, + "type": 88, + "width": 1.28, + "height": 0.38, + "location": "Wall 4", + "orientation": 7 + }, + { + "name": 89, + "type": 89, + "width": 1.28, + "height": 0.38, + "location": "Wall 4", + "orientation": 7 + }, + { + "name": 90, + "type": 90, + "width": 1.35, + "height": 0.38, + "location": "Wall 4", + "orientation": 7 + }, + { + "name": 91, + "type": 91, + "width": 1.35, + "height": 0.38, + "location": "Wall 4", + "orientation": 7 + }, + { + "name": 92, + "type": 92, + "width": 1.35, + "height": 0.38, + "location": "Wall 4", + "orientation": 7 + }, + { + "name": 93, + "type": 93, + "width": 0.55, + "height": 0.38, + "location": "Wall 1", + "orientation": 7 + }, + { + "name": 94, + "type": 94, + "width": 0.55, + "height": 0.38, + "location": "Wall 1", + "orientation": 7 + }, + { + "name": 95, + "type": 95, + "width": 0.55, + "height": 0.38, + "location": "Wall 1", + "orientation": 7 + }, + { + "name": 96, + "type": 96, + "width": 0.55, + "height": 0.38, + "location": "Wall 1", + "orientation": 7 + }, + { + "name": 97, + "type": 97, + "width": 0.55, + "height": 0.38, + "location": "Wall 1", + "orientation": 7 + }, + { + "name": 98, + "type": 98, + "width": 0.92, + "height": 0.38, + "location": "Wall 1", + "orientation": 7 + }, + { + "name": 99, + "type": 99, + "width": 0.92, + "height": 0.38, + "location": "Wall 1", + "orientation": 7 + }, + { + "name": 100, + "type": 100, + "width": 1.23, + "height": 0.38, + "location": "Wall 1", + "orientation": 7 + }, + { + "name": 101, + "type": 101, + "width": 1.23, + "height": 0.38, + "location": "Wall 1", + "orientation": 7 + } + ], + "construction_year": 2010, + "sap_thermal_bridges": { + "thermal_bridge_code": 1 + }, + "building_part_number": 1, + "sap_floor_dimensions": [ + { + "storey": 0, + "u_value": 0.13, + "floor_type": 1, + "description": "Floor 1", + "storey_height": 2.65, + "heat_loss_area": 341.2, + "total_floor_area": 127.88 + }, + { + "storey": 1, + "u_value": 0, + "floor_type": 3, + "storey_height": 2.88, + "heat_loss_area": 0, + "total_floor_area": 213.32 + }, + { + "storey": 2, + "u_value": 0, + "floor_type": 3, + "storey_height": 3.07, + "heat_loss_area": 0, + "total_floor_area": 347.69 + }, + { + "storey": 3, + "u_value": 0, + "floor_type": 3, + "storey_height": 2.3, + "heat_loss_area": 0, + "total_floor_area": 20.45 + } + ], + "thermal_mass_parameter": 250 + } + ], + "bedf_revision_number": 333, + "heating_cost_current": 2132, + "co2_emissions_current": 6.5, + "energy_rating_average": 60, + "energy_rating_current": 85, + "lighting_cost_current": 154, + "main_heating_controls": [ + { + "description": "Time and temperature zone control", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + } + ], + "has_hot_water_cylinder": "true", + "heating_cost_potential": 2132, + "hot_water_cost_current": 105, + "co2_emissions_potential": 6.5, + "energy_rating_potential": 85, + "lighting_cost_potential": 154, + "hot_water_cost_potential": 105, + "is_in_smoke_control_area": "unknown", + "renewable_heat_incentive": { + "rhi_new_dwelling": { + "space_heating": 41938, + "water_heating": 2876 + } + }, + "seller_commission_report": "Y", + "energy_consumption_current": 63, + "has_fixed_air_conditioning": "false", + "calculation_software_version": 5.4, + "energy_consumption_potential": 63, + "environmental_impact_current": 88, + "current_energy_efficiency_band": "B", + "environmental_impact_potential": 88, + "has_heated_separate_conservatory": "false", + "potential_energy_efficiency_band": "B", + "co2_emissions_current_per_floor_area": 9 +} \ No newline at end of file diff --git a/backend/epc_api/json_samples/SAP-Schema-16.3/rdsap.json b/backend/epc_api/json_samples/SAP-Schema-16.3/rdsap.json new file mode 100644 index 00000000..caa7b681 --- /dev/null +++ b/backend/epc_api/json_samples/SAP-Schema-16.3/rdsap.json @@ -0,0 +1,371 @@ +{ + "uprn": 12457, + "roofs": [ + { + "description": "Pitched, 200 mm loft insulation", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "walls": [ + { + "description": "Cavity wall, as built, insulated (assumed)", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "floors": [ + { + "description": "Suspended, no insulation (assumed)", + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + } + ], + "status": "entered", + "tenure": 2, + "windows": [ + { + "description": "Fully double glazed", + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 3 + } + ], + "lighting": { + "description": "Low energy lighting in 71% of fixed outlets", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + }, + "postcode": "AA1 1AA", + "hot_water": { + "description": "Electric immersion, off-peak", + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 1 + }, + "post_town": "Town", + "built_form": 2, + "door_count": 2, + "glazed_area": 1, + "region_code": 6, + "report_type": 2, + "sap_heating": { + "wwhrs": { + "rooms_with_bath_and_or_shower": 1, + "rooms_with_mixer_shower_no_bath": 0, + "rooms_with_bath_and_mixer_shower": 0 + }, + "cylinder_size": 4, + "water_heating_code": 903, + "water_heating_fuel": 29, + "secondary_fuel_type": 29, + "main_heating_details": [ + { + "has_fghrs": "N", + "main_fuel_type": 29, + "heat_emitter_type": 0, + "main_heating_number": 1, + "main_heating_control": 2401, + "main_heating_category": 7, + "main_heating_fraction": 1, + "sap_main_heating_code": 401, + "main_heating_data_source": 2 + } + ], + "immersion_heating_type": 1, + "secondary_heating_type": 691, + "cylinder_insulation_type": 2, + "has_fixed_air_conditioning": "false", + "cylinder_insulation_thickness": 25 + }, + "sap_version": 9.91, + "schema_type": "SAP-Schema-16.3", + "uprn_source": "Energy Assessor", + "country_code": "EAW", + "main_heating": [ + { + "description": "Electric storage heaters", + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 1 + } + ], + "dwelling_type": "Semi-detached house", + "language_code": 1, + "property_type": 0, + "address_line_1": "11, Some Close", + "address_line_2": "Long Itchington", + "schema_version": "LIG-16.1", + "assessment_type": "RdSAP", + "completion_date": "2014-12-06", + "inspection_date": "2014-12-05", + "extensions_count": 0, + "measurement_type": 1, + "total_floor_area": 70, + "transaction_type": 8, + "conservatory_type": 1, + "heated_room_count": 3, + "registration_date": "2014-12-06", + "restricted_access": 0, + "sap_energy_source": { + "main_gas": "N", + "meter_type": 1, + "photovoltaic_supply": { + "percent_roof_area": 0 + }, + "wind_turbines_count": 0, + "wind_turbines_terrain_type": 2 + }, + "secondary_heating": { + "description": "Room heaters, electric", + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + }, + "lzc_energy_sources": [ + 9 + ], + "sap_building_parts": [ + { + "identifier": "Main Dwelling", + "wall_dry_lined": "N", + "wall_thickness": 270, + "floor_heat_loss": 7, + "roof_construction": 4, + "wall_construction": 4, + "building_part_number": 1, + "sap_floor_dimensions": [ + { + "floor": 0, + "room_height": 2.3, + "floor_insulation": 1, + "total_floor_area": 35.159, + "floor_construction": 3, + "heat_loss_perimeter": 16.837 + }, + { + "floor": 1, + "room_height": 2.3, + "total_floor_area": 35.159, + "heat_loss_perimeter": 16.837 + } + ], + "wall_insulation_type": 4, + "construction_age_band": "H", + "wall_thickness_measured": "Y", + "roof_insulation_location": 2, + "roof_insulation_thickness": "200mm" + } + ], + "low_energy_lighting": 71, + "solar_water_heating": "N", + "bedf_revision_number": 370, + "habitable_room_count": 3, + "heating_cost_current": { + "value": 611, + "currency": "GBP" + }, + "insulated_door_count": 0, + "co2_emissions_current": 5.9, + "energy_rating_average": 60, + "energy_rating_current": 59, + "lighting_cost_current": { + "value": 65, + "currency": "GBP" + }, + "main_heating_controls": [ + { + "description": "Manual charge control", + "energy_efficiency_rating": 2, + "environmental_efficiency_rating": 2 + } + ], + "multiple_glazing_type": 1, + "open_fireplaces_count": 0, + "has_hot_water_cylinder": "true", + "heating_cost_potential": { + "value": 471, + "currency": "GBP" + }, + "hot_water_cost_current": { + "value": 226, + "currency": "GBP" + }, + "mechanical_ventilation": 0, + "percent_draughtproofed": 100, + "suggested_improvements": [ + { + "sequence": 1, + "typical_saving": { + "value": 88, + "currency": "GBP" + }, + "indicative_cost": "£800 - £1,200", + "improvement_type": "W", + "improvement_details": { + "improvement_number": 47 + }, + "improvement_category": 5, + "energy_performance_rating": 63, + "environmental_impact_rating": 41 + }, + { + "sequence": 2, + "typical_saving": { + "value": 35, + "currency": "GBP" + }, + "indicative_cost": "£15 - £30", + "improvement_type": "C", + "improvement_details": { + "improvement_number": 2 + }, + "improvement_category": 5, + "energy_performance_rating": 65, + "environmental_impact_rating": 43 + }, + { + "sequence": 3, + "typical_saving": { + "value": 11, + "currency": "GBP" + }, + "indicative_cost": "£10", + "improvement_type": "E", + "improvement_details": { + "improvement_number": 35 + }, + "improvement_category": 5, + "energy_performance_rating": 65, + "environmental_impact_rating": 44 + }, + { + "sequence": 4, + "typical_saving": { + "value": 100, + "currency": "GBP" + }, + "indicative_cost": "£900 - £1,200", + "improvement_type": "L", + "improvement_details": { + "improvement_number": 25 + }, + "improvement_category": 5, + "energy_performance_rating": 69, + "environmental_impact_rating": 47 + }, + { + "sequence": 5, + "typical_saving": { + "value": 44, + "currency": "GBP" + }, + "indicative_cost": "£4,000 - £6,000", + "improvement_type": "N", + "improvement_details": { + "improvement_number": 19 + }, + "improvement_category": 5, + "energy_performance_rating": 71, + "environmental_impact_rating": 52 + }, + { + "sequence": 6, + "typical_saving": { + "value": 261, + "currency": "GBP" + }, + "indicative_cost": "£9,000 - £14,000", + "improvement_type": "U", + "improvement_details": { + "improvement_number": 34 + }, + "improvement_category": 5, + "energy_performance_rating": 83, + "environmental_impact_rating": 62 + }, + { + "sequence": 7, + "typical_saving": { + "value": 22, + "currency": "GBP" + }, + "indicative_cost": "£1,500 - £4,000", + "improvement_type": "V", + "improvement_details": { + "improvement_number": 44 + }, + "improvement_category": 5, + "energy_performance_rating": 84, + "environmental_impact_rating": 63 + } + ], + "co2_emissions_potential": 3.1, + "energy_rating_potential": 84, + "lighting_cost_potential": { + "value": 51, + "currency": "GBP" + }, + "alternative_improvements": [ + { + "sequence": 1, + "typical_saving": { + "value": 50, + "currency": "GBP" + }, + "improvement_type": "J2", + "improvement_details": { + "improvement_number": 54 + }, + "improvement_category": 6, + "energy_performance_rating": 68, + "environmental_impact_rating": 88 + }, + { + "sequence": 2, + "typical_saving": { + "value": 178, + "currency": "GBP" + }, + "improvement_type": "Z1", + "improvement_details": { + "improvement_number": 51 + }, + "improvement_category": 6, + "energy_performance_rating": 75, + "environmental_impact_rating": 76 + }, + { + "sequence": 3, + "typical_saving": { + "value": 179, + "currency": "GBP" + }, + "improvement_type": "Z3", + "improvement_details": { + "improvement_number": 53 + }, + "improvement_category": 6, + "energy_performance_rating": 73, + "environmental_impact_rating": 72 + } + ], + "hot_water_cost_potential": { + "value": 105, + "currency": "GBP" + }, + "renewable_heat_incentive": { + "water_heating": 3424, + "space_heating_existing_dwelling": 7554 + }, + "seller_commission_report": "Y", + "energy_consumption_current": 473, + "has_fixed_air_conditioning": "false", + "multiple_glazed_proportion": 100, + "calculation_software_version": 8.3, + "energy_consumption_potential": 247, + "environmental_impact_current": 37, + "fixed_lighting_outlets_count": 7, + "current_energy_efficiency_band": "D", + "environmental_impact_potential": 63, + "has_heated_separate_conservatory": "false", + "potential_energy_efficiency_band": "B", + "co2_emissions_current_per_floor_area": 84, + "low_energy_fixed_lighting_outlets_count": 5 +} \ No newline at end of file diff --git a/backend/epc_api/json_samples/SAP-Schema-16.3/sap.json b/backend/epc_api/json_samples/SAP-Schema-16.3/sap.json new file mode 100644 index 00000000..d5005cc7 --- /dev/null +++ b/backend/epc_api/json_samples/SAP-Schema-16.3/sap.json @@ -0,0 +1,456 @@ +{ + "uprn": 12457, + "roofs": [ + { + "description": "Average thermal transmittance 0.12 W/m²K", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + } + ], + "walls": [ + { + "description": "Average thermal transmittance 0.28 W/m²K", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + } + ], + "floors": [ + { + "description": "Average thermal transmittance 0.17 W/m²K", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + } + ], + "status": "entered", + "windows": { + "description": "High performance glazing", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + }, + "lighting": { + "description": "Low energy lighting in all fixed outlets", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + }, + "postcode": "AA1 1AA", + "data_type": 2, + "hot_water": { + "description": "From main system", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + }, + "post_town": "Town", + "built_form": 4, + "living_area": 20.15, + "orientation": 7, + "region_code": 3, + "report_type": 3, + "sap_heating": { + "thermal_store": 1, + "has_solar_panel": "false", + "water_fuel_type": 1, + "water_heating_code": 901, + "hot_water_store_size": 180, + "main_heating_details": [ + { + "burner_control": 3, + "main_fuel_type": 1, + "heat_emitter_type": 1, + "boiler_index_number": 16179, + "is_flue_fan_present": "true", + "main_heating_number": 1, + "main_heating_control": 2110, + "is_interlocked_system": "true", + "main_heating_category": 2, + "main_heating_fraction": 1, + "main_heating_flue_type": 2, + "main_heating_data_source": 1, + "has_delayed_start_thermostat": "true", + "load_or_weather_compensation": 2, + "is_central_heating_pump_in_heated_space": "true" + } + ], + "has_hot_water_cylinder": "true", + "has_cylinder_thermostat": "true", + "hot_water_store_heat_loss": 1.63, + "has_fixed_air_conditioning": "false", + "secondary_heating_category": 1, + "is_cylinder_in_heated_space": "true", + "is_hot_water_separately_timed": "true", + "is_primary_pipework_insulated": "true", + "hot_water_store_heat_loss_source": 2 + }, + "sap_version": 9.9, + "schema_type": "SAP-Schema-16.3", + "uprn_source": "Energy Assessor", + "country_code": "EAW", + "main_heating": [ + { + "description": "Boiler and radiators, mains gas", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "air_tightness": { + "description": "Air permeability 3.9 m³/h.m² (assumed)", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + }, + "dwelling_type": "Mid-terrace house", + "language_code": 1, + "property_type": 0, + "address_line_1": "17, Street Terrace", + "schema_version": "LIG-16.0", + "assessment_date": "2014-04-04", + "assessment_type": "SAP", + "completion_date": "2014-04-04", + "inspection_date": "2014-04-04", + "sap_ventilation": { + "psv_count": 0, + "pressure_test": 1, + "air_permeability": 3.89, + "open_flues_count": 0, + "ventilation_type": 1, + "extract_fans_count": 3, + "open_fireplaces_count": 0, + "sheltered_sides_count": 2, + "flueless_gas_fires_count": 0 + }, + "design_water_use": 1, + "total_floor_area": 96, + "transaction_type": 6, + "conservatory_type": 1, + "registration_date": "2014-04-05", + "restricted_access": 0, + "sap_energy_source": { + "electricity_tariff": 1, + "wind_turbines_count": 0, + "wind_turbine_terrain_type": 1, + "fixed_lighting_outlets_count": 16, + "low_energy_fixed_lighting_outlets_count": 16, + "low_energy_fixed_lighting_outlets_percentage": 100 + }, + "sap_opening_types": [ + { + "name": "Doors (1)", + "type": 1, + "u_value": 1.4, + "data_source": 3, + "glazing_type": 1 + }, + { + "name": "Doors (2)", + "type": 1, + "u_value": 1.5, + "data_source": 2, + "glazing_type": 1 + }, + { + "name": "Windows (1)", + "type": 4, + "u_value": 1.5, + "data_source": 2, + "frame_factor": 0.7, + "glazing_type": 6, + "solar_transmittance": 0.63 + } + ], + "secondary_heating": { + "description": "None", + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + }, + "lzc_energy_sources": [ + 9 + ], + "sap_building_parts": [ + { + "sap_roofs": [ + { + "name": "Roof (1)", + "u_value": 0.11, + "roof_type": 2, + "description": "Pitched roof insulation at ceiling level", + "kappa_value": 8.75, + "total_roof_area": 41.96 + }, + { + "name": "Roof (2)", + "u_value": 0.2, + "roof_type": 2, + "description": "Flat roof - internal insulation", + "kappa_value": 8.75, + "total_roof_area": 4.69 + } + ], + "sap_walls": [ + { + "name": "Walls (1)", + "u_value": 0.29, + "wall_type": 2, + "description": "Masonry cavity wall", + "kappa_value": 46.25, + "total_wall_area": 26.5, + "is_curtain_walling": "false" + }, + { + "name": "Walls (2)", + "u_value": 0.28, + "wall_type": 2, + "description": "Timber frame wall", + "kappa_value": 8.75, + "total_wall_area": 48, + "is_curtain_walling": "false" + }, + { + "name": "Walls (3)", + "u_value": 0.22, + "wall_type": 3, + "description": "Insulated wall to garage", + "kappa_value": 8.75, + "total_wall_area": 7.29, + "is_curtain_walling": "false" + }, + { + "name": "Party wall (1)", + "u_value": 0, + "wall_type": 4, + "kappa_value": 17.5, + "total_wall_area": 48.6 + } + ], + "identifier": "Main Dwelling", + "overshading": 2, + "sap_openings": [ + { + "name": 1, + "type": "Doors (2)", + "width": 2.69, + "height": 2.1, + "location": "Walls (2)", + "orientation": 7 + }, + { + "name": 2, + "type": "Windows (1)", + "width": 0.46, + "height": 1.88, + "location": "Walls (2)", + "orientation": 7 + }, + { + "name": 3, + "type": "Windows (1)", + "width": 0.67, + "height": 0.9, + "location": "Walls (2)", + "orientation": 3 + }, + { + "name": 4, + "type": "Windows (1)", + "width": 0.46, + "height": 1.88, + "location": "Walls (2)", + "orientation": 7 + }, + { + "name": 5, + "type": "Windows (1)", + "width": 1.45, + "height": 1.35, + "location": "Walls (2)", + "orientation": 3 + }, + { + "name": 6, + "type": "Windows (1)", + "width": 1.45, + "height": 1.35, + "location": "Walls (2)", + "orientation": 3 + }, + { + "name": 7, + "type": "Doors (1)", + "width": 1.01, + "height": 2, + "location": "Walls (3)", + "orientation": 0 + }, + { + "name": 8, + "type": "Doors (2)", + "width": 1.74, + "height": 2.1, + "location": "Walls (2)", + "orientation": 7 + }, + { + "name": 9, + "type": "Doors (2)", + "width": 1.79, + "height": 2.1, + "location": "Walls (2)", + "orientation": 3 + }, + { + "name": 10, + "type": "Doors (1)", + "width": 1.01, + "height": 2.1, + "location": "Walls (1)", + "orientation": 0 + } + ], + "construction_year": 2013, + "sap_thermal_bridges": { + "thermal_bridges": [ + { + "length": 9.8, + "psi_value": 0.06, + "psi_value_source": 2, + "thermal_bridge_type": 10 + }, + { + "length": 4.2, + "psi_value": 0.04, + "psi_value_source": 2, + "thermal_bridge_type": 19 + }, + { + "length": 2.6, + "psi_value": 0.28, + "psi_value_source": 2, + "thermal_bridge_type": 20 + }, + { + "length": 6, + "psi_value": 0.08, + "psi_value_source": 2, + "thermal_bridge_type": 22 + }, + { + "length": 33.6, + "psi_value": 0, + "psi_value_source": 2, + "thermal_bridge_type": 23 + }, + { + "length": 25.86, + "psi_value": 0.05, + "psi_value_source": 2, + "thermal_bridge_type": 4 + }, + { + "length": 12.73, + "psi_value": 0.3, + "psi_value_source": 2, + "thermal_bridge_type": 2 + }, + { + "length": 12.73, + "psi_value": 0.04, + "psi_value_source": 2, + "thermal_bridge_type": 3 + } + ], + "thermal_bridge_code": 5 + }, + "building_part_number": 1, + "sap_floor_dimensions": [ + { + "storey": 0, + "u_value": 0.16, + "floor_type": 2, + "kappa_value": 30, + "storey_height": 2.4, + "heat_loss_area": 11.8, + "total_floor_area": 11.72 + }, + { + "storey": 1, + "u_value": 0.17, + "floor_type": 3, + "kappa_value": 30, + "storey_height": 2.4, + "heat_loss_area": 24.68, + "total_floor_area": 41.95 + }, + { + "storey": 2, + "u_value": 0, + "floor_type": 4, + "kappa_value": 0, + "storey_height": 2.4, + "heat_loss_area": 0, + "total_floor_area": 41.95 + } + ] + } + ], + "bedf_revision_number": 333, + "heating_cost_current": 260, + "co2_emissions_current": 1.5, + "energy_rating_average": 60, + "energy_rating_current": 83, + "lighting_cost_current": 64, + "main_heating_controls": [ + { + "description": "Time and temperature zone control", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + } + ], + "has_hot_water_cylinder": "true", + "heating_cost_potential": 260, + "hot_water_cost_current": 93, + "suggested_improvements": [ + { + "sequence": 1, + "typical_saving": 37, + "indicative_cost": "£4,000 - £6,000", + "improvement_type": "N", + "improvement_details": { + "improvement_number": 19 + }, + "improvement_category": 3, + "energy_performance_rating": 84, + "environmental_impact_rating": 88 + }, + { + "sequence": 2, + "typical_saving": 226, + "indicative_cost": "£11,000 - £20,000", + "improvement_type": "U", + "improvement_details": { + "improvement_number": 34 + }, + "improvement_category": 3, + "energy_performance_rating": 94, + "environmental_impact_rating": 96 + } + ], + "co2_emissions_potential": 1.5, + "energy_rating_potential": 83, + "lighting_cost_potential": 64, + "hot_water_cost_potential": 93, + "is_in_smoke_control_area": "unknown", + "renewable_heat_incentive": { + "rhi_new_dwelling": { + "space_heating": 3127, + "water_heating": 2230 + } + }, + "seller_commission_report": "N", + "energy_consumption_current": 85, + "has_fixed_air_conditioning": "false", + "calculation_software_version": 3.59, + "energy_consumption_potential": 85, + "environmental_impact_current": 85, + "current_energy_efficiency_band": "B", + "environmental_impact_potential": 85, + "has_heated_separate_conservatory": "false", + "potential_energy_efficiency_band": "B", + "co2_emissions_current_per_floor_area": 16 +} \ No newline at end of file diff --git a/backend/epc_api/json_samples/SAP-Schema-17.0/epc.json b/backend/epc_api/json_samples/SAP-Schema-17.0/epc.json new file mode 100644 index 00000000..12957001 --- /dev/null +++ b/backend/epc_api/json_samples/SAP-Schema-17.0/epc.json @@ -0,0 +1,375 @@ +{ + "uprn": 12457, + "roofs": [ + { + "description": "Average thermal transmittance 0.14 W/m²K", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + } + ], + "walls": [ + { + "description": "Average thermal transmittance 0.28 W/m²K", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + } + ], + "floors": [ + { + "description": "Average thermal transmittance 0.19 W/m²K", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + } + ], + "status": "entered", + "tenure": "ND", + "windows": { + "description": "High performance glazing", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + }, + "lighting": { + "description": "Low energy lighting in all fixed outlets", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + }, + "postcode": "PT34 5BG", + "data_type": 2, + "hot_water": { + "description": "From main system", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + }, + "post_town": "POSTTOWN", + "built_form": 1, + "living_area": 18.13, + "orientation": 1, + "region_code": 6, + "report_type": 3, + "sap_heating": { + "thermal_store": 1, + "water_fuel_type": 1, + "water_heating_code": 901, + "hot_water_store_size": 180, + "main_heating_details": [ + { + "main_fuel_type": 1, + "heat_emitter_type": 1, + "emitter_temperature": 1, + "is_flue_fan_present": "true", + "main_heating_number": 1, + "main_heating_control": 2110, + "is_interlocked_system": "true", + "main_heating_category": 2, + "main_heating_fraction": 1, + "main_heating_flue_type": 2, + "central_heating_pump_age": 2, + "main_heating_data_source": 1, + "main_heating_index_number": 10321, + "has_separate_delayed_start": "false", + "load_or_weather_compensation": 0, + "is_central_heating_pump_in_heated_space": "true" + } + ], + "has_hot_water_cylinder": "true", + "has_cylinder_thermostat": "true", + "hot_water_store_heat_loss": 1.48, + "has_fixed_air_conditioning": "false", + "secondary_heating_category": 1, + "is_cylinder_in_heated_space": "true", + "primary_pipework_insulation": 4, + "is_hot_water_separately_timed": "true", + "hot_water_store_heat_loss_source": 2 + }, + "sap_version": 9.92, + "schema_type": "SAP-Schema-17.0", + "uprn_source": "Energy Assessor", + "country_code": "ENG", + "main_heating": [ + { + "description": "Boiler and radiators, mains gas", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "air_tightness": { + "description": "Air permeability 4.9 m³/h.m² (as tested)", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + }, + "dwelling_type": "Detached house", + "language_code": 1, + "property_type": 0, + "address_line_1": "2, Place Park", + "address_line_2": "Central District", + "assessment_date": "2013-07-08", + "assessment_type": "SAP", + "completion_date": "2015-06-30", + "inspection_date": "2015-06-30", + "sap_ventilation": { + "psv_count": 0, + "pressure_test": 1, + "air_permeability": 4.94, + "open_flues_count": 0, + "ventilation_type": 1, + "extract_fans_count": 5, + "open_fireplaces_count": 0, + "sheltered_sides_count": 2, + "flueless_gas_fires_count": 0 + }, + "sap_data_version": 9.9, + "sap_flat_details": { + "level": 1 + }, + "total_floor_area": 139, + "transaction_type": 6, + "conservatory_type": 1, + "registration_date": "2015-06-30", + "sap_energy_source": { + "electricity_tariff": 1, + "wind_turbines_count": 0, + "wind_turbine_terrain_type": 1, + "fixed_lighting_outlets_count": 20, + "low_energy_fixed_lighting_outlets_count": 20, + "low_energy_fixed_lighting_outlets_percentage": 100 + }, + "sap_opening_types": [ + { + "name": "Opening Type 1", + "type": 4, + "u_value": 1.5, + "data_source": 2, + "frame_factor": 0.7, + "glazing_type": 3, + "solar_transmittance": 0.76 + }, + { + "name": "Opening Type 2", + "type": 5, + "u_value": 1.5, + "data_source": 2, + "frame_factor": 0.7, + "glazing_type": 3, + "solar_transmittance": 0.76 + } + ], + "secondary_heating": { + "description": "None", + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + }, + "lzc_energy_sources": [ + 10 + ], + "sap_building_parts": [ + { + "sap_roofs": [ + { + "name": "Roof 1", + "u_value": 0.14, + "roof_type": 2, + "description": "External Roof 1", + "total_roof_area": 69.63 + } + ], + "sap_walls": [ + { + "name": "External Wall 1", + "u_value": 0.28, + "wall_type": 2, + "description": "External Wall 1", + "total_wall_area": 197.76, + "is_curtain_walling": "false" + } + ], + "identifier": "Main Dwelling", + "overshading": 2, + "sap_openings": [ + { + "name": "Opening 1", + "type": "Opening Type 1", + "width": 7.47, + "height": 1, + "location": "External Wall 1", + "orientation": 1 + }, + { + "name": "Opening 2", + "type": "Opening Type 1", + "width": 6.88, + "height": 1, + "location": "External Wall 1", + "orientation": 5 + }, + { + "name": "Opening 3", + "type": "Opening Type 1", + "width": 5.44, + "height": 1, + "location": "External Wall 1", + "orientation": 7 + }, + { + "name": "Opening 4", + "type": "Opening Type 1", + "width": 6.88, + "height": 1, + "location": "External Wall 1", + "orientation": 3 + }, + { + "name": "Opening 5", + "type": "Opening Type 2", + "pitch": 35, + "width": 1.77, + "height": 1, + "location": "Roof 1", + "orientation": 7 + }, + { + "name": "Opening 6", + "type": "Opening Type 2", + "pitch": 35, + "width": 0.59, + "height": 1, + "location": "Roof 1", + "orientation": 3 + } + ], + "construction_year": 2013, + "sap_thermal_bridges": { + "thermal_bridges": [ + { + "length": 41.2, + "psi_value": 0.32, + "psi_value_source": 4, + "thermal_bridge_type": "E5" + }, + { + "length": 41.2, + "psi_value": 0.14, + "psi_value_source": 4, + "thermal_bridge_type": "E6" + }, + { + "length": 19.2, + "psi_value": 0.18, + "psi_value_source": 4, + "thermal_bridge_type": "E16" + } + ], + "thermal_bridge_code": 5 + }, + "building_part_number": 1, + "sap_floor_dimensions": [ + { + "storey": 0, + "u_value": 0.19, + "floor_type": 2, + "description": "Heat Loss Floor 1", + "storey_height": 2.4, + "heat_loss_area": 69.63, + "total_floor_area": 69.63 + }, + { + "storey": 1, + "u_value": 0, + "floor_type": 3, + "storey_height": 2.4, + "heat_loss_area": 0, + "total_floor_area": 69.63 + } + ], + "thermal_mass_parameter": 100 + } + ], + "heating_cost_current": { + "value": 378, + "currency": "GBP" + }, + "co2_emissions_current": 2.2, + "energy_rating_average": 60, + "energy_rating_current": 84, + "lighting_cost_current": { + "value": 73, + "currency": "GBP" + }, + "main_heating_controls": [ + { + "description": "Time and temperature zone control", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + } + ], + "has_hot_water_cylinder": "true", + "heating_cost_potential": { + "value": 379, + "currency": "GBP" + }, + "hot_water_cost_current": { + "value": 114, + "currency": "GBP" + }, + "suggested_improvements": [ + { + "sequence": 1, + "typical_saving": { + "value": 52, + "currency": "GBP" + }, + "indicative_cost": "£4,000 - £6,000", + "improvement_type": "N", + "improvement_details": { + "improvement_number": 19 + }, + "improvement_category": 5, + "energy_performance_rating": 85, + "environmental_impact_rating": 86 + }, + { + "sequence": 2, + "typical_saving": { + "value": 275, + "currency": "GBP" + }, + "indicative_cost": "£5,000 - £8,000", + "improvement_type": "U", + "improvement_details": { + "improvement_number": 34 + }, + "improvement_category": 5, + "energy_performance_rating": 92, + "environmental_impact_rating": 92 + } + ], + "co2_emissions_potential": 1.0, + "energy_rating_potential": 92, + "lighting_cost_potential": { + "value": 73, + "currency": "GBP" + }, + "schema_version_original": "LIG-17.0", + "hot_water_cost_potential": { + "value": 62, + "currency": "GBP" + }, + "is_in_smoke_control_area": "unknown", + "renewable_heat_incentive": { + "rhi_new_dwelling": { + "space_heating": 5622, + "water_heating": 2279 + } + }, + "seller_commission_report": "Y", + "energy_consumption_current": 91, + "has_fixed_air_conditioning": "false", + "multiple_glazed_percentage": 100, + "calculation_software_version": "4.03r03", + "energy_consumption_potential": 39, + "environmental_impact_current": 84, + "current_energy_efficiency_band": "B", + "environmental_impact_potential": 92, + "has_heated_separate_conservatory": "false", + "potential_energy_efficiency_band": "A", + "co2_emissions_current_per_floor_area": 16 +} \ No newline at end of file diff --git a/backend/epc_api/json_samples/SAP-Schema-17.1/epc.json b/backend/epc_api/json_samples/SAP-Schema-17.1/epc.json new file mode 100644 index 00000000..b690d220 --- /dev/null +++ b/backend/epc_api/json_samples/SAP-Schema-17.1/epc.json @@ -0,0 +1,562 @@ +{ + "uprn": 12457, + "roofs": [ + { + "description": "Average thermal transmittance 0.11 W/m²K", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + } + ], + "walls": [ + { + "description": "Average thermal transmittance 0.27 W/m²K", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + } + ], + "floors": [ + { + "description": "Average thermal transmittance 0.14 W/m²K", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + } + ], + "status": "entered", + "tenure": "ND", + "windows": { + "description": "High performance glazing", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + }, + "lighting": { + "description": "Low energy lighting in all fixed outlets", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + }, + "postcode": "A1 1AA", + "data_type": 2, + "hot_water": { + "description": "From main system", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + }, + "post_town": "Town", + "built_form": 1, + "living_area": 20.36, + "orientation": 1, + "region_code": 6, + "report_type": 3, + "sap_heating": { + "thermal_store": 1, + "water_fuel_type": 1, + "water_heating_code": 901, + "hot_water_store_size": 180, + "main_heating_details": [ + { + "main_fuel_type": 1, + "heat_emitter_type": 1, + "emitter_temperature": 1, + "is_flue_fan_present": "true", + "main_heating_number": 1, + "main_heating_control": 2110, + "is_interlocked_system": "true", + "main_heating_category": 2, + "main_heating_fraction": 1, + "main_heating_flue_type": 2, + "central_heating_pump_age": 2, + "main_heating_data_source": 1, + "main_heating_index_number": 18042, + "has_separate_delayed_start": "true", + "load_or_weather_compensation": 0, + "is_central_heating_pump_in_heated_space": "true" + } + ], + "has_hot_water_cylinder": "true", + "has_cylinder_thermostat": "true", + "hot_water_store_heat_loss": 1.2, + "has_fixed_air_conditioning": "false", + "secondary_heating_category": 1, + "is_cylinder_in_heated_space": "true", + "primary_pipework_insulation": 4, + "is_hot_water_separately_timed": "true", + "hot_water_store_heat_loss_source": 2 + }, + "sap_version": 9.92, + "schema_type": "SAP-Schema-17.1", + "uprn_source": "Energy Assessor", + "country_code": "ENG", + "main_heating": [ + { + "description": "Boiler and radiators, mains gas", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "air_tightness": { + "description": "Air permeability 4.9 m³/h.m² (as tested)", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + }, + "dwelling_type": "Detached house", + "language_code": 1, + "property_type": 0, + "address_line_1": "47, Address Lane", + "address_line_2": "Long Itchington", + "assessment_date": "2020-08-17", + "assessment_type": "SAP", + "completion_date": "2020-08-17", + "inspection_date": "2020-08-17", + "sap_ventilation": { + "psv_count": 0, + "pressure_test": 1, + "air_permeability": 4.94, + "open_flues_count": 0, + "ventilation_type": 1, + "extract_fans_count": 5, + "open_fireplaces_count": 0, + "sheltered_sides_count": 1, + "flueless_gas_fires_count": 0 + }, + "design_water_use": 1, + "sap_data_version": 9.92, + "sap_flat_details": { + "level": 1 + }, + "total_floor_area": 146, + "transaction_type": 6, + "conservatory_type": 1, + "registration_date": "2020-08-17", + "sap_energy_source": { + "electricity_tariff": 1, + "wind_turbines_count": 0, + "wind_turbine_terrain_type": 2, + "fixed_lighting_outlets_count": 14, + "low_energy_fixed_lighting_outlets_count": 14, + "low_energy_fixed_lighting_outlets_percentage": 100 + }, + "sap_opening_types": [ + { + "name": "Opening Type 1", + "type": 2, + "u_value": 1.5, + "data_source": 2, + "glazing_type": 4 + }, + { + "name": "Opening Type 2", + "type": 4, + "u_value": 1.41, + "data_source": 2, + "frame_factor": 0.7, + "glazing_type": 4, + "solar_transmittance": 0.71 + }, + { + "name": "Opening Type 4", + "type": 4, + "u_value": 1.41, + "data_source": 2, + "frame_factor": 0.7, + "glazing_type": 4, + "solar_transmittance": 0.72 + } + ], + "secondary_heating": { + "description": "None", + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + }, + "lzc_energy_sources": [ + 11 + ], + "sap_building_parts": [ + { + "sap_roofs": [ + { + "name": "Roof 1", + "u_value": 0.11, + "roof_type": 2, + "description": "400mm Mineral Wool", + "total_roof_area": 72.6 + }, + { + "name": "Roof 2", + "u_value": 0.17, + "roof_type": 2, + "description": "Flat Roof", + "total_roof_area": 1.07 + } + ], + "sap_walls": [ + { + "name": "External Wall 1", + "u_value": 0.27, + "wall_type": 2, + "description": "50mm A Platinum", + "total_wall_area": 186.21, + "is_curtain_walling": "false" + } + ], + "identifier": "Main Dwelling", + "overshading": 2, + "sap_openings": [ + { + "name": "D1", + "type": "Opening Type 1", + "width": 1.01, + "height": 2.33, + "location": "External Wall 1", + "orientation": 0 + }, + { + "name": "D2", + "type": "Opening Type 2", + "width": 2.4, + "height": 2.1, + "location": "External Wall 1", + "orientation": 6 + }, + { + "name": "D3", + "type": "Opening Type 2", + "width": 2.4, + "height": 2.1, + "location": "External Wall 1", + "orientation": 4 + }, + { + "name": "W1", + "type": "Opening Type 4", + "width": 1.14, + "height": 1.5, + "location": "External Wall 1", + "orientation": 2 + }, + { + "name": "W1a", + "type": "Opening Type 4", + "width": 0.56, + "height": 1.5, + "location": "External Wall 1", + "orientation": 3 + }, + { + "name": "W1b", + "type": "Opening Type 4", + "width": 0.56, + "height": 1.5, + "location": "External Wall 1", + "orientation": 1 + }, + { + "name": "W2", + "type": "Opening Type 4", + "width": 1.14, + "height": 1.5, + "location": "External Wall 1", + "orientation": 2 + }, + { + "name": "W2a", + "type": "Opening Type 4", + "width": 0.56, + "height": 1.5, + "location": "External Wall 1", + "orientation": 3 + }, + { + "name": "W2b", + "type": "Opening Type 4", + "width": 0.56, + "height": 1.5, + "location": "External Wall 1", + "orientation": 1 + }, + { + "name": "W3", + "type": "Opening Type 4", + "width": 1.02, + "height": 1.35, + "location": "External Wall 1", + "orientation": 8 + }, + { + "name": "W4", + "type": "Opening Type 4", + "width": 1.02, + "height": 1.35, + "location": "External Wall 1", + "orientation": 8 + }, + { + "name": "W5", + "type": "Opening Type 4", + "width": 1.02, + "height": 1.35, + "location": "External Wall 1", + "orientation": 8 + }, + { + "name": "W6", + "type": "Opening Type 4", + "width": 1.02, + "height": 1.35, + "location": "External Wall 1", + "orientation": 6 + }, + { + "name": "W7", + "type": "Opening Type 4", + "width": 1.02, + "height": 1.35, + "location": "External Wall 1", + "orientation": 2 + }, + { + "name": "W8", + "type": "Opening Type 4", + "width": 1.02, + "height": 1.2, + "location": "External Wall 1", + "orientation": 2 + }, + { + "name": "W9", + "type": "Opening Type 4", + "width": 1.02, + "height": 1.35, + "location": "External Wall 1", + "orientation": 2 + }, + { + "name": "W10", + "type": "Opening Type 4", + "width": 1.02, + "height": 1.35, + "location": "External Wall 1", + "orientation": 8 + }, + { + "name": "W11", + "type": "Opening Type 4", + "width": 1.02, + "height": 1.35, + "location": "External Wall 1", + "orientation": 8 + }, + { + "name": "W12", + "type": "Opening Type 4", + "width": 1.02, + "height": 1.35, + "location": "External Wall 1", + "orientation": 8 + }, + { + "name": "W13", + "type": "Opening Type 4", + "width": 0.63, + "height": 1.05, + "location": "External Wall 1", + "orientation": 4 + }, + { + "name": "W14", + "type": "Opening Type 4", + "width": 1.02, + "height": 1.35, + "location": "External Wall 1", + "orientation": 6 + } + ], + "construction_year": 2017, + "sap_thermal_bridges": { + "thermal_bridges": [ + { + "length": 21.8, + "psi_value": 0.211, + "psi_value_source": 3, + "thermal_bridge_type": "E2" + }, + { + "length": 6.12, + "psi_value": 0.049, + "psi_value_source": 3, + "thermal_bridge_type": "E2" + }, + { + "length": 20.79, + "psi_value": 0.023, + "psi_value_source": 3, + "thermal_bridge_type": "E3" + }, + { + "length": 63.76, + "psi_value": 0.028, + "psi_value_source": 3, + "thermal_bridge_type": "E4" + }, + { + "length": 38.46, + "psi_value": 0.047, + "psi_value_source": 3, + "thermal_bridge_type": "E5" + }, + { + "length": 3.48, + "psi_value": 0.14, + "psi_value_source": 4, + "thermal_bridge_type": "E6" + }, + { + "length": 34.33, + "psi_value": 0.003, + "psi_value_source": 3, + "thermal_bridge_type": "E6" + }, + { + "length": 26.68, + "psi_value": 0.094, + "psi_value_source": 3, + "thermal_bridge_type": "E10" + }, + { + "length": 9.4, + "psi_value": 0.049, + "psi_value_source": 3, + "thermal_bridge_type": "E12" + }, + { + "length": 4.41, + "psi_value": 0.08, + "psi_value_source": 4, + "thermal_bridge_type": "E14" + }, + { + "length": 34.2, + "psi_value": 0.047, + "psi_value_source": 3, + "thermal_bridge_type": "E16" + }, + { + "length": 15.36, + "psi_value": -0.097, + "psi_value_source": 3, + "thermal_bridge_type": "E17" + } + ], + "thermal_bridge_code": 5 + }, + "building_part_number": 1, + "sap_floor_dimensions": [ + { + "storey": 0, + "u_value": 0.14, + "floor_type": 2, + "description": "B&B", + "storey_height": 2.33, + "heat_loss_area": 73.67, + "total_floor_area": 73.67 + }, + { + "storey": 1, + "u_value": 0, + "floor_type": 3, + "storey_height": 2.56, + "heat_loss_area": 0, + "total_floor_area": 72.6 + } + ], + "thermal_mass_parameter": 134.43 + } + ], + "heating_cost_current": { + "value": 339, + "currency": "GBP" + }, + "co2_emissions_current": 2.1, + "energy_rating_average": 60, + "energy_rating_current": 85, + "lighting_cost_current": { + "value": 93, + "currency": "GBP" + }, + "main_heating_controls": [ + { + "description": "Time and temperature zone control", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + } + ], + "has_hot_water_cylinder": "true", + "heating_cost_potential": { + "value": 340, + "currency": "GBP" + }, + "hot_water_cost_current": { + "value": 99, + "currency": "GBP" + }, + "suggested_improvements": [ + { + "sequence": 1, + "typical_saving": { + "value": 43, + "currency": "GBP" + }, + "indicative_cost": "£4,000 - £6,000", + "improvement_type": "N", + "improvement_details": { + "improvement_number": 19 + }, + "improvement_category": 5, + "energy_performance_rating": 86, + "environmental_impact_rating": 87 + }, + { + "sequence": 2, + "typical_saving": { + "value": 340, + "currency": "GBP" + }, + "indicative_cost": "£3,500 - £5,500", + "improvement_type": "U", + "improvement_details": { + "improvement_number": 34 + }, + "improvement_category": 5, + "energy_performance_rating": 93, + "environmental_impact_rating": 93 + } + ], + "co2_emissions_potential": 0.9, + "energy_rating_potential": 93, + "lighting_cost_potential": { + "value": 93, + "currency": "GBP" + }, + "schema_version_original": "LIG-17.0", + "hot_water_cost_potential": { + "value": 55, + "currency": "GBP" + }, + "is_in_smoke_control_area": "unknown", + "renewable_heat_incentive": { + "rhi_new_dwelling": { + "space_heating": 5361, + "water_heating": 2143 + } + }, + "seller_commission_report": "Y", + "energy_consumption_current": 82, + "has_fixed_air_conditioning": "false", + "multiple_glazed_percentage": 100, + "calculation_software_version": "4.12r02", + "energy_consumption_potential": 34, + "environmental_impact_current": 85, + "current_energy_efficiency_band": "B", + "environmental_impact_potential": 93, + "has_heated_separate_conservatory": "false", + "potential_energy_efficiency_band": "A", + "co2_emissions_current_per_floor_area": 14 +} \ No newline at end of file diff --git a/backend/epc_api/json_samples/SAP-Schema-18.0.0/epc.json b/backend/epc_api/json_samples/SAP-Schema-18.0.0/epc.json new file mode 100644 index 00000000..b2d27bbf --- /dev/null +++ b/backend/epc_api/json_samples/SAP-Schema-18.0.0/epc.json @@ -0,0 +1,510 @@ +{ + "name": "-", + "uprn": 12457, + "roofs": [ + { + "description": "Average thermal transmittance 0.10 W/m²K", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + } + ], + "walls": [ + { + "description": "Average thermal transmittance 0.10 W/m²K", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + } + ], + "floors": [ + { + "description": "Average thermal transmittance 0.09 W/m²K", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + } + ], + "status": "entered", + "tenure": "ND", + "windows": { + "description": "High performance glazing", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + }, + "addendum": { + "stone_walls": "true", + "system_build": "true" + }, + "lighting": { + "description": "Low energy lighting in all fixed outlets", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + }, + "postcode": "LS0 0AA", + "data_type": 2, + "hot_water": { + "description": "Electric immersion, standard tariff", + "energy_efficiency_rating": 1, + "environmental_efficiency_rating": 2 + }, + "post_town": "Town", + "built_form": 4, + "living_area": 38.91, + "orientation": 6, + "region_code": 3, + "report_type": 3, + "sap_heating": { + "water_fuel_type": 39, + "water_heating_code": 903, + "hot_water_store_size": 210, + "main_heating_details": [ + { + "main_fuel_type": 39, + "main_heating_code": 691, + "emitter_temperature": "NA", + "main_heating_number": 1, + "main_heating_control": 2603, + "main_heating_category": 10, + "main_heating_fraction": 1, + "main_heating_data_source": 3 + } + ], + "has_hot_water_cylinder": "true", + "immersion_heating_type": 1, + "has_cylinder_thermostat": "true", + "hot_water_store_heat_loss": 1.31, + "has_fixed_air_conditioning": "false", + "secondary_heating_category": 1, + "is_cylinder_in_heated_space": "true", + "hot_water_store_heat_loss_source": 2 + }, + "sap_version": 9.92, + "schema_type": "SAP-Schema-18.0.0", + "uprn_source": "Energy Assessor", + "country_code": "ENG", + "main_heating": [ + { + "description": "Room heaters, electric", + "energy_efficiency_rating": 1, + "environmental_efficiency_rating": 2 + } + ], + "air_tightness": { + "description": "Air permeability 0.7 m³/h.m² (as tested)", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + }, + "dwelling_type": "Mid-terrace house", + "language_code": 1, + "property_type": 0, + "address_line_1": "30 Lovely Place", + "address_line_2": "Nice Road", + "assessment_date": "2021-01-01", + "assessment_type": "SAP", + "completion_date": "2021-01-01", + "inspection_date": "2021-01-01", + "sap_ventilation": { + "psv_count": 0, + "pressure_test": 1, + "wet_rooms_count": 3, + "air_permeability": 0.69, + "open_flues_count": 0, + "ventilation_type": 8, + "extract_fans_count": 0, + "open_fireplaces_count": 0, + "sheltered_sides_count": 3, + "flueless_gas_fires_count": 0, + "mechanical_vent_duct_type": 2, + "mechanical_vent_duct_insulation": 2, + "mechanical_ventilation_data_source": 1, + "mechanical_vent_system_index_number": 500482, + "is_mechanical_vent_approved_installer_scheme": "true" + }, + "design_water_use": 1, + "sap_data_version": 9.92, + "sap_flat_details": { + "level": 1 + }, + "total_floor_area": 117, + "transaction_type": 6, + "conservatory_type": 1, + "registration_date": "2021-01-01", + "sap_energy_source": { + "pv_arrays": [ + { + "pitch": 2, + "peak_power": 0.75, + "orientation": 6, + "overshading": 1, + "pv_connection": 2 + } + ], + "electricity_tariff": 1, + "wind_turbines_count": 0, + "wind_turbine_terrain_type": 1, + "fixed_lighting_outlets_count": 20, + "low_energy_fixed_lighting_outlets_count": 20, + "low_energy_fixed_lighting_outlets_percentage": 100 + }, + "sap_opening_types": [ + { + "name": "Doors", + "type": 1, + "u_value": 0.75, + "data_source": 2, + "glazing_type": 1 + }, + { + "name": "Windows - W-001A", + "type": 4, + "u_value": 0.72, + "data_source": 2, + "frame_factor": 0.85, + "glazing_type": 12, + "solar_transmittance": 0.52 + }, + { + "name": "Windows - W-001B", + "type": 4, + "u_value": 0.73, + "data_source": 2, + "frame_factor": 0.84, + "glazing_type": 12, + "solar_transmittance": 0.52 + }, + { + "name": "W-003 Louvre system", + "type": 4, + "u_value": 1.02, + "data_source": 2, + "frame_factor": 0.01, + "glazing_type": 12, + "solar_transmittance": 0 + }, + { + "name": "Window - W-003R", + "type": 4, + "u_value": 0.82, + "data_source": 2, + "frame_factor": 0.78, + "glazing_type": 12, + "solar_transmittance": 0.53 + }, + { + "name": "Sliding door - EX002A", + "type": 4, + "u_value": 0.86, + "data_source": 2, + "frame_factor": 0.85, + "glazing_type": 12, + "solar_transmittance": 0.52 + }, + { + "name": "EX002B Louvre system", + "type": 4, + "u_value": 1, + "data_source": 2, + "frame_factor": 0.01, + "glazing_type": 12, + "solar_transmittance": 0 + }, + { + "name": "Window - EX002B", + "type": 4, + "u_value": 0.76, + "data_source": 2, + "frame_factor": 0.84, + "glazing_type": 12, + "solar_transmittance": 0.53 + } + ], + "secondary_heating": { + "description": "None", + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + }, + "lzc_energy_sources": [ + 11, + 9 + ], + "sap_building_parts": [ + { + "sap_roofs": [ + { + "name": "Roof 1", + "u_value": 0.1, + "roof_type": 2, + "description": "External Roof 1", + "total_roof_area": 38.91 + } + ], + "sap_walls": [ + { + "name": "External Wall 1", + "u_value": 0.1, + "wall_type": 2, + "description": "External Wall 1", + "total_wall_area": 77.31, + "is_curtain_walling": "false" + }, + { + "name": "Party Wall 0", + "u_value": 0, + "wall_type": 4, + "total_wall_area": 129.44 + } + ], + "identifier": "Main Dwelling", + "overshading": 2, + "sap_openings": [ + { + "name": "Opening 1", + "type": "Doors", + "width": 2.23, + "height": 1, + "location": "External Wall 1", + "orientation": 0 + }, + { + "name": "Opening 2", + "type": "Windows - W-001A", + "width": 2.3, + "height": 1, + "location": "External Wall 1", + "orientation": 6 + }, + { + "name": "Opening 3", + "type": "W-003 Louvre system", + "width": 0.63, + "height": 1, + "location": "External Wall 1", + "orientation": 6 + }, + { + "name": "Opening 4", + "type": "Window - W-003R", + "width": 2.38, + "height": 1, + "location": "External Wall 1", + "orientation": 6 + }, + { + "name": "Opening 5", + "type": "Sliding door - EX002A", + "width": 5.37, + "height": 1, + "location": "External Wall 1", + "orientation": 6 + }, + { + "name": "Opening 6", + "type": "Windows - W-001A", + "width": 2.3, + "height": 1, + "location": "External Wall 1", + "orientation": 2 + }, + { + "name": "Opening 7", + "type": "Windows - W-001B", + "width": 2.06, + "height": 1, + "location": "External Wall 1", + "orientation": 2 + }, + { + "name": "Opening 8", + "type": "Window - W-003R", + "width": 1.18, + "height": 1, + "location": "External Wall 1", + "orientation": 2 + }, + { + "name": "Opening 9", + "type": "EX002B Louvre system", + "width": 0.95, + "height": 1, + "location": "External Wall 1", + "orientation": 2 + }, + { + "name": "Opening 10", + "type": "Window - EX002B", + "width": 4.73, + "height": 1, + "location": "External Wall 1", + "orientation": 2 + } + ], + "construction_year": 2017, + "sap_thermal_bridges": { + "thermal_bridges": [ + { + "length": 13.13, + "psi_value": 0.07, + "psi_value_source": 3, + "thermal_bridge_type": "E2" + }, + { + "length": 9.7, + "psi_value": 0.055, + "psi_value_source": 3, + "thermal_bridge_type": "E3" + }, + { + "length": 31.33, + "psi_value": 0.066, + "psi_value_source": 3, + "thermal_bridge_type": "E4" + }, + { + "length": 9.64, + "psi_value": 0.074, + "psi_value_source": 3, + "thermal_bridge_type": "E20" + }, + { + "length": 19.28, + "psi_value": 0.049, + "psi_value_source": 3, + "thermal_bridge_type": "E6" + }, + { + "length": 9.84, + "psi_value": 0.097, + "psi_value_source": 3, + "thermal_bridge_type": "E15" + }, + { + "length": 32.08, + "psi_value": 0.02, + "psi_value_source": 3, + "thermal_bridge_type": "E18" + }, + { + "length": 32.28, + "psi_value": 0, + "psi_value_source": 4, + "thermal_bridge_type": "P2" + }, + { + "length": 16.14, + "psi_value": 0.16, + "psi_value_source": 4, + "thermal_bridge_type": "P7" + }, + { + "length": 16.14, + "psi_value": 0.005, + "psi_value_source": 3, + "thermal_bridge_type": "P4" + } + ], + "thermal_bridge_code": 5 + }, + "building_part_number": 1, + "sap_floor_dimensions": [ + { + "storey": 0, + "u_value": 0.09, + "floor_type": 3, + "description": "Heat Loss Floor 1", + "storey_height": 2.48, + "heat_loss_area": 38.91, + "total_floor_area": 38.91 + }, + { + "storey": 1, + "u_value": 0, + "floor_type": 3, + "storey_height": 2.8, + "heat_loss_area": 0, + "total_floor_area": 38.91 + }, + { + "storey": 2, + "u_value": 0, + "floor_type": 3, + "storey_height": 2.74, + "heat_loss_area": 0, + "total_floor_area": 38.91 + } + ], + "thermal_mass_parameter": 100 + } + ], + "heating_cost_current": { + "value": 133, + "currency": "GBP" + }, + "co2_emissions_current": 1.3, + "energy_rating_average": 60, + "energy_rating_current": 88, + "lighting_cost_current": { + "value": 86, + "currency": "GBP" + }, + "main_heating_controls": [ + { + "description": "Programmer and appliance thermostats", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "has_hot_water_cylinder": "true", + "heating_cost_potential": { + "value": 136, + "currency": "GBP" + }, + "hot_water_cost_current": { + "value": 361, + "currency": "GBP" + }, + "suggested_improvements": [ + { + "sequence": 1, + "typical_saving": { + "value": 184, + "currency": "GBP" + }, + "indicative_cost": "£4,000 - £6,000", + "improvement_type": "N", + "improvement_details": { + "improvement_number": 19 + }, + "improvement_category": 5, + "energy_performance_rating": 93, + "environmental_impact_rating": 94 + } + ], + "co2_emissions_potential": 0.8, + "energy_rating_potential": 93, + "lighting_cost_potential": { + "value": 86, + "currency": "GBP" + }, + "schema_version_original": "18.0.0", + "hot_water_cost_potential": { + "value": 175, + "currency": "GBP" + }, + "is_in_smoke_control_area": "unknown", + "renewable_heat_incentive": { + "rhi_new_dwelling": { + "space_heating": 413, + "water_heating": 1890 + } + }, + "seller_commission_report": "Y", + "energy_consumption_current": 64, + "has_fixed_air_conditioning": "false", + "multiple_glazed_percentage": 100, + "calculation_software_version": "4.14r16", + "energy_consumption_potential": 39, + "environmental_impact_current": 89, + "current_energy_efficiency_band": "B", + "environmental_impact_potential": 94, + "has_heated_separate_conservatory": "false", + "potential_energy_efficiency_band": "A", + "co2_emissions_current_per_floor_area": 11 +} \ No newline at end of file diff --git a/backend/epc_api/json_samples/SAP-Schema-19.0.0/epc.json b/backend/epc_api/json_samples/SAP-Schema-19.0.0/epc.json new file mode 100644 index 00000000..66e58247 --- /dev/null +++ b/backend/epc_api/json_samples/SAP-Schema-19.0.0/epc.json @@ -0,0 +1,610 @@ +{ + "uprn": 12457, + "roofs": [ + { + "description": "Average thermal transmittance 0.13 W/m²K", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + } + ], + "walls": [ + { + "description": "Average thermal transmittance 0.18 W/m²K", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + } + ], + "floors": [ + { + "description": "Average thermal transmittance 0.12 W/m²K", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + } + ], + "status": "entered", + "tenure": 1, + "windows": { + "description": "High performance glazing", + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 5 + }, + "addendum": { + "stone_walls": "true" + }, + "lighting": { + "description": "Energy saving bulbs", + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + }, + "postcode": "A0 0AA", + "data_type": 1, + "hot_water": { + "description": "From main system, waste water heat recovery", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 3 + }, + "post_town": "Whitbury", + "built_form": 4, + "living_area": 41.35, + "orientation": 0, + "region_code": 16, + "report_type": 3, + "sap_heating": { + "thermal_store": 1, + "shower_outlets": [ + { + "shower_wwhrs": 1, + "shower_flow_rate": 7, + "shower_outlet_type": 1 + }, + { + "shower_wwhrs": 2, + "shower_flow_rate": 7, + "shower_outlet_type": 1 + } + ], + "water_fuel_type": 39, + "water_heating_code": 901, + "instantaneous_wwhrs": { + "wwhrs_index_number1": 491123 + }, + "hot_water_store_size": 600, + "main_heating_details": [ + { + "has_fghrs": "false", + "main_fuel_type": 39, + "combi_boiler_type": 4, + "heat_emitter_type": 1, + "main_heating_code": 192, + "main_heating_number": 1, + "is_condensing_boiler": "false", + "main_heating_control": 2106, + "is_interlocked_system": "false", + "main_heating_category": 2, + "main_heating_fraction": 1, + "central_heating_pump_age": 2, + "main_heating_data_source": 3, + "has_separate_delayed_start": "true", + "is_oil_pump_in_heated_space": "false", + "is_main_heating_hetas_approved": "false", + "electric_cpsu_operating_temperature": 80, + "is_central_heating_pump_in_heated_space": "true" + } + ], + "has_hot_water_cylinder": "true", + "has_cylinder_thermostat": "true", + "hot_water_store_heat_loss": 2.45, + "has_fixed_air_conditioning": "false", + "secondary_heating_category": 1, + "is_immersion_for_summer_use": "false", + "is_hot_water_separately_timed": "false", + "hot_water_store_heat_loss_source": 2, + "is_heat_pump_assisted_by_immersion": "false" + }, + "sap_version": 10.2, + "schema_type": "SAP-Schema-19.0.0", + "uprn_source": "Energy Assessor", + "country_code": "ENG", + "main_heating": [ + { + "description": "Boiler and radiators, electric", + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 2 + } + ], + "sap_lighting": [ + [ + { + "lighting_power": 60, + "lighting_outlets": 1, + "lighting_efficacy": 11.2 + }, + { + "lighting_power": 14, + "lighting_outlets": 10, + "lighting_efficacy": 66.9 + }, + { + "lighting_power": 15, + "lighting_outlets": 7, + "lighting_efficacy": 69.9 + } + ] + ], + "terrain_type": 1, + "air_tightness": { + "description": "Air permeability 2.0 m³/h.m² (assumed)", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + }, + "dwelling_type": "Mid-terrace house", + "language_code": 1, + "property_type": 0, + "address_line_1": "1 Some Street", + "address_line_2": "Some Area", + "address_line_3": "Some County", + "assessment_date": "2022-05-09", + "assessment_type": "SAP", + "completion_date": "2022-05-09", + "inspection_date": "2022-05-09", + "sap_ventilation": { + "psv_count": 0, + "wall_type": 2, + "pressure_test": 2, + "wet_rooms_count": 2, + "air_permeability": 2, + "open_flues_count": 0, + "ventilation_type": 8, + "has_draught_lobby": "false", + "other_flues_count": 0, + "closed_flues_count": 0, + "extract_fans_count": 0, + "boilers_flues_count": 0, + "open_chimneys_count": 0, + "sheltered_sides_count": 2, + "blocked_chimneys_count": 0, + "flueless_gas_fires_count": 0, + "mechanical_vent_duct_type": 2, + "mechanical_vent_duct_placement": 1, + "mechanical_ventilation_data_source": 1, + "mechanical_vent_system_index_number": 500206, + "mechanical_vent_duct_insulation_level": 2, + "is_mechanical_vent_approved_installer_scheme": "true" + }, + "sap_data_version": 10.2, + "sap_flat_details": { + "level": 1, + "storeys": 1 + }, + "total_floor_area": 165, + "transaction_type": 1, + "cold_water_source": 1, + "conservatory_type": 1, + "registration_date": "2022-05-09", + "sap_energy_source": { + "wind_turbines": [ + { + "wind_turbine_hub_height": 3, + "wind_turbine_rotor_diameter": 1.7 + } + ], + "electricity_tariff": 4 + }, + "sap_opening_types": [ + { + "name": "Doors", + "type": 1, + "u_value": 1.5, + "data_source": 2, + "glazing_type": 1, + "isargonfilled": "false" + }, + { + "name": "Windows (1)", + "type": 4, + "u_value": 1.4, + "frame_type": 1, + "data_source": 3, + "glazing_gap": 3, + "frame_factor": 0.7, + "glazing_type": 11, + "isargonfilled": "true", + "solar_transmittance": 0.57 + }, + { + "name": "Windows (2)", + "type": 4, + "u_value": 1.5, + "frame_type": 1, + "data_source": 3, + "glazing_gap": 3, + "frame_factor": 0.7, + "glazing_type": 9, + "isargonfilled": "true", + "solar_transmittance": 0.64 + } + ], + "secondary_heating": { + "description": "Electric heater", + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + }, + "lowest_storey_area": 57.4, + "lzc_energy_sources": [ + 11 + ], + "sap_building_parts": [ + { + "sap_roofs": [ + { + "name": "Roof (1)", + "u_value": 0.13, + "roof_type": 2, + "kappa_value": 9, + "total_roof_area": 57.4 + } + ], + "sap_walls": [ + { + "name": "Walls (1)", + "u_value": 0.18, + "wall_type": 2, + "kappa_value": 60, + "total_wall_area": 111.34, + "is_curtain_walling": "false" + }, + { + "name": "Party wall", + "u_value": 0, + "wall_type": 4, + "kappa_value": 0, + "total_wall_area": 167, + "is_curtain_walling": "false" + }, + { + "name": "Internal wall (1)", + "u_value": 0, + "wall_type": 5, + "kappa_value": 0, + "total_wall_area": 320, + "is_curtain_walling": "false" + } + ], + "identifier": "Main Dwelling", + "sap_openings": [ + { + "name": 1, + "type": "Doors", + "width": 1, + "height": 2.25, + "location": "Walls (1)", + "orientation": 3 + }, + { + "name": 2, + "type": "Doors", + "width": 0.8, + "height": 2.1, + "location": "Walls (1)", + "orientation": 3 + }, + { + "name": 3, + "type": "Windows (2)", + "width": 1.6, + "height": 2.7, + "location": "Walls (1)", + "orientation": 7 + }, + { + "name": 4, + "type": "Windows (1)", + "width": 1.1, + "height": 1.75, + "location": "Walls (1)", + "orientation": 3 + }, + { + "name": 5, + "type": "Windows (1)", + "width": 1.1, + "height": 1.75, + "location": "Walls (1)", + "orientation": 3 + }, + { + "name": 6, + "type": "Windows (1)", + "width": 1.1, + "height": 1.85, + "location": "Walls (1)", + "orientation": 3 + }, + { + "name": 7, + "type": "Windows (1)", + "width": 1.1, + "height": 1.85, + "location": "Walls (1)", + "orientation": 3 + }, + { + "name": 8, + "type": "Windows (1)", + "width": 1.1, + "height": 1.8, + "location": "Walls (1)", + "orientation": 3 + }, + { + "name": 9, + "type": "Windows (1)", + "width": 1.1, + "height": 1.75, + "location": "Walls (1)", + "orientation": 7 + }, + { + "name": 10, + "type": "Windows (1)", + "width": 1.1, + "height": 1.85, + "location": "Walls (1)", + "orientation": 7 + }, + { + "name": 11, + "type": "Windows (1)", + "width": 1.1, + "height": 1.85, + "location": "Walls (1)", + "orientation": 7 + }, + { + "name": 12, + "type": "Windows (1)", + "width": 0.6, + "height": 0.8, + "location": "Walls (1)", + "orientation": 7 + }, + { + "name": 13, + "type": "Windows (1)", + "width": 0.8, + "height": 1.25, + "location": "Walls (1)", + "orientation": 7 + }, + { + "name": 14, + "type": "Windows (1)", + "width": 0.8, + "height": 1.25, + "location": "Walls (1)", + "orientation": 7 + }, + { + "name": 15, + "type": "Windows (1)", + "width": 1.1, + "height": 1.25, + "location": "Walls (1)", + "orientation": 7 + } + ], + "construction_year": 1750, + "sap_thermal_bridges": { + "thermal_bridges": [ + { + "length": 15.5, + "psi_value": 0.3, + "psi_value_source": 2, + "thermal_bridge_type": "E2" + }, + { + "length": 12.1, + "psi_value": 0.04, + "psi_value_source": 2, + "thermal_bridge_type": "E3" + }, + { + "length": 52.1, + "psi_value": 0.05, + "psi_value_source": 2, + "thermal_bridge_type": "E4" + }, + { + "length": 14.4, + "psi_value": 0.16, + "psi_value_source": 2, + "thermal_bridge_type": "E5" + }, + { + "length": 25.8, + "psi_value": 0.07, + "psi_value_source": 2, + "thermal_bridge_type": "E6" + }, + { + "length": 10.4, + "psi_value": 0.06, + "psi_value_source": 2, + "thermal_bridge_type": "E10" + }, + { + "length": 5.4, + "psi_value": 0.06, + "psi_value_source": 3, + "thermal_bridge_type": "E14" + }, + { + "length": 5.8, + "psi_value": 0.09, + "psi_value_source": 2, + "thermal_bridge_type": "E16" + }, + { + "length": 5.8, + "psi_value": -0.09, + "psi_value_source": 2, + "thermal_bridge_type": "E17" + }, + { + "length": 34, + "psi_value": 0.06, + "psi_value_source": 2, + "thermal_bridge_type": "E18" + }, + { + "length": 20.6, + "psi_value": 0.12, + "psi_value_source": 3, + "thermal_bridge_type": "P1" + }, + { + "length": 20.6, + "psi_value": 0, + "psi_value_source": 4, + "thermal_bridge_type": "P2" + }, + { + "length": 20.6, + "psi_value": 0.12, + "psi_value_source": 3, + "thermal_bridge_type": "P4" + } + ], + "thermal_bridge_code": 5 + }, + "building_part_number": 1, + "sap_floor_dimensions": [ + { + "storey": 0, + "u_value": 0.12, + "floor_type": 2, + "kappa_value": 80, + "storey_height": 2.8, + "heat_loss_area": 57.4, + "total_floor_area": 57.4 + }, + { + "storey": 1, + "u_value": 0, + "floor_type": 3, + "kappa_value": 18, + "storey_height": 3, + "heat_loss_area": 0, + "total_floor_area": 57.4, + "kappa_value_from_below": 9 + }, + { + "storey": 2, + "u_value": 0, + "floor_type": 3, + "kappa_value": 19, + "storey_height": 2.7, + "heat_loss_area": 0, + "total_floor_area": 57.4, + "kappa_value_from_below": 9 + } + ], + "construction_age_band": "A" + } + ], + "user_interface_name": "BRE SAP interface 10.2", + "windows_overshading": 2, + "heating_cost_current": { + "value": 365.98, + "currency": "GBP" + }, + "co2_emissions_current": 2.4, + "energy_rating_average": 60, + "energy_rating_current": 72, + "lighting_cost_current": { + "value": 123.45, + "currency": "GBP" + }, + "main_heating_controls": [ + { + "description": "Programmer, room thermostat and TRVs", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "has_hot_water_cylinder": "false", + "heating_cost_potential": { + "value": 250.34, + "currency": "GBP" + }, + "hot_water_cost_current": { + "value": 200.4, + "currency": "GBP" + }, + "suggested_improvements": [ + { + "sequence": 1, + "typical_saving": { + "value": 88, + "currency": "GBP" + }, + "indicative_cost": "£4,000 - £6,000", + "improvement_type": "N", + "improvement_details": { + "improvement_number": 19 + }, + "improvement_category": 5, + "energy_performance_rating": 74, + "environmental_impact_rating": 94 + }, + { + "sequence": 2, + "typical_saving": { + "value": 88, + "currency": "GBP" + }, + "indicative_cost": "£9,000 - £14,000", + "improvement_type": "U", + "improvement_details": { + "improvement_number": 34 + }, + "improvement_category": 5, + "energy_performance_rating": 80, + "environmental_impact_rating": 96 + } + ], + "user_interface_version": "1.0.1-alpha", + "co2_emissions_potential": 1.4, + "energy_rating_potential": 72, + "gas_smart_meter_present": "false", + "lighting_cost_potential": { + "value": 84.23, + "currency": "GBP" + }, + "schema_version_original": "SAP-Schema-19.0.0", + "hot_water_cost_potential": { + "value": 180.43, + "currency": "GBP" + }, + "is_in_smoke_control_area": "unknown", + "renewable_heat_incentive": { + "rhi_new_dwelling": { + "space_heating": 13120, + "water_heating": 2285 + } + }, + "seller_commission_report": "Y", + "energy_consumption_current": 59, + "has_fixed_air_conditioning": "false", + "is_dwelling_export_capable": "true", + "multiple_glazed_percentage": 100, + "calculation_software_version": "13.05r16", + "energy_consumption_potential": 53, + "environmental_impact_current": 94, + "current_energy_efficiency_band": "C", + "environmental_impact_potential": 96, + "electricity_smart_meter_present": "false", + "has_heated_separate_conservatory": "false", + "potential_energy_efficiency_band": "C", + "co2_emissions_current_per_floor_area": 5.6 +} \ No newline at end of file diff --git a/backend/epc_api/json_samples/SAP-Schema-19.1.0/epc.json b/backend/epc_api/json_samples/SAP-Schema-19.1.0/epc.json new file mode 100644 index 00000000..2e94810f --- /dev/null +++ b/backend/epc_api/json_samples/SAP-Schema-19.1.0/epc.json @@ -0,0 +1,613 @@ +{ + "uprn": 12457, + "roofs": [ + { + "description": "Average thermal transmittance 0.13 W/m²K", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + } + ], + "walls": [ + { + "description": "Average thermal transmittance 0.18 W/m²K", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + } + ], + "floors": [ + { + "description": "Average thermal transmittance 0.12 W/m²K", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + } + ], + "status": "entered", + "tenure": 1, + "windows": { + "description": "High performance glazing", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + }, + "lighting": { + "description": "Low energy lighting in 91% of fixed outlets", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + }, + "postcode": "A0 0AA", + "data_type": 1, + "hot_water": { + "description": "From main system, waste water heat recovery", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 3 + }, + "post_town": "Whitbury", + "built_form": 4, + "living_area": 41.35, + "orientation": 0, + "region_code": 16, + "report_type": 3, + "sap_heating": { + "thermal_store": 1, + "shower_outlets": [ + { + "shower_wwhrs": 1, + "shower_flow_rate": 7, + "shower_outlet_type": 1 + }, + { + "shower_wwhrs": 2, + "shower_flow_rate": 7, + "shower_outlet_type": 1 + } + ], + "water_fuel_type": 39, + "water_heating_code": 901, + "instantaneous_wwhrs": { + "wwhrs_index_number1": 491123 + }, + "hot_water_store_size": 600, + "main_heating_details": [ + { + "has_fghrs": "false", + "main_fuel_type": 47, + "combi_boiler_type": 4, + "heat_emitter_type": 1, + "main_heating_code": 192, + "main_heating_number": 1, + "is_condensing_boiler": "false", + "main_heating_control": 2106, + "is_interlocked_system": "false", + "main_heating_category": 2, + "main_heating_fraction": 1, + "central_heating_pump_age": 2, + "main_heating_data_source": 3, + "has_separate_delayed_start": "true", + "is_oil_pump_in_heated_space": "false", + "is_main_heating_hetas_approved": "false", + "electric_cpsu_operating_temperature": 80, + "is_central_heating_pump_in_heated_space": "true" + } + ], + "has_hot_water_cylinder": "true", + "has_cylinder_thermostat": "true", + "hot_water_store_heat_loss": 2.45, + "has_fixed_air_conditioning": "false", + "secondary_heating_category": 1, + "is_immersion_for_summer_use": "false", + "is_hot_water_separately_timed": "false", + "sap_community_heating_systems": [ + { + "sub_network_name": "Test 1", + "community_heating_use": 3, + "heat_network_existing": "true", + "heat_network_index_number": 496402, + "heat_network_assessed_as_new": "true", + "community_heating_distribution_type": 5, + "community_heating_distribution_loss_factor": 1.26 + } + ], + "hot_water_store_heat_loss_source": 2, + "is_heat_pump_assisted_by_immersion": "false" + }, + "sap_version": 10.2, + "schema_type": "SAP-Schema-19.1.0", + "uprn_source": "Energy Assessor", + "country_code": "ENG", + "main_heating": [ + { + "description": "Boiler and radiators, electric", + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 2 + } + ], + "sap_lighting": [ + [ + { + "lighting_power": 60, + "lighting_outlets": 1, + "lighting_efficacy": 11.2 + }, + { + "lighting_power": 14, + "lighting_outlets": 10, + "lighting_efficacy": 66.9 + } + ] + ], + "terrain_type": 1, + "air_tightness": { + "description": "Air permeability 2.0 m³/h.m² (assumed)", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + }, + "dwelling_type": "Mid-terrace house", + "language_code": 1, + "property_type": 0, + "address_line_1": "1 Some Street", + "address_line_2": "Some Area", + "address_line_3": "Some County", + "assessment_date": "2022-05-09", + "assessment_type": "SAP", + "completion_date": "2022-05-09", + "inspection_date": "2022-05-09", + "sap_ventilation": { + "psv_count": 0, + "wall_type": 2, + "pressure_test": 2, + "wet_rooms_count": 2, + "air_permeability": 2, + "open_flues_count": 0, + "ventilation_type": 8, + "has_draught_lobby": "false", + "other_flues_count": 0, + "closed_flues_count": 0, + "extract_fans_count": 0, + "boilers_flues_count": 0, + "open_chimneys_count": 0, + "sheltered_sides_count": 2, + "blocked_chimneys_count": 0, + "flueless_gas_fires_count": 0, + "mechanical_vent_duct_type": 2, + "mechanical_vent_duct_placement": 1, + "mechanical_ventilation_data_source": 1, + "mechanical_vent_system_index_number": 500206, + "mechanical_vent_duct_insulation_level": 2, + "is_mechanical_vent_approved_installer_scheme": "true" + }, + "sap_data_version": 10.2, + "sap_flat_details": { + "level": 1, + "storeys": 1 + }, + "total_floor_area": 165, + "transaction_type": 1, + "cold_water_source": 1, + "conservatory_type": 1, + "registration_date": "2022-05-09", + "sap_energy_source": { + "wind_turbines": [ + { + "wind_turbine_hub_height": 3, + "wind_turbine_rotor_diameter": 1.7 + }, + { + "wind_turbine_hub_height": 3, + "wind_turbine_rotor_diameter": 1.7 + } + ], + "electricity_tariff": 5 + }, + "sap_opening_types": [ + { + "name": "Doors", + "type": 1, + "u_value": 1.5, + "data_source": 2, + "glazing_type": 1, + "isargonfilled": "false" + }, + { + "name": "Windows (1)", + "type": 4, + "u_value": 1.4, + "frame_type": 1, + "data_source": 3, + "glazing_gap": 3, + "frame_factor": 0.7, + "glazing_type": 11, + "isargonfilled": "true", + "solar_transmittance": 0.57 + }, + { + "name": "Windows (2)", + "type": 4, + "u_value": 1.5, + "frame_type": 1, + "data_source": 3, + "glazing_gap": 3, + "frame_factor": 0.7, + "glazing_type": 9, + "isargonfilled": "true", + "solar_transmittance": 0.64 + } + ], + "secondary_heating": { + "description": "None", + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + }, + "lowest_storey_area": 57.4, + "sap_building_parts": [ + { + "sap_roofs": [ + { + "name": "Roof (1)", + "u_value": 0.13, + "roof_type": 2, + "kappa_value": 9, + "total_roof_area": 57.4 + } + ], + "sap_walls": [ + { + "name": "Walls (1)", + "u_value": 0.18, + "wall_type": 2, + "kappa_value": 60, + "total_wall_area": 111.34, + "is_curtain_walling": "false" + }, + { + "name": "Party wall", + "u_value": 0, + "wall_type": 4, + "kappa_value": 0, + "total_wall_area": 167, + "is_curtain_walling": "false" + }, + { + "name": "Internal wall (1)", + "u_value": 0, + "wall_type": 5, + "kappa_value": 0, + "total_wall_area": 320, + "is_curtain_walling": "false" + } + ], + "identifier": "Main Dwelling", + "sap_openings": [ + { + "name": 1, + "type": "Doors", + "width": 1, + "height": 2.25, + "location": "Walls (1)", + "orientation": 3 + }, + { + "name": 2, + "type": "Doors", + "width": 0.8, + "height": 2.1, + "location": "Walls (1)", + "orientation": 3 + }, + { + "name": 3, + "type": "Windows (2)", + "width": 1.6, + "height": 2.7, + "location": "Walls (1)", + "orientation": 7 + }, + { + "name": 4, + "type": "Windows (1)", + "width": 1.1, + "height": 1.75, + "location": "Walls (1)", + "orientation": 3 + }, + { + "name": 5, + "type": "Windows (1)", + "width": 1.1, + "height": 1.75, + "location": "Walls (1)", + "orientation": 3 + }, + { + "name": 6, + "type": "Windows (1)", + "width": 1.1, + "height": 1.85, + "location": "Walls (1)", + "orientation": 3 + }, + { + "name": 7, + "type": "Windows (1)", + "width": 1.1, + "height": 1.85, + "location": "Walls (1)", + "orientation": 3 + }, + { + "name": 8, + "type": "Windows (1)", + "width": 1.1, + "height": 1.8, + "location": "Walls (1)", + "orientation": 3 + }, + { + "name": 9, + "type": "Windows (1)", + "width": 1.1, + "height": 1.75, + "location": "Walls (1)", + "orientation": 7 + }, + { + "name": 10, + "type": "Windows (1)", + "width": 1.1, + "height": 1.85, + "location": "Walls (1)", + "orientation": 7 + }, + { + "name": 11, + "type": "Windows (1)", + "width": 1.1, + "height": 1.85, + "location": "Walls (1)", + "orientation": 7 + }, + { + "name": 12, + "type": "Windows (1)", + "width": 0.6, + "height": 0.8, + "location": "Walls (1)", + "orientation": 7 + }, + { + "name": 13, + "type": "Windows (1)", + "width": 0.8, + "height": 1.25, + "location": "Walls (1)", + "orientation": 7 + }, + { + "name": 14, + "type": "Windows (1)", + "width": 0.8, + "height": 1.25, + "location": "Walls (1)", + "orientation": 7 + }, + { + "name": 15, + "type": "Windows (1)", + "width": 1.1, + "height": 1.25, + "location": "Walls (1)", + "orientation": 7 + } + ], + "sap_thermal_bridges": { + "thermal_bridges": [ + { + "length": 15.5, + "psi_value": 0.3, + "psi_value_source": 2, + "thermal_bridge_type": "E2" + }, + { + "length": 12.1, + "psi_value": 0.04, + "psi_value_source": 2, + "thermal_bridge_type": "E3" + }, + { + "length": 52.1, + "psi_value": 0.05, + "psi_value_source": 2, + "thermal_bridge_type": "E4" + }, + { + "length": 14.4, + "psi_value": 0.16, + "psi_value_source": 2, + "thermal_bridge_type": "E5" + }, + { + "length": 25.8, + "psi_value": 0.07, + "psi_value_source": 2, + "thermal_bridge_type": "E6" + }, + { + "length": 10.4, + "psi_value": 0.06, + "psi_value_source": 2, + "thermal_bridge_type": "E10" + }, + { + "length": 5.4, + "psi_value": 0.06, + "psi_value_source": 3, + "thermal_bridge_type": "E14" + }, + { + "length": 5.8, + "psi_value": 0.09, + "psi_value_source": 2, + "thermal_bridge_type": "E16" + }, + { + "length": 5.8, + "psi_value": -0.09, + "psi_value_source": 2, + "thermal_bridge_type": "E17" + }, + { + "length": 34, + "psi_value": 0.06, + "psi_value_source": 2, + "thermal_bridge_type": "E18" + }, + { + "length": 20.6, + "psi_value": 0.12, + "psi_value_source": 3, + "thermal_bridge_type": "P1" + }, + { + "length": 20.6, + "psi_value": 0, + "psi_value_source": 4, + "thermal_bridge_type": "P2" + }, + { + "length": 20.6, + "psi_value": 0.12, + "psi_value_source": 3, + "thermal_bridge_type": "P4" + } + ], + "thermal_bridge_code": 5 + }, + "building_part_number": 1, + "sap_floor_dimensions": [ + { + "storey": 0, + "u_value": 0.12, + "floor_type": 2, + "kappa_value": 80, + "storey_height": 2.8, + "heat_loss_area": 57.4, + "total_floor_area": 57.4 + }, + { + "storey": 1, + "u_value": 0, + "floor_type": 3, + "kappa_value": 18, + "storey_height": 3, + "heat_loss_area": 0, + "total_floor_area": 57.4, + "kappa_value_from_below": 9 + }, + { + "storey": 2, + "u_value": 0, + "floor_type": 3, + "kappa_value": 19, + "storey_height": 2.7, + "heat_loss_area": 0, + "total_floor_area": 57.4, + "kappa_value_from_below": 9 + } + ], + "construction_age_band": "A" + } + ], + "user_interface_name": "BRE SAP interface 10.2", + "windows_overshading": 2, + "heating_cost_current": { + "value": 365.98, + "currency": "GBP" + }, + "co2_emissions_current": 2.4, + "energy_rating_average": 60, + "energy_rating_current": 72, + "lighting_cost_current": { + "value": 123.45, + "currency": "GBP" + }, + "main_heating_controls": [ + { + "description": "Programmer, room thermostat and TRVs", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "has_hot_water_cylinder": "true", + "heating_cost_potential": { + "value": 250.34, + "currency": "GBP" + }, + "hot_water_cost_current": { + "value": 200.4, + "currency": "GBP" + }, + "suggested_improvements": [ + { + "sequence": 1, + "typical_saving": { + "value": 88, + "currency": "GBP" + }, + "indicative_cost": "£4,000 - £6,000", + "improvement_type": "N", + "improvement_details": { + "improvement_number": 19 + }, + "improvement_category": 5, + "energy_performance_rating": 74, + "environmental_impact_rating": 94 + }, + { + "sequence": 2, + "typical_saving": { + "value": 88, + "currency": "GBP" + }, + "indicative_cost": "£9,000 - £14,000", + "improvement_type": "U", + "improvement_details": { + "improvement_number": 34 + }, + "improvement_category": 5, + "energy_performance_rating": 80, + "environmental_impact_rating": 96 + } + ], + "user_interface_version": "1.0.1-alpha", + "co2_emissions_potential": 1.4, + "energy_rating_potential": 72, + "gas_smart_meter_present": "false", + "lighting_cost_potential": { + "value": 84.23, + "currency": "GBP" + }, + "schema_version_original": "SAP-Schema-19.1.0", + "hot_water_cost_potential": { + "value": 180.43, + "currency": "GBP" + }, + "is_in_smoke_control_area": "unknown", + "renewable_heat_incentive": { + "rhi_new_dwelling": { + "space_heating": 2666, + "water_heating": 2650 + } + }, + "seller_commission_report": "Y", + "energy_consumption_current": 59, + "has_fixed_air_conditioning": "false", + "is_dwelling_export_capable": "true", + "multiple_glazed_percentage": 100, + "calculation_software_version": "13.05r16", + "energy_consumption_potential": 53, + "environmental_impact_current": 94, + "current_energy_efficiency_band": "C", + "environmental_impact_potential": 96, + "electricity_smart_meter_present": "false", + "has_heated_separate_conservatory": "false", + "potential_energy_efficiency_band": "C", + "co2_emissions_current_per_floor_area": 5.6 +} \ No newline at end of file diff --git a/backend/epc_api/json_samples/SAP-Schema-19.2.0/epc.json b/backend/epc_api/json_samples/SAP-Schema-19.2.0/epc.json new file mode 100644 index 00000000..26a1cd4d --- /dev/null +++ b/backend/epc_api/json_samples/SAP-Schema-19.2.0/epc.json @@ -0,0 +1,612 @@ +{ + "uprn": 12457, + "roofs": [ + { + "description": "Average thermal transmittance 0.13 W/m²K", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + } + ], + "walls": [ + { + "description": "Average thermal transmittance 0.18 W/m²K", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + } + ], + "floors": [ + { + "description": "Average thermal transmittance 0.12 W/m²K", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + } + ], + "status": "entered", + "tenure": 1, + "windows": { + "description": "High performance glazing", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + }, + "lighting": { + "description": "Low energy lighting in 91% of fixed outlets", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + }, + "postcode": "A0 0AA", + "data_type": 1, + "hot_water": { + "description": "From main system, waste water heat recovery", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 3 + }, + "post_town": "Whitbury", + "built_form": 4, + "living_area": 41.35, + "orientation": 0, + "region_code": 16, + "report_type": 3, + "sap_heating": { + "thermal_store": 1, + "shower_outlets": [ + { + "shower_wwhrs": 1, + "shower_flow_rate": 7, + "shower_outlet_type": 1 + }, + { + "shower_wwhrs": 2, + "shower_flow_rate": 7, + "shower_outlet_type": 1 + } + ], + "water_fuel_type": 39, + "water_heating_code": 901, + "instantaneous_wwhrs": { + "wwhrs_index_number1": 491123 + }, + "hot_water_store_size": 600, + "main_heating_details": [ + { + "has_fghrs": "false", + "main_fuel_type": 5, + "combi_boiler_type": 4, + "heat_emitter_type": 1, + "main_heating_code": 192, + "main_heating_number": 1, + "is_condensing_boiler": "false", + "main_heating_control": 2106, + "is_interlocked_system": "false", + "main_heating_category": 2, + "main_heating_fraction": 1, + "central_heating_pump_age": 2, + "main_heating_data_source": 3, + "has_separate_delayed_start": "true", + "is_oil_pump_in_heated_space": "false", + "is_main_heating_hetas_approved": "false", + "electric_cpsu_operating_temperature": 80, + "is_central_heating_pump_in_heated_space": "true" + } + ], + "has_hot_water_cylinder": "true", + "has_cylinder_thermostat": "true", + "hot_water_store_heat_loss": 2.45, + "has_fixed_air_conditioning": "false", + "secondary_heating_category": 1, + "is_immersion_for_summer_use": "false", + "is_hot_water_separately_timed": "false", + "sap_community_heating_systems": [ + { + "sub_network_name": "Test 1", + "heat_network_sleeved": 1, + "community_heating_use": 3, + "heat_network_existing": "true", + "heat_network_index_number": 496402, + "heat_network_assessed_as_new": "true", + "heat_network_community_heating": 0, + "community_heating_distribution_type": 5, + "community_heating_distribution_loss_factor": 1.26 + } + ], + "hot_water_store_heat_loss_source": 2, + "is_heat_pump_assisted_by_immersion": "false" + }, + "sap_version": 10.3, + "schema_type": "SAP-Schema-19.2.0", + "uprn_source": "Energy Assessor", + "country_code": "ENG", + "main_heating": [ + { + "description": "Boiler and radiators, electric", + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 2 + } + ], + "sap_lighting": [ + [ + { + "lighting_power": 60, + "lighting_outlets": 1, + "lighting_efficacy": 11.2 + }, + { + "lighting_power": 14, + "lighting_outlets": 10, + "lighting_efficacy": 66.9 + } + ] + ], + "terrain_type": 1, + "air_tightness": { + "description": "Air permeability 2.0 m³/h.m² (assumed)", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + }, + "dwelling_type": "Mid-terrace house", + "language_code": 1, + "property_type": 0, + "address_line_1": "1 Some Street", + "address_line_2": "Some Area", + "address_line_3": "Some County", + "assessment_date": "2022-05-09", + "assessment_type": "SAP", + "completion_date": "2022-05-09", + "inspection_date": "2022-05-09", + "sap_ventilation": { + "psv_count": 0, + "wall_type": 2, + "pressure_test": 2, + "wet_rooms_count": 2, + "air_permeability": 2, + "open_flues_count": 0, + "ventilation_type": 8, + "has_draught_lobby": "false", + "other_flues_count": 0, + "closed_flues_count": 0, + "extract_fans_count": 0, + "boilers_flues_count": 0, + "open_chimneys_count": 0, + "sheltered_sides_count": 2, + "blocked_chimneys_count": 0, + "flueless_gas_fires_count": 0, + "mechanical_vent_duct_type": 2, + "mechanical_vent_duct_placement": 1, + "mechanical_ventilation_data_source": 1, + "mechanical_vent_system_index_number": 500206, + "mechanical_vent_duct_insulation_level": 2, + "is_mechanical_vent_approved_installer_scheme": "true" + }, + "sap_data_version": 10.3, + "total_floor_area": 165, + "transaction_type": 1, + "cold_water_source": 1, + "conservatory_type": 1, + "registration_date": "2022-05-09", + "sap_energy_source": { + "wind_turbines": [ + { + "wind_turbine_hub_height": 3, + "wind_turbine_rotor_diameter": 1.7 + }, + { + "wind_turbine_hub_height": 3, + "wind_turbine_rotor_diameter": 1.7 + } + ], + "electricity_tariff": 5 + }, + "sap_opening_types": [ + { + "name": "Doors", + "type": 1, + "u_value": 1.5, + "data_source": 2, + "glazing_type": 1, + "isargonfilled": "false" + }, + { + "name": "Windows (1)", + "type": 4, + "u_value": 1.4, + "frame_type": 1, + "data_source": 3, + "glazing_gap": 3, + "frame_factor": 0.7, + "glazing_type": 11, + "isargonfilled": "true", + "solar_transmittance": 0.57 + }, + { + "name": "Windows (2)", + "type": 4, + "u_value": 1.5, + "frame_type": 1, + "data_source": 3, + "glazing_gap": 3, + "frame_factor": 0.7, + "glazing_type": 9, + "isargonfilled": "true", + "solar_transmittance": 0.64 + } + ], + "secondary_heating": { + "description": "None", + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + }, + "lowest_storey_area": 57.4, + "sap_building_parts": [ + { + "sap_roofs": [ + { + "name": "Roof (1)", + "u_value": 0.13, + "roof_type": 2, + "kappa_value": 9, + "total_roof_area": 57.4 + } + ], + "sap_walls": [ + { + "name": "Walls (1)", + "u_value": 0.18, + "wall_type": 2, + "kappa_value": 60, + "total_wall_area": 111.34, + "is_curtain_walling": "false" + }, + { + "name": "Party wall", + "u_value": 0, + "wall_type": 4, + "kappa_value": 0, + "total_wall_area": 167, + "is_curtain_walling": "false" + }, + { + "name": "Internal wall (1)", + "u_value": 0, + "wall_type": 5, + "kappa_value": 0, + "total_wall_area": 320, + "is_curtain_walling": "false" + } + ], + "identifier": "Main Dwelling", + "sap_openings": [ + { + "name": 1, + "type": "Doors", + "width": 1, + "height": 2.25, + "location": "Walls (1)", + "orientation": 3 + }, + { + "name": 2, + "type": "Doors", + "width": 0.8, + "height": 2.1, + "location": "Walls (1)", + "orientation": 3 + }, + { + "name": 3, + "type": "Windows (2)", + "width": 1.6, + "height": 2.7, + "location": "Walls (1)", + "orientation": 7 + }, + { + "name": 4, + "type": "Windows (1)", + "width": 1.1, + "height": 1.75, + "location": "Walls (1)", + "orientation": 3 + }, + { + "name": 5, + "type": "Windows (1)", + "width": 1.1, + "height": 1.75, + "location": "Walls (1)", + "orientation": 3 + }, + { + "name": 6, + "type": "Windows (1)", + "width": 1.1, + "height": 1.85, + "location": "Walls (1)", + "orientation": 3 + }, + { + "name": 7, + "type": "Windows (1)", + "width": 1.1, + "height": 1.85, + "location": "Walls (1)", + "orientation": 3 + }, + { + "name": 8, + "type": "Windows (1)", + "width": 1.1, + "height": 1.8, + "location": "Walls (1)", + "orientation": 3 + }, + { + "name": 9, + "type": "Windows (1)", + "width": 1.1, + "height": 1.75, + "location": "Walls (1)", + "orientation": 7 + }, + { + "name": 10, + "type": "Windows (1)", + "width": 1.1, + "height": 1.85, + "location": "Walls (1)", + "orientation": 7 + }, + { + "name": 11, + "type": "Windows (1)", + "width": 1.1, + "height": 1.85, + "location": "Walls (1)", + "orientation": 7 + }, + { + "name": 12, + "type": "Windows (1)", + "width": 0.6, + "height": 0.8, + "location": "Walls (1)", + "orientation": 7 + }, + { + "name": 13, + "type": "Windows (1)", + "width": 0.8, + "height": 1.25, + "location": "Walls (1)", + "orientation": 7 + }, + { + "name": 14, + "type": "Windows (1)", + "width": 0.8, + "height": 1.25, + "location": "Walls (1)", + "orientation": 7 + }, + { + "name": 15, + "type": "Windows (1)", + "width": 1.1, + "height": 1.25, + "location": "Walls (1)", + "orientation": 7 + } + ], + "sap_thermal_bridges": { + "thermal_bridges": [ + { + "length": 15.5, + "psi_value": 0.3, + "psi_value_source": 2, + "thermal_bridge_type": "E2" + }, + { + "length": 12.1, + "psi_value": 0.04, + "psi_value_source": 2, + "thermal_bridge_type": "E3" + }, + { + "length": 52.1, + "psi_value": 0.05, + "psi_value_source": 2, + "thermal_bridge_type": "E4" + }, + { + "length": 14.4, + "psi_value": 0.16, + "psi_value_source": 2, + "thermal_bridge_type": "E5" + }, + { + "length": 25.8, + "psi_value": 0.07, + "psi_value_source": 2, + "thermal_bridge_type": "E6" + }, + { + "length": 10.4, + "psi_value": 0.06, + "psi_value_source": 2, + "thermal_bridge_type": "E10" + }, + { + "length": 5.4, + "psi_value": 0.06, + "psi_value_source": 3, + "thermal_bridge_type": "E14" + }, + { + "length": 5.8, + "psi_value": 0.09, + "psi_value_source": 2, + "thermal_bridge_type": "E16" + }, + { + "length": 5.8, + "psi_value": -0.09, + "psi_value_source": 2, + "thermal_bridge_type": "E17" + }, + { + "length": 34, + "psi_value": 0.06, + "psi_value_source": 2, + "thermal_bridge_type": "E18" + }, + { + "length": 20.6, + "psi_value": 0.12, + "psi_value_source": 3, + "thermal_bridge_type": "P1" + }, + { + "length": 20.6, + "psi_value": 0, + "psi_value_source": 4, + "thermal_bridge_type": "P2" + }, + { + "length": 20.6, + "psi_value": 0.12, + "psi_value_source": 3, + "thermal_bridge_type": "P4" + } + ], + "thermal_bridge_code": 5 + }, + "building_part_number": 1, + "sap_floor_dimensions": [ + { + "storey": 0, + "u_value": 0.12, + "floor_type": 2, + "kappa_value": 80, + "storey_height": 2.8, + "heat_loss_area": 57.4, + "total_floor_area": 57.4 + }, + { + "storey": 1, + "u_value": 0, + "floor_type": 3, + "kappa_value": 18, + "storey_height": 3, + "heat_loss_area": 0, + "total_floor_area": 57.4, + "kappa_value_from_below": 9 + }, + { + "storey": 2, + "u_value": 0, + "floor_type": 3, + "kappa_value": 19, + "storey_height": 2.7, + "heat_loss_area": 0, + "total_floor_area": 57.4, + "kappa_value_from_below": 9 + } + ], + "construction_age_band": "A" + } + ], + "user_interface_name": "BRE SAP interface 10.3", + "windows_overshading": 2, + "heating_cost_current": { + "value": 365.98, + "currency": "GBP" + }, + "co2_emissions_current": 2.4, + "energy_rating_average": 60, + "energy_rating_current": 72, + "lighting_cost_current": { + "value": 123.45, + "currency": "GBP" + }, + "main_heating_controls": [ + { + "description": "Programmer, room thermostat and TRVs", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "has_hot_water_cylinder": "true", + "heating_cost_potential": { + "value": 250.34, + "currency": "GBP" + }, + "hot_water_cost_current": { + "value": 200.4, + "currency": "GBP" + }, + "suggested_improvements": [ + { + "sequence": 1, + "typical_saving": { + "value": 88, + "currency": "GBP" + }, + "indicative_cost": "£4,000 - £6,000", + "improvement_type": "N", + "improvement_details": { + "improvement_number": 19 + }, + "improvement_category": 5, + "energy_performance_rating": 74, + "environmental_impact_rating": 94 + }, + { + "sequence": 2, + "typical_saving": { + "value": 88, + "currency": "GBP" + }, + "indicative_cost": "£9,000 - £14,000", + "improvement_type": "U", + "improvement_details": { + "improvement_number": 34 + }, + "improvement_category": 5, + "energy_performance_rating": 80, + "environmental_impact_rating": 96 + } + ], + "user_interface_version": "1.0.1-alpha", + "co2_emissions_potential": 1.4, + "energy_rating_potential": 72, + "gas_smart_meter_present": "false", + "lighting_cost_potential": { + "value": 84.23, + "currency": "GBP" + }, + "part_o_cooling_required": 1, + "schema_version_original": "SAP-Schema-19.2.0", + "hot_water_cost_potential": { + "value": 180.43, + "currency": "GBP" + }, + "is_in_smoke_control_area": "unknown", + "renewable_heat_incentive": { + "rhi_new_dwelling": { + "space_heating": 2666, + "water_heating": 2650 + } + }, + "seller_commission_report": "Y", + "energy_consumption_current": 59, + "has_fixed_air_conditioning": "false", + "is_dwelling_export_capable": "true", + "multiple_glazed_percentage": 100, + "calculation_software_version": "13.05r16", + "energy_consumption_potential": 53, + "environmental_impact_current": 94, + "current_energy_efficiency_band": "C", + "environmental_impact_potential": 96, + "electricity_smart_meter_present": "false", + "has_heated_separate_conservatory": "false", + "potential_energy_efficiency_band": "C", + "co2_emissions_current_per_floor_area": 5.6 +} \ No newline at end of file diff --git a/etl/find_my_epc/RetrieveFindMyEpc.py b/etl/find_my_epc/RetrieveFindMyEpc.py index 392e6aaa..f5eb0166 100644 --- a/etl/find_my_epc/RetrieveFindMyEpc.py +++ b/etl/find_my_epc/RetrieveFindMyEpc.py @@ -472,8 +472,8 @@ class RetrieveFindMyEpc: address_response = requests.get(chosen_epc, headers=self.HEADERS) epc_page_source = address_response.text address_res = BeautifulSoup(address_response.text, features="html.parser") - elif self.rrn: - epc_certificate = self.rrn + elif self.rrn or rrn: + epc_certificate = self.rrn if self.rrn else rrn chosen_epc = f"{self.BASE_ENERGY_URL}/energy-certificate/{epc_certificate}" address_response = requests.get(chosen_epc, headers=self.HEADERS) epc_page_source = address_response.text @@ -497,7 +497,7 @@ class RetrieveFindMyEpc: current_sap = int(current_rating.split(' ')[-1]) if self.sap_rating: - if current_sap != self.sap_rating: + if current_sap != self.sap_rating and not rrn: # This means we likely have the wrong data. If we are in this scenario, we return nothing return { "epc_certificate": None, @@ -820,6 +820,10 @@ class RetrieveFindMyEpc: "recommendations": find_epc_data.get("recommendations", []), } + lodgment_date = find_epc_data.get("Date of certificate", None) + if not pd.isnull(lodgment_date): + lodgment_date = str(datetime.strptime(str(lodgment_date), "%d %B %Y")) + # We need to add the patch information patch = { "current-energy-rating": find_epc_data.get("current_epc_rating"), @@ -827,6 +831,7 @@ class RetrieveFindMyEpc: "potential-energy-rating": find_epc_data.get("potential_epc_rating"), "potential-energy-efficiency": find_epc_data.get("potential_epc_efficiency"), **find_epc_data.get("epc_data", {}), + "lodgement-date": lodgment_date } page_source = { From 4425b28d4fd15e8ab23fc20abcb25b1728dcc085 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 9 Apr 2026 14:24:52 +0000 Subject: [PATCH 27/47] address review comments and add logging --- etl/hubspot/hubspotDataTodB.py | 24 +++---- etl/hubspot/scripts/scraper/main.py | 99 ++++++++++++++++------------- 2 files changed, 64 insertions(+), 59 deletions(-) diff --git a/etl/hubspot/hubspotDataTodB.py b/etl/hubspot/hubspotDataTodB.py index 210c9593..65fad572 100644 --- a/etl/hubspot/hubspotDataTodB.py +++ b/etl/hubspot/hubspotDataTodB.py @@ -78,22 +78,6 @@ class HubspotDataToDb: .one_or_none() ) - def _parse_hs_date(self, value: Optional[str]) -> Optional[datetime]: - if not value: - return None - try: - return datetime.fromisoformat(value.replace("Z", "+00:00")) - except ValueError: - return None - - def _sha256(self, file_path: str) -> str: - """Compute SHA-256 checksum of a file.""" - sha256 = hashlib.sha256() - with open(file_path, "rb") as f: - for chunk in iter(lambda: f.read(8192), b""): - sha256.update(chunk) - return sha256.hexdigest() - def update_deal_with_checks( self, deal_in_db: HubspotDealData, @@ -185,6 +169,14 @@ class HubspotDataToDb: session.refresh(new_record) return new_record + def _sha256(self, file_path: str) -> str: + """Compute SHA-256 checksum of a file.""" + sha256 = hashlib.sha256() + with open(file_path, "rb") as f: + for chunk in iter(lambda: f.read(8192), b""): + sha256.update(chunk) + return sha256.hexdigest() + def _update_existing_deal( self, existing: HubspotDealData, diff --git a/etl/hubspot/scripts/scraper/main.py b/etl/hubspot/scripts/scraper/main.py index cec03da8..8fa71bf7 100644 --- a/etl/hubspot/scripts/scraper/main.py +++ b/etl/hubspot/scripts/scraper/main.py @@ -38,56 +38,69 @@ def handler(body: dict[str, Any], context: Any) -> None: hubspot_deal_id ) + deal_changed = False if not db_deal: # New hubspot deal, no diffing to do + logger.info(f"New HubSpot deal of ID {hubspot_deal_id}. Loading to database...") if company: company_data: CompanyData = hubspot_client.get_company_information(company) db_client: HubspotDataToDb = HubspotDataToDb() db_client.upsert_company(company_data) db_client.upsert_deal(hubspot_deal, company, listing, hubspot_client) - return - - deal_unchanged = True - - # Deal already in db, check whether anything has changed - if HubspotDealDiffer.check_for_db_update_trigger( - new_deal=hubspot_deal, - new_company=company, - new_listing=listing, - old_deal=db_deal, - ): - db_client.update_deal_with_checks( - deal_in_db=db_deal, - hubspot_client=hubspot_client, - hs_deal=hubspot_deal, - hs_company_id=company, - hs_listing=listing, - ) - deal_unchanged = False - - if deal_unchanged: - return - - # ============================== - # Orchestration of other lambdas - # ============================== - if HubspotDealDiffer.check_for_pashub_trigger( - new_deal=hubspot_deal, old_deal=db_deal - ): - message_body: Dict[str, Optional[str]] = { - "pashub_link": hubspot_deal["pashub_link"], - "address": None, # can we get this? - "sharepoint_link": hubspot_deal["sharepoint_link"], - "uprn": hubspot_deal["national_uprn"], - "landlord_property_id": hubspot_deal["owner_property_id"], - "deal_stage": hubspot_deal["deal_stage"], - } - - response = sqs_client.send_message( - QueueUrl=PASHUB_TRIGGER_QUEUE_URL, MessageBody=json.dumps(message_body) - ) - + else: + # Deal already in db, check whether anything has changed logger.info( - f"Sent message to Pashub To Ara queue. MessageId: {response['MessageId']}" + f"HubSpot deal {hubspot_deal_id} already in database. Checking for changes..." ) + if HubspotDealDiffer.check_for_db_update_trigger( + new_deal=hubspot_deal, + new_company=company, + new_listing=listing, + old_deal=db_deal, + ): + logger.info( + f"Deal {hubspot_deal_id} has been changed, updating database..." + ) + db_client.update_deal_with_checks( + deal_in_db=db_deal, + hubspot_client=hubspot_client, + hs_deal=hubspot_deal, + hs_company_id=company, + hs_listing=listing, + ) + deal_changed = True + + if not deal_changed: + logger.info(f"No changes to deal {hubspot_deal_id}") + return + + # ============================== + # Orchestration of other lambdas + # ============================== + if HubspotDealDiffer.check_for_pashub_trigger( + new_deal=hubspot_deal, old_deal=db_deal + ): + logger.info( + f"Triggering Pas Hub file fetcher for HubSpot deal ID {hubspot_deal_id}" + ) + message_body: Dict[str, Optional[str]] = { + "pashub_link": hubspot_deal["pashub_link"], + "address": None, # potentially available from Listing, leave as None for now + "sharepoint_link": hubspot_deal["sharepoint_link"], + "uprn": hubspot_deal["national_uprn"], + "landlord_property_id": hubspot_deal["owner_property_id"], + "deal_stage": hubspot_deal["deal_stage"], + } + + response = sqs_client.send_message( + QueueUrl=PASHUB_TRIGGER_QUEUE_URL, MessageBody=json.dumps(message_body) + ) + + logger.info( + f"Sent message to Pashub To Ara queue. MessageId: {response['MessageId']}" + ) + else: + logger.info( + f"Not Triggering PasHub file fetcher for HubSpot deal ID {hubspot_deal_id}" + ) From bd891a7a85365b4fca64ad95ebc7c4fb0ed4b82a Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 9 Apr 2026 14:41:14 +0000 Subject: [PATCH 28/47] address JTK review comments --- backend/app/db/models/hubspot_deal_data.py | 8 ++++---- etl/hubspot/hubspotDataTodB.py | 2 +- etl/hubspot/hubspot_deal_differ.py | 6 ++---- etl/hubspot/scripts/onboarding/new_organisation.py | 2 +- etl/hubspot/scripts/scraper/main.py | 2 +- 5 files changed, 9 insertions(+), 11 deletions(-) diff --git a/backend/app/db/models/hubspot_deal_data.py b/backend/app/db/models/hubspot_deal_data.py index 1d7607e0..758f688d 100644 --- a/backend/app/db/models/hubspot_deal_data.py +++ b/backend/app/db/models/hubspot_deal_data.py @@ -65,8 +65,8 @@ class HubspotDealData(SQLModel, table=True): server_default=text("(NOW() AT TIME ZONE 'utc')"), nullable=False, ), - default=None, # Nullable in db but optional here as value is set on db save for new record - ) + default=func.now(), + ) # Nullable in db but optional here as value is set on db save for new record updated_at: Optional[datetime] = Field( sa_column=Column( @@ -75,5 +75,5 @@ class HubspotDealData(SQLModel, table=True): onupdate=func.now(), nullable=False, ), - default=None, # Nullable in db but optional here as value is set on db save for new record - ) + default=func.now(), + ) # Nullable in db but optional here as value is set on db save for new record diff --git a/etl/hubspot/hubspotDataTodB.py b/etl/hubspot/hubspotDataTodB.py index 65fad572..a50c99da 100644 --- a/etl/hubspot/hubspotDataTodB.py +++ b/etl/hubspot/hubspotDataTodB.py @@ -31,7 +31,7 @@ class HubspotDataToDb: records = self.read_org_table(limit) return [org.name for org in records if org.name] - def upsert_company(self, company_data: CompanyData) -> Organisation: + def upsert_organisation(self, company_data: CompanyData) -> Organisation: """Upserts a company record. Updates if hubspot_company_id exists, otherwise creates new.""" with db_read_session() as session: hubspot_id = company_data.get("hs_object_id") diff --git a/etl/hubspot/hubspot_deal_differ.py b/etl/hubspot/hubspot_deal_differ.py index 42def3b2..4db303ab 100644 --- a/etl/hubspot/hubspot_deal_differ.py +++ b/etl/hubspot/hubspot_deal_differ.py @@ -8,6 +8,7 @@ class HubspotDealDiffer: COORDINATION_COMPLETE: List[str] = [ "v1 ioe/mtp complete", "v2 ioe/mtp complete", + "v3 ioe/mtp complete", ] RETROFIT_DESIGN_COMPLETE = "uploaded" LODGEMENT_COMPLETE: List[str] = ["lodgement complete", "measures lodged"] @@ -72,6 +73,7 @@ class HubspotDealDiffer: "lodgement_status": "lodgement_status", "design_type": "design_type", "surveyor": "surveyor", + "confirmed_survey_time": "confirmed_survey_time", } for hs_field, db_field in FIELD_MAP.items(): @@ -101,10 +103,6 @@ class HubspotDealDiffer: if old_value != new_value: return True - # --- Time field --- - if old_deal.confirmed_survey_time != new_deal.get("confirmed_survey_time"): - return True - # No differences found return False diff --git a/etl/hubspot/scripts/onboarding/new_organisation.py b/etl/hubspot/scripts/onboarding/new_organisation.py index f8c6ba7a..0785949a 100644 --- a/etl/hubspot/scripts/onboarding/new_organisation.py +++ b/etl/hubspot/scripts/onboarding/new_organisation.py @@ -22,7 +22,7 @@ companies_to_add_or_ensure_it_exists = [ for company in companies_to_add_or_ensure_it_exists: company_info: CompanyData = hubspot.get_company_information(company.value) - dbRead.upsert_company(company_info) + dbRead.upsert_organisation(company_info) dbRead = HubspotDataToDb() diff --git a/etl/hubspot/scripts/scraper/main.py b/etl/hubspot/scripts/scraper/main.py index 8fa71bf7..31945705 100644 --- a/etl/hubspot/scripts/scraper/main.py +++ b/etl/hubspot/scripts/scraper/main.py @@ -45,7 +45,7 @@ def handler(body: dict[str, Any], context: Any) -> None: if company: company_data: CompanyData = hubspot_client.get_company_information(company) db_client: HubspotDataToDb = HubspotDataToDb() - db_client.upsert_company(company_data) + db_client.upsert_organisation(company_data) db_client.upsert_deal(hubspot_deal, company, listing, hubspot_client) else: From 62fe46adc43d5c9ae98ad3b9de798bfaa1a82374 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 9 Apr 2026 15:00:36 +0000 Subject: [PATCH 29/47] get queue name from settings --- .github/workflows/deploy_terraform.yml | 2 +- backend/app/config.py | 1 + etl/hubspot/scripts/scraper/main.py | 3 ++- .../terraform/lambda/hubspot_deal_etl/main.tf | 10 ++++++++++ .../terraform/lambda/pashub_to_ara/outputs.tf | 4 ++++ 5 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 infrastructure/terraform/lambda/pashub_to_ara/outputs.tf diff --git a/.github/workflows/deploy_terraform.yml b/.github/workflows/deploy_terraform.yml index fccc6da4..22f16fee 100644 --- a/.github/workflows/deploy_terraform.yml +++ b/.github/workflows/deploy_terraform.yml @@ -505,7 +505,7 @@ jobs: # Deploy Hubspot ETL Lambda # ============================================================ hubspot_etl_lambda: - needs: [hubspot_etl_image, determine_stage] + needs: [hubspot_etl_image, determine_stage, pashub_to_ara_lambda] uses: ./.github/workflows/_deploy_lambda.yml with: lambda_name: hubspot-etl-to-ara diff --git a/backend/app/config.py b/backend/app/config.py index 80a2d46a..9532ddd6 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -38,6 +38,7 @@ class Settings(BaseSettings): PLAN_TRIGGER_BUCKET: str = "changeme" ENGINE_SQS_URL: str = "changeme" CATEGORISATION_SQS_URL: str = "changeme" + PASHUB_TO_ARA_SQS_URL: str = "changeme" # Third parties EPC_AUTH_TOKEN: str = "changeme" diff --git a/etl/hubspot/scripts/scraper/main.py b/etl/hubspot/scripts/scraper/main.py index 31945705..ea79bc18 100644 --- a/etl/hubspot/scripts/scraper/main.py +++ b/etl/hubspot/scripts/scraper/main.py @@ -2,6 +2,7 @@ import json import boto3 from typing import Any, Dict, Optional +from backend.app.config import get_settings from etl.hubspot.hubspotClient import HubspotClient from etl.hubspot.hubspotDataTodB import CompanyData, HubspotDataToDb from etl.hubspot.hubspot_deal_differ import HubspotDealDiffer @@ -21,7 +22,7 @@ def handler(body: dict[str, Any], context: Any) -> None: hubspot_client = HubspotClient() sqs_client = boto3.client("sqs") - PASHUB_TRIGGER_QUEUE_URL = "pashub_to_ara-queue-dev" # TODO: get from env var + PASHUB_TRIGGER_QUEUE_URL = get_settings().PASHUB_TO_ARA_SQS_URL payload = HubspotTriggerOrchestratorTriggerRequest.model_validate(body) hubspot_deal_id: str = payload.hubspot_deal_id diff --git a/infrastructure/terraform/lambda/hubspot_deal_etl/main.tf b/infrastructure/terraform/lambda/hubspot_deal_etl/main.tf index 6ce7a386..518e1e05 100644 --- a/infrastructure/terraform/lambda/hubspot_deal_etl/main.tf +++ b/infrastructure/terraform/lambda/hubspot_deal_etl/main.tf @@ -7,6 +7,14 @@ data "terraform_remote_state" "shared" { } } +data "terraform_remote_state" "pashub_to_ara" { + backend = "s3" + config = { + bucket = "pashub-to-ara-terraform-state" + key = "ev:/${var.stage}/terraform.tfstate" + region = "eu-west-2" + } +} data "aws_secretsmanager_secret_version" "db_credentials" { secret_id = "${var.stage}/assessment_model/db_credentials" @@ -39,6 +47,8 @@ module "hubspot_deal_etl" { DB_NAME = var.db_name DB_PORT = var.db_port HUBSPOT_API_KEY = var.hubspot_api_key + + PASHUB_TO_ARA_SQS_URL = data.terraform_remote_state.pashub_to_ara.pashhub_to_ara_queue_url } } diff --git a/infrastructure/terraform/lambda/pashub_to_ara/outputs.tf b/infrastructure/terraform/lambda/pashub_to_ara/outputs.tf new file mode 100644 index 00000000..738aa4fc --- /dev/null +++ b/infrastructure/terraform/lambda/pashub_to_ara/outputs.tf @@ -0,0 +1,4 @@ +output "pashhub_to_ara_queue_url" { + value = module.lambda.queue_url + description = "URL of the PasHub to Ara SQS queue" +} From 3380b6cbc880c248cf0851f500c46101aff35fd6 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Thu, 9 Apr 2026 16:01:33 +0100 Subject: [PATCH 30/47] hacky fix to pick up lmk key or epc cert # --- backend/addresses/Addresses.py | 3 +++ backend/app/db/functions/epc_functions.py | 2 +- etl/bill_savings/KwhData.py | 6 +++--- etl/find_my_epc/RetrieveFindMyEpc.py | 2 ++ 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/backend/addresses/Addresses.py b/backend/addresses/Addresses.py index 0496e95e..4c046554 100644 --- a/backend/addresses/Addresses.py +++ b/backend/addresses/Addresses.py @@ -138,7 +138,10 @@ class Addresses: # Handle NAN if pd.isnull(lmk_key): lmk_key = None + epc_certificate_number = row.get("certificate_number", None) + if pd.isnull(epc_certificate_number): + epc_certificate_number = None landlord_heating_system = row.get("epc_heating_type", None) if pd.isnull(landlord_heating_system): diff --git a/backend/app/db/functions/epc_functions.py b/backend/app/db/functions/epc_functions.py index 1dcb92fe..90b2bd02 100644 --- a/backend/app/db/functions/epc_functions.py +++ b/backend/app/db/functions/epc_functions.py @@ -11,7 +11,7 @@ class EpcStoreService: Service layer for EPC data lookup and persistence. """ - FRESHNESS_DAYS = 180 # Upgraded to 180 days + FRESHNESS_DAYS = 30 # Upgraded to 30 days # status labels FRESH = "fresh" diff --git a/etl/bill_savings/KwhData.py b/etl/bill_savings/KwhData.py index 266f4b72..30e11698 100644 --- a/etl/bill_savings/KwhData.py +++ b/etl/bill_savings/KwhData.py @@ -203,7 +203,7 @@ class KwhData: # TODO: New is a temporary parameter, which will transform the epc descriptions to their transformed features # in anticipation of the new model - data["lodgement-date"] = pd.to_datetime(data["lodgement-date"]) + data["lodgement-date"] = pd.to_datetime(data["lodgement-date"], format="mixed", errors="coerce") data["lodgement-year"] = data["lodgement-date"].dt.year data["lodgement-month"] = data["lodgement-date"].dt.month @@ -331,8 +331,8 @@ class KwhData: def prepare_epc(self, input_properties: list[Property]): scoring_data = pd.DataFrame([self._prepare_epc(p) for p in input_properties]) - scoring_data["lodgement-year"] = pd.to_datetime(scoring_data["lodgement-date"]).dt.year - scoring_data["lodgement-month"] = pd.to_datetime(scoring_data["lodgement-date"]).dt.month + scoring_data["lodgement-year"] = pd.to_datetime(scoring_data["lodgement-date"], format="mixed").dt.year + scoring_data["lodgement-month"] = pd.to_datetime(scoring_data["lodgement-date"], format="mixed").dt.month scoring_data["id"] = scoring_data["uprn"].copy() diff --git a/etl/find_my_epc/RetrieveFindMyEpc.py b/etl/find_my_epc/RetrieveFindMyEpc.py index f5eb0166..16b7d8b9 100644 --- a/etl/find_my_epc/RetrieveFindMyEpc.py +++ b/etl/find_my_epc/RetrieveFindMyEpc.py @@ -734,6 +734,8 @@ class RetrieveFindMyEpc: "Step 1:": [], "Step 2:": [], 'Step 3:': [], + 'Step 4:': [], + 'Step 5:': [], "Biomass stove with boiler": [], "Replace boiler with biomass boiler": [], "Heating controls (room thermostat and thermostatic radiator valves)": [ From fbf23bc898052c24f79df7c8dc0f55f20fad444e Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Thu, 9 Apr 2026 16:31:50 +0100 Subject: [PATCH 31/47] added env impact scores to db --- backend/Property.py | 1 + backend/app/db/models/portfolio.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/backend/Property.py b/backend/Property.py index 5e994cae..1d79b16d 100644 --- a/backend/Property.py +++ b/backend/Property.py @@ -890,6 +890,7 @@ class Property: "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, + "environment_impact_current": self.epc_record.environment_impact_current } return property_details_epc diff --git a/backend/app/db/models/portfolio.py b/backend/app/db/models/portfolio.py index c511b6c9..a4f9a675 100644 --- a/backend/app/db/models/portfolio.py +++ b/backend/app/db/models/portfolio.py @@ -263,6 +263,8 @@ class PropertyDetailsEpcModel(Base): lodged_heat_demand = Column(Float) has_been_remodelled = Column(Boolean, default=False) + environment_impact_current = Column(Float) + class PropertyDetailsSpatial(Base): __tablename__ = "property_details_spatial" From f1f3b84cbdadcecd4010658f0b119295a805e4ee Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 9 Apr 2026 15:49:13 +0000 Subject: [PATCH 32/47] simplify photo upload logic --- etl/hubspot/hubspotDataTodB.py | 89 +++++------------------------ etl/hubspot/scripts/scraper/main.py | 9 ++- 2 files changed, 19 insertions(+), 79 deletions(-) diff --git a/etl/hubspot/hubspotDataTodB.py b/etl/hubspot/hubspotDataTodB.py index a50c99da..6763f19c 100644 --- a/etl/hubspot/hubspotDataTodB.py +++ b/etl/hubspot/hubspotDataTodB.py @@ -78,53 +78,6 @@ class HubspotDataToDb: .one_or_none() ) - def update_deal_with_checks( - self, - deal_in_db: HubspotDealData, - hubspot_client: HubspotClient, - hs_deal: Dict[str, str], - hs_company_id: Optional[str], - hs_listing: Optional[Dict[str, str]], - ) -> bool: - """ - Updates deal in database and handles major_condition_issue_photos file upload to S3 with integrity check. - """ - self.upsert_deal(hs_deal, hs_company_id, hs_listing, hubspot_client) - - # Handle photo upload if it exists but S3 URL is missing - if self._needs_photo_upload(deal_in_db): - print( - f"🖼️ Found photo for deal_id {deal_in_db.deal_id} — uploading to S3..." - ) - - photo_url = hs_deal.get("major_condition_issue_photos") - - if photo_url: - self._upload_photo_to_s3( - deal_in_db, - photo_url, - hubspot_client, - verify=True, - ) - - # persist change - with db_read_session() as session: - db_record = session.get(HubspotDealData, deal_in_db.id) - db_record.major_condition_issue_evidence_s3_url = ( - deal_in_db.major_condition_issue_evidence_s3_url - ) - session.add(db_record) - session.commit() - - return False - else: - print(f"⚠️ Photo URL missing for deal_id {deal_in_db.deal_id}") - - else: - print(f"✅ No update or upload required for deal_id {deal_in_db.deal_id}.") - - return True - def upsert_deal( self, deal_data: Dict[str, str], @@ -169,14 +122,6 @@ class HubspotDataToDb: session.refresh(new_record) return new_record - def _sha256(self, file_path: str) -> str: - """Compute SHA-256 checksum of a file.""" - sha256 = hashlib.sha256() - with open(file_path, "rb") as f: - for chunk in iter(lambda: f.read(8192), b""): - sha256.update(chunk) - return sha256.hexdigest() - def _update_existing_deal( self, existing: HubspotDealData, @@ -315,18 +260,20 @@ class HubspotDataToDb: def _handle_existing_photo_upload( self, - existing: HubspotDealData, + existing_deal: HubspotDealData, hubspot_client: HubspotClient, ): - if self._needs_photo_upload(existing): - fresh_deal = hubspot_client.from_deal_id_get_info(existing.deal_id) - photo_url = fresh_deal.get("major_condition_issue_photos") + # if self._needs_photo_upload(existing): - if not photo_url: - print(f"⚠️ Photo URL missing for deal_id {existing.deal_id}") - return + fresh_deal = hubspot_client.from_deal_id_get_info(existing_deal.deal_id) + fresh_photo_url = fresh_deal.get("major_condition_issue_photos") - self._upload_photo_to_s3(existing, photo_url, hubspot_client) + if not fresh_photo_url: + print(f"⚠️ Photo URL missing for deal_id {existing_deal.deal_id}") + return + + if fresh_photo_url != existing_deal.major_condition_issue_photos: + self._upload_photo_to_s3(existing_deal, fresh_photo_url, hubspot_client) def _handle_new_photo_upload( self, @@ -343,12 +290,11 @@ class HubspotDataToDb: def _upload_photo_to_s3( self, record: HubspotDealData, - photo_url: str, + hubspot_photo_url: str, hubspot_client: HubspotClient, - verify: bool = False, ): try: - local_file = hubspot_client.download_file_from_url(photo_url) + local_file = hubspot_client.download_file_from_url(hubspot_photo_url) s3_url = self.s3.upload_file( local_file, @@ -356,11 +302,6 @@ class HubspotDataToDb: prefix="hubspot/awaabs_law_evidence/", ) - if verify: - downloaded = self.s3.download_from_url(s3_url) - if self._sha256(local_file) != self._sha256(downloaded): - raise ValueError("File integrity check failed after S3 upload.") - record.major_condition_issue_evidence_s3_url = s3_url except Exception as e: @@ -369,8 +310,8 @@ class HubspotDataToDb: if "local_file" in locals() and os.path.exists(local_file): os.remove(local_file) - def _needs_photo_upload(self, deal: HubspotDealData) -> bool: + def _needs_photo_upload(self, old_deal: HubspotDealData) -> bool: return bool( - deal.major_condition_issue_photos - and not deal.major_condition_issue_evidence_s3_url + old_deal.major_condition_issue_photos + and not old_deal.major_condition_issue_evidence_s3_url ) diff --git a/etl/hubspot/scripts/scraper/main.py b/etl/hubspot/scripts/scraper/main.py index ea79bc18..f41ef154 100644 --- a/etl/hubspot/scripts/scraper/main.py +++ b/etl/hubspot/scripts/scraper/main.py @@ -63,12 +63,11 @@ def handler(body: dict[str, Any], context: Any) -> None: logger.info( f"Deal {hubspot_deal_id} has been changed, updating database..." ) - db_client.update_deal_with_checks( - deal_in_db=db_deal, + db_client.upsert_deal( + deal_data=hubspot_deal, + company=company, + listing=listing, hubspot_client=hubspot_client, - hs_deal=hubspot_deal, - hs_company_id=company, - hs_listing=listing, ) deal_changed = True From a495c930a1bdc0510f7339890467cc0da050268f Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 9 Apr 2026 15:49:37 +0000 Subject: [PATCH 33/47] remove unused import --- etl/hubspot/hubspotDataTodB.py | 1 - 1 file changed, 1 deletion(-) diff --git a/etl/hubspot/hubspotDataTodB.py b/etl/hubspot/hubspotDataTodB.py index 6763f19c..c24d5813 100644 --- a/etl/hubspot/hubspotDataTodB.py +++ b/etl/hubspot/hubspotDataTodB.py @@ -1,4 +1,3 @@ -import hashlib import os from sqlmodel import select from datetime import datetime, timezone From 757c2241132a6796c5c75133bd5b8c14466340c5 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 9 Apr 2026 15:54:33 +0000 Subject: [PATCH 34/47] add image update logging --- etl/hubspot/hubspotDataTodB.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/etl/hubspot/hubspotDataTodB.py b/etl/hubspot/hubspotDataTodB.py index c24d5813..b7171290 100644 --- a/etl/hubspot/hubspotDataTodB.py +++ b/etl/hubspot/hubspotDataTodB.py @@ -10,6 +10,10 @@ from etl.hubspot.s3_uploader import S3Uploader from backend.app.db.connection import db_read_session from backend.app.db.models.organisation import Organisation from etl.hubspot.utils import parse_hs_date +from utils.logger import setup_logger + + +logger = setup_logger() class HubspotDataToDb: @@ -272,7 +276,12 @@ class HubspotDataToDb: return if fresh_photo_url != existing_deal.major_condition_issue_photos: + logger.info( + f"Hubspot image URL changed from {existing_deal.major_condition_issue_photos} to {fresh_photo_url}" + ) self._upload_photo_to_s3(existing_deal, fresh_photo_url, hubspot_client) + else: + logger.info(f"Hubspot iamge URL unchanged: {fresh_photo_url}") def _handle_new_photo_upload( self, From 2fb14858ebaafe5285e19e2623c956e703da5faa Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Thu, 9 Apr 2026 17:17:41 +0100 Subject: [PATCH 35/47] reduced freshness down to 14 days --- backend/app/db/functions/epc_functions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/app/db/functions/epc_functions.py b/backend/app/db/functions/epc_functions.py index 90b2bd02..f43c270b 100644 --- a/backend/app/db/functions/epc_functions.py +++ b/backend/app/db/functions/epc_functions.py @@ -11,7 +11,7 @@ class EpcStoreService: Service layer for EPC data lookup and persistence. """ - FRESHNESS_DAYS = 30 # Upgraded to 30 days + FRESHNESS_DAYS = 14 # Upgraded to 14 days # status labels FRESH = "fresh" @@ -22,7 +22,7 @@ class EpcStoreService: def get_epc_for_uprn(cls, session: Session, uprn: int): """ Query EPC data for a given UPRN and return a dict describing: - - epc_api: only if within last 30 days + - epc_api: only if within last 21 days - epc_page: only if epc_api exists - status: 'fresh', 'expired', or 'missing' """ From 3123723e8b284811d5459befdb277ed2b77e695a Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 9 Apr 2026 16:25:44 +0000 Subject: [PATCH 36/47] =?UTF-8?q?differ=20handles=20missing=20timezone=20f?= =?UTF-8?q?rom=20hubspot=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- etl/hubspot/hubspotDataTodB.py | 6 ++--- etl/hubspot/scripts/scraper/main.py | 7 +++++ etl/hubspot/tests/test_hubspot_deal_differ.py | 27 ++++++++++++++++++- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/etl/hubspot/hubspotDataTodB.py b/etl/hubspot/hubspotDataTodB.py index b7171290..9756833b 100644 --- a/etl/hubspot/hubspotDataTodB.py +++ b/etl/hubspot/hubspotDataTodB.py @@ -101,11 +101,11 @@ class HubspotDataToDb: existing = session.exec(statement).first() if existing: + self._handle_existing_photo_upload(existing, hubspot_client) + print(f"🔄 Updating existing deal (deal_id={deal_id})") self._update_existing_deal(existing, deal_data, listing, company) - self._handle_existing_photo_upload(existing, hubspot_client) - session.add(existing) session.commit() session.refresh(existing) @@ -281,7 +281,7 @@ class HubspotDataToDb: ) self._upload_photo_to_s3(existing_deal, fresh_photo_url, hubspot_client) else: - logger.info(f"Hubspot iamge URL unchanged: {fresh_photo_url}") + logger.info(f"Hubspot image URL unchanged: {fresh_photo_url}") def _handle_new_photo_upload( self, diff --git a/etl/hubspot/scripts/scraper/main.py b/etl/hubspot/scripts/scraper/main.py index f41ef154..d754cbb1 100644 --- a/etl/hubspot/scripts/scraper/main.py +++ b/etl/hubspot/scripts/scraper/main.py @@ -104,3 +104,10 @@ def handler(body: dict[str, Any], context: Any) -> None: logger.info( f"Not Triggering PasHub file fetcher for HubSpot deal ID {hubspot_deal_id}" ) + + print("done") + + +if __name__ == "__main__": + handler({"hubspot_deal_id": "371470706915"}, "") + print("beep") diff --git a/etl/hubspot/tests/test_hubspot_deal_differ.py b/etl/hubspot/tests/test_hubspot_deal_differ.py index 74d3f057..9f41a5e6 100644 --- a/etl/hubspot/tests/test_hubspot_deal_differ.py +++ b/etl/hubspot/tests/test_hubspot_deal_differ.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import datetime, timezone from typing import Any, Dict import uuid @@ -352,6 +352,31 @@ def test_db_update_trigger__company_changed__returns_true() -> None: assert result is True +def test_db_update_trigger__missing_hubspot_timezone__returns_false() -> None: + deal_id = uuid.uuid4() + + old_deal = make_old_deal( + id=deal_id, + design_completion_date=datetime(2025, 11, 3, 0, 0, tzinfo=timezone.utc), + ) + + new_deal = make_new_deal( + deal_id, + design_completion_date=datetime(2025, 11, 3, 0, 0), + ) + + new_company = "new_company" + + result = HubspotDealDiffer.check_for_db_update_trigger( + new_deal=new_deal, + new_company=new_company, + new_listing=None, + old_deal=old_deal, + ) + + assert result is False + + def test_db_update_trigger__listing_changed__returns_true() -> None: deal_id = uuid.uuid4() From 9852aa2809ad61667da39c2d612cadb79d55f9b2 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 9 Apr 2026 16:40:47 +0000 Subject: [PATCH 37/47] =?UTF-8?q?differ=20handles=20missing=20timezone=20f?= =?UTF-8?q?rom=20hubspot=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- etl/hubspot/hubspot_deal_differ.py | 4 ++++ etl/hubspot/tests/test_hubspot_deal_differ.py | 7 +++---- etl/hubspot/utils.py | 9 +++++++-- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/etl/hubspot/hubspot_deal_differ.py b/etl/hubspot/hubspot_deal_differ.py index 4db303ab..b95b544c 100644 --- a/etl/hubspot/hubspot_deal_differ.py +++ b/etl/hubspot/hubspot_deal_differ.py @@ -103,6 +103,10 @@ class HubspotDealDiffer: if old_value != new_value: return True + # --- Time field --- + if old_deal.confirmed_survey_time != new_deal.get("confirmed_survey_time"): + return True + # No differences found return False diff --git a/etl/hubspot/tests/test_hubspot_deal_differ.py b/etl/hubspot/tests/test_hubspot_deal_differ.py index 9f41a5e6..69f7668b 100644 --- a/etl/hubspot/tests/test_hubspot_deal_differ.py +++ b/etl/hubspot/tests/test_hubspot_deal_differ.py @@ -362,14 +362,13 @@ def test_db_update_trigger__missing_hubspot_timezone__returns_false() -> None: new_deal = make_new_deal( deal_id, - design_completion_date=datetime(2025, 11, 3, 0, 0), + hs_object_id="1", + design_completion_date=datetime(2025, 11, 3, 0, 0).isoformat(), ) - new_company = "new_company" - result = HubspotDealDiffer.check_for_db_update_trigger( new_deal=new_deal, - new_company=new_company, + new_company=None, new_listing=None, old_deal=old_deal, ) diff --git a/etl/hubspot/utils.py b/etl/hubspot/utils.py index 9fbeae62..b7331f94 100644 --- a/etl/hubspot/utils.py +++ b/etl/hubspot/utils.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import datetime, timezone from typing import Optional @@ -6,6 +6,11 @@ def parse_hs_date(value: Optional[str]) -> Optional[datetime]: if not value: return None try: - return datetime.fromisoformat(value.replace("Z", "+00:00")) + dt = datetime.fromisoformat(value.replace("Z", "+00:00")) + + if dt.tzinfo is None: + return dt.replace(tzinfo=timezone.utc) + + return dt.astimezone(timezone.utc) except ValueError: return None From 025b18a29cac7eb9ee00be225ed747fd2a86abd7 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Thu, 9 Apr 2026 20:51:04 +0100 Subject: [PATCH 38/47] hot fix --- backend/engine/engine.py | 4 ++++ etl/find_my_epc/RetrieveFindMyEpc.py | 6 ++++++ recommendations/HeatingRecommender.py | 6 ++++++ recommendations/LightingRecommendations.py | 3 +++ recommendations/SecondaryHeating.py | 4 +++- recommendations/WallRecommendations.py | 2 +- recommendations/WindowsRecommendations.py | 3 ++- 7 files changed, 25 insertions(+), 3 deletions(-) diff --git a/backend/engine/engine.py b/backend/engine/engine.py index 67c9dd4e..e24a3b95 100644 --- a/backend/engine/engine.py +++ b/backend/engine/engine.py @@ -724,6 +724,10 @@ async def model_engine(body: PlanTriggerRequest): # 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) + # Hack - skip + if "SAP05" in p.epc_record.walls_description: + continue + if needs_rebaselining: p.create_base_difference_epc_record(cleaned_lookup=cleaned) scoring_data = p.base_difference_record.df.copy() diff --git a/etl/find_my_epc/RetrieveFindMyEpc.py b/etl/find_my_epc/RetrieveFindMyEpc.py index 16b7d8b9..e6e4e5fd 100644 --- a/etl/find_my_epc/RetrieveFindMyEpc.py +++ b/etl/find_my_epc/RetrieveFindMyEpc.py @@ -778,6 +778,12 @@ class RetrieveFindMyEpc: 'Air or ground source heat pump': ["air_source_heat_pump"], "Add PV Battery": ["solar_pv_battery"], "Add PV diverter": ["solar_pv_diverter"], # Don't have a recommendation yet + "Draughtproof single-glazed windows": ["double_glazing"], + "Upgrade heating controls": ["roomstat_programmer_trvs", "time_temperature_zone_control"], + "Low energy lighting recommendation": ["low_energy_lighting"], + "Install cavity wall insulation": ["cavity_wall_insulation"], + "Install solar water heating": ["solar_water_heating"], + 'Install photovoltaics, 25% of roof area': ["solar_pv"], } survey = True diff --git a/recommendations/HeatingRecommender.py b/recommendations/HeatingRecommender.py index 5a7a0e03..6aa5c93a 100644 --- a/recommendations/HeatingRecommender.py +++ b/recommendations/HeatingRecommender.py @@ -140,6 +140,9 @@ class HeatingRecommender: # All heat systems are in here so we identify whether two of these are true # MainHeatAttributes.HEAT_SYSTEMS + if "sap05" in self.property.main_heating["clean_description"].lower(): + return False + n_trues = 0 for heat_system in MainHeatAttributes.HEAT_SYSTEMS: if self.property.main_heating[f"has_{heat_system.replace(' ', '_')}"]: @@ -318,6 +321,9 @@ class HeatingRecommender: :param measures: A list of measures for the recommendations """ + if "sap05" in self.property.main_heating["clean_description"].lower(): + return + measures = MEASURE_MAP["heating"] if measures is None else measures # if we have a non-invasive ashp recommendation, we get the configuration directly from the property instance diff --git a/recommendations/LightingRecommendations.py b/recommendations/LightingRecommendations.py index 61b1f66a..91db19c9 100644 --- a/recommendations/LightingRecommendations.py +++ b/recommendations/LightingRecommendations.py @@ -100,6 +100,9 @@ class LightingRecommendations: :return: """ + if "sap05" in self.property.lighting["clean_description"].lower(): + return + if self.property.lighting["low_energy_proportion"] >= 1: return diff --git a/recommendations/SecondaryHeating.py b/recommendations/SecondaryHeating.py index c2250e1e..5b7f4ff9 100644 --- a/recommendations/SecondaryHeating.py +++ b/recommendations/SecondaryHeating.py @@ -18,7 +18,9 @@ class SecondaryHeating: def recommend(self, phase: int): # Reset self.recommendation = [] - if self.property.epc_record.secondheat_description in ["None", None]: + if self.property.epc_record.secondheat_description in ["None", None] or ( + "sap05" in self.property.epc_record.secondheat_description.lower() + ): # No secondary heating system, so no recommendation to remove it return diff --git a/recommendations/WallRecommendations.py b/recommendations/WallRecommendations.py index a696e878..6baa85aa 100644 --- a/recommendations/WallRecommendations.py +++ b/recommendations/WallRecommendations.py @@ -169,7 +169,7 @@ class WallRecommendations(Definitions): if ( (insulation_thickness in ["average", "above average"]) or self.property.walls["is_filled_cavity"] - or self.property.walls["clean_description"] is None + or self.property.walls["clean_description"] in [None, "Sap05:walls"] ) and ("cavity_extract_and_refill" not in measures ): return diff --git a/recommendations/WindowsRecommendations.py b/recommendations/WindowsRecommendations.py index ff75e72d..54445eb1 100644 --- a/recommendations/WindowsRecommendations.py +++ b/recommendations/WindowsRecommendations.py @@ -45,7 +45,8 @@ class WindowsRecommendations: measures = MEASURE_MAP["windows"] if measures is None else measures # If we have no windows recs, leave - if not any(x in measures for x in MEASURE_MAP["windows"]): + if not any(x in measures for x in MEASURE_MAP["windows"]) or "sap05" in self.property.windows[ + "clean_description"].lower(): return if self.property.windows["glazing_type"] in ["triple", "high performance"]: From 06503cd989b472b99627ff7c1aca6760cf5a984f Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 10 Apr 2026 10:23:38 +0000 Subject: [PATCH 39/47] correct use of terraform state and fix typo --- infrastructure/terraform/lambda/hubspot_deal_etl/main.tf | 2 +- infrastructure/terraform/lambda/pashub_to_ara/outputs.tf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/infrastructure/terraform/lambda/hubspot_deal_etl/main.tf b/infrastructure/terraform/lambda/hubspot_deal_etl/main.tf index 518e1e05..516ec282 100644 --- a/infrastructure/terraform/lambda/hubspot_deal_etl/main.tf +++ b/infrastructure/terraform/lambda/hubspot_deal_etl/main.tf @@ -48,7 +48,7 @@ module "hubspot_deal_etl" { DB_PORT = var.db_port HUBSPOT_API_KEY = var.hubspot_api_key - PASHUB_TO_ARA_SQS_URL = data.terraform_remote_state.pashub_to_ara.pashhub_to_ara_queue_url + PASHUB_TO_ARA_SQS_URL = data.terraform_remote_state.pashub_to_ara.outputs.pashub_to_ara_queue_url } } diff --git a/infrastructure/terraform/lambda/pashub_to_ara/outputs.tf b/infrastructure/terraform/lambda/pashub_to_ara/outputs.tf index 738aa4fc..d44b8763 100644 --- a/infrastructure/terraform/lambda/pashub_to_ara/outputs.tf +++ b/infrastructure/terraform/lambda/pashub_to_ara/outputs.tf @@ -1,4 +1,4 @@ -output "pashhub_to_ara_queue_url" { +output "pashub_to_ara_queue_url" { value = module.lambda.queue_url description = "URL of the PasHub to Ara SQS queue" } From d95be6ce6510746a4db7b7e1cdd1f6fd42d86b3d Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 10 Apr 2026 11:33:17 +0000 Subject: [PATCH 40/47] first draft dataclasses with loading tests --- datatypes/epc/schema/__init__.py | 17 + datatypes/epc/schema/common.py | 22 + datatypes/epc/schema/rdsap_schema_17_0.py | 222 ++++++++++ datatypes/epc/schema/rdsap_schema_17_1.py | 234 +++++++++++ datatypes/epc/schema/rdsap_schema_18_0.py | 245 +++++++++++ datatypes/epc/schema/rdsap_schema_19_0.py | 251 ++++++++++++ datatypes/epc/schema/rdsap_schema_20_0_0.py | 282 +++++++++++++ datatypes/epc/schema/rdsap_schema_21_0_0.py | 339 ++++++++++++++++ datatypes/epc/schema/rdsap_schema_21_0_1.py | 340 ++++++++++++++++ datatypes/epc/schema/tests/__init__.py | 0 datatypes/epc/schema/tests/fixtures/17_0.json | 218 ++++++++++ datatypes/epc/schema/tests/fixtures/17_1.json | 243 +++++++++++ datatypes/epc/schema/tests/fixtures/18_0.json | 251 ++++++++++++ datatypes/epc/schema/tests/fixtures/19_0.json | 213 ++++++++++ .../epc/schema/tests/fixtures/20_0_0.json | 225 +++++++++++ .../epc/schema/tests/fixtures/21_0_0.json | 245 +++++++++++ .../epc/schema/tests/fixtures/21_0_1.json | 222 ++++++++++ datatypes/epc/schema/tests/helpers.py | 73 ++++ .../epc/schema/tests/test_schema_loading.py | 380 ++++++++++++++++++ pytest.ini | 2 +- 20 files changed, 4023 insertions(+), 1 deletion(-) create mode 100644 datatypes/epc/schema/__init__.py create mode 100644 datatypes/epc/schema/common.py create mode 100644 datatypes/epc/schema/rdsap_schema_17_0.py create mode 100644 datatypes/epc/schema/rdsap_schema_17_1.py create mode 100644 datatypes/epc/schema/rdsap_schema_18_0.py create mode 100644 datatypes/epc/schema/rdsap_schema_19_0.py create mode 100644 datatypes/epc/schema/rdsap_schema_20_0_0.py create mode 100644 datatypes/epc/schema/rdsap_schema_21_0_0.py create mode 100644 datatypes/epc/schema/rdsap_schema_21_0_1.py create mode 100644 datatypes/epc/schema/tests/__init__.py create mode 100644 datatypes/epc/schema/tests/fixtures/17_0.json create mode 100644 datatypes/epc/schema/tests/fixtures/17_1.json create mode 100644 datatypes/epc/schema/tests/fixtures/18_0.json create mode 100644 datatypes/epc/schema/tests/fixtures/19_0.json create mode 100644 datatypes/epc/schema/tests/fixtures/20_0_0.json create mode 100644 datatypes/epc/schema/tests/fixtures/21_0_0.json create mode 100644 datatypes/epc/schema/tests/fixtures/21_0_1.json create mode 100644 datatypes/epc/schema/tests/helpers.py create mode 100644 datatypes/epc/schema/tests/test_schema_loading.py diff --git a/datatypes/epc/schema/__init__.py b/datatypes/epc/schema/__init__.py new file mode 100644 index 00000000..e49f2317 --- /dev/null +++ b/datatypes/epc/schema/__init__.py @@ -0,0 +1,17 @@ +from .rdsap_schema_17_0 import RdSapSchema17_0 +from .rdsap_schema_17_1 import RdSapSchema17_1 +from .rdsap_schema_18_0 import RdSapSchema18_0 +from .rdsap_schema_19_0 import RdSapSchema19_0 +from .rdsap_schema_20_0_0 import RdSapSchema20_0_0 +from .rdsap_schema_21_0_0 import RdSapSchema21_0_0 +from .rdsap_schema_21_0_1 import RdSapSchema21_0_1 + +__all__ = [ + "RdSapSchema17_0", + "RdSapSchema17_1", + "RdSapSchema18_0", + "RdSapSchema19_0", + "RdSapSchema20_0_0", + "RdSapSchema21_0_0", + "RdSapSchema21_0_1", +] diff --git a/datatypes/epc/schema/common.py b/datatypes/epc/schema/common.py new file mode 100644 index 00000000..aa9ea512 --- /dev/null +++ b/datatypes/epc/schema/common.py @@ -0,0 +1,22 @@ +from dataclasses import dataclass + + +@dataclass +class Measurement: + """A numeric value with a physical unit, e.g. {"value": 2.4, "quantity": "metres"}.""" + value: float + quantity: str + + +@dataclass +class DescriptionV1: + """Localised description object used in schemas 17.x, 18.x, 19.x, and 21.0.1.""" + value: str + language: str + + +@dataclass +class CostAmount: + """Monetary amount used in schemas 17.x, 18.x, and 19.x.""" + value: int + currency: str diff --git a/datatypes/epc/schema/rdsap_schema_17_0.py b/datatypes/epc/schema/rdsap_schema_17_0.py new file mode 100644 index 00000000..22aaded4 --- /dev/null +++ b/datatypes/epc/schema/rdsap_schema_17_0.py @@ -0,0 +1,222 @@ +from dataclasses import dataclass +from typing import List, Optional, Union + +from .common import CostAmount, DescriptionV1, Measurement + + +@dataclass +class EnergyElement: + description: DescriptionV1 + energy_efficiency_rating: int + environmental_efficiency_rating: int + + +@dataclass +class InstantaneousWwhrs: + rooms_with_bath_and_or_shower: int + rooms_with_mixer_shower_no_bath: int + rooms_with_bath_and_mixer_shower: int + + +@dataclass +class MainHeatingDetail: + has_fghrs: str + main_fuel_type: int + heat_emitter_type: int + emitter_temperature: Union[int, str] + main_heating_number: int + main_heating_control: int + main_heating_category: int + main_heating_fraction: int + main_heating_data_source: int + sap_main_heating_code: Optional[int] = None + + +@dataclass +class SapHeating: + cylinder_size: int + water_heating_code: int + water_heating_fuel: int + instantaneous_wwhrs: InstantaneousWwhrs + main_heating_details: List[MainHeatingDetail] + immersion_heating_type: Union[int, str] + cylinder_insulation_type: int + has_fixed_air_conditioning: str + + +@dataclass +class PhotovoltaicSupplyNoneOrNoDetails: + pv_connection: int + percent_roof_area: int + + +@dataclass +class PhotovoltaicSupply: + none_or_no_details: PhotovoltaicSupplyNoneOrNoDetails + + +@dataclass +class SapEnergySource: + mains_gas: str + meter_type: int + photovoltaic_supply: PhotovoltaicSupply + wind_turbines_count: int + wind_turbines_terrain_type: int + + +@dataclass +class SapFloorDimension: + floor: int + room_height: Measurement + total_floor_area: Measurement + party_wall_length: Measurement + heat_loss_perimeter: Measurement + + +@dataclass +class SapBuildingPart: + identifier: str + wall_dry_lined: str + wall_thickness: int + floor_heat_loss: int + roof_construction: int + wall_construction: int + building_part_number: int + sap_floor_dimensions: List[SapFloorDimension] + wall_insulation_type: int + construction_age_band: str + party_wall_construction: Union[int, str] + wall_thickness_measured: str + roof_insulation_location: Union[int, str] + roof_insulation_thickness: str + wall_insulation_thickness: Optional[str] = None + + +@dataclass +class SapFlatDetails: + level: int + top_storey: str + flat_location: int + heat_loss_corridor: int + + +@dataclass +class ImprovementDetails: + improvement_number: int + + +@dataclass +class SuggestedImprovement: + sequence: int + typical_saving: CostAmount + indicative_cost: str + improvement_type: str + improvement_details: ImprovementDetails + improvement_category: int + energy_performance_rating: int + environmental_impact_rating: int + + +@dataclass +class AlternativeImprovement: + sequence: int + typical_saving: CostAmount + improvement_type: str + improvement_details: ImprovementDetails + improvement_category: int + energy_performance_rating: int + environmental_impact_rating: int + + +@dataclass +class RenewableHeatIncentive: + water_heating: int + space_heating_existing_dwelling: int + impact_of_loft_insulation: Optional[int] = None + impact_of_solid_wall_insulation: Optional[int] = None + + +@dataclass +class RdSapSchema17_0: + uprn: int + roofs: List[EnergyElement] + walls: List[EnergyElement] + floors: List[EnergyElement] + status: str + tenure: int + window: EnergyElement + lighting: EnergyElement + postcode: str + hot_water: EnergyElement + post_town: str + built_form: int + door_count: int + glazed_area: int + glazing_gap: str + region_code: int + report_type: int + sap_heating: SapHeating + sap_version: float + schema_type: str + uprn_source: str + country_code: str + main_heating: List[EnergyElement] + dwelling_type: DescriptionV1 + language_code: int + property_type: int + address_line_1: str + assessment_type: str + completion_date: str + inspection_date: str + extensions_count: int + measurement_type: int + total_floor_area: int + transaction_type: int + conservatory_type: int + heated_room_count: int + pvc_window_frames: str + registration_date: str + sap_energy_source: SapEnergySource + secondary_heating: EnergyElement + lzc_energy_sources: List[int] + sap_building_parts: List[SapBuildingPart] + low_energy_lighting: int + solar_water_heating: str + habitable_room_count: int + heating_cost_current: CostAmount + insulated_door_count: int + co2_emissions_current: float + energy_rating_average: int + energy_rating_current: int + lighting_cost_current: CostAmount + main_heating_controls: List[EnergyElement] + multiple_glazing_type: int + open_fireplaces_count: int + has_hot_water_cylinder: str + heating_cost_potential: CostAmount + hot_water_cost_current: CostAmount + mechanical_ventilation: int + percent_draughtproofed: int + suggested_improvements: List[SuggestedImprovement] + co2_emissions_potential: float + energy_rating_potential: int + lighting_cost_potential: CostAmount + schema_version_original: str + hot_water_cost_potential: CostAmount + renewable_heat_incentive: RenewableHeatIncentive + energy_consumption_current: int + has_fixed_air_conditioning: str + multiple_glazed_proportion: int + calculation_software_version: str + energy_consumption_potential: int + environmental_impact_current: int + fixed_lighting_outlets_count: int + current_energy_efficiency_band: str + environmental_impact_potential: int + has_heated_separate_conservatory: str + potential_energy_efficiency_band: str + co2_emissions_current_per_floor_area: int + low_energy_fixed_lighting_outlets_count: int + sap_flat_details: Optional[SapFlatDetails] = None + address_line_2: Optional[str] = None + alternative_improvements: Optional[List[AlternativeImprovement]] = None diff --git a/datatypes/epc/schema/rdsap_schema_17_1.py b/datatypes/epc/schema/rdsap_schema_17_1.py new file mode 100644 index 00000000..a4c007ed --- /dev/null +++ b/datatypes/epc/schema/rdsap_schema_17_1.py @@ -0,0 +1,234 @@ +from dataclasses import dataclass +from typing import List, Optional, Union + +from .common import CostAmount, DescriptionV1, Measurement + + +@dataclass +class EnergyElement: + description: DescriptionV1 + energy_efficiency_rating: int + environmental_efficiency_rating: int + + +@dataclass +class InstantaneousWwhrs: + rooms_with_bath_and_or_shower: int + rooms_with_mixer_shower_no_bath: int + rooms_with_bath_and_mixer_shower: int + + +@dataclass +class MainHeatingDetail: + has_fghrs: str + main_fuel_type: int + heat_emitter_type: int + emitter_temperature: Union[int, str] + main_heating_number: int + main_heating_control: int + main_heating_category: int + main_heating_fraction: int + main_heating_data_source: int + boiler_flue_type: Optional[int] = None + fan_flue_present: Optional[str] = None + mcs_installed_heat_pump: Optional[str] = None + main_heating_index_number: Optional[int] = None + sap_main_heating_code: Optional[int] = None + + +@dataclass +class SapHeating: + cylinder_size: int + water_heating_code: int + water_heating_fuel: int + instantaneous_wwhrs: InstantaneousWwhrs + main_heating_details: List[MainHeatingDetail] + immersion_heating_type: Union[int, str] + cylinder_insulation_type: int + has_fixed_air_conditioning: str + cylinder_thermostat: Optional[str] = None + secondary_fuel_type: Optional[int] = None + secondary_heating_type: Optional[int] = None + cylinder_insulation_thickness: Optional[int] = None + + +@dataclass +class PhotovoltaicSupplyNoneOrNoDetails: + pv_connection: int + percent_roof_area: int + + +@dataclass +class PhotovoltaicSupply: + none_or_no_details: PhotovoltaicSupplyNoneOrNoDetails + + +@dataclass +class SapEnergySource: + mains_gas: str + meter_type: int + photovoltaic_supply: PhotovoltaicSupply + wind_turbines_count: int + wind_turbines_terrain_type: int + + +@dataclass +class SapFloorDimension: + floor: int + room_height: Measurement + total_floor_area: Measurement + # Can be a Measurement object or 0 (int) for party walls of zero length + party_wall_length: Union[Measurement, int] + heat_loss_perimeter: Measurement + floor_insulation: Optional[int] = None + floor_construction: Optional[int] = None + + +@dataclass +class SapBuildingPart: + identifier: str + wall_dry_lined: str + wall_thickness: int + floor_heat_loss: int + roof_construction: int + wall_construction: int + building_part_number: int + sap_floor_dimensions: List[SapFloorDimension] + wall_insulation_type: int + construction_age_band: str + party_wall_construction: Union[int, str] + wall_thickness_measured: str + roof_insulation_location: Union[int, str] + # Can be a thickness string (e.g. "100mm") or 0 for uninsulated flat roofs + roof_insulation_thickness: Union[str, int] + wall_insulation_thickness: Optional[str] = None + + +@dataclass +class SapFlatDetails: + level: int + top_storey: str + flat_location: int + heat_loss_corridor: int + + +@dataclass +class ImprovementDetails: + improvement_number: int + + +@dataclass +class SuggestedImprovement: + sequence: int + typical_saving: CostAmount + indicative_cost: str + improvement_type: str + improvement_details: ImprovementDetails + improvement_category: int + energy_performance_rating: int + environmental_impact_rating: int + + +@dataclass +class AlternativeImprovement: + sequence: int + typical_saving: CostAmount + improvement_type: str + improvement_details: ImprovementDetails + improvement_category: int + energy_performance_rating: int + environmental_impact_rating: int + + +@dataclass +class RenewableHeatIncentive: + water_heating: int + space_heating_existing_dwelling: int + impact_of_loft_insulation: Optional[int] = None + impact_of_solid_wall_insulation: Optional[int] = None + + +@dataclass +class RdSapSchema17_1: + uprn: int + roofs: List[EnergyElement] + walls: List[EnergyElement] + floors: List[EnergyElement] + status: str + tenure: int + window: EnergyElement + lighting: EnergyElement + postcode: str + hot_water: EnergyElement + post_town: str + built_form: int + door_count: int + glazed_area: int + glazing_gap: str + region_code: int + report_type: int + sap_heating: SapHeating + sap_version: float + schema_type: str + uprn_source: str + country_code: str + main_heating: List[EnergyElement] + dwelling_type: DescriptionV1 + language_code: int + property_type: int + address_line_1: str + assessment_type: str + completion_date: str + inspection_date: str + extensions_count: int + measurement_type: int + total_floor_area: int + transaction_type: int + conservatory_type: int + heated_room_count: int + pvc_window_frames: str + registration_date: str + sap_energy_source: SapEnergySource + secondary_heating: EnergyElement + lzc_energy_sources: List[int] + sap_building_parts: List[SapBuildingPart] + low_energy_lighting: int + solar_water_heating: str + habitable_room_count: int + heating_cost_current: CostAmount + insulated_door_count: int + co2_emissions_current: float + energy_rating_average: int + energy_rating_current: int + lighting_cost_current: CostAmount + main_heating_controls: List[EnergyElement] + multiple_glazing_type: int + open_fireplaces_count: int + has_hot_water_cylinder: str + heating_cost_potential: CostAmount + hot_water_cost_current: CostAmount + mechanical_ventilation: int + percent_draughtproofed: int + suggested_improvements: List[SuggestedImprovement] + co2_emissions_potential: float + energy_rating_potential: int + lighting_cost_potential: CostAmount + schema_version_original: str + hot_water_cost_potential: CostAmount + renewable_heat_incentive: RenewableHeatIncentive + energy_consumption_current: int + has_fixed_air_conditioning: str + multiple_glazed_proportion: int + calculation_software_version: str + energy_consumption_potential: int + environmental_impact_current: int + fixed_lighting_outlets_count: int + current_energy_efficiency_band: str + environmental_impact_potential: int + has_heated_separate_conservatory: str + potential_energy_efficiency_band: str + co2_emissions_current_per_floor_area: int + low_energy_fixed_lighting_outlets_count: int + sap_flat_details: Optional[SapFlatDetails] = None + address_line_2: Optional[str] = None + alternative_improvements: Optional[List[AlternativeImprovement]] = None diff --git a/datatypes/epc/schema/rdsap_schema_18_0.py b/datatypes/epc/schema/rdsap_schema_18_0.py new file mode 100644 index 00000000..a038dc9b --- /dev/null +++ b/datatypes/epc/schema/rdsap_schema_18_0.py @@ -0,0 +1,245 @@ +from dataclasses import dataclass +from typing import List, Optional, Union + +from .common import CostAmount, DescriptionV1, Measurement + + +@dataclass +class EnergyElement: + description: DescriptionV1 + energy_efficiency_rating: int + environmental_efficiency_rating: int + + +@dataclass +class InstantaneousWwhrs: + rooms_with_bath_and_or_shower: int + rooms_with_mixer_shower_no_bath: int + rooms_with_bath_and_mixer_shower: int + + +@dataclass +class MainHeatingDetail: + has_fghrs: str + main_fuel_type: int + heat_emitter_type: int + emitter_temperature: Union[int, str] + main_heating_number: int + main_heating_control: int + main_heating_category: int + main_heating_fraction: int + main_heating_data_source: int + boiler_flue_type: Optional[int] = None + fan_flue_present: Optional[str] = None + central_heating_pump_age: Optional[int] = None + main_heating_index_number: Optional[int] = None + sap_main_heating_code: Optional[int] = None + + +@dataclass +class SapHeating: + cylinder_size: int + water_heating_code: int + water_heating_fuel: int + instantaneous_wwhrs: InstantaneousWwhrs + main_heating_details: List[MainHeatingDetail] + immersion_heating_type: Union[int, str] + has_fixed_air_conditioning: str + cylinder_insulation_type: Optional[int] = None + cylinder_thermostat: Optional[str] = None + secondary_fuel_type: Optional[int] = None + secondary_heating_type: Optional[int] = None + cylinder_insulation_thickness: Optional[int] = None + + +@dataclass +class PhotovoltaicSupplyNoneOrNoDetails: + pv_connection: int + percent_roof_area: int + + +@dataclass +class PhotovoltaicSupply: + none_or_no_details: PhotovoltaicSupplyNoneOrNoDetails + + +@dataclass +class SapEnergySource: + mains_gas: str + meter_type: int + photovoltaic_supply: PhotovoltaicSupply + wind_turbines_count: int + wind_turbines_terrain_type: int + + +@dataclass +class SapFloorDimension: + floor: int + room_height: Measurement + total_floor_area: Measurement + party_wall_length: Union[Measurement, int] + heat_loss_perimeter: Measurement + floor_insulation: Optional[int] = None + floor_construction: Optional[int] = None + + +@dataclass +class SapRoomInRoof: + """Room-in-roof details. floor_area is a Measurement object in schema 18.0.""" + floor_area: Measurement + insulation: str + roof_room_connected: str + construction_age_band: str + + +@dataclass +class SapBuildingPart: + identifier: str + wall_dry_lined: str + wall_thickness: int + floor_heat_loss: int + roof_construction: int + wall_construction: int + building_part_number: int + sap_floor_dimensions: List[SapFloorDimension] + wall_insulation_type: int + construction_age_band: str + party_wall_construction: Union[int, str] + wall_thickness_measured: str + roof_insulation_location: Union[int, str] + roof_insulation_thickness: Union[str, int] + sap_room_in_roof: Optional[SapRoomInRoof] = None + wall_insulation_thickness: Optional[str] = None + floor_insulation_thickness: Optional[str] = None + flat_roof_insulation_thickness: Optional[Union[str, int]] = None + + +@dataclass +class SapFlatDetails: + level: int + top_storey: str + flat_location: int + heat_loss_corridor: int + + +@dataclass +class ImprovementDetails: + improvement_number: int + + +@dataclass +class SuggestedImprovement: + sequence: int + typical_saving: CostAmount + indicative_cost: str + improvement_type: str + improvement_details: ImprovementDetails + improvement_category: int + energy_performance_rating: int + environmental_impact_rating: int + + +@dataclass +class AlternativeImprovement: + sequence: int + typical_saving: CostAmount + improvement_type: str + improvement_details: ImprovementDetails + improvement_category: int + energy_performance_rating: int + environmental_impact_rating: int + + +@dataclass +class RenewableHeatIncentive: + water_heating: int + space_heating_existing_dwelling: int + impact_of_loft_insulation: Optional[int] = None + impact_of_solid_wall_insulation: Optional[int] = None + + +@dataclass +class RdSapSchema18_0: + uprn: int + roofs: List[EnergyElement] + walls: List[EnergyElement] + floors: List[EnergyElement] + status: str + tenure: int + window: EnergyElement + lighting: EnergyElement + postcode: str + hot_water: EnergyElement + post_town: str + built_form: int + door_count: int + glazed_area: int + # glazing_gap is an integer in 18.0 (e.g. 12 mm), unlike 17.x where it was a string + glazing_gap: int + region_code: int + report_type: int + sap_heating: SapHeating + sap_version: float + schema_type: str + uprn_source: str + country_code: str + main_heating: List[EnergyElement] + dwelling_type: DescriptionV1 + language_code: int + property_type: int + address_line_1: str + assessment_type: str + completion_date: str + inspection_date: str + extensions_count: int + measurement_type: int + total_floor_area: int + transaction_type: int + conservatory_type: int + heated_room_count: int + pvc_window_frames: str + registration_date: str + sap_energy_source: SapEnergySource + secondary_heating: EnergyElement + lzc_energy_sources: List[int] + sap_building_parts: List[SapBuildingPart] + low_energy_lighting: int + solar_water_heating: str + habitable_room_count: int + heating_cost_current: CostAmount + insulated_door_count: int + co2_emissions_current: float + energy_rating_average: int + energy_rating_current: int + lighting_cost_current: CostAmount + main_heating_controls: List[EnergyElement] + multiple_glazing_type: int + open_fireplaces_count: int + has_hot_water_cylinder: str + heating_cost_potential: CostAmount + hot_water_cost_current: CostAmount + mechanical_ventilation: int + percent_draughtproofed: int + suggested_improvements: List[SuggestedImprovement] + co2_emissions_potential: float + energy_rating_potential: int + lighting_cost_potential: CostAmount + schema_version_original: str + hot_water_cost_potential: CostAmount + renewable_heat_incentive: RenewableHeatIncentive + energy_consumption_current: int + has_fixed_air_conditioning: str + multiple_glazed_proportion: int + calculation_software_version: str + energy_consumption_potential: int + environmental_impact_current: int + fixed_lighting_outlets_count: int + current_energy_efficiency_band: str + environmental_impact_potential: int + has_heated_separate_conservatory: str + potential_energy_efficiency_band: str + co2_emissions_current_per_floor_area: int + low_energy_fixed_lighting_outlets_count: int + sap_flat_details: Optional[SapFlatDetails] = None + address_line_2: Optional[str] = None + alternative_improvements: Optional[List[AlternativeImprovement]] = None diff --git a/datatypes/epc/schema/rdsap_schema_19_0.py b/datatypes/epc/schema/rdsap_schema_19_0.py new file mode 100644 index 00000000..b94d9bb3 --- /dev/null +++ b/datatypes/epc/schema/rdsap_schema_19_0.py @@ -0,0 +1,251 @@ +from dataclasses import dataclass +from typing import List, Optional, Union + +from .common import CostAmount, DescriptionV1, Measurement + + +@dataclass +class EnergyElement: + description: DescriptionV1 + energy_efficiency_rating: int + environmental_efficiency_rating: int + + +@dataclass +class InstantaneousWwhrs: + rooms_with_bath_and_or_shower: int + rooms_with_mixer_shower_no_bath: int + rooms_with_bath_and_mixer_shower: int + + +@dataclass +class MainHeatingDetail: + has_fghrs: str + main_fuel_type: int + heat_emitter_type: int + emitter_temperature: Union[int, str] + main_heating_number: int + main_heating_control: int + main_heating_category: int + main_heating_fraction: int + main_heating_data_source: int + boiler_flue_type: Optional[int] = None + fan_flue_present: Optional[str] = None + central_heating_pump_age: Optional[int] = None + main_heating_index_number: Optional[int] = None + sap_main_heating_code: Optional[int] = None + + +@dataclass +class SapHeating: + cylinder_size: int + water_heating_code: int + water_heating_fuel: int + instantaneous_wwhrs: InstantaneousWwhrs + main_heating_details: List[MainHeatingDetail] + immersion_heating_type: Union[int, str] + has_fixed_air_conditioning: str + cylinder_insulation_type: Optional[int] = None + cylinder_thermostat: Optional[str] = None + secondary_fuel_type: Optional[int] = None + secondary_heating_type: Optional[int] = None + cylinder_insulation_thickness: Optional[int] = None + + +@dataclass +class PhotovoltaicSupplyNoneOrNoDetails: + pv_connection: int + percent_roof_area: int + + +@dataclass +class PhotovoltaicSupply: + none_or_no_details: PhotovoltaicSupplyNoneOrNoDetails + + +@dataclass +class SapEnergySource: + mains_gas: str + meter_type: int + photovoltaic_supply: PhotovoltaicSupply + wind_turbines_count: int + wind_turbines_terrain_type: int + + +@dataclass +class SapFloorDimension: + floor: int + room_height: Measurement + total_floor_area: Measurement + party_wall_length: Union[Measurement, int] + heat_loss_perimeter: Measurement + floor_insulation: Optional[int] = None + floor_construction: Optional[int] = None + + +@dataclass +class SapRoomInRoof: + floor_area: Measurement + insulation: str + roof_room_connected: str + construction_age_band: str + + +@dataclass +class SapBuildingPart: + identifier: str + wall_dry_lined: str + wall_thickness: int + floor_heat_loss: int + roof_construction: int + wall_construction: int + building_part_number: int + sap_floor_dimensions: List[SapFloorDimension] + wall_insulation_type: int + construction_age_band: str + party_wall_construction: Union[int, str] + wall_thickness_measured: str + roof_insulation_location: Union[int, str] + roof_insulation_thickness: Union[str, int] + sap_room_in_roof: Optional[SapRoomInRoof] = None + wall_insulation_thickness: Optional[str] = None + floor_insulation_thickness: Optional[str] = None + flat_roof_insulation_thickness: Optional[Union[str, int]] = None + + +@dataclass +class SapFlatDetails: + level: int + top_storey: str + flat_location: int + heat_loss_corridor: int + + +@dataclass +class WindowsTransmissionDetails: + u_value: float + data_source: int + solar_transmittance: float + + +@dataclass +class ImprovementDetails: + improvement_number: int + + +@dataclass +class SuggestedImprovement: + sequence: int + typical_saving: CostAmount + indicative_cost: str + improvement_type: str + improvement_details: ImprovementDetails + improvement_category: int + energy_performance_rating: int + environmental_impact_rating: int + + +@dataclass +class AlternativeImprovement: + sequence: int + typical_saving: CostAmount + improvement_type: str + improvement_details: ImprovementDetails + improvement_category: int + energy_performance_rating: int + environmental_impact_rating: int + + +@dataclass +class RenewableHeatIncentive: + water_heating: int + space_heating_existing_dwelling: int + impact_of_loft_insulation: Optional[int] = None + impact_of_solid_wall_insulation: Optional[int] = None + + +@dataclass +class RdSapSchema19_0: + uprn: int + roofs: List[EnergyElement] + walls: List[EnergyElement] + floors: List[EnergyElement] + status: str + tenure: int + window: EnergyElement + lighting: EnergyElement + postcode: str + hot_water: EnergyElement + post_town: str + built_form: int + door_count: int + glazed_area: int + region_code: int + report_type: int + sap_heating: SapHeating + sap_version: float + schema_type: str + uprn_source: str + country_code: str + main_heating: List[EnergyElement] + dwelling_type: DescriptionV1 + language_code: int + property_type: int + address_line_1: str + assessment_type: str + completion_date: str + inspection_date: str + extensions_count: int + measurement_type: int + total_floor_area: int + transaction_type: int + conservatory_type: int + heated_room_count: int + pvc_window_frames: str + registration_date: str + sap_energy_source: SapEnergySource + secondary_heating: EnergyElement + lzc_energy_sources: List[int] + sap_building_parts: List[SapBuildingPart] + low_energy_lighting: int + solar_water_heating: str + habitable_room_count: int + heating_cost_current: CostAmount + insulated_door_count: int + co2_emissions_current: float + energy_rating_average: int + energy_rating_current: int + lighting_cost_current: CostAmount + main_heating_controls: List[EnergyElement] + multiple_glazing_type: int + open_fireplaces_count: int + has_hot_water_cylinder: str + heating_cost_potential: CostAmount + hot_water_cost_current: CostAmount + mechanical_ventilation: int + percent_draughtproofed: int + suggested_improvements: List[SuggestedImprovement] + co2_emissions_potential: float + energy_rating_potential: int + lighting_cost_potential: CostAmount + schema_version_original: str + hot_water_cost_potential: CostAmount + renewable_heat_incentive: RenewableHeatIncentive + windows_transmission_details: WindowsTransmissionDetails + energy_consumption_current: int + has_fixed_air_conditioning: str + multiple_glazed_proportion: int + calculation_software_version: str + energy_consumption_potential: int + environmental_impact_current: int + fixed_lighting_outlets_count: int + current_energy_efficiency_band: str + environmental_impact_potential: int + has_heated_separate_conservatory: str + potential_energy_efficiency_band: str + co2_emissions_current_per_floor_area: int + low_energy_fixed_lighting_outlets_count: int + sap_flat_details: Optional[SapFlatDetails] = None + address_line_2: Optional[str] = None + glazing_gap: Optional[Union[str, int]] = None + alternative_improvements: Optional[List[AlternativeImprovement]] = None diff --git a/datatypes/epc/schema/rdsap_schema_20_0_0.py b/datatypes/epc/schema/rdsap_schema_20_0_0.py new file mode 100644 index 00000000..8f3986a2 --- /dev/null +++ b/datatypes/epc/schema/rdsap_schema_20_0_0.py @@ -0,0 +1,282 @@ +from dataclasses import dataclass +from typing import List, Optional, Union + +from .common import Measurement + + +@dataclass +class EnergyElement: + # description is a plain string in schema 20.0.0 onwards (no longer a localised object) + description: str + energy_efficiency_rating: int + environmental_efficiency_rating: int + + +@dataclass +class Addendum: + addendum_numbers: List[int] + stone_walls: Optional[str] = None + system_build: Optional[str] = None + + +@dataclass +class InstantaneousWwhrs: + rooms_with_bath_and_or_shower: int + rooms_with_mixer_shower_no_bath: int + rooms_with_bath_and_mixer_shower: int + + +@dataclass +class MainHeatingDetail: + has_fghrs: str + main_fuel_type: int + heat_emitter_type: int + emitter_temperature: Union[int, str] + main_heating_number: int + main_heating_control: int + main_heating_category: int + main_heating_fraction: int + main_heating_data_source: int + boiler_flue_type: Optional[int] = None + fan_flue_present: Optional[str] = None + central_heating_pump_age: Optional[int] = None + main_heating_index_number: Optional[int] = None + sap_main_heating_code: Optional[int] = None + + +@dataclass +class SapHeating: + cylinder_size: int + water_heating_code: int + water_heating_fuel: int + instantaneous_wwhrs: InstantaneousWwhrs + main_heating_details: List[MainHeatingDetail] + immersion_heating_type: Union[int, str] + has_fixed_air_conditioning: str + cylinder_insulation_type: Optional[int] = None + cylinder_thermostat: Optional[str] = None + secondary_fuel_type: Optional[int] = None + secondary_heating_type: Optional[int] = None + cylinder_insulation_thickness: Optional[int] = None + + +@dataclass +class PhotovoltaicSupplyNoneOrNoDetails: + pv_connection: int + percent_roof_area: int + + +@dataclass +class PhotovoltaicSupply: + none_or_no_details: PhotovoltaicSupplyNoneOrNoDetails + + +@dataclass +class SapEnergySource: + mains_gas: str + meter_type: int + photovoltaic_supply: PhotovoltaicSupply + wind_turbines_count: int + wind_turbines_terrain_type: int + + +@dataclass +class SapWindow: + orientation: int + window_area: float + window_type: int + glazing_type: int + window_location: int + + +@dataclass +class SapFloorDimension: + floor: int + room_height: Measurement + total_floor_area: Measurement + party_wall_length: Union[Measurement, int] + heat_loss_perimeter: Measurement + floor_insulation: Optional[int] = None + floor_construction: Optional[int] = None + + +@dataclass +class SapRoomInRoof: + """Room-in-roof details. floor_area is a plain number in schema 20.0.0 (not a Measurement object).""" + floor_area: Union[int, float] + insulation: str + roof_room_connected: str + construction_age_band: str + + +@dataclass +class SapAlternativeWall: + wall_area: float + wall_dry_lined: str + wall_construction: int + wall_insulation_type: int + wall_thickness_measured: str + wall_insulation_thickness: Optional[str] = None + + +@dataclass +class SapBuildingPart: + identifier: str + wall_dry_lined: str + floor_heat_loss: int + roof_construction: int + wall_construction: int + building_part_number: int + sap_floor_dimensions: List[SapFloorDimension] + wall_insulation_type: int + construction_age_band: str + party_wall_construction: Union[int, str] + wall_thickness_measured: str + roof_insulation_location: Union[int, str] + roof_insulation_thickness: Union[str, int] + sap_room_in_roof: Optional[SapRoomInRoof] = None + wall_thickness: Optional[int] = None + wall_insulation_thickness: Optional[str] = None + floor_insulation_thickness: Optional[str] = None + flat_roof_insulation_thickness: Optional[Union[str, int]] = None + + +@dataclass +class SapFlatDetails: + level: int + top_storey: str + flat_location: int + heat_loss_corridor: int + storey_count: Optional[int] = None + unheated_corridor_length: Optional[int] = None + + +@dataclass +class WindowsTransmissionDetails: + u_value: float + data_source: int + solar_transmittance: float + + +@dataclass +class ImprovementTexts: + improvement_description: str + improvement_summary: Optional[str] = None + + +@dataclass +class ImprovementDetails: + improvement_number: Optional[int] = None + improvement_texts: Optional[ImprovementTexts] = None + + +@dataclass +class SuggestedImprovement: + sequence: int + # typical_saving is a plain number in schema 20.0.0 (not a CostAmount object) + typical_saving: float + # indicative_cost can be a formatted string (e.g. "£100 - £350") or a plain integer + indicative_cost: Union[str, int] + improvement_type: str + improvement_details: ImprovementDetails + improvement_category: int + energy_performance_rating: int + environmental_impact_rating: int + + +@dataclass +class RenewableHeatIncentive: + water_heating: int + space_heating_existing_dwelling: int + impact_of_loft_insulation: Optional[int] = None + impact_of_cavity_insulation: Optional[int] = None + impact_of_solid_wall_insulation: Optional[int] = None + + +@dataclass +class RdSapSchema20_0_0: + uprn: int + roofs: List[EnergyElement] + walls: List[EnergyElement] + floors: List[EnergyElement] + status: str + tenure: int + window: EnergyElement + lighting: EnergyElement + postcode: str + hot_water: EnergyElement + post_town: str + built_form: int + door_count: int + glazed_area: int + region_code: int + report_type: int + sap_heating: SapHeating + sap_version: float + sap_windows: List[SapWindow] + schema_type: str + uprn_source: str + country_code: str + main_heating: List[EnergyElement] + # dwelling_type is a plain string in schema 20.0.0 onwards + dwelling_type: str + language_code: int + property_type: int + address_line_1: str + assessment_type: str + completion_date: str + inspection_date: str + extensions_count: int + measurement_type: int + total_floor_area: int + transaction_type: int + conservatory_type: int + heated_room_count: int + registration_date: str + sap_energy_source: SapEnergySource + secondary_heating: EnergyElement + lzc_energy_sources: List[int] + sap_building_parts: List[SapBuildingPart] + low_energy_lighting: int + solar_water_heating: str + habitable_room_count: int + heating_cost_current: float + insulated_door_count: int + co2_emissions_current: float + energy_rating_average: int + energy_rating_current: int + lighting_cost_current: float + main_heating_controls: List[EnergyElement] + multiple_glazing_type: int + open_fireplaces_count: int + heating_cost_potential: float + hot_water_cost_current: float + insulated_door_u_value: float + mechanical_ventilation: int + percent_draughtproofed: int + suggested_improvements: List[SuggestedImprovement] + co2_emissions_potential: float + energy_rating_potential: int + lighting_cost_potential: float + schema_version_original: str + hot_water_cost_potential: float + renewable_heat_incentive: RenewableHeatIncentive + windows_transmission_details: WindowsTransmissionDetails + energy_consumption_current: int + multiple_glazed_proportion: int + calculation_software_version: str + energy_consumption_potential: int + environmental_impact_current: int + fixed_lighting_outlets_count: int + multiple_glazed_proportion_nr: Optional[str] + current_energy_efficiency_band: str + environmental_impact_potential: int + potential_energy_efficiency_band: str + co2_emissions_current_per_floor_area: int + low_energy_fixed_lighting_outlets_count: int + sap_flat_details: Optional[SapFlatDetails] = None + addendum: Optional[Addendum] = None + address_line_2: Optional[str] = None + has_hot_water_cylinder: Optional[str] = None + has_fixed_air_conditioning: Optional[str] = None + has_heated_separate_conservatory: Optional[str] = None diff --git a/datatypes/epc/schema/rdsap_schema_21_0_0.py b/datatypes/epc/schema/rdsap_schema_21_0_0.py new file mode 100644 index 00000000..eee00cb8 --- /dev/null +++ b/datatypes/epc/schema/rdsap_schema_21_0_0.py @@ -0,0 +1,339 @@ +from dataclasses import dataclass +from typing import List, Optional, Union + +from .common import Measurement + + +@dataclass +class EnergyElement: + # description is a plain string in schema 21.0.0 (no longer a localised object) + description: str + energy_efficiency_rating: int + environmental_efficiency_rating: int + + +@dataclass +class Addendum: + addendum_numbers: List[int] + stone_walls: Optional[str] = None + system_build: Optional[str] = None + + +@dataclass +class ShowerOutlet: + shower_wwhrs: int + shower_outlet_type: int + + +@dataclass +class ShowerOutlets: + shower_outlet: ShowerOutlet + + +@dataclass +class InstantaneousWwhrs: + """Changed in 21.0.0: references WWHRS product index numbers instead of room counts.""" + wwhrs_index_number1: Optional[int] = None + wwhrs_index_number2: Optional[int] = None + + +@dataclass +class MainHeatingDetail: + has_fghrs: str + main_fuel_type: int + heat_emitter_type: int + emitter_temperature: Union[int, str] + main_heating_number: int + main_heating_control: int + main_heating_category: int + main_heating_fraction: int + main_heating_data_source: int + boiler_flue_type: Optional[int] = None + fan_flue_present: Optional[str] = None + boiler_ignition_type: Optional[int] = None + central_heating_pump_age: Optional[int] = None + main_heating_index_number: Optional[int] = None + sap_main_heating_code: Optional[int] = None + + +@dataclass +class SapHeating: + cylinder_size: int + water_heating_code: int + water_heating_fuel: int + instantaneous_wwhrs: InstantaneousWwhrs + main_heating_details: List[MainHeatingDetail] + immersion_heating_type: Union[int, str] + has_fixed_air_conditioning: str + shower_outlets: Optional[ShowerOutlets] = None + cylinder_insulation_type: Optional[int] = None + cylinder_thermostat: Optional[str] = None + secondary_fuel_type: Optional[int] = None + secondary_heating_type: Optional[int] = None + cylinder_insulation_thickness: Optional[int] = None + + +@dataclass +class PvBattery: + battery_capacity: float + + +@dataclass +class PvBatteries: + pv_battery: PvBattery + + +@dataclass +class WindTurbineDetails: + hub_height: float + rotor_diameter: float + + +@dataclass +class PhotovoltaicSupplyNoneOrNoDetails: + percent_roof_area: int + + +@dataclass +class PhotovoltaicSupply: + none_or_no_details: PhotovoltaicSupplyNoneOrNoDetails + + +@dataclass +class SapEnergySource: + mains_gas: str + meter_type: int + pv_connection: int + pv_battery_count: int + photovoltaic_supply: PhotovoltaicSupply + wind_turbines_count: int + wind_turbine_details: WindTurbineDetails + gas_smart_meter_present: str + is_dwelling_export_capable: str + wind_turbines_terrain_type: int + electricity_smart_meter_present: str + pv_batteries: Optional[PvBatteries] = None + + +@dataclass +class WindowTransmissionDetails: + u_value: float + data_source: int + solar_transmittance: float + + +@dataclass +class SapWindow: + pvc_frame: str + glazing_gap: int + orientation: int + window_type: int + frame_factor: float + glazing_type: int + window_width: float + window_height: float + draught_proofed: str + window_location: int + window_wall_type: int + permanent_shutters_present: str + window_transmission_details: WindowTransmissionDetails + permanent_shutters_insulated: str + + +@dataclass +class SapFloorDimension: + floor: int + room_height: Measurement + total_floor_area: Measurement + party_wall_length: Union[Measurement, int] + heat_loss_perimeter: Measurement + floor_insulation: Optional[int] = None + floor_construction: Optional[int] = None + + +@dataclass +class SapRoomInRoof: + """Room-in-roof details. insulation and roof_room_connected removed in schema 21.0.0.""" + floor_area: Union[int, float] + construction_age_band: str + + +@dataclass +class SapAlternativeWall: + wall_area: float + wall_dry_lined: str + wall_construction: int + wall_insulation_type: int + wall_thickness_measured: str + wall_insulation_thickness: Optional[str] = None + + +@dataclass +class SapBuildingPart: + identifier: str + wall_dry_lined: str + floor_heat_loss: int + roof_construction: int + wall_construction: int + building_part_number: int + sap_floor_dimensions: List[SapFloorDimension] + wall_insulation_type: int + construction_age_band: str + party_wall_construction: Union[int, str] + wall_thickness_measured: str + roof_insulation_location: Union[int, str] + roof_insulation_thickness: Union[str, int] + sap_room_in_roof: Optional[SapRoomInRoof] = None + sap_alternative_wall_1: Optional[SapAlternativeWall] = None + sap_alternative_wall_2: Optional[SapAlternativeWall] = None + wall_thickness: Optional[int] = None + wall_insulation_thickness: Optional[str] = None + floor_insulation_thickness: Optional[str] = None + flat_roof_insulation_thickness: Optional[Union[str, int]] = None + + +@dataclass +class SapFlatDetails: + level: int + top_storey: str + flat_location: int + heat_loss_corridor: int + storey_count: Optional[int] = None + unheated_corridor_length: Optional[int] = None + + +@dataclass +class WindowsTransmissionDetails: + u_value: float + data_source: int + solar_transmittance: float + + +@dataclass +class ImprovementTexts: + improvement_description: str + improvement_summary: Optional[str] = None + + +@dataclass +class ImprovementDetails: + improvement_number: Optional[int] = None + improvement_texts: Optional[ImprovementTexts] = None + + +@dataclass +class SuggestedImprovement: + sequence: int + typical_saving: float + indicative_cost: Union[str, int] + improvement_type: str + improvement_details: ImprovementDetails + improvement_category: int + energy_performance_rating: int + environmental_impact_rating: int + + +@dataclass +class RenewableHeatIncentive: + water_heating: int + space_heating_existing_dwelling: int + impact_of_loft_insulation: Optional[int] = None + impact_of_cavity_insulation: Optional[int] = None + impact_of_solid_wall_insulation: Optional[int] = None + + +@dataclass +class RdSapSchema21_0_0: + uprn: int + roofs: List[EnergyElement] + walls: List[EnergyElement] + floors: List[EnergyElement] + status: str + tenure: int + window: EnergyElement + lighting: EnergyElement + postcode: str + hot_water: EnergyElement + post_town: str + built_form: int + door_count: int + region_code: int + report_type: int + sap_heating: SapHeating + sap_version: float + sap_windows: List[SapWindow] + schema_type: str + uprn_source: str + country_code: str + main_heating: List[EnergyElement] + dwelling_type: str + language_code: int + pressure_test: int + property_type: int + address_line_1: str + assessment_type: str + completion_date: str + inspection_date: str + wet_rooms_count: int + extensions_count: int + measurement_type: int + total_floor_area: int + transaction_type: int + conservatory_type: int + heated_room_count: int + registration_date: str + sap_energy_source: SapEnergySource + secondary_heating: EnergyElement + sap_building_parts: List[SapBuildingPart] + open_chimneys_count: int + solar_water_heating: str + habitable_room_count: int + heating_cost_current: float + insulated_door_count: int + co2_emissions_current: float + energy_rating_average: int + energy_rating_current: int + lighting_cost_current: float + main_heating_controls: List[EnergyElement] + has_hot_water_cylinder: str + heating_cost_potential: float + hot_water_cost_current: float + insulated_door_u_value: float + mechanical_ventilation: int + percent_draughtproofed: int + suggested_improvements: List[SuggestedImprovement] + co2_emissions_potential: float + energy_rating_potential: int + lighting_cost_potential: float + schema_version_original: str + hot_water_cost_potential: float + renewable_heat_incentive: RenewableHeatIncentive + draughtproofed_door_count: int + mechanical_vent_duct_type: int + windows_transmission_details: WindowsTransmissionDetails + cfl_fixed_lighting_bulbs_count: int + energy_consumption_current: int + has_fixed_air_conditioning: str + multiple_glazed_proportion: int + calculation_software_version: str + energy_consumption_potential: int + environmental_impact_current: int + led_fixed_lighting_bulbs_count: int + mechanical_vent_duct_placement: int + mechanical_vent_duct_insulation: int + potential_energy_efficiency_band: str + pressure_test_certificate_number: int + mechanical_ventilation_index_number: int + co2_emissions_current_per_floor_area: int + current_energy_efficiency_band: str + environmental_impact_potential: int + low_energy_fixed_lighting_bulbs_count: int + mechanical_vent_duct_insulation_level: int + mechanical_vent_measured_installation: str + incandescent_fixed_lighting_bulbs_count: int + sap_flat_details: Optional[SapFlatDetails] = None + addendum: Optional[Addendum] = None + address_line_2: Optional[str] = None + has_heated_separate_conservatory: Optional[str] = None + fixed_lighting_outlets_count: Optional[int] = None + low_energy_fixed_lighting_outlets_count: Optional[int] = None diff --git a/datatypes/epc/schema/rdsap_schema_21_0_1.py b/datatypes/epc/schema/rdsap_schema_21_0_1.py new file mode 100644 index 00000000..046e4fec --- /dev/null +++ b/datatypes/epc/schema/rdsap_schema_21_0_1.py @@ -0,0 +1,340 @@ +from dataclasses import dataclass +from typing import List, Optional, Union + +from .common import DescriptionV1, Measurement + + +@dataclass +class EnergyElement: + # Descriptions revert to localised objects in schema 21.0.1 (were plain strings in 21.0.0) + description: DescriptionV1 + energy_efficiency_rating: int + environmental_efficiency_rating: int + + +@dataclass +class Addendum: + addendum_numbers: List[int] + stone_walls: Optional[str] = None + system_build: Optional[str] = None + + +@dataclass +class ShowerOutlet: + shower_wwhrs: int + shower_outlet_type: int + + +@dataclass +class ShowerOutlets: + shower_outlet: ShowerOutlet + + +@dataclass +class InstantaneousWwhrs: + """References WWHRS product index numbers (introduced in 21.0.0).""" + wwhrs_index_number1: Optional[int] = None + wwhrs_index_number2: Optional[int] = None + + +@dataclass +class MainHeatingDetail: + has_fghrs: str + main_fuel_type: int + heat_emitter_type: int + emitter_temperature: Union[int, str] + main_heating_number: int + main_heating_control: int + main_heating_category: int + main_heating_fraction: int + main_heating_data_source: int + boiler_flue_type: Optional[int] = None + fan_flue_present: Optional[str] = None + boiler_ignition_type: Optional[int] = None + central_heating_pump_age: Optional[int] = None + main_heating_index_number: Optional[int] = None + sap_main_heating_code: Optional[int] = None + + +@dataclass +class SapHeating: + cylinder_size: int + water_heating_code: int + water_heating_fuel: int + instantaneous_wwhrs: InstantaneousWwhrs + main_heating_details: List[MainHeatingDetail] + immersion_heating_type: Union[int, str] + has_fixed_air_conditioning: str + shower_outlets: Optional[ShowerOutlets] = None + cylinder_insulation_type: Optional[int] = None + cylinder_thermostat: Optional[str] = None + secondary_fuel_type: Optional[int] = None + secondary_heating_type: Optional[int] = None + cylinder_insulation_thickness: Optional[int] = None + + +@dataclass +class PvBattery: + battery_capacity: float + + +@dataclass +class PvBatteries: + pv_battery: PvBattery + + +@dataclass +class WindTurbineDetails: + hub_height: float + rotor_diameter: float + + +@dataclass +class PhotovoltaicSupplyNoneOrNoDetails: + percent_roof_area: int + + +@dataclass +class PhotovoltaicSupply: + none_or_no_details: PhotovoltaicSupplyNoneOrNoDetails + + +@dataclass +class SapEnergySource: + mains_gas: str + meter_type: int + pv_connection: int + pv_battery_count: int + photovoltaic_supply: PhotovoltaicSupply + wind_turbines_count: int + wind_turbine_details: WindTurbineDetails + gas_smart_meter_present: str + is_dwelling_export_capable: str + wind_turbines_terrain_type: int + electricity_smart_meter_present: str + pv_batteries: Optional[PvBatteries] = None + + +@dataclass +class WindowTransmissionDetails: + u_value: float + data_source: int + solar_transmittance: float + + +@dataclass +class SapWindow: + pvc_frame: str + glazing_gap: int + orientation: int + window_type: int + frame_factor: float + glazing_type: int + window_width: float + window_height: float + draught_proofed: str + window_location: int + window_wall_type: int + permanent_shutters_present: str + window_transmission_details: WindowTransmissionDetails + permanent_shutters_insulated: str + + +@dataclass +class SapFloorDimension: + floor: int + room_height: Measurement + total_floor_area: Measurement + party_wall_length: Union[Measurement, int] + heat_loss_perimeter: Measurement + floor_insulation: Optional[int] = None + floor_construction: Optional[int] = None + + +@dataclass +class SapRoomInRoof: + floor_area: Union[int, float] + construction_age_band: str + + +@dataclass +class SapAlternativeWall: + wall_area: float + wall_dry_lined: str + wall_construction: int + wall_insulation_type: int + wall_thickness_measured: str + wall_insulation_thickness: Optional[str] = None + + +@dataclass +class SapBuildingPart: + identifier: str + wall_dry_lined: str + floor_heat_loss: int + roof_construction: int + wall_construction: int + building_part_number: int + sap_floor_dimensions: List[SapFloorDimension] + wall_insulation_type: int + construction_age_band: str + party_wall_construction: Union[int, str] + wall_thickness_measured: str + roof_insulation_location: Union[int, str] + roof_insulation_thickness: Union[str, int] + sap_room_in_roof: Optional[SapRoomInRoof] = None + sap_alternative_wall_1: Optional[SapAlternativeWall] = None + sap_alternative_wall_2: Optional[SapAlternativeWall] = None + wall_thickness: Optional[int] = None + wall_insulation_thickness: Optional[str] = None + floor_insulation_thickness: Optional[str] = None + flat_roof_insulation_thickness: Optional[Union[str, int]] = None + + +@dataclass +class SapFlatDetails: + level: int + top_storey: str + flat_location: int + heat_loss_corridor: int + storey_count: Optional[int] = None + # Changed from plain int in 21.0.0 to a Measurement object in 21.0.1 + unheated_corridor_length: Optional[Union[Measurement, int]] = None + + +@dataclass +class WindowsTransmissionDetails: + u_value: float + data_source: int + solar_transmittance: float + + +@dataclass +class ImprovementTexts: + improvement_description: str + improvement_summary: Optional[str] = None + + +@dataclass +class ImprovementDetails: + improvement_number: Optional[int] = None + improvement_texts: Optional[ImprovementTexts] = None + + +@dataclass +class SuggestedImprovement: + sequence: int + typical_saving: float + indicative_cost: Union[str, int] + improvement_type: str + improvement_details: ImprovementDetails + improvement_category: int + energy_performance_rating: int + environmental_impact_rating: int + + +@dataclass +class RenewableHeatIncentive: + water_heating: int + space_heating_existing_dwelling: int + impact_of_loft_insulation: Optional[int] = None + impact_of_cavity_insulation: Optional[int] = None + impact_of_solid_wall_insulation: Optional[int] = None + + +@dataclass +class RdSapSchema21_0_1: + uprn: int + roofs: List[EnergyElement] + walls: List[EnergyElement] + floors: List[EnergyElement] + status: str + tenure: int + window: EnergyElement + lighting: EnergyElement + postcode: str + hot_water: EnergyElement + post_town: str + built_form: int + door_count: int + region_code: int + report_type: int + sap_heating: SapHeating + sap_version: float + sap_windows: List[SapWindow] + schema_type: str + uprn_source: str + country_code: str + main_heating: List[EnergyElement] + # dwelling_type remains a plain string (not reverted to DescriptionV1) in 21.0.1 + dwelling_type: str + language_code: int + pressure_test: int + property_type: int + address_line_1: str + assessment_type: str + completion_date: str + inspection_date: str + wet_rooms_count: int + extensions_count: int + measurement_type: int + total_floor_area: int + transaction_type: int + conservatory_type: int + heated_room_count: int + registration_date: str + sap_energy_source: SapEnergySource + secondary_heating: EnergyElement + sap_building_parts: List[SapBuildingPart] + open_chimneys_count: int + solar_water_heating: str + habitable_room_count: int + heating_cost_current: float + insulated_door_count: int + co2_emissions_current: float + energy_rating_average: int + energy_rating_current: int + lighting_cost_current: float + main_heating_controls: List[EnergyElement] + has_hot_water_cylinder: str + heating_cost_potential: float + hot_water_cost_current: float + insulated_door_u_value: float + mechanical_ventilation: int + percent_draughtproofed: int + suggested_improvements: List[SuggestedImprovement] + co2_emissions_potential: float + energy_rating_potential: int + lighting_cost_potential: float + schema_version_original: str + hot_water_cost_potential: float + renewable_heat_incentive: RenewableHeatIncentive + draughtproofed_door_count: int + mechanical_vent_duct_type: int + windows_transmission_details: WindowsTransmissionDetails + cfl_fixed_lighting_bulbs_count: int + energy_consumption_current: int + has_fixed_air_conditioning: str + multiple_glazed_proportion: int + calculation_software_version: str + energy_consumption_potential: int + environmental_impact_current: int + led_fixed_lighting_bulbs_count: int + mechanical_vent_duct_placement: int + mechanical_vent_duct_insulation: int + potential_energy_efficiency_band: str + pressure_test_certificate_number: int + mechanical_ventilation_index_number: int + co2_emissions_current_per_floor_area: int + current_energy_efficiency_band: str + environmental_impact_potential: int + low_energy_fixed_lighting_bulbs_count: int + mechanical_vent_duct_insulation_level: int + mechanical_vent_measured_installation: str + incandescent_fixed_lighting_bulbs_count: int + sap_flat_details: Optional[SapFlatDetails] = None + addendum: Optional[Addendum] = None + address_line_2: Optional[str] = None + has_heated_separate_conservatory: Optional[str] = None + fixed_lighting_outlets_count: Optional[int] = None + low_energy_fixed_lighting_outlets_count: Optional[int] = None diff --git a/datatypes/epc/schema/tests/__init__.py b/datatypes/epc/schema/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/datatypes/epc/schema/tests/fixtures/17_0.json b/datatypes/epc/schema/tests/fixtures/17_0.json new file mode 100644 index 00000000..b17ca7f3 --- /dev/null +++ b/datatypes/epc/schema/tests/fixtures/17_0.json @@ -0,0 +1,218 @@ +{ + "uprn": 12457, + "roofs": [ + { + "description": {"value": "(another dwelling above)", "language": "1"}, + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + } + ], + "walls": [ + { + "description": {"value": "System built, with internal insulation", "language": "1"}, + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "floors": [ + { + "description": {"value": "(another dwelling below)", "language": "1"}, + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + } + ], + "status": "entered", + "tenure": 2, + "window": { + "description": {"value": "Fully double glazed", "language": "1"}, + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 3 + }, + "lighting": { + "description": {"value": "Low energy lighting in 57% of fixed outlets", "language": "1"}, + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + }, + "postcode": "PT5 4RZ", + "hot_water": { + "description": {"value": "Electric immersion, off-peak", "language": "1"}, + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 2 + }, + "post_town": "POSTTOWN", + "built_form": 2, + "door_count": 2, + "glazed_area": 1, + "glazing_gap": "16+", + "region_code": 3, + "report_type": 2, + "sap_heating": { + "cylinder_size": 2, + "water_heating_code": 903, + "water_heating_fuel": 29, + "instantaneous_wwhrs": { + "rooms_with_bath_and_or_shower": 1, + "rooms_with_mixer_shower_no_bath": 0, + "rooms_with_bath_and_mixer_shower": 0 + }, + "main_heating_details": [ + { + "has_fghrs": "N", + "main_fuel_type": 29, + "heat_emitter_type": 0, + "emitter_temperature": "NA", + "main_heating_number": 1, + "main_heating_control": 2401, + "main_heating_category": 7, + "main_heating_fraction": 1, + "sap_main_heating_code": 402, + "main_heating_data_source": 2 + } + ], + "immersion_heating_type": 1, + "cylinder_insulation_type": 0, + "has_fixed_air_conditioning": "false" + }, + "sap_version": 9.92, + "schema_type": "RdSAP-Schema-17.0", + "uprn_source": "Energy Assessor", + "country_code": "EAW", + "main_heating": [ + { + "description": {"value": "Electric storage heaters", "language": "1"}, + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 1 + } + ], + "dwelling_type": {"value": "Mid-floor flat", "language": "1"}, + "language_code": 1, + "property_type": 2, + "address_line_1": "42, Moria Mines Lane", + "assessment_type": "RdSAP", + "completion_date": "2016-01-12", + "inspection_date": "2016-01-12", + "extensions_count": 0, + "measurement_type": 1, + "sap_flat_details": { + "level": 2, + "top_storey": "N", + "flat_location": 1, + "heat_loss_corridor": 0 + }, + "total_floor_area": 55, + "transaction_type": 8, + "conservatory_type": 1, + "heated_room_count": 1, + "pvc_window_frames": "true", + "registration_date": "2016-01-12", + "sap_energy_source": { + "mains_gas": "N", + "meter_type": 1, + "photovoltaic_supply": { + "none_or_no_details": {"pv_connection": 0, "percent_roof_area": 0} + }, + "wind_turbines_count": 0, + "wind_turbines_terrain_type": 2 + }, + "secondary_heating": { + "description": {"value": "Portable electric heaters (assumed)", "language": "1"}, + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + }, + "lzc_energy_sources": [11], + "sap_building_parts": [ + { + "identifier": "Main Dwelling", + "wall_dry_lined": "N", + "wall_thickness": 240, + "floor_heat_loss": 6, + "roof_construction": 3, + "wall_construction": 8, + "building_part_number": 1, + "sap_floor_dimensions": [ + { + "floor": 0, + "room_height": {"value": 2.4, "quantity": "metres"}, + "total_floor_area": {"value": 54.6, "quantity": "square metres"}, + "party_wall_length": {"value": 7.3, "quantity": "metres"}, + "heat_loss_perimeter": {"value": 23.3, "quantity": "metres"} + } + ], + "wall_insulation_type": 3, + "construction_age_band": "D", + "party_wall_construction": 0, + "wall_thickness_measured": "Y", + "roof_insulation_location": "ND", + "roof_insulation_thickness": "ND", + "wall_insulation_thickness": "50mm" + } + ], + "low_energy_lighting": 57, + "solar_water_heating": "N", + "habitable_room_count": 3, + "heating_cost_current": {"value": 214, "currency": "GBP"}, + "insulated_door_count": 0, + "co2_emissions_current": 3.9, + "energy_rating_average": 60, + "energy_rating_current": 66, + "lighting_cost_current": {"value": 61, "currency": "GBP"}, + "main_heating_controls": [ + { + "description": {"value": "Manual charge control", "language": "1"}, + "energy_efficiency_rating": 2, + "environmental_efficiency_rating": 2 + } + ], + "multiple_glazing_type": 3, + "open_fireplaces_count": 0, + "has_hot_water_cylinder": "true", + "heating_cost_potential": {"value": 216, "currency": "GBP"}, + "hot_water_cost_current": {"value": 396, "currency": "GBP"}, + "mechanical_ventilation": 0, + "percent_draughtproofed": 100, + "suggested_improvements": [ + { + "sequence": 1, + "typical_saving": {"value": 158, "currency": "GBP"}, + "indicative_cost": "£15 - £30", + "improvement_type": "C", + "improvement_details": {"improvement_number": 1}, + "improvement_category": 5, + "energy_performance_rating": 74, + "environmental_impact_rating": 60 + } + ], + "co2_emissions_potential": 2.5, + "energy_rating_potential": 79, + "lighting_cost_potential": {"value": 42, "currency": "GBP"}, + "schema_version_original": "LIG-17.0", + "alternative_improvements": [ + { + "sequence": 1, + "typical_saving": {"value": 141, "currency": "GBP"}, + "improvement_type": "J2", + "improvement_details": {"improvement_number": 54}, + "improvement_category": 6, + "energy_performance_rating": 81, + "environmental_impact_rating": 96 + } + ], + "hot_water_cost_potential": {"value": 154, "currency": "GBP"}, + "renewable_heat_incentive": { + "water_heating": 4818, + "space_heating_existing_dwelling": 2415 + }, + "energy_consumption_current": 427, + "has_fixed_air_conditioning": "false", + "multiple_glazed_proportion": 100, + "calculation_software_version": "9.0.0", + "energy_consumption_potential": 267, + "environmental_impact_current": 48, + "fixed_lighting_outlets_count": 7, + "current_energy_efficiency_band": "D", + "environmental_impact_potential": 66, + "has_heated_separate_conservatory": "false", + "potential_energy_efficiency_band": "C", + "co2_emissions_current_per_floor_area": 72, + "low_energy_fixed_lighting_outlets_count": 4 +} diff --git a/datatypes/epc/schema/tests/fixtures/17_1.json b/datatypes/epc/schema/tests/fixtures/17_1.json new file mode 100644 index 00000000..ef0613d1 --- /dev/null +++ b/datatypes/epc/schema/tests/fixtures/17_1.json @@ -0,0 +1,243 @@ +{ + "uprn": 12457, + "roofs": [ + { + "description": {"value": "Pitched, 100 mm loft insulation", "language": "1"}, + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 3 + }, + { + "description": {"value": "Pitched, insulated (assumed)", "language": "1"}, + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "walls": [ + { + "description": {"value": "Cavity wall, filled cavity", "language": "1"}, + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "floors": [ + { + "description": {"value": "Solid, no insulation (assumed)", "language": "1"}, + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + } + ], + "status": "entered", + "tenure": 1, + "window": { + "description": {"value": "Fully double glazed", "language": "1"}, + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 3 + }, + "lighting": { + "description": {"value": "Low energy lighting in 23% of fixed outlets", "language": "1"}, + "energy_efficiency_rating": 2, + "environmental_efficiency_rating": 2 + }, + "postcode": "PT42 5HL", + "hot_water": { + "description": {"value": "From main system", "language": "1"}, + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 3 + }, + "post_town": "POSTTOWN", + "built_form": 1, + "door_count": 4, + "glazed_area": 1, + "glazing_gap": "16+", + "region_code": 17, + "report_type": 2, + "sap_heating": { + "cylinder_size": 2, + "water_heating_code": 901, + "water_heating_fuel": 28, + "cylinder_thermostat": "Y", + "instantaneous_wwhrs": { + "rooms_with_bath_and_or_shower": 1, + "rooms_with_mixer_shower_no_bath": 0, + "rooms_with_bath_and_mixer_shower": 1 + }, + "secondary_fuel_type": 29, + "main_heating_details": [ + { + "has_fghrs": "N", + "main_fuel_type": 28, + "boiler_flue_type": 1, + "fan_flue_present": "N", + "heat_emitter_type": 1, + "emitter_temperature": "NA", + "main_heating_number": 1, + "main_heating_control": 2104, + "main_heating_category": 2, + "main_heating_fraction": 1, + "mcs_installed_heat_pump": "false", + "main_heating_data_source": 1, + "main_heating_index_number": 9049 + } + ], + "immersion_heating_type": "NA", + "secondary_heating_type": 691, + "cylinder_insulation_type": 1, + "has_fixed_air_conditioning": "false", + "cylinder_insulation_thickness": 12 + }, + "sap_version": 9.92, + "schema_type": "RdSAP-Schema-17.1", + "uprn_source": "Energy Assessor", + "country_code": "EAW", + "main_heating": [ + { + "description": {"value": "Boiler and radiators, oil", "language": "1"}, + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 3 + } + ], + "dwelling_type": {"value": "Detached house", "language": "1"}, + "language_code": 1, + "property_type": 0, + "address_line_1": "15, Hedge Lane", + "address_line_2": "Lower Moria", + "assessment_type": "RdSAP", + "completion_date": "2018-05-29", + "inspection_date": "2018-05-29", + "extensions_count": 1, + "measurement_type": 2, + "total_floor_area": 101, + "transaction_type": 1, + "conservatory_type": 1, + "heated_room_count": 7, + "pvc_window_frames": "true", + "registration_date": "2018-05-27", + "sap_energy_source": { + "mains_gas": "N", + "meter_type": 2, + "photovoltaic_supply": { + "none_or_no_details": {"pv_connection": 0, "percent_roof_area": 0} + }, + "wind_turbines_count": 0, + "wind_turbines_terrain_type": 2 + }, + "secondary_heating": { + "description": {"value": "Room heaters, electric", "language": "1"}, + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + }, + "lzc_energy_sources": [11], + "sap_building_parts": [ + { + "identifier": "Main Dwelling", + "wall_dry_lined": "N", + "wall_thickness": 270, + "floor_heat_loss": 7, + "roof_construction": 4, + "wall_construction": 4, + "building_part_number": 1, + "sap_floor_dimensions": [ + { + "floor": 0, + "room_height": {"value": 2.4, "quantity": "metres"}, + "floor_insulation": 1, + "total_floor_area": {"value": 48.45, "quantity": "square metres"}, + "party_wall_length": {"value": 0, "quantity": "metres"}, + "floor_construction": 1, + "heat_loss_perimeter": {"value": 22.3, "quantity": "metres"} + } + ], + "wall_insulation_type": 2, + "construction_age_band": "G", + "party_wall_construction": "NA", + "wall_thickness_measured": "Y", + "roof_insulation_location": 2, + "roof_insulation_thickness": "100mm" + }, + { + "identifier": "Extension 1", + "wall_dry_lined": "N", + "wall_thickness": 260, + "floor_heat_loss": 7, + "roof_construction": 5, + "wall_construction": 4, + "building_part_number": 2, + "sap_floor_dimensions": [ + { + "floor": 0, + "room_height": {"value": 2.4, "quantity": "metres"}, + "floor_insulation": 1, + "total_floor_area": {"value": 21.84, "quantity": "square metres"}, + "party_wall_length": 0, + "floor_construction": 1, + "heat_loss_perimeter": {"value": 16.6, "quantity": "metres"} + } + ], + "wall_insulation_type": 4, + "construction_age_band": "H", + "party_wall_construction": "NA", + "wall_thickness_measured": "Y", + "roof_insulation_location": 4, + "roof_insulation_thickness": 0, + "wall_insulation_thickness": "NI" + } + ], + "low_energy_lighting": 23, + "solar_water_heating": "N", + "habitable_room_count": 7, + "heating_cost_current": {"value": 659, "currency": "GBP"}, + "insulated_door_count": 0, + "co2_emissions_current": 5.8, + "energy_rating_average": 60, + "energy_rating_current": 53, + "lighting_cost_current": {"value": 115, "currency": "GBP"}, + "main_heating_controls": [ + { + "description": {"value": "Programmer and room thermostat", "language": "1"}, + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 3 + } + ], + "multiple_glazing_type": 3, + "open_fireplaces_count": 0, + "has_hot_water_cylinder": "true", + "heating_cost_potential": {"value": 470, "currency": "GBP"}, + "hot_water_cost_current": {"value": 161, "currency": "GBP"}, + "mechanical_ventilation": 0, + "percent_draughtproofed": 100, + "suggested_improvements": [ + { + "sequence": 1, + "typical_saving": {"value": 25, "currency": "GBP"}, + "indicative_cost": "£100 - £350", + "improvement_type": "A", + "improvement_details": {"improvement_number": 5}, + "improvement_category": 5, + "energy_performance_rating": 54, + "environmental_impact_rating": 47 + } + ], + "co2_emissions_potential": 2.7, + "energy_rating_potential": 78, + "lighting_cost_potential": {"value": 65, "currency": "GBP"}, + "schema_version_original": "LIG-17.1", + "hot_water_cost_potential": {"value": 77, "currency": "GBP"}, + "renewable_heat_incentive": { + "water_heating": 3301, + "impact_of_loft_insulation": -565, + "space_heating_existing_dwelling": 11351 + }, + "energy_consumption_current": 234, + "has_fixed_air_conditioning": "false", + "multiple_glazed_proportion": 100, + "calculation_software_version": "v92.0.1.1", + "energy_consumption_potential": 95, + "environmental_impact_current": 46, + "fixed_lighting_outlets_count": 13, + "current_energy_efficiency_band": "E", + "environmental_impact_potential": 72, + "has_heated_separate_conservatory": "false", + "potential_energy_efficiency_band": "C", + "co2_emissions_current_per_floor_area": 57, + "low_energy_fixed_lighting_outlets_count": 3 +} diff --git a/datatypes/epc/schema/tests/fixtures/18_0.json b/datatypes/epc/schema/tests/fixtures/18_0.json new file mode 100644 index 00000000..502ac329 --- /dev/null +++ b/datatypes/epc/schema/tests/fixtures/18_0.json @@ -0,0 +1,251 @@ +{ + "uprn": 12457, + "roofs": [ + { + "description": {"value": "Pitched, 100 mm loft insulation", "language": "1"}, + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 3 + }, + { + "description": {"value": "Flat, insulated", "language": "1"}, + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 3 + }, + { + "description": {"value": "Roof room(s), insulated (assumed)", "language": "1"}, + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "walls": [ + { + "description": {"value": "Solid brick, as built, no insulation (assumed)", "language": "1"}, + "energy_efficiency_rating": 1, + "environmental_efficiency_rating": 1 + } + ], + "floors": [ + { + "description": {"value": "Solid, no insulation (assumed)", "language": "1"}, + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + } + ], + "status": "entered", + "tenure": 1, + "window": { + "description": {"value": "Fully double glazed", "language": "1"}, + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 3 + }, + "lighting": { + "description": {"value": "Low energy lighting in 67% of fixed outlets", "language": "1"}, + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + }, + "postcode": "PT11 4RF", + "hot_water": { + "description": {"value": "From main system", "language": "1"}, + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + }, + "post_town": "POSTTOWN", + "built_form": 4, + "door_count": 2, + "glazed_area": 1, + "glazing_gap": 12, + "region_code": 1, + "report_type": 2, + "sap_heating": { + "cylinder_size": 1, + "water_heating_code": 901, + "water_heating_fuel": 26, + "instantaneous_wwhrs": { + "rooms_with_bath_and_or_shower": 1, + "rooms_with_mixer_shower_no_bath": 0, + "rooms_with_bath_and_mixer_shower": 0 + }, + "main_heating_details": [ + { + "has_fghrs": "N", + "main_fuel_type": 26, + "boiler_flue_type": 2, + "heat_emitter_type": 1, + "emitter_temperature": 0, + "main_heating_number": 1, + "main_heating_control": 2106, + "main_heating_category": 2, + "main_heating_fraction": 1, + "central_heating_pump_age": 0, + "main_heating_data_source": 1, + "main_heating_index_number": 16137 + } + ], + "immersion_heating_type": "NA", + "has_fixed_air_conditioning": "false" + }, + "sap_version": 9.92, + "schema_type": "RdSAP-Schema-18.0", + "uprn_source": "Energy Assessor", + "country_code": "EAW", + "main_heating": [ + { + "description": {"value": "Boiler and radiators, mains gas", "language": "1"}, + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "dwelling_type": {"value": "Mid-terrace house", "language": "1"}, + "language_code": 1, + "property_type": 0, + "address_line_1": "1, Bagshot Lane", + "address_line_2": "Village", + "assessment_type": "RdSAP", + "completion_date": "2017-03-19", + "inspection_date": "2017-03-19", + "extensions_count": 1, + "measurement_type": 1, + "total_floor_area": 93, + "transaction_type": 5, + "conservatory_type": 1, + "heated_room_count": 5, + "pvc_window_frames": "true", + "registration_date": "2017-03-19", + "sap_energy_source": { + "mains_gas": "Y", + "meter_type": 1, + "photovoltaic_supply": { + "none_or_no_details": {"pv_connection": 0, "percent_roof_area": 0} + }, + "wind_turbines_count": 0, + "wind_turbines_terrain_type": 2 + }, + "secondary_heating": { + "description": {"value": "None", "language": "1"}, + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + }, + "lzc_energy_sources": [11], + "sap_building_parts": [ + { + "identifier": "Main Dwelling", + "wall_dry_lined": "N", + "wall_thickness": 330, + "floor_heat_loss": 7, + "sap_room_in_roof": { + "floor_area": {"value": 9, "quantity": "square metres"}, + "insulation": "AB", + "roof_room_connected": "N", + "construction_age_band": "G" + }, + "roof_construction": 4, + "wall_construction": 3, + "building_part_number": 1, + "sap_floor_dimensions": [ + { + "floor": 0, + "room_height": {"value": 2.4, "quantity": "metres"}, + "floor_insulation": 1, + "total_floor_area": {"value": 29.12, "quantity": "square metres"}, + "party_wall_length": {"value": 11.2, "quantity": "metres"}, + "floor_construction": 1, + "heat_loss_perimeter": {"value": 5.2, "quantity": "metres"} + } + ], + "wall_insulation_type": 4, + "construction_age_band": "C", + "party_wall_construction": 0, + "wall_thickness_measured": "Y", + "roof_insulation_location": 2, + "roof_insulation_thickness": "100mm", + "wall_insulation_thickness": "NI" + }, + { + "identifier": "Extension", + "wall_dry_lined": "N", + "wall_thickness": 290, + "floor_heat_loss": 7, + "roof_construction": 1, + "wall_construction": 4, + "building_part_number": 2, + "sap_floor_dimensions": [ + { + "floor": 0, + "room_height": {"value": 2.4, "quantity": "metres"}, + "floor_insulation": 1, + "total_floor_area": {"value": 15.6, "quantity": "square metres"}, + "party_wall_length": {"value": 6, "quantity": "metres"}, + "floor_construction": 1, + "heat_loss_perimeter": {"value": 5.2, "quantity": "metres"} + } + ], + "wall_insulation_type": 4, + "construction_age_band": "G", + "party_wall_construction": 0, + "wall_thickness_measured": "Y", + "roof_insulation_location": 6, + "roof_insulation_thickness": "NI", + "wall_insulation_thickness": "NI", + "flat_roof_insulation_thickness": "NI" + } + ], + "low_energy_lighting": 67, + "solar_water_heating": "N", + "habitable_room_count": 5, + "heating_cost_current": {"value": 619, "currency": "GBP"}, + "insulated_door_count": 0, + "co2_emissions_current": 3.4, + "energy_rating_average": 60, + "energy_rating_current": 69, + "lighting_cost_current": {"value": 81, "currency": "GBP"}, + "main_heating_controls": [ + { + "description": {"value": "Programmer, room thermostat and TRVs", "language": "1"}, + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "multiple_glazing_type": 3, + "open_fireplaces_count": 0, + "has_hot_water_cylinder": "false", + "heating_cost_potential": {"value": 534, "currency": "GBP"}, + "hot_water_cost_current": {"value": 100, "currency": "GBP"}, + "mechanical_ventilation": 0, + "percent_draughtproofed": 100, + "suggested_improvements": [ + { + "sequence": 1, + "typical_saving": {"value": 88, "currency": "GBP"}, + "indicative_cost": "£4,000 - £14,000", + "improvement_type": "Q", + "improvement_details": {"improvement_number": 7}, + "improvement_category": 5, + "energy_performance_rating": 72, + "environmental_impact_rating": 71 + } + ], + "co2_emissions_potential": 1.7, + "energy_rating_potential": 85, + "lighting_cost_potential": {"value": 61, "currency": "GBP"}, + "schema_version_original": "LIG-17.0", + "hot_water_cost_potential": {"value": 68, "currency": "GBP"}, + "renewable_heat_incentive": { + "water_heating": 2087, + "impact_of_loft_insulation": -214, + "impact_of_solid_wall_insulation": -1864, + "space_heating_existing_dwelling": 10483 + }, + "energy_consumption_current": 230, + "has_fixed_air_conditioning": "false", + "multiple_glazed_proportion": 100, + "calculation_software_version": "2.0.0.0", + "energy_consumption_potential": 115, + "environmental_impact_current": 66, + "fixed_lighting_outlets_count": 9, + "current_energy_efficiency_band": "C", + "environmental_impact_potential": 83, + "has_heated_separate_conservatory": "false", + "potential_energy_efficiency_band": "B", + "co2_emissions_current_per_floor_area": 41, + "low_energy_fixed_lighting_outlets_count": 6 +} diff --git a/datatypes/epc/schema/tests/fixtures/19_0.json b/datatypes/epc/schema/tests/fixtures/19_0.json new file mode 100644 index 00000000..f9995286 --- /dev/null +++ b/datatypes/epc/schema/tests/fixtures/19_0.json @@ -0,0 +1,213 @@ +{ + "uprn": 12457, + "roofs": [ + { + "description": {"value": "Pitched, 150 mm loft insulation", "language": "1"}, + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "walls": [ + { + "description": {"value": "Cavity wall, filled cavity", "language": "1"}, + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 3 + } + ], + "floors": [ + { + "description": {"value": "Solid, no insulation (assumed)", "language": "1"}, + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + } + ], + "status": "entered", + "tenure": 3, + "window": { + "description": {"value": "Fully double glazed", "language": "1"}, + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 3 + }, + "lighting": { + "description": {"value": "Low energy lighting in 87% of fixed outlets", "language": "1"}, + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + }, + "postcode": "A1 1AA", + "hot_water": { + "description": {"value": "From main system", "language": "1"}, + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + }, + "post_town": "Town", + "built_form": 2, + "door_count": 1, + "glazed_area": 1, + "region_code": 19, + "report_type": 2, + "sap_heating": { + "cylinder_size": 1, + "water_heating_code": 901, + "water_heating_fuel": 26, + "instantaneous_wwhrs": { + "rooms_with_bath_and_or_shower": 1, + "rooms_with_mixer_shower_no_bath": 0, + "rooms_with_bath_and_mixer_shower": 0 + }, + "secondary_fuel_type": 9, + "main_heating_details": [ + { + "has_fghrs": "N", + "main_fuel_type": 26, + "boiler_flue_type": 2, + "heat_emitter_type": 1, + "emitter_temperature": 0, + "main_heating_number": 1, + "main_heating_control": 2106, + "main_heating_category": 2, + "main_heating_fraction": 1, + "central_heating_pump_age": 0, + "main_heating_data_source": 1, + "main_heating_index_number": 15274 + } + ], + "immersion_heating_type": "NA", + "secondary_heating_type": 631, + "has_fixed_air_conditioning": "false" + }, + "sap_version": 9.94, + "schema_type": "RdSAP-Schema-19.0", + "uprn_source": "Energy Assessor", + "country_code": "EAW", + "main_heating": [ + { + "description": {"value": "Boiler and radiators, mains gas", "language": "1"}, + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "dwelling_type": {"value": "Semi-detached house", "language": "1"}, + "language_code": 1, + "property_type": 0, + "address_line_1": "15, Address Lane", + "address_line_2": "New Town", + "assessment_type": "RdSAP", + "completion_date": "2020-06-04", + "inspection_date": "2020-06-03", + "extensions_count": 1, + "measurement_type": 1, + "total_floor_area": 94, + "transaction_type": 8, + "conservatory_type": 2, + "heated_room_count": 5, + "pvc_window_frames": "false", + "registration_date": "2020-06-04", + "sap_energy_source": { + "mains_gas": "Y", + "meter_type": 3, + "photovoltaic_supply": { + "none_or_no_details": {"pv_connection": 0, "percent_roof_area": 0} + }, + "wind_turbines_count": 0, + "wind_turbines_terrain_type": 2 + }, + "secondary_heating": { + "description": {"value": "Room heaters, dual fuel (mineral and wood)", "language": "1"}, + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + }, + "lzc_energy_sources": [11, 9], + "sap_building_parts": [ + { + "identifier": "Main Dwelling", + "wall_dry_lined": "N", + "wall_thickness": 300, + "floor_heat_loss": 7, + "roof_construction": 4, + "wall_construction": 4, + "building_part_number": 1, + "sap_floor_dimensions": [ + { + "floor": 0, + "room_height": {"value": 2.47, "quantity": "metres"}, + "floor_insulation": 1, + "total_floor_area": {"value": 39.91, "quantity": "square metres"}, + "party_wall_length": {"value": 8.81, "quantity": "metres"}, + "floor_construction": 1, + "heat_loss_perimeter": {"value": 13.65, "quantity": "metres"} + } + ], + "wall_insulation_type": 2, + "construction_age_band": "D", + "party_wall_construction": 1, + "wall_thickness_measured": "Y", + "roof_insulation_location": 2, + "roof_insulation_thickness": "150mm", + "wall_insulation_thickness": "NI", + "floor_insulation_thickness": "NI" + } + ], + "low_energy_lighting": 87, + "solar_water_heating": "N", + "habitable_room_count": 5, + "heating_cost_current": {"value": 666, "currency": "GBP"}, + "insulated_door_count": 0, + "co2_emissions_current": 3.8, + "energy_rating_average": 60, + "energy_rating_current": 66, + "lighting_cost_current": {"value": 81, "currency": "GBP"}, + "main_heating_controls": [ + { + "description": {"value": "Programmer, room thermostat and TRVs", "language": "1"}, + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "multiple_glazing_type": 3, + "open_fireplaces_count": 0, + "has_hot_water_cylinder": "false", + "heating_cost_potential": {"value": 615, "currency": "GBP"}, + "hot_water_cost_current": {"value": 107, "currency": "GBP"}, + "mechanical_ventilation": 0, + "percent_draughtproofed": 100, + "suggested_improvements": [ + { + "sequence": 1, + "typical_saving": {"value": 51, "currency": "GBP"}, + "indicative_cost": "£4,000 - £6,000", + "improvement_type": "W2", + "improvement_details": {"improvement_number": 58}, + "improvement_category": 5, + "energy_performance_rating": 68, + "environmental_impact_rating": 65 + } + ], + "co2_emissions_potential": 2.4, + "energy_rating_potential": 79, + "lighting_cost_potential": {"value": 81, "currency": "GBP"}, + "schema_version_original": "LIG-19.0", + "hot_water_cost_potential": {"value": 74, "currency": "GBP"}, + "renewable_heat_incentive": { + "water_heating": 2207, + "impact_of_loft_insulation": -394, + "space_heating_existing_dwelling": 9825 + }, + "energy_consumption_current": 222, + "has_fixed_air_conditioning": "false", + "multiple_glazed_proportion": 100, + "calculation_software_version": "2.1.1.0", + "energy_consumption_potential": 137, + "environmental_impact_current": 62, + "fixed_lighting_outlets_count": 15, + "windows_transmission_details": { + "u_value": 3.1, + "data_source": 2, + "solar_transmittance": 0.76 + }, + "current_energy_efficiency_band": "D", + "environmental_impact_potential": 75, + "has_heated_separate_conservatory": "false", + "potential_energy_efficiency_band": "C", + "co2_emissions_current_per_floor_area": 41, + "low_energy_fixed_lighting_outlets_count": 13 +} diff --git a/datatypes/epc/schema/tests/fixtures/20_0_0.json b/datatypes/epc/schema/tests/fixtures/20_0_0.json new file mode 100644 index 00000000..397c2758 --- /dev/null +++ b/datatypes/epc/schema/tests/fixtures/20_0_0.json @@ -0,0 +1,225 @@ +{ + "uprn": 12457, + "roofs": [ + {"description": "Pitched, 25 mm loft insulation", "energy_efficiency_rating": 2, "environmental_efficiency_rating": 2}, + {"description": "Pitched, 250 mm loft insulation", "energy_efficiency_rating": 4, "environmental_efficiency_rating": 4} + ], + "walls": [ + {"description": "Solid brick, as built, no insulation (assumed)", "energy_efficiency_rating": 1, "environmental_efficiency_rating": 1}, + {"description": "Cavity wall, as built, insulated (assumed)", "energy_efficiency_rating": 4, "environmental_efficiency_rating": 4} + ], + "floors": [ + {"description": "Suspended, no insulation (assumed)", "energy_efficiency_rating": 0, "environmental_efficiency_rating": 0}, + {"description": "Solid, insulated (assumed)", "energy_efficiency_rating": 0, "environmental_efficiency_rating": 0} + ], + "status": "entered", + "tenure": 1, + "window": {"description": "Fully double glazed", "energy_efficiency_rating": 3, "environmental_efficiency_rating": 3}, + "addendum": { + "stone_walls": "true", + "system_build": "true", + "addendum_numbers": [1, 8] + }, + "lighting": {"description": "Low energy lighting in 50% of fixed outlets", "energy_efficiency_rating": 4, "environmental_efficiency_rating": 4}, + "postcode": "A0 0AA", + "hot_water": {"description": "From main system", "energy_efficiency_rating": 4, "environmental_efficiency_rating": 4}, + "post_town": "Whitbury", + "built_form": 2, + "door_count": 2, + "glazed_area": 1, + "region_code": 1, + "report_type": 2, + "sap_heating": { + "cylinder_size": 1, + "water_heating_code": 901, + "water_heating_fuel": 26, + "instantaneous_wwhrs": { + "rooms_with_bath_and_or_shower": 1, + "rooms_with_mixer_shower_no_bath": 0, + "rooms_with_bath_and_mixer_shower": 0 + }, + "secondary_fuel_type": 25, + "main_heating_details": [ + { + "has_fghrs": "N", + "main_fuel_type": 26, + "boiler_flue_type": 2, + "fan_flue_present": "N", + "heat_emitter_type": 1, + "emitter_temperature": 0, + "main_heating_number": 1, + "main_heating_control": 2106, + "main_heating_category": 2, + "main_heating_fraction": 1, + "sap_main_heating_code": 101, + "central_heating_pump_age": 0, + "main_heating_data_source": 1, + "main_heating_index_number": 17507 + } + ], + "immersion_heating_type": "NA", + "has_fixed_air_conditioning": "false" + }, + "sap_version": 9.8, + "sap_windows": [ + {"orientation": 1, "window_area": 200.1, "window_type": 2, "glazing_type": 1, "window_location": 0}, + {"orientation": 2, "window_area": 180.2, "window_type": 1, "glazing_type": 2, "window_location": 1} + ], + "schema_type": "RdSAP-Schema-20.0.0", + "uprn_source": "Energy Assessor", + "country_code": "EAW", + "main_heating": [ + {"description": "Boiler and radiators, anthracite", "energy_efficiency_rating": 3, "environmental_efficiency_rating": 1}, + {"description": "Boiler and radiators, mains gas", "energy_efficiency_rating": 4, "environmental_efficiency_rating": 4} + ], + "dwelling_type": "Mid-terrace house", + "language_code": 1, + "property_type": 0, + "address_line_1": "1 Some Street", + "assessment_type": "RdSAP", + "completion_date": "2020-05-04", + "inspection_date": "2020-05-04", + "extensions_count": 0, + "measurement_type": 1, + "sap_flat_details": { + "level": 1, + "top_storey": "N", + "storey_count": 3, + "flat_location": 1, + "heat_loss_corridor": 2, + "unheated_corridor_length": 10 + }, + "total_floor_area": 55, + "transaction_type": 1, + "conservatory_type": 1, + "heated_room_count": 5, + "registration_date": "2020-05-04", + "sap_energy_source": { + "mains_gas": "Y", + "meter_type": 2, + "photovoltaic_supply": { + "none_or_no_details": {"pv_connection": 0, "percent_roof_area": 50} + }, + "wind_turbines_count": 0, + "wind_turbines_terrain_type": 2 + }, + "secondary_heating": {"description": "Room heaters, electric", "energy_efficiency_rating": 0, "environmental_efficiency_rating": 0}, + "lzc_energy_sources": [11], + "sap_building_parts": [ + { + "identifier": "Main Dwelling", + "wall_dry_lined": "N", + "floor_heat_loss": 7, + "sap_room_in_roof": { + "floor_area": 100, + "insulation": "AB", + "roof_room_connected": "N", + "construction_age_band": "B" + }, + "roof_construction": 4, + "wall_construction": 4, + "building_part_number": 1, + "sap_floor_dimensions": [ + { + "floor": 0, + "room_height": {"value": 2.45, "quantity": "metres"}, + "floor_insulation": 1, + "total_floor_area": {"value": 45.82, "quantity": "square metres"}, + "party_wall_length": {"value": 7.9, "quantity": "metres"}, + "floor_construction": 1, + "heat_loss_perimeter": {"value": 19.5, "quantity": "metres"} + } + ], + "wall_insulation_type": 2, + "construction_age_band": "K", + "party_wall_construction": 0, + "wall_thickness_measured": "N", + "roof_insulation_location": 2, + "roof_insulation_thickness": "200mm", + "wall_insulation_thickness": "NI", + "floor_insulation_thickness": "NI" + } + ], + "low_energy_lighting": 100, + "solar_water_heating": "N", + "habitable_room_count": 5, + "heating_cost_current": 365.98, + "insulated_door_count": 2, + "co2_emissions_current": 2.4, + "energy_rating_average": 60, + "energy_rating_current": 50, + "lighting_cost_current": 123.45, + "main_heating_controls": [ + {"description": "Programmer, room thermostat and TRVs", "energy_efficiency_rating": 4, "environmental_efficiency_rating": 4}, + {"description": "Time and temperature zone control", "energy_efficiency_rating": 5, "environmental_efficiency_rating": 5} + ], + "multiple_glazing_type": 2, + "open_fireplaces_count": 0, + "heating_cost_potential": 250.34, + "hot_water_cost_current": 200.4, + "insulated_door_u_value": 3, + "mechanical_ventilation": 0, + "percent_draughtproofed": 100, + "suggested_improvements": [ + { + "sequence": 1, + "typical_saving": 360, + "indicative_cost": "£100 - £350", + "improvement_type": "Z3", + "improvement_details": {"improvement_number": 5}, + "improvement_category": 6, + "energy_performance_rating": 50, + "environmental_impact_rating": 50 + }, + { + "sequence": 2, + "typical_saving": 99, + "indicative_cost": 2000, + "improvement_type": "Z2", + "improvement_details": {"improvement_number": 1}, + "improvement_category": 2, + "energy_performance_rating": 60, + "environmental_impact_rating": 64 + }, + { + "sequence": 3, + "typical_saving": 99, + "indicative_cost": 1000, + "improvement_type": "Z2", + "improvement_details": { + "improvement_texts": { + "improvement_summary": "An improvement summary", + "improvement_description": "An improvement desc" + } + }, + "improvement_category": 2, + "energy_performance_rating": 60, + "environmental_impact_rating": 64 + } + ], + "co2_emissions_potential": 1.4, + "energy_rating_potential": 72, + "lighting_cost_potential": 84.23, + "schema_version_original": "SAP-19.0", + "hot_water_cost_potential": 180.43, + "renewable_heat_incentive": { + "water_heating": 2285, + "impact_of_loft_insulation": -2114, + "impact_of_cavity_insulation": -122, + "impact_of_solid_wall_insulation": -3560, + "space_heating_existing_dwelling": 13120 + }, + "energy_consumption_current": 230, + "multiple_glazed_proportion": 100, + "calculation_software_version": "13.05r16", + "energy_consumption_potential": 88, + "environmental_impact_current": 52, + "fixed_lighting_outlets_count": 16, + "windows_transmission_details": {"u_value": 2, "data_source": 2, "solar_transmittance": 0.72}, + "multiple_glazed_proportion_nr": "NR", + "current_energy_efficiency_band": "E", + "environmental_impact_potential": 74, + "potential_energy_efficiency_band": "C", + "co2_emissions_current_per_floor_area": 20, + "low_energy_fixed_lighting_outlets_count": 16 +} diff --git a/datatypes/epc/schema/tests/fixtures/21_0_0.json b/datatypes/epc/schema/tests/fixtures/21_0_0.json new file mode 100644 index 00000000..6781ac6e --- /dev/null +++ b/datatypes/epc/schema/tests/fixtures/21_0_0.json @@ -0,0 +1,245 @@ +{ + "uprn": 12457, + "roofs": [ + {"description": "Pitched, 25 mm loft insulation", "energy_efficiency_rating": 2, "environmental_efficiency_rating": 2}, + {"description": "Pitched, 250 mm loft insulation", "energy_efficiency_rating": 4, "environmental_efficiency_rating": 4} + ], + "walls": [ + {"description": "Solid brick, as built, no insulation (assumed)", "energy_efficiency_rating": 1, "environmental_efficiency_rating": 1}, + {"description": "Cavity wall, as built, insulated (assumed)", "energy_efficiency_rating": 4, "environmental_efficiency_rating": 4} + ], + "floors": [ + {"description": "Suspended, no insulation (assumed)", "energy_efficiency_rating": 0, "environmental_efficiency_rating": 0}, + {"description": "Solid, insulated (assumed)", "energy_efficiency_rating": 0, "environmental_efficiency_rating": 0} + ], + "status": "entered", + "tenure": 1, + "window": {"description": "Fully double glazed", "energy_efficiency_rating": 3, "environmental_efficiency_rating": 3}, + "addendum": {"stone_walls": "true", "system_build": "true", "addendum_numbers": [1, 8]}, + "lighting": {"description": "Low energy lighting in 50% of fixed outlets", "energy_efficiency_rating": 4, "environmental_efficiency_rating": 4}, + "postcode": "A0 0AA", + "hot_water": {"description": "From main system", "energy_efficiency_rating": 4, "environmental_efficiency_rating": 4}, + "post_town": "Whitbury", + "built_form": 2, + "door_count": 3, + "region_code": 1, + "report_type": 2, + "sap_heating": { + "cylinder_size": 1, + "shower_outlets": { + "shower_outlet": {"shower_wwhrs": 1, "shower_outlet_type": 1} + }, + "water_heating_code": 901, + "water_heating_fuel": 26, + "instantaneous_wwhrs": {"wwhrs_index_number1": 1, "wwhrs_index_number2": 2}, + "secondary_fuel_type": 25, + "main_heating_details": [ + { + "has_fghrs": "N", + "main_fuel_type": 26, + "boiler_flue_type": 2, + "fan_flue_present": "N", + "heat_emitter_type": 1, + "emitter_temperature": 0, + "main_heating_number": 1, + "boiler_ignition_type": 1, + "main_heating_control": 2106, + "main_heating_category": 2, + "main_heating_fraction": 1, + "sap_main_heating_code": 101, + "central_heating_pump_age": 0, + "main_heating_data_source": 1, + "main_heating_index_number": 17507 + } + ], + "immersion_heating_type": "NA", + "has_fixed_air_conditioning": "false" + }, + "sap_version": 10.2, + "sap_windows": [ + { + "pvc_frame": "false", + "glazing_gap": 6, + "orientation": 1, + "window_type": 2, + "frame_factor": 1.0, + "glazing_type": 14, + "window_width": 1.2, + "window_height": 2.0, + "draught_proofed": "true", + "window_location": 0, + "window_wall_type": 6, + "permanent_shutters_present": "N", + "window_transmission_details": {"u_value": 1.0, "data_source": 2, "solar_transmittance": 1.0}, + "permanent_shutters_insulated": "N" + } + ], + "schema_type": "RdSAP-Schema-21.0.0", + "uprn_source": "Energy Assessor", + "country_code": "ENG", + "main_heating": [ + {"description": "Boiler and radiators, anthracite", "energy_efficiency_rating": 3, "environmental_efficiency_rating": 1}, + {"description": "Boiler and radiators, mains gas", "energy_efficiency_rating": 4, "environmental_efficiency_rating": 4} + ], + "dwelling_type": "Mid-terrace house", + "language_code": 1, + "pressure_test": 6, + "property_type": 0, + "address_line_1": "1 Some Street", + "assessment_type": "RdSAP", + "completion_date": "2023-12-01", + "inspection_date": "2023-12-01", + "wet_rooms_count": 0, + "extensions_count": 0, + "measurement_type": 1, + "sap_flat_details": { + "level": 1, + "top_storey": "N", + "storey_count": 3, + "flat_location": 1, + "heat_loss_corridor": 2, + "unheated_corridor_length": 10 + }, + "total_floor_area": 55, + "transaction_type": 16, + "conservatory_type": 1, + "heated_room_count": 5, + "registration_date": "2023-12-01", + "sap_energy_source": { + "mains_gas": "Y", + "meter_type": 2, + "pv_batteries": {"pv_battery": {"battery_capacity": 5}}, + "pv_connection": 0, + "pv_battery_count": 1, + "photovoltaic_supply": {"none_or_no_details": {"percent_roof_area": 0}}, + "wind_turbines_count": 0, + "wind_turbine_details": {"hub_height": 0, "rotor_diameter": 0}, + "gas_smart_meter_present": "false", + "is_dwelling_export_capable": "false", + "wind_turbines_terrain_type": 4, + "electricity_smart_meter_present": "true" + }, + "secondary_heating": {"description": "Room heaters, electric", "energy_efficiency_rating": 0, "environmental_efficiency_rating": 0}, + "sap_building_parts": [ + { + "identifier": "Main Dwelling", + "wall_dry_lined": "N", + "floor_heat_loss": 7, + "sap_room_in_roof": {"floor_area": 100, "construction_age_band": "B"}, + "roof_construction": 4, + "wall_construction": 4, + "building_part_number": 1, + "sap_floor_dimensions": [ + { + "floor": 0, + "room_height": {"value": 2.45, "quantity": "metres"}, + "floor_insulation": 1, + "total_floor_area": {"value": 45.82, "quantity": "square metres"}, + "party_wall_length": {"value": 7.9, "quantity": "metres"}, + "floor_construction": 1, + "heat_loss_perimeter": {"value": 19.5, "quantity": "metres"} + } + ], + "wall_insulation_type": 2, + "construction_age_band": "M", + "sap_alternative_wall_1": { + "wall_area": 10.4, + "wall_dry_lined": "N", + "wall_construction": 4, + "wall_insulation_type": 2, + "wall_thickness_measured": "N" + }, + "sap_alternative_wall_2": { + "wall_area": 10.8, + "wall_dry_lined": "N", + "wall_construction": 4, + "wall_insulation_type": 2, + "wall_thickness_measured": "N" + }, + "party_wall_construction": 0, + "wall_thickness_measured": "N", + "roof_insulation_location": 2, + "roof_insulation_thickness": "200mm", + "wall_insulation_thickness": "NI", + "floor_insulation_thickness": "NI" + } + ], + "open_chimneys_count": 1, + "solar_water_heating": "N", + "habitable_room_count": 5, + "heating_cost_current": 365.98, + "insulated_door_count": 2, + "co2_emissions_current": 2.4, + "energy_rating_average": 60, + "energy_rating_current": 50, + "lighting_cost_current": 123.45, + "main_heating_controls": [ + {"description": "Programmer, room thermostat and TRVs", "energy_efficiency_rating": 4, "environmental_efficiency_rating": 4}, + {"description": "Time and temperature zone control", "energy_efficiency_rating": 5, "environmental_efficiency_rating": 5} + ], + "has_hot_water_cylinder": "true", + "heating_cost_potential": 250.34, + "hot_water_cost_current": 200.4, + "insulated_door_u_value": 3, + "mechanical_ventilation": 6, + "percent_draughtproofed": 100, + "suggested_improvements": [ + { + "sequence": 1, + "typical_saving": 360, + "indicative_cost": "£100 - £350", + "improvement_type": "Z3", + "improvement_details": {"improvement_number": 5}, + "improvement_category": 6, + "energy_performance_rating": 50, + "environmental_impact_rating": 50 + }, + { + "sequence": 3, + "typical_saving": 99, + "indicative_cost": 1000, + "improvement_type": "Z2", + "improvement_details": { + "improvement_texts": {"improvement_description": "Improvement desc"} + }, + "improvement_category": 2, + "energy_performance_rating": 60, + "environmental_impact_rating": 64 + } + ], + "co2_emissions_potential": 1.4, + "energy_rating_potential": 72, + "lighting_cost_potential": 84.23, + "schema_version_original": "SAP-19.0", + "hot_water_cost_potential": 180.43, + "renewable_heat_incentive": { + "water_heating": 2285, + "impact_of_loft_insulation": -2114, + "impact_of_cavity_insulation": -122, + "impact_of_solid_wall_insulation": -3560, + "space_heating_existing_dwelling": 13120 + }, + "draughtproofed_door_count": 1, + "mechanical_vent_duct_type": 3, + "energy_consumption_current": 230, + "has_fixed_air_conditioning": "false", + "multiple_glazed_proportion": 100, + "calculation_software_version": "13.05r16", + "energy_consumption_potential": 88, + "environmental_impact_current": 52, + "windows_transmission_details": {"u_value": 2, "data_source": 2, "solar_transmittance": 0.72}, + "cfl_fixed_lighting_bulbs_count": 5, + "current_energy_efficiency_band": "E", + "environmental_impact_potential": 74, + "led_fixed_lighting_bulbs_count": 10, + "mechanical_vent_duct_placement": 2, + "mechanical_vent_duct_insulation": 2, + "potential_energy_efficiency_band": "C", + "pressure_test_certificate_number": 0, + "mechanical_ventilation_index_number": 12, + "co2_emissions_current_per_floor_area": 20, + "low_energy_fixed_lighting_bulbs_count": 16, + "mechanical_vent_duct_insulation_level": 2, + "mechanical_vent_measured_installation": "false", + "incandescent_fixed_lighting_bulbs_count": 5 +} diff --git a/datatypes/epc/schema/tests/fixtures/21_0_1.json b/datatypes/epc/schema/tests/fixtures/21_0_1.json new file mode 100644 index 00000000..45361227 --- /dev/null +++ b/datatypes/epc/schema/tests/fixtures/21_0_1.json @@ -0,0 +1,222 @@ +{ + "uprn": 12457, + "roofs": [ + {"description": {"value": "Pitched, 25 mm loft insulation", "language": "1"}, "energy_efficiency_rating": 2, "environmental_efficiency_rating": 2}, + {"description": {"value": "Pitched, 250 mm loft insulation", "language": "1"}, "energy_efficiency_rating": 4, "environmental_efficiency_rating": 4} + ], + "walls": [ + {"description": {"value": "Solid brick, as built, no insulation (assumed)", "language": "1"}, "energy_efficiency_rating": 1, "environmental_efficiency_rating": 1}, + {"description": {"value": "Cavity wall, as built, insulated (assumed)", "language": "1"}, "energy_efficiency_rating": 4, "environmental_efficiency_rating": 4} + ], + "floors": [ + {"description": {"value": "Suspended, no insulation (assumed)", "language": "1"}, "energy_efficiency_rating": 0, "environmental_efficiency_rating": 0} + ], + "status": "entered", + "tenure": 1, + "window": {"description": {"value": "Fully double glazed", "language": "1"}, "energy_efficiency_rating": 3, "environmental_efficiency_rating": 3}, + "addendum": {"stone_walls": "true", "system_build": "true", "addendum_numbers": [1, 13]}, + "lighting": {"description": {"value": "Low energy lighting in 50% of fixed outlets", "language": "1"}, "energy_efficiency_rating": 4, "environmental_efficiency_rating": 4}, + "postcode": "A0 0AA", + "hot_water": {"description": {"value": "From main system", "language": "1"}, "energy_efficiency_rating": 4, "environmental_efficiency_rating": 4}, + "post_town": "Whitbury", + "built_form": 2, + "door_count": 3, + "region_code": 1, + "report_type": 2, + "sap_heating": { + "cylinder_size": 1, + "shower_outlets": {"shower_outlet": {"shower_wwhrs": 1, "shower_outlet_type": 1}}, + "water_heating_code": 901, + "water_heating_fuel": 26, + "instantaneous_wwhrs": {"wwhrs_index_number1": 1, "wwhrs_index_number2": 2}, + "secondary_fuel_type": 25, + "main_heating_details": [ + { + "has_fghrs": "N", + "main_fuel_type": 26, + "boiler_flue_type": 2, + "fan_flue_present": "N", + "heat_emitter_type": 1, + "emitter_temperature": 0, + "main_heating_number": 1, + "boiler_ignition_type": 1, + "main_heating_control": 2106, + "main_heating_category": 2, + "main_heating_fraction": 1, + "sap_main_heating_code": 101, + "central_heating_pump_age": 0, + "main_heating_data_source": 1, + "main_heating_index_number": 17507 + } + ], + "immersion_heating_type": "NA", + "has_fixed_air_conditioning": "false" + }, + "sap_version": 10.2, + "sap_windows": [ + { + "pvc_frame": "false", + "glazing_gap": 6, + "orientation": 1, + "window_type": 2, + "frame_factor": 1.0, + "glazing_type": 14, + "window_width": 1.2, + "window_height": 2.0, + "draught_proofed": "true", + "window_location": 0, + "window_wall_type": 6, + "permanent_shutters_present": "N", + "window_transmission_details": {"u_value": 1.0, "data_source": 2, "solar_transmittance": 1.0}, + "permanent_shutters_insulated": "N" + } + ], + "schema_type": "RdSAP-Schema-21.0.1", + "uprn_source": "Energy Assessor", + "country_code": "ENG", + "main_heating": [ + {"description": {"value": "Boiler and radiators, anthracite", "language": "1"}, "energy_efficiency_rating": 3, "environmental_efficiency_rating": 1}, + {"description": {"value": "Boiler and radiators, mains gas", "language": "1"}, "energy_efficiency_rating": 4, "environmental_efficiency_rating": 4} + ], + "dwelling_type": "Mid-terrace house", + "language_code": 1, + "pressure_test": 6, + "property_type": 0, + "address_line_1": "1 Some Street", + "assessment_type": "RdSAP", + "completion_date": "2025-04-04", + "inspection_date": "2025-04-04", + "wet_rooms_count": 0, + "extensions_count": 0, + "measurement_type": 1, + "sap_flat_details": { + "level": 1, + "top_storey": "N", + "storey_count": 3, + "flat_location": 1, + "heat_loss_corridor": 2, + "unheated_corridor_length": {"value": 10, "quantity": "metres"} + }, + "total_floor_area": 55, + "transaction_type": 16, + "conservatory_type": 1, + "heated_room_count": 5, + "registration_date": "2025-04-04", + "sap_energy_source": { + "mains_gas": "Y", + "meter_type": 2, + "pv_batteries": {"pv_battery": {"battery_capacity": 5}}, + "pv_connection": 0, + "pv_battery_count": 1, + "photovoltaic_supply": {"none_or_no_details": {"percent_roof_area": 0}}, + "wind_turbines_count": 0, + "wind_turbine_details": {"hub_height": 0, "rotor_diameter": 0}, + "gas_smart_meter_present": "false", + "is_dwelling_export_capable": "false", + "wind_turbines_terrain_type": 4, + "electricity_smart_meter_present": "true" + }, + "secondary_heating": { + "description": {"value": "Room heaters, electric", "language": "1"}, + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + }, + "sap_building_parts": [ + { + "identifier": "Main Dwelling", + "wall_dry_lined": "N", + "floor_heat_loss": 7, + "sap_room_in_roof": {"floor_area": 100, "construction_age_band": "B"}, + "roof_construction": 4, + "wall_construction": 4, + "building_part_number": 1, + "sap_floor_dimensions": [ + { + "floor": 0, + "room_height": {"value": 2.45, "quantity": "metres"}, + "floor_insulation": 1, + "total_floor_area": {"value": 45.82, "quantity": "square metres"}, + "party_wall_length": {"value": 7.9, "quantity": "metres"}, + "floor_construction": 1, + "heat_loss_perimeter": {"value": 19.5, "quantity": "metres"} + } + ], + "wall_insulation_type": 2, + "construction_age_band": "M", + "sap_alternative_wall_1": {"wall_area": 10.4, "wall_dry_lined": "N", "wall_construction": 4, "wall_insulation_type": 2, "wall_thickness_measured": "N"}, + "sap_alternative_wall_2": {"wall_area": 10.8, "wall_dry_lined": "N", "wall_construction": 4, "wall_insulation_type": 2, "wall_thickness_measured": "N"}, + "party_wall_construction": 0, + "wall_thickness_measured": "N", + "roof_insulation_location": 2, + "roof_insulation_thickness": "200mm", + "wall_insulation_thickness": "NI", + "floor_insulation_thickness": "NI" + } + ], + "open_chimneys_count": 1, + "solar_water_heating": "N", + "habitable_room_count": 5, + "heating_cost_current": 365.98, + "insulated_door_count": 2, + "co2_emissions_current": 2.4, + "energy_rating_average": 60, + "energy_rating_current": 50, + "lighting_cost_current": 123.45, + "main_heating_controls": [ + {"description": {"value": "Programmer, room thermostat and TRVs", "language": "1"}, "energy_efficiency_rating": 4, "environmental_efficiency_rating": 4}, + {"description": {"value": "Time and temperature zone control", "language": "1"}, "energy_efficiency_rating": 5, "environmental_efficiency_rating": 5} + ], + "has_hot_water_cylinder": "true", + "heating_cost_potential": 250.34, + "hot_water_cost_current": 200.4, + "insulated_door_u_value": 3, + "mechanical_ventilation": 6, + "percent_draughtproofed": 100, + "suggested_improvements": [ + { + "sequence": 1, + "typical_saving": 139, + "indicative_cost": "£220 - £250", + "improvement_type": "G", + "improvement_details": {"improvement_number": 66}, + "improvement_category": 5, + "energy_performance_rating": 70, + "environmental_impact_rating": 70 + } + ], + "co2_emissions_potential": 1.4, + "energy_rating_potential": 72, + "lighting_cost_potential": 84.23, + "schema_version_original": "SAP-19.0", + "hot_water_cost_potential": 180.43, + "renewable_heat_incentive": { + "water_heating": 2285, + "impact_of_loft_insulation": -2114, + "impact_of_cavity_insulation": -122, + "impact_of_solid_wall_insulation": -3560, + "space_heating_existing_dwelling": 13120 + }, + "draughtproofed_door_count": 1, + "mechanical_vent_duct_type": 3, + "energy_consumption_current": 230, + "has_fixed_air_conditioning": "false", + "multiple_glazed_proportion": 100, + "calculation_software_version": "13.05r16", + "energy_consumption_potential": 88, + "environmental_impact_current": 52, + "windows_transmission_details": {"u_value": 2, "data_source": 2, "solar_transmittance": 0.72}, + "cfl_fixed_lighting_bulbs_count": 5, + "current_energy_efficiency_band": "E", + "environmental_impact_potential": 74, + "led_fixed_lighting_bulbs_count": 10, + "mechanical_vent_duct_placement": 2, + "mechanical_vent_duct_insulation": 2, + "potential_energy_efficiency_band": "C", + "pressure_test_certificate_number": 0, + "mechanical_ventilation_index_number": 12, + "co2_emissions_current_per_floor_area": 20, + "low_energy_fixed_lighting_bulbs_count": 16, + "mechanical_vent_duct_insulation_level": 2, + "mechanical_vent_measured_installation": "false", + "incandescent_fixed_lighting_bulbs_count": 0 +} diff --git a/datatypes/epc/schema/tests/helpers.py b/datatypes/epc/schema/tests/helpers.py new file mode 100644 index 00000000..677bd8b7 --- /dev/null +++ b/datatypes/epc/schema/tests/helpers.py @@ -0,0 +1,73 @@ +import dataclasses +import typing +from typing import Any, Dict, Type, TypeVar + +T = TypeVar("T") + + +def from_dict(cls: Type[T], data: Dict[str, Any]) -> T: + """ + Recursively convert a plain dict (e.g. from json.loads) into the given + dataclass type, using the field type hints to convert nested structures. + + Handles: + - Nested dataclasses + - List[SomeDataclass] + - Optional[X] / Union[X, None] + - Union[DataclassType, primitive] (e.g. Union[Measurement, int]) + - Primitive pass-through for Union[str, int] etc. + """ + return _from_dict_impl(cls, data) # type: ignore[return-value] + + +def _from_dict_impl(cls: Any, data: Any) -> Any: + hints = typing.get_type_hints(cls) + kwargs: Dict[str, Any] = {} + + for field in dataclasses.fields(cls): # type: ignore[arg-type] + has_default = ( + field.default is not dataclasses.MISSING + or field.default_factory is not dataclasses.MISSING # type: ignore[misc] + ) + if field.name not in data: + if has_default: + continue + raise ValueError(f"{cls.__name__}: missing required field '{field.name}'") + + kwargs[field.name] = _coerce(data[field.name], hints[field.name]) + + return cls(**kwargs) + + +def _coerce(value: Any, hint: Any) -> Any: + if value is None: + return None + + origin = typing.get_origin(hint) + args = typing.get_args(hint) + + # Union (includes Optional[X] which is Union[X, None]) + if origin is typing.Union: + if value is None: + return None + non_none_args = [a for a in args if a is not type(None)] + if len(non_none_args) == 1: + # Optional[X] — recurse so List[X] and nested dataclasses are handled + return _coerce(value, non_none_args[0]) + # Multi-type Union (e.g. Union[Measurement, int]): try dataclasses first + for arg in non_none_args: + if dataclasses.is_dataclass(arg) and isinstance(value, dict): + return _from_dict_impl(arg, value) + # All remaining args are primitives — return value as-is + return value + + # List[X] + if origin is list: + item_hint = args[0] + return [_coerce(item, item_hint) for item in value] + + # Plain dataclass + if dataclasses.is_dataclass(hint) and isinstance(value, dict): + return _from_dict_impl(hint, value) + + return value diff --git a/datatypes/epc/schema/tests/test_schema_loading.py b/datatypes/epc/schema/tests/test_schema_loading.py new file mode 100644 index 00000000..dc80ddc0 --- /dev/null +++ b/datatypes/epc/schema/tests/test_schema_loading.py @@ -0,0 +1,380 @@ +import json +import os +from typing import Any, Dict + +import pytest + +from datatypes.epc.schema import ( + RdSapSchema17_0, + RdSapSchema17_1, + RdSapSchema18_0, + RdSapSchema19_0, + RdSapSchema20_0_0, + RdSapSchema21_0_0, + RdSapSchema21_0_1, +) +from datatypes.epc.schema.common import CostAmount, DescriptionV1, Measurement +from datatypes.epc.schema.tests.helpers import from_dict + +FIXTURES = os.path.join(os.path.dirname(__file__), "fixtures") + + +def load(filename: str) -> Dict[str, Any]: + with open(os.path.join(FIXTURES, filename)) as f: + return json.load(f) # type: ignore[no-any-return] + + +class TestRdSapSchema17_0: + + @pytest.fixture + def epc(self) -> RdSapSchema17_0: + return from_dict(RdSapSchema17_0, load("17_0.json")) + + def test_schema_type(self, epc: RdSapSchema17_0) -> None: + assert epc.schema_type == "RdSAP-Schema-17.0" + + def test_uprn(self, epc: RdSapSchema17_0) -> None: + assert epc.uprn == 12457 + + def test_description_is_localised_object(self, epc: RdSapSchema17_0) -> None: + assert isinstance(epc.roofs[0].description, DescriptionV1) + assert epc.roofs[0].description.value == "(another dwelling above)" + assert epc.roofs[0].description.language == "1" + + def test_dwelling_type_is_localised_object(self, epc: RdSapSchema17_0) -> None: + assert isinstance(epc.dwelling_type, DescriptionV1) + assert epc.dwelling_type.value == "Mid-floor flat" + + def test_costs_are_cost_amount_objects(self, epc: RdSapSchema17_0) -> None: + assert isinstance(epc.heating_cost_current, CostAmount) + assert epc.heating_cost_current.value == 214 + assert epc.heating_cost_current.currency == "GBP" + + def test_suggested_improvement_saving_is_cost_amount(self, epc: RdSapSchema17_0) -> None: + assert isinstance(epc.suggested_improvements[0].typical_saving, CostAmount) + assert epc.suggested_improvements[0].typical_saving.value == 158 + + def test_flat_details_populated(self, epc: RdSapSchema17_0) -> None: + assert epc.sap_flat_details is not None + assert epc.sap_flat_details.level == 2 + + def test_floor_dimensions_use_measurement(self, epc: RdSapSchema17_0) -> None: + dim = epc.sap_building_parts[0].sap_floor_dimensions[0] + assert isinstance(dim.room_height, Measurement) + assert dim.room_height.value == 2.4 + assert dim.room_height.quantity == "metres" + + def test_alternative_improvements_present(self, epc: RdSapSchema17_0) -> None: + assert epc.alternative_improvements is not None + assert len(epc.alternative_improvements) == 1 + assert epc.alternative_improvements[0].improvement_type == "J2" + + def test_renewable_heat_incentive(self, epc: RdSapSchema17_0) -> None: + assert epc.renewable_heat_incentive.water_heating == 4818 + assert epc.renewable_heat_incentive.space_heating_existing_dwelling == 2415 + assert epc.renewable_heat_incentive.impact_of_loft_insulation is None + + def test_glazing_gap_is_string(self, epc: RdSapSchema17_0) -> None: + assert epc.glazing_gap == "16+" + + def test_lzc_energy_sources(self, epc: RdSapSchema17_0) -> None: + assert epc.lzc_energy_sources == [11] + + +class TestRdSapSchema17_1: + + @pytest.fixture + def epc(self) -> RdSapSchema17_1: + return from_dict(RdSapSchema17_1, load("17_1.json")) + + def test_schema_type(self, epc: RdSapSchema17_1) -> None: + assert epc.schema_type == "RdSAP-Schema-17.1" + + def test_description_is_localised_object(self, epc: RdSapSchema17_1) -> None: + assert isinstance(epc.roofs[0].description, DescriptionV1) + assert epc.roofs[0].description.value == "Pitched, 100 mm loft insulation" + + def test_address_line_2(self, epc: RdSapSchema17_1) -> None: + assert epc.address_line_2 == "Lower Moria" + + def test_cylinder_thermostat(self, epc: RdSapSchema17_1) -> None: + assert epc.sap_heating.cylinder_thermostat == "Y" + + def test_cylinder_insulation_thickness(self, epc: RdSapSchema17_1) -> None: + assert epc.sap_heating.cylinder_insulation_thickness == 12 + + def test_boiler_flue_type_in_heating_detail(self, epc: RdSapSchema17_1) -> None: + detail = epc.sap_heating.main_heating_details[0] + assert detail.boiler_flue_type == 1 + assert detail.main_heating_index_number == 9049 + + def test_extension_party_wall_length_is_int(self, epc: RdSapSchema17_1) -> None: + # Extension floor dimension has party_wall_length as a plain int (0) + extension = epc.sap_building_parts[1] + assert extension.identifier == "Extension 1" + dim = extension.sap_floor_dimensions[0] + assert dim.party_wall_length == 0 + + def test_extension_roof_insulation_thickness_is_int(self, epc: RdSapSchema17_1) -> None: + extension = epc.sap_building_parts[1] + assert extension.roof_insulation_thickness == 0 + + def test_renewable_heat_incentive_has_loft_impact(self, epc: RdSapSchema17_1) -> None: + assert epc.renewable_heat_incentive.impact_of_loft_insulation == -565 + + def test_multiple_roofs(self, epc: RdSapSchema17_1) -> None: + assert len(epc.roofs) == 2 + + +class TestRdSapSchema18_0: + + @pytest.fixture + def epc(self) -> RdSapSchema18_0: + return from_dict(RdSapSchema18_0, load("18_0.json")) + + def test_schema_type(self, epc: RdSapSchema18_0) -> None: + assert epc.schema_type == "RdSAP-Schema-18.0" + + def test_glazing_gap_is_integer(self, epc: RdSapSchema18_0) -> None: + assert epc.glazing_gap == 12 + assert isinstance(epc.glazing_gap, int) + + def test_room_in_roof_present(self, epc: RdSapSchema18_0) -> None: + main = epc.sap_building_parts[0] + assert main.sap_room_in_roof is not None + assert main.sap_room_in_roof.insulation == "AB" + assert main.sap_room_in_roof.construction_age_band == "G" + + def test_room_in_roof_floor_area_is_measurement(self, epc: RdSapSchema18_0) -> None: + room_in_roof = epc.sap_building_parts[0].sap_room_in_roof + assert room_in_roof is not None + assert isinstance(room_in_roof.floor_area, Measurement) + assert room_in_roof.floor_area.value == 9.0 + assert room_in_roof.floor_area.quantity == "square metres" + + def test_flat_roof_insulation_on_extension(self, epc: RdSapSchema18_0) -> None: + extension = epc.sap_building_parts[1] + assert extension.flat_roof_insulation_thickness == "NI" + + def test_description_is_localised_object(self, epc: RdSapSchema18_0) -> None: + assert isinstance(epc.walls[0].description, DescriptionV1) + + def test_three_roofs(self, epc: RdSapSchema18_0) -> None: + assert len(epc.roofs) == 3 + + def test_renewable_heat_incentive_has_solid_wall_impact(self, epc: RdSapSchema18_0) -> None: + assert epc.renewable_heat_incentive.impact_of_solid_wall_insulation == -1864 + + +class TestRdSapSchema19_0: + + @pytest.fixture + def epc(self) -> RdSapSchema19_0: + return from_dict(RdSapSchema19_0, load("19_0.json")) + + def test_schema_type(self, epc: RdSapSchema19_0) -> None: + assert epc.schema_type == "RdSAP-Schema-19.0" + + def test_windows_transmission_details(self, epc: RdSapSchema19_0) -> None: + wtd = epc.windows_transmission_details + assert wtd.u_value == 3.1 + assert wtd.data_source == 2 + assert wtd.solar_transmittance == 0.76 + + def test_description_is_localised_object(self, epc: RdSapSchema19_0) -> None: + assert isinstance(epc.roofs[0].description, DescriptionV1) + assert epc.roofs[0].description.value == "Pitched, 150 mm loft insulation" + + def test_glazing_gap_absent(self, epc: RdSapSchema19_0) -> None: + assert epc.glazing_gap is None + + def test_secondary_heating_type(self, epc: RdSapSchema19_0) -> None: + assert epc.sap_heating.secondary_heating_type == 631 + + def test_multiple_lzc_sources(self, epc: RdSapSchema19_0) -> None: + assert epc.lzc_energy_sources == [11, 9] + + def test_floor_insulation_thickness_on_building_part(self, epc: RdSapSchema19_0) -> None: + assert epc.sap_building_parts[0].floor_insulation_thickness == "NI" + + +class TestRdSapSchema20_0_0: + + @pytest.fixture + def epc(self) -> RdSapSchema20_0_0: + return from_dict(RdSapSchema20_0_0, load("20_0_0.json")) + + def test_schema_type(self, epc: RdSapSchema20_0_0) -> None: + assert epc.schema_type == "RdSAP-Schema-20.0.0" + + def test_description_is_plain_string(self, epc: RdSapSchema20_0_0) -> None: + assert isinstance(epc.roofs[0].description, str) + assert epc.roofs[0].description == "Pitched, 25 mm loft insulation" + + def test_dwelling_type_is_plain_string(self, epc: RdSapSchema20_0_0) -> None: + assert isinstance(epc.dwelling_type, str) + assert epc.dwelling_type == "Mid-terrace house" + + def test_costs_are_floats(self, epc: RdSapSchema20_0_0) -> None: + assert isinstance(epc.heating_cost_current, float) + assert epc.heating_cost_current == pytest.approx(365.98) + + def test_suggested_improvement_saving_is_numeric(self, epc: RdSapSchema20_0_0) -> None: + assert epc.suggested_improvements[0].typical_saving == 360 + + def test_indicative_cost_can_be_int(self, epc: RdSapSchema20_0_0) -> None: + # Second improvement has indicative_cost as a plain integer + assert epc.suggested_improvements[1].indicative_cost == 2000 + + def test_improvement_details_with_texts(self, epc: RdSapSchema20_0_0) -> None: + # Third improvement uses improvement_texts instead of improvement_number + third = epc.suggested_improvements[2] + assert third.improvement_details.improvement_number is None + assert third.improvement_details.improvement_texts is not None + assert third.improvement_details.improvement_texts.improvement_summary == "An improvement summary" + + def test_sap_windows_present(self, epc: RdSapSchema20_0_0) -> None: + assert len(epc.sap_windows) == 2 + assert epc.sap_windows[0].window_area == pytest.approx(200.1) + assert epc.sap_windows[0].glazing_type == 1 + + def test_addendum(self, epc: RdSapSchema20_0_0) -> None: + assert epc.addendum is not None + assert epc.addendum.stone_walls == "true" + assert epc.addendum.addendum_numbers == [1, 8] + + def test_flat_details_has_storey_count(self, epc: RdSapSchema20_0_0) -> None: + assert epc.sap_flat_details is not None + assert epc.sap_flat_details.storey_count == 3 + assert epc.sap_flat_details.unheated_corridor_length == 10 + + def test_room_in_roof_floor_area_is_plain_number(self, epc: RdSapSchema20_0_0) -> None: + room_in_roof = epc.sap_building_parts[0].sap_room_in_roof + assert room_in_roof is not None + assert room_in_roof.floor_area == 100 + assert not isinstance(room_in_roof.floor_area, Measurement) + + def test_renewable_heat_incentive_has_cavity_impact(self, epc: RdSapSchema20_0_0) -> None: + assert epc.renewable_heat_incentive.impact_of_cavity_insulation == -122 + + def test_multiple_glazed_proportion_nr(self, epc: RdSapSchema20_0_0) -> None: + assert epc.multiple_glazed_proportion_nr == "NR" + + +class TestRdSapSchema21_0_0: + + @pytest.fixture + def epc(self) -> RdSapSchema21_0_0: + return from_dict(RdSapSchema21_0_0, load("21_0_0.json")) + + def test_schema_type(self, epc: RdSapSchema21_0_0) -> None: + assert epc.schema_type == "RdSAP-Schema-21.0.0" + + def test_description_is_plain_string(self, epc: RdSapSchema21_0_0) -> None: + assert isinstance(epc.roofs[0].description, str) + + def test_pressure_test(self, epc: RdSapSchema21_0_0) -> None: + assert epc.pressure_test == 6 + + def test_wet_rooms_count(self, epc: RdSapSchema21_0_0) -> None: + assert epc.wet_rooms_count == 0 + + def test_instantaneous_wwhrs_uses_index_numbers(self, epc: RdSapSchema21_0_0) -> None: + wwhrs = epc.sap_heating.instantaneous_wwhrs + assert wwhrs.wwhrs_index_number1 == 1 + assert wwhrs.wwhrs_index_number2 == 2 + + def test_shower_outlets(self, epc: RdSapSchema21_0_0) -> None: + outlets = epc.sap_heating.shower_outlets + assert outlets is not None + assert outlets.shower_outlet.shower_wwhrs == 1 + assert outlets.shower_outlet.shower_outlet_type == 1 + + def test_boiler_ignition_type(self, epc: RdSapSchema21_0_0) -> None: + assert epc.sap_heating.main_heating_details[0].boiler_ignition_type == 1 + + def test_smart_meters(self, epc: RdSapSchema21_0_0) -> None: + src = epc.sap_energy_source + assert src.electricity_smart_meter_present == "true" + assert src.gas_smart_meter_present == "false" + + def test_pv_batteries(self, epc: RdSapSchema21_0_0) -> None: + assert epc.sap_energy_source.pv_batteries is not None + assert epc.sap_energy_source.pv_batteries.pv_battery.battery_capacity == 5 + + def test_alternative_walls(self, epc: RdSapSchema21_0_0) -> None: + part = epc.sap_building_parts[0] + assert part.sap_alternative_wall_1 is not None + assert part.sap_alternative_wall_1.wall_area == pytest.approx(10.4) + assert part.sap_alternative_wall_2 is not None + assert part.sap_alternative_wall_2.wall_area == pytest.approx(10.8) + + def test_detailed_sap_windows(self, epc: RdSapSchema21_0_0) -> None: + win = epc.sap_windows[0] + assert win.glazing_gap == 6 + assert win.window_width == pytest.approx(1.2) + assert win.window_transmission_details.u_value == pytest.approx(1.0) + + def test_mechanical_vent_fields(self, epc: RdSapSchema21_0_0) -> None: + assert epc.mechanical_vent_duct_type == 3 + assert epc.mechanical_ventilation_index_number == 12 + + def test_lighting_bulb_counts(self, epc: RdSapSchema21_0_0) -> None: + assert epc.led_fixed_lighting_bulbs_count == 10 + assert epc.cfl_fixed_lighting_bulbs_count == 5 + assert epc.incandescent_fixed_lighting_bulbs_count == 5 + + def test_open_chimneys_count(self, epc: RdSapSchema21_0_0) -> None: + assert epc.open_chimneys_count == 1 + + def test_room_in_roof_has_no_insulation_field(self, epc: RdSapSchema21_0_0) -> None: + room_in_roof = epc.sap_building_parts[0].sap_room_in_roof + assert room_in_roof is not None + assert room_in_roof.construction_age_band == "B" + assert not hasattr(room_in_roof, "insulation") + + +class TestRdSapSchema21_0_1: + + @pytest.fixture + def epc(self) -> RdSapSchema21_0_1: + return from_dict(RdSapSchema21_0_1, load("21_0_1.json")) + + def test_schema_type(self, epc: RdSapSchema21_0_1) -> None: + assert epc.schema_type == "RdSAP-Schema-21.0.1" + + def test_description_reverts_to_localised_object(self, epc: RdSapSchema21_0_1) -> None: + # Descriptions on energy elements revert to DescriptionV1 in 21.0.1 + assert isinstance(epc.roofs[0].description, DescriptionV1) + assert epc.roofs[0].description.value == "Pitched, 25 mm loft insulation" + + def test_main_heating_description_is_localised_object(self, epc: RdSapSchema21_0_1) -> None: + assert isinstance(epc.main_heating[0].description, DescriptionV1) + assert epc.main_heating[0].description.value == "Boiler and radiators, anthracite" + + def test_dwelling_type_remains_plain_string(self, epc: RdSapSchema21_0_1) -> None: + # dwelling_type does NOT revert — stays as plain str in 21.0.1 + assert isinstance(epc.dwelling_type, str) + assert epc.dwelling_type == "Mid-terrace house" + + def test_unheated_corridor_length_is_measurement(self, epc: RdSapSchema21_0_1) -> None: + # Changed from plain int (21.0.0) to Measurement object in 21.0.1 + assert epc.sap_flat_details is not None + corridor_length = epc.sap_flat_details.unheated_corridor_length + assert isinstance(corridor_length, Measurement) + assert corridor_length.value == 10 + assert corridor_length.quantity == "metres" + + def test_completion_date(self, epc: RdSapSchema21_0_1) -> None: + assert epc.completion_date == "2025-04-04" + + def test_addendum_numbers(self, epc: RdSapSchema21_0_1) -> None: + assert epc.addendum is not None + assert epc.addendum.addendum_numbers == [1, 13] + + def test_pv_battery_capacity(self, epc: RdSapSchema21_0_1) -> None: + assert epc.sap_energy_source.pv_batteries is not None + assert epc.sap_energy_source.pv_batteries.pv_battery.battery_capacity == 5 + + def test_incandescent_bulb_count(self, epc: RdSapSchema21_0_1) -> None: + assert epc.incandescent_fixed_lighting_bulbs_count == 0 diff --git a/pytest.ini b/pytest.ini index 792b27e0..4a5327c1 100644 --- a/pytest.ini +++ b/pytest.ini @@ -3,6 +3,6 @@ pythonpath = . 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 backend/hubspot_trigger_orchestrator/tests +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 backend/hubspot_trigger_orchestrator/tests datatypes/epc/schema/tests markers = integration: mark a test as an integration test From 9378c417e1240abd8d51d3a3a3d1961728175bb9 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 10 Apr 2026 13:01:37 +0000 Subject: [PATCH 41/47] fix typo in terraform state key --- infrastructure/terraform/lambda/hubspot_deal_etl/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infrastructure/terraform/lambda/hubspot_deal_etl/main.tf b/infrastructure/terraform/lambda/hubspot_deal_etl/main.tf index 516ec282..e8762337 100644 --- a/infrastructure/terraform/lambda/hubspot_deal_etl/main.tf +++ b/infrastructure/terraform/lambda/hubspot_deal_etl/main.tf @@ -11,7 +11,7 @@ data "terraform_remote_state" "pashub_to_ara" { backend = "s3" config = { bucket = "pashub-to-ara-terraform-state" - key = "ev:/${var.stage}/terraform.tfstate" + key = "env:/${var.stage}/terraform.tfstate" region = "eu-west-2" } } From 67df3a810a634f70a85dc501e64d523fc5a62079 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Fri, 10 Apr 2026 15:15:39 +0100 Subject: [PATCH 42/47] switching off rebaselining temp --- backend/engine/engine.py | 3 ++- etl/epc/Record.py | 17 ++++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/backend/engine/engine.py b/backend/engine/engine.py index e24a3b95..f7a374e0 100644 --- a/backend/engine/engine.py +++ b/backend/engine/engine.py @@ -652,7 +652,8 @@ async def model_engine(body: PlanTriggerRequest): epc_records["original_epc"]["estimated"] = False prepared_epc = EPCRecord( - epc_records=epc_records, run_mode="newdata", cleaning_data=cleaning_data, address_metadata=addr + epc_records=epc_records, run_mode="newdata", cleaning_data=cleaning_data, + # address_metadata=addr Switched off to remove injecting landlord inputs ) input_properties.append( diff --git a/etl/epc/Record.py b/etl/epc/Record.py index defe13f4..8dbb5ba5 100644 --- a/etl/epc/Record.py +++ b/etl/epc/Record.py @@ -580,7 +580,22 @@ class EPCRecord: if existing is not None and v is not None and abs(existing - v) > 1: # 1m tolerance self.landlord_differences[k] = v else: - if v != self._prepared_epc.get(k) and (not pd.isnull(v)) and (not pd.isnull(self._prepared_epc.get(k))): + + # Check if something has been cleaned. We want to avoid triggering re-baselining if we cleaned + # a value. In the address meta, it will possibly contain the original value, so we'd pick up a + # diference if the original value was something to be cleaned, we clean that value and then end up + # comparing the original value to the new clean one + cleaned_value = self._prepared_epc.get(k) + original_value = self.original_epc.get(k.replace("_", "-")) + + # We check if the value has been cleaned + if cleaned_value != original_value: + # The thing we want to compare against, is the original value + compare_to = original_value + else: + compare_to = cleaned_value + + if v != compare_to and (not pd.isnull(v)) and (not pd.isnull(self._prepared_epc.get(k))): self.landlord_differences[k] = v self._prepared_epc.update(self.landlord_differences) From 58a5c9f5d0a88f924405ef8801db61f71b16d0f8 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 10 Apr 2026 16:20:00 +0000 Subject: [PATCH 43/47] ensure all events in task handler are processed, plus error handling --- backend/utils/subtasks.py | 83 +++++++++++++++++++++++---------------- 1 file changed, 50 insertions(+), 33 deletions(-) diff --git a/backend/utils/subtasks.py b/backend/utils/subtasks.py index e5668c53..6be3a742 100644 --- a/backend/utils/subtasks.py +++ b/backend/utils/subtasks.py @@ -113,12 +113,17 @@ def task_handler(): @wraps(func) def wrapper(event: dict[str, Any], context: Any, *args, **kwargs): - logger = setup_logger() - # Parse body: Records-style SQS or plain dict event - if "Records" in event: - raw_body = event["Records"][0].get("body", {}) + records = event.get("Records", [event]) # fallback for non-SQS + + results = [] + failures = [] + + for record in records: + # Parse body + raw_body = record.get("body", record) + if isinstance(raw_body, str): try: body = json.loads(raw_body) @@ -126,43 +131,55 @@ def task_handler(): body = {} else: body = raw_body or {} - else: - body = event - # Create fresh task + subtask - logger.info("Creating task for source: %s", task_source) - task_id, subtask_id = TasksInterface.create_task( - task_source=task_source, - inputs=body, - ) - logger.info("Created task_id=%s subtask_id=%s", task_id, subtask_id) + # Create task per message + logger.info("Creating task for source: %s", task_source) + task_id, subtask_id = TasksInterface.create_task( + task_source=task_source, + inputs=body, + ) - interface = SubTaskInterface() + logger.info("Created task_id=%s subtask_id=%s", task_id, subtask_id) - interface.update_subtask_status( - subtask_id=subtask_id, - status="in progress", - ) - - try: - result = func(body, context, *args, **kwargs) + interface = SubTaskInterface() interface.update_subtask_status( subtask_id=subtask_id, - status="complete", - outputs={"result": result} if result else None, + status="in progress", ) - logger.info("Task %s completed successfully", task_id) - return result - except Exception as e: - logger.exception("Task %s failed: %s", task_id, e) - interface.update_subtask_status( - subtask_id=subtask_id, - status="failed", - outputs={"error": str(e)}, - ) - raise + try: + result = func(body, context, *args, **kwargs) + + interface.update_subtask_status( + subtask_id=subtask_id, + status="complete", + outputs={"result": result} if result else None, + ) + + logger.info("Task %s completed successfully", task_id) + results.append(result) + + except Exception as e: + logger.exception("Task %s failed: %s", task_id, e) + + interface.update_subtask_status( + subtask_id=subtask_id, + status="failed", + outputs={"error": str(e)}, + ) + + if "Records" in event: + failures.append({"itemIdentifier": record["messageId"]}) + else: + # Handle non-SQS events + raise + + if "Records" in event: + return {"batchItemFailures": failures} + + # Handle non-SQS events + return results return wrapper From 17b8f22840bbf431f440c6795fa5bab6a346db2c Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Mon, 13 Apr 2026 14:59:00 +0000 Subject: [PATCH 44/47] fix BOM issue --- asset_list/app.py | 18 +++--- backend/address2UPRN/main.py | 9 ++- backend/export/property_scenarios/main.py | 70 ++++++++++++----------- 3 files changed, 52 insertions(+), 45 deletions(-) diff --git a/asset_list/app.py b/asset_list/app.py index 5794eaf3..b0030667 100644 --- a/asset_list/app.py +++ b/asset_list/app.py @@ -74,23 +74,23 @@ def app(): """ data_folder = "/workspaces/model/asset_list" - data_filename = "Calico ARA Upload Review.xlsx" - sheet_name = "Sheet1" - postcode_column = "Postcode" - address1_column = "Units" + data_filename = "Waverley UPRN Match.xlsx" + sheet_name = "in" + postcode_column = "postcode_clean" + address1_column = "domna_found_address" address1_method = None - fulladdress_column = "Units" - address_cols_to_concat = ["Units"] + fulladdress_column = "domna_found_address" + address_cols_to_concat = [] missing_postcodes_method = None landlord_year_built = None - landlord_os_uprn = None - landlord_property_type = None # Good to include if landlord gave + landlord_os_uprn = "domna_found_uprn" + landlord_property_type = "Property Type 1" # Good to include if landlord gave landlord_built_form = None # Good to include if landlord gave landlord_wall_construction = None landlord_roof_construction = None landlord_heating_system = None landlord_existing_pv = None - landlord_property_id = "llid" + landlord_property_id = "WBC Ref" landlord_sap = None outcomes_filename = None outcomes_sheetname = None diff --git a/backend/address2UPRN/main.py b/backend/address2UPRN/main.py index 97e2037a..647d46be 100644 --- a/backend/address2UPRN/main.py +++ b/backend/address2UPRN/main.py @@ -351,9 +351,9 @@ def handler(event, context, local=False): { "body": json.dumps( { - "task_id": "e31f2f21-175b-4a91-a3ec-a6baa325e917", - "sub_task_id": "6a427b6e-1ece-4983-b1e5-9bffccc53d1d", - "s3_uri": "s3://retrofit-data-dev/ara_postcode_splitter_batches/e31f2f21-175b-4a91-a3ec-a6baa325e917/8673913b-1a88-42d7-8578-0449123d94b0/2026-02-18T11:47:00.822579_f95467f5.csv", + "sub_task_id": "d7363c83-2ef7-4474-b30f-980fd587350c", + "task_id": "a042af13-8b57-4709-ad22-ecac1ccca4bd", + "s3_uri": "s3://retrofit-data-dev/ara_raw_inputs/essex/Copy of EPC register Essex(August 2025)(in) (2).csv", } ) } @@ -424,6 +424,9 @@ def handler(event, context, local=False): bucket, key = parse_s3_uri(s3_uri) csv_data = read_csv_from_s3_dict(bucket, key) df = pd.DataFrame(csv_data) + df.columns = [ + c.lstrip("\ufeff") for c in df.columns + ] # strip BOM from column names logger.info(f"Loaded {len(df)} rows from S3") except Exception as s3_error: logger.error(f"Failed to read data from S3: {s3_error}") diff --git a/backend/export/property_scenarios/main.py b/backend/export/property_scenarios/main.py index f3ea0100..b179531d 100644 --- a/backend/export/property_scenarios/main.py +++ b/backend/export/property_scenarios/main.py @@ -26,15 +26,14 @@ def has_solar_with_battery(materials_list: Optional[List[Dict[str, Any]]]) -> bo :return: """ for m in materials_list or []: - if ( - m.get("type") == "solar_pv" - and m.get("includes_battery") is True - ): + if m.get("type") == "solar_pv" and m.get("includes_battery") is True: return True return False -def process_export(payload: ExportRequest, session: Session) -> Dict[Union[str, int], pd.DataFrame]: +def process_export( + payload: ExportRequest, session: Session +) -> Dict[Union[str, int], pd.DataFrame]: export_files: Dict[Union[str, int], pd.DataFrame] = {} db_methods = DbMethods(session) @@ -52,7 +51,9 @@ def process_export(payload: ExportRequest, session: Session) -> Dict[Union[str, logger.info("Retrieved %s plans for export", len(plans_df)) if plans_df.empty: - logger.info("Empty plans dataframe - no plans to export. Returning empty export.") + logger.info( + "Empty plans dataframe - no plans to export. Returning empty export." + ) return export_files plan_ids: List[int] = plans_df["id"].tolist() recommendations_df: pd.DataFrame = db_methods.get_recommendations(plan_ids) @@ -61,13 +62,12 @@ def process_export(payload: ExportRequest, session: Session) -> Dict[Union[str, recommendations_df = db_methods.attach_materials(recommendations_df) - recommendations_df["has_solar_with_battery"] = ( - recommendations_df["materials"].apply(has_solar_with_battery) - ) + recommendations_df["has_solar_with_battery"] = recommendations_df[ + "materials" + ].apply(has_solar_with_battery) - _filter = ( - (recommendations_df["measure_type"] == "solar_pv") - & (recommendations_df["has_solar_with_battery"]) + _filter = (recommendations_df["measure_type"] == "solar_pv") & ( + recommendations_df["has_solar_with_battery"] ) recommendations_df.loc[_filter, "measure_type"] = ( @@ -83,10 +83,13 @@ def process_export(payload: ExportRequest, session: Session) -> Dict[Union[str, else: scenario_recs = recommendations_df[ recommendations_df["scenario_id"] == group_key - ] + ] if scenario_recs.empty: - logger.info("No recommendations found for group_key %s - skipping export for this group", group_key) + logger.info( + "No recommendations found for group_key %s - skipping export for this group", + group_key, + ) continue measures_df: pd.DataFrame = scenario_recs[ @@ -99,14 +102,12 @@ def process_export(payload: ExportRequest, session: Session) -> Dict[Union[str, values="estimated_cost", ).reset_index() - pivot["total_retrofit_cost"] = ( - pivot.drop(columns=["property_id", "plan_name"]).sum(axis=1) - ) + pivot["total_retrofit_cost"] = pivot.drop( + columns=["property_id", "plan_name"] + ).sum(axis=1) post_sap: pd.DataFrame = ( - scenario_recs.groupby("property_id")[["sap_points"]] - .sum() - .reset_index() + scenario_recs.groupby("property_id")[["sap_points"]].sum().reset_index() ) df: pd.DataFrame = ( @@ -117,7 +118,9 @@ def process_export(payload: ExportRequest, session: Session) -> Dict[Union[str, df["sap_points"] = df["sap_points"].fillna(0) df["predicted_post_works_sap"] = df["current_sap_points"] + df["sap_points"] - df["predicted_post_works_epc"] = df["predicted_post_works_sap"].apply(sap_to_epc) + df["predicted_post_works_epc"] = df["predicted_post_works_sap"].apply( + sap_to_epc + ) export_files[group_key] = df @@ -128,22 +131,17 @@ def process_export(payload: ExportRequest, session: Session) -> Dict[Union[str, # Lambda Handler # ============================================================ -def handler(event: Mapping[str, Any], context: Optional[Any]) -> Mapping[str, Union[int, str]]: + +def handler( + event: Mapping[str, Any], context: Optional[Any] +) -> Mapping[str, Union[int, str]]: """ Example event: body_dict = { "task_id": "test", "subtask_id": "test", - "portfolio_id": 655, - "scenario_ids": [], - "default_plans_only": True, - } - - body_dict = { - "task_id": "test", - "subtask_id": "test", - "portfolio_id": 655, - "scenario_ids": [1174], + "portfolio_id": 670, + "scenario_ids": [1199], "default_plans_only": False, } :param event: Lambda event containing export request details @@ -167,8 +165,14 @@ def handler(event: Mapping[str, Any], context: Optional[Any]) -> Mapping[str, Un with db_read_session() as session: exported_files = process_export(payload, session) + output_path = f"/tmp/export_{payload.portfolio_id}.xlsx" + with pd.ExcelWriter(output_path, engine="openpyxl") as writer: + for group_key, df in exported_files.items(): + sheet_name = str(group_key)[:31] # Excel sheet name limit + df.to_excel(writer, sheet_name=sheet_name, index=False) + + logger.info("Exported to %s", output_path) # TODO: Need to handle the exported files - e.g. upload to s3 and email a presigned url - _ = exported_files return { "statusCode": 200, "body": json.dumps({}), From 98c9a1df74f9330ef88542b76918cbc71915f310 Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Mon, 13 Apr 2026 15:15:42 +0000 Subject: [PATCH 45/47] added fix for utf --- backend/address2UPRN/main.py | 3 --- utils/s3.py | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/backend/address2UPRN/main.py b/backend/address2UPRN/main.py index 647d46be..79c0de69 100644 --- a/backend/address2UPRN/main.py +++ b/backend/address2UPRN/main.py @@ -424,9 +424,6 @@ def handler(event, context, local=False): bucket, key = parse_s3_uri(s3_uri) csv_data = read_csv_from_s3_dict(bucket, key) df = pd.DataFrame(csv_data) - df.columns = [ - c.lstrip("\ufeff") for c in df.columns - ] # strip BOM from column names logger.info(f"Loaded {len(df)} rows from S3") except Exception as s3_error: logger.error(f"Failed to read data from S3: {s3_error}") diff --git a/utils/s3.py b/utils/s3.py index 242e0db5..930e2e15 100644 --- a/utils/s3.py +++ b/utils/s3.py @@ -330,7 +330,7 @@ def read_csv_from_s3(bucket_name: str, filepath: str) -> list[dict[str, str]]: body = s3_object["Body"].read() # Use StringIO to create a file-like object from the string - csv_data = StringIO(body.decode("utf-8")) + csv_data = StringIO(body.decode("utf-8-sig")) # Use csv library to read it into a list of dictionaries reader = csv.DictReader(csv_data) From ef366f1cd5eade72cd479767699c7bb9cca96df7 Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Mon, 13 Apr 2026 15:20:10 +0000 Subject: [PATCH 46/47] be the same as main --- backend/export/property_scenarios/main.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/backend/export/property_scenarios/main.py b/backend/export/property_scenarios/main.py index b179531d..0ab59e27 100644 --- a/backend/export/property_scenarios/main.py +++ b/backend/export/property_scenarios/main.py @@ -140,8 +140,16 @@ def handler( body_dict = { "task_id": "test", "subtask_id": "test", - "portfolio_id": 670, - "scenario_ids": [1199], + "portfolio_id": 655, + "scenario_ids": [], + "default_plans_only": True, + } + + body_dict = { + "task_id": "test", + "subtask_id": "test", + "portfolio_id": 655, + "scenario_ids": [1174], "default_plans_only": False, } :param event: Lambda event containing export request details @@ -165,14 +173,8 @@ def handler( with db_read_session() as session: exported_files = process_export(payload, session) - output_path = f"/tmp/export_{payload.portfolio_id}.xlsx" - with pd.ExcelWriter(output_path, engine="openpyxl") as writer: - for group_key, df in exported_files.items(): - sheet_name = str(group_key)[:31] # Excel sheet name limit - df.to_excel(writer, sheet_name=sheet_name, index=False) - - logger.info("Exported to %s", output_path) # TODO: Need to handle the exported files - e.g. upload to s3 and email a presigned url + _ = exported_files return { "statusCode": 200, "body": json.dumps({}), From 9c1181475ea5b4ba41d3b2c2e1f87ca73b3f003b Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Mon, 13 Apr 2026 15:20:55 +0000 Subject: [PATCH 47/47] be the same as main --- backend/export/property_scenarios/main.py | 50 ++++++++++------------- 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/backend/export/property_scenarios/main.py b/backend/export/property_scenarios/main.py index 0ab59e27..f3ea0100 100644 --- a/backend/export/property_scenarios/main.py +++ b/backend/export/property_scenarios/main.py @@ -26,14 +26,15 @@ def has_solar_with_battery(materials_list: Optional[List[Dict[str, Any]]]) -> bo :return: """ for m in materials_list or []: - if m.get("type") == "solar_pv" and m.get("includes_battery") is True: + if ( + m.get("type") == "solar_pv" + and m.get("includes_battery") is True + ): return True return False -def process_export( - payload: ExportRequest, session: Session -) -> Dict[Union[str, int], pd.DataFrame]: +def process_export(payload: ExportRequest, session: Session) -> Dict[Union[str, int], pd.DataFrame]: export_files: Dict[Union[str, int], pd.DataFrame] = {} db_methods = DbMethods(session) @@ -51,9 +52,7 @@ def process_export( logger.info("Retrieved %s plans for export", len(plans_df)) if plans_df.empty: - logger.info( - "Empty plans dataframe - no plans to export. Returning empty export." - ) + logger.info("Empty plans dataframe - no plans to export. Returning empty export.") return export_files plan_ids: List[int] = plans_df["id"].tolist() recommendations_df: pd.DataFrame = db_methods.get_recommendations(plan_ids) @@ -62,12 +61,13 @@ def process_export( recommendations_df = db_methods.attach_materials(recommendations_df) - recommendations_df["has_solar_with_battery"] = recommendations_df[ - "materials" - ].apply(has_solar_with_battery) + recommendations_df["has_solar_with_battery"] = ( + recommendations_df["materials"].apply(has_solar_with_battery) + ) - _filter = (recommendations_df["measure_type"] == "solar_pv") & ( - recommendations_df["has_solar_with_battery"] + _filter = ( + (recommendations_df["measure_type"] == "solar_pv") + & (recommendations_df["has_solar_with_battery"]) ) recommendations_df.loc[_filter, "measure_type"] = ( @@ -83,13 +83,10 @@ def process_export( else: scenario_recs = recommendations_df[ recommendations_df["scenario_id"] == group_key - ] + ] if scenario_recs.empty: - logger.info( - "No recommendations found for group_key %s - skipping export for this group", - group_key, - ) + logger.info("No recommendations found for group_key %s - skipping export for this group", group_key) continue measures_df: pd.DataFrame = scenario_recs[ @@ -102,12 +99,14 @@ def process_export( values="estimated_cost", ).reset_index() - pivot["total_retrofit_cost"] = pivot.drop( - columns=["property_id", "plan_name"] - ).sum(axis=1) + pivot["total_retrofit_cost"] = ( + pivot.drop(columns=["property_id", "plan_name"]).sum(axis=1) + ) post_sap: pd.DataFrame = ( - scenario_recs.groupby("property_id")[["sap_points"]].sum().reset_index() + scenario_recs.groupby("property_id")[["sap_points"]] + .sum() + .reset_index() ) df: pd.DataFrame = ( @@ -118,9 +117,7 @@ def process_export( df["sap_points"] = df["sap_points"].fillna(0) df["predicted_post_works_sap"] = df["current_sap_points"] + df["sap_points"] - df["predicted_post_works_epc"] = df["predicted_post_works_sap"].apply( - sap_to_epc - ) + df["predicted_post_works_epc"] = df["predicted_post_works_sap"].apply(sap_to_epc) export_files[group_key] = df @@ -131,10 +128,7 @@ def process_export( # Lambda Handler # ============================================================ - -def handler( - event: Mapping[str, Any], context: Optional[Any] -) -> Mapping[str, Union[int, str]]: +def handler(event: Mapping[str, Any], context: Optional[Any]) -> Mapping[str, Union[int, str]]: """ Example event: body_dict = {