inviestigation with hyde values

This commit is contained in:
Jun-te Kim 2026-06-15 12:13:11 +00:00
parent 5923f8d072
commit 0079752eab
9 changed files with 2774 additions and 1 deletions

View file

@ -0,0 +1,93 @@
---
name: epc-to-elmhurst-rdsap-inputs
description: Convert an EPC certificate (by UPRN, certificate number, or local epc.json) into a markdown sheet of Elmhurst Energy RdSAP entry-tool inputs, page by page, so a human can key them in and compare Elmhurst's SAP score against this repo's engine. Use when verifying SAP-calculator accuracy against Elmhurst, reproducing a lodged cert in Elmhurst, or when the user mentions Elmhurst, RdSAP inputs, or checking the SAP score for a UPRN/certificate.
---
# EPC → Elmhurst RdSAP inputs
Produces a markdown crib sheet for re-keying a real EPC certificate into
Elmhurst Energy's RdSAP entry tool, so the operator can read off Elmhurst's
SAP score and compare it to this engine's. The accuracy comparison is the
whole point — the markdown leads with **our engine's SAP score** as the
number to beat, and flags known divergences.
This is prompt-driven: you read the cert's real values, look up each Elmhurst
field in [reference/mapping.md](reference/mapping.md), and format the result.
**Ground every number in the loaded `EpcPropertyData` and the engine's
computed values — never guess a code or area.** Codes you can't find in the
mapping reference must be looked up in the cited source file, not invented.
## Workflow
1. **Resolve the cert to an `EpcPropertyData`** (one of):
- **UPRN**`scripts/fetch_real_life_epc_sample.py <uprn>` (fetches, saves to
`backend/epc_api/json_samples/real_life_examples/<schema>/uprn_<uprn>/epc.json`,
prints schema + lodged rating + engine output), or
`EpcClientService(auth_token=...).get_by_uprn(<uprn>)`.
- **Certificate number**`EpcClientService.get_by_certificate_number(<cert>)`.
- **Local json**`EpcPropertyDataMapper.from_api_response(json.load(...))`.
Token is in `backend/.env` (`OPEN_EPC_API_TOKEN`, else `EPC_AUTH_TOKEN`).
For a saved json, mock `httpx.get` to return `{"data": <json>}` (see the
fetch script), or call the mapper directly.
2. **Compute the engine's view** so the sheet shows real numbers, not guesses:
```python
from domain.sap10_calculator.calculator import Sap10Calculator
from domain.sap10_calculator.rdsap.cert_to_inputs import cert_to_inputs
inputs = cert_to_inputs(epc) # window areas, U-values, fuel costs, cylinder
result = Sap10Calculator().calculate(epc) # our SAP score + per-end-use kWh
```
Pull window area from `inputs.heat_transmission.windows_w_per_k` + the
synthesised `epc.sap_windows`; U-values from `inputs.heat_transmission`;
fuel £/kWh from `inputs.*_fuel_cost_gbp_per_kwh`.
3. **Write the markdown**, one section per Elmhurst page, in this order:
Property Description · Dimensions · Conservatory · Walls (incl. Party wall) ·
Roofs · Floors · Openings (Windows, Doors) · Ventilation & Lighting
(Ventilation, Mechanical Ventilation, Air Pressure Test, Lighting) ·
Space Heating (Main Heating 1, Main Heating 2, Community Heating, Meters) ·
Water Heating (Water Heating + cylinder, Community Hot Water, Solar Water
Heating, WWHRS, FGHRS) · New Technologies (PV, Wind, Hydro, Special Features).
For each field give the **Elmhurst label**, the **value to enter**, and where
useful the **EES dropdown path** and **SAP code**. Use the lookup tables and
gotchas in [reference/mapping.md](reference/mapping.md).
4. **Save** the file next to the cert json as `elmhurst_inputs.md`
(e.g. `.../real_life_examples/<schema>/uprn_<uprn>/elmhurst_inputs.md`).
5. **Tell the operator**: key it into Elmhurst, then report the SAP score (and
heating cost £ if shown). If it differs from our engine's score, that's a
calculator finding — capture it.
## Output shape
Start the file with a header block:
```
# Elmhurst RdSAP inputs — UPRN <uprn> (cert <cert>, <schema>)
**Lodged SAP:** <energy_rating_current> **Our engine:** <result.sap_score> ← compare Elmhurst against this
**Known divergences:** <e.g. off-peak fuel-cost bug see Meters>
```
Then the page sections as tables: `| Elmhurst field | Value | Notes (SAP code / EES path) |`.
## Critical gotchas (full detail in reference)
- **Economy-7 / off-peak electricity** (`main_fuel_type`/`water_heating_fuel` 29):
Elmhurst meter type **must be Dual-rate / Economy 7 (7-hour)**, not Single.
Our engine has a **known over-rating bug** here — it prices 100% of off-peak
space heating + hot water at the 5.50p low rate instead of the SAP Table 12a
high/low split. Always flag this in the output for all-electric off-peak certs.
- **WWHRS**: `sap_heating.instantaneous_wwhrs` is **bath/shower ROOM counts**
(ADR-0028), NOT a heat-recovery device → WWHRS = **No** unless a real unit is lodged.
- **Party wall** code 1 = Solid (U=0, Elmhurst "Solid"), not "Unable to determine".
- **Cylinder insulation** type 1 = Foam, 2 = Jacket.
- **Water heating** 903 = Electric immersion off-peak → Elmhurst "Water Heater"
category, not "Boiler Circulator" (901 = from main system).
- **Windows** are synthesised from `glazed_area` band × TFA — not real geometry.
## Canonical example
UPRN **10002468137** (cert `0215-2818-7357-9703-2145`, RdSAP-Schema-17.1):
all-electric high-heat-retention storage heaters on Economy 7, solid-brick
uninsulated end-terrace. **Lodged SAP 55 vs engine 62** — the over-rating that
motivated this skill. Use it to sanity-check output.

View file

@ -0,0 +1,215 @@
# EpcPropertyData → Elmhurst RdSAP field mapping
Complete code→value reference for the `epc-to-elmhurst-rdsap-inputs` skill.
Every mapping cites its source in this repo. When a lodged code isn't listed
here, look it up in the cited source file — do **not** guess.
Field names below are the GOV.UK API / `EpcPropertyData` cert fields. Elmhurst
labels are the entry-tool's on-screen labels.
---
## Property Description
| Cert field | Code → value |
|---|---|
| `property_type` | 0=House, 1=Bungalow, 2=Flat, 3=Maisonette, 4=Park home |
| `built_form` | 1=Detached, 2=Semi-detached, 3=End-terrace, 4=Mid-terrace, 5=Enclosed end-terrace, 6=Enclosed mid-terrace |
| `construction_age_band` (England/Wales) | A=before 1900, B=19001929, C=19301949, D=19501966, E=19671975, F=19761982, G=19831990, H=19911995, I=19962002, J=20032006, K=2007 onwards |
| Storeys | count of distinct floors in `sap_building_parts[].sap_floor_dimensions` |
| Habitable / Heated rooms | `habitable_room_count` / `heated_room_count` |
| Extensions / Rooms in roof | `extensions_count`; rooms-in-roof only if a room-in-roof building part is lodged |
## Dimensions
From `sap_building_parts[].sap_floor_dimensions[]`. Type = `measurement_type`
(1 = Internal). One row per floor (`floor` 0 = ground/Lowest, 1 = 1st, …):
- Floor Area = `total_floor_area`
- Room Height = `room_height`
- Heat Loss Perimeter = `heat_loss_perimeter`
- Party Wall Length = `party_wall_length`
- Heated Basement only if a basement floor is lodged.
## Conservatory
`conservatory_type` 1 = none → unchecked. `has_heated_separate_conservatory`.
## Walls (Main + Party)
**External / Main wall:**
| Cert field | Mapping |
|---|---|
| `wall_construction` | 3 = Solid brick (look up others in source if not 3) |
| Insulation | `wall_insulation_thickness` "NI" or `wall_insulation_type` NONE → **As Built**; genuine retrofit (External/Internal/Filled) → that type |
| `wall_dry_lined` | Y/N → Dry-lining Yes/No |
| `wall_thickness` (mm) | Wall Thickness; `wall_thickness_measured` Y → "Wall Thickness Unknown" unchecked |
| `sap_alternative_wall` | enter as Alternative Wall 1 (`wall_area`, same code mapping) |
**Party wall** — `party_wall_construction` → SAP10 wall code → U-value
(source: `datatypes/epc/domain/mapper.py` `_API_PARTY_WALL_CONSTRUCTION_TO_SAP10`):
| Code | Meaning | U | Elmhurst "Type" |
|---|---|---|---|
| 0 / None | no lodging | — | (cascade default) |
| 1 | Solid masonry / timber / system | **0.0** | **Solid** |
| 2 | Cavity masonry, unfilled | 0.5 | Cavity (unfilled) |
| 3 | Cavity masonry, filled | 0.2 | Cavity (filled) |
| 4 (house) / 5 (flat) | Unable to determine | 0.25 | Unable to determine |
⚠️ Do **not** leave a code-1 party wall as "Unable to determine" — that wrongly
adds ~0.25 × area of heat loss and depresses the Elmhurst score.
## Roofs
| Cert field | Mapping |
|---|---|
| Type | loft access → "PA Pitched (slates/tiles), access to loft"; flat / room-in-roof per description |
| `roof_insulation_location` | 2 = loft (at joists) → Insulation "Joists" |
| `roof_insulation_thickness` | string e.g. "200mm" → Insulation Thickness 200 mm (drives the default U) |
## Floors
| Cert field | Mapping |
|---|---|
| `floor_construction` | 1 = Solid; 4 = Solid (no-insulation variant); a "Suspended"-prefixed type only if genuinely suspended (timber vs not-timber matters for infiltration) |
| Insulation | "no insulation (assumed)" / `floor_insulation` absent → **As built** |
Source: `domain/sap10_calculator/worksheet/heat_transmission.py` floor logic.
## Openings — Windows (RdSAP reduced-data, synthesised)
RdSAP certs carry **no real window geometry**. The engine synthesises it
(`datatypes/epc/domain/mapper.py` `_synthesise_reduced_field_windows`):
```
total_glazing_area = 0.148 × total_floor_area × band_multiplier # _RDSAP20_GLAZING_RATIO = 0.148
split 4-way across orientations (1,3,5,7) = N, E, S, W # _RDSAP20_SYNTH_ORIENTATIONS
each window: width = area/4, height = 1.0 (height=1 so width carries the area)
```
`glazed_area` band multiplier (`_RDSAP20_GLAZED_AREA_BAND_MULTIPLIER`):
| Code | Band | × |
|---|---|---|
| 1 | Normal | 1.00 |
| 2 | More than typical | 1.25 |
| 3 | Less than typical | 0.81 |
| 4 | Much more than typical | 1.51 |
| 5 | Much less than typical | 0.62 |
Per-window fields:
- Glazing Type: from `multiple_glazing_type`; when no explicit install date is
lodged use **"Double with unknown install date"** (don't assert a date band).
⚠️ RdSAP-17.x/18/19 inherit RdSAP-20.0.0 glazing coefficients — the code→date-band
translation across the 17.1 ↔ RdSAP-10 boundary is a known fidelity risk; report the
U-value Elmhurst assigns vs the engine's effective `windows_w_per_k ÷ total area`.
- Frame Type: `pvc_window_frames` true → PVC
- Glazing Gap: `glazing_gap` mm
- Orientation: not lodged — spread evenly N/E/S/W (matches the engine)
- Location: **External wall**
- Draught Proofed: `percent_draughtproofed` (100 → checked)
- U-value / g-value: leave Elmhurst defaults; note them for comparison.
## Openings — Doors
`door_count` (Total), `insulated_door_count` (Insulated), draughtproofed =
`door_count` when `percent_draughtproofed` 100. Engine default uninsulated door
U = 3.0 W/m²K, area 1.85 m² → `doors_w_per_k` ≈ count × 1.85 × 3.0.
## Ventilation & Lighting
- **Ventilation**: open chimneys = `open_fireplaces_count`; flues/passive
vents/flueless gas fires/extract fans = lodged counts (0 if not lodged);
Fixed space cooling = `has_fixed_air_conditioning`. Draught Lobby not in RdSAP
house reduced-data → leave default.
- **Mechanical Ventilation**: `mechanical_ventilation` 0 = natural → unchecked.
- **Air Pressure Test**: RdSAP certs → "Not available" (uses % draughtproofing).
- **Lighting**: SAP-2012 certs lodge **outlets**, not bulbs —
Total bulbs = `fixed_lighting_outlets_count`,
low energy = `low_energy_fixed_lighting_outlets_count`. RdSAP-10's bulb
methodology differs slightly, but lighting is a minor energy term.
## Space Heating
**Main Heating** — `sap_heating.main_heating_details[]`:
- `sap_main_heating_code` is the SAP code. e.g. **409 = High heat retention
storage heaters** (EES path: Electric → Electric → Storage → High heat retention).
- `main_heating_control` is the controls SAP code. e.g. **2404 = Controls for
high heat retention storage heaters** (EES: Storage Radiator Systems → CSD).
- `main_heating_fraction` → Percentage of Heat (1 = 100%).
- `storage_heaters[]`: count + `high_heat_retention` flag → the heater list.
- Only one main system → leave Main Heating 2 empty. PCDF refs 0 unless a PCDF
boiler/control is lodged. Heat Emitter / Flue / Pump Age are wet-system fields —
N/A once a storage-heater code is chosen.
**Secondary** — e.g. "Portable electric heaters (assumed)" → Electric →
Electric → Room Heaters → Panel/convector/radiant (SAP 691), standard-tariff
electricity (fuel 30). `secondary_heating_fraction` default 0.1.
**Community Heating**: None unless community-heating lodged.
**Meters** — `sap_energy_source`:
- `mains_gas` Y/N → "Mains gas supply available" checkbox.
- Electricity meter type from the heating/HW **fuel code** (see below).
- `meter_type`; smart-meter flags if lodged.
### Fuel codes & Economy-7 (CRITICAL — known engine bug)
Table 32 unit costs, p/kWh (`domain/sap10_calculator/tables/table_32.py`):
| Code | Fuel | p/kWh |
|---|---|---|
| 30 | Electricity, standard tariff | 13.19 |
| 31 | 7-hour tariff **low / off-peak** | 5.50 |
| 32 | 7-hour tariff **high** | 15.29 |
| 33 | 10-hour low | 7.50 |
| 34 | 10-hour high | 14.68 |
| 35 | 24-hour heating tariff | 6.61 |
| 38 / 40 | 18-hour high / low | 13.67 / 7.41 |
**`main_fuel_type` / `water_heating_fuel` 29 = off-peak (7-hour) electricity** →
Elmhurst Electricity meter type = **Dual-rate / Economy 7 (7-hour)**, NOT Single.
⚠️ **Known over-rating bug:** the engine prices **100% of off-peak space heating
AND hot water at the 5.50p low rate** (`inputs.space_heating_fuel_cost_gbp_per_kwh`
= 0.055), instead of the SAP **Table 12a high/low split** (a portion at the 15.29p
high rate). This under-costs all-electric Economy-7 dwellings and inflates the SAP
score. Always surface this in the output's "Known divergences". Canonical case:
UPRN 10002468137 — lodged 55, engine 62.
## Water Heating
| Cert field | Mapping |
|---|---|
| `water_heating_code` | 901 = From main heating system (Elmhurst "Boiler Circulator"); **903 = Electric immersion, off-peak → Elmhurst "Water Heater" category** (NOT Boiler Circulator) |
| `water_heating_fuel` | as Fuel codes above (29 = off-peak) |
| `has_hot_water_cylinder` | → "Hot Water Cylinder Present" |
| `cylinder_size` | band: 1=Small, 2=Medium, 3=Large |
| `cylinder_insulation_type` | **1 = factory Foam, 2 = loose Jacket** (source: `cert_to_inputs.py` `_CYLINDER_INSULATION_TYPE_LOOSE_JACKET = 2`) |
| `cylinder_insulation_thickness` | mm (38 mm ≈ factory foam; jackets 80 mm+) |
| `immersion_heating_type` | 1 = single |
- **Community Hot Water**: 0 unless lodged.
- **Solar Water Heating**: `solar_water_heating` Y/N.
- **WWHRS**: ⚠️ `sap_heating.instantaneous_wwhrs` holds **bath/shower ROOM
counts** (ADR-0028: `rooms_with_bath_and_or_shower`, `rooms_with_mixer_shower_no_bath`,
`rooms_with_bath_and_mixer_shower`) — it is **NOT** a heat-recovery device.
Set WWHRS = **No / not present** unless a genuine WWHRS unit is lodged. A
phantom WWHRS recovers heat and wrongly raises the Elmhurst score.
- **FGHRS**: `main_heating_details[].has_fghrs` Y/N (per main heating system).
## New Technologies
- **PV**: `sap_energy_source.photovoltaic_supply``none_or_no_details` → None.
- **Wind**: `wind_turbines_count` 0 → not present (terrain type irrelevant then).
- **Hydro**: 0 unless lodged.
- **Special Features (Appendix Q)**: none unless lodged.
- "Export capable meter" has no effect with no generation.
---
## Source files
| Concern | File |
|---|---|
| API → EpcPropertyData mapper, party-wall & window synthesis | `datatypes/epc/domain/mapper.py` |
| cert → calculator inputs, cylinder insulation, fuel costs | `domain/sap10_calculator/rdsap/cert_to_inputs.py` |
| heat transmission (U-values, floors, party walls) | `domain/sap10_calculator/worksheet/heat_transmission.py` |
| fuel unit costs | `domain/sap10_calculator/tables/table_32.py` |
| EPC fetch by UPRN | `scripts/fetch_real_life_epc_sample.py` |
| EPC client | `infrastructure/epc_client/epc_client_service.py` |

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,189 @@
# Elmhurst RdSAP inputs — UPRN 10002468137 (cert 0215-2818-7357-9703-2145, RdSAP-Schema-17.1)
**Lodged SAP:** 55 **Our engine:** 62 (continuous 62.47) ← compare Elmhurst against **62**
**Known divergences:**
- **Economy-7 fuel-cost bug (see Meters / Water Heating):** the engine prices 100% of off-peak space heating + hot water at the 5.50p low rate (£0.055/kWh) instead of the SAP Table 12a high/low split. Engine total fuel cost **£595.68** vs lodged heating £619 + HW £140 + lighting £39 ≈ **£798**. This under-costing is the lead suspect for the +7 over-rating.
- **Glazing date band (see Windows):** RdSAP-17.1 glazing codes inherit RdSAP-20.0.0 coefficients — record the U-value Elmhurst assigns vs the engine's effective 2.27 W/m²K.
Property: End-terrace house, 1 Foundry Cottages, Heyshott, Midhurst GU29 0DB. All-electric, high-heat-retention storage heaters on Economy 7, solid-brick uninsulated.
---
## Property Description
| Elmhurst field | Value | Notes |
|---|---|---|
| Property type | House | `property_type` 0 |
| Built form | End-Terrace | `built_form` 3 |
| Storeys | 2 | ground + first floor |
| Habitable Rooms | 3 | `habitable_room_count` |
| Heated Habitable Rooms | 3 | `heated_room_count` |
| Date built — Main | **A — before 1900** | `construction_age_band` A (E&W) |
| Extensions / Rooms in Roof | none | `extensions_count` 0; loft, not room-in-roof |
## Dimensions (Type: Internal)
| Floor | Floor Area [m²] | Room Height [m] | Heat Loss Perimeter [m] | Party Wall Length [m] |
|---|---|---|---|---|
| Lowest Floor (ground) | 24.00 | 2.40 | 12.16 | 7.06 |
| 1st Floor | 24.00 | 2.50 | 13.86 | 7.06 |
Total floor area 48 m². Heated Basement: unchecked.
## Conservatory
| Elmhurst field | Value | Notes |
|---|---|---|
| Is there a conservatory? | No (unchecked) | `conservatory_type` 1 = none |
## Walls
**External / Main wall**
| Elmhurst field | Value | Notes |
|---|---|---|
| Type | Solid Brick | `wall_construction` 3 |
| Insulation | As Built | `wall_insulation_thickness` "NI" (none) |
| Dry-lining | No | `wall_dry_lined` N |
| Wall Thickness | 300 mm | `wall_thickness` 300, measured (Unknown unchecked) |
| U-value Known | unchecked | none lodged |
**Alternative Wall 1** — area 3.40 m², Solid Brick, As Built, No dry-lining, 300 mm (`sap_alternative_wall`). Alternative Wall 2: 0.00.
**Party wall**
| Elmhurst field | Value | Notes |
|---|---|---|
| Type | **Solid** (U-value 0.00) | `party_wall_construction` 1 → solid masonry, U=0. **NOT "Unable to determine"** (that would wrongly add ~0.25 × area). Engine `party_walls_w_per_k` = 0.0 ✓ |
## Roofs
| Elmhurst field | Value | Notes |
|---|---|---|
| Type | PA Pitched (slates/tiles), access to loft | loft insulation |
| Insulation | Joists | `roof_insulation_location` 2 |
| Insulation Thickness | **200 mm** | `roof_insulation_thickness` "200mm" (default U ≈ 0.21; engine roof 5.04 W/K) |
| U-value Known | unchecked | |
## Floors
| Elmhurst field | Value | Notes |
|---|---|---|
| Location | Ground floor | |
| Type | **Solid** | `floor_construction` 1 (NOT suspended) |
| Insulation | As built | "no insulation (assumed)" |
| U-value Known | unchecked | engine floor 16.8 W/K |
## Openings — Windows
RdSAP reduced-data: **no real geometry** — synthesised as 4 windows, one per cardinal direction. Total glazing **5.75 m²** (0.148 × 48 × 0.81; `glazed_area` 3 = "less than typical", ×0.81).
Add **4 identical windows**, orientations **North / East / South / West**:
| Elmhurst field | Value | Notes |
|---|---|---|
| Width [m] | 1.44 | area/4 |
| Height [m] | 1.00 | height=1 so width carries area |
| Glazing Type | **Double with unknown install date** | `multiple_glazing_type` 3, no explicit date; engine eff U **2.27 W/m²K** — compare to Elmhurst's default (≈2.70) |
| Frame Type | PVC | `pvc_window_frames` true |
| Glazing Gap | 12 mm | `glazing_gap` 12 |
| Location | **External wall** | not Alternative wall |
| Orientation | N / E / S / W (one each) | not lodged; engine spreads evenly |
| Draught Proofed | ✓ (100%) | `percent_draughtproofed` 100 |
| U-value / g-value | leave Elmhurst default | note for comparison |
## Openings — Doors
| Elmhurst field | Value | Notes |
|---|---|---|
| Total Number of Doors | 2 | `door_count` |
| Number of Insulated Doors | 0 | `insulated_door_count` |
| Number of Draught Proofed Doors | 2 | 100% draughtproofed |
(Engine doors 11.1 W/K = 2 × 1.85 m² × 3.0 default U ✓)
## Ventilation & Lighting
**Ventilation** — all 0 / unchecked: open chimneys 0 (`open_fireplaces_count`), no flues/passive vents/flueless gas fires; Fixed space cooling unchecked (`has_fixed_air_conditioning` false). Draught Lobby: leave default (not in RdSAP house data).
**Mechanical Ventilation** — unchecked (`mechanical_ventilation` 0 = natural).
**Air Pressure Test** — Not available (RdSAP uses % draughtproofing).
**Lighting**
| Elmhurst field | Value | Notes |
|---|---|---|
| Total number of bulbs | 6 | `fixed_lighting_outlets_count` (SAP-2012 lodges outlets, not bulbs) |
| Low energy | 6 (100%) | `low_energy_fixed_lighting_outlets_count`; minor energy term (engine 133 kWh) |
| Incandescents | 0 | |
## Space Heating
**Main Heating 1**
| Elmhurst field | Value | Notes |
|---|---|---|
| Main Heating EES Code | **SEK → SAP 409, High heat retention storage heaters** | EES: Electric → Electric → Storage → High heat retention. `sap_main_heating_code` 409 |
| Main Heating Controls EES | **CSD → SAP 2404, Controls for high heat retention storage heaters** | EES: Storage Radiator Systems → CSD. `main_heating_control` 2404 |
| Percentage of Heat | 100 | `main_heating_fraction` 1 |
| Storage heater list | 6 heaters, all high heat retention | `storage_heaters` 2+2+2 |
| PCDF refs / Compensator | 0 | none lodged |
| Heat Emitter / Flue / Pump Age | N/A | wet-system fields — disabled for storage heaters |
**Main Heating 2** — none (clear "COM"; % heat 0). Only one main system.
**Community Heating** — None.
**Meters** ⚠️
| Elmhurst field | Value | Notes |
|---|---|---|
| Electricity meter type | **Dual-rate / Economy 7 (7-hour)** | NOT Single. `main_fuel_type`/`water_heating_fuel` 29 = off-peak 7-hour. **Engine bug: prices 100% at 5.50p low rate (£0.055/kWh) — should apply SAP Table 12a high/low split (high rate 15.29p)** |
| Mains gas | **unchecked** | `mains_gas` N |
| Electricity / Gas Smart Meter | unchecked | |
**Secondary heating**
| Elmhurst field | Value | Notes |
|---|---|---|
| Is there secondary heating? | Yes | engine 10% fraction |
| Secondary Heating EES Code | REA → SAP 691, Electric Panel/convector/radiant heaters | "Portable electric heaters (assumed)"; EES Electric → Electric → Room Heaters → Panel/convector/radiant. **Standard** tariff (fuel 30, 15.29p) — engine got this rate right |
## Water Heating
| Elmhurst field | Value | Notes |
|---|---|---|
| Water Heating EES Code | **SAP 903, Electric immersion (off-peak)** | Elmhurst "**Water Heater**" category — NOT "Boiler Circulator" (901 = from main system). `water_heating_code` 903 |
| Hot Water Cylinder Present | ✓ checked | `has_hot_water_cylinder` true |
| Cylinder Size | Medium | `cylinder_size` 2 |
| Insulated | **Foam** | `cylinder_insulation_type` 1 = factory foam |
| Insulation Thickness | 38 mm | `cylinder_insulation_thickness` |
| Immersion Heater | Single | `immersion_heating_type` 1 |
(HW also priced at the buggy £0.055/kWh — engine HW 2134 kWh.)
- **Community Hot Water** — PCDF ref 0 (none).
- **Solar Water Heating** — unchecked (`solar_water_heating` N).
- **WWHRS** ⚠️ — **No / not present**. `sap_heating.instantaneous_wwhrs` here = bath/shower ROOM counts (1 bath/shower room + 1 bath-with-mixer), NOT a heat-recovery device (ADR-0028). Do **not** add a Showersave unit — it would wrongly raise the score.
- **FGHRS** — both unchecked (`has_fghrs` N).
## New Technologies
| Elmhurst field | Value | Notes |
|---|---|---|
| Photovoltaic panel | None | no PV lodged |
| Wind turbine present? | No | `wind_turbines_count` 0 |
| Small-Scale Hydro | 0.00 | |
| Special Features (Appendix Q) | none | |
(Export capable meter has no effect with no generation.)
---
## Engine reference values (for cross-check)
| Quantity | Engine value |
|---|---|
| SAP score | **62** (continuous 62.47) |
| Total fuel cost | £595.68 |
| Space heating | 6717.47 kWh/yr (main fuel 6045.73, secondary 671.75) |
| Hot water | 2134.0 kWh/yr |
| Lighting | 133.35 kWh/yr |
| Heat loss (total) | 144.28 W/K — walls 80.98, roof 5.04, floor 16.80, windows 13.07 (eff U 2.27), doors 11.10, party wall 0.0, thermal bridging 17.30 |
| Space heating / HW fuel | £0.055/kWh (off-peak low rate — **see bug note**) |
**Next step:** key these into Elmhurst, then read off Elmhurst's SAP score. If it lands near **55** (lodged) rather than our **62**, the Economy-7 fuel-cost split is confirmed as the over-rating cause.

View file

@ -0,0 +1,349 @@
{
"uprn": 10002468137,
"roofs": [
{
"description": "Pitched, 200 mm loft insulation",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"walls": [
{
"description": "Solid brick, as built, no insulation (assumed)",
"energy_efficiency_rating": 1,
"environmental_efficiency_rating": 1
}
],
"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": 3,
"environmental_efficiency_rating": 3
},
"lighting": {
"description": "Low energy lighting in all fixed outlets",
"energy_efficiency_rating": 5,
"environmental_efficiency_rating": 5
},
"postcode": "GU29 0DB",
"hot_water": {
"description": "Electric immersion, off-peak",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 2
},
"post_town": "MIDHURST",
"built_form": 3,
"created_at": "2017-05-29 21:33:44.000000",
"door_count": 2,
"glazed_area": 3,
"glazing_gap": 12,
"region_code": 14,
"report_type": 2,
"sap_heating": {
"cylinder_size": 2,
"water_heating_code": 903,
"water_heating_fuel": 29,
"instantaneous_wwhrs": {
"rooms_with_bath_and_or_shower": 1,
"rooms_with_mixer_shower_no_bath": 0,
"rooms_with_bath_and_mixer_shower": 1
},
"main_heating_details": [
{
"has_fghrs": "N",
"main_fuel_type": 29,
"storage_heaters": [
{
"index_number": 230013,
"number_of_heaters": 2,
"high_heat_retention": "true"
},
{
"index_number": 230001,
"number_of_heaters": 2,
"high_heat_retention": "true"
},
{
"index_number": 230002,
"number_of_heaters": 2,
"high_heat_retention": "true"
}
],
"heat_emitter_type": 0,
"emitter_temperature": "NA",
"main_heating_number": 1,
"main_heating_control": 2404,
"main_heating_category": 7,
"main_heating_fraction": 1,
"sap_main_heating_code": 409,
"main_heating_data_source": 2
}
],
"immersion_heating_type": 1,
"cylinder_insulation_type": 1,
"has_fixed_air_conditioning": "false",
"cylinder_insulation_thickness": 38
},
"sap_version": 9.92,
"schema_type": "RdSAP-Schema-17.1",
"uprn_source": "Energy Assessor",
"country_code": "EAW",
"main_heating": [
{
"description": "Electric storage heaters",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 2
}
],
"dwelling_type": "End-terrace house",
"language_code": 1,
"property_type": 0,
"address_line_1": "1 Foundry Cottages",
"address_line_2": "Heyshott",
"assessment_type": "RdSAP",
"completion_date": "2017-05-29",
"inspection_date": "2017-05-08",
"extensions_count": 0,
"measurement_type": 1,
"total_floor_area": 48,
"transaction_type": 5,
"conservatory_type": 1,
"heated_room_count": 3,
"pvc_window_frames": "true",
"registration_date": "2017-05-29",
"sap_energy_source": {
"mains_gas": "N",
"meter_type": 1,
"photovoltaic_supply": {
"none_or_no_details": {
"pv_connection": 0,
"percent_roof_area": 0
}
},
"wind_turbines_count": 0,
"wind_turbines_terrain_type": 2
},
"secondary_heating": {
"description": "Portable electric heaters (assumed)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"sap_building_parts": [
{
"identifier": "Main Dwelling",
"wall_dry_lined": "N",
"wall_thickness": 300,
"floor_heat_loss": 7,
"roof_construction": 4,
"wall_construction": 3,
"building_part_number": 1,
"sap_alternative_wall": {
"wall_area": 3.4,
"wall_dry_lined": "N",
"wall_thickness": 300,
"wall_construction": 3,
"wall_insulation_type": 4,
"wall_thickness_measured": "Y",
"wall_insulation_thickness": "NI"
},
"sap_floor_dimensions": [
{
"floor": 0,
"room_height": {
"value": 2.4,
"quantity": "metres"
},
"floor_insulation": 1,
"total_floor_area": {
"value": 24,
"quantity": "square metres"
},
"party_wall_length": {
"value": 7.06,
"quantity": "metres"
},
"floor_construction": 1,
"heat_loss_perimeter": {
"value": 12.16,
"quantity": "metres"
}
},
{
"floor": 1,
"room_height": {
"value": 2.5,
"quantity": "metres"
},
"total_floor_area": {
"value": 24,
"quantity": "square metres"
},
"party_wall_length": {
"value": 7.06,
"quantity": "metres"
},
"heat_loss_perimeter": {
"value": 13.86,
"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": "200mm",
"wall_insulation_thickness": "NI"
}
],
"low_energy_lighting": 100,
"solar_water_heating": "N",
"habitable_room_count": 3,
"heating_cost_current": {
"value": 619,
"currency": "GBP"
},
"insulated_door_count": 0,
"co2_emissions_current": 4.9,
"energy_rating_average": 60,
"energy_rating_current": 55,
"lighting_cost_current": {
"value": 39,
"currency": "GBP"
},
"main_heating_controls": [
{
"description": "Controls for high heat retention storage heaters",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"multiple_glazing_type": 3,
"open_fireplaces_count": 0,
"has_hot_water_cylinder": "true",
"heating_cost_potential": {
"value": 253,
"currency": "GBP"
},
"hot_water_cost_current": {
"value": 140,
"currency": "GBP"
},
"mechanical_ventilation": 0,
"percent_draughtproofed": 100,
"suggested_improvements": [
{
"sequence": 1,
"typical_saving": {
"value": 309,
"currency": "GBP"
},
"indicative_cost": "14,000",
"improvement_type": "Q",
"improvement_details": {
"improvement_number": 7
},
"improvement_category": 5,
"energy_performance_rating": 72,
"environmental_impact_rating": 54
},
{
"sequence": 2,
"typical_saving": {
"value": 39,
"currency": "GBP"
},
"indicative_cost": "6,000",
"improvement_type": "W2",
"improvement_details": {
"improvement_number": 58
},
"improvement_category": 5,
"energy_performance_rating": 74,
"environmental_impact_rating": 58
},
{
"sequence": 3,
"typical_saving": {
"value": 63,
"currency": "GBP"
},
"indicative_cost": "6,000",
"improvement_type": "N",
"improvement_details": {
"improvement_number": 19
},
"improvement_category": 5,
"energy_performance_rating": 77,
"environmental_impact_rating": 64
},
{
"sequence": 4,
"typical_saving": {
"value": 20,
"currency": "GBP"
},
"indicative_cost": "1,000",
"improvement_type": "X",
"improvement_details": {
"improvement_number": 48
},
"improvement_category": 5,
"energy_performance_rating": 78,
"environmental_impact_rating": 66
},
{
"sequence": 5,
"typical_saving": {
"value": 318,
"currency": "GBP"
},
"indicative_cost": "8,000",
"improvement_type": "U",
"improvement_details": {
"improvement_number": 34
},
"improvement_category": 5,
"energy_performance_rating": 93,
"environmental_impact_rating": 78
}
],
"co2_emissions_potential": 1.1,
"energy_rating_potential": 93,
"lighting_cost_potential": {
"value": 39,
"currency": "GBP"
},
"schema_version_original": "LIG-17.1",
"hot_water_cost_potential": {
"value": 74,
"currency": "GBP"
},
"renewable_heat_incentive": {
"water_heating": 1657,
"impact_of_solid_wall_insulation": -3811,
"space_heating_existing_dwelling": 7524
},
"energy_consumption_current": 602,
"has_fixed_air_conditioning": "false",
"multiple_glazed_proportion": 100,
"calculation_software_version": "2.11r11",
"energy_consumption_potential": 136,
"environmental_impact_current": 33,
"fixed_lighting_outlets_count": 6,
"current_energy_efficiency_band": "D",
"environmental_impact_potential": 78,
"has_heated_separate_conservatory": "false",
"potential_energy_efficiency_band": "A",
"co2_emissions_current_per_floor_area": 102,
"low_energy_fixed_lighting_outlets_count": 6
}

View file

@ -0,0 +1,293 @@
{
"uprn": 100020450179,
"roofs": [
{
"description": "Pitched, 200 mm loft insulation",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"walls": [
{
"description": "Cavity wall, filled cavity",
"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": "Fully double glazed",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
},
"lighting": {
"description": "Low energy lighting in 78% of fixed outlets",
"energy_efficiency_rating": 5,
"environmental_efficiency_rating": 5
},
"postcode": "BR5 2TD",
"hot_water": {
"description": "From main system",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
},
"post_town": "ORPINGTON",
"built_form": 2,
"created_at": "2018-10-08 14:02:11.000000",
"door_count": 1,
"glazed_area": 1,
"glazing_gap": "16+",
"region_code": 14,
"report_type": 2,
"sap_heating": {
"cylinder_size": 1,
"water_heating_code": 901,
"water_heating_fuel": 26,
"instantaneous_wwhrs": {
"rooms_with_bath_and_or_shower": 1,
"rooms_with_mixer_shower_no_bath": 0,
"rooms_with_bath_and_mixer_shower": 1
},
"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": 17505
}
],
"immersion_heating_type": "NA",
"has_fixed_air_conditioning": "false"
},
"sap_version": 9.93,
"schema_type": "RdSAP-Schema-18.0",
"uprn_source": "Energy Assessor",
"country_code": "EAW",
"main_heating": [
{
"description": "Boiler and radiators, mains gas",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"dwelling_type": "Semi-detached house",
"language_code": 1,
"property_type": 0,
"address_line_1": "20, Brenchley Road",
"assessment_type": "RdSAP",
"completion_date": "2018-10-08",
"inspection_date": "2018-10-05",
"extensions_count": 0,
"measurement_type": 1,
"total_floor_area": 73,
"transaction_type": 8,
"conservatory_type": 2,
"heated_room_count": 5,
"pvc_window_frames": "true",
"registration_date": "2018-10-08",
"sap_energy_source": {
"mains_gas": "Y",
"meter_type": 2,
"photovoltaic_supply": {
"none_or_no_details": {
"pv_connection": 0,
"percent_roof_area": 0
}
},
"wind_turbines_count": 0,
"wind_turbines_terrain_type": 2
},
"secondary_heating": {
"description": "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": 7,
"roof_construction": 4,
"wall_construction": 4,
"building_part_number": 1,
"sap_floor_dimensions": [
{
"floor": 0,
"room_height": {
"value": 2.5,
"quantity": "metres"
},
"floor_insulation": 1,
"total_floor_area": {
"value": 36.55,
"quantity": "square metres"
},
"party_wall_length": {
"value": 6.48,
"quantity": "metres"
},
"floor_construction": 1,
"heat_loss_perimeter": {
"value": 17.76,
"quantity": "metres"
}
},
{
"floor": 1,
"room_height": {
"value": 2.43,
"quantity": "metres"
},
"total_floor_area": {
"value": 36.55,
"quantity": "square metres"
},
"party_wall_length": {
"value": 6.48,
"quantity": "metres"
},
"heat_loss_perimeter": {
"value": 17.76,
"quantity": "metres"
}
}
],
"wall_insulation_type": 2,
"construction_age_band": "D",
"party_wall_construction": 1,
"wall_thickness_measured": "Y",
"roof_insulation_location": 2,
"roof_insulation_thickness": "200mm",
"wall_insulation_thickness": "NI",
"floor_insulation_thickness": "NI"
}
],
"low_energy_lighting": 78,
"solar_water_heating": "N",
"habitable_room_count": 5,
"heating_cost_current": {
"value": 396,
"currency": "GBP"
},
"insulated_door_count": 0,
"co2_emissions_current": 2.3,
"energy_rating_average": 60,
"energy_rating_current": 73,
"lighting_cost_current": {
"value": 65,
"currency": "GBP"
},
"main_heating_controls": [
{
"description": "Programmer, room thermostat and TRVs",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"multiple_glazing_type": 3,
"open_fireplaces_count": 0,
"has_hot_water_cylinder": "false",
"heating_cost_potential": {
"value": 367,
"currency": "GBP"
},
"hot_water_cost_current": {
"value": 83,
"currency": "GBP"
},
"mechanical_ventilation": 0,
"percent_draughtproofed": 100,
"suggested_improvements": [
{
"sequence": 1,
"typical_saving": {
"value": 29,
"currency": "GBP"
},
"indicative_cost": "6,000",
"improvement_type": "W2",
"improvement_details": {
"improvement_number": 58
},
"improvement_category": 5,
"energy_performance_rating": 74,
"environmental_impact_rating": 73
},
{
"sequence": 2,
"typical_saving": {
"value": 29,
"currency": "GBP"
},
"indicative_cost": "6,000",
"improvement_type": "N",
"improvement_details": {
"improvement_number": 19
},
"improvement_category": 5,
"energy_performance_rating": 75,
"environmental_impact_rating": 75
},
{
"sequence": 3,
"typical_saving": {
"value": 305,
"currency": "GBP"
},
"indicative_cost": "8,000",
"improvement_type": "U",
"improvement_details": {
"improvement_number": 34
},
"improvement_category": 5,
"energy_performance_rating": 87,
"environmental_impact_rating": 86
}
],
"co2_emissions_potential": 1.0,
"energy_rating_potential": 87,
"lighting_cost_potential": {
"value": 65,
"currency": "GBP"
},
"schema_version_original": "LIG-18.0",
"hot_water_cost_potential": {
"value": 54,
"currency": "GBP"
},
"renewable_heat_incentive": {
"water_heating": 1866,
"space_heating_existing_dwelling": 6466
},
"energy_consumption_current": 178,
"has_fixed_air_conditioning": "false",
"multiple_glazed_proportion": 100,
"calculation_software_version": "3.08r07",
"energy_consumption_potential": 75,
"environmental_impact_current": 71,
"fixed_lighting_outlets_count": 9,
"current_energy_efficiency_band": "C",
"environmental_impact_potential": 86,
"has_heated_separate_conservatory": "false",
"potential_energy_efficiency_band": "B",
"co2_emissions_current_per_floor_area": 31,
"low_energy_fixed_lighting_outlets_count": 7
}

View file

@ -0,0 +1,433 @@
{
"uprn": 10092973954,
"roofs": [
{
"description": {
"value": "(other premises above)",
"language": "1"
},
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
}
],
"walls": [
{
"description": {
"value": "Average thermal transmittance 0.17 W/m\u00b2K",
"language": "1"
},
"energy_efficiency_rating": 5,
"environmental_efficiency_rating": 5
}
],
"floors": [
{
"description": {
"value": "Average thermal transmittance 0.13 W/m\u00b2K",
"language": "1"
},
"energy_efficiency_rating": 5,
"environmental_efficiency_rating": 5
}
],
"status": "entered",
"tenure": "ND",
"windows": {
"description": {
"value": "High performance glazing",
"language": "1"
},
"energy_efficiency_rating": 5,
"environmental_efficiency_rating": 5
},
"lighting": {
"description": {
"value": "Low energy lighting in all fixed outlets",
"language": "1"
},
"energy_efficiency_rating": 5,
"environmental_efficiency_rating": 5
},
"postcode": "ME1 3WR",
"data_type": 2,
"hot_water": {
"description": {
"value": "From main system",
"language": "1"
},
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
},
"post_town": "ROCHESTER",
"built_form": 1,
"created_at": "2020-03-12 08:41:35",
"living_area": 23.45,
"orientation": 6,
"region_code": 14,
"report_type": 3,
"sap_heating": {
"water_fuel_type": 1,
"water_heating_code": 901,
"main_heating_details": [
{
"main_fuel_type": 1,
"heat_emitter_type": 1,
"emitter_temperature": 0,
"is_flue_fan_present": "true",
"main_heating_number": 1,
"main_heating_control": 2110,
"is_interlocked_system": "true",
"main_heating_category": 2,
"main_heating_fraction": 1,
"main_heating_flue_type": 2,
"central_heating_pump_age": 2,
"main_heating_data_source": 1,
"main_heating_index_number": 17929,
"has_separate_delayed_start": "false",
"load_or_weather_compensation": 4,
"compensating_controller_index_number": 200004,
"is_central_heating_pump_in_heated_space": "true"
}
],
"has_hot_water_cylinder": "false",
"has_fixed_air_conditioning": "false",
"secondary_heating_category": 1,
"sap_heating_design_water_use": 1
},
"sap_version": 9.92,
"schema_type": "SAP-Schema-17.1",
"uprn_source": "Address Matched",
"country_code": "ENG",
"main_heating": [
{
"description": {
"value": "Boiler and radiators, mains gas",
"language": "1"
},
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"air_tightness": {
"description": {
"value": "Air permeability 2.6 m\u00b3/h.m\u00b2 (as tested)",
"language": "1"
},
"energy_efficiency_rating": 5,
"environmental_efficiency_rating": 5
},
"dwelling_type": {
"value": "Ground-floor flat",
"language": "1"
},
"language_code": 1,
"property_type": 2,
"address_line_1": "Flat 1 William House",
"address_line_2": "22, Keepers Cottage Lane",
"address_line_3": "Wouldham",
"assessment_date": "2020-03-12",
"assessment_type": "SAP",
"completion_date": "2020-03-12",
"inspection_date": "2020-03-12",
"sap_ventilation": {
"psv_count": 0,
"pressure_test": 1,
"wet_rooms_count": 2,
"air_permeability": 2.6,
"open_flues_count": 0,
"ventilation_type": 6,
"extract_fans_count": 0,
"open_fireplaces_count": 0,
"sheltered_sides_count": 1,
"kitchen_duct_fans_count": 0,
"kitchen_room_fans_count": 0,
"kitchen_wall_fans_count": 1,
"flueless_gas_fires_count": 0,
"mechanical_vent_duct_type": 2,
"non_kitchen_duct_fans_count": 0,
"non_kitchen_room_fans_count": 0,
"non_kitchen_wall_fans_count": 1,
"mechanical_ventilation_data_source": 1,
"mechanical_vent_system_index_number": 500229,
"is_mechanical_vent_approved_installer_scheme": "true"
},
"design_water_use": 1,
"sap_data_version": 9.92,
"sap_flat_details": {
"level": 1
},
"total_floor_area": 68,
"transaction_type": 6,
"conservatory_type": 1,
"registration_date": "2020-03-12",
"sap_energy_source": {
"electricity_tariff": 1,
"wind_turbines_count": 0,
"wind_turbine_terrain_type": 2,
"fixed_lighting_outlets_count": 1,
"low_energy_fixed_lighting_outlets_count": 1,
"low_energy_fixed_lighting_outlets_percentage": 100
},
"sap_opening_types": [
{
"name": "Door (1)",
"type": 1,
"u_value": 1.4,
"data_source": 2,
"description": "Data from Manufacturer",
"glazing_type": 1
},
{
"name": "Windows (1)",
"type": 4,
"u_value": 1.4,
"frame_type": 2,
"data_source": 2,
"description": "Data from Manufacturer",
"frame_factor": 0.7,
"glazing_type": 7,
"solar_transmittance": 0.63
}
],
"secondary_heating": {
"description": {
"value": "None",
"language": "1"
},
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"sap_building_parts": [
{
"sap_roofs": [
{
"name": "Exposed Roof",
"u_value": 0,
"roof_type": 2,
"kappa_value": 0,
"total_roof_area": 0
},
{
"name": "Ceiling",
"u_value": 0,
"roof_type": 4,
"kappa_value": 20,
"total_roof_area": 67.84
}
],
"sap_walls": [
{
"name": "Brickwork",
"u_value": 0.18,
"wall_type": 2,
"kappa_value": 18,
"total_wall_area": 13.1,
"is_curtain_walling": "false"
},
{
"name": "Weatherboarding",
"u_value": 0.18,
"wall_type": 2,
"kappa_value": 18,
"total_wall_area": 46.95,
"is_curtain_walling": "false"
},
{
"name": "Sole Plate Detail",
"u_value": 0.18,
"wall_type": 2,
"kappa_value": 18,
"total_wall_area": 3.1,
"is_curtain_walling": "false"
},
{
"name": "Stair Wall",
"u_value": 0.19,
"wall_type": 2,
"kappa_value": 18,
"total_wall_area": 28.39,
"is_curtain_walling": "false"
},
{
"name": "Stud Walls",
"u_value": 0,
"wall_type": 5,
"kappa_value": 9,
"total_wall_area": 125.952
}
],
"identifier": "Main Dwelling",
"overshading": 2,
"sap_openings": [
{
"name": 1,
"type": "Door (1)",
"width": 1,
"height": 2,
"location": "Stair Wall",
"orientation": 6
},
{
"name": 2,
"type": "Windows (1)",
"width": 1,
"height": 5.92,
"location": "Brickwork",
"orientation": 8
},
{
"name": 3,
"type": "Windows (1)",
"width": 1,
"height": 1.45,
"location": "Brickwork",
"orientation": 2
},
{
"name": 4,
"type": "Windows (1)",
"width": 1,
"height": 1.45,
"location": "Brickwork",
"orientation": 6
},
{
"name": 5,
"type": "Windows (1)",
"width": 1,
"height": 5.28,
"location": "Weatherboarding",
"orientation": 2
},
{
"name": 6,
"type": "Windows (1)",
"width": 1,
"height": 1.5,
"location": "Weatherboarding",
"orientation": 4
}
],
"construction_year": 2020,
"sap_thermal_bridges": {
"thermal_bridges": [
{
"length": 9.79,
"psi_value": 0.3,
"psi_value_source": 2,
"thermal_bridge_type": "E2"
},
{
"length": 3.18,
"psi_value": 0.04,
"psi_value_source": 2,
"thermal_bridge_type": "E3"
},
{
"length": 23.7,
"psi_value": 0.05,
"psi_value_source": 2,
"thermal_bridge_type": "E4"
},
{
"length": 38.14,
"psi_value": 0.16,
"psi_value_source": 2,
"thermal_bridge_type": "E5"
},
{
"length": 38.14,
"psi_value": 0.07,
"psi_value_source": 2,
"thermal_bridge_type": "E7"
},
{
"length": 21.6,
"psi_value": 0.09,
"psi_value_source": 2,
"thermal_bridge_type": "E16"
},
{
"length": 12,
"psi_value": -0.09,
"psi_value_source": 2,
"thermal_bridge_type": "E17"
}
],
"thermal_bridge_code": 5
},
"building_part_number": 1,
"sap_floor_dimensions": [
{
"storey": 0,
"u_value": 0.13,
"floor_type": 2,
"kappa_value": 75,
"storey_height": 2.4,
"heat_loss_area": 67.84,
"total_floor_area": 67.84
}
]
}
],
"heating_cost_current": {
"value": 201,
"currency": "GBP"
},
"co2_emissions_current": 1.1,
"energy_rating_average": 60,
"energy_rating_current": 83,
"lighting_cost_current": {
"value": 55,
"currency": "GBP"
},
"main_heating_controls": [
{
"description": {
"value": "Time and temperature zone control",
"language": "1"
},
"energy_efficiency_rating": 5,
"environmental_efficiency_rating": 5
}
],
"has_hot_water_cylinder": "false",
"heating_cost_potential": {
"value": 201,
"currency": "GBP"
},
"hot_water_cost_current": {
"value": 68,
"currency": "GBP"
},
"co2_emissions_potential": 1.1,
"energy_rating_potential": 83,
"lighting_cost_potential": {
"value": 55,
"currency": "GBP"
},
"schema_version_original": "LIG-17.0",
"hot_water_cost_potential": {
"value": 68,
"currency": "GBP"
},
"is_in_smoke_control_area": "unknown",
"renewable_heat_incentive": {
"rhi_new_dwelling": {
"space_heating": 2140,
"water_heating": 1522
}
},
"seller_commission_report": "Y",
"energy_consumption_current": 91,
"has_fixed_air_conditioning": "false",
"multiple_glazed_percentage": 100,
"calculation_software_version": "Version: 1.0.4.25",
"energy_consumption_potential": 91,
"environmental_impact_current": 86,
"current_energy_efficiency_band": "B",
"environmental_impact_potential": 86,
"has_heated_separate_conservatory": "false",
"potential_energy_efficiency_band": "B",
"co2_emissions_current_per_floor_area": 16
}

View file

@ -10,9 +10,11 @@ raises only on a missing *required* field.
"""
from dataclasses import dataclass
from typing import Union
@dataclass
class SapSchema17_1:
# Stub — slice 1 will grow this to parse the real cert's identity fields.
uprn: int
schema_type: str
total_floor_area: Union[int, float]

View file

@ -0,0 +1,199 @@
"""Real-cert accuracy pins for the SAP 10 calculator.
Each case is a real certificate captured one-off from the GOV.UK EPB
register and frozen as a JSON sample under
`backend/epc_api/json_samples/real_life_examples/<name>/epc.json`. The test feeds that
sample through the *real* call path `EpcClientService` (with `httpx`
mocked, so no network and so the JSON can be hand-tweaked to probe
cases) `EpcPropertyDataMapper` `EpcPropertyData`
`Sap10Calculator` and pins `SapResult` fields against expected
values confirmed by a business domain expert.
This complements the curated Elmhurst cohort (`worksheet/
test_e2e_elmhurst_sap_score.py`): that pins against U985 worksheet PDFs
via in-code `build_epc()` fixtures; this exercises the raw-API front
end (the mapper) on real lodged certs.
Per `[[feedback-e2e-validation-philosophy]]`: pins are exact (ints) or
`abs=1e-4` (floats). A failing pin is a calculator bug to fix, not a
tolerance to relax. Start each case with the expert-confirmed
`sap_score`; add per-end-use kWh pins as each is validated against a
worksheet.
"""
import json
import pathlib
from dataclasses import dataclass
from typing import Final, Optional
from unittest.mock import patch
import httpx
import pytest
from domain.sap10_calculator.calculator import Sap10Calculator
from infrastructure.epc_client.epc_client_service import EpcClientService
_SAMPLES_DIR: Final[pathlib.Path] = pathlib.Path(
"backend/epc_api/json_samples/real_life_examples"
)
@dataclass(frozen=True)
class RealCertExpectation:
"""Expert-confirmed expected `SapResult` values for one real cert.
Samples are bucketed by `schema` so the tree stays legible as cases
grow: `_SAMPLES_DIR / schema / sample / epc.json`. `cert_num` is the
certificate number the cert is fetched by (cosmetic here the mocked
client ignores it but kept for traceability). Float fields are
`None` until validated against a worksheet, so a case can land with
just the `sap_score` and grow over time.
`unsupported_schema=True` marks a cert whose schema the mapper can't
yet consume (full SAP vs RdSAP). Those cases are expected to FAIL at
the mapper until support lands see `test_real_cert_sap_score`.
"""
schema: str
sample: str
cert_num: str
sap_score: int
space_heating_kwh_per_yr: Optional[float] = None
main_heating_fuel_kwh_per_yr: Optional[float] = None
hot_water_kwh_per_yr: Optional[float] = None
co2_kg_per_yr: Optional[float] = None
unsupported_schema: bool = False
# Absolute tolerance for float pins — matches the Elmhurst cohort.
_FLOAT_PIN_ABS: Final[float] = 1e-4
_EXPECTATIONS: Final[tuple[RealCertExpectation, ...]] = (
# UPRN 100020450179 → cert 9543-2865-6207-9208-6301. RdSAP-Schema-18.0,
# band C, lodged 2018-10-08. Lodged `energy_rating_current` = 73; our
# calculator reproduces 73 exactly. kWh pins to be added once confirmed
# against a worksheet with the domain expert.
RealCertExpectation(
schema="RdSAP-Schema-18.0",
sample="uprn_100020450179",
cert_num="9543-2865-6207-9208-6301",
sap_score=73,
),
# UPRN 10092973954 → cert 0862-3892-7875-2690-2325. SAP-Schema-17.1 —
# a FULL-SAP cert (new-build/on-construction), NOT RdSAP. The mapper
# only supports RdSAP schemas, so the chain raises `Unsupported EPC
# schema` today. Kept as a strict-xfail boundary case: lodged rating
# is 83, and when full-SAP mapper support lands this xfail flips to a
# failure prompting us to fill in the real expected score.
RealCertExpectation(
schema="SAP-Schema-17.1",
sample="uprn_10092973954",
cert_num="0862-3892-7875-2690-2325",
sap_score=83,
unsupported_schema=True,
),
)
def _as_param(exp: RealCertExpectation) -> object:
"""Wrap a case as a pytest param, marking unsupported-schema certs as
strict xfails (they raise `ValueError` at the mapper until full-SAP
support exists; strict so the marker can't silently outlive the gap)."""
marks = (
[
pytest.mark.xfail(
reason="full-SAP (non-RdSAP) schema not yet supported by the mapper",
raises=ValueError,
strict=True,
)
]
if exp.unsupported_schema
else []
)
return pytest.param(exp, id=exp.sample, marks=marks)
# Cases that can actually be mapped + calculated — used by the kWh-pin
# test, which has nothing to assert for an unmappable cert.
_MAPPABLE: Final[tuple[RealCertExpectation, ...]] = tuple(
e for e in _EXPECTATIONS if not e.unsupported_schema
)
def _mock_certificate_response(cert_data: dict[str, object]) -> httpx.Response:
"""A 200 from `/api/certificate` wrapping `cert_data` as the register
does (payload under the `data` key)."""
return httpx.Response(
200,
json={"data": cert_data},
request=httpx.Request("GET", "https://example.test/api/certificate"),
)
def _load_cert(exp: RealCertExpectation) -> dict[str, object]:
return json.loads(
(_SAMPLES_DIR / exp.schema / exp.sample / "epc.json").read_text()
)
@pytest.mark.integration
@pytest.mark.parametrize("exp", [_as_param(e) for e in _EXPECTATIONS])
def test_real_cert_sap_score(exp: RealCertExpectation) -> None:
"""SAP score for a real lodged cert matches the expert-confirmed value.
Full real chain: `get_by_certificate_number` mapper
`EpcPropertyData` `Sap10Calculator.calculate`, with `httpx` mocked
so the frozen sample stands in for the live register. Unsupported-
schema cases are strict xfails they raise at the mapper.
"""
# Arrange — frozen real-API sample, served through the mocked client
cert_data = _load_cert(exp)
service = EpcClientService(auth_token="test-token")
# Act
with patch("httpx.get", return_value=_mock_certificate_response(cert_data)):
epc = service.get_by_certificate_number(exp.cert_num)
result = Sap10Calculator().calculate(epc)
# Assert — exact pin; no tolerance widening
assert result.sap_score == exp.sap_score, (
f"{exp.sample}: sap_score actual={result.sap_score}, "
f"expected={exp.sap_score}"
)
@pytest.mark.integration
@pytest.mark.parametrize("exp", [_as_param(e) for e in _MAPPABLE])
def test_real_cert_end_use_kwh_pins(exp: RealCertExpectation) -> None:
"""Per-end-use kWh pins for fields the expert has confirmed.
Skips fields still set to `None` (not yet worksheet-validated), so a
case contributes pins incrementally as it matures. Once every float
field on a case is populated this stops skipping for that case.
"""
# Arrange
cert_data = _load_cert(exp)
service = EpcClientService(auth_token="test-token")
float_fields = (
"space_heating_kwh_per_yr",
"main_heating_fuel_kwh_per_yr",
"hot_water_kwh_per_yr",
"co2_kg_per_yr",
)
confirmed = {f: getattr(exp, f) for f in float_fields if getattr(exp, f) is not None}
if not confirmed:
pytest.skip(f"{exp.sample}: no worksheet-confirmed kWh pins yet")
# Act
with patch("httpx.get", return_value=_mock_certificate_response(cert_data)):
epc = service.get_by_certificate_number(exp.cert_num)
result = Sap10Calculator().calculate(epc)
# Assert
for field_name, expected in confirmed.items():
actual = getattr(result, field_name)
assert actual == pytest.approx(expected, abs=_FLOAT_PIN_ABS), (
f"{exp.sample}.{field_name}: actual={actual}, expected={expected}, "
f"diff={abs(actual - expected):.4f}"
)