mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
Merge remote-tracking branch 'origin/main' into feature/historic-epc-repository
This commit is contained in:
commit
ab245de68d
245 changed files with 27388 additions and 1658 deletions
|
|
@ -16,6 +16,35 @@ Ask for **portfolio_id** and **scenario_id** if the user didn't give them.
|
|||
`scenario_id` is optional — without it, each Property's *default* plan (the one
|
||||
shown in the FE) is audited.
|
||||
|
||||
## Query safety (READ FIRST — a bad query can take the shared DB down)
|
||||
|
||||
The `recommendation` table is **~26m rows and has NO index on `plan_id`** (only
|
||||
`id` and `property_id`). Any query that reaches `recommendation` through
|
||||
`plan_id` — a `JOIN ... ON r.plan_id = pl.id`, or a correlated
|
||||
`EXISTS (SELECT 1 FROM recommendation WHERE plan_id = pl.id ...)` — forces a full
|
||||
seq-scan of all 26m rows. On a large portfolio (e.g. 796/1268 has ~32k default
|
||||
plans) the planner picks that seq-scan **even when the query also joins
|
||||
`property_id`**. This is what blocked the DB during the portfolio-796 audit.
|
||||
|
||||
Rules for EVERY query you write or run here (the runner already follows them):
|
||||
|
||||
- **Confirm with the user before running any ad-hoc SQL** that touches
|
||||
`recommendation`, and show them the `EXPLAIN` plan. Never improvise a
|
||||
`recommendation` query against the live DB unprompted.
|
||||
- **`EXPLAIN` first** (no `ANALYZE` — it executes nothing). If the plan contains
|
||||
`Seq Scan on recommendation`, do NOT run it. Rewrite, or add the index below.
|
||||
- **Never reach `recommendation` via `plan_id` or a correlated subquery.** Scope
|
||||
to the portfolio's properties and reach `recommendation` via the indexed
|
||||
`property_id`, and prefer ONE scoped aggregate over per-property subqueries.
|
||||
- The runner sets a **`statement_timeout` (120s)** on its connection as a hard
|
||||
ceiling, and the `recommendation` rollup is **opt-in** (see Phase 1). Keep any
|
||||
ad-hoc connection equally bounded.
|
||||
- **The real fix is an index:** `CREATE INDEX CONCURRENTLY
|
||||
idx_recommendation_plan_id ON recommendation (plan_id)` (non-blocking build).
|
||||
Until it exists, the solar checks can't run cheaply on a large portfolio — that
|
||||
is a deliberate trade-off, not a bug. Propose it as a migration if the solar
|
||||
checks are needed portfolio-wide.
|
||||
|
||||
## Phase 1 — Build the dataset
|
||||
|
||||
```
|
||||
|
|
@ -26,6 +55,16 @@ Writes `modelling_audit.md` (grouped, ranked by severity) and
|
|||
`modelling_audit.csv` (every flagged row: property_id, uprn, severity, check,
|
||||
detail). Read the printed summary and `modelling_audit.md`.
|
||||
|
||||
**Recommendation checks are OFF by default.** The two solar checks
|
||||
(`excessive-solar-sap`, `low-solar-bill-savings`) read the `recommendation`
|
||||
rollup, which is the only query that scans the 26m-row table. They are gated
|
||||
behind `--with-recommendations`, which is EXPLAIN-guarded and will **abort** if
|
||||
the plan seq-scans `recommendation` (raising `RecommendationScanError`). Without
|
||||
the flag those two checks are inert and every other check is bounded by the
|
||||
portfolio — this is the safe default. Only pass `--with-recommendations` once
|
||||
`idx_recommendation_plan_id` exists (or on a small portfolio where EXPLAIN is
|
||||
clean), and confirm with the user first.
|
||||
|
||||
## Phase 2 — Review the high-level results
|
||||
|
||||
For each check group, HIGH severity first: note the count, read a few example
|
||||
|
|
@ -53,6 +92,13 @@ group, cluster the flagged properties by a distinguishing trait — EPC source
|
|||
(lodged / predicted), `rebaseline_reason`, property type, the dominant measure,
|
||||
fuel — using SQL against the DB. Report the sub-classes and their sizes.
|
||||
|
||||
Traits from `property` / `property_baseline_performance` / `plan` are cheap to
|
||||
cluster on. **Traits that need `recommendation` (dominant measure, measure mix)
|
||||
are NOT** — obey the Query-safety rules above: scope to the *flagged* property
|
||||
ids only (a few hundred, so the `property_id` index is used), `EXPLAIN` first,
|
||||
and confirm the query with the user. Never cluster the whole portfolio's
|
||||
measures in one `recommendation` query.
|
||||
|
||||
## Phase 5 — Cross-reference open work
|
||||
|
||||
Before proposing fixes, check whether one is already in flight:
|
||||
|
|
@ -114,9 +160,27 @@ durable, compounding output of every audit.
|
|||
## Notes
|
||||
|
||||
- Read-only on the DB. `run_modelling_e2e` is a dry run.
|
||||
- **Audit the default plan only** — every check, and every characterisation
|
||||
query, must filter `plan.is_default = TRUE` (the FE-shown plan). The runner's
|
||||
`_QUERY`/`_ROLLUP_QUERY` already do; keep ad-hoc SQL consistent or the counts
|
||||
mix superseded plans into the picture.
|
||||
- **The stored default plan can be STALE vs the live model** — the persisted plan
|
||||
is the output of an *earlier* modelling run; `run_modelling_e2e` re-models
|
||||
against current logic + live EPC/solar. A stored-vs-live gap is the single
|
||||
biggest driver of MEDIUM/HIGH anomalies on a not-yet-re-modelled portfolio, and
|
||||
it spans more checks than the one below names. The fix is operational
|
||||
(**re-model the portfolio, then re-audit**), not a code change — confirm a
|
||||
sample with `run_modelling_e2e` before debugging the calculator.
|
||||
- **Expected, not bugs** (until the override-aware-rebaseline + persistence-fidelity
|
||||
PR deploys and the portfolio is re-modelled): much of `zero-works-post-differs`
|
||||
and `plan-score-below-baseline` is the pre-fix baseline-vs-plan divergence and
|
||||
should shrink after re-model — note it, don't re-debug it.
|
||||
work — see `docs/adr/` — is re-modelled into the portfolio): much of
|
||||
`zero-works-post-differs`, `plan-score-below-baseline`, `already-meets-goal-with-works`,
|
||||
and the non-fuel-switch slice of `negative-bill-savings` is the same stale
|
||||
stored-plan-vs-live divergence and should shrink after re-model — note it,
|
||||
don't re-debug it. Confirm a sample's stored plan differs from `run_modelling_e2e`
|
||||
(stored `air_source_heat_pump` for tens of £k where the live plan is a cheap
|
||||
profitable `solar_pv`; stored works on a property the live model leaves at £0
|
||||
because it already meets goal). The `negative-bill` certs that DO carry an
|
||||
`air_source_heat_pump` are a genuine gas→electric fuel-switch (bills rise at
|
||||
current price ratios) — expected by design.
|
||||
- Adding a check is one decorated `(PropertyAudit) -> Optional[str]` function in
|
||||
`scripts/audit/anomalies.py`; see its module docstring.
|
||||
|
|
|
|||
|
|
@ -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_
|
||||
|
|
|
|||
281
.claude/skills/find-weird-recommendations/SKILL.md
Normal file
281
.claude/skills/find-weird-recommendations/SKILL.md
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
---
|
||||
name: find-weird-recommendations
|
||||
description: Hunt for dubious *recommendations* in a modelled portfolio — measures that were wrongly withheld (SAP could have moved to the goal band but didn't), measures that contradict the dwelling (e.g. HHRSH on community heating), and neighbours of the same type/postcode that were modelled differently. Deterministic scan → neighbour scan → deep-dive with the live re-model → root-cause → codify. Use when the tech team flags "weird" or "dubious" recommendations on a portfolio and wants to understand why the engine did it. Asks for portfolio id and scenario id.
|
||||
---
|
||||
|
||||
# Find weird recommendations
|
||||
|
||||
Sibling to `audit-ara-portfolio`. That skill audits **baselines, plans and SAP
|
||||
scores**; this one audits the **recommendations themselves** — *why a measure was
|
||||
or wasn't offered*. It exists because the tech team keeps finding individual
|
||||
dodgy recommendations (portfolio 814: Khalim's HHRSH-on-community-heating,
|
||||
missing-HHRSH, missing-secondary-heating-removal, and a neighbour split) and we
|
||||
want a repeatable way to turn "this one looks wrong" into a root cause and a
|
||||
durable check that makes the engine better.
|
||||
|
||||
The engine's job on an unlimited-budget scenario is to move each dwelling to the
|
||||
goal band (usually C) with a sensible, physically-valid set of measures.
|
||||
"Weird" = the engine did something a competent surveyor wouldn't: **left SAP on
|
||||
the table**, **recommended a measure the dwelling can't take**, or **treated two
|
||||
identical neighbours differently**. Each has a deterministic tell; this skill
|
||||
runs the tells, then uses the live re-model to explain each hit.
|
||||
|
||||
## Input
|
||||
|
||||
Ask for **portfolio_id** and **scenario_id** if the user didn't give them. The
|
||||
scenario matters here: its `goal_value` is the band we expect the plan to reach,
|
||||
and its `budget` decides whether a shortfall is a bug or just "the money ran
|
||||
out". Without a scenario_id the deterministic scan audits each Property's
|
||||
*default* plan (the FE-shown one).
|
||||
|
||||
## Query safety (READ FIRST — inherited from `audit-ara-portfolio`)
|
||||
|
||||
The `recommendation` table is **~26m rows with NO index on `plan_id`**. Any query
|
||||
reaching it via `plan_id` (a `JOIN ... ON r.plan_id = pl.id`, or a correlated
|
||||
`EXISTS`) forces a full seq-scan and can take the shared DB down — this is what
|
||||
blocked the portfolio-796 audit. The full rules are in the `audit-ara-portfolio`
|
||||
skill; the ones that bite *here*:
|
||||
|
||||
- **Confirm any ad-hoc `recommendation` SQL with the user, and show the
|
||||
`EXPLAIN` plan first** (no `ANALYZE`). If it contains `Seq Scan on
|
||||
recommendation`, do NOT run it.
|
||||
- **Reach `recommendation` only via the indexed `property_id`, scoped to a
|
||||
handful of flagged property ids** — never portfolio-wide, never via `plan_id`.
|
||||
- The two scripts below are safe by construction: `anomalies.py`'s recommendation
|
||||
rollup is opt-in + EXPLAIN-gated, and `neighbour_divergence.py` never touches
|
||||
`recommendation` at all. The measure-set comparison that Khalim's neighbour
|
||||
case needs is done in the **deep-dive** (Phase 4) with the live re-model, not a
|
||||
portfolio-wide `recommendation` query.
|
||||
|
||||
## Phase 1 — Deterministic recommendation scan
|
||||
|
||||
```
|
||||
python -m scripts.audit.anomalies --portfolio <portfolio_id> --scenario <scenario_id>
|
||||
```
|
||||
|
||||
Writes `modelling_audit.md` / `.csv`. The check that carries this skill:
|
||||
|
||||
- **`plan-stops-short-of-goal`** (HIGH) — the default plan ends *below* the goal
|
||||
band on an **unlimited-budget** scenario. This is the deterministic worklist
|
||||
for the "why didn't this get recommended X" class (Khalim's pid 742121). With
|
||||
no budget cap, the only reasons to fall short are (a) a measure was wrongly
|
||||
withheld — a bug — or (b) the dwelling genuinely can't reach the band. Phase 4
|
||||
decides which. Budget-capped scenarios are excluded (falling short is expected).
|
||||
**Caveat: this check is noisy on flat-heavy portfolios** — it fired 322/338 on
|
||||
portfolio 814 (every flat a band short of B). Use `plan-stuck-in-low-band` to
|
||||
cut straight to the real hits.
|
||||
- **`plan-stuck-in-low-band`** (HIGH) — the plan leaves the dwelling **still
|
||||
rated E/F/G with the band unchanged** from baseline (unlimited budget). This is
|
||||
the sharp "E→E, no big movement" worklist — the genuinely-trapped tail, not
|
||||
every dwelling a band short of goal. On portfolio 814 it flagged just **4**
|
||||
(pids 742121, 742210, 742265, 742347) vs `plan-stops-short-of-goal`'s 322. A
|
||||
wrongly-withheld measure or a physically-trapped dwelling both surface here;
|
||||
Phase 4 tells them apart. Band-keyed (no SAP-points threshold), and the
|
||||
strictly-worse case is left to `plan-below-baseline-band` so the two partition.
|
||||
|
||||
Also read `plan-below-baseline-band`, `already-meets-goal-with-works`, and — if
|
||||
you pass `--with-recommendations` (EXPLAIN-gated; confirm with the user first) —
|
||||
`excessive-solar-sap` / `low-solar-bill-savings`. These bound the "the plan did
|
||||
something odd" surface.
|
||||
|
||||
## Phase 2 — Neighbour-divergence scan
|
||||
|
||||
```
|
||||
python -m scripts.audit.neighbour_divergence --portfolio <portfolio_id>
|
||||
```
|
||||
|
||||
Writes `neighbour_divergence.md` / `.csv`. Groups the portfolio into cohorts of
|
||||
(postcode, property_type, built_form) and flags any member whose **effective
|
||||
baseline SAP** sits `--min-gap` (default 12) points from its cohort median.
|
||||
Near-identical neighbours should model alike; a SAP split is what drives a
|
||||
recommendation split (Khalim's neighbour case). This is the SAP half of the
|
||||
heuristic — cheap and portfolio-wide because it never touches `recommendation`.
|
||||
The **measure-set** half is Phase 4: re-model both neighbours and diff their
|
||||
plans.
|
||||
|
||||
Floor area is reported, not keyed on — discount a genuinely bigger/smaller
|
||||
neighbour before trusting a hit.
|
||||
|
||||
## Phase 3 — Triage the hits
|
||||
|
||||
For each group, HIGH first: note the count, read a few rows, and give a one-line
|
||||
**root-cause hypothesis** + a verdict (real bug / expected / threshold to tune).
|
||||
Cross-check the two scans against each other — a `plan-stops-short-of-goal`
|
||||
property that is *also* a divergent neighbour is a high-value case (the two
|
||||
signals agree).
|
||||
|
||||
## Phase 4 — Deep-dive with the live re-model (the core of this skill)
|
||||
|
||||
Reproduce end-to-end, NO DB writes (never pass `--persist`):
|
||||
|
||||
```
|
||||
python -m scripts.run_modelling_e2e <property_id> [<neighbour_id> ...] --scenario-id <scenario_id>
|
||||
```
|
||||
|
||||
This re-fetches the live EPC + solar and runs the full DDD Modelling stage. It
|
||||
writes three files — read all three:
|
||||
|
||||
- `modelling_e2e.md` — the chosen plan, measure by measure, with post-SAP.
|
||||
- `modelling_e2e_candidates.csv` — **every candidate Option the generators
|
||||
produced, including the ones the Optimiser didn't pick.** This is how you tell
|
||||
a *withheld* measure from a *rejected* one:
|
||||
- Measure **absent from candidates** → a **generator eligibility gate**
|
||||
excluded it. This is the bug surface for "why no HHRSH / no
|
||||
secondary_heating_removal". Open the generator and read its `_*_eligible`
|
||||
predicate against the live EPC.
|
||||
- Measure **present in candidates but not chosen** → the Optimiser judged it
|
||||
not worth it (cost/SAP/goal already met). Usually correct; confirm the goal
|
||||
was reached without it.
|
||||
- `modelling_e2e.csv` — the row-level plan.
|
||||
|
||||
For a neighbour pair, run **both** in one invocation and diff their candidate
|
||||
sets and chosen plans — that is the measure-set comparison Khalim asked for,
|
||||
done safely (no `recommendation` table).
|
||||
|
||||
### Known recommendation-bug patterns (seed catalogue — grew from portfolio 814)
|
||||
|
||||
Match each deep-dive against these before hypothesising a novel cause:
|
||||
|
||||
- **HHRSH offered on a community-heated dwelling** (pids 742174, 742175).
|
||||
**Confirmed bug.** The DDD generator's `_hhr_storage_eligible`
|
||||
([`domain/modelling/generators/heating_recommendation.py`](../../../domain/modelling/generators/heating_recommendation.py))
|
||||
returns eligible when the dwelling is `off_gas` — but a community-heated
|
||||
dwelling is off-gas, and the legacy engine's
|
||||
`is_high_heat_retention_valid` ([`recommendations/HeatingRecommender.py`](../../../recommendations/HeatingRecommender.py))
|
||||
gated on `main_fuel["is_community"]` and refused it. The community-heating
|
||||
guard was **lost in the legacy→DDD translation**. Confirm by checking the EPC's
|
||||
main fuel / heat-network flag, then fix by restoring the gate in
|
||||
`_hhr_storage_eligible` (community heating ⇒ not HHR-eligible). A community
|
||||
network is a shared asset a single dwelling can't rip out for storage heaters.
|
||||
- **Measure withheld → plan stops short of goal** (the "why no HHRSH" class).
|
||||
The dwelling can't reach the goal band, and the expected bundle is *absent from
|
||||
candidates*. Read the generator's eligibility predicate against the live EPC —
|
||||
an over-tight gate (a fuel/description/category condition that shouldn't
|
||||
exclude this dwelling) is the usual cause.
|
||||
- **Measure OFFERED but not selected because it *lowers* SAP** (pid 742121, "why
|
||||
no HHRSH" — re-modelled 2026-07, NOT the stale mains-gas story). This dwelling
|
||||
is an **electric** maisonette (`main_fuel_type=29`), so HHRSH **is** in the
|
||||
candidate set — eligibility passes. But scoring the HHRSH overlay in isolation
|
||||
gives **−6.3 SAP** (54.3 → 48.0): swapping its efficient existing electric
|
||||
system for off-peak storage heaters *worsens* the cost-based SAP, so the
|
||||
Optimiser correctly drops it. Its fabric is already done (double-glazed, walls
|
||||
insulated, no cylinder, no own roof), so the plan is **empty** and it sits at E,
|
||||
unreachable to B — a legitimate £0 result, not a bug. **Lesson: a measure
|
||||
*absent from candidates* is a generator-gate bug; a measure *present but not
|
||||
chosen* is the Optimiser judging it not worth it — score the overlay
|
||||
(`PackageScorer.score(epc, [EpcSimulation(...)])`) before blaming a gate.** The
|
||||
old note here claimed 742121 was a mains-gas maisonette that "correctly gets
|
||||
only lighting" — that was stale; verify fuel against the live EPC every time.
|
||||
- **Community-heated flat stuck low, correctly** (pid 742265, "might need loft
|
||||
insulation?"). A **community-heated** flat (SAP main-heating code 301, category
|
||||
6) on portfolio 814. HHRSH and ASHP are both gated out by `is_heat_network_main`
|
||||
(the topology guard) — right, per the community bug above. Its roof is
|
||||
`"Pitched (vaulted ceiling)"` with insulation lodged as **`ND` (no data)**, so
|
||||
the loft generator withholds insulation: it only fires on a definite `0 mm`
|
||||
(`roof_insulation_thickness != _ROOF_UNINSULATED_MM` → None), and a vaulted
|
||||
ceiling isn't the loft-fallback's target anyway. Its only real lever is IWI
|
||||
(+6.7), not enough to clear E — so it stays E→E, legitimately. **Watch item:**
|
||||
a vaulted-ceiling roof with `ND` insulation gets *no* roof measure at all
|
||||
(neither the sloping-ceiling branch, which matches the literal `"sloping
|
||||
ceiling"`, nor the loft branch, which needs `0 mm`). If several such roofs turn
|
||||
out to be genuinely uninsulated upstream, that's a mapper/gate gap — but `ND`
|
||||
is unknown, not zero, so withholding is defensible for now.
|
||||
- **Missing `secondary_heating_removal`** (pid 742264, "gov site shows secondary
|
||||
heating"). Check whether the live EPC carries a secondary-heating system and
|
||||
whether
|
||||
[`domain/modelling/generators/secondary_heating_recommendation.py`](../../../domain/modelling/generators/secondary_heating_recommendation.py)
|
||||
detects it. A gap between the gov register's secondary-heating field and what
|
||||
our EpcPropertyData mapper carries is a common upstream cause — verify the
|
||||
mapped value, not just the register.
|
||||
- **Neighbour split** — two same-cohort dwellings, one gets a bundle the other
|
||||
doesn't. Almost always traces to a **baseline-input divergence** (a different
|
||||
mapped fuel / heating code / floor area / an override on one and not the
|
||||
other), which then flips a generator's eligibility gate. Diff the two EPCs'
|
||||
mapped inputs first; the recommendation split is downstream of that.
|
||||
|
||||
## Phase 5 — Root-cause and cross-reference open work
|
||||
|
||||
Isolate **where** it goes wrong: a generator eligibility gate, the EPC→
|
||||
EpcPropertyData mapping, an override, or the Optimiser. Then check nothing is
|
||||
already in flight:
|
||||
|
||||
```
|
||||
gh pr list --repo Hestia-Homes/Model --state open
|
||||
gh pr list --repo Hestia-Homes/Model --state all --search "<keyword>"
|
||||
```
|
||||
|
||||
Map each confirmed cause to an existing PR/ADR or flag it as new work. State,
|
||||
per case, the smallest correct fix (usually: tighten/loosen one generator
|
||||
predicate, or fix one mapper field) and which real property ids reproduce it.
|
||||
|
||||
## Phase 6 — Self-improve (the compounding loop)
|
||||
|
||||
When the review confirms a **novel, systematic** recommendation problem, codify
|
||||
it so every future run catches it — same gates as `audit-ara-portfolio` Phase 6:
|
||||
|
||||
1. **Systematic** — reproduced on **≥ 5** properties and root-caused, not a
|
||||
one-off (a single weird property is a ticket, not a check).
|
||||
2. **Not already covered** — no existing check fires on it; no open/merged PR or
|
||||
ADR already addresses the cause.
|
||||
3. **Pressure-tested** — for any threshold/heuristic, run `/grill-me` on the
|
||||
proposed check: false-positive rate on this portfolio? threshold defensible
|
||||
against the real distribution? overlaps an existing check?
|
||||
|
||||
**What to change, smallest first:**
|
||||
- **A per-property tell** → a decorated `(PropertyAudit) -> Optional[str]` check
|
||||
in `scripts/audit/anomalies.py` (docstring records the motivating pids + the
|
||||
one-line cause, like `plan-stops-short-of-goal` does). Extend the
|
||||
`PropertyAudit` bundle + `_QUERY` if it needs a field — keep every query bounded
|
||||
by the portfolio and off the `recommendation` table's `plan_id`.
|
||||
- **A cross-property tell** (a neighbour/cohort pattern) → a detector in
|
||||
`scripts/audit/neighbour_divergence.py`. The obvious next one: promote the
|
||||
measure-set diff into a scripted check that reaches `recommendation` via
|
||||
property-id-scoped, EXPLAIN-gated queries per cohort (kept out of v1 to stay
|
||||
portfolio-wide-safe — do it once the SAP tell's false-positive rate is known).
|
||||
- **This catalogue** → add any newly-confirmed bug pattern to Phase 4's list with
|
||||
its pids and root cause, so the next reviewer starts ahead.
|
||||
|
||||
Commit each codified check on its own referencing the motivating run, then re-run
|
||||
Phases 1–2 to confirm it fires on the motivating cases and nothing surprising
|
||||
else. The check registry — with provenance — is the durable output of every hunt.
|
||||
|
||||
## Notes
|
||||
|
||||
- Read-only on the DB. `run_modelling_e2e` is a dry run — never `--persist`.
|
||||
- **Two engines exist.** Portfolios are modelled by the **DDD** engine
|
||||
(`domain/modelling/generators/…`, what `run_modelling_e2e` runs); the legacy
|
||||
`recommendations/…` engine is the historical reference. When a gate looks
|
||||
wrong, compare the DDD predicate against its legacy counterpart — several
|
||||
portfolio-814 bugs are gates the DDD translation **dropped** (community heating
|
||||
is the confirmed one). The legacy code is the spec of intent, not dead code.
|
||||
- **Stored plan can be STALE vs live.** The persisted default plan is an earlier
|
||||
run's output; `run_modelling_e2e` re-models against current logic + live EPC.
|
||||
A stored-vs-live gap is a big driver of Phase 1 hits and is fixed by
|
||||
re-modelling, not by debugging the calculator — confirm a sample with the
|
||||
re-model before blaming a generator (see `audit-ara-portfolio` Notes).
|
||||
- A `plan-stops-short-of-goal` hit is **not automatically a bug** — the user's
|
||||
own framing: "surely there's a reason, but if a sensible reason we should
|
||||
understand why". The deliverable for a physically-unreachable dwelling is the
|
||||
*explanation* (which measures were tried, why the band is out of reach), not a
|
||||
code change.
|
||||
- **On flat/maisonette-heavy portfolios, `plan-stops-short-of-goal` fires in
|
||||
bulk — mostly by design, not bugs.** Characterise the cohort by property_type
|
||||
and post-band before deep-diving (cheap: `property` + `epc_property` + `plan`,
|
||||
no `recommendation` table). Portfolio 814 (94% flats/maisonettes, goal band B)
|
||||
landed 311/338 short of B, but **every** D-or-worse lander and every £0-works
|
||||
shortfall was a flat/maisonette — all 19 houses reached C+. The structural
|
||||
cause: the heating-decarbonisation levers are gated away from flats —
|
||||
`_ashp_eligible` offers ASHP **only to houses/bungalows** (`_is_house_or_bungalow`),
|
||||
the gas-boiler upgrade needs an existing **wet** boiler (point-of-use gas water
|
||||
heaters like WHC 908 don't qualify — see pid 742121, a mains-gas maisonette at
|
||||
E that correctly gets only lighting), and HHRSH needs electric/off-gas. So a
|
||||
mains-gas or community-heated flat with insulated walls + DG has almost no lever
|
||||
and lands at D/E legitimately. Two takeaways: (1) triage the *houses* short of
|
||||
goal first — those are the real bugs; (2) the flat cohort is a **product**
|
||||
question (should ASHP/communal-heat measures be offered to flats?), not a
|
||||
per-property modelling bug — surface it as such.
|
||||
- **A measure can be SELECTED while *lowering* SAP** if it's a forced companion
|
||||
(pid 742175: `mechanical_ventilation` at **−4.1 SAP**, bundled after
|
||||
`cavity_wall_insulation`). Net plan SAP still rose, but the companion drags it
|
||||
and worsens the bill — worth a look when a plan's post-SAP barely moves or dips
|
||||
(overlaps `plan-score-below-baseline`). Not yet root-caused; note the pids.
|
||||
22
CONTEXT.md
22
CONTEXT.md
|
|
@ -101,6 +101,18 @@ _Avoid_: energy assessment, site survey, field survey, Domna survey, Hestia surv
|
|||
Property data supplied by a landlord that may correct or supplement the public EPC for a single Property; triggers Rebaselining when applied; not applicable when Site Notes are present.
|
||||
_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)**:
|
||||
The RdSAP string sentinels a cert may lodge in place of a numeric value (e.g. `roof_insulation_thickness`). They are data, not noise, and are **not interchangeable**: **`ND`** (Not Defined) — no data; resolve to the construction age band's **as-built** state (roof as-built: A–D uninsulated, E–F limited insulation, G+ insulated). **`NI`** — insulation **present** but thickness unknown (RdSAP 10 §5.11.4: score as 50 mm); never "no insulation". **`AB`** — As Built; resolve by age band like ND. The mapper deliberately passes roof sentinels through raw — the calculator's resolution depends on distinguishing them (ADR-0047). Scoring and eligibility may legitimately resolve the same sentinel differently (`NI`: 50 mm to the U-cascade, not-eligible to the roof generator).
|
||||
_Avoid_: reading `ND` as "no insulation"; reading `NI` as "no insulation" (it means the opposite); normalising sentinels at the mapper boundary; comparing a sentinel string against an int (`"ND" != 0` is always True)
|
||||
|
||||
### Modelling
|
||||
|
||||
**Effective EPC**:
|
||||
|
|
@ -116,11 +128,11 @@ Deterministically translating an **old / reduced-data EPC schema** into the curr
|
|||
_Avoid_: gap-fill (means the neighbour-ML path), reduced-data expansion (overloaded with the calculator's Table-5 step), remapping (the schema-translation part only)
|
||||
|
||||
**Baseline Performance**:
|
||||
A Property's current performance aggregate, holding both Lodged Performance and Effective Performance plus the energy block: delivered kWh **per end use** (heating, hot water, lighting, appliances, cooking, pumps/fans, cooling) and the **annual bill** composed into per-section costs plus a total, produced by **Bill Derivation** from SAP10 Calculation's per-end-use kWh × current Fuel Rates. Persisted as one row (flat typed columns, per-section kWh + cost + total); surfaced as one block in the UI.
|
||||
A Property's current performance aggregate, holding both Lodged Performance and Effective Performance plus the energy block: delivered kWh **per end use** (heating, hot water, lighting, appliances, cooking, pumps/fans, cooling) and the **annual bill** composed into per-section costs plus a total, produced by **Bill Derivation** from SAP10 Calculation's per-end-use kWh × current Fuel Rates. Persisted as one row (flat typed columns, per-section kWh + cost + total); surfaced as one block in the UI. The **Lodged half is optional**: a predicted Property has no lodged record (see Lodged Performance), so its `lodged_*` are `NULL` and only the Effective half + energy block are populated (ADR-0004 amendment, #1361).
|
||||
_Avoid_: baseline predictions, predicted baseline, rebaselined values
|
||||
|
||||
**Lodged Performance**:
|
||||
The SAP / EPC Band / carbon emissions / Primary Energy Intensity recorded on the public EPC (or the Site Notes' as-surveyed values when Site Notes are the source) — unmodified by modelling. The half of Baseline Performance that says "what the government register says about this Property".
|
||||
The SAP / EPC Band / carbon emissions / Primary Energy Intensity recorded on the public EPC (or the Site Notes' as-surveyed values when Site Notes are the source) — unmodified by modelling. The half of Baseline Performance that says "what the government register says about this Property". **Requires a record _of this Property_** — a lodged cert or a Site Notes survey — so it is **absent (`None`/NULL) for a predicted Property**: **EPC Prediction** synthesises the picture from neighbours, copying a comparable's recorded figures, so there is no government-register record of _this_ Property to lodge. Only the Effective half is persisted then (ADR-0004 amendment, #1361). Reading a predicted EPC's recorded fields as Lodged Performance manufactures a phantom — a neighbour's SAP presented as this Property's lodged figure.
|
||||
_Avoid_: original performance, raw EPC values, recorded baseline
|
||||
|
||||
**Effective Performance**:
|
||||
|
|
@ -305,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**:
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -0,0 +1,11 @@
|
|||
version: "3.9"
|
||||
|
||||
services:
|
||||
audit-generator-lambda:
|
||||
build:
|
||||
context: ../../../
|
||||
dockerfile: applications/audit_generator/handler/Dockerfile
|
||||
ports:
|
||||
- "9001:8080"
|
||||
env_file:
|
||||
- ../../../.env
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
#!/usr/bin/env python3
|
||||
import json
|
||||
import requests
|
||||
|
||||
HOST = "localhost"
|
||||
PORT = "9001"
|
||||
|
||||
LAMBDA_URL = f"http://{HOST}:{PORT}/2015-03-31/functions/function/invocations"
|
||||
|
||||
payload = {
|
||||
"Records": [
|
||||
{
|
||||
"messageId": "test-message-id",
|
||||
"body": json.dumps(
|
||||
{
|
||||
"task_id": "00000000-0000-0000-0000-000000000001",
|
||||
"sub_task_id": "00000000-0000-0000-0000-000000000002",
|
||||
"hubspot_deal_id": "500311510234",
|
||||
}
|
||||
),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
response = requests.post(LAMBDA_URL, json=payload)
|
||||
|
||||
print("Status code:", response.status_code)
|
||||
print("Response:")
|
||||
print(response.text)
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Run AuditGeneratorOrchestrator directly, bypassing @subtask_handler.
|
||||
|
||||
Loads credentials from the repo-root .env file so no DB task/subtask rows
|
||||
are needed.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import boto3
|
||||
from dotenv import load_dotenv
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[3]
|
||||
load_dotenv(REPO_ROOT / ".env")
|
||||
|
||||
sys.path.insert(0, str(REPO_ROOT))
|
||||
|
||||
from utilities.logger import setup_logger
|
||||
|
||||
setup_logger()
|
||||
|
||||
from infrastructure.postgres.config import PostgresConfig
|
||||
from infrastructure.postgres.engine import make_engine, make_session
|
||||
from infrastructure.s3.s3_client import S3Client
|
||||
from orchestration.audit_generator_orchestrator import AuditGeneratorOrchestrator
|
||||
from orchestration.audit_generator_unit_of_work import AuditGeneratorUnitOfWork
|
||||
|
||||
HUBSPOT_DEAL_ID = "505712829658"
|
||||
|
||||
boto3_client: Any = boto3.client # type: ignore[attr-defined]
|
||||
s3_client = S3Client(
|
||||
boto_s3_client=boto3_client("s3"),
|
||||
# bucket=os.environ["S3_BUCKET_NAME"],
|
||||
bucket="retrofit-energy-assessments-dev",
|
||||
)
|
||||
|
||||
engine = make_engine(PostgresConfig.from_env(os.environ))
|
||||
|
||||
|
||||
def session_factory() -> Any:
|
||||
return make_session(engine)
|
||||
|
||||
|
||||
def uow_factory() -> AuditGeneratorUnitOfWork:
|
||||
return AuditGeneratorUnitOfWork(session_factory)
|
||||
|
||||
|
||||
print(f"Running AuditGeneratorOrchestrator for deal: {HUBSPOT_DEAL_ID!r}")
|
||||
AuditGeneratorOrchestrator(
|
||||
hubspot_deal_id=HUBSPOT_DEAL_ID,
|
||||
s3_client=s3_client,
|
||||
uow_factory=uow_factory,
|
||||
).run()
|
||||
print("Done")
|
||||
|
|
@ -10,11 +10,22 @@ from applications.landlord_description_overrides.landlord_description_overrides_
|
|||
from domain.epc.property_overrides.built_form_type import BuiltFormType
|
||||
from domain.epc.property_overrides.construction_age_band import ConstructionAgeBand
|
||||
from domain.epc.property_overrides.glazing_type import GlazingType
|
||||
from domain.epc.property_overrides.glazing_mix_guard import glazing_mix_guard
|
||||
from domain.epc.property_overrides.main_fuel_type import MainFuelType
|
||||
from domain.epc.property_overrides.main_fuel_guard import main_fuel_guard
|
||||
from domain.epc.property_overrides.main_heating_system_type import MainHeatingSystemType
|
||||
from domain.epc.property_overrides.main_heating_guard import main_heating_guard
|
||||
from domain.epc.property_overrides.property_type import PropertyType
|
||||
from domain.epc.property_overrides.property_type_guard import property_type_guard
|
||||
from domain.epc.property_overrides.roof_type import RoofType
|
||||
from domain.epc.property_overrides.roof_party_ceiling_guard import (
|
||||
roof_party_ceiling_guard,
|
||||
)
|
||||
from domain.data_transformation.guarded_column_classifier import (
|
||||
GuardedColumnClassifier,
|
||||
)
|
||||
from domain.epc.property_overrides.water_heating_type import WaterHeatingType
|
||||
from domain.epc.property_overrides.water_heating_guard import water_heating_guard
|
||||
from domain.epc.property_overrides.wall_type import WallType
|
||||
from domain.epc.property_overrides.wall_type_construction_dates import (
|
||||
wall_type_construction_date_prompt_hint,
|
||||
|
|
@ -82,8 +93,15 @@ def _build_columns(
|
|||
"property_type": lambda src: ClassifiableColumn(
|
||||
name="property_type",
|
||||
source_column=src,
|
||||
classifier=ChatGptColumnClassifier(
|
||||
chat_gpt, PropertyType, PropertyType.UNKNOWN
|
||||
# The dwelling type is the leading token of the "<type>: <built form>"
|
||||
# split; the deterministic guard claims it so the built-form tail can't
|
||||
# flip the type (the LLM read "Bungalow: EndTerrace" as House), and the
|
||||
# LLM handles varied phrasings (#1376).
|
||||
classifier=GuardedColumnClassifier(
|
||||
guard=property_type_guard,
|
||||
fallback=ChatGptColumnClassifier(
|
||||
chat_gpt, PropertyType, PropertyType.UNKNOWN
|
||||
),
|
||||
),
|
||||
repo=LandlordOverridesRepository[PropertyType](
|
||||
session, LandlordPropertyTypeOverrideRow
|
||||
|
|
@ -115,8 +133,13 @@ def _build_columns(
|
|||
"roof_type": lambda src: ClassifiableColumn(
|
||||
name="roof_type",
|
||||
source_column=src,
|
||||
classifier=ChatGptColumnClassifier(
|
||||
chat_gpt, RoofType, RoofType.UNKNOWN
|
||||
# A party ceiling ("another/same dwelling or premises above") has ~0
|
||||
# heat loss and must never be classified as an external roof; the
|
||||
# deterministic guard resolves those markers and the LLM handles the
|
||||
# rest (#1376).
|
||||
classifier=GuardedColumnClassifier(
|
||||
guard=roof_party_ceiling_guard,
|
||||
fallback=ChatGptColumnClassifier(chat_gpt, RoofType, RoofType.UNKNOWN),
|
||||
),
|
||||
repo=LandlordOverridesRepository[RoofType](
|
||||
session, LandlordRoofTypeOverrideRow
|
||||
|
|
@ -125,8 +148,14 @@ def _build_columns(
|
|||
"main_fuel": lambda src: ClassifiableColumn(
|
||||
name="main_fuel",
|
||||
source_column=src,
|
||||
classifier=ChatGptColumnClassifier(
|
||||
chat_gpt, MainFuelType, MainFuelType.UNKNOWN
|
||||
# An individual wood-logs fuel had no LLM target and was funnelled into
|
||||
# "biomass (community)"; the deterministic guard resolves it and the LLM
|
||||
# handles the rest (#1376).
|
||||
classifier=GuardedColumnClassifier(
|
||||
guard=main_fuel_guard,
|
||||
fallback=ChatGptColumnClassifier(
|
||||
chat_gpt, MainFuelType, MainFuelType.UNKNOWN
|
||||
),
|
||||
),
|
||||
repo=LandlordOverridesRepository[MainFuelType](
|
||||
session, LandlordMainFuelOverrideRow
|
||||
|
|
@ -135,8 +164,15 @@ def _build_columns(
|
|||
"glazing": lambda src: ClassifiableColumn(
|
||||
name="glazing",
|
||||
source_column=src,
|
||||
classifier=ChatGptColumnClassifier(
|
||||
chat_gpt, GlazingType, GlazingType.UNKNOWN
|
||||
# An aggregate glazing mix ("40% double, 60% single") can't be applied
|
||||
# per-window, so the deterministic guard resolves the structured split
|
||||
# to MIXED (no overlay → keep the cert's per-window glazing) and the LLM
|
||||
# handles uniform / varied phrasings (#1376, ADR-0042).
|
||||
classifier=GuardedColumnClassifier(
|
||||
guard=glazing_mix_guard,
|
||||
fallback=ChatGptColumnClassifier(
|
||||
chat_gpt, GlazingType, GlazingType.UNKNOWN
|
||||
),
|
||||
),
|
||||
repo=LandlordOverridesRepository[GlazingType](
|
||||
session, LandlordGlazingOverrideRow
|
||||
|
|
@ -155,8 +191,16 @@ def _build_columns(
|
|||
"water_heating": lambda src: ClassifiableColumn(
|
||||
name="water_heating",
|
||||
source_column=src,
|
||||
classifier=ChatGptColumnClassifier(
|
||||
chat_gpt, WaterHeatingType, WaterHeatingType.UNKNOWN
|
||||
# A biomass / wood / dual-fuel / biodiesel DHW description has no
|
||||
# dedicated LLM target and was funnelled into "house coal"; the
|
||||
# deterministic guard resolves the structured fuels (and the "electric
|
||||
# immersion assumed" no-system case) and the LLM handles the rest
|
||||
# (#1376, ADR-0043).
|
||||
classifier=GuardedColumnClassifier(
|
||||
guard=water_heating_guard,
|
||||
fallback=ChatGptColumnClassifier(
|
||||
chat_gpt, WaterHeatingType, WaterHeatingType.UNKNOWN
|
||||
),
|
||||
),
|
||||
repo=LandlordOverridesRepository[WaterHeatingType](
|
||||
session, LandlordWaterHeatingOverrideRow
|
||||
|
|
@ -165,8 +209,15 @@ def _build_columns(
|
|||
"main_heating_system": lambda src: ClassifiableColumn(
|
||||
name="main_heating_system",
|
||||
source_column=src,
|
||||
classifier=ChatGptColumnClassifier(
|
||||
chat_gpt, MainHeatingSystemType, MainHeatingSystemType.UNKNOWN
|
||||
# High heat retention storage heaters have no LLM target distinct from
|
||||
# old storage, so they were funnelled into "Electric storage heaters,
|
||||
# old" (401); the deterministic guard resolves HHRSH to its own
|
||||
# archetype (SAP 409) and the LLM handles the rest (#1376, ADR-0044).
|
||||
classifier=GuardedColumnClassifier(
|
||||
guard=main_heating_guard,
|
||||
fallback=ChatGptColumnClassifier(
|
||||
chat_gpt, MainHeatingSystemType, MainHeatingSystemType.UNKNOWN
|
||||
),
|
||||
),
|
||||
repo=LandlordOverridesRepository[MainHeatingSystemType](
|
||||
session, LandlordMainHeatingSystemOverrideRow
|
||||
|
|
|
|||
|
|
@ -108,7 +108,10 @@ from repositories.product.composite_product_repository import (
|
|||
from repositories.property.in_memory_property_overrides_reader import (
|
||||
InMemoryPropertyOverridesReader,
|
||||
)
|
||||
from repositories.property.landlord_override_overlays import overlays_from
|
||||
from repositories.property.landlord_override_overlays import (
|
||||
flag_fuel_mismatch,
|
||||
overlays_from,
|
||||
)
|
||||
from repositories.property.override_backed_prediction_attributes_reader import (
|
||||
OverrideBackedPredictionAttributesReader,
|
||||
)
|
||||
|
|
@ -509,12 +512,11 @@ def handler(
|
|||
# held-open read Session. (The ``finally`` is the safety net.)
|
||||
scenario = ScenarioPostgresRepository(read_session).get_many([scenario_id])[0]
|
||||
products = catalogue_snapshot_with_off_catalogue_overrides(read_session)
|
||||
# Stored Solar is read whatever refetch_solar says — the flag decides
|
||||
# whether a UPRN with no stored row gets a live Google fetch, never
|
||||
# whether solar is modelled at all.
|
||||
stored_solar: dict[int, Optional[dict[str, Any]]] = (
|
||||
{}
|
||||
if not refetch_solar
|
||||
else SolarPostgresRepository(read_session).get_many(
|
||||
list(set(uprns.values()))
|
||||
)
|
||||
SolarPostgresRepository(read_session).get_many(list(set(uprns.values())))
|
||||
)
|
||||
epc_repo = EpcPostgresRepository(read_session)
|
||||
stored_lodged_epcs: dict[int, EpcPropertyData] = (
|
||||
|
|
@ -561,7 +563,9 @@ def handler(
|
|||
epc = (
|
||||
None # no stored lodged EPC; prediction path handles this property
|
||||
)
|
||||
overrides = overlays_from(overrides_reader.overrides_for(pid))
|
||||
resolved_overrides = overrides_reader.overrides_for(pid)
|
||||
flag_fuel_mismatch(resolved_overrides)
|
||||
overrides = overlays_from(resolved_overrides)
|
||||
predicted_epc: Optional[EpcPropertyData] = None
|
||||
predicted_epc_is_new = False
|
||||
|
||||
|
|
@ -610,15 +614,16 @@ def handler(
|
|||
landlord_overrides=overrides,
|
||||
).effective_epc
|
||||
|
||||
solar_insights: Optional[dict[str, Any]]
|
||||
# refetch_solar gates ONLY the paid Google call for UPRNs with no
|
||||
# stored row; stored insights always feed the Modelling stage.
|
||||
# (False here once meant "no solar at all" — the pre-rename
|
||||
# `no_solar` semantics — which silently stripped solar from every
|
||||
# plan in a re-model batch.)
|
||||
solar_insights: Optional[dict[str, Any]] = stored_solar.get(uprn)
|
||||
solar_was_fetched = False
|
||||
if not refetch_solar:
|
||||
solar_insights = None
|
||||
else:
|
||||
solar_insights = stored_solar.get(uprn)
|
||||
if solar_insights is None:
|
||||
solar_insights = _solar_insights_for(solar_client, spatial)
|
||||
solar_was_fetched = solar_insights is not None
|
||||
if solar_insights is None and refetch_solar:
|
||||
solar_insights = _solar_insights_for(solar_client, spatial)
|
||||
solar_was_fetched = solar_insights is not None
|
||||
|
||||
plan = run_modelling(
|
||||
effective_epc,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
@ -23,6 +23,7 @@ class HubspotDealData(SQLModel, table=True):
|
|||
landlord_property_id: Optional[str] = Field(default=None)
|
||||
uprn: Optional[str] = Field(default=None)
|
||||
outcome: Optional[str] = Field(default=None)
|
||||
number_of_attempts: Optional[str] = Field(default=None)
|
||||
outcome_notes: Optional[str] = Field(default=None)
|
||||
booking_status: Optional[str] = Field(default=None)
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,10 @@ from datetime import date
|
|||
import pytest
|
||||
|
||||
from backend.documents_parser.elmhurst_extractor import ElmhurstSiteNotesExtractor
|
||||
from datatypes.epc.domain.epc_property_data import BuildingPartIdentifier, EpcPropertyData
|
||||
from datatypes.epc.domain.epc_property_data import (
|
||||
BuildingPartIdentifier,
|
||||
EpcPropertyData,
|
||||
)
|
||||
from datatypes.epc.domain.mapper import EpcPropertyDataMapper
|
||||
|
||||
FIXTURE_PATH = os.path.join(
|
||||
|
|
@ -179,10 +182,15 @@ class TestBuildingPart:
|
|||
assert len(result.sap_building_parts[0].sap_floor_dimensions) == 1
|
||||
|
||||
def test_floor_dimension_area(self, result: EpcPropertyData) -> None:
|
||||
assert result.sap_building_parts[0].sap_floor_dimensions[0].total_floor_area_m2 == 44.89
|
||||
assert (
|
||||
result.sap_building_parts[0].sap_floor_dimensions[0].total_floor_area_m2
|
||||
== 44.89
|
||||
)
|
||||
|
||||
def test_floor_dimension_room_height(self, result: EpcPropertyData) -> None:
|
||||
assert result.sap_building_parts[0].sap_floor_dimensions[0].room_height_m == 2.24
|
||||
assert (
|
||||
result.sap_building_parts[0].sap_floor_dimensions[0].room_height_m == 2.24
|
||||
)
|
||||
|
||||
def test_floor_dimension_heat_loss_perimeter(self, result: EpcPropertyData) -> None:
|
||||
assert (
|
||||
|
|
@ -245,11 +253,17 @@ class TestWindows:
|
|||
|
||||
def test_transmission_solar_transmittance(self, result: EpcPropertyData) -> None:
|
||||
assert result.sap_windows[0].window_transmission_details is not None
|
||||
assert result.sap_windows[0].window_transmission_details.solar_transmittance == 0.72
|
||||
assert (
|
||||
result.sap_windows[0].window_transmission_details.solar_transmittance
|
||||
== 0.72
|
||||
)
|
||||
|
||||
def test_transmission_data_source(self, result: EpcPropertyData) -> None:
|
||||
assert result.sap_windows[0].window_transmission_details is not None
|
||||
assert result.sap_windows[0].window_transmission_details.data_source == "Manufacturer"
|
||||
assert (
|
||||
result.sap_windows[0].window_transmission_details.data_source
|
||||
== "Manufacturer"
|
||||
)
|
||||
|
||||
|
||||
class TestHeating:
|
||||
|
|
@ -308,7 +322,7 @@ class TestHeating:
|
|||
|
||||
class TestEnergySource:
|
||||
def test_mains_gas(self, result: EpcPropertyData) -> None:
|
||||
assert result.sap_energy_source.mains_gas is True
|
||||
assert result.sap_energy_source.gas_connection_available is True
|
||||
|
||||
def test_meter_type(self, result: EpcPropertyData) -> None:
|
||||
assert result.sap_energy_source.meter_type == "Single"
|
||||
|
|
@ -384,5 +398,7 @@ class TestWindowFrameMaterial:
|
|||
|
||||
|
||||
class TestLowEnergyLighting:
|
||||
def test_low_energy_fixed_lighting_bulbs_count(self, result2: EpcPropertyData) -> None:
|
||||
def test_low_energy_fixed_lighting_bulbs_count(
|
||||
self, result2: EpcPropertyData
|
||||
) -> None:
|
||||
assert result2.low_energy_fixed_lighting_bulbs_count == 5
|
||||
|
|
|
|||
|
|
@ -177,7 +177,7 @@ class TestPdfToEpcPropertyData:
|
|||
),
|
||||
],
|
||||
sap_energy_source=SapEnergySource(
|
||||
mains_gas=True,
|
||||
gas_connection_available=True,
|
||||
meter_type="Single",
|
||||
pv_battery_count=0,
|
||||
wind_turbines_count=0,
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,368 @@
|
|||
{
|
||||
"uprn": 100020942571,
|
||||
"roofs": [
|
||||
{
|
||||
"description": "(another dwelling above)",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
}
|
||||
],
|
||||
"walls": [
|
||||
{
|
||||
"description": "System built, with internal insulation",
|
||||
"energy_efficiency_rating": 4,
|
||||
"environmental_efficiency_rating": 4
|
||||
}
|
||||
],
|
||||
"floors": [
|
||||
{
|
||||
"description": "Solid, no insulation (assumed)",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
},
|
||||
{
|
||||
"description": "To external air, no insulation (assumed)",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
}
|
||||
],
|
||||
"status": "entered",
|
||||
"tenure": 1,
|
||||
"window": {
|
||||
"description": "Fully double glazed",
|
||||
"energy_efficiency_rating": 4,
|
||||
"environmental_efficiency_rating": 4
|
||||
},
|
||||
"lighting": {
|
||||
"description": "Low energy lighting in all fixed outlets",
|
||||
"energy_efficiency_rating": 5,
|
||||
"environmental_efficiency_rating": 5
|
||||
},
|
||||
"postcode": "SE28 8BW",
|
||||
"hot_water": {
|
||||
"description": "Gas multipoint",
|
||||
"energy_efficiency_rating": 3,
|
||||
"environmental_efficiency_rating": 3
|
||||
},
|
||||
"post_town": "LONDON",
|
||||
"built_form": 3,
|
||||
"created_at": "2019-02-11 19:55:01.000000",
|
||||
"door_count": 1,
|
||||
"glazed_area": 1,
|
||||
"region_code": 17,
|
||||
"report_type": 2,
|
||||
"sap_heating": {
|
||||
"cylinder_size": 1,
|
||||
"water_heating_code": 908,
|
||||
"water_heating_fuel": 26,
|
||||
"instantaneous_wwhrs": {
|
||||
"rooms_with_bath_and_or_shower": 2,
|
||||
"rooms_with_mixer_shower_no_bath": 0,
|
||||
"rooms_with_bath_and_mixer_shower": 0
|
||||
},
|
||||
"main_heating_details": [
|
||||
{
|
||||
"has_fghrs": "N",
|
||||
"main_fuel_type": 29,
|
||||
"heat_emitter_type": 0,
|
||||
"emitter_temperature": "NA",
|
||||
"main_heating_number": 1,
|
||||
"main_heating_control": 2602,
|
||||
"main_heating_category": 10,
|
||||
"main_heating_fraction": 1,
|
||||
"sap_main_heating_code": 691,
|
||||
"main_heating_data_source": 2
|
||||
}
|
||||
],
|
||||
"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": "Room heaters, electric",
|
||||
"energy_efficiency_rating": 1,
|
||||
"environmental_efficiency_rating": 2
|
||||
}
|
||||
],
|
||||
"dwelling_type": "Ground-floor maisonette",
|
||||
"language_code": 1,
|
||||
"property_type": 3,
|
||||
"address_line_1": "8, Booth Close",
|
||||
"assessment_type": "RdSAP",
|
||||
"completion_date": "2019-02-11",
|
||||
"inspection_date": "2019-02-09",
|
||||
"extensions_count": 1,
|
||||
"measurement_type": 1,
|
||||
"sap_flat_details": {
|
||||
"level": 1,
|
||||
"top_storey": "N",
|
||||
"flat_location": 0,
|
||||
"heat_loss_corridor": 0
|
||||
},
|
||||
"total_floor_area": 78,
|
||||
"transaction_type": 8,
|
||||
"conservatory_type": 1,
|
||||
"heated_room_count": 4,
|
||||
"registration_date": "2019-02-11",
|
||||
"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": 1
|
||||
},
|
||||
"secondary_heating": {
|
||||
"description": "None",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
},
|
||||
"sap_building_parts": [
|
||||
{
|
||||
"identifier": "Main Dwelling",
|
||||
"wall_dry_lined": "N",
|
||||
"wall_thickness": 260,
|
||||
"floor_heat_loss": 7,
|
||||
"roof_construction": 3,
|
||||
"wall_construction": 8,
|
||||
"building_part_number": 1,
|
||||
"sap_floor_dimensions": [
|
||||
{
|
||||
"floor": 0,
|
||||
"room_height": {
|
||||
"value": 2.36,
|
||||
"quantity": "metres"
|
||||
},
|
||||
"floor_insulation": 1,
|
||||
"total_floor_area": {
|
||||
"value": 36.42,
|
||||
"quantity": "square metres"
|
||||
},
|
||||
"party_wall_length": {
|
||||
"value": 5.64,
|
||||
"quantity": "metres"
|
||||
},
|
||||
"floor_construction": 1,
|
||||
"heat_loss_perimeter": {
|
||||
"value": 19.05,
|
||||
"quantity": "metres"
|
||||
}
|
||||
},
|
||||
{
|
||||
"floor": 1,
|
||||
"room_height": {
|
||||
"value": 2.24,
|
||||
"quantity": "metres"
|
||||
},
|
||||
"total_floor_area": {
|
||||
"value": 36.42,
|
||||
"quantity": "square metres"
|
||||
},
|
||||
"party_wall_length": {
|
||||
"value": 5.64,
|
||||
"quantity": "metres"
|
||||
},
|
||||
"heat_loss_perimeter": {
|
||||
"value": 12.95,
|
||||
"quantity": "metres"
|
||||
}
|
||||
}
|
||||
],
|
||||
"wall_insulation_type": 3,
|
||||
"construction_age_band": "E",
|
||||
"party_wall_construction": 0,
|
||||
"wall_thickness_measured": "Y",
|
||||
"roof_insulation_location": "ND",
|
||||
"roof_insulation_thickness": "ND",
|
||||
"wall_insulation_thickness": "NI",
|
||||
"floor_insulation_thickness": "NI"
|
||||
},
|
||||
{
|
||||
"identifier": "Extension 1",
|
||||
"wall_dry_lined": "N",
|
||||
"wall_thickness": 260,
|
||||
"floor_heat_loss": 1,
|
||||
"roof_construction": 3,
|
||||
"wall_construction": 8,
|
||||
"building_part_number": 2,
|
||||
"sap_floor_dimensions": [
|
||||
{
|
||||
"floor": 0,
|
||||
"room_height": {
|
||||
"value": 2.24,
|
||||
"quantity": "metres"
|
||||
},
|
||||
"floor_insulation": 1,
|
||||
"total_floor_area": {
|
||||
"value": 5,
|
||||
"quantity": "square metres"
|
||||
},
|
||||
"party_wall_length": {
|
||||
"value": 0.82,
|
||||
"quantity": "metres"
|
||||
},
|
||||
"floor_construction": 1,
|
||||
"heat_loss_perimeter": {
|
||||
"value": 6.82,
|
||||
"quantity": "metres"
|
||||
}
|
||||
}
|
||||
],
|
||||
"wall_insulation_type": 3,
|
||||
"construction_age_band": "E",
|
||||
"party_wall_construction": 0,
|
||||
"wall_thickness_measured": "Y",
|
||||
"roof_insulation_location": "ND",
|
||||
"roof_insulation_thickness": "ND",
|
||||
"wall_insulation_thickness": "NI",
|
||||
"floor_insulation_thickness": "NI"
|
||||
}
|
||||
],
|
||||
"low_energy_lighting": 100,
|
||||
"solar_water_heating": "N",
|
||||
"habitable_room_count": 4,
|
||||
"heating_cost_current": {
|
||||
"value": 944,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"insulated_door_count": 0,
|
||||
"co2_emissions_current": 3.3,
|
||||
"energy_rating_average": 60,
|
||||
"energy_rating_current": 50,
|
||||
"lighting_cost_current": {
|
||||
"value": 60,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"main_heating_controls": [
|
||||
{
|
||||
"description": "Appliance thermostats",
|
||||
"energy_efficiency_rating": 4,
|
||||
"environmental_efficiency_rating": 4
|
||||
}
|
||||
],
|
||||
"multiple_glazing_type": 2,
|
||||
"open_fireplaces_count": 0,
|
||||
"has_hot_water_cylinder": "false",
|
||||
"heating_cost_potential": {
|
||||
"value": 297,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"hot_water_cost_current": {
|
||||
"value": 92,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"mechanical_ventilation": 0,
|
||||
"percent_draughtproofed": 100,
|
||||
"suggested_improvements": [
|
||||
{
|
||||
"sequence": 1,
|
||||
"typical_saving": {
|
||||
"value": 122,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"indicative_cost": "6,000",
|
||||
"improvement_type": "W2",
|
||||
"improvement_details": {
|
||||
"improvement_number": 58
|
||||
},
|
||||
"improvement_category": 5,
|
||||
"energy_performance_rating": 55,
|
||||
"environmental_impact_rating": 63
|
||||
},
|
||||
{
|
||||
"sequence": 2,
|
||||
"typical_saving": {
|
||||
"value": 519,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"indicative_cost": "7,000",
|
||||
"improvement_type": "T",
|
||||
"improvement_details": {
|
||||
"improvement_number": 29
|
||||
},
|
||||
"improvement_category": 5,
|
||||
"energy_performance_rating": 77,
|
||||
"environmental_impact_rating": 77
|
||||
}
|
||||
],
|
||||
"co2_emissions_potential": 1.8,
|
||||
"energy_rating_potential": 77,
|
||||
"lighting_cost_potential": {
|
||||
"value": 60,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"schema_version_original": "LIG-18.0",
|
||||
"alternative_improvements": [
|
||||
{
|
||||
"sequence": 1,
|
||||
"typical_saving": {
|
||||
"value": 515,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"improvement_type": "J2",
|
||||
"improvement_details": {
|
||||
"improvement_number": 54
|
||||
},
|
||||
"improvement_category": 6,
|
||||
"energy_performance_rating": 78,
|
||||
"environmental_impact_rating": 96
|
||||
},
|
||||
{
|
||||
"sequence": 2,
|
||||
"typical_saving": {
|
||||
"value": 413,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"improvement_type": "Z1",
|
||||
"improvement_details": {
|
||||
"improvement_number": 51
|
||||
},
|
||||
"improvement_category": 6,
|
||||
"energy_performance_rating": 77,
|
||||
"environmental_impact_rating": 79
|
||||
},
|
||||
{
|
||||
"sequence": 3,
|
||||
"typical_saving": {
|
||||
"value": 500,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"improvement_type": "Z3",
|
||||
"improvement_details": {
|
||||
"improvement_number": 53
|
||||
},
|
||||
"improvement_category": 6,
|
||||
"energy_performance_rating": 76,
|
||||
"environmental_impact_rating": 73
|
||||
}
|
||||
],
|
||||
"hot_water_cost_potential": {
|
||||
"value": 97,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"renewable_heat_incentive": {
|
||||
"water_heating": 1519,
|
||||
"space_heating_existing_dwelling": 5050
|
||||
},
|
||||
"energy_consumption_current": 250,
|
||||
"has_fixed_air_conditioning": "false",
|
||||
"multiple_glazed_proportion": 100,
|
||||
"calculation_software_version": "3.08r09",
|
||||
"energy_consumption_potential": 134,
|
||||
"environmental_impact_current": 58,
|
||||
"fixed_lighting_outlets_count": 6,
|
||||
"current_energy_efficiency_band": "E",
|
||||
"environmental_impact_potential": 77,
|
||||
"has_heated_separate_conservatory": "false",
|
||||
"potential_energy_efficiency_band": "C",
|
||||
"co2_emissions_current_per_floor_area": 43,
|
||||
"low_energy_fixed_lighting_outlets_count": 6
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -361,7 +361,7 @@ class PhotovoltaicArray:
|
|||
|
||||
@dataclass
|
||||
class SapEnergySource:
|
||||
mains_gas: bool
|
||||
gas_connection_available: bool
|
||||
meter_type: str # int in API, str (e.g. "Single") in site notes
|
||||
pv_battery_count: int
|
||||
wind_turbines_count: int
|
||||
|
|
|
|||
|
|
@ -1,3 +1,21 @@
|
|||
from typing import Optional
|
||||
|
||||
from datatypes.epc.domain.epc_property_data import MainHeatingDetail
|
||||
|
||||
PROPERTY_TYPE_LOOKUP = {0: "House", 1: "Bungalow", 2: "Flat", 3: "Maisonette"}
|
||||
ROOF_CONSTRUCTION_LOOKUP = {}
|
||||
ROOF_INSULATION_LOCATION_LOOKUP = {}
|
||||
|
||||
HEAT_NETWORK_MAIN_CODES: frozenset[int] = frozenset({301, 302, 303, 304})
|
||||
HEAT_NETWORK_CATEGORY: int = 6
|
||||
|
||||
|
||||
def is_heat_network_main(main: Optional[MainHeatingDetail]) -> bool:
|
||||
"""True when the dwelling's main heating is connected to a shared heat
|
||||
network — by SAP Table 4a code (301–304) or by main_heating_category 6."""
|
||||
if main is None:
|
||||
return False
|
||||
code = main.sap_main_heating_code
|
||||
if isinstance(code, int) and code in HEAT_NETWORK_MAIN_CODES:
|
||||
return True
|
||||
return main.main_heating_category == HEAT_NETWORK_CATEGORY
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -201,8 +201,14 @@ class TestFromRdSapSchema20_0_0:
|
|||
# dict-tolerant array reader captures the arrays.
|
||||
data = load("20_0_0.json")
|
||||
data["sap_energy_source"]["photovoltaic_supply"] = [
|
||||
[{"pitch": 2, "peak_power": {"value": 1.14, "quantity": "kW"},
|
||||
"orientation": 5, "overshading": 1}],
|
||||
[
|
||||
{
|
||||
"pitch": 2,
|
||||
"peak_power": {"value": 1.14, "quantity": "kW"},
|
||||
"orientation": 5,
|
||||
"overshading": 1,
|
||||
}
|
||||
],
|
||||
[{"pitch": 2, "peak_power": 1.14, "orientation": 5, "overshading": 1}],
|
||||
]
|
||||
schema = from_dict(RdSapSchema20_0_0, data)
|
||||
|
|
@ -263,18 +269,30 @@ class TestFromRdSapSchema21_0_0:
|
|||
# the only one those certs omit is roof_insulation_thickness.)
|
||||
data = load("21_0_0.json")
|
||||
for window in data.get("sap_windows", []):
|
||||
for k in ("frame_factor", "glazing_gap", "pvc_frame",
|
||||
"window_transmission_details"):
|
||||
for k in (
|
||||
"frame_factor",
|
||||
"glazing_gap",
|
||||
"pvc_frame",
|
||||
"window_transmission_details",
|
||||
):
|
||||
window.pop(k, None)
|
||||
for k in ("wet_rooms_count", "open_chimneys_count",
|
||||
"pressure_test_certificate_number", "windows_transmission_details",
|
||||
"mechanical_ventilation_index_number", "mechanical_vent_duct_type",
|
||||
"mechanical_vent_duct_placement", "mechanical_vent_duct_insulation",
|
||||
"mechanical_vent_duct_insulation_level",
|
||||
"mechanical_vent_measured_installation", "insulated_door_u_value",
|
||||
"low_energy_fixed_lighting_bulbs_count",
|
||||
"cfl_fixed_lighting_bulbs_count", "led_fixed_lighting_bulbs_count",
|
||||
"suggested_improvements"):
|
||||
for k in (
|
||||
"wet_rooms_count",
|
||||
"open_chimneys_count",
|
||||
"pressure_test_certificate_number",
|
||||
"windows_transmission_details",
|
||||
"mechanical_ventilation_index_number",
|
||||
"mechanical_vent_duct_type",
|
||||
"mechanical_vent_duct_placement",
|
||||
"mechanical_vent_duct_insulation",
|
||||
"mechanical_vent_duct_insulation_level",
|
||||
"mechanical_vent_measured_installation",
|
||||
"insulated_door_u_value",
|
||||
"low_energy_fixed_lighting_bulbs_count",
|
||||
"cfl_fixed_lighting_bulbs_count",
|
||||
"led_fixed_lighting_bulbs_count",
|
||||
"suggested_improvements",
|
||||
):
|
||||
data.pop(k, None)
|
||||
data["sap_energy_source"].pop("wind_turbine_details", None)
|
||||
data["sap_energy_source"].pop("pv_battery_count", None)
|
||||
|
|
@ -440,7 +458,9 @@ class TestFromRdSapSchema21_0_1:
|
|||
|
||||
assert result.open_chimneys_count == 0
|
||||
|
||||
def test_omitted_cfl_fixed_lighting_bulbs_count_defaults_to_zero_not_none(self) -> None:
|
||||
def test_omitted_cfl_fixed_lighting_bulbs_count_defaults_to_zero_not_none(
|
||||
self,
|
||||
) -> None:
|
||||
data = load("21_0_1.json")
|
||||
data.pop("cfl_fixed_lighting_bulbs_count", None)
|
||||
schema = from_dict(RdSapSchema21_0_1, data)
|
||||
|
|
@ -449,7 +469,9 @@ class TestFromRdSapSchema21_0_1:
|
|||
|
||||
assert result.cfl_fixed_lighting_bulbs_count == 0
|
||||
|
||||
def test_omitted_led_fixed_lighting_bulbs_count_defaults_to_zero_not_none(self) -> None:
|
||||
def test_omitted_led_fixed_lighting_bulbs_count_defaults_to_zero_not_none(
|
||||
self,
|
||||
) -> None:
|
||||
data = load("21_0_1.json")
|
||||
data.pop("led_fixed_lighting_bulbs_count", None)
|
||||
schema = from_dict(RdSapSchema21_0_1, data)
|
||||
|
|
@ -542,7 +564,8 @@ class TestFromRdSapSchema21_0_1:
|
|||
schema,
|
||||
sap_building_parts=[
|
||||
dataclasses.replace(
|
||||
bps[0], roof_insulation_location=1,
|
||||
bps[0],
|
||||
roof_insulation_location=1,
|
||||
rafter_insulation_thickness="225mm",
|
||||
),
|
||||
*bps[1:],
|
||||
|
|
@ -555,9 +578,7 @@ class TestFromRdSapSchema21_0_1:
|
|||
# Assert
|
||||
assert result.sap_building_parts[0].rafter_insulation_thickness == "225mm"
|
||||
|
||||
def test_cylinder_heat_loss_threaded(
|
||||
self, schema: RdSapSchema21_0_1
|
||||
) -> None:
|
||||
def test_cylinder_heat_loss_threaded(self, schema: RdSapSchema21_0_1) -> None:
|
||||
# Arrange — the gov API lodges the manufacturer's declared cylinder
|
||||
# loss factor (kWh/day) in `sap_heating.cylinder_heat_loss` (SAP
|
||||
# 10.2 §4 branch a). Previously undeclared → `from_dict` dropped it
|
||||
|
|
@ -656,7 +677,9 @@ class TestFromRdSapSchema21_0_1:
|
|||
|
||||
def test_lighting_element_description(self, result: EpcPropertyData) -> None:
|
||||
assert result.lighting is not None
|
||||
assert result.lighting.description == "Low energy lighting in 50% of fixed outlets"
|
||||
assert (
|
||||
result.lighting.description == "Low energy lighting in 50% of fixed outlets"
|
||||
)
|
||||
|
||||
def test_hot_water_element_description(self, result: EpcPropertyData) -> None:
|
||||
assert result.hot_water is not None
|
||||
|
|
@ -676,7 +699,7 @@ class TestFromRdSapSchema21_0_1:
|
|||
|
||||
def test_mains_gas(self, result: EpcPropertyData) -> None:
|
||||
# mains_gas: "Y"
|
||||
assert result.sap_energy_source.mains_gas is True
|
||||
assert result.sap_energy_source.gas_connection_available is True
|
||||
|
||||
def test_electricity_smart_meter(self, result: EpcPropertyData) -> None:
|
||||
# electricity_smart_meter_present: "true"
|
||||
|
|
@ -784,16 +807,27 @@ class TestFromRdSapSchema21_0_1:
|
|||
assert len(result.sap_building_parts[0].sap_floor_dimensions) == 1
|
||||
|
||||
def test_floor_area(self, result: EpcPropertyData) -> None:
|
||||
assert result.sap_building_parts[0].sap_floor_dimensions[0].total_floor_area_m2 == 45.82
|
||||
assert (
|
||||
result.sap_building_parts[0].sap_floor_dimensions[0].total_floor_area_m2
|
||||
== 45.82
|
||||
)
|
||||
|
||||
def test_floor_height(self, result: EpcPropertyData) -> None:
|
||||
assert result.sap_building_parts[0].sap_floor_dimensions[0].room_height_m == 2.45
|
||||
assert (
|
||||
result.sap_building_parts[0].sap_floor_dimensions[0].room_height_m == 2.45
|
||||
)
|
||||
|
||||
def test_heat_loss_perimeter(self, result: EpcPropertyData) -> None:
|
||||
assert result.sap_building_parts[0].sap_floor_dimensions[0].heat_loss_perimeter_m == 19.5
|
||||
assert (
|
||||
result.sap_building_parts[0].sap_floor_dimensions[0].heat_loss_perimeter_m
|
||||
== 19.5
|
||||
)
|
||||
|
||||
def test_party_wall_length(self, result: EpcPropertyData) -> None:
|
||||
assert result.sap_building_parts[0].sap_floor_dimensions[0].party_wall_length_m == 7.9
|
||||
assert (
|
||||
result.sap_building_parts[0].sap_floor_dimensions[0].party_wall_length_m
|
||||
== 7.9
|
||||
)
|
||||
|
||||
# --- room-in-roof (sap_room_in_roof.room_in_roof_type_1) ---
|
||||
|
||||
|
|
@ -899,7 +933,6 @@ class TestFromRdSapSchema21_0_1:
|
|||
assert rhi.impact_of_solid_wall_insulation_kwh == -3560.0
|
||||
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Measured wall insulation thickness (`wall_insulation_thickness == "measured"`)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -915,7 +948,9 @@ class TestApiWallConstructionCode:
|
|||
|
||||
def test_gov_api_cob_code_9_remaps_to_wall_cob_7(self) -> None:
|
||||
# Arrange
|
||||
from datatypes.epc.domain.mapper import _api_wall_construction_code # pyright: ignore[reportPrivateUsage]
|
||||
from datatypes.epc.domain.mapper import (
|
||||
_api_wall_construction_code,
|
||||
) # pyright: ignore[reportPrivateUsage]
|
||||
|
||||
# Act
|
||||
result = _api_wall_construction_code(9)
|
||||
|
|
@ -927,7 +962,9 @@ class TestApiWallConstructionCode:
|
|||
# Arrange — codes 1-5 already align; gov 8 (system built) is left
|
||||
# for u_wall to resolve (calc WALL_PARK_HOME=8 is never dispatched);
|
||||
# gov 6 (basement) is left to the basement machinery.
|
||||
from datatypes.epc.domain.mapper import _api_wall_construction_code # pyright: ignore[reportPrivateUsage]
|
||||
from datatypes.epc.domain.mapper import (
|
||||
_api_wall_construction_code,
|
||||
) # pyright: ignore[reportPrivateUsage]
|
||||
|
||||
# Act / Assert
|
||||
for code in (1, 2, 3, 4, 5, 6, 8):
|
||||
|
|
@ -1015,9 +1052,7 @@ class TestApiResolveSlopingCeilingThickness:
|
|||
|
||||
# Act — code 8, no loft-joist thickness, age B (pre-1950), but the
|
||||
# sloping ceiling carries a lodged 100 mm.
|
||||
resolved: object = _api_resolve_sloping_ceiling_thickness(
|
||||
8, None, "B", "100mm"
|
||||
)
|
||||
resolved: object = _api_resolve_sloping_ceiling_thickness(8, None, "B", "100mm")
|
||||
|
||||
# Assert — the lodged sloping-ceiling thickness wins over the
|
||||
# pre-1950 None → 0 mm fallback.
|
||||
|
|
@ -1032,9 +1067,7 @@ class TestApiResolveSlopingCeilingThickness:
|
|||
)
|
||||
|
||||
# Act — code 8, no thickness anywhere, pre-1950 age.
|
||||
resolved: object = _api_resolve_sloping_ceiling_thickness(
|
||||
8, None, "C", None
|
||||
)
|
||||
resolved: object = _api_resolve_sloping_ceiling_thickness(8, None, "C", None)
|
||||
|
||||
# Assert — existing Slice 57 behaviour preserved: 0 mm (U=2.30).
|
||||
assert resolved == 0
|
||||
|
|
@ -1047,9 +1080,7 @@ class TestApiResolveSlopingCeilingThickness:
|
|||
|
||||
# Act — code 8, age B (pre-1950), sloping lodged "AB" (As Built —
|
||||
# categorical, NOT a measured thickness).
|
||||
resolved: object = _api_resolve_sloping_ceiling_thickness(
|
||||
8, None, "B", "AB"
|
||||
)
|
||||
resolved: object = _api_resolve_sloping_ceiling_thickness(8, None, "B", "AB")
|
||||
|
||||
# Assert — "AB" is not a numeric thickness, so it must NOT win; the
|
||||
# Slice 57 pre-1950 None → 0 mm (U=2.30) rule still applies.
|
||||
|
|
@ -1088,9 +1119,7 @@ class TestElmhurstGlazingTypeWrappedGap:
|
|||
from datatypes.epc.domain.mapper import _elmhurst_glazing_type_code
|
||||
|
||||
# Act
|
||||
code = _elmhurst_glazing_type_code(
|
||||
"Double between 2002 and 2021 16 mm or"
|
||||
)
|
||||
code = _elmhurst_glazing_type_code("Double between 2002 and 2021 16 mm or")
|
||||
|
||||
# Assert — clean "Double between 2002 and 2021" → SAP10 code 3
|
||||
assert code == 3
|
||||
|
|
@ -1100,9 +1129,7 @@ class TestElmhurstGlazingTypeWrappedGap:
|
|||
from datatypes.epc.domain.mapper import _elmhurst_glazing_type_code
|
||||
|
||||
# Act
|
||||
code = _elmhurst_glazing_type_code(
|
||||
"Double between 2002 and 2021 16 mm or 1st"
|
||||
)
|
||||
code = _elmhurst_glazing_type_code("Double between 2002 and 2021 16 mm or 1st")
|
||||
|
||||
# Assert
|
||||
assert code == 3
|
||||
|
|
@ -1129,7 +1156,9 @@ class TestApiFloorTypeCode:
|
|||
|
||||
def test_code_6_maps_to_another_dwelling_below(self) -> None:
|
||||
# Arrange
|
||||
from datatypes.epc.domain.mapper import _api_floor_type_str # pyright: ignore[reportPrivateUsage]
|
||||
from datatypes.epc.domain.mapper import (
|
||||
_api_floor_type_str,
|
||||
) # pyright: ignore[reportPrivateUsage]
|
||||
|
||||
# Act
|
||||
result = _api_floor_type_str(6)
|
||||
|
|
@ -1140,7 +1169,9 @@ class TestApiFloorTypeCode:
|
|||
def test_code_7_still_maps_to_ground_floor(self) -> None:
|
||||
# Arrange — regression guard: the ground-floor signal the §5 (12)
|
||||
# suspended-timber rule keys on is unchanged.
|
||||
from datatypes.epc.domain.mapper import _api_floor_type_str # pyright: ignore[reportPrivateUsage]
|
||||
from datatypes.epc.domain.mapper import (
|
||||
_api_floor_type_str,
|
||||
) # pyright: ignore[reportPrivateUsage]
|
||||
|
||||
# Act / Assert
|
||||
assert _api_floor_type_str(7) == "Ground floor"
|
||||
|
|
@ -1158,7 +1189,9 @@ class TestApiFloorTypeCode:
|
|||
# (consumed by heat_transmission's party-floor override); it is
|
||||
# != "Ground floor", so the §5 (12) suspended-timber rule stays
|
||||
# inert. Pre-this, code 8 raised UnmappedApiCode, blocking the cert.
|
||||
from datatypes.epc.domain.mapper import _api_floor_type_str # pyright: ignore[reportPrivateUsage]
|
||||
from datatypes.epc.domain.mapper import (
|
||||
_api_floor_type_str,
|
||||
) # pyright: ignore[reportPrivateUsage]
|
||||
|
||||
# Act / Assert — no-heat-loss signal (not None, not "Ground floor").
|
||||
assert _api_floor_type_str(8) == "(another dwelling below)"
|
||||
|
|
@ -1171,7 +1204,9 @@ class TestApiFloorTypeCode:
|
|||
# constant U=0.7. The string is != "Ground floor" / "(another
|
||||
# dwelling below)", so it is inert metadata; the U-routing is driven
|
||||
# by the `is_above_partially_heated_space` floor-dimension flag.
|
||||
from datatypes.epc.domain.mapper import _api_floor_type_str # pyright: ignore[reportPrivateUsage]
|
||||
from datatypes.epc.domain.mapper import (
|
||||
_api_floor_type_str,
|
||||
) # pyright: ignore[reportPrivateUsage]
|
||||
|
||||
# Act / Assert
|
||||
assert _api_floor_type_str(3) == "(other premises below)"
|
||||
|
|
@ -1182,7 +1217,9 @@ class TestApiFloorTypeCode:
|
|||
# only, so the cascade routes that floor to U=0.7 (§5.14) and the
|
||||
# heat-transmission step keeps its area even on a flat whose
|
||||
# dwelling-level exposure defaults has_exposed_floor=False.
|
||||
from datatypes.epc.domain.mapper import _api_build_sap_floor_dimensions # pyright: ignore[reportPrivateUsage]
|
||||
from datatypes.epc.domain.mapper import (
|
||||
_api_build_sap_floor_dimensions,
|
||||
) # pyright: ignore[reportPrivateUsage]
|
||||
from datatypes.epc.schema.rdsap_schema_21_0_1 import (
|
||||
SapFloorDimension as ApiSapFloorDimension,
|
||||
)
|
||||
|
|
@ -1217,7 +1254,9 @@ class TestApiFloorConstructionCode:
|
|||
|
||||
def test_code_3_maps_to_suspended_not_timber(self) -> None:
|
||||
# Arrange
|
||||
from datatypes.epc.domain.mapper import _api_floor_construction_str # pyright: ignore[reportPrivateUsage]
|
||||
from datatypes.epc.domain.mapper import (
|
||||
_api_floor_construction_str,
|
||||
) # pyright: ignore[reportPrivateUsage]
|
||||
|
||||
# Act
|
||||
result = _api_floor_construction_str(3)
|
||||
|
|
@ -1230,7 +1269,9 @@ class TestApiFloorConstructionCode:
|
|||
|
||||
def test_code_2_still_maps_to_suspended_timber(self) -> None:
|
||||
# Arrange — regression guard: the timber code is unchanged.
|
||||
from datatypes.epc.domain.mapper import _api_floor_construction_str # pyright: ignore[reportPrivateUsage]
|
||||
from datatypes.epc.domain.mapper import (
|
||||
_api_floor_construction_str,
|
||||
) # pyright: ignore[reportPrivateUsage]
|
||||
|
||||
# Act
|
||||
result = _api_floor_construction_str(2)
|
||||
|
|
@ -1249,7 +1290,9 @@ class TestApiFloorConstructionCode:
|
|||
# floor_construction. Empirically inert: floor W/K is identical to
|
||||
# any explicit construction string across all observed code-0
|
||||
# certs (the heat loss is governed by floor_heat_loss, not this).
|
||||
from datatypes.epc.domain.mapper import _api_floor_construction_str # pyright: ignore[reportPrivateUsage]
|
||||
from datatypes.epc.domain.mapper import (
|
||||
_api_floor_construction_str,
|
||||
) # pyright: ignore[reportPrivateUsage]
|
||||
|
||||
# Act
|
||||
result = _api_floor_construction_str(0)
|
||||
|
|
@ -1267,7 +1310,10 @@ class TestDefaultMissingPostTown:
|
|||
|
||||
def test_absent_post_town_is_defaulted_to_empty_string(self) -> None:
|
||||
# Arrange
|
||||
from datatypes.epc.domain.mapper import _default_missing_post_town # pyright: ignore[reportPrivateUsage]
|
||||
from datatypes.epc.domain.mapper import (
|
||||
_default_missing_post_town,
|
||||
) # pyright: ignore[reportPrivateUsage]
|
||||
|
||||
doc: dict[str, object] = {"postcode": "EX31 2LE"}
|
||||
|
||||
# Act
|
||||
|
|
@ -1278,7 +1324,10 @@ class TestDefaultMissingPostTown:
|
|||
|
||||
def test_present_post_town_is_untouched(self) -> None:
|
||||
# Arrange — regression guard: a lodged town passes through.
|
||||
from datatypes.epc.domain.mapper import _default_missing_post_town # pyright: ignore[reportPrivateUsage]
|
||||
from datatypes.epc.domain.mapper import (
|
||||
_default_missing_post_town,
|
||||
) # pyright: ignore[reportPrivateUsage]
|
||||
|
||||
doc: dict[str, object] = {"post_town": "BARNSTAPLE"}
|
||||
|
||||
# Act
|
||||
|
|
@ -1290,31 +1339,48 @@ 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 # pyright: ignore[reportPrivateUsage]
|
||||
from datatypes.epc.domain.mapper import (
|
||||
_api_roof_construction_str,
|
||||
) # pyright: ignore[reportPrivateUsage]
|
||||
|
||||
# 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
|
||||
from datatypes.epc.domain.mapper import _api_roof_construction_str # pyright: ignore[reportPrivateUsage]
|
||||
from datatypes.epc.domain.mapper import (
|
||||
_api_roof_construction_str,
|
||||
) # pyright: ignore[reportPrivateUsage]
|
||||
|
||||
# Act
|
||||
result = _api_roof_construction_str(6)
|
||||
|
|
@ -1325,7 +1391,9 @@ class TestApiRoofConstructionCode:
|
|||
|
||||
def test_code_8_still_maps_to_sloping_ceiling(self) -> None:
|
||||
# Arrange — regression guard: the sloping-ceiling code is unchanged.
|
||||
from datatypes.epc.domain.mapper import _api_roof_construction_str # pyright: ignore[reportPrivateUsage]
|
||||
from datatypes.epc.domain.mapper import (
|
||||
_api_roof_construction_str,
|
||||
) # pyright: ignore[reportPrivateUsage]
|
||||
|
||||
# Act
|
||||
result = _api_roof_construction_str(8)
|
||||
|
|
@ -1345,7 +1413,9 @@ class TestApiGlazingTransmissionTable24:
|
|||
def test_single_glazing_code_5_is_table_24_u_4p8(self) -> None:
|
||||
# Arrange — RdSAP 21 glazing_type 5 = "single glazing"; Table 24
|
||||
# row "Single / Any period" → U 4.8 (PVC/wooden), g 0.85.
|
||||
from datatypes.epc.domain.mapper import _api_glazing_transmission # pyright: ignore[reportPrivateUsage]
|
||||
from datatypes.epc.domain.mapper import (
|
||||
_api_glazing_transmission,
|
||||
) # pyright: ignore[reportPrivateUsage]
|
||||
|
||||
# Act
|
||||
result = _api_glazing_transmission(5, None)
|
||||
|
|
@ -1356,7 +1426,9 @@ class TestApiGlazingTransmissionTable24:
|
|||
def test_single_glazing_known_data_code_15_is_table_24_u_4p8(self) -> None:
|
||||
# Arrange — code 15 = "single glazing, known data"; same Table 24
|
||||
# Single row when no measured U is lodged on the reduced-data path.
|
||||
from datatypes.epc.domain.mapper import _api_glazing_transmission # pyright: ignore[reportPrivateUsage]
|
||||
from datatypes.epc.domain.mapper import (
|
||||
_api_glazing_transmission,
|
||||
) # pyright: ignore[reportPrivateUsage]
|
||||
|
||||
# Act
|
||||
result = _api_glazing_transmission(15, None)
|
||||
|
|
@ -1367,7 +1439,9 @@ class TestApiGlazingTransmissionTable24:
|
|||
def test_secondary_glazing_normal_emissivity_code_11_is_u_2p9(self) -> None:
|
||||
# Arrange — code 11 = "secondary glazing, normal emissivity";
|
||||
# Table 24 Secondary "Normal emissivity" row → U 2.9, g 0.85.
|
||||
from datatypes.epc.domain.mapper import _api_glazing_transmission # pyright: ignore[reportPrivateUsage]
|
||||
from datatypes.epc.domain.mapper import (
|
||||
_api_glazing_transmission,
|
||||
) # pyright: ignore[reportPrivateUsage]
|
||||
|
||||
# Act
|
||||
result = _api_glazing_transmission(11, None)
|
||||
|
|
@ -1378,7 +1452,9 @@ class TestApiGlazingTransmissionTable24:
|
|||
def test_secondary_glazing_low_emissivity_code_12_is_u_2p2(self) -> None:
|
||||
# Arrange — code 12 = "secondary glazing, low emissivity"; Table 24
|
||||
# Secondary "Low emissivity" row → U 2.2, g 0.85.
|
||||
from datatypes.epc.domain.mapper import _api_glazing_transmission # pyright: ignore[reportPrivateUsage]
|
||||
from datatypes.epc.domain.mapper import (
|
||||
_api_glazing_transmission,
|
||||
) # pyright: ignore[reportPrivateUsage]
|
||||
|
||||
# Act
|
||||
result = _api_glazing_transmission(12, None)
|
||||
|
|
@ -1389,7 +1465,9 @@ class TestApiGlazingTransmissionTable24:
|
|||
def test_triple_glazing_2002_to_2022_code_9_is_u_2p0(self) -> None:
|
||||
# Arrange — code 9 = "triple glazing, installed 2002-2022"; Table 24
|
||||
# "Double or triple, 2002+ (pre-2022), any gap" → U 2.0, g 0.72.
|
||||
from datatypes.epc.domain.mapper import _api_glazing_transmission # pyright: ignore[reportPrivateUsage]
|
||||
from datatypes.epc.domain.mapper import (
|
||||
_api_glazing_transmission,
|
||||
) # pyright: ignore[reportPrivateUsage]
|
||||
|
||||
# Act
|
||||
result = _api_glazing_transmission(9, None)
|
||||
|
|
@ -1400,7 +1478,9 @@ class TestApiGlazingTransmissionTable24:
|
|||
def test_triple_glazing_pre_2002_code_10_default_gap_is_u_2p1(self) -> None:
|
||||
# Arrange — code 10 = "triple glazing, pre-2002"; Table 24 Triple
|
||||
# pre-2002 12mm-gap default → U 2.1, g 0.68.
|
||||
from datatypes.epc.domain.mapper import _api_glazing_transmission # pyright: ignore[reportPrivateUsage]
|
||||
from datatypes.epc.domain.mapper import (
|
||||
_api_glazing_transmission,
|
||||
) # pyright: ignore[reportPrivateUsage]
|
||||
|
||||
# Act
|
||||
result = _api_glazing_transmission(10, None)
|
||||
|
|
@ -1412,7 +1492,9 @@ class TestApiGlazingTransmissionTable24:
|
|||
# Arrange — regression guard: the already-mapped double-glazing
|
||||
# 2002+ entry (U 2.0, g 0.72) is untouched by the single/secondary/
|
||||
# triple extension.
|
||||
from datatypes.epc.domain.mapper import _api_glazing_transmission # pyright: ignore[reportPrivateUsage]
|
||||
from datatypes.epc.domain.mapper import (
|
||||
_api_glazing_transmission,
|
||||
) # pyright: ignore[reportPrivateUsage]
|
||||
|
||||
# Act
|
||||
result = _api_glazing_transmission(2, None)
|
||||
|
|
@ -1433,7 +1515,9 @@ class TestApiCascadeGlazingCodeDivergentRemap:
|
|||
def test_single_glazing_code_5_remaps_to_cascade_single_slot_1(self) -> None:
|
||||
# Arrange — RdSAP-21 code 5 = single glazing; cascade single slot
|
||||
# is 1 (g⊥ 0.85, g_L 0.90), not cascade 5 (secondary, 0.76/0.80).
|
||||
from datatypes.epc.domain.mapper import _api_cascade_glazing_type # pyright: ignore[reportPrivateUsage]
|
||||
from datatypes.epc.domain.mapper import (
|
||||
_api_cascade_glazing_type,
|
||||
) # pyright: ignore[reportPrivateUsage]
|
||||
|
||||
# Act
|
||||
result = _api_cascade_glazing_type(5)
|
||||
|
|
@ -1444,7 +1528,9 @@ class TestApiCascadeGlazingCodeDivergentRemap:
|
|||
def test_secondary_glazing_code_4_remaps_to_cascade_secondary_slot_5(self) -> None:
|
||||
# Arrange — RdSAP-21 code 4 = secondary glazing; cascade secondary
|
||||
# slot is 5 (g⊥ 0.76, g_L 0.80), not cascade 4 (double low-E 0.63).
|
||||
from datatypes.epc.domain.mapper import _api_cascade_glazing_type # pyright: ignore[reportPrivateUsage]
|
||||
from datatypes.epc.domain.mapper import (
|
||||
_api_cascade_glazing_type,
|
||||
) # pyright: ignore[reportPrivateUsage]
|
||||
|
||||
# Act
|
||||
result = _api_cascade_glazing_type(4)
|
||||
|
|
@ -1454,7 +1540,9 @@ class TestApiCascadeGlazingCodeDivergentRemap:
|
|||
|
||||
def test_double_pre_2002_code_1_remap_unchanged(self) -> None:
|
||||
# Arrange — regression guard: the existing code-1 remap (→2) stands.
|
||||
from datatypes.epc.domain.mapper import _api_cascade_glazing_type # pyright: ignore[reportPrivateUsage]
|
||||
from datatypes.epc.domain.mapper import (
|
||||
_api_cascade_glazing_type,
|
||||
) # pyright: ignore[reportPrivateUsage]
|
||||
|
||||
# Act
|
||||
result = _api_cascade_glazing_type(1)
|
||||
|
|
@ -1465,7 +1553,9 @@ class TestApiCascadeGlazingCodeDivergentRemap:
|
|||
def test_rdsap21_native_codes_pass_through(self) -> None:
|
||||
# Arrange — codes 9-15 already coincide with the g-table's RdSAP-21
|
||||
# extension slots, so they must pass through untranslated.
|
||||
from datatypes.epc.domain.mapper import _api_cascade_glazing_type # pyright: ignore[reportPrivateUsage]
|
||||
from datatypes.epc.domain.mapper import (
|
||||
_api_cascade_glazing_type,
|
||||
) # pyright: ignore[reportPrivateUsage]
|
||||
|
||||
# Act / Assert
|
||||
assert _api_cascade_glazing_type(14) == 14
|
||||
|
|
@ -1644,7 +1734,9 @@ class TestRdSap20_0_0ReducedFieldSynthesis:
|
|||
# built_form. Build sap_ventilation with sheltered_sides from built_form
|
||||
# (else the calculator defaults every dwelling to mid-terrace=2). A cert
|
||||
# with an open fireplace.
|
||||
from datatypes.epc.domain.mapper import _api_sheltered_sides # pyright: ignore[reportPrivateUsage]
|
||||
from datatypes.epc.domain.mapper import (
|
||||
_api_sheltered_sides,
|
||||
) # pyright: ignore[reportPrivateUsage]
|
||||
|
||||
corpus = _load_20_0_0_corpus()
|
||||
if not corpus:
|
||||
|
|
@ -1653,7 +1745,8 @@ class TestRdSap20_0_0ReducedFieldSynthesis:
|
|||
(
|
||||
c
|
||||
for c in corpus
|
||||
if not c.get("sap_windows") and (c.get("open_fireplaces_count") or 0) >= 1
|
||||
if not c.get("sap_windows")
|
||||
and (c.get("open_fireplaces_count") or 0) >= 1
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
|
@ -1691,12 +1784,13 @@ class TestRdSap20_0_0ReducedFieldSynthesis:
|
|||
if cert is None:
|
||||
pytest.skip("no corpus cert with instantaneous_wwhrs")
|
||||
iw = cert["sap_heating"]["instantaneous_wwhrs"]
|
||||
expected_baths = iw["rooms_with_bath_and_or_shower"] + iw[
|
||||
"rooms_with_bath_and_mixer_shower"
|
||||
]
|
||||
expected_mixers = iw["rooms_with_mixer_shower_no_bath"] + iw[
|
||||
"rooms_with_bath_and_mixer_shower"
|
||||
]
|
||||
expected_baths = (
|
||||
iw["rooms_with_bath_and_or_shower"] + iw["rooms_with_bath_and_mixer_shower"]
|
||||
)
|
||||
expected_mixers = (
|
||||
iw["rooms_with_mixer_shower_no_bath"]
|
||||
+ iw["rooms_with_bath_and_mixer_shower"]
|
||||
)
|
||||
|
||||
# Act
|
||||
result = EpcPropertyDataMapper.from_api_response(cert)
|
||||
|
|
@ -1724,8 +1818,7 @@ class TestRdSap20_0_0ReducedFieldSynthesis:
|
|||
c
|
||||
for c in corpus
|
||||
if any(
|
||||
"identifier" not in part
|
||||
for part in c.get("sap_building_parts", [])
|
||||
"identifier" not in part for part in c.get("sap_building_parts", [])
|
||||
)
|
||||
),
|
||||
None,
|
||||
|
|
@ -1984,7 +2077,9 @@ class TestRdSap18_0ReducedFieldSynthesis:
|
|||
# percent_draughtproofed, and built_form. Build sap_ventilation with
|
||||
# sheltered_sides from built_form (else the calculator defaults every
|
||||
# dwelling to mid-terrace=2). A cert with an open fireplace.
|
||||
from datatypes.epc.domain.mapper import _api_sheltered_sides # pyright: ignore[reportPrivateUsage]
|
||||
from datatypes.epc.domain.mapper import (
|
||||
_api_sheltered_sides,
|
||||
) # pyright: ignore[reportPrivateUsage]
|
||||
|
||||
corpus = _load_18_0_corpus()
|
||||
if not corpus:
|
||||
|
|
@ -1993,7 +2088,8 @@ class TestRdSap18_0ReducedFieldSynthesis:
|
|||
(
|
||||
c
|
||||
for c in corpus
|
||||
if not c.get("sap_windows") and (c.get("open_fireplaces_count") or 0) >= 1
|
||||
if not c.get("sap_windows")
|
||||
and (c.get("open_fireplaces_count") or 0) >= 1
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
|
@ -2031,12 +2127,13 @@ class TestRdSap18_0ReducedFieldSynthesis:
|
|||
if cert is None:
|
||||
pytest.skip("no corpus cert with instantaneous_wwhrs")
|
||||
iw = cert["sap_heating"]["instantaneous_wwhrs"]
|
||||
expected_baths = iw["rooms_with_bath_and_or_shower"] + iw[
|
||||
"rooms_with_bath_and_mixer_shower"
|
||||
]
|
||||
expected_mixers = iw["rooms_with_mixer_shower_no_bath"] + iw[
|
||||
"rooms_with_bath_and_mixer_shower"
|
||||
]
|
||||
expected_baths = (
|
||||
iw["rooms_with_bath_and_or_shower"] + iw["rooms_with_bath_and_mixer_shower"]
|
||||
)
|
||||
expected_mixers = (
|
||||
iw["rooms_with_mixer_shower_no_bath"]
|
||||
+ iw["rooms_with_bath_and_mixer_shower"]
|
||||
)
|
||||
|
||||
# Act
|
||||
result = EpcPropertyDataMapper.from_api_response(cert)
|
||||
|
|
@ -2111,7 +2208,11 @@ class TestRdSap17_1ReducedFieldSynthesis:
|
|||
if not corpus:
|
||||
pytest.skip("no RdSAP-Schema-17.1 corpus harvested")
|
||||
cert = next(
|
||||
(c for c in corpus if not c.get("sap_windows") and c.get("glazed_area") == 1),
|
||||
(
|
||||
c
|
||||
for c in corpus
|
||||
if not c.get("sap_windows") and c.get("glazed_area") == 1
|
||||
),
|
||||
None,
|
||||
)
|
||||
if cert is None:
|
||||
|
|
@ -2132,7 +2233,11 @@ class TestRdSap17_1ReducedFieldSynthesis:
|
|||
if not corpus:
|
||||
pytest.skip("no RdSAP-Schema-17.1 corpus harvested")
|
||||
cert = next(
|
||||
(c for c in corpus if not c.get("sap_windows") and c.get("glazed_area") == 2),
|
||||
(
|
||||
c
|
||||
for c in corpus
|
||||
if not c.get("sap_windows") and c.get("glazed_area") == 2
|
||||
),
|
||||
None,
|
||||
)
|
||||
if cert is None:
|
||||
|
|
@ -2219,7 +2324,9 @@ class TestRdSap17_1ReducedFieldSynthesis:
|
|||
) -> None:
|
||||
# ADR-0028: open_fireplaces_count -> chimneys, percent_draughtproofed,
|
||||
# sheltered_sides from built_form.
|
||||
from datatypes.epc.domain.mapper import _api_sheltered_sides # pyright: ignore[reportPrivateUsage]
|
||||
from datatypes.epc.domain.mapper import (
|
||||
_api_sheltered_sides,
|
||||
) # pyright: ignore[reportPrivateUsage]
|
||||
|
||||
corpus = _load_17_1_corpus()
|
||||
if not corpus:
|
||||
|
|
@ -2228,7 +2335,8 @@ class TestRdSap17_1ReducedFieldSynthesis:
|
|||
(
|
||||
c
|
||||
for c in corpus
|
||||
if not c.get("sap_windows") and (c.get("open_fireplaces_count") or 0) >= 1
|
||||
if not c.get("sap_windows")
|
||||
and (c.get("open_fireplaces_count") or 0) >= 1
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
|
@ -2261,12 +2369,13 @@ class TestRdSap17_1ReducedFieldSynthesis:
|
|||
if cert is None:
|
||||
pytest.skip("no corpus cert with instantaneous_wwhrs")
|
||||
iw = cert["sap_heating"]["instantaneous_wwhrs"]
|
||||
expected_baths = iw["rooms_with_bath_and_or_shower"] + iw[
|
||||
"rooms_with_bath_and_mixer_shower"
|
||||
]
|
||||
expected_mixers = iw["rooms_with_mixer_shower_no_bath"] + iw[
|
||||
"rooms_with_bath_and_mixer_shower"
|
||||
]
|
||||
expected_baths = (
|
||||
iw["rooms_with_bath_and_or_shower"] + iw["rooms_with_bath_and_mixer_shower"]
|
||||
)
|
||||
expected_mixers = (
|
||||
iw["rooms_with_mixer_shower_no_bath"]
|
||||
+ iw["rooms_with_bath_and_mixer_shower"]
|
||||
)
|
||||
|
||||
result = EpcPropertyDataMapper.from_api_response(cert)
|
||||
|
||||
|
|
@ -2314,16 +2423,22 @@ class TestRoomInRoofDetailedSlopeAndStudWall:
|
|||
rir = cert["sap_building_parts"][0]["sap_room_in_roof"]
|
||||
rir.pop("room_in_roof_type_1", None)
|
||||
rir["room_in_roof_details"] = {
|
||||
"slope_length_1": 7.0, "slope_height_1": 1.4,
|
||||
"slope_length_2": 7.0, "slope_height_2": 1.4,
|
||||
"slope_length_1": 7.0,
|
||||
"slope_height_1": 1.4,
|
||||
"slope_length_2": 7.0,
|
||||
"slope_height_2": 1.4,
|
||||
"slope_insulation_thickness_1": "100mm",
|
||||
"slope_insulation_thickness_2": "100mm",
|
||||
"stud_wall_length_1": 7.0, "stud_wall_height_1": 1.03,
|
||||
"stud_wall_length_2": 7.0, "stud_wall_height_2": 1.03,
|
||||
"stud_wall_length_1": 7.0,
|
||||
"stud_wall_height_1": 1.03,
|
||||
"stud_wall_length_2": 7.0,
|
||||
"stud_wall_height_2": 1.03,
|
||||
"stud_wall_insulation_thickness_1": "75mm",
|
||||
"stud_wall_insulation_thickness_2": "75mm",
|
||||
"common_wall_length_1": 8.6, "common_wall_height_1": 1.2,
|
||||
"common_wall_length_2": 8.6, "common_wall_height_2": 1.2,
|
||||
"common_wall_length_1": 8.6,
|
||||
"common_wall_height_1": 1.2,
|
||||
"common_wall_length_2": 8.6,
|
||||
"common_wall_height_2": 1.2,
|
||||
}
|
||||
|
||||
# Act
|
||||
|
|
@ -2369,12 +2484,16 @@ class TestRoomInRoofType2SimplifiedQuadratic:
|
|||
rir = cert["sap_building_parts"][0]["sap_room_in_roof"]
|
||||
rir.pop("room_in_roof_type_1", None)
|
||||
rir["room_in_roof_type_2"] = {
|
||||
"gable_wall_type_1": 1, "gable_wall_length_1": 10.0,
|
||||
"gable_wall_type_1": 1,
|
||||
"gable_wall_length_1": 10.0,
|
||||
"gable_wall_height_1": 2.0,
|
||||
"gable_wall_type_2": 0, "gable_wall_length_2": 6.0,
|
||||
"gable_wall_type_2": 0,
|
||||
"gable_wall_length_2": 6.0,
|
||||
"gable_wall_height_2": 2.0,
|
||||
"common_wall_length_1": 8.0, "common_wall_height_1": 1.0,
|
||||
"common_wall_length_2": 8.0, "common_wall_height_2": 1.0,
|
||||
"common_wall_length_1": 8.0,
|
||||
"common_wall_height_1": 1.0,
|
||||
"common_wall_length_2": 8.0,
|
||||
"common_wall_height_2": 1.0,
|
||||
}
|
||||
|
||||
# Act
|
||||
|
|
@ -2505,7 +2624,9 @@ class TestNonSeparatedConservatoryApiMirror19_0:
|
|||
)
|
||||
|
||||
dwelling_part_count = len(
|
||||
EpcPropertyDataMapper.from_api_response(load("19_0.json")).sap_building_parts
|
||||
EpcPropertyDataMapper.from_api_response(
|
||||
load("19_0.json")
|
||||
).sap_building_parts
|
||||
)
|
||||
|
||||
cert = load("19_0.json")
|
||||
|
|
@ -2568,7 +2689,9 @@ class TestNonSeparatedConservatoryApiMirror20_0_0:
|
|||
)
|
||||
|
||||
dwelling_part_count = len(
|
||||
EpcPropertyDataMapper.from_api_response(load("20_0_0.json")).sap_building_parts
|
||||
EpcPropertyDataMapper.from_api_response(
|
||||
load("20_0_0.json")
|
||||
).sap_building_parts
|
||||
)
|
||||
|
||||
cert = load("20_0_0.json")
|
||||
|
|
@ -2637,7 +2760,12 @@ def test_gov_mappers_split_non_separated_conservatory(fixture: str) -> None:
|
|||
cert = load(fixture)
|
||||
cert["conservatory_type"] = 4
|
||||
cert["sap_building_parts"].append(
|
||||
{"floor_area": 12.0, "room_height": 1, "double_glazed": "Y", "glazed_perimeter": 9.0}
|
||||
{
|
||||
"floor_area": 12.0,
|
||||
"room_height": 1,
|
||||
"double_glazed": "Y",
|
||||
"glazed_perimeter": 9.0,
|
||||
}
|
||||
)
|
||||
|
||||
# Act
|
||||
|
|
@ -2709,9 +2837,17 @@ def test_rdsap_mappers_carry_main_heating_controls_display(
|
|||
[
|
||||
(RdSapSchema17_1, EpcPropertyDataMapper.from_rdsap_schema_17_1, "17_1.json"),
|
||||
(RdSapSchema18_0, EpcPropertyDataMapper.from_rdsap_schema_18_0, "18_0.json"),
|
||||
(RdSapSchema20_0_0, EpcPropertyDataMapper.from_rdsap_schema_20_0_0, "20_0_0.json"),
|
||||
(
|
||||
RdSapSchema20_0_0,
|
||||
EpcPropertyDataMapper.from_rdsap_schema_20_0_0,
|
||||
"20_0_0.json",
|
||||
),
|
||||
(RdSapSchema19_0, EpcPropertyDataMapper.from_rdsap_schema_19_0, "19_0.json"),
|
||||
(RdSapSchema21_0_0, EpcPropertyDataMapper.from_rdsap_schema_21_0_0, "21_0_0.json"),
|
||||
(
|
||||
RdSapSchema21_0_0,
|
||||
EpcPropertyDataMapper.from_rdsap_schema_21_0_0,
|
||||
"21_0_0.json",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_rdsap_mappers_map_mechanical_ventilation_kind(
|
||||
|
|
@ -2738,7 +2874,11 @@ def test_rdsap_mappers_map_mechanical_ventilation_kind(
|
|||
"schema_cls, mapper, fixture",
|
||||
[
|
||||
(RdSapSchema19_0, EpcPropertyDataMapper.from_rdsap_schema_19_0, "19_0.json"),
|
||||
(RdSapSchema21_0_0, EpcPropertyDataMapper.from_rdsap_schema_21_0_0, "21_0_0.json"),
|
||||
(
|
||||
RdSapSchema21_0_0,
|
||||
EpcPropertyDataMapper.from_rdsap_schema_21_0_0,
|
||||
"21_0_0.json",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_rdsap_19_0_21_0_0_construct_sap_ventilation_block(
|
||||
|
|
@ -2755,3 +2895,109 @@ def test_rdsap_19_0_21_0_0_construct_sap_ventilation_block(
|
|||
result = mapper(schema)
|
||||
|
||||
assert result.sap_ventilation.sheltered_sides == 1
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ADR-0028 single-glazed honouring on the reduced-field "ND" fallback.
|
||||
#
|
||||
# A reduced-field cert whose numeric `multiple_glazing_type` is the "ND" (Not
|
||||
# Defined) sentinel used to default to double glazing (cascade code 2) on every
|
||||
# pre-SAP10 synthesis seam — over-rating a genuinely single-glazed dwelling AND
|
||||
# hiding it from the glazing generator (which upgrades only single-glazed
|
||||
# windows). The 16.x normaliser already honoured an explicit "Single glazed"
|
||||
# window description; these tests pin that same rule now firing on the 17.0 /
|
||||
# 17.1 / 18.0 / 19.0 / 20.0.0 seams. Motivating cert: 2780-3922-3202-6042-9200
|
||||
# (uprn 10033526327), a single-glazed community-heated flat lodging
|
||||
# multiple_glazing_type="ND", window "Single glazed", multiple_glazed_proportion 0.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_REDUCED_FIELD_SEAMS = [
|
||||
(RdSapSchema17_0, EpcPropertyDataMapper.from_rdsap_schema_17_0, "17_0.json"),
|
||||
(RdSapSchema17_1, EpcPropertyDataMapper.from_rdsap_schema_17_1, "17_1.json"),
|
||||
(RdSapSchema18_0, EpcPropertyDataMapper.from_rdsap_schema_18_0, "18_0.json"),
|
||||
(RdSapSchema19_0, EpcPropertyDataMapper.from_rdsap_schema_19_0, "19_0.json"),
|
||||
(RdSapSchema20_0_0, EpcPropertyDataMapper.from_rdsap_schema_20_0_0, "20_0_0.json"),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("schema_cls, mapper, fixture", _REDUCED_FIELD_SEAMS)
|
||||
def test_nd_glazing_with_single_glazed_description_synthesises_single(
|
||||
schema_cls: Any, mapper: Any, fixture: str
|
||||
) -> None:
|
||||
# Arrange — force the ND + single-glazed shape onto the fixture, dropping any
|
||||
# per-window array so the reduced-field synthesis seam runs.
|
||||
data = load(fixture)
|
||||
data["multiple_glazing_type"] = "ND"
|
||||
data["multiple_glazed_proportion"] = 0
|
||||
data["window"] = {
|
||||
"description": "Single glazed",
|
||||
"energy_efficiency_rating": 1,
|
||||
"environmental_efficiency_rating": 1,
|
||||
}
|
||||
data.pop("sap_windows", None)
|
||||
|
||||
# Act
|
||||
result = mapper(from_dict(schema_cls, data))
|
||||
|
||||
# Assert — synthesised as single (SAP10 cascade code 1), the code the glazing
|
||||
# generator's single-glazed set {1, 15} matches, NOT the old double default 2.
|
||||
assert result.sap_windows
|
||||
assert all(w.glazing_type == 1 for w in result.sap_windows)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("schema_cls, mapper, fixture", _REDUCED_FIELD_SEAMS)
|
||||
def test_nd_glazing_without_single_signal_keeps_double_modal_default(
|
||||
schema_cls: Any, mapper: Any, fixture: str
|
||||
) -> None:
|
||||
# Arrange — ND with a double-glazed description and a non-zero multiple-glazed
|
||||
# proportion: the honouring must NOT fire, so the DG-modal default stands.
|
||||
data = load(fixture)
|
||||
data["multiple_glazing_type"] = "ND"
|
||||
data["multiple_glazed_proportion"] = 100
|
||||
data["window"] = {
|
||||
"description": "Fully double glazed",
|
||||
"energy_efficiency_rating": 3,
|
||||
"environmental_efficiency_rating": 3,
|
||||
}
|
||||
data.pop("sap_windows", None)
|
||||
|
||||
# Act
|
||||
result = mapper(from_dict(schema_cls, data))
|
||||
|
||||
# Assert — unchanged DG-modal default (cascade code 2).
|
||||
assert result.sap_windows
|
||||
assert all(w.glazing_type == 2 for w in result.sap_windows)
|
||||
|
||||
|
||||
class TestReducedFieldNdGlazingResolver:
|
||||
"""Unit-level pins for the shared ND resolver (schema-independent)."""
|
||||
|
||||
def test_single_in_description_is_single_glazed(self) -> None:
|
||||
from datatypes.epc.domain.mapper import (
|
||||
_reduced_field_is_single_glazed, # pyright: ignore[reportPrivateUsage]
|
||||
)
|
||||
|
||||
assert _reduced_field_is_single_glazed("Single glazed", 50) is True
|
||||
|
||||
def test_zero_multiple_glazed_proportion_is_single_glazed(self) -> None:
|
||||
from datatypes.epc.domain.mapper import (
|
||||
_reduced_field_is_single_glazed, # pyright: ignore[reportPrivateUsage]
|
||||
)
|
||||
|
||||
# 0% multiple-glazed = 100% single, even if the description is ambiguous.
|
||||
assert _reduced_field_is_single_glazed("Not recorded", 0) is True
|
||||
|
||||
def test_double_description_with_glazing_is_not_single(self) -> None:
|
||||
from datatypes.epc.domain.mapper import (
|
||||
_reduced_field_is_single_glazed, # pyright: ignore[reportPrivateUsage]
|
||||
)
|
||||
|
||||
assert _reduced_field_is_single_glazed("Fully double glazed", 100) is False
|
||||
|
||||
def test_resolver_returns_cascade_single_or_the_seam_default(self) -> None:
|
||||
from datatypes.epc.domain.mapper import (
|
||||
_reduced_field_nd_glazing_type, # pyright: ignore[reportPrivateUsage]
|
||||
)
|
||||
|
||||
assert _reduced_field_nd_glazing_type("Single glazed", 0, 2) == 1
|
||||
assert _reduced_field_nd_glazing_type("Fully double glazed", 100, 2) == 2
|
||||
|
|
|
|||
|
|
@ -137,6 +137,71 @@ class TestFromSapSchema17_1RebaselineFields:
|
|||
assert result.assessment_type == "SAP"
|
||||
|
||||
|
||||
class TestFullSapPhotovoltaics:
|
||||
"""Full-SAP certs lodge measured PV under `sap_energy_source.pv_arrays`;
|
||||
the mapper must carry it to the domain `photovoltaic_arrays` so the
|
||||
Appendix-M generation credit isn't silently dropped."""
|
||||
|
||||
def test_maps_the_lodged_pv_array(self) -> None:
|
||||
# sap_17_1_house.json lodges one 1.62 kWp array (pitch 3, orientation 6,
|
||||
# overshading 1).
|
||||
schema = from_dict(SapSchema17_1, load("sap_17_1_house.json"))
|
||||
|
||||
result = EpcPropertyDataMapper.from_sap_schema_17_1(schema)
|
||||
|
||||
arrays = result.sap_energy_source.photovoltaic_arrays
|
||||
assert arrays is not None
|
||||
assert len(arrays) == 1
|
||||
assert arrays[0].peak_power == 1.62
|
||||
|
||||
|
||||
class TestFullSapElectricityTariffTranslation:
|
||||
"""The full-SAP `energy_tariff` code space differs from the RdSAP
|
||||
`meter_type` one the calculator reads, so the mapper must translate it."""
|
||||
|
||||
def test_economy7_tariff_is_not_read_as_single_rate(self) -> None:
|
||||
# full-SAP energy_tariff 2 = "off-peak 7 hour" (Economy 7). Passing the
|
||||
# code straight through read it as RdSAP meter 2 = Single → no off-peak
|
||||
# split (under-rated). It must resolve to the SEVEN_HOUR off-peak tariff.
|
||||
from domain.sap10_calculator.tables.table_12a import (
|
||||
Tariff,
|
||||
tariff_from_meter_type,
|
||||
)
|
||||
|
||||
from datatypes.epc.domain.mapper import _sap_17_1_meter_type
|
||||
|
||||
meter_type = _sap_17_1_meter_type(2)
|
||||
|
||||
assert tariff_from_meter_type(meter_type) is Tariff.SEVEN_HOUR
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("energy_tariff", "expected"),
|
||||
[
|
||||
(1, "STANDARD"), # standard tariff → single-rate (was over-rated as E7)
|
||||
(2, "SEVEN_HOUR"), # off-peak 7-hour → Economy 7
|
||||
(3, "SEVEN_HOUR"), # off-peak 10-hour → dual (§12 resolves 7/10hr)
|
||||
(
|
||||
4,
|
||||
"TWENTY_FOUR_HOUR",
|
||||
), # 24-hour tariff (e.g. property 709874 — unchanged)
|
||||
(5, "EIGHTEEN_HOUR"), # off-peak 18-hour — was dropped to "" → STANDARD
|
||||
(None, "STANDARD"), # absent/ND → unknown → standard
|
||||
],
|
||||
)
|
||||
def test_energy_tariff_resolves_to_the_correct_calculator_tariff(
|
||||
self, energy_tariff: Any, expected: str
|
||||
) -> None:
|
||||
from domain.sap10_calculator.tables.table_12a import (
|
||||
tariff_from_meter_type,
|
||||
)
|
||||
|
||||
from datatypes.epc.domain.mapper import _sap_17_1_meter_type
|
||||
|
||||
resolved = tariff_from_meter_type(_sap_17_1_meter_type(energy_tariff))
|
||||
|
||||
assert resolved.name == expected
|
||||
|
||||
|
||||
class TestFromSapSchema17_1DisplayElements:
|
||||
"""Display EnergyElements the WIP mapper dropped, leaving the FE
|
||||
property-details panel "Unknown" for full-SAP certs (ADR-0037). Brings
|
||||
|
|
@ -222,8 +287,12 @@ class TestFromSapSchema17_1FabricDescriptions:
|
|||
def test_wall_description_carries_measured_u(self, sample: EpcPropertyData) -> None:
|
||||
assert sample.walls[0].description == "Average thermal transmittance 0.17 W/m²K"
|
||||
|
||||
def test_floor_description_carries_measured_u(self, sample: EpcPropertyData) -> None:
|
||||
assert sample.floors[0].description == "Average thermal transmittance 0.13 W/m²K"
|
||||
def test_floor_description_carries_measured_u(
|
||||
self, sample: EpcPropertyData
|
||||
) -> None:
|
||||
assert (
|
||||
sample.floors[0].description == "Average thermal transmittance 0.13 W/m²K"
|
||||
)
|
||||
|
||||
def test_roof_description_carries_other_premises(
|
||||
self, sample: EpcPropertyData
|
||||
|
|
@ -443,7 +512,7 @@ class TestFromSapSchema17_1EnergySource:
|
|||
|
||||
def test_mains_gas_derived_from_heating_fuel(self, sample: EpcPropertyData) -> None:
|
||||
# Main heating fuel is mains gas (code 1) → mains_gas True.
|
||||
assert sample.sap_energy_source.mains_gas is True
|
||||
assert sample.sap_energy_source.gas_connection_available is True
|
||||
|
||||
def test_lighting_outlet_counts_to_bulbs(self, sample: EpcPropertyData) -> None:
|
||||
# 1 fixed outlet, 1 low-energy → 1 low-energy bulb, 0 incandescent.
|
||||
|
|
@ -481,7 +550,9 @@ class TestFromSapSchema17_1Ventilation:
|
|||
schema = from_dict(SapSchema17_1, load("sap_17_1.json"))
|
||||
return EpcPropertyDataMapper.from_sap_schema_17_1(schema)
|
||||
|
||||
def test_measured_air_permeability_fed_as_ap50(self, sample: EpcPropertyData) -> None:
|
||||
def test_measured_air_permeability_fed_as_ap50(
|
||||
self, sample: EpcPropertyData
|
||||
) -> None:
|
||||
# The lodged `air_permeability` is a q50 Blower-Door result, so it feeds the
|
||||
# engine's AP50 path `(18) = AP50/20 + (8)` — NOT the AP4/Pulse formula
|
||||
# `0.263 × AP4^0.924` (the air-permeability AP50 fix, uprn_10093116528).
|
||||
|
|
@ -492,7 +563,10 @@ class TestFromSapSchema17_1Ventilation:
|
|||
def test_ventilation_type_6_is_extract(self, sample: EpcPropertyData) -> None:
|
||||
# ventilation_type 6 = MEV decentralised → EXTRACT_OR_PIV_OUTSIDE.
|
||||
assert sample.sap_ventilation is not None
|
||||
assert sample.sap_ventilation.mechanical_ventilation_kind == "EXTRACT_OR_PIV_OUTSIDE"
|
||||
assert (
|
||||
sample.sap_ventilation.mechanical_ventilation_kind
|
||||
== "EXTRACT_OR_PIV_OUTSIDE"
|
||||
)
|
||||
|
||||
def test_sheltered_sides_and_wet_rooms(self, sample: EpcPropertyData) -> None:
|
||||
assert sample.sap_ventilation is not None
|
||||
|
|
@ -587,14 +661,18 @@ class TestFromSapSchema17_1Perimeter:
|
|||
# measured exposed-wall area 87.85 m² (uniform per-storey split).
|
||||
result = self._map("sap_17_1_house.json")
|
||||
fds = result.sap_building_parts[0].sap_floor_dimensions
|
||||
gross = sum((fd.heat_loss_perimeter_m or 0.0) * (fd.room_height_m or 0.0) for fd in fds)
|
||||
gross = sum(
|
||||
(fd.heat_loss_perimeter_m or 0.0) * (fd.room_height_m or 0.0) for fd in fds
|
||||
)
|
||||
assert gross == pytest.approx(87.85, abs=1e-2)
|
||||
|
||||
def test_house_routes_party_walls_to_party_length(self) -> None:
|
||||
# Σ(party_length_i × height_i) must equal the measured party-wall area.
|
||||
result = self._map("sap_17_1_house.json")
|
||||
fds = result.sap_building_parts[0].sap_floor_dimensions
|
||||
party = sum((fd.party_wall_length_m or 0.0) * (fd.room_height_m or 0.0) for fd in fds)
|
||||
party = sum(
|
||||
(fd.party_wall_length_m or 0.0) * (fd.room_height_m or 0.0) for fd in fds
|
||||
)
|
||||
assert party == pytest.approx(105.44, abs=1e-2)
|
||||
|
||||
def test_unknown_wall_type_fails_loud(self) -> None:
|
||||
|
|
@ -671,8 +749,9 @@ class TestFromSapSchema16_2:
|
|||
epc = EpcPropertyDataMapper.from_api_response(load("sap_17_0.json"))
|
||||
assert isinstance(epc, EpcPropertyData)
|
||||
assert epc.uprn == 10023444324
|
||||
# lodged 82; engine produces 80.
|
||||
assert Sap10Calculator().calculate(epc).sap_score == 80
|
||||
# lodged 82; the engine now also produces 82 — the cert's lodged PV is
|
||||
# credited (previously dropped by the full-SAP mapper, under-rating to 80).
|
||||
assert Sap10Calculator().calculate(epc).sap_score == 82
|
||||
|
||||
def test_18_0_0_dispatches_via_full_sap_path(self) -> None:
|
||||
# SAP-Schema-18.0.0 is the full-SAP 17.1 shape; dispatched to
|
||||
|
|
@ -807,6 +886,27 @@ class TestFullSapSchema16xRouting:
|
|||
assert epc.door_count == 1
|
||||
assert epc.dwelling_type == "Detached house"
|
||||
|
||||
def test_full_sap_16_x_electricity_tariff_is_translated_not_passed_through(
|
||||
self,
|
||||
) -> None:
|
||||
# A full-SAP 16.x cert lodges the SAP `energy_tariff` code space, not the
|
||||
# RdSAP `meter_type` one. Routing it through the full-SAP mapper (via the
|
||||
# assessment_type=SAP gate) must apply `_sap_17_1_meter_type`: tariff 1 =
|
||||
# standard → STANDARD. Were it raw-passed to the RdSAP normaliser, code 1
|
||||
# = "dual" → SEVEN_HOUR (the prime-lead over-rating bug this guards).
|
||||
from domain.sap10_calculator.tables.table_12a import (
|
||||
Tariff,
|
||||
tariff_from_meter_type,
|
||||
)
|
||||
|
||||
data = load("sap_16_0_full.json") # electricity_tariff = 1 (standard)
|
||||
|
||||
epc = EpcPropertyDataMapper.from_api_response(data)
|
||||
|
||||
assert tariff_from_meter_type(epc.sap_energy_source.meter_type) is (
|
||||
Tariff.STANDARD
|
||||
)
|
||||
|
||||
def test_reduced_16_x_cert_unaffected_by_full_sap_routing(self) -> None:
|
||||
# Arrange — a reduced 16.2 cert (assessment_type RdSAP) must stay on the
|
||||
# RdSAP path, keeping its top-level property_type.
|
||||
|
|
@ -925,5 +1025,7 @@ class TestFullSapSchema16xNoFloorDimensions:
|
|||
data.pop("sap_flat_details", None)
|
||||
|
||||
# Act / Assert
|
||||
with pytest.raises(ValueError, match="Wardalls Grove.*SE14 5FB.*sap_floor_dimensions"):
|
||||
with pytest.raises(
|
||||
ValueError, match="Wardalls Grove.*SE14 5FB.*sap_floor_dimensions"
|
||||
):
|
||||
EpcPropertyDataMapper.from_api_response(data)
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ class TestFromSiteNotesExample1:
|
|||
|
||||
def test_mains_gas_available(self, result: EpcPropertyData) -> None:
|
||||
# general.mains_gas_available: true
|
||||
assert result.sap_energy_source.mains_gas is True
|
||||
assert result.sap_energy_source.gas_connection_available is True
|
||||
|
||||
def test_electricity_smart_meter(self, result: EpcPropertyData) -> None:
|
||||
# general.electricity_smart_meter: true
|
||||
|
|
@ -393,7 +393,9 @@ class TestFromSiteNotesExample1:
|
|||
cylinder_insulation_type="Factory fitted",
|
||||
cylinder_insulation_thickness_mm=12,
|
||||
shower_outlets=ShowerOutlets(
|
||||
shower_outlet=ShowerOutlet(shower_outlet_type="Non-Electric Shower"),
|
||||
shower_outlet=ShowerOutlet(
|
||||
shower_outlet_type="Non-Electric Shower"
|
||||
),
|
||||
),
|
||||
),
|
||||
# Windows
|
||||
|
|
@ -453,7 +455,7 @@ class TestFromSiteNotesExample1:
|
|||
],
|
||||
# Energy source
|
||||
sap_energy_source=SapEnergySource(
|
||||
mains_gas=True,
|
||||
gas_connection_available=True,
|
||||
meter_type="Single",
|
||||
pv_battery_count=0,
|
||||
wind_turbines_count=0,
|
||||
|
|
@ -620,7 +622,10 @@ class TestFromSiteNotesFloorConstruction:
|
|||
|
||||
def test_floor_construction_type(self, result: EpcPropertyData) -> None:
|
||||
# building_construction.floor.floor_construction: "Suspended, not timber"
|
||||
assert result.sap_building_parts[0].floor_construction_type == "Suspended, not timber"
|
||||
assert (
|
||||
result.sap_building_parts[0].floor_construction_type
|
||||
== "Suspended, not timber"
|
||||
)
|
||||
|
||||
def test_floor_insulation_type_str(self, result: EpcPropertyData) -> None:
|
||||
# building_construction.floor.floor_insulation_type: "As Built"
|
||||
|
|
@ -657,7 +662,10 @@ class TestFromSiteNotesHeatingBoiler:
|
|||
|
||||
def test_central_heating_pump_age_str(self, result: EpcPropertyData) -> None:
|
||||
# heating_and_hot_water.main_heating.central_heating_pump_age: "Unknown"
|
||||
assert result.sap_heating.main_heating_details[0].central_heating_pump_age_str == "Unknown"
|
||||
assert (
|
||||
result.sap_heating.main_heating_details[0].central_heating_pump_age_str
|
||||
== "Unknown"
|
||||
)
|
||||
|
||||
|
||||
class TestFromSiteNotesMiscTopLevel:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -107,6 +107,20 @@ class SapVentilation:
|
|||
mechanical_vent_duct_type: Optional[int] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class SapPvArray:
|
||||
"""One measured photovoltaic array lodged on a full-SAP cert (under
|
||||
`sap_energy_source.pv_arrays`): peak power (kWp), pitch, SAP octant
|
||||
orientation (1-8), overshading code, and the connection type. Mirrors the
|
||||
domain `PhotovoltaicArray` the calculator's Appendix M generation uses."""
|
||||
|
||||
peak_power: Optional[float] = None
|
||||
pitch: Optional[int] = None
|
||||
orientation: Optional[int] = None
|
||||
overshading: Optional[int] = None
|
||||
pv_connection: Optional[int] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class SapEnergySource:
|
||||
"""Electricity tariff, on-site generation and lighting. Lighting outlet
|
||||
|
|
@ -118,6 +132,7 @@ class SapEnergySource:
|
|||
wind_turbine_terrain_type: Optional[int] = None
|
||||
fixed_lighting_outlets_count: Optional[int] = None
|
||||
low_energy_fixed_lighting_outlets_count: Optional[int] = None
|
||||
pv_arrays: Optional[List[SapPvArray]] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -232,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
|
||||
|
|
|
|||
|
|
@ -39,3 +39,58 @@ than churning the table twice.
|
|||
The SQLModel row is defined in `infrastructure/postgres/` so the ephemeral-Postgres tests build it
|
||||
via `create_all`; the production migration is FE-owned (Drizzle ORM) and tracked in
|
||||
`docs/migrations/`.
|
||||
|
||||
### Amendment (2026-06-30, #1361 Class B): the Lodged half is absent for a predicted Property
|
||||
|
||||
The original invariant — **every** `PropertyBaselinePerformance` populates **both** halves, even when
|
||||
equal — assumed every Property has a record *of its own* performance to lodge: a public EPC cert (the
|
||||
`epc_with_overlay` path) or a Site Notes survey. It does **not** hold for the **EPC Prediction** path
|
||||
(ADR-0029/0031). A predicted Property has no record of itself — its `EpcPropertyData` is deterministic
|
||||
neighbour synthesis that copies a representative comparable's structure wholesale, so the
|
||||
`energy_rating_current` / band / CO2 / Primary Energy Intensity on the synthesised picture are a
|
||||
*different dwelling's* lodged figures. Reading **Lodged Performance** off it manufactures a **phantom**:
|
||||
a borrowed neighbour's SAP presented as this Property's government-register figure. This affected
|
||||
**every** predicted Property (~12,236 rows estate-wide), not the 8 the `effective-lodged-divergence`
|
||||
audit surfaced — that audit only fires when the borrowed figure lands ≥15 SAP from the Effective, so it
|
||||
under-counts the phantom by three orders of magnitude.
|
||||
|
||||
**Decision.** The Lodged half is **optional**. When `source_path == "predicted"` there is no Lodged
|
||||
Performance: `PropertyBaselinePerformance.lodged` is `None` and the four `lodged_*` columns are `NULL`.
|
||||
The **Effective half is unchanged and still persisted** — a predicted Property is a first-class modelled
|
||||
output (it flows through Rebaselining, Bill Derivation, and Modelling like any other; its Effective
|
||||
Performance and bill block are correct and load-bearing for the FE and the plan-vs-effective audit
|
||||
checks). `rebaseline_reason` stays `physical_state_changed` / `both`, which already records that the
|
||||
Effective figure was scored from a changed picture.
|
||||
|
||||
**Principle (the boundary).** Lodged Performance requires a record **of this Property** — a lodged cert
|
||||
*or* a Site Notes survey (the as-surveyed values are a real observation of this dwelling). EPC
|
||||
Prediction borrows a neighbour's, so it has none. The branch is therefore `source_path == "predicted"` —
|
||||
**not** `physical_state_changed` (true for Site Notes and Landlord Overrides too) and **not** "no public
|
||||
EPC" (Site Notes has none yet keeps a legitimate Lodged Performance). Whether the Site Notes as-surveyed
|
||||
values are truly "lodged" is a separate question, deferred.
|
||||
|
||||
**Rejected — a sentinel (`lodged = 0`).** Considered, to make "no record" visibly present rather than an
|
||||
empty cell. Rejected: `0` is a valid-looking SAP score that (a) re-trips `effective-lodged-divergence`
|
||||
for every predicted Property (`|effective − 0| ≥ 15`), (b) poisons every `AVG(lodged_*)` aggregate that
|
||||
`NULL` is correctly excluded from, and (c) has no coherent `lodged_epc_band` enum member. The "no lodged
|
||||
record" signal belongs in `NULL` (honest absence) plus the **structural provenance** already carried by
|
||||
the distinct predicted-EPC slot — which the FE renders as a predicted badge — not overloaded onto the
|
||||
score column.
|
||||
|
||||
**Consequences.**
|
||||
- The four `lodged_*` columns become **nullable**. The production table is **FE-owned (Drizzle)**, so the
|
||||
migration (`ALTER … DROP NOT NULL`) lands in the FE repo and **must precede** any backend write of a
|
||||
`NULL` lodged, or the predicted-Property INSERT violates the constraint and aborts the batch
|
||||
(ADR-0012). The SQLModel mirror in `infrastructure/postgres/` is updated to `Optional` so the
|
||||
ephemeral-Postgres tests build the nullable shape.
|
||||
- The fix lives in `PropertyBaselineOrchestrator.run()` — the single chokepoint both the First Run
|
||||
pipeline and `applications/modelling_e2e/handler.py` call — so one change repairs both entry points.
|
||||
The Rebaseliner port takes `Optional[Performance]`; for a predicted Property `physical_state_changed`
|
||||
is always true, so `CalculatorRebaseliner` adopts the calculator output and never reads the absent
|
||||
lodged half (the divergence-log path is the pristine-cert case only, where lodged is non-null).
|
||||
- A one-time backfill (`scripts/`, dry-run default + `--apply`, idempotent) NULLs the four `lodged_*`
|
||||
columns on existing predicted-source rows (~12,236). It corrects only the Lodged half; Effective, the
|
||||
bill block, and `rebaseline_reason` are left intact.
|
||||
- `effective-lodged-divergence` already short-circuits on `lodged_sap IS NULL`, so it goes green for
|
||||
predicted Properties once backfilled. Class C of #1361 (a floor that ignores implausibly-low lodged
|
||||
scores on *real* certs) is an additive guard, independent of this change.
|
||||
|
|
|
|||
30
docs/adr/0041-flat-roofs-scored-by-insulation-thickness.md
Normal file
30
docs/adr/0041-flat-roofs-scored-by-insulation-thickness.md
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# Flat roofs are scored by insulation thickness (RdSAP Table 16 col 1), not the age-band default
|
||||
|
||||
## Context
|
||||
|
||||
A **Landlord Override** naming a flat roof with a known insulation depth (`flat: 50mm` / `100mm` / `150mm`) is classified to the canonical `RoofType` value `"Flat, insulated (assumed)"`, which the roof **Simulation Overlay** resolves to `roof_construction_type='Flat'` with **no thickness**. The calculator then takes the U-value from the flat age-band default (`_FLAT_ROOF_BY_AGE`, RdSAP Table 18 column (3)). The stated depth is **discarded** — ~102 `property_overrides` rows.
|
||||
|
||||
The overlay's own comment asserted *"No flat RoofType value carries an explicit mm depth"*. That is a **taxonomy artefact, not a truth about the data**: the `RoofType` enum has flat members only for insulation *state* (`FLAT_INSULATED / _ASSUMED / LIMITED / NO_INSULATION`) and **no flat-thickness members** — whereas pitched has the full `PITCHED_LOFT_12MM … 400MM` ladder. So a flat depth has nowhere to land and is lost at **classification**, before the overlay ever runs.
|
||||
|
||||
Reviewing the **RdSAP 10 Specification (10-06-2025)** during grilling (issue #1361 override audit / #1376) settled the domain fact: **Table 16** ("Roof U-values when loft insulation thickness is known"), **column (1)**, is headed *"Insulation at joists at ceiling level **and flat roof**"*. A flat roof **is** scored by thickness — 50 mm → 0.68, 100 mm → 0.40, 150 mm → 0.30. Table 18 (the age-band default) is explicitly only the fallback *"used when thickness of insulation cannot be determined"*. The earlier assumption that flat roofs are age-band-only was wrong.
|
||||
|
||||
The calculator already implements this correctly: `u_roof` routes a flat roof **with** a thickness through `_ROOF_BY_THICKNESS` (which *is* Table 16 col (1)). So a flat roof given its depth scores right today — the only defect is that the depth never reaches the calculator, because the taxonomy can't carry it.
|
||||
|
||||
This is the opposite of the wall / flat "as built (assumed)" cases (ADR-0033), where **no** real datum exists and the age band is the correct lever. Here a **real measurement** exists and RdSAP scores it.
|
||||
|
||||
## Decision
|
||||
|
||||
Make a flat roof's known insulation thickness first-class, so it reaches Table 16 col (1):
|
||||
|
||||
- Add **flat-thickness members** to the `RoofType` taxonomy — `FLAT_12MM … FLAT_400_PLUS_MM` (values like `"Flat, 150 mm insulation"` — **not** "loft"; a flat roof has no loft). Minimum set for today's data: 50, 100, 150; the full ladder mirrors the pitched members for symmetry.
|
||||
- The roof **Simulation Overlay** emits `roof_insulation_thickness` for a flat roof with a known depth (reusing the existing mm regex), alongside `roof_construction_type='Flat'`, so `u_roof` reaches Table 16 col (1) instead of the age-band default.
|
||||
- **Reclassify** the existing `flat: Nmm` rows off `"Flat, insulated (assumed)"` onto the new members.
|
||||
- Correct the misleading overlay comment.
|
||||
|
||||
The flat/pitched distinction is retained even though Table 16 col (1) gives the same U at a given thickness for both — it stays load-bearing for non-U concerns (measure eligibility, shape), so flat depths are **not** collapsed onto the pitched members.
|
||||
|
||||
## Consequences
|
||||
|
||||
- ~102 flat-roof properties re-score from the age-band default to their true thickness-based U-value — **both directions** (a newer-band flat roof with 150 mm improves; an old-band flat with 50 mm may worsen relative to its default).
|
||||
- **Cross-repo (FE-owned pgEnum, Dan).** The `RoofType` `value` column is a Drizzle-owned Postgres enum (see `main-heating-system-pgenum-is-fe-owned`; PR #1361 Class A/B). The new members must be added to the pgEnum by the FE owner before the classifier-cache `value` writes; the `property_overrides` (TEXT) reclassify is immediate. So this is an **enum-dependent slice**, grouped with the other new-archetype slices of #1376 — not the no-enum roof/glazing resolver slice.
|
||||
- The reclassify follows the established one-time-script shape (dry-run default, `--apply` in a transaction, idempotent), and surfaces any members the live enum does not yet carry as deferred, exactly as Class A did.
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
# Landlord-heating classification targets a complete, modellable taxonomy; unmapped input is no-override
|
||||
|
||||
## Status
|
||||
|
||||
accepted
|
||||
|
||||
## Context
|
||||
|
||||
Landlord supplementary data describes a dwelling's components in **unbounded
|
||||
free-text** — a wall is "cavity insulated" / "CWI" / "Cav filled"; a heating
|
||||
system is "communal gas boiler" / "Quantum storage" / a boiler make. An LLM
|
||||
classifier maps that free-text onto a **closed internal taxonomy** of recognised
|
||||
descriptions, each of which binds to a Simulation Overlay applied to
|
||||
`EpcPropertyData` (ADR-0032). For heating the taxonomy is the
|
||||
`MainHeatingSystemType` enum, mapped to a representative SAP Table 4a/4b code by
|
||||
`main_heating_system_overlay`, with coherent companions dragged from the code
|
||||
(ADR-0035). The LLM is the right tool for the unbounded-free-text problem; the
|
||||
deterministic dicts in legacy `asset_list` were a *reviewed cache of LLM output*,
|
||||
not a replacement, and the `landlord_*_overrides` table already serves as that
|
||||
verified cache (keyed `portfolio_id + description`).
|
||||
|
||||
The taxonomy was **too small** — 9 heating archetypes against a ~13-family RdSAP
|
||||
main-heating taxonomy. With no correct target the classifier force-picked the
|
||||
nearest wrong archetype, and treated **"Gas CPSU" as a garbage drawer**: oil and
|
||||
solid-fuel room heaters, community heating, and `"boiler: a rated na"` all
|
||||
classified to Gas CPSU; the word "convector" in `"electric (direct acting) room
|
||||
heaters: panel, convector or radiant heaters"` pulled it to `"Electric storage
|
||||
heaters, convector"` (code 403, off-peak storage) instead of `"Electric room
|
||||
heaters"` (691, direct-acting, single-rate).
|
||||
|
||||
Folded into the Effective EPC, a single-rate dwelling modelled as off-peak
|
||||
storage scores **SAP ~10 (band G)**. This is the true cause of **PRD #1361
|
||||
Class A** — 14 properties lodged in band C/D rebaselining to band G — which had
|
||||
been mis-attributed to a fabric "no insulation (assumed)" assumption and the
|
||||
pre-SAP10 rebaseline. The walls are correct (landlord asserted as-built); the
|
||||
heating is the crater. Exemplar **property 718066**: lodged SAP 57, landlord
|
||||
described "electric (direct acting) room heaters: panel, convector or radiant"
|
||||
(correct code 691, single-rate), mis-stored as code 403 → live SAP **9.9**. The
|
||||
stored override was classified `source=classifier` on 2026-06-20, **before** the
|
||||
691 "Electric room heaters" archetype existed (commit ada2bc07, 2026-06-29) — so
|
||||
it is also stale.
|
||||
|
||||
## Decision
|
||||
|
||||
The fix is the **target taxonomy, not the mapper**. The LLM stays.
|
||||
|
||||
1. **The classifier targets a complete, modellable taxonomy.** Expand
|
||||
`MainHeatingSystemType` + `_MAIN_HEATING_CODES` to cover every RdSAP
|
||||
main-heating family the calculator can score: room heaters by fuel
|
||||
(gas / oil / solid), warm air, **heat pumps**, and **community heating**. Each
|
||||
maps to a representative Table 4a/4b code; coherent companions drag from the
|
||||
code per ADR-0035, so adding an archetype is still "just add its code".
|
||||
|
||||
2. **Unmapped free-text → `None`, never a forced archetype.** The overlay already
|
||||
returns `None` (no overlay → the lodged EPC heating stands) for any value not
|
||||
in `_MAIN_HEATING_CODES`; the classifier must *emit* Unknown/`None` when it
|
||||
cannot confidently place the text, rather than the nearest wrong archetype.
|
||||
"Gas CPSU" (or any other archetype) is never a fallback. The `None` mapping is
|
||||
persisted and surfaced to the user as "no suitable match" for later edit. A
|
||||
forced archetype overwrites a correct lodged cert; `None` (keep lodged) is the
|
||||
only safe and honest behaviour.
|
||||
|
||||
3. **Heat pumps are modellable without a PCDB index** — a "model unknown" heat
|
||||
pump maps to a default Table 4a heat-pump code (211–224) with the table's
|
||||
default seasonal efficiency; no `main_heating_index_number` is required.
|
||||
|
||||
4. **Community heating is modelled via its explicit codes** — 301 (boiler
|
||||
community) / 302 (CHP + boilers) / 304 (electric heat-pump community), which
|
||||
the catalogue strings disambiguate ("community boilers only" / "community CHP
|
||||
and boilers" / "community heat pump"). The calculator (Appendix C §C3.2
|
||||
distribution loss, Table 12c DLF, Table 12 source factors) and Bill Derivation
|
||||
(`HEAT_NETWORK` indicative rate) already support these. **Load-bearing
|
||||
assumption:** when the landlord text does not supply them, the **DLF defaults
|
||||
to Table 12c 1.50** and the source to a boiler — community SAP is *sensitive*
|
||||
to the DLF (1.0 vs 1.5 is a large swing), so this default is documented here
|
||||
and surfaced as an assumption. Coarse "communal heating" with **no named
|
||||
source** falls to `None` rather than inventing a heat-network model.
|
||||
|
||||
5. **Re-classify the stale `source=classifier` overrides** once the taxonomy is
|
||||
complete, so the band-G properties pick up their correct code.
|
||||
|
||||
## Considered Options
|
||||
|
||||
- **Replace the LLM with a deterministic catalogue map** — rejected: landlord
|
||||
input is unbounded free-text, not a closed vocabulary. The LLM is the right
|
||||
front door; determinism belongs as a downstream *verified* cache (the override
|
||||
table), built from LLM output + review, exactly as `asset_list` was.
|
||||
- **Keep the 9-member enum, tune the LLM prompt** — rejected: with no correct
|
||||
target the LLM must pick a wrong archetype regardless of prompt. Target
|
||||
completeness is the fix; prompt quality is secondary.
|
||||
- **Force unmapped input to a conservative archetype** (e.g. direct-acting
|
||||
electric) — rejected: any forced archetype overwrites a correct lodged cert.
|
||||
- **Defer community heating to `None`** (grill option b) — rejected in favour of
|
||||
modelling the three explicit community codes, since calculator + bill already
|
||||
support heat networks and the catalogue strings disambiguate the cases; only
|
||||
coarse unnamed "communal heating" falls to `None`.
|
||||
|
||||
This extends [ADR-0032](0032-landlord-override-epc-overlay.md) (the override
|
||||
overlay) and [ADR-0035](0035-coherent-heating-system-synthesis.md) (coherent
|
||||
companions drag from the code). The visible baseline shift it produces is
|
||||
correct Rebaselining per [ADR-0039](0039-override-aware-rebaselining.md). It
|
||||
**corrects the stated cause of PRD #1361 Class A** (classifier taxonomy gap, not
|
||||
fabric/rebaseline).
|
||||
|
||||
### Amendment: natural-fuel coherence
|
||||
|
||||
A room-heater / direct-acting archetype names a *system*, not its fuel — and fuel
|
||||
arrives as its own composable `main_fuel` override. Two rules keep the system
|
||||
self-coherent without coupling the two overlays:
|
||||
|
||||
- **Every archetype drags a natural fuel so the system self-coheres without a
|
||||
`main_fuel` override.** Where the fuel is unambiguous it is exact — electric
|
||||
room heaters / direct-acting / storage → electricity (RdSAP `main_fuel` 29), a
|
||||
gas boiler → mains gas (26), an oil room heater → oil (28). **Solid fuel is
|
||||
ambiguous** (coal / anthracite / smokeless / dual fuel / wood logs / pellets),
|
||||
so it **defaults to house coal (33)** — the most common solid fuel — which a
|
||||
specific `main_fuel` override then refines. (The `main_fuel` override's own
|
||||
vocabulary must grow to carry the full solid-fuel list — a parallel taxonomy
|
||||
gap, same shape as this one.)
|
||||
- **A present `main_fuel` override wins** by the applicator's last-wins
|
||||
composition (`apply_simulations` setattrs non-`None` fields in order), so the
|
||||
`main_fuel` overlay is applied **after** the heating overlay. The agreeing
|
||||
case (electric system + electricity fuel) is order-immaterial; only a
|
||||
*disagreeing* one depends on the ordering.
|
||||
- **A landlord fuel that contradicts the archetype's natural fuel is *logged*,
|
||||
not raised** (e.g. "gas" fuel on a solid-fuel room heater) — an
|
||||
override-plausibility signal (cf. the ADR-0039 scanner); what to do with it is
|
||||
deferred. The override is still honoured (the landlord wins); we only record
|
||||
the implausibility.
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
# Landlord glazing override reconciles against the cert's per-window composition, not a whole-dwelling type
|
||||
|
||||
## Context
|
||||
|
||||
A **Landlord Override** for glazing is a whole-dwelling categorical (`GlazingType` — Single / Double pre- and post-2002 / Triple / Unknown). The glazing **Simulation Overlay** resolves it to one SAP10 `glazing_type` code, and `_fold_glazing` overwrites **every** window's `glazing_type` (and clears its lodged U-/g-value) — flattening the cert to a single type.
|
||||
|
||||
The landlord descriptions are frequently **aggregate splits** ("40% double, 60% single"), and the LLM classifier collapses *any-double-present* to `"Double glazing"`. So ~319 `property_overrides` rows describing a **single-dominant** mix are scored as **fully double-glazed** — a material over-credit to the window U-values, hence to SAP.
|
||||
|
||||
The Effective EPC already carries **per-window glazing**: `sap_windows`, each with its own `glazing_type` **and** `window_width`×`window_height` (so area is derivable), plus the dwelling-level `multiple_glazed_proportion`. This is **more granular** than any whole-dwelling summary, and a whole-dwelling percentage **cannot be faithfully assigned to specific windows** — "which windows are the single ones" is unknowable from an aggregate. `_fold_glazing` already holds the EPC at apply time, so the per-window data is available where the override is applied.
|
||||
|
||||
Fixing this by "dominant type wins" would still **clobber real per-window variation** (a 60/40 dwelling flattened to one type). The faithful move is to treat the cert's per-window data as **authoritative** and the landlord aggregate as a **check** on it — the descriptions are, after all, aggregations of the same per-window split the cert records.
|
||||
|
||||
## Decision
|
||||
|
||||
The landlord glazing override **reconciles against the cert's per-window composition** instead of imposing a whole-dwelling type.
|
||||
|
||||
- **Classifier emits a proportion, not just a dominant type.** The asserted glazing **proportion** (e.g. multiple-glazed %) is extracted by the **LLM** — which is there precisely because landlord inputs vary; we do **not** hard-parse one string format.
|
||||
- **Compute the cert's actual composition** from `sap_windows`, **area-weighted** by window (bigger windows dominate the dwelling U-window).
|
||||
- **Reconcile in `_fold_glazing`** (it holds the EPC), three outcomes:
|
||||
- **Uniform assertion (~100% one type)** → apply the blanket type. Unambiguous, and a real correction (e.g. a full reglaze). Unchanged from today for clean cases.
|
||||
- **Mixed, proportion within tolerance of the cert** → **no-op**. The cert already reflects it, per-window and more precisely — leave the better data alone.
|
||||
- **Mixed, proportion materially different from the cert** → **no-op + flag**. The landlord genuinely disagrees, but per-window assignment is unknowable, so **do not fabricate a split** — surface it for review.
|
||||
- **Tolerance** is a tunable band on the area-weighted multiple-glazed %, pinned against the real distribution, not guessed.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The ~319 over-credited rows stop being flattened to double; a mix that matches the cert is left on its per-window data, and a clean uniform reglaze still applies.
|
||||
- **No new enum.** A no-op reconciliation leaves the cert glazing untouched (as `Unknown` already does). But it needs a **classifier-output change** (emit a proportion) **plus** the reconcile in the **apply seam** (`_fold_glazing`) — so it is its **own slice**, distinct from the no-enum roof party-ceiling fix that ships first.
|
||||
- Genuine landlord-vs-cert glazing disagreements are **surfaced** rather than silently trusted (old behaviour: flatten to double) or silently overwritten.
|
||||
- **Alternatives rejected.** *Dominant type wins* — still clobbers per-window variation. *Carry a full landlord composition to re-derive per-window types* — per-window assignment from an aggregate is unknowable; we decline to fabricate it. *Deterministic regex of "X% double, Y% single"* — brittle against varied input, which is the LLM's job.
|
||||
- Pairs with the per-window fidelity the calculator already relies on: the reconciliation is only correct on a faithful Effective EPC whose `sap_windows` round-trip (cf. ADR-0040).
|
||||
|
||||
## Amendment (#1376 implementation): the reconcile collapses to a `MIXED` sentinel
|
||||
|
||||
Implementing this surfaced that the full "carry a proportion → compare to the cert → flag when far" design buys less than its cost. The cert **already** carries the best-possible per-window mapping (`sap_windows`, each with its own `glazing_type`); a landlord aggregate ("40% double, 60% single") says *how much* is double but **not which windows**, so applying it would overwrite real per-window data with an arbitrary guess. Per-window assignment from an aggregate is therefore not just unknowable but strictly worse than the cert — so a mixed override **always defers**, regardless of the exact proportion. The proportion comparison only ever fed the "flag when far" branch, and carrying a proportion needs a new schema field (an FE/Drizzle dependency).
|
||||
|
||||
**Realized decision.** Add a `GlazingType.MIXED` sentinel. The LLM classifies a genuine aggregate-mix to `MIXED` (the LLM handles the varied phrasings — no regex); `MIXED` is absent from the overlay's `_GLAZING_CODES`, so it resolves to **no overlay** and `_fold_glazing` never runs — the cert's per-window glazing is preserved. A **uniform** assertion (one type ≳ 90%) still resolves to that type and is applied to every window (unambiguous — no guess). Existing mixed-as-double rows are reclassified to `MIXED` (a deterministic parse of the `%X / %Y` format is acceptable for fixing *known* rows). The "uniform vs mixed" threshold (~90% one type) is tunable, pinned against the real distribution.
|
||||
|
||||
**Consequences of the amendment.**
|
||||
- The defer behaviour needs **no apply-seam logic and no proportion field** — it falls out of "no overlay". The slice is: add the `MIXED` member, give the classifier the option, reclassify the ~319 rows.
|
||||
- `MIXED` is a **new FE-owned pgEnum value** (one member) — added by the FE owner (Dan); `property_overrides` (TEXT) reclassify runs immediately, the cache pgEnum write defers until the migration (the Class-A/B pattern).
|
||||
- The **"flag when landlord disagrees with the cert" is dropped for now** — since a mix always defers, the flag was informational only. `MIXED` is the hook if a future slice wants to surface those disagreements.
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
# Water-heating override: community DHW is scored by its heat-network main, individual DHW by its own fuel
|
||||
|
||||
## Status
|
||||
|
||||
accepted
|
||||
|
||||
## Context
|
||||
|
||||
A **Landlord Override** for water heating names a canonical `"<system>, <fuel>"`
|
||||
**Recognised Internal Description** (`WaterHeatingType`), which the water-heating
|
||||
**Simulation Overlay** (`water_heating_overlay_for`) decomposes into the SAP
|
||||
`water_heating_code` (Table 4a system) + `water_heating_fuel` int codes the
|
||||
calculator reads (ADR-0032). The taxonomy was **too small** — it had no biomass /
|
||||
wood / dual-fuel / biodiesel water fuel — so the LLM classifier funnelled every
|
||||
such description into the nearest bucket, **`"From main system, house coal"`**
|
||||
(fuel 33). An audit found **131** `property_overrides` rows on that value; only
|
||||
**3** are genuinely house coal. This is the same missing-archetype defect as
|
||||
ADR-0041 (heating) — a dumping ground standing in for a gap in the taxonomy.
|
||||
|
||||
Investigating the mis-score revealed the fix is **not** a uniform "swap coal for
|
||||
biomass". The 131 rows split by whether the dwelling's **main heating is a heat
|
||||
network**, because the calculator scores hot water differently in the two cases:
|
||||
|
||||
- **Community main** (SAP Table 4a 301/302/304). When DHW inherits from the main
|
||||
(`water_heating_code` ∈ {901, 902, 914}) and the main is a costable heat
|
||||
network, `cert_to_inputs._is_community_heating_hw_from_main` computes the hot-
|
||||
water CO2/PE/cost from the **main's** heat-network fuel (via
|
||||
`_heat_network_community_fuel_code`, e.g. biomass-community fuel 31 → Table 12
|
||||
code 43), scaled by heat-source efficiency — and **ignores
|
||||
`water_heating_fuel` entirely**. Verified: HW CO2 is `0.036` whether the water
|
||||
fuel is 33 (coal) or 31 (biomass). So the **84** community rows already score
|
||||
their DHW as biomass-heat-network; the `"house coal"` value is a **cosmetic
|
||||
display lie**, not a scoring error.
|
||||
- **Individual main** (a real boiler / room heater). DHW inherits the explicitly
|
||||
lodged `water_heating_fuel`, so `33` scores as **house coal (0.395 kgCO2/kWh)** —
|
||||
a genuine ~14× carbon overstatement for a wood/biomass dwelling. **47** rows are
|
||||
mis-scored this way (20 wood logs, 4 dual fuel, 23 "electric immersion
|
||||
assumed"); **3** genuine house-coal rows score correctly.
|
||||
|
||||
The community-fuel collision (API enum 30/31/32 collide in value with the Table-32
|
||||
electricity tariff codes 30/31/32) is **already handled** at the fuel-type
|
||||
boundary, gated on heat-network context (`_heat_network_community_fuel_code`) — a
|
||||
deliberate design that must **not** be globally canonicalised, because the cascade
|
||||
uses bare code 30 internally as standard-rate grid electricity
|
||||
(`table_32.py` comment). So **no calculator change is required**: community DHW is
|
||||
scored correctly *given the coherent community main the override data already
|
||||
carries* (`main_heating_system = "Community heating, boilers"`,
|
||||
`main_fuel = "biomass (community)"`).
|
||||
|
||||
## Decision
|
||||
|
||||
Complete the water-heating taxonomy and reclassify the dumping-ground rows to
|
||||
**faithful** archetypes. No calculator change.
|
||||
|
||||
1. **Add the missing water archetypes**, each mapping to its RdSAP
|
||||
`water_heating_fuel` code in `water_heating_overlay._WATER_HEATING_CODES`:
|
||||
- `"From main system, wood logs"` → (901, **6**)
|
||||
- `"From main system, biomass (community)"` → (901, **31**)
|
||||
- `"From main system, dual fuel (mineral and wood)"` → (901, **9**)
|
||||
- `"From main system, biodiesel (community)"` → (901, **34**)
|
||||
|
||||
2. **Community DHW maps to its community fuel (31 / 34) and is score-neutral.**
|
||||
Because the community main drives the HW score, mapping the 75 community
|
||||
biomass/biodiesel rows to their honest community fuel corrects the **displayed
|
||||
value** without changing the score — we do **not** flatten them to individual
|
||||
wood (6), which would be a semantic lie. Correct labelling, zero scoring risk.
|
||||
|
||||
3. **Individual DHW maps to its real fuel and moves the score.** Wood-logs → 6
|
||||
(0.028), dual fuel → 9, off the coal 0.395 — the genuine fix for the 47
|
||||
mis-scored individual-main rows.
|
||||
|
||||
4. **"No hot water system present – electric immersion assumed" → the existing
|
||||
`"Electric immersion, electricity"` archetype (903, 29).** The description
|
||||
states the assessment's own assumption; honouring it is faithful. This corrects
|
||||
the 23 individual-main rows (coal → electricity) and re-labels the 9
|
||||
community-main rows off the accidental biomass score onto the assessed
|
||||
immersion — a small, defensible score change, surfaced here as the one non-
|
||||
cosmetic community reclassification.
|
||||
|
||||
5. **Genuine `"From main system, house coal"` (3 rows) is retained** — the taxonomy
|
||||
keeps house coal for the dwellings that really burn it.
|
||||
|
||||
6. **Deterministic guard + reclassify, TEXT-first / pgEnum-deferred.** A pure
|
||||
`water_heating_guard(description) -> Optional[WaterHeatingType]` (mirroring
|
||||
`glazing_mix_guard`) recognises the structured `"<system>: <fuel>"` phrasings
|
||||
and is wired via `GuardedColumnClassifier(guard, fallback=ChatGpt…)`; varied
|
||||
phrasings stay with the LLM. A one-time `scripts/reclassify_water_heating.py`
|
||||
reuses the same guard as its decision function (so backfill and live path can't
|
||||
drift), updates `property_overrides.override_value` (TEXT) unconditionally, and
|
||||
defers the `landlord_water_heating_overrides.value` pgEnum-cache write for any
|
||||
value the live enum doesn't yet carry — printing the deferred set for the FE
|
||||
owner (the `main-heating-system-pgenum-is-fe-owned` constraint).
|
||||
|
||||
## Consequences
|
||||
|
||||
- **FE-owned pgEnum additions (Dan).** Four new `landlord_water_heating_overrides`
|
||||
values: `"From main system, wood logs"`, `"From main system, biomass
|
||||
(community)"`, `"From main system, dual fuel (mineral and wood)"`, `"From main
|
||||
system, biodiesel (community)"`. The TEXT read path is fixed immediately; the
|
||||
cache write for these four defers until the migration lands.
|
||||
- **No `cert_to_inputs.py` change**, so no risk to the thousands of lodged certs
|
||||
that carry water fuel 31/33/34 directly. (A separate latent question — whether a
|
||||
lodged *individual*-main cert with `water_heating_fuel = 31` or `34` mis-scores
|
||||
as electricity because those codes are un-normalised outside heat-network
|
||||
context — is **out of scope** here and tracked separately; it does not affect any
|
||||
override row, all of which resolve correctly under this decision.)
|
||||
- Extends [ADR-0041](0041-landlord-heating-classification-targets-a-complete-modellable-taxonomy.md)
|
||||
(complete taxonomy, no dumping ground) and [ADR-0035](0035-coherent-heating-system-synthesis.md)
|
||||
(the community main and its DHW cohere — here the coherence is what makes the
|
||||
community rows score correctly). The visible baseline shift on the 47 individual
|
||||
rows is correct Rebaselining ([ADR-0039](0039-override-aware-rebaselining.md)).
|
||||
|
||||
### Alternatives rejected
|
||||
|
||||
- **Flatten community biomass to individual wood (code 6).** Scores ~identically
|
||||
(0.028 vs 0.029) but mislabels a district-heating dwelling as burning its own
|
||||
wood — a semantic lie for no benefit, since the community main already scores it.
|
||||
- **A calculator "collision fix" normalising 31/33/34 globally.** Rejected: 33 is
|
||||
already normalised (`canonical_fuel_code` 33→11); 31/32 must stay un-normalised
|
||||
outside heat-network context or genuine grid electricity mis-prices as community
|
||||
waste (`table_32.py`). No override row needs it.
|
||||
- **Defer the community rows to `None`.** Rejected per ADR-0041 dec-4 (community is
|
||||
modelled, not deferred) — and unnecessary, since they already score correctly.
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
# A baseline HHRSH override drags its intrinsic SAP-409 controls, not the generic manual storage default
|
||||
|
||||
## Status
|
||||
|
||||
accepted
|
||||
|
||||
## Context
|
||||
|
||||
A **Landlord Override** naming *"High heat retention storage heaters"* (HHRSH) had
|
||||
no home in the heating taxonomy: `MainHeatingSystemType` carried
|
||||
`ELECTRIC_STORAGE_OLD` (SAP 401), `_SLIMLINE` (402), `_CONVECTOR` (403) and
|
||||
`_FAN` (404), but no High-Heat-Retention member. So the LLM classifier funnelled
|
||||
the **best modern storage** into **`"Electric storage heaters, old"` (401)** — the
|
||||
**worst** large-volume type. An audit found **111** `property_overrides` rows on
|
||||
that mapping (`'Electric Storage Systems: High heat retention storage heaters'` →
|
||||
`'Electric storage heaters, old'`). Same missing-archetype defect as ADR-0041.
|
||||
|
||||
HHRSH is a distinct, modellable RdSAP system: **SAP Table 4a code 409** (high heat
|
||||
retention storage heaters), which the calculator already scores
|
||||
(`sap_efficiencies` carries `409: 1.00`). The **HHRSH *measure*** already installs
|
||||
this exact code (`_HHR_STORAGE_SAP_CODE = 409`,
|
||||
`domain/modelling/generators/heating_recommendation.py`) on a **Dual** off-peak
|
||||
meter with **control 2404**.
|
||||
|
||||
The subtlety is the **charge control**. `main_heating_system_overlay._control_for`
|
||||
drags the **conservative manual** charge control (`2401`) for *every* storage code
|
||||
401–409, following ADR-0041's amendment ("an *unobserved* storage charge control
|
||||
defaults to the conservative manual one"). Table 4e Group 4's mean-internal-
|
||||
temperature adjustments are **manual +0.7 °C, automatic/Celect +0.4 °C, HHR 0 °C**
|
||||
— so a manual default carries a penalty. Applying it to a 409 heater would swap
|
||||
"scored as old-storage 401" for "scored as 409-but-penalised" — still under-
|
||||
crediting the 111 dwellings, and pairing 409 with manual control is not a
|
||||
combination RdSAP produces.
|
||||
|
||||
**HHRSH has a single, intrinsic charge control (2404).** The control is therefore
|
||||
*named by the archetype*, not *unobserved* — unlike a generic storage heater whose
|
||||
control genuinely is unknown. So ADR-0041's manual-default rule does not apply to
|
||||
409.
|
||||
|
||||
## Decision
|
||||
|
||||
Add the baseline HHRSH archetype and let it drag its **coherent, intrinsic**
|
||||
companions — mirroring how a gas-boiler archetype drags full modern controls
|
||||
(`_control_for`) rather than a conservative default.
|
||||
|
||||
1. **`MainHeatingSystemType.ELECTRIC_STORAGE_HIGH_HEAT_RETENTION` =
|
||||
`"Electric storage heaters, high heat retention"`**, mapped in
|
||||
`_MAIN_HEATING_CODES` to **SAP Table 4a code 409**.
|
||||
2. **Control 2404 (HHRSH controls), not the manual default.** `_control_for`
|
||||
special-cases 409 → `2404` ahead of the generic storage → `2401`. HHRSH's single
|
||||
intrinsic control is archetype-implied, an **exception to ADR-0041's
|
||||
unobserved-storage manual default** (which stands for 401–404).
|
||||
3. **Electricity fuel (RdSAP 29).** `_natural_fuel_for` drags electricity for 409,
|
||||
per ADR-0041's "storage → electricity" — so a mis-lodged non-electric fuel can't
|
||||
leave an electric-storage dwelling incoherent. Scoped to 409 ("scoped per family
|
||||
as archetypes land"); the pre-existing 401–404 fuel-drag gap is not touched here.
|
||||
4. **Dual/off-peak meter falls out of the code** — 409 is already in
|
||||
`_ASSUMED_DUAL_METER_CODES`, so the off-peak meter drags automatically (ADR-0035
|
||||
coherent companions).
|
||||
5. **Deterministic guard + reclassify, TEXT-first / pgEnum-deferred.** A
|
||||
`main_heating_guard` recognises the structured `"Electric Storage Systems: High
|
||||
heat retention storage heaters"` phrasing (wired via `GuardedColumnClassifier`);
|
||||
`scripts/reclassify_hhrsh_storage.py` reuses the same guard as its decision
|
||||
function, updates `property_overrides.override_value` (TEXT) unconditionally, and
|
||||
defers the `landlord_main_heating_system_overrides.value` pgEnum-cache write for
|
||||
the new value.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The 111 HHRSH dwellings score as **409 + HHR control 2404 + off-peak meter +
|
||||
electricity** — the efficient modern archetype — instead of the worst old
|
||||
large-volume 401. Correct Rebaselining (ADR-0039).
|
||||
- **FE-owned pgEnum addition (Dan):** one new `main_heating_system` value,
|
||||
`"Electric storage heaters, high heat retention"` (memory
|
||||
`main-heating-system-pgenum-is-fe-owned`). TEXT read path is fixed on merge; the
|
||||
cache write defers until the migration lands.
|
||||
- **Baseline vs measure stay distinct but consistent:** both use code 409 +
|
||||
control 2404; the measure additionally installs a designed HW cylinder / immersion
|
||||
end-state, which a baseline (existing-system) override does not.
|
||||
- Extends [ADR-0041](0041-landlord-heating-classification-targets-a-complete-modellable-taxonomy.md)
|
||||
(complete taxonomy; **refines** its manual-default amendment for the single-
|
||||
control HHRSH case) and [ADR-0035](0035-coherent-heating-system-synthesis.md)
|
||||
(companions drag from the code).
|
||||
|
||||
### Alternatives rejected
|
||||
|
||||
- **409 + manual control (2401), holding to ADR-0041's default.** Rejected: HHRSH's
|
||||
control is intrinsic (single option), not unobserved; the +0.7 °C manual penalty
|
||||
under-credits, and 409+manual is not a real RdSAP combination.
|
||||
- **Reuse an existing storage archetype (401–404).** Rejected: none carries the
|
||||
HHR retention/control — the whole point of the mis-score.
|
||||
- **Leave fuel unresolved (as 401–404 do).** Rejected for 409: an explicit
|
||||
electricity drag keeps the system coherent if the lodged fuel is wrong; the older
|
||||
archetypes' gap is a separate, out-of-scope cleanup.
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
# The "Gas CPSU" dumping ground splits into solid-fuel room-heater and electric-boiler archetypes
|
||||
|
||||
## Status
|
||||
|
||||
accepted
|
||||
|
||||
## Context
|
||||
|
||||
ADR-0041 named **"Gas CPSU" (SAP Table 4a 120, mains gas)** as the original
|
||||
garbage-drawer archetype the LLM classifier over-used when the heating taxonomy
|
||||
lacked a correct target. An audit of every `main_heating_system` override still
|
||||
resolving to `"Gas CPSU"` found it standing in for three *different*, unrelated
|
||||
systems — a mains-gas CPSU is neither solid fuel nor electric, so every one of
|
||||
these is mis-scored (wrong system **and**, for the electric ones, wrong fuel):
|
||||
|
||||
| description | rows | true system |
|
||||
| --- | --- | --- |
|
||||
| Solid fuel room heaters: Open fire in grate | 16 | open fire (solid) |
|
||||
| Solid fuel room heaters: Open fire with back boiler (no radiators) | 19 | open fire + back boiler |
|
||||
| Solid fuel room heaters: Closed room heater with boiler (no radiators) | 2 | closed room heater + boiler |
|
||||
| Boiler: A rated NA | 58 | **electric** boiler |
|
||||
| Boiler: A rated CPSU | 1 | electric CPSU |
|
||||
| Boiler: C rated CPSU | 1 | *genuine* gas CPSU — correct |
|
||||
|
||||
**The electric boilers are the sharpest mis-score.** All 58 `"Boiler: A rated NA"`
|
||||
rows carry a `main_fuel = electricity` override — "NA" is the Hyde boiler-type
|
||||
value meaning "not a gas combi / regular / CPSU", i.e. an **electric boiler**.
|
||||
Classified as Gas CPSU they are scored as a mains-gas stored-water unit. A
|
||||
lodged-cert check showed **defer-to-lodged is not viable**: 34 of the 58 lodge
|
||||
**no** main-heating code at all (fuel 0), 11 lodge electric *room heaters* (691),
|
||||
a few lodge community heating — the very gap the landlord override exists to fill.
|
||||
So the faithful move is to model the electric boiler the landlord names, informed
|
||||
by the electricity fuel we hold, not to defer to an empty cert.
|
||||
|
||||
The correct SAP Table 4a codes are exact (confirmed against
|
||||
`domain/sap10_ml/sap_efficiencies.py`): **631** open fire (32%), **632** open fire
|
||||
+ back boiler (50%), **634** closed room heater + boiler (65%); **191**
|
||||
direct-acting electric boiler (100%). All are already scored by the calculator,
|
||||
and 631/632/634 already sit in the overlay's `_SOLID_FUEL_ROOM_HEATER_CODES` set,
|
||||
so their coherent companions (house-coal fuel 33, room-heater control 2601,
|
||||
category 10) drag automatically (ADR-0035).
|
||||
|
||||
## Decision
|
||||
|
||||
Add the missing archetypes and reclassify the bucket. No calculator change.
|
||||
|
||||
1. **Three solid-fuel room-heater archetypes** in `_MAIN_HEATING_CODES`:
|
||||
`"Solid fuel room heater, open fire"` → 631,
|
||||
`"Solid fuel room heater, open fire with back boiler"` → 632,
|
||||
`"Solid fuel room heater, closed with boiler"` → 634. Companions drag from the
|
||||
code (already-covered 631–636 set) — no overlay-logic change.
|
||||
2. **`"Electric boiler"` → SAP 191** and **`"Electric CPSU"` → SAP 192**, both
|
||||
dragging **electricity (29)** via `_natural_fuel_for` — electric boilers are
|
||||
unambiguously electric (ADR-0041 "direct-acting/storage → electricity"), also
|
||||
closing the pre-existing gap where 191/192 dragged no fuel. 191 stays on a
|
||||
Single meter; 192 (electric CPSU) is already in `_ASSUMED_DUAL_METER_CODES`, so
|
||||
it drags an off-peak Dual meter (§12 Rule 1, 10-hour).
|
||||
3. **Deterministic `main_heating_guard` for the description-determined cases +
|
||||
reclassify**, TEXT-first / pgEnum-deferred. The guard resolves the structured
|
||||
phrasings by description alone: `"…open fire … back boiler"` → 632 (tested
|
||||
before the bare `"open fire"` → 631), `"…closed … with boiler"` → 634, and
|
||||
`"Boiler: <band> NA"` → `"Electric boiler"` (the NA type is the electric
|
||||
signal). `scripts/reclassify_main_heating.py` reuses the guard as its decision
|
||||
function.
|
||||
4. **Electric CPSU is fuel-determined, not description-determined.** `"Boiler: A/C
|
||||
rated CPSU"` is identical bar the efficiency band, so the electric/gas split
|
||||
cannot be read from the description — the guard cannot see it. The reclassify
|
||||
therefore resolves it with a **fuel-aware, per-property** step (`electric_cpsu_
|
||||
for(description, main_fuel)`): a CPSU boiler whose property carries a
|
||||
`main_fuel = electricity` override → `"Electric CPSU"` (192); a gas / unknown
|
||||
fuel stays Gas CPSU (120). Only the **TEXT** layer is updated for it — the
|
||||
description-keyed classifier cache cannot represent a fuel-dependent value. The
|
||||
**live** guard stays fuel-blind, so a new electric-CPSU intake (rare — 1 row in
|
||||
the corpus) falls to the LLM rather than being silently mis-guarded.
|
||||
|
||||
## Consequences
|
||||
|
||||
- 96 rows leave the Gas CPSU garbage drawer for faithful archetypes: 37 solid
|
||||
fuel scored as solid fuel (not mains gas), 58 electric boilers + 1 electric CPSU
|
||||
scored as electric (not a mains-gas CPSU); the 1 genuine gas CPSU stays on 120.
|
||||
Correct Rebaselining (ADR-0039).
|
||||
- **FE-owned pgEnum additions (Dan):** five new `main_heating_system` values —
|
||||
`"Solid fuel room heater, open fire"`, `"Solid fuel room heater, open fire with
|
||||
back boiler"`, `"Solid fuel room heater, closed with boiler"`, `"Electric
|
||||
boiler"`, `"Electric CPSU"`. TEXT read path fixed on merge; cache writes defer to
|
||||
the migration.
|
||||
- Extends [ADR-0041](0041-landlord-heating-classification-targets-a-complete-modellable-taxonomy.md)
|
||||
(complete taxonomy — retiring the Gas CPSU garbage drawer it named) and
|
||||
[ADR-0035](0035-coherent-heating-system-synthesis.md) (companions drag from the
|
||||
code).
|
||||
|
||||
### Out of scope
|
||||
|
||||
- **Whether "NA" ever denotes a non-electric boiler.** It is 100% electric in the
|
||||
corpus and the guard treats it as electric; a future non-electric "NA" would be
|
||||
a re-open signal, not a silent mis-map (the fuel override would disagree).
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
# Electric underfloor is scored by its own SAP codes; the meter defers to the cert when the tariff is ambiguous
|
||||
|
||||
## Status
|
||||
|
||||
accepted
|
||||
|
||||
## Context
|
||||
|
||||
Electric-underfloor `main_heating_system` overrides had no archetype, so the LLM
|
||||
classifier funnelled them into the nearest wrong bucket (#1376):
|
||||
|
||||
| description | rows | mis-mapped to |
|
||||
| --- | --- | --- |
|
||||
| Electric Underfloor Heating: In concrete slab (off-peak only) | 15 | Direct-acting electric (191), **Single** meter |
|
||||
| Electric Underfloor Heating: In screed above insulation (standard or off peak) | 63 | Direct-acting electric (191), **Single** meter |
|
||||
| Electric Underfloor Heating: Integrated (storage+direct-acting) (off peak) | 12 | Electric storage heaters, old (401) |
|
||||
|
||||
The **meter is the crux**. A Landlord heating override drags an assumed meter from
|
||||
its SAP code (ADR-0035), and the `keep_existing_off_peak_meter` mitigation
|
||||
([overlay_applicator.py](../../domain/modelling/scoring/overlay_applicator.py))
|
||||
protects a cert's lodged off-peak meter — but *only when the overlay's own desired
|
||||
meter is off-peak*. Direct-acting (191) is a **Single**-meter system, so the
|
||||
mitigation never fires and the overlay **forces Single**, overwriting the cert's
|
||||
meter. For off-peak underfloor that bills overnight heat at the peak rate and
|
||||
craters SAP — the exact Class-A meter defect.
|
||||
|
||||
SAP Table 4a has **dedicated electric-underfloor codes** (labels from
|
||||
`cert_to_inputs.py`): **421** in concrete slab (off-peak only), **422** integrated
|
||||
(storage+direct-acting), **424** in screed above insulation (423 integrated-low-
|
||||
off-peak and 425 timber-floor are not in the corpus). Per SAP 10.2 §12, **421/422
|
||||
are off-peak (Rule 2, 7-hour → Dual)**; **424 is not off-peak-implying (Single)**.
|
||||
|
||||
A lodged-meter audit is decisive for the screed case: **51 of 63 (81%)** of the
|
||||
`"…in screed (standard or off peak)"` dwellings lodge an **off-peak** meter (24-hour
|
||||
dual or 7/10-hour), the rest single. The description itself says "standard **or**
|
||||
off peak" — so the archetype genuinely cannot know the tariff; the cert does.
|
||||
Forcing 424/Single would downgrade 81%; forcing Dual would over-credit the ~19% on
|
||||
single.
|
||||
|
||||
## Decision
|
||||
|
||||
Add three electric-underfloor archetypes mapping to their own SAP codes, and treat
|
||||
the meter per what the description actually asserts.
|
||||
|
||||
1. **New archetypes** in `_MAIN_HEATING_CODES`:
|
||||
`"Electric underfloor, in concrete slab (off-peak)"` → **421**,
|
||||
`"Electric underfloor, integrated storage and direct-acting"` → **422**,
|
||||
`"Electric underfloor, in screed above insulation"` → **424**.
|
||||
2. **Electricity fuel (29)** drags for 421/422/424 via `_natural_fuel_for` — electric
|
||||
underfloor is unambiguously electric (ADR-0041), closing the prior no-fuel gap.
|
||||
3. **421 and 422 assert an off-peak Dual meter.** Their descriptions positively
|
||||
claim off-peak ("off-peak only" / "off peak"), and both are already in
|
||||
`_ASSUMED_DUAL_METER_CODES`; the `keep_existing_off_peak_meter` mitigation keeps
|
||||
a more-specific cert off-peak meter and applies Dual to a single-rate cert.
|
||||
4. **424 (screed) defers the meter to the cert.** Because "standard or off peak" is
|
||||
tariff-agnostic and 81% lodge off-peak, `_meter_for` returns **`None`** for 424
|
||||
(a new meter-agnostic set), so the overlay leaves `meter_type` unset and the
|
||||
lodged meter stands — each dwelling scores on its actual tariff. This is a
|
||||
**targeted exception to the "always set the meter, never let it bleed"
|
||||
invariant**: that invariant guards against a switched-*off* storage system
|
||||
leaving a stale Dual meter, but an underfloor override *confirms* underfloor —
|
||||
there is no switch-off, so deferring is faithful, not a bleed.
|
||||
5. **Deterministic `main_heating_guard` + reclassify** (TEXT-first, pgEnum-deferred),
|
||||
reusing the generalized `reclassify_main_heating.py`. All three underfloor
|
||||
descriptions are description-determined, so the guard resolves them directly (no
|
||||
fuel-awareness needed) and the reclassify follows.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The 15 concrete-slab-off-peak rows stop being billed at the peak rate (191/Single
|
||||
→ 421/Dual); the 12 integrated rows get the correct underfloor code (401 → 422,
|
||||
meter already Dual); the 63 screed rows keep their lodged (81% off-peak) meter and
|
||||
score on code 424 with correct screed responsiveness (Table 4d R=0.75). Correct
|
||||
Rebaselining (ADR-0039).
|
||||
- **FE-owned pgEnum additions (Dan):** three new `main_heating_system` values —
|
||||
`"Electric underfloor, in concrete slab (off-peak)"`, `"Electric underfloor,
|
||||
integrated storage and direct-acting"`, `"Electric underfloor, in screed above
|
||||
insulation"`. TEXT read path fixed on merge; cache writes defer to the migration.
|
||||
- `_meter_for` becomes `Optional[str]` (a code may defer the meter). Every existing
|
||||
code keeps its current Single/Dual answer; only 424 returns `None`.
|
||||
- Extends [ADR-0041](0041-landlord-heating-classification-targets-a-complete-modellable-taxonomy.md)
|
||||
and [ADR-0035](0035-coherent-heating-system-synthesis.md); refines the
|
||||
`keep_existing_off_peak_meter` behaviour ADR-0035 introduced.
|
||||
|
||||
### Alternatives rejected
|
||||
|
||||
- **Force Single (424 per spec).** Rejected: downgrades the 81% lodged off-peak,
|
||||
reproducing the meter bug.
|
||||
- **Force Dual for screed.** Rejected: over-credits the ~19% genuinely on single and
|
||||
asserts a tariff the "standard or off peak" description does not claim.
|
||||
- **Map underfloor to Direct-acting (191) / old storage (401) as today.** Rejected:
|
||||
wrong code (responsiveness/meter) and the source of the mis-score.
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
# Roof Eligibility Resolves Sentinel Thickness by Age Band (vaulted joins sloping; ND ≠ NI)
|
||||
|
||||
Property 742265 (uprn 10033526327, cert 2780-3922-3202-6042-9200) lodges a
|
||||
`Pitched (vaulted ceiling)` roof (gov-API `roof_construction` 5) with
|
||||
`roof_insulation_thickness = "ND"` and got **no roof Recommendation**, leaving
|
||||
its plan short of the scenario's goal band on an unlimited budget. Two
|
||||
generator gaps compound (ADR-0021's dispatch table has no vaulted row, and its
|
||||
uninsulated trigger `!= 0` compares the `"ND"` *string* against int 0, so it
|
||||
never fires), while the SAP10 Calculation already handles both: it treats
|
||||
vaulted as a slope-following ceiling and resolves the `"ND"` sentinel via the
|
||||
roof description / age-band defaults (`heat_transmission.py` — the
|
||||
`is_sloping_ceiling` disjunction and the raw-sentinel inspection). The
|
||||
generator diverged from the calculator — the exact split ADR-0021 set out to
|
||||
prevent.
|
||||
|
||||
## Decision
|
||||
|
||||
**Amends ADR-0021.** The roof Recommendation Generator changes in two ways;
|
||||
the mapper and the calculator change in none.
|
||||
|
||||
**1. Vaulted is a sloping ceiling.** The dispatch matches the same substring
|
||||
disjunction the calculator uses (`"sloping ceiling" in roof_type or "vaulted"
|
||||
in roof_type`): a vaulted ceiling has no loft void, so its one applicable
|
||||
Measure is `sloping_ceiling_insulation` (rafters, 100 mm) — never the loft
|
||||
fallback. ADR-0021's dispatch table gains the row:
|
||||
|
||||
| Roof type (`roof_construction_type` substring) | Measure | Uninsulated trigger | Overlay |
|
||||
|---|---|---|---|
|
||||
| `sloping ceiling` **or `vaulted`** | `sloping_ceiling_insulation` | resolved thickness 0 (below) | → 100 mm |
|
||||
|
||||
**2. Sentinel-aware uninsulated trigger, resolved by age band — codes, not
|
||||
descriptions.** `roof_insulation_thickness` is `Optional[Union[str, int]]`; a
|
||||
lodged string sentinel is data, not noise. The generator resolves eligibility
|
||||
for the two pitched branches (sloping/vaulted and loft) as:
|
||||
|
||||
| Lodged value | Meaning (RdSAP 10) | Eligibility |
|
||||
|---|---|---|
|
||||
| int `0` | explicitly uninsulated | eligible (unchanged) |
|
||||
| int `> 0` | measured thickness | not eligible (unchanged) |
|
||||
| `"NI"` | insulation **present**, thickness unknown (§5.11.4: assume 50 mm) | **not eligible** |
|
||||
| `"ND"` / `None` | not defined — resolve to the age band's **as-built** state | eligible iff as-built uninsulated (bands **A–D**) |
|
||||
|
||||
Age-band as-built states (construction age code): **A–D uninsulated · E–F
|
||||
limited insulation · G+ insulated.** Only A–D resolves to eligible. This
|
||||
extends the codebase's existing precedent (`_PRE_1950_AGE_CODES = {A,B,C,D}`
|
||||
in the mapper's code-8 rule; pinned by cert 001479 Ext2 at U=2.30) from the
|
||||
scoring path to eligibility.
|
||||
|
||||
**Sentinel resolution requires a KNOWN pitched/thatched roof type.** An
|
||||
unlodged `roof_construction_type` reaching the loft fallback can be a **party
|
||||
ceiling** — gov-API codes 6/7 ("(another dwelling above)" / thatch with
|
||||
additional insulation) map the type to `None`, and real code-7 certs lodge
|
||||
`"ND"` — with zero roof heat loss (RdSAP 10 Table 18), so insulating it
|
||||
contradicts the dwelling. An unlodged type therefore keeps the explicit-0-only
|
||||
trigger (the pre-ADR behaviour for that shape); only a type containing
|
||||
`pitched`/`thatch` (or the sloping/vaulted branch, known by construction)
|
||||
resolves sentinels by age band.
|
||||
|
||||
**Coherence rule (load-bearing):** sentinel resolution must never make an
|
||||
undefined cert *more* eligible than an explicitly-lodged one. An explicit
|
||||
50 mm lodgement is not eligible today, so `"ND"` on a band-E/F ("limited
|
||||
insulation") dwelling must also resolve to not-eligible — recommending a
|
||||
top-up to the dwelling whose insulation we *don't* know while withholding it
|
||||
from the identical dwelling whose thin insulation we *do* know would be
|
||||
incoherent. Whether limited-insulation **top-up** should be a Measure at all
|
||||
is a separate product question that applies equally to explicit thin
|
||||
lodgements; it is out of scope here (see Consequences).
|
||||
|
||||
**The mapper stays untouched — the `"ND"` passthrough is load-bearing.** The
|
||||
calculator inspects the *raw* lodgement to distinguish explicit `int(0)`
|
||||
(drop the description; uninsulated) from `"NI"` (keep the description; §5.11.4
|
||||
retrofit-50 mm) from `"ND"` (defer to description / age default). Normalising
|
||||
sentinels at the mapper boundary would change scoring inputs for every ND-roof
|
||||
cert in the corpus for zero eligibility benefit.
|
||||
|
||||
**Two homes for sentinel resolution, deliberately.** Scoring asks "what
|
||||
thickness should the U-cascade see" (`NI` → 50 mm); eligibility asks "is this
|
||||
roof a candidate" (`NI` → no). Same sentinel, different questions, different
|
||||
answers — a single shared resolver would just relocate the split into its call
|
||||
sites. Each home carries a regression test pinning the *disagreement* as
|
||||
intentional. Consolidate only if a third consumer appears (rule of three).
|
||||
|
||||
## Considered options
|
||||
|
||||
- **Normalise `ND` → 0 in the mapper** (extend the code-8 rule to code 5 and
|
||||
to the `"ND"` string). Rejected: changes calculator inputs corpus-wide —
|
||||
`roof_thickness_explicitly_zero` would fire and drop the roof description
|
||||
the §5.11 resolution depends on — risking the pinned Elmhurst accuracy
|
||||
corpus to fix an eligibility bug.
|
||||
- **Defer to `roofs[].description`** ("no insulation (assumed)") for ND
|
||||
resolution, mirroring the calculator exactly. Rejected: eligibility should
|
||||
key on codes, not free-text description parsing; descriptions drift by
|
||||
lodgement software and the age-band code carries the same as-built signal
|
||||
deterministically.
|
||||
- **Gate eligibility on the calculator's effective roof U-value.** Rejected:
|
||||
no other generator gates on scored output (walls/floors gate on lodged
|
||||
attributes), and it re-couples eligibility to scoring against ADR-0016's
|
||||
separation.
|
||||
- **Treat `"ND"` alone as uninsulated** (sentinel-literal). Rejected: ND means
|
||||
*not defined*, not *none* — a band-G ND roof is almost certainly insulated
|
||||
as built and would get a junk Recommendation.
|
||||
|
||||
## Consequences
|
||||
|
||||
- 742265 (band B, vaulted, ND) becomes eligible: `sloping_ceiling_insulation`
|
||||
over 55.82 m² (~£2.2k, verified by local re-model). Any pitched-loft cert
|
||||
lodging `"ND"` with age band A–D is likewise un-withheld.
|
||||
- The **flat branch is deliberately untouched**: its inverted convention
|
||||
(`None` = uninsulated, per the Elmhurst mapping) means an `"ND"` flat roof
|
||||
is *not eligible* today and stays so — a separately-verifiable follow-up,
|
||||
not silently folded into this change.
|
||||
- **Open follow-up (product):** should limited-insulation dwellings (explicit
|
||||
thin lodgements *and* ND + bands E–F alike) be offered an insulation
|
||||
top-up? Today neither is; changing that is a trigger redesign, not a
|
||||
sentinel question.
|
||||
- ADR-0021's "one dispatching generator, one Measure per roof" and its
|
||||
dispatch order (sloping → flat → no-access → loft) are unchanged; the
|
||||
sloping test simply matches one more substring, ahead of the fallbacks as
|
||||
before.
|
||||
|
|
@ -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,76 @@
|
|||
# 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 (probed on
|
||||
portfolio 796: 0/300 sampled certs lodge one, so no live evidence yet);
|
||||
(b) roof/floor generators have the same anti-pattern in principle (roof
|
||||
age-band eligibility is partly covered by ADR-0047). **Amended:** the floor
|
||||
arm landed as `floor_u_value_gate` (probed live: solid_floor_insulation at
|
||||
£3-4k for +0.00 on age-band-L dwellings 709782/709790); a 40-dwelling
|
||||
band-K/L probe found no roof no-ops, so the roof arm stays unimplemented
|
||||
until evidence appears.
|
||||
|
||||
### 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.
|
||||
|
|
@ -18,10 +18,10 @@ straight lift-and-shift of the columns below.
|
|||
|---|---|---|
|
||||
| `id` | serial PK | |
|
||||
| `property_id` | int, FK → `property.id`, **unique** | one Baseline Performance per Property |
|
||||
| `lodged_sap_score` | int | Lodged Performance — gov register, off the Effective EPC |
|
||||
| `lodged_epc_band` | text | the `Epc` enum, stored as its string value (e.g. `"C"`) |
|
||||
| `lodged_co2_emissions_t_per_yr` | float | tonnes CO₂/yr (whole dwelling) |
|
||||
| `lodged_primary_energy_intensity_kwh_per_m2_yr` | int | PEUI (kWh/m²/yr); **not** "heat demand" — see CONTEXT.md |
|
||||
| `lodged_sap_score` | int, **nullable** | Lodged Performance — gov register, off the Effective EPC. **NULL for a predicted Property** (no lodged cert — #1361, see below) |
|
||||
| `lodged_epc_band` | text, **nullable** | the `Epc` enum, stored as its string value (e.g. `"C"`) |
|
||||
| `lodged_co2_emissions_t_per_yr` | float, **nullable** | tonnes CO₂/yr (whole dwelling) |
|
||||
| `lodged_primary_energy_intensity_kwh_per_m2_yr` | int, **nullable** | PEUI (kWh/m²/yr); **not** "heat demand" — see CONTEXT.md |
|
||||
| `effective_sap_score` | int | Effective Performance — what modelling scored against |
|
||||
| `effective_epc_band` | text | |
|
||||
| `effective_co2_emissions_t_per_yr` | float | tonnes CO₂/yr (whole dwelling) |
|
||||
|
|
@ -30,6 +30,21 @@ straight lift-and-shift of the columns below.
|
|||
| `space_heating_kwh` | float | EPC `renewable_heat_incentive` recorded demand. **Superseded** by `heating_kwh` (delivered) when the bill block populates; kept until then to avoid an empty-kWh gap, dropped in the population slice. |
|
||||
| `water_heating_kwh` | float | EPC `renewable_heat_incentive`; **superseded** by `hot_water_kwh`. |
|
||||
|
||||
### Lodged half is nullable (#1361 Class B, 2026-06-30)
|
||||
|
||||
The four `lodged_*` columns are **nullable**. A **predicted** Property (EPC Prediction, ADR-0029/0031:
|
||||
no lodged cert, its `EpcPropertyData` synthesised from neighbours) has **no Lodged Performance** — the
|
||||
backend now writes `lodged_* = NULL` for it and persists only the Effective half (ADR-0004 amendment).
|
||||
Reading the synthesised EPC's recorded fields as a lodged figure had been manufacturing a phantom (a
|
||||
neighbour's SAP), which the migration + a one-time backfill remove.
|
||||
|
||||
**FE-owned Drizzle migration required:** `ALTER TABLE property_baseline_performance ALTER COLUMN
|
||||
lodged_sap_score DROP NOT NULL` (and the same for `lodged_epc_band`, `lodged_co2_emissions_t_per_yr`,
|
||||
`lodged_primary_energy_intensity_kwh_per_m2_yr`). This **must land before** the backend deploys the
|
||||
orchestrator change or runs `scripts/null_predicted_lodged_performance.py` — a `NULL` write against a
|
||||
`NOT NULL` column aborts the batch (ADR-0012). The backfill NULLs the four columns on the ~12,236
|
||||
existing predicted-source rows; Effective, the bill block, and `rebaseline_reason` are left intact.
|
||||
|
||||
### Bill block (ADR-0014) — the energy bill, composed per section
|
||||
|
||||
Produced by **Bill Derivation**: the calculator's **delivered** kWh per end use priced at current
|
||||
|
|
|
|||
0
domain/abri/__init__.py
Normal file
0
domain/abri/__init__.py
Normal file
18
domain/abri/descriptions.py
Normal file
18
domain/abri/descriptions.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class JobDescriptions:
|
||||
short_description: str
|
||||
long_description: str
|
||||
|
||||
|
||||
def build_job_descriptions(deal_name: str) -> JobDescriptions:
|
||||
# Placeholder copy — final wording is owned by Abri's schedulers (issue #1455).
|
||||
return JobDescriptions(
|
||||
short_description=f"Domna condition survey - {deal_name}",
|
||||
long_description=(
|
||||
f"Domna condition survey visit at {deal_name}, "
|
||||
"booked by Domna operations."
|
||||
),
|
||||
)
|
||||
32
domain/abri/models.py
Normal file
32
domain/abri/models.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
from dataclasses import dataclass
|
||||
from datetime import date
|
||||
from typing import Literal, NewType, Union
|
||||
|
||||
PlaceRef = NewType("PlaceRef", str)
|
||||
|
||||
SlotCode = Literal["AM", "PM", "AD"]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LogJobRequest:
|
||||
client_ref: str
|
||||
place_ref: PlaceRef
|
||||
appointment_date: date
|
||||
appointment_time: SlotCode
|
||||
short_description: str
|
||||
long_description: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class JobLogged:
|
||||
job_no: str
|
||||
logged_info: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AbriRequestRejected:
|
||||
code: str
|
||||
message: str
|
||||
|
||||
|
||||
LogJobResult = Union[JobLogged, AbriRequestRejected]
|
||||
13
domain/abri/slots.py
Normal file
13
domain/abri/slots.py
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
from datetime import datetime, time
|
||||
from typing import Optional
|
||||
|
||||
from domain.abri.models import SlotCode
|
||||
|
||||
_MIDDAY = time(12, 0)
|
||||
|
||||
|
||||
def slot_for_confirmed_time(confirmed_time: Optional[str]) -> SlotCode:
|
||||
if confirmed_time is None or confirmed_time.strip() == "":
|
||||
return "AD"
|
||||
parsed = datetime.strptime(confirmed_time.strip(), "%H:%M").time()
|
||||
return "AM" if parsed < _MIDDAY else "PM"
|
||||
42
domain/data_transformation/guarded_column_classifier.py
Normal file
42
domain/data_transformation/guarded_column_classifier.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
from typing import Callable, Optional, TypeVar
|
||||
|
||||
from domain.data_transformation.column_classifier import ColumnClassifier
|
||||
|
||||
E = TypeVar("E", bound=Enum)
|
||||
|
||||
|
||||
class GuardedColumnClassifier(ColumnClassifier[E]):
|
||||
"""A ``ColumnClassifier`` that resolves the descriptions a deterministic guard
|
||||
is certain about, and delegates the rest to a fallback classifier.
|
||||
|
||||
The ``guard`` maps a raw description to a category member when it recognises it
|
||||
deterministically (e.g. a party-ceiling roof marker — #1376), else ``None``.
|
||||
Guard hits never reach the fallback, so an unreliable classifier (the LLM)
|
||||
cannot override a description the guard is sure of — and the LLM is not billed
|
||||
for it. Every description still appears in the result (guarded or fallen-back).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
guard: Callable[[str], Optional[E]],
|
||||
fallback: ColumnClassifier[E],
|
||||
) -> None:
|
||||
self._guard = guard
|
||||
self._fallback = fallback
|
||||
|
||||
def classify(self, descriptions: set[str]) -> dict[str, E]:
|
||||
guarded: dict[str, E] = {}
|
||||
misses: set[str] = set()
|
||||
for description in descriptions:
|
||||
member = self._guard(description)
|
||||
if member is not None:
|
||||
guarded[description] = member
|
||||
else:
|
||||
misses.add(description)
|
||||
# Only the misses reach the fallback — a fully-guarded batch never calls it.
|
||||
if misses:
|
||||
guarded.update(self._fallback.classify(misses))
|
||||
return guarded
|
||||
|
|
@ -24,6 +24,7 @@ _GLAZING_CODES: dict[str, int] = {
|
|||
"Double glazing, pre-2002": 3,
|
||||
"Triple glazing, pre-2002": 6,
|
||||
"Triple glazing, 2002 or later": 9,
|
||||
"Secondary glazing": 5,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -28,12 +28,16 @@ _FUEL_CODES: dict[str, int] = {
|
|||
"house coal": 33,
|
||||
"smokeless coal": 15,
|
||||
"dual fuel (mineral and wood)": 10,
|
||||
# Individual wood-logs solid fuel — API fuel enum 6 (RdSAP Table 32 code 20,
|
||||
# 0.028 kgCO2e/kWh), matching the water overlay's `wood logs` fuel. Distinct
|
||||
# from biomass (community) 31; the LLM funnelled it there for want of a member.
|
||||
"wood logs": 6,
|
||||
"biomass (community)": 31,
|
||||
}
|
||||
|
||||
# A "mains gas" main fuel asserts the dwelling has a mains-gas connection, so the
|
||||
# overlay must also flip `sap_energy_source.mains_gas` — not just the fuel code.
|
||||
# Without it the effective EPC says "fuel = mains gas" while `mains_gas` stays
|
||||
# overlay must also flip `sap_energy_source.gas_connection_available` — not just the fuel code.
|
||||
# Without it the effective EPC says "fuel = mains gas" while `gas_connection_available` stays
|
||||
# False, which (a) gates out the gas-boiler-upgrade Measure and (b) makes the
|
||||
# heating Generator read the dwelling as off-gas and wrongly offer HHRSH storage
|
||||
# (property 728513). Only the **private** mains-gas connection (code 26) sets it;
|
||||
|
|
@ -51,7 +55,7 @@ def fuel_overlay_for(
|
|||
code = _FUEL_CODES.get(main_fuel_value)
|
||||
if code is None:
|
||||
return None
|
||||
mains_gas = True if main_fuel_value in _MAINS_GAS_FUEL_VALUES else None
|
||||
gas_connection_available = True if main_fuel_value in _MAINS_GAS_FUEL_VALUES else None
|
||||
return EpcSimulation(
|
||||
heating=HeatingOverlay(main_fuel_type=code, mains_gas=mains_gas)
|
||||
heating=HeatingOverlay(main_fuel_type=code, gas_connection_available=gas_connection_available)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,10 +12,11 @@ field-wise with the main_fuel / water_heating overlays.
|
|||
electricity tariff (meter) and, for storage heaters, its charge control. Rather
|
||||
than hand-attach those per archetype (easy to forget when a new system is
|
||||
added), they are **derived from the SAP code**: the off-peak meter from the
|
||||
calculator's single off-peak classification (`OFF_PEAK_IMPLYING_HEATING_CODES`,
|
||||
SAP §12), and the conservative manual charge control for storage heaters. So
|
||||
adding a heating archetype is just adding its code — coherent companions fall
|
||||
out. Synthesis owns coherence; the calculator never normalises a lodged cert.
|
||||
overlay's assumed-Dual classification (`_ASSUMED_DUAL_METER_CODES` — the §12
|
||||
off-peak systems plus all-electric room-heater dwellings), and the conservative
|
||||
manual charge control for storage heaters. So adding a heating archetype is just
|
||||
adding its code — coherent companions fall out. Synthesis owns coherence; the
|
||||
calculator never normalises a lodged cert.
|
||||
|
||||
The SEDBUK A-G efficiency band the Hyde "Heating" column carries is NOT honoured
|
||||
yet (no efficiency slot on the overlay/MainHeatingDetail) -- archetypes map to
|
||||
|
|
@ -27,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
|
||||
|
|
@ -34,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).
|
||||
|
|
@ -44,6 +48,23 @@ _OFF_PEAK_METER = "Dual"
|
|||
# meter bleed through and bill the new gas/direct-acting system on an Economy-7
|
||||
# split (the mirror of the storage→Dual drag, ADR-0035).
|
||||
_SINGLE_RATE_METER = "Single"
|
||||
# Meter-agnostic codes: the archetype cannot determine the tariff, so the overlay
|
||||
# defers `meter_type` to the cert (`_meter_for` → None). Screed underfloor (424) is
|
||||
# "standard or off peak" — 81% of the corpus lodge off-peak (ADR-0046).
|
||||
_METER_AGNOSTIC_CODES = frozenset({424})
|
||||
|
||||
# Electric room heaters (SAP Table 4a 691). They don't *require* off-peak the way
|
||||
# storage/CPSU do, so they're absent from the calculator's §12
|
||||
# `OFF_PEAK_IMPLYING_HEATING_CODES` (Rules 1-2). But a dwelling heated by them is
|
||||
# all-electric and realistically billed on Economy 7 — its immersion hot water
|
||||
# charges overnight and §12 Rule 3 gives the room heaters a 10-hour off-peak
|
||||
# window. So when the landlord names only the system, the coherent meter to
|
||||
# assume is Dual; the §12 dispatch then applies the realistic high/low split
|
||||
# (not a single-rate over-penalty, nor an all-low over-credit).
|
||||
_ROOM_HEATER_CODES = frozenset({691})
|
||||
# Codes for which the overlay assumes a Dual (off-peak) meter: the §12-mandated
|
||||
# off-peak systems plus the all-electric room-heater dwellings above.
|
||||
_ASSUMED_DUAL_METER_CODES = OFF_PEAK_IMPLYING_HEATING_CODES | _ROOM_HEATER_CODES
|
||||
|
||||
# SAP Table 4e Group 4 storage charge-control code. Manual charge control is the
|
||||
# *conservative* assumption when the landlord didn't tell us the control: its
|
||||
|
|
@ -53,6 +74,94 @@ _SINGLE_RATE_METER = "Single"
|
|||
# systems that take a charge control.
|
||||
_MANUAL_CHARGE_CONTROL = 2401
|
||||
_STORAGE_HEATER_CODES = frozenset(range(401, 410))
|
||||
# SAP Table 4a "electric storage heaters" category. Set on a storage-system
|
||||
# override so the overlaid cert is internally consistent (code 401-409 ↔ category
|
||||
# 7) — mirroring the control/fuel/meter companions the overlay already drags. A
|
||||
# storage code with a stale non-storage category is a latent trap: the SAP 10.2
|
||||
# Table 12a resolver classifies by category, so a leftover room-heater category
|
||||
# 10 would price the off-peak storage heating at the peak rate.
|
||||
_STORAGE_HEATER_CATEGORY = 7
|
||||
# High heat retention storage heaters (SAP Table 4a 409) take a SINGLE intrinsic
|
||||
# charge control — Table 4e 2404 (HHR, 0 C adjustment). It is named by the
|
||||
# archetype, not unobserved, so 409 drags 2404 rather than the manual default
|
||||
# (ADR-0044) — the only storage code that overrides `_MANUAL_CHARGE_CONTROL`.
|
||||
_HHR_STORAGE_CODE = 409
|
||||
_HHR_CHARGE_CONTROL = 2404
|
||||
|
||||
# SAP Table 4a category 10 ("Room heaters") and its conservative Table 4e Group 6
|
||||
# control. A landlord names a room heater, not its control; code 2601 ("no
|
||||
# thermostatic control of room temperature") is the lowest-SAP room-heater
|
||||
# control — the modal one real solid-fuel room-heater certs lodge — so it never
|
||||
# over-credits an unobserved control (the room-heater mirror of the storage
|
||||
# manual-charge default). Scoped per family as archetypes land; solid-fuel room
|
||||
# heaters are Table 4a 631-636.
|
||||
_ROOM_HEATER_CATEGORY = 10
|
||||
_ROOM_HEATER_CONTROL = 2601
|
||||
_SOLID_FUEL_ROOM_HEATER_CODES = frozenset(range(631, 637))
|
||||
# Oil room heaters (SAP Table 4a 621-625) — category 10, conservative room-heater
|
||||
# control, natural fuel heating oil (RdSAP main_fuel 28).
|
||||
_OIL_ROOM_HEATER_CODES = frozenset(range(621, 626))
|
||||
_OIL_FUEL = 28
|
||||
# Gas (incl. LPG) room heaters (SAP Table 4a 601-613) — category 10, conservative
|
||||
# control, natural fuel mains gas (26); an LPG dwelling is refined by a main_fuel
|
||||
# override (the overlay can't see the mains connection).
|
||||
_GAS_ROOM_HEATER_CODES = frozenset(range(601, 614))
|
||||
# Fuel-burning room heaters (solid + oil + gas) share category 10 + the
|
||||
# conservative room-heater control; only the natural fuel differs by family.
|
||||
# (Distinct from `_ROOM_HEATER_CODES` above, the electric-room-heater dual-meter
|
||||
# set.)
|
||||
_CATEGORY_10_ROOM_HEATER_CODES = (
|
||||
_SOLID_FUEL_ROOM_HEATER_CODES | _OIL_ROOM_HEATER_CODES | _GAS_ROOM_HEATER_CODES
|
||||
)
|
||||
|
||||
# Natural-fuel coherence (ADR-0041): where an archetype unambiguously implies a
|
||||
# fuel, the overlay drags it so a system-only override is self-coherent on a cert
|
||||
# that lodged a different fuel. A later `main_fuel` override still wins (last-wins
|
||||
# composition); a contradicting landlord fuel is logged, not silently overridden.
|
||||
# Electric room heaters (Table 4a 691-701) are unambiguously electricity (RdSAP
|
||||
# main_fuel code 29). Solid fuel is ambiguous (coal / anthracite / smokeless /
|
||||
# dual fuel / wood logs / pellets), so it defaults to house coal (33) — the most
|
||||
# common solid fuel — when the landlord gives no `main_fuel` override; a specific
|
||||
# solid-fuel override still wins by last-wins composition.
|
||||
_ELECTRICITY_FUEL = 29
|
||||
_HOUSE_COAL_FUEL = 33
|
||||
_ELECTRIC_ROOM_HEATER_CODES = frozenset(range(691, 702))
|
||||
# Electric boilers — SAP Table 4a 191 (direct-acting) and 192 (electric CPSU) —
|
||||
# unambiguously electric, so they drag electricity like the electric room heaters
|
||||
# (ADR-0045, closing the prior no-fuel gap on 191/192).
|
||||
_ELECTRIC_BOILER_CODES = frozenset({191, 192})
|
||||
# Electric underfloor — SAP Table 4a 421/422 (off-peak) and 424 (screed) — also
|
||||
# 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).
|
||||
_HEAT_PUMP_CATEGORY = 4
|
||||
_HEAT_PUMP_CODES = frozenset(range(211, 225)) | frozenset(range(521, 528))
|
||||
|
||||
# Community / heat-network heating (SAP Table 4a 301-304) is category 6; the
|
||||
# calculator's `_is_heat_network` keys off code OR category 6. The boiler-driven
|
||||
# schemes (301/302/303) are dominantly mains gas (community) — RdSAP main_fuel 20
|
||||
# — per real community certs; the heat-pump scheme (304) is electricity
|
||||
# (community), added with its archetype (ADR-0041).
|
||||
_HEAT_NETWORK_CATEGORY = 6
|
||||
_HEAT_NETWORK_CODES = frozenset({301, 302, 303, 304})
|
||||
_COMMUNITY_BOILER_CODES = frozenset({301, 302, 303})
|
||||
_COMMUNITY_GAS_FUEL = 20
|
||||
|
||||
# SAP Table 4c full boiler-control code: programmer + room thermostat + TRVs. The
|
||||
# landlord names the boiler, not its controls — but a gas boiler installed under
|
||||
|
|
@ -80,9 +189,11 @@ _FROM_MAIN_WATER_HEATING_CODE = 901
|
|||
# Canonical system archetype → representative SAP `sap_main_heating_code`. Codes
|
||||
# map to the modern/condensing variant (A-G efficiency deferred): 102 regular
|
||||
# condensing, 104 condensing combi, 120 CPSU, 401-404 storage heaters, 191
|
||||
# direct-acting electric. Companion fields (meter / control / fuel / hot water)
|
||||
# are NOT listed here — they are derived from the code below, so a new archetype
|
||||
# is just a code (ADR-0035 drag-along).
|
||||
# direct-acting electric, 691 panel/convector/radiant electric room heaters
|
||||
# (Table 4a — direct-acting, so a single-rate meter, NOT off-peak storage).
|
||||
# Companion fields (meter / control / fuel / hot water) are NOT listed here —
|
||||
# they are derived from the code below, so a new archetype is just a code
|
||||
# (ADR-0035 drag-along).
|
||||
_MAIN_HEATING_CODES: dict[str, int] = {
|
||||
"Gas boiler, combi": 104,
|
||||
"Gas boiler, regular": 102,
|
||||
|
|
@ -91,15 +202,73 @@ _MAIN_HEATING_CODES: dict[str, int] = {
|
|||
"Electric storage heaters, slimline": 402,
|
||||
"Electric storage heaters, convector": 403,
|
||||
"Electric storage heaters, fan": 404,
|
||||
# High heat retention storage heaters — SAP Table 4a 409. Its intrinsic HHR
|
||||
# charge control (2404) and electricity fuel drag from the code below, not the
|
||||
# generic manual-storage default (ADR-0044).
|
||||
"Electric storage heaters, high heat retention": 409,
|
||||
"Direct-acting electric": 191,
|
||||
# A-rated electric boiler off the Gas CPSU dumping ground — SAP Table 4a 191
|
||||
# (direct-acting electric boiler). Unambiguously electric, so it drags
|
||||
# electricity below (ADR-0045).
|
||||
"Electric boiler": 191,
|
||||
# Electric CPSU — SAP Table 4a 192. Electric (drags 29), on an off-peak Dual
|
||||
# meter (already in _ASSUMED_DUAL_METER_CODES; §12 Rule 1 10-hour) — ADR-0045.
|
||||
"Electric CPSU": 192,
|
||||
# Electric underfloor — dedicated SAP Table 4a codes, all electric (drag 29).
|
||||
# 421 concrete slab / 422 integrated are off-peak (Dual, §12 Rule 2); 424
|
||||
# screed is tariff-ambiguous ("standard or off peak") so it DEFERS the meter to
|
||||
# the cert (ADR-0046).
|
||||
"Electric underfloor, in concrete slab (off-peak)": 421,
|
||||
"Electric underfloor, integrated storage and direct-acting": 422,
|
||||
"Electric underfloor, in screed above insulation": 424,
|
||||
"Electric room heaters": 691,
|
||||
"Solid fuel room heater, closed": 633,
|
||||
# Solid-fuel room heaters off the Gas CPSU dumping ground (ADR-0045). SAP
|
||||
# Table 4a: 631 open fire, 632 open fire + back boiler, 634 closed + boiler.
|
||||
# Companions (house-coal fuel 33, room-heater control 2601, category 10) drag
|
||||
# from the code — 631/632/634 are already in the 631-636 solid-fuel set.
|
||||
"Solid fuel room heater, open fire": 631,
|
||||
"Solid fuel room heater, open fire with back boiler": 632,
|
||||
"Solid fuel room heater, closed with boiler": 634,
|
||||
# Default air-source heat pump — SAP Table 4a 211 ("flow temp in other
|
||||
# cases", default SPF 2.30). Modellable without a PCDB index; an actual
|
||||
# product index would refine it (ADR-0041).
|
||||
"Air source heat pump": 211,
|
||||
# Boiler-driven community / heat-network heating — SAP Table 4a 301. The
|
||||
# calculator derives the heat-source efficiency (80%) + DLF (1.50 default)
|
||||
# from the code (cert_to_inputs). A generic "Community heating" with no named
|
||||
# source stays unmapped (-> None) — the source can't be assumed (ADR-0041).
|
||||
"Community heating, boilers": 301,
|
||||
# Community CHP + boilers — SAP Table 4a 302 (heat network, category 6).
|
||||
"Community heating, CHP and boilers": 302,
|
||||
# Modern standalone oil room heater — SAP Table 4a 623 ("Oil room heater,
|
||||
# 2000 or later", no boiler). Natural fuel heating oil (ADR-0041).
|
||||
"Oil room heater, 2000 or later": 623,
|
||||
# Gas room heaters — each catalogue variant to its own SAP Table 4a code
|
||||
# (wide efficiency spread, so no single representative). Natural fuel mains
|
||||
# gas; an LPG dwelling is refined by a `main_fuel` override (ADR-0041).
|
||||
"Gas room heater, condensing fire": 611,
|
||||
"Gas room heater, decorative fuel-effect": 612,
|
||||
"Gas room heater, flush live-effect": 605,
|
||||
"Gas room heater, open flue 1980 or later": 603,
|
||||
"Gas room heater, open flue pre-1980": 601,
|
||||
}
|
||||
|
||||
|
||||
def _meter_for(code: int) -> str:
|
||||
def _meter_for(code: int) -> Optional[str]:
|
||||
"""The coherent meter a heating code implies: an off-peak ("Dual") meter for
|
||||
the calculator's §12 off-peak systems, an explicit single-rate ("Single")
|
||||
meter for every other system. Always set — never left to bleed."""
|
||||
return _OFF_PEAK_METER if code in OFF_PEAK_IMPLYING_HEATING_CODES else _SINGLE_RATE_METER
|
||||
the §12 off-peak systems and all-electric room-heater dwellings
|
||||
(`_ASSUMED_DUAL_METER_CODES`), an explicit single-rate ("Single") meter for
|
||||
every other system — set explicitly so it never bleeds.
|
||||
|
||||
Exception: a **meter-agnostic** code (`_METER_AGNOSTIC_CODES` — screed
|
||||
underfloor 424, "standard or off peak") returns ``None`` so the overlay leaves
|
||||
`meter_type` unset and the cert's lodged tariff stands. The archetype genuinely
|
||||
cannot know the tariff, and unlike a switched-off storage system it is not a
|
||||
re-metering, so deferring is faithful, not a bleed (ADR-0046)."""
|
||||
if code in _METER_AGNOSTIC_CODES:
|
||||
return None
|
||||
return _OFF_PEAK_METER if code in _ASSUMED_DUAL_METER_CODES else _SINGLE_RATE_METER
|
||||
|
||||
|
||||
def _control_for(code: int) -> Optional[int]:
|
||||
|
|
@ -107,10 +276,61 @@ def _control_for(code: int) -> Optional[int]:
|
|||
conservative manual charge control for storage heaters, full modern controls
|
||||
for a gas boiler, None for systems that take neither (direct-acting electric).
|
||||
Overwrites a stale control inherited from the system being replaced."""
|
||||
if code == _HHR_STORAGE_CODE:
|
||||
return _HHR_CHARGE_CONTROL
|
||||
if code in _STORAGE_HEATER_CODES:
|
||||
return _MANUAL_CHARGE_CONTROL
|
||||
if code in _GAS_BOILER_CODES:
|
||||
return _FULL_BOILER_CONTROL
|
||||
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
|
||||
|
||||
|
||||
def _category_for(code: int) -> Optional[int]:
|
||||
"""The SAP Table 4a heating category a code implies, where the archetype
|
||||
fixes it. Set so a system-only override reads as the new system's category,
|
||||
not whatever category the replaced system carried — room heaters are 10,
|
||||
electric storage heaters (incl. HHR 409) are 7, heat pumps 4, heat networks
|
||||
6. Setting storage → 7 keeps the overlaid cert consistent (code 401-409 ↔
|
||||
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 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:
|
||||
return _HEAT_NETWORK_CATEGORY
|
||||
return None
|
||||
|
||||
|
||||
def _natural_fuel_for(code: int) -> Optional[int]:
|
||||
"""The fuel an archetype unambiguously implies (a coherent default), or None
|
||||
where the fuel is ambiguous and must come from the `main_fuel` override. A
|
||||
present `main_fuel` override wins by last-wins composition."""
|
||||
if (
|
||||
code in _ELECTRIC_ROOM_HEATER_CODES
|
||||
or code in _HEAT_PUMP_CODES
|
||||
or code == _HHR_STORAGE_CODE
|
||||
or code in _ELECTRIC_BOILER_CODES
|
||||
or code in _ELECTRIC_UNDERFLOOR_CODES
|
||||
):
|
||||
return _ELECTRICITY_FUEL
|
||||
if code in _SOLID_FUEL_ROOM_HEATER_CODES:
|
||||
return _HOUSE_COAL_FUEL
|
||||
if code in _OIL_ROOM_HEATER_CODES:
|
||||
return _OIL_FUEL
|
||||
if code in _GAS_ROOM_HEATER_CODES:
|
||||
return _MAINS_GAS_FUEL
|
||||
if code in _COMMUNITY_BOILER_CODES:
|
||||
return _COMMUNITY_GAS_FUEL
|
||||
return None
|
||||
|
||||
|
||||
|
|
@ -123,7 +343,7 @@ def _gas_boiler_overlay(code: int) -> HeatingOverlay:
|
|||
sap_main_heating_code=code,
|
||||
main_heating_category=_GAS_BOILER_CATEGORY,
|
||||
main_fuel_type=_MAINS_GAS_FUEL,
|
||||
mains_gas=True,
|
||||
gas_connection_available=True,
|
||||
main_heating_control=_FULL_BOILER_CONTROL,
|
||||
fan_flue_present=True,
|
||||
meter_type=_SINGLE_RATE_METER,
|
||||
|
|
@ -133,6 +353,16 @@ def _gas_boiler_overlay(code: int) -> HeatingOverlay:
|
|||
)
|
||||
|
||||
|
||||
def natural_fuel_for(main_heating_value: str) -> Optional[int]:
|
||||
"""The RdSAP `main_fuel` code a heating archetype unambiguously implies — its
|
||||
natural fuel (ADR-0041) — or None for an unmapped archetype. Exposed so a
|
||||
plausibility check can compare it against a landlord `main_fuel` override."""
|
||||
code = _MAIN_HEATING_CODES.get(main_heating_value)
|
||||
if code is None:
|
||||
return None
|
||||
return _natural_fuel_for(code)
|
||||
|
||||
|
||||
def main_heating_overlay_for(
|
||||
main_heating_value: str, building_part: int
|
||||
) -> Optional[EpcSimulation]:
|
||||
|
|
@ -141,10 +371,36 @@ 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,
|
||||
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,
|
||||
# build their HeatingOverlay directly and leave this False.
|
||||
keep_existing_off_peak_meter=True,
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ from datatypes.epc.domain.epc_property_data import BuildingPartIdentifier
|
|||
from domain.modelling.simulation import BuildingPartOverlay, EpcSimulation
|
||||
|
||||
_LOFT_MM = re.compile(r"(\d+)\+?\s*mm loft insulation")
|
||||
# A flat roof carries no loft, so its depth reads "N mm insulation" (no "loft").
|
||||
_FLAT_MM = re.compile(r"(\d+)\+?\s*mm insulation")
|
||||
|
||||
|
||||
def roof_overlay_for(
|
||||
|
|
@ -49,8 +51,18 @@ def _overlay_for(roof_type_value: str) -> Optional[BuildingPartOverlay]:
|
|||
if match is not None:
|
||||
return BuildingPartOverlay(roof_insulation_thickness=int(match.group(1)))
|
||||
if roof_type_value.startswith("Flat,"):
|
||||
# Flat roof: U-value is the age-band default; flag the shape and let the
|
||||
# age-band overlay drive `_FLAT_ROOF_BY_AGE` (ADR-0033).
|
||||
flat_depth = _FLAT_MM.search(roof_type_value)
|
||||
if flat_depth is not None:
|
||||
# A flat roof with a known depth: RdSAP 10 Table 16 col (1) ("joists at
|
||||
# ceiling level and flat roof") scores it by thickness, so carry the
|
||||
# depth (the calculator routes flat + thickness through Table 16, not
|
||||
# the age-band default). ADR-0041.
|
||||
return BuildingPartOverlay(
|
||||
roof_construction_type="Flat",
|
||||
roof_insulation_thickness=int(flat_depth.group(1)),
|
||||
)
|
||||
# Flat roof with no stated depth: U-value is the age-band default; flag the
|
||||
# shape and let the age-band overlay drive `_FLAT_ROOF_BY_AGE` (ADR-0033).
|
||||
return BuildingPartOverlay(roof_construction_type="Flat")
|
||||
if roof_type_value == "Pitched, Unknown loft insulation":
|
||||
# Unknown loft depth: U-value is the pitched age-band default. Assert the
|
||||
|
|
|
|||
|
|
@ -31,6 +31,9 @@ _MATERIAL_CONSTRUCTION: dict[str, int] = {
|
|||
"Curtain Wall": 9,
|
||||
}
|
||||
|
||||
# RdSAP `WALL_SYSTEM_BUILT` — shares code 6 with the gov-EPC basement sentinel.
|
||||
_WALL_SYSTEM_BUILT = 6
|
||||
|
||||
# RdSAP `wall_insulation_type` codes by insulation-state suffix
|
||||
# (domain/sap10_ml/rdsap_uvalues.py): external 1, filled-cavity 2, internal 3,
|
||||
# as-built/uninsulated 4, cavity+external 6, cavity+internal 7.
|
||||
|
|
@ -66,11 +69,18 @@ def wall_overlay_for(
|
|||
if building_part == 0
|
||||
else BuildingPartIdentifier.extension(building_part)
|
||||
)
|
||||
# System-built is RdSAP code 6, which collides with the gov-EPC code-6
|
||||
# basement sentinel that `main_wall_is_basement` falls back to. A landlord
|
||||
# naming a System-built wall is asserting the material, not a basement — pin
|
||||
# the flag False so the override isn't mis-read as one (ADR-0033 / the
|
||||
# overlay mirror of the mapper's `_clear_basement_flag_when_system_built`).
|
||||
wall_is_basement = False if construction == _WALL_SYSTEM_BUILT else None
|
||||
return EpcSimulation(
|
||||
building_parts={
|
||||
identifier: BuildingPartOverlay(
|
||||
wall_construction=construction,
|
||||
wall_insulation_type=insulation,
|
||||
wall_is_basement=wall_is_basement,
|
||||
)
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -26,6 +26,18 @@ _WATER_HEATING_CODES: dict[str, tuple[int, int]] = {
|
|||
"From main system, LPG (bulk)": (901, 27),
|
||||
"From main system, bottled LPG": (901, 3),
|
||||
"From main system, house coal": (901, 33),
|
||||
# Biomass / wood water fuels routed off the house-coal dumping ground
|
||||
# (ADR-0043), all from main (901) with their own RdSAP water_heating_fuel:
|
||||
# 6 wood logs (biomass ~0.028 kgCO2/kWh vs house coal 33 ~0.395)
|
||||
# 9 dual fuel (mineral + wood)
|
||||
# 31 biomass (community) — scored via the coherent community main; the
|
||||
# fuel is still emitted so an individual main would score biomass
|
||||
# 34 biodiesel (community)
|
||||
# A wood-fired individual-main dwelling's DHW must never fall to coal 33.
|
||||
"From main system, wood logs": (901, 6),
|
||||
"From main system, dual fuel (mineral and wood)": (901, 9),
|
||||
"From main system, biomass (community)": (901, 31),
|
||||
"From main system, biodiesel (community)": (901, 34),
|
||||
"Electric immersion, electricity": (903, 29),
|
||||
# "boiler/circulator for water heating only" — SAP Table 4a code 911 (gas).
|
||||
"Gas boiler/circulator, mains gas": (911, 26),
|
||||
|
|
|
|||
82
domain/epc/property_overrides/glazing_mix_guard.py
Normal file
82
domain/epc/property_overrides/glazing_mix_guard.py
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Optional
|
||||
|
||||
from domain.epc.property_overrides.glazing_type import GlazingType
|
||||
|
||||
# "N% <base glazing type> [era]" — the base type decides whether it is a mix; the
|
||||
# era ("2002 or later" / "pre-2002") is captured too, so a dominant double/triple
|
||||
# can be resolved deterministically when its era is stated (see the guard below).
|
||||
# An absent or non-canonical era ("unknown age", "(SAP 9.94)") leaves the era group
|
||||
# empty, so that dominant split stays the LLM's job.
|
||||
_PERCENT_TYPE = re.compile(
|
||||
r"(\d+)%\s*(single|double|triple|secondary)(?:\s*glazing)?"
|
||||
r"(?:[\s,]*(pre-?2002|2002 or later))?"
|
||||
)
|
||||
# One base type at or above this share is a uniform assertion, not a mix — applied
|
||||
# as that type (a near-uniform reglaze), not deferred.
|
||||
_UNIFORM_THRESHOLD_PERCENT = 90
|
||||
# A dominant base type + stated era → the fully-determined canonical member.
|
||||
# Single and secondary glazing are era-free, so they need no era. Double/triple
|
||||
# with no (or non-canonical) era is absent here and falls through to the LLM.
|
||||
_DOMINANT_MEMBER: dict[tuple[str, Optional[str]], GlazingType] = {
|
||||
("single", None): GlazingType.SINGLE,
|
||||
("secondary", None): GlazingType.SECONDARY,
|
||||
("double", "2002 or later"): GlazingType.DOUBLE_POST_2002,
|
||||
("double", "pre-2002"): GlazingType.DOUBLE_PRE_2002,
|
||||
("triple", "2002 or later"): GlazingType.TRIPLE_POST_2002,
|
||||
("triple", "pre-2002"): GlazingType.TRIPLE_PRE_2002,
|
||||
}
|
||||
|
||||
|
||||
def _canonical_era(era: str) -> Optional[str]:
|
||||
"""Normalise a matched era phrase to the key used in ``_DOMINANT_MEMBER``."""
|
||||
if not era:
|
||||
return None
|
||||
return "2002 or later" if "2002 or later" in era else "pre-2002"
|
||||
|
||||
|
||||
def glazing_mix_guard(description: str) -> Optional[GlazingType]:
|
||||
"""Deterministically resolve an aggregate-mix glazing description to ``MIXED``.
|
||||
|
||||
A landlord glazing description is often a whole-dwelling proportion split
|
||||
("40% double glazing 2002 or later, 60% single glazing"). Such a mix cannot be
|
||||
faithfully applied per-window — it says *how much* is each type, not *which*
|
||||
windows — and the cert already carries the true per-window glazing, so a mix
|
||||
must resolve to ``GlazingType.MIXED`` (no overlay → the cert is kept), never to
|
||||
a single dominant type that would flatten every window (ADR-0042).
|
||||
|
||||
Recognises the structured ``"N% <type>, M% <type>"`` split and returns:
|
||||
|
||||
* ``MIXED`` for a genuine mix — two or more glazing types present and none
|
||||
dominant (≥ the uniform threshold);
|
||||
* the fully-determined member for a dominant near-uniform split whose type
|
||||
carries no era ambiguity — single or secondary glazing (era-free), or a
|
||||
dominant double/triple whose era is stated ("pre-2002" / "2002 or later");
|
||||
the LLM would otherwise flatten such a split onto its *minority* type;
|
||||
* ``None`` otherwise — an unparseable description, or a dominant double/triple
|
||||
with an unstated/non-canonical era ("unknown age") that still carries genuine
|
||||
era ambiguity — so the LLM classifier resolves it.
|
||||
"""
|
||||
matches = _PERCENT_TYPE.findall(description.lower())
|
||||
base_types = {base_type for _, base_type, _ in matches}
|
||||
if len(base_types) < 2:
|
||||
# One (or no) glazing type named — a uniform assertion, not a mix.
|
||||
return None
|
||||
dominant_percent, dominant_type, dominant_era = max(
|
||||
((int(percent), base_type, era) for percent, base_type, era in matches),
|
||||
key=lambda triple: triple[0],
|
||||
)
|
||||
if dominant_percent >= _UNIFORM_THRESHOLD_PERCENT:
|
||||
# One type dominates — a near-uniform assertion, not a mix. A split is fully
|
||||
# determined (and safe to apply deterministically) when the dominant type
|
||||
# carries no era ambiguity: single glazing is era-free, and a dominant
|
||||
# double/triple whose era is stated ("pre-2002" / "2002 or later") is
|
||||
# equally pinned. In those cases claim the member ourselves — the LLM
|
||||
# otherwise flattens the split onto the *minority* type. A dominant
|
||||
# double/triple with an unstated or non-canonical era ("unknown age") still
|
||||
# carries genuine era ambiguity, so it stays the LLM's job (falls through to
|
||||
# ``None``).
|
||||
return _DOMINANT_MEMBER.get((dominant_type, _canonical_era(dominant_era)))
|
||||
return GlazingType.MIXED
|
||||
|
|
@ -21,4 +21,15 @@ class GlazingType(Enum):
|
|||
DOUBLE_PRE_2002 = "Double glazing, pre-2002"
|
||||
TRIPLE_PRE_2002 = "Triple glazing, pre-2002"
|
||||
TRIPLE_POST_2002 = "Triple glazing, 2002 or later"
|
||||
# An aggregate mix of glazing types across the dwelling ("40% double, 60%
|
||||
# single"). Deliberately absent from the overlay's `_GLAZING_CODES`, so it
|
||||
# resolves to no overlay and the cert's per-window `sap_windows` are kept
|
||||
# rather than flattened to one type — a whole-dwelling proportion cannot say
|
||||
# which windows are which (ADR-0042 amendment, #1376). The FE-owned pgEnum
|
||||
# gains this member via a Drizzle migration.
|
||||
MIXED = "Mixed glazing"
|
||||
# Secondary glazing (a pane added inside the original window) is era-free like
|
||||
# SINGLE — SAP10 glazing code 5, U = 2.9 regardless of install year. The
|
||||
# FE-owned pgEnum gains this member via assessment-model#345.
|
||||
SECONDARY = "Secondary glazing"
|
||||
UNKNOWN = "Unknown"
|
||||
|
|
|
|||
28
domain/epc/property_overrides/main_fuel_guard.py
Normal file
28
domain/epc/property_overrides/main_fuel_guard.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from domain.epc.property_overrides.main_fuel_type import MainFuelType
|
||||
|
||||
|
||||
def main_fuel_guard(description: str) -> Optional[MainFuelType]:
|
||||
"""Deterministically resolve an individual wood-logs main fuel, which the LLM
|
||||
funnelled into ``"biomass (community)"`` for want of a wood-logs member (#1376).
|
||||
|
||||
A landlord main-fuel description arrives as ``"<family>: <fuel>"`` (e.g.
|
||||
``"Solid Fuel: Wood Logs"``). With no individual wood-logs archetype the
|
||||
classifier force-picked ``BIOMASS_COMMUNITY`` — inventing a community heat
|
||||
network the dwelling isn't on (0.028 kgCO2e/kWh is right by luck, but the
|
||||
community flag mis-costs it and mislabels the connection). Wood logs is a real
|
||||
RdSAP solid fuel (API enum 6 / Table 32 code 20), so this guard claims it.
|
||||
|
||||
Mirrors ``water_heating_guard`` (which already rescues the same fuel token for
|
||||
DHW, ADR-0043). Returns ``None`` for fuels the LLM already classifies correctly
|
||||
(mains gas, electricity, oil, dual fuel, genuine community biomass) and for
|
||||
varied phrasings, so those still reach the LLM via ``GuardedColumnClassifier``.
|
||||
``"dual fuel: mineral and wood"`` contains ``"wood"`` but not ``"wood log"``,
|
||||
so it keeps its own ``DUAL_FUEL_MINERAL_WOOD`` member.
|
||||
"""
|
||||
if "wood log" in description.lower():
|
||||
return MainFuelType.WOOD_LOGS
|
||||
return None
|
||||
|
|
@ -25,5 +25,6 @@ class MainFuelType(Enum):
|
|||
HOUSE_COAL = "house coal"
|
||||
SMOKELESS_COAL = "smokeless coal"
|
||||
DUAL_FUEL_MINERAL_WOOD = "dual fuel (mineral and wood)"
|
||||
WOOD_LOGS = "wood logs"
|
||||
BIOMASS_COMMUNITY = "biomass (community)"
|
||||
UNKNOWN = "Unknown"
|
||||
|
|
|
|||
53
domain/epc/property_overrides/main_heating_guard.py
Normal file
53
domain/epc/property_overrides/main_heating_guard.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from domain.epc.property_overrides.main_heating_system_type import (
|
||||
MainHeatingSystemType,
|
||||
)
|
||||
|
||||
|
||||
def main_heating_guard(description: str) -> Optional[MainHeatingSystemType]:
|
||||
"""Deterministically resolve the structured main-heating descriptions the LLM
|
||||
funnelled into a garbage-drawer archetype for want of a correct target (#1376).
|
||||
|
||||
Two dumping grounds are rescued:
|
||||
|
||||
- **HHRSH** (ADR-0044): ``"…High heat retention storage heaters"`` went to
|
||||
``"Electric storage heaters, old"`` (SAP 401) — the *worst* old type. HHRSH is
|
||||
SAP Table 4a 409 with its own intrinsic charge control (2404).
|
||||
- **Gas CPSU** (ADR-0045): the mains-gas CPSU garbage drawer also held
|
||||
solid-fuel room heaters (open fire 631 / + back boiler 632 / closed + boiler
|
||||
634) and electric ``"NA"``-type boilers (SAP 191). "NA" is the boiler-type
|
||||
value meaning "not a gas combi/regular/CPSU", i.e. an electric boiler.
|
||||
- **Electric underfloor** (ADR-0046): dumped into Direct-acting (191) / old
|
||||
storage (401) for want of the dedicated SAP underfloor codes — in concrete
|
||||
slab (421, off-peak), integrated storage+direct-acting (422), in screed (424,
|
||||
meter deferred to the cert).
|
||||
|
||||
Returns ``None`` for descriptions the LLM already classifies correctly (genuine
|
||||
gas boilers, other storage subtypes, a plain closed room heater) and for varied
|
||||
phrasings, so they still reach the LLM via ``GuardedColumnClassifier``. Order
|
||||
matters: the back-boiler open fire is tested before the bare open fire it
|
||||
contains.
|
||||
"""
|
||||
text = description.lower()
|
||||
if "high heat retention" in text:
|
||||
return MainHeatingSystemType.ELECTRIC_STORAGE_HIGH_HEAT_RETENTION
|
||||
if "underfloor" in text:
|
||||
if "concrete slab" in text:
|
||||
return MainHeatingSystemType.ELECTRIC_UNDERFLOOR_SLAB_OFF_PEAK
|
||||
if "integrated" in text:
|
||||
return MainHeatingSystemType.ELECTRIC_UNDERFLOOR_INTEGRATED
|
||||
if "screed" in text:
|
||||
return MainHeatingSystemType.ELECTRIC_UNDERFLOOR_SCREED
|
||||
return None
|
||||
if "rated na" in text:
|
||||
return MainHeatingSystemType.ELECTRIC_BOILER
|
||||
if "open fire" in text and "back boiler" in text:
|
||||
return MainHeatingSystemType.SOLID_FUEL_ROOM_HEATER_OPEN_FIRE_BACK_BOILER
|
||||
if "open fire" in text:
|
||||
return MainHeatingSystemType.SOLID_FUEL_ROOM_HEATER_OPEN_FIRE
|
||||
if "closed room heater" in text and "with boiler" in text:
|
||||
return MainHeatingSystemType.SOLID_FUEL_ROOM_HEATER_CLOSED_WITH_BOILER
|
||||
return None
|
||||
|
|
@ -23,5 +23,35 @@ class MainHeatingSystemType(Enum):
|
|||
ELECTRIC_STORAGE_SLIMLINE = "Electric storage heaters, slimline"
|
||||
ELECTRIC_STORAGE_CONVECTOR = "Electric storage heaters, convector"
|
||||
ELECTRIC_STORAGE_FAN = "Electric storage heaters, fan"
|
||||
ELECTRIC_STORAGE_HIGH_HEAT_RETENTION = (
|
||||
"Electric storage heaters, high heat retention"
|
||||
)
|
||||
DIRECT_ELECTRIC = "Direct-acting electric"
|
||||
ELECTRIC_BOILER = "Electric boiler"
|
||||
ELECTRIC_CPSU = "Electric CPSU"
|
||||
ELECTRIC_UNDERFLOOR_SLAB_OFF_PEAK = (
|
||||
"Electric underfloor, in concrete slab (off-peak)"
|
||||
)
|
||||
ELECTRIC_UNDERFLOOR_INTEGRATED = (
|
||||
"Electric underfloor, integrated storage and direct-acting"
|
||||
)
|
||||
ELECTRIC_UNDERFLOOR_SCREED = "Electric underfloor, in screed above insulation"
|
||||
ELECTRIC_ROOM_HEATERS = "Electric room heaters"
|
||||
SOLID_FUEL_ROOM_HEATER_CLOSED = "Solid fuel room heater, closed"
|
||||
SOLID_FUEL_ROOM_HEATER_OPEN_FIRE = "Solid fuel room heater, open fire"
|
||||
SOLID_FUEL_ROOM_HEATER_OPEN_FIRE_BACK_BOILER = (
|
||||
"Solid fuel room heater, open fire with back boiler"
|
||||
)
|
||||
SOLID_FUEL_ROOM_HEATER_CLOSED_WITH_BOILER = (
|
||||
"Solid fuel room heater, closed with boiler"
|
||||
)
|
||||
AIR_SOURCE_HEAT_PUMP = "Air source heat pump"
|
||||
COMMUNITY_BOILERS = "Community heating, boilers"
|
||||
COMMUNITY_CHP_AND_BOILERS = "Community heating, CHP and boilers"
|
||||
OIL_ROOM_HEATER_POST_2000 = "Oil room heater, 2000 or later"
|
||||
GAS_FIRE_CONDENSING = "Gas room heater, condensing fire"
|
||||
GAS_FIRE_DECORATIVE = "Gas room heater, decorative fuel-effect"
|
||||
GAS_FIRE_FLUSH_LIVE_EFFECT = "Gas room heater, flush live-effect"
|
||||
GAS_FIRE_OPEN_FLUE_POST_1980 = "Gas room heater, open flue 1980 or later"
|
||||
GAS_FIRE_OPEN_FLUE_PRE_1980 = "Gas room heater, open flue pre-1980"
|
||||
UNKNOWN = "Unknown"
|
||||
|
|
|
|||
32
domain/epc/property_overrides/property_type_guard.py
Normal file
32
domain/epc/property_overrides/property_type_guard.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from domain.epc.property_overrides.property_type import PropertyType
|
||||
|
||||
# The landlord property-type description is a "<dwelling type>: <built form>: …"
|
||||
# split (e.g. "Bungalow: EndTerrace"). The dwelling type is the leading token and
|
||||
# is independent of the built-form tail — which the LLM sometimes over-reads,
|
||||
# mislabelling a "Bungalow: EndTerrace" as House (#1376).
|
||||
_LEADING_TYPE: dict[str, PropertyType] = {
|
||||
"house": PropertyType.HOUSE,
|
||||
"bungalow": PropertyType.BUNGALOW,
|
||||
"flat": PropertyType.FLAT,
|
||||
"maisonette": PropertyType.MAISONETTE,
|
||||
"park home": PropertyType.PARK_HOME,
|
||||
}
|
||||
|
||||
|
||||
def property_type_guard(description: str) -> Optional[PropertyType]:
|
||||
"""Deterministically resolve the dwelling type from the leading token of a
|
||||
landlord property-type description.
|
||||
|
||||
The description is a structured ``"<dwelling type>: <built form>: <floor>"``
|
||||
split whose first token is the dwelling type; the tail (EndTerrace, Mid Floor,
|
||||
…) is built form, not type. This guard claims the recognised leading tokens so
|
||||
the built-form tail can never flip the type (the LLM read "Bungalow: EndTerrace"
|
||||
as House). Returns ``None`` for an unrecognised leading token, so varied
|
||||
phrasings still reach the LLM classifier via ``GuardedColumnClassifier``.
|
||||
"""
|
||||
leading = description.split(":", 1)[0].strip().lower()
|
||||
return _LEADING_TYPE.get(leading)
|
||||
43
domain/epc/property_overrides/roof_party_ceiling_guard.py
Normal file
43
domain/epc/property_overrides/roof_party_ceiling_guard.py
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Optional
|
||||
|
||||
from domain.epc.property_overrides.roof_type import RoofType
|
||||
|
||||
# The roof-type token (before any ``: <insulation>`` suffix), normalised to lower
|
||||
# alphanumerics, → its party-ceiling RoofType member. Normalising strips spacing
|
||||
# and case so both ``"another dwelling above"`` and ``"anotherdwellingabove"``
|
||||
# match the same marker.
|
||||
_PARTY_CEILING_MARKERS: dict[str, RoofType] = {
|
||||
"anotherdwellingabove": RoofType.ADJACENT_ANOTHER_DWELLING_ABOVE,
|
||||
"samedwellingabove": RoofType.ADJACENT_SAME_DWELLING_ABOVE,
|
||||
"otherpremisesabove": RoofType.ADJACENT_OTHER_PREMISES_ABOVE,
|
||||
# Both the "(another premises above)" adjacency and the redundant "Another
|
||||
# Premises Above" taxonomy member normalise here; map to the parenthesised
|
||||
# family (both resolve to no overlay, so there is no scoring difference).
|
||||
"anotherpremisesabove": RoofType.ADJACENT_ANOTHER_PREMISES_ABOVE,
|
||||
}
|
||||
|
||||
|
||||
def _normalise_roof_token(description: str) -> str:
|
||||
token = description.split(":", 1)[0]
|
||||
return re.sub(r"[^a-z0-9]", "", token.lower())
|
||||
|
||||
|
||||
def roof_party_ceiling_guard(description: str) -> Optional[RoofType]:
|
||||
"""Deterministically resolve a party-ceiling roof description to its RoofType.
|
||||
|
||||
A building part whose top boundary is another (or the same) dwelling / premises
|
||||
above has ~0 heat loss (RdSAP 10 Table 18: "There is no heat loss through the
|
||||
roof of a building part that has the same dwelling or another dwelling above"),
|
||||
so it must resolve to a party-ceiling `RoofType` member — which the roof
|
||||
Simulation Overlay leaves as no overlay, keeping the lodged EPC — and never to
|
||||
an external `Pitched` / `Flat` roof.
|
||||
|
||||
Recognises the party-ceiling markers regardless of a trailing insulation token
|
||||
(``: 100mm`` / ``: unknown``) or spacing/case, and returns ``None`` for anything
|
||||
that is not a party-ceiling marker so the LLM classifier still handles it
|
||||
(#1376).
|
||||
"""
|
||||
return _PARTY_CEILING_MARKERS.get(_normalise_roof_token(description))
|
||||
|
|
@ -22,6 +22,28 @@ class RoofType(Enum):
|
|||
FLAT_NO_INSULATION = "Flat, no insulation"
|
||||
FLAT_NO_INSULATION_ASSUMED = "Flat, no insulation (assumed)"
|
||||
|
||||
# A flat roof with a known insulation depth. RdSAP 10 Table 16 col (1)
|
||||
# ("joists at ceiling level and flat roof") scores flat roofs by thickness, so
|
||||
# these carry the depth into the overlay (mirroring the PITCHED_LOFT_* ladder;
|
||||
# no "loft" — a flat roof has none). ADR-0041. The value column is an FE-owned
|
||||
# pgEnum, so these members are added to the Drizzle enum by the FE owner.
|
||||
FLAT_12MM = "Flat, 12 mm insulation"
|
||||
FLAT_25MM = "Flat, 25 mm insulation"
|
||||
FLAT_50MM = "Flat, 50 mm insulation"
|
||||
FLAT_75MM = "Flat, 75 mm insulation"
|
||||
FLAT_100MM = "Flat, 100 mm insulation"
|
||||
FLAT_125MM = "Flat, 125 mm insulation"
|
||||
FLAT_150MM = "Flat, 150 mm insulation"
|
||||
FLAT_175MM = "Flat, 175 mm insulation"
|
||||
FLAT_200MM = "Flat, 200 mm insulation"
|
||||
FLAT_225MM = "Flat, 225 mm insulation"
|
||||
FLAT_250MM = "Flat, 250 mm insulation"
|
||||
FLAT_270MM = "Flat, 270 mm insulation"
|
||||
FLAT_300MM = "Flat, 300 mm insulation"
|
||||
FLAT_350MM = "Flat, 350 mm insulation"
|
||||
FLAT_400MM = "Flat, 400 mm insulation"
|
||||
FLAT_400_PLUS = "Flat, 400+ mm insulation"
|
||||
|
||||
PITCHED_INSULATED = "Pitched, insulated"
|
||||
PITCHED_INSULATED_ASSUMED = "Pitched, insulated (assumed)"
|
||||
PITCHED_INSULATED_AT_RAFTERS = "Pitched, insulated at rafters"
|
||||
|
|
|
|||
41
domain/epc/property_overrides/water_heating_guard.py
Normal file
41
domain/epc/property_overrides/water_heating_guard.py
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from domain.epc.property_overrides.water_heating_type import WaterHeatingType
|
||||
|
||||
|
||||
def water_heating_guard(description: str) -> Optional[WaterHeatingType]:
|
||||
"""Deterministically resolve the structured water-heating descriptions the LLM
|
||||
funnelled into the ``"From main system, house coal"`` dumping ground (#1376).
|
||||
|
||||
Landlord water-heating descriptions arrive as ``"<system>: <fuel>"`` (e.g.
|
||||
``"From main heating system: Wood Logs"``). Where the taxonomy had no biomass /
|
||||
wood / dual-fuel / biodiesel water fuel, the classifier force-picked house coal,
|
||||
scoring a biomass dwelling's DHW at ~14x its carbon (ADR-0043). This guard keys
|
||||
on the **fuel token** and returns the faithful archetype — the ``"Hot-water only
|
||||
community scheme"`` system flattens to ``"From main system, …"`` exactly as the
|
||||
community mains-gas description already does. ``"No Heating or Hot Water"`` under
|
||||
an ``"electric immersion assumed"`` system is the assessment's own immersion
|
||||
assumption, so it resolves to ``ELECTRIC_IMMERSION``.
|
||||
|
||||
Returns ``None`` for fuels the LLM already classifies correctly (mains gas,
|
||||
electricity, oil, …) and for varied / unstructured phrasings, so those still
|
||||
reach the LLM classifier via ``GuardedColumnClassifier``. Order matters:
|
||||
biodiesel and dual fuel are tested before the bare biomass / wood tokens they
|
||||
contain.
|
||||
"""
|
||||
text = description.lower()
|
||||
if "biodiesel" in text:
|
||||
return WaterHeatingType.FROM_MAIN_BIODIESEL_COMMUNITY
|
||||
if "biomass" in text:
|
||||
return WaterHeatingType.FROM_MAIN_BIOMASS_COMMUNITY
|
||||
if "dual fuel" in text:
|
||||
return WaterHeatingType.FROM_MAIN_DUAL_FUEL_MINERAL_WOOD
|
||||
if "wood log" in text:
|
||||
return WaterHeatingType.FROM_MAIN_WOOD_LOGS
|
||||
if "house coal" in text:
|
||||
return WaterHeatingType.FROM_MAIN_HOUSE_COAL
|
||||
if "no heating or hot water" in text:
|
||||
return WaterHeatingType.ELECTRIC_IMMERSION
|
||||
return None
|
||||
|
|
@ -21,6 +21,10 @@ class WaterHeatingType(Enum):
|
|||
FROM_MAIN_LPG_BULK = "From main system, LPG (bulk)"
|
||||
FROM_MAIN_BOTTLED_LPG = "From main system, bottled LPG"
|
||||
FROM_MAIN_HOUSE_COAL = "From main system, house coal"
|
||||
FROM_MAIN_WOOD_LOGS = "From main system, wood logs"
|
||||
FROM_MAIN_DUAL_FUEL_MINERAL_WOOD = "From main system, dual fuel (mineral and wood)"
|
||||
FROM_MAIN_BIOMASS_COMMUNITY = "From main system, biomass (community)"
|
||||
FROM_MAIN_BIODIESEL_COMMUNITY = "From main system, biodiesel (community)"
|
||||
ELECTRIC_IMMERSION = "Electric immersion, electricity"
|
||||
GAS_BOILER_CIRCULATOR_MAINS_GAS = "Gas boiler/circulator, mains gas"
|
||||
UNKNOWN = "Unknown"
|
||||
|
|
|
|||
|
|
@ -99,8 +99,17 @@ def _map_window_ventilation(
|
|||
opening_type=_try_get_str_value("Opening Type"),
|
||||
num_openings=_try_get_int_value("Number of Openings (In Same Window)"),
|
||||
pct_openable=_try_get_int_value("% of Window Openable"),
|
||||
trickle_vent_area_mm2=_try_get_int_value(
|
||||
"Trickle Vent Effective Area (mm2) (No Code Then Width x Height)"
|
||||
trickle_vent_area_mm2=next(
|
||||
(
|
||||
v
|
||||
for label in [
|
||||
"Total Trickle Vent Effective Area (mm2) (No Code Then Width x Height)",
|
||||
"Individual Trickle Vent Effective Area (mm2) (No Code Then Width x Height)",
|
||||
"Trickle Vent Effective Area (mm2) (No Code Then Width x Height)",
|
||||
]
|
||||
if (v := _try_get_int_value(label)) is not None
|
||||
),
|
||||
None,
|
||||
),
|
||||
num_trickle_vents=_try_get_int_value("No. of Trickle Vents"),
|
||||
)
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue