Skip to content

CI for srcfl/ftw#1905 - #4

Draft
segran2 wants to merge 495 commits into
masterfrom
fix/confirmed-pilot-feedback-1874
Draft

segran2 wants to merge 495 commits into
masterfrom
fix/confirmed-pilot-feedback-1874

Conversation

@segran2

@segran2 segran2 commented Nov 7, 2026

Copy link
Copy Markdown
Owner

Temporary CI run for srcfl#1905 at e874c43. The test workflow triggers on PRs targeting master.

frahlg and others added 30 commits October 18, 2026 08:31
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
fix(telemetry): derive Home use from energy and preserve history gaps
* feat(agents): expose scoped diagnostic API reads

Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>

* Expose the full technical read API and debugging context

---------

Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
* Keep URL-encoded diagnostic path parameters intact

* Keep diagnostic path struct tags unchanged

* Give diagnostic exports their own five-minute call lifetime
…fl#1684)

* feat(web): the Access tab names the car and charger in a request

A request waiting for the owner on the box page showed a car by its
32-digit id. GET /api/agents now adds names to each request: for every
argument of kind loadpoint or car that the box still knows, what the box
calls it. The page shows the name and falls back to the id.

The args stay the box's own ids, as before. Only this local, owner-only
route carries names; protocol.Request and everything an agent or FTW
Cloud reads are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep diagnostic agent lists in step with named requests

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
feat(web): the box page shows the owner's cars
…0261018

Keep browser logins for 30 days across Core restarts
…0261018

Order test-client shutdown and cancel after command admission
fix(history): keep P1 household and PV energy observations
…1018

fix: separate current Home use from its energy mean
frahlg and others added 30 commits November 1, 2026 12:24
…1946)

* fix(localzone): name the zone by the rules the clock runs on

A Docker box that mounts the host's /etc/localtime ran on the host's rules
but told clients it was on Etc/UTC: Debian links /etc/localtime to Etc/UTC,
so the mount overwrites that file, and /etc/timezone keeps the image's name.
Clients then read the household clock (a car's ready time, a goal's days)
an hour or two off.

Every name candidate must now have time.Local's rules from 1970 to 2038,
and a UTC name needs a clock that is UTC. A copied /etc/localtime is named
by the zoneinfo file with the same bytes, else the first with the same rules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(site): the home's time zone is a stored fact of the site

A Docker box runs on UTC whatever house it is in, so a car goal of 17:00
was met at 18:00 in a Stockholm winter. Core now reads the household clock
from site.time_zone, set once at start, instead of the host's zone.

- localzone.Use sets time.Local (and TZ for workers) from the stored zone
  before any goroutine reads the clock; Name reports it.
- site.time_zone is validated, shown under Settings → Site, written by the
  on-box setup from the browser, and listed as a restart reason.
- site.time_zone.propose lets the app and the web client offer their zone.
  The box takes it only while it keeps none (nothing stored, host on UTC),
  then restarts once. A kept zone answers E_PRECONDITION with args.tz.
- Cached local-day energy is tied to the zone it was computed on; a new
  zone clears it and the days are computed again from UTC buckets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…open contactor (srcfl#1952)

Moves the pin to srcfl/device-drivers@107eb609 (srcfl#204). Only the Tesla Wall
Connector driver changes. The test copy in go/internal/drivers/testdata was
still the version from srcfl#978; it now matches the bundled driver, and the
tests follow its contract: Core refuses control of the read-only driver,
legs at 0 V carry no power, and an open contactor is idle.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…battery storage (srcfl#1950)

* feat(history): the figure for Skatteverket's energy tax refund after battery storage

A Swedish household gets back the energy tax on grid electricity it stored
in a battery and later fed back to the same grid (11 kap. 13 § LSE). History
now works out that figure for one calendar year, month by month, with the
hourly basis as CSV. The count stays low wherever the readings cannot tell
where energy went.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(history): a meter that changes identity stays one meter

The home box's Sungrow moved from a MAC-based to a serial-based device id
in September, so the ledger holds two grid meters, batteries and solar
assets. The report took that for two batteries and for meters that fell
silent, and gave no figure for the year. Assets of one kind that never
read at the same time now form one meter; the hours between them are
still owed and stay incomplete.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(history): count the house's use as grid energy first, never more than the readings allow

The proportional mix overstated. Nothing says how much the battery holds,
and with the content taken as a full battery, grid energy the house had
already used stayed in the mix and was credited to the evening's export:
5 kWh from the grid at night, 5 kWh to the house in the morning, 5 kWh of
solar at midday and 5 kWh to the grid in the evening returned about 0.6 kWh
a day where the truth is 0.

What the house takes from the battery now counts as grid energy first, and
what goes to the grid as other energy first. Grid energy enters after the
least efficiency its own run of battery readings allows, so a short run or
one cut by a silent battery cannot borrow an efficiency; other energy
enters whole. A full battery sheds grid energy first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(history): play each hour in the order that credits least, and keep the charge loss

A third review found the strict account could still overstate:

- It played discharge before charge, so the house never took grid energy
  that came in earlier in the same hour (4 kWh where the least figure the
  flows allow is 0).
- It dropped the charge loss, so the account fell below what the battery
  held; the house then took other energy the real battery still had, and
  the export reached grid energy early (1230.8 Wh against 0).
- A new battery asset that took over within an hour kept the old account.

Each hour now plays: other energy in, the export out (other energy first),
grid energy in after the least efficiency with its loss kept as other
energy, the house's share (grid energy first), then a full battery sheds
grid energy first. A battery asset that takes over starts a new account.
The review's cases are tests and fail on the previous code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…cfl#1951)

Skatteverket's refund of energy tax after battery storage is worked out
hour by hour for a calendar year and may be claimed until three years
after the year ends. With two years of hours, the start of an older year
was already daily totals and counted as incomplete.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…gure (srcfl#1953)

Sites with more than one battery had no figure, because one shared account
let one battery's exports carry another's grid energy. Each battery meter
now keeps its own account, size and run efficiencies, and for each battery
solar, a car and the other batteries count first in every hour. A driver's
battery size goes to the meter with an asset of its name, so a new identity
keeps it. The hourly CSV has one row per battery and hour.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* chore(release): version packages

* chore(release): the changelog names each battery's size

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…fl#1958)

The strict order counted the house's use of a battery as grid energy first
and its export as other energy first, so a battery with solar almost never
got a figure: the home box gave 0 for 2026. Now whatever leaves a battery,
to the house or the grid, carries the grid's share of what it held, and
energy enters after the battery's efficiency over the year, so losses are
never refunded. Within an hour the charge mixes in before the discharge.
The hourly bounds, complete-hour rules and per-battery accounts stay.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…te meter again (srcfl#1961)

Moves the pin to srcfl/device-drivers@d662c3eb (srcfl#208). Only the Zap driver
changes. A Zap on firmware 7.x serves its local API in data model v2, and
zap 3.1.1 read only the old names, so FTW lost its site meter.

The new tests use the payload a Zap-HAN on 7.0.3 sent, and they fail against
3.1.1. read_pv and read_battery on 7.x are not covered yet.

Signed-off-by: David Mozart <dmozartandraws@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…for a restart (srcfl#1960)

* test(site): drop r3 and r4 known lines fixed on master, give r4 room for a restart

A3 and A4 (srcfl#1876, srcfl#1878, srcfl#1916) and E1 (srcfl#1787) merged before the R1-R5
scenarios did, so r3-25 and r4-25 failed every full run since 28 Oct with
"passes now: delete its known line".

r4 also failed "the day's Home use kept within 1 %" in 1 run of 5. The
day may have 10 % unknown time, counted in whole ledger minutes; when the
car's unknown reading after the restart crossed a minute boundary, two
minutes were marked and the 17 min day reached 12.2 %. r4 now waits
25 min instead of 10 before the hold, as s7 waits 15 min.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(site): r3 judges the measured SoC at the planner floor

With its known lines gone, r3's floor criteria are gates, and both read
Core's order. A battery handed back to the inverter reads as a 0 W order
and would pass while it runs on to the inverter's 5 %. The measured SoC
must now stay at or above the floor too. Review of srcfl#1960.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…ware 7.x (srcfl#1962)

Moves the pin to srcfl/device-drivers@e463c653 (srcfl#209). Only the Zap driver
changes. The new tests use the payload a Zap on 7.0.3 sent for a simulated
Solis.

Signed-off-by: David Mozart <dmozartandraws@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…1964)

* fix(config): a battery soc_max of 0 no longer blocks charging

Settings saved a blank number field as 0. Since soc_max became the
dispatch charge ceiling, a battery whose Max SoC was saved that way
never charged, manual Charge included. Core treats soc_max <= 0 as
unset, and Settings saves a cleared number field as unset.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(config): leave the shared battery map alone and test the stored path

Validate runs on shallow copies of the live config, so the zero drop
clones the batteries map before it writes. A new test loads a zero that
an older Core stored in the config database.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…rcfl#1969)

* fix(control): a battery that may not export holds the grid above 0 W

In planner_self, planner_cheap and passive arbitrage the reactive path
aimed a discharging battery at 0 W with a symmetric dead band. Each drop
in house load then sold battery energy until the next correction, about
as often as the site imported. While such a battery discharges more than
the dead band, the target is now at least grid_tolerance_w, so the band
ends at 0 W.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(control): bias the no-export target on the planned discharge, not the reading

The bias keyed on the battery's measured power, so an idle battery under
a legacy plan with a negative grid target discharged toward it for one
tick and exported. It now applies whenever the plain target would have
the battery discharge more than the dead band. Charging from PV surplus
no longer sees the bias at all.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
)

The budget sentence still said Core allows 500 ms or 5 s. Core also
gives 1.5 s with an active car, skips the call when less than 500 ms of
the first slot remain, caps time_limit_s at 5.6 s, and keeps a reply
margin besides the 50 ms for validation and publication. State what
go/internal/mpc/energyplan.go does today.

Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
… demand_elapsed_hours (srcfl#1966)

With 15-minute prices Core replans at :15, :30 and :45, and
bindDemandCharges sent only the tariff hours that start at or after
slot 0. The hour already running was left out of the peak-power tariff
the planner sees, so a plan could set a new monthly peak in it, and
today's completed peak counted twice in the top-N mean.

Core now sends the hour in progress with the import measured so far
(the site meter's hourly counter, else the history; uncovered time
counts at no less than slot 0's forecast import) and puts today's
completed peak in today's group. Only a planner whose handshake
announces demand_elapsed_hours gets this shape; any other planner,
including the bundled one, gets exactly the request it got before.

Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…l#1968)

Core fixed time_limit_s from the budget at the frozen execution start.
With forecast caution, the caution call follows the point call and its
deadline counts from the time left in the slot now. After a point call
that used its whole limit, the caution call could ask for 1.5 s with
about 950 ms left before its deadline, so a worker that uses its full
time limit answered after Core had given up, and Core published the
point plan.

Each call's time_limit_s now fits the time before that call's deadline,
minus Core's reply margin, after request preparation. A call with its
full budget left keeps it. Below Core's no-call threshold the worker is
not started.

Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
srcfl#1973)

srcfl/device-drivers#203 skips the battery emit when every battery
reading is 0, so a PV-only Growatt no longer draws a phantom battery
beside the site's real one (srcfl#1669). The pin moves e463c653 to e07d2703;
growatt is the only bundled driver that changes.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…srcfl#1972)

xorath, 5 Nov 2026: a battery charged 2.3 kW while FTW asked it to
discharge 812 W. The box's page said "Not following"; no phone heard.

The new control.not_following rule reads the box's own answer to "Are
we in control?" (the control_feedback GET /api/status serves) at most
every 30 s on the HealthTick, and fires once a device function has been
not_following for ten minutes. It fires once until the device follows
again, with an hour's cooldown on top. On by default for owners with
notifications on (Fredrik, 5 Nov 2026); a box whose stored rules predate
the kind takes the default. The words come from the push catalogue.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…1977)

verify.py skipped execution only on hosts it does not know. On a known
host whose worker the bundle leaves out, such as macOS when only the
required Linux workers ship, it failed with a KeyError instead of
checking the bundle. It now verifies every file and skips execution on
any host without its own worker; --host-binary prints nothing there, so
make native-solver-test skips the process tests as before.

Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Bundle the worker from srcfl/energyplan 847936a (0.7.0, tag v0.7.0-beta.1)
for linux-arm64 and linux-amd64. The macOS worker is no longer bundled;
srcfl#1977 lets verify.py check such a bundle and skip execution on a Mac. The
worker lists demand_elapsed_hours, so Core (srcfl#1966) sends it the demand
hour in progress and the day's completed peak in that day's group, and
the native demand test now requires the feature.

The contract describes the feature and the demand-charge fields, each
call's time limit since srcfl#1968, the batteries' total recovery rule and
optional reply diagnostics, and it stays identical to Energyplan's copy.
The worker version pins and the README follow. Licenses and forecast
schemas are unchanged; the notices list two more microlp modifications.
Protocol v1 is unchanged.

Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
# Conflicts:
#	go/internal/control/sitesim_field_test.go
A fresh pilot reads as charger_pilot_limit only when it is 0.5 A below
the order and within 0.5 A of the limit that explains the draw. A valid
pilot past its 90 s window becomes unknown and leaves the driver's limit
standing, so a late Easee cloud read no longer turns load balancing into
a warning. The charger's own limit still explains a draw when a fresh
pilot comes without a current limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants