Skip to content

Commit e0dfbc1

Browse files
committed
fix(roofmodel): emit rated watts after si-core-units
Derived arrays pre-fill weather.pv_arrays, which now stores rated_w. The Python module and Go host speak watts; kWp stays a test helper.
1 parent 14fe6d6 commit e0dfbc1

7 files changed

Lines changed: 39 additions & 15 deletions

File tree

go/internal/roofmodel/roofmodel.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ const (
5353
// the document can pre-fill weather.pv_arrays directly.
5454
type Array struct {
5555
Name string `json:"name"`
56-
KWp float64 `json:"kwp"`
56+
RatedW float64 `json:"rated_w"`
5757
TiltDeg float64 `json:"tilt_deg"`
5858
AzimuthDeg float64 `json:"azimuth_deg"`
5959
AreaM2 float64 `json:"area_m2"`
@@ -285,7 +285,7 @@ func (m *Model) ToPVArrays() []config.PVArray {
285285
tiltDeg, azimuthDeg := a.TiltDeg, a.AzimuthDeg
286286
out = append(out, config.PVArray{
287287
Name: a.Name,
288-
KWp: a.KWp,
288+
RatedW: a.RatedW,
289289
TiltDeg: &tiltDeg,
290290
AzimuthDeg: &azimuthDeg,
291291
})

go/internal/roofmodel/roofmodel_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ func TestDeriveParsesAModel(t *testing.T) {
208208
doc := `{"schema_version":1,"planes_found":3,` +
209209
`"site":{"latitude":59.33,"longitude":18.07,"radius_m":40},` +
210210
`"source":{"provider":"lantmateriet","item_count":2,"dataset_datetime":"2018-03-01T00:00:00+00:00"},` +
211-
`"arrays":[{"name":"Roof south","kwp":7.2,"tilt_deg":35,"azimuth_deg":180,"area_m2":51.4,"segment_id":"seg-0"}],` +
211+
`"arrays":[{"name":"Roof south","rated_w":7200,"tilt_deg":35,"azimuth_deg":180,"area_m2":51.4,"segment_id":"seg-0"}],` +
212212
`"captured_at_ms":1519862400000,"derived_at_ms":1785456000000}`
213213
cmd := stubModule(t, "stdout", doc)
214214
s := svc(t, &config.RoofModel{Enabled: true, Command: cmd, GeotorgetUsername: "u", GeotorgetToken: "t"})
@@ -482,8 +482,8 @@ func TestDeriveIsTimeBoxed(t *testing.T) {
482482

483483
func TestToPVArraysMatchesConfigShape(t *testing.T) {
484484
m := &Model{Arrays: []Array{
485-
{Name: "Roof south", KWp: 7.2, TiltDeg: 35, AzimuthDeg: 180, AreaM2: 51.4},
486-
{Name: "Roof west", KWp: 4.1, TiltDeg: 35, AzimuthDeg: 270, AreaM2: 29.3},
485+
{Name: "Roof south", RatedW: 7200, TiltDeg: 35, AzimuthDeg: 180, AreaM2: 51.4},
486+
{Name: "Roof west", RatedW: 4100, TiltDeg: 35, AzimuthDeg: 270, AreaM2: 29.3},
487487
}}
488488
got := m.ToPVArrays()
489489
if len(got) != 2 {
@@ -492,7 +492,7 @@ func TestToPVArraysMatchesConfigShape(t *testing.T) {
492492
if got[0].TiltDeg == nil || got[0].AzimuthDeg == nil {
493493
t.Fatalf("derived array must carry both angles, got %+v", got[0])
494494
}
495-
if got[0].Name != "Roof south" || got[0].KWp != 7.2 ||
495+
if got[0].Name != "Roof south" || got[0].RatedW != 7200 ||
496496
*got[0].TiltDeg != 35 || *got[0].AzimuthDeg != 180 {
497497
t.Errorf("array 0 = %+v tilt=%v az=%v", got[0], *got[0].TiltDeg, *got[0].AzimuthDeg)
498498
}

go/internal/units/consistency_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/srcfl/ftw/go/internal/loadpoint"
1212
"github.com/srcfl/ftw/go/internal/mpc"
1313
"github.com/srcfl/ftw/go/internal/pvperf"
14+
"github.com/srcfl/ftw/go/internal/roofmodel"
1415
"github.com/srcfl/ftw/go/internal/telemetry"
1516
"github.com/srcfl/ftw/go/internal/units"
1617
"github.com/srcfl/ftw/go/internal/v2x"
@@ -54,6 +55,16 @@ func TestPVPerfArrayHasNoKWp(t *testing.T) {
5455
}
5556
}
5657

58+
func TestRoofmodelArrayHasNoKWp(t *testing.T) {
59+
typ := reflect.TypeOf(roofmodel.Array{})
60+
if _, ok := typ.FieldByName("KWp"); ok {
61+
t.Fatal("roofmodel.Array must not have KWp; store RatedW")
62+
}
63+
if _, ok := typ.FieldByName("RatedW"); !ok {
64+
t.Fatal("roofmodel.Array must store RatedW (watts)")
65+
}
66+
}
67+
5768
func TestMPCParamsSoCIsFraction(t *testing.T) {
5869
typ := reflect.TypeOf(mpc.Params{})
5970
for _, banned := range []string{"SoCMinPct", "SoCMaxPct", "InitialSoCPct"} {
@@ -255,6 +266,7 @@ func TestCoreBannedSoCPercentFieldNames(t *testing.T) {
255266
reflect.TypeOf(mpc.SlotDirective{}),
256267
reflect.TypeOf(forecast.Array{}),
257268
reflect.TypeOf(pvperf.Array{}),
269+
reflect.TypeOf(roofmodel.Array{}),
258270
}
259271
banned := []string{"CurrentSoCPct", "TargetSoCPct", "PluginSoCPct", "VehicleSoCPct", "SoCPct", "SoCMinPct", "SoCMaxPct", "SoCTargetPct", "LivePVSurplusSoCCapPct", "LoadpointSoCTargetPct", "KWp"}
260272
for _, typ := range types {

roofmodel/ftw_roofmodel/pipeline.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ class RoofModelError(RuntimeError):
6464
@dataclasses.dataclass
6565
class DerivedArray:
6666
name: str
67-
kwp: float
67+
rated_w: float
6868
tilt_deg: float
6969
azimuth_deg: float
7070
area_m2: float
@@ -73,7 +73,7 @@ class DerivedArray:
7373
def to_json(self) -> dict[str, Any]:
7474
return {
7575
"name": self.name,
76-
"kwp": round(self.kwp, 2),
76+
"rated_w": round(self.rated_w),
7777
"tilt_deg": round(self.tilt_deg, 1),
7878
"azimuth_deg": round(self.azimuth_deg, 1),
7979
"area_m2": round(self.area_m2, 1),
@@ -122,7 +122,7 @@ def planes_to_arrays(
122122
arrays.append(
123123
DerivedArray(
124124
name=name,
125-
kwp=plane.kwp(packing_factor, module_w_per_m2),
125+
rated_w=plane.rated_w(packing_factor, module_w_per_m2),
126126
tilt_deg=plane.tilt_deg,
127127
azimuth_deg=plane.azimuth_deg,
128128
area_m2=plane.area_m2,

roofmodel/ftw_roofmodel/segment.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,21 @@ class RoofPlane:
5353
point_count: int
5454
mean_height_m: float
5555

56+
def rated_w(
57+
self,
58+
packing_factor: float = DEFAULT_PACKING_FACTOR,
59+
module_w_per_m2: float = DEFAULT_MODULE_W_PER_M2,
60+
) -> float:
61+
"""Installable DC capacity for this surface, in watts."""
62+
return self.area_m2 * packing_factor * module_w_per_m2
63+
5664
def kwp(
5765
self,
5866
packing_factor: float = DEFAULT_PACKING_FACTOR,
5967
module_w_per_m2: float = DEFAULT_MODULE_W_PER_M2,
6068
) -> float:
61-
"""Installable DC capacity for this surface, in kWp."""
62-
return self.area_m2 * packing_factor * module_w_per_m2 / 1000.0
69+
"""Installable DC capacity for this surface, in kWp (test helper)."""
70+
return self.rated_w(packing_factor, module_w_per_m2) / 1000.0
6371

6472

6573
def _fit_plane(points: np.ndarray) -> np.ndarray:

roofmodel/tests/test_pipeline.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -188,8 +188,8 @@ def test_array_json_matches_the_config_field_names():
188188
"""The document pre-fills weather.pv_arrays, so the keys must line up."""
189189
planes = [RoofPlane(tilt_deg=35, azimuth_deg=180, area_m2=60, point_count=200, mean_height_m=6)]
190190
payload = planes_to_arrays(planes)[0].to_json()
191-
assert set(payload) >= {"name", "kwp", "tilt_deg", "azimuth_deg"}
192-
assert payload["kwp"] > 0
191+
assert set(payload) >= {"name", "rated_w", "tilt_deg", "azimuth_deg"}
192+
assert payload["rated_w"] > 0
193193

194194

195195
# --- end to end ------------------------------------------------------------
@@ -221,7 +221,7 @@ def test_derive_produces_a_versioned_document(monkeypatch):
221221
south = model["arrays"][0]
222222
assert south["azimuth_deg"] == pytest.approx(180.0, abs=3.0)
223223
assert south["tilt_deg"] == pytest.approx(35.0, abs=3.0)
224-
assert south["kwp"] > 0
224+
assert south["rated_w"] > 0
225225
# Must be JSON-serialisable for the subprocess contract.
226226
json.dumps(model)
227227

web/settings/tabs/weather.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -477,8 +477,12 @@
477477
// Fill the form rather than saving: the operator sees the numbers and
478478
// presses Save, so the panel config never changes behind their back.
479479
ctx.config.weather.pv_arrays = arrays.map(function (a) {
480+
var rated = Number(a.rated_w);
481+
if (!(rated > 0) && Number(a.kwp) > 0) {
482+
rated = ratedWattsFromLegacyKwp(a.kwp);
483+
}
480484
return {
481-
name: a.name || "", kwp: a.kwp,
485+
name: a.name || "", rated_w: rated || 0,
482486
tilt_deg: a.tilt_deg, azimuth_deg: a.azimuth_deg,
483487
};
484488
});

0 commit comments

Comments
 (0)