Skip to content

Place GOPACS redispatch orders through Distro Energy - #135

Open
Miggets7 wants to merge 33 commits into
mainfrom
feature/distro-gopacs-orders
Open

Miggets7 wants to merge 33 commits into
mainfrom
feature/distro-gopacs-orders

Conversation

@Miggets7

@Miggets7 Miggets7 commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Description

Closes #133. Part of openremote/denhaag#209, which stays open until the orders are tested on production.

Adds DistroGopacsHandler, which places GOPACS redispatch orders through Distro Energy's GOPACS Connector API. It is switched on per EmsDistroEnergyAsset with gopacsEnabled and places orders under the asset's portfolio, the same one the day-ahead submissions use. EmsOptimisationService copies the redispatch announcement from the EmsGOPACSAsset to every EmsDistroEnergyAsset under the same parent, so a deployment needs no attribute links and the GOPACS and Distro handlers stay independent.

The handler pre-fills order type and quantity from the announcement and the batteries. The operator sets a price and confirms, and nothing is ordered automatically. After that it polls the order status, replaces open orders when the price changes, and cancels them when the announcement closes, on a manual cancel, or when gopacsEnabled is turned off or the asset is deleted. Setup is described in ems/docs/DistroEnergyIntegration.md.

JAX-RS proxy and wire records for creating, reading, replacing and
cancelling GOPACS orders and listing their settlements. Status and
order type stay strings so new values in the draft API do not break
deserialisation.
Announcement inputs are read-only and meant to be filled through
attribute links from an EmsGOPACSAsset, so the Distro integration does
not depend on the GOPACS handlers. Order type and quantity are
pre-filled by the handler and can be overridden; price, confirm and
cancel are set by the operator. The existing day-ahead panels are
renamed so both groups read clearly in the asset view.
Pre-fills order type and quantity from the linked announcement: the
EAN effectivity category picks the side, and the batteries under the
parent optimisation asset size a constant power that their state of
charge can hold for the whole window. On operator confirmation the
order is posted to Distro; a price change replaces open orders, a
closed announcement or the cancel flag cancels them, and a poll tracks
status until a final one, recording the trades of settled orders.

Tokens come from OpenRemote's OAuth filter, which caches and refreshes
the client-credentials token.
One handler per EmsDistroEnergyAsset while gopacsEnabled is true,
reconciled from both the persistence route and attribute events under
a lock, independent of the day-ahead handler. Other gopacs* attribute
events are forwarded to the running handler.
Unit spec for the effectivity-to-side mapping, battery sizing,
expiration and the attribute-driven create/replace/cancel/poll flows;
WireMock spec for the OAuth token call, bearer reuse and wire format;
service spec for the gopacsEnabled reconcile.
Configuration, the attribute links from EmsGOPACSAsset, order sizing,
the order lifecycle and the new tests. Also replace the made-up
effectivity example in the GOPACS docs with a real category.
Track an order as PENDING before posting it, so a create that times
out after Distro accepted it is still polled, cancelled and settled.
Only a 4xx untracks it again.

Store a polled order before fetching its settlements, so a failed fetch
is retried next poll without repeating the STATUS entry. Read every
settlements page. Skip the suggestion when the batteries cannot hold
the 100 kW minimum. Drop queued work once the handler is undeployed and
close the client only after a running order action has finished.
Day-ahead stays on the left, so the two Distro features no longer
interleave on the asset page.
Operators work in these two panels, so they sit next to the day-ahead
panels while configuration, status and history stay on the right.
Turning gopacsEnabled off or deleting the asset stopped polling but left
PENDING and SUBMITTED orders live at Distro, where they could still be
matched with nothing tracking them. Both now cancel the open orders
before closing the client. A manager stop still leaves them in place,
since the next start restores and keeps tracking them.
The updated Distro spec takes at most one decimal for the quantity in MW
and two for the price, requires a price of 0 or more, needs the order
to expire at least 15 minutes before start, and only lets SUBMITTED
orders be replaced.

Round an operator-entered quantity down to 100 kW steps and the price
to cents. Refuse a negative price, on confirm and on a price change,
and refuse a window that starts within 15 minutes. Replace only the
SUBMITTED orders of the announcement.
GOPACS no longer takes bids for an announcement once its bid validity
end has passed. Such an order went out with Distro's default expiry and
stayed live without any chance to help. Refuse it when the validity end
is known.
The updated Distro spec says the settlements endpoint is not implemented
yet. A settled order whose trade fetch failed stayed tracked for a
retry, so it would have been polled every five minutes forever. Record
the failure as SETTLEMENTS_FAILED and drop the order instead.
The handler built a pending GopacsOrder from a request and a replace
request from an order by hand. GopacsOrder.pending(request) and
toRequest(price) now own those copies.
Routing every gopacs* attribute also sent the handler's own writes
(orders, history, last sync, the pre-filled type and quantity) back to
it, and each queued a task that did nothing. Route on the explicit
DistroGopacsHandler.INPUTS set instead.
Build the three updateSuggestion log messages with String.format
instead of long string concatenations.
An undeploy less than a second after a confirm or cancel skipped the
delayed reset, leaving the flag true. Setting it to true again after
re-enabling was then an unchanged value and was ignored. Deploy now
resets either flag that is still true.
Log the handler config at deploy (without the secret), every changed
input at FINE, and confirms, cancels, placed and replaced orders and
status changes at INFO. Warn when a flag is ignored because it is
already true or because no handler is running for the asset.
A bare HTTP 403 does not say whether the tenant, the IP whitelist or the
URL is wrong. Log Distro's error body, cut to 500 characters, and record
it in the order history. HTTP errors are logged without the stack trace,
which adds nothing for them.
gopacsOrderType was free text, so an operator could type anything and
only learn at confirm time that it was refused. An enum value type
renders as a dropdown in the manager UI. The handler now works with the
enum and only turns it into a string for the Distro request.
placeOrder built the refusal reason through a long else-if chain. The
checks now live in a static validateInput that returns early, so
placeOrder only reads the attributes, refuses or sends.

The quarter-hour alignment check is gone: the window is linked from the
GOPACS announcement and is read-only, so it is always aligned. The
separate rounding log is folded into the Placing log, which now also
shows the quantity the operator entered.
gopacsOrderPrice is now a POSITIVE_NUMBER (min 0). OpenRemote validates
every attribute event against the value constraints, from the UI as
well as from rules and links, so a negative price never reaches the
handler. The handler's own negative-price checks on create and replace
are removed.
CI runs spotlessCheck, which failed on both test files. Formatting only.
Distro confirmed the settlement endpoints go live shortly and that the
trades are available as soon as an order is SETTLED. The Javadoc and
docs no longer say the endpoints are missing. Behaviour is unchanged:
one fetch per settled order, no retry.
The Distro GOPACS order handler reads the announcement from six
read-only gopacs* inputs, but nothing in the extension filled them:
each deployment had to add attribute links from the EmsGOPACSAsset by
hand and copy the current values once.

EmsOptimisationService now copies contractedEAN and the redispatch
announcement attributes to every EmsDistroEnergyAsset that shares the
GOPACS asset's parent. It does so on a change of one of those GOPACS
attributes, on a save of either asset, and at start. Only inputs that
differ are written, so the redispatch poller rewriting the same
announcement does not rewrite the Distro asset on every poll. Nothing
is copied while a parent has more than one GOPACS asset.
The Distro asset no longer needs attribute links from the GOPACS asset:
EmsOptimisationService copies the announcement to the Distro asset under
the same parent. The setup steps, attribute table and workflow diagram
now describe the copy.
@Miggets7 Miggets7 added the Feature Development of new feature label Oct 28, 2026
@Miggets7
Miggets7 requested review from igorrutka and wborn October 28, 2026 15:59
@Miggets7 Miggets7 self-assigned this Oct 28, 2026
Distro renamed the tenantId in its GOPACS Connector API to portfolio,
with the same value. The handler now takes the portfolio from the
EmsDistroEnergyAsset, the one the day-ahead submissions already use, so
DISTRO_GOPACS_TENANT_ID is gone. The request URLs are unchanged.

The handler only starts while the asset has a portfolio. A portfolio
change stops it like a disable, cancelling the open orders placed under
the old portfolio, and starts a new handler under the new one.
On the GOPACS asset the UFTP panels are day-ahead, not intraday. On the
Distro asset the GOPACS panels are the redispatch orders. Use plain
dashes instead of em dashes and write "Day ahead" without a hyphen.
The 4 Nov revision of the Distro GOPACS Connector spec drops MATCHED
and REVERTED: a matched ETPA order now stays SUBMITTED, and a failed
or cancelled one becomes REJECTED. It adds EXPIRED, a terminal status
for orders the clean-orderbook run removed. Without EXPIRED among the
final statuses, an expired order would be polled forever.

The other changes in the revision need no code change: tenantId is
renamed to portfolio, which our resource already uses as the path
parameter and the DTOs never read, and price becomes required, which
the handler already enforces.
The GOPACS order handler only runs under a portfolio, but only a
gopacsEnabled write reconciled it. Setting the portfolio from the UI,
which writes the attribute, left an enabled asset without a handler,
and changing it kept the handler on the old portfolio. A portfolio
write now starts, restarts or stops the handler the same way.
Main upgraded Spotless, whose Groovy formatter indents continuation lines further.
@Miggets7
Miggets7 requested a review from denniskuijs November 4, 2026 13:29
Spec 0.0.3-draft drops the filters and paging from the settlements list
and returns plain arrays. The per-reference endpoint now lists every
trade of an order, so the handler calls it once instead of paging the
portfolio list by delivery window.

Settlement records lose matchedAt and gain the EAN, modifiedAt and unit
fields. Units may now be kW-based, so they are kept on each recorded
trade. A settled order that comes back without trades is now recorded
as SETTLEMENTS_FAILED instead of being dropped without a trace.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Feature Development of new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

EMS: place GOPACS redispatch orders through Distro Energy

1 participant