Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/plan-battery-level.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ftw": minor
---

The plan an app reads (`plan.get` and the plan pushed after a command) now carries the battery's planned level at the end of each slot, `socPermille`, in thousandths: the same name and unit as the FTW Cloud plan class. A phone can then say where the battery ends up, not only what it does. A level the planner did not give as a number is left out. Older apps ignore the field.
13 changes: 11 additions & 2 deletions go/internal/appproto/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"time"

"github.com/srcfl/ftw/go/internal/mpc"
"github.com/srcfl/ftw/go/internal/units"
)

// planFrom projects the planner's output onto the wire.
Expand Down Expand Up @@ -39,14 +40,22 @@ func planFrom(p *mpc.Plan, rev uint64, ceilingW *int64, uptimeMs int64, now time

for _, a := range p.Actions {
price := int64(math.Round(a.PriceOre))
out.Slots = append(out.Slots, PlanSlot{
slot := PlanSlot{
StartMs: a.ExecutionStart(),
DurationMs: int64(math.Round(a.DurationHours() * 3600000)),
BatteryW: roundW(a.BatteryW),
GridW: roundW(a.GridW),
PriceMinor: &price,
Reason: reasonCode(a, mean, minSoC, ceilingW),
})
}
// The level the plan leads to, so a phone can say where the battery
// ends up and not only what it does. Left out rather than guessed
// when the planner's number is not a level.
if units.ValidFraction(a.SoC) {
soc := units.PermilleFromFraction(a.SoC)
slot.SocPermille = &soc
Comment on lines +54 to +56

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Omit SoC for batteryless plans

On an Energyplan site without home storage, buildMPC deliberately creates plans with CapacityWh == 0, and validation requires every Action.SoC to be zero. Because zero is a valid fraction here, every slot is serialized with socPermille: 0, falsely telling the app that an empty battery exists rather than omitting the unavailable level. Gate this field on the plan having positive battery capacity as well as a valid SoC.

Useful? React with 馃憤聽/ 馃憥.

}
out.Slots = append(out.Slots, slot)
}

return out
Expand Down
25 changes: 25 additions & 0 deletions go/internal/appproto/plan_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package appproto

import (
"math"
"testing"
"time"

Expand Down Expand Up @@ -55,6 +56,30 @@ func TestPlanGetAnswersOnTheBulkLaneAndEchoesTheRequestId(t *testing.T) {
}
}

// Each slot carries the battery's planned level at its end, in thousandths,
// so a phone can say where the plan leads and not only what it does. A
// number that is not a level is left out, never sent as a guess.
func TestPlanCarriesTheBatteryLevelAtEachSlotsEnd(t *testing.T) {
h, box, rec, clock := newRig(t)
subscribe(t, h, rec)
plan := samplePlan(clock.now)
plan.Actions[3].SoC = math.NaN()
box.plan = plan

deliver(t, h, MsgPlanGet, ptrU32(5), nil)

p := body[Plan](t, rec.only(t, MsgPlan))
want := []int64{400, 600, 450}
for i, w := range want {
if p.Slots[i].SocPermille == nil || *p.Slots[i].SocPermille != w {
t.Fatalf("slot %d socPermille = %v, want %d", i, p.Slots[i].SocPermille, w)
}
}
if p.Slots[3].SocPermille != nil {
t.Fatalf("a level that is not a number was sent: %d", *p.Slots[3].SocPermille)
}
}

// Reason codes are stable machine strings. The box has no idea what language
// anyone reads, and the app owns every word the user sees.
func TestPlanReasonsAreStableCodes(t *testing.T) {
Expand Down
4 changes: 4 additions & 0 deletions go/protocol/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,10 @@ type PlanSlot struct {
// slot's price is not known.
PriceMinor *int64 `cbor:"priceMinor"`
Reason PlanReason `cbor:"reason"`
// SocPermille is the battery's planned level at the end of the slot, in
// thousandths, absent when the planner gave none. The same name and unit
// as the FTW Cloud plan class. An older app ignores it.
SocPermille *int64 `cbor:"socPermille,omitempty"`
}

// Plan is what the box intends to do.
Expand Down
Loading