mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
Merge branch 'main' into feature/abri-api-integration
This commit is contained in:
commit
6a7cc68c9a
96 changed files with 14897 additions and 350 deletions
|
|
@ -169,6 +169,34 @@ Pattern: `with E.session() as (ctx,page): E.goto(...); E.set_text/set_select(...
|
|||
lines 17/18). This drove the first campaign mapper fix — see Banked findings.
|
||||
|
||||
## Banked findings (fold new ones in here as the corpus grows)
|
||||
- **Register cost-triangulation (no Elmhurst needed):** the register lodges
|
||||
`heating_cost_current` / `hot_water_cost_current` / `lighting_cost_current`
|
||||
at RETAIL prices — absolute £ never match SAP Table-12 spec prices, but for
|
||||
same-fuel end-uses the RATIOS localize a divergence without a worksheet.
|
||||
Normalize by lighting: `(lodged_hw/lodged_light) / (our_hw/our_light)` ≈ 1
|
||||
means our water cascade agrees with the lodging software; »1 flags the
|
||||
divergent end-use. CAVEAT: the register's "hot water cost" INCLUDES the
|
||||
SAP (64a) electric-shower electricity — add `instant_shower` cost to our
|
||||
hw bucket before comparing on any electric-shower cert, else every
|
||||
shower-outlet-type-2 cert false-positives at ratio ~2 (corpus-1000 band
|
||||
triage, 36 certs, mean ratio 2.0 → 1.04 after adding (64a)).
|
||||
- **Lodging-software conventions diverge from Elmhurst on two known axes**
|
||||
(mark such corpus certs ⚠, don't tune): (a) out-of-range-PSR database
|
||||
heat pumps — SAP 10.2 N2 footnotes 44/45 mandate the reciprocal extension
|
||||
toward 100% at 2× the record's largest PSR (Elmhurst does this, golden
|
||||
case 56); at least one software bills 100% direct electric + standard
|
||||
schedule instead (corpus 4510053280, lodged 51 vs engine/Elmhurst-method
|
||||
75). (b) a lodged 0 extract fans — Elmhurst renders 0 as "unknown" →
|
||||
Table 5 age-band default (worksheet case 46); LIG-21.0 lodgements
|
||||
(calculation_software_version 10.2.2.0) honour the 0 literally (corpus
|
||||
quadruplet at Δ −2.56, fans account for 1.42).
|
||||
- **"insulated (assumed)" roof ≠ observed retrofit:** RdSAP renders
|
||||
"(assumed)" when insulation PRESENCE is an age-band assumption — Table 18
|
||||
age default governs, for BOTH the "ND" and "NI" thickness sentinels
|
||||
(which are lodging-software noise). The §5.11.4 50 mm row (U 0.68) is
|
||||
only for the bare "insulated" description (observed retrofit, depth
|
||||
unknown). Fixed in u_roof (corpus 74.2% → 75.5%); unit-pinned in
|
||||
test_rdsap_uvalues.
|
||||
- **MAPPER GAP — cylinder insulation thickness dropped (RdSAP-17.0+):** the mapper
|
||||
carries `cylinder_size` + `cylinder_insulation_type` but NOT
|
||||
`cylinder_insulation_thickness` → `EpcPropertyData.sap_heating.cylinder_
|
||||
|
|
|
|||
10
CONTEXT.md
10
CONTEXT.md
|
|
@ -103,6 +103,10 @@ _Avoid_: patches (deprecated), corrections, manual EPC, edits
|
|||
|
||||
**Landlord-Description Classification**:
|
||||
Resolving a **Landlord Description** (unbounded free-text a landlord supplies for one component — "CWI" / "Cav filled" / "cavity insulated" all name one thing) onto a **Recognised Internal Description** via an LLM classifier, persisted in the `landlord_*_overrides` table (`source=classifier`) as a reviewed cache. Four vocabularies are kept **distinct** and must not be conflated: a **Landlord Description** (unbounded input); a **Recognised Internal Description** (the closed target taxonomy — e.g. a `MainHeatingSystemType` archetype — each binding to a Simulation Overlay); a **Lodged Description** (the gov-EPC `main_heating[].description` rendering, e.g. "Room heaters, electric" — only an example of which system *types* occur, never a map key); and the **SAP main heating code** (Table 4a/4b, what the calculator consumes). The classifier maps Landlord → Recognised Internal → SAP code. When it cannot confidently place the text it emits **`None`** (no overlay → the lodged EPC stands, surfaced to the user as "no suitable match"), **never the nearest wrong archetype** — the target taxonomy must be complete enough that a real system always has a correct home, so the classifier never overflows into a garbage-drawer archetype (ADR-0041).
|
||||
|
||||
**Heating Companion Set**:
|
||||
The coherent SAP inputs a **main heating system** override drags alongside the **SAP main heating code** so the **Effective EPC** reads as one internally-consistent system, not a hybrid of the new code and the *replaced* system's leftovers — heating **category** (SAP Table 4a group: gas→2, heat pump→4, network→6, storage→7, electric underfloor→8, warm air→9, room heater→10), **charge/heating control** (Table 4e group), **natural fuel**, and **meter** (see **Off-Peak Meter**). Companions divide by how firmly the archetype fixes them (ADR-0048): **category and control are archetype-forced** — always written, never inherited, because a wrong value silently mis-scores or mis-bills (a leftover storage **category 7** makes the Table 12a resolver bill peaky room heaters at the all-night rate; a leftover storage **control 2401** adds a +0.7 °C Table-4e penalty a room heater should not carry). **Fuel and meter defer** only where multiple values are genuinely coherent — fuel for an ambiguous carrier (community heating: gas CHP / biomass / waste heat), meter for a meter-flexible system (room heaters run on either; screed underfloor per ADR-0046) — while a meter-*locked* system still forces (storage/CPSU/HHRSH→Dual, non-electric→Single, ADR-0035). Because the overlay composes last-wins, an **unset (`None`)** forced companion is *inherited from the replaced system*, not cleared — so an archetype the overlay cannot yet fully companion (heat pump / community / underfloor / electric boiler control defaults are unmapped) is **logged as an error and continues** (log-not-raise, matching `flag_fuel_mismatch`), making the gap visible for review rather than shipping a silently-incoherent cert; the intended end-state is to fill each and flip the log to a raise.
|
||||
_Avoid_: coherent companions (informal), dragged fields, heating defaults
|
||||
_Avoid_: "the LLM mapper is unreliable" (the failure mode is a too-small target taxonomy, not LLM language ability); conflating the landlord input vocabulary with the gov-EPC lodged rendering or the RdSAP entry-tool catalogue; treating a deterministic dict as a *replacement* for the LLM rather than a reviewed cache of its output
|
||||
|
||||
**Lodgement Sentinels (ND / NI / AB)**:
|
||||
|
|
@ -313,7 +317,11 @@ The two competing **Measure Options** for insulating a *solid* (non-cavity) main
|
|||
_Avoid_: solid wall insulation (that names the pair, not one Option), cladding, drylining
|
||||
|
||||
**Wall Insulation Eligibility**:
|
||||
The rule fixing which wall Option(s) the main-wall **Recommendation** offers, by wall construction then planning status. By construction: **cavity** → cavity fill only (never solid insulation); **solid brick** / **system-built** → IWI + EWI; **timber-frame** → IWI only (EWI is not constructable); **cob** / **granite or whinstone** / **sandstone or limestone** → none (breathable fabric — standard insulation risks trapping moisture). Then, as planning gates: a **conservation area** or **flat** removes EWI (external-appearance / whole-block constraints), and a **listed** or **heritage** building removes **both** EWI and IWI (protected fabric).
|
||||
The rule fixing which wall Option(s) the main-wall **Recommendation** offers, by wall construction then planning status. By construction: **cavity** → cavity fill only (never solid insulation); **solid brick** / **system-built** → IWI + EWI; **timber-frame** → IWI only (EWI is not constructable); **cob** / **granite or whinstone** / **sandstone or limestone** → none (breathable fabric — standard insulation risks trapping moisture). Then, as planning gates: a **conservation area** or **flat** removes EWI (external-appearance / whole-block constraints), and a **listed** or **heritage** building removes **both** EWI and IWI (protected fabric). Finally the **Wall U-Value Gate** applies.
|
||||
|
||||
**Wall U-Value Gate**:
|
||||
The eligibility rule that a wall insulation Option is offered **only when it would lower the wall's derived U-value** (by more than a 0.01 W/m²K float-noise guard) — comparing `u_wall` on the effective wall's current state vs its post-measure state (ADR-0051). Kills the **no-op fabric offer**: on a modern construction age band (e.g. band L, 2012–2022) RdSAP's cascade assigns the as-built wall its at-regs U-value and added insulation resolves to the *same* value, so the measure costs thousands for +0.00 SAP. The gate is physical possibility only — how *much* improvement is worth the cost stays with the Optimiser (ADR-0016/0024).
|
||||
_Avoid_: no-op filter (it gates generation, not scored output), U-value threshold (there is no absolute target-U; the comparison is before-vs-after)
|
||||
_Avoid_: restricted measures (legacy collapsed conservation/listed/heritage into one boolean — they now gate different Options, so keep them distinct)
|
||||
|
||||
**Roof Insulation Eligibility**:
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@ from orchestration.sharepoint_renamer_orchestrator import SharepointRenamerOrche
|
|||
from utils.sharepoint.domna_sharepoint_client import DomnaSharepointClient
|
||||
from utils.sharepoint.domna_sites import DomnaSites
|
||||
|
||||
CSV_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "sero_address_list.csv")
|
||||
CSV_PATH = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)), "sero_address_list.csv"
|
||||
)
|
||||
|
||||
|
||||
def handler(event: dict[str, Any], context: Any) -> None:
|
||||
|
|
|
|||
|
|
@ -1,182 +1,366 @@
|
|||
UPRN,Address,Postcode
|
||||
U1027392,"26 Silverdale Road, Oprington",BR5 2LT
|
||||
U1003906,"54 Barnsdale Crescent, Oprington",BR5 2AX
|
||||
U1034479,"90 Mead Way, Bromley",BR2 9EU
|
||||
U1005549,"79 Lower Gravel Road, Bromley",BR2 8LP
|
||||
U1016743,"16 Princes Plain, Bromley",BR2 8LE
|
||||
U1041937,"75 Turpington Lane, Bromley",BR2 8JD
|
||||
U1034805,"38 Narrow Way, Bromley",BR2 8JB
|
||||
U1041933,"31 Turpington Lane, Bromley",BR2 8JA
|
||||
U1037833,"3 Stiles Close, Bromley",BR2 8EQ
|
||||
U1042734,"86 Whitebeam Avenue, Bromley",BR2 8DW
|
||||
U1042575,"90 Whitebeam Avenue, Bromley",BR2 8DW
|
||||
U1033177,"30 Larch Way, Bromley",BR2 8DU
|
||||
U1027989,"5 Larch Way, Bromley",BR2 8DT
|
||||
U1012309,"13 Almond Close, Bromley",BR2 8DS
|
||||
U1022525,"30 Lovelace Avenue, Bromley",BR2 8DQ
|
||||
U1047613,"13 Whitebeam Avenue, Bromley",BR2 8DJ
|
||||
U1027549,"14 Thorn Close, Bromley",BR2 8DH
|
||||
U1022726,"31 Hornbeam Way, Bromley",BR2 8DB
|
||||
U1021308,"70 Faringdon Avenue, Bromley",BR2 8BU
|
||||
U1026958,"77 Faringdon Avenue, Bromley",BR2 8BT
|
||||
U1007553,"115d Faringdon Avenue, Bromley",BR2 8BT
|
||||
U1032132,"115f Faringdon Avenue, Bromley",BR2 8BT
|
||||
U1014627,"81 Faringdon Avenue, Bromley",BR2 8BT
|
||||
U1009607,"20 Parkfield Way, Bromley",BR2 8AF
|
||||
U1027838,"15 Holmcroft Way, Bromley",BR2 8AD
|
||||
U1052376,"3 Shoreham Way, Bromley",BR2 7PU
|
||||
U1015499,"25 Boughton Avenue, Bromley",BR2 7PL
|
||||
U1005741,"2 Malling Way, Bromley",BR2 7PJ
|
||||
U1019906,"26 Eastry Avenue, Bromley",BR2 7PF
|
||||
U1021769,"49 Eastry Avenue, Bromley",BR2 7PE
|
||||
U1031121,"32 Laburnum Way, Bromley",BR2 8BZ
|
||||
U1022281,"35 Kingsdown Way, Bromley",BR2 7PT
|
||||
U1035115,"30 Thorn Close, Bromley",BR2 8DH
|
||||
U1027493,"35 Sudbury Crescent, Bromley",BR1 4PY
|
||||
U1016746,"68 Princes Plain, Bromley",BR2 8LE
|
||||
U1005168,"38 Holbrook Way, Bromley",BR2 8EE
|
||||
U1036446,"14 Meath Close, Oprington",BR5 2HF
|
||||
U1010452,"5 Canbury Path, Oprington",BR5 2EU
|
||||
U1019785,"50 Cray Valley Road, Oprington",BR5 2EZ
|
||||
U1024065,"64 Marion Crescent, Oprington",BR5 2HD
|
||||
U1042248,"16 Stanley Way, Oprington",BR5 2HE
|
||||
U1029229,"2 Meath Close, Oprington",BR5 2HF
|
||||
U1037768,"13 Silverdale Road, Oprington",BR5 2LU
|
||||
U1014589,"71 Empress Drive, Chislehurst",BR7 5BQ
|
||||
U1024698,"4 Palace View, Bromley",BR1 3EL
|
||||
U1052536,"12 Thorn Close, Bromley",BR2 8DH
|
||||
U1022018,"12 Hazel Walk, Bromley",BR2 8DF
|
||||
U1007728,"2 Hazel Walk, Bromley",BR2 8DF
|
||||
U1002456,"54 Birch Row, Bromley",BR2 8DA
|
||||
U1020349,"21 Laburnum Way, Bromley",BR2 8BY
|
||||
U1032129,"78 Faringdon Avenue, Bromley",BR2 8BU
|
||||
U1032130,"86 Faringdon Avenue, Bromley",BR2 8BU
|
||||
U1021824,"115g Faringdon Avenue, Bromley",BR2 8BT
|
||||
U1021827,"121 Faringdon Avenue, Bromley",BR2 8BT
|
||||
U1026960,"105 Faringdon Avenue, Bromley",BR2 8BT
|
||||
U1010578,"6 Cranbrook Close, Bromley",BR2 7QA
|
||||
U1024709,"3 Parkfield Way, Bromley",BR2 8AE
|
||||
U1024580,"24 Parkfield Way, Bromley",BR2 8AF
|
||||
U1011190,"11 Shoreham Way, Bromley",BR2 7PU
|
||||
U1011191,"32 Shoreham Way, Bromley",BR2 7PU
|
||||
U1021535,"4 Chilham Way, Bromley",BR2 7PR
|
||||
U1007556,"11 Farleigh Avenue, Bromley",BR2 7PP
|
||||
U1010255,"7 Boughton Avenue, Bromley",BR2 7PL
|
||||
U1034810,"24 Newbury Road, Bromley",BR2 0QW
|
||||
U1009032,"10 Malling Way, Bromley",BR2 7PJ
|
||||
U1004686,"55 Baston Road, Bromley",BR2 7BD
|
||||
U1032061,"30 Eastry Avenue, Bromley",BR2 7PF
|
||||
U1010582,"13 Cranworth Cottages, Keston",BR2 6DB
|
||||
U1009511,"27 Newbury Road, Bromley",BR2 0QN
|
||||
U1037954,"28 Treewall Gardens, Bromley",BR1 5BT
|
||||
U1014793,"59 Headcorn Road, Bromley",BR1 4SQ
|
||||
U1005123,"24 Bonville Road, Bromley",BR1 4QA
|
||||
U1037872,"20 Sudbury Crescent, Bromley",BR1 4PZ
|
||||
U1022305,"18a Lansdowne Road, Bromley",BR1 3LZ
|
||||
U1035052,"1 Sudbury Crescent, Bromley",BR1 4PY
|
||||
U1042298,"39 Sudbury Crescent, Bromley",BR1 4PY
|
||||
U1019564,"32 Brook Lane, Bromley",BR1 4PU
|
||||
U1024511,"81 Nightingale Lane, Bromley",BR1 2SA
|
||||
U1032133,"119 Faringdon Avenue, Bromley",BR2 8BT
|
||||
U1032134,"125 Faringdon Avenue, Bromley",BR2 8BT
|
||||
U1032131,"93 Faringdon Avenue, Bromley",BR2 8BT
|
||||
U1021825,"117a Faringdon Avenue, Bromley",BR2 8BT
|
||||
U1010107,"42 Birch Row, Bromley",BR2 8DA
|
||||
U1016880,"25 Almond Way, Bromley",BR2 8DR
|
||||
U1038107,"152 Whitebeam Avenue, Bromley",BR2 8DW
|
||||
U1052455,"10 Stiles Close, Bromley",BR2 8EQ
|
||||
U1028328,"76 Magpie Hall Lane, Bromley",BR2 8ER
|
||||
U1020064,"28 Green Way, Bromley",BR2 8EY
|
||||
U1041934,"51 Turpington Lane, Bromley",BR2 8JA
|
||||
U1005696,"158 Magpie Hall Lane, Bromley",BR2 8JG
|
||||
U1030892,"140 Poverest Road, Oprington",BR5 1RH
|
||||
U1011072,"45 Rookery Gardens, Oprington",BR5 4BA
|
||||
U1031058,"303 Keedonwood Road, Bromley",BR1 4QR
|
||||
U1052429,"76 Southover, Bromley",BR1 4RY
|
||||
U1042153,"4 Scotts Road, Bromley",BR1 3QD
|
||||
U1037814,"42 Stanley Road, Bromley",BR2 9JH
|
||||
U1008158,"71 Lower Gravel Road, Bromley",BR2 8LP
|
||||
U1032062,"46 Eastry Avenue, Bromley",BR2 7PF
|
||||
U1016742,"4 Princes Plain, Bromley",BR2 8LE
|
||||
U1038106,"84 Whitebeam Avenue, Bromley",BR2 8DW
|
||||
U1013831,"33 Ash Row, Bromley",BR2 8DZ
|
||||
U1005742,"18 Malling Way, Bromley",BR2 7PJ
|
||||
U1042572,"37 Whitebeam Avenue, Bromley",BR2 8DJ
|
||||
U1005167,"30 Holbrook Way, Bromley",BR2 8EE
|
||||
U1024581,"30 Parkfield Way, Bromley",BR2 8AF
|
||||
U1024050,"10 Marden Avenue, Bromley",BR2 7PX
|
||||
U1020328,"31 Kingsdown Way, Bromley",BR2 7PT
|
||||
U1020327,"5 Kingsdown Way, Bromley",BR2 7PT
|
||||
U1005131,"10 Boughton Avenue, Bromley",BR2 7PL
|
||||
U1023415,"27 Malling Way, Bromley",BR2 7PJ
|
||||
U1052580,"25 Trentham Drive, Oprington",BR5 2EP
|
||||
U1026447,"50 Princes Plain, Bromley",BR2 8LE
|
||||
U1007432,"21 Detling Road, Bromley",BR1 4SH
|
||||
U1053388,"52 Whitebeam Avenue, Bromley",BR2 8DL
|
||||
U1022307,"20 Larch Way, Bromley",BR2 8DU
|
||||
U1027743,"17 Whitebeam Avenue, Bromley",BR2 8DJ
|
||||
U1022533,"81 Lower Gravel Road, Bromley",BR2 8LP
|
||||
U1014630,"118 Faringdon Avenue, Bromley",BR2 8BU
|
||||
U1030897,"60 Princes Plain, Bromley",BR2 8LE
|
||||
U1022931,"15 Lennard Road, Bromley",BR2 8LN
|
||||
U1042735,"108 Whitebeam Avenue, Bromley",BR2 8DW
|
||||
U1013830,"31 Ash Row, Bromley",BR2 8DZ
|
||||
U1020368,"11 Larch Way, Bromley",BR2 8DT
|
||||
U1020369,"13 Larch Way, Bromley",BR2 8DT
|
||||
U1042576,"112 Whitebeam Avenue, Bromley",BR2 8DW
|
||||
U1027830,"26 Holbrook Way, Bromley",BR2 8EE
|
||||
U1016744,"18 Princes Plain, Bromley",BR2 8LE
|
||||
U1041772,"13 Stanley Road, Bromley",BR2 9JE
|
||||
U1042573,"47 Whitebeam Avenue, Bromley",BR2 8DJ
|
||||
U1008151,"61 Lovelace Avenue, Bromley",BR2 8EA
|
||||
U1034985,"272 Southborough Lane, Bromley",BR2 8AS
|
||||
U1007947,"8 Laburnum Way, Bromley",BR2 8BZ
|
||||
U1027744,"33 Whitebeam Avenue, Bromley",BR2 8DJ
|
||||
U1038102,"14 Whitebeam Avenue, Bromley",BR2 8DL
|
||||
U1027745,"148 Whitebeam Avenue, Bromley",BR2 8DW
|
||||
U1020208,"16 Holbrook Way, Bromley",BR2 8EE
|
||||
U1023463,"30 MANOR WAY, Bromley",BR2 8ES
|
||||
U1032647,"32 Narrow Way, Bromley",BR2 8JB
|
||||
U1033406,"67 Lower Gravel Road, Bromley",BR2 8LP
|
||||
U1000649,"42 Barnsdale Crescent, Oprington",BR5 2AX
|
||||
U1036332,"50 Marion Crescent, Oprington",BR5 2HD
|
||||
U1038103,"29 Whitebeam Avenue, Bromley",BR2 8DJ
|
||||
U1042574,"66 Whitebeam Avenue, Bromley",BR2 8DL
|
||||
U1042256,"2 Stiles Close, Bromley",BR2 8EQ
|
||||
U1034017,"48 Princes Plain, Bromley",BR2 8LE
|
||||
U1041435,"64 Princes Plain, Bromley",BR2 8LE
|
||||
U1053386,"27 Whitebeam Avenue, Bromley",BR2 8DJ
|
||||
U1038104,"32 Whitebeam Avenue, Bromley",BR2 8DL
|
||||
U1019567,"8 Broom Close, Bromley",BR2 8EU
|
||||
U1023539,"2 Marsham Close, Chislehurst",BR7 6JD
|
||||
U1037465,"6 Princes Plain, Bromley",BR2 8LE
|
||||
U1042733,"68 Whitebeam Avenue, Bromley",BR2 8DL
|
||||
U1004442,"7 Birch Row, Bromley",BR2 8BX
|
||||
U1005006,"44 Birch Row, Bromley",BR2 8DA
|
||||
U1035331,"104 Whitebeam Avenue, Bromley",BR2 8DW
|
||||
U1024017,"13 Manor Way, Bromley",BR2 8ES
|
||||
U1014078,"43 Aylesbury Road, Bromley",BR2 0QR
|
||||
U1052186,"4 Ravensleigh Gardens, Bromley",BR1 5SN
|
||||
U1037450,"70 Pontefract Road, Bromley",BR1 4RB
|
||||
U1031294,"70 London Lane, Bromley",BR1 4HE
|
||||
U1014077,"32 Aylesbury Road, Bromley",BR2 0QP
|
||||
U1030962,"7 Ravensleigh Gardens, Bromley",BR1 5SN
|
||||
U1042232,"154 Southover, Bromley",BR1 4RZ
|
||||
U1024484,"55 Mounthurst Road, Bromley",BR2 7PG
|
||||
U1007701,"46 Harwood Avenue, Bromley",BR1 3DU
|
||||
U1020199,"78 Hillside Road, Bromley",BR2 0ST
|
||||
U1036758,"46 Newbury Road, Bromley",BR2 0QW
|
||||
U1009369,"17 Minster Road, Bromley",BR1 4DY
|
||||
U1009194,"84 Mays Hill Road, Bromley",BR2 0HT
|
||||
U1013358,"3 Bird In Hand Lane, Bromley",BR1 2NA
|
||||
U1009202,"63 Mead Way, Bromley",BR2 9ER
|
||||
U1022991,"5 Link Way, Bromley",BR2 8JH
|
||||
U1025820,"46 Princes Plain, Bromley",BR2 8LE
|
||||
U1020237,"33 Hornbeam Way, Bromley",BR2 8DB
|
||||
U1021310,"126 Faringdon Avenue, Bromley",BR2 8BU
|
||||
U1021353,"66 George Lane, Bromley",BR2 7LQ
|
||||
U1033165,"2 Laburnum Way, Bromley",BR2 8BZ
|
||||
U1010811,"13 Gilbert Road, Bromley",BR1 3QP
|
||||
U1027449,"11 Station Road, Bromley",BR1 3LP
|
||||
U1035326,"2 Whitebeam Avenue, Bromley",BR2 8DL
|
||||
U1020351,"27 Laburnum Way, Bromley",BR2 8BY
|
||||
UPRN,Address,Postcode
|
||||
U1041316,"U1041316_58 Ravensworth Road, Mottingham",SE9 4LW
|
||||
U2022716,"U2022716_306 St Helier Avenue, Morden",SM4 6JX
|
||||
U2018001,"U2018001_37 Leominster Road, Morden",SM4 6HN
|
||||
U2019131,"U2019131_34 Middleton Road, Morden",SM4 6RT
|
||||
U1035331,"104 Whitebeam Avenue, Bromley",BR2 8DW
|
||||
U1010255,"7 Boughton Avenue, Bromley",BR2 7PL
|
||||
U1024581,"30 Parkfield Way, Bromley",BR2 8AF
|
||||
U1032130,"86 Faringdon Avenue, Bromley",BR2 8BU
|
||||
U1014077,"32 Aylesbury Road, Bromley",BR2 0QP
|
||||
U1007728,"2 Hazel Walk, Bromley",BR2 8DF
|
||||
U1020327,"5 Kingsdown Way, Bromley",BR2 7PT
|
||||
U1032134,"125 Faringdon Avenue, Bromley",BR2 8BT
|
||||
U1053411,"U1053411_78 Windsor Drive, Orpington",BR6 6HF
|
||||
U2022615,"U2022615_56 St Helier Avenue, Morden",SM4 6LF
|
||||
U1022991,"5 Link Way, Bromley",BR2 8JH
|
||||
U1010811,"13 Gilbert Road, Bromley",BR1 3QP
|
||||
U1020328,"31 Kingsdown Way, Bromley",BR2 7PT
|
||||
U1005131,"10 Boughton Avenue, Bromley",BR2 7PL
|
||||
U1021353,"66 George Lane, Bromley",BR2 7LQ
|
||||
U1034017,"48 Princes Plain, Bromley",BR2 8LE
|
||||
U1036758,"46 Newbury Road, Bromley",BR2 0QW
|
||||
U1020351,"27 Laburnum Way, Bromley",BR2 8BY
|
||||
U1035052,"1 Sudbury Crescent, Bromley",BR1 4PY
|
||||
U1027073,"U1027073_29 Haddon Road, St Mary Cray",BR5 4BU
|
||||
U1034887,"U1034887_2 Saltwood Close, Orpington",BR6 9BU
|
||||
U1035329,"U1035329_79 Whitebeam Avenue, Bromley",BR2 8DN
|
||||
U1042571,"U1042571_19 Whitebeam Avenue, Bromley",BR2 8DJ
|
||||
U1035330,"U1035330_87 Whitebeam Avenue, Bromley",BR2 8DN
|
||||
U1047521,"U1047521_19 Walnuts Road, Orpington",BR6 0RG
|
||||
U1033037,"U1033037_73 Homefield Rise, Orpington",BR6 0RL
|
||||
U1027845,"U1027845_92 Homefield Rise, Orpington",BR6 0RW
|
||||
U1031691,"U1031691_46 Boyland Road, Bromley",BR1 4QF
|
||||
U1026916,"U1026916_19 Elizabeth Way, St Mary Cray",BR5 4BJ
|
||||
U1015363,"U1015363_44a Birch Row, Bromley",BR2 8DA
|
||||
U1010400,"U1010400_42 Brook Lane, Bromley",BR1 4PU
|
||||
U1052426,"U1052426_49 Southover, Bromley",BR1 4SL
|
||||
U1019591,"U1019591_18 Buckland Road, Orpington",BR6 9SR
|
||||
U1011434,"U1011434_24 Upper Elmers End Road, Beckenham",BR3 4AL
|
||||
U1023417,"U1023417_44 Malling Way, Hayes, Bromley",BR2 7PJ
|
||||
U1014671,"U1014671_61 George Lane, Hayes, Bromley",BR2 7LG
|
||||
U1037550,"U1037550_228 Repton Road, Orpington",BR6 9JB
|
||||
U1014433,"U1014433_57 Cowden Road, Orpington",BR6 0TP
|
||||
U1015779,"U1015779_21 Beck Lane, Beckenham",BR3 4RD
|
||||
U1042236,"U1042236_250 Southover, Bromley",BR1 4SE
|
||||
U1033036,"U1033036_61 Homefield Rise, Orpington",BR6 0RL
|
||||
U1027951,"U1027951_2 Kingsdown Way, Hayes, Bromley",BR2 7PT
|
||||
U1032770,"U1032770_18 Pembury Close, Hayes, Bromley",BR2 7PS
|
||||
U1011129,"U1011129_40 Saltwood Close, Orpington",BR6 9BU
|
||||
U1023267,"U1023267_47a Mackenzie Road, Beckenham",BR3 4RT
|
||||
U1016998,"U1016998_68 Arundel Drive, Orpington",BR6 9JG
|
||||
U1026576,"U1026576_1 Burwash Court, St Mary Cray",BR5 4BG
|
||||
U1037867,"U1037867_45 Stowe Road, Orpington",BR6 9HG
|
||||
U1027839,"U1027839_30 Holmcroft Way, Bromley",BR2 8AD
|
||||
U1005351,"U1005351_2 Leaveland Close, Beckenham",BR3 3PL
|
||||
U1041968,"U1041968_59 Uplands Road, Orpington",BR6 0RH
|
||||
U1032149,"U1032149_12 Fordwich Close, Orpington",BR6 0TT
|
||||
U1047347,"U1047347_30 Sudbury Crescent, Bromley",BR1 4PZ
|
||||
U1004249,"U1004249_6 Beck Lane, Beckenham",BR3 4RE
|
||||
U1005772,"U1005772_10 Manor Park Road, West Wickham",BR4 0JZ
|
||||
U1010810,"U1010810_79 George Lane, Hayes, Bromley",BR2 7LG
|
||||
U1011008,"U1011008_170 Repton Road, Orpington",BR6 9JA
|
||||
U1026969,"U1026969_2 Fordwich Close, Orpington",BR6 0TT
|
||||
U1032000,"U1032000_16 Detling Road, Bromley",BR1 4SH
|
||||
U1013268,"U1013268_9 Bell Gardens, St Mary Cray",BR5 4DA
|
||||
U1020217,"U1020217_71 Homefield Rise, Orpington",BR6 0RL
|
||||
U1033287,"U1033287_12 Lincoln Green Road, St Mary Cray",BR5 2DX
|
||||
U1041997,"U1041997_31 Walnuts Road, Orpington",BR6 0RG
|
||||
U1047520,"U1047520_2 Walnuts Road, Orpington",BR6 0RQ
|
||||
U1019606,"U1019606_20 Burrfield Drive, St Mary Cray",BR5 4BZ
|
||||
U1007354,"U1007354_30 Cowden Road, Orpington",BR6 0TR
|
||||
U1015603,"U1015603_1 Barwood Avenue, West Wickham",BR4 0LB
|
||||
U1035393,"U1035393_18 Yalding Grove, St Mary Cray",BR5 3SE
|
||||
U1000177,"U1000177_79 Arundel Drive, Orpington",BR6 9JQ
|
||||
U1011428,"U1011428_21 Uplands Road, Orpington",BR6 0RH
|
||||
U1024671,"U1024671_115 Oregon Square, Orpington",BR6 8BE
|
||||
U1021513,"U1021513_18 Chamberlain Crescent, West Wickham",BR4 0LL
|
||||
U1024663,"U1024663_32b Okemore Gardens, Orpington",BR5 3PJ
|
||||
U1020879,"U1020879_8 Brockwell Close, St Mary Cray",BR5 1RJ
|
||||
U1022713,"U1022713_53 Homefield Rise, Orpington",BR6 0RL
|
||||
U1031157,"U1031157_94 Leesons Hill, St Pauls Cray",BR5 2NH
|
||||
U1013666,"U1013666_11 Ampleforth Close, Orpington",BR6 9ES
|
||||
U1052617,"U1052617_44 Uplands Road, Orpington",BR6 0RJ
|
||||
U1010783,"U1010783_32 Foxbury Drive, Chelsfield, Orpington",BR6 6EL
|
||||
U1021700,"U1021700_25 Daleside, Orpington",BR6 6EG
|
||||
U1011163,"U1011163_35 Selwyn Place, St Pauls Cray",BR5 3BA
|
||||
U1022175,"U1022175_54 Homefield Rise, Orpington",BR6 0RW
|
||||
U1021461,"U1021461_27 Burrfield Drive, St Mary Cray",BR5 4BZ
|
||||
U1007184,"U1007184_7 Buckland Road, Orpington",BR6 9SR
|
||||
U1023112,"U1023112_97 Lower Gravel Road, Bromley",BR2 8LP
|
||||
U1024516,"U1024516_18 Northolme Rise, Off Tubbenden Lane, Orpington",BR6 9RF
|
||||
U1023910,"U1023910_40 Moorfield Road, Orpington",BR6 0HQ
|
||||
U1031342,"U1031342_55 Lower Gravel Road, Bromley",BR2 8LP
|
||||
U1037905,"U1037905_37 Sweeps Lane, St Mary Cray",BR5 3PE
|
||||
U2019260,"U2019260_88 Montacute Road, Morden",SM4 6RL
|
||||
U2019214,"U2019214_11 Missenden Gardens, Morden",SM4 6HW
|
||||
U2022891,"U2022891_20 Swains Road, Tooting, London",SW17 9HS
|
||||
U2022684,"U2022684_231 St Helier Avenue, Morden",SM4 6JH
|
||||
U2023196,"U2023196_210 Tudor Drive, Morden",SM4 4PQ
|
||||
U2019671,"U2019671_16 Netley Gardens, Morden",SM4 6JW
|
||||
U2018944,"U2018944_79 Malmesbury Road, Morden",SM4 6HG
|
||||
U2015873,"U2015873_1 Furness Road, Morden",SM4 6PS
|
||||
U1041503,"U1041503_103 Ravensworth Road, Mottingham",SE9 4LX
|
||||
U2023185,"U2023185_157 Tudor Drive, Morden",SM4 4PJ
|
||||
U1053395,"U1053395_90 Widecombe Road, Mottingham",SE9 4HJ
|
||||
U2018009,"U2018009_23 Leominster Walk, Morden",SM4 6HB
|
||||
U2018941,"U2018941_74 Malmesbury Road, Morden",SM4 6HE
|
||||
U2013950,"U2013950_2 Buckland Walk, Morden",SM4 5RZ
|
||||
U2018160,"U2018160_6 Llanthony Road, Morden",SM4 6DX
|
||||
U2015790,"U2015790_12 Flaxley Road, Morden",SM4 6LJ
|
||||
U2013914,"U2013914_36 Bruton Road, Morden",SM4 5RY
|
||||
U2022611,"U2022611_50 St Helier Avenue, Morden",SM4 6LF
|
||||
U2015270,"U2015270_39 Dorchester Road, Morden",SM4 6QE
|
||||
U1009098,"U1009098_37b Maple Road, Penge",SE20 8LA
|
||||
U2018926,"U2018926_19 Malmesbury Road, Morden",SM4 6HD
|
||||
U2023192,"U2023192_195 Tudor Drive, Morden",SM4 4PJ
|
||||
U1027362,"U1027362_187 Seymour Villas, Penge",SE20 8TP
|
||||
U2018973,"U2018973_26 Marham Gardens, Morden",SM4 6JJ
|
||||
U2013913,"U2013913_30 Bruton Road, Morden",SM4 5RY
|
||||
U1042214,"U1042214_25 Snowdown Close, London",SE20 7RU
|
||||
U2013911,"U2013911_23 Bruton Road, Morden",SM4 5RY
|
||||
U2013583,"U2013583_300 Bishopsford Road, Morden",SM4 6BY
|
||||
U2018925,"U2018925_14 Malmesbury Road, Morden",SM4 6HD
|
||||
U1037938,"U1037938_5 Torr Road, Penge",SE20 7PS
|
||||
U1038117,"U1038117_40 Widecombe Road, Mottingham",SE9 4HQ
|
||||
U1052365,"U1052365_127 Seymour Villas, Penge",SE20 8TP
|
||||
U1024726,"U1024726_2 Penge Lane, Penge",SE20 7DU
|
||||
U1026505,"U1026505_7 Bridlington Close, Biggin Hill",TN16 3PD
|
||||
U2018913,"U2018913_23 Malling Gardens, Morden",SM4 6JG
|
||||
U2015708,"U2015708_46 Faversham Road, Morden",SM4 6RE
|
||||
U2019530,"U2019530_30 Neath Gardens, Morden",SM4 6JN
|
||||
U2015710,"U2015710_49 Faversham Road, Morden",SM4 6RE
|
||||
U2014478,"U2014478_222 Central Road, Morden",SM4 5PP
|
||||
U2018922,"U2018922_11 Malmesbury Road, Morden",SM4 6HD
|
||||
U2013615,"U2013615_47 Blanchland Road, Morden",SM4 5ND
|
||||
U2022641,"U2022641_127 St Helier Avenue, Morden",SM4 6JD
|
||||
U2019268,"U2019268_97 Montacute Road, Morden",SM4 6RJ
|
||||
U2013947,"U2013947_95 Buckfast Road, Morden",SM4 5NA
|
||||
U2018025,"U2018025_21 Lessness Road, Morden",SM4 6HP
|
||||
U2013415,"U2013415_51 Bodnant Gardens, Raynes Park, London",SW20 0UD
|
||||
U2022728,"U2022728_340 St Helier Avenue, Morden",SM4 6JU
|
||||
U1009623,"U1009623_23 Pawleyne Close, Penge",SE20 8JH
|
||||
U2015884,"U2015884_30 Furness Road, Morden",SM4 6PR
|
||||
U2019728,"U2019728_17 Newhouse Walk, Morden",SM4 6BS
|
||||
U1024719,"U1024719_14 Pawleyne Close, Penge",SE20 8JH
|
||||
U2013935,"U2013935_43 Buckfast Road, Morden",SM4 5NA
|
||||
U2013623,"U2013623_4 Bodmin Grove, Morden",SM4 5LU
|
||||
U2018811,"U2018811_354 Lynmouth Avenue, Morden",SM4 4RT
|
||||
U2019672,"U2019672_19 Netley Gardens, Morden",SM4 6JW
|
||||
U2018927,"U2018927_29 Malmesbury Road, Morden",SM4 6HD
|
||||
U2019237,"U2019237_37 Montacute Road, Morden",SM4 6RJ
|
||||
U2019730,"U2019730_23 Newhouse Walk, Morden",SM4 6BS
|
||||
U1038118,"U1038118_46 Widecombe Road, Mottingham",SE9 4HQ
|
||||
U1000359,"U1000359_5 Avenue Road, Penge",SE20 7RT
|
||||
U2018164,"U2018164_24 Llanthony Road, Morden",SM4 6DX
|
||||
U2013982,"U2013982_18 Bury Grove, Morden",SM4 5NG
|
||||
U1030964,"U1030964_13 Ravensworth Road, Mottingham",SE9 4LN
|
||||
U2015882,"U2015882_25 Furness Road, Morden",SM4 6PS
|
||||
U1031105,"U1031105_21 Kingsmead, Biggin Hill, Westerham",TN16 3UB
|
||||
U1014008,"U1014008_9 Avenue Road, Penge",SE20 7RT
|
||||
U2015779,"U2015779_57 Flanders Crescent, Tooting, London",SW17 9JB
|
||||
U2013413,"U2013413_49 Bodnant Gardens, Raynes Park, London",SW20 0UD
|
||||
U1021199,"U1021199_7 Dittisham Road, Mottingham",SE9 4BJ
|
||||
U1041705,"U1041705_87 Seymour Villas, Penge",SE20 8TR
|
||||
U1041706,"U1041706_91 Seymour Villas, Penge",SE20 8TR
|
||||
U1042213,"U1042213_9 Snowdown Close, London",SE20 7RU
|
||||
U2014444,"U2014444_86 Central Road, Morden",SM4 5RJ
|
||||
U2013940,"U2013940_63 Buckfast Road, Morden",SM4 5NA
|
||||
U1037789,"U1037789_5 Snowdown Close, London",SE20 7RU
|
||||
U2013630,"U2013630_16 Bodmin Grove, Morden",SM4 5LU
|
||||
U2014445,"U2014445_88 Central Road, Morden",SM4 5RJ
|
||||
U2013602,"U2013602_5 Blanchland Road, Morden",SM4 5ND
|
||||
U1017178,"U1017178_11 Avenue Road, Penge",SE20 7RT
|
||||
U1013690,"U1013690_74 Anerley Vale, London",SE19 2BG
|
||||
U2013676,"U2013676_52 Bordesley Road, Morden",SM4 5LR
|
||||
U1042212,"U1042212_1 Snowdown Close, London",SE20 7RU
|
||||
U2018810,"U2018810_331 Lynmouth Avenue, Morden",SM4 4RY
|
||||
U2013476,"U2013476_1 Bristol Road, Morden",SM4 5SA
|
||||
U1026660,"U1026660_40 Chilham Road, Mottingham",SE9 4BG
|
||||
U1036877,"U1036877_2 Pawleyne Close, Penge",SE20 8JH
|
||||
U2018814,"U2018814_364 Lynmouth Avenue, Morden",SM4 4RT
|
||||
U2013573,"U2013573_264 Bishopsford Road, Morden",SM4 6BZ
|
||||
U1007236,"U1007236_6 Castledine Road, Penge",SE20 8PL
|
||||
U1027701,"U1027701_4 Wayside Grove, Mottingham",SE9 4ND
|
||||
U1047208,"U1047208_83 Seymour Villas, Penge",SE20 8TR
|
||||
U2013695,"U2013695_111 Bordesley Road, Morden",SM4 5LW
|
||||
U1004961,"U1004961_36 Beverley Road, Penge",SE20 8SJ
|
||||
U2015795,"U2015795_24 Flaxley Road, Morden",SM4 6LJ
|
||||
U2014451,"U2014451_102 Central Road, Morden",SM4 5RJ
|
||||
U2016606,"U2016606_3 Grayswood Gardens, Raynes Park, London",SW20 0UF
|
||||
U1035025,"U1035025_29 Steyning Grove, Mottingham",SE9 4NG
|
||||
U1027588,"U1027588_94 Tovil Close, Penge",SE20 8SZ
|
||||
U2022632,"U2022632_112 St Helier Avenue, Morden",SM4 6LB
|
||||
U1037462,"U1037462_68 Prestbury Square, Mottingham",SE9 4NA
|
||||
U2018027,"U2018027_29 Lessness Road, Morden",SM4 6HP
|
||||
U2018909,"U2018909_12 Malling Gardens, Morden",SM4 6JG
|
||||
U1001247,"U1001247_13 Pawleyne Close, Penge",SE20 8JH
|
||||
U1018547,"U1018547_9b Avenue Road, Penge",SE20 7RT
|
||||
U1008780,"U1008780_7b Avenue Road, Penge",SE20 7RT
|
||||
U1007212,"U1007212_2 Calcott Walk, Mottingham",SE9 4BN
|
||||
U1003650,"U1003650_5b Avenue Road, Penge",SE20 7RT
|
||||
U1010359,"U1010359_2 Bredhurst Close, Penge",SE20 7BG
|
||||
U1041772,"13 Stanley Road, Bromley",BR2 9JE
|
||||
U1011072,"45 Rookery Gardens, Oprington",BR5 4BA
|
||||
U1020208,"16 Holbrook Way, Bromley",BR2 8EE
|
||||
U1052580,"25 Trentham Drive, Oprington",BR5 2EP
|
||||
U1038106,"84 Whitebeam Avenue, Bromley",BR2 8DW
|
||||
U1010107,"42 Birch Row, Bromley",BR2 8DA
|
||||
U1022307,"20 Larch Way, Bromley",BR2 8DU
|
||||
U1020064,"28 Green Way, Bromley",BR2 8EY
|
||||
U1023463,"30 MANOR WAY, Bromley",BR2 8ES
|
||||
U1027745,"148 Whitebeam Avenue, Bromley",BR2 8DW
|
||||
U1008151,"61 Lovelace Avenue, Bromley",BR2 8EA
|
||||
U1019785,"50 Cray Valley Road, Oprington",BR5 2EZ
|
||||
U1029229,"2 Meath Close, Oprington",BR5 2HF
|
||||
U1005696,"158 Magpie Hall Lane, Bromley",BR2 8JG
|
||||
U1036332,"50 Marion Crescent, Oprington",BR5 2HD
|
||||
U1027830,"26 Holbrook Way, Bromley",BR2 8EE
|
||||
U1052455,"10 Stiles Close, Bromley",BR2 8EQ
|
||||
U1023539,"2 Marsham Close, Chislehurst",BR7 6JD
|
||||
U1030962,"7 Ravensleigh Gardens, Bromley",BR1 5SN
|
||||
U1037954,"28 Treewall Gardens, Bromley",BR1 5BT
|
||||
U1020368,"11 Larch Way, Bromley",BR2 8DT
|
||||
U1027743,"17 Whitebeam Avenue, Bromley",BR2 8DJ
|
||||
U1021825,"117a Faringdon Avenue, Bromley",BR2 8BT
|
||||
U1028328,"76 Magpie Hall Lane, Bromley",BR2 8ER
|
||||
U1025820,"46 Princes Plain, Bromley",BR2 8LE
|
||||
U1024580,"24 Parkfield Way, Bromley",BR2 8AF
|
||||
U1010452,"5 Canbury Path, Oprington",BR5 2EU
|
||||
U1022018,"12 Hazel Walk, Bromley",BR2 8DF
|
||||
U1037814,"42 Stanley Road, Bromley",BR2 9JH
|
||||
U1016744,"18 Princes Plain, Bromley",BR2 8LE
|
||||
U1021310,"126 Faringdon Avenue, Bromley",BR2 8BU
|
||||
U1035115,"30 Thorn Close, Bromley",BR2 8DH
|
||||
U1033165,"2 Laburnum Way, Bromley",BR2 8BZ
|
||||
U1000649,"42 Barnsdale Crescent, Oprington",BR5 2AX
|
||||
U1052536,"12 Thorn Close, Bromley",BR2 8DH
|
||||
U1042298,"39 Sudbury Crescent, Bromley",BR1 4PY
|
||||
U1053388,"52 Whitebeam Avenue, Bromley",BR2 8DL
|
||||
U1007556,"11 Farleigh Avenue, Bromley",BR2 7PP
|
||||
U1007947,"8 Laburnum Way, Bromley",BR2 8BZ
|
||||
U1019567,"8 Broom Close, Bromley",BR2 8EU
|
||||
U1013358,"3 Bird In Hand Lane, Bromley",BR1 2NA
|
||||
U1022533,"81 Lower Gravel Road, Bromley",BR2 8LP
|
||||
U1021824,"115g Faringdon Avenue, Bromley",BR2 8BT
|
||||
U1009369,"17 Minster Road, Bromley",BR1 4DY
|
||||
U1022931,"15 Lennard Road, Bromley",BR2 8LN
|
||||
U1024065,"64 Marion Crescent, Oprington",BR5 2HD
|
||||
U1005168,"38 Holbrook Way, Bromley",BR2 8EE
|
||||
U1038104,"32 Whitebeam Avenue, Bromley",BR2 8DL
|
||||
U1021827,"121 Faringdon Avenue, Bromley",BR2 8BT
|
||||
U1030892,"140 Poverest Road, Oprington",BR5 1RH
|
||||
U1013831,"33 Ash Row, Bromley",BR2 8DZ
|
||||
U1004442,"7 Birch Row, Bromley",BR2 8BX
|
||||
U1019564,"32 Brook Lane, Bromley",BR1 4PU
|
||||
U1032131,"93 Faringdon Avenue, Bromley",BR2 8BT
|
||||
U1026960,"105 Faringdon Avenue, Bromley",BR2 8BT
|
||||
U1005006,"44 Birch Row, Bromley",BR2 8DA
|
||||
U1005742,"18 Malling Way, Bromley",BR2 7PJ
|
||||
U1022281,"35 Kingsdown Way, Bromley",BR2 7PT
|
||||
U1042574,"66 Whitebeam Avenue, Bromley",BR2 8DL
|
||||
U1011191,"32 Shoreham Way, Bromley",BR2 7PU
|
||||
U1032129,"78 Faringdon Avenue, Bromley",BR2 8BU
|
||||
U1009032,"10 Malling Way, Bromley",BR2 7PJ
|
||||
U1031121,"32 Laburnum Way, Bromley",BR2 8BZ
|
||||
U1022305,"18a Lansdowne Road, Bromley",BR1 3LZ
|
||||
U1042248,"16 Stanley Way, Oprington",BR5 2HE
|
||||
U1038102,"14 Whitebeam Avenue, Bromley",BR2 8DL
|
||||
U1007432,"21 Detling Road, Bromley",BR1 4SH
|
||||
U1041934,"51 Turpington Lane, Bromley",BR2 8JA
|
||||
U1032647,"32 Narrow Way, Bromley",BR2 8JB
|
||||
U1052429,"76 Southover, Bromley",BR1 4RY
|
||||
U1007701,"46 Harwood Avenue, Bromley",BR1 3DU
|
||||
U1042572,"37 Whitebeam Avenue, Bromley",BR2 8DJ
|
||||
U1042576,"112 Whitebeam Avenue, Bromley",BR2 8DW
|
||||
U1020349,"21 Laburnum Way, Bromley",BR2 8BY
|
||||
U1021535,"4 Chilham Way, Bromley",BR2 7PR
|
||||
U1004686,"55 Baston Road, Bromley",BR2 7BD
|
||||
U1027744,"33 Whitebeam Avenue, Bromley",BR2 8DJ
|
||||
U1016880,"25 Almond Way, Bromley",BR2 8DR
|
||||
U1023415,"27 Malling Way, Bromley",BR2 7PJ
|
||||
U1011190,"11 Shoreham Way, Bromley",BR2 7PU
|
||||
U1042256,"2 Stiles Close, Bromley",BR2 8EQ
|
||||
U1053386,"27 Whitebeam Avenue, Bromley",BR2 8DJ
|
||||
U1020237,"33 Hornbeam Way, Bromley",BR2 8DB
|
||||
U1005167,"30 Holbrook Way, Bromley",BR2 8EE
|
||||
U1014630,"118 Faringdon Avenue, Bromley",BR2 8BU
|
||||
U1034810,"24 Newbury Road, Bromley",BR2 0QW
|
||||
U1037768,"13 Silverdale Road, Oprington",BR5 2LU
|
||||
U1024709,"3 Parkfield Way, Bromley",BR2 8AE
|
||||
U1010582,"13 Cranworth Cottages, Keston",BR2 6DB
|
||||
U1010578,"6 Cranbrook Close, Bromley",BR2 7QA
|
||||
U1031058,"303 Keedonwood Road, Bromley",BR1 4QR
|
||||
U1026447,"50 Princes Plain, Bromley",BR2 8LE
|
||||
U1016746,"68 Princes Plain, Bromley",BR2 8LE
|
||||
U1038107,"152 Whitebeam Avenue, Bromley",BR2 8DW
|
||||
U1036446,"14 Meath Close, Oprington",BR5 2HF
|
||||
U1014589,"71 Empress Drive, Chislehurst",BR7 5BQ
|
||||
U1002456,"54 Birch Row, Bromley",BR2 8DA
|
||||
U1020369,"13 Larch Way, Bromley",BR2 8DT
|
||||
U1034985,"272 Southborough Lane, Bromley",BR2 8AS
|
||||
U1041435,"64 Princes Plain, Bromley",BR2 8LE
|
||||
U1031294,"70 London Lane, Bromley",BR1 4HE
|
||||
U1033406,"67 Lower Gravel Road, Bromley",BR2 8LP
|
||||
U1032061,"30 Eastry Avenue, Bromley",BR2 7PF
|
||||
U1042573,"47 Whitebeam Avenue, Bromley",BR2 8DJ
|
||||
U1032133,"119 Faringdon Avenue, Bromley",BR2 8BT
|
||||
U1013830,"31 Ash Row, Bromley",BR2 8DZ
|
||||
U1009511,"27 Newbury Road, Bromley",BR2 0QN
|
||||
U1042735,"108 Whitebeam Avenue, Bromley",BR2 8DW
|
||||
U1024511,"81 Nightingale Lane, Bromley",BR1 2SA
|
||||
U1024050,"10 Marden Avenue, Bromley",BR2 7PX
|
||||
U1035326,"2 Whitebeam Avenue, Bromley",BR2 8DL
|
||||
U1009202,"63 Mead Way, Bromley",BR2 9ER
|
||||
U1020199,"78 Hillside Road, Bromley",BR2 0ST
|
||||
U1042232,"154 Southover, Bromley",BR1 4RZ
|
||||
U1030897,"60 Princes Plain, Bromley",BR2 8LE
|
||||
U1027989,"5 Larch Way, Bromley",BR2 8DT
|
||||
U1021308,"70 Faringdon Avenue, Bromley",BR2 8BU
|
||||
U1019906,"26 Eastry Avenue, Bromley",BR2 7PF
|
||||
U1022726,"31 Hornbeam Way, Bromley",BR2 8DB
|
||||
U1024698,"4 Palace View, Bromley",BR1 3EL
|
||||
U1005549,"79 Lower Gravel Road, Bromley",BR2 8LP
|
||||
U1052376,"3 Shoreham Way, Bromley",BR2 7PU
|
||||
U1041937,"75 Turpington Lane, Bromley",BR2 8JD
|
||||
U1005123,"24 Bonville Road, Bromley",BR1 4QA
|
||||
U1008158,"71 Lower Gravel Road, Bromley",BR2 8LP
|
||||
U1042575,"90 Whitebeam Avenue, Bromley",BR2 8DW
|
||||
U1014793,"59 Headcorn Road, Bromley",BR1 4SQ
|
||||
U1012309,"13 Almond Close, Bromley",BR2 8DS
|
||||
U1009194,"84 Mays Hill Road, Bromley",BR2 0HT
|
||||
U1027838,"15 Holmcroft Way, Bromley",BR2 8AD
|
||||
U1027449,"11 Station Road, Bromley",BR1 3LP
|
||||
U1014627,"81 Faringdon Avenue, Bromley",BR2 8BT
|
||||
U1027493,"35 Sudbury Crescent, Bromley",BR1 4PY
|
||||
U1032132,"115f Faringdon Avenue, Bromley",BR2 8BT
|
||||
U1037833,"3 Stiles Close, Bromley",BR2 8EQ
|
||||
U1016743,"16 Princes Plain, Bromley",BR2 8LE
|
||||
U1007553,"115d Faringdon Avenue, Bromley",BR2 8BT
|
||||
U1009607,"20 Parkfield Way, Bromley",BR2 8AF
|
||||
U1034805,"38 Narrow Way, Bromley",BR2 8JB
|
||||
U1032062,"46 Eastry Avenue, Bromley",BR2 7PF
|
||||
U1047613,"13 Whitebeam Avenue, Bromley",BR2 8DJ
|
||||
U1024484,"55 Mounthurst Road, Bromley",BR2 7PG
|
||||
U1038103,"29 Whitebeam Avenue, Bromley",BR2 8DJ
|
||||
U1022525,"30 Lovelace Avenue, Bromley",BR2 8DQ
|
||||
U1042153,"4 Scotts Road, Bromley",BR1 3QD
|
||||
U1037465,"6 Princes Plain, Bromley",BR2 8LE
|
||||
U1041933,"31 Turpington Lane, Bromley",BR2 8JA
|
||||
U1021769,"49 Eastry Avenue, Bromley",BR2 7PE
|
||||
U1042734,"86 Whitebeam Avenue, Bromley",BR2 8DW
|
||||
U1026958,"77 Faringdon Avenue, Bromley",BR2 8BT
|
||||
U1037872,"20 Sudbury Crescent, Bromley",BR1 4PZ
|
||||
U1027549,"14 Thorn Close, Bromley",BR2 8DH
|
||||
U1037450,"70 Pontefract Road, Bromley",BR1 4RB
|
||||
U1016742,"4 Princes Plain, Bromley",BR2 8LE
|
||||
U1042733,"68 Whitebeam Avenue, Bromley",BR2 8DL
|
||||
U1003906,"54 Barnsdale Crescent, Oprington",BR5 2AX
|
||||
U1027392,"26 Silverdale Road, Oprington",BR5 2LT
|
||||
U1052186,"4 Ravensleigh Gardens, Bromley",BR1 5SN
|
||||
U1034479,"90 Mead Way, Bromley",BR2 9EU
|
||||
U1005741,"2 Malling Way, Bromley",BR2 7PJ
|
||||
U1024017,"13 Manor Way, Bromley",BR2 8ES
|
||||
U1014078,"43 Aylesbury Road, Bromley",BR2 0QR
|
||||
U1015499,"25 Boughton Avenue, Bromley",BR2 7PL
|
||||
U1033177,"30 Larch Way, Bromley",BR2 8DU
|
||||
|
|
|
|||
|
|
|
@ -1,2 +0,0 @@
|
|||
UPRN,Address,Postcode
|
||||
U1014630,"118 Faringdon Avenue, Bromley",BR2 8BU
|
||||
|
|
|
@ -0,0 +1,510 @@
|
|||
{
|
||||
"uprn": 100040550095,
|
||||
"roofs": [
|
||||
{
|
||||
"description": "Pitched, 175 mm loft insulation",
|
||||
"energy_efficiency_rating": 4,
|
||||
"environmental_efficiency_rating": 4
|
||||
}
|
||||
],
|
||||
"walls": [
|
||||
{
|
||||
"description": "Granite or whin, 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": "Solid, no insulation (assumed)",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
}
|
||||
],
|
||||
"status": "entered",
|
||||
"tenure": 3,
|
||||
"window": {
|
||||
"description": "Fully double glazed",
|
||||
"energy_efficiency_rating": 2,
|
||||
"environmental_efficiency_rating": 2
|
||||
},
|
||||
"addendum": {
|
||||
"stone_walls": "true"
|
||||
},
|
||||
"lighting": {
|
||||
"description": "Excellent lighting efficiency",
|
||||
"energy_efficiency_rating": 5,
|
||||
"environmental_efficiency_rating": 5
|
||||
},
|
||||
"postcode": "TQ1 2BW",
|
||||
"hot_water": {
|
||||
"description": "Electric immersion, standard tariff",
|
||||
"energy_efficiency_rating": 1,
|
||||
"environmental_efficiency_rating": 5
|
||||
},
|
||||
"post_town": "TORQUAY",
|
||||
"built_form": 4,
|
||||
"created_at": "2026-05-20 13:21:34",
|
||||
"door_count": 2,
|
||||
"region_code": 15,
|
||||
"report_type": 2,
|
||||
"sap_heating": {
|
||||
"number_baths": 1,
|
||||
"cylinder_size": 2,
|
||||
"shower_outlets": [
|
||||
{
|
||||
"shower_wwhrs": 1,
|
||||
"shower_outlet_type": 2
|
||||
}
|
||||
],
|
||||
"number_baths_wwhrs": 0,
|
||||
"water_heating_code": 903,
|
||||
"water_heating_fuel": 29,
|
||||
"main_heating_details": [
|
||||
{
|
||||
"has_fghrs": "N",
|
||||
"main_fuel_type": 29,
|
||||
"heat_emitter_type": 0,
|
||||
"emitter_temperature": "NA",
|
||||
"main_heating_number": 1,
|
||||
"main_heating_control": 2603,
|
||||
"main_heating_category": 10,
|
||||
"main_heating_fraction": 1,
|
||||
"sap_main_heating_code": 691,
|
||||
"main_heating_data_source": 2
|
||||
}
|
||||
],
|
||||
"immersion_heating_type": 2,
|
||||
"cylinder_insulation_type": 2,
|
||||
"has_fixed_air_conditioning": "false",
|
||||
"cylinder_insulation_thickness": 38
|
||||
},
|
||||
"sap_version": 10.2,
|
||||
"sap_windows": [
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"glazing_gap": "16+",
|
||||
"orientation": 1,
|
||||
"window_type": 1,
|
||||
"glazing_type": 3,
|
||||
"window_width": 0.91,
|
||||
"window_height": 1.64,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"glazing_gap": "16+",
|
||||
"orientation": 1,
|
||||
"window_type": 1,
|
||||
"glazing_type": 3,
|
||||
"window_width": 0.91,
|
||||
"window_height": 1.64,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"glazing_gap": "16+",
|
||||
"orientation": 1,
|
||||
"window_type": 1,
|
||||
"glazing_type": 3,
|
||||
"window_width": 0.62,
|
||||
"window_height": 0.91,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"glazing_gap": "16+",
|
||||
"orientation": 5,
|
||||
"window_type": 1,
|
||||
"glazing_type": 3,
|
||||
"window_width": 1.2,
|
||||
"window_height": 0.88,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 1,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"glazing_gap": "16+",
|
||||
"orientation": 5,
|
||||
"window_type": 1,
|
||||
"glazing_type": 3,
|
||||
"window_width": 1.02,
|
||||
"window_height": 0.82,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 1,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"glazing_gap": "16+",
|
||||
"orientation": 5,
|
||||
"window_type": 1,
|
||||
"glazing_type": 3,
|
||||
"window_width": 0.6,
|
||||
"window_height": 1.14,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 1,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
}
|
||||
],
|
||||
"schema_type": "RdSAP-Schema-21.0.1",
|
||||
"uprn_source": "Energy Assessor",
|
||||
"country_code": "ENG",
|
||||
"main_heating": [
|
||||
{
|
||||
"description": "Room heaters, electric",
|
||||
"energy_efficiency_rating": 1,
|
||||
"environmental_efficiency_rating": 5
|
||||
}
|
||||
],
|
||||
"air_tightness": {
|
||||
"description": "(not tested)",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
},
|
||||
"dwelling_type": "Mid-terrace house",
|
||||
"language_code": 1,
|
||||
"pressure_test": 4,
|
||||
"property_type": 0,
|
||||
"address_line_1": "11 Meadfoot Lane",
|
||||
"assessment_type": "RdSAP",
|
||||
"completion_date": "2026-05-20",
|
||||
"inspection_date": "2026-05-20",
|
||||
"extensions_count": 1,
|
||||
"measurement_type": 1,
|
||||
"total_floor_area": 59,
|
||||
"transaction_type": 8,
|
||||
"conservatory_type": 1,
|
||||
"heated_room_count": 3,
|
||||
"registration_date": "2026-05-20",
|
||||
"sap_energy_source": {
|
||||
"mains_gas": "Y",
|
||||
"meter_type": 2,
|
||||
"pv_connection": 0,
|
||||
"photovoltaic_supply": {
|
||||
"none_or_no_details": {
|
||||
"percent_roof_area": 0
|
||||
}
|
||||
},
|
||||
"wind_turbines_count": 0,
|
||||
"gas_smart_meter_present": "true",
|
||||
"is_dwelling_export_capable": "false",
|
||||
"wind_turbines_terrain_type": 2,
|
||||
"electricity_smart_meter_present": "true"
|
||||
},
|
||||
"secondary_heating": {
|
||||
"description": "None",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
},
|
||||
"extract_fans_count": 1,
|
||||
"sap_building_parts": [
|
||||
{
|
||||
"identifier": "Main Dwelling",
|
||||
"wall_dry_lined": "N",
|
||||
"wall_thickness": 510,
|
||||
"floor_heat_loss": 7,
|
||||
"roof_construction": 4,
|
||||
"wall_construction": 1,
|
||||
"building_part_number": 1,
|
||||
"sap_floor_dimensions": [
|
||||
{
|
||||
"floor": 0,
|
||||
"room_height": {
|
||||
"value": 2.31,
|
||||
"quantity": "metres"
|
||||
},
|
||||
"floor_insulation": 1,
|
||||
"total_floor_area": {
|
||||
"value": 17.29,
|
||||
"quantity": "square metres"
|
||||
},
|
||||
"party_wall_length": {
|
||||
"value": 7.27,
|
||||
"quantity": "metres"
|
||||
},
|
||||
"floor_construction": 1,
|
||||
"heat_loss_perimeter": {
|
||||
"value": 4.72,
|
||||
"quantity": "metres"
|
||||
}
|
||||
},
|
||||
{
|
||||
"floor": 1,
|
||||
"room_height": {
|
||||
"value": 2.35,
|
||||
"quantity": "metres"
|
||||
},
|
||||
"total_floor_area": {
|
||||
"value": 17.29,
|
||||
"quantity": "square metres"
|
||||
},
|
||||
"party_wall_length": {
|
||||
"value": 7.27,
|
||||
"quantity": "metres"
|
||||
},
|
||||
"heat_loss_perimeter": {
|
||||
"value": 4.72,
|
||||
"quantity": "metres"
|
||||
}
|
||||
}
|
||||
],
|
||||
"wall_insulation_type": 4,
|
||||
"construction_age_band": "A",
|
||||
"party_wall_construction": 1,
|
||||
"wall_thickness_measured": "Y",
|
||||
"roof_insulation_location": 2,
|
||||
"roof_insulation_thickness": "175mm",
|
||||
"wall_insulation_thickness": "NI",
|
||||
"floor_insulation_thickness": "NI"
|
||||
},
|
||||
{
|
||||
"identifier": "Extension 1",
|
||||
"wall_dry_lined": "N",
|
||||
"wall_thickness": 185,
|
||||
"floor_heat_loss": 7,
|
||||
"sap_room_in_roof": {
|
||||
"floor_area": 12.24,
|
||||
"room_in_roof_type_2": {
|
||||
"gable_wall_type_1": 1,
|
||||
"gable_wall_type_2": 3,
|
||||
"gable_wall_height_1": 2.5,
|
||||
"gable_wall_height_2": 2.5,
|
||||
"gable_wall_length_1": 7.39,
|
||||
"gable_wall_length_2": 7.39,
|
||||
"common_wall_height_1": 1.5,
|
||||
"common_wall_height_2": 1.5,
|
||||
"common_wall_length_1": 1.4,
|
||||
"common_wall_length_2": 1.4
|
||||
},
|
||||
"construction_age_band": "A"
|
||||
},
|
||||
"roof_construction": 5,
|
||||
"wall_construction": 3,
|
||||
"building_part_number": 2,
|
||||
"sap_floor_dimensions": [
|
||||
{
|
||||
"floor": 0,
|
||||
"room_height": {
|
||||
"value": 2.27,
|
||||
"quantity": "metres"
|
||||
},
|
||||
"floor_insulation": 1,
|
||||
"total_floor_area": {
|
||||
"value": 12.24,
|
||||
"quantity": "square metres"
|
||||
},
|
||||
"party_wall_length": {
|
||||
"value": 7.39,
|
||||
"quantity": "metres"
|
||||
},
|
||||
"floor_construction": 1,
|
||||
"heat_loss_perimeter": {
|
||||
"value": 2.8,
|
||||
"quantity": "metres"
|
||||
}
|
||||
}
|
||||
],
|
||||
"wall_insulation_type": 4,
|
||||
"construction_age_band": "A",
|
||||
"party_wall_construction": 0,
|
||||
"wall_thickness_measured": "Y",
|
||||
"roof_insulation_location": 4,
|
||||
"roof_insulation_thickness": "ND",
|
||||
"wall_insulation_thickness": "NI",
|
||||
"floor_insulation_thickness": "NI"
|
||||
}
|
||||
],
|
||||
"solar_water_heating": "N",
|
||||
"habitable_room_count": 3,
|
||||
"heating_cost_current": {
|
||||
"value": 1266,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"insulated_door_count": 0,
|
||||
"co2_emissions_current": 1.1,
|
||||
"energy_rating_average": 60,
|
||||
"energy_rating_current": 40,
|
||||
"lighting_cost_current": {
|
||||
"value": 43,
|
||||
"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": 366,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"hot_water_cost_current": {
|
||||
"value": 679,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"mechanical_ventilation": 0,
|
||||
"percent_draughtproofed": 100,
|
||||
"suggested_improvements": [
|
||||
{
|
||||
"sequence": 1,
|
||||
"typical_saving": {
|
||||
"value": 449,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"indicative_cost": "\u00a37,500 - \u00a311,000",
|
||||
"improvement_type": "Q",
|
||||
"improvement_details": {
|
||||
"improvement_number": 7
|
||||
},
|
||||
"improvement_category": 5,
|
||||
"energy_performance_rating": 55,
|
||||
"environmental_impact_rating": 88
|
||||
},
|
||||
{
|
||||
"sequence": 2,
|
||||
"typical_saving": {
|
||||
"value": 57,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"indicative_cost": "\u00a35,000 - \u00a310,000",
|
||||
"improvement_type": "W2",
|
||||
"improvement_details": {
|
||||
"improvement_number": 58
|
||||
},
|
||||
"improvement_category": 5,
|
||||
"energy_performance_rating": 56,
|
||||
"environmental_impact_rating": 89
|
||||
},
|
||||
{
|
||||
"sequence": 3,
|
||||
"typical_saving": {
|
||||
"value": 100,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"indicative_cost": "\u00a320 - \u00a340",
|
||||
"improvement_type": "C",
|
||||
"improvement_details": {
|
||||
"improvement_number": 2
|
||||
},
|
||||
"improvement_category": 5,
|
||||
"energy_performance_rating": 59,
|
||||
"environmental_impact_rating": 89
|
||||
},
|
||||
{
|
||||
"sequence": 4,
|
||||
"typical_saving": {
|
||||
"value": 671,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"indicative_cost": "\u00a33,500 - \u00a310,000",
|
||||
"improvement_type": "T",
|
||||
"improvement_details": {
|
||||
"improvement_number": 29
|
||||
},
|
||||
"improvement_category": 5,
|
||||
"energy_performance_rating": 76,
|
||||
"environmental_impact_rating": 83
|
||||
},
|
||||
{
|
||||
"sequence": 5,
|
||||
"typical_saving": {
|
||||
"value": 220,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"indicative_cost": "\u00a38,000 - \u00a310,000",
|
||||
"improvement_type": "U",
|
||||
"improvement_details": {
|
||||
"improvement_number": 34
|
||||
},
|
||||
"improvement_category": 5,
|
||||
"energy_performance_rating": 81,
|
||||
"environmental_impact_rating": 84
|
||||
}
|
||||
],
|
||||
"co2_emissions_potential": 1.1,
|
||||
"energy_rating_potential": 81,
|
||||
"lighting_cost_potential": {
|
||||
"value": 43,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"schema_version_original": "21.0.1",
|
||||
"alternative_improvements": [
|
||||
{
|
||||
"sequence": 1,
|
||||
"typical_saving": {
|
||||
"value": 822,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"improvement_type": "J2",
|
||||
"improvement_details": {
|
||||
"improvement_number": 54
|
||||
},
|
||||
"improvement_category": 6,
|
||||
"energy_performance_rating": 79,
|
||||
"environmental_impact_rating": 96
|
||||
},
|
||||
{
|
||||
"sequence": 2,
|
||||
"typical_saving": {
|
||||
"value": 524,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"improvement_type": "Z3",
|
||||
"improvement_details": {
|
||||
"improvement_number": 53
|
||||
},
|
||||
"improvement_category": 6,
|
||||
"energy_performance_rating": 71,
|
||||
"environmental_impact_rating": 74
|
||||
}
|
||||
],
|
||||
"hot_water_cost_potential": {
|
||||
"value": 301,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"renewable_heat_incentive": {
|
||||
"water_heating": 2452.35,
|
||||
"space_heating_existing_dwelling": 4576.09
|
||||
},
|
||||
"draughtproofed_door_count": 2,
|
||||
"energy_consumption_current": 189,
|
||||
"has_fixed_air_conditioning": "false",
|
||||
"multiple_glazed_proportion": 100,
|
||||
"calculation_software_version": "5.02r0344",
|
||||
"energy_consumption_potential": 102,
|
||||
"environmental_impact_current": 84,
|
||||
"current_energy_efficiency_band": "E",
|
||||
"environmental_impact_potential": 84,
|
||||
"led_fixed_lighting_bulbs_count": 7,
|
||||
"has_heated_separate_conservatory": "false",
|
||||
"potential_energy_efficiency_band": "B",
|
||||
"co2_emissions_current_per_floor_area": 18,
|
||||
"incandescent_fixed_lighting_bulbs_count": 0
|
||||
}
|
||||
|
|
@ -0,0 +1,700 @@
|
|||
{
|
||||
"uprn": 100050355518,
|
||||
"roofs": [
|
||||
{
|
||||
"description": "Pitched, 200 mm loft insulation",
|
||||
"energy_efficiency_rating": 4,
|
||||
"environmental_efficiency_rating": 4
|
||||
},
|
||||
{
|
||||
"description": "Pitched, limited insulation",
|
||||
"energy_efficiency_rating": 1,
|
||||
"environmental_efficiency_rating": 1
|
||||
}
|
||||
],
|
||||
"walls": [
|
||||
{
|
||||
"description": "Cavity wall, as built, no insulation (assumed)",
|
||||
"energy_efficiency_rating": 2,
|
||||
"environmental_efficiency_rating": 2
|
||||
}
|
||||
],
|
||||
"floors": [
|
||||
{
|
||||
"description": "Suspended, no insulation (assumed)",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
}
|
||||
],
|
||||
"status": "entered",
|
||||
"tenure": 1,
|
||||
"window": {
|
||||
"description": "Partial double glazing",
|
||||
"energy_efficiency_rating": 1,
|
||||
"environmental_efficiency_rating": 1
|
||||
},
|
||||
"addendum": {
|
||||
"addendum_numbers": [
|
||||
15
|
||||
],
|
||||
"cavity_fill_recommended": "true"
|
||||
},
|
||||
"lighting": {
|
||||
"description": "Below average lighting efficiency",
|
||||
"energy_efficiency_rating": 3,
|
||||
"environmental_efficiency_rating": 3
|
||||
},
|
||||
"postcode": "BD23 1BG",
|
||||
"hot_water": {
|
||||
"description": "From main system, no cylinder thermostat",
|
||||
"energy_efficiency_rating": 2,
|
||||
"environmental_efficiency_rating": 2
|
||||
},
|
||||
"post_town": "SKIPTON",
|
||||
"psv_count": 0,
|
||||
"built_form": 2,
|
||||
"created_at": "2026-05-18 08:00:25",
|
||||
"door_count": 1,
|
||||
"region_code": 7,
|
||||
"report_type": 2,
|
||||
"sap_heating": {
|
||||
"number_baths": 1,
|
||||
"cylinder_size": 2,
|
||||
"shower_outlets": [
|
||||
{
|
||||
"shower_wwhrs": 1,
|
||||
"shower_outlet_type": 2
|
||||
},
|
||||
{
|
||||
"shower_wwhrs": 1,
|
||||
"shower_outlet_type": 2
|
||||
}
|
||||
],
|
||||
"water_heating_code": 901,
|
||||
"water_heating_fuel": 26,
|
||||
"cylinder_thermostat": "N",
|
||||
"secondary_fuel_type": 5,
|
||||
"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": 2102,
|
||||
"main_heating_category": 2,
|
||||
"main_heating_fraction": 1,
|
||||
"sap_main_heating_code": 117,
|
||||
"central_heating_pump_age": 0,
|
||||
"main_heating_data_source": 2
|
||||
}
|
||||
],
|
||||
"immersion_heating_type": "NA",
|
||||
"secondary_heating_type": 631,
|
||||
"cylinder_insulation_type": 2,
|
||||
"has_fixed_air_conditioning": "false",
|
||||
"cylinder_insulation_thickness": 80
|
||||
},
|
||||
"sap_version": 10.2,
|
||||
"sap_windows": [
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 1,
|
||||
"window_type": 1,
|
||||
"glazing_type": 5,
|
||||
"window_width": 0.4,
|
||||
"window_height": 1.18,
|
||||
"draught_proofed": "false",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 1,
|
||||
"window_type": 1,
|
||||
"glazing_type": 5,
|
||||
"window_width": 5.03,
|
||||
"window_height": 1.72,
|
||||
"draught_proofed": "false",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"glazing_gap": "16+",
|
||||
"orientation": 5,
|
||||
"window_type": 1,
|
||||
"glazing_type": 3,
|
||||
"window_width": 1.19,
|
||||
"window_height": 1,
|
||||
"draught_proofed": "false",
|
||||
"window_location": 2,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"glazing_gap": "16+",
|
||||
"orientation": 5,
|
||||
"window_type": 1,
|
||||
"glazing_type": 3,
|
||||
"window_width": 1.74,
|
||||
"window_height": 2.05,
|
||||
"draught_proofed": "false",
|
||||
"window_location": 2,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"glazing_gap": "16+",
|
||||
"orientation": 5,
|
||||
"window_type": 2,
|
||||
"glazing_type": 3,
|
||||
"window_width": 0.6,
|
||||
"window_height": 0.9,
|
||||
"draught_proofed": "false",
|
||||
"window_location": 2,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"glazing_gap": "16+",
|
||||
"orientation": 5,
|
||||
"window_type": 2,
|
||||
"glazing_type": 3,
|
||||
"window_width": 0.6,
|
||||
"window_height": 0.9,
|
||||
"draught_proofed": "false",
|
||||
"window_location": 2,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"glazing_gap": "16+",
|
||||
"orientation": 1,
|
||||
"window_type": 1,
|
||||
"glazing_type": 3,
|
||||
"window_width": 2.34,
|
||||
"window_height": 1.18,
|
||||
"draught_proofed": "false",
|
||||
"window_location": 1,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"glazing_gap": "16+",
|
||||
"orientation": 5,
|
||||
"window_type": 1,
|
||||
"glazing_type": 3,
|
||||
"window_width": 1.79,
|
||||
"window_height": 2.06,
|
||||
"draught_proofed": "false",
|
||||
"window_location": 2,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"glazing_gap": "16+",
|
||||
"orientation": 1,
|
||||
"window_type": 1,
|
||||
"glazing_type": 5,
|
||||
"window_width": 5.03,
|
||||
"window_height": 1.23,
|
||||
"draught_proofed": "false",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"glazing_gap": "16+",
|
||||
"orientation": 1,
|
||||
"window_type": 1,
|
||||
"glazing_type": 5,
|
||||
"window_width": 1.11,
|
||||
"window_height": 1.17,
|
||||
"draught_proofed": "false",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"glazing_gap": "16+",
|
||||
"orientation": 5,
|
||||
"window_type": 1,
|
||||
"glazing_type": 3,
|
||||
"window_width": 1.19,
|
||||
"window_height": 1,
|
||||
"draught_proofed": "false",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"glazing_gap": "16+",
|
||||
"orientation": 1,
|
||||
"window_type": 1,
|
||||
"glazing_type": 3,
|
||||
"window_width": 1.18,
|
||||
"window_height": 1.15,
|
||||
"draught_proofed": "false",
|
||||
"window_location": 1,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"glazing_gap": "16+",
|
||||
"orientation": 5,
|
||||
"window_type": 1,
|
||||
"glazing_type": 3,
|
||||
"window_width": 1.1,
|
||||
"window_height": 1,
|
||||
"draught_proofed": "false",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"glazing_gap": "16+",
|
||||
"orientation": 5,
|
||||
"window_type": 1,
|
||||
"glazing_type": 3,
|
||||
"window_width": 1.66,
|
||||
"window_height": 1.17,
|
||||
"draught_proofed": "false",
|
||||
"window_location": 1,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
}
|
||||
],
|
||||
"schema_type": "RdSAP-Schema-21.0.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": "(not tested)",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
},
|
||||
"dwelling_type": "Semi-detached house",
|
||||
"language_code": 1,
|
||||
"pressure_test": 4,
|
||||
"property_type": 0,
|
||||
"address_line_1": "26 Regent Crescent",
|
||||
"assessment_type": "RdSAP",
|
||||
"completion_date": "2026-05-18",
|
||||
"inspection_date": "2026-05-15",
|
||||
"extensions_count": 2,
|
||||
"measurement_type": 1,
|
||||
"open_flues_count": 0,
|
||||
"total_floor_area": 122,
|
||||
"transaction_type": 15,
|
||||
"conservatory_type": 1,
|
||||
"has_draught_lobby": "true",
|
||||
"heated_room_count": 5,
|
||||
"other_flues_count": 0,
|
||||
"registration_date": "2026-05-18",
|
||||
"sap_energy_source": {
|
||||
"mains_gas": "Y",
|
||||
"meter_type": 2,
|
||||
"pv_diverter": "false",
|
||||
"pv_connection": 0,
|
||||
"pv_battery_count": 0,
|
||||
"photovoltaic_supply": {
|
||||
"none_or_no_details": {
|
||||
"percent_roof_area": 0
|
||||
}
|
||||
},
|
||||
"wind_turbines_count": 0,
|
||||
"gas_smart_meter_present": "false",
|
||||
"is_dwelling_export_capable": "false",
|
||||
"wind_turbines_terrain_type": 2,
|
||||
"electricity_smart_meter_present": "false",
|
||||
"is_hydro_output_connected_to_dwelling_meter": "false"
|
||||
},
|
||||
"secondary_heating": {
|
||||
"description": "Room heaters, anthracite",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
},
|
||||
"closed_flues_count": 0,
|
||||
"extract_fans_count": 1,
|
||||
"sap_building_parts": [
|
||||
{
|
||||
"identifier": "Main Dwelling",
|
||||
"wall_dry_lined": "N",
|
||||
"wall_thickness": 230,
|
||||
"floor_heat_loss": 7,
|
||||
"roof_construction": 4,
|
||||
"wall_construction": 4,
|
||||
"building_part_number": 1,
|
||||
"sap_floor_dimensions": [
|
||||
{
|
||||
"floor": 0,
|
||||
"room_height": {
|
||||
"value": 2.53,
|
||||
"quantity": "metres"
|
||||
},
|
||||
"floor_insulation": 1,
|
||||
"total_floor_area": {
|
||||
"value": 35.63,
|
||||
"quantity": "square metres"
|
||||
},
|
||||
"party_wall_length": {
|
||||
"value": 6.09,
|
||||
"quantity": "metres"
|
||||
},
|
||||
"floor_construction": 2,
|
||||
"heat_loss_perimeter": {
|
||||
"value": 7.95,
|
||||
"quantity": "metres"
|
||||
}
|
||||
},
|
||||
{
|
||||
"floor": 1,
|
||||
"room_height": {
|
||||
"value": 2.38,
|
||||
"quantity": "metres"
|
||||
},
|
||||
"total_floor_area": {
|
||||
"value": 35.63,
|
||||
"quantity": "square metres"
|
||||
},
|
||||
"party_wall_length": {
|
||||
"value": 6.09,
|
||||
"quantity": "metres"
|
||||
},
|
||||
"heat_loss_perimeter": {
|
||||
"value": 13.37,
|
||||
"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": "200mm",
|
||||
"wall_insulation_thickness": "NI"
|
||||
},
|
||||
{
|
||||
"identifier": "Extension 1",
|
||||
"wall_dry_lined": "N",
|
||||
"wall_thickness": 320,
|
||||
"floor_heat_loss": 7,
|
||||
"roof_construction": 4,
|
||||
"wall_construction": 4,
|
||||
"building_part_number": 2,
|
||||
"sap_floor_dimensions": [
|
||||
{
|
||||
"floor": 0,
|
||||
"room_height": {
|
||||
"value": 2.29,
|
||||
"quantity": "metres"
|
||||
},
|
||||
"floor_insulation": 1,
|
||||
"total_floor_area": {
|
||||
"value": 15.62,
|
||||
"quantity": "square metres"
|
||||
},
|
||||
"party_wall_length": 0,
|
||||
"floor_construction": 2,
|
||||
"heat_loss_perimeter": {
|
||||
"value": 7.95,
|
||||
"quantity": "metres"
|
||||
}
|
||||
},
|
||||
{
|
||||
"floor": 1,
|
||||
"room_height": {
|
||||
"value": 2.24,
|
||||
"quantity": "metres"
|
||||
},
|
||||
"total_floor_area": {
|
||||
"value": 15.62,
|
||||
"quantity": "square metres"
|
||||
},
|
||||
"party_wall_length": 0,
|
||||
"heat_loss_perimeter": {
|
||||
"value": 11.19,
|
||||
"quantity": "metres"
|
||||
}
|
||||
}
|
||||
],
|
||||
"wall_insulation_type": 4,
|
||||
"construction_age_band": "C",
|
||||
"party_wall_construction": "NA",
|
||||
"wall_thickness_measured": "Y",
|
||||
"roof_insulation_location": 2,
|
||||
"roof_insulation_thickness": "200mm",
|
||||
"wall_insulation_thickness": "NI"
|
||||
},
|
||||
{
|
||||
"identifier": "Extension 2",
|
||||
"wall_dry_lined": "N",
|
||||
"wall_thickness": 320,
|
||||
"floor_heat_loss": 7,
|
||||
"roof_construction": 8,
|
||||
"wall_construction": 4,
|
||||
"building_part_number": 3,
|
||||
"sap_floor_dimensions": [
|
||||
{
|
||||
"floor": 0,
|
||||
"room_height": {
|
||||
"value": 2.86,
|
||||
"quantity": "metres"
|
||||
},
|
||||
"floor_insulation": 1,
|
||||
"total_floor_area": {
|
||||
"value": 19.28,
|
||||
"quantity": "square metres"
|
||||
},
|
||||
"party_wall_length": 0,
|
||||
"floor_construction": 2,
|
||||
"heat_loss_perimeter": {
|
||||
"value": 12.66,
|
||||
"quantity": "metres"
|
||||
}
|
||||
}
|
||||
],
|
||||
"wall_insulation_type": 4,
|
||||
"construction_age_band": "C",
|
||||
"party_wall_construction": "NA",
|
||||
"wall_thickness_measured": "Y",
|
||||
"roof_insulation_location": 7,
|
||||
"wall_insulation_thickness": "NI",
|
||||
"sloping_ceiling_insulation_thickness": "AB"
|
||||
}
|
||||
],
|
||||
"boilers_flues_count": 0,
|
||||
"open_chimneys_count": 0,
|
||||
"solar_water_heating": "N",
|
||||
"habitable_room_count": 5,
|
||||
"heating_cost_current": {
|
||||
"value": 3523,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"insulated_door_count": 0,
|
||||
"co2_emissions_current": 12,
|
||||
"energy_rating_average": 60,
|
||||
"energy_rating_current": 36,
|
||||
"lighting_cost_current": {
|
||||
"value": 84,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"main_heating_controls": [
|
||||
{
|
||||
"description": "Programmer, no room thermostat",
|
||||
"energy_efficiency_rating": 1,
|
||||
"environmental_efficiency_rating": 1
|
||||
}
|
||||
],
|
||||
"blocked_chimneys_count": 1,
|
||||
"has_hot_water_cylinder": "true",
|
||||
"heating_cost_potential": {
|
||||
"value": 1387,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"hot_water_cost_current": {
|
||||
"value": 574,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"mechanical_ventilation": 0,
|
||||
"percent_draughtproofed": 0,
|
||||
"suggested_improvements": [
|
||||
{
|
||||
"sequence": 1,
|
||||
"typical_saving": 260,
|
||||
"indicative_cost": "\u00a3900 - \u00a31,200",
|
||||
"improvement_type": "A2",
|
||||
"improvement_details": {
|
||||
"improvement_number": 45
|
||||
},
|
||||
"improvement_category": 5,
|
||||
"energy_performance_rating": 39,
|
||||
"environmental_impact_rating": 30
|
||||
},
|
||||
{
|
||||
"sequence": 2,
|
||||
"typical_saving": 606,
|
||||
"indicative_cost": "\u00a3900 - \u00a31,500",
|
||||
"improvement_type": "B",
|
||||
"improvement_details": {
|
||||
"improvement_number": 6
|
||||
},
|
||||
"improvement_category": 5,
|
||||
"energy_performance_rating": 47,
|
||||
"environmental_impact_rating": 38
|
||||
},
|
||||
{
|
||||
"sequence": 3,
|
||||
"typical_saving": 251,
|
||||
"indicative_cost": "\u00a35,000 - \u00a310,000",
|
||||
"improvement_type": "W1",
|
||||
"improvement_details": {
|
||||
"improvement_number": 57
|
||||
},
|
||||
"improvement_category": 5,
|
||||
"energy_performance_rating": 51,
|
||||
"environmental_impact_rating": 42
|
||||
},
|
||||
{
|
||||
"sequence": 4,
|
||||
"typical_saving": 134,
|
||||
"indicative_cost": "\u00a3150 - \u00a3250",
|
||||
"improvement_type": "D",
|
||||
"improvement_details": {
|
||||
"improvement_number": 10
|
||||
},
|
||||
"improvement_category": 5,
|
||||
"energy_performance_rating": 53,
|
||||
"environmental_impact_rating": 44
|
||||
},
|
||||
{
|
||||
"sequence": 5,
|
||||
"typical_saving": 276,
|
||||
"indicative_cost": "\u00a3220 - \u00a3250",
|
||||
"improvement_type": "G",
|
||||
"improvement_details": {
|
||||
"improvement_number": 12
|
||||
},
|
||||
"improvement_category": 5,
|
||||
"energy_performance_rating": 57,
|
||||
"environmental_impact_rating": 49
|
||||
},
|
||||
{
|
||||
"sequence": 6,
|
||||
"typical_saving": 629,
|
||||
"indicative_cost": "\u00a32,200 - \u00a33,500",
|
||||
"improvement_type": "I",
|
||||
"improvement_details": {
|
||||
"improvement_number": 20
|
||||
},
|
||||
"improvement_category": 5,
|
||||
"energy_performance_rating": 67,
|
||||
"environmental_impact_rating": 61
|
||||
},
|
||||
{
|
||||
"sequence": 7,
|
||||
"typical_saving": 197,
|
||||
"indicative_cost": "\u00a34,500 - \u00a36,000",
|
||||
"improvement_type": "O",
|
||||
"improvement_details": {
|
||||
"improvement_number": 8
|
||||
},
|
||||
"improvement_category": 5,
|
||||
"energy_performance_rating": 70,
|
||||
"environmental_impact_rating": 66
|
||||
},
|
||||
{
|
||||
"sequence": 8,
|
||||
"typical_saving": 233,
|
||||
"indicative_cost": "\u00a38,000 - \u00a310,000",
|
||||
"improvement_type": "U",
|
||||
"improvement_details": {
|
||||
"improvement_number": 34
|
||||
},
|
||||
"improvement_category": 5,
|
||||
"energy_performance_rating": 74,
|
||||
"environmental_impact_rating": 67
|
||||
}
|
||||
],
|
||||
"co2_emissions_potential": 4.6,
|
||||
"energy_rating_potential": 74,
|
||||
"lighting_cost_potential": {
|
||||
"value": 84,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"schema_version_original": "LIG-21.0.1",
|
||||
"alternative_improvements": [
|
||||
{
|
||||
"sequence": 1,
|
||||
"typical_saving": 172,
|
||||
"improvement_type": "Q2",
|
||||
"improvement_details": {
|
||||
"improvement_number": 55
|
||||
},
|
||||
"improvement_category": 6,
|
||||
"energy_performance_rating": 50,
|
||||
"environmental_impact_rating": 40
|
||||
},
|
||||
{
|
||||
"sequence": 2,
|
||||
"typical_saving": 774,
|
||||
"improvement_type": "J2",
|
||||
"improvement_details": {
|
||||
"improvement_number": 54
|
||||
},
|
||||
"improvement_category": 6,
|
||||
"energy_performance_rating": 64,
|
||||
"environmental_impact_rating": 82
|
||||
},
|
||||
{
|
||||
"sequence": 3,
|
||||
"typical_saving": 828,
|
||||
"improvement_type": "Z3",
|
||||
"improvement_details": {
|
||||
"improvement_number": 53
|
||||
},
|
||||
"improvement_category": 6,
|
||||
"energy_performance_rating": 69,
|
||||
"environmental_impact_rating": 66
|
||||
}
|
||||
],
|
||||
"flueless_gas_fires_count": 0,
|
||||
"hot_water_cost_potential": {
|
||||
"value": 358,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"renewable_heat_incentive": {
|
||||
"water_heating": 3736.92,
|
||||
"space_heating_existing_dwelling": 24523.93
|
||||
},
|
||||
"draughtproofed_door_count": 0,
|
||||
"energy_consumption_current": 466,
|
||||
"has_fixed_air_conditioning": "false",
|
||||
"multiple_glazed_proportion": 52,
|
||||
"calculation_software_version": "4.3.74",
|
||||
"energy_consumption_potential": 170,
|
||||
"environmental_impact_current": 27,
|
||||
"current_energy_efficiency_band": "F",
|
||||
"environmental_impact_potential": 67,
|
||||
"has_heated_separate_conservatory": "false",
|
||||
"potential_energy_efficiency_band": "C",
|
||||
"co2_emissions_current_per_floor_area": 98,
|
||||
"low_energy_fixed_lighting_bulbs_count": 17,
|
||||
"incandescent_fixed_lighting_bulbs_count": 4
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,426 @@
|
|||
{
|
||||
"uprn": 100050881708,
|
||||
"roofs": [
|
||||
{
|
||||
"description": "Pitched, 225 mm loft insulation",
|
||||
"energy_efficiency_rating": 4,
|
||||
"environmental_efficiency_rating": 4
|
||||
}
|
||||
],
|
||||
"walls": [
|
||||
{
|
||||
"description": "Cavity wall, filled cavity",
|
||||
"energy_efficiency_rating": 4,
|
||||
"environmental_efficiency_rating": 4
|
||||
}
|
||||
],
|
||||
"floors": [
|
||||
{
|
||||
"description": "To unheated space, no insulation (assumed)",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
}
|
||||
],
|
||||
"status": "entered",
|
||||
"tenure": 3,
|
||||
"window": {
|
||||
"description": "Fully double glazed",
|
||||
"energy_efficiency_rating": 3,
|
||||
"environmental_efficiency_rating": 3
|
||||
},
|
||||
"lighting": {
|
||||
"description": "Below average lighting efficiency",
|
||||
"energy_efficiency_rating": 2,
|
||||
"environmental_efficiency_rating": 2
|
||||
},
|
||||
"postcode": "S65 3RW",
|
||||
"hot_water": {
|
||||
"description": "From main system",
|
||||
"energy_efficiency_rating": 4,
|
||||
"environmental_efficiency_rating": 4
|
||||
},
|
||||
"post_town": "ROTHERHAM",
|
||||
"built_form": 2,
|
||||
"created_at": "2026-05-27 15:09:39",
|
||||
"door_count": 2,
|
||||
"region_code": 3,
|
||||
"report_type": 2,
|
||||
"sap_heating": {
|
||||
"number_baths": 1,
|
||||
"cylinder_size": 1,
|
||||
"number_baths_wwhrs": 0,
|
||||
"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,
|
||||
"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": 17741
|
||||
}
|
||||
],
|
||||
"immersion_heating_type": "NA",
|
||||
"has_fixed_air_conditioning": "false"
|
||||
},
|
||||
"sap_version": 10.2,
|
||||
"sap_windows": [
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 7,
|
||||
"window_type": 1,
|
||||
"glazing_type": 2,
|
||||
"window_width": 1.7,
|
||||
"window_height": 1.4,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 7,
|
||||
"window_type": 1,
|
||||
"glazing_type": 2,
|
||||
"window_width": 1.2,
|
||||
"window_height": 1,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 7,
|
||||
"window_type": 1,
|
||||
"glazing_type": 2,
|
||||
"window_width": 0.6,
|
||||
"window_height": 1,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 3,
|
||||
"window_type": 1,
|
||||
"glazing_type": 2,
|
||||
"window_width": 0.6,
|
||||
"window_height": 0.7,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 3,
|
||||
"window_type": 1,
|
||||
"glazing_type": 2,
|
||||
"window_width": 0.6,
|
||||
"window_height": 1,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 3,
|
||||
"window_type": 1,
|
||||
"glazing_type": 2,
|
||||
"window_width": 1.2,
|
||||
"window_height": 1,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 3,
|
||||
"window_type": 1,
|
||||
"glazing_type": 2,
|
||||
"window_width": 1.2,
|
||||
"window_height": 1,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 3,
|
||||
"window_type": 1,
|
||||
"glazing_type": 2,
|
||||
"window_width": 1.1,
|
||||
"window_height": 0.8,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 1,
|
||||
"window_type": 1,
|
||||
"glazing_type": 2,
|
||||
"window_width": 0.6,
|
||||
"window_height": 0.7,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 1,
|
||||
"window_type": 1,
|
||||
"glazing_type": 2,
|
||||
"window_width": 0.6,
|
||||
"window_height": 1.1,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
}
|
||||
],
|
||||
"schema_type": "RdSAP-Schema-21.0.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": "(not tested)",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
},
|
||||
"dwelling_type": "Semi-detached house",
|
||||
"language_code": 1,
|
||||
"pressure_test": 4,
|
||||
"property_type": 0,
|
||||
"address_line_1": "9 Woodgrove Road",
|
||||
"assessment_type": "RdSAP",
|
||||
"completion_date": "2026-05-27",
|
||||
"inspection_date": "2026-05-27",
|
||||
"extensions_count": 0,
|
||||
"measurement_type": 1,
|
||||
"total_floor_area": 63,
|
||||
"transaction_type": 14,
|
||||
"conservatory_type": 1,
|
||||
"heated_room_count": 3,
|
||||
"registration_date": "2026-05-27",
|
||||
"sap_energy_source": {
|
||||
"mains_gas": "Y",
|
||||
"meter_type": 2,
|
||||
"pv_connection": 0,
|
||||
"photovoltaic_supply": {
|
||||
"none_or_no_details": {
|
||||
"percent_roof_area": 0
|
||||
}
|
||||
},
|
||||
"wind_turbines_count": 0,
|
||||
"gas_smart_meter_present": "true",
|
||||
"is_dwelling_export_capable": "false",
|
||||
"wind_turbines_terrain_type": 2,
|
||||
"electricity_smart_meter_present": "true"
|
||||
},
|
||||
"secondary_heating": {
|
||||
"description": "None",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
},
|
||||
"extract_fans_count": 2,
|
||||
"sap_building_parts": [
|
||||
{
|
||||
"identifier": "Main Dwelling",
|
||||
"wall_dry_lined": "N",
|
||||
"floor_heat_loss": 2,
|
||||
"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": 31.62,
|
||||
"quantity": "square metres"
|
||||
},
|
||||
"party_wall_length": {
|
||||
"value": 6.2,
|
||||
"quantity": "metres"
|
||||
},
|
||||
"floor_construction": 2,
|
||||
"heat_loss_perimeter": {
|
||||
"value": 16.4,
|
||||
"quantity": "metres"
|
||||
}
|
||||
},
|
||||
{
|
||||
"floor": 1,
|
||||
"room_height": {
|
||||
"value": 2.41,
|
||||
"quantity": "metres"
|
||||
},
|
||||
"total_floor_area": {
|
||||
"value": 31.62,
|
||||
"quantity": "square metres"
|
||||
},
|
||||
"party_wall_length": {
|
||||
"value": 6.2,
|
||||
"quantity": "metres"
|
||||
},
|
||||
"heat_loss_perimeter": {
|
||||
"value": 16.4,
|
||||
"quantity": "metres"
|
||||
}
|
||||
}
|
||||
],
|
||||
"wall_insulation_type": 2,
|
||||
"construction_age_band": "B",
|
||||
"party_wall_construction": 1,
|
||||
"wall_thickness_measured": "N",
|
||||
"roof_insulation_location": 2,
|
||||
"roof_insulation_thickness": "225mm",
|
||||
"wall_insulation_thickness": "NI",
|
||||
"floor_insulation_thickness": "NI"
|
||||
}
|
||||
],
|
||||
"solar_water_heating": "N",
|
||||
"habitable_room_count": 3,
|
||||
"heating_cost_current": {
|
||||
"value": 777,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"insulated_door_count": 0,
|
||||
"co2_emissions_current": 2.3,
|
||||
"energy_rating_average": 60,
|
||||
"energy_rating_current": 70,
|
||||
"lighting_cost_current": {
|
||||
"value": 79,
|
||||
"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": 671,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"hot_water_cost_current": {
|
||||
"value": 172,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"mechanical_ventilation": 0,
|
||||
"percent_draughtproofed": 100,
|
||||
"suggested_improvements": [
|
||||
{
|
||||
"sequence": 1,
|
||||
"typical_saving": {
|
||||
"value": 110,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"indicative_cost": "\u00a35,000 - \u00a310,000",
|
||||
"improvement_type": "W1",
|
||||
"improvement_details": {
|
||||
"improvement_number": 57
|
||||
},
|
||||
"improvement_category": 5,
|
||||
"energy_performance_rating": 73,
|
||||
"environmental_impact_rating": 75
|
||||
},
|
||||
{
|
||||
"sequence": 2,
|
||||
"typical_saving": {
|
||||
"value": 30,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"indicative_cost": "\u00a3120 - \u00a3140",
|
||||
"improvement_type": "E",
|
||||
"improvement_details": {
|
||||
"improvement_number": 35
|
||||
},
|
||||
"improvement_category": 5,
|
||||
"energy_performance_rating": 74,
|
||||
"environmental_impact_rating": 75
|
||||
},
|
||||
{
|
||||
"sequence": 3,
|
||||
"typical_saving": {
|
||||
"value": 184,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"indicative_cost": "\u00a38,000 - \u00a310,000",
|
||||
"improvement_type": "U",
|
||||
"improvement_details": {
|
||||
"improvement_number": 34
|
||||
},
|
||||
"improvement_category": 5,
|
||||
"energy_performance_rating": 79,
|
||||
"environmental_impact_rating": 77
|
||||
}
|
||||
],
|
||||
"co2_emissions_potential": 1.9,
|
||||
"energy_rating_potential": 79,
|
||||
"lighting_cost_potential": {
|
||||
"value": 45,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"schema_version_original": "21.0.1",
|
||||
"hot_water_cost_potential": {
|
||||
"value": 172,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"renewable_heat_incentive": {
|
||||
"water_heating": 2070.91,
|
||||
"space_heating_existing_dwelling": 7543.77
|
||||
},
|
||||
"draughtproofed_door_count": 2,
|
||||
"energy_consumption_current": 205,
|
||||
"has_fixed_air_conditioning": "false",
|
||||
"multiple_glazed_proportion": 100,
|
||||
"calculation_software_version": "5.02r0344",
|
||||
"energy_consumption_potential": 161,
|
||||
"environmental_impact_current": 71,
|
||||
"current_energy_efficiency_band": "C",
|
||||
"environmental_impact_potential": 77,
|
||||
"has_heated_separate_conservatory": "false",
|
||||
"potential_energy_efficiency_band": "C",
|
||||
"co2_emissions_current_per_floor_area": 37,
|
||||
"low_energy_fixed_lighting_bulbs_count": 4,
|
||||
"incandescent_fixed_lighting_bulbs_count": 4
|
||||
}
|
||||
|
|
@ -0,0 +1,351 @@
|
|||
{
|
||||
"uprn": 100051051866,
|
||||
"roofs": [
|
||||
{
|
||||
"description": "Pitched, insulated (assumed)",
|
||||
"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, limited insulation (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": {
|
||||
"addendum_numbers": [
|
||||
15
|
||||
]
|
||||
},
|
||||
"lighting": {
|
||||
"description": "Excellent lighting efficiency",
|
||||
"energy_efficiency_rating": 5,
|
||||
"environmental_efficiency_rating": 5
|
||||
},
|
||||
"postcode": "S20 7NQ",
|
||||
"hot_water": {
|
||||
"description": "From main system",
|
||||
"energy_efficiency_rating": 4,
|
||||
"environmental_efficiency_rating": 4
|
||||
},
|
||||
"post_town": "SHEFFIELD",
|
||||
"psv_count": 0,
|
||||
"built_form": 1,
|
||||
"created_at": "2026-05-18 17:37:45",
|
||||
"door_count": 1,
|
||||
"region_code": 11,
|
||||
"report_type": 2,
|
||||
"sap_heating": {
|
||||
"number_baths": 1,
|
||||
"cylinder_size": 2,
|
||||
"shower_outlets": [
|
||||
{
|
||||
"shower_wwhrs": 1,
|
||||
"shower_outlet_type": 4
|
||||
}
|
||||
],
|
||||
"number_baths_wwhrs": 0,
|
||||
"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,
|
||||
"emitter_temperature": 1,
|
||||
"main_heating_number": 1,
|
||||
"main_heating_control": 2104,
|
||||
"main_heating_category": 2,
|
||||
"main_heating_fraction": 1,
|
||||
"sap_main_heating_code": 102,
|
||||
"central_heating_pump_age": 0,
|
||||
"main_heating_data_source": 2,
|
||||
"main_heating_index_number": 18773
|
||||
}
|
||||
],
|
||||
"immersion_heating_type": "NA",
|
||||
"cylinder_insulation_type": 1,
|
||||
"has_fixed_air_conditioning": "false",
|
||||
"cylinder_insulation_thickness": 50
|
||||
},
|
||||
"sap_version": 10.2,
|
||||
"sap_windows": [
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 7,
|
||||
"window_type": 1,
|
||||
"glazing_type": 2,
|
||||
"window_width": {
|
||||
"value": 1.6,
|
||||
"quantity": "m"
|
||||
},
|
||||
"window_height": {
|
||||
"value": 1.2,
|
||||
"quantity": "m"
|
||||
},
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 7,
|
||||
"window_type": 1,
|
||||
"glazing_type": 2,
|
||||
"window_width": {
|
||||
"value": 1.2,
|
||||
"quantity": "m"
|
||||
},
|
||||
"window_height": {
|
||||
"value": 1.2,
|
||||
"quantity": "m"
|
||||
},
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 7,
|
||||
"window_type": 1,
|
||||
"glazing_type": 2,
|
||||
"window_width": {
|
||||
"value": 1.6,
|
||||
"quantity": "m"
|
||||
},
|
||||
"window_height": {
|
||||
"value": 1.6,
|
||||
"quantity": "m"
|
||||
},
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 3,
|
||||
"window_type": 1,
|
||||
"glazing_type": 2,
|
||||
"window_width": {
|
||||
"value": 1.6,
|
||||
"quantity": "m"
|
||||
},
|
||||
"window_height": {
|
||||
"value": 1.2,
|
||||
"quantity": "m"
|
||||
},
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 3,
|
||||
"window_type": 1,
|
||||
"glazing_type": 2,
|
||||
"window_width": {
|
||||
"value": 0.5,
|
||||
"quantity": "m"
|
||||
},
|
||||
"window_height": {
|
||||
"value": 1.2,
|
||||
"quantity": "m"
|
||||
},
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
}
|
||||
],
|
||||
"schema_type": "RdSAP-Schema-21.0.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": "(not tested)",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
},
|
||||
"dwelling_type": "Detached bungalow",
|
||||
"language_code": 1,
|
||||
"pressure_test": 4,
|
||||
"property_type": 1,
|
||||
"address_line_1": "3 Pentland Gardens",
|
||||
"address_line_2": "Waterthorpe",
|
||||
"assessment_type": "RdSAP",
|
||||
"completion_date": "2026-05-18",
|
||||
"inspection_date": "2026-05-18",
|
||||
"extensions_count": 0,
|
||||
"measurement_type": 1,
|
||||
"open_flues_count": 0,
|
||||
"total_floor_area": 60,
|
||||
"transaction_type": 1,
|
||||
"conservatory_type": 2,
|
||||
"heated_room_count": 4,
|
||||
"other_flues_count": 0,
|
||||
"registration_date": "2026-05-18",
|
||||
"sap_energy_source": {
|
||||
"mains_gas": "Y",
|
||||
"meter_type": 3,
|
||||
"pv_connection": 0,
|
||||
"photovoltaic_supply": {
|
||||
"none_or_no_details": {
|
||||
"percent_roof_area": 0
|
||||
}
|
||||
},
|
||||
"wind_turbines_count": 0,
|
||||
"gas_smart_meter_present": "false",
|
||||
"is_dwelling_export_capable": "false",
|
||||
"wind_turbines_terrain_type": 2,
|
||||
"electricity_smart_meter_present": "false"
|
||||
},
|
||||
"secondary_heating": {
|
||||
"description": "None",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
},
|
||||
"closed_flues_count": 0,
|
||||
"extract_fans_count": 0,
|
||||
"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.4,
|
||||
"floor_insulation": 1,
|
||||
"total_floor_area": 60,
|
||||
"party_wall_length": 0,
|
||||
"floor_construction": 2,
|
||||
"heat_loss_perimeter": 32
|
||||
}
|
||||
],
|
||||
"wall_insulation_type": 4,
|
||||
"construction_age_band": "I",
|
||||
"party_wall_construction": "NA",
|
||||
"wall_thickness_measured": "Y",
|
||||
"roof_insulation_location": 4,
|
||||
"roof_insulation_thickness": "NI"
|
||||
}
|
||||
],
|
||||
"boilers_flues_count": 0,
|
||||
"open_chimneys_count": 0,
|
||||
"solar_water_heating": "N",
|
||||
"habitable_room_count": 4,
|
||||
"heating_cost_current": {
|
||||
"value": 701,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"insulated_door_count": 0,
|
||||
"co2_emissions_current": 2.3,
|
||||
"energy_rating_average": 60,
|
||||
"energy_rating_current": 70,
|
||||
"lighting_cost_current": {
|
||||
"value": 40,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"main_heating_controls": [
|
||||
{
|
||||
"description": "Programmer and room thermostat",
|
||||
"energy_efficiency_rating": 3,
|
||||
"environmental_efficiency_rating": 3
|
||||
}
|
||||
],
|
||||
"blocked_chimneys_count": 0,
|
||||
"has_hot_water_cylinder": "true",
|
||||
"heating_cost_potential": {
|
||||
"value": 701,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"hot_water_cost_current": {
|
||||
"value": 235,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"mechanical_ventilation": 0,
|
||||
"percent_draughtproofed": 100,
|
||||
"schema_version_current": "LIG-21.0",
|
||||
"suggested_improvements": [
|
||||
{
|
||||
"sequence": 1,
|
||||
"typical_saving": 238,
|
||||
"indicative_cost": "\u00a38,000 - \u00a310,000",
|
||||
"improvement_type": "U",
|
||||
"improvement_details": {
|
||||
"improvement_number": 34
|
||||
},
|
||||
"improvement_category": 5,
|
||||
"energy_performance_rating": 82,
|
||||
"environmental_impact_rating": 73
|
||||
}
|
||||
],
|
||||
"co2_emissions_potential": 2.1,
|
||||
"energy_rating_potential": 82,
|
||||
"lighting_cost_potential": {
|
||||
"value": 40,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"schema_version_original": "LIG-21.0",
|
||||
"hot_water_cost_potential": {
|
||||
"value": 235,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"renewable_heat_incentive": {
|
||||
"water_heating": 2544.67,
|
||||
"space_heating_existing_dwelling": 6292.1
|
||||
},
|
||||
"draughtproofed_door_count": 1,
|
||||
"energy_consumption_current": 209,
|
||||
"has_fixed_air_conditioning": "false",
|
||||
"multiple_glazed_proportion": 100,
|
||||
"calculation_software_version": "10.2.2.0",
|
||||
"energy_consumption_potential": 185,
|
||||
"environmental_impact_current": 70,
|
||||
"cfl_fixed_lighting_bulbs_count": 0,
|
||||
"current_energy_efficiency_band": "C",
|
||||
"environmental_impact_potential": 73,
|
||||
"led_fixed_lighting_bulbs_count": 8,
|
||||
"has_heated_separate_conservatory": "false",
|
||||
"potential_energy_efficiency_band": "B",
|
||||
"co2_emissions_current_per_floor_area": 38,
|
||||
"incandescent_fixed_lighting_bulbs_count": 0
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,410 @@
|
|||
{
|
||||
"uprn": 100061275133,
|
||||
"roofs": [
|
||||
{
|
||||
"description": "Pitched, 300 mm loft insulation",
|
||||
"energy_efficiency_rating": 5,
|
||||
"environmental_efficiency_rating": 5
|
||||
}
|
||||
],
|
||||
"walls": [
|
||||
{
|
||||
"description": "Solid brick, with external insulation",
|
||||
"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,
|
||||
"window": {
|
||||
"description": "High performance glazing",
|
||||
"energy_efficiency_rating": 4,
|
||||
"environmental_efficiency_rating": 4
|
||||
},
|
||||
"lighting": {
|
||||
"description": "Excellent lighting efficiency",
|
||||
"energy_efficiency_rating": 5,
|
||||
"environmental_efficiency_rating": 5
|
||||
},
|
||||
"postcode": "ME7 2ES",
|
||||
"hot_water": {
|
||||
"description": "From main system",
|
||||
"energy_efficiency_rating": 4,
|
||||
"environmental_efficiency_rating": 4
|
||||
},
|
||||
"post_town": "GILLINGHAM",
|
||||
"built_form": 2,
|
||||
"created_at": "2026-05-20 08:39:48",
|
||||
"door_count": 2,
|
||||
"region_code": 14,
|
||||
"report_type": 2,
|
||||
"sap_heating": {
|
||||
"number_baths": 1,
|
||||
"cylinder_size": 1,
|
||||
"number_baths_wwhrs": 0,
|
||||
"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,
|
||||
"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": 18908
|
||||
}
|
||||
],
|
||||
"immersion_heating_type": "NA",
|
||||
"has_fixed_air_conditioning": "false"
|
||||
},
|
||||
"sap_version": 10.2,
|
||||
"sap_windows": [
|
||||
{
|
||||
"orientation": 8,
|
||||
"window_type": 1,
|
||||
"glazing_type": 13,
|
||||
"window_width": 1.19,
|
||||
"window_height": 1.13,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"orientation": 8,
|
||||
"window_type": 1,
|
||||
"glazing_type": 13,
|
||||
"window_width": 0.57,
|
||||
"window_height": 0.55,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"orientation": 8,
|
||||
"window_type": 1,
|
||||
"glazing_type": 13,
|
||||
"window_width": 1.19,
|
||||
"window_height": 1.11,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"orientation": 2,
|
||||
"window_type": 1,
|
||||
"glazing_type": 13,
|
||||
"window_width": 1.05,
|
||||
"window_height": 0.85,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"orientation": 2,
|
||||
"window_type": 1,
|
||||
"glazing_type": 13,
|
||||
"window_width": 0.57,
|
||||
"window_height": 0.56,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"orientation": 4,
|
||||
"window_type": 1,
|
||||
"glazing_type": 13,
|
||||
"window_width": 2.11,
|
||||
"window_height": 1.13,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"orientation": 4,
|
||||
"window_type": 1,
|
||||
"glazing_type": 13,
|
||||
"window_width": 1.8,
|
||||
"window_height": 0.97,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"orientation": 4,
|
||||
"window_type": 1,
|
||||
"glazing_type": 13,
|
||||
"window_width": 1.8,
|
||||
"window_height": 0.97,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"orientation": 4,
|
||||
"window_type": 1,
|
||||
"glazing_type": 13,
|
||||
"window_width": 1.2,
|
||||
"window_height": 0.99,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"orientation": 8,
|
||||
"window_type": 1,
|
||||
"glazing_type": 13,
|
||||
"window_width": 1.8,
|
||||
"window_height": 0.97,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"orientation": 8,
|
||||
"window_type": 1,
|
||||
"glazing_type": 13,
|
||||
"window_width": 1.19,
|
||||
"window_height": 0.97,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"orientation": 8,
|
||||
"window_type": 1,
|
||||
"glazing_type": 13,
|
||||
"window_width": 1.18,
|
||||
"window_height": 0.99,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
}
|
||||
],
|
||||
"schema_type": "RdSAP-Schema-21.0.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": "(not tested)",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
},
|
||||
"dwelling_type": "Semi-detached house",
|
||||
"language_code": 1,
|
||||
"pressure_test": 4,
|
||||
"property_type": 0,
|
||||
"address_line_1": "75 Toronto Road",
|
||||
"assessment_type": "RdSAP",
|
||||
"completion_date": "2026-05-20",
|
||||
"inspection_date": "2026-05-19",
|
||||
"extensions_count": 0,
|
||||
"measurement_type": 1,
|
||||
"total_floor_area": 78,
|
||||
"transaction_type": 8,
|
||||
"conservatory_type": 1,
|
||||
"heated_room_count": 3,
|
||||
"registration_date": "2026-05-20",
|
||||
"sap_energy_source": {
|
||||
"mains_gas": "Y",
|
||||
"meter_type": 2,
|
||||
"pv_connection": 0,
|
||||
"photovoltaic_supply": {
|
||||
"none_or_no_details": {
|
||||
"percent_roof_area": 0
|
||||
}
|
||||
},
|
||||
"wind_turbines_count": 0,
|
||||
"gas_smart_meter_present": "true",
|
||||
"is_dwelling_export_capable": "false",
|
||||
"wind_turbines_terrain_type": 2,
|
||||
"electricity_smart_meter_present": "true"
|
||||
},
|
||||
"secondary_heating": {
|
||||
"description": "None",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
},
|
||||
"sap_building_parts": [
|
||||
{
|
||||
"identifier": "Main Dwelling",
|
||||
"wall_dry_lined": "N",
|
||||
"wall_thickness": 400,
|
||||
"floor_heat_loss": 7,
|
||||
"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": 38.99,
|
||||
"quantity": "square metres"
|
||||
},
|
||||
"party_wall_length": {
|
||||
"value": 5.09,
|
||||
"quantity": "metres"
|
||||
},
|
||||
"floor_construction": 0,
|
||||
"heat_loss_perimeter": {
|
||||
"value": 17.92,
|
||||
"quantity": "metres"
|
||||
}
|
||||
},
|
||||
{
|
||||
"floor": 1,
|
||||
"room_height": {
|
||||
"value": 2.4,
|
||||
"quantity": "metres"
|
||||
},
|
||||
"total_floor_area": {
|
||||
"value": 38.99,
|
||||
"quantity": "square metres"
|
||||
},
|
||||
"party_wall_length": {
|
||||
"value": 5.09,
|
||||
"quantity": "metres"
|
||||
},
|
||||
"heat_loss_perimeter": {
|
||||
"value": 17.92,
|
||||
"quantity": "metres"
|
||||
}
|
||||
}
|
||||
],
|
||||
"wall_insulation_type": 1,
|
||||
"construction_age_band": "B",
|
||||
"party_wall_construction": 1,
|
||||
"wall_thickness_measured": "Y",
|
||||
"roof_insulation_location": 2,
|
||||
"roof_insulation_thickness": "300mm",
|
||||
"wall_insulation_thickness": "100mm",
|
||||
"floor_insulation_thickness": "NI"
|
||||
}
|
||||
],
|
||||
"solar_water_heating": "N",
|
||||
"habitable_room_count": 3,
|
||||
"heating_cost_current": {
|
||||
"value": 608,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"insulated_door_count": 0,
|
||||
"co2_emissions_current": 1.9,
|
||||
"energy_rating_average": 60,
|
||||
"energy_rating_current": 76,
|
||||
"lighting_cost_current": {
|
||||
"value": 58,
|
||||
"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": 608,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"hot_water_cost_current": {
|
||||
"value": 182,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"mechanical_ventilation": 0,
|
||||
"percent_draughtproofed": 100,
|
||||
"suggested_improvements": [
|
||||
{
|
||||
"sequence": 1,
|
||||
"typical_saving": {
|
||||
"value": 216,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"indicative_cost": "\u00a38,000 - \u00a310,000",
|
||||
"improvement_type": "U",
|
||||
"improvement_details": {
|
||||
"improvement_number": 34
|
||||
},
|
||||
"improvement_category": 5,
|
||||
"energy_performance_rating": 81,
|
||||
"environmental_impact_rating": 79
|
||||
}
|
||||
],
|
||||
"co2_emissions_potential": 1.8,
|
||||
"energy_rating_potential": 81,
|
||||
"lighting_cost_potential": {
|
||||
"value": 58,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"schema_version_original": "21.0.1",
|
||||
"hot_water_cost_potential": {
|
||||
"value": 182,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"renewable_heat_incentive": {
|
||||
"water_heating": 2203.16,
|
||||
"space_heating_existing_dwelling": 5517.85
|
||||
},
|
||||
"draughtproofed_door_count": 2,
|
||||
"energy_consumption_current": 133,
|
||||
"has_fixed_air_conditioning": "false",
|
||||
"multiple_glazed_proportion": 100,
|
||||
"calculation_software_version": "5.02r0344",
|
||||
"energy_consumption_potential": 118,
|
||||
"environmental_impact_current": 78,
|
||||
"current_energy_efficiency_band": "C",
|
||||
"environmental_impact_potential": 79,
|
||||
"led_fixed_lighting_bulbs_count": 5,
|
||||
"has_heated_separate_conservatory": "false",
|
||||
"potential_energy_efficiency_band": "B",
|
||||
"co2_emissions_current_per_floor_area": 24,
|
||||
"incandescent_fixed_lighting_bulbs_count": 0
|
||||
}
|
||||
|
|
@ -0,0 +1,337 @@
|
|||
{
|
||||
"uprn": 10012334488,
|
||||
"roofs": [
|
||||
{
|
||||
"description": "(another dwelling above)",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
}
|
||||
],
|
||||
"walls": [
|
||||
{
|
||||
"description": "System built, as built, insulated (assumed)",
|
||||
"energy_efficiency_rating": 4,
|
||||
"environmental_efficiency_rating": 4
|
||||
}
|
||||
],
|
||||
"floors": [
|
||||
{
|
||||
"description": "(another dwelling below)",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
}
|
||||
],
|
||||
"status": "entered",
|
||||
"tenure": 3,
|
||||
"window": {
|
||||
"description": "Fully double glazed",
|
||||
"energy_efficiency_rating": 3,
|
||||
"environmental_efficiency_rating": 3
|
||||
},
|
||||
"lighting": {
|
||||
"description": "Excellent lighting efficiency",
|
||||
"energy_efficiency_rating": 5,
|
||||
"environmental_efficiency_rating": 5
|
||||
},
|
||||
"postcode": "SL4 3BP",
|
||||
"hot_water": {
|
||||
"description": "From main system, no cylinder thermostat",
|
||||
"energy_efficiency_rating": 1,
|
||||
"environmental_efficiency_rating": 4
|
||||
},
|
||||
"post_town": "WINDSOR",
|
||||
"built_form": "NR",
|
||||
"created_at": "2026-05-14 18:33:57",
|
||||
"door_count": 1,
|
||||
"region_code": 17,
|
||||
"report_type": 2,
|
||||
"sap_heating": {
|
||||
"number_baths": 1,
|
||||
"cylinder_size": 0,
|
||||
"shower_outlets": [
|
||||
{
|
||||
"shower_wwhrs": 1,
|
||||
"shower_outlet_type": 1
|
||||
}
|
||||
],
|
||||
"number_baths_wwhrs": 0,
|
||||
"water_heating_code": 901,
|
||||
"water_heating_fuel": 29,
|
||||
"main_heating_details": [
|
||||
{
|
||||
"has_fghrs": "N",
|
||||
"main_fuel_type": 29,
|
||||
"heat_emitter_type": 2,
|
||||
"emitter_temperature": "NA",
|
||||
"main_heating_number": 1,
|
||||
"main_heating_control": 2110,
|
||||
"main_heating_category": 2,
|
||||
"main_heating_fraction": 1,
|
||||
"sap_main_heating_code": 195,
|
||||
"central_heating_pump_age": 0,
|
||||
"main_heating_data_source": 2
|
||||
}
|
||||
],
|
||||
"immersion_heating_type": "NA",
|
||||
"has_fixed_air_conditioning": "false"
|
||||
},
|
||||
"sap_version": 10.2,
|
||||
"sap_windows": [
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 3,
|
||||
"window_type": 2,
|
||||
"glazing_type": 2,
|
||||
"window_width": 0.85,
|
||||
"window_height": 1.11,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 3,
|
||||
"window_type": 2,
|
||||
"glazing_type": 2,
|
||||
"window_width": 0.85,
|
||||
"window_height": 1.11,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 3,
|
||||
"window_type": 2,
|
||||
"glazing_type": 2,
|
||||
"window_width": 0.85,
|
||||
"window_height": 1.11,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 3,
|
||||
"window_type": 2,
|
||||
"glazing_type": 2,
|
||||
"window_width": 0.85,
|
||||
"window_height": 1.11,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 3,
|
||||
"window_type": 2,
|
||||
"glazing_type": 2,
|
||||
"window_width": 0.85,
|
||||
"window_height": 1.11,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 3,
|
||||
"window_type": 2,
|
||||
"glazing_type": 2,
|
||||
"window_width": 0.85,
|
||||
"window_height": 1.11,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
}
|
||||
],
|
||||
"schema_type": "RdSAP-Schema-21.0.1",
|
||||
"uprn_source": "Energy Assessor",
|
||||
"country_code": "ENG",
|
||||
"main_heating": [
|
||||
{
|
||||
"description": "Boiler and underfloor heating, electric",
|
||||
"energy_efficiency_rating": 4,
|
||||
"environmental_efficiency_rating": 5
|
||||
}
|
||||
],
|
||||
"air_tightness": {
|
||||
"description": "(not tested)",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
},
|
||||
"dwelling_type": "Mid-floor flat",
|
||||
"language_code": 1,
|
||||
"pressure_test": 4,
|
||||
"property_type": 2,
|
||||
"address_line_1": "Flat 12",
|
||||
"address_line_2": "Alexandra Court 25a",
|
||||
"address_line_3": "St. Leonards Road",
|
||||
"assessment_type": "RdSAP",
|
||||
"completion_date": "2026-05-14",
|
||||
"inspection_date": "2026-05-14",
|
||||
"extensions_count": 0,
|
||||
"measurement_type": 1,
|
||||
"sap_flat_details": {
|
||||
"level": 2,
|
||||
"top_storey": "N",
|
||||
"storey_count": 4,
|
||||
"flat_location": 3,
|
||||
"heat_loss_corridor": 1
|
||||
},
|
||||
"total_floor_area": 62,
|
||||
"transaction_type": 14,
|
||||
"conservatory_type": 1,
|
||||
"has_draught_lobby": "true",
|
||||
"heated_room_count": 2,
|
||||
"registration_date": "2026-05-14",
|
||||
"sap_energy_source": {
|
||||
"mains_gas": "N",
|
||||
"meter_type": 1,
|
||||
"pv_connection": 0,
|
||||
"photovoltaic_supply": {
|
||||
"none_or_no_details": {
|
||||
"percent_roof_area": 0
|
||||
}
|
||||
},
|
||||
"wind_turbines_count": 0,
|
||||
"gas_smart_meter_present": "false",
|
||||
"is_dwelling_export_capable": "false",
|
||||
"wind_turbines_terrain_type": 1,
|
||||
"electricity_smart_meter_present": "false"
|
||||
},
|
||||
"secondary_heating": {
|
||||
"description": "None",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
},
|
||||
"extract_fans_count": 2,
|
||||
"sap_building_parts": [
|
||||
{
|
||||
"identifier": "Main Dwelling",
|
||||
"wall_dry_lined": "N",
|
||||
"wall_thickness": 260,
|
||||
"floor_heat_loss": 6,
|
||||
"roof_construction": 3,
|
||||
"wall_construction": 8,
|
||||
"building_part_number": 1,
|
||||
"sap_floor_dimensions": [
|
||||
{
|
||||
"floor": 0,
|
||||
"room_height": {
|
||||
"value": 2.48,
|
||||
"quantity": "metres"
|
||||
},
|
||||
"total_floor_area": {
|
||||
"value": 62,
|
||||
"quantity": "square metres"
|
||||
},
|
||||
"party_wall_length": {
|
||||
"value": 23.99,
|
||||
"quantity": "metres"
|
||||
},
|
||||
"heat_loss_perimeter": {
|
||||
"value": 11.89,
|
||||
"quantity": "metres"
|
||||
}
|
||||
}
|
||||
],
|
||||
"wall_insulation_type": 4,
|
||||
"construction_age_band": "L",
|
||||
"party_wall_construction": 1,
|
||||
"wall_thickness_measured": "Y",
|
||||
"roof_insulation_location": "ND",
|
||||
"roof_insulation_thickness": "ND",
|
||||
"wall_insulation_thickness": "NI"
|
||||
}
|
||||
],
|
||||
"solar_water_heating": "N",
|
||||
"habitable_room_count": 2,
|
||||
"heating_cost_current": {
|
||||
"value": 136,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"insulated_door_count": 0,
|
||||
"co2_emissions_current": 0.5,
|
||||
"energy_rating_average": 60,
|
||||
"energy_rating_current": 72,
|
||||
"lighting_cost_current": {
|
||||
"value": 51,
|
||||
"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": 136,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"hot_water_cost_current": {
|
||||
"value": 893,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"mechanical_ventilation": 0,
|
||||
"percent_draughtproofed": 100,
|
||||
"suggested_improvements": [
|
||||
{
|
||||
"sequence": 1,
|
||||
"typical_saving": {
|
||||
"value": 91,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"indicative_cost": "\u00a3600 - \u00a31,500",
|
||||
"improvement_type": "Y",
|
||||
"improvement_details": {
|
||||
"improvement_number": 49
|
||||
},
|
||||
"improvement_category": 5,
|
||||
"energy_performance_rating": 74,
|
||||
"environmental_impact_rating": 93
|
||||
}
|
||||
],
|
||||
"co2_emissions_potential": 0.5,
|
||||
"energy_rating_potential": 74,
|
||||
"lighting_cost_potential": {
|
||||
"value": 51,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"schema_version_original": "21.0.1",
|
||||
"hot_water_cost_potential": {
|
||||
"value": 802,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"renewable_heat_incentive": {
|
||||
"water_heating": 2802.07,
|
||||
"space_heating_existing_dwelling": 628.72
|
||||
},
|
||||
"draughtproofed_door_count": 1,
|
||||
"energy_consumption_current": 92,
|
||||
"has_fixed_air_conditioning": "false",
|
||||
"multiple_glazed_proportion": 100,
|
||||
"calculation_software_version": "5.02r0344",
|
||||
"energy_consumption_potential": 85,
|
||||
"environmental_impact_current": 93,
|
||||
"current_energy_efficiency_band": "C",
|
||||
"environmental_impact_potential": 93,
|
||||
"led_fixed_lighting_bulbs_count": 23,
|
||||
"has_heated_separate_conservatory": "false",
|
||||
"potential_energy_efficiency_band": "C",
|
||||
"co2_emissions_current_per_floor_area": 9,
|
||||
"incandescent_fixed_lighting_bulbs_count": 0
|
||||
}
|
||||
|
|
@ -0,0 +1,491 @@
|
|||
{
|
||||
"uprn": 10070056075,
|
||||
"roofs": [
|
||||
{
|
||||
"description": "Pitched, insulated",
|
||||
"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, 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": {
|
||||
"addendum_numbers": [
|
||||
15
|
||||
]
|
||||
},
|
||||
"lighting": {
|
||||
"description": "Excellent lighting efficiency",
|
||||
"energy_efficiency_rating": 5,
|
||||
"environmental_efficiency_rating": 5
|
||||
},
|
||||
"postcode": "BD10 9UG",
|
||||
"hot_water": {
|
||||
"description": "From main system",
|
||||
"energy_efficiency_rating": 4,
|
||||
"environmental_efficiency_rating": 4
|
||||
},
|
||||
"post_town": "BRADFORD",
|
||||
"psv_count": 0,
|
||||
"built_form": 1,
|
||||
"created_at": "2026-05-15 10:22:34",
|
||||
"door_count": 1,
|
||||
"region_code": 11,
|
||||
"report_type": 2,
|
||||
"sap_heating": {
|
||||
"number_baths": 1,
|
||||
"cylinder_size": 4,
|
||||
"shower_outlets": [
|
||||
{
|
||||
"shower_wwhrs": 1,
|
||||
"shower_outlet_type": 4
|
||||
}
|
||||
],
|
||||
"number_baths_wwhrs": 0,
|
||||
"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,
|
||||
"emitter_temperature": 1,
|
||||
"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": 19012
|
||||
}
|
||||
],
|
||||
"immersion_heating_type": "NA",
|
||||
"cylinder_insulation_type": 1,
|
||||
"has_fixed_air_conditioning": "false",
|
||||
"cylinder_insulation_thickness": 80
|
||||
},
|
||||
"sap_version": 10.2,
|
||||
"sap_windows": [
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 5,
|
||||
"window_type": 1,
|
||||
"glazing_type": 2,
|
||||
"window_width": {
|
||||
"value": 1,
|
||||
"quantity": "m"
|
||||
},
|
||||
"window_height": {
|
||||
"value": 1.2,
|
||||
"quantity": "m"
|
||||
},
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 5,
|
||||
"window_type": 1,
|
||||
"glazing_type": 2,
|
||||
"window_width": {
|
||||
"value": 1,
|
||||
"quantity": "m"
|
||||
},
|
||||
"window_height": {
|
||||
"value": 1.2,
|
||||
"quantity": "m"
|
||||
},
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 5,
|
||||
"window_type": 1,
|
||||
"glazing_type": 2,
|
||||
"window_width": {
|
||||
"value": 1,
|
||||
"quantity": "m"
|
||||
},
|
||||
"window_height": {
|
||||
"value": 1.2,
|
||||
"quantity": "m"
|
||||
},
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 5,
|
||||
"window_type": 1,
|
||||
"glazing_type": 2,
|
||||
"window_width": {
|
||||
"value": 1,
|
||||
"quantity": "m"
|
||||
},
|
||||
"window_height": {
|
||||
"value": 1.2,
|
||||
"quantity": "m"
|
||||
},
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 5,
|
||||
"window_type": 1,
|
||||
"glazing_type": 2,
|
||||
"window_width": {
|
||||
"value": 1,
|
||||
"quantity": "m"
|
||||
},
|
||||
"window_height": {
|
||||
"value": 1.2,
|
||||
"quantity": "m"
|
||||
},
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 5,
|
||||
"window_type": 1,
|
||||
"glazing_type": 2,
|
||||
"window_width": {
|
||||
"value": 1,
|
||||
"quantity": "m"
|
||||
},
|
||||
"window_height": {
|
||||
"value": 1.2,
|
||||
"quantity": "m"
|
||||
},
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 5,
|
||||
"window_type": 1,
|
||||
"glazing_type": 2,
|
||||
"window_width": {
|
||||
"value": 1,
|
||||
"quantity": "m"
|
||||
},
|
||||
"window_height": {
|
||||
"value": 1.2,
|
||||
"quantity": "m"
|
||||
},
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 5,
|
||||
"window_type": 1,
|
||||
"glazing_type": 2,
|
||||
"window_width": {
|
||||
"value": 1,
|
||||
"quantity": "m"
|
||||
},
|
||||
"window_height": {
|
||||
"value": 1.2,
|
||||
"quantity": "m"
|
||||
},
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 5,
|
||||
"window_type": 1,
|
||||
"glazing_type": 2,
|
||||
"window_width": {
|
||||
"value": 1,
|
||||
"quantity": "m"
|
||||
},
|
||||
"window_height": {
|
||||
"value": 1.2,
|
||||
"quantity": "m"
|
||||
},
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 5,
|
||||
"window_type": 1,
|
||||
"glazing_type": 2,
|
||||
"window_width": {
|
||||
"value": 1,
|
||||
"quantity": "m"
|
||||
},
|
||||
"window_height": {
|
||||
"value": 1.2,
|
||||
"quantity": "m"
|
||||
},
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 5,
|
||||
"window_type": 1,
|
||||
"glazing_type": 2,
|
||||
"window_width": {
|
||||
"value": 1,
|
||||
"quantity": "m"
|
||||
},
|
||||
"window_height": {
|
||||
"value": 1.2,
|
||||
"quantity": "m"
|
||||
},
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 5,
|
||||
"window_type": 1,
|
||||
"glazing_type": 2,
|
||||
"window_width": {
|
||||
"value": 1,
|
||||
"quantity": "m"
|
||||
},
|
||||
"window_height": {
|
||||
"value": 1.2,
|
||||
"quantity": "m"
|
||||
},
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
}
|
||||
],
|
||||
"schema_type": "RdSAP-Schema-21.0.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": "(not tested)",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
},
|
||||
"dwelling_type": "Detached house",
|
||||
"language_code": 1,
|
||||
"pressure_test": 4,
|
||||
"property_type": 0,
|
||||
"address_line_1": "36 Longlands",
|
||||
"address_line_2": "Idle",
|
||||
"assessment_type": "RdSAP",
|
||||
"completion_date": "2026-05-15",
|
||||
"inspection_date": "2026-05-08",
|
||||
"extensions_count": 0,
|
||||
"measurement_type": 1,
|
||||
"open_flues_count": 0,
|
||||
"total_floor_area": 139,
|
||||
"transaction_type": 5,
|
||||
"conservatory_type": 1,
|
||||
"heated_room_count": 8,
|
||||
"other_flues_count": 0,
|
||||
"registration_date": "2026-05-15",
|
||||
"sap_energy_source": {
|
||||
"mains_gas": "Y",
|
||||
"meter_type": 1,
|
||||
"pv_connection": 0,
|
||||
"photovoltaic_supply": {
|
||||
"none_or_no_details": {
|
||||
"percent_roof_area": 0
|
||||
}
|
||||
},
|
||||
"wind_turbines_count": 0,
|
||||
"gas_smart_meter_present": "false",
|
||||
"is_dwelling_export_capable": "false",
|
||||
"wind_turbines_terrain_type": 2,
|
||||
"electricity_smart_meter_present": "false"
|
||||
},
|
||||
"secondary_heating": {
|
||||
"description": "None",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
},
|
||||
"closed_flues_count": 0,
|
||||
"extract_fans_count": 0,
|
||||
"sap_building_parts": [
|
||||
{
|
||||
"identifier": "Main Dwelling",
|
||||
"wall_dry_lined": "N",
|
||||
"wall_thickness": 400,
|
||||
"floor_heat_loss": 7,
|
||||
"roof_construction": 8,
|
||||
"wall_construction": 4,
|
||||
"building_part_number": 1,
|
||||
"sap_floor_dimensions": [
|
||||
{
|
||||
"floor": 0,
|
||||
"room_height": 2.3,
|
||||
"floor_insulation": 1,
|
||||
"total_floor_area": 69.6,
|
||||
"party_wall_length": 0,
|
||||
"floor_construction": 2,
|
||||
"heat_loss_perimeter": 37.7
|
||||
},
|
||||
{
|
||||
"floor": 1,
|
||||
"room_height": 2.3,
|
||||
"total_floor_area": 69.6,
|
||||
"party_wall_length": 0,
|
||||
"heat_loss_perimeter": 37.7
|
||||
}
|
||||
],
|
||||
"wall_insulation_type": 4,
|
||||
"construction_age_band": "K",
|
||||
"party_wall_construction": "NA",
|
||||
"wall_thickness_measured": "N",
|
||||
"roof_insulation_location": 7,
|
||||
"wall_insulation_thickness": "NI",
|
||||
"sloping_ceiling_insulation_thickness": "AB"
|
||||
}
|
||||
],
|
||||
"boilers_flues_count": 0,
|
||||
"open_chimneys_count": 0,
|
||||
"solar_water_heating": "N",
|
||||
"habitable_room_count": 8,
|
||||
"heating_cost_current": {
|
||||
"value": 938,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"insulated_door_count": 0,
|
||||
"co2_emissions_current": 3.0,
|
||||
"energy_rating_average": 60,
|
||||
"energy_rating_current": 80,
|
||||
"lighting_cost_current": {
|
||||
"value": 89,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"main_heating_controls": [
|
||||
{
|
||||
"description": "Programmer, room thermostat and TRVs",
|
||||
"energy_efficiency_rating": 4,
|
||||
"environmental_efficiency_rating": 4
|
||||
}
|
||||
],
|
||||
"blocked_chimneys_count": 0,
|
||||
"has_hot_water_cylinder": "true",
|
||||
"heating_cost_potential": {
|
||||
"value": 938,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"hot_water_cost_current": {
|
||||
"value": 236,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"mechanical_ventilation": 0,
|
||||
"percent_draughtproofed": 100,
|
||||
"schema_version_current": "LIG-21.0",
|
||||
"suggested_improvements": [
|
||||
{
|
||||
"sequence": 1,
|
||||
"typical_saving": 308,
|
||||
"indicative_cost": "\u00a38,000 - \u00a310,000",
|
||||
"improvement_type": "U",
|
||||
"improvement_details": {
|
||||
"improvement_number": 34
|
||||
},
|
||||
"improvement_category": 5,
|
||||
"energy_performance_rating": 87,
|
||||
"environmental_impact_rating": 81
|
||||
}
|
||||
],
|
||||
"co2_emissions_potential": 2.8,
|
||||
"energy_rating_potential": 87,
|
||||
"lighting_cost_potential": {
|
||||
"value": 89,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"schema_version_original": "LIG-21.0",
|
||||
"hot_water_cost_potential": {
|
||||
"value": 236,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"renewable_heat_incentive": {
|
||||
"water_heating": 2744.25,
|
||||
"space_heating_existing_dwelling": 9473.16
|
||||
},
|
||||
"draughtproofed_door_count": 1,
|
||||
"energy_consumption_current": 117,
|
||||
"has_fixed_air_conditioning": "false",
|
||||
"multiple_glazed_proportion": 100,
|
||||
"calculation_software_version": "10.2.2.0",
|
||||
"energy_consumption_potential": 106,
|
||||
"environmental_impact_current": 79,
|
||||
"cfl_fixed_lighting_bulbs_count": 0,
|
||||
"current_energy_efficiency_band": "C",
|
||||
"environmental_impact_potential": 81,
|
||||
"led_fixed_lighting_bulbs_count": 18,
|
||||
"has_heated_separate_conservatory": "false",
|
||||
"potential_energy_efficiency_band": "B",
|
||||
"co2_emissions_current_per_floor_area": 21,
|
||||
"incandescent_fixed_lighting_bulbs_count": 0
|
||||
}
|
||||
|
|
@ -0,0 +1,316 @@
|
|||
{
|
||||
"uprn": 10091578598,
|
||||
"roofs": [
|
||||
{
|
||||
"description": "(another dwelling above)",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
}
|
||||
],
|
||||
"walls": [
|
||||
{
|
||||
"description": "Cavity wall, as built, insulated (assumed)",
|
||||
"energy_efficiency_rating": 4,
|
||||
"environmental_efficiency_rating": 4
|
||||
}
|
||||
],
|
||||
"floors": [
|
||||
{
|
||||
"description": "(another dwelling below)",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
}
|
||||
],
|
||||
"status": "entered",
|
||||
"tenure": 2,
|
||||
"window": {
|
||||
"description": "Fully double glazed",
|
||||
"energy_efficiency_rating": 3,
|
||||
"environmental_efficiency_rating": 3
|
||||
},
|
||||
"lighting": {
|
||||
"description": "Good lighting efficiency",
|
||||
"energy_efficiency_rating": 4,
|
||||
"environmental_efficiency_rating": 4
|
||||
},
|
||||
"postcode": "RM1 3FB",
|
||||
"hot_water": {
|
||||
"description": "From main system",
|
||||
"energy_efficiency_rating": 1,
|
||||
"environmental_efficiency_rating": 5
|
||||
},
|
||||
"post_town": "ROMFORD",
|
||||
"built_form": "NR",
|
||||
"created_at": "2026-05-16 17:47:19",
|
||||
"door_count": 1,
|
||||
"region_code": 2,
|
||||
"report_type": 2,
|
||||
"sap_heating": {
|
||||
"number_baths": 1,
|
||||
"cylinder_size": 4,
|
||||
"number_baths_wwhrs": 0,
|
||||
"water_heating_code": 901,
|
||||
"water_heating_fuel": 29,
|
||||
"cylinder_thermostat": "Y",
|
||||
"main_heating_details": [
|
||||
{
|
||||
"has_fghrs": "N",
|
||||
"main_fuel_type": 29,
|
||||
"heat_emitter_type": 1,
|
||||
"emitter_temperature": "NA",
|
||||
"main_heating_number": 1,
|
||||
"main_heating_control": 2106,
|
||||
"main_heating_category": 2,
|
||||
"main_heating_fraction": 1,
|
||||
"sap_main_heating_code": 195,
|
||||
"central_heating_pump_age": 0,
|
||||
"main_heating_data_source": 2
|
||||
}
|
||||
],
|
||||
"immersion_heating_type": "NA",
|
||||
"cylinder_insulation_type": 1,
|
||||
"has_fixed_air_conditioning": "false",
|
||||
"cylinder_insulation_thickness": 50
|
||||
},
|
||||
"sap_version": 10.2,
|
||||
"sap_windows": [
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 2,
|
||||
"window_type": 1,
|
||||
"glazing_type": 2,
|
||||
"window_width": 1.15,
|
||||
"window_height": 1.65,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 2,
|
||||
"window_type": 1,
|
||||
"glazing_type": 2,
|
||||
"window_width": 1.15,
|
||||
"window_height": 1.65,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 2,
|
||||
"window_type": 1,
|
||||
"glazing_type": 2,
|
||||
"window_width": 1.15,
|
||||
"window_height": 1.65,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 2,
|
||||
"window_type": 1,
|
||||
"glazing_type": 2,
|
||||
"window_width": 1.15,
|
||||
"window_height": 1.65,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
}
|
||||
],
|
||||
"schema_type": "RdSAP-Schema-21.0.1",
|
||||
"uprn_source": "Energy Assessor",
|
||||
"country_code": "ENG",
|
||||
"main_heating": [
|
||||
{
|
||||
"description": "Boiler and radiators, electric",
|
||||
"energy_efficiency_rating": 4,
|
||||
"environmental_efficiency_rating": 5
|
||||
}
|
||||
],
|
||||
"air_tightness": {
|
||||
"description": "(not tested)",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
},
|
||||
"dwelling_type": "Mid-floor flat",
|
||||
"language_code": 1,
|
||||
"pressure_test": 4,
|
||||
"property_type": 2,
|
||||
"address_line_1": "41 Verve Apartments",
|
||||
"address_line_2": "5 Mercury Gardens",
|
||||
"assessment_type": "RdSAP",
|
||||
"completion_date": "2026-05-16",
|
||||
"inspection_date": "2026-05-16",
|
||||
"extensions_count": 0,
|
||||
"measurement_type": 1,
|
||||
"sap_flat_details": {
|
||||
"level": 2,
|
||||
"top_storey": "N",
|
||||
"storey_count": 6,
|
||||
"flat_location": 3,
|
||||
"heat_loss_corridor": 2,
|
||||
"unheated_corridor_length": 6
|
||||
},
|
||||
"total_floor_area": 60,
|
||||
"transaction_type": 8,
|
||||
"conservatory_type": 1,
|
||||
"heated_room_count": 2,
|
||||
"registration_date": "2026-05-16",
|
||||
"sap_energy_source": {
|
||||
"mains_gas": "N",
|
||||
"meter_type": 4,
|
||||
"pv_connection": 0,
|
||||
"photovoltaic_supply": {
|
||||
"none_or_no_details": {
|
||||
"percent_roof_area": 0
|
||||
}
|
||||
},
|
||||
"wind_turbines_count": 0,
|
||||
"gas_smart_meter_present": "false",
|
||||
"is_dwelling_export_capable": "false",
|
||||
"wind_turbines_terrain_type": 2,
|
||||
"electricity_smart_meter_present": "true"
|
||||
},
|
||||
"secondary_heating": {
|
||||
"description": "None",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
},
|
||||
"sap_building_parts": [
|
||||
{
|
||||
"identifier": "Main Dwelling",
|
||||
"wall_dry_lined": "N",
|
||||
"wall_thickness": 300,
|
||||
"floor_heat_loss": 6,
|
||||
"roof_construction": 3,
|
||||
"wall_construction": 4,
|
||||
"building_part_number": 1,
|
||||
"sap_floor_dimensions": [
|
||||
{
|
||||
"floor": 0,
|
||||
"room_height": {
|
||||
"value": 2.5,
|
||||
"quantity": "metres"
|
||||
},
|
||||
"total_floor_area": {
|
||||
"value": 60,
|
||||
"quantity": "square metres"
|
||||
},
|
||||
"party_wall_length": {
|
||||
"value": 20,
|
||||
"quantity": "metres"
|
||||
},
|
||||
"heat_loss_perimeter": {
|
||||
"value": 12,
|
||||
"quantity": "metres"
|
||||
}
|
||||
}
|
||||
],
|
||||
"wall_insulation_type": 4,
|
||||
"construction_age_band": "I",
|
||||
"sap_alternative_wall_1": {
|
||||
"wall_area": 15,
|
||||
"sheltered_wall": "Y",
|
||||
"wall_dry_lined": "N",
|
||||
"wall_thickness": 300,
|
||||
"wall_construction": 4,
|
||||
"wall_insulation_type": 4,
|
||||
"wall_thickness_measured": "Y",
|
||||
"wall_insulation_thickness": "NI"
|
||||
},
|
||||
"party_wall_construction": 0,
|
||||
"wall_thickness_measured": "Y",
|
||||
"roof_insulation_location": "ND",
|
||||
"roof_insulation_thickness": "ND",
|
||||
"wall_insulation_thickness": "NI"
|
||||
}
|
||||
],
|
||||
"solar_water_heating": "N",
|
||||
"habitable_room_count": 2,
|
||||
"heating_cost_current": {
|
||||
"value": 202,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"insulated_door_count": 0,
|
||||
"co2_emissions_current": 0.6,
|
||||
"energy_rating_average": 60,
|
||||
"energy_rating_current": 71,
|
||||
"lighting_cost_current": {
|
||||
"value": 55,
|
||||
"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": 370,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"hot_water_cost_current": {
|
||||
"value": 847,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"mechanical_ventilation": 0,
|
||||
"percent_draughtproofed": 80,
|
||||
"suggested_improvements": [
|
||||
{
|
||||
"sequence": 1,
|
||||
"typical_saving": {
|
||||
"value": 76,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"indicative_cost": "\u00a3150 - \u00a3250",
|
||||
"improvement_type": "D",
|
||||
"improvement_details": {
|
||||
"improvement_number": 10
|
||||
},
|
||||
"improvement_category": 5,
|
||||
"energy_performance_rating": 79,
|
||||
"environmental_impact_rating": 92
|
||||
}
|
||||
],
|
||||
"co2_emissions_potential": 0.6,
|
||||
"energy_rating_potential": 79,
|
||||
"lighting_cost_potential": {
|
||||
"value": 48,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"schema_version_original": "21.0.1",
|
||||
"hot_water_cost_potential": {
|
||||
"value": 611,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"renewable_heat_incentive": {
|
||||
"water_heating": 2659.46,
|
||||
"space_heating_existing_dwelling": 1061.64
|
||||
},
|
||||
"draughtproofed_door_count": 0,
|
||||
"energy_consumption_current": 102,
|
||||
"has_fixed_air_conditioning": "false",
|
||||
"multiple_glazed_proportion": 100,
|
||||
"calculation_software_version": "5.02r0344",
|
||||
"energy_consumption_potential": 102,
|
||||
"environmental_impact_current": 92,
|
||||
"current_energy_efficiency_band": "C",
|
||||
"environmental_impact_potential": 92,
|
||||
"has_heated_separate_conservatory": "false",
|
||||
"potential_energy_efficiency_band": "C",
|
||||
"co2_emissions_current_per_floor_area": 10,
|
||||
"low_energy_fixed_lighting_bulbs_count": 20,
|
||||
"incandescent_fixed_lighting_bulbs_count": 0
|
||||
}
|
||||
|
|
@ -0,0 +1,366 @@
|
|||
{
|
||||
"uprn": 10094975827,
|
||||
"roofs": [
|
||||
{
|
||||
"description": "Pitched, insulated (assumed)",
|
||||
"energy_efficiency_rating": 3,
|
||||
"environmental_efficiency_rating": 3
|
||||
}
|
||||
],
|
||||
"walls": [
|
||||
{
|
||||
"description": "Solid brick, as built, no insulation (assumed)",
|
||||
"energy_efficiency_rating": 2,
|
||||
"environmental_efficiency_rating": 2
|
||||
}
|
||||
],
|
||||
"floors": [
|
||||
{
|
||||
"description": "(other premises below)",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
}
|
||||
],
|
||||
"status": "entered",
|
||||
"tenure": 3,
|
||||
"window": {
|
||||
"description": "Fully double glazed",
|
||||
"energy_efficiency_rating": 2,
|
||||
"environmental_efficiency_rating": 2
|
||||
},
|
||||
"lighting": {
|
||||
"description": "Excellent lighting efficiency",
|
||||
"energy_efficiency_rating": 5,
|
||||
"environmental_efficiency_rating": 5
|
||||
},
|
||||
"postcode": "B8 2NG",
|
||||
"hot_water": {
|
||||
"description": "From main system",
|
||||
"energy_efficiency_rating": 4,
|
||||
"environmental_efficiency_rating": 4
|
||||
},
|
||||
"post_town": "BIRMINGHAM",
|
||||
"psv_count": 0,
|
||||
"built_form": 3,
|
||||
"created_at": "2026-05-14 13:27:59",
|
||||
"door_count": 1,
|
||||
"region_code": 6,
|
||||
"report_type": 2,
|
||||
"sap_heating": {
|
||||
"number_baths": 1,
|
||||
"cylinder_size": 1,
|
||||
"shower_outlets": [
|
||||
{
|
||||
"shower_wwhrs": 1,
|
||||
"shower_outlet_type": 1
|
||||
}
|
||||
],
|
||||
"number_baths_wwhrs": 0,
|
||||
"water_heating_code": 901,
|
||||
"water_heating_fuel": 26,
|
||||
"cylinder_thermostat": "N",
|
||||
"main_heating_details": [
|
||||
{
|
||||
"has_fghrs": "N",
|
||||
"main_fuel_type": 26,
|
||||
"boiler_flue_type": 2,
|
||||
"fan_flue_present": "Y",
|
||||
"heat_emitter_type": 1,
|
||||
"emitter_temperature": 1,
|
||||
"main_heating_number": 1,
|
||||
"main_heating_control": 2107,
|
||||
"main_heating_category": 2,
|
||||
"main_heating_fraction": 1,
|
||||
"central_heating_pump_age": 0,
|
||||
"main_heating_data_source": 1,
|
||||
"main_heating_index_number": 18559
|
||||
}
|
||||
],
|
||||
"immersion_heating_type": "NA",
|
||||
"has_fixed_air_conditioning": "false"
|
||||
},
|
||||
"sap_version": 10.2,
|
||||
"sap_windows": [
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"glazing_gap": "16+",
|
||||
"orientation": 1,
|
||||
"window_type": 1,
|
||||
"glazing_type": 3,
|
||||
"window_width": {
|
||||
"value": 1.01,
|
||||
"quantity": "m"
|
||||
},
|
||||
"window_height": {
|
||||
"value": 1.61,
|
||||
"quantity": "m"
|
||||
},
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"glazing_gap": "16+",
|
||||
"orientation": 1,
|
||||
"window_type": 1,
|
||||
"glazing_type": 3,
|
||||
"window_width": {
|
||||
"value": 1.66,
|
||||
"quantity": "m"
|
||||
},
|
||||
"window_height": {
|
||||
"value": 1.46,
|
||||
"quantity": "m"
|
||||
},
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"glazing_gap": "16+",
|
||||
"orientation": 3,
|
||||
"window_type": 1,
|
||||
"glazing_type": 3,
|
||||
"window_width": {
|
||||
"value": 1.09,
|
||||
"quantity": "m"
|
||||
},
|
||||
"window_height": {
|
||||
"value": 1.29,
|
||||
"quantity": "m"
|
||||
},
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"glazing_gap": "16+",
|
||||
"orientation": 3,
|
||||
"window_type": 1,
|
||||
"glazing_type": 3,
|
||||
"window_width": {
|
||||
"value": 1.09,
|
||||
"quantity": "m"
|
||||
},
|
||||
"window_height": {
|
||||
"value": 1.55,
|
||||
"quantity": "m"
|
||||
},
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"glazing_gap": "16+",
|
||||
"orientation": 3,
|
||||
"window_type": 1,
|
||||
"glazing_type": 3,
|
||||
"window_width": {
|
||||
"value": 1.79,
|
||||
"quantity": "m"
|
||||
},
|
||||
"window_height": {
|
||||
"value": 1.92,
|
||||
"quantity": "m"
|
||||
},
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
}
|
||||
],
|
||||
"schema_type": "RdSAP-Schema-21.0.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": "(not tested)",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
},
|
||||
"dwelling_type": "Top-floor flat",
|
||||
"language_code": 1,
|
||||
"pressure_test": 4,
|
||||
"property_type": 2,
|
||||
"address_line_1": "Flat 2",
|
||||
"address_line_2": "874 Washwood Heath Road",
|
||||
"assessment_type": "RdSAP",
|
||||
"completion_date": "2026-05-14",
|
||||
"inspection_date": "2026-05-13",
|
||||
"extensions_count": 0,
|
||||
"measurement_type": 1,
|
||||
"open_flues_count": 0,
|
||||
"sap_flat_details": {
|
||||
"level": 3,
|
||||
"top_storey": "N",
|
||||
"storey_count": 2,
|
||||
"flat_location": 1,
|
||||
"heat_loss_corridor": 0
|
||||
},
|
||||
"total_floor_area": 78,
|
||||
"transaction_type": 8,
|
||||
"conservatory_type": 1,
|
||||
"heated_room_count": 3,
|
||||
"other_flues_count": 0,
|
||||
"registration_date": "2026-05-14",
|
||||
"sap_energy_source": {
|
||||
"mains_gas": "Y",
|
||||
"meter_type": 3,
|
||||
"pv_connection": 0,
|
||||
"photovoltaic_supply": {
|
||||
"none_or_no_details": {
|
||||
"percent_roof_area": 0
|
||||
}
|
||||
},
|
||||
"wind_turbines_count": 0,
|
||||
"gas_smart_meter_present": "false",
|
||||
"is_dwelling_export_capable": "false",
|
||||
"wind_turbines_terrain_type": 2,
|
||||
"electricity_smart_meter_present": "false"
|
||||
},
|
||||
"secondary_heating": {
|
||||
"description": "None",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
},
|
||||
"closed_flues_count": 0,
|
||||
"extract_fans_count": 0,
|
||||
"sap_building_parts": [
|
||||
{
|
||||
"identifier": "Main Dwelling",
|
||||
"wall_dry_lined": "N",
|
||||
"wall_thickness": 340,
|
||||
"floor_heat_loss": 3,
|
||||
"roof_construction": 4,
|
||||
"wall_construction": 3,
|
||||
"building_part_number": 1,
|
||||
"sap_floor_dimensions": [
|
||||
{
|
||||
"floor": 0,
|
||||
"room_height": 2.44,
|
||||
"total_floor_area": 77.87,
|
||||
"party_wall_length": 11.57,
|
||||
"heat_loss_perimeter": 25.03
|
||||
}
|
||||
],
|
||||
"wall_insulation_type": 4,
|
||||
"construction_age_band": "B",
|
||||
"party_wall_construction": 1,
|
||||
"wall_thickness_measured": "Y",
|
||||
"roof_insulation_location": 4,
|
||||
"roof_insulation_thickness": "NI",
|
||||
"wall_insulation_thickness": "NI"
|
||||
}
|
||||
],
|
||||
"boilers_flues_count": 0,
|
||||
"open_chimneys_count": 0,
|
||||
"solar_water_heating": "N",
|
||||
"habitable_room_count": 3,
|
||||
"heating_cost_current": {
|
||||
"value": 952,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"insulated_door_count": 0,
|
||||
"co2_emissions_current": 3.2,
|
||||
"energy_rating_average": 60,
|
||||
"energy_rating_current": 66,
|
||||
"lighting_cost_current": {
|
||||
"value": 56,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"main_heating_controls": [
|
||||
{
|
||||
"description": "Programmer, TRVs and bypass",
|
||||
"energy_efficiency_rating": 3,
|
||||
"environmental_efficiency_rating": 3
|
||||
}
|
||||
],
|
||||
"blocked_chimneys_count": 0,
|
||||
"has_hot_water_cylinder": "false",
|
||||
"heating_cost_potential": {
|
||||
"value": 735,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"hot_water_cost_current": {
|
||||
"value": 197,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"mechanical_ventilation": 0,
|
||||
"percent_draughtproofed": 100,
|
||||
"schema_version_current": "LIG-21.0",
|
||||
"suggested_improvements": [
|
||||
{
|
||||
"sequence": 1,
|
||||
"typical_saving": 171,
|
||||
"indicative_cost": "\u00a37,500 - \u00a311,000",
|
||||
"improvement_type": "Q",
|
||||
"improvement_details": {
|
||||
"improvement_number": 7
|
||||
},
|
||||
"improvement_category": 5,
|
||||
"energy_performance_rating": 70,
|
||||
"environmental_impact_rating": 70
|
||||
},
|
||||
{
|
||||
"sequence": 2,
|
||||
"typical_saving": 45,
|
||||
"indicative_cost": "\u00a3220 - \u00a3250",
|
||||
"improvement_type": "G",
|
||||
"improvement_details": {
|
||||
"improvement_number": 14
|
||||
},
|
||||
"improvement_category": 5,
|
||||
"energy_performance_rating": 72,
|
||||
"environmental_impact_rating": 71
|
||||
}
|
||||
],
|
||||
"co2_emissions_potential": 2.6,
|
||||
"energy_rating_potential": 72,
|
||||
"lighting_cost_potential": {
|
||||
"value": 56,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"schema_version_original": "LIG-21.0",
|
||||
"hot_water_cost_potential": {
|
||||
"value": 198,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"renewable_heat_incentive": {
|
||||
"water_heating": 2548.95,
|
||||
"space_heating_existing_dwelling": 10093.12
|
||||
},
|
||||
"draughtproofed_door_count": 1,
|
||||
"energy_consumption_current": 227,
|
||||
"has_fixed_air_conditioning": "false",
|
||||
"multiple_glazed_proportion": 100,
|
||||
"calculation_software_version": "10.2.2.0",
|
||||
"energy_consumption_potential": 181,
|
||||
"environmental_impact_current": 64,
|
||||
"cfl_fixed_lighting_bulbs_count": 0,
|
||||
"current_energy_efficiency_band": "D",
|
||||
"environmental_impact_potential": 71,
|
||||
"led_fixed_lighting_bulbs_count": 8,
|
||||
"has_heated_separate_conservatory": "false",
|
||||
"potential_energy_efficiency_band": "C",
|
||||
"co2_emissions_current_per_floor_area": 41,
|
||||
"incandescent_fixed_lighting_bulbs_count": 0
|
||||
}
|
||||
|
|
@ -0,0 +1,321 @@
|
|||
{
|
||||
"uprn": 103001004,
|
||||
"roofs": [
|
||||
{
|
||||
"description": "Pitched, Unknown loft insulation",
|
||||
"energy_efficiency_rating": 3,
|
||||
"environmental_efficiency_rating": 3
|
||||
}
|
||||
],
|
||||
"walls": [
|
||||
{
|
||||
"description": "Cavity wall, filled cavity",
|
||||
"energy_efficiency_rating": 4,
|
||||
"environmental_efficiency_rating": 4
|
||||
}
|
||||
],
|
||||
"floors": [
|
||||
{
|
||||
"description": "Solid, no insulation (assumed)",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
}
|
||||
],
|
||||
"status": "entered",
|
||||
"tenure": 2,
|
||||
"window": {
|
||||
"description": "Fully double glazed",
|
||||
"energy_efficiency_rating": 2,
|
||||
"environmental_efficiency_rating": 2
|
||||
},
|
||||
"lighting": {
|
||||
"description": "Good lighting efficiency",
|
||||
"energy_efficiency_rating": 4,
|
||||
"environmental_efficiency_rating": 4
|
||||
},
|
||||
"postcode": "NE33 1TN",
|
||||
"hot_water": {
|
||||
"description": "Electric immersion, off-peak",
|
||||
"energy_efficiency_rating": 2,
|
||||
"environmental_efficiency_rating": 4
|
||||
},
|
||||
"post_town": "SOUTH SHIELDS",
|
||||
"built_form": "NR",
|
||||
"created_at": "2026-04-16 10:25:37",
|
||||
"door_count": 0,
|
||||
"region_code": 1,
|
||||
"report_type": 2,
|
||||
"sap_heating": {
|
||||
"number_baths": 0,
|
||||
"cylinder_size": 3,
|
||||
"shower_outlets": [
|
||||
{
|
||||
"shower_outlet": {
|
||||
"shower_wwhrs": 1,
|
||||
"shower_outlet_type": 1
|
||||
}
|
||||
}
|
||||
],
|
||||
"cylinder_heat_loss": 1.8,
|
||||
"number_baths_wwhrs": 0,
|
||||
"water_heating_code": 903,
|
||||
"water_heating_fuel": 29,
|
||||
"main_heating_details": [
|
||||
{
|
||||
"has_fghrs": "N",
|
||||
"main_fuel_type": 20,
|
||||
"heat_emitter_type": 0,
|
||||
"emitter_temperature": "NA",
|
||||
"main_heating_number": 1,
|
||||
"main_heating_control": 2307,
|
||||
"main_heating_category": 6,
|
||||
"main_heating_fraction": 1,
|
||||
"sap_main_heating_code": 301,
|
||||
"main_heating_data_source": 2,
|
||||
"community_heat_distribution_type": 6
|
||||
}
|
||||
],
|
||||
"immersion_heating_type": 2,
|
||||
"has_fixed_air_conditioning": "false"
|
||||
},
|
||||
"sap_version": 10.2,
|
||||
"sap_windows": [
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"glazing_gap": 12,
|
||||
"orientation": 7,
|
||||
"window_type": 1,
|
||||
"glazing_type": 3,
|
||||
"window_width": 1.21,
|
||||
"window_height": 1.44,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"glazing_gap": 12,
|
||||
"orientation": 7,
|
||||
"window_type": 1,
|
||||
"glazing_type": 3,
|
||||
"window_width": 1.77,
|
||||
"window_height": 1.14,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
}
|
||||
],
|
||||
"schema_type": "RdSAP-Schema-21.0.1",
|
||||
"uprn_source": "Energy Assessor",
|
||||
"country_code": "ENG",
|
||||
"main_heating": [
|
||||
{
|
||||
"description": "Community scheme",
|
||||
"energy_efficiency_rating": 3,
|
||||
"environmental_efficiency_rating": 3
|
||||
}
|
||||
],
|
||||
"air_tightness": {
|
||||
"description": "(not tested)",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
},
|
||||
"dwelling_type": "Ground-floor flat",
|
||||
"language_code": 1,
|
||||
"pressure_test": 4,
|
||||
"property_type": 2,
|
||||
"address_line_1": "3 Julius Court",
|
||||
"address_line_2": "Mile End Road",
|
||||
"assessment_type": "RdSAP",
|
||||
"completion_date": "2026-04-16",
|
||||
"inspection_date": "2026-04-16",
|
||||
"extensions_count": 0,
|
||||
"measurement_type": 1,
|
||||
"sap_flat_details": {
|
||||
"level": 1,
|
||||
"top_storey": "N",
|
||||
"storey_count": 1,
|
||||
"flat_location": 0,
|
||||
"heat_loss_corridor": 1
|
||||
},
|
||||
"total_floor_area": 31,
|
||||
"transaction_type": 8,
|
||||
"conservatory_type": 1,
|
||||
"has_draught_lobby": "true",
|
||||
"heated_room_count": 2,
|
||||
"registration_date": "2026-04-16",
|
||||
"sap_energy_source": {
|
||||
"mains_gas": "Y",
|
||||
"meter_type": 1,
|
||||
"pv_connection": 0,
|
||||
"photovoltaic_supply": {
|
||||
"none_or_no_details": {
|
||||
"percent_roof_area": 0
|
||||
}
|
||||
},
|
||||
"wind_turbines_count": 0,
|
||||
"gas_smart_meter_present": "false",
|
||||
"is_dwelling_export_capable": "false",
|
||||
"wind_turbines_terrain_type": 2,
|
||||
"electricity_smart_meter_present": "false"
|
||||
},
|
||||
"secondary_heating": {
|
||||
"description": "None",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
},
|
||||
"extract_fans_count": 2,
|
||||
"sap_building_parts": [
|
||||
{
|
||||
"identifier": "Main Dwelling",
|
||||
"wall_dry_lined": "N",
|
||||
"floor_heat_loss": 7,
|
||||
"roof_construction": 4,
|
||||
"wall_construction": 4,
|
||||
"building_part_number": 1,
|
||||
"sap_floor_dimensions": [
|
||||
{
|
||||
"floor": 0,
|
||||
"room_height": {
|
||||
"value": 2.37,
|
||||
"quantity": "metres"
|
||||
},
|
||||
"floor_insulation": 1,
|
||||
"total_floor_area": {
|
||||
"value": 30.55,
|
||||
"quantity": "square metres"
|
||||
},
|
||||
"party_wall_length": {
|
||||
"value": 16.75,
|
||||
"quantity": "metres"
|
||||
},
|
||||
"floor_construction": 1,
|
||||
"heat_loss_perimeter": {
|
||||
"value": 5.37,
|
||||
"quantity": "metres"
|
||||
}
|
||||
}
|
||||
],
|
||||
"wall_insulation_type": 2,
|
||||
"construction_age_band": "F",
|
||||
"party_wall_construction": 0,
|
||||
"wall_thickness_measured": "N",
|
||||
"roof_insulation_location": 2,
|
||||
"roof_insulation_thickness": "NI",
|
||||
"wall_insulation_thickness": "NI",
|
||||
"floor_insulation_thickness": "NI"
|
||||
}
|
||||
],
|
||||
"solar_water_heating": "N",
|
||||
"habitable_room_count": 2,
|
||||
"heating_cost_current": {
|
||||
"value": 251,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"insulated_door_count": 0,
|
||||
"co2_emissions_current": 1.2,
|
||||
"energy_rating_average": 60,
|
||||
"energy_rating_current": 50,
|
||||
"lighting_cost_current": {
|
||||
"value": 29,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"main_heating_controls": [
|
||||
{
|
||||
"description": "Flat rate charging, TRVs",
|
||||
"energy_efficiency_rating": 3,
|
||||
"environmental_efficiency_rating": 3
|
||||
}
|
||||
],
|
||||
"has_hot_water_cylinder": "true",
|
||||
"heating_cost_potential": {
|
||||
"value": 313,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"hot_water_cost_current": {
|
||||
"value": 1054,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"mechanical_ventilation": 0,
|
||||
"percent_draughtproofed": 100,
|
||||
"suggested_improvements": [
|
||||
{
|
||||
"sequence": 1,
|
||||
"typical_saving": {
|
||||
"value": 35,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"indicative_cost": "\u00a3900 - \u00a31,200",
|
||||
"improvement_type": "A",
|
||||
"improvement_details": {
|
||||
"improvement_number": 5
|
||||
},
|
||||
"improvement_category": 5,
|
||||
"energy_performance_rating": 52,
|
||||
"environmental_impact_rating": 81
|
||||
},
|
||||
{
|
||||
"sequence": 2,
|
||||
"typical_saving": {
|
||||
"value": 598,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"indicative_cost": "\u00a320 - \u00a340",
|
||||
"improvement_type": "C",
|
||||
"improvement_details": {
|
||||
"improvement_number": 1
|
||||
},
|
||||
"improvement_category": 5,
|
||||
"energy_performance_rating": 68,
|
||||
"environmental_impact_rating": 83
|
||||
},
|
||||
{
|
||||
"sequence": 3,
|
||||
"typical_saving": {
|
||||
"value": 51,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"indicative_cost": "\u00a3600 - \u00a31,500",
|
||||
"improvement_type": "Y",
|
||||
"improvement_details": {
|
||||
"improvement_number": 49
|
||||
},
|
||||
"improvement_category": 5,
|
||||
"energy_performance_rating": 70,
|
||||
"environmental_impact_rating": 83
|
||||
}
|
||||
],
|
||||
"co2_emissions_potential": 1.0,
|
||||
"energy_rating_potential": 70,
|
||||
"lighting_cost_potential": {
|
||||
"value": 29,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"schema_version_original": "21.0.1",
|
||||
"hot_water_cost_potential": {
|
||||
"value": 308,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"renewable_heat_incentive": {
|
||||
"water_heating": 5719.21,
|
||||
"space_heating_existing_dwelling": 1270.63
|
||||
},
|
||||
"draughtproofed_door_count": 0,
|
||||
"energy_consumption_current": 367,
|
||||
"has_fixed_air_conditioning": "false",
|
||||
"multiple_glazed_proportion": 100,
|
||||
"calculation_software_version": "5.02r0342",
|
||||
"energy_consumption_potential": 214,
|
||||
"environmental_impact_current": 78,
|
||||
"current_energy_efficiency_band": "E",
|
||||
"environmental_impact_potential": 83,
|
||||
"has_heated_separate_conservatory": "false",
|
||||
"potential_energy_efficiency_band": "C",
|
||||
"co2_emissions_current_per_floor_area": 41,
|
||||
"low_energy_fixed_lighting_bulbs_count": 5,
|
||||
"incandescent_fixed_lighting_bulbs_count": 0
|
||||
}
|
||||
|
|
@ -0,0 +1,372 @@
|
|||
{
|
||||
"uprn": 200004017091,
|
||||
"roofs": [
|
||||
{
|
||||
"description": "Pitched, 100 mm loft insulation",
|
||||
"energy_efficiency_rating": 3,
|
||||
"environmental_efficiency_rating": 3
|
||||
}
|
||||
],
|
||||
"walls": [
|
||||
{
|
||||
"description": "Timber frame, 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",
|
||||
"tenure": 1,
|
||||
"window": {
|
||||
"description": "Fully double glazed",
|
||||
"energy_efficiency_rating": 2,
|
||||
"environmental_efficiency_rating": 2
|
||||
},
|
||||
"addendum": {
|
||||
"addendum_numbers": [
|
||||
15
|
||||
]
|
||||
},
|
||||
"lighting": {
|
||||
"description": "Good lighting efficiency",
|
||||
"energy_efficiency_rating": 4,
|
||||
"environmental_efficiency_rating": 4
|
||||
},
|
||||
"postcode": "BN14 9EL",
|
||||
"hot_water": {
|
||||
"description": "From main system, no cylinder thermostat",
|
||||
"energy_efficiency_rating": 3,
|
||||
"environmental_efficiency_rating": 3
|
||||
},
|
||||
"post_town": "WORTHING",
|
||||
"built_form": 3,
|
||||
"created_at": "2026-04-21 13:18:06",
|
||||
"door_count": 2,
|
||||
"region_code": 14,
|
||||
"report_type": 2,
|
||||
"sap_heating": {
|
||||
"number_baths": 1,
|
||||
"cylinder_size": 0,
|
||||
"number_baths_wwhrs": 0,
|
||||
"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,
|
||||
"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": 17803
|
||||
}
|
||||
],
|
||||
"immersion_heating_type": "NA",
|
||||
"has_fixed_air_conditioning": "false"
|
||||
},
|
||||
"sap_version": 10.2,
|
||||
"sap_windows": [
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"glazing_gap": "16+",
|
||||
"orientation": 1,
|
||||
"window_type": 1,
|
||||
"glazing_type": 3,
|
||||
"window_width": 1.1,
|
||||
"window_height": 1.2,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"glazing_gap": "16+",
|
||||
"orientation": 1,
|
||||
"window_type": 1,
|
||||
"glazing_type": 3,
|
||||
"window_width": 1.1,
|
||||
"window_height": 1.2,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"glazing_gap": "16+",
|
||||
"orientation": 5,
|
||||
"window_type": 1,
|
||||
"glazing_type": 3,
|
||||
"window_width": 1.1,
|
||||
"window_height": 1.2,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"glazing_gap": "16+",
|
||||
"orientation": 5,
|
||||
"window_type": 1,
|
||||
"glazing_type": 3,
|
||||
"window_width": 0.8,
|
||||
"window_height": 0.8,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"glazing_gap": "16+",
|
||||
"orientation": 5,
|
||||
"window_type": 1,
|
||||
"glazing_type": 3,
|
||||
"window_width": 1.5,
|
||||
"window_height": 0.8,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
}
|
||||
],
|
||||
"schema_type": "RdSAP-Schema-21.0.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": "(not tested)",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
},
|
||||
"dwelling_type": "End-terrace house",
|
||||
"language_code": 1,
|
||||
"pressure_test": 4,
|
||||
"property_type": 0,
|
||||
"address_line_1": "3 Radbone Close",
|
||||
"assessment_type": "RdSAP",
|
||||
"completion_date": "2026-04-21",
|
||||
"inspection_date": "2026-04-20",
|
||||
"extensions_count": 0,
|
||||
"measurement_type": 1,
|
||||
"total_floor_area": 65,
|
||||
"transaction_type": 1,
|
||||
"conservatory_type": 1,
|
||||
"heated_room_count": 4,
|
||||
"registration_date": "2026-04-21",
|
||||
"sap_energy_source": {
|
||||
"mains_gas": "Y",
|
||||
"meter_type": 2,
|
||||
"pv_connection": 0,
|
||||
"photovoltaic_supply": {
|
||||
"none_or_no_details": {
|
||||
"percent_roof_area": 0
|
||||
}
|
||||
},
|
||||
"wind_turbines_count": 0,
|
||||
"gas_smart_meter_present": "false",
|
||||
"is_dwelling_export_capable": "false",
|
||||
"wind_turbines_terrain_type": 2,
|
||||
"electricity_smart_meter_present": "false"
|
||||
},
|
||||
"secondary_heating": {
|
||||
"description": "None",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
},
|
||||
"extract_fans_count": 1,
|
||||
"sap_building_parts": [
|
||||
{
|
||||
"identifier": "Main Dwelling",
|
||||
"wall_dry_lined": "N",
|
||||
"wall_thickness": 280,
|
||||
"floor_heat_loss": 7,
|
||||
"roof_construction": 4,
|
||||
"wall_construction": 5,
|
||||
"building_part_number": 1,
|
||||
"sap_floor_dimensions": [
|
||||
{
|
||||
"floor": 0,
|
||||
"room_height": {
|
||||
"value": 2.37,
|
||||
"quantity": "metres"
|
||||
},
|
||||
"floor_insulation": 1,
|
||||
"total_floor_area": {
|
||||
"value": 32.38,
|
||||
"quantity": "square metres"
|
||||
},
|
||||
"party_wall_length": {
|
||||
"value": 7.42,
|
||||
"quantity": "metres"
|
||||
},
|
||||
"floor_construction": 1,
|
||||
"heat_loss_perimeter": {
|
||||
"value": 16.15,
|
||||
"quantity": "metres"
|
||||
}
|
||||
},
|
||||
{
|
||||
"floor": 1,
|
||||
"room_height": {
|
||||
"value": 2.35,
|
||||
"quantity": "metres"
|
||||
},
|
||||
"total_floor_area": {
|
||||
"value": 32.37,
|
||||
"quantity": "square metres"
|
||||
},
|
||||
"party_wall_length": {
|
||||
"value": 7.42,
|
||||
"quantity": "metres"
|
||||
},
|
||||
"heat_loss_perimeter": {
|
||||
"value": 16.15,
|
||||
"quantity": "metres"
|
||||
}
|
||||
}
|
||||
],
|
||||
"wall_insulation_type": 4,
|
||||
"construction_age_band": "G",
|
||||
"party_wall_construction": 0,
|
||||
"wall_thickness_measured": "Y",
|
||||
"roof_insulation_location": 2,
|
||||
"roof_insulation_thickness": "100mm",
|
||||
"wall_insulation_thickness": "NI",
|
||||
"floor_insulation_thickness": "NI"
|
||||
}
|
||||
],
|
||||
"solar_water_heating": "N",
|
||||
"habitable_room_count": 4,
|
||||
"heating_cost_current": {
|
||||
"value": 556,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"insulated_door_count": 0,
|
||||
"co2_emissions_current": 2.2,
|
||||
"energy_rating_average": 60,
|
||||
"energy_rating_current": 71,
|
||||
"lighting_cost_current": {
|
||||
"value": 51,
|
||||
"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": 516,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"hot_water_cost_current": {
|
||||
"value": 333,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"mechanical_ventilation": 0,
|
||||
"percent_draughtproofed": 71,
|
||||
"suggested_improvements": [
|
||||
{
|
||||
"sequence": 1,
|
||||
"typical_saving": {
|
||||
"value": 44,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"indicative_cost": "\u00a35,000 - \u00a310,000",
|
||||
"improvement_type": "W2",
|
||||
"improvement_details": {
|
||||
"improvement_number": 58
|
||||
},
|
||||
"improvement_category": 5,
|
||||
"energy_performance_rating": 73,
|
||||
"environmental_impact_rating": 74
|
||||
},
|
||||
{
|
||||
"sequence": 2,
|
||||
"typical_saving": {
|
||||
"value": 143,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"indicative_cost": "\u00a34,000 - \u00a37,000",
|
||||
"improvement_type": "N",
|
||||
"improvement_details": {
|
||||
"improvement_number": 19
|
||||
},
|
||||
"improvement_category": 5,
|
||||
"energy_performance_rating": 76,
|
||||
"environmental_impact_rating": 79
|
||||
},
|
||||
{
|
||||
"sequence": 3,
|
||||
"typical_saving": {
|
||||
"value": 212,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"indicative_cost": "\u00a38,000 - \u00a310,000",
|
||||
"improvement_type": "U",
|
||||
"improvement_details": {
|
||||
"improvement_number": 34
|
||||
},
|
||||
"improvement_category": 5,
|
||||
"energy_performance_rating": 81,
|
||||
"environmental_impact_rating": 80
|
||||
}
|
||||
],
|
||||
"co2_emissions_potential": 1.5,
|
||||
"energy_rating_potential": 81,
|
||||
"lighting_cost_potential": {
|
||||
"value": 51,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"schema_version_original": "21.0.1",
|
||||
"hot_water_cost_potential": {
|
||||
"value": 186,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"renewable_heat_incentive": {
|
||||
"water_heating": 3562.39,
|
||||
"space_heating_existing_dwelling": 4593.09
|
||||
},
|
||||
"draughtproofed_door_count": 0,
|
||||
"energy_consumption_current": 184,
|
||||
"has_fixed_air_conditioning": "false",
|
||||
"multiple_glazed_proportion": 100,
|
||||
"calculation_software_version": "5.02r0342",
|
||||
"energy_consumption_potential": 118,
|
||||
"environmental_impact_current": 72,
|
||||
"current_energy_efficiency_band": "C",
|
||||
"environmental_impact_potential": 80,
|
||||
"has_heated_separate_conservatory": "false",
|
||||
"potential_energy_efficiency_band": "B",
|
||||
"co2_emissions_current_per_floor_area": 33,
|
||||
"low_energy_fixed_lighting_bulbs_count": 12,
|
||||
"incandescent_fixed_lighting_bulbs_count": 0
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
# Elmhurst validation — UPRN 200004296092 (corpus, slice-5 wall fix)
|
||||
|
||||
Built in accredited Elmhurst RdSAP10-Online on the lodged register inputs
|
||||
(end-terrace house, solid brick + **external insulation, thickness "NI"**,
|
||||
pitched 150 mm loft, solid ground floor, mains-gas combi, TFA 62).
|
||||
Artefacts: `elmhurst_summary.pdf` (Input Summary), `elmhurst_worksheet.pdf`
|
||||
(SAP Worksheets).
|
||||
|
||||
## Headline
|
||||
- **Lodged SAP 71**; **engine 71.74** (post slice-5 §5.4 100 mm fix, was 64.35);
|
||||
**accredited Elmhurst current SAP 66** (band D), potential 75.
|
||||
- Slice 5 is **directionally validated**: the externally-insulated wall must be
|
||||
billed insulated, not at the uninsulated 1.40 the §5.8 gate produced. Both
|
||||
Elmhurst (0.48) and the engine (0.29) are far below 1.40.
|
||||
|
||||
## Fabric line-by-line (engine dive vs Elmhurst worksheet)
|
||||
| Element | Engine | Elmhurst | Note |
|
||||
|---|---|---|---|
|
||||
| External wall | 21.1 W/K (U 0.29) | 30.7 W/K (U **0.48**, "SolidWallDensePlaster**200**") | see WALL below |
|
||||
| Roof | 9.35 W/K (U 0.30) | 9.35 W/K (U 0.30) | **exact match** |
|
||||
| Ground floor | 21.2 W/K | 22.7 W/K (U 0.73) | close |
|
||||
| Party wall | **8.57 W/K** | **0.00 W/K** | see PARTY below |
|
||||
| Windows | 7.96 W/K | ~same area (4.31 + 4×2.89 split) | ~ |
|
||||
|
||||
The wall (engine −9.6) and party wall (engine +8.6) differences nearly cancel,
|
||||
so the two HTCs are close; the residual SAP gap (71.7 vs 66) is dominated by the
|
||||
heating system — this build used a GENERIC Post-98 condensing combi, whereas the
|
||||
engine uses the lodged PCDB 16137 (Ideal Logic combi 30). Not a clean 1e-4
|
||||
comparison for that reason.
|
||||
|
||||
## Leads (both partly confounded by an imperfect build — see caveats)
|
||||
1. **WALL — base thickness on an unmeasured wall.** The cert lodges
|
||||
`wall_thickness 360` but `wall_thickness_measured = N`. Elmhurst modelled a
|
||||
**~200 mm** base wall ("SolidWallDensePlaster200") — but note my build did
|
||||
NOT enter a wall thickness, so Elmhurst used its own default; had I keyed
|
||||
360 mm it might match the engine. The open question is genuine though:
|
||||
RdSAP §5.3 uses the documentary/measured thickness, and when NOT measured
|
||||
the engine perhaps should fall to the Table-3 default rather than trust the
|
||||
unmeasured lodged 360 mm. WORTH A DEDICATED CHECK (build with the thickness
|
||||
keyed both ways).
|
||||
2. **PARTY WALL — NOT an engine gap.** Elmhurst rendered the party wall as
|
||||
"Solid, U 0.00", but the engine's **0.25** is SPEC-CORRECT: RdSAP §5.10
|
||||
Table 15 row 4 ("unable to determine, house") = 0.25. The cert lodges
|
||||
`party_wall_construction = 0` (unable to determine), so 0.25 is right; the
|
||||
Elmhurst 0.00 reflects how my build's party-wall dropdown resolved (a
|
||||
"Solid" masonry party wall, Table 15 row 1 = 0), not the cert's "unknown".
|
||||
Build artifact, not a bug.
|
||||
|
||||
## Build caveats (why the 71.7-vs-66 residual isn't cleanly attributable)
|
||||
- Heating: built a GENERIC Post-98 condensing combi; the engine uses the lodged
|
||||
PCDB 16137 (Ideal Logic combi 30) — different efficiency.
|
||||
- Wall thickness: not keyed into Elmhurst (defaulted 200 mm) vs engine's 360 mm.
|
||||
- Party wall: entered as a resolved type (0.00) vs the cert's "unable to
|
||||
determine" (engine 0.25).
|
||||
So this build's clean, confirmed results are: (a) the ROOF matches exactly
|
||||
(U 0.30, 9.35 W/K), (b) slice 5's DIRECTION is validated (insulated wall, not
|
||||
the uninsulated 1.40). The wall-thickness question (lead 1) is the one genuine
|
||||
follow-up.
|
||||
|
||||
## Slice-5 calibration note (banked)
|
||||
The accredited "External insulation, **Unknown** thickness" selection in Elmhurst
|
||||
produced U 0.48 (≈ a 50 mm equivalent on its 200 mm base), NOT the 0.29 the
|
||||
§5.4-literal "assume 100 mm" gives on the engine's 360 mm base. The 100 mm fix
|
||||
nevertheless matches the LODGED corpus better (MAE 0.658 → 0.647, within held) —
|
||||
lodged assessors typically knew the real (~100 mm) thickness the register then
|
||||
flattens to "NI". Per the campaign rule (engine = lodged is the goal; Elmhurst
|
||||
arbitrates only when lodged can't be explained from register data), the 100 mm
|
||||
fix STANDS; the lodged-vs-Elmhurst gap here is explained by the register's "NI"
|
||||
discarding the assessor's thickness knowledge. But the base-thickness and
|
||||
party-wall leads above are genuine and independent of the insulation default.
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,361 @@
|
|||
{
|
||||
"uprn": 200004296092,
|
||||
"roofs": [
|
||||
{
|
||||
"description": "Pitched, 150 mm loft insulation",
|
||||
"energy_efficiency_rating": 4,
|
||||
"environmental_efficiency_rating": 4
|
||||
}
|
||||
],
|
||||
"walls": [
|
||||
{
|
||||
"description": "Solid brick, with external insulation",
|
||||
"energy_efficiency_rating": 4,
|
||||
"environmental_efficiency_rating": 4
|
||||
}
|
||||
],
|
||||
"floors": [
|
||||
{
|
||||
"description": "Solid, no insulation (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
|
||||
},
|
||||
"lighting": {
|
||||
"description": "Excellent lighting efficiency",
|
||||
"energy_efficiency_rating": 5,
|
||||
"environmental_efficiency_rating": 5
|
||||
},
|
||||
"postcode": "LL16 3RL",
|
||||
"hot_water": {
|
||||
"description": "From main system",
|
||||
"energy_efficiency_rating": 4,
|
||||
"environmental_efficiency_rating": 4
|
||||
},
|
||||
"post_town": "DENBIGH",
|
||||
"psv_count": 0,
|
||||
"built_form": 3,
|
||||
"created_at": "2026-05-14 14:33:05",
|
||||
"door_count": 2,
|
||||
"region_code": 7,
|
||||
"report_type": 2,
|
||||
"sap_heating": {
|
||||
"number_baths": 0,
|
||||
"cylinder_size": 1,
|
||||
"shower_outlets": [
|
||||
{
|
||||
"shower_wwhrs": 1,
|
||||
"shower_outlet_type": 2
|
||||
}
|
||||
],
|
||||
"number_baths_wwhrs": 0,
|
||||
"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,
|
||||
"emitter_temperature": 1,
|
||||
"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": 10.2,
|
||||
"sap_windows": [
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 5,
|
||||
"window_type": 1,
|
||||
"glazing_type": 2,
|
||||
"window_width": {
|
||||
"value": 1.07,
|
||||
"quantity": "m"
|
||||
},
|
||||
"window_height": {
|
||||
"value": 1.34,
|
||||
"quantity": "m"
|
||||
},
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 5,
|
||||
"window_type": 1,
|
||||
"glazing_type": 2,
|
||||
"window_width": {
|
||||
"value": 1.07,
|
||||
"quantity": "m"
|
||||
},
|
||||
"window_height": {
|
||||
"value": 1.34,
|
||||
"quantity": "m"
|
||||
},
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 1,
|
||||
"window_type": 1,
|
||||
"glazing_type": 2,
|
||||
"window_width": {
|
||||
"value": 0.47,
|
||||
"quantity": "m"
|
||||
},
|
||||
"window_height": {
|
||||
"value": 0.85,
|
||||
"quantity": "m"
|
||||
},
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 1,
|
||||
"window_type": 1,
|
||||
"glazing_type": 2,
|
||||
"window_width": {
|
||||
"value": 0.47,
|
||||
"quantity": "m"
|
||||
},
|
||||
"window_height": {
|
||||
"value": 0.85,
|
||||
"quantity": "m"
|
||||
},
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 1,
|
||||
"window_type": 1,
|
||||
"glazing_type": 2,
|
||||
"window_width": {
|
||||
"value": 0.76,
|
||||
"quantity": "m"
|
||||
},
|
||||
"window_height": {
|
||||
"value": 0.84,
|
||||
"quantity": "m"
|
||||
},
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
}
|
||||
],
|
||||
"schema_type": "RdSAP-Schema-21.0.1",
|
||||
"uprn_source": "Energy Assessor",
|
||||
"country_code": "WLS",
|
||||
"main_heating": [
|
||||
{
|
||||
"description": "Boiler and radiators, mains gas",
|
||||
"energy_efficiency_rating": 4,
|
||||
"environmental_efficiency_rating": 4
|
||||
}
|
||||
],
|
||||
"air_tightness": {
|
||||
"description": "(not tested)",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
},
|
||||
"dwelling_type": "End-terrace house",
|
||||
"language_code": 1,
|
||||
"pressure_test": 4,
|
||||
"property_type": 0,
|
||||
"address_line_1": "54 Myddleton Avenue",
|
||||
"assessment_type": "RdSAP",
|
||||
"completion_date": "2026-05-14",
|
||||
"inspection_date": "2026-05-14",
|
||||
"extensions_count": 0,
|
||||
"measurement_type": 1,
|
||||
"open_flues_count": 0,
|
||||
"total_floor_area": 62,
|
||||
"transaction_type": 1,
|
||||
"conservatory_type": 1,
|
||||
"heated_room_count": 3,
|
||||
"other_flues_count": 0,
|
||||
"registration_date": "2026-05-14",
|
||||
"sap_energy_source": {
|
||||
"mains_gas": "Y",
|
||||
"meter_type": 2,
|
||||
"pv_connection": 0,
|
||||
"photovoltaic_supply": {
|
||||
"none_or_no_details": {
|
||||
"percent_roof_area": 0
|
||||
}
|
||||
},
|
||||
"wind_turbines_count": 0,
|
||||
"gas_smart_meter_present": "true",
|
||||
"is_dwelling_export_capable": "false",
|
||||
"wind_turbines_terrain_type": 2,
|
||||
"electricity_smart_meter_present": "true"
|
||||
},
|
||||
"secondary_heating": {
|
||||
"description": "None",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
},
|
||||
"closed_flues_count": 0,
|
||||
"extract_fans_count": 0,
|
||||
"sap_building_parts": [
|
||||
{
|
||||
"identifier": "Main Dwelling",
|
||||
"wall_dry_lined": "N",
|
||||
"wall_thickness": 360,
|
||||
"floor_heat_loss": 7,
|
||||
"roof_construction": 4,
|
||||
"wall_construction": 3,
|
||||
"building_part_number": 1,
|
||||
"sap_floor_dimensions": [
|
||||
{
|
||||
"floor": 0,
|
||||
"room_height": 2.4,
|
||||
"floor_insulation": 1,
|
||||
"total_floor_area": 31.16,
|
||||
"party_wall_length": 6.79,
|
||||
"floor_construction": 1,
|
||||
"heat_loss_perimeter": 15.97
|
||||
},
|
||||
{
|
||||
"floor": 1,
|
||||
"room_height": 2.4,
|
||||
"total_floor_area": 31.16,
|
||||
"party_wall_length": 6.79,
|
||||
"heat_loss_perimeter": 15.97
|
||||
}
|
||||
],
|
||||
"wall_insulation_type": 1,
|
||||
"construction_age_band": "B",
|
||||
"party_wall_construction": 0,
|
||||
"wall_thickness_measured": "Y",
|
||||
"roof_insulation_location": 2,
|
||||
"roof_insulation_thickness": "150mm",
|
||||
"wall_insulation_thickness": "NI"
|
||||
}
|
||||
],
|
||||
"boilers_flues_count": 0,
|
||||
"open_chimneys_count": 0,
|
||||
"solar_water_heating": "N",
|
||||
"habitable_room_count": 3,
|
||||
"heating_cost_current": {
|
||||
"value": 589,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"insulated_door_count": 0,
|
||||
"co2_emissions_current": 1.8,
|
||||
"energy_rating_average": 60,
|
||||
"energy_rating_current": 71,
|
||||
"lighting_cost_current": {
|
||||
"value": 50,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"main_heating_controls": [
|
||||
{
|
||||
"description": "Programmer, room thermostat and TRVs",
|
||||
"energy_efficiency_rating": 4,
|
||||
"environmental_efficiency_rating": 4
|
||||
}
|
||||
],
|
||||
"blocked_chimneys_count": 0,
|
||||
"has_hot_water_cylinder": "false",
|
||||
"heating_cost_potential": {
|
||||
"value": 543,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"hot_water_cost_current": {
|
||||
"value": 275,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"mechanical_ventilation": 0,
|
||||
"percent_draughtproofed": 100,
|
||||
"schema_version_current": "LIG-21.0",
|
||||
"suggested_improvements": [
|
||||
{
|
||||
"sequence": 1,
|
||||
"typical_saving": 47,
|
||||
"indicative_cost": "\u00a35,000 - \u00a310,000",
|
||||
"improvement_type": "W2",
|
||||
"improvement_details": {
|
||||
"improvement_number": 58
|
||||
},
|
||||
"improvement_category": 5,
|
||||
"energy_performance_rating": 72,
|
||||
"environmental_impact_rating": 79
|
||||
},
|
||||
{
|
||||
"sequence": 2,
|
||||
"typical_saving": 275,
|
||||
"indicative_cost": "\u00a38,000 - \u00a310,000",
|
||||
"improvement_type": "U",
|
||||
"improvement_details": {
|
||||
"improvement_number": 34
|
||||
},
|
||||
"improvement_category": 5,
|
||||
"energy_performance_rating": 84,
|
||||
"environmental_impact_rating": 81
|
||||
}
|
||||
],
|
||||
"co2_emissions_potential": 1.4,
|
||||
"energy_rating_potential": 84,
|
||||
"lighting_cost_potential": {
|
||||
"value": 50,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"schema_version_original": "LIG-21.0",
|
||||
"hot_water_cost_potential": {
|
||||
"value": 275,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"renewable_heat_incentive": {
|
||||
"water_heating": 1768.31,
|
||||
"space_heating_existing_dwelling": 5933.75
|
||||
},
|
||||
"draughtproofed_door_count": 2,
|
||||
"energy_consumption_current": 169,
|
||||
"has_fixed_air_conditioning": "false",
|
||||
"multiple_glazed_proportion": 100,
|
||||
"calculation_software_version": "10.2.2.0",
|
||||
"energy_consumption_potential": 130,
|
||||
"environmental_impact_current": 77,
|
||||
"cfl_fixed_lighting_bulbs_count": 0,
|
||||
"current_energy_efficiency_band": "C",
|
||||
"environmental_impact_potential": 81,
|
||||
"led_fixed_lighting_bulbs_count": 12,
|
||||
"has_heated_separate_conservatory": "false",
|
||||
"potential_energy_efficiency_band": "B",
|
||||
"co2_emissions_current_per_floor_area": 29,
|
||||
"incandescent_fixed_lighting_bulbs_count": 0
|
||||
}
|
||||
|
|
@ -0,0 +1,254 @@
|
|||
Summary Information
|
||||
Surveyor: P960-0001
|
||||
Name: Richard Matthew Ratcliff Title: Mr. Tel Number: 07760 443 469
|
||||
Survey Reference: 001431 My Reference: Khalim-test
|
||||
Current SAP rating: D 66 Potential SAP rating: C 75 Emissions (t/year): 1.927 tonnes
|
||||
Current EI rating: C 72 Potential EI rating: C 76 Fuel Bill: £1026
|
||||
Property Details:
|
||||
RdSAP version: RdSAP10
|
||||
Reference Number: P960-0001-001431
|
||||
My Reference: Khalim-test
|
||||
Lodgement Required: No
|
||||
Regs Region: England
|
||||
EPC Language: English
|
||||
UPRN:
|
||||
Postcode: W6 9BF
|
||||
Region: Thames Valley
|
||||
House Name: 28 Distillery Wharf
|
||||
House No: 28
|
||||
Street: Distillery Wharf
|
||||
Locality:
|
||||
Town: London
|
||||
County:
|
||||
Property Tenure: Owner-occupied
|
||||
Transaction Type: Marketed sale
|
||||
Inspection Date: 25/06/2025
|
||||
Process date: 02/07/2026
|
||||
Check for the existence of No
|
||||
an EPC:
|
||||
Does an EPC exist at the No
|
||||
point of carrying out this
|
||||
energy assessment:
|
||||
Reason why another energy
|
||||
assessment needs to be
|
||||
undertaken:
|
||||
|
||||
RdSAP Inputs
|
||||
|
||||
Property Description:
|
||||
1.0 Property type: H House
|
||||
E End-Terrace
|
||||
2.0 Number of Storeys: 2
|
||||
Habitable Rooms: 3
|
||||
Heated Habitable Rooms: 3
|
||||
3.0 Date Built: Main Property B 1900-1929
|
||||
|
||||
|
||||
4.0 Dimensions:
|
||||
Dimension type: Internal
|
||||
Main Property
|
||||
Floor Room Heat Loss Party Wall
|
||||
Area Height Wall Perimeter Length
|
||||
[m2] [m] [m] [m]
|
||||
|
||||
1st Floor: 31.16 2.40 15.97 6.79
|
||||
Lowest Floor: 31.16 2.40 15.97 6.79 No
|
||||
|
||||
5.0 Conservatory:
|
||||
Is there a conservatory? No
|
||||
7.0 Walls:
|
||||
|
||||
|
||||
|
||||
|
||||
© Elmhurst Energy Systems Limited Registered Office Unit 16, St Johns Business Park, Lutterworth, Leicestershire LE17 4HB
|
||||
Summary Information
|
||||
Main Property
|
||||
|
||||
Type SO Solid Brick
|
||||
Insulation E External
|
||||
Insulation Thickness Unknown
|
||||
Conductivity Known No
|
||||
Thermal Conductivity Lambda = 0.04 W/m*K
|
||||
Wall Thickness Unknown No
|
||||
Wall Thickness 260 mm
|
||||
U-value Known No
|
||||
Party Wall Type S Solid masonry / timber / system build
|
||||
8.0 Roofs:
|
||||
Main Property
|
||||
|
||||
Type PA Pitched (slates/tiles), access to loft
|
||||
Insulation J Joists
|
||||
Insulation Thickness 150 mm
|
||||
U-value Known No
|
||||
8.1 Rooms in Roof:
|
||||
9.0 Floors:
|
||||
Main Property
|
||||
|
||||
Location G Ground floor
|
||||
Type S Solid
|
||||
Insulation A As built
|
||||
Default U-value 0.73
|
||||
U-value Known No
|
||||
10.0 Doors:
|
||||
Total Number of Doors 2
|
||||
Number of Insulated Doors 0
|
||||
11.0 Windows:
|
||||
Frame Frame Glazing Building U g Draught Permanent
|
||||
W H Area Glazing Type Location Orient. Data-Source
|
||||
Type Factor Gap Part value value Proofed Shutters
|
||||
Double with unknown
|
||||
4.31 1.00 4.31 PVC 0.70 12 mm Main External wall North Manufacturer 2.80 0.76 Yes None
|
||||
install date
|
||||
Double with unknown
|
||||
2.89 1.00 2.89 PVC 0.70 12 mm Main External wall West Manufacturer 2.80 0.76 Yes None
|
||||
install date
|
||||
Double with unknown
|
||||
2.89 1.00 2.89 PVC 0.70 12 mm Main External wall North Manufacturer 2.80 0.76 Yes None
|
||||
install date
|
||||
Double with unknown
|
||||
2.89 1.00 2.89 PVC 0.70 12 mm Main External wall East Manufacturer 2.80 0.76 Yes None
|
||||
install date
|
||||
|
||||
Draught Proofing 67 %
|
||||
12.0 Ventilation & Cooling
|
||||
No. of open chimneys 0
|
||||
No. of open flues 0
|
||||
No. of open chimneys/open flues attached to closed fire 0
|
||||
No. of flues attached to solid fuel boiler 0
|
||||
No. of open flues attached to other heater 0
|
||||
No. of blocked chimneys 0
|
||||
No. of intermittent extract fans 0
|
||||
No. of passive vents 0
|
||||
No. of flueless gas fires 0
|
||||
Fixed Space Cooling No
|
||||
Draught Lobby Unable to determine
|
||||
|
||||
|
||||
|
||||
|
||||
© Elmhurst Energy Systems Limited Registered Office Unit 16, St Johns Business Park, Lutterworth, Leicestershire LE17 4HB
|
||||
Summary Information
|
||||
12.1 Mechanical Ventilation
|
||||
Mechanical Ventilation No
|
||||
12.2 Air Pressure Test
|
||||
Test Method Not available
|
||||
13.0 Lighting
|
||||
Total number of bulbs 6
|
||||
Number of LED and CFL Known Yes
|
||||
Number of LED lights 0
|
||||
Number of CFL lights 0
|
||||
Total number of Low Energy 0
|
||||
Total number of incandescents 6
|
||||
14.0 Main Heating1
|
||||
PCDF boiler Reference 0
|
||||
Main Heating EES Code BGW
|
||||
Main Heating SAP Code 104
|
||||
Heat Emitter Radiators
|
||||
Heat pump age 2013 or later
|
||||
Flue Type Balanced
|
||||
Fan Assisted Flue Yes
|
||||
Design flow temperature <= 35°C
|
||||
PCDF Heating Controls 0
|
||||
Main Heating Controls EES CBE
|
||||
Main Heating Controls Sap SAP code 2106, Programmer, room thermostat and TRVs
|
||||
PCDF Compensator 0
|
||||
Percentage of Heat 100 %
|
||||
14.1 Main Heating2
|
||||
PCDF boiler Reference 0
|
||||
Main Heating EES Code
|
||||
Main Heating SAP Code 0
|
||||
Percentage of Heat 0%
|
||||
14.1 Community Heating/Heat Network
|
||||
Heating Type None
|
||||
14.2 Meters
|
||||
Electricity meter type Single
|
||||
Main gas Yes
|
||||
Electricity Smart Meter Present No
|
||||
Gas Smart Meter Present No
|
||||
15.0 Water Heating
|
||||
Water Heating Code HWP
|
||||
Water Heating SapCode 901
|
||||
Water Heating Fuel Type Mains gas
|
||||
15.1 Hot Water Cylinder
|
||||
Hot Water Cylinder Present No
|
||||
15.2 Community Hot Water
|
||||
PCDF boiler Reference 0
|
||||
16.0 Solar water heating
|
||||
Solar Water Heating No
|
||||
17.0 Waste Water Heat Recovery System
|
||||
Is WWHRS present in the property? No / Unknown
|
||||
1x.0 Baths and Showers
|
||||
Total Number of Baths 2
|
||||
Number of Baths Connected 0
|
||||
|
||||
Description Type Connected
|
||||
|
||||
1 Non-electric shower None
|
||||
|
||||
|
||||
|
||||
|
||||
© Elmhurst Energy Systems Limited Registered Office Unit 16, St Johns Business Park, Lutterworth, Leicestershire LE17 4HB
|
||||
Summary Information
|
||||
18.0 Flue Gas Heat Recovery System
|
||||
Present No
|
||||
19.0 Photovoltaic Panel
|
||||
Photovoltaic Panel None
|
||||
Export capable meter No
|
||||
20.0 Wind Turbine
|
||||
Terrain Type Urban
|
||||
Wind turbine present? No
|
||||
22.0 Special Features
|
||||
21.0 Small-Scale Hydro
|
||||
Electricity generated [kWh/year] 0.00
|
||||
|
||||
|
||||
|
||||
|
||||
© Elmhurst Energy Systems Limited Registered Office Unit 16, St Johns Business Park, Lutterworth, Leicestershire LE17 4HB
|
||||
Summary Information
|
||||
|
||||
Recommendations
|
||||
|
||||
Loft insulation (SAP increase too small)
|
||||
Flat roof insulation (Not applicable)
|
||||
Room-in-roof insulation (Not applicable)
|
||||
Cavity wall insulation (Not applicable)
|
||||
Solid wall insulation (SAP increase too small)
|
||||
Floor insulation (solid floor) (Recommended)
|
||||
Hot water cylinder insulation (Not applicable)
|
||||
Draught proofing (SAP increase too small)
|
||||
Low energy lighting (Recommended)
|
||||
Cylinder thermostat (Not applicable)
|
||||
Heating controls for wet central heating system (Already installed)
|
||||
Upgrade boiler, same fuel (Already installed)
|
||||
Change heating to condensing gas condensing boiler (fuel switch) (Not applicable)
|
||||
Flue gas heat recovery in conjunction with new boiler (Not applicable)
|
||||
Solar water heating (SAP increase too small)
|
||||
Heat recovery system for mixer showers (SAP increase too small)
|
||||
Double glazed windows (Already installed)
|
||||
Insulated doors (SAP increase too small)
|
||||
Solar photovoltaic panels (Recommended)
|
||||
Wind turbine (Not applicable)
|
||||
PV diverter (Not applicable)
|
||||
PV battery (Not applicable)
|
||||
Water heating controls (Not applicable)
|
||||
|
||||
Alternative Recommendations
|
||||
External wall insulation with cavity insulation (Not applicable)
|
||||
Biomass boiler (alternative) (Not applicable)
|
||||
Micro CHP (alternative) (Not applicable)
|
||||
|
||||
Related Party Disclosure
|
||||
|
||||
No related party
|
||||
|
||||
Addenda
|
||||
When considering the PV installation consider installing PV battery and a PV diverter for water heating
|
||||
|
||||
|
||||
|
||||
|
||||
© Elmhurst Energy Systems Limited Registered Office Unit 16, St Johns Business Park, Lutterworth, Leicestershire LE17 4HB
|
||||
|
||||
File diff suppressed because it is too large
Load diff
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,384 @@
|
|||
{
|
||||
"uprn": 217091901,
|
||||
"roofs": [
|
||||
{
|
||||
"description": "Flat, no insulation",
|
||||
"energy_efficiency_rating": 1,
|
||||
"environmental_efficiency_rating": 1
|
||||
}
|
||||
],
|
||||
"walls": [
|
||||
{
|
||||
"description": "Solid brick, as built, no insulation (assumed)",
|
||||
"energy_efficiency_rating": 2,
|
||||
"environmental_efficiency_rating": 2
|
||||
}
|
||||
],
|
||||
"floors": [
|
||||
{
|
||||
"description": "(another dwelling below)",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
}
|
||||
],
|
||||
"status": "entered",
|
||||
"tenure": 3,
|
||||
"window": {
|
||||
"description": "Single glazed",
|
||||
"energy_efficiency_rating": 1,
|
||||
"environmental_efficiency_rating": 1
|
||||
},
|
||||
"lighting": {
|
||||
"description": "Good lighting efficiency",
|
||||
"energy_efficiency_rating": 4,
|
||||
"environmental_efficiency_rating": 4
|
||||
},
|
||||
"postcode": "W11 1EP",
|
||||
"hot_water": {
|
||||
"description": "From main system",
|
||||
"energy_efficiency_rating": 4,
|
||||
"environmental_efficiency_rating": 4
|
||||
},
|
||||
"post_town": "LONDON",
|
||||
"built_form": "NR",
|
||||
"created_at": "2026-05-16 15:45:17",
|
||||
"door_count": 0,
|
||||
"region_code": 17,
|
||||
"report_type": 2,
|
||||
"sap_heating": {
|
||||
"number_baths": 1,
|
||||
"cylinder_size": 1,
|
||||
"shower_outlets": [
|
||||
{
|
||||
"shower_wwhrs": 1,
|
||||
"shower_outlet_type": 1
|
||||
}
|
||||
],
|
||||
"number_baths_wwhrs": 0,
|
||||
"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,
|
||||
"emitter_temperature": 0,
|
||||
"main_heating_number": 1,
|
||||
"main_heating_control": 2107,
|
||||
"main_heating_category": 2,
|
||||
"main_heating_fraction": 1,
|
||||
"central_heating_pump_age": 0,
|
||||
"main_heating_data_source": 1,
|
||||
"main_heating_index_number": 18658
|
||||
}
|
||||
],
|
||||
"immersion_heating_type": "NA",
|
||||
"has_fixed_air_conditioning": "false"
|
||||
},
|
||||
"sap_version": 10.2,
|
||||
"sap_windows": [
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 5,
|
||||
"window_type": 1,
|
||||
"glazing_type": 5,
|
||||
"window_width": 1.2,
|
||||
"window_height": 1.8,
|
||||
"draught_proofed": "false",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 5,
|
||||
"window_type": 1,
|
||||
"glazing_type": 5,
|
||||
"window_width": 1.2,
|
||||
"window_height": 1.8,
|
||||
"draught_proofed": "false",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 9,
|
||||
"window_type": 2,
|
||||
"glazing_type": 5,
|
||||
"window_width": 0.5,
|
||||
"window_height": 0.5,
|
||||
"draught_proofed": "false",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 9,
|
||||
"window_type": 2,
|
||||
"glazing_type": 5,
|
||||
"window_width": 0.7,
|
||||
"window_height": 0.5,
|
||||
"draught_proofed": "false",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 1,
|
||||
"window_type": 1,
|
||||
"glazing_type": 5,
|
||||
"window_width": 1.2,
|
||||
"window_height": 1.8,
|
||||
"draught_proofed": "false",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 1,
|
||||
"window_type": 1,
|
||||
"glazing_type": 5,
|
||||
"window_width": 1.1,
|
||||
"window_height": 1.4,
|
||||
"draught_proofed": "false",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
}
|
||||
],
|
||||
"schema_type": "RdSAP-Schema-21.0.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": "(not tested)",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
},
|
||||
"dwelling_type": "Top-floor flat",
|
||||
"language_code": 1,
|
||||
"pressure_test": 4,
|
||||
"property_type": 2,
|
||||
"address_line_1": "Second Floor Flat",
|
||||
"address_line_2": "216 Westbourne Park Road",
|
||||
"assessment_type": "RdSAP",
|
||||
"completion_date": "2026-05-16",
|
||||
"inspection_date": "2026-05-16",
|
||||
"extensions_count": 0,
|
||||
"measurement_type": 1,
|
||||
"sap_flat_details": {
|
||||
"level": 3,
|
||||
"top_storey": "Y",
|
||||
"storey_count": 3,
|
||||
"flat_location": 2,
|
||||
"heat_loss_corridor": 0
|
||||
},
|
||||
"total_floor_area": 63,
|
||||
"transaction_type": 8,
|
||||
"conservatory_type": 1,
|
||||
"has_draught_lobby": "true",
|
||||
"heated_room_count": 3,
|
||||
"registration_date": "2026-05-16",
|
||||
"sap_energy_source": {
|
||||
"mains_gas": "Y",
|
||||
"meter_type": 2,
|
||||
"pv_connection": 0,
|
||||
"photovoltaic_supply": {
|
||||
"none_or_no_details": {
|
||||
"percent_roof_area": 0
|
||||
}
|
||||
},
|
||||
"wind_turbines_count": 0,
|
||||
"gas_smart_meter_present": "true",
|
||||
"is_dwelling_export_capable": "false",
|
||||
"wind_turbines_terrain_type": 2,
|
||||
"electricity_smart_meter_present": "true"
|
||||
},
|
||||
"secondary_heating": {
|
||||
"description": "None",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
},
|
||||
"sap_building_parts": [
|
||||
{
|
||||
"identifier": "Main Dwelling",
|
||||
"wall_dry_lined": "N",
|
||||
"wall_thickness": 280,
|
||||
"floor_heat_loss": 6,
|
||||
"roof_construction": 1,
|
||||
"wall_construction": 3,
|
||||
"building_part_number": 1,
|
||||
"sap_floor_dimensions": [
|
||||
{
|
||||
"floor": 0,
|
||||
"room_height": {
|
||||
"value": 2.85,
|
||||
"quantity": "metres"
|
||||
},
|
||||
"total_floor_area": {
|
||||
"value": 63.29,
|
||||
"quantity": "square metres"
|
||||
},
|
||||
"party_wall_length": {
|
||||
"value": 22.8,
|
||||
"quantity": "metres"
|
||||
},
|
||||
"heat_loss_perimeter": {
|
||||
"value": 14,
|
||||
"quantity": "metres"
|
||||
}
|
||||
}
|
||||
],
|
||||
"wall_insulation_type": 4,
|
||||
"construction_age_band": "A",
|
||||
"party_wall_construction": 0,
|
||||
"wall_thickness_measured": "Y",
|
||||
"roof_insulation_location": 6,
|
||||
"wall_insulation_thickness": "NI",
|
||||
"flat_roof_insulation_thickness": "AB"
|
||||
}
|
||||
],
|
||||
"solar_water_heating": "N",
|
||||
"habitable_room_count": 3,
|
||||
"heating_cost_current": {
|
||||
"value": 967,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"insulated_door_count": 0,
|
||||
"co2_emissions_current": 2.8,
|
||||
"energy_rating_average": 60,
|
||||
"energy_rating_current": 62,
|
||||
"lighting_cost_current": {
|
||||
"value": 43,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"main_heating_controls": [
|
||||
{
|
||||
"description": "Programmer, TRVs and bypass",
|
||||
"energy_efficiency_rating": 3,
|
||||
"environmental_efficiency_rating": 3
|
||||
}
|
||||
],
|
||||
"blocked_chimneys_count": 1,
|
||||
"has_hot_water_cylinder": "false",
|
||||
"heating_cost_potential": {
|
||||
"value": 330,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"hot_water_cost_current": {
|
||||
"value": 167,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"mechanical_ventilation": 0,
|
||||
"percent_draughtproofed": 0,
|
||||
"suggested_improvements": [
|
||||
{
|
||||
"sequence": 1,
|
||||
"typical_saving": {
|
||||
"value": 411,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"indicative_cost": "\u00a3900 - \u00a31,200",
|
||||
"improvement_type": "A2",
|
||||
"improvement_details": {
|
||||
"improvement_number": 45
|
||||
},
|
||||
"improvement_category": 5,
|
||||
"energy_performance_rating": 74,
|
||||
"environmental_impact_rating": 76
|
||||
},
|
||||
{
|
||||
"sequence": 2,
|
||||
"typical_saving": {
|
||||
"value": 129,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"indicative_cost": "\u00a37,500 - \u00a311,000",
|
||||
"improvement_type": "Q",
|
||||
"improvement_details": {
|
||||
"improvement_number": 7
|
||||
},
|
||||
"improvement_category": 5,
|
||||
"energy_performance_rating": 78,
|
||||
"environmental_impact_rating": 81
|
||||
},
|
||||
{
|
||||
"sequence": 3,
|
||||
"typical_saving": {
|
||||
"value": 18,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"indicative_cost": "\u00a3150 - \u00a3250",
|
||||
"improvement_type": "D",
|
||||
"improvement_details": {
|
||||
"improvement_number": 10
|
||||
},
|
||||
"improvement_category": 5,
|
||||
"energy_performance_rating": 79,
|
||||
"environmental_impact_rating": 82
|
||||
},
|
||||
{
|
||||
"sequence": 4,
|
||||
"typical_saving": {
|
||||
"value": 78,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"indicative_cost": "\u00a34,500 - \u00a36,000",
|
||||
"improvement_type": "O",
|
||||
"improvement_details": {
|
||||
"improvement_number": 8
|
||||
},
|
||||
"improvement_category": 5,
|
||||
"energy_performance_rating": 81,
|
||||
"environmental_impact_rating": 85
|
||||
}
|
||||
],
|
||||
"co2_emissions_potential": 1.0,
|
||||
"energy_rating_potential": 81,
|
||||
"lighting_cost_potential": {
|
||||
"value": 45,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"schema_version_original": "21.0.1",
|
||||
"hot_water_cost_potential": {
|
||||
"value": 167,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"renewable_heat_incentive": {
|
||||
"water_heating": 2022.69,
|
||||
"space_heating_existing_dwelling": 9246.82
|
||||
},
|
||||
"draughtproofed_door_count": 0,
|
||||
"energy_consumption_current": 245,
|
||||
"has_fixed_air_conditioning": "false",
|
||||
"multiple_glazed_proportion": 0,
|
||||
"calculation_software_version": "5.02r0344",
|
||||
"energy_consumption_potential": 90,
|
||||
"environmental_impact_current": 59,
|
||||
"cfl_fixed_lighting_bulbs_count": 2,
|
||||
"current_energy_efficiency_band": "D",
|
||||
"environmental_impact_potential": 85,
|
||||
"led_fixed_lighting_bulbs_count": 9,
|
||||
"has_heated_separate_conservatory": "false",
|
||||
"potential_energy_efficiency_band": "B",
|
||||
"co2_emissions_current_per_floor_area": 45,
|
||||
"incandescent_fixed_lighting_bulbs_count": 0
|
||||
}
|
||||
|
|
@ -0,0 +1,299 @@
|
|||
{
|
||||
"uprn": 4510053280,
|
||||
"roofs": [
|
||||
{
|
||||
"description": "(another dwelling above)",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
}
|
||||
],
|
||||
"walls": [
|
||||
{
|
||||
"description": "Solid brick, as built, no insulation (assumed)",
|
||||
"energy_efficiency_rating": 2,
|
||||
"environmental_efficiency_rating": 2
|
||||
},
|
||||
{
|
||||
"description": "Solid brick, as built, partial insulation (assumed)",
|
||||
"energy_efficiency_rating": 3,
|
||||
"environmental_efficiency_rating": 3
|
||||
}
|
||||
],
|
||||
"floors": [
|
||||
{
|
||||
"description": "Solid, no insulation (assumed)",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
}
|
||||
],
|
||||
"status": "entered",
|
||||
"tenure": 2,
|
||||
"window": {
|
||||
"description": "Full secondary glazing",
|
||||
"energy_efficiency_rating": 2,
|
||||
"environmental_efficiency_rating": 2
|
||||
},
|
||||
"lighting": {
|
||||
"description": "Good lighting efficiency",
|
||||
"energy_efficiency_rating": 4,
|
||||
"environmental_efficiency_rating": 4
|
||||
},
|
||||
"postcode": "NE1 4DG",
|
||||
"hot_water": {
|
||||
"description": "From main system, no cylinder thermostat",
|
||||
"energy_efficiency_rating": 1,
|
||||
"environmental_efficiency_rating": 4
|
||||
},
|
||||
"post_town": "NEWCASTLE UPON TYNE",
|
||||
"built_form": "NR",
|
||||
"created_at": "2026-05-24 12:41:32",
|
||||
"door_count": 0,
|
||||
"region_code": 1,
|
||||
"report_type": 2,
|
||||
"sap_heating": {
|
||||
"number_baths": 1,
|
||||
"cylinder_size": 1,
|
||||
"number_baths_wwhrs": 0,
|
||||
"water_heating_code": 901,
|
||||
"water_heating_fuel": 29,
|
||||
"secondary_fuel_type": 29,
|
||||
"main_heating_details": [
|
||||
{
|
||||
"has_fghrs": "N",
|
||||
"main_fuel_type": 29,
|
||||
"heat_emitter_type": 1,
|
||||
"emitter_temperature": 0,
|
||||
"main_heating_number": 1,
|
||||
"main_heating_control": 2203,
|
||||
"main_heating_category": 4,
|
||||
"main_heating_fraction": 1,
|
||||
"mcs_installed_heat_pump": "false",
|
||||
"central_heating_pump_age": 0,
|
||||
"main_heating_data_source": 1,
|
||||
"main_heating_index_number": 100053
|
||||
}
|
||||
],
|
||||
"immersion_heating_type": "NA",
|
||||
"secondary_heating_type": 691,
|
||||
"has_fixed_air_conditioning": "false"
|
||||
},
|
||||
"sap_version": 10.2,
|
||||
"sap_windows": [
|
||||
{
|
||||
"orientation": 4,
|
||||
"window_type": 1,
|
||||
"glazing_type": 4,
|
||||
"window_width": 1.3,
|
||||
"window_height": 1.46,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"orientation": 4,
|
||||
"window_type": 1,
|
||||
"glazing_type": 4,
|
||||
"window_width": 1.3,
|
||||
"window_height": 1.46,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
}
|
||||
],
|
||||
"schema_type": "RdSAP-Schema-21.0.1",
|
||||
"uprn_source": "Energy Assessor",
|
||||
"country_code": "ENG",
|
||||
"main_heating": [
|
||||
{
|
||||
"description": "Air source heat pump, radiators, electric",
|
||||
"energy_efficiency_rating": 4,
|
||||
"environmental_efficiency_rating": 5
|
||||
}
|
||||
],
|
||||
"air_tightness": {
|
||||
"description": "(not tested)",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
},
|
||||
"dwelling_type": "Ground-floor flat",
|
||||
"language_code": 1,
|
||||
"pressure_test": 4,
|
||||
"property_type": 2,
|
||||
"address_line_1": "64 Waterloo Street",
|
||||
"assessment_type": "RdSAP",
|
||||
"completion_date": "2026-05-24",
|
||||
"inspection_date": "2026-05-22",
|
||||
"extensions_count": 0,
|
||||
"measurement_type": 1,
|
||||
"sap_flat_details": {
|
||||
"level": 1,
|
||||
"top_storey": "N",
|
||||
"storey_count": 3,
|
||||
"flat_location": 0,
|
||||
"heat_loss_corridor": 2,
|
||||
"unheated_corridor_length": 3.5
|
||||
},
|
||||
"total_floor_area": 47,
|
||||
"transaction_type": 8,
|
||||
"conservatory_type": 1,
|
||||
"has_draught_lobby": "true",
|
||||
"heated_room_count": 2,
|
||||
"registration_date": "2026-05-24",
|
||||
"sap_energy_source": {
|
||||
"mains_gas": "N",
|
||||
"meter_type": 2,
|
||||
"pv_connection": 0,
|
||||
"photovoltaic_supply": {
|
||||
"none_or_no_details": {
|
||||
"percent_roof_area": 0
|
||||
}
|
||||
},
|
||||
"wind_turbines_count": 0,
|
||||
"gas_smart_meter_present": "false",
|
||||
"is_dwelling_export_capable": "false",
|
||||
"wind_turbines_terrain_type": 1,
|
||||
"electricity_smart_meter_present": "false"
|
||||
},
|
||||
"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": 520,
|
||||
"floor_heat_loss": 7,
|
||||
"roof_construction": 3,
|
||||
"wall_construction": 3,
|
||||
"building_part_number": 1,
|
||||
"sap_floor_dimensions": [
|
||||
{
|
||||
"floor": 0,
|
||||
"room_height": {
|
||||
"value": 2.32,
|
||||
"quantity": "metres"
|
||||
},
|
||||
"floor_insulation": 1,
|
||||
"total_floor_area": {
|
||||
"value": 47,
|
||||
"quantity": "square metres"
|
||||
},
|
||||
"party_wall_length": {
|
||||
"value": 8,
|
||||
"quantity": "metres"
|
||||
},
|
||||
"floor_construction": 1,
|
||||
"heat_loss_perimeter": {
|
||||
"value": 14.15,
|
||||
"quantity": "metres"
|
||||
}
|
||||
}
|
||||
],
|
||||
"wall_insulation_type": 4,
|
||||
"construction_age_band": "B",
|
||||
"sap_alternative_wall_1": {
|
||||
"wall_area": 8.12,
|
||||
"sheltered_wall": "Y",
|
||||
"wall_dry_lined": "N",
|
||||
"wall_thickness": 520,
|
||||
"wall_construction": 3,
|
||||
"wall_insulation_type": 4,
|
||||
"wall_thickness_measured": "Y",
|
||||
"wall_insulation_thickness": "NI"
|
||||
},
|
||||
"party_wall_construction": 0,
|
||||
"wall_thickness_measured": "Y",
|
||||
"roof_insulation_location": "ND",
|
||||
"roof_insulation_thickness": "ND",
|
||||
"wall_insulation_thickness": "NI",
|
||||
"floor_insulation_thickness": "NI"
|
||||
}
|
||||
],
|
||||
"solar_water_heating": "N",
|
||||
"habitable_room_count": 2,
|
||||
"heating_cost_current": {
|
||||
"value": 278,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"insulated_door_count": 0,
|
||||
"co2_emissions_current": 0.8,
|
||||
"energy_rating_average": 60,
|
||||
"energy_rating_current": 51,
|
||||
"lighting_cost_current": {
|
||||
"value": 39,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"main_heating_controls": [
|
||||
{
|
||||
"description": "Room thermostat only",
|
||||
"energy_efficiency_rating": 2,
|
||||
"environmental_efficiency_rating": 2
|
||||
}
|
||||
],
|
||||
"has_hot_water_cylinder": "false",
|
||||
"heating_cost_potential": {
|
||||
"value": 272,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"hot_water_cost_current": {
|
||||
"value": 1295,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"mechanical_ventilation": 0,
|
||||
"percent_draughtproofed": 100,
|
||||
"suggested_improvements": [
|
||||
{
|
||||
"sequence": 1,
|
||||
"typical_saving": {
|
||||
"value": 68,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"indicative_cost": "\u00a3220 - \u00a3250",
|
||||
"improvement_type": "G",
|
||||
"improvement_details": {
|
||||
"improvement_number": 16
|
||||
},
|
||||
"improvement_category": 5,
|
||||
"energy_performance_rating": 53,
|
||||
"environmental_impact_rating": 88
|
||||
}
|
||||
],
|
||||
"co2_emissions_potential": 0.8,
|
||||
"energy_rating_potential": 53,
|
||||
"lighting_cost_potential": {
|
||||
"value": 39,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"schema_version_original": "21.0.1",
|
||||
"hot_water_cost_potential": {
|
||||
"value": 1233,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"renewable_heat_incentive": {
|
||||
"water_heating": 5072.68,
|
||||
"space_heating_existing_dwelling": 2884.0
|
||||
},
|
||||
"draughtproofed_door_count": 0,
|
||||
"energy_consumption_current": 190,
|
||||
"has_fixed_air_conditioning": "false",
|
||||
"multiple_glazed_proportion": 100,
|
||||
"calculation_software_version": "5.02r0344",
|
||||
"energy_consumption_potential": 182,
|
||||
"environmental_impact_current": 88,
|
||||
"current_energy_efficiency_band": "E",
|
||||
"environmental_impact_potential": 88,
|
||||
"has_heated_separate_conservatory": "false",
|
||||
"potential_energy_efficiency_band": "E",
|
||||
"co2_emissions_current_per_floor_area": 18,
|
||||
"low_energy_fixed_lighting_bulbs_count": 6,
|
||||
"incandescent_fixed_lighting_bulbs_count": 0
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,324 @@
|
|||
{
|
||||
"uprn": 47084930,
|
||||
"roofs": [
|
||||
{
|
||||
"description": "Flat, insulated",
|
||||
"energy_efficiency_rating": 3,
|
||||
"environmental_efficiency_rating": 3
|
||||
}
|
||||
],
|
||||
"walls": [
|
||||
{
|
||||
"description": "Cavity wall, with internal insulation",
|
||||
"energy_efficiency_rating": 4,
|
||||
"environmental_efficiency_rating": 4
|
||||
}
|
||||
],
|
||||
"floors": [
|
||||
{
|
||||
"description": "(other premises below)",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
}
|
||||
],
|
||||
"status": "entered",
|
||||
"tenure": 3,
|
||||
"window": {
|
||||
"description": "Fully double glazed",
|
||||
"energy_efficiency_rating": 3,
|
||||
"environmental_efficiency_rating": 3
|
||||
},
|
||||
"lighting": {
|
||||
"description": "Excellent lighting efficiency",
|
||||
"energy_efficiency_rating": 5,
|
||||
"environmental_efficiency_rating": 5
|
||||
},
|
||||
"postcode": "NE12 8AQ",
|
||||
"hot_water": {
|
||||
"description": "From main system",
|
||||
"energy_efficiency_rating": 4,
|
||||
"environmental_efficiency_rating": 4
|
||||
},
|
||||
"post_town": "NEWCASTLE UPON TYNE",
|
||||
"built_form": "NR",
|
||||
"created_at": "2026-05-06 10:59:38",
|
||||
"door_count": 0,
|
||||
"region_code": 1,
|
||||
"report_type": 2,
|
||||
"sap_heating": {
|
||||
"number_baths": 1,
|
||||
"cylinder_size": 1,
|
||||
"shower_outlets": [
|
||||
{
|
||||
"shower_wwhrs": 1,
|
||||
"shower_outlet_type": 1
|
||||
}
|
||||
],
|
||||
"number_baths_wwhrs": 0,
|
||||
"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,
|
||||
"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": 17815
|
||||
}
|
||||
],
|
||||
"immersion_heating_type": "NA",
|
||||
"has_fixed_air_conditioning": "false"
|
||||
},
|
||||
"sap_version": 10.2,
|
||||
"sap_windows": [
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 3,
|
||||
"window_type": 1,
|
||||
"glazing_type": 2,
|
||||
"window_width": 1.23,
|
||||
"window_height": 1.61,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 3,
|
||||
"window_type": 1,
|
||||
"glazing_type": 2,
|
||||
"window_width": 1.23,
|
||||
"window_height": 1.61,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 3,
|
||||
"window_type": 1,
|
||||
"glazing_type": 2,
|
||||
"window_width": 1.23,
|
||||
"window_height": 1.61,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 3,
|
||||
"window_type": 1,
|
||||
"glazing_type": 2,
|
||||
"window_width": 1.23,
|
||||
"window_height": 1.61,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 3,
|
||||
"window_type": 1,
|
||||
"glazing_type": 2,
|
||||
"window_width": 1.23,
|
||||
"window_height": 1.61,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
}
|
||||
],
|
||||
"schema_type": "RdSAP-Schema-21.0.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": "(not tested)",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
},
|
||||
"dwelling_type": "Top-floor flat",
|
||||
"language_code": 1,
|
||||
"pressure_test": 4,
|
||||
"property_type": 2,
|
||||
"address_line_1": "83 Station Road",
|
||||
"address_line_2": "Forest Hall",
|
||||
"assessment_type": "RdSAP",
|
||||
"completion_date": "2026-05-06",
|
||||
"inspection_date": "2026-05-05",
|
||||
"extensions_count": 0,
|
||||
"measurement_type": 1,
|
||||
"sap_flat_details": {
|
||||
"level": 3,
|
||||
"top_storey": "Y",
|
||||
"storey_count": 2,
|
||||
"flat_location": 1,
|
||||
"heat_loss_corridor": 1
|
||||
},
|
||||
"total_floor_area": 64,
|
||||
"transaction_type": 8,
|
||||
"conservatory_type": 1,
|
||||
"has_draught_lobby": "true",
|
||||
"heated_room_count": 3,
|
||||
"registration_date": "2026-05-06",
|
||||
"sap_energy_source": {
|
||||
"mains_gas": "Y",
|
||||
"meter_type": 2,
|
||||
"pv_connection": 0,
|
||||
"photovoltaic_supply": {
|
||||
"none_or_no_details": {
|
||||
"percent_roof_area": 0
|
||||
}
|
||||
},
|
||||
"wind_turbines_count": 0,
|
||||
"gas_smart_meter_present": "true",
|
||||
"is_dwelling_export_capable": "false",
|
||||
"wind_turbines_terrain_type": 2,
|
||||
"electricity_smart_meter_present": "true"
|
||||
},
|
||||
"secondary_heating": {
|
||||
"description": "None",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
},
|
||||
"sap_building_parts": [
|
||||
{
|
||||
"identifier": "Main Dwelling",
|
||||
"wall_dry_lined": "N",
|
||||
"wall_thickness": 380,
|
||||
"floor_heat_loss": 3,
|
||||
"roof_construction": 1,
|
||||
"wall_construction": 4,
|
||||
"building_part_number": 1,
|
||||
"sap_floor_dimensions": [
|
||||
{
|
||||
"floor": 0,
|
||||
"room_height": {
|
||||
"value": 2.32,
|
||||
"quantity": "metres"
|
||||
},
|
||||
"total_floor_area": {
|
||||
"value": 64.26,
|
||||
"quantity": "square metres"
|
||||
},
|
||||
"party_wall_length": {
|
||||
"value": 18.15,
|
||||
"quantity": "metres"
|
||||
},
|
||||
"heat_loss_perimeter": {
|
||||
"value": 18.15,
|
||||
"quantity": "metres"
|
||||
}
|
||||
}
|
||||
],
|
||||
"wall_insulation_type": 3,
|
||||
"construction_age_band": "E",
|
||||
"party_wall_construction": 0,
|
||||
"wall_thickness_measured": "Y",
|
||||
"roof_insulation_location": 6,
|
||||
"wall_insulation_thickness": "100mm",
|
||||
"flat_roof_insulation_thickness": "75mm"
|
||||
}
|
||||
],
|
||||
"solar_water_heating": "N",
|
||||
"habitable_room_count": 3,
|
||||
"heating_cost_current": {
|
||||
"value": 623,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"insulated_door_count": 0,
|
||||
"co2_emissions_current": 2.0,
|
||||
"energy_rating_average": 60,
|
||||
"energy_rating_current": 74,
|
||||
"lighting_cost_current": {
|
||||
"value": 42,
|
||||
"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": 553,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"hot_water_cost_current": {
|
||||
"value": 207,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"mechanical_ventilation": 0,
|
||||
"percent_draughtproofed": 100,
|
||||
"suggested_improvements": [
|
||||
{
|
||||
"sequence": 1,
|
||||
"typical_saving": {
|
||||
"value": 69,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"indicative_cost": "\u00a3900 - \u00a31,200",
|
||||
"improvement_type": "A2",
|
||||
"improvement_details": {
|
||||
"improvement_number": 45
|
||||
},
|
||||
"improvement_category": 5,
|
||||
"energy_performance_rating": 76,
|
||||
"environmental_impact_rating": 78
|
||||
}
|
||||
],
|
||||
"co2_emissions_potential": 1.8,
|
||||
"energy_rating_potential": 76,
|
||||
"lighting_cost_potential": {
|
||||
"value": 42,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"schema_version_original": "21.0.1",
|
||||
"hot_water_cost_potential": {
|
||||
"value": 208,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"renewable_heat_incentive": {
|
||||
"water_heating": 2384.75,
|
||||
"space_heating_existing_dwelling": 5681.26
|
||||
},
|
||||
"draughtproofed_door_count": 0,
|
||||
"energy_consumption_current": 170,
|
||||
"has_fixed_air_conditioning": "false",
|
||||
"multiple_glazed_proportion": 100,
|
||||
"calculation_software_version": "5.02r0344",
|
||||
"energy_consumption_potential": 154,
|
||||
"environmental_impact_current": 76,
|
||||
"current_energy_efficiency_band": "C",
|
||||
"environmental_impact_potential": 78,
|
||||
"led_fixed_lighting_bulbs_count": 9,
|
||||
"has_heated_separate_conservatory": "false",
|
||||
"potential_energy_efficiency_band": "C",
|
||||
"co2_emissions_current_per_floor_area": 31,
|
||||
"incandescent_fixed_lighting_bulbs_count": 0
|
||||
}
|
||||
|
|
@ -0,0 +1,315 @@
|
|||
{
|
||||
"uprn": 74061136,
|
||||
"roofs": [
|
||||
{
|
||||
"description": "(another dwelling above)",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
}
|
||||
],
|
||||
"walls": [
|
||||
{
|
||||
"description": "Solid brick, with internal insulation",
|
||||
"energy_efficiency_rating": 4,
|
||||
"environmental_efficiency_rating": 4
|
||||
},
|
||||
{
|
||||
"description": "Timber frame, with additional insulation",
|
||||
"energy_efficiency_rating": 4,
|
||||
"environmental_efficiency_rating": 4
|
||||
}
|
||||
],
|
||||
"floors": [
|
||||
{
|
||||
"description": "(other premises below)",
|
||||
"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
|
||||
},
|
||||
"lighting": {
|
||||
"description": "Excellent lighting efficiency",
|
||||
"energy_efficiency_rating": 5,
|
||||
"environmental_efficiency_rating": 5
|
||||
},
|
||||
"postcode": "S41 7TP",
|
||||
"hot_water": {
|
||||
"description": "Electric instantaneous at point of use",
|
||||
"energy_efficiency_rating": 1,
|
||||
"environmental_efficiency_rating": 5
|
||||
},
|
||||
"post_town": "Chesterfield,",
|
||||
"psv_count": 0,
|
||||
"built_form": 4,
|
||||
"created_at": "2026-05-20 13:45:13",
|
||||
"door_count": 1,
|
||||
"region_code": 6,
|
||||
"report_type": 2,
|
||||
"sap_heating": {
|
||||
"number_baths": 1,
|
||||
"cylinder_size": 1,
|
||||
"shower_outlets": [
|
||||
{
|
||||
"shower_wwhrs": 1,
|
||||
"shower_outlet_type": 1
|
||||
}
|
||||
],
|
||||
"number_baths_wwhrs": 0,
|
||||
"water_heating_code": 909,
|
||||
"water_heating_fuel": 29,
|
||||
"cylinder_thermostat": "N",
|
||||
"main_heating_details": [
|
||||
{
|
||||
"has_fghrs": "N",
|
||||
"main_fuel_type": 29,
|
||||
"storage_heaters": [
|
||||
{
|
||||
"index_number": 230002,
|
||||
"number_of_heaters": 1,
|
||||
"high_heat_retention": "true"
|
||||
}
|
||||
],
|
||||
"fan_flue_present": "Y",
|
||||
"heat_emitter_type": 0,
|
||||
"emitter_temperature": "NA",
|
||||
"main_heating_number": 1,
|
||||
"main_heating_control": 2404,
|
||||
"main_heating_category": 7,
|
||||
"main_heating_fraction": 50,
|
||||
"sap_main_heating_code": 409,
|
||||
"main_heating_data_source": 2
|
||||
}
|
||||
],
|
||||
"immersion_heating_type": "NA",
|
||||
"has_fixed_air_conditioning": "false"
|
||||
},
|
||||
"sap_version": 10.2,
|
||||
"sap_windows": [
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 5,
|
||||
"window_type": 1,
|
||||
"glazing_type": 2,
|
||||
"window_width": {
|
||||
"value": 1,
|
||||
"quantity": "m"
|
||||
},
|
||||
"window_height": {
|
||||
"value": 1.6,
|
||||
"quantity": "m"
|
||||
},
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"pvc_frame": "true",
|
||||
"orientation": 5,
|
||||
"window_type": 1,
|
||||
"glazing_type": 2,
|
||||
"window_width": {
|
||||
"value": 1,
|
||||
"quantity": "m"
|
||||
},
|
||||
"window_height": {
|
||||
"value": 1.6,
|
||||
"quantity": "m"
|
||||
},
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
}
|
||||
],
|
||||
"schema_type": "RdSAP-Schema-21.0.1",
|
||||
"uprn_source": "Address Matched",
|
||||
"country_code": "ENG",
|
||||
"main_heating": [
|
||||
{
|
||||
"description": "Electric storage heaters",
|
||||
"energy_efficiency_rating": 4,
|
||||
"environmental_efficiency_rating": 5
|
||||
}
|
||||
],
|
||||
"air_tightness": {
|
||||
"description": "(not tested)",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
},
|
||||
"dwelling_type": "Mid-floor flat",
|
||||
"language_code": 1,
|
||||
"pressure_test": 4,
|
||||
"property_type": 2,
|
||||
"address_line_1": "Flat 3,",
|
||||
"address_line_2": "14-20 Corporation Street,",
|
||||
"assessment_type": "RdSAP",
|
||||
"completion_date": "2026-05-20",
|
||||
"inspection_date": "2026-05-20",
|
||||
"extensions_count": 0,
|
||||
"measurement_type": 1,
|
||||
"open_flues_count": 0,
|
||||
"sap_flat_details": {
|
||||
"level": 2,
|
||||
"top_storey": "N",
|
||||
"storey_count": 3,
|
||||
"flat_location": 1,
|
||||
"heat_loss_corridor": 2,
|
||||
"unheated_corridor_length": 6
|
||||
},
|
||||
"total_floor_area": 24,
|
||||
"transaction_type": 1,
|
||||
"conservatory_type": 2,
|
||||
"heated_room_count": 1,
|
||||
"other_flues_count": 0,
|
||||
"registration_date": "2026-05-20",
|
||||
"sap_energy_source": {
|
||||
"mains_gas": "N",
|
||||
"meter_type": 3,
|
||||
"pv_connection": 0,
|
||||
"photovoltaic_supply": {
|
||||
"none_or_no_details": {
|
||||
"percent_roof_area": 0
|
||||
}
|
||||
},
|
||||
"wind_turbines_count": 0,
|
||||
"gas_smart_meter_present": "false",
|
||||
"is_dwelling_export_capable": "false",
|
||||
"wind_turbines_terrain_type": 2,
|
||||
"electricity_smart_meter_present": "false"
|
||||
},
|
||||
"secondary_heating": {
|
||||
"description": "Portable electric heaters (assumed)",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
},
|
||||
"closed_flues_count": 0,
|
||||
"extract_fans_count": 0,
|
||||
"sap_building_parts": [
|
||||
{
|
||||
"identifier": "Main Dwelling",
|
||||
"wall_dry_lined": "N",
|
||||
"wall_thickness": 300,
|
||||
"floor_heat_loss": 3,
|
||||
"roof_construction": 3,
|
||||
"wall_construction": 3,
|
||||
"building_part_number": 1,
|
||||
"sap_floor_dimensions": [
|
||||
{
|
||||
"floor": 0,
|
||||
"room_height": 2.4,
|
||||
"total_floor_area": 24,
|
||||
"party_wall_length": 8,
|
||||
"heat_loss_perimeter": 12
|
||||
}
|
||||
],
|
||||
"wall_insulation_type": 3,
|
||||
"construction_age_band": "B",
|
||||
"sap_alternative_wall_1": {
|
||||
"wall_area": 14.4,
|
||||
"sheltered_wall": "Y",
|
||||
"wall_dry_lined": "N",
|
||||
"wall_thickness": 150,
|
||||
"wall_construction": 5,
|
||||
"wall_insulation_type": 3,
|
||||
"wall_thickness_measured": "Y",
|
||||
"wall_insulation_thickness": "NI"
|
||||
},
|
||||
"party_wall_construction": 0,
|
||||
"wall_thickness_measured": "Y",
|
||||
"roof_insulation_location": "ND",
|
||||
"roof_insulation_thickness": "ND",
|
||||
"wall_insulation_thickness": "measured",
|
||||
"wall_insulation_thickness_measured": 25,
|
||||
"wall_insulation_thermal_conductivity": 3
|
||||
}
|
||||
],
|
||||
"boilers_flues_count": 0,
|
||||
"open_chimneys_count": 0,
|
||||
"solar_water_heating": "N",
|
||||
"habitable_room_count": 1,
|
||||
"heating_cost_current": {
|
||||
"value": 348,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"insulated_door_count": 0,
|
||||
"co2_emissions_current": 0.4,
|
||||
"energy_rating_average": 60,
|
||||
"energy_rating_current": 73,
|
||||
"lighting_cost_current": {
|
||||
"value": 23,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"main_heating_controls": [
|
||||
{
|
||||
"description": "Controls for high heat retention storage heaters",
|
||||
"energy_efficiency_rating": 4,
|
||||
"environmental_efficiency_rating": 4
|
||||
}
|
||||
],
|
||||
"blocked_chimneys_count": 0,
|
||||
"has_hot_water_cylinder": "false",
|
||||
"heating_cost_potential": {
|
||||
"value": 348,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"hot_water_cost_current": {
|
||||
"value": 344,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"mechanical_ventilation": 0,
|
||||
"percent_draughtproofed": 100,
|
||||
"schema_version_current": "LIG-21.0",
|
||||
"suggested_improvements": [
|
||||
{
|
||||
"sequence": 1,
|
||||
"typical_saving": 66,
|
||||
"indicative_cost": "\u00a3600 - \u00a31,500",
|
||||
"improvement_type": "Y",
|
||||
"improvement_details": {
|
||||
"improvement_number": 49
|
||||
},
|
||||
"improvement_category": 5,
|
||||
"energy_performance_rating": 76,
|
||||
"environmental_impact_rating": 92
|
||||
}
|
||||
],
|
||||
"co2_emissions_potential": 0.4,
|
||||
"energy_rating_potential": 76,
|
||||
"lighting_cost_potential": {
|
||||
"value": 23,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"schema_version_original": "LIG-21.0",
|
||||
"hot_water_cost_potential": {
|
||||
"value": 278,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"renewable_heat_incentive": {
|
||||
"water_heating": 1079.39,
|
||||
"space_heating_existing_dwelling": 2013.05
|
||||
},
|
||||
"draughtproofed_door_count": 1,
|
||||
"energy_consumption_current": 200,
|
||||
"has_fixed_air_conditioning": "false",
|
||||
"multiple_glazed_proportion": 100,
|
||||
"calculation_software_version": "10.2.2.0",
|
||||
"energy_consumption_potential": 187,
|
||||
"environmental_impact_current": 91,
|
||||
"cfl_fixed_lighting_bulbs_count": 0,
|
||||
"current_energy_efficiency_band": "C",
|
||||
"environmental_impact_potential": 92,
|
||||
"led_fixed_lighting_bulbs_count": 6,
|
||||
"has_heated_separate_conservatory": "false",
|
||||
"potential_energy_efficiency_band": "C",
|
||||
"co2_emissions_current_per_floor_area": 19,
|
||||
"incandescent_fixed_lighting_bulbs_count": 0
|
||||
}
|
||||
|
|
@ -0,0 +1,507 @@
|
|||
{
|
||||
"der": 3.23,
|
||||
"ter": 13.27,
|
||||
"dfee": 35.2,
|
||||
"dper": 41.21,
|
||||
"tfee": 36.7,
|
||||
"tper": 70.27,
|
||||
"uprn": 10094615101,
|
||||
"roofs": [
|
||||
{
|
||||
"description": {
|
||||
"value": "Average thermal transmittance 0.09 W/m\u00b2K",
|
||||
"language": "1"
|
||||
},
|
||||
"energy_efficiency_rating": 5,
|
||||
"environmental_efficiency_rating": 5
|
||||
}
|
||||
],
|
||||
"walls": [
|
||||
{
|
||||
"description": {
|
||||
"value": "Average thermal transmittance 0.19 W/m\u00b2K",
|
||||
"language": "1"
|
||||
},
|
||||
"energy_efficiency_rating": 5,
|
||||
"environmental_efficiency_rating": 5
|
||||
}
|
||||
],
|
||||
"floors": [
|
||||
{
|
||||
"description": {
|
||||
"value": "(other premises below)",
|
||||
"language": "1"
|
||||
},
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
}
|
||||
],
|
||||
"status": "entered",
|
||||
"tenure": "ND",
|
||||
"windows": {
|
||||
"description": {
|
||||
"value": "High performance glazing",
|
||||
"language": "1"
|
||||
},
|
||||
"energy_efficiency_rating": 5,
|
||||
"environmental_efficiency_rating": 5
|
||||
},
|
||||
"lighting": {
|
||||
"description": {
|
||||
"value": "Good lighting efficiency",
|
||||
"language": "1"
|
||||
},
|
||||
"energy_efficiency_rating": 4,
|
||||
"environmental_efficiency_rating": 4
|
||||
},
|
||||
"postcode": "TN37 7GF",
|
||||
"data_type": 2,
|
||||
"hot_water": {
|
||||
"description": {
|
||||
"value": "From main system, waste water heat recovery",
|
||||
"language": "1"
|
||||
},
|
||||
"energy_efficiency_rating": 5,
|
||||
"environmental_efficiency_rating": 5
|
||||
},
|
||||
"post_town": "ST LEONARDS-ON-SEA",
|
||||
"built_form": 1,
|
||||
"created_at": "2025-03-04 11:13:23",
|
||||
"living_area": 28.88,
|
||||
"orientation": 2,
|
||||
"region_code": 14,
|
||||
"report_type": 3,
|
||||
"sap_heating": {
|
||||
"number_baths": 1,
|
||||
"thermal_store": 1,
|
||||
"shower_outlets": [
|
||||
{
|
||||
"shower_wwhrs": 2,
|
||||
"shower_flow_rate": 8,
|
||||
"shower_outlet_type": 3
|
||||
}
|
||||
],
|
||||
"water_fuel_type": 39,
|
||||
"water_heating_code": 914,
|
||||
"instantaneous_wwhrs": {
|
||||
"wwhrs_index_number1": 80140
|
||||
},
|
||||
"main_heating_details": [
|
||||
{
|
||||
"has_fghrs": "false",
|
||||
"main_fuel_type": 39,
|
||||
"storage_heaters": [
|
||||
{
|
||||
"index_number": 230003,
|
||||
"number_of_heaters": 4,
|
||||
"high_heat_retention": "true"
|
||||
}
|
||||
],
|
||||
"main_heating_code": 409,
|
||||
"main_heating_number": 1,
|
||||
"is_condensing_boiler": "false",
|
||||
"main_heating_control": 2404,
|
||||
"main_heating_category": 7,
|
||||
"main_heating_fraction": 1,
|
||||
"main_heating_data_source": 3,
|
||||
"is_oil_pump_in_heated_space": "false",
|
||||
"is_main_heating_hetas_approved": "false"
|
||||
},
|
||||
{
|
||||
"has_fghrs": "false",
|
||||
"main_fuel_type": 39,
|
||||
"main_heating_number": 2,
|
||||
"main_heating_control": 2100,
|
||||
"main_heating_category": 4,
|
||||
"main_heating_fraction": 0,
|
||||
"main_heating_data_source": 1,
|
||||
"main_heating_index_number": 104014,
|
||||
"is_oil_pump_in_heated_space": "false",
|
||||
"is_main_heating_hetas_approved": "false"
|
||||
}
|
||||
],
|
||||
"has_hot_water_cylinder": "false",
|
||||
"has_cylinder_thermostat": "true",
|
||||
"has_fixed_air_conditioning": "false",
|
||||
"secondary_heating_category": 1,
|
||||
"is_immersion_for_summer_use": "false",
|
||||
"is_heat_pump_installed_to_mis": "false",
|
||||
"is_hot_water_separately_timed": "true",
|
||||
"main_heating_systems_interaction": 2,
|
||||
"is_heat_pump_assisted_by_immersion": "false"
|
||||
},
|
||||
"sap_version": 10.2,
|
||||
"schema_type": "SAP-Schema-19.1.0",
|
||||
"uprn_source": "Address Matched",
|
||||
"country_code": "ENG",
|
||||
"main_heating": [
|
||||
{
|
||||
"description": {
|
||||
"value": "Electric storage heaters",
|
||||
"language": "1"
|
||||
},
|
||||
"energy_efficiency_rating": 3,
|
||||
"environmental_efficiency_rating": 5
|
||||
}
|
||||
],
|
||||
"sap_lighting": [
|
||||
[
|
||||
{
|
||||
"lighting_power": 10,
|
||||
"lighting_outlets": 18,
|
||||
"lighting_efficacy": 90
|
||||
}
|
||||
]
|
||||
],
|
||||
"terrain_type": 2,
|
||||
"air_tightness": {
|
||||
"description": {
|
||||
"value": "Air permeability [AP50] = 4.7 m\u00b3/h.m\u00b2 (as tested)",
|
||||
"language": "1"
|
||||
},
|
||||
"energy_efficiency_rating": 4,
|
||||
"environmental_efficiency_rating": 4
|
||||
},
|
||||
"dwelling_type": "Top-floor flat",
|
||||
"language_code": 1,
|
||||
"property_type": 2,
|
||||
"pv_connection": 2,
|
||||
"address_line_1": "7 Tyler House",
|
||||
"address_line_2": "51 Potter Street",
|
||||
"assessment_date": "2025-03-04",
|
||||
"assessment_type": "SAP",
|
||||
"completion_date": "2025-03-04",
|
||||
"inspection_date": "2025-03-04",
|
||||
"sap_ventilation": {
|
||||
"psv_count": 0,
|
||||
"wall_type": 1,
|
||||
"pressure_test": 1,
|
||||
"wet_rooms_count": 1,
|
||||
"air_permeability": 4.65,
|
||||
"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": 1,
|
||||
"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": 500298,
|
||||
"mechanical_vent_duct_insulation_level": 2,
|
||||
"is_mechanical_vent_approved_installer_scheme": "false"
|
||||
},
|
||||
"design_water_use": 1,
|
||||
"sap_data_version": 10.2,
|
||||
"sap_flat_details": {
|
||||
"level": 3,
|
||||
"storeys": 4
|
||||
},
|
||||
"total_floor_area": 76,
|
||||
"transaction_type": 6,
|
||||
"cold_water_source": 1,
|
||||
"conservatory_type": 1,
|
||||
"registration_date": "2025-03-04",
|
||||
"sap_energy_source": {
|
||||
"pv_arrays": [
|
||||
{
|
||||
"pitch": 1,
|
||||
"peak_power": 1.26,
|
||||
"orientation": 8,
|
||||
"overshading": 1
|
||||
}
|
||||
],
|
||||
"electricity_tariff": 2
|
||||
},
|
||||
"sap_opening_types": [
|
||||
{
|
||||
"name": "Window",
|
||||
"type": 4,
|
||||
"u_value": 1.2,
|
||||
"data_source": 2,
|
||||
"frame_factor": 0.7,
|
||||
"glazing_type": 7,
|
||||
"isargonfilled": "false",
|
||||
"solar_transmittance": 0.52
|
||||
},
|
||||
{
|
||||
"name": "Apt door",
|
||||
"type": 1,
|
||||
"u_value": 0.71,
|
||||
"data_source": 2,
|
||||
"glazing_type": 1,
|
||||
"isargonfilled": "false"
|
||||
}
|
||||
],
|
||||
"secondary_heating": {
|
||||
"description": {
|
||||
"value": "None",
|
||||
"language": "1"
|
||||
},
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
},
|
||||
"lowest_storey_area": 75.94,
|
||||
"lzc_energy_sources": [
|
||||
11
|
||||
],
|
||||
"sap_building_parts": [
|
||||
{
|
||||
"sap_roofs": [
|
||||
{
|
||||
"name": "Roof (1)",
|
||||
"u_value": 0.09,
|
||||
"roof_type": 2,
|
||||
"total_roof_area": 75.94
|
||||
},
|
||||
{
|
||||
"name": "Party roof 1",
|
||||
"u_value": 0,
|
||||
"roof_type": 4,
|
||||
"total_roof_area": 20
|
||||
}
|
||||
],
|
||||
"sap_walls": [
|
||||
{
|
||||
"name": "Walls (1)",
|
||||
"u_value": 0.21,
|
||||
"wall_type": 2,
|
||||
"total_wall_area": 61.26,
|
||||
"is_curtain_walling": "false"
|
||||
},
|
||||
{
|
||||
"name": "Walls (2)",
|
||||
"u_value": 0.14,
|
||||
"wall_type": 2,
|
||||
"total_wall_area": 24.82,
|
||||
"is_curtain_walling": "false"
|
||||
}
|
||||
],
|
||||
"sap_openings": [
|
||||
{
|
||||
"name": "Opening 1",
|
||||
"type": "Window",
|
||||
"width": 8.28,
|
||||
"height": 1,
|
||||
"location": "Walls (1)",
|
||||
"orientation": 2
|
||||
},
|
||||
{
|
||||
"name": "Opening 2",
|
||||
"type": "Window",
|
||||
"width": 3.2,
|
||||
"height": 1,
|
||||
"location": "Walls (1)",
|
||||
"orientation": 4
|
||||
},
|
||||
{
|
||||
"name": "Opening 4",
|
||||
"type": "Apt door",
|
||||
"width": 2.12,
|
||||
"height": 1,
|
||||
"location": "Walls (2)",
|
||||
"orientation": 5
|
||||
},
|
||||
{
|
||||
"name": "Opening 3",
|
||||
"type": "Window",
|
||||
"width": 2.56,
|
||||
"height": 1,
|
||||
"location": "Walls (1)",
|
||||
"orientation": 8
|
||||
}
|
||||
],
|
||||
"construction_year": 2023,
|
||||
"sap_thermal_bridges": {
|
||||
"thermal_bridges": [
|
||||
{
|
||||
"length": 10.1,
|
||||
"psi_value": 0.08,
|
||||
"psi_value_source": 1,
|
||||
"thermal_bridge_type": "E2"
|
||||
},
|
||||
{
|
||||
"length": 5.91,
|
||||
"psi_value": 0.063,
|
||||
"psi_value_source": 1,
|
||||
"thermal_bridge_type": "E3"
|
||||
},
|
||||
{
|
||||
"length": 24.94,
|
||||
"psi_value": 0.06,
|
||||
"psi_value_source": 1,
|
||||
"thermal_bridge_type": "E4"
|
||||
},
|
||||
{
|
||||
"length": 0,
|
||||
"psi_value": 0.107,
|
||||
"psi_value_source": 1,
|
||||
"thermal_bridge_type": "E5"
|
||||
},
|
||||
{
|
||||
"length": 0,
|
||||
"psi_value": 0.076,
|
||||
"psi_value_source": 1,
|
||||
"thermal_bridge_type": "E5"
|
||||
},
|
||||
{
|
||||
"length": 25.74,
|
||||
"psi_value": 0.037,
|
||||
"psi_value_source": 1,
|
||||
"thermal_bridge_type": "E7"
|
||||
},
|
||||
{
|
||||
"length": 10.43,
|
||||
"psi_value": 0.026,
|
||||
"psi_value_source": 1,
|
||||
"thermal_bridge_type": "E7"
|
||||
},
|
||||
{
|
||||
"length": 11.9,
|
||||
"psi_value": 0.043,
|
||||
"psi_value_source": 1,
|
||||
"thermal_bridge_type": "E16"
|
||||
},
|
||||
{
|
||||
"length": 0,
|
||||
"psi_value": 0.12,
|
||||
"psi_value_source": 1,
|
||||
"thermal_bridge_type": "E25"
|
||||
},
|
||||
{
|
||||
"length": 0,
|
||||
"psi_value": 0.045,
|
||||
"psi_value_source": 1,
|
||||
"thermal_bridge_type": "P1"
|
||||
},
|
||||
{
|
||||
"length": 2.38,
|
||||
"psi_value": -0.044,
|
||||
"psi_value_source": 1,
|
||||
"thermal_bridge_type": "E17"
|
||||
},
|
||||
{
|
||||
"length": 15.34,
|
||||
"psi_value": 0.064,
|
||||
"psi_value_source": 1,
|
||||
"thermal_bridge_type": "E10"
|
||||
},
|
||||
{
|
||||
"length": 10.4,
|
||||
"psi_value": 0.053,
|
||||
"psi_value_source": 1,
|
||||
"thermal_bridge_type": "E12"
|
||||
},
|
||||
{
|
||||
"length": 10.43,
|
||||
"psi_value": 0.038,
|
||||
"psi_value_source": 1,
|
||||
"thermal_bridge_type": "E12"
|
||||
},
|
||||
{
|
||||
"length": 0,
|
||||
"psi_value": 0.05,
|
||||
"psi_value_source": 1,
|
||||
"thermal_bridge_type": "E18"
|
||||
},
|
||||
{
|
||||
"length": 0,
|
||||
"psi_value": 0.15,
|
||||
"psi_value_source": 4,
|
||||
"thermal_bridge_type": "E11"
|
||||
},
|
||||
{
|
||||
"length": 0,
|
||||
"psi_value": 0.012,
|
||||
"psi_value_source": 1,
|
||||
"thermal_bridge_type": "E13"
|
||||
},
|
||||
{
|
||||
"length": 0,
|
||||
"psi_value": 0.24,
|
||||
"psi_value_source": 1,
|
||||
"thermal_bridge_type": "P5"
|
||||
}
|
||||
],
|
||||
"thermal_bridge_code": 5
|
||||
},
|
||||
"building_part_number": 1,
|
||||
"sap_floor_dimensions": [
|
||||
{
|
||||
"storey": 1,
|
||||
"u_value": 0,
|
||||
"floor_type": 4,
|
||||
"storey_height": 2.38,
|
||||
"heat_loss_area": 0,
|
||||
"total_floor_area": 75.94
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"user_interface_name": "Design SAP 10",
|
||||
"windows_overshading": 2,
|
||||
"heating_cost_current": {
|
||||
"value": 269,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"co2_emissions_current": 0.2,
|
||||
"energy_rating_average": 60,
|
||||
"energy_rating_current": 92,
|
||||
"lighting_cost_current": {
|
||||
"value": 51,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"main_heating_controls": [
|
||||
{
|
||||
"description": {
|
||||
"value": "Controls for high heat retention storage heaters",
|
||||
"language": "1"
|
||||
},
|
||||
"energy_efficiency_rating": 4,
|
||||
"environmental_efficiency_rating": 4
|
||||
}
|
||||
],
|
||||
"has_hot_water_cylinder": "false",
|
||||
"heating_cost_potential": {
|
||||
"value": 269,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"hot_water_cost_current": {
|
||||
"value": 94,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"thermal_mass_parameter": 64,
|
||||
"user_interface_version": "2.21.9",
|
||||
"co2_emissions_potential": 0.2,
|
||||
"energy_rating_potential": 92,
|
||||
"gas_smart_meter_present": "false",
|
||||
"lighting_cost_potential": {
|
||||
"value": 51,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"schema_version_original": "SAP-Schema-19.1.0",
|
||||
"hot_water_cost_potential": {
|
||||
"value": 94,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"is_in_smoke_control_area": "unknown",
|
||||
"seller_commission_report": "Y",
|
||||
"energy_consumption_current": 35,
|
||||
"has_fixed_air_conditioning": "false",
|
||||
"is_dwelling_export_capable": "true",
|
||||
"multiple_glazed_percentage": 100,
|
||||
"calculation_software_version": "2.21.9",
|
||||
"energy_consumption_potential": 35,
|
||||
"environmental_impact_current": 97,
|
||||
"current_energy_efficiency_band": "A",
|
||||
"environmental_impact_potential": 97,
|
||||
"electricity_smart_meter_present": "true",
|
||||
"has_heated_separate_conservatory": "false",
|
||||
"potential_energy_efficiency_band": "A",
|
||||
"co2_emissions_current_per_floor_area": 2.6
|
||||
}
|
||||
|
|
@ -986,7 +986,15 @@ class EpcPropertyDataMapper:
|
|||
pv_battery_count=0,
|
||||
wind_turbines_count=schema.sap_energy_source.wind_turbines_count or 0,
|
||||
gas_smart_meter_present=False,
|
||||
is_dwelling_export_capable=False,
|
||||
# Top-level full-SAP export flag (SAP 10-era lodgements; None
|
||||
# on the SAP 2012 family → False, unchanged). Appendix M1 zeroes
|
||||
# the PV export credit without the flag, under-rating any
|
||||
# export-capable PV cert (PRD #1435 WS3). `pv_connection` is NOT
|
||||
# carried — the full-SAP enum disagrees with the RdSAP one the
|
||||
# calculator's connection gate reads (see SapSchema17_1).
|
||||
is_dwelling_export_capable=(
|
||||
schema.is_dwelling_export_capable == "true"
|
||||
),
|
||||
wind_turbines_terrain_type=str(
|
||||
schema.sap_energy_source.wind_turbine_terrain_type or ""
|
||||
),
|
||||
|
|
@ -4415,28 +4423,47 @@ _API_FLOOR_CONSTRUCTION_TO_STR: Dict[int, Optional[str]] = {
|
|||
# enum (1=Flat, 3=Pitched no-access, 5=Vaulted, etc.) is mapped as
|
||||
# best-effort against SAP10 nomenclature.
|
||||
#
|
||||
# Codes 6 and 7 → None. This field is read ONLY for the sloping-ceiling
|
||||
# inclined factor; the base roof U-value comes from the global
|
||||
# roofs[].description, so a non-sloping code carries no information the
|
||||
# cascade consumes here, and None correctly avoids the cos(30°) false-
|
||||
# trigger:
|
||||
# Code 6 → None. This field is read ONLY for the sloping-ceiling inclined
|
||||
# factor; the base roof U-value comes from the global roofs[].description,
|
||||
# so a non-sloping code carries no information the cascade consumes here:
|
||||
# 6 = "Thatched, with additional insulation" — its U is set by the
|
||||
# global description; not a sloping ceiling.
|
||||
# 7 = "(same dwelling above)" / "(another dwelling above)" — an
|
||||
# internal ceiling with no roof heat loss (the roof-side analogue
|
||||
# of floor_construction code 0). Heat loss is governed by the
|
||||
# roof_heat_loss / description path, not this field.
|
||||
# Empirically inert: roof W/K is identical whether 6/7 map to None or to
|
||||
# an explicit pitched string across all code-6/7 certs in the 2026
|
||||
# sample (were raising UnmappedApiCode, blocking the cert).
|
||||
# global description; not a sloping ceiling. None correctly avoids
|
||||
# the cos(30°) false-trigger.
|
||||
#
|
||||
# Codes 7 and 9 → "(another dwelling above)", NOT None. RdSAP 10
|
||||
# Specification (10-06-2025) §5.2.5 (p.31): "If a dwelling or part of a
|
||||
# dwelling has commercial premises above record as another dwelling
|
||||
# above" — i.e. premises-above (code 9) gets the SAME treatment as
|
||||
# dwelling-above (code 7), not the semi-exposed "partially heated space"
|
||||
# treatment used for premises *below* (see `_API_FLOOR_HEAT_LOSS_ABOVE_
|
||||
# PARTIALLY_HEATED`) — the spec's above/below split is asymmetric. Per
|
||||
# spec p.~55: "There is no heat loss through the roof of a building part
|
||||
# that has the same dwelling above or another dwelling above" — zero
|
||||
# heat loss, mirrored by the landlord-override system's independent
|
||||
# `roof_party_ceiling_guard` (`domain/epc/property_overrides/`), which
|
||||
# already resolves this exact "(another premises above)" marker to a
|
||||
# ~0-heat-loss RoofType citing the same spec line.
|
||||
#
|
||||
# `heat_transmission.py`'s per-part suppression (`part_roof_is_party =
|
||||
# "another dwelling above" in roof_type`) only fires when this field
|
||||
# CONTAINS that literal substring — it existed already for the Elmhurst
|
||||
# path (which sets it via `_strip_code(roof.roof_type)`) but the API path
|
||||
# previously mapped 7 (and, before this fix, 9) to None, so the
|
||||
# suppression never fired for ANY API-derived cert: `has_exposed_roof`
|
||||
# (the dwelling_type-label-only fallback) was the sole gate, and it has
|
||||
# no way to know about a per-part party-ceiling lodgement. Surfaced by
|
||||
# live cert uprn 10013320122 ("44 Regent House", code 9); code 7 has the
|
||||
# same latent gap (11 building parts in the RdSAP-21.0.1 corpus), fixed
|
||||
# here too rather than left half-fixed.
|
||||
_API_ROOF_CONSTRUCTION_TO_STR: Dict[int, Optional[str]] = {
|
||||
1: "Flat",
|
||||
3: "Pitched (slates/tiles), no access to loft",
|
||||
4: "Pitched (slates/tiles), access to loft",
|
||||
5: "Pitched (vaulted ceiling)",
|
||||
6: None,
|
||||
7: None,
|
||||
7: "(another dwelling above)",
|
||||
8: "Pitched, sloping ceiling",
|
||||
9: "(another dwelling above)",
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1339,18 +1339,18 @@ class TestDefaultMissingPostTown:
|
|||
|
||||
class TestApiRoofConstructionCode:
|
||||
"""`_api_roof_construction_str` maps the GOV.UK API integer
|
||||
roof_construction code to the string the cascade reads ONLY for the
|
||||
"sloping ceiling" cos(30°) inclined-surface factor (Slice 89). Codes
|
||||
6 and 7 are neither sloping ceilings nor base-U drivers (the roof
|
||||
U-value comes from the global roofs[].description), so both map to
|
||||
None: code 6 = "Thatched" (its U is set by the description, not this
|
||||
field) and code 7 = "(same/another dwelling above)" — an internal
|
||||
ceiling with no roof heat loss, the roof-side analogue of
|
||||
floor_construction code 0. Empirically inert: roof W/K is identical
|
||||
whether 6/7 map to None or to an explicit pitched string across all
|
||||
code-6/7 certs in the 2026 sample."""
|
||||
roof_construction code to the string `heat_transmission.py` reads for
|
||||
two purposes: the "sloping ceiling" cos(30°) inclined-surface factor
|
||||
(Slice 89), and — for codes 7/9 — the "another dwelling above"
|
||||
substring that suppresses that building part's roof entirely (zero
|
||||
heat loss). Code 6 = "Thatched" is neither: its U-value comes from
|
||||
the global roofs[].description, not this field, so it maps to None
|
||||
(avoids the cos(30°) false-trigger). Codes 7 and 9 both map to
|
||||
"(another dwelling above)" per RdSAP 10 Specification (10-06-2025)
|
||||
§5.2.5: commercial premises above (code 9) is recorded identically to
|
||||
another dwelling above (code 7) — both zero heat loss."""
|
||||
|
||||
def test_code_7_same_dwelling_above_maps_to_none(self) -> None:
|
||||
def test_code_7_same_dwelling_above_suppresses_roof_heat_loss(self) -> None:
|
||||
# Arrange
|
||||
from datatypes.epc.domain.mapper import (
|
||||
_api_roof_construction_str,
|
||||
|
|
@ -1359,9 +1359,22 @@ class TestApiRoofConstructionCode:
|
|||
# Act
|
||||
result = _api_roof_construction_str(7)
|
||||
|
||||
# Assert — None: no sloping-ceiling signal (avoids the cos(30°)
|
||||
# false-trigger); the internal ceiling has no roof heat loss.
|
||||
assert result is None
|
||||
# Assert — the party-ceiling marker, so heat_transmission.py's
|
||||
# per-part "another dwelling above" suppression fires.
|
||||
assert result == "(another dwelling above)"
|
||||
|
||||
def test_code_9_another_premises_above_suppresses_roof_heat_loss(self) -> None:
|
||||
# Arrange — RdSAP 10 Spec §5.2.5: commercial premises above is
|
||||
# recorded as "another dwelling above", same as code 7.
|
||||
from datatypes.epc.domain.mapper import (
|
||||
_api_roof_construction_str,
|
||||
) # pyright: ignore[reportPrivateUsage]
|
||||
|
||||
# Act
|
||||
result = _api_roof_construction_str(9)
|
||||
|
||||
# Assert
|
||||
assert result == "(another dwelling above)"
|
||||
|
||||
def test_code_6_thatched_maps_to_none(self) -> None:
|
||||
# Arrange
|
||||
|
|
|
|||
|
|
@ -44,7 +44,9 @@ class SapHeating:
|
|||
instantaneous_wwhrs: Optional[InstantaneousWwhrs]
|
||||
main_heating_details: List[MainHeatingDetail]
|
||||
immersion_heating_type: Union[int, str]
|
||||
has_fixed_air_conditioning: str
|
||||
# Nested duplicate of the outer schema's has_fixed_air_conditioning
|
||||
# (already Optional below) — some certs omit it here too.
|
||||
has_fixed_air_conditioning: Optional[str] = None
|
||||
# ADR-0028: cylinder_insulation_type is absent in 308/1000 17.0 certs.
|
||||
cylinder_insulation_type: Optional[int] = None
|
||||
cylinder_thermostat: Optional[str] = None
|
||||
|
|
|
|||
|
|
@ -44,7 +44,9 @@ class SapHeating:
|
|||
instantaneous_wwhrs: Optional[InstantaneousWwhrs]
|
||||
main_heating_details: List[MainHeatingDetail]
|
||||
immersion_heating_type: Union[int, str]
|
||||
has_fixed_air_conditioning: str
|
||||
# Nested duplicate of the outer schema's has_fixed_air_conditioning
|
||||
# (also made Optional below) — some certs omit it here too.
|
||||
has_fixed_air_conditioning: Optional[str] = None
|
||||
# ADR-0028: 325/1000 omit cylinder_insulation_type — default it.
|
||||
cylinder_insulation_type: Optional[int] = None
|
||||
cylinder_thermostat: Optional[str] = None
|
||||
|
|
@ -247,7 +249,7 @@ class RdSapSchema17_1:
|
|||
hot_water_cost_potential: CostAmount
|
||||
renewable_heat_incentive: RenewableHeatIncentive
|
||||
energy_consumption_current: int
|
||||
has_fixed_air_conditioning: str
|
||||
has_fixed_air_conditioning: Optional[str] = None
|
||||
multiple_glazed_proportion: int
|
||||
calculation_software_version: str
|
||||
energy_consumption_potential: int
|
||||
|
|
|
|||
|
|
@ -44,7 +44,9 @@ class SapHeating:
|
|||
instantaneous_wwhrs: Optional[InstantaneousWwhrs]
|
||||
main_heating_details: List[MainHeatingDetail]
|
||||
immersion_heating_type: Union[int, str]
|
||||
has_fixed_air_conditioning: str
|
||||
# Nested duplicate of the outer schema's has_fixed_air_conditioning
|
||||
# (also made Optional below) — some certs omit it here too.
|
||||
has_fixed_air_conditioning: Optional[str] = None
|
||||
cylinder_insulation_type: Optional[int] = None
|
||||
cylinder_thermostat: Optional[str] = None
|
||||
secondary_fuel_type: Optional[int] = None
|
||||
|
|
@ -266,7 +268,7 @@ class RdSapSchema18_0:
|
|||
hot_water_cost_potential: CostAmount
|
||||
renewable_heat_incentive: RenewableHeatIncentive
|
||||
energy_consumption_current: int
|
||||
has_fixed_air_conditioning: str
|
||||
has_fixed_air_conditioning: Optional[str] = None
|
||||
multiple_glazed_proportion: int
|
||||
calculation_software_version: str
|
||||
energy_consumption_potential: int
|
||||
|
|
|
|||
|
|
@ -44,7 +44,9 @@ class SapHeating:
|
|||
instantaneous_wwhrs: Optional[InstantaneousWwhrs]
|
||||
main_heating_details: List[MainHeatingDetail]
|
||||
immersion_heating_type: Union[int, str]
|
||||
has_fixed_air_conditioning: str
|
||||
# Nested duplicate of the outer schema's has_fixed_air_conditioning
|
||||
# (also made Optional below) — some certs omit it here too.
|
||||
has_fixed_air_conditioning: Optional[str] = None
|
||||
cylinder_insulation_type: Optional[int] = None
|
||||
cylinder_thermostat: Optional[str] = None
|
||||
secondary_fuel_type: Optional[int] = None
|
||||
|
|
@ -281,7 +283,7 @@ class RdSapSchema19_0:
|
|||
# 19.0-specific block, absent in 713/1000 — Optional + default.
|
||||
windows_transmission_details: Optional[WindowsTransmissionDetails] = None
|
||||
energy_consumption_current: int
|
||||
has_fixed_air_conditioning: str
|
||||
has_fixed_air_conditioning: Optional[str] = None
|
||||
multiple_glazed_proportion: int
|
||||
calculation_software_version: str
|
||||
energy_consumption_potential: int
|
||||
|
|
|
|||
|
|
@ -56,7 +56,9 @@ class SapHeating:
|
|||
instantaneous_wwhrs: Optional[InstantaneousWwhrs]
|
||||
main_heating_details: List[MainHeatingDetail]
|
||||
immersion_heating_type: Union[int, str]
|
||||
has_fixed_air_conditioning: str
|
||||
# Nested duplicate of the outer schema's has_fixed_air_conditioning
|
||||
# (already Optional below) — some certs omit it here too.
|
||||
has_fixed_air_conditioning: Optional[str] = None
|
||||
cylinder_insulation_type: Optional[int] = None
|
||||
cylinder_thermostat: Optional[str] = None
|
||||
secondary_fuel_type: Optional[int] = None
|
||||
|
|
|
|||
|
|
@ -63,7 +63,9 @@ class SapHeating:
|
|||
water_heating_fuel: int
|
||||
main_heating_details: List[MainHeatingDetail]
|
||||
immersion_heating_type: Union[int, str]
|
||||
has_fixed_air_conditioning: str
|
||||
# Nested duplicate of the outer schema's has_fixed_air_conditioning
|
||||
# (also made Optional below) — some certs omit it here too.
|
||||
has_fixed_air_conditioning: Optional[str] = None
|
||||
instantaneous_wwhrs: Optional[InstantaneousWwhrs] = None
|
||||
# Real-API certs carry shower_outlets as a list, not the synthetic
|
||||
# single-object form; list elements are normalised to the wrapped
|
||||
|
|
@ -459,7 +461,7 @@ class RdSapSchema21_0_0:
|
|||
windows_transmission_details: Optional[WindowsTransmissionDetails] = None
|
||||
cfl_fixed_lighting_bulbs_count: Optional[int] = None
|
||||
energy_consumption_current: int
|
||||
has_fixed_air_conditioning: str
|
||||
has_fixed_air_conditioning: Optional[str] = None
|
||||
multiple_glazed_proportion: int
|
||||
calculation_software_version: str
|
||||
energy_consumption_potential: int
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from .common import DescriptionV1, Measurement
|
||||
|
|
@ -64,7 +64,9 @@ class SapHeating:
|
|||
water_heating_fuel: int
|
||||
main_heating_details: List[MainHeatingDetail]
|
||||
immersion_heating_type: Union[int, str]
|
||||
has_fixed_air_conditioning: str
|
||||
# Nested duplicate of the outer schema's has_fixed_air_conditioning
|
||||
# (also made Optional below) — some certs omit it here too.
|
||||
has_fixed_air_conditioning: Optional[str] = None
|
||||
instantaneous_wwhrs: Optional[InstantaneousWwhrs] = None
|
||||
# Real-API certs carry shower_outlets as a list, not the synthetic single-object form;
|
||||
# accept both shapes so older fixtures keep parsing. List elements
|
||||
|
|
@ -510,7 +512,9 @@ class RdSapSchema21_0_1:
|
|||
renewable_heat_incentive: RenewableHeatIncentive
|
||||
draughtproofed_door_count: int
|
||||
energy_consumption_current: int
|
||||
has_fixed_air_conditioning: str
|
||||
# kw_only on just this field: the class isn't kw_only overall, and several
|
||||
# required fields (calculation_software_version etc.) already follow it.
|
||||
has_fixed_air_conditioning: Optional[str] = field(default=None, kw_only=True)
|
||||
calculation_software_version: str
|
||||
energy_consumption_potential: int
|
||||
environmental_impact_current: int
|
||||
|
|
|
|||
|
|
@ -247,3 +247,13 @@ class SapSchema17_1:
|
|||
has_hot_water_cylinder: Optional[str] = None
|
||||
# Present for flat-type dwellings; absence means not a flat.
|
||||
sap_flat_details: Optional[SapFlatDetails] = None
|
||||
# Top-level PV export-capability flag (lodged by the SAP 10-era full-SAP
|
||||
# schemas; absent on the SAP 2012 family, where SEG export did not exist).
|
||||
# SAP 10.2 Appendix M1 zeroes the PV export credit unless the dwelling is
|
||||
# connected to an export-capable meter, so dropping the flag under-rates
|
||||
# any export-capable PV cert (~3 SAP on cert 0380-3044-6070-2305-5925).
|
||||
# The sibling top-level `pv_connection` is deliberately NOT parsed: the
|
||||
# full-SAP enum disagrees with the RdSAP/gov-API one the calculator gates
|
||||
# on (full-SAP certs lodge 1 with a PV credit included; RdSAP 1 = "not
|
||||
# connected" = no credit), so carrying it across would zero real arrays.
|
||||
is_dwelling_export_capable: Optional[str] = None
|
||||
|
|
|
|||
|
|
@ -0,0 +1,132 @@
|
|||
# A system-replacing heating override forces category and control for coherence, and defers fuel and meter where multiple values are valid
|
||||
|
||||
## Status
|
||||
|
||||
accepted
|
||||
|
||||
## Context
|
||||
|
||||
A **Landlord Override** naming a main heating system resolves to a SAP Table 4a
|
||||
code, and `main_heating_system_overlay` synthesises the **coherent companions**
|
||||
that code implies (ADR-0035): heating category, charge control, natural fuel,
|
||||
meter. The overlay is applied over the lodged cert by last-wins composition, so a
|
||||
companion the overlay leaves **unset (`None`)** is *inherited from the replaced
|
||||
system* — not cleared.
|
||||
|
||||
That inheritance is a silent mis-rating when the archetype genuinely determines
|
||||
the companion. A portfolio-796 audit found the case end-to-end on **86+
|
||||
properties**: a lodged **slimline storage heater** (SAP 402, category 7, control
|
||||
2401, off-peak) overridden to **"Electric room heaters"** (SAP 691). The overlay
|
||||
stamped code 691, fuel, and a Dual meter — but `_category_for(691)` and
|
||||
`_control_for(691)` both returned `None`, because their room-heater branch keyed
|
||||
on `_CATEGORY_10_ROOM_HEATER_CODES`, a set that held only the *fuel-burning*
|
||||
(gas/oil/solid) room heaters and **excluded electric 691–701** despite electric
|
||||
room heaters also being Table 4a **Category 10**.
|
||||
|
||||
Two independent mis-ratings followed from the surviving storage companions:
|
||||
|
||||
1. **Category 7 survived.** The SAP 10.2 Table 12a resolver keys on category
|
||||
first, read `OTHER_STORAGE_HEATERS`, and — with the dwelling on the §12 Rule 3
|
||||
10-hour tariff — hit `(OTHER_STORAGE_HEATERS, TEN_HOUR)`, a combination the
|
||||
Grid-1 table does not carry, so it fell to the all-low-rate fallback. The
|
||||
peaky room heaters were billed **100% at the night rate** — the precise
|
||||
"all-low over-credit" the overlay's own Dual-meter comment claims to avoid.
|
||||
Effect: baseline SAP over-rated **64.1 vs the spec-correct 56.4**; the HHRSH
|
||||
upgrade measure showed a **negative** bill saving (−£13/yr) because the
|
||||
baseline it was compared against was itself under-billed.
|
||||
2. **Control 2401 survived.** `main_heating_control` fed Table 4e as the storage
|
||||
**manual charge control → +0.7 °C** mean-internal-temperature adjustment — the
|
||||
largest in the table — riding on a direct-acting room heater, which should
|
||||
carry the Group 6 room-heater control **2601 → +0.3 °C**.
|
||||
|
||||
The companions are **not uniform** in how strongly the archetype determines
|
||||
them, which is the crux of the decision:
|
||||
|
||||
- **Category is deterministic** — SAP Table 4a assigns exactly one category per
|
||||
code (gas→2, heat pump→4, network→6, storage→7, **electric underfloor→8**,
|
||||
warm-air→9, **room heater→10**). A `None` category is never valid.
|
||||
- **Control is an assumption but always archetype-supplied** — the Table 4e
|
||||
*Group* is fixed by category; the specific conservative default within the
|
||||
group is a modelling choice (storage→manual 2401, HHRSH→2404 per
|
||||
[ADR-0044](0044-hhrsh-baseline-archetype-drags-its-intrinsic-controls.md),
|
||||
room heater→2601, gas boiler→full controls). It is never "inherit the replaced
|
||||
system".
|
||||
- **Fuel is sometimes genuinely ambiguous** — electric/gas/storage/heat-pump
|
||||
archetypes pin their fuel, but **community heating** could be gas CHP, biomass,
|
||||
or waste heat, so its fuel legitimately **defers** to a `main_fuel` override,
|
||||
else the lodged value (ADR-0041 last-wins).
|
||||
- **Meter is physics-locked for some systems, flexible for others** — storage /
|
||||
CPSU / HHRSH **require** Dual (they charge overnight); non-electric systems are
|
||||
stamped Single to stop an Economy-7 split bleeding onto a gas boiler
|
||||
([ADR-0035](0035-coherent-heating-system-synthesis.md)); screed underfloor
|
||||
defers to the cert ([ADR-0046](0046-electric-underfloor-scored-by-its-own-codes-meter-deferred-when-tariff-ambiguous.md)).
|
||||
|
||||
## Decision
|
||||
|
||||
A system-replacing heating override must emit a **complete, coherent companion
|
||||
set**. Companions split by whether the archetype *determines* the value:
|
||||
|
||||
1. **Category and control are archetype-forced** — always written, never
|
||||
inherited, because a wrong value mis-scores or mis-bills the cert. Electric
|
||||
room heaters (Table 4a 691–701) join the Category-10 room-heater branch of
|
||||
both `_category_for` (→ category **10**) and `_control_for` (→ control
|
||||
**2601**, the Group 6 conservative no-thermostat default, matching the
|
||||
gas/oil/solid siblings). Their fuel (electricity 29) and Dual meter were
|
||||
already correct and are unchanged.
|
||||
|
||||
2. **Fuel and meter defer where multiple values are coherent** — this is *not*
|
||||
a gap. Fuel defers only for genuinely ambiguous carriers (community heating);
|
||||
meter defers only for meter-flexible systems (room heaters run coherently on
|
||||
either meter; screed underfloor per ADR-0046). Meter-locked systems still
|
||||
force (storage/CPSU/HHRSH → Dual; non-electric → Single).
|
||||
|
||||
3. **An incomplete companion set is logged, not raised (for now).** Where the
|
||||
overlay cannot yet supply a confident category or control — heat pumps
|
||||
(Group 2), community (Group 3), electric underfloor (Group 7), electric
|
||||
boiler / CPSU (Group 1) — `main_heating_overlay_for` **logs an error** naming
|
||||
the archetype and code and continues, matching the `flag_fuel_mismatch`
|
||||
log-not-raise convention. Log-and-continue keeps a mid-portfolio run alive
|
||||
and surfaces the gap (CloudWatch) for review, rather than turning a latent
|
||||
inheritance bug into an outage. The intended end-state is to fill each
|
||||
archetype's category/control with spec sight and then **flip the log to a
|
||||
raise**, so the coherence invariant is enforced.
|
||||
|
||||
Category expansion for the still-`None` archetypes (electric boiler/CPSU→2,
|
||||
underfloor→8) is deterministic from Table 4a but **deferred** to its own change:
|
||||
it flows into the category-keyed Table 12a resolver and needs a corpus check, so
|
||||
it does not ride on the room-heater fix.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The 86+ room-heater dwellings score against a correctly-billed baseline: the
|
||||
space-heating high-rate fraction is the spec **0.50** (10-hour direct-acting),
|
||||
not 0.0; baseline SAP corrects **64.1 → 56.4**; the HHRSH measure's bill saving
|
||||
flips **−£13/yr → +£235/yr** (property 710788, uprn 22003310, scenario 1268).
|
||||
- The `refetch_solar`-corrected portfolio's `negative-bill-savings` audit group
|
||||
loses its ~86-property HHRSH-on-room-heater cluster after re-model.
|
||||
- The overlay now emits a visible error for any archetype it cannot fully
|
||||
companion — a standing signal (wire a CloudWatch metric filter on the log
|
||||
string) for which control default to fill next.
|
||||
- Extends [ADR-0035](0035-coherent-heating-system-synthesis.md) (companions drag
|
||||
from the code) and [ADR-0041](0041-landlord-heating-classification-targets-a-complete-modellable-taxonomy.md)
|
||||
(complete taxonomy); sits beside
|
||||
[ADR-0044](0044-hhrsh-baseline-archetype-drags-its-intrinsic-controls.md)
|
||||
(a control named by the archetype) and
|
||||
[ADR-0046](0046-electric-underfloor-scored-by-its-own-codes-meter-deferred-when-tariff-ambiguous.md)
|
||||
(meter deferred where the tariff is ambiguous).
|
||||
|
||||
### Alternatives rejected
|
||||
|
||||
- **Make every companion total (force fuel and meter too).** Rejected: it breaks
|
||||
the deliberate deferrals — community-heating fuel (ambiguous carrier) and the
|
||||
ADR-0046 screed-underfloor meter — whose `None` is a load-bearing "defer to the
|
||||
cert/override" signal, not a gap.
|
||||
- **Fix the Table 12a resolver to prefer the SAP code over a stale category.**
|
||||
A sound defence-in-depth follow-up, but it treats the symptom; the overlay
|
||||
emitting an incoherent cert is the root cause, and category/control coherence
|
||||
must hold for every consumer (predicted EPCs, raw lodged certs), not only the
|
||||
billing resolver.
|
||||
- **Raise on an incomplete companion set now.** Rejected for this change: heat
|
||||
pump / community / underfloor overrides model today (by luck of inheritance),
|
||||
so an immediate raise is a blast-radius change mid-campaign. Log now, raise once
|
||||
the common archetypes are filled.
|
||||
98
docs/adr/0049-ashp-sized-to-the-dwelling-design-heat-loss.md
Normal file
98
docs/adr/0049-ashp-sized-to-the-dwelling-design-heat-loss.md
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
# The ASHP recommendation sizes the heat pump to the dwelling's design heat loss
|
||||
|
||||
## Status
|
||||
|
||||
accepted
|
||||
|
||||
## Context
|
||||
|
||||
The ASHP measure (ADR-0024) installed **one fixed heat pump** — a 4.37 kW Vaillant
|
||||
aroTHERM plus (PCDB Table 362 record 110257) — on every eligible dwelling, as a
|
||||
"representative, contractor-installable" end-state. SAP 10.2 Appendix N reads a
|
||||
heat pump's efficiency at the dwelling's **PSR** (Power-to-Space-heating-Ratio =
|
||||
rated output / design heat loss): efficiency peaks around PSR 0.8 and, per
|
||||
footnote 44, **collapses toward 100% below the record's smallest PSR**.
|
||||
|
||||
A portfolio-796 audit found the fixed pump scoring the ASHP **below the baseline**
|
||||
on high-heat-loss dwellings. Property 712794 (uprn 10002468116): a leaky dwelling
|
||||
with a **17.5 kW design heat loss**, so a 4.37 kW pump lands at **PSR 0.25** — the
|
||||
efficiency collapse zone. The calculator correctly applied ~195% SPF; combined
|
||||
with pricing electricity at the standard tariff against an off-peak storage
|
||||
baseline, the ASHP scored **SAP 10.33 vs the baseline 11.96 (−1.63)** and the
|
||||
Optimiser never selected it. Every electric-heated dwelling too leaky for a 4.37 kW
|
||||
pump was capped below the goal band by this — a modelling defect, not a calculator
|
||||
one (SAP's PSR curve is correct: a grossly undersized heat pump *is* inefficient).
|
||||
|
||||
The fix needs the dwelling's design heat loss, which the calculator computes
|
||||
internally (annual-average HLC × the SAP design temperature difference, 24.2 K)
|
||||
but did not surface.
|
||||
|
||||
Sizing raised a second question the accredited data answered. MCS capacity sizing
|
||||
targets **PSR ≈ 1.0** (output ≥ design heat loss). But the **relodged Elmhurst ASHP
|
||||
cert** — a real dwelling our calculator gives a 6.40 kW design heat loss — was
|
||||
fitted with the **5 kW** Vaillant (110257, 4.37 kW output), i.e. **PSR 0.68**, and
|
||||
scores SAP 72.08. A PSR-1.0 rule picks the 7 kW pump for that dwelling → SAP 73.86,
|
||||
**oversizing by a rung and inflating SAP by ~1.8**, and no longer reproduces the
|
||||
accredited cert. SAP efficiency at PSR 0.68 and 1.0 is near-identical (~400%), so
|
||||
the real installer's smaller, cheaper choice is the realistic one.
|
||||
|
||||
## Decision
|
||||
|
||||
Size the heat pump to the dwelling and expose what sizing needs:
|
||||
|
||||
1. **`SapResult.design_heat_loss_kw`** — the calculator surfaces the design heat
|
||||
loss (annual-average HLC × 24.2 K / 1000), the quantity Appendix N's PSR
|
||||
divides the pump output by.
|
||||
|
||||
2. **A curated aroTHERM plus ladder** — one representative PCDB record per nominal
|
||||
size across the domestic range (`_ASHP_SIZING_LADDER`): 3.5 kW (110249), 5 kW
|
||||
(110257, the prior anchor), 7 kW (110265), 10 kW (110273), 12 kW (110281). All
|
||||
the same "& AI VIH RW" variant, so the ladder is a consistent product line.
|
||||
|
||||
3. **Size to the Appendix-N efficiency peak, not MCS capacity.**
|
||||
`select_ashp_pcdb_id` picks the rung whose rated output is nearest **0.8 ×
|
||||
design heat loss** (PSR ≈ 0.8, the efficiency peak). This reproduces the pump a
|
||||
real installer fits — validated against the relodged Elmhurst cert at delta 0 —
|
||||
and keeps the pump clear of the low-PSR collapse. A load beyond the 12 kW range
|
||||
naturally selects the largest rung (an honest, capped undersize; those
|
||||
dwellings need fabric-first).
|
||||
|
||||
4. **Cost and efficiency share one figure.** `ashp_cost_inputs` takes the same
|
||||
design heat loss (over the floor-area proxy), so the rate-sheet install band
|
||||
(ADR-0025) and the pump's efficiency record are sized from one number. The
|
||||
orchestrator reads `design_heat_loss_kw` off a baseline score and threads it
|
||||
into the heating generator; the harness computes it via a calculator pass.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Property 712794's ASHP goes from **−1.63 SAP (never selected)** to **+44.2 SAP
|
||||
(selected)**; its plan lifts **G (12.0) → B (89.1)**. Electric-heated dwellings
|
||||
previously capped below goal C by the undersized pump can now reach it — the
|
||||
largest HIGH-severity mover on the portfolio-796 `plan-stops-short-of-goal` and
|
||||
`plan-stuck-in-low-band` groups.
|
||||
- The accredited relodged Elmhurst ASHP cert still reproduces at **delta 0** (the
|
||||
6.40 kW dwelling sizes to the 5 kW Vaillant it was actually fitted with). Two
|
||||
self-snapshot cascade pins re-pin to their correctly-sized larger pumps (both
|
||||
dwellings were undersized by the fixed unit, so their end-state SAP rises).
|
||||
- Adding `design_heat_loss_kw` to `SapResult` is a required field; manual
|
||||
constructions (test stubs) carry it.
|
||||
- Extends [ADR-0024](0024-ashp-bundle-absolute-end-state.md) (the ASHP end-state)
|
||||
and [ADR-0025](0025-ashp-cost-composed-from-the-rate-sheet.md) (size-banded
|
||||
cost); the cost bands already scaled — they now receive the real heat loss.
|
||||
|
||||
### Alternatives rejected
|
||||
|
||||
- **MCS capacity sizing (PSR ≈ 1.0, output ≥ design heat loss).** Rejected: it
|
||||
oversizes relative to real installs, inflating SAP ~1.8 per dwelling and
|
||||
breaking the accredited-cert reproduction. SAP efficiency is equivalent at
|
||||
PSR 0.68–1.0, so the realistic (installer) size is preferred.
|
||||
- **Keep the fixed 4.37 kW pump.** Rejected: the collapse on high-heat-loss
|
||||
dwellings is the defect under repair.
|
||||
- **Fabric-first sequencing (score the ASHP only after insulation).** A real
|
||||
concern — the Optimiser scores each measure independently against the
|
||||
un-insulated baseline (ADR-0016) — but a separate change to measure scoring/
|
||||
sequencing, not pump sizing. Sizing the pump fixes the standalone score
|
||||
regardless; noted for follow-up.
|
||||
- **Nearest record from all ~10,000 Table 362 heat pumps.** Rejected: an opaque,
|
||||
unauditable selection; a curated single-product ladder keeps ADR-0024's
|
||||
"representative, contractor-installable" intent.
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
# An electric-underfloor override drags category 8 and the conservative Group 7 control
|
||||
|
||||
## Status
|
||||
|
||||
accepted
|
||||
|
||||
## Context
|
||||
|
||||
[ADR-0048](0048-heating-override-forces-category-and-control-defers-fuel-and-meter.md)
|
||||
established that a system-replacing heating override must emit a **complete,
|
||||
coherent companion set** — category and control are archetype-forced, never
|
||||
inherited — and explicitly deferred the category expansion for the still-`None`
|
||||
archetypes ("electric boiler/CPSU→2, underfloor→8 … flows into the
|
||||
category-keyed Table 12a resolver and needs a corpus check") to its own change.
|
||||
|
||||
The portfolio-796 audit (PRD #1435, WS2) found the underfloor half of that gap
|
||||
end-to-end on property **711795** (uprn 10090343115): a lodged **community
|
||||
heating** cert (SAP 301, category 6, control 2311) overridden to *"Electric
|
||||
underfloor, in screed above insulation"* (SAP 424,
|
||||
[ADR-0046](0046-electric-underfloor-scored-by-its-own-codes-meter-deferred-when-tariff-ambiguous.md)).
|
||||
`_category_for(424)` and `_control_for(424)` both returned `None`, so the
|
||||
effective cert kept the replaced heat network's **category 6** and community
|
||||
control. Two independent mis-ratings followed:
|
||||
|
||||
1. **No heating pathway.** `is_heat_network_main` keys on category 6, so the
|
||||
heating Recommendation Generator excluded both HHRSH and ASHP by the
|
||||
heat-network topology rule — `recommend_heating` returned `None` and the
|
||||
flat's whole candidate menu was a no-op wall measure plus lighting. The
|
||||
dwelling was unliftable from band E (the `plan-stuck-in-low-band` audit
|
||||
group counts ~7 electric-underfloor dwellings).
|
||||
2. **Baseline mis-rated.** The calculator scored the electric underfloor as
|
||||
community heating: baseline SAP **51 (E)** vs **67 (D)** with coherent
|
||||
companions. With the pathway restored, HHRSH scores **80 (C)** for ~£1k —
|
||||
the dwelling reaches the scenario goal.
|
||||
|
||||
## Decision
|
||||
|
||||
The three electric-underfloor archetypes (SAP Table 4a **421 / 422 / 424**,
|
||||
ADR-0046) join the forced-companion branches of `main_heating_system_overlay`:
|
||||
|
||||
1. **Category 8** ("Electric underfloor heating") in `_category_for` —
|
||||
deterministic from SAP Table 4a, per ADR-0048's category rule.
|
||||
2. **Control 2701** (SAP Table 4e **Group 7**, "no time or thermostatic control
|
||||
of room temperature", **+0.3 °C**) in `_control_for` — the conservative
|
||||
default within the group the category fixes: the largest mean-internal-
|
||||
temperature adjustment in Group 7, so an unobserved control is never
|
||||
over-credited. Mirrors the storage manual-charge 2401
|
||||
([ADR-0035](0035-coherent-heating-system-synthesis.md)) and room-heater 2601
|
||||
(ADR-0048) precedents.
|
||||
|
||||
Fuel (electricity 29) and meter (Dual for 421/422; **deferred to the cert** for
|
||||
tariff-ambiguous 424) are already correct per ADR-0046 and are unchanged.
|
||||
|
||||
**Scope**: underfloor only. Electric boiler / CPSU (Group 1) remain
|
||||
log-and-continue gaps tracked by #1444 — their category (2) feeds the wet-boiler
|
||||
fallback classification in the generators and needs its own check.
|
||||
|
||||
## Consequences
|
||||
|
||||
- An underfloor override on any lodged system now yields a coherent cert: the
|
||||
Table 12a resolver and the heat-network gates read the dwelling as electric
|
||||
underfloor, the baseline re-rates correctly, and the HHRSH bundle is offered
|
||||
where eligible (a flat's only heating pathway — ASHP stays house/bungalow-only
|
||||
per ADR-0024).
|
||||
- The "incomplete companion set" error log no longer fires for 421/422/424.
|
||||
- **Known deferred gap (pre-existing, unchanged by this ADR):** the Table 12a
|
||||
space-heating resolver's underfloor rows are wired in the fraction table but
|
||||
`_table_12a_system_for_main` never returns `UNDERFLOOR_HEATING` ("Underfloor
|
||||
heating (421-422) — TODO"), so an off-peak-metered underfloor dwelling falls
|
||||
to the all-low-rate fallback. That affects genuinely-lodged underfloor certs
|
||||
identically and needs spec sight (SAP 10.2 Table 12a Grid 1, PDF p.191) to
|
||||
split slab (storage-like) from screed (direct-acting-like); tracked as a
|
||||
follow-up, not ridden on this override fix.
|
||||
|
||||
### Alternatives rejected
|
||||
|
||||
- **A zero-adjustment Group 7 control (2703/2704).** Rejected: credits a room
|
||||
thermostat the landlord never reported; the conservative default is the
|
||||
ADR-0035/0048 pattern.
|
||||
- **Completing electric boiler / CPSU (category 2) in the same change.**
|
||||
Rejected: no evidence property in this workstream, and category 2 flows into
|
||||
the generators' wet-boiler fallback (`_WET_BOILER_CATEGORIES`), which would
|
||||
make an electric-boiler override newly eligible for tune-up options — a
|
||||
behaviour change needing its own corpus check (#1444).
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
# Wall insulation is offered only when it lowers the derived wall U-value
|
||||
|
||||
## Status
|
||||
|
||||
accepted
|
||||
|
||||
## Context
|
||||
|
||||
The wall Recommendation Generators trigger on the wall's **insulation state**
|
||||
alone: `recommend_cavity_wall` offers cavity fill for any cavity wall whose
|
||||
`wall_insulation_type` is as-built (4), and `recommend_solid_wall` offers
|
||||
EWI/IWI for any as-built suitable construction
|
||||
([ADR-0019](0019-solid-wall-insulation-options.md)). Neither asks whether
|
||||
adding insulation can actually **change the modelled wall**.
|
||||
|
||||
For modern construction age bands it cannot: RdSAP's Table 6-10 cascade
|
||||
(`u_wall` in `domain/sap10_ml/rdsap_uvalues.py`) assigns an as-built wall its
|
||||
age band's at-regs U-value, and the post-measure state resolves to the **same
|
||||
value**. Portfolio-796 evidence (PRD #1435, WS2): property **711795**, timber
|
||||
frame at age band **L** (2012–2022) — `u_wall` returns **0.280 W/m²K both
|
||||
as-built and with 100 mm IWI**, so the persisted plan offered
|
||||
`internal_wall_insulation` at **£4,027 for +0.00 SAP**. Any modern-age-band
|
||||
dwelling gets the same no-op fabric offer; on an unliftable dwelling it is the
|
||||
*entire* remedial menu, which reads as "we recommend spending £4k for nothing".
|
||||
|
||||
Scoring already prices the option at +0.00, but the Optimiser keeps
|
||||
zero-uplift candidates in the persisted menu (they are legitimate for some
|
||||
measure types, e.g. secondary-heating removal saves bills without SAP), so the
|
||||
fix belongs at **generation eligibility**, not a blanket score filter.
|
||||
|
||||
## Decision
|
||||
|
||||
Both wall generators consult the **derived wall U-value** before offering: the
|
||||
measure is offered only when the post-measure state's `u_wall` is **lower than
|
||||
the current state's by more than 0.01 W/m²K** (a float-noise guard, not a
|
||||
materiality threshold — economics stay with the Optimiser, ADR-0016/0024).
|
||||
|
||||
- **Current state**: `u_wall` with the effective main wall's construction, age
|
||||
band, lodged insulation type/thickness — a thin parameter mapping in the
|
||||
`envelope.py` style, deliberately omitting the calculator's rich extras
|
||||
(surveyor description, dry-lining, documentary-thickness routes). Omissions
|
||||
bias the gate toward **offering** (both sides are computed identically, so a
|
||||
no-op still compares equal), never toward suppressing a real measure.
|
||||
- **Post-measure state**: the same call with the Option's overlay applied —
|
||||
filled cavity (`wall_insulation_type=2`) for cavity fill; 100 mm
|
||||
internal/external insulation for IWI/EWI.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Age-band-L (and any at-regs age band) walls stop polluting candidate menus
|
||||
with £4k no-ops; genuinely treatable walls (e.g. age-band-D solid brick,
|
||||
U 2.1 → ~0.3) are offered exactly as before.
|
||||
- The generators gain a read on `domain/sap10_ml/rdsap_uvalues.u_wall` — same
|
||||
precedent as the heating generator reading the calculator's PCDB/Table 4b.
|
||||
- **Deferred:** (a) a cert that lodges an explicit `wall_u_value` overrides the
|
||||
cascade in the calculator, making fabric measures score +0.00 regardless of
|
||||
this gate — the measure overlay should adjust the lodged U; (b) roof/floor
|
||||
generators have the same anti-pattern in principle (roof age-band eligibility
|
||||
is partly covered by ADR-0047); both tracked as follow-ups.
|
||||
|
||||
### Alternatives rejected
|
||||
|
||||
- **Filter zero-SAP candidates after scoring.** Rejected: zero-uplift options
|
||||
are legitimate for some measure types (bill-driven), and the pollution would
|
||||
survive in any consumer that reads Generator output before scoring.
|
||||
- **Hard-code "age band ≥ K ⇒ no wall offer".** Rejected: duplicates the U
|
||||
tables coarsely; the tables already encode exactly which
|
||||
construction × age-band × insulation combinations move.
|
||||
- **Full-fidelity U parity with the calculator (description, dry-lining,
|
||||
lodged U).** Rejected for now: heavier coupling for no observed benefit —
|
||||
the omissions only ever bias toward offering, which is the status quo.
|
||||
|
|
@ -28,6 +28,7 @@ are left UNKNOWN until modelled. Unresolvable values produce no overlay.
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from domain.modelling.simulation import EpcSimulation, HeatingOverlay
|
||||
|
|
@ -35,6 +36,8 @@ from domain.sap10_calculator.tables.table_12a import (
|
|||
OFF_PEAK_IMPLYING_HEATING_CODES,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Off-peak (Economy 7) meter. Electric storage / CPSU systems charge overnight at
|
||||
# the low rate and cannot run economically on a single-rate meter; "Dual" lets
|
||||
# the §12 dispatch resolve the specific tariff (storage 7-hour, CPSU 10-hour).
|
||||
|
|
@ -131,6 +134,19 @@ _ELECTRIC_BOILER_CODES = frozenset({191, 192})
|
|||
# unambiguously electric (ADR-0046).
|
||||
_ELECTRIC_UNDERFLOOR_CODES = frozenset({421, 422, 424})
|
||||
|
||||
# SAP Table 4a "electric underfloor heating" category. Stamped so an underfloor
|
||||
# override never leaves the replaced system's category behind — property 711795
|
||||
# (PRD #1435 WS2) kept a replaced heat network's category 6, which the heating
|
||||
# generator's `is_heat_network_main` gate read as community heating: no heating
|
||||
# upgrade was offered at all and the baseline mis-rated 51 vs 67 (ADR-0050).
|
||||
_UNDERFLOOR_CATEGORY = 8
|
||||
# SAP Table 4e Group 7 underfloor control: 2701 ("no time or thermostatic
|
||||
# control of room temperature", +0.3 C) is the conservative default when the
|
||||
# landlord named only the system — the largest mean-internal-temperature
|
||||
# adjustment in the group, so an unobserved control is never over-credited
|
||||
# (the underfloor mirror of storage-manual 2401 and room-heater 2601, ADR-0050).
|
||||
_UNDERFLOOR_CONTROL = 2701
|
||||
|
||||
# Heat pumps (SAP Table 4a 211-224 wet, 521-527 warm-air) are category 4 and
|
||||
# unambiguously electric (natural fuel 29). Modellable on the default code's SPF
|
||||
# without a PCDB index (ADR-0041).
|
||||
|
|
@ -266,8 +282,10 @@ def _control_for(code: int) -> Optional[int]:
|
|||
return _MANUAL_CHARGE_CONTROL
|
||||
if code in _GAS_BOILER_CODES:
|
||||
return _FULL_BOILER_CONTROL
|
||||
if code in _CATEGORY_10_ROOM_HEATER_CODES:
|
||||
if code in _CATEGORY_10_ROOM_HEATER_CODES or code in _ELECTRIC_ROOM_HEATER_CODES:
|
||||
return _ROOM_HEATER_CONTROL
|
||||
if code in _ELECTRIC_UNDERFLOOR_CODES:
|
||||
return _UNDERFLOOR_CONTROL
|
||||
return None
|
||||
|
||||
|
||||
|
|
@ -280,10 +298,12 @@ def _category_for(code: int) -> Optional[int]:
|
|||
category 7); without it a storage override left a stale category behind,
|
||||
which the SAP 10.2 Table 12a resolver (keyed on category) could misread as a
|
||||
non-storage system and price the off-peak storage heating at the peak rate."""
|
||||
if code in _CATEGORY_10_ROOM_HEATER_CODES:
|
||||
if code in _CATEGORY_10_ROOM_HEATER_CODES or code in _ELECTRIC_ROOM_HEATER_CODES:
|
||||
return _ROOM_HEATER_CATEGORY
|
||||
if code in _STORAGE_HEATER_CODES:
|
||||
return _STORAGE_HEATER_CATEGORY
|
||||
if code in _ELECTRIC_UNDERFLOOR_CODES:
|
||||
return _UNDERFLOOR_CATEGORY
|
||||
if code in _HEAT_PUMP_CODES:
|
||||
return _HEAT_PUMP_CATEGORY
|
||||
if code in _HEAT_NETWORK_CODES:
|
||||
|
|
@ -351,13 +371,32 @@ def main_heating_overlay_for(
|
|||
return None
|
||||
if code in _GAS_BOILER_CODES:
|
||||
return EpcSimulation(heating=_gas_boiler_overlay(code))
|
||||
category = _category_for(code)
|
||||
control = _control_for(code)
|
||||
if category is None or control is None:
|
||||
# A system-replacing override should stamp a complete, coherent companion
|
||||
# set; an unmapped category or control leaves the field unset, so the
|
||||
# effective cert inherits the REPLACED system's value (a silent mis-rating
|
||||
# — e.g. a room heater keeping a storage heater's category/control). We do
|
||||
# not yet have confident defaults for every archetype, so log-and-continue
|
||||
# (matching `flag_fuel_mismatch`) to make the gap visible for review
|
||||
# rather than raise mid-run.
|
||||
logger.error(
|
||||
"Landlord main_heating_system %r (SAP code %d) overlaid with an "
|
||||
"incomplete companion set (category=%s, control=%s); the effective "
|
||||
"cert may inherit the replaced system's value — known gap, review",
|
||||
main_heating_value,
|
||||
code,
|
||||
category,
|
||||
control,
|
||||
)
|
||||
return EpcSimulation(
|
||||
heating=HeatingOverlay(
|
||||
sap_main_heating_code=code,
|
||||
main_heating_category=_category_for(code),
|
||||
main_heating_category=category,
|
||||
main_fuel_type=_natural_fuel_for(code),
|
||||
meter_type=_meter_for(code),
|
||||
main_heating_control=_control_for(code),
|
||||
main_heating_control=control,
|
||||
# A landlord override describes the existing dwelling, so its assumed
|
||||
# off-peak meter must not downgrade a more-off-peak cert meter
|
||||
# (e.g. a 24-hour all-low tariff). Measures, which re-meter for real,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ boiler bundles land in later slices. Detection + pricing only — impact is
|
|||
produced by scoring (ADR-0016).
|
||||
"""
|
||||
|
||||
from dataclasses import replace
|
||||
from typing import Optional
|
||||
|
||||
from datatypes.epc.domain.epc_property_data import EpcPropertyData, MainHeatingDetail
|
||||
|
|
@ -137,6 +138,42 @@ _ASHP_OVERLAY = HeatingOverlay(
|
|||
)
|
||||
|
||||
|
||||
# ASHP sizing ladder (ADR-0049): the Vaillant aroTHERM plus "& AI VIH RW" family,
|
||||
# one representative PCDB Table 362 record per nominal size, ascending by rated
|
||||
# max output (kW). The pump is sized to the dwelling's design heat loss so SAP
|
||||
# 10.2 Appendix N reads the heat-pump efficiency near its PSR peak (~1.0), rather
|
||||
# than a single fixed unit that is grossly undersized on high-heat-loss dwellings
|
||||
# — where the PSR collapses and the ASHP scores at/below the resistance baseline.
|
||||
# 110257 (the 5 kW rung) is the previously-fixed anchor, validated against the
|
||||
# relodged after-cert.
|
||||
_ASHP_SIZING_LADDER: tuple[tuple[float, int], ...] = (
|
||||
(3.76, 110249), # aroTHERM plus 3.5 kW
|
||||
(4.37, 110257), # aroTHERM plus 5 kW
|
||||
(6.40, 110265), # aroTHERM plus 7 kW
|
||||
(7.93, 110273), # aroTHERM plus 10 kW
|
||||
(11.48, 110281), # aroTHERM plus 12 kW
|
||||
)
|
||||
# SAP 10.2 Appendix-N heat-pump efficiency peaks around PSR 0.8 (output / design
|
||||
# heat loss); sizing to it reproduces real installer choices (accredited-cert
|
||||
# validated) rather than the MCS capacity target (PSR 1.0), which oversizes.
|
||||
_EFFICIENCY_PEAK_PSR: float = 0.8
|
||||
|
||||
|
||||
def select_ashp_pcdb_id(design_heat_loss_kw: float) -> int:
|
||||
"""The PCDB heat-pump record to install for a dwelling with
|
||||
`design_heat_loss_kw` (ADR-0049): the ladder rung whose rated output sits
|
||||
nearest the SAP 10.2 Appendix-N efficiency peak — PSR ~= 0.8, i.e. a rated
|
||||
output of ``0.8 x design_heat_loss`` — which reproduces the pump a real
|
||||
installer fits (validated against the relodged Elmhurst ASHP cert) and keeps
|
||||
the heat pump clear of the low-PSR efficiency collapse. A load beyond the
|
||||
domestic aroTHERM plus range naturally selects the largest rung."""
|
||||
target_output_kw: float = _EFFICIENCY_PEAK_PSR * design_heat_loss_kw
|
||||
return min(
|
||||
_ASHP_SIZING_LADDER,
|
||||
key=lambda rung: abs(rung[0] - target_output_kw),
|
||||
)[1]
|
||||
|
||||
|
||||
# --- Gas boiler upgrade (Heating/HW expansion): replace an existing wet boiler
|
||||
# with a modern gas condensing boiler. Validated against Elmhurst before/after
|
||||
# re-lodgements (cert 001431): the upgrade always targets mains gas — gas->gas
|
||||
|
|
@ -254,16 +291,25 @@ _OIL_FUEL_CODES = frozenset({28, 4, 71, 73, 75, 76})
|
|||
_LPG_FUEL_CODES = frozenset({27, 2, 3, 5, 9})
|
||||
|
||||
|
||||
def ashp_cost_inputs(epc: EpcPropertyData) -> AshpCostInputs:
|
||||
def ashp_cost_inputs(
|
||||
epc: EpcPropertyData, design_heat_loss_kw: Optional[float] = None
|
||||
) -> AshpCostInputs:
|
||||
"""Read an `EpcPropertyData` into the typed inputs `Products.ashp_bundle_cost`
|
||||
needs: the existing system, property-size band, design heat loss (floor-area
|
||||
proxy), radiator count, and whether a wet system can be reused (ADR-0025)."""
|
||||
needs: the existing system, property-size band, design heat loss, radiator
|
||||
count, and whether a wet system can be reused (ADR-0025). ``design_heat_loss_kw``
|
||||
(the calculator's value, ADR-0049) sizes the cost band; when omitted it falls
|
||||
back to the floor-area proxy so cost and pump size share one figure."""
|
||||
system: AshpExistingSystem = _existing_system(epc)
|
||||
floor_area: float = epc.total_floor_area_m2
|
||||
heat_loss_kw: float = (
|
||||
design_heat_loss_kw
|
||||
if design_heat_loss_kw is not None
|
||||
else floor_area * _KW_PER_M2
|
||||
)
|
||||
return AshpCostInputs(
|
||||
existing_system=system,
|
||||
is_small_property=floor_area <= _SMALL_PROPERTY_MAX_M2,
|
||||
design_heat_loss_kw=floor_area * _KW_PER_M2,
|
||||
design_heat_loss_kw=heat_loss_kw,
|
||||
radiator_count=_radiator_count(epc),
|
||||
has_reusable_wet_system=system
|
||||
in (AshpExistingSystem.GAS, AshpExistingSystem.OIL, AshpExistingSystem.LPG),
|
||||
|
|
@ -307,17 +353,23 @@ def recommend_heating(
|
|||
products: ProductRepository,
|
||||
restrictions: PlanningRestrictions = PlanningRestrictions(),
|
||||
considered_measures: Optional[frozenset[MeasureType]] = None,
|
||||
*,
|
||||
design_heat_loss_kw: Optional[float] = None,
|
||||
) -> Optional[Recommendation]:
|
||||
"""Return a "Heating & Hot Water" Recommendation of competing whole-system
|
||||
bundles for the dwelling, else None when no bundle is eligible. ASHP is
|
||||
additionally gated by the Property's planning protections (ADR-0024)."""
|
||||
additionally gated by the Property's planning protections (ADR-0024).
|
||||
|
||||
``design_heat_loss_kw`` (the calculator's SAP design heat loss) sizes the
|
||||
ASHP to the dwelling so SAP's Appendix-N PSR efficiency reads near its peak
|
||||
(ADR-0049); when omitted, sizing falls back to the floor-area proxy."""
|
||||
options: list[MeasureOption] = []
|
||||
|
||||
hhr_option = _hhr_storage_option(epc, products)
|
||||
if hhr_option is not None:
|
||||
options.append(hhr_option)
|
||||
|
||||
ashp_option = _ashp_option(epc, products, restrictions)
|
||||
ashp_option = _ashp_option(epc, products, restrictions, design_heat_loss_kw)
|
||||
if ashp_option is not None:
|
||||
options.append(ashp_option)
|
||||
|
||||
|
|
@ -702,23 +754,37 @@ def _ashp_option(
|
|||
epc: EpcPropertyData,
|
||||
products: ProductRepository,
|
||||
restrictions: PlanningRestrictions,
|
||||
design_heat_loss_kw: Optional[float] = None,
|
||||
) -> Optional[MeasureOption]:
|
||||
"""The air-source heat-pump bundle, offered for any non-flat house/bungalow
|
||||
that is not listed/heritage and not already a heat pump."""
|
||||
that is not listed/heritage and not already a heat pump. The pump is sized to
|
||||
``design_heat_loss_kw`` (ADR-0049), falling back to the floor-area proxy."""
|
||||
if not _ashp_eligible(epc, restrictions):
|
||||
return None
|
||||
# Size the pump to the dwelling: the calculator's design heat loss when the
|
||||
# caller threaded it, else the floor-area proxy (ADR-0049).
|
||||
heat_loss_kw: float = (
|
||||
design_heat_loss_kw
|
||||
if design_heat_loss_kw is not None
|
||||
else epc.total_floor_area_m2 * _KW_PER_M2
|
||||
)
|
||||
sized_overlay: HeatingOverlay = replace(
|
||||
_ASHP_OVERLAY, main_heating_index_number=select_ashp_pcdb_id(heat_loss_kw)
|
||||
)
|
||||
# Cost is composed per-dwelling from the rate sheet (ADR-0025), not the
|
||||
# single catalogue scalar; the catalogue row is read only for its id, so an
|
||||
# absent ASHP row must not suppress the bundle — it just carries no id.
|
||||
product: Optional[Product] = products.get_optional(_ASHP_MEASURE_TYPE)
|
||||
cost: Cost = Products().ashp_bundle_cost(ashp_cost_inputs(epc))
|
||||
cost: Cost = Products().ashp_bundle_cost(
|
||||
ashp_cost_inputs(epc, design_heat_loss_kw=heat_loss_kw)
|
||||
)
|
||||
return MeasureOption(
|
||||
measure_type=_ASHP_MEASURE_TYPE,
|
||||
description=(
|
||||
"Replace the heating with an air-source heat pump, time-and-"
|
||||
"temperature-zone controls and a heat-pump hot-water cylinder"
|
||||
),
|
||||
overlay=EpcSimulation(heating=_ASHP_OVERLAY),
|
||||
overlay=EpcSimulation(heating=sized_overlay),
|
||||
cost=cost,
|
||||
material_id=product.id if product is not None else None,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ from domain.building_geometry import gross_heat_loss_wall_area
|
|||
from domain.geospatial.planning_restrictions import PlanningRestrictions
|
||||
from domain.modelling.measure_type import MeasureType
|
||||
from domain.modelling.recommendation import Cost, MeasureOption, Recommendation
|
||||
from domain.modelling.generators.wall_u_value_gate import wall_insulation_lowers_u
|
||||
from domain.modelling.simulation import BuildingPartOverlay, EpcSimulation
|
||||
from repositories.product.product_repository import ProductRepository
|
||||
|
||||
|
|
@ -158,6 +159,15 @@ def recommend_solid_wall(
|
|||
measure_type
|
||||
for measure_type in measure_types
|
||||
if _allowed(measure_type, restrictions, is_flat)
|
||||
# Wall U-Value Gate (ADR-0051): withhold an Option that cannot lower
|
||||
# the wall's derived U — e.g. an at-regs modern age band, where the
|
||||
# as-built and insulated cascades resolve to the same value.
|
||||
and wall_insulation_lowers_u(
|
||||
main,
|
||||
epc.country_code,
|
||||
insulation_type=_INSULATION_TYPE[measure_type],
|
||||
insulation_thickness_mm=_SOLID_WALL_INSULATION_MM,
|
||||
)
|
||||
)
|
||||
if not allowed:
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from datatypes.epc.domain.epc_property_data import (
|
|||
EpcPropertyData,
|
||||
)
|
||||
from domain.building_geometry import gross_heat_loss_wall_area
|
||||
from domain.modelling.generators.wall_u_value_gate import wall_insulation_lowers_u
|
||||
from domain.modelling.measure_type import MeasureType
|
||||
from domain.modelling.recommendation import Cost, MeasureOption, Recommendation
|
||||
from domain.modelling.simulation import BuildingPartOverlay, EpcSimulation
|
||||
|
|
@ -46,6 +47,14 @@ def recommend_cavity_wall(
|
|||
):
|
||||
return None
|
||||
|
||||
# Wall U-Value Gate (ADR-0051): withhold the fill when it cannot lower the
|
||||
# wall's derived U — e.g. an at-regs modern age band, where the as-built and
|
||||
# filled-cavity cascades resolve to the same (or a no-better) value.
|
||||
if not wall_insulation_lowers_u(
|
||||
main, epc.country_code, insulation_type=_FILLED_CAVITY
|
||||
):
|
||||
return None
|
||||
|
||||
product = products.get(_CAVITY_MEASURE_TYPE)
|
||||
wall_area: float = gross_heat_loss_wall_area(epc, BuildingPartIdentifier.MAIN)
|
||||
cost = Cost(
|
||||
|
|
|
|||
80
domain/modelling/generators/wall_u_value_gate.py
Normal file
80
domain/modelling/generators/wall_u_value_gate.py
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
"""The Wall U-Value Gate (ADR-0051).
|
||||
|
||||
A wall insulation Option is offered only when it would actually lower the main
|
||||
wall's derived U-value: on a modern construction age band RdSAP's Table 6-10
|
||||
cascade assigns an as-built wall its at-regs U-value, and the post-measure
|
||||
state resolves to the *same* value — the measure costs thousands for +0.00 SAP
|
||||
(property 711795, PRD #1435 WS2: timber frame at age band L, 0.280 → 0.280).
|
||||
|
||||
The comparison uses a deliberately thin `u_wall` parameter mapping (the
|
||||
`envelope.py` style — construction, age band, insulation type/thickness,
|
||||
documentary wall thickness), omitting the calculator's rich extras (surveyor
|
||||
description, dry-lining, lodged `wall_u_value`). The omissions bias the gate
|
||||
toward OFFERING — both sides are computed identically, so a genuine no-op still
|
||||
compares equal — never toward suppressing a real measure.
|
||||
"""
|
||||
|
||||
from typing import Any, Final, Optional
|
||||
|
||||
from datatypes.epc.domain.epc_property_data import SapBuildingPart
|
||||
from domain.sap10_ml.rdsap_uvalues import WALL_UNKNOWN, Country, u_wall
|
||||
|
||||
# Improvements below this are float noise, not a physical change. NOT a
|
||||
# materiality threshold — how much improvement is worth the cost stays with the
|
||||
# Optimiser (ADR-0016/0024).
|
||||
_NOISE_FLOOR_W_PER_M2K: Final[float] = 0.01
|
||||
|
||||
# RdSAP `wall_insulation_type` 4 = as-built / assumed (no added insulation).
|
||||
_WALL_INSULATION_NONE: Final[int] = 4
|
||||
|
||||
|
||||
def _int_or_none(value: Any) -> Optional[int]:
|
||||
return value if isinstance(value, int) else None
|
||||
|
||||
|
||||
def _parse_thickness_mm(value: Any) -> Optional[int]:
|
||||
if value is None or isinstance(value, int):
|
||||
return value
|
||||
if isinstance(value, str) and value.strip().isdigit():
|
||||
return int(value.strip())
|
||||
return None # "NI" / free text — thickness not known
|
||||
|
||||
|
||||
def wall_insulation_lowers_u(
|
||||
main: SapBuildingPart,
|
||||
country_code: Optional[str],
|
||||
*,
|
||||
insulation_type: int,
|
||||
insulation_thickness_mm: Optional[int] = None,
|
||||
) -> bool:
|
||||
"""Whether insulating the main wall to ``insulation_type`` (+ optional
|
||||
thickness) lowers its derived U-value by more than float noise — the Wall
|
||||
U-Value Gate (ADR-0051)."""
|
||||
country: Country = Country.from_code(country_code)
|
||||
age_band = main.construction_age_band
|
||||
construction: Optional[int] = _int_or_none(main.wall_construction)
|
||||
if construction == WALL_UNKNOWN:
|
||||
construction = None
|
||||
wall_thickness_mm = main.wall_thickness_mm
|
||||
current_type: Optional[int] = _int_or_none(main.wall_insulation_type)
|
||||
current_u: float = u_wall(
|
||||
country,
|
||||
age_band,
|
||||
construction,
|
||||
_parse_thickness_mm(main.wall_insulation_thickness),
|
||||
insulation_present=(
|
||||
current_type is not None and current_type != _WALL_INSULATION_NONE
|
||||
),
|
||||
wall_insulation_type=current_type,
|
||||
wall_thickness_mm=wall_thickness_mm,
|
||||
)
|
||||
post_u: float = u_wall(
|
||||
country,
|
||||
age_band,
|
||||
construction,
|
||||
insulation_thickness_mm,
|
||||
insulation_present=True,
|
||||
wall_insulation_type=insulation_type,
|
||||
wall_thickness_mm=wall_thickness_mm,
|
||||
)
|
||||
return current_u - post_u > _NOISE_FLOOR_W_PER_M2K
|
||||
|
|
@ -65,6 +65,11 @@ from domain.sap10_calculator.worksheet.rating import (
|
|||
|
||||
_AIR_HEAT_CAPACITY_WH_PER_M3_K: Final[float] = 0.33
|
||||
_TIME_CONSTANT_DIVISOR_KJ_TO_WH: Final[float] = 3.6
|
||||
# SAP design temperature difference (21 C internal - -3.2 C external): the delta
|
||||
# the design heat loss and Appendix N's PSR are evaluated at (matches
|
||||
# `_SAP_DESIGN_HEAT_LOSS_DELTA_T_K` in rdsap.cert_to_inputs).
|
||||
_DESIGN_HEAT_LOSS_DELTA_T_K: Final[float] = 24.2
|
||||
_WATTS_PER_KW: Final[float] = 1000.0
|
||||
|
||||
# §9a default — used as `CalculatorInputs.energy_requirements` default for
|
||||
# synthetic constructions that bypass cert_to_inputs. All-zero fuel; the
|
||||
|
|
@ -403,6 +408,11 @@ class SapResult:
|
|||
space_heating_kwh_per_yr: float
|
||||
space_cooling_kwh_per_yr: float
|
||||
fabric_energy_efficiency_kwh_per_m2_yr: float
|
||||
# The dwelling's SAP design heat loss (kW) — the annual-average heat loss
|
||||
# coefficient at the design temperature difference (24.2 K). Output-only:
|
||||
# it is the quantity Appendix N's PSR divides a heat pump's rated output by,
|
||||
# so the ASHP recommendation sizes the pump to the dwelling from it.
|
||||
design_heat_loss_kw: float
|
||||
main_heating_fuel_kwh_per_yr: float
|
||||
main_2_heating_fuel_kwh_per_yr: float
|
||||
secondary_heating_fuel_kwh_per_yr: float
|
||||
|
|
@ -872,6 +882,9 @@ def calculate_sap_from_inputs(inputs: CalculatorInputs) -> SapResult:
|
|||
space_heating_kwh_per_yr=space_heating_kwh,
|
||||
space_cooling_kwh_per_yr=space_cooling_kwh,
|
||||
fabric_energy_efficiency_kwh_per_m2_yr=inputs.fabric_energy_efficiency_kwh_per_m2_yr,
|
||||
design_heat_loss_kw=(sum(monthly_hlc) / 12.0)
|
||||
* _DESIGN_HEAT_LOSS_DELTA_T_K
|
||||
/ _WATTS_PER_KW,
|
||||
main_heating_fuel_kwh_per_yr=main_fuel_kwh,
|
||||
main_2_heating_fuel_kwh_per_yr=inputs.energy_requirements.main_2_fuel_kwh_per_yr,
|
||||
secondary_heating_fuel_kwh_per_yr=secondary_fuel_kwh,
|
||||
|
|
|
|||
|
|
@ -2888,6 +2888,15 @@ def _hot_water_fuel_cost_gbp_per_kwh(
|
|||
)
|
||||
blended = high_frac * high_rate + (1.0 - high_frac) * low_rate
|
||||
return blended * _PENCE_TO_GBP
|
||||
if water_heating_code in _INSTANTANEOUS_WATER_CODES:
|
||||
# SAP 10.2 §12 (PDF line 2680): the Table-13 off-peak on-peak HW
|
||||
# split is keyed on the CYLINDER size — it presumes stored water
|
||||
# charged overnight. An instantaneous point-of-use heater (WHC
|
||||
# 907/909, no cylinder) heats on demand and cannot shift to the
|
||||
# off-peak window, so 100% bills at the HIGH rate (Table 12a WH
|
||||
# high-rate fraction 1.0), not the off-peak low rate.
|
||||
high_rate, _low = _tariff_high_low_rates_p_per_kwh(tariff)
|
||||
return high_rate * _PENCE_TO_GBP
|
||||
return _off_peak_low_rate_gbp_per_kwh(tariff)
|
||||
if water_heating_fuel is not None:
|
||||
return prices.unit_price_p_per_kwh(water_heating_fuel) * _PENCE_TO_GBP
|
||||
|
|
@ -2942,6 +2951,11 @@ def _hot_water_high_rate_fraction(
|
|||
single_immersion=effective_single,
|
||||
tariff=tariff,
|
||||
)
|
||||
if water_heating_code in _INSTANTANEOUS_WATER_CODES:
|
||||
# Instantaneous point-of-use electric DHW (WHC 907/909) has no
|
||||
# cylinder to charge overnight — it heats on demand, wholly at the
|
||||
# high/day rate (see `_hot_water_fuel_cost_gbp_per_kwh`).
|
||||
return 1.0
|
||||
return 0.0
|
||||
|
||||
|
||||
|
|
@ -5307,6 +5321,35 @@ _AGE_BANDS_A_TO_E: Final[frozenset[str]] = frozenset({"A", "B", "C", "D", "E"})
|
|||
_SUSPENDED_TIMBER_FLOOR_TYPE: Final[str] = "Suspended timber"
|
||||
_GROUND_FLOOR_TYPE: Final[str] = "Ground floor"
|
||||
_FLOOR_U_SEALED_THRESHOLD: Final[float] = 0.5
|
||||
# Table 19 footnote (1): age bands whose default floor_construction is
|
||||
# suspended timber when the construction is unknown. Mirrors
|
||||
# `_SUSPENDED_TIMBER_DEFAULT_BANDS` in rdsap_uvalues.py — duplicated
|
||||
# locally rather than cross-imported (private symbol) per this module's
|
||||
# existing convention (see `_main_floor_u_value`).
|
||||
_TABLE_19_FOOTNOTE_1_SUSPENDED_TIMBER_BANDS: Final[frozenset[str]] = frozenset({"A", "B"})
|
||||
|
||||
|
||||
def _effective_floor_description(
|
||||
epc: EpcPropertyData, main: SapBuildingPart,
|
||||
) -> Optional[str]:
|
||||
"""Mirror `heat_transmission_section_from_cert`'s
|
||||
`effective_floor_description` rule: the per-bp `floor_construction_type`
|
||||
lodgement ("Suspended timber" / "Solid") takes precedence over the
|
||||
global `epc.floors[].description` since it's the explicit per-part
|
||||
Elmhurst Summary §3/§9 lodgement. Falls back to the joined global
|
||||
description when the per-bp field is unlodged (the gov-API mapper
|
||||
often leaves it `None`). Inlined (vs importing from
|
||||
heat_transmission) to keep cert_to_inputs free of cross-module
|
||||
private symbol imports.
|
||||
"""
|
||||
if main.floor_construction_type:
|
||||
return main.floor_construction_type
|
||||
descs = [
|
||||
d for d in
|
||||
(getattr(f, "description", None) for f in (epc.floors or []))
|
||||
if d
|
||||
]
|
||||
return " | ".join(descs) if descs else None
|
||||
|
||||
|
||||
def _main_floor_u_value(epc: EpcPropertyData) -> Optional[float]:
|
||||
|
|
@ -5343,21 +5386,6 @@ def _main_floor_u_value(epc: EpcPropertyData) -> Optional[float]:
|
|||
int(raw_floor_ins) if isinstance(raw_floor_ins, (int, float))
|
||||
else (0 if raw_floor_ins == "NI" else None)
|
||||
)
|
||||
# Mirror heat_transmission's `effective_floor_description`: the per-bp
|
||||
# `floor_construction_type` takes precedence over a joined
|
||||
# `epc.floors[].description` since the per-part lodgement is the
|
||||
# explicit Elmhurst Summary §3/§9 surface. Inline the join (vs
|
||||
# importing from heat_transmission) to keep cert_to_inputs free of
|
||||
# cross-module private symbol imports.
|
||||
if main.floor_construction_type:
|
||||
effective_floor_description = main.floor_construction_type
|
||||
else:
|
||||
descs = [
|
||||
d for d in
|
||||
(getattr(f, "description", None) for f in (epc.floors or []))
|
||||
if d
|
||||
]
|
||||
effective_floor_description = " | ".join(descs) if descs else None
|
||||
return u_floor(
|
||||
country=Country.from_code(epc.country_code) if epc.country_code else None,
|
||||
age_band=main.construction_age_band,
|
||||
|
|
@ -5366,10 +5394,49 @@ def _main_floor_u_value(epc: EpcPropertyData) -> Optional[float]:
|
|||
area_m2=ground_fd.total_floor_area_m2,
|
||||
perimeter_m=ground_fd.heat_loss_perimeter_m,
|
||||
wall_thickness_mm=main.wall_thickness_mm,
|
||||
description=effective_floor_description,
|
||||
description=_effective_floor_description(epc, main),
|
||||
)
|
||||
|
||||
|
||||
def _floor_construction_is_suspended_timber(
|
||||
epc: EpcPropertyData, main: SapBuildingPart,
|
||||
) -> bool:
|
||||
"""RdSAP 10 Table 19 footnote (1) construction resolution for the §5
|
||||
(12) floor-infiltration rule. Tri-state, mirroring `u_floor`'s
|
||||
`_floor_is_suspended_from_description` / `_SUSPENDED_TIMBER_DEFAULT_
|
||||
BANDS` cascade (rdsap_uvalues.py) so the (12) infiltration rule and
|
||||
the floor U-value calc agree on which floors are suspended timber:
|
||||
|
||||
1. An explicit construction lodgement (`_effective_floor_description`
|
||||
— per-bp `floor_construction_type` else the joined
|
||||
`epc.floors[].description`) decides: "Suspended, ..." is timber
|
||||
UNLESS it's "Suspended, not timber" (suspended concrete etc. has
|
||||
no floorboard-gap infiltration); "Solid, ..." is not suspended.
|
||||
2. No description signal AND no lodged `floor_construction` code at
|
||||
all → Table 19 footnote (1): unknown construction defaults to
|
||||
suspended timber only for age bands A/B.
|
||||
"""
|
||||
desc = _effective_floor_description(epc, main)
|
||||
if desc is not None:
|
||||
d = desc.strip().lower()
|
||||
if d.startswith("suspended"):
|
||||
return "not timber" not in d
|
||||
if d.startswith("solid"):
|
||||
return False
|
||||
ground_fd = next(
|
||||
(fd for fd in main.sap_floor_dimensions if fd.floor == 0),
|
||||
main.sap_floor_dimensions[0] if main.sap_floor_dimensions else None,
|
||||
)
|
||||
construction_known = (
|
||||
ground_fd is not None
|
||||
and _int_or_none(ground_fd.floor_construction) is not None
|
||||
)
|
||||
if construction_known:
|
||||
return False
|
||||
age = (main.construction_age_band or "").strip().upper()
|
||||
return age in _TABLE_19_FOOTNOTE_1_SUSPENDED_TIMBER_BANDS
|
||||
|
||||
|
||||
def _has_suspended_timber_floor_per_spec(
|
||||
epc: EpcPropertyData,
|
||||
) -> tuple[bool, bool]:
|
||||
|
|
@ -5395,9 +5462,13 @@ def _has_suspended_timber_floor_per_spec(
|
|||
infiltration 0.2.
|
||||
|
||||
The rule only applies when the Main bp's lowest floor is a
|
||||
"Ground floor" with "Suspended timber" construction. All other
|
||||
combinations fall through to `(False, False)` and the cascade
|
||||
enters 0 for (12).
|
||||
"Ground floor" with "Suspended timber" construction. Construction is
|
||||
resolved the same tri-state way `u_floor` resolves it (RdSAP 10
|
||||
Table 19 footnote 1): the explicit per-bp `floor_construction_type` /
|
||||
joined `epc.floors[].description` lodgement, else — when neither
|
||||
lodges a construction signal — the footnote-1 age-band default
|
||||
(suspended timber for bands A/B only). All other combinations fall
|
||||
through to `(False, False)` and the cascade enters 0 for (12).
|
||||
"""
|
||||
if not epc.sap_building_parts:
|
||||
return False, False
|
||||
|
|
@ -5407,7 +5478,7 @@ def _has_suspended_timber_floor_per_spec(
|
|||
return True, False
|
||||
if main.floor_type != _GROUND_FLOOR_TYPE:
|
||||
return False, False
|
||||
if main.floor_construction_type != _SUSPENDED_TIMBER_FLOOR_TYPE:
|
||||
if not _floor_construction_is_suspended_timber(epc, main):
|
||||
return False, False
|
||||
age = (main.construction_age_band or "").strip().upper()
|
||||
if age in _AGE_BANDS_F_TO_M:
|
||||
|
|
@ -7090,6 +7161,18 @@ def _cylinder_storage_loss_override(
|
|||
# 550 kWh/yr). The Table-2b temperature factor still applies (49)→(50).
|
||||
declared_loss = _float_or_none(getattr(sh, "cylinder_heat_loss", None))
|
||||
volume_l = _cylinder_volume_l_from_code(epc)
|
||||
if volume_l is None and _int_or_none(sh.cylinder_size) == 0:
|
||||
# gov-API cylinder_size=0 = "size not determined" for a present
|
||||
# cylinder (has_hot_water_cylinder=true). RdSAP 10 §10.5 Table 28:
|
||||
# "If the actual size is not determined, the size ... is taken as
|
||||
# according to Table 28" → the "Normal" 110 L baseline, which STILL
|
||||
# incurs a storage loss. Without this the code lookup returned None
|
||||
# and the `if volume_l is None: return None` below dropped the loss,
|
||||
# under-costing the DHW (7 corpus certs; e.g. 200004017091 +1.75 ->
|
||||
# +0.39). Gated on the EXPLICIT 0 so a full-SAP cert whose RdSAP
|
||||
# cylinder_size is simply unlodged (None, e.g. 10091568921) keeps its
|
||||
# own cylinder handling rather than a forced 110 L RdSAP default.
|
||||
volume_l = _CYLINDER_SIZE_NOT_DETERMINED_L
|
||||
if declared_loss is not None:
|
||||
storage_56m = cylinder_storage_loss_monthly_kwh(
|
||||
volume_l=volume_l or 0.0,
|
||||
|
|
@ -7517,8 +7600,17 @@ def _fuel_cost(
|
|||
secondary_fuel = (
|
||||
epc.sap_heating.secondary_fuel_type if epc.sap_heating else None
|
||||
)
|
||||
# Canonicalise the colliding gov-API secondary fuel enums (5 anthracite /
|
||||
# 9 dual fuel / 33 coal — `_GOV_API_COLLISION_FUELS`) BEFORE the price
|
||||
# lookup, exactly as the main-fuel boundary and the CO2/display
|
||||
# `_secondary_fuel_cost_gbp_per_kwh` already do. Without this the raw
|
||||
# same-value Table-32 lookup mis-priced e.g. anthracite (enum 5) at the
|
||||
# bulk-LPG code-5 rate (12.19 vs 3.64 p/kWh) — a 3× over-cost that sank
|
||||
# cert 100050355518 (lodged 36, engine 20.8). This inline cost path had
|
||||
# diverged from the already-canonicalised display helper.
|
||||
secondary_high_rate_gbp_per_kwh = (
|
||||
table_32_unit_price_p_per_kwh(secondary_fuel) * _PENCE_TO_GBP
|
||||
table_32_unit_price_p_per_kwh(canonical_fuel_code(secondary_fuel))
|
||||
* _PENCE_TO_GBP
|
||||
if secondary_fuel is not None
|
||||
else other_uses_gbp_per_kwh
|
||||
)
|
||||
|
|
|
|||
|
|
@ -922,6 +922,22 @@ def heat_transmission_from_cert(
|
|||
raw_rafter_thickness = getattr(part, "rafter_insulation_thickness", None)
|
||||
if raw_rafter_thickness is not None:
|
||||
raw_roof_thickness = raw_rafter_thickness
|
||||
elif "flat" in (part.roof_construction_type or "").lower():
|
||||
# RdSAP 10 §5.11 — a FLAT roof lodges its insulation thickness in
|
||||
# the dedicated gov-API `flat_roof_insulation_thickness` field
|
||||
# (e.g. "75mm"), NOT `roof_insulation_thickness` (which stays None
|
||||
# for flat roofs). Prefer it so a measured flat-roof thickness
|
||||
# bills at its Table-16 U instead of the uninsulated age-band flat
|
||||
# default. Mirrors the rafter branch above. Cert 47084930: flat
|
||||
# roof "75mm" -> U 0.5 (accredited Elmhurst worksheet, "insulated
|
||||
# flat roof") vs the age-E default 1.5 (roof 96.4 -> 32.1 W/K). An
|
||||
# "AB"/"NI" (as-built/unknown) value parses to None and keeps the
|
||||
# age-band default, unchanged.
|
||||
raw_flat_thickness = getattr(
|
||||
part, "flat_roof_insulation_thickness", None
|
||||
)
|
||||
if raw_flat_thickness is not None:
|
||||
raw_roof_thickness = raw_flat_thickness
|
||||
roof_thickness = _parse_thickness_mm(raw_roof_thickness)
|
||||
floor_ins_thickness = _parse_thickness_mm(getattr(part, "floor_insulation_thickness", None))
|
||||
|
||||
|
|
|
|||
|
|
@ -157,6 +157,11 @@ _WALL_INSULATION_INTERNAL: Final[int] = 3
|
|||
WALL_INSULATION_CAVITY_PLUS_EXTERNAL: Final[int] = 6
|
||||
WALL_INSULATION_CAVITY_PLUS_INTERNAL: Final[int] = 7
|
||||
|
||||
# RdSAP 10 §5.4 (PDF p.32): "assume 100 mm if thickness is unknown" — the
|
||||
# default added-insulation thickness for an External / Internal wall
|
||||
# insulation lodgement (type 1/3) with no measured thickness.
|
||||
_WALL_ADDED_INSULATION_UNKNOWN_THICKNESS_MM: Final[int] = 100
|
||||
|
||||
# RdSAP 10 §4-6 (page 73): default thermal conductivity of insulation when
|
||||
# no documentary evidence is available. Used to convert the lodged
|
||||
# `wall_insulation_thickness` (mm) into thermal resistance (m²K/W) via
|
||||
|
|
@ -348,7 +353,13 @@ def _insulation_bucket(thickness_mm: Optional[int], insulation_present: bool) ->
|
|||
to the 50 mm bucket when `insulation_present=True`; when not
|
||||
present, the as-built (bucket 0) row applies regardless.
|
||||
"""
|
||||
if insulation_present and (thickness_mm is None or thickness_mm == 0):
|
||||
# Insulation known to be present must never fall to the uninsulated
|
||||
# (bucket 0) row. RdSAP 10 Table 6 footnote routes present-but-unknown
|
||||
# thickness to the 50 mm row; a KNOWN thin lodgement (5-24 mm, below the
|
||||
# smallest tabulated 50 mm column) must not read as WORSE (higher U) than
|
||||
# the unknown case — so it too rounds up to the 50 mm row rather than to
|
||||
# bucket 0. Only genuinely absent insulation (present=False) maps to 0.
|
||||
if insulation_present and (thickness_mm is None or thickness_mm < 25):
|
||||
return 50
|
||||
if thickness_mm is None:
|
||||
return 0
|
||||
|
|
@ -589,6 +600,28 @@ def u_wall(
|
|||
ctry = country if country is not None else Country.ENG
|
||||
age_idx = _age_index(age_band)
|
||||
band = _AGE_BANDS[age_idx]
|
||||
# RdSAP 10 §5.4 (PDF p.32): "the U-value with additional insulation is
|
||||
# based on the thickness of insulation of mineral wool type (assume
|
||||
# 100 mm if thickness is unknown)". A SOLID-BRICK or STONE wall lodging
|
||||
# External or Internal insulation (wall_insulation_type 1/3) but no
|
||||
# measured insulation thickness (gov-API "NI" → None) takes the 100 mm
|
||||
# default so the §5.8 Table-14 R-value adjustment (brick/stone branches
|
||||
# below) fires instead of billing the wall at its uninsulated
|
||||
# §5.6/Table-6 U (over-counting fabric loss → under-rate; corpus cert
|
||||
# 200004296092, solid brick 360 mm, walls 101.7 W/K → PE +59.6). Scoped
|
||||
# to brick/stone because only those constructions have a §5.8 added-
|
||||
# insulation R-value path here — cavity uses the separate
|
||||
# `WALL_INSULATION_CAVITY_PLUS_*` composite, and timber/system-built
|
||||
# carry no §5.8 branch, so a thickness default there would be inert or
|
||||
# mis-routed.
|
||||
if (
|
||||
construction
|
||||
in (WALL_STONE_GRANITE, WALL_STONE_SANDSTONE, WALL_SOLID_BRICK)
|
||||
and wall_insulation_type
|
||||
in (_WALL_INSULATION_EXTERNAL, _WALL_INSULATION_INTERNAL)
|
||||
and (insulation_thickness_mm is None or insulation_thickness_mm == 0)
|
||||
):
|
||||
insulation_thickness_mm = _WALL_ADDED_INSULATION_UNKNOWN_THICKNESS_MM
|
||||
# RdSAP 10 Tables 6-10 stone rows + footnote (a) (PDF p.33-39), §5.6
|
||||
# formula (PDF p.40), §5.8 + Table 14 (PDF p.41-42). A documentary wall
|
||||
# thickness (per §5.3) routes stone in age bands A-E off the §5.6 formula,
|
||||
|
|
@ -998,13 +1031,30 @@ def u_roof(
|
|||
# (band J = 0.16), matching the cohort's "ND" (thickness None)
|
||||
# vaulted roofs which already reach col (1) by falling through.
|
||||
return _ROOF_BY_AGE.get(age_band.upper(), 0.4)
|
||||
if (
|
||||
age_band is not None
|
||||
and insulation_thickness_mm == 0
|
||||
and _described_as_insulated(description)
|
||||
and description is not None
|
||||
and "(assumed)" in description.lower()
|
||||
):
|
||||
# RdSAP renders "insulated (assumed)" when insulation PRESENCE is
|
||||
# itself assumed (age-band / as-built assumption), not an observed
|
||||
# retrofit — so the §5.11.4 50 mm footnote (below) does not apply;
|
||||
# the thickness "cannot be determined" and the Table 18 age-band
|
||||
# default governs (§5.11.4 opening clause). Identical
|
||||
# "Pitched, insulated (assumed)" corpus dwellings lodge thickness
|
||||
# "ND" (None → Table 18 by fallthrough) or "NI" (0 → previously the
|
||||
# 50 mm row) purely per lodging software; both take Table 18.
|
||||
table_18 = _FLAT_ROOF_BY_AGE if is_flat_roof else roof_by_age
|
||||
return table_18.get(age_band.upper(), 0.4)
|
||||
if insulation_thickness_mm == 0 and _described_as_insulated(description):
|
||||
# Spec §5.11.4 (page 44 footnote): "If retrofit insulation
|
||||
# present of unknown thickness use 50 mm". The cert encodes
|
||||
# "thickness unknown but retrofit insulation present" as the
|
||||
# "NI" sentinel which `_parse_thickness_mm` parses to 0. Without
|
||||
# this override the Table 16 row-0 lookup below returns the
|
||||
# uninsulated 2.30 W/m²K.
|
||||
# "thickness unknown but OBSERVED retrofit insulation" as the
|
||||
# "NI" sentinel (parsed to 0) + an unqualified "insulated"
|
||||
# description (no "(assumed)"). Without this override the Table
|
||||
# 16 row-0 lookup below returns the uninsulated 2.30 W/m²K.
|
||||
return 0.68 # Table 16 row 50, "Insulation at joists at ceiling level"
|
||||
if insulation_thickness_mm is not None:
|
||||
# nearest tabulated thickness <= supplied (Table 16 column (1)
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ from domain.sap10_ml.rdsap_uvalues import (
|
|||
WALL_STONE_SANDSTONE,
|
||||
WALL_SYSTEM_BUILT,
|
||||
WALL_TIMBER_FRAME,
|
||||
_insulation_bucket,
|
||||
thermal_bridging_y,
|
||||
u_door,
|
||||
u_exposed_floor,
|
||||
|
|
@ -114,6 +115,43 @@ def test_u_wall_solid_brick_with_ni_thickness_uses_50mm_row_per_table6_footnote(
|
|||
assert result == pytest.approx(0.55, abs=0.001)
|
||||
|
||||
|
||||
def test_u_wall_solid_brick_present_insulation_thin_known_thickness_not_uninsulated() -> None:
|
||||
# Arrange — a solid-brick wall lodging internal insulation (type 3)
|
||||
# with a KNOWN thin thickness (10 mm) but no documentary wall
|
||||
# thickness (so the §5.8 formula branch, which needs wall_thickness,
|
||||
# cannot fire) falls to the Table-6 bucket. A known 10 mm value sits
|
||||
# below the smallest tabulated 50 mm column, but insulation IS present
|
||||
# — it must NOT read as WORSE (higher U) than the present-but-unknown
|
||||
# case, which the footnote routes to the 50 mm row. Pre-fix,
|
||||
# `_insulation_bucket(10, present=True)` returned 0 (uninsulated,
|
||||
# U=1.7), under-crediting the wall (corpus cert 5117280: Δ −4.44).
|
||||
|
||||
# Act
|
||||
result = u_wall(
|
||||
country=Country.ENG,
|
||||
age_band="B",
|
||||
construction=WALL_SOLID_BRICK,
|
||||
insulation_thickness_mm=10,
|
||||
insulation_present=True,
|
||||
wall_insulation_type=3,
|
||||
)
|
||||
|
||||
# Assert — present insulation → 50 mm row (U=0.55), never uninsulated 1.7.
|
||||
assert result == pytest.approx(0.55, abs=0.001)
|
||||
|
||||
|
||||
def test_insulation_bucket_present_thin_thickness_rounds_up_to_50_not_zero() -> None:
|
||||
# Direct guard on the bucket: present insulation never maps to bucket 0.
|
||||
assert _insulation_bucket(10, True) == 50
|
||||
assert _insulation_bucket(24, True) == 50
|
||||
assert _insulation_bucket(None, True) == 50
|
||||
assert _insulation_bucket(50, True) == 50
|
||||
assert _insulation_bucket(100, True) == 100
|
||||
# Genuinely absent insulation still routes thin/zero to bucket 0.
|
||||
assert _insulation_bucket(10, False) == 0
|
||||
assert _insulation_bucket(None, False) == 0
|
||||
|
||||
|
||||
def test_u_wall_cavity_as_built_insulated_assumed_routes_to_as_built_row() -> None:
|
||||
# Arrange — a cavity lodged "Cavity wall, as built, insulated (assumed)"
|
||||
# with wall_insulation_type=4 is in its AS-BUILT state, NOT a retrofit
|
||||
|
|
@ -835,6 +873,34 @@ def test_u_wall_stone_granite_with_external_insulation_applies_5_8_table_14_r_va
|
|||
assert abs(result - 0.57) <= 1e-4
|
||||
|
||||
|
||||
def test_u_wall_solid_brick_external_insulation_unknown_thickness_assumes_100mm() -> None:
|
||||
# Arrange — RdSAP 10 §5.4 (PDF p.32): "the U-value with additional
|
||||
# insulation is based on the thickness of insulation of mineral wool
|
||||
# type (assume 100 mm if thickness is unknown)". A solid brick / stone
|
||||
# wall lodging External or Internal insulation (wall_insulation_type
|
||||
# 1/3) but NO measured insulation thickness (gov-API "NI" → parsed to
|
||||
# None) must take the 100 mm §5.4 default so the §5.8 Table-14 R-value
|
||||
# fires — NOT fall through to the uninsulated §5.6 U. Corpus cert
|
||||
# 200004296092 (age-B solid brick 360 mm, "with external insulation",
|
||||
# thickness NI) was billed at the uninsulated 1.40 (walls 101.7 W/K,
|
||||
# HLP 3.46) → PE +59.6, SAP −6.65. With 100 mm: U₀(§5.6, 360 mm brick)
|
||||
# + R(100 mm, λ=0.04 → 2.5) → 0.29.
|
||||
|
||||
# Act
|
||||
result = u_wall(
|
||||
country=Country.ENG,
|
||||
age_band="B",
|
||||
construction=WALL_SOLID_BRICK,
|
||||
insulation_thickness_mm=None, # gov-API "NI" — thickness unknown
|
||||
insulation_present=True,
|
||||
wall_insulation_type=1, # External
|
||||
wall_thickness_mm=360,
|
||||
)
|
||||
|
||||
# Assert — 100 mm default applied, not the uninsulated 1.40.
|
||||
assert abs(result - 0.29) <= 1e-4
|
||||
|
||||
|
||||
def test_u_wall_stone_granite_age_g_with_wall_thickness_ignores_5_6_formula_per_age_a_to_e_gate() -> None:
|
||||
# Arrange — §5.6 (PDF p.40) heading explicitly scopes the formula
|
||||
# to "age bands A to E". For age F onwards Table 6 gives literal
|
||||
|
|
@ -1059,16 +1125,41 @@ def test_u_roof_description_with_measured_transmittance_returns_parsed_value() -
|
|||
assert result == pytest.approx(0.11, abs=0.001)
|
||||
|
||||
|
||||
def test_u_roof_ni_thickness_with_insulated_description_applies_50mm_per_section_5_11_4() -> None:
|
||||
# Arrange — 346 corpus certs lodge roof_insulation_thickness="NI"
|
||||
# (Not Indicated, parsed to 0 by _parse_thickness_mm). When the
|
||||
# description also signals retrofit insulation ("Pitched, insulated
|
||||
# (assumed)" / "Flat, insulated" / "Roof room(s), insulated
|
||||
# (assumed)"), RdSAP 10 §5.11.4 (page 44) footnote applies:
|
||||
def test_u_roof_ni_thickness_with_observed_insulated_description_applies_50mm_per_section_5_11_4() -> None:
|
||||
# Arrange — a roof lodging thickness "NI" (parsed to 0) whose
|
||||
# description signals OBSERVED insulation without the "(assumed)"
|
||||
# qualifier ("Pitched, insulated" — RdSAP renders the bare form when
|
||||
# the assessor saw the insulation but not its depth). RdSAP 10
|
||||
# §5.11.4 (page 44) footnote applies:
|
||||
# "If retrofit insulation present of unknown thickness use 50 mm".
|
||||
# That maps to Table 16 row "50 mm at joists at ceiling level" = 0.68
|
||||
# W/m²K — vs the current 2.30 we return when thickness=0 hits the
|
||||
# Table 16 row-0 lookup.
|
||||
# W/m²K — vs the 2.30 the Table 16 row-0 lookup would give.
|
||||
|
||||
# Act
|
||||
result = u_roof(
|
||||
country=Country.ENG,
|
||||
age_band="C",
|
||||
insulation_thickness_mm=0, # parsed from "NI"
|
||||
description="Pitched, insulated",
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert result == pytest.approx(0.68, abs=0.01)
|
||||
|
||||
|
||||
def test_u_roof_ni_thickness_with_assumed_insulated_description_uses_table18_default() -> None:
|
||||
# Arrange — RdSAP renders "insulated (assumed)" when insulation
|
||||
# presence is ASSUMED (age-band / as-built assumption), not an
|
||||
# observed retrofit — so the §5.11.4 "retrofit insulation present of
|
||||
# unknown thickness use 50 mm" footnote does NOT apply; the thickness
|
||||
# "cannot be determined" and Table 18's age-band default governs
|
||||
# (§5.11.4 opening clause). The corpus lodges the SAME description
|
||||
# with thickness "ND" (102 certs → already Table 18 via the
|
||||
# thickness-None fallthrough) or "NI" (27 certs → previously took the
|
||||
# 50 mm row 0.68), purely per lodging software; both must land on the
|
||||
# same Table 18 value (band C pitched → 0.40). The loc-4 NI cohort
|
||||
# was systematically under-rated by ~1 SAP before this (e.g. corpus
|
||||
# cert 10094975827 band B: Δ −1.87).
|
||||
|
||||
# Act
|
||||
result = u_roof(
|
||||
|
|
@ -1078,8 +1169,8 @@ def test_u_roof_ni_thickness_with_insulated_description_applies_50mm_per_section
|
|||
description="Pitched, insulated (assumed)",
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert result == pytest.approx(0.68, abs=0.01)
|
||||
# Assert — Table 18 column (1) band C.
|
||||
assert result == pytest.approx(0.40, abs=0.01)
|
||||
|
||||
|
||||
def test_u_roof_ni_thickness_with_no_insulation_description_stays_at_2_30() -> None:
|
||||
|
|
@ -1324,18 +1415,20 @@ def test_u_roof_vaulted_ni_unknown_band_j_uses_col1_age_band_not_50mm() -> None:
|
|||
assert abs(result - 0.16) <= 1e-4
|
||||
|
||||
|
||||
def test_u_roof_normal_pitched_ni_insulated_still_returns_50mm_per_5_11_4() -> None:
|
||||
def test_u_roof_normal_pitched_ni_observed_insulated_still_returns_50mm_per_5_11_4() -> None:
|
||||
# Arrange — regression guard: the is_sloping_ceiling flag defaults
|
||||
# False, so a NORMAL pitched roof (with loft) lodged NI + "insulated
|
||||
# (assumed)" must STILL hit the §5.11.4 retrofit-50 mm row (U=0.68).
|
||||
# Same inputs as the sloping test above minus is_sloping_ceiling.
|
||||
# False, so a NORMAL pitched roof (with loft) lodged NI + an
|
||||
# unqualified "insulated" description (observed retrofit, no
|
||||
# "(assumed)") must STILL hit the §5.11.4 retrofit-50 mm row
|
||||
# (U=0.68). The "(assumed)" form routes to Table 18 instead — see
|
||||
# test_u_roof_ni_thickness_with_assumed_insulated_description_uses_table18_default.
|
||||
|
||||
# Act
|
||||
result = u_roof(
|
||||
country=Country.ENG,
|
||||
age_band="J",
|
||||
insulation_thickness_mm=0,
|
||||
description="Pitched, insulated (assumed)",
|
||||
description="Pitched, insulated",
|
||||
)
|
||||
|
||||
# Assert
|
||||
|
|
|
|||
|
|
@ -273,10 +273,13 @@ def candidate_recommendations(
|
|||
if solar_insights is not None and "solarPotential" in solar_insights
|
||||
else None
|
||||
)
|
||||
# The SAP design heat loss sizes the ASHP to the dwelling (ADR-0049).
|
||||
design_heat_loss_kw: float = Sap10Calculator().calculate(epc).design_heat_loss_kw
|
||||
return _candidate_recommendations(
|
||||
epc,
|
||||
products or ProductJsonRepository(catalogue_path),
|
||||
planning_restrictions,
|
||||
solar_potential,
|
||||
considered_measures,
|
||||
design_heat_loss_kw,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -264,6 +264,7 @@ def _candidate_recommendations(
|
|||
planning_restrictions: PlanningRestrictions,
|
||||
solar_potential: Optional[SolarPotential],
|
||||
considered_measures: Optional[frozenset[MeasureType]],
|
||||
design_heat_loss_kw: Optional[float] = None,
|
||||
) -> list[Recommendation]:
|
||||
"""Run the applicable Recommendation Generators; keep the ones that apply.
|
||||
Solid-wall insulation, glazing, heating and solar are additionally gated by
|
||||
|
|
@ -331,7 +332,13 @@ def _candidate_recommendations(
|
|||
MeasureType.SYSTEM_TUNE_UP,
|
||||
MeasureType.SYSTEM_TUNE_UP_ZONED,
|
||||
),
|
||||
lambda: recommend_heating(effective_epc, products, planning_restrictions, considered_measures),
|
||||
lambda: recommend_heating(
|
||||
effective_epc,
|
||||
products,
|
||||
planning_restrictions,
|
||||
considered_measures,
|
||||
design_heat_loss_kw=design_heat_loss_kw,
|
||||
),
|
||||
),
|
||||
(
|
||||
admitted(MeasureType.SECONDARY_HEATING_REMOVAL),
|
||||
|
|
@ -384,9 +391,20 @@ def _scored_candidate_groups(
|
|||
) -> list[list[ScoredOption]]:
|
||||
"""One group per Recommendation: each Option scored independently against
|
||||
the baseline (role-1 warm-start signal, ADR-0016)."""
|
||||
# The SAP design heat loss sizes the ASHP to the dwelling (ADR-0049); read it
|
||||
# off a baseline score, which the group scoring computes anyway.
|
||||
baseline_result = scorer.score(effective_epc, []).sap_result
|
||||
design_heat_loss_kw: Optional[float] = (
|
||||
baseline_result.design_heat_loss_kw if baseline_result is not None else None
|
||||
)
|
||||
groups: list[list[ScoredOption]] = []
|
||||
for recommendation in _candidate_recommendations(
|
||||
effective_epc, products, planning_restrictions, solar_potential, considered_measures
|
||||
effective_epc,
|
||||
products,
|
||||
planning_restrictions,
|
||||
solar_potential,
|
||||
considered_measures,
|
||||
design_heat_loss_kw,
|
||||
):
|
||||
options = list(recommendation.options)
|
||||
impacts: list[MeasureImpact] = independent_option_impacts(
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ BASE_PATH = (
|
|||
"Sero Project Documents/Property Folders"
|
||||
)
|
||||
ASSESSMENT_SUBFOLDER = "A. Assessment"
|
||||
BATCH_FOLDER_PREFIX = "_Sero Batch"
|
||||
|
||||
logger = setup_logger()
|
||||
|
||||
|
|
@ -65,7 +66,18 @@ class SharepointRenamerOrchestrator:
|
|||
self._csv_path = csv_path
|
||||
self._dry_run = dry_run
|
||||
|
||||
def _discover_roots(self) -> list[str]:
|
||||
contents = self._sp_client.get_folders_in_path(BASE_PATH)
|
||||
batch_roots = sorted(
|
||||
f"{BASE_PATH}/{item['name']}"
|
||||
for item in contents.get("value", [])
|
||||
if "folder" in item
|
||||
and item["name"].lower().startswith(BATCH_FOLDER_PREFIX.lower())
|
||||
)
|
||||
return [BASE_PATH, *batch_roots]
|
||||
|
||||
def run(self) -> None:
|
||||
roots = self._discover_roots()
|
||||
with open(self._csv_path, newline="", encoding="utf-8-sig") as f:
|
||||
reader = csv.DictReader(f)
|
||||
required = {"UPRN", "Address", "Postcode"}
|
||||
|
|
@ -75,23 +87,37 @@ class SharepointRenamerOrchestrator:
|
|||
)
|
||||
|
||||
for row in reader:
|
||||
uprn = row["UPRN"].strip()
|
||||
address = row["Address"].strip()
|
||||
postcode = row["Postcode"].strip()
|
||||
folder_path = (
|
||||
f"{BASE_PATH}/{address}, {postcode}"
|
||||
f"/{SharepointSubfolders.ASSESSMENT.value}/{ASSESSMENT_SUBFOLDER}"
|
||||
self._process_row(
|
||||
roots,
|
||||
uprn=row["UPRN"].strip(),
|
||||
address=row["Address"].strip(),
|
||||
postcode=row["Postcode"].strip(),
|
||||
)
|
||||
self._process_folder(folder_path, uprn, address, postcode)
|
||||
|
||||
def _process_row(
|
||||
self, roots: list[str], uprn: str, address: str, postcode: str
|
||||
) -> None:
|
||||
# Batch folders name properties "{uprn}_{address}"; filenames must not
|
||||
# repeat the UPRN, so the folder lookup and the canonical name diverge.
|
||||
display_address = address.removeprefix(f"{uprn}_")
|
||||
for root in roots:
|
||||
folder_path = (
|
||||
f"{root}/{address}, {postcode}"
|
||||
f"/{SharepointSubfolders.ASSESSMENT.value}/{ASSESSMENT_SUBFOLDER}"
|
||||
)
|
||||
if self._process_folder(folder_path, uprn, display_address, postcode):
|
||||
return
|
||||
logger.warning(
|
||||
f"Missing folder for UPRN {uprn} in any root: {address}, {postcode}"
|
||||
)
|
||||
|
||||
def _process_folder(
|
||||
self, folder_path: str, uprn: str, address: str, postcode: str
|
||||
) -> None:
|
||||
) -> bool:
|
||||
try:
|
||||
contents = self._sp_client.get_folders_in_path(folder_path)
|
||||
except ValueError:
|
||||
logger.warning(f"Missing folder for UPRN {uprn}: {folder_path}")
|
||||
return
|
||||
return False
|
||||
|
||||
for item in contents.get("value", []):
|
||||
if "folder" in item:
|
||||
|
|
@ -123,3 +149,4 @@ class SharepointRenamerOrchestrator:
|
|||
logger.error(
|
||||
f'Failed to rename "{original_name}" → "{new_name}" (UPRN: {uprn}): {e}'
|
||||
)
|
||||
return True
|
||||
|
|
|
|||
243
scripts/corpus_1000/build_worklist.py
Normal file
243
scripts/corpus_1000/build_worklist.py
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
"""Build the RdSAP-21.0.1 corpus campaign ledger (scripts/corpus_1000/worklist.md).
|
||||
|
||||
Runs all 1000 corpus certs through the gauge's own path
|
||||
(from_api_response -> Sap10Calculator.calculate, mirroring
|
||||
tests/infrastructure/epc_client/test_sap_accuracy_corpus.py), ranks by
|
||||
|engine - lodged| descending, attaches a short dwelling signature per cert,
|
||||
and groups same-signature certs into named clusters.
|
||||
|
||||
USAGE
|
||||
-----
|
||||
PYTHONPATH=/workspaces/model python scripts/corpus_1000/build_worklist.py
|
||||
|
||||
Rewrites worklist.md in place, PRESERVING existing per-cert statuses
|
||||
([x] / 🔧 / ⚠ / ⛔ and trailing `<-` notes) keyed by uprn, so it doubles as
|
||||
the re-rank step after each fix lands.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
from datatypes.epc.domain.mapper import EpcPropertyDataMapper
|
||||
from domain.sap10_calculator.calculator import Sap10Calculator
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
CORPUS = ROOT / "backend/epc_api/json_samples/RdSAP-Schema-21.0.1/corpus.jsonl"
|
||||
WORKLIST = ROOT / "scripts/corpus_1000/worklist.md"
|
||||
|
||||
# Table 12 fuel names (abbreviated) for the signature column.
|
||||
_FUEL = {
|
||||
1: "gas", 2: "gas-bulk", 3: "biogas", 4: "biogas", 7: "bLPG", 9: "LPG",
|
||||
17: "LPG-sc", 10: "oil", 15: "bioliquid", 16: "B30K", 18: "rapeseed",
|
||||
58: "biodiesel", 51: "housecoal", 20: "coal-anth", 21: "smokeless",
|
||||
22: "anthracite", 23: "manuf-smokeless", 12: "wood-log", 13: "wood-pellet",
|
||||
75: "wood-pellet-bag", 71: "wood-chip", 48: "dual-mineral-wood",
|
||||
26: "gas", 25: "elec", 29: "elec", 30: "elec7hr", 31: "elec10hr",
|
||||
32: "elec18hr", 33: "elec24hr", 34: "elec-heatnet", 35: "elec",
|
||||
36: "elec", 37: "elec", 47: "heatnet", 6: "heatnet",
|
||||
}
|
||||
|
||||
|
||||
def _fuel_name(code: Optional[int]) -> str:
|
||||
if code is None:
|
||||
return "?"
|
||||
return _FUEL.get(code, f"fuel{code}")
|
||||
|
||||
|
||||
def _lead(desc: Optional[str], n: int = 3) -> str:
|
||||
"""First n comma/word tokens of a register description, lowercased."""
|
||||
if not desc:
|
||||
return "?"
|
||||
return " ".join(desc.lower().replace(",", " ").split()[:n])
|
||||
|
||||
|
||||
def signature(doc: dict[str, Any]) -> tuple[str, str]:
|
||||
"""(cluster_key, human signature) for one cert."""
|
||||
h = doc.get("sap_heating") or {}
|
||||
mhd = (h.get("main_heating_details") or [{}])
|
||||
m0 = mhd[0] if mhd else {}
|
||||
code = m0.get("sap_main_heating_code")
|
||||
idx = m0.get("main_heating_index_number")
|
||||
fuel = _fuel_name(m0.get("main_fuel_type"))
|
||||
heat = f"h{code or ('pcdb' + str(idx) if idx else '?')}/{fuel}"
|
||||
if len(mhd) > 1:
|
||||
heat += "+2mains"
|
||||
|
||||
walls = doc.get("walls") or []
|
||||
wall = _lead(walls[0].get("description") if walls else None, 2)
|
||||
roofs = doc.get("roofs") or []
|
||||
roof = _lead(roofs[0].get("description") if roofs else None, 2)
|
||||
|
||||
es = doc.get("sap_energy_source") or {}
|
||||
sec = (doc.get("secondary_heating") or {}).get("description") or "None"
|
||||
feats: list[str] = []
|
||||
pv = es.get("photovoltaic_supply") or {}
|
||||
if es.get("pv_connection") not in (0, None) or not pv.get("none_or_no_details"):
|
||||
feats.append("PV")
|
||||
if (es.get("wind_turbines_count") or 0) > 0:
|
||||
feats.append("wind")
|
||||
if sec != "None":
|
||||
feats.append("sec:" + _lead(sec, 2))
|
||||
if (doc.get("extensions_count") or 0) > 0:
|
||||
feats.append(f"ext{doc.get('extensions_count')}")
|
||||
if (doc.get("conservatory_type") or 1) != 1:
|
||||
feats.append(f"consv{doc.get('conservatory_type')}")
|
||||
for bp in doc.get("sap_building_parts") or []:
|
||||
if bp.get("sap_room_in_roof") is not None:
|
||||
feats.append("RR")
|
||||
break
|
||||
mv = doc.get("mechanical_ventilation")
|
||||
if mv not in (0, None):
|
||||
feats.append(f"mv{mv}")
|
||||
if (h.get("water_heating_code") or 901) != 901:
|
||||
feats.append(f"whw{h.get('water_heating_code')}")
|
||||
|
||||
key = f"{heat} | {wall} | {roof}" + (" | " + ",".join(sorted(set(feats))) if feats else "")
|
||||
sig = key
|
||||
return key, sig
|
||||
|
||||
|
||||
_LINE_RE = re.compile(
|
||||
r"^- \[(?P<st>[ x])\] |^- (?P<emoji>🔧|⚠|⛔) "
|
||||
)
|
||||
|
||||
|
||||
def load_existing_statuses() -> dict[str, tuple[str, str]]:
|
||||
"""uprn -> (status token, trailing note) from the current worklist."""
|
||||
out: dict[str, tuple[str, str]] = {}
|
||||
if not WORKLIST.exists():
|
||||
return out
|
||||
for line in WORKLIST.read_text().splitlines():
|
||||
m = re.match(r"^- (?:\[(?P<st>[ x])\]|(?P<emoji>🔧|⚠|⛔)) `(?P<uprn>\w+)`", line)
|
||||
if not m:
|
||||
continue
|
||||
status = f"[{m.group('st')}]" if m.group("st") is not None else m.group("emoji")
|
||||
note = ""
|
||||
if "<-" in line:
|
||||
note = "<-" + line.split("<-", 1)[1]
|
||||
out[m.group("uprn")] = (status, note)
|
||||
return out
|
||||
|
||||
|
||||
def main() -> None:
|
||||
docs = [json.loads(l) for l in CORPUS.read_text().splitlines() if l.strip()]
|
||||
calc = Sap10Calculator()
|
||||
rows: list[dict[str, Any]] = []
|
||||
skipped: list[tuple[str, str]] = []
|
||||
sap_abs: list[float] = []
|
||||
|
||||
for idx, doc in enumerate(docs, 1):
|
||||
# Some corpus docs lodge no top-level uprn; fall back to the corpus
|
||||
# line number so ledger statuses don't collide on "None".
|
||||
uprn = str(doc.get("uprn") or f"line{idx}")
|
||||
lodged = doc.get("energy_rating_current")
|
||||
if lodged is None:
|
||||
skipped.append((uprn, "no lodged SAP"))
|
||||
continue
|
||||
try:
|
||||
epc = EpcPropertyDataMapper.from_api_response(doc)
|
||||
result = calc.calculate(epc)
|
||||
except Exception as e:
|
||||
skipped.append((uprn, f"{type(e).__name__}: {str(e)[:70]}"))
|
||||
continue
|
||||
eng = result.sap_score_continuous
|
||||
delta = eng - lodged
|
||||
sap_abs.append(abs(delta))
|
||||
key, sig = signature(doc)
|
||||
rows.append({
|
||||
"uprn": uprn, "lodged": lodged, "eng": eng, "delta": delta,
|
||||
"key": key, "sig": sig,
|
||||
})
|
||||
|
||||
n = len(sap_abs)
|
||||
within = sum(1 for e in sap_abs if e < 0.5) / n
|
||||
mae = sum(sap_abs) / n
|
||||
|
||||
# Cluster: same signature key, only certs still outside 0.5.
|
||||
clusters: dict[str, list[dict[str, Any]]] = defaultdict(list)
|
||||
for r in rows:
|
||||
if abs(r["delta"]) >= 0.5:
|
||||
clusters[r["key"]].append(r)
|
||||
ranked = sorted(
|
||||
clusters.items(),
|
||||
key=lambda kv: -sum(abs(r["delta"]) for r in kv[1]),
|
||||
)
|
||||
|
||||
prev = load_existing_statuses()
|
||||
|
||||
lines: list[str] = []
|
||||
lines.append("# RdSAP-21.0.1 corpus campaign ledger")
|
||||
lines.append("")
|
||||
lines.append(
|
||||
f"**Gauge:** {n} computed / {len(skipped)} skipped · "
|
||||
f"SAP within-0.5 = **{within:.1%}** · MAE = **{mae:.3f}** "
|
||||
f"(floors in test_sap_accuracy_corpus.py)"
|
||||
)
|
||||
lines.append("")
|
||||
lines.append(
|
||||
"Statuses: `[ ]` todo · `[x]` resolved ≤0.5 · 🔧 fix landed · "
|
||||
"⚠ xfail engine bug · ⛔ unbuildable. "
|
||||
"Line: `uprn · lodged L / eng E / Δ · signature`. "
|
||||
"Rebuild/re-rank: `PYTHONPATH=. python scripts/corpus_1000/build_worklist.py` "
|
||||
"(preserves statuses & `<-` notes)."
|
||||
)
|
||||
lines.append("")
|
||||
lines.append(f"## Clusters (certs with |Δ| ≥ 0.5, ranked by summed |Δ|) — {sum(len(v) for v in clusters.values())} certs in {len(ranked)} clusters")
|
||||
lines.append("")
|
||||
|
||||
for i, (key, members) in enumerate(ranked, 1):
|
||||
members.sort(key=lambda r: -abs(r["delta"]))
|
||||
tot = sum(abs(r["delta"]) for r in members)
|
||||
lines.append(
|
||||
f"### C{i:03d} · Σ|Δ| {tot:.1f} · {len(members)} certs · `{key}`"
|
||||
)
|
||||
for r in members:
|
||||
st, note = prev.get(r["uprn"], ("[ ]", ""))
|
||||
frag = (
|
||||
f"- {st} `{r['uprn']}` · L {r['lodged']} / E {r['eng']:.2f} / "
|
||||
f"Δ {r['delta']:+.2f}"
|
||||
)
|
||||
if note:
|
||||
frag += f" {note}"
|
||||
lines.append(frag)
|
||||
lines.append("")
|
||||
|
||||
if skipped:
|
||||
lines.append(f"## Skipped ({len(skipped)})")
|
||||
for uprn, why in skipped:
|
||||
st, note = prev.get(uprn, ("⛔", ""))
|
||||
lines.append(f"- {st} `{uprn}` · {why} {note}".rstrip())
|
||||
lines.append("")
|
||||
|
||||
resolved = [r for r in rows if abs(r["delta"]) < 0.5]
|
||||
lines.append(f"## Resolved ≤0.5 ({len(resolved)} certs — no action)")
|
||||
lines.append("")
|
||||
lines.append("<details><summary>expand</summary>")
|
||||
lines.append("")
|
||||
for r in sorted(resolved, key=lambda r: -abs(r["delta"])):
|
||||
st, note = prev.get(r["uprn"], ("[x]", ""))
|
||||
if st == "[ ]":
|
||||
st = "[x]"
|
||||
lines.append(
|
||||
f"- {st} `{r['uprn']}` · L {r['lodged']} / E {r['eng']:.2f} / "
|
||||
f"Δ {r['delta']:+.2f} · `{r['key']}` {note}".rstrip()
|
||||
)
|
||||
lines.append("")
|
||||
lines.append("</details>")
|
||||
lines.append("")
|
||||
|
||||
WORKLIST.write_text("\n".join(lines))
|
||||
print(f"wrote {WORKLIST}")
|
||||
print(f"gauge: {n} computed / {len(skipped)} skipped, within-0.5 {within:.1%}, MAE {mae:.3f}")
|
||||
print(f"clusters: {len(ranked)}, worst 5:")
|
||||
for key, members in ranked[:5]:
|
||||
tot = sum(abs(r["delta"]) for r in members)
|
||||
print(f" Σ{tot:7.1f} n={len(members):3d} {key}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
58
scripts/corpus_1000/demand_cohort.py
Normal file
58
scripts/corpus_1000/demand_cohort.py
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
"""Demand-driven divergence analyzer. Flags corpus certs where the SAP miss is
|
||||
driven by FABRIC/DEMAND (PE & CO2 move with SAP), not cost — the fixable class.
|
||||
Tags nbp (building parts) and room-in-roof so multi-part / RR bugs surface."""
|
||||
from __future__ import annotations
|
||||
import json
|
||||
from pathlib import Path
|
||||
from datatypes.epc.domain.mapper import EpcPropertyDataMapper
|
||||
from domain.sap10_calculator.calculator import calculate_sap_from_inputs
|
||||
from domain.sap10_calculator.rdsap.cert_to_inputs import (
|
||||
SAP_10_2_SPEC_PRICES, cert_to_demand_inputs, cert_to_inputs)
|
||||
|
||||
CORPUS = Path("backend/epc_api/json_samples/RdSAP-Schema-21.0.1/corpus.jsonl")
|
||||
|
||||
def has_rr(doc):
|
||||
for bp in doc.get("sap_building_parts") or []:
|
||||
r = bp.get("room_in_roof") or bp.get("rooms_in_roof")
|
||||
if r: return True
|
||||
for r in doc.get("roofs") or []:
|
||||
if "room in roof" in (r.get("description") or "").lower(): return True
|
||||
return False
|
||||
|
||||
rows = []
|
||||
for line in CORPUS.read_text().splitlines():
|
||||
doc = json.loads(line)
|
||||
L = doc.get("energy_rating_current"); peL = doc.get("energy_consumption_current")
|
||||
co2L = doc.get("co2_emissions_current")
|
||||
if L is None: continue
|
||||
try:
|
||||
epc = EpcPropertyDataMapper.from_api_response(doc)
|
||||
r = calculate_sap_from_inputs(cert_to_inputs(epc, prices=SAP_10_2_SPEC_PRICES))
|
||||
d = calculate_sap_from_inputs(cert_to_demand_inputs(epc, prices=SAP_10_2_SPEC_PRICES))
|
||||
except Exception:
|
||||
continue
|
||||
dsap = r.sap_score_continuous - L
|
||||
if abs(dsap) < 0.5: continue
|
||||
peE = getattr(d, "primary_energy_kwh_per_m2", None)
|
||||
dpe = (peE - peL) if (peE is not None and peL is not None) else None
|
||||
co2E = d.co2_kg_per_yr/1000 if d.co2_kg_per_yr is not None else None
|
||||
dco2 = (co2E - co2L) if (co2E is not None and co2L is not None) else None
|
||||
nbp = len(doc.get("sap_building_parts") or [])
|
||||
rr = has_rr(doc)
|
||||
# demand-driven: PE moves same direction as SAP-miss inverse (PE up -> SAP down)
|
||||
# i.e. dpe and dsap have OPPOSITE signs, and |dpe| is material
|
||||
demand = (dpe is not None and abs(dpe) >= 4 and (dpe > 0) == (dsap < 0))
|
||||
rows.append((doc.get("uprn"), L, round(r.sap_score_continuous,1), round(dsap,2),
|
||||
round(dpe,1) if dpe is not None else None,
|
||||
round(dco2,3) if dco2 is not None else None, nbp, rr, demand))
|
||||
|
||||
demand_rows = [r for r in rows if r[8]]
|
||||
demand_rows.sort(key=lambda x:-abs(x[3]))
|
||||
print(f"diverging certs: {len(rows)} | demand-driven: {len(demand_rows)}")
|
||||
mp = [r for r in demand_rows if r[6] > 1]
|
||||
rr = [r for r in demand_rows if r[7]]
|
||||
print(f" demand+multipart(nbp>1): {len(mp)} | demand+RR: {len(rr)}")
|
||||
print(f"\n=== top demand-driven divergences ===")
|
||||
print(f"{'uprn':13s} {'L':>3} {'E':>5} {'dSAP':>6} {'dPE':>6} {'dCO2':>6} {'nbp':>3} {'RR':>3}")
|
||||
for r in demand_rows[:32]:
|
||||
print(f"{str(r[0]):13s} {r[1]:3d} {r[2]:5.1f} {r[3]:+6.2f} {str(r[4]):>6} {str(r[5]):>6} {r[6]:3d} {'Y' if r[7] else '':>3}")
|
||||
61
scripts/corpus_1000/elec_cohort.py
Normal file
61
scripts/corpus_1000/elec_cohort.py
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
"""Electric-main cohort analysis: for every corpus cert whose main heating is
|
||||
electric, dump heating SAP code / tariff (meter_type) / lodged vs engine SAP /
|
||||
cost-vs-demand signature. Used to steer a spec-grounded Table-12a tariff fix
|
||||
without regressing in-band certs."""
|
||||
from __future__ import annotations
|
||||
import json
|
||||
from pathlib import Path
|
||||
from datatypes.epc.domain.mapper import EpcPropertyDataMapper
|
||||
from domain.sap10_calculator.calculator import calculate_sap_from_inputs
|
||||
from domain.sap10_calculator.rdsap.cert_to_inputs import (
|
||||
SAP_10_2_SPEC_PRICES, cert_to_demand_inputs, cert_to_inputs, _is_electric_main,
|
||||
_first_main_heating,
|
||||
)
|
||||
|
||||
CORPUS = Path("backend/epc_api/json_samples/RdSAP-Schema-21.0.1/corpus.jsonl")
|
||||
|
||||
def hcode(doc):
|
||||
sh = doc.get("sap_heating") or {}
|
||||
ds = sh.get("main_heating_details") or [{}]
|
||||
return ds[0].get("main_heating_category"), ds[0].get("main_fuel_type")
|
||||
|
||||
rows = []
|
||||
for line in CORPUS.read_text().splitlines():
|
||||
doc = json.loads(line)
|
||||
try:
|
||||
epc = EpcPropertyDataMapper.from_api_response(doc)
|
||||
except Exception:
|
||||
continue
|
||||
main = _first_main_heating(epc)
|
||||
if not _is_electric_main(main):
|
||||
continue
|
||||
try:
|
||||
r = calculate_sap_from_inputs(cert_to_inputs(epc, prices=SAP_10_2_SPEC_PRICES))
|
||||
d = calculate_sap_from_inputs(cert_to_demand_inputs(epc, prices=SAP_10_2_SPEC_PRICES))
|
||||
except Exception as e:
|
||||
continue
|
||||
L = doc.get("energy_rating_current"); pe_l = doc.get("energy_consumption_current")
|
||||
if L is None: continue
|
||||
dsap = r.sap_score_continuous - L
|
||||
dpe = (d.pe_per_m2 - pe_l) if (pe_l is not None and hasattr(d,'pe_per_m2')) else None
|
||||
cat, fuel = hcode(doc)
|
||||
mt = (doc.get("sap_energy_source") or {}).get("meter_type")
|
||||
sh = doc.get("sap_heating") or {}
|
||||
code = (sh.get("main_heating_details") or [{}])[0].get("main_heating_code") \
|
||||
or (sh.get("main_heating_details") or [{}])[0].get("main_heating_index_number")
|
||||
rows.append((doc.get("uprn"), code, cat, mt, sh.get("water_heating_code"),
|
||||
L, round(r.sap_score_continuous,1), round(dsap,2)))
|
||||
|
||||
rows.sort(key=lambda x:-abs(x[7]))
|
||||
print(f"electric-main certs: {len(rows)}")
|
||||
print(f"{'uprn':14s} {'hcode':>6} {'cat':>4} {'mtr':>4} {'whc':>5} {'L':>3} {'E':>6} {'dSAP':>6}")
|
||||
for r in rows:
|
||||
print(f"{str(r[0]):14s} {str(r[1]):>6} {str(r[2]):>4} {str(r[3]):>4} {str(r[4]):>5} {r[5]:3d} {r[6]:6.1f} {r[7]:+6.2f}")
|
||||
# summary by (hcode, meter)
|
||||
from collections import defaultdict
|
||||
agg = defaultdict(lambda:[0,0.0,0,0])
|
||||
for r in rows:
|
||||
k=(r[1], r[3]); a=agg[k]; a[0]+=1; a[1]+=r[7]; a[2]+=(abs(r[7])>0.5); a[3]+=(abs(r[7])<=0.5)
|
||||
print("\n=== by (hcode, meter): n, meanΔ, #div>0.5, #inband ===")
|
||||
for k,a in sorted(agg.items(), key=lambda x:-abs(x[1][1])):
|
||||
print(f" code {str(k[0]):>6} meter {str(k[1]):>4}: n={a[0]:3d} meanΔ={a[1]/a[0]:+6.2f} div={a[2]:3d} inband={a[3]:3d}")
|
||||
46
scripts/corpus_1000/field_bias.py
Normal file
46
scripts/corpus_1000/field_bias.py
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
"""Field-level systematic-bias scan: mean Δ(SAP) grouped by each mapped
|
||||
attribute. A subgroup with large |meanΔ| AND enough n is a systematic mapping
|
||||
error for that attribute (would cancel in the overall ~0 bias)."""
|
||||
from __future__ import annotations
|
||||
import json, statistics
|
||||
from collections import defaultdict
|
||||
from datatypes.epc.domain.mapper import EpcPropertyDataMapper
|
||||
from domain.sap10_calculator.calculator import calculate_sap_from_inputs
|
||||
from domain.sap10_calculator.rdsap.cert_to_inputs import SAP_10_2_SPEC_PRICES, cert_to_inputs
|
||||
CORPUS="backend/epc_api/json_samples/RdSAP-Schema-21.0.1/corpus.jsonl"
|
||||
G=defaultdict(lambda: defaultdict(list))
|
||||
def gv(x): return x.get('value') if isinstance(x,dict) else x
|
||||
for line in open(CORPUS):
|
||||
d=json.loads(line); L=d.get('energy_rating_current')
|
||||
if L is None: continue
|
||||
try:
|
||||
epc=EpcPropertyDataMapper.from_api_response(d)
|
||||
r=calculate_sap_from_inputs(cert_to_inputs(epc,prices=SAP_10_2_SPEC_PRICES))
|
||||
except Exception: continue
|
||||
dsap=r.sap_score_continuous-L
|
||||
bp=(d.get('sap_building_parts') or [{}])[0]
|
||||
sh=d.get('sap_heating') or {}; md=(sh.get('main_heating_details') or [{}])[0]
|
||||
G['wall_con'][bp.get('wall_construction')].append(dsap)
|
||||
G['wall_ins'][bp.get('wall_insulation_type')].append(dsap)
|
||||
G['wall_con+ins'][(bp.get('wall_construction'),bp.get('wall_insulation_type'))].append(dsap)
|
||||
G['roof_con'][bp.get('roof_construction')].append(dsap)
|
||||
G['floor_hl'][bp.get('floor_heat_loss')].append(dsap)
|
||||
G['age'][bp.get('construction_age_band')].append(dsap)
|
||||
G['main_cat'][md.get('main_heating_category')].append(dsap)
|
||||
G['whc'][sh.get('water_heating_code')].append(dsap)
|
||||
G['meter'][(d.get('sap_energy_source') or {}).get('meter_type')].append(dsap)
|
||||
G['country'][d.get('country_code')].append(dsap)
|
||||
G['dwelling'][d.get('dwelling_type')].append(dsap)
|
||||
G['nbp'][len(d.get('sap_building_parts') or [])].append(dsap)
|
||||
G['glz'][tuple(sorted(set(w.get('glazing_type') for w in (d.get('sap_windows') or []))))[:1]].append(dsap)
|
||||
print("=== subgroups with |meanΔ| >= 0.5 and n >= 8 (systematic-bias candidates) ===")
|
||||
hits=[]
|
||||
for field, groups in G.items():
|
||||
for key, vals in groups.items():
|
||||
if len(vals) >= 8:
|
||||
m=statistics.mean(vals)
|
||||
if abs(m) >= 0.5:
|
||||
hits.append((abs(m), field, key, len(vals), m, statistics.median(vals)))
|
||||
for am,field,key,n,m,med in sorted(hits, reverse=True):
|
||||
print(f" {field:12s} = {str(key):20s} n={n:4d} meanΔ={m:+.2f} medianΔ={med:+.2f}")
|
||||
if not hits: print(" (none — no systematic subgroup bias)")
|
||||
56
scripts/corpus_1000/mapper_audit.py
Normal file
56
scripts/corpus_1000/mapper_audit.py
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
"""Mapper faithfulness audit: for identity pass-through fields (dims, counts,
|
||||
thicknesses) compare the RAW cert value to what EpcPropertyDataMapper produced.
|
||||
Any mismatch is a mapper bug silently feeding the SAP engine wrong inputs."""
|
||||
from __future__ import annotations
|
||||
import json
|
||||
from collections import defaultdict
|
||||
from datatypes.epc.domain.mapper import EpcPropertyDataMapper
|
||||
CORPUS="backend/epc_api/json_samples/RdSAP-Schema-21.0.1/corpus.jsonl"
|
||||
def gv(x): return x.get('value') if isinstance(x,dict) else x
|
||||
def num(x):
|
||||
x=gv(x)
|
||||
if x is None: return None
|
||||
s=str(x).lower().replace('mm','').replace('m2','').strip()
|
||||
try: return float(s)
|
||||
except: return s # keep strings like 'measured'/'NI' for equality
|
||||
mism=defaultdict(list)
|
||||
n=0
|
||||
for line in open(CORPUS):
|
||||
d=json.loads(line)
|
||||
try: epc=EpcPropertyDataMapper.from_api_response(d)
|
||||
except Exception: continue
|
||||
n+=1; u=d.get('uprn')
|
||||
# top-level counts
|
||||
checks=[('door_count', d.get('door_count'), epc.door_count),
|
||||
('habitable', d.get('habitable_room_count'), getattr(epc,'habitable_room_count',None)),
|
||||
('led', d.get('led_fixed_lighting_bulbs_count'), epc.led_fixed_lighting_bulbs_count),
|
||||
('cfl', d.get('cfl_fixed_lighting_bulbs_count'), epc.cfl_fixed_lighting_bulbs_count),
|
||||
('incand', d.get('incandescent_fixed_lighting_bulbs_count'), epc.incandescent_fixed_lighting_bulbs_count)]
|
||||
for name,rv,ev in checks:
|
||||
if rv is not None and ev is not None and num(rv)!=num(ev):
|
||||
mism[name].append((u,rv,ev))
|
||||
# per-building-part fabric
|
||||
rbps=d.get('sap_building_parts') or []; ebps=epc.sap_building_parts or []
|
||||
for i,rbp in enumerate(rbps):
|
||||
if i>=len(ebps):
|
||||
mism['DROPPED_building_part'].append((u,len(rbps),len(ebps))); break
|
||||
ebp=ebps[i]
|
||||
for name,rv,ev in [('wall_thick',rbp.get('wall_thickness'),getattr(ebp,'wall_thickness_mm',None)),
|
||||
('wall_ins_thick',rbp.get('wall_insulation_thickness'),getattr(ebp,'wall_insulation_thickness',None)),
|
||||
('roof_ins_thick',rbp.get('roof_insulation_thickness'),getattr(ebp,'roof_insulation_thickness',None)),
|
||||
('wall_con',rbp.get('wall_construction'),getattr(ebp,'wall_construction',None)),
|
||||
('wall_ins_type',rbp.get('wall_insulation_type'),getattr(ebp,'wall_insulation_type',None))]:
|
||||
if rv is not None and ev is not None and num(rv)!=num(ev):
|
||||
mism[name].append((u,rv,ev))
|
||||
# windows: count + total area
|
||||
rw=d.get('sap_windows') or []; ew=epc.sap_windows or []
|
||||
if len(rw)!=len(ew): mism['window_count'].append((u,len(rw),len(ew)))
|
||||
ra=sum((gv(w.get('window_width')) or 0)*(gv(w.get('window_height')) or 0) for w in rw)
|
||||
ea=sum((float(w.window_width) if w.window_width else 0)*(float(w.window_height) if w.window_height else 0) for w in ew)
|
||||
if abs(ra-ea)>0.02: mism['window_area'].append((u,round(ra,2),round(ea,2)))
|
||||
print(f"audited {n} certs")
|
||||
print("=== field mismatches (raw cert vs EpcPropertyData) ===")
|
||||
for k,v in sorted(mism.items(), key=lambda x:-len(x[1])):
|
||||
print(f"\n[{len(v):4d}] {k}")
|
||||
for u,rv,ev in v[:5]: print(f" {u}: raw={rv} epc={ev}")
|
||||
if not mism: print(" (no mismatches — mapper is identity-faithful on all checked fields)")
|
||||
76
scripts/corpus_1000/why_diverge.py
Normal file
76
scripts/corpus_1000/why_diverge.py
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
"""Categorize every corpus cert outside +-0.5 SAP by the REASON it diverges,
|
||||
combining the computed cost-vs-demand signature, the electric heating code /
|
||||
meter, and the ledger's RR/multipart tokens."""
|
||||
from __future__ import annotations
|
||||
import json, re
|
||||
from pathlib import Path
|
||||
from collections import defaultdict
|
||||
from datatypes.epc.domain.mapper import EpcPropertyDataMapper
|
||||
from domain.sap10_calculator.calculator import calculate_sap_from_inputs
|
||||
from domain.sap10_calculator.rdsap.cert_to_inputs import (
|
||||
SAP_10_2_SPEC_PRICES, cert_to_demand_inputs, cert_to_inputs, _is_electric_main,
|
||||
_first_main_heating)
|
||||
|
||||
CORPUS = Path("backend/epc_api/json_samples/RdSAP-Schema-21.0.1/corpus.jsonl")
|
||||
# ledger signatures (for RR / multipart tokens)
|
||||
sig_of={}; cur=""
|
||||
for l in Path("scripts/corpus_1000/worklist.md").read_text().splitlines():
|
||||
if l.startswith('### C'):
|
||||
m=re.search(r'`([^`]*)`',l); cur=m.group(1) if m else ""
|
||||
m=re.match(r'^- .*?`([^`]+)`',l)
|
||||
if m: sig_of[m.group(1)]=cur
|
||||
|
||||
def bucket(doc, dsap, dpe, dco2, code, mt, sig):
|
||||
rr = 'RR' in (sig or '')
|
||||
elec = code in range(191,600) if code else False
|
||||
demand = dpe is not None and abs(dpe) >= 4 and (dpe>0)==(dsap<0)
|
||||
cost = dpe is not None and abs(dpe) < 4
|
||||
# 1. electric storage-boiler / CPSU on off-peak (spec §12 Rule 2/1)
|
||||
if code in (192,193,194,195,196) and dsap>0:
|
||||
return "elec storage/CPSU boiler → §12 off-peak (spec-faithful; lodged over-billed)"
|
||||
# 2. electric direct + storage heaters, tariff/cost
|
||||
if elec and cost:
|
||||
return "electric tariff/cost (meter-column & high-rate fraction; spec-faithful vs lodged)"
|
||||
if elec and demand:
|
||||
return "electric + fabric/demand mismatch"
|
||||
if elec:
|
||||
return "electric other"
|
||||
# 3. RR
|
||||
if rr:
|
||||
return "room-in-roof surface geometry (spec-mandated, worksheet-validated; high variance)"
|
||||
# 4. non-elec demand/fabric
|
||||
if demand:
|
||||
return "non-electric fabric/demand (per-cert: glazing area, lodged geometry, mixed walls)"
|
||||
if cost:
|
||||
return "non-electric cost (secondary fuel / cylinder / tariff)"
|
||||
return "unclassified"
|
||||
|
||||
cnt=defaultdict(list)
|
||||
band=defaultdict(int)
|
||||
for line in CORPUS.read_text().splitlines():
|
||||
doc=json.loads(line); L=doc.get('energy_rating_current')
|
||||
if L is None: continue
|
||||
try:
|
||||
epc=EpcPropertyDataMapper.from_api_response(doc)
|
||||
r=calculate_sap_from_inputs(cert_to_inputs(epc, prices=SAP_10_2_SPEC_PRICES))
|
||||
d=calculate_sap_from_inputs(cert_to_demand_inputs(epc, prices=SAP_10_2_SPEC_PRICES))
|
||||
except Exception: continue
|
||||
dsap=r.sap_score_continuous-L
|
||||
if abs(dsap)<0.5: continue
|
||||
peL=doc.get('energy_consumption_current'); co2L=doc.get('co2_emissions_current')
|
||||
dpe=(d.primary_energy_kwh_per_m2-peL) if peL is not None else None
|
||||
dco2=(d.co2_kg_per_yr/1000-co2L) if co2L is not None else None
|
||||
sh=doc.get('sap_heating') or {}; det=(sh.get('main_heating_details') or [{}])[0]
|
||||
code=det.get('sap_main_heating_code'); mt=(doc.get('sap_energy_source') or {}).get('meter_type')
|
||||
b=bucket(doc,dsap,dpe,dco2,code,mt,sig_of.get(str(doc.get('uprn')),''))
|
||||
cnt[b].append((doc.get('uprn'),L,round(r.sap_score_continuous,1),round(dsap,1)))
|
||||
ad=abs(dsap); band['0.5-1' if ad<1 else '1-2' if ad<2 else '2-4' if ad<4 else '4+']+=1
|
||||
|
||||
total=sum(len(v) for v in cnt.values())
|
||||
print(f"TOTAL diverging (|Δ|≥0.5): {total}\n")
|
||||
print("by |Δ| band:", dict(band), "\n")
|
||||
print("=== by REASON (count, mean|Δ|, examples) ===")
|
||||
for b,rows in sorted(cnt.items(), key=lambda x:-len(x[1])):
|
||||
mabs=sum(abs(x[3]) for x in rows)/len(rows)
|
||||
ex=", ".join(f"{x[0]}({x[3]:+.1f})" for x in sorted(rows,key=lambda x:-abs(x[3]))[:3])
|
||||
print(f"\n[{len(rows):3d}] mean|Δ|={mabs:.1f} {b}\n e.g. {ex}")
|
||||
1446
scripts/corpus_1000/worklist.md
Normal file
1446
scripts/corpus_1000/worklist.md
Normal file
File diff suppressed because it is too large
Load diff
231
scripts/hyde/build_100021924710.py
Normal file
231
scripts/hyde/build_100021924710.py
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
"""Elmhurst build for UPRN 100021924710 (RdSAP-21.0.1, MID-TERRACE HOUSE, band B,
|
||||
SOLID BRICK 250 mm uninsulated, pitched insulated (assumed) loft, floor to
|
||||
UNHEATED space, mains-gas COMBI (PCDB 19008) + gas room-heater SECONDARY, DUAL
|
||||
meter, 11 DG windows ~18.32 m², TFA 116. Lodged SAP 63; engine 65.0 (Δ +2.0).
|
||||
|
||||
Per-cert Elmhurst validation. Uses set_single_window (deletes the shared
|
||||
assessment's carried-over window rows) + reset_transient_state at the end so the
|
||||
assessment is left neutral. Run:
|
||||
DISPLAY=:99 python scripts/hyde/build_100021924710.py <page>
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import elmhurst_lib as E
|
||||
|
||||
DIM = "TabContainer_TabPanelMain_WebUserControlDimensionsMain_"
|
||||
WALL = ("TabContainer_TabPanelMain_InnerTabContainerMain_"
|
||||
"TabPanelExternalWallMain_WebUserControlWallMain_")
|
||||
PWALL = ("TabContainer_TabPanelMain_InnerTabContainerMain_"
|
||||
"TabPanelPartyWallMain_WebUserControlPartyWallMain_")
|
||||
ROOF = "TabContainer_TabPanelMain_WebUserControlRoofMain_"
|
||||
FLOOR = "TabContainer_TabPanelMain_WebUserControlFloorsMain_"
|
||||
DP = "TabContainer_TabPanelDoorsPanel_"
|
||||
VP = "TabContainer_TabPanelVentilationPanel_"
|
||||
APT = "TabContainer_TabPanelAirPressureTest_"
|
||||
LP = "TabContainer_TabPanelLighting_"
|
||||
MV = "TabContainer_TabPanelMechVent_"
|
||||
WH = "TabContainer_TabPanelWaterHeating_"
|
||||
MH1B = "TabContainer_TabPanelMainHeating1_WebUserControlMainHeating1_"
|
||||
_SAMPLE_DIR = (Path(__file__).parent.parent.parent
|
||||
/ "backend/epc_api/json_samples/real_life_examples"
|
||||
/ "RdSAP-Schema-21.0.1/uprn_100021924710")
|
||||
|
||||
|
||||
def _pick(page, suffix, contains):
|
||||
val = page.evaluate(
|
||||
"""(a)=>{const s=document.getElementById(a[0]);if(!s)return null;
|
||||
for(const o of s.options){if(o.text.toLowerCase().includes(a[1].toLowerCase()))return o.value;}return null;}""",
|
||||
[f"{E.FP}{suffix}", contains])
|
||||
if val is not None:
|
||||
E.set_select(page, suffix, val)
|
||||
return val
|
||||
|
||||
|
||||
def property_description(page):
|
||||
E.goto(page, "PropertyDescription", "WebFormPropertyDescription.aspx")
|
||||
E.set_select(page, "DropDownListPropertyType1", "H House")
|
||||
_pick(page, "DropDownListPropertyType2", "mid-terrace") or \
|
||||
_pick(page, "DropDownListPropertyType2", "mid terrace")
|
||||
E.set_text(page, "TextBoxStoreys", "2")
|
||||
E.set_text(page, "TextBoxHabitableRooms", "5")
|
||||
E.set_text(page, "TextBoxHeatedHabitableRooms", "5")
|
||||
print("date ->", _pick(page, "DropDownListDateBuiltMain", "1900-1929"))
|
||||
E.set_select(page, "DropDownListDateBuiltFirst", "")
|
||||
E.set_select(page, "DropDownListRoomInRoofMain", "")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def dimensions(page):
|
||||
E.goto(page, "Dimensions", "WebFormDimensions.aspx")
|
||||
E.set_text(page, f"{DIM}TextBoxFloorAreaLowestFloor", "59.20")
|
||||
E.set_text(page, f"{DIM}TextBoxRoomHeightLowestFloor", "2.74")
|
||||
E.set_text(page, f"{DIM}TextBoxWallPerimeterLowestFloor", "16.0")
|
||||
E.set_text(page, f"{DIM}TextBoxPartyWallLengthLowestFloor", "21.1")
|
||||
E.set_text(page, f"{DIM}TextBoxFloorArea1stFloor", "57.10")
|
||||
E.set_text(page, f"{DIM}TextBoxRoomHeight1stFloor", "2.74")
|
||||
E.set_text(page, f"{DIM}TextBoxWallPerimeter1stFloor", "15.7")
|
||||
E.set_text(page, f"{DIM}TextBoxPartyWallLength1stFloor", "21.7")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def walls(page):
|
||||
E.goto(page, "Walls", "WebFormWalls.aspx")
|
||||
_pick(page, f"{WALL}DropDownListType", "solid brick")
|
||||
page.wait_for_timeout(500)
|
||||
print("insulation ->", _pick(page, f"{WALL}DropDownListInsulation", "as built"))
|
||||
print("party ->", _pick(page, f"{PWALL}DropDownListPartyWallType", "determine")
|
||||
or _pick(page, f"{PWALL}DropDownListPartyWallType", "masonry"))
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def roofs(page):
|
||||
E.goto(page, "Roofs", "WebFormRoofs.aspx")
|
||||
_pick(page, f"{ROOF}DropDownListType", "access to loft") or \
|
||||
_pick(page, f"{ROOF}DropDownListType", "loft")
|
||||
_pick(page, f"{ROOF}DropDownListInsulationAt", "joists")
|
||||
_pick(page, f"{ROOF}DropDownListThickness", "unknown") or \
|
||||
_pick(page, f"{ROOF}DropDownListThickness", "assumed")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def floors(page):
|
||||
E.goto(page, "Floors", "WebFormFloors.aspx")
|
||||
_pick(page, f"{FLOOR}DropDownListLocation", "unheated")
|
||||
page.wait_for_timeout(400)
|
||||
_pick(page, f"{FLOOR}DropDownListType", "solid")
|
||||
ins = page.locator(f"#{E.FP}{FLOOR}DropDownListInsulation")
|
||||
if ins.count():
|
||||
E.set_select(page, f"{FLOOR}DropDownListInsulation", "A As built")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def openings(page):
|
||||
E.goto(page, "Openings", "WebFormOpenings.aspx")
|
||||
E.click_tab(page, "TabContainer_TabPanelWindowsPanel")
|
||||
page.wait_for_timeout(500)
|
||||
E.set_single_window(page, 18.32, "North",
|
||||
_glazing(page) if False else "Double post or during 2022")
|
||||
E.click_tab(page, "TabContainer_TabPanelDoorsPanel")
|
||||
E.set_text(page, f"{DP}TextBoxDoors", "1")
|
||||
E.set_text(page, f"{DP}TextBoxDoorsInsulated", "0")
|
||||
E.set_text(page, f"{DP}TextBoxDraughtProofedDoors", "0")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def _glazing(page):
|
||||
return "Double with unknown install date"
|
||||
|
||||
|
||||
def ventilation(page):
|
||||
E.goto(page, "VentilationAndCooling", "WebFormVentilationAndCooling.aspx")
|
||||
E.click_tab(page, "TabContainer_TabPanelVentilationPanel")
|
||||
E.set_text(page, f"{VP}TextBoxIntermittentFans", "1")
|
||||
cool = page.locator(f"#{E.FP}{VP}CheckBoxFixedSpaceCooling")
|
||||
if cool.count() and cool.is_checked():
|
||||
E.commit(page, cool.uncheck)
|
||||
E.click_tab(page, "TabContainer_TabPanelMechVent")
|
||||
mv = page.locator(f"#{E.FP}{MV}CheckBoxMechanicalVentilation")
|
||||
if mv.count() and mv.is_checked():
|
||||
E.commit(page, mv.uncheck)
|
||||
E.click_tab(page, "TabContainer_TabPanelAirPressureTest")
|
||||
E.set_select(page, f"{APT}DropDownListTestMethod", "Not available")
|
||||
E.click_tab(page, "TabContainer_TabPanelLighting")
|
||||
E.set_text(page, f"{LP}TextBoxLightsTotal", "8")
|
||||
E.set_text(page, f"{LP}TextBoxLedLightsTotal", "0")
|
||||
E.set_text(page, f"{LP}TextBoxCflLightsTotal", "0")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def space_heating(page):
|
||||
E.goto(page, "SpaceHeating", "WebFormSpaceHeating.aspx")
|
||||
page.wait_for_timeout(1000)
|
||||
mhc = page.locator(f"#{E.MH1}TextBoxMainHeatingCode")
|
||||
code = mhc.input_value() if mhc.count() else ""
|
||||
if code and code not in ("0",):
|
||||
print(f"clearing leftover MainHeatingCode {code}")
|
||||
page.evaluate("""(id)=>{const e=document.getElementById(id);if(e){e.value='';
|
||||
e.dispatchEvent(new Event('change',{bubbles:true}));}}""", f"{E.MH1}TextBoxMainHeatingCode")
|
||||
page.wait_for_timeout(400)
|
||||
E.save_close(page)
|
||||
return
|
||||
E.set_heating_dialog(page, f"{MH1B}ButtonMainHeatingCode",
|
||||
"^Gas", "Mains gas", "Boilers", "Post 1998", "Condensing",
|
||||
"Combi condens")
|
||||
print("code:", page.locator(f"#{E.MH1}TextBoxMainHeatingCode").input_value())
|
||||
E.set_heating_dialog(page, f"{MH1B}ButtonMainHeatingControls",
|
||||
"^Boilers", "^Standard", "CBE Programmer, room thermostat and TRVs")
|
||||
print("control:", page.locator(f"#{E.MH1}TextBoxMainHeatingControls").input_value())
|
||||
# dual meter (cert meter_type 1)
|
||||
E.click_tab(page, "TabContainer_TabPanelMeters")
|
||||
try:
|
||||
E.set_select(page, "TabContainer_TabPanelMeters_RadioButtonListElectricityType", "Dual")
|
||||
except Exception:
|
||||
pass
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def secondary(page):
|
||||
E.goto(page, "SpaceHeating", "WebFormSpaceHeating.aspx")
|
||||
page.wait_for_timeout(600)
|
||||
E.set_select(page, "DropDownListSecondaryHeatingPresent", "Yes")
|
||||
page.wait_for_timeout(900)
|
||||
E.set_heating_dialog(page, "ButtonSecondaryHeatingCode",
|
||||
"^Gas", "Mains gas", "Room Heater")
|
||||
tb = page.locator(f"#{E.FP}TextBoxSecondaryHeatingCode")
|
||||
print("secondary code:", tb.input_value() if tb.count() else "?")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def water_heating(page):
|
||||
E.goto(page, "WaterHeating", "WebFormWaterHeating.aspx")
|
||||
E.click_tab(page, "TabContainer_TabPanelWaterHeating")
|
||||
page.wait_for_timeout(400)
|
||||
E.clear_hot_water_cylinder(page)
|
||||
E.set_heating_dialog(page, f"{WH}ButtonWaterHeatingCode",
|
||||
"From Space Heating", "From the primary heating system")
|
||||
print("water code:", page.locator(f"#{E.FP}{WH}TextBoxWaterHeatingCode").input_value())
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def download(page):
|
||||
_SAMPLE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
E.goto(page, "Recommendations", "WebFormRecommendations.aspx")
|
||||
page.wait_for_timeout(1500)
|
||||
with page.expect_navigation(wait_until="networkidle", timeout=40000):
|
||||
page.click("#ContentBody_buttonActionSummary_Link", timeout=15000)
|
||||
page.wait_for_timeout(2000)
|
||||
for btn, fname in (("ContentBody_ContentPlaceHolder1_LinkButtonSummary", "elmhurst_summary.pdf"),
|
||||
("ContentBody_ContentPlaceHolder1_ButtonDebugInforPdf", "elmhurst_worksheet.pdf")):
|
||||
try:
|
||||
with page.expect_download(timeout=60000) as dl:
|
||||
page.click(f"#{btn}", timeout=15000)
|
||||
dl.value.save_as(str(_SAMPLE_DIR / fname))
|
||||
print("saved", fname)
|
||||
except Exception as e:
|
||||
print("!! download failed", fname, type(e).__name__, str(e)[:120])
|
||||
|
||||
|
||||
def cleanup(page):
|
||||
"""Leave the shared assessment neutral for the next build."""
|
||||
E.reset_transient_state(page)
|
||||
print("reset transient state (RR/secondary/meter)")
|
||||
|
||||
|
||||
_ORDER = ["property_description", "dimensions", "walls", "roofs", "floors",
|
||||
"openings", "ventilation", "space_heating", "secondary",
|
||||
"water_heating", "download", "cleanup"]
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2 or sys.argv[1] not in _ORDER:
|
||||
print("usage: build_100021924710.py <" + "|".join(_ORDER) + ">")
|
||||
return 2
|
||||
with E.session() as (ctx, page):
|
||||
globals()[sys.argv[1]](page)
|
||||
print("done:", sys.argv[1], "->", page.url)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
223
scripts/hyde/build_100050881708.py
Normal file
223
scripts/hyde/build_100050881708.py
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
"""Elmhurst build for UPRN 100050881708 (RdSAP-21.0.1, MID-TERRACE HOUSE, band B,
|
||||
SOLID BRICK 250 mm uninsulated, pitched insulated (assumed) loft, floor to
|
||||
UNHEATED space, mains-gas COMBI (PCDB 19008) + gas room-heater SECONDARY, DUAL
|
||||
meter, 11 DG windows ~18.32 m², TFA 116. Lodged SAP 63; engine 65.0 (Δ +2.0).
|
||||
|
||||
Per-cert Elmhurst validation. Uses set_single_window (deletes the shared
|
||||
assessment's carried-over window rows) + reset_transient_state at the end so the
|
||||
assessment is left neutral. Run:
|
||||
DISPLAY=:99 python scripts/hyde/build_100050881708.py <page>
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import elmhurst_lib as E
|
||||
|
||||
DIM = "TabContainer_TabPanelMain_WebUserControlDimensionsMain_"
|
||||
WALL = ("TabContainer_TabPanelMain_InnerTabContainerMain_"
|
||||
"TabPanelExternalWallMain_WebUserControlWallMain_")
|
||||
PWALL = ("TabContainer_TabPanelMain_InnerTabContainerMain_"
|
||||
"TabPanelPartyWallMain_WebUserControlPartyWallMain_")
|
||||
ROOF = "TabContainer_TabPanelMain_WebUserControlRoofMain_"
|
||||
FLOOR = "TabContainer_TabPanelMain_WebUserControlFloorsMain_"
|
||||
DP = "TabContainer_TabPanelDoorsPanel_"
|
||||
VP = "TabContainer_TabPanelVentilationPanel_"
|
||||
APT = "TabContainer_TabPanelAirPressureTest_"
|
||||
LP = "TabContainer_TabPanelLighting_"
|
||||
MV = "TabContainer_TabPanelMechVent_"
|
||||
WH = "TabContainer_TabPanelWaterHeating_"
|
||||
MH1B = "TabContainer_TabPanelMainHeating1_WebUserControlMainHeating1_"
|
||||
_SAMPLE_DIR = (Path(__file__).parent.parent.parent
|
||||
/ "backend/epc_api/json_samples/real_life_examples"
|
||||
/ "RdSAP-Schema-21.0.1/uprn_100050881708")
|
||||
|
||||
|
||||
def _pick(page, suffix, contains):
|
||||
val = page.evaluate(
|
||||
"""(a)=>{const s=document.getElementById(a[0]);if(!s)return null;
|
||||
for(const o of s.options){if(o.text.toLowerCase().includes(a[1].toLowerCase()))return o.value;}return null;}""",
|
||||
[f"{E.FP}{suffix}", contains])
|
||||
if val is not None:
|
||||
E.set_select(page, suffix, val)
|
||||
return val
|
||||
|
||||
|
||||
def property_description(page):
|
||||
E.goto(page, "PropertyDescription", "WebFormPropertyDescription.aspx")
|
||||
E.set_select(page, "DropDownListPropertyType1", "H House")
|
||||
_pick(page, "DropDownListPropertyType2", "semi")
|
||||
E.set_text(page, "TextBoxStoreys", "2")
|
||||
E.set_text(page, "TextBoxHabitableRooms", "3")
|
||||
E.set_text(page, "TextBoxHeatedHabitableRooms", "3")
|
||||
print("date ->", _pick(page, "DropDownListDateBuiltMain", "1900-1929"))
|
||||
E.set_select(page, "DropDownListDateBuiltFirst", "")
|
||||
E.set_select(page, "DropDownListRoomInRoofMain", "")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def dimensions(page):
|
||||
E.goto(page, "Dimensions", "WebFormDimensions.aspx")
|
||||
E.set_text(page, f"{DIM}TextBoxFloorAreaLowestFloor", "31.62")
|
||||
E.set_text(page, f"{DIM}TextBoxRoomHeightLowestFloor", "2.40")
|
||||
E.set_text(page, f"{DIM}TextBoxWallPerimeterLowestFloor", "16.4")
|
||||
E.set_text(page, f"{DIM}TextBoxPartyWallLengthLowestFloor", "6.2")
|
||||
E.set_text(page, f"{DIM}TextBoxFloorArea1stFloor", "31.62")
|
||||
E.set_text(page, f"{DIM}TextBoxRoomHeight1stFloor", "2.40")
|
||||
E.set_text(page, f"{DIM}TextBoxWallPerimeter1stFloor", "16.4")
|
||||
E.set_text(page, f"{DIM}TextBoxPartyWallLength1stFloor", "6.2")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def walls(page):
|
||||
E.goto(page, "Walls", "WebFormWalls.aspx")
|
||||
_pick(page, f"{WALL}DropDownListType", "cavity")
|
||||
page.wait_for_timeout(500)
|
||||
print("insulation ->", _pick(page, f"{WALL}DropDownListInsulation", "filled"))
|
||||
print("party ->", _pick(page, f"{PWALL}DropDownListPartyWallType", "determine")
|
||||
or _pick(page, f"{PWALL}DropDownListPartyWallType", "masonry"))
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def roofs(page):
|
||||
E.goto(page, "Roofs", "WebFormRoofs.aspx")
|
||||
_pick(page, f"{ROOF}DropDownListType", "access to loft") or \
|
||||
_pick(page, f"{ROOF}DropDownListType", "loft")
|
||||
_pick(page, f"{ROOF}DropDownListInsulationAt", "joists")
|
||||
_pick(page, f"{ROOF}DropDownListThickness", "225")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def floors(page):
|
||||
E.goto(page, "Floors", "WebFormFloors.aspx")
|
||||
_pick(page, f"{FLOOR}DropDownListLocation", "unheated")
|
||||
page.wait_for_timeout(400)
|
||||
_pick(page, f"{FLOOR}DropDownListType", "solid")
|
||||
ins = page.locator(f"#{E.FP}{FLOOR}DropDownListInsulation")
|
||||
if ins.count():
|
||||
E.set_select(page, f"{FLOOR}DropDownListInsulation", "A As built")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def openings(page):
|
||||
E.goto(page, "Openings", "WebFormOpenings.aspx")
|
||||
E.click_tab(page, "TabContainer_TabPanelWindowsPanel")
|
||||
page.wait_for_timeout(500)
|
||||
E.set_single_window(page, 9.56, "North",
|
||||
_glazing(page) if False else "Double post or during 2022")
|
||||
E.click_tab(page, "TabContainer_TabPanelDoorsPanel")
|
||||
E.set_text(page, f"{DP}TextBoxDoors", "2")
|
||||
E.set_text(page, f"{DP}TextBoxDoorsInsulated", "0")
|
||||
E.set_text(page, f"{DP}TextBoxDraughtProofedDoors", "0")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def _glazing(page):
|
||||
return "Double with unknown install date"
|
||||
|
||||
|
||||
def ventilation(page):
|
||||
E.goto(page, "VentilationAndCooling", "WebFormVentilationAndCooling.aspx")
|
||||
E.click_tab(page, "TabContainer_TabPanelVentilationPanel")
|
||||
E.set_text(page, f"{VP}TextBoxIntermittentFans", "2")
|
||||
cool = page.locator(f"#{E.FP}{VP}CheckBoxFixedSpaceCooling")
|
||||
if cool.count() and cool.is_checked():
|
||||
E.commit(page, cool.uncheck)
|
||||
E.click_tab(page, "TabContainer_TabPanelMechVent")
|
||||
mv = page.locator(f"#{E.FP}{MV}CheckBoxMechanicalVentilation")
|
||||
if mv.count() and mv.is_checked():
|
||||
E.commit(page, mv.uncheck)
|
||||
E.click_tab(page, "TabContainer_TabPanelAirPressureTest")
|
||||
E.set_select(page, f"{APT}DropDownListTestMethod", "Not available")
|
||||
E.click_tab(page, "TabContainer_TabPanelLighting")
|
||||
E.set_text(page, f"{LP}TextBoxLightsTotal", "8")
|
||||
E.set_text(page, f"{LP}TextBoxLedLightsTotal", "0")
|
||||
E.set_text(page, f"{LP}TextBoxCflLightsTotal", "0")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def space_heating(page):
|
||||
E.goto(page, "SpaceHeating", "WebFormSpaceHeating.aspx")
|
||||
page.wait_for_timeout(1000)
|
||||
mhc = page.locator(f"#{E.MH1}TextBoxMainHeatingCode")
|
||||
code = mhc.input_value() if mhc.count() else ""
|
||||
if code and code not in ("0",):
|
||||
print(f"clearing leftover MainHeatingCode {code}")
|
||||
page.evaluate("""(id)=>{const e=document.getElementById(id);if(e){e.value='';
|
||||
e.dispatchEvent(new Event('change',{bubbles:true}));}}""", f"{E.MH1}TextBoxMainHeatingCode")
|
||||
page.wait_for_timeout(400)
|
||||
E.save_close(page)
|
||||
return
|
||||
E.set_heating_dialog(page, f"{MH1B}ButtonMainHeatingCode",
|
||||
"^Gas", "Mains gas", "Boilers", "Post 1998", "Condensing",
|
||||
"Combi condens")
|
||||
print("code:", page.locator(f"#{E.MH1}TextBoxMainHeatingCode").input_value())
|
||||
E.set_heating_dialog(page, f"{MH1B}ButtonMainHeatingControls",
|
||||
"^Boilers", "^Standard", "CBE Programmer, room thermostat and TRVs")
|
||||
print("control:", page.locator(f"#{E.MH1}TextBoxMainHeatingControls").input_value())
|
||||
E.set_select(page, "DropDownListSecondaryHeatingPresent", "No")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def secondary(page):
|
||||
E.goto(page, "SpaceHeating", "WebFormSpaceHeating.aspx")
|
||||
page.wait_for_timeout(600)
|
||||
E.set_select(page, "DropDownListSecondaryHeatingPresent", "Yes")
|
||||
page.wait_for_timeout(900)
|
||||
E.set_heating_dialog(page, "ButtonSecondaryHeatingCode",
|
||||
"^Gas", "Mains gas", "Room Heater")
|
||||
tb = page.locator(f"#{E.FP}TextBoxSecondaryHeatingCode")
|
||||
print("secondary code:", tb.input_value() if tb.count() else "?")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def water_heating(page):
|
||||
E.goto(page, "WaterHeating", "WebFormWaterHeating.aspx")
|
||||
E.click_tab(page, "TabContainer_TabPanelWaterHeating")
|
||||
page.wait_for_timeout(400)
|
||||
E.clear_hot_water_cylinder(page)
|
||||
E.set_heating_dialog(page, f"{WH}ButtonWaterHeatingCode",
|
||||
"From Space Heating", "From the primary heating system")
|
||||
print("water code:", page.locator(f"#{E.FP}{WH}TextBoxWaterHeatingCode").input_value())
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def download(page):
|
||||
_SAMPLE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
E.goto(page, "Recommendations", "WebFormRecommendations.aspx")
|
||||
page.wait_for_timeout(1500)
|
||||
with page.expect_navigation(wait_until="networkidle", timeout=40000):
|
||||
page.click("#ContentBody_buttonActionSummary_Link", timeout=15000)
|
||||
page.wait_for_timeout(2000)
|
||||
for btn, fname in (("ContentBody_ContentPlaceHolder1_LinkButtonSummary", "elmhurst_summary.pdf"),
|
||||
("ContentBody_ContentPlaceHolder1_ButtonDebugInforPdf", "elmhurst_worksheet.pdf")):
|
||||
try:
|
||||
with page.expect_download(timeout=60000) as dl:
|
||||
page.click(f"#{btn}", timeout=15000)
|
||||
dl.value.save_as(str(_SAMPLE_DIR / fname))
|
||||
print("saved", fname)
|
||||
except Exception as e:
|
||||
print("!! download failed", fname, type(e).__name__, str(e)[:120])
|
||||
|
||||
|
||||
def cleanup(page):
|
||||
"""Leave the shared assessment neutral for the next build."""
|
||||
E.reset_transient_state(page)
|
||||
print("reset transient state (RR/secondary/meter)")
|
||||
|
||||
|
||||
_ORDER = ["property_description", "dimensions", "walls", "roofs", "floors",
|
||||
"openings", "ventilation", "space_heating", "water_heating", "download", "cleanup"]
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2 or sys.argv[1] not in _ORDER:
|
||||
print("usage: build_100050881708.py <" + "|".join(_ORDER) + ">")
|
||||
return 2
|
||||
with E.session() as (ctx, page):
|
||||
globals()[sys.argv[1]](page)
|
||||
print("done:", sys.argv[1], "->", page.url)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
230
scripts/hyde/build_100061275133.py
Normal file
230
scripts/hyde/build_100061275133.py
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
"""Elmhurst build for UPRN 100061275133 (RdSAP-21.0.1, SEMI-DETACHED HOUSE, band B,
|
||||
SOLID BRICK 400 mm with EXTERNAL insulation, pitched 300 mm loft, ground floor
|
||||
SUSPENDED, no insulation (assumed) — no per-bp floor_construction_type lodged,
|
||||
mains-gas COMBI (PCDB 18908, Worcester Greenstar 4000 88.70%), no secondary
|
||||
heating, single meter, 12 DG windows ~15.32 m², TFA 78. Lodged SAP 76; engine
|
||||
76 (76.33, exact match after the §5 (12) floor-infiltration fix).
|
||||
|
||||
Per-cert Elmhurst validation. Uses set_single_window (deletes the shared
|
||||
assessment's carried-over window rows) + reset_transient_state at the end so the
|
||||
assessment is left neutral. Run:
|
||||
DISPLAY=:99 python scripts/hyde/build_100061275133.py <page>
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import elmhurst_lib as E
|
||||
|
||||
DIM = "TabContainer_TabPanelMain_WebUserControlDimensionsMain_"
|
||||
WALL = ("TabContainer_TabPanelMain_InnerTabContainerMain_"
|
||||
"TabPanelExternalWallMain_WebUserControlWallMain_")
|
||||
PWALL = ("TabContainer_TabPanelMain_InnerTabContainerMain_"
|
||||
"TabPanelPartyWallMain_WebUserControlPartyWallMain_")
|
||||
ROOF = "TabContainer_TabPanelMain_WebUserControlRoofMain_"
|
||||
FLOOR = "TabContainer_TabPanelMain_WebUserControlFloorsMain_"
|
||||
DP = "TabContainer_TabPanelDoorsPanel_"
|
||||
VP = "TabContainer_TabPanelVentilationPanel_"
|
||||
APT = "TabContainer_TabPanelAirPressureTest_"
|
||||
LP = "TabContainer_TabPanelLighting_"
|
||||
MV = "TabContainer_TabPanelMechVent_"
|
||||
WH = "TabContainer_TabPanelWaterHeating_"
|
||||
MH1B = "TabContainer_TabPanelMainHeating1_WebUserControlMainHeating1_"
|
||||
_SAMPLE_DIR = (Path(__file__).parent.parent.parent
|
||||
/ "backend/epc_api/json_samples/real_life_examples"
|
||||
/ "RdSAP-Schema-21.0.1/uprn_100061275133")
|
||||
|
||||
|
||||
def _pick(page, suffix, contains):
|
||||
val = page.evaluate(
|
||||
"""(a)=>{const s=document.getElementById(a[0]);if(!s)return null;
|
||||
for(const o of s.options){if(o.text.toLowerCase().includes(a[1].toLowerCase()))return o.value;}return null;}""",
|
||||
[f"{E.FP}{suffix}", contains])
|
||||
if val is not None:
|
||||
E.set_select(page, suffix, val)
|
||||
return val
|
||||
|
||||
|
||||
def property_description(page):
|
||||
E.goto(page, "PropertyDescription", "WebFormPropertyDescription.aspx")
|
||||
E.set_select(page, "DropDownListPropertyType1", "H House")
|
||||
_pick(page, "DropDownListPropertyType2", "semi")
|
||||
E.set_text(page, "TextBoxStoreys", "2")
|
||||
E.set_text(page, "TextBoxHabitableRooms", "3")
|
||||
E.set_text(page, "TextBoxHeatedHabitableRooms", "3")
|
||||
print("date ->", _pick(page, "DropDownListDateBuiltMain", "1900-1929"))
|
||||
E.set_select(page, "DropDownListDateBuiltFirst", "")
|
||||
E.set_select(page, "DropDownListRoomInRoofMain", "")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def dimensions(page):
|
||||
E.goto(page, "Dimensions", "WebFormDimensions.aspx")
|
||||
E.set_text(page, f"{DIM}TextBoxFloorAreaLowestFloor", "38.99")
|
||||
E.set_text(page, f"{DIM}TextBoxRoomHeightLowestFloor", "2.40")
|
||||
E.set_text(page, f"{DIM}TextBoxWallPerimeterLowestFloor", "17.92")
|
||||
E.set_text(page, f"{DIM}TextBoxPartyWallLengthLowestFloor", "5.09")
|
||||
E.set_text(page, f"{DIM}TextBoxFloorArea1stFloor", "38.99")
|
||||
E.set_text(page, f"{DIM}TextBoxRoomHeight1stFloor", "2.40")
|
||||
E.set_text(page, f"{DIM}TextBoxWallPerimeter1stFloor", "17.92")
|
||||
E.set_text(page, f"{DIM}TextBoxPartyWallLength1stFloor", "5.09")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def walls(page):
|
||||
E.goto(page, "Walls", "WebFormWalls.aspx")
|
||||
_pick(page, f"{WALL}DropDownListType", "solid brick")
|
||||
page.wait_for_timeout(500)
|
||||
print("insulation ->", _pick(page, f"{WALL}DropDownListInsulation", "external"))
|
||||
print("party ->", _pick(page, f"{PWALL}DropDownListPartyWallType", "determine")
|
||||
or _pick(page, f"{PWALL}DropDownListPartyWallType", "masonry"))
|
||||
# wall thickness (cert lodges 400 mm; Elmhurst else defaults 260)
|
||||
for suf in (f"{WALL}TextBoxThickness", f"{WALL}TextBoxWallThickness"):
|
||||
if page.locator(f"#{E.FP}{suf}").count():
|
||||
E.set_text(page, suf, "400"); print("wall thickness set 400 via", suf); break
|
||||
else:
|
||||
print("thickness field not found; wall fields:", [i for i in page.evaluate("()=>Array.from(document.querySelectorAll('[id*=Wall]')).map(e=>e.id)")][:20])
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def roofs(page):
|
||||
E.goto(page, "Roofs", "WebFormRoofs.aspx")
|
||||
_pick(page, f"{ROOF}DropDownListType", "access to loft") or \
|
||||
_pick(page, f"{ROOF}DropDownListType", "loft")
|
||||
_pick(page, f"{ROOF}DropDownListInsulationAt", "joists")
|
||||
_pick(page, f"{ROOF}DropDownListThickness", "300")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def floors(page):
|
||||
E.goto(page, "Floors", "WebFormFloors.aspx")
|
||||
_pick(page, f"{FLOOR}DropDownListLocation", "ground floor")
|
||||
page.wait_for_timeout(400)
|
||||
_pick(page, f"{FLOOR}DropDownListType", "suspended")
|
||||
ins = page.locator(f"#{E.FP}{FLOOR}DropDownListInsulation")
|
||||
if ins.count():
|
||||
E.set_select(page, f"{FLOOR}DropDownListInsulation", "A As built")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def openings(page):
|
||||
E.goto(page, "Openings", "WebFormOpenings.aspx")
|
||||
E.click_tab(page, "TabContainer_TabPanelWindowsPanel")
|
||||
page.wait_for_timeout(500)
|
||||
E.set_single_window(page, 15.32, "North", _glazing(page))
|
||||
E.click_tab(page, "TabContainer_TabPanelDoorsPanel")
|
||||
E.set_text(page, f"{DP}TextBoxDoors", "2")
|
||||
E.set_text(page, f"{DP}TextBoxDoorsInsulated", "0")
|
||||
E.set_text(page, f"{DP}TextBoxDraughtProofedDoors", "2") # cert: percent_draughtproofed=100
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def _glazing(page):
|
||||
# glazed_type 13 -> double glazed, 2022 or later (U 1.4)
|
||||
return "Double post or during 2022"
|
||||
|
||||
|
||||
def ventilation(page):
|
||||
E.goto(page, "VentilationAndCooling", "WebFormVentilationAndCooling.aspx")
|
||||
E.click_tab(page, "TabContainer_TabPanelVentilationPanel")
|
||||
# Cert lodges no extract-fan count; RdSAP 10 Table 5 age-band default for
|
||||
# band B (A-E) is 0, and our engine's ventilation_from_cert computes 0 —
|
||||
# was wrongly entered as 1 here, inflating Elmhurst's (8)/(21) ACH vs ours.
|
||||
E.set_text(page, f"{VP}TextBoxIntermittentFans", "0")
|
||||
cool = page.locator(f"#{E.FP}{VP}CheckBoxFixedSpaceCooling")
|
||||
if cool.count() and cool.is_checked():
|
||||
E.commit(page, cool.uncheck)
|
||||
E.click_tab(page, "TabContainer_TabPanelMechVent")
|
||||
mv = page.locator(f"#{E.FP}{MV}CheckBoxMechanicalVentilation")
|
||||
if mv.count() and mv.is_checked():
|
||||
E.commit(page, mv.uncheck)
|
||||
E.click_tab(page, "TabContainer_TabPanelAirPressureTest")
|
||||
E.set_select(page, f"{APT}DropDownListTestMethod", "Not available")
|
||||
E.click_tab(page, "TabContainer_TabPanelLighting")
|
||||
E.set_text(page, f"{LP}TextBoxLightsTotal", "5")
|
||||
E.set_text(page, f"{LP}TextBoxLedLightsTotal", "5")
|
||||
E.set_text(page, f"{LP}TextBoxCflLightsTotal", "0")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def space_heating(page):
|
||||
# PCDB exact-boiler entry (elmhurst_lib.set_pcdb_boiler): the cert's
|
||||
# main_heating_index_number 18908 resolves to Worcester Greenstar 4000
|
||||
# GR4700iW 25 C NG, 88.70% winter efficiency — matching the SAP engine's
|
||||
# PCDB lookup exactly, vs the generic "Combi condens" SAP-Table code
|
||||
# (84%) the old dialog-driven entry carried a keep-hot artifact from.
|
||||
E.goto(page, "SpaceHeating", "WebFormSpaceHeating.aspx")
|
||||
page.wait_for_timeout(1000)
|
||||
E.clear_main_heating_code(page)
|
||||
desc = E.set_pcdb_boiler(page, 18908)
|
||||
print("PCDB boiler ->", desc)
|
||||
E.set_heating_dialog(page, f"{MH1B}ButtonMainHeatingControls",
|
||||
"^Boilers", "^Standard", "CBE Programmer, room thermostat and TRVs")
|
||||
print("control:", page.locator(f"#{E.MH1}TextBoxMainHeatingControls").input_value())
|
||||
E.set_select(page, "DropDownListSecondaryHeatingPresent", "No")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def _unused_secondary(page):
|
||||
E.goto(page, "SpaceHeating", "WebFormSpaceHeating.aspx")
|
||||
page.wait_for_timeout(600)
|
||||
E.set_select(page, "DropDownListSecondaryHeatingPresent", "Yes")
|
||||
page.wait_for_timeout(900)
|
||||
E.set_heating_dialog(page, "ButtonSecondaryHeatingCode",
|
||||
"^Gas", "Mains gas", "Room Heater")
|
||||
tb = page.locator(f"#{E.FP}TextBoxSecondaryHeatingCode")
|
||||
print("secondary code:", tb.input_value() if tb.count() else "?")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def water_heating(page):
|
||||
E.goto(page, "WaterHeating", "WebFormWaterHeating.aspx")
|
||||
E.click_tab(page, "TabContainer_TabPanelWaterHeating")
|
||||
page.wait_for_timeout(400)
|
||||
E.clear_hot_water_cylinder(page)
|
||||
E.set_heating_dialog(page, f"{WH}ButtonWaterHeatingCode",
|
||||
"From Space Heating", "From the primary heating system")
|
||||
print("water code:", page.locator(f"#{E.FP}{WH}TextBoxWaterHeatingCode").input_value())
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def download(page):
|
||||
_SAMPLE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
E.goto(page, "Recommendations", "WebFormRecommendations.aspx")
|
||||
page.wait_for_timeout(1500)
|
||||
with page.expect_navigation(wait_until="networkidle", timeout=40000):
|
||||
page.click("#ContentBody_buttonActionSummary_Link", timeout=15000)
|
||||
page.wait_for_timeout(2000)
|
||||
for btn, fname in (("ContentBody_ContentPlaceHolder1_LinkButtonSummary", "elmhurst_summary.pdf"),
|
||||
("ContentBody_ContentPlaceHolder1_ButtonDebugInforPdf", "elmhurst_worksheet.pdf")):
|
||||
try:
|
||||
with page.expect_download(timeout=60000) as dl:
|
||||
page.click(f"#{btn}", timeout=15000)
|
||||
dl.value.save_as(str(_SAMPLE_DIR / fname))
|
||||
print("saved", fname)
|
||||
except Exception as e:
|
||||
print("!! download failed", fname, type(e).__name__, str(e)[:120])
|
||||
|
||||
|
||||
def cleanup(page):
|
||||
"""Leave the shared assessment neutral for the next build."""
|
||||
E.reset_transient_state(page)
|
||||
print("reset transient state (RR/secondary/meter)")
|
||||
|
||||
|
||||
_ORDER = ["property_description", "dimensions", "walls", "roofs", "floors",
|
||||
"openings", "ventilation", "space_heating",
|
||||
"water_heating", "download", "cleanup"]
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2 or sys.argv[1] not in _ORDER:
|
||||
print("usage: build_100061275133.py <" + "|".join(_ORDER) + ">")
|
||||
return 2
|
||||
with E.session() as (ctx, page):
|
||||
globals()[sys.argv[1]](page)
|
||||
print("done:", sys.argv[1], "->", page.url)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
297
scripts/hyde/build_200004296092.py
Normal file
297
scripts/hyde/build_200004296092.py
Normal file
|
|
@ -0,0 +1,297 @@
|
|||
"""Elmhurst build for UPRN 200004296092 (RdSAP-Schema-21.0.1, END-TERRACE HOUSE,
|
||||
2-storey, band B, SOLID BRICK 360 mm WITH EXTERNAL INSULATION (thickness lodged
|
||||
"NI" = unknown), pitched 150 mm loft, solid uninsulated ground floor, mains-gas
|
||||
COMBI (PCDB 16137 Ideal Logic combi 30 ~ generic Post-98 condensing combi),
|
||||
control 2106 (CBE), double glazed ~4.31 m², 2 doors, TFA ~62, party wall 6.79 m.
|
||||
Engine 71.74 = lodged 71 AFTER the §5.4 "assume 100 mm if unknown" wall fix
|
||||
(slice 5); this build validates that against the accredited Elmhurst worksheet.
|
||||
|
||||
The KEY page is `walls`: it prints Elmhurst's insulation dropdown so we can see
|
||||
how the accredited tool handles External insulation of unknown thickness. Run:
|
||||
DISPLAY=:99 python scripts/hyde/build_200004296092.py <page>
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import elmhurst_lib as E
|
||||
|
||||
DIM = "TabContainer_TabPanelMain_WebUserControlDimensionsMain_"
|
||||
WALL = ("TabContainer_TabPanelMain_InnerTabContainerMain_"
|
||||
"TabPanelExternalWallMain_WebUserControlWallMain_")
|
||||
PWALL = ("TabContainer_TabPanelMain_InnerTabContainerMain_"
|
||||
"TabPanelPartyWallMain_WebUserControlPartyWallMain_")
|
||||
ROOF = "TabContainer_TabPanelMain_WebUserControlRoofMain_"
|
||||
FLOOR = "TabContainer_TabPanelMain_WebUserControlFloorsMain_"
|
||||
WP = "TabContainer_TabPanelWindowsPanel_"
|
||||
DP = "TabContainer_TabPanelDoorsPanel_"
|
||||
VP = "TabContainer_TabPanelVentilationPanel_"
|
||||
APT = "TabContainer_TabPanelAirPressureTest_"
|
||||
LP = "TabContainer_TabPanelLighting_"
|
||||
MV = "TabContainer_TabPanelMechVent_"
|
||||
WH = "TabContainer_TabPanelWaterHeating_"
|
||||
MH1B = "TabContainer_TabPanelMainHeating1_WebUserControlMainHeating1_"
|
||||
|
||||
_SAMPLE_DIR = (Path(__file__).parent.parent.parent
|
||||
/ "backend/epc_api/json_samples/real_life_examples"
|
||||
/ "RdSAP-Schema-21.0.1/uprn_200004296092")
|
||||
|
||||
|
||||
def _pick(page, suffix, contains):
|
||||
val = page.evaluate(
|
||||
"""(a)=>{const s=document.getElementById(a[0]);if(!s)return null;
|
||||
for(const o of s.options){if(o.text.toLowerCase().includes(a[1].toLowerCase()))return o.value;}return null;}""",
|
||||
[f"{E.FP}{suffix}", contains])
|
||||
if val is not None:
|
||||
E.set_select(page, suffix, val)
|
||||
return val
|
||||
|
||||
|
||||
def _options(page, suffix):
|
||||
return page.evaluate(
|
||||
"""(id)=>{const s=document.getElementById(id);if(!s)return [];
|
||||
return Array.from(s.options).map(o=>o.text);}""", f"{E.FP}{suffix}")
|
||||
|
||||
|
||||
def property_description(page):
|
||||
E.goto(page, "PropertyDescription", "WebFormPropertyDescription.aspx")
|
||||
E.set_select(page, "DropDownListPropertyType1", "H House")
|
||||
print("built_form ->", _pick(page, "DropDownListPropertyType2", "end-terrace")
|
||||
or _pick(page, "DropDownListPropertyType2", "end terrace"))
|
||||
E.set_text(page, "TextBoxStoreys", "2")
|
||||
E.set_text(page, "TextBoxHabitableRooms", "3")
|
||||
E.set_text(page, "TextBoxHeatedHabitableRooms", "3")
|
||||
print("date ->", _pick(page, "DropDownListDateBuiltMain", "1900-1929")
|
||||
or _pick(page, "DropDownListDateBuiltMain", "1900"))
|
||||
E.set_select(page, "DropDownListDateBuiltFirst", "")
|
||||
E.set_select(page, "DropDownListRoomInRoofMain", "")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def dimensions(page):
|
||||
E.goto(page, "Dimensions", "WebFormDimensions.aspx")
|
||||
E.set_text(page, f"{DIM}TextBoxFloorAreaLowestFloor", "31.16")
|
||||
E.set_text(page, f"{DIM}TextBoxRoomHeightLowestFloor", "2.40")
|
||||
E.set_text(page, f"{DIM}TextBoxWallPerimeterLowestFloor", "15.97")
|
||||
E.set_text(page, f"{DIM}TextBoxPartyWallLengthLowestFloor", "6.79")
|
||||
E.set_text(page, f"{DIM}TextBoxFloorArea1stFloor", "31.16")
|
||||
E.set_text(page, f"{DIM}TextBoxRoomHeight1stFloor", "2.40")
|
||||
E.set_text(page, f"{DIM}TextBoxWallPerimeter1stFloor", "15.97")
|
||||
E.set_text(page, f"{DIM}TextBoxPartyWallLength1stFloor", "6.79")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def walls(page):
|
||||
E.goto(page, "Walls", "WebFormWalls.aspx")
|
||||
print("WALL TYPE options:", _options(page, f"{WALL}DropDownListType"))
|
||||
_pick(page, f"{WALL}DropDownListType", "solid brick") \
|
||||
or _pick(page, f"{WALL}DropDownListType", "brick")
|
||||
page.wait_for_timeout(600)
|
||||
print("INSULATION options:", _options(page, f"{WALL}DropDownListInsulation"))
|
||||
ins = (_pick(page, f"{WALL}DropDownListInsulation", "external")
|
||||
or _pick(page, f"{WALL}DropDownListInsulation", "ext"))
|
||||
print("insulation ->", ins)
|
||||
page.wait_for_timeout(600)
|
||||
# If a thickness dropdown appears for external insulation, show it (this is
|
||||
# the slice-5 question: what does Elmhurst do with UNKNOWN thickness?).
|
||||
for suf in (f"{WALL}DropDownListInsulationThickness",
|
||||
f"{WALL}DropDownListExternalInsulationThickness"):
|
||||
if page.locator(f"#{E.FP}{suf}").count():
|
||||
print("INSULATION THICKNESS options:", _options(page, suf),
|
||||
"current:", page.locator(f"#{E.FP}{suf}").input_value())
|
||||
print("party wall ->",
|
||||
_pick(page, f"{PWALL}DropDownListPartyWallType", "determine")
|
||||
or _pick(page, f"{PWALL}DropDownListPartyWallType", "masonry"))
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def roofs(page):
|
||||
E.goto(page, "Roofs", "WebFormRoofs.aspx")
|
||||
_pick(page, f"{ROOF}DropDownListType", "access to loft") \
|
||||
or _pick(page, f"{ROOF}DropDownListType", "loft")
|
||||
_pick(page, f"{ROOF}DropDownListInsulationAt", "joists")
|
||||
E.set_select(page, f"{ROOF}DropDownListThickness", "150 mm")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def floors(page):
|
||||
E.goto(page, "Floors", "WebFormFloors.aspx")
|
||||
E.set_select(page, f"{FLOOR}DropDownListLocation", "G Ground floor")
|
||||
_pick(page, f"{FLOOR}DropDownListType", "solid")
|
||||
E.set_select(page, f"{FLOOR}DropDownListInsulation", "A As built")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def openings(page):
|
||||
E.goto(page, "Openings", "WebFormOpenings.aspx")
|
||||
E.click_tab(page, "TabContainer_TabPanelWindowsPanel")
|
||||
_add_window(page, 4.31, "North", _glazing(page))
|
||||
_delete_zero_rows(page)
|
||||
E.click_tab(page, "TabContainer_TabPanelDoorsPanel")
|
||||
E.set_text(page, f"{DP}TextBoxDoors", "2")
|
||||
E.set_text(page, f"{DP}TextBoxDoorsInsulated", "0")
|
||||
E.set_text(page, f"{DP}TextBoxDraughtProofedDoors", "0")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def _glazing(page):
|
||||
for needle in ("unknown install date", "before 2002", "pre 2002"):
|
||||
for opt in _options(page, f"{WP}DropDownListExtGlazing"):
|
||||
low = opt.lower()
|
||||
if needle in low and "triple" not in low and "single" not in low and "known data" not in low:
|
||||
return opt
|
||||
return "Double post or during 2022"
|
||||
|
||||
|
||||
def _add_window(page, area, orientation, glazing):
|
||||
print("glazing ->", glazing)
|
||||
E.set_select(page, f"{WP}DropDownListExtGlazing", glazing)
|
||||
page.wait_for_timeout(400)
|
||||
ft = page.locator(f"#{E.FP}{WP}DropDownListExtFrameType")
|
||||
if ft.count():
|
||||
ft.select_option("PVC")
|
||||
gg = page.locator(f"#{E.FP}{WP}DropDownListExtGlazingGap")
|
||||
if gg.count():
|
||||
gg.select_option("12 mm")
|
||||
wid = f"{E.FP}{WP}TextBoxExtWidth"
|
||||
page.evaluate(
|
||||
"""(a)=>{const e=document.getElementById(a[0]);if(e){e.value=a[1];
|
||||
e.dispatchEvent(new Event('input',{bubbles:true}));
|
||||
e.dispatchEvent(new Event('change',{bubbles:true}));
|
||||
e.dispatchEvent(new Event('blur',{bubbles:true}));}}""", [wid, str(area)])
|
||||
page.locator(f"#{E.FP}{WP}TextBoxExtHeight").fill("1.00")
|
||||
page.locator(f"#{E.FP}{WP}DropDownListExtOrientation").select_option(orientation)
|
||||
page.locator(f"#{E.FP}{WP}DropDownListExtBuildingPartId").select_option("Main")
|
||||
page.locator(f"#{E.FP}{WP}DropDownListExtLocation").select_option("External wall")
|
||||
page.wait_for_timeout(300)
|
||||
before = E.window_row_count(page)
|
||||
page.evaluate("(id)=>{const e=document.getElementById(id); if(e)e.click();}", f"{E.FP}{WP}ButtonAddWindow")
|
||||
for _ in range(25):
|
||||
page.wait_for_timeout(200)
|
||||
if E.window_row_count(page) > before:
|
||||
break
|
||||
|
||||
|
||||
def _grid_rows(page):
|
||||
return page.evaluate(
|
||||
"""()=>{const t=document.querySelector("[id*=GridViewExtendedWidows]");
|
||||
if(!t)return[];return Array.from(t.querySelectorAll('tr')).slice(1)
|
||||
.map(r=>Array.from(r.querySelectorAll('td')).map(c=>c.innerText.trim()));}""")
|
||||
|
||||
|
||||
def _delete_zero_rows(page):
|
||||
g = 0
|
||||
while g < 6 and E.window_row_count(page) > 1:
|
||||
g += 1
|
||||
rows = _grid_rows(page)
|
||||
bad = next((i for i, c in enumerate(rows) if len(c) > 1 and c[1] in ("0.00", "0", "0.0")), None)
|
||||
if bad is None:
|
||||
break
|
||||
_delete_row(page, bad)
|
||||
page.wait_for_timeout(400)
|
||||
|
||||
|
||||
def _delete_row(page, idx):
|
||||
before = E.window_row_count(page)
|
||||
btn = page.evaluate(
|
||||
"""(i)=>{const b=document.querySelectorAll("[id*='GridViewExtendedWidows_DeleteButton_']");return b[i]?b[i].id:null;}""", idx)
|
||||
if not btn:
|
||||
return
|
||||
page.evaluate("(id)=>{const e=document.getElementById(id); if(e)e.click();}", btn)
|
||||
page.wait_for_selector(f"#{E.FP}DeleteWindowDialog_LinkButtonYes", state="visible", timeout=5000)
|
||||
page.evaluate("(id)=>{const e=document.getElementById(id); if(e)e.click();}", f"{E.FP}DeleteWindowDialog_LinkButtonYes")
|
||||
for _ in range(20):
|
||||
page.wait_for_timeout(200)
|
||||
if E.window_row_count(page) < before:
|
||||
break
|
||||
|
||||
|
||||
def ventilation(page):
|
||||
E.goto(page, "VentilationAndCooling", "WebFormVentilationAndCooling.aspx")
|
||||
E.click_tab(page, "TabContainer_TabPanelVentilationPanel")
|
||||
E.set_text(page, f"{VP}TextBoxIntermittentFans", "0")
|
||||
cool = page.locator(f"#{E.FP}{VP}CheckBoxFixedSpaceCooling")
|
||||
if cool.count() and cool.is_checked():
|
||||
E.commit(page, cool.uncheck)
|
||||
E.click_tab(page, "TabContainer_TabPanelMechVent")
|
||||
mv = page.locator(f"#{E.FP}{MV}CheckBoxMechanicalVentilation")
|
||||
if mv.count() and mv.is_checked():
|
||||
E.commit(page, mv.uncheck)
|
||||
E.click_tab(page, "TabContainer_TabPanelAirPressureTest")
|
||||
E.set_select(page, f"{APT}DropDownListTestMethod", "Not available")
|
||||
E.click_tab(page, "TabContainer_TabPanelLighting")
|
||||
E.set_text(page, f"{LP}TextBoxLightsTotal", "6")
|
||||
E.set_text(page, f"{LP}TextBoxLedLightsTotal", "0")
|
||||
E.set_text(page, f"{LP}TextBoxCflLightsTotal", "0")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def space_heating(page):
|
||||
E.goto(page, "SpaceHeating", "WebFormSpaceHeating.aspx")
|
||||
page.wait_for_timeout(1000)
|
||||
mhc = page.locator(f"#{E.MH1}TextBoxMainHeatingCode")
|
||||
code = mhc.input_value() if mhc.count() else ""
|
||||
if code and code not in ("0",):
|
||||
print(f"clearing leftover MainHeatingCode {code}")
|
||||
page.evaluate("""(id)=>{const e=document.getElementById(id);if(e){e.value='';
|
||||
e.dispatchEvent(new Event('change',{bubbles:true}));}}""", f"{E.MH1}TextBoxMainHeatingCode")
|
||||
page.wait_for_timeout(400)
|
||||
E.save_close(page)
|
||||
return
|
||||
# Generic Post-1998 condensing combi (~89% SAP 4b, ≈ PCDB 16137 Ideal Logic
|
||||
# combi 30). combi vs regular is set on the water page (from primary + no
|
||||
# cylinder). Control 2106 = CBE (programmer, room thermostat and TRVs).
|
||||
E.set_heating_dialog(page, f"{MH1B}ButtonMainHeatingCode",
|
||||
"^Gas", "Mains gas", "Boilers", "Post 1998", "Condensing",
|
||||
"Combi condens")
|
||||
print("code:", page.locator(f"#{E.MH1}TextBoxMainHeatingCode").input_value())
|
||||
E.set_heating_dialog(page, f"{MH1B}ButtonMainHeatingControls",
|
||||
"^Boilers", "^Standard", "CBE Programmer, room thermostat and TRVs")
|
||||
print("control:", page.locator(f"#{E.MH1}TextBoxMainHeatingControls").input_value())
|
||||
E.set_select(page, "DropDownListSecondaryHeatingPresent", "No")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def water_heating(page):
|
||||
E.goto(page, "WaterHeating", "WebFormWaterHeating.aspx")
|
||||
E.click_tab(page, "TabContainer_TabPanelWaterHeating")
|
||||
page.wait_for_timeout(400)
|
||||
E.clear_hot_water_cylinder(page)
|
||||
E.set_heating_dialog(page, f"{WH}ButtonWaterHeatingCode",
|
||||
"From Space Heating", "From the primary heating system")
|
||||
print("water code:", page.locator(f"#{E.FP}{WH}TextBoxWaterHeatingCode").input_value())
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def download(page):
|
||||
_SAMPLE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
print("navigating via Recommendations ...")
|
||||
with page.expect_navigation(wait_until="networkidle", timeout=60_000):
|
||||
page.click("#ContentBody_buttonActionRecommendations_Link", timeout=15_000)
|
||||
page.wait_for_timeout(1500)
|
||||
for btn, fname in (("ContentBody_ContentPlaceHolder1_LinkButtonSummary", "elmhurst_summary.pdf"),
|
||||
("ContentBody_ContentPlaceHolder1_ButtonDebugInforPdf", "elmhurst_worksheet.pdf")):
|
||||
try:
|
||||
with page.expect_download(timeout=60_000) as dl:
|
||||
page.click(f"#{btn}", timeout=15_000)
|
||||
dl.value.save_as(str(_SAMPLE_DIR / fname))
|
||||
print("saved", fname)
|
||||
except Exception as e:
|
||||
print("!! download failed", fname, type(e).__name__, str(e)[:120])
|
||||
|
||||
|
||||
_ORDER = ["property_description", "dimensions", "walls", "roofs", "floors",
|
||||
"openings", "ventilation", "space_heating", "water_heating", "download"]
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2 or sys.argv[1] not in _ORDER:
|
||||
print("usage: build_200004296092.py <" + "|".join(_ORDER) + ">")
|
||||
return 2
|
||||
with E.session() as (ctx, page):
|
||||
globals()[sys.argv[1]](page)
|
||||
print("done:", sys.argv[1], "->", page.url)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
287
scripts/hyde/build_217091901.py
Normal file
287
scripts/hyde/build_217091901.py
Normal file
|
|
@ -0,0 +1,287 @@
|
|||
"""Elmhurst build for UPRN 217091901 (RdSAP-21.0.1, TOP-FLOOR FLAT, band A,
|
||||
SOLID BRICK 280 mm UNINSULATED, FLAT ROOF no insulation (As Built), floor over
|
||||
ANOTHER DWELLING below (zero loss), mains-gas COMBI (PCDB 18658), 6 DG windows
|
||||
~8.62 m², no doors, TFA 63. Lodged SAP 62; engine 60.82 (Δ −1.18, PE +8.7).
|
||||
|
||||
Per-cert worksheet validation. The roof dominates loss (145.6 W/K at flat-default
|
||||
U 2.30); this build confirms the accredited uninsulated flat-roof + solid-brick
|
||||
wall U against the engine. Run:
|
||||
DISPLAY=:99 python scripts/hyde/build_217091901.py <page>
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import elmhurst_lib as E
|
||||
|
||||
DIM = "TabContainer_TabPanelMain_WebUserControlDimensionsMain_"
|
||||
WALL = ("TabContainer_TabPanelMain_InnerTabContainerMain_"
|
||||
"TabPanelExternalWallMain_WebUserControlWallMain_")
|
||||
ROOF = "TabContainer_TabPanelMain_WebUserControlRoofMain_"
|
||||
FLOOR = "TabContainer_TabPanelMain_WebUserControlFloorsMain_"
|
||||
WP = "TabContainer_TabPanelWindowsPanel_"
|
||||
DP = "TabContainer_TabPanelDoorsPanel_"
|
||||
VP = "TabContainer_TabPanelVentilationPanel_"
|
||||
APT = "TabContainer_TabPanelAirPressureTest_"
|
||||
LP = "TabContainer_TabPanelLighting_"
|
||||
MV = "TabContainer_TabPanelMechVent_"
|
||||
WH = "TabContainer_TabPanelWaterHeating_"
|
||||
MH1B = "TabContainer_TabPanelMainHeating1_WebUserControlMainHeating1_"
|
||||
_SAMPLE_DIR = (Path(__file__).parent.parent.parent
|
||||
/ "backend/epc_api/json_samples/real_life_examples"
|
||||
/ "RdSAP-Schema-21.0.1/uprn_217091901")
|
||||
|
||||
|
||||
def _pick(page, suffix, contains):
|
||||
val = page.evaluate(
|
||||
"""(a)=>{const s=document.getElementById(a[0]);if(!s)return null;
|
||||
for(const o of s.options){if(o.text.toLowerCase().includes(a[1].toLowerCase()))return o.value;}return null;}""",
|
||||
[f"{E.FP}{suffix}", contains])
|
||||
if val is not None:
|
||||
E.set_select(page, suffix, val)
|
||||
return val
|
||||
|
||||
|
||||
def _options(page, suffix):
|
||||
return page.evaluate(
|
||||
"""(id)=>{const s=document.getElementById(id);if(!s)return [];
|
||||
return Array.from(s.options).map(o=>o.text);}""", f"{E.FP}{suffix}")
|
||||
|
||||
|
||||
def property_description(page):
|
||||
E.goto(page, "PropertyDescription", "WebFormPropertyDescription.aspx")
|
||||
_pick(page, "DropDownListPropertyType1", "flat") or \
|
||||
E.set_select(page, "DropDownListPropertyType1", "F Flat")
|
||||
E.set_text(page, "TextBoxStoreys", "1")
|
||||
E.set_text(page, "TextBoxHabitableRooms", "3")
|
||||
E.set_text(page, "TextBoxHeatedHabitableRooms", "3")
|
||||
print("date ->", _pick(page, "DropDownListDateBuiltMain", "before 1900"))
|
||||
E.set_select(page, "DropDownListDateBuiltFirst", "")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def flats(page):
|
||||
E.goto(page, "Flats", "WebFormFlats.aspx")
|
||||
print("flat positions:", _options(page, "" if False else "DropDownListPositionOfFlat"))
|
||||
_pick(page, "DropDownListPositionOfFlat", "top") or \
|
||||
E.set_select(page, "DropDownListPositionOfFlat", "Top Floor")
|
||||
E.set_text(page, "TextBoxFloor", "3")
|
||||
E.set_select(page, "RadioButtonListFlatCoridor", "None")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def dimensions(page):
|
||||
E.goto(page, "Dimensions", "WebFormDimensions.aspx")
|
||||
E.set_text(page, f"{DIM}TextBoxFloorAreaLowestFloor", "63.29")
|
||||
E.set_text(page, f"{DIM}TextBoxRoomHeightLowestFloor", "2.85")
|
||||
E.set_text(page, f"{DIM}TextBoxWallPerimeterLowestFloor", "14.0")
|
||||
E.set_text(page, f"{DIM}TextBoxPartyWallLengthLowestFloor", "22.8")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def walls(page):
|
||||
E.goto(page, "Walls", "WebFormWalls.aspx")
|
||||
_pick(page, f"{WALL}DropDownListType", "solid brick")
|
||||
page.wait_for_timeout(500)
|
||||
print("insulation opts:", _options(page, f"{WALL}DropDownListInsulation"))
|
||||
_pick(page, f"{WALL}DropDownListInsulation", "as built") or \
|
||||
_pick(page, f"{WALL}DropDownListInsulation", "none")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def roofs(page):
|
||||
E.goto(page, "Roofs", "WebFormRoofs.aspx")
|
||||
print("ROOF TYPE opts:", _options(page, f"{ROOF}DropDownListType"))
|
||||
_pick(page, f"{ROOF}DropDownListType", "flat")
|
||||
page.wait_for_timeout(600)
|
||||
_pick(page, f"{ROOF}DropDownListInsulationAt", "flat roof insulation")
|
||||
page.wait_for_timeout(600)
|
||||
# discover the thickness/additional field that appears for flat-roof insulation
|
||||
for suf in (f"{ROOF}DropDownListThickness", f"{ROOF}DropDownListInsulation",
|
||||
f"{ROOF}DropDownListAdditionalInsulation"):
|
||||
if page.locator(f"#{E.FP}{suf}").count():
|
||||
print(f" {suf.split('_')[-1]} opts:", _options(page, suf))
|
||||
_pick(page, suf, "as built") or _pick(page, suf, "unknown") or \
|
||||
_pick(page, suf, "none") or _pick(page, suf, "nil")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def floors(page):
|
||||
E.goto(page, "Floors", "WebFormFloors.aspx")
|
||||
print("FLOOR LOCATION opts:", _options(page, f"{FLOOR}DropDownListLocation"))
|
||||
# "(other premises below)" = above a partially-heated space
|
||||
_pick(page, f"{FLOOR}DropDownListLocation", "another dwelling below")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def openings(page):
|
||||
E.goto(page, "Openings", "WebFormOpenings.aspx")
|
||||
E.click_tab(page, "TabContainer_TabPanelWindowsPanel")
|
||||
E.set_single_window(page, 8.62, "North", _glazing(page))
|
||||
_delete_zero_rows(page)
|
||||
E.click_tab(page, "TabContainer_TabPanelDoorsPanel")
|
||||
E.set_text(page, f"{DP}TextBoxDoors", "0")
|
||||
E.set_text(page, f"{DP}TextBoxDoorsInsulated", "0")
|
||||
E.set_text(page, f"{DP}TextBoxDraughtProofedDoors", "0")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def _glazing(page):
|
||||
# Cert glazed_type=5 → SINGLE glazing (epc_codes.csv, RdSAP-Schema-21).
|
||||
for opt in _options(page, f"{WP}DropDownListExtGlazing"):
|
||||
if "single" in opt.lower():
|
||||
return opt
|
||||
return "Single glazed"
|
||||
|
||||
|
||||
def _add_window(page, area, orientation, glazing):
|
||||
print("glazing ->", glazing)
|
||||
E.set_select(page, f"{WP}DropDownListExtGlazing", glazing)
|
||||
page.wait_for_timeout(400)
|
||||
ft = page.locator(f"#{E.FP}{WP}DropDownListExtFrameType")
|
||||
if ft.count():
|
||||
ft.select_option("PVC")
|
||||
gg = page.locator(f"#{E.FP}{WP}DropDownListExtGlazingGap")
|
||||
if gg.count():
|
||||
gg.select_option("12 mm")
|
||||
wid = f"{E.FP}{WP}TextBoxExtWidth"
|
||||
page.evaluate(
|
||||
"""(a)=>{const e=document.getElementById(a[0]);if(e){e.value=a[1];
|
||||
e.dispatchEvent(new Event('input',{bubbles:true}));
|
||||
e.dispatchEvent(new Event('change',{bubbles:true}));
|
||||
e.dispatchEvent(new Event('blur',{bubbles:true}));}}""", [wid, str(area)])
|
||||
page.locator(f"#{E.FP}{WP}TextBoxExtHeight").fill("1.00")
|
||||
page.locator(f"#{E.FP}{WP}DropDownListExtOrientation").select_option(orientation)
|
||||
page.locator(f"#{E.FP}{WP}DropDownListExtBuildingPartId").select_option("Main")
|
||||
page.locator(f"#{E.FP}{WP}DropDownListExtLocation").select_option("External wall")
|
||||
page.wait_for_timeout(300)
|
||||
before = E.window_row_count(page)
|
||||
page.evaluate("(id)=>{const e=document.getElementById(id); if(e)e.click();}", f"{E.FP}{WP}ButtonAddWindow")
|
||||
for _ in range(25):
|
||||
page.wait_for_timeout(200)
|
||||
if E.window_row_count(page) > before:
|
||||
break
|
||||
|
||||
|
||||
def _grid_rows(page):
|
||||
return page.evaluate(
|
||||
"""()=>{const t=document.querySelector("[id*=GridViewExtendedWidows]");
|
||||
if(!t)return[];return Array.from(t.querySelectorAll('tr')).slice(1)
|
||||
.map(r=>Array.from(r.querySelectorAll('td')).map(c=>c.innerText.trim()));}""")
|
||||
|
||||
|
||||
def _delete_zero_rows(page):
|
||||
g = 0
|
||||
while g < 6 and E.window_row_count(page) > 1:
|
||||
g += 1
|
||||
rows = _grid_rows(page)
|
||||
bad = next((i for i, c in enumerate(rows) if len(c) > 1 and c[1] in ("0.00", "0", "0.0")), None)
|
||||
if bad is None:
|
||||
break
|
||||
_delete_row(page, bad)
|
||||
page.wait_for_timeout(400)
|
||||
|
||||
|
||||
def _delete_row(page, idx):
|
||||
before = E.window_row_count(page)
|
||||
btn = page.evaluate(
|
||||
"""(i)=>{const b=document.querySelectorAll("[id*='GridViewExtendedWidows_DeleteButton_']");return b[i]?b[i].id:null;}""", idx)
|
||||
if not btn:
|
||||
return
|
||||
page.evaluate("(id)=>{const e=document.getElementById(id); if(e)e.click();}", btn)
|
||||
page.wait_for_selector(f"#{E.FP}DeleteWindowDialog_LinkButtonYes", state="visible", timeout=5000)
|
||||
page.evaluate("(id)=>{const e=document.getElementById(id); if(e)e.click();}", f"{E.FP}DeleteWindowDialog_LinkButtonYes")
|
||||
for _ in range(20):
|
||||
page.wait_for_timeout(200)
|
||||
if E.window_row_count(page) < before:
|
||||
break
|
||||
|
||||
|
||||
def ventilation(page):
|
||||
E.goto(page, "VentilationAndCooling", "WebFormVentilationAndCooling.aspx")
|
||||
E.click_tab(page, "TabContainer_TabPanelVentilationPanel")
|
||||
E.set_text(page, f"{VP}TextBoxIntermittentFans", "0")
|
||||
cool = page.locator(f"#{E.FP}{VP}CheckBoxFixedSpaceCooling")
|
||||
if cool.count() and cool.is_checked():
|
||||
E.commit(page, cool.uncheck)
|
||||
E.click_tab(page, "TabContainer_TabPanelMechVent")
|
||||
mv = page.locator(f"#{E.FP}{MV}CheckBoxMechanicalVentilation")
|
||||
if mv.count() and mv.is_checked():
|
||||
E.commit(page, mv.uncheck)
|
||||
E.click_tab(page, "TabContainer_TabPanelAirPressureTest")
|
||||
E.set_select(page, f"{APT}DropDownListTestMethod", "Not available")
|
||||
E.click_tab(page, "TabContainer_TabPanelLighting")
|
||||
E.set_text(page, f"{LP}TextBoxLightsTotal", "6")
|
||||
E.set_text(page, f"{LP}TextBoxLedLightsTotal", "0")
|
||||
E.set_text(page, f"{LP}TextBoxCflLightsTotal", "0")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def space_heating(page):
|
||||
E.goto(page, "SpaceHeating", "WebFormSpaceHeating.aspx")
|
||||
page.wait_for_timeout(1000)
|
||||
mhc = page.locator(f"#{E.MH1}TextBoxMainHeatingCode")
|
||||
code = mhc.input_value() if mhc.count() else ""
|
||||
if code and code not in ("0",):
|
||||
print(f"clearing leftover MainHeatingCode {code}")
|
||||
page.evaluate("""(id)=>{const e=document.getElementById(id);if(e){e.value='';
|
||||
e.dispatchEvent(new Event('change',{bubbles:true}));}}""", f"{E.MH1}TextBoxMainHeatingCode")
|
||||
page.wait_for_timeout(400)
|
||||
E.save_close(page)
|
||||
return
|
||||
E.set_heating_dialog(page, f"{MH1B}ButtonMainHeatingCode",
|
||||
"^Gas", "Mains gas", "Boilers", "Post 1998", "Condensing",
|
||||
"Combi condens")
|
||||
print("code:", page.locator(f"#{E.MH1}TextBoxMainHeatingCode").input_value())
|
||||
E.set_heating_dialog(page, f"{MH1B}ButtonMainHeatingControls",
|
||||
"^Boilers", "^Standard", "CBE Programmer, room thermostat and TRVs")
|
||||
print("control:", page.locator(f"#{E.MH1}TextBoxMainHeatingControls").input_value())
|
||||
E.set_select(page, "DropDownListSecondaryHeatingPresent", "No")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def water_heating(page):
|
||||
E.goto(page, "WaterHeating", "WebFormWaterHeating.aspx")
|
||||
E.click_tab(page, "TabContainer_TabPanelWaterHeating")
|
||||
page.wait_for_timeout(400)
|
||||
E.clear_hot_water_cylinder(page)
|
||||
E.set_heating_dialog(page, f"{WH}ButtonWaterHeatingCode",
|
||||
"From Space Heating", "From the primary heating system")
|
||||
print("water code:", page.locator(f"#{E.FP}{WH}TextBoxWaterHeatingCode").input_value())
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def download(page):
|
||||
_SAMPLE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
E.goto(page, "Recommendations", "WebFormRecommendations.aspx")
|
||||
page.wait_for_timeout(1500)
|
||||
with page.expect_navigation(wait_until="networkidle", timeout=40000):
|
||||
page.click("#ContentBody_buttonActionSummary_Link", timeout=15000)
|
||||
page.wait_for_timeout(2000)
|
||||
for btn, fname in (("ContentBody_ContentPlaceHolder1_LinkButtonSummary", "elmhurst_summary.pdf"),
|
||||
("ContentBody_ContentPlaceHolder1_ButtonDebugInforPdf", "elmhurst_worksheet.pdf")):
|
||||
try:
|
||||
with page.expect_download(timeout=60000) as dl:
|
||||
page.click(f"#{btn}", timeout=15000)
|
||||
dl.value.save_as(str(_SAMPLE_DIR / fname))
|
||||
print("saved", fname)
|
||||
except Exception as e:
|
||||
print("!! download failed", fname, type(e).__name__, str(e)[:120])
|
||||
|
||||
|
||||
_ORDER = ["property_description", "flats", "dimensions", "walls", "roofs",
|
||||
"floors", "openings", "ventilation", "space_heating", "water_heating",
|
||||
"download"]
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2 or sys.argv[1] not in _ORDER:
|
||||
print("usage: build_217091901.py <" + "|".join(_ORDER) + ">")
|
||||
return 2
|
||||
with E.session() as (ctx, page):
|
||||
globals()[sys.argv[1]](page)
|
||||
print("done:", sys.argv[1], "->", page.url)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
299
scripts/hyde/build_47084930.py
Normal file
299
scripts/hyde/build_47084930.py
Normal file
|
|
@ -0,0 +1,299 @@
|
|||
"""Elmhurst build for UPRN 47084930 (RdSAP-21.0.1, TOP-FLOOR FLAT, band E,
|
||||
CAVITY wall with INTERNAL insulation (100 mm), FLAT ROOF insulated 75 mm, floor
|
||||
over OTHER PREMISES below, mains-gas COMBI (PCDB 17815), 5 DG windows ~9.9 m²,
|
||||
no doors, TFA 64. Lodged SAP 74; engine 69.51 (Δ −4.49, PE +47 over-count).
|
||||
|
||||
Per-cert worksheet validation. Suspected engine gap: the flat-roof insulation
|
||||
thickness is lodged in `flat_roof_insulation_thickness` (75 mm) but the U-value
|
||||
calc reads `roof_insulation_thickness` (None) → bills the roof at the age-E flat
|
||||
default 1.5 (96 W/K) instead of the 75 mm insulated ~0.5 (~30 W/K). This build
|
||||
gets the accredited flat-roof + floor U. Run:
|
||||
DISPLAY=:99 python scripts/hyde/build_47084930.py <page>
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import elmhurst_lib as E
|
||||
|
||||
DIM = "TabContainer_TabPanelMain_WebUserControlDimensionsMain_"
|
||||
WALL = ("TabContainer_TabPanelMain_InnerTabContainerMain_"
|
||||
"TabPanelExternalWallMain_WebUserControlWallMain_")
|
||||
ROOF = "TabContainer_TabPanelMain_WebUserControlRoofMain_"
|
||||
FLOOR = "TabContainer_TabPanelMain_WebUserControlFloorsMain_"
|
||||
WP = "TabContainer_TabPanelWindowsPanel_"
|
||||
DP = "TabContainer_TabPanelDoorsPanel_"
|
||||
VP = "TabContainer_TabPanelVentilationPanel_"
|
||||
APT = "TabContainer_TabPanelAirPressureTest_"
|
||||
LP = "TabContainer_TabPanelLighting_"
|
||||
MV = "TabContainer_TabPanelMechVent_"
|
||||
WH = "TabContainer_TabPanelWaterHeating_"
|
||||
MH1B = "TabContainer_TabPanelMainHeating1_WebUserControlMainHeating1_"
|
||||
_SAMPLE_DIR = (Path(__file__).parent.parent.parent
|
||||
/ "backend/epc_api/json_samples/real_life_examples"
|
||||
/ "RdSAP-Schema-21.0.1/uprn_47084930")
|
||||
|
||||
|
||||
def _pick(page, suffix, contains):
|
||||
val = page.evaluate(
|
||||
"""(a)=>{const s=document.getElementById(a[0]);if(!s)return null;
|
||||
for(const o of s.options){if(o.text.toLowerCase().includes(a[1].toLowerCase()))return o.value;}return null;}""",
|
||||
[f"{E.FP}{suffix}", contains])
|
||||
if val is not None:
|
||||
E.set_select(page, suffix, val)
|
||||
return val
|
||||
|
||||
|
||||
def _options(page, suffix):
|
||||
return page.evaluate(
|
||||
"""(id)=>{const s=document.getElementById(id);if(!s)return [];
|
||||
return Array.from(s.options).map(o=>o.text);}""", f"{E.FP}{suffix}")
|
||||
|
||||
|
||||
def property_description(page):
|
||||
E.goto(page, "PropertyDescription", "WebFormPropertyDescription.aspx")
|
||||
_pick(page, "DropDownListPropertyType1", "flat") or \
|
||||
E.set_select(page, "DropDownListPropertyType1", "F Flat")
|
||||
E.set_text(page, "TextBoxStoreys", "1")
|
||||
E.set_text(page, "TextBoxHabitableRooms", "3")
|
||||
E.set_text(page, "TextBoxHeatedHabitableRooms", "3")
|
||||
print("date ->", _pick(page, "DropDownListDateBuiltMain", "1967-1975"))
|
||||
E.set_select(page, "DropDownListDateBuiltFirst", "")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def flats(page):
|
||||
E.goto(page, "Flats", "WebFormFlats.aspx")
|
||||
print("flat positions:", _options(page, "" if False else "DropDownListPositionOfFlat"))
|
||||
_pick(page, "DropDownListPositionOfFlat", "top") or \
|
||||
E.set_select(page, "DropDownListPositionOfFlat", "Top Floor")
|
||||
E.set_text(page, "TextBoxFloor", "3")
|
||||
E.set_select(page, "RadioButtonListFlatCoridor", "None")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def dimensions(page):
|
||||
E.goto(page, "Dimensions", "WebFormDimensions.aspx")
|
||||
E.set_text(page, f"{DIM}TextBoxFloorAreaLowestFloor", "64.26")
|
||||
E.set_text(page, f"{DIM}TextBoxRoomHeightLowestFloor", "2.32")
|
||||
E.set_text(page, f"{DIM}TextBoxWallPerimeterLowestFloor", "18.15")
|
||||
E.set_text(page, f"{DIM}TextBoxPartyWallLengthLowestFloor", "18.15")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def walls(page):
|
||||
E.goto(page, "Walls", "WebFormWalls.aspx")
|
||||
E.set_select(page, f"{WALL}DropDownListType", "CA Cavity")
|
||||
page.wait_for_timeout(500)
|
||||
print("insulation opts:", _options(page, f"{WALL}DropDownListInsulation"))
|
||||
_pick(page, f"{WALL}DropDownListInsulation", "internal")
|
||||
page.wait_for_timeout(500)
|
||||
for suf in (f"{WALL}DropDownListInsulationThickness",):
|
||||
if page.locator(f"#{E.FP}{suf}").count():
|
||||
print("wall ins thickness opts:", _options(page, suf))
|
||||
_pick(page, suf, "100")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def roofs(page):
|
||||
E.goto(page, "Roofs", "WebFormRoofs.aspx")
|
||||
print("ROOF TYPE opts:", _options(page, f"{ROOF}DropDownListType"))
|
||||
_pick(page, f"{ROOF}DropDownListType", "flat")
|
||||
page.wait_for_timeout(600)
|
||||
# discover flat-roof insulation fields
|
||||
for suf in (f"{ROOF}DropDownListInsulationAt", f"{ROOF}DropDownListThickness",
|
||||
f"{ROOF}DropDownListInsulation"):
|
||||
if page.locator(f"#{E.FP}{suf}").count():
|
||||
print(f" {suf.split('_')[-1]} opts:", _options(page, suf))
|
||||
# flat roof insulated 75 mm
|
||||
if page.locator(f"#{E.FP}{ROOF}DropDownListThickness").count():
|
||||
_pick(page, f"{ROOF}DropDownListThickness", "75")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def floors(page):
|
||||
E.goto(page, "Floors", "WebFormFloors.aspx")
|
||||
print("FLOOR LOCATION opts:", _options(page, f"{FLOOR}DropDownListLocation"))
|
||||
# "(other premises below)" = above a partially-heated space
|
||||
(_pick(page, f"{FLOOR}DropDownListLocation", "partially heated")
|
||||
or _pick(page, f"{FLOOR}DropDownListLocation", "other premises")
|
||||
or _pick(page, f"{FLOOR}DropDownListLocation", "unheated"))
|
||||
page.wait_for_timeout(400)
|
||||
tsel = page.locator(f"#{E.FP}{FLOOR}DropDownListType")
|
||||
if tsel.count():
|
||||
_pick(page, f"{FLOOR}DropDownListType", "solid")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def openings(page):
|
||||
E.goto(page, "Openings", "WebFormOpenings.aspx")
|
||||
E.click_tab(page, "TabContainer_TabPanelWindowsPanel")
|
||||
_add_window(page, 9.9, "North", _glazing(page))
|
||||
_delete_zero_rows(page)
|
||||
E.click_tab(page, "TabContainer_TabPanelDoorsPanel")
|
||||
E.set_text(page, f"{DP}TextBoxDoors", "0")
|
||||
E.set_text(page, f"{DP}TextBoxDoorsInsulated", "0")
|
||||
E.set_text(page, f"{DP}TextBoxDraughtProofedDoors", "0")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def _glazing(page):
|
||||
for needle in ("unknown install date", "before 2002", "pre 2002"):
|
||||
for opt in _options(page, f"{WP}DropDownListExtGlazing"):
|
||||
low = opt.lower()
|
||||
if needle in low and "triple" not in low and "single" not in low and "known data" not in low:
|
||||
return opt
|
||||
return "Double post or during 2022"
|
||||
|
||||
|
||||
def _add_window(page, area, orientation, glazing):
|
||||
print("glazing ->", glazing)
|
||||
E.set_select(page, f"{WP}DropDownListExtGlazing", glazing)
|
||||
page.wait_for_timeout(400)
|
||||
ft = page.locator(f"#{E.FP}{WP}DropDownListExtFrameType")
|
||||
if ft.count():
|
||||
ft.select_option("PVC")
|
||||
gg = page.locator(f"#{E.FP}{WP}DropDownListExtGlazingGap")
|
||||
if gg.count():
|
||||
gg.select_option("12 mm")
|
||||
wid = f"{E.FP}{WP}TextBoxExtWidth"
|
||||
page.evaluate(
|
||||
"""(a)=>{const e=document.getElementById(a[0]);if(e){e.value=a[1];
|
||||
e.dispatchEvent(new Event('input',{bubbles:true}));
|
||||
e.dispatchEvent(new Event('change',{bubbles:true}));
|
||||
e.dispatchEvent(new Event('blur',{bubbles:true}));}}""", [wid, str(area)])
|
||||
page.locator(f"#{E.FP}{WP}TextBoxExtHeight").fill("1.00")
|
||||
page.locator(f"#{E.FP}{WP}DropDownListExtOrientation").select_option(orientation)
|
||||
page.locator(f"#{E.FP}{WP}DropDownListExtBuildingPartId").select_option("Main")
|
||||
page.locator(f"#{E.FP}{WP}DropDownListExtLocation").select_option("External wall")
|
||||
page.wait_for_timeout(300)
|
||||
before = E.window_row_count(page)
|
||||
page.evaluate("(id)=>{const e=document.getElementById(id); if(e)e.click();}", f"{E.FP}{WP}ButtonAddWindow")
|
||||
for _ in range(25):
|
||||
page.wait_for_timeout(200)
|
||||
if E.window_row_count(page) > before:
|
||||
break
|
||||
|
||||
|
||||
def _grid_rows(page):
|
||||
return page.evaluate(
|
||||
"""()=>{const t=document.querySelector("[id*=GridViewExtendedWidows]");
|
||||
if(!t)return[];return Array.from(t.querySelectorAll('tr')).slice(1)
|
||||
.map(r=>Array.from(r.querySelectorAll('td')).map(c=>c.innerText.trim()));}""")
|
||||
|
||||
|
||||
def _delete_zero_rows(page):
|
||||
g = 0
|
||||
while g < 6 and E.window_row_count(page) > 1:
|
||||
g += 1
|
||||
rows = _grid_rows(page)
|
||||
bad = next((i for i, c in enumerate(rows) if len(c) > 1 and c[1] in ("0.00", "0", "0.0")), None)
|
||||
if bad is None:
|
||||
break
|
||||
_delete_row(page, bad)
|
||||
page.wait_for_timeout(400)
|
||||
|
||||
|
||||
def _delete_row(page, idx):
|
||||
before = E.window_row_count(page)
|
||||
btn = page.evaluate(
|
||||
"""(i)=>{const b=document.querySelectorAll("[id*='GridViewExtendedWidows_DeleteButton_']");return b[i]?b[i].id:null;}""", idx)
|
||||
if not btn:
|
||||
return
|
||||
page.evaluate("(id)=>{const e=document.getElementById(id); if(e)e.click();}", btn)
|
||||
page.wait_for_selector(f"#{E.FP}DeleteWindowDialog_LinkButtonYes", state="visible", timeout=5000)
|
||||
page.evaluate("(id)=>{const e=document.getElementById(id); if(e)e.click();}", f"{E.FP}DeleteWindowDialog_LinkButtonYes")
|
||||
for _ in range(20):
|
||||
page.wait_for_timeout(200)
|
||||
if E.window_row_count(page) < before:
|
||||
break
|
||||
|
||||
|
||||
def ventilation(page):
|
||||
E.goto(page, "VentilationAndCooling", "WebFormVentilationAndCooling.aspx")
|
||||
E.click_tab(page, "TabContainer_TabPanelVentilationPanel")
|
||||
E.set_text(page, f"{VP}TextBoxIntermittentFans", "0")
|
||||
cool = page.locator(f"#{E.FP}{VP}CheckBoxFixedSpaceCooling")
|
||||
if cool.count() and cool.is_checked():
|
||||
E.commit(page, cool.uncheck)
|
||||
E.click_tab(page, "TabContainer_TabPanelMechVent")
|
||||
mv = page.locator(f"#{E.FP}{MV}CheckBoxMechanicalVentilation")
|
||||
if mv.count() and mv.is_checked():
|
||||
E.commit(page, mv.uncheck)
|
||||
E.click_tab(page, "TabContainer_TabPanelAirPressureTest")
|
||||
E.set_select(page, f"{APT}DropDownListTestMethod", "Not available")
|
||||
E.click_tab(page, "TabContainer_TabPanelLighting")
|
||||
E.set_text(page, f"{LP}TextBoxLightsTotal", "6")
|
||||
E.set_text(page, f"{LP}TextBoxLedLightsTotal", "0")
|
||||
E.set_text(page, f"{LP}TextBoxCflLightsTotal", "0")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def space_heating(page):
|
||||
E.goto(page, "SpaceHeating", "WebFormSpaceHeating.aspx")
|
||||
page.wait_for_timeout(1000)
|
||||
mhc = page.locator(f"#{E.MH1}TextBoxMainHeatingCode")
|
||||
code = mhc.input_value() if mhc.count() else ""
|
||||
if code and code not in ("0",):
|
||||
print(f"clearing leftover MainHeatingCode {code}")
|
||||
page.evaluate("""(id)=>{const e=document.getElementById(id);if(e){e.value='';
|
||||
e.dispatchEvent(new Event('change',{bubbles:true}));}}""", f"{E.MH1}TextBoxMainHeatingCode")
|
||||
page.wait_for_timeout(400)
|
||||
E.save_close(page)
|
||||
return
|
||||
E.set_heating_dialog(page, f"{MH1B}ButtonMainHeatingCode",
|
||||
"^Gas", "Mains gas", "Boilers", "Post 1998", "Condensing",
|
||||
"Combi condens")
|
||||
print("code:", page.locator(f"#{E.MH1}TextBoxMainHeatingCode").input_value())
|
||||
E.set_heating_dialog(page, f"{MH1B}ButtonMainHeatingControls",
|
||||
"^Boilers", "^Standard", "CBE Programmer, room thermostat and TRVs")
|
||||
print("control:", page.locator(f"#{E.MH1}TextBoxMainHeatingControls").input_value())
|
||||
E.set_select(page, "DropDownListSecondaryHeatingPresent", "No")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def water_heating(page):
|
||||
E.goto(page, "WaterHeating", "WebFormWaterHeating.aspx")
|
||||
E.click_tab(page, "TabContainer_TabPanelWaterHeating")
|
||||
page.wait_for_timeout(400)
|
||||
E.clear_hot_water_cylinder(page)
|
||||
E.set_heating_dialog(page, f"{WH}ButtonWaterHeatingCode",
|
||||
"From Space Heating", "From the primary heating system")
|
||||
print("water code:", page.locator(f"#{E.FP}{WH}TextBoxWaterHeatingCode").input_value())
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def download(page):
|
||||
_SAMPLE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
E.goto(page, "Recommendations", "WebFormRecommendations.aspx")
|
||||
page.wait_for_timeout(1500)
|
||||
with page.expect_navigation(wait_until="networkidle", timeout=40000):
|
||||
page.click("#ContentBody_buttonActionSummary_Link", timeout=15000)
|
||||
page.wait_for_timeout(2000)
|
||||
for btn, fname in (("ContentBody_ContentPlaceHolder1_LinkButtonSummary", "elmhurst_summary.pdf"),
|
||||
("ContentBody_ContentPlaceHolder1_ButtonDebugInforPdf", "elmhurst_worksheet.pdf")):
|
||||
try:
|
||||
with page.expect_download(timeout=60000) as dl:
|
||||
page.click(f"#{btn}", timeout=15000)
|
||||
dl.value.save_as(str(_SAMPLE_DIR / fname))
|
||||
print("saved", fname)
|
||||
except Exception as e:
|
||||
print("!! download failed", fname, type(e).__name__, str(e)[:120])
|
||||
|
||||
|
||||
_ORDER = ["property_description", "flats", "dimensions", "walls", "roofs",
|
||||
"floors", "openings", "ventilation", "space_heating", "water_heating",
|
||||
"download"]
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2 or sys.argv[1] not in _ORDER:
|
||||
print("usage: build_47084930.py <" + "|".join(_ORDER) + ">")
|
||||
return 2
|
||||
with E.session() as (ctx, page):
|
||||
globals()[sys.argv[1]](page)
|
||||
print("done:", sys.argv[1], "->", page.url)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -235,24 +235,129 @@ def add_combined_window(
|
|||
) -> None:
|
||||
"""Add ONE window row of `total_area_m2` (width=area, height=1.0). Sets the
|
||||
required per-row fields (orientation/location/building-part) so the
|
||||
Recommendations validation passes. Caller then deletes the prior rows."""
|
||||
Recommendations validation passes. Caller then deletes the prior rows.
|
||||
|
||||
Glazing MUST be selected first (it AutoPostBacks and re-renders the form);
|
||||
width/height are then set via a Playwright `.fill()` + Tab so the ASP.NET
|
||||
AutoPostBack fires and the server-side Area (w×h) commits — a JS-set of the
|
||||
value bypasses the postback and Elmhurst persists a 0.00-area row.
|
||||
Frame Type + Glazing Gap are REQUIRED by the Recommendations validation
|
||||
gate (else "Frame Type/Glazing Gap must be entered" blocks the worksheet)."""
|
||||
set_select(page, f"{_WP}DropDownListExtGlazing", glazing)
|
||||
page.locator(f"#{FP}{_WP}TextBoxExtWidth").fill(str(total_area_m2))
|
||||
h = page.locator(f"#{FP}{_WP}TextBoxExtHeight")
|
||||
h.fill("1.00")
|
||||
commit(page, lambda: h.press("Tab"))
|
||||
page.locator(f"#{FP}{_WP}DropDownListExtOrientation").select_option(orientation)
|
||||
page.locator(f"#{FP}{_WP}DropDownListExtBuildingPartId").select_option("Main")
|
||||
page.locator(f"#{FP}{_WP}DropDownListExtLocation").select_option("External wall")
|
||||
page.wait_for_timeout(200)
|
||||
page.wait_for_timeout(SETTLE_MS)
|
||||
|
||||
def _fill_tab(suffix: str, value: str) -> None:
|
||||
loc = page.locator(f"#{FP}{suffix}")
|
||||
loc.fill(value)
|
||||
loc.press("Tab")
|
||||
page.wait_for_timeout(1200) # AutoPostBack settle
|
||||
|
||||
_fill_tab(f"{_WP}TextBoxExtWidth", str(total_area_m2))
|
||||
_fill_tab(f"{_WP}TextBoxExtHeight", "1.00")
|
||||
# Frame Type (PVC) + Glazing Gap (12 mm) are validation-required per row.
|
||||
for suffix, opt in (
|
||||
(f"{_WP}DropDownListExtFrameType", "PVC"),
|
||||
(f"{_WP}DropDownListExtGlazingGap", "12 mm"),
|
||||
(f"{_WP}DropDownListExtOrientation", orientation),
|
||||
(f"{_WP}DropDownListExtBuildingPartId", "Main"),
|
||||
(f"{_WP}DropDownListExtLocation", "External wall"),
|
||||
):
|
||||
loc = page.locator(f"#{FP}{suffix}")
|
||||
if loc.count():
|
||||
try:
|
||||
loc.select_option(opt)
|
||||
except Exception:
|
||||
pass
|
||||
page.wait_for_timeout(250)
|
||||
page.wait_for_timeout(300)
|
||||
before = window_row_count(page)
|
||||
_jsclick(page, f"{FP}{_WP}ButtonAddWindow")
|
||||
for _ in range(20):
|
||||
page.wait_for_timeout(200)
|
||||
page.wait_for_timeout(250)
|
||||
if window_row_count(page) > before:
|
||||
break
|
||||
|
||||
|
||||
def _delete_window_at_index(page: Page, i: int) -> bool:
|
||||
"""Delete the window grid row at DOM index `i` via its per-row Delete
|
||||
image button + the Yes modal (JS-clicked to bypass the modal background
|
||||
that intercepts real clicks). Returns True if a delete fired."""
|
||||
bid = (
|
||||
"ContentBody_ContentPlaceHolder1_TabContainer_TabPanelWindowsPanel_"
|
||||
f"GridViewExtendedWidows_DeleteButton_{i}"
|
||||
)
|
||||
if not page.evaluate("(id)=>!!document.getElementById(id)", bid):
|
||||
return False
|
||||
page.evaluate("(id)=>{const e=document.getElementById(id);if(e)e.click();}", bid)
|
||||
page.wait_for_timeout(2000)
|
||||
yid = "ContentBody_ContentPlaceHolder1_DeleteWindowDialog_LinkButtonYes"
|
||||
if not page.evaluate(
|
||||
"(id)=>{const e=document.getElementById(id);return e?e.offsetParent!==null:false;}",
|
||||
yid,
|
||||
):
|
||||
return False
|
||||
page.evaluate("(id)=>{const e=document.getElementById(id);if(e)e.click();}", yid)
|
||||
page.wait_for_timeout(2500)
|
||||
return True
|
||||
|
||||
|
||||
def set_single_window(
|
||||
page: Page, total_area_m2: float, orientation: str = "North",
|
||||
glazing: str = "Double post or during 2022",
|
||||
) -> None:
|
||||
"""Leave the window grid holding EXACTLY one correct row of
|
||||
`total_area_m2`, cleaning any rows carried over from a prior build of the
|
||||
SHARED assessment.
|
||||
|
||||
The grid can't be emptied (Elmhurst refuses to delete the last row) and it
|
||||
has no inline edit, so per [[window-grid]] the reliable pattern is: ADD the
|
||||
fresh window (it inserts at DOM index 0), then delete the STALE rows BELOW
|
||||
it (indices high->low, never index 0) so the freshly-added row survives as
|
||||
the single remaining window. Deleting index 0 (delete_first_window) would
|
||||
drop the good row and strand a 0.00-area junk row that then blocks the
|
||||
Recommendations gate."""
|
||||
add_combined_window(page, total_area_m2, orientation, glazing)
|
||||
page.wait_for_timeout(500)
|
||||
guard = 0
|
||||
while window_row_count(page) > 1 and guard < 25:
|
||||
guard += 1
|
||||
i = window_row_count(page) - 1 # bottom-most (oldest) stale row
|
||||
if not _delete_window_at_index(page, i):
|
||||
break
|
||||
|
||||
|
||||
def reset_transient_state(page: Page) -> None:
|
||||
"""Return the shared assessment to a NEUTRAL state after a run so the next
|
||||
build (or Khalim's) is not polluted by this cert's carried-over settings.
|
||||
Clears the room-in-roof age band, forces secondary-heating present = No,
|
||||
and the electricity meter back to Single. The window grid is left to the
|
||||
next build's `set_single_window` (Elmhurst can't empty it to zero rows).
|
||||
Call once at the end of a build session."""
|
||||
goto(page, "PropertyDescription", "WebFormPropertyDescription.aspx")
|
||||
rr = page.locator("#ContentBody_ContentPlaceHolder1_DropDownListRoomInRoofMain")
|
||||
if rr.count() and rr.input_value():
|
||||
rr.select_option("")
|
||||
page.wait_for_timeout(SETTLE_MS)
|
||||
save_close(page)
|
||||
goto(page, "SpaceHeating", "WebFormSpaceHeating.aspx")
|
||||
page.wait_for_timeout(SETTLE_MS)
|
||||
sec = page.locator(f"#{FP}DropDownListSecondaryHeatingPresent")
|
||||
if sec.count():
|
||||
try:
|
||||
set_select(page, "DropDownListSecondaryHeatingPresent", "No")
|
||||
except Exception:
|
||||
pass
|
||||
meters = "TabContainer_TabPanelMeters"
|
||||
click_tab(page, meters)
|
||||
et = page.locator(f"#{FP}{meters}_RadioButtonListElectricityType")
|
||||
if et.count():
|
||||
try:
|
||||
set_select(page, f"{meters}_RadioButtonListElectricityType", "Single")
|
||||
except Exception:
|
||||
pass
|
||||
save_close(page)
|
||||
|
||||
|
||||
def delete_first_window(page: Page) -> None:
|
||||
"""Delete the first window row via the Yes/No modal. Will refuse to drop the
|
||||
LAST row (Elmhurst blocks it) — so add the replacement BEFORE deleting."""
|
||||
|
|
@ -362,6 +467,63 @@ def select_boiler(page: Page, query: str, ref: str) -> str:
|
|||
return "ok"
|
||||
|
||||
|
||||
def clear_main_heating_code(page: Page) -> None:
|
||||
"""Empty Main Heating 1's SAP code via a real server postback (set the
|
||||
cascade's top-level dropdown to '' + OK). REQUIRED before `set_pcdb_boiler`:
|
||||
while any code is set, the PCDF reference textbox and the boiler-search
|
||||
magnifying glass both render `aspNetDisabled`. They only enable when the
|
||||
code is empty server-side (a JS-clear alone flips the class cosmetically but
|
||||
leaves the postback unwired)."""
|
||||
page.evaluate(
|
||||
"(id)=>{const e=document.getElementById(id); if(e)e.click();}",
|
||||
f"{MH1}ButtonMainHeatingCode",
|
||||
)
|
||||
page.wait_for_timeout(SETTLE_MS * 3)
|
||||
page.evaluate(
|
||||
"""(id)=>{const s=document.getElementById(id);if(s){s.value='';
|
||||
s.dispatchEvent(new Event('change',{bubbles:true}));}}""",
|
||||
f"{HEATING_DIALOG}DropDownList1",
|
||||
)
|
||||
page.wait_for_timeout(SETTLE_MS * 3)
|
||||
dialog_commit(page, "OK")
|
||||
page.wait_for_timeout(SETTLE_MS * 2)
|
||||
|
||||
|
||||
def set_pcdb_boiler(page: Page, pcdb_index: "int | str") -> str:
|
||||
"""Set Main Heating 1 to the EXACT PCDB boiler by its EPC
|
||||
`main_heating_index_number` (e.g. 18908 = Worcester Greenstar 4000,
|
||||
GR4700iW 25 C NG, 88.70%). Elmhurst resolves it to the product's PCDB
|
||||
winter efficiency — the same value the SAP engine uses — so the worksheet
|
||||
and our calculator compare like-for-like (not against the generic BGW
|
||||
SAP-Table 84%). Returns the resolved boiler description.
|
||||
|
||||
Hard-won mechanism (supersedes the old "PCDB disabled" note):
|
||||
1. The PCDF reference textbox is only writable while the main heating CODE
|
||||
is empty — call `clear_main_heating_code(page)` first.
|
||||
2. The index must be entered via REAL keyboard + Tab. A JS-set `.value`
|
||||
does NOT survive the field's AutoPostBack, so the boiler never
|
||||
resolves (the original failure mode). Ctrl-A + type + Tab works.
|
||||
3. The `TextBoxPCDFBoilerReference` direct-entry path is far simpler than
|
||||
the magnifying-glass grid (whose modal has no layout box, so
|
||||
mouse/Playwright clicks can't target rows). The grid search DOES work
|
||||
(JS-focus the FullSearch box + `keyboard.type` + fire
|
||||
HyperLinkActionBoilerSearch → "Ref No" column == the PCDB index) if a
|
||||
name lookup is ever needed, but direct entry is preferred."""
|
||||
fld = f"{MH1}TextBoxPCDFBoilerReference"
|
||||
page.evaluate(
|
||||
"(id)=>{const e=document.getElementById(id);if(e){e.focus();e.select();}}", fld
|
||||
)
|
||||
page.wait_for_timeout(SETTLE_MS)
|
||||
page.keyboard.press("Control+a")
|
||||
page.keyboard.type(str(pcdb_index), delay=40)
|
||||
page.keyboard.press("Tab")
|
||||
page.wait_for_timeout(SETTLE_MS * 5) # AutoPostBack resolves the PCDB record
|
||||
return page.evaluate(
|
||||
"""()=>{const e=document.querySelector('[id*=LabelMHSSapCodeDescription]');
|
||||
return e?e.innerText.trim():'';}"""
|
||||
)
|
||||
|
||||
|
||||
def read_default_uvalue(
|
||||
page: Page, suffix: Optional[str] = None, label_regex: str = r"Default U-value"
|
||||
) -> Optional[float]:
|
||||
|
|
|
|||
|
|
@ -910,10 +910,12 @@ def main() -> None:
|
|||
print_table=False,
|
||||
)
|
||||
# The full candidate menu (every Generator Option + its cost), so
|
||||
# measures the Optimiser did not select are still visible. A predicted
|
||||
# Property has no lodged cert, so the synthesised Effective EPC is used.
|
||||
# measures the Optimiser did not select are still visible. Generated
|
||||
# from the Effective EPC — the same picture the plan above modelled;
|
||||
# the lodged cert would print a menu the Optimiser never saw (e.g.
|
||||
# cavity fill on a wall an override moved to timber frame).
|
||||
candidates: list[Recommendation] = candidate_recommendations(
|
||||
epc if epc is not None else effective_epc,
|
||||
effective_epc,
|
||||
planning_restrictions=restrictions,
|
||||
solar_insights=solar_insights,
|
||||
considered_measures=considered,
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ def _sap_result(
|
|||
space_heating_kwh_per_yr=0.0,
|
||||
space_cooling_kwh_per_yr=0.0,
|
||||
fabric_energy_efficiency_kwh_per_m2_yr=0.0,
|
||||
design_heat_loss_kw=0.0,
|
||||
main_heating_fuel_kwh_per_yr=main_heating_fuel_kwh_per_yr,
|
||||
main_2_heating_fuel_kwh_per_yr=main_2_heating_fuel_kwh_per_yr,
|
||||
secondary_heating_fuel_kwh_per_yr=secondary_heating_fuel_kwh_per_yr,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ calculator reads from the primary system; the overlay is whole-dwelling.
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
|
||||
from domain.epc.property_overrides.main_heating_system_type import MainHeatingSystemType
|
||||
|
|
@ -58,6 +60,68 @@ def test_electric_room_heaters_assume_a_dual_economy7_meter() -> None:
|
|||
assert simulation.heating.meter_type == "Dual"
|
||||
|
||||
|
||||
def test_electric_room_heaters_overlay_the_room_heater_category() -> None:
|
||||
# A landlord "Electric room heaters" override replaces the lodged system, so
|
||||
# the overlay must stamp the room-heater heating category (SAP Table 4a
|
||||
# Category 10). Leaving it unset lets a replaced storage heater's category 7
|
||||
# survive onto the effective cert, which the SAP 10.2 Table 12a resolver
|
||||
# (keyed on category) mis-reads as off-peak storage and bills the peaky room
|
||||
# heaters at the all-night low rate.
|
||||
# Act
|
||||
simulation = main_heating_overlay_for("Electric room heaters", 0)
|
||||
|
||||
# Assert — SAP Table 4a Category 10 (Room heaters).
|
||||
assert simulation is not None
|
||||
assert simulation.heating is not None
|
||||
assert simulation.heating.main_heating_category == 10
|
||||
|
||||
|
||||
def test_electric_room_heaters_overlay_the_room_heater_charge_control() -> None:
|
||||
# A system-replacing override must stamp its own control, not inherit the
|
||||
# replaced system's. Electric room heaters take the Table 4e Group 6
|
||||
# room-heater control 2601 (+0.3 C, the conservative no-thermostat default,
|
||||
# matching the gas/oil/solid room heaters). Leaving it unset lets a replaced
|
||||
# storage heater's manual charge control 2401 (+0.7 C) survive and over-warm
|
||||
# the modelled dwelling.
|
||||
# Act
|
||||
simulation = main_heating_overlay_for("Electric room heaters", 0)
|
||||
|
||||
# Assert — SAP Table 4e Group 6 room-heater control.
|
||||
assert simulation is not None
|
||||
assert simulation.heating is not None
|
||||
assert simulation.heating.main_heating_control == 2601
|
||||
|
||||
|
||||
def test_overlay_logs_when_it_cannot_complete_a_companion_set(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
# An archetype the overlay cannot yet fully companion — here an air-source
|
||||
# heat pump, whose Table 4e Group 2 conservative control is not yet mapped —
|
||||
# is still overlaid (log-and-continue), but the incomplete companion set is
|
||||
# logged as an error so the known gap is visible (CloudWatch) rather than
|
||||
# silently inheriting the replaced system's stale value.
|
||||
# Act
|
||||
with caplog.at_level(logging.ERROR):
|
||||
main_heating_overlay_for("Air source heat pump", 0)
|
||||
|
||||
# Assert — the gap is reported and names the archetype.
|
||||
assert "Air source heat pump" in caplog.text
|
||||
|
||||
|
||||
def test_complete_room_heater_overlay_does_not_log_a_companion_gap(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
# Electric room heaters now resolve a full companion set (category 10 +
|
||||
# control 2601), so the overlay must NOT log an incomplete-companion gap —
|
||||
# guarding that the fix keeps the set complete.
|
||||
# Act
|
||||
with caplog.at_level(logging.ERROR):
|
||||
main_heating_overlay_for("Electric room heaters", 0)
|
||||
|
||||
# Assert
|
||||
assert "incomplete companion set" not in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("main_heating_value", "code"),
|
||||
[
|
||||
|
|
@ -169,6 +233,80 @@ def test_off_peak_underfloor_asserts_a_dual_meter(main_heating_value: str) -> No
|
|||
assert simulation.heating.meter_type == "Dual"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"main_heating_value",
|
||||
[
|
||||
"Electric underfloor, in concrete slab (off-peak)",
|
||||
"Electric underfloor, integrated storage and direct-acting",
|
||||
"Electric underfloor, in screed above insulation",
|
||||
],
|
||||
)
|
||||
def test_electric_underfloor_overlays_the_underfloor_category(
|
||||
main_heating_value: str,
|
||||
) -> None:
|
||||
# A system-replacing override must stamp the underfloor heating category
|
||||
# (SAP Table 4a Category 8). Leaving it unset lets the replaced system's
|
||||
# category survive onto the effective cert — property 711795's community
|
||||
# heating (category 6) survived an underfloor override, so the heating
|
||||
# generator read the dwelling as a heat network and offered no heating
|
||||
# upgrade at all, and the baseline mis-rated 51 vs 67 (ADR-0050).
|
||||
# Act
|
||||
simulation = main_heating_overlay_for(main_heating_value, 0)
|
||||
|
||||
# Assert — SAP Table 4a Category 8 (Electric underfloor heating).
|
||||
assert simulation is not None
|
||||
assert simulation.heating is not None
|
||||
assert simulation.heating.main_heating_category == 8
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"main_heating_value",
|
||||
[
|
||||
"Electric underfloor, in concrete slab (off-peak)",
|
||||
"Electric underfloor, integrated storage and direct-acting",
|
||||
"Electric underfloor, in screed above insulation",
|
||||
],
|
||||
)
|
||||
def test_electric_underfloor_drags_the_conservative_group_7_control(
|
||||
main_heating_value: str,
|
||||
) -> None:
|
||||
# The landlord names the system, not its control, so the overlay assumes the
|
||||
# conservative Table 4e Group 7 default: 2701 ("no time or thermostatic
|
||||
# control", +0.3 C — the largest adjustment in the group, so an unobserved
|
||||
# control is never over-credited). Mirrors storage-manual 2401 / room-heater
|
||||
# 2601; also overwrites a stale control inherited from the replaced system
|
||||
# (711795 kept a community control 2311) — ADR-0050.
|
||||
# Act
|
||||
simulation = main_heating_overlay_for(main_heating_value, 0)
|
||||
|
||||
# Assert — SAP Table 4e Group 7 conservative underfloor control.
|
||||
assert simulation is not None
|
||||
assert simulation.heating is not None
|
||||
assert simulation.heating.main_heating_control == 2701
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"main_heating_value",
|
||||
[
|
||||
"Electric underfloor, in concrete slab (off-peak)",
|
||||
"Electric underfloor, integrated storage and direct-acting",
|
||||
"Electric underfloor, in screed above insulation",
|
||||
],
|
||||
)
|
||||
def test_complete_underfloor_overlay_does_not_log_a_companion_gap(
|
||||
main_heating_value: str, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
# Electric underfloor now resolves a full companion set (category 8 +
|
||||
# control 2701, ADR-0050), so the overlay must NOT log an
|
||||
# incomplete-companion gap — guarding that the set stays complete.
|
||||
# Act
|
||||
with caplog.at_level(logging.ERROR):
|
||||
main_heating_overlay_for(main_heating_value, 0)
|
||||
|
||||
# Assert
|
||||
assert "incomplete companion set" not in caplog.text
|
||||
|
||||
|
||||
def test_screed_underfloor_defers_the_meter_to_the_cert() -> None:
|
||||
# "In screed above insulation (standard or off peak)" is tariff-ambiguous (81%
|
||||
# of the corpus lodge off-peak), so the archetype cannot know the meter — it
|
||||
|
|
|
|||
|
|
@ -82,3 +82,19 @@ def test_oil_and_lpg_dwellings_are_reusable_wet_systems() -> None:
|
|||
assert ashp_cost_inputs(_with_fuel(28)).existing_system is AshpExistingSystem.OIL
|
||||
assert ashp_cost_inputs(_with_fuel(27)).existing_system is AshpExistingSystem.LPG
|
||||
assert ashp_cost_inputs(_with_fuel(28)).has_reusable_wet_system is True
|
||||
|
||||
|
||||
def test_threaded_design_heat_loss_drives_the_cost_band_over_the_proxy() -> None:
|
||||
# When the caller threads the calculator's real design heat loss (ADR-0049),
|
||||
# the ASHP cost sizes to it — not the floor-area proxy — so a leaky dwelling's
|
||||
# bigger pump is priced on the right band.
|
||||
# Arrange
|
||||
epc = parse_recommendation_summary(
|
||||
"ashp_from_system_boiler_with_cylinder_001431_before.pdf"
|
||||
)
|
||||
|
||||
# Act
|
||||
inputs: AshpCostInputs = ashp_cost_inputs(epc, design_heat_loss_kw=17.5)
|
||||
|
||||
# Assert — the threaded value, not the 4.5 kW proxy.
|
||||
assert inputs.design_heat_loss_kw == 17.5
|
||||
|
|
|
|||
|
|
@ -664,7 +664,11 @@ def test_ashp_overlay_scores_the_vaillant_end_state_from_a_gas_boiler() -> None:
|
|||
before: EpcPropertyData = parse_recommendation_summary(
|
||||
"ashp_from_gas_boiler_001431_before.pdf"
|
||||
)
|
||||
recommendation: Recommendation | None = recommend_heating(before, _AnyProduct())
|
||||
recommendation: Recommendation | None = recommend_heating(
|
||||
before,
|
||||
_AnyProduct(),
|
||||
design_heat_loss_kw=Sap10Calculator().calculate(before).design_heat_loss_kw,
|
||||
)
|
||||
assert recommendation is not None
|
||||
option = next(
|
||||
o for o in recommendation.options if o.measure_type == "air_source_heat_pump"
|
||||
|
|
@ -678,12 +682,15 @@ def test_ashp_overlay_scores_the_vaillant_end_state_from_a_gas_boiler() -> None:
|
|||
# CO2/PE are the postcode DEMAND cascade now that `Sap10Calculator.
|
||||
# calculate` computes EPC emissions/PE on local weather (SAP 10.2
|
||||
# Appendix U p.124); SAP is unchanged (UK-average rating cascade).
|
||||
# Re-pinned after ADR-0049 sizes the pump to this dwelling's 9.69 kW design
|
||||
# heat loss (the 10 kW aroTHERM plus, 110273) instead of the fixed 5 kW unit,
|
||||
# which was undersized here — the correctly-sized pump raises the end-state SAP.
|
||||
_assert_overlay_scores(
|
||||
before,
|
||||
option.overlay,
|
||||
sap=51.99820176096402,
|
||||
co2=1065.7593506066496,
|
||||
pe=10995.781557709413,
|
||||
sap=69.6498827017577,
|
||||
co2=698.896585317197,
|
||||
pe=7271.044741124457,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -697,7 +704,11 @@ def test_ashp_overlay_scores_the_vaillant_end_state_from_a_gas_boiler_instant_hw
|
|||
before: EpcPropertyData = parse_recommendation_summary(
|
||||
"ashp_from_gas_boiler_instant_hw_001431_before.pdf"
|
||||
)
|
||||
recommendation: Recommendation | None = recommend_heating(before, _AnyProduct())
|
||||
recommendation: Recommendation | None = recommend_heating(
|
||||
before,
|
||||
_AnyProduct(),
|
||||
design_heat_loss_kw=Sap10Calculator().calculate(before).design_heat_loss_kw,
|
||||
)
|
||||
assert recommendation is not None
|
||||
option = next(
|
||||
o for o in recommendation.options if o.measure_type == "air_source_heat_pump"
|
||||
|
|
@ -709,12 +720,15 @@ def test_ashp_overlay_scores_the_vaillant_end_state_from_a_gas_boiler_instant_hw
|
|||
# see `test_gas_boiler_instant_hw_before_baselines`.
|
||||
# CO2/PE are the postcode DEMAND cascade now (see the boiler-1 pin above);
|
||||
# SAP is unchanged (UK-average rating cascade).
|
||||
# Re-pinned after ADR-0049 sizes the pump to this dwelling's 12.87 kW design
|
||||
# heat loss (the capped 12 kW aroTHERM plus, 110281) instead of the fixed 5 kW
|
||||
# unit, which was badly undersized here — the sized pump raises the SAP.
|
||||
_assert_overlay_scores(
|
||||
before,
|
||||
option.overlay,
|
||||
sap=39.00740809309464,
|
||||
co2=1845.8588018295509,
|
||||
pe=18944.42568846759,
|
||||
sap=72.22779402266684,
|
||||
co2=870.0854694592007,
|
||||
pe=9025.363011043328,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -734,7 +748,11 @@ def test_ashp_overlay_reproduces_the_relodged_after_from_a_system_boiler_with_cy
|
|||
after: EpcPropertyData = parse_recommendation_summary(
|
||||
"ashp_from_system_boiler_with_cylinder_001431_after.pdf"
|
||||
)
|
||||
recommendation: Recommendation | None = recommend_heating(before, _AnyProduct())
|
||||
recommendation: Recommendation | None = recommend_heating(
|
||||
before,
|
||||
_AnyProduct(),
|
||||
design_heat_loss_kw=Sap10Calculator().calculate(before).design_heat_loss_kw,
|
||||
)
|
||||
assert recommendation is not None
|
||||
option = next(
|
||||
o for o in recommendation.options if o.measure_type == "air_source_heat_pump"
|
||||
|
|
|
|||
|
|
@ -7,7 +7,10 @@ later slices. Detection + pricing only; impact is produced by scoring (ADR-0016)
|
|||
|
||||
from datatypes.epc.domain.epc_property_data import EpcPropertyData
|
||||
from domain.geospatial.planning_restrictions import PlanningRestrictions
|
||||
from domain.modelling.generators.heating_recommendation import recommend_heating
|
||||
from domain.modelling.generators.heating_recommendation import (
|
||||
recommend_heating,
|
||||
select_ashp_pcdb_id,
|
||||
)
|
||||
from domain.modelling.measure_type import MeasureType
|
||||
from domain.modelling.product import Product
|
||||
from domain.modelling.recommendation import Recommendation
|
||||
|
|
@ -200,8 +203,11 @@ def test_gas_boiler_house_yields_an_ashp_bundle() -> None:
|
|||
# bungalow regardless of current system or efficiency (ADR-0024).
|
||||
baseline: EpcPropertyData = _gas_boiler_house()
|
||||
|
||||
# Act
|
||||
recommendation: Recommendation | None = recommend_heating(baseline, _StubProducts())
|
||||
# Act — a 5.5 kW design heat loss sizes to the 5 kW aroTHERM plus (110257):
|
||||
# PSR-0.8 target ~4.4 kW, nearest the 4.37 kW rung.
|
||||
recommendation: Recommendation | None = recommend_heating(
|
||||
baseline, _StubProducts(), design_heat_loss_kw=5.5
|
||||
)
|
||||
|
||||
# Assert — the ASHP bundle carries the absolute heat-pump end-state.
|
||||
assert recommendation is not None
|
||||
|
|
@ -764,3 +770,48 @@ def test_boiler_upgrade_leaves_adequate_controls_unchanged() -> None:
|
|||
).overlay.heating
|
||||
assert overlay is not None
|
||||
assert overlay.main_heating_control is None
|
||||
|
||||
|
||||
def test_ashp_sizing_targets_the_appendix_n_efficiency_peak() -> None:
|
||||
# A 6 kW design heat loss sizes to the SAP Appendix-N efficiency peak
|
||||
# (PSR ~= 0.8, so ~4.8 kW rated output): the 5 kW aroTHERM plus (PCDB 110257,
|
||||
# ~4.37 kW) — what a real installer fits — not the oversized 7 kW unit, and
|
||||
# well clear of the low-PSR efficiency collapse.
|
||||
# Act
|
||||
pcdb_id = select_ashp_pcdb_id(design_heat_loss_kw=6.0)
|
||||
|
||||
# Assert — the 5 kW aroTHERM plus rung.
|
||||
assert pcdb_id == 110257
|
||||
|
||||
|
||||
def test_ashp_sizing_caps_at_the_largest_pump_for_a_high_heat_loss_dwelling() -> None:
|
||||
# A very leaky dwelling (17.5 kW design heat loss, e.g. UPRN 10002468116)
|
||||
# exceeds the domestic aroTHERM plus range, so sizing caps at the largest
|
||||
# rung — the 12 kW aroTHERM plus (PCDB 110281, ~11.48 kW) — an honest, still
|
||||
# far better than the fixed 5 kW pump whose PSR collapsed to ~0.25.
|
||||
# Act
|
||||
pcdb_id = select_ashp_pcdb_id(design_heat_loss_kw=17.5)
|
||||
|
||||
# Assert — capped at the 12 kW rung, not left undersized at 5 kW.
|
||||
assert pcdb_id == 110281
|
||||
|
||||
|
||||
def test_ashp_bundle_sizes_the_pump_to_the_design_heat_loss() -> None:
|
||||
# A high-heat-loss house (17.5 kW) must get a pump sized to it — the capped
|
||||
# 12 kW aroTHERM plus (PCDB 110281) — so SAP's Appendix-N PSR efficiency reads
|
||||
# near its peak, not the fixed 5 kW unit (110257) whose PSR collapses.
|
||||
# Arrange
|
||||
baseline: EpcPropertyData = _gas_boiler_house()
|
||||
|
||||
# Act
|
||||
recommendation: Recommendation | None = recommend_heating(
|
||||
baseline, _StubProducts(), design_heat_loss_kw=17.5
|
||||
)
|
||||
|
||||
# Assert — the ASHP overlay carries the sized 12 kW record, not the fixed 5 kW.
|
||||
assert recommendation is not None
|
||||
ashp = next(
|
||||
o for o in recommendation.options if o.measure_type == "air_source_heat_pump"
|
||||
)
|
||||
assert ashp.overlay.heating is not None
|
||||
assert ashp.overlay.heating.main_heating_index_number == 110281
|
||||
|
|
|
|||
|
|
@ -90,6 +90,69 @@ def test_non_cavity_main_wall_yields_no_recommendation() -> None:
|
|||
assert recommendation is None
|
||||
|
||||
|
||||
def test_at_regs_age_band_cavity_yields_no_cavity_fill_recommendation() -> None:
|
||||
# Arrange — a cavity wall lodged as-built at construction age band L
|
||||
# (2012-2022): the as-built cascade already assigns the at-regs U-value, so
|
||||
# filling the cavity cannot lower it. The Wall U-Value Gate withholds the
|
||||
# Option (ADR-0051); the legacy insulation-state trigger alone would offer
|
||||
# a paid no-op.
|
||||
baseline: EpcPropertyData = build_epc() # MAIN: cavity (4), uninsulated (4)
|
||||
_part(baseline, BuildingPartIdentifier.MAIN).construction_age_band = "L"
|
||||
|
||||
# Act
|
||||
recommendation: Recommendation | None = recommend_cavity_wall(
|
||||
baseline, _StubProducts()
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert recommendation is None
|
||||
|
||||
|
||||
def test_at_regs_age_band_wall_yields_no_solid_wall_recommendation() -> None:
|
||||
# Arrange — a timber-frame wall lodged as-built at construction age band L
|
||||
# (2012-2022). RdSAP's cascade assigns it the at-regs U-value (0.28), and
|
||||
# 100 mm IWI resolves to the SAME value — the measure cannot change the
|
||||
# modelled wall, so offering it sells £4k of insulation for +0.00 SAP
|
||||
# (property 711795, PRD #1435 WS2). The Wall U-Value Gate withholds it
|
||||
# (ADR-0051).
|
||||
baseline: EpcPropertyData = build_epc()
|
||||
main: SapBuildingPart = _part(baseline, BuildingPartIdentifier.MAIN)
|
||||
main.wall_construction = 5 # timber frame — IWI-only construction
|
||||
main.wall_insulation_type = 4 # as-built / uninsulated — the legacy trigger
|
||||
main.construction_age_band = "L"
|
||||
|
||||
# Act
|
||||
recommendation: Recommendation | None = recommend_solid_wall(
|
||||
baseline, _StubProducts()
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert recommendation is None
|
||||
|
||||
|
||||
def test_treatable_old_band_solid_wall_keeps_its_insulation_options() -> None:
|
||||
# Arrange — solid brick as-built at age band B: the cascade U (~2.1) far
|
||||
# exceeds what 100 mm EWI/IWI achieves, so the Wall U-Value Gate must let
|
||||
# both Options through unchanged (the gate is physical possibility only —
|
||||
# ADR-0051).
|
||||
baseline: EpcPropertyData = build_epc() # age band B
|
||||
main: SapBuildingPart = _part(baseline, BuildingPartIdentifier.MAIN)
|
||||
main.wall_construction = 3 # solid brick — EWI + IWI constructable
|
||||
main.wall_insulation_type = 4
|
||||
|
||||
# Act
|
||||
recommendation: Recommendation | None = recommend_solid_wall(
|
||||
baseline, _StubProducts()
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert recommendation is not None
|
||||
assert {option.measure_type for option in recommendation.options} == {
|
||||
"external_wall_insulation",
|
||||
"internal_wall_insulation",
|
||||
}
|
||||
|
||||
|
||||
def test_park_home_wall_yields_no_solid_wall_recommendation() -> None:
|
||||
# Arrange — a park home (wall_construction code 8) with an uninsulated
|
||||
# as-built wall. Code 8 is NOT system-built (ADR-0019); a park home's
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ def _sap_result(
|
|||
space_heating_kwh_per_yr=0.0,
|
||||
space_cooling_kwh_per_yr=0.0,
|
||||
fabric_energy_efficiency_kwh_per_m2_yr=0.0,
|
||||
design_heat_loss_kw=0.0,
|
||||
main_heating_fuel_kwh_per_yr=0.0,
|
||||
main_2_heating_fuel_kwh_per_yr=0.0,
|
||||
secondary_heating_fuel_kwh_per_yr=0.0,
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ from datatypes.epc.surveys.elmhurst_site_notes import (
|
|||
)
|
||||
|
||||
from datatypes.epc.domain.epc_property_data import (
|
||||
EnergyElement,
|
||||
EpcPropertyData,
|
||||
MainHeatingDetail,
|
||||
PhotovoltaicArray,
|
||||
|
|
@ -738,6 +739,30 @@ def test_cylinder_present_but_size_not_determined_defaults_to_normal_110l() -> N
|
|||
assert volume_l is not None and abs(volume_l - 110.0) <= 1e-9
|
||||
|
||||
|
||||
def test_cylinder_size_not_determined_still_incurs_storage_loss() -> None:
|
||||
# Arrange — the size-0 (not determined) cylinder's Table 28 default of
|
||||
# 110 L must flow into the STORAGE-LOSS override too, not just the Table
|
||||
# 13 high-rate fraction. `_cylinder_storage_loss_override` read the
|
||||
# volume via `_cylinder_volume_l_from_code` (None for size 0) and bailed
|
||||
# `if volume_l is None: return None`, dropping the loss entirely — while
|
||||
# the rest of the DHW cascade used `_hot_water_cylinder_volume_l`
|
||||
# (110 L). A present cylinder with no storage loss under-costs the DHW
|
||||
# and over-rates the dwelling (7 corpus certs lodge cylinder_size=0 +
|
||||
# has_cylinder=true). RdSAP 10 §10.5 Table 28: a present-but-unsized
|
||||
# cylinder is a 110 L cylinder and incurs the Table 2 storage loss.
|
||||
from domain.sap10_calculator.rdsap.cert_to_inputs import (
|
||||
_cylinder_storage_loss_override, # pyright: ignore[reportPrivateUsage]
|
||||
)
|
||||
epc = _cylinder_epc(cylinder_size=0)
|
||||
|
||||
# Act
|
||||
storage_56m = _cylinder_storage_loss_override(epc, None)
|
||||
|
||||
# Assert — a real 110 L cylinder loss series, not the dropped None.
|
||||
assert storage_56m is not None
|
||||
assert sum(storage_56m) > 0.0
|
||||
|
||||
|
||||
def test_cylinder_size_inaccessible_code_5_solid_fuel_boiler_uses_160l() -> None:
|
||||
# Arrange — RdSAP 10 §10.5 Table 28: an "Inaccessible" cylinder (code 5)
|
||||
# heated "from a solid fuel boiler" uses 160 litres.
|
||||
|
|
@ -1867,6 +1892,79 @@ def test_main_floor_u_value_routes_suspended_timber_via_floor_construction_type(
|
|||
assert sealed is False
|
||||
|
||||
|
||||
def test_has_suspended_timber_floor_per_spec_falls_back_to_floors_description() -> None:
|
||||
"""RdSAP 10 §5 (12) reads the Main bp's `floor_construction_type`
|
||||
lodgement, but the gov-API mapper often leaves that field `None`
|
||||
while the global `epc.floors[].description` still carries the
|
||||
assessor's "Suspended, ..." observation (cert 100061275133: Main bp
|
||||
`floor_construction_type=None`, `epc.floors[0].description=
|
||||
"Suspended, no insulation (assumed)"`). `_main_floor_u_value` already
|
||||
falls back to this joined description (`effective_floor_description`);
|
||||
the (12) infiltration rule must apply the same fallback so a
|
||||
suspended-timber ground floor doesn't silently enter (12)=0."""
|
||||
from dataclasses import replace
|
||||
# Arrange
|
||||
main = replace(
|
||||
make_building_part(
|
||||
construction_age_band="B",
|
||||
floor_dimensions=[make_floor_dimension(floor=0)],
|
||||
),
|
||||
floor_type="Ground floor",
|
||||
floor_construction_type=None,
|
||||
)
|
||||
epc = make_minimal_sap10_epc(sap_building_parts=[main])
|
||||
epc.floors = [
|
||||
EnergyElement(
|
||||
description="Suspended, no insulation (assumed)",
|
||||
energy_efficiency_rating=0,
|
||||
environmental_efficiency_rating=0,
|
||||
),
|
||||
]
|
||||
|
||||
# Act
|
||||
has_susp, sealed = _has_suspended_timber_floor_per_spec(epc)
|
||||
|
||||
# Assert — description signal routes to the suspended-timber branch;
|
||||
# band B is A-E with no supplied U-value and no retrofit insulation
|
||||
# signal, so rule (b) applies "unsealed" → (12) = 0.2.
|
||||
assert has_susp is True
|
||||
assert sealed is False
|
||||
|
||||
|
||||
def test_has_suspended_timber_floor_per_spec_excludes_suspended_not_timber() -> None:
|
||||
""""Suspended, not timber" (e.g. suspended concrete) starts with the
|
||||
same "Suspended" prefix as timber floors but must NOT trigger the
|
||||
§5 (12) rule — the rule is specifically about air infiltration
|
||||
through timber floorboard gaps (see the API mapper's
|
||||
`_API_FLOOR_CONSTRUCTION_TO_STR` comment: "only 'Suspended timber'
|
||||
triggers the §5 (12)... adjustment")."""
|
||||
from dataclasses import replace
|
||||
# Arrange
|
||||
main = replace(
|
||||
make_building_part(
|
||||
construction_age_band="B",
|
||||
floor_dimensions=[make_floor_dimension(floor=0)],
|
||||
),
|
||||
floor_type="Ground floor",
|
||||
floor_construction_type=None,
|
||||
)
|
||||
epc = make_minimal_sap10_epc(sap_building_parts=[main])
|
||||
epc.floors = [
|
||||
EnergyElement(
|
||||
description="Suspended, not timber",
|
||||
energy_efficiency_rating=0,
|
||||
environmental_efficiency_rating=0,
|
||||
),
|
||||
]
|
||||
|
||||
# Act
|
||||
has_susp, sealed = _has_suspended_timber_floor_per_spec(epc)
|
||||
|
||||
# Assert
|
||||
assert has_susp is False
|
||||
assert sealed is False
|
||||
|
||||
|
||||
def test_rdsap_extract_fans_default_per_table_5() -> None:
|
||||
# Arrange — RdSAP 10 §4.1 Table 5 (PDF p.28) "Extract fans" default
|
||||
# when the lodged number is unknown. The Summary §12.0 "No. of
|
||||
|
|
@ -2686,6 +2784,46 @@ def test_dual_fuel_secondary_api_enum_9_prices_as_dual_fuel_not_lpg() -> None:
|
|||
assert secondary_factor_code == 10
|
||||
|
||||
|
||||
def test_fuel_cost_secondary_colliding_fuel_priced_at_canonical_rate() -> None:
|
||||
# Arrange — the SAP-DRIVING `_fuel_cost` cascade prices the secondary
|
||||
# via its own inline `secondary_high_rate_gbp_per_kwh`, distinct from
|
||||
# the CO2/display `_secondary_fuel_cost_gbp_per_kwh`. It must apply the
|
||||
# SAME `_GOV_API_COLLISION_FUELS` canonicalisation, else an ANTHRACITE
|
||||
# secondary (gov-API `secondary_fuel_type=5`) resolves to the same-value
|
||||
# Table-32 code 5 = bulk LPG (12.19 p/kWh) instead of anthracite
|
||||
# (3.64 p/kWh) — a 3× over-cost that sank corpus cert 100050355518
|
||||
# (lodged 36, engine 20.8). Anthracite room-heater secondary on a
|
||||
# mains-gas boiler main.
|
||||
gas_boiler_main = MainHeatingDetail(
|
||||
has_fghrs=False, main_fuel_type=26, heat_emitter_type=1,
|
||||
emitter_temperature=1, main_heating_control=2106,
|
||||
main_heating_category=2, sap_main_heating_code=102,
|
||||
)
|
||||
anthracite_secondary_epc = make_minimal_sap10_epc(
|
||||
total_floor_area_m2=_TYPICAL_TFA_M2,
|
||||
habitable_rooms_count=4,
|
||||
country_code="ENG",
|
||||
sap_building_parts=[make_building_part(construction_age_band="D")],
|
||||
sap_heating=make_sap_heating(
|
||||
main_heating_details=[gas_boiler_main],
|
||||
secondary_fuel_type=5, # gov-API enum: anthracite
|
||||
secondary_heating_type=631, # room heater
|
||||
),
|
||||
)
|
||||
|
||||
# Act
|
||||
inputs = cert_to_inputs(anthracite_secondary_epc)
|
||||
result = Sap10Calculator().calculate(anthracite_secondary_epc)
|
||||
fc = inputs.fuel_cost
|
||||
secondary_kwh = result.secondary_heating_fuel_kwh_per_yr
|
||||
|
||||
# Assert — anthracite 3.64 p/kWh (0.0364), NOT the collision LPG
|
||||
# 12.19 p/kWh the raw same-value Table-32 lookup returns.
|
||||
assert secondary_kwh > 0.0
|
||||
assert abs(fc.secondary_total_cost_gbp - secondary_kwh * 0.0364) <= 1e-6
|
||||
assert abs(fc.secondary_total_cost_gbp - secondary_kwh * 0.1219) > 1.0
|
||||
|
||||
|
||||
def test_is_electric_water_dual_fuel_table_32_code_10_is_not_electric() -> None:
|
||||
# Arrange — same API/Table 32 collision as `_is_electric_main` per
|
||||
# S0380.136 docstring.
|
||||
|
|
@ -4891,6 +5029,42 @@ def test_hot_water_from_pcdb_heat_pump_bills_at_app_n_wh_high_rate() -> None:
|
|||
assert abs(rate_immersion - 0.0750) <= 1e-6
|
||||
|
||||
|
||||
def test_instantaneous_electric_dhw_off_peak_bills_at_high_rate() -> None:
|
||||
# Arrange — SAP 10.2 §12 (PDF line 2680): the off-peak on-peak HW
|
||||
# proportion (Table 13) is "dependent on the total floor area and the
|
||||
# CYLINDER SIZE" — it presumes a stored-water cylinder that can be
|
||||
# charged overnight. An INSTANTANEOUS point-of-use electric heater
|
||||
# (WHC 907/909, no cylinder) heats water on demand and CANNOT shift to
|
||||
# the off-peak window, so 100% of its consumption is at the high rate
|
||||
# (Table 12a high-rate fraction 1.0). The generic off-peak else-branch
|
||||
# billed it at 100% off-peak LOW rate (5.50 p/kWh at 7-hour), 3× under-
|
||||
# costing the DHW and over-rating the dwelling — corpus cert 74061136
|
||||
# (HHR-storage flat, WHC 909) lodged 73, engine 80.7.
|
||||
from domain.sap10_calculator.tables.table_12a import Tariff
|
||||
from domain.sap10_calculator.rdsap.cert_to_inputs import (
|
||||
_hot_water_fuel_cost_gbp_per_kwh, # pyright: ignore[reportPrivateUsage]
|
||||
_hot_water_high_rate_fraction, # pyright: ignore[reportPrivateUsage]
|
||||
)
|
||||
storage_main = MainHeatingDetail(
|
||||
has_fghrs=False, main_fuel_type=29, heat_emitter_type=0,
|
||||
emitter_temperature="NA", main_heating_control=2404,
|
||||
main_heating_category=7, sap_main_heating_code=409,
|
||||
)
|
||||
|
||||
# Act — instantaneous electric DHW (WHC 909) on a 7-hour off-peak tariff.
|
||||
rate = _hot_water_fuel_cost_gbp_per_kwh(
|
||||
29, storage_main, Tariff.SEVEN_HOUR, SAP_10_2_SPEC_PRICES,
|
||||
water_heating_code=909,
|
||||
)
|
||||
fraction = _hot_water_high_rate_fraction(
|
||||
29, storage_main, Tariff.SEVEN_HOUR, water_heating_code=909,
|
||||
)
|
||||
|
||||
# Assert — 100% at the 7-hour HIGH rate (15.29 p/kWh), NOT the low 5.50.
|
||||
assert abs(rate - 0.1529) <= 1e-6
|
||||
assert fraction == 1.0
|
||||
|
||||
|
||||
def test_heat_pump_water_efficiency_is_floored_at_100pct_per_app_n3_7() -> None:
|
||||
# Arrange — SAP 10.2 Appendix N3.7 ("Thermal efficiency for water
|
||||
# heating – heat pumps", PDF p.109): "multiply the thermal efficiency
|
||||
|
|
|
|||
|
|
@ -815,3 +815,20 @@ def test_split_tariff_charges_space_heating_at_off_peak_rate() -> None:
|
|||
+ (r_e7.pumps_fans_kwh_per_yr + r_e7.lighting_kwh_per_yr) * 0.1319
|
||||
)
|
||||
assert r_e7.total_fuel_cost_gbp == pytest.approx(expected_cost, rel=1e-6)
|
||||
|
||||
|
||||
def test_calculate_exposes_the_design_heat_loss_for_heat_pump_sizing() -> None:
|
||||
# The ASHP recommendation sizes the pump to the dwelling's SAP design heat
|
||||
# loss — the annual-average HLC at the design temperature difference (24.2 K),
|
||||
# the quantity Appendix N's PSR divides the pump's rated output by. The
|
||||
# calculator surfaces it as a typed output, derived from the HLC it already
|
||||
# computes.
|
||||
# Arrange
|
||||
inputs = _baseline_inputs()
|
||||
|
||||
# Act
|
||||
result = calculate_sap_from_inputs(inputs)
|
||||
|
||||
# Assert — HLC (W/K) x 24.2 K / 1000 = design heat loss (kW).
|
||||
hlc = result.intermediate["heat_transfer_coefficient_w_per_k"]
|
||||
assert result.design_heat_loss_kw == pytest.approx(hlc * 24.2 / 1000)
|
||||
|
|
|
|||
|
|
@ -303,11 +303,39 @@ _EXPECTATIONS: Final[tuple[RealCertExpectation, ...]] = (
|
|||
# the schema dropped at parse, so the Appendix-M generation credit was lost.
|
||||
# Carrying it (mapper `_sap_17_1_pv_arrays`) credits the generation and lifts
|
||||
# this flat 82→84, closing most of the gap to the lodged 85 the array explains.
|
||||
# WAS 84 until the full-SAP export-capable mapping landed (PRD #1435 WS3):
|
||||
# this cert lodges `is_dwelling_export_capable: true`, which the mapper
|
||||
# hardcoded False, so Appendix M1 zeroed the array's export credit. Carrying
|
||||
# the lodged flag restores it and the engine now reproduces the lodged 85
|
||||
# EXACTLY — the last −1 was the missing export credit, not a methodology gap.
|
||||
RealCertExpectation(
|
||||
schema="SAP-Schema-19.1.0",
|
||||
sample="uprn_10096028301",
|
||||
cert_num="0390-3321-6060-2405-7985",
|
||||
sap_score=84,
|
||||
sap_score=85,
|
||||
),
|
||||
# UPRN 10094615101 → cert 0380-3044-6070-2305-5925. SAP-Schema-19.1.0 — a
|
||||
# FULL-SAP TOP-FLOOR FLAT (2023 new build, St Leonards-on-Sea, TFA 76 m²,
|
||||
# PRD #1435 WS3 evidence property 741840): PCDB HHRSH (230003 ×4) on an
|
||||
# off-peak tariff, hot-water heat pump (code 914) + WWHRS, MEV (PCDB
|
||||
# 500298), measured AP50 4.65, calculated per-junction thermal bridges, and
|
||||
# a 1.26 kWp EXPORT-CAPABLE PV array (`is_dwelling_export_capable: true`,
|
||||
# pv_connection 2). Lodged under SAP 10.2 itself (Design SAP 10 2.21.9), so
|
||||
# the lodged 92 is the same-spec accredited ground truth — no Elmhurst
|
||||
# rebuild needed. The engine produced 89 until the full-SAP export-capable
|
||||
# mapping landed: `from_sap_schema_17_1` hardcoded
|
||||
# `is_dwelling_export_capable=False` (the flag and `pv_connection` are
|
||||
# top-level full-SAP fields the schema dataclass never parsed), so SAP 10.2
|
||||
# Appendix M1 zeroed the PV export credit (~460 kWh × 13.19p ≈ £61/yr).
|
||||
# Carrying the lodged flag restores the credit and the engine reproduces
|
||||
# the lodged 92 EXACTLY. Residual CO2/PE deltas (~252 vs ~200 kg CO2/yr;
|
||||
# PE 41.7 vs 35) are demand-side follow-ups (lodged TMP 64 vs default 250,
|
||||
# WWHRS, MEV PCDB fan power) — within SAP tolerance, tracked separately.
|
||||
RealCertExpectation(
|
||||
schema="SAP-Schema-19.1.0",
|
||||
sample="uprn_10094615101",
|
||||
cert_num="0380-3044-6070-2305-5925",
|
||||
sap_score=92,
|
||||
),
|
||||
# UPRN 44012843 → cert 0775-2898-6628-9594-8005. SAP-Schema-16.3 — a
|
||||
# reduced-field (RdSAP-shaped) ground-floor FLAT, band K (2007-2011), cavity
|
||||
|
|
@ -699,6 +727,313 @@ _EXPECTATIONS: Final[tuple[RealCertExpectation, ...]] = (
|
|||
cert_num="0018-2049-6232-4431-6934",
|
||||
sap_score=54,
|
||||
),
|
||||
# UPRN 4510053280 (corpus-1000 C001; the corpus doc lodges no cert number).
|
||||
# RdSAP-Schema-21.0.1 native — GROUND-FLOOR FLAT 47 m², band B, solid brick
|
||||
# 520 mm uninsulated (+ 8.12 m² sheltered alternative wall), roof "(another
|
||||
# dwelling above)", ASHP radiators PCDB 100053 (Mitsubishi Ecodan 5 kW,
|
||||
# 2010 record: PSR table 0.2–2.0, η_space 332.8% @ 2.0, η_water 187.5%,
|
||||
# duration "24"), no cylinder, electric room-heater secondary, no PV.
|
||||
# Lodged 51 / engine 74.94 (Δ +23.9, the corpus' worst cert). Dwelling PSR
|
||||
# = 4.62 kW / (94.0 W/K × 24.2 K) = 2.031, just past the record's largest
|
||||
# 2.0 → SAP 10.2 N2 (PDF p.101 footnotes 44/45) heat-pump extension:
|
||||
# reciprocal-interpolate η_space toward 100% at PSR 4.0 → 321% × 0.95 =
|
||||
# 305%. That is what the engine does, and what accredited Elmhurst did on
|
||||
# the SAME record family / same PSR shape (golden case 56, record 100061,
|
||||
# PSR 3.107 → 139.66%). The LODGED software instead treated the
|
||||
# out-of-range record as invalid and billed direct electric: forcing
|
||||
# η_space = η_water = 100% + the standard 9-h schedule reproduces the
|
||||
# lodged CO2 to 1% (0.808 vs 0.800 t) and PE within 6% (178 vs 190),
|
||||
# whereas the engine's spec path gives 0.444 t / 98.8. A lodged-software
|
||||
# methodology gap (worklist ⚠), NOT an engine bug — do not tune. PINNED to
|
||||
# the observed engine 75.
|
||||
RealCertExpectation(
|
||||
schema="RdSAP-Schema-21.0.1",
|
||||
sample="uprn_4510053280",
|
||||
cert_num="uprn-4510053280",
|
||||
sap_score=75,
|
||||
),
|
||||
# UPRN 10094975827 (corpus; no cert number lodged). RdSAP-Schema-21.0.1
|
||||
# native — TOP-FLOOR FLAT, band B, solid brick, pitched roof lodged
|
||||
# thickness "NI" + description "Pitched, insulated (assumed)", electric
|
||||
# room heaters. The "insulated (assumed)" Table-18 cert: presence-assumed
|
||||
# insulation is NOT the §5.11.4 observed-retrofit 50 mm row (0.68) — it
|
||||
# takes the Table 18 age-band default (band B pitched → 0.40), the same
|
||||
# value the "ND" lodgement of this description reaches. Was engine 64.13
|
||||
# (Δ −1.87 vs lodged 66); the fix lands 65.83 → rounds to 66 = lodged.
|
||||
# PINNED to the observed engine 66 = lodged.
|
||||
RealCertExpectation(
|
||||
schema="RdSAP-Schema-21.0.1",
|
||||
sample="uprn_10094975827",
|
||||
cert_num="uprn-10094975827",
|
||||
sap_score=66,
|
||||
),
|
||||
# UPRN 100050355518 (corpus C003; no cert number lodged). RdSAP-21.0.1
|
||||
# native — SEMI-DETACHED HOUSE, 122 m², 2 extensions (3 building parts),
|
||||
# mains-gas boiler main (SAP 117) + ANTHRACITE room-heater secondary
|
||||
# (gov-API secondary_fuel_type=5, SAP 631). The secondary-fuel collision
|
||||
# cert: enum 5 collides with Table-32 code 5 (bulk LPG 12.19 p/kWh); the
|
||||
# SAP-driving _fuel_cost path priced the anthracite secondary at the LPG
|
||||
# rate instead of anthracite 3.64 p/kWh — a 3× over-cost on ~1431 kWh
|
||||
# (+£100/yr → ECF 6.24 vs 4.67). Engine was 20.8 (Δ −15.2 vs lodged 36);
|
||||
# canonicalising the secondary fuel lands 35.5 = lodged 36. CO2/PE
|
||||
# already matched lodged (0.055 t / 1.7 kWh) — a pure cost-side gap, the
|
||||
# signature that localised it. PINNED to the observed engine 36 = lodged.
|
||||
RealCertExpectation(
|
||||
schema="RdSAP-Schema-21.0.1",
|
||||
sample="uprn_100050355518",
|
||||
cert_num="uprn-100050355518",
|
||||
sap_score=36,
|
||||
),
|
||||
# UPRN 74061136 (corpus; no cert number lodged). RdSAP-21.0.1 native —
|
||||
# MID-FLOOR FLAT 24 m², band B, HHR electric storage heaters (SAP 409) on
|
||||
# an Unknown meter (type 3 → off-peak 7-hour per RdSAP 10 §12 Rule 2),
|
||||
# INSTANTANEOUS electric point-of-use DHW (WHC 909, no cylinder). The
|
||||
# instantaneous-DHW off-peak cert: its DHW was billed 100% at the 7-hour
|
||||
# LOW rate (5.50 p) but SAP 10.2 §12 (Table-13 on-peak split is keyed on
|
||||
# CYLINDER size) presumes stored water — an instantaneous heater has no
|
||||
# cylinder, heats on demand, so 100% bills at the HIGH rate (15.29 p).
|
||||
# Mid-floor flat → roof/floor both zero-loss, so PE matched lodged (+1.6)
|
||||
# and the +7.72 over-rate isolated cleanly to the DHW tariff. Fix lands
|
||||
# 71.75 (Δ −1.25 vs lodged 73); the tariff error is gone, the residual is
|
||||
# a separate small fabric effect. PINNED to the observed engine 72.
|
||||
RealCertExpectation(
|
||||
schema="RdSAP-Schema-21.0.1",
|
||||
sample="uprn_74061136",
|
||||
cert_num="uprn-74061136",
|
||||
sap_score=72,
|
||||
),
|
||||
# UPRN 200004296092 (corpus; no cert number lodged). RdSAP-21.0.1 native —
|
||||
# END-TERRACE HOUSE, TFA 62, age B, mains-gas COMBI (PCDB 16137), SOLID
|
||||
# BRICK 360 mm "with external insulation" (wall_insulation_type 1) but
|
||||
# thickness lodged "NI" (unknown). The wall added-insulation unknown-
|
||||
# thickness cert: the §5.8 R-value gate needs thickness > 0, so the
|
||||
# externally-insulated wall fell through to its UNINSULATED §5.6 U (1.40,
|
||||
# walls 101.7 W/K, HLP 3.46) → PE +59.6, engine 64.35 (Δ −6.65 vs lodged
|
||||
# 71). RdSAP 10 §5.4 (PDF p.32) "assume 100 mm if thickness is unknown"
|
||||
# applies the 100 mm §5.8 default → U ~0.29, engine 71.74 = lodged. PINNED
|
||||
# to the observed engine 72.
|
||||
RealCertExpectation(
|
||||
schema="RdSAP-Schema-21.0.1",
|
||||
sample="uprn_200004296092",
|
||||
cert_num="uprn-200004296092",
|
||||
sap_score=72,
|
||||
),
|
||||
# UPRN 200004017091 (corpus; no cert number lodged). RdSAP-21.0.1 native —
|
||||
# END-TERRACE HOUSE, mains-gas boiler, hot-water cylinder lodged with
|
||||
# `cylinder_size=0` (present but "size not determined") + has_cylinder=true.
|
||||
# The unsized-cylinder storage-loss cert: `_cylinder_storage_loss_override`
|
||||
# read the volume via `_cylinder_volume_l_from_code` → None for size 0 →
|
||||
# bailed and DROPPED the cylinder's Table-2 storage loss, under-costing the
|
||||
# DHW → engine 72.75 (Δ +1.75 vs lodged 71). RdSAP 10 §10.5 Table 28: an
|
||||
# unsized present cylinder is the 110 L "Normal" default and still incurs
|
||||
# the loss → engine 71.39 = lodged 71. PINNED to the observed engine 71.
|
||||
RealCertExpectation(
|
||||
schema="RdSAP-Schema-21.0.1",
|
||||
sample="uprn_200004017091",
|
||||
cert_num="uprn-200004017091",
|
||||
sap_score=71,
|
||||
),
|
||||
# UPRN 47084930 (corpus; no cert number lodged). RdSAP-21.0.1 native —
|
||||
# TOP-FLOOR FLAT, band E, cavity wall + 100 mm internal insulation, FLAT
|
||||
# ROOF insulated 75 mm, floor over other premises, mains-gas combi. The
|
||||
# flat-roof insulation-thickness cert: the 75 mm is lodged in
|
||||
# `flat_roof_insulation_thickness`, but heat_transmission read
|
||||
# `roof_insulation_thickness` (None) → billed the flat roof at the
|
||||
# uninsulated age-E default 1.5 (roof 96.4 W/K) → PE +47, engine 69.51
|
||||
# (Δ −4.49 vs lodged 74). Preferring the flat field → U 0.5 (roof 32.1
|
||||
# W/K), engine 74.34 = lodged 74. VALIDATED against the accredited
|
||||
# Elmhurst worksheet (built on the lodged inputs, evidence saved:
|
||||
# elmhurst_summary.pdf / elmhurst_worksheet.pdf): "insulated flat roof"
|
||||
# U 0.5, floor 0.70 and wall 0.25 both matching the engine. PINNED 74.
|
||||
RealCertExpectation(
|
||||
schema="RdSAP-Schema-21.0.1",
|
||||
sample="uprn_47084930",
|
||||
cert_num="uprn-47084930",
|
||||
sap_score=74,
|
||||
),
|
||||
# UPRN 100050881708 (corpus; no cert number lodged). RdSAP-21.0.1 native —
|
||||
# SEMI-DETACHED HOUSE, band B, cavity FILLED wall, pitched 225 mm loft,
|
||||
# SUSPENDED ground floor "to unheated space", mains-gas combi, no cylinder.
|
||||
# Lodged 70 / engine 72 (71.55). Built in accredited Elmhurst RdSAP10
|
||||
# (evidence saved: elmhurst_summary.pdf / elmhurst_worksheet.pdf): the WALL
|
||||
# (cavity, U 0.70, 69.56 m²) and ROOF (insulated slope 225 mm, U 0.19)
|
||||
# match the engine EXACTLY (walls 48.8 W/K, roof 6.0 W/K). The engine is
|
||||
# faithful on the checkable fabric; the +1.55 vs lodged is a small
|
||||
# methodology gap. (Elmhurst''s worksheet SAP 61 is NOT comparable — the
|
||||
# build entered the suspended ground floor as an upper "above unheated
|
||||
# space" floor, which Elmhurst bills via Table-20 exposed 1.20; the cert''s
|
||||
# §3.12 suspended-ground-floor U is 0.74, which the engine uses.) PINNED to
|
||||
# the observed engine 72.
|
||||
RealCertExpectation(
|
||||
schema="RdSAP-Schema-21.0.1",
|
||||
sample="uprn_100050881708",
|
||||
cert_num="uprn-100050881708",
|
||||
sap_score=72,
|
||||
),
|
||||
# UPRN 217091901 (corpus; no cert number lodged). RdSAP-21.0.1 native —
|
||||
# TOP-FLOOR FLAT, band A (before 1900), SOLID BRICK 280 mm uninsulated,
|
||||
# FLAT ROOF no insulation (As Built), floor over ANOTHER DWELLING below
|
||||
# (zero loss), mains-gas COMBI, SINGLE glazing (glazed_type 5), TFA 63.
|
||||
# Lodged 62 / engine 61 (60.82). Built in accredited Elmhurst RdSAP10
|
||||
# (evidence saved: elmhurst_summary.pdf / elmhurst_worksheet.pdf): EVERY
|
||||
# heat-loss element matches the engine EXACTLY — walls (solid U 1.70,
|
||||
# 53.18 W/K), flat roof (uninsulated U 2.30, 145.57 W/K), floor (0),
|
||||
# party (0), and — with the faithful SINGLE glazing entered — WINDOWS
|
||||
# (8.62 m² × effective U 4.0268 = 34.7114 W/K, identical to the engine).
|
||||
# This confirms the mapper''s glazed_type 5 → single (raw U 4.8) is
|
||||
# spec-correct (epc_codes.csv: glazed_type 5 = single glazing), NOT the
|
||||
# double it superficially resembles. The −1.18 vs lodged is a cost-side
|
||||
# methodology residual (PE +8.7 kWh/m², CO2/PE-driven), not a fabric bug.
|
||||
# PINNED to the observed engine 61.
|
||||
RealCertExpectation(
|
||||
schema="RdSAP-Schema-21.0.1",
|
||||
sample="uprn_217091901",
|
||||
cert_num="uprn-217091901",
|
||||
sap_score=61,
|
||||
),
|
||||
# UPRN 100061275133 (corpus; no cert number lodged). RdSAP-21.0.1 native —
|
||||
# 2-storey SEMI-DETACHED HOUSE, band B, SOLID BRICK 400 mm with EXTERNAL
|
||||
# insulation, pitched 300 mm loft, SUSPENDED uninsulated ground floor,
|
||||
# mains-gas COMBI, double glazing (glazed_type 13, 2022+), TFA 78.
|
||||
# Lodged 76 / engine 76 (76.33, was 77.24 before the §5 (12) fix below).
|
||||
# Built in accredited Elmhurst RdSAP10 (evidence saved: elmhurst_summary.pdf
|
||||
# / elmhurst_worksheet.pdf): the checkable FABRIC matches the engine
|
||||
# closely — ROOF (300 mm loft, U 0.14) and WINDOWS (15.32 m² × eff U
|
||||
# 1.3258 = 20.3106 W/K) match EXACTLY; the externally-insulated WALL is
|
||||
# U 0.29 (engine) vs 0.30 (Elmhurst) — which VALIDATES the slice-5
|
||||
# external-insulation default (100 mm added over solid brick,
|
||||
# `_WALL_ADDED_INSULATION_UNKNOWN_THICKNESS_MM`); the suspended ground
|
||||
# floor is U 0.68 (engine) vs 0.68 (Elmhurst) — EXACT after rebuilding
|
||||
# with 0 extract fans + PCDB 18908 (was 0.70 pre-rebuild). Ventilation
|
||||
# (21) shelter-adjusted ACH is EXACT too (0.6938 = 0.6938), and dwelling
|
||||
# volume (5) 196.8995 m³ matches (confirming the mapper's +0.25 m
|
||||
# upper-floor height convention mirrors Elmhurst's own internal one).
|
||||
# §5 (12) fix: the Main bp lodges no `floor_construction_type` (gov-API
|
||||
# leaves it None), but `epc.floors[0].description` = "Suspended, no
|
||||
# insulation (assumed)" — `_has_suspended_timber_floor_per_spec` was
|
||||
# only reading the per-bp field and silently entered (12)=0 for a
|
||||
# genuinely suspended-timber floor. Now falls back to the joined
|
||||
# description (mirroring `_main_floor_u_value`'s existing fallback),
|
||||
# applying (12)=0.2 unsealed; engine converges EXACTLY on lodged (76).
|
||||
# Elmhurst's OWN overall worksheet SAP (72) still differs from ours
|
||||
# (76.33) even on these now-identical inputs — traced to SAP 10.2
|
||||
# Table 11: Elmhurst's (201)/(202)/(215) show it assumes 10% of space
|
||||
# heat comes from a portable-electric secondary (658 kWh/yr @ the
|
||||
# electric rate) purely from the main system's category (gas boiler),
|
||||
# with NO secondary lodged (`secondary_heating.description == "None"`).
|
||||
# `_secondary_fraction` only applies Table 11 when a secondary is
|
||||
# actually lodged or forced by §A.2.2 — deliberately, per the S-B30
|
||||
# note a few lines below: applying Table 11 unconditionally regressed
|
||||
# the full 1000-cert corpus MAE by +0.16 (lodged software doesn't
|
||||
# apply Elmhurst's blanket convention). Verified: forcing Table 11 on
|
||||
# for just this cert moves 76.33 -> 73.62, ~62% of the 76.33-vs-72 gap.
|
||||
# The remaining ~1.6 pts is a second, separate cause: (6) Solar gains —
|
||||
# the BUILD entered all 12 windows as one 15.32 m2 "North" group
|
||||
# (E.set_single_window), but the cert's real sap_windows lodge 3
|
||||
# orientations (NW 7.05 m2, NE 1.21 m2, SE 7.06 m2 incl. the largest
|
||||
# single window) — collapsing to due-north understates solar gain
|
||||
# (Elmhurst Jan (83)=56.9 W vs our real-orientation 123.4 W). Verified:
|
||||
# forcing our engine to ALSO use a single synthetic North window (i.e.
|
||||
# replicating the build's simplification, not a cert or engine fact)
|
||||
# plus the forced Table 11 above reproduces 72.62 (precise Elmhurst
|
||||
# value is 72.0681, not the rounded "72" -- true residual after Table
|
||||
# 11 + windows was ~0.55, not ~1.6). Third cause found by walking the
|
||||
# rest of the worksheet monthly: (43) Average daily hot water use is
|
||||
# 117.4056 L/day (Elmhurst) vs our 110.7281 L/day -- SAME occupancy
|
||||
# (42)=2.4234 both sides, so the gap is in shower/bath demand, not
|
||||
# occupancy. Root cause: cert's `mixer_shower_count` is unlodged (raw
|
||||
# `shower_outlets=None`), which `_mixer_shower_flow_rates_from_cert`
|
||||
# maps to 0 showers (see its docstring: gov-API "no shower_outlets
|
||||
# entry" convention, validated on cert 2225) -- but Elmhurst's build
|
||||
# shows a "Non-electric shower" fixture (1 vented mixer @ Table J4),
|
||||
# since its UI silently defaults one when the build script never
|
||||
# explicitly configured "0 showers" to match the cert. Verified: a
|
||||
# single 7 L/min vented mixer reproduces Elmhurst's (42a) monthly
|
||||
# series EXACTLY (4 d.p., all 12 months) and closes hot water to
|
||||
# 2616.04 vs Elmhurst's 2616.03 (Δ0.005). Corpus-wide check (170
|
||||
# certs sharing "bath present, no shower data lodged") shows NO
|
||||
# systematic bias (mean dSAP +0.12, median -0.04, 46.5% over-rated) --
|
||||
# confirms the mapper's 0-showers convention is correct on average;
|
||||
# this is a THIRD build-entry artifact, not a mapper bug. Combining
|
||||
# all three (windows + Table 11 + 1 shower): SAP 72.4567 vs Elmhurst's
|
||||
# precise 72.0681 -- residual 0.39, ~91% of the original 4.26-point
|
||||
# gap (76.33 vs 72.07) now precisely explained. Conclusion: the
|
||||
# calculator is faithful (matches lodged 76 exactly using the cert's
|
||||
# REAL windows and REAL shower data); all three residuals are Elmhurst
|
||||
# -reconciliation-build artifacts or deliberate spec choices, not
|
||||
# mapping or calculator bugs. PINNED to the observed exact match vs
|
||||
# lodged.
|
||||
RealCertExpectation(
|
||||
schema="RdSAP-Schema-21.0.1",
|
||||
sample="uprn_100061275133",
|
||||
cert_num="uprn-100061275133",
|
||||
sap_score=76,
|
||||
),
|
||||
# UPRN 100051051866 (corpus; no cert number lodged). RdSAP-21.0.1 native —
|
||||
# DETACHED BUNGALOW, band I, CAVITY wall as-built insulated, pitched
|
||||
# insulated loft, SUSPENDED limited-insulation floor, mains-gas regular
|
||||
# boiler + cylinder (size 2, insulation type 1, 50 mm — a properly
|
||||
# insulated cylinder, so NOT the type-0/NULL zero-storage-loss bug), TFA 60.
|
||||
# Lodged 70 / engine 71 (71.04). Reduced-field cavity fabric already
|
||||
# Elmhurst-validated on sibling certs; the +1.04 is a cost-side residual
|
||||
# (PE −13.6 kWh/m² under-count) with no fabric anomaly — walls 29.9, roof
|
||||
# 15.6, floor 29.4, windows 15.6 W/K all register-explained. PINNED to the
|
||||
# observed engine 71; the register data fully accounts for the engine output.
|
||||
RealCertExpectation(
|
||||
schema="RdSAP-Schema-21.0.1",
|
||||
sample="uprn_100051051866",
|
||||
cert_num="uprn-100051051866",
|
||||
sap_score=71,
|
||||
),
|
||||
# UPRN 10070056075 (corpus; no cert number lodged). RdSAP-21.0.1 native —
|
||||
# DETACHED HOUSE, band K, CAVITY wall as-built insulated, pitched insulated
|
||||
# loft, SUSPENDED insulated floor, mains-gas regular boiler + cylinder
|
||||
# (size 4, insulation type 1, 80 mm — properly insulated), dual meter,
|
||||
# TFA 139. Lodged 80 / engine 79 (78.99). Cavity fabric already
|
||||
# Elmhurst-validated on sibling certs; the −1.01 is a cost-side residual
|
||||
# (PE +5.4 kWh/m² over-count) with no fabric anomaly — walls 50.0, roof
|
||||
# 20.1, floor 16.7, windows 26.7 W/K all register-explained. PINNED to the
|
||||
# observed engine 79; the register data fully accounts for the engine output.
|
||||
RealCertExpectation(
|
||||
schema="RdSAP-Schema-21.0.1",
|
||||
sample="uprn_10070056075",
|
||||
cert_num="uprn-10070056075",
|
||||
sap_score=79,
|
||||
),
|
||||
# UPRN 10012334488 (corpus; no cert number lodged). RdSAP-21.0.1 native —
|
||||
# mid-floor FLAT, mains-electric with SAP heating code 195 ("Electric water
|
||||
# storage boiler", Table 4a PDF p.170) serving underfloor heating, on an
|
||||
# off-peak (7-hour) meter, TFA 62. Lodged 72 / engine 85 (85.20). This is a
|
||||
# PURE COST gap, NOT a fabric bug: PE (−1.4 kWh/m²) and CO2 (+0.01 t) both
|
||||
# match lodged, so demand is right. Code 195 is a STORAGE boiler → SAP 10.2
|
||||
# §12 Rule 2 routes it to the 7-hour tariff and bills space+water heat mostly
|
||||
# at the off-peak low rate (5.5 p/kWh) — the spec-faithful treatment. The
|
||||
# lodged software billed it far higher (implying peak-rate billing of a
|
||||
# storage system), the same lodged-outlier pattern as the §12/PSR xfail
|
||||
# cohort. PINNED to the spec-faithful engine 85 — the engine follows SAP
|
||||
# §12/Table-4a; matching lodged 72 would require billing a storage boiler at
|
||||
# peak rate, against spec. (Not Elmhurst-verified: my RdSAP10 builds carry a
|
||||
# combi/heating artifact that makes them unreliable on the E7 storage side.)
|
||||
RealCertExpectation(
|
||||
schema="RdSAP-Schema-21.0.1",
|
||||
sample="uprn_10012334488",
|
||||
cert_num="uprn-10012334488",
|
||||
sap_score=85,
|
||||
),
|
||||
# UPRN 10091578598 (corpus; no cert number lodged). RdSAP-21.0.1 native —
|
||||
# FLAT, mains-electric SAP code 195 ("Electric water storage boiler"),
|
||||
# off-peak meter, TFA 60. Lodged 71 / engine 79 (78.81). Same signature as
|
||||
# 10012334488: PURE COST gap (PE +1.6, CO2 −0.04 both match lodged → demand
|
||||
# right); code 195 is a §12-Rule-2 storage system billed at the 7-hour
|
||||
# off-peak rate per spec. Lodged over-billed the storage boiler at a higher
|
||||
# rate. PINNED to the spec-faithful engine 79 — matching lodged would tune
|
||||
# against SAP §12/Table-4a.
|
||||
RealCertExpectation(
|
||||
schema="RdSAP-Schema-21.0.1",
|
||||
sample="uprn_10091578598",
|
||||
cert_num="uprn-10091578598",
|
||||
sap_score=79,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -245,15 +245,18 @@ def test_part_geometry_floorless_part_honours_full_key_contract() -> None:
|
|||
assert early["cantilever_floor_area_m2"] == 0.0
|
||||
|
||||
|
||||
def test_roof_insulated_assumed_with_ni_thickness_uses_50mm_per_section_5_11_4() -> None:
|
||||
# Arrange — 346 corpus certs lodge roof_insulation_thickness="NI"
|
||||
# with descriptions like "Pitched, insulated (assumed)". The
|
||||
# retrofit-insulation signal in the description must flow from
|
||||
# epc.roofs[0].description through heat_transmission_from_cert
|
||||
# into u_roof so the 50 mm assumption fires per RdSAP 10 §5.11.4.
|
||||
# Geometry: 100 m² ground floor → top floor area also 100 m² →
|
||||
# roof_area = 100 m². roof_w_per_k expected = 0.68 × 100 = 68
|
||||
# W/K (vs uninsulated 2.30 × 100 = 230 W/K).
|
||||
def test_roof_insulated_assumed_with_ni_thickness_uses_table18_default() -> None:
|
||||
# Arrange — corpus certs lodge roof_insulation_thickness="NI" with
|
||||
# descriptions like "Pitched, insulated (assumed)". The description
|
||||
# must flow from epc.roofs[0].description through
|
||||
# heat_transmission_from_cert into u_roof, where "(assumed)"
|
||||
# (presence assumed, not observed retrofit) routes to the Table 18
|
||||
# age-band default per RdSAP 10 §5.11.4 — band C pitched → 0.40 —
|
||||
# matching the "ND" lodgement of the same description. (The bare
|
||||
# "insulated" form keeps the §5.11.4 50 mm row; unit-pinned in
|
||||
# test_rdsap_uvalues.) Geometry: 100 m² ground floor → top floor
|
||||
# area also 100 m² → roof_area = 100 m². roof_w_per_k expected =
|
||||
# 0.40 × 100 = 40 W/K (vs uninsulated 2.30 × 100 = 230 W/K).
|
||||
main = make_building_part(
|
||||
identifier="Main Dwelling",
|
||||
construction_age_band="C",
|
||||
|
|
@ -287,7 +290,7 @@ def test_roof_insulated_assumed_with_ni_thickness_uses_50mm_per_section_5_11_4()
|
|||
result = heat_transmission_from_cert(epc)
|
||||
|
||||
# Assert
|
||||
assert result.roof_w_per_k == pytest.approx(68.0, abs=2.0)
|
||||
assert result.roof_w_per_k == pytest.approx(40.0, abs=2.0)
|
||||
|
||||
|
||||
def test_roof_insulation_location_rafters_drives_table16_column_2_api_int_path() -> None:
|
||||
|
|
@ -367,6 +370,46 @@ def test_rafter_insulation_thickness_field_drives_table16_column_2() -> None:
|
|||
assert abs(result.roof_w_per_k - 25.0) <= 1e-4
|
||||
|
||||
|
||||
def test_flat_roof_insulation_thickness_field_drives_insulated_u() -> None:
|
||||
# Arrange — a FLAT roof lodges its insulation thickness in the DEDICATED
|
||||
# gov-EPC API `flat_roof_insulation_thickness` field (e.g. "75mm"),
|
||||
# leaving `roof_insulation_thickness` None (that field is for pitched-loft
|
||||
# joists). heat_transmission must prefer the flat field when the part is a
|
||||
# flat roof so the measured thickness bills at its RdSAP 10 §5.11 Table-16
|
||||
# U (75 mm → 0.5) instead of the uninsulated age-band flat default (age E
|
||||
# → 1.5). Cert 47084930 (top-floor flat, flat roof "75mm"): roof 96.4 ->
|
||||
# 32.1 W/K, engine 69.51 -> 74.34 = lodged 74, matching the accredited
|
||||
# Elmhurst worksheet ("insulated flat roof", U 0.5). Mirrors the rafter
|
||||
# field. Geometry: 100 m² plan → roof 100 m². 0.5 × 100 = 50 W/K.
|
||||
main = make_building_part(
|
||||
construction_age_band="E",
|
||||
wall_construction=4,
|
||||
wall_insulation_type=4,
|
||||
party_wall_construction=1,
|
||||
roof_construction=1, # flat
|
||||
floor_dimensions=[
|
||||
make_floor_dimension(
|
||||
total_floor_area_m2=100.0, room_height_m=2.5,
|
||||
party_wall_length_m=0.0, heat_loss_perimeter_m=40.0, floor=0,
|
||||
),
|
||||
],
|
||||
)
|
||||
main.roof_construction_type = "Flat"
|
||||
main.roof_insulation_thickness = None # gov leaves this None for flat roofs
|
||||
main.flat_roof_insulation_thickness = "75mm" # the thickness lives here
|
||||
epc = make_minimal_sap10_epc(
|
||||
total_floor_area_m2=100.0,
|
||||
country_code="ENG",
|
||||
sap_building_parts=[main],
|
||||
)
|
||||
|
||||
# Act
|
||||
result = heat_transmission_from_cert(epc)
|
||||
|
||||
# Assert — 75 mm insulated flat roof at U 0.5, not the age-E default 1.5.
|
||||
assert abs(result.roof_w_per_k - 50.0) <= 1e-4
|
||||
|
||||
|
||||
def test_lodged_roof_u_value_overrides_construction_default() -> None:
|
||||
# Arrange — RdSAP 10 §5.1: where an element's U-value is known from the
|
||||
# assessment (documentary evidence / the lodged RdSAP output) it is used
|
||||
|
|
|
|||
|
|
@ -30,9 +30,13 @@ _GOLDEN = (
|
|||
_WITHIN_TOLERANCE = "0036-6325-1100-0063-1226"
|
||||
_DIVERGENT = "0240-0200-5706-2365-8010"
|
||||
|
||||
# 0330 fires all three trigger kinds: an uninsulated cavity wall (cavity fill),
|
||||
# its dependent mechanical ventilation, and an uninsulated solid floor.
|
||||
_THREE_MEASURES = "0330-2249-8150-2326-4121"
|
||||
# 0390 fires three measures — an uninsulated solid floor, low-energy lighting,
|
||||
# and the ASHP bundle — so every fired measure's trigger attributes are
|
||||
# exercised together. (0330, the previous fixture, now reaches band C on the
|
||||
# correctly-sized ASHP alone: ADR-0049 sizes the pump to the dwelling's design
|
||||
# heat loss, so the undersized-pump-era companion solid_floor_insulation is no
|
||||
# longer needed there.)
|
||||
_THREE_MEASURES = "0390-2254-6420-2126-5561"
|
||||
|
||||
|
||||
def _triggers_by_measure(report: PropertyReport) -> dict[str, MeasureTrigger]:
|
||||
|
|
@ -80,17 +84,12 @@ def test_each_fired_measure_carries_the_attributes_that_triggered_it() -> None:
|
|||
# Assert — the Plan ran and every fired measure names its trigger fields.
|
||||
assert report.plan is not None
|
||||
assert report.plan_error is None
|
||||
# The gain-maximising package: the efficient representative ASHP (ADR-0025)
|
||||
# plus solid-floor insulation. The cavity wall + its forced mechanical
|
||||
# ventilation (ADR-0016) are NOT selected — the wall earns +SAP alone but
|
||||
# the forced-ventilation penalty makes the pair net-negative, so the
|
||||
# Optimiser correctly leaves them out (see test_measure_dependency /
|
||||
# test_optimiser for the forced-edge unit coverage; cavity_wall +
|
||||
# mechanical_ventilation trigger fields are exercised in
|
||||
# test_cavity_wall_recommendation / the ventilation generator tests).
|
||||
# The selected package: the dwelling-sized ASHP (ADR-0025/ADR-0049) plus
|
||||
# solid-floor insulation and low-energy lighting.
|
||||
triggers: dict[str, MeasureTrigger] = _triggers_by_measure(report)
|
||||
assert set(triggers) == {
|
||||
"solid_floor_insulation",
|
||||
"low_energy_lighting",
|
||||
"air_source_heat_pump",
|
||||
}
|
||||
# Solid-floor insulation fired off an uninsulated solid ground floor.
|
||||
|
|
@ -98,6 +97,12 @@ def test_each_fired_measure_carries_the_attributes_that_triggered_it() -> None:
|
|||
"floor_insulation_thickness": None,
|
||||
"floor_construction_type": "Solid",
|
||||
}
|
||||
# Low-energy lighting fired off the dwelling's lodged bulb counts.
|
||||
assert triggers["low_energy_lighting"].triggers == {
|
||||
"incandescent_fixed_lighting_bulbs_count": 0,
|
||||
"cfl_fixed_lighting_bulbs_count": 0,
|
||||
"low_energy_fixed_lighting_bulbs_count": 9,
|
||||
}
|
||||
# The ASHP bundle fired off the gas-dwelling main it replaces.
|
||||
assert triggers["air_source_heat_pump"].triggers == {
|
||||
"property_type": "0",
|
||||
|
|
|
|||
|
|
@ -196,7 +196,50 @@ _CORPUS = Path(
|
|||
# 0.74 -> 0.742 via the heat-pump water-heating 100% floor (App N3.7): cert
|
||||
# 100110101713 moves inside +-0.5 (|err| 4.97 -> 0.49). See the _MAX_SAP_MAE
|
||||
# log below for the paired space-heating PSR-extension + water-floor slices.
|
||||
_MIN_WITHIN_HALF_SAP = 0.742
|
||||
# 0.742 -> 0.755 (MAE 0.722 -> 0.708, CO2 0.09 -> 0.075, PE 3.5 -> 3.2) via
|
||||
# the "insulated (assumed)" roof Table-18 fix (RdSAP 10 §5.11.4): a pitched
|
||||
# roof lodging thickness "NI" + description "insulated (assumed)" was hitting
|
||||
# the §5.11.4 retrofit-50 mm row (0.68) while the SAME description lodged
|
||||
# with "ND" fell through to the Table 18 age-band default (e.g. 0.40 A-G) —
|
||||
# the sentinel choice is lodging-software noise, and "(assumed)" means the
|
||||
# insulation PRESENCE is an age-band assumption, not an observed retrofit,
|
||||
# so Table 18's "thickness cannot be determined" clause governs both forms.
|
||||
# The bare "insulated" description (observed retrofit) keeps the 50 mm row.
|
||||
# The 27-cert loc4+NI cohort was systematically under-rated ~1 SAP (e.g.
|
||||
# 10094975827 band B -1.87 -> inside). Unit-pinned in test_rdsap_uvalues.
|
||||
# 0.755 -> 0.770 (MAE 0.708 -> 0.668) via the secondary-fuel collision fix
|
||||
# on the SAP-DRIVING cost path (`_fuel_cost`): the gov-API secondary fuel
|
||||
# enums 5 (anthracite) / 9 (dual fuel) / 33 (coal) collide with same-valued
|
||||
# Table-32 codes for OTHER fuels, and `_fuel_cost`'s inline secondary rate
|
||||
# passed the raw enum to the price lookup (unlike the already-canonicalised
|
||||
# main-fuel boundary + CO2/display helper). Anthracite priced at the bulk-LPG
|
||||
# code-5 rate (12.19 vs 3.64 p/kWh) 3x over-cost the secondary -> under-rate.
|
||||
# Worst: cert 100050355518 (2 extensions, anthracite room-heater 2ndary)
|
||||
# lodged 36 / engine 20.8 -> 35.9. 55 corpus certs carry a colliding
|
||||
# secondary. Unit-pinned in test_cert_to_inputs (fuel_cost cost path, not
|
||||
# just the display helper the prior enum-9 test guarded).
|
||||
# 0.770 -> 0.773 via the unsized-cylinder storage-loss fix: a present
|
||||
# cylinder lodging gov-API cylinder_size=0 ("size not determined") resolved
|
||||
# to None volume in `_cylinder_storage_loss_override`, so `if volume_l is
|
||||
# None: return None` DROPPED its storage loss (under-costing the DHW ->
|
||||
# over-rate). RdSAP 10 §10.5 Table 28: an unsized present cylinder is the
|
||||
# 110 L "Normal" default and still incurs the Table-2 loss (the value the
|
||||
# Table-13 fraction path already used). Gated on the explicit 0 so a
|
||||
# full-SAP unlodged size (None) keeps its own handling. 7 corpus certs;
|
||||
# within 77.0% -> 77.3%, PE 3.2 -> 3.05, CO2 0.074 -> 0.073.
|
||||
# 0.773 -> 0.777 via the flat-roof insulation-thickness fix: a FLAT roof
|
||||
# lodges its thickness in the dedicated `flat_roof_insulation_thickness`
|
||||
# field (e.g. "75mm"), but heat_transmission read `roof_insulation_thickness`
|
||||
# (None for flat roofs) -> billed the roof at the uninsulated age-band flat
|
||||
# default (age E 1.5) instead of the insulated Table-16 U. Prefer the flat
|
||||
# field when the part is flat (mirrors the rafter-thickness branch). Cert
|
||||
# 47084930 (top-floor flat, flat roof 75mm): roof 96.4 -> 32.1 W/K, engine
|
||||
# 69.51 -> 74.34 = lodged 74, VALIDATED against the accredited Elmhurst
|
||||
# worksheet ("insulated flat roof" U 0.5). within 77.3% -> 77.7%, PE 3.1 ->
|
||||
# 2.97, CO2 0.074 -> 0.072.
|
||||
# 77.7% -> 77.8% via the §5 (12) suspended-timber floor-infiltration
|
||||
# fallback (see the `_MAX_SAP_MAE` note below for the mechanism).
|
||||
_MIN_WITHIN_HALF_SAP = 0.778
|
||||
# 0.793 -> 0.789 via the §12 Unknown-meter + dual-electric-immersion off-peak
|
||||
# trigger (RdSAP 10 PDF p.62): Apartment 241 (main 691 + 903 dual immersion)
|
||||
# -5.38 -> -1.05. Worksheet-validated on "simulated case 48" (Elmhurst SAP 57,
|
||||
|
|
@ -266,9 +309,34 @@ _MIN_WITHIN_HALF_SAP = 0.742
|
|||
# oversized-PSR water eff 0.60 x 128.55% = 77.13% is floored to 100% (=
|
||||
# accredited Elmhurst (216)), taking the cert 68.03 -> 72.51 (|err| 4.97 ->
|
||||
# 0.49, now inside +-0.5). In-range heat pumps keep their > 100% water COP.
|
||||
_MAX_SAP_MAE = 0.722
|
||||
_MAX_CO2_MAE_TONNES = 0.09 # t CO2 / yr vs co2_emissions_current
|
||||
_MAX_PE_PER_M2_MAE = 3.5 # kWh / m2 / yr vs energy_consumption_current
|
||||
# 0.669 -> 0.659 via the instantaneous-electric-DHW off-peak fix: WHC 907/909
|
||||
# (point-of-use, no cylinder) on an off-peak tariff was billed 100% at the
|
||||
# off-peak LOW rate, but SAP 10.2 §12 (Table 13 on-peak proportion is keyed on
|
||||
# CYLINDER size) presumes stored water; an instantaneous heater has no cylinder
|
||||
# and heats on demand -> 100% high rate. Cert 74061136 (HHR-storage flat,
|
||||
# WHC 909) +7.72 -> -1.25. Both the cost rate and the ADR-0014/CO2-PE fraction
|
||||
# fixed (fraction 0 -> 1.0). Unit-pinned in test_cert_to_inputs.
|
||||
# 0.659 -> 0.648 via the wall added-insulation unknown-thickness fix: a
|
||||
# SOLID-BRICK/STONE wall lodging External/Internal insulation (type 1/3) with
|
||||
# thickness "NI" (unknown) was billed UNINSULATED (§5.8 R-value gate needs a
|
||||
# thickness > 0), over-counting wall heat loss. RdSAP 10 §5.4 (PDF p.32):
|
||||
# "assume 100 mm if thickness is unknown" -> apply the 100 mm §5.8 default.
|
||||
# Cert 200004296092 (age-B solid brick 360 mm + external, NI): walls 101.7 W/K
|
||||
# (U~1.4) -> ~0.29, PE +59.6 -> ~0, SAP 64.35 -> 71.74 (lodged 71). Scoped to
|
||||
# brick/stone (the only §5.8 R-value path); within-0.5 held at 77.0%, MAE
|
||||
# 0.658 -> 0.647, PE 3.2 -> 3.1. Unit-pinned in test_rdsap_uvalues.
|
||||
# 77.7% -> 77.8% (MAE 0.637 -> 0.636) via the §5 (12) suspended-timber
|
||||
# floor-infiltration fallback: `_has_suspended_timber_floor_per_spec`
|
||||
# only read the Main bp's per-part `floor_construction_type` lodgement,
|
||||
# which the gov-API mapper frequently leaves `None` even when the global
|
||||
# `epc.floors[].description` carries an explicit "Suspended, ..."
|
||||
# observation (`_main_floor_u_value` already fell back to this joined
|
||||
# description for the U-value calc — the infiltration rule did not).
|
||||
# Cert 100061275133 (Elmhurst-validated build): SAP 77.24 -> 76.33, now
|
||||
# an EXACT match to lodged (76). Unit-pinned in test_cert_to_inputs.
|
||||
_MAX_SAP_MAE = 0.637
|
||||
_MAX_CO2_MAE_TONNES = 0.072 # t CO2 / yr vs co2_emissions_current
|
||||
_MAX_PE_PER_M2_MAE = 3.0 # kWh / m2 / yr vs energy_consumption_current
|
||||
|
||||
|
||||
def _load_corpus() -> list[dict[str, Any]]:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
"""The ModellingOrchestrator threads the dwelling's SAP design heat loss into
|
||||
the heating Recommendation Generator so the ASHP is sized to the dwelling
|
||||
(ADR-0049), mirroring how planning_restrictions and the solar potential are
|
||||
threaded. Tests the candidate-wiring seam directly; the end-to-end run-through-
|
||||
repos path is covered by the DB integration tests.
|
||||
"""
|
||||
|
||||
from datatypes.epc.domain.epc_property_data import EpcPropertyData
|
||||
from domain.geospatial.planning_restrictions import PlanningRestrictions
|
||||
from domain.modelling.measure_type import MeasureType
|
||||
from domain.modelling.product import Product
|
||||
from domain.modelling.recommendation import Recommendation
|
||||
from orchestration.modelling_orchestrator import (
|
||||
_candidate_recommendations, # pyright: ignore[reportPrivateUsage]
|
||||
)
|
||||
from repositories.product.product_repository import ProductRepository
|
||||
from tests.domain.modelling._elmhurst_recommendation import (
|
||||
parse_recommendation_summary,
|
||||
)
|
||||
|
||||
|
||||
class _StubProducts(ProductRepository):
|
||||
def get(self, measure_type: str) -> Product:
|
||||
return Product(
|
||||
measure_type=measure_type,
|
||||
unit_cost_per_m2=0.0,
|
||||
contingency_rate=0.15,
|
||||
id=909,
|
||||
)
|
||||
|
||||
|
||||
def _ashp_eligible_house() -> EpcPropertyData:
|
||||
epc = parse_recommendation_summary(
|
||||
"ashp_from_system_boiler_with_cylinder_001431_before.pdf"
|
||||
)
|
||||
epc.property_type = "House"
|
||||
return epc
|
||||
|
||||
|
||||
def test_candidate_recommendations_sizes_the_ashp_to_the_threaded_design_heat_loss() -> (
|
||||
None
|
||||
):
|
||||
# Arrange — an ASHP-eligible house and a high (17.5 kW) design heat loss.
|
||||
epc = _ashp_eligible_house()
|
||||
|
||||
# Act
|
||||
recommendations: list[Recommendation] = _candidate_recommendations(
|
||||
epc, _StubProducts(), PlanningRestrictions(), None, None, design_heat_loss_kw=17.5
|
||||
)
|
||||
|
||||
# Assert — the ASHP is sized to the load (capped 12 kW aroTHERM plus 110281),
|
||||
# not the fixed 5 kW / floor-area-proxy unit.
|
||||
heating = next(r for r in recommendations if r.surface == "Heating & Hot Water")
|
||||
ashp = next(
|
||||
o
|
||||
for o in heating.options
|
||||
if o.measure_type == MeasureType.AIR_SOURCE_HEAT_PUMP
|
||||
)
|
||||
assert ashp.overlay.heating is not None
|
||||
assert ashp.overlay.heating.main_heating_index_number == 110281
|
||||
|
|
@ -128,6 +128,7 @@ def _sap_result_with_lighting() -> SapResult:
|
|||
space_heating_kwh_per_yr=0.0,
|
||||
space_cooling_kwh_per_yr=0.0,
|
||||
fabric_energy_efficiency_kwh_per_m2_yr=0.0,
|
||||
design_heat_loss_kw=0.0,
|
||||
main_heating_fuel_kwh_per_yr=0.0,
|
||||
main_2_heating_fuel_kwh_per_yr=0.0,
|
||||
secondary_heating_fuel_kwh_per_yr=0.0,
|
||||
|
|
|
|||
|
|
@ -1,14 +1,32 @@
|
|||
from pathlib import Path
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from backend.pashub_fetcher.sharepoint_subfolders import SharepointSubfolders
|
||||
from orchestration.sharepoint_renamer_orchestrator import (
|
||||
ASSESSMENT_SUBFOLDER,
|
||||
BASE_PATH,
|
||||
SharepointRenamerOrchestrator,
|
||||
build_canonical_filename,
|
||||
)
|
||||
|
||||
|
||||
def _assessment_path(root: str, address: str, postcode: str) -> str:
|
||||
return (
|
||||
f"{root}/{address}, {postcode}"
|
||||
f"/{SharepointSubfolders.ASSESSMENT.value}/{ASSESSMENT_SUBFOLDER}"
|
||||
)
|
||||
|
||||
|
||||
def _write_csv(tmp_path: Path, rows: list[tuple[str, str, str]]) -> str:
|
||||
lines = ["UPRN,Address,Postcode"] + [",".join(row) for row in rows]
|
||||
csv_file = tmp_path / "addresses.csv"
|
||||
csv_file.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
return str(csv_file)
|
||||
|
||||
|
||||
def _make_file(name: str, item_id: str = "id-1") -> dict[str, Any]:
|
||||
return {"name": name, "id": item_id, "file": {}}
|
||||
|
||||
|
|
@ -47,6 +65,92 @@ def test_no_prefix_still_canonical() -> None:
|
|||
assert result == "100_1 High St AB1 2CD_Survey.pdf"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _discover_roots — batch subfolders become extra roots
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_discover_roots_includes_sero_batch_folders() -> None:
|
||||
# Arrange
|
||||
sp = MagicMock()
|
||||
sp.get_folders_in_path.return_value = {
|
||||
"value": [
|
||||
_make_folder("1 High St, AB1 2CD"),
|
||||
_make_folder("_Sero Batch 3"),
|
||||
_make_folder("_Sero Batch 2"),
|
||||
_make_file("stray.pdf"),
|
||||
]
|
||||
}
|
||||
|
||||
# Act
|
||||
roots = _make_orchestrator(sp)._discover_roots()
|
||||
|
||||
# Assert
|
||||
assert roots == [
|
||||
BASE_PATH,
|
||||
f"{BASE_PATH}/_Sero Batch 2",
|
||||
f"{BASE_PATH}/_Sero Batch 3",
|
||||
]
|
||||
sp.get_folders_in_path.assert_called_once_with(BASE_PATH)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# run — properties under a batch root are still found and renamed
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_run_renames_files_for_property_under_batch_root(tmp_path: Path) -> None:
|
||||
# Arrange
|
||||
csv_path = _write_csv(tmp_path, [("100", "1 High St", "AB1 2CD")])
|
||||
batch_assessment = _assessment_path(
|
||||
f"{BASE_PATH}/_Sero Batch 2", "1 High St", "AB1 2CD"
|
||||
)
|
||||
|
||||
sp = MagicMock()
|
||||
|
||||
def fake_get(path: str) -> dict[str, Any]:
|
||||
if path == BASE_PATH:
|
||||
return {"value": [_make_folder("_Sero Batch 2")]}
|
||||
if path == batch_assessment:
|
||||
return {"value": [_make_file("Survey.pdf", "id-1")]}
|
||||
raise ValueError("not found")
|
||||
|
||||
sp.get_folders_in_path.side_effect = fake_get
|
||||
|
||||
# Act
|
||||
SharepointRenamerOrchestrator(sp, csv_path).run()
|
||||
|
||||
# Assert
|
||||
sp.rename_file.assert_called_once_with("id-1", "100_1 High St AB1 2CD_Survey.pdf")
|
||||
|
||||
|
||||
def test_run_uprn_prefixed_address_yields_single_uprn_filename(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
# Arrange
|
||||
csv_path = _write_csv(tmp_path, [("100", "100_1 High St", "AB1 2CD")])
|
||||
batch_assessment = _assessment_path(
|
||||
f"{BASE_PATH}/_Sero Batch 2", "100_1 High St", "AB1 2CD"
|
||||
)
|
||||
|
||||
sp = MagicMock()
|
||||
|
||||
def fake_get(path: str) -> dict[str, Any]:
|
||||
if path == BASE_PATH:
|
||||
return {"value": [_make_folder("_Sero Batch 2")]}
|
||||
if path == batch_assessment:
|
||||
return {"value": [_make_file("Survey.pdf", "id-1")]}
|
||||
raise ValueError("not found")
|
||||
|
||||
sp.get_folders_in_path.side_effect = fake_get
|
||||
|
||||
# Act
|
||||
SharepointRenamerOrchestrator(sp, csv_path).run()
|
||||
|
||||
# Assert
|
||||
sp.rename_file.assert_called_once_with("id-1", "100_1 High St AB1 2CD_Survey.pdf")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _process_folder — files only at root level
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -121,14 +225,46 @@ def test_ignores_package_items() -> None:
|
|||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_missing_folder_logs_warning_and_returns(caplog: pytest.LogCaptureFixture) -> None:
|
||||
def test_missing_folder_returns_false_without_warning(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
# Arrange
|
||||
sp = MagicMock()
|
||||
sp.get_folders_in_path.side_effect = ValueError("not found")
|
||||
|
||||
_make_orchestrator(sp)._process_folder("missing/path", "400", "4 Elm St", "AA2 2BB")
|
||||
# Act
|
||||
found = _make_orchestrator(sp)._process_folder(
|
||||
"missing/path", "400", "4 Elm St", "AA2 2BB"
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert found is False
|
||||
sp.rename_file.assert_not_called()
|
||||
assert any("Missing folder" in r.message and "400" in r.message for r in caplog.records)
|
||||
assert not any("Missing folder" in r.message for r in caplog.records)
|
||||
|
||||
|
||||
def test_run_warns_once_when_property_missing_from_all_roots(
|
||||
tmp_path: Path, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
# Arrange
|
||||
csv_path = _write_csv(tmp_path, [("400", "4 Elm St", "AA2 2BB")])
|
||||
sp = MagicMock()
|
||||
|
||||
def fake_get(path: str) -> dict[str, Any]:
|
||||
if path == BASE_PATH:
|
||||
return {"value": [_make_folder("_Sero Batch 2")]}
|
||||
raise ValueError("not found")
|
||||
|
||||
sp.get_folders_in_path.side_effect = fake_get
|
||||
|
||||
# Act
|
||||
SharepointRenamerOrchestrator(sp, csv_path).run()
|
||||
|
||||
# Assert
|
||||
sp.rename_file.assert_not_called()
|
||||
warnings = [r for r in caplog.records if "Missing folder" in r.message]
|
||||
assert len(warnings) == 1
|
||||
assert "400" in warnings[0].message
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
0
tests/utils/__init__.py
Normal file
0
tests/utils/__init__.py
Normal file
81
tests/utils/test_sharepoint_client_error_handling.py
Normal file
81
tests/utils/test_sharepoint_client_error_handling.py
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import logging
|
||||
from typing import Any, Callable, Optional, cast
|
||||
|
||||
import pytest
|
||||
|
||||
from utils.sharepoint.sharepoint_client import (
|
||||
ResourceNotFoundError,
|
||||
api_call_decorator, # pyright: ignore[reportUnknownVariableType]
|
||||
)
|
||||
|
||||
# The legacy decorator is untyped; cast so this module stays pyright-strict.
|
||||
_decorate = cast(
|
||||
Callable[[Callable[..., Any]], Callable[..., Any]], api_call_decorator
|
||||
)
|
||||
|
||||
|
||||
class _FakeResponse:
|
||||
def __init__(self, status_code: int, error_code: str, message: str) -> None:
|
||||
self.status_code = status_code
|
||||
self._error_code = error_code
|
||||
self._message = message
|
||||
self.headers: dict[str, str] = {}
|
||||
|
||||
def json(self) -> dict[str, Any]:
|
||||
return {"error": {"code": self._error_code, "message": self._message}}
|
||||
|
||||
|
||||
class _FakeClient:
|
||||
headers: Optional[dict[str, str]] = {}
|
||||
|
||||
def is_access_token_expired(self) -> bool:
|
||||
return False
|
||||
|
||||
@_decorate
|
||||
def fetch(self, path: str) -> Any:
|
||||
return "GET", f"https://graph.example.invalid/root:/{path}:/children", None
|
||||
|
||||
|
||||
def _patch_response(
|
||||
monkeypatch: pytest.MonkeyPatch, response: _FakeResponse
|
||||
) -> None:
|
||||
def fake_request(*args: Any, **kwargs: Any) -> _FakeResponse:
|
||||
return response
|
||||
|
||||
monkeypatch.setattr(
|
||||
"utils.sharepoint.sharepoint_client.requests.request", fake_request
|
||||
)
|
||||
|
||||
|
||||
def test_404_raises_resource_not_found_without_error_logs(
|
||||
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
# Arrange
|
||||
_patch_response(
|
||||
monkeypatch,
|
||||
_FakeResponse(404, "itemNotFound", "The resource could not be found."),
|
||||
)
|
||||
|
||||
# Act
|
||||
with pytest.raises(ResourceNotFoundError):
|
||||
_FakeClient().fetch("missing/folder")
|
||||
|
||||
# Assert
|
||||
error_records = [r for r in caplog.records if r.levelno >= logging.ERROR]
|
||||
assert error_records == []
|
||||
|
||||
|
||||
def test_other_client_errors_still_log_at_error_level(
|
||||
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
# Arrange
|
||||
_patch_response(
|
||||
monkeypatch, _FakeResponse(403, "accessDenied", "Access denied.")
|
||||
)
|
||||
|
||||
# Act
|
||||
with pytest.raises(ValueError):
|
||||
_FakeClient().fetch("forbidden/folder")
|
||||
|
||||
# Assert
|
||||
assert any(r.levelno >= logging.ERROR for r in caplog.records)
|
||||
|
|
@ -21,6 +21,10 @@ from utils.logger import setup_logger
|
|||
logger = setup_logger()
|
||||
|
||||
|
||||
class ResourceNotFoundError(ValueError):
|
||||
"""Raised when the API returns 404 — an expected miss, not a failure."""
|
||||
|
||||
|
||||
def handle_error(response):
|
||||
"""
|
||||
Handle errors based on HTTP status codes and log detailed information.
|
||||
|
|
@ -35,6 +39,12 @@ def handle_error(response):
|
|||
inner_error = error_json.get("innererror", {})
|
||||
details = error_json.get("details", [])
|
||||
|
||||
if response.status_code == 404:
|
||||
logger.debug(f"Not found ({error_code}): {error_message}")
|
||||
raise ResourceNotFoundError(
|
||||
f"API request failed with status code 404 - {error_message}"
|
||||
)
|
||||
|
||||
logger.error(f"Error Code: {error_code}")
|
||||
logger.error(f"Error Message: {error_message}")
|
||||
if inner_error:
|
||||
|
|
@ -46,8 +56,6 @@ def handle_error(response):
|
|||
logger.error("Unauthorized. Token might be invalid.")
|
||||
elif response.status_code == 403:
|
||||
logger.error("Forbidden. Access denied to the requested resource.")
|
||||
elif response.status_code == 404:
|
||||
logger.error("Not Found. The requested resource doesn’t exist.")
|
||||
elif response.status_code == 429:
|
||||
retry_after = int(
|
||||
response.headers.get("Retry-After", 5)
|
||||
|
|
@ -121,6 +129,8 @@ def api_call_decorator(func):
|
|||
|
||||
return response_data
|
||||
|
||||
except ResourceNotFoundError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception("An error occurred during the API call.")
|
||||
raise e
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue