Skip to content

Energy beside OEE on a Satisfactory plant: MW against capacity, headroom, kWh a part against the recipe - #213

Merged
kalwei merged 4 commits into
mainfrom
feat/satisfactory-energy
Oct 30, 2026
Merged

kalwei merged 4 commits into
mainfrom
feat/satisfactory-energy

Conversation

@kalwei

@kalwei kalwei commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

What

An Energy panel on Shift analysis for a Satisfactory plant, and one energy line on each machine page's OEE tab. Energy sits beside OEE and is never part of it. OEE, quality (always 100 %) and every OEE figure are unchanged.

  • The bar. MW drawn now against circuit capacity, with peak demand marked. Peak demand is what every building would draw at once. Headroom is capacity less peak demand; below zero it reads as "X MW short at peak demand".
  • Fuse trips. A tripped fuse is named with the demand that blew it: "fuse tripped: demand 141 MW over capacity 100 MW".
  • The table. MWh used in the window, then per building: kWh a part, the recipe's ideal kWh a part, the gap, MW now, MWh and parts. Rows are ordered worst gap first, and the list says its total.
  • Ideal. The ideal is nominal MW ÷ the recipe's FactoryRate (× 1000/60). It comes from ref/getRecipes.json beside the poller's day files. A recipe that is not in that file has ideal unknown, with the sentence saying why; no ideal is ever guessed.
  • Generators are equipment. They sit under work centre GENERATORS and are running or idle (or down on a fuse). Their output, capacity, load and fuel are readings, and the panel lists what each one burns.
  • Readings. The reader writes everything through TagValue, as an OPC agent's readings are:
    • per building: <CODE>.PowerMW, .PowerMaxMW, .EnergyMWh, .IdealKWhPerUnit;
    • per generator: .OutputMW, .CapacityMW, .LoadPercent, .Fuel, .FuelItems;
    • per circuit: POWER.CIRCUIT-<n>.CapacityMW, .ConsumedMW, .PeakDemandMW, .ProductionMW, .BatteryPercent, .BatteryCapacityMWh, .FuseTripped.
  • Energy booking. Energy is booked per poll, only for an interval the MES saw both ends of. A gap or a pause books none, just as it books no production. The existing pause and gap handling is untouched.
  • API. The figures ride in /analysis/oee as energy, so the AI tab's existing oee_breakdown tool carries them; its docstring says how to read them. They are also served alone at /analysis/energy?hours=&shift=&equipment=, which the machine page uses.
  • A plant with no power readings gets no energy key and no panel. That is every plant that is not a game.

Why

"In the job shop the constraint is volume, in Satisfactory it's energy." The accepted design for the Satisfactory plant keeps OEE as OEE, with quality at 100 and saying so, and puts energy beside it as its own figure. It is never swapped in for quality.

How it was verified

  • Tests named as prose. tests/test_energy_stands_beside_oee_on_a_satisfactory_plant.py, 18 tests:
    • 4 MW for 10 min at 15 a minute is 4.44 kWh a part, and the ideal is 4.44;
    • half-fed, the building burns 8.89 kWh a part against that 4.44;
    • an absent recipe gives ideal "unknown" with its reason;
    • a gap books no energy, and neither does a pause;
    • a tripped circuit has its demand named;
    • a generator's fuel and rate are readings;
    • a building the last poll did not list has no MW now but keeps its energy;
    • tonight's fixture replays to 1 circuit and 6 generators;
    • a plant with no power readings has no energy key, and the API returns null;
    • no station's OEE carries an energy figure.
  • Browser test. tests/test_the_energy_panel_is_drawn_only_on_a_plant_that_reports_power.py (-m browser): the demo plant has no panel. Once a circuit and a machine report power, the panel draws the bar (used, over-capacity, capacity and peak marks), names the trip, and shows a dash, not "0 kWh", where nothing was made.
  • Fast tier and browser tier both green locally, and ruff is clean.
  • Scratch plant. I replayed the fixture into a scratch plant on 127.0.0.1:8197 (never 8130) and looked at it in headless Chromium, on loopback only. The replay applied 43 polls and skipped 7, with 70 state changes and 11,580 units booked. gh cannot attach images, so here is what the screenshots show:
    • The Energy panel. The bar is green to 42.6 MW of a 120 MW scale, with the peak-demand mark at 112.0 MW and the capacity mark at 120.0 MW. The line under it reads "33.587 MWh used in this window · 8.0 MW headroom at peak demand · 1 circuit · 40.357 MWh made by 6 generators". The table lists 31 buildings, worst gap first: SMELTERMK1-2147442749 (Iron Ingot) at 5.15 kWh a part against an ideal of 2.22 (+2.93), then the copper smelter at 4.31 against 2.22, and so on down to the wire constructors at 2.22 against 2.22. Below the table: "6 generators — … 8.9 MW of 30.0 MW, burning Biomass …".
    • The machine page (CONSTRUCTORMK1-2146851399, Concrete, OEE tab) reads: "Energy: drawing 4.0 MW of 4.0 MW at full speed · 2.741 MWh over 616 parts · 4.45 kWh a part (ideal 4.44 kWh, gap +0.01 kWh)".
  • That machine checked by hand against the raw feed:
    • Energy. The building is listed in 41 consecutive, unpaused polls with PowerConsumed = 4 MW: 2,467 s watched. 4 MW × 2,467 s ÷ 3,600 = 2.7411 MWh.
    • Parts. 2,467 s × 15 a minute ÷ 60 = 616.75, and the reader books whole units: 616.
    • kWh a part. 2,741.1 kWh ÷ 616 = 4.45 kWh a part.
    • Ideal. Concrete is FactoryRate 15 a minute in getRecipes.json, and a Constructor at 100 % clock reports MaxPowerConsumed 4 MW. 4 × 1000 ÷ 60 ÷ 15 = 4.44 kWh a part.
  • Windows read-through. The recipe file opens through pathlib with encoding="utf-8", gzip or plain. No subprocess, signal or shell is added. The tests write their files with an explicit encoding.

What the feed taught, said plainly

  • PowerProduction reads 0 on every circuit of every poll so far, even while the circuit carried 48.5 MW. Nothing treats it as output; it is kept as a reading because it is what the feed said.
  • Generators carry no ProductionRate. RegulatedDemandProd is the figure that moves with the load, and it is used as "MW made now".
  • The mod's building list is partial. Each poll lists 20–25 of the 31 buildings. A building left out of the latest list has no "MW now" (a dash, with its last-read time in the tooltip) and books no energy across its absence. As a result, the buildings' MWh do not add up to the circuit's consumption, and the panel does not pretend they do.
  • Idle draw counts. A building waiting for input still draws 0.1 MW, so a starved building's kWh a part rises above its ideal. That is the point of the gap column.
  • A late session answer. About one poll in ten, the poller writes getSessionInfo and getGenerators a second after the rest. The reader skips that line as a poll of its own; this is existing behaviour, and pause and gap handling were out of scope. Generators' energy still integrates across it, but their readings miss that moment.

Out of scope

Parts mix, rate orders, anything predictive, talking to the game, and changing pause or gap handling.

🤖 Generated with Claude Code

kalwei and others added 4 commits October 30, 2026 06:16
…adings

Each poll writes every building's draw (MW now and at full speed), its
energy since the previous observed poll (MWh), and the kWh one part of its
recipe should cost; every circuit's capacity, consumption, peak demand,
production, battery and fuse; every generator's output, capacity, load and
fuel. Generators are equipment, under a GENERATORS work centre.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: scottkalw <scottkalwei@gmail.com>
… part against the recipe

services/energy reads the power readings back: circuits standing at the
window's end with headroom and the last trip named in its own demand and
capacity, MWh per building, kWh a part over units booked, the ideal from
the recipe, generators with their fuel. /analysis/oee carries it as
'energy' (absent on a plant with no power readings); /analysis/energy
serves it alone, optionally for one machine. Fixture: the night of
2026-10-30, the first with generators, and a trimmed recipe reference.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: scottkalw <scottkalwei@gmail.com>
…README, CHANGELOG, AI tool doc

The panel is drawn only when the plant has power readings: a bar of MW
drawn against circuit capacity with the peak demand marked, headroom, a
tripped fuse named with its demand, MWh in the window, and the buildings
worst gap first with kWh a part against the recipe ideal. The reading
names move down into services.energy so no service imports an edge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: scottkalw <scottkalwei@gmail.com>
…st for the panel; plant-wide heading

The mod lists only part of the factory in each poll, so a building left out
of the latest list keeps its energy but has no 'now' figure. The panel
says it is the whole plant's, and a browser test holds that a plant with
no power readings draws no panel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: scottkalw <scottkalwei@gmail.com>
@github-actions

Copy link
Copy Markdown

Thanks for the first pull request. Checks: every commit signed off (git commit -s), ruff and pytest green, and the house-rules checklist in the template. The maintainer reviews within a few days.

@kalwei
kalwei merged commit 36aec52 into main Oct 30, 2026
28 checks passed
@kalwei
kalwei deleted the feat/satisfactory-energy branch October 30, 2026 12:51
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.

1 participant