diff --git a/Content.Client/Entry/IgnoredComponents.cs b/Content.Client/Entry/IgnoredComponents.cs
index 61c31b7a6e..3de1604e03 100644
--- a/Content.Client/Entry/IgnoredComponents.cs
+++ b/Content.Client/Entry/IgnoredComponents.cs
@@ -138,6 +138,7 @@ namespace Content.Client.Entry
"Flash",
"Docking",
"Telecrystal",
+ "PowerMonitoringConsole",
"RCD",
"RCDAmmo",
"CursedEntityStorage",
diff --git a/Content.Client/Power/PowerMonitoringWindow.xaml b/Content.Client/Power/PowerMonitoringWindow.xaml
new file mode 100644
index 0000000000..826da19d90
--- /dev/null
+++ b/Content.Client/Power/PowerMonitoringWindow.xaml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Power/PowerMonitoringWindow.xaml.cs b/Content.Client/Power/PowerMonitoringWindow.xaml.cs
new file mode 100644
index 0000000000..f3343fce7b
--- /dev/null
+++ b/Content.Client/Power/PowerMonitoringWindow.xaml.cs
@@ -0,0 +1,85 @@
+using System;
+using System.Linq;
+using Content.Client.Computer;
+using Content.Client.IoC;
+using Content.Shared.Power;
+using JetBrains.Annotations;
+using Robust.Client.AutoGenerated;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Localization;
+using Robust.Shared.Maths;
+using Robust.Shared.Timing;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Power;
+
+[GenerateTypedNameReferences]
+public sealed partial class PowerMonitoringWindow : DefaultWindow, IComputerWindow
+{
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+
+ public PowerMonitoringWindow()
+ {
+ RobustXamlLoader.Load(this);
+ SetSize = MinSize = (300, 450);
+ IoCManager.InjectDependencies(this);
+ MasterTabContainer.SetTabTitle(0, Loc.GetString("power-monitoring-window-tab-sources"));
+ MasterTabContainer.SetTabTitle(1, Loc.GetString("power-monitoring-window-tab-loads"));
+ }
+
+ public void UpdateState(PowerMonitoringConsoleBoundInterfaceState scc)
+ {
+ UpdateList(TotalSourcesNum, scc.TotalSources, SourcesList, scc.Sources);
+ var loads = scc.Loads;
+ if (!ShowInactiveConsumersCheckBox.Pressed)
+ {
+ // Not showing inactive consumers, so hiding them.
+ // This means filtering out loads that are not either:
+ // + Batteries (always important)
+ // + Meaningful (size above 0)
+ loads = loads.Where(a => a.IsBattery || (a.Size > 0.0f)).ToArray();
+ }
+ UpdateList(TotalLoadsNum, scc.TotalLoads, LoadsList, loads);
+ }
+
+ public void UpdateList(Label number, double numberVal, ItemList list, PowerMonitoringConsoleEntry[] listVal)
+ {
+ number.Text = Loc.GetString("power-monitoring-window-value", ("value", numberVal));
+ // This magic is important to prevent scrolling issues.
+ while (list.Count > listVal.Length)
+ {
+ list.RemoveAt(list.Count - 1);
+ }
+ while (list.Count < listVal.Length)
+ {
+ list.AddItem("YOU SHOULD NEVER SEE THIS (REALLY!)", null, false);
+ }
+ // Now overwrite the items properly...
+ for (var i = 0; i < listVal.Length; i++)
+ {
+ var ent = listVal[i];
+ _prototypeManager.TryIndex(ent.IconEntityPrototypeId, out EntityPrototype? entityPrototype);
+ IRsiStateLike? iconState = null;
+ if (entityPrototype != null)
+ iconState = SpriteComponent.GetPrototypeIcon(entityPrototype, StaticIoC.ResC);
+ var icon = iconState?.GetFrame(RSI.State.Direction.South, 0);
+ var item = list[i];
+ item.Text = $"{ent.NameLocalized} {Loc.GetString("power-monitoring-window-value", ("value", ent.Size))}";
+ item.Icon = icon;
+ }
+ }
+}
+
+[UsedImplicitly]
+public sealed class PowerMonitoringConsoleBoundUserInterface : ComputerBoundUserInterface
+{
+ public PowerMonitoringConsoleBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) {}
+}
+
diff --git a/Content.Server/Power/Components/PowerMonitoringConsoleComponent.cs b/Content.Server/Power/Components/PowerMonitoringConsoleComponent.cs
new file mode 100644
index 0000000000..72137293bf
--- /dev/null
+++ b/Content.Server/Power/Components/PowerMonitoringConsoleComponent.cs
@@ -0,0 +1,16 @@
+using System;
+using System.Collections.Generic;
+using Content.Server.Power.NodeGroups;
+using Content.Server.Utility;
+using Robust.Server.GameObjects;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.ViewVariables;
+
+namespace Content.Server.Power.Components;
+
+[RegisterComponent]
+public sealed class PowerMonitoringConsoleComponent : Component
+{
+}
+
diff --git a/Content.Server/Power/EntitySystems/PowerMonitoringConsoleSystem.cs b/Content.Server/Power/EntitySystems/PowerMonitoringConsoleSystem.cs
new file mode 100644
index 0000000000..772107d29d
--- /dev/null
+++ b/Content.Server/Power/EntitySystems/PowerMonitoringConsoleSystem.cs
@@ -0,0 +1,102 @@
+using Content.Shared.Popups;
+using Content.Shared.Power;
+using Content.Server.NodeContainer;
+using Content.Server.NodeContainer.Nodes;
+using Content.Server.Power.Components;
+using Content.Server.Power.NodeGroups;
+using Content.Server.UserInterface;
+using Content.Server.WireHacking;
+using JetBrains.Annotations;
+using Robust.Server.GameObjects;
+
+namespace Content.Server.Power.EntitySystems;
+
+[UsedImplicitly]
+internal sealed class PowerMonitoringConsoleSystem : EntitySystem
+{
+ private float _updateTimer = 0.0f;
+ private const float UpdateTime = 1.0f;
+
+ [Dependency]
+ private UserInterfaceSystem _userInterfaceSystem = default!;
+
+ public override void Update(float frameTime)
+ {
+ _updateTimer += frameTime;
+ if (_updateTimer >= UpdateTime)
+ {
+ _updateTimer -= UpdateTime;
+ foreach (var component in EntityQuery())
+ {
+ UpdateUIState(component.Owner, component);
+ }
+ }
+ }
+
+ public void UpdateUIState(EntityUid target, PowerMonitoringConsoleComponent? pmcComp = null, NodeContainerComponent? ncComp = null)
+ {
+ if (!Resolve(target, ref pmcComp))
+ return;
+ if (!Resolve(target, ref ncComp))
+ return;
+
+ var totalSources = 0.0d;
+ var totalLoads = 0.0d;
+ var sources = new List();
+ var loads = new List();
+ PowerMonitoringConsoleEntry LoadOrSource(Component comp, double rate, bool isBattery)
+ {
+ var md = MetaData(comp.Owner);
+ var prototype = md.EntityPrototype?.ID ?? "";
+ return new PowerMonitoringConsoleEntry(md.EntityName, prototype, rate, isBattery);
+ }
+ // Right, so, here's what needs to be considered here.
+ var netQ = ncComp.GetNode("hv").NodeGroup as PowerNet;
+ if (netQ != null)
+ {
+ var net = netQ!;
+ foreach (PowerConsumerComponent pcc in net.Consumers)
+ {
+ loads.Add(LoadOrSource(pcc, pcc.DrawRate, false));
+ totalLoads += pcc.DrawRate;
+ }
+ foreach (BatteryChargerComponent pcc in net.Chargers)
+ {
+ if (!TryComp(pcc.Owner, out PowerNetworkBatteryComponent? batteryComp))
+ {
+ continue;
+ }
+ var rate = batteryComp.NetworkBattery.CurrentReceiving;
+ loads.Add(LoadOrSource(pcc, rate, true));
+ totalLoads += rate;
+ }
+ foreach (PowerSupplierComponent pcc in net.Suppliers)
+ {
+ sources.Add(LoadOrSource(pcc, pcc.MaxSupply, false));
+ totalSources += pcc.MaxSupply;
+ }
+ foreach (BatteryDischargerComponent pcc in net.Dischargers)
+ {
+ if (!TryComp(pcc.Owner, out PowerNetworkBatteryComponent? batteryComp))
+ {
+ continue;
+ }
+ var rate = batteryComp.NetworkBattery.CurrentSupply;
+ sources.Add(LoadOrSource(pcc, rate, true));
+ totalSources += rate;
+ }
+ }
+ // Sort
+ loads.Sort(CompareLoadOrSources);
+ sources.Sort(CompareLoadOrSources);
+ // Actually set state.
+ var state = new PowerMonitoringConsoleBoundInterfaceState(totalSources, totalLoads, sources.ToArray(), loads.ToArray());
+ _userInterfaceSystem.GetUiOrNull(target, PowerMonitoringConsoleUiKey.Key)?.SetState(state);
+ }
+
+ private int CompareLoadOrSources(PowerMonitoringConsoleEntry x, PowerMonitoringConsoleEntry y)
+ {
+ return -x.Size.CompareTo(y.Size);
+ }
+}
+
diff --git a/Content.Shared/Power/SharedPowerMonitoringConsoleComponent.cs b/Content.Shared/Power/SharedPowerMonitoringConsoleComponent.cs
new file mode 100644
index 0000000000..f2b3011b5a
--- /dev/null
+++ b/Content.Shared/Power/SharedPowerMonitoringConsoleComponent.cs
@@ -0,0 +1,46 @@
+#nullable enable
+using System;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Maths;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Power;
+
+[Serializable, NetSerializable]
+public sealed class PowerMonitoringConsoleBoundInterfaceState : BoundUserInterfaceState
+{
+ public double TotalSources;
+ public double TotalLoads;
+ public PowerMonitoringConsoleEntry[] Sources;
+ public PowerMonitoringConsoleEntry[] Loads;
+ public PowerMonitoringConsoleBoundInterfaceState(double totalSources, double totalLoads, PowerMonitoringConsoleEntry[] sources, PowerMonitoringConsoleEntry[] loads)
+ {
+ TotalSources = totalSources;
+ TotalLoads = totalLoads;
+ Sources = sources;
+ Loads = loads;
+ }
+}
+
+[Serializable, NetSerializable]
+public sealed class PowerMonitoringConsoleEntry
+{
+ public string NameLocalized;
+ public string IconEntityPrototypeId;
+ public double Size;
+ public bool IsBattery;
+ public PowerMonitoringConsoleEntry(string nl, string ipi, double size, bool isBattery)
+ {
+ NameLocalized = nl;
+ IconEntityPrototypeId = ipi;
+ Size = size;
+ IsBattery = isBattery;
+ }
+}
+
+[Serializable, NetSerializable]
+public enum PowerMonitoringConsoleUiKey
+{
+ Key
+}
+
diff --git a/Resources/Locale/en-US/components/power-monitoring-component.ftl b/Resources/Locale/en-US/components/power-monitoring-component.ftl
new file mode 100644
index 0000000000..ade9e2d933
--- /dev/null
+++ b/Resources/Locale/en-US/components/power-monitoring-component.ftl
@@ -0,0 +1,8 @@
+power-monitoring-window-title = Power Monitoring Console
+power-monitoring-window-tab-sources = Sources
+power-monitoring-window-tab-loads = Loads
+power-monitoring-window-total-sources = Total Sources:
+power-monitoring-window-total-loads = Total Loads:
+power-monitoring-window-value = { POWERWATTS($value) }
+power-monitoring-window-show-inactive-consumers = Show Inactive Consumers
+
diff --git a/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml b/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml
index e27b4314f4..6fb9327302 100644
--- a/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml
+++ b/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml
@@ -66,6 +66,14 @@
- type: ComputerBoard
prototype: ComputerSupplyRequest
+- type: entity
+ parent: BaseComputerCircuitboard
+ id: PowerMonitoringComputerCircuitboard
+ name: power monitoring computer board
+ components:
+ - type: ComputerBoard
+ prototype: ComputerPowerMonitoring
+
- type: entity
parent: BaseComputerCircuitboard
id: ResearchComputerCircuitboard
diff --git a/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml b/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml
index 44108cf513..ad95457b1d 100644
--- a/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml
+++ b/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml
@@ -75,6 +75,19 @@
color: "#c9c042"
- type: Computer
board: PowerComputerCircuitboard
+ - type: PowerMonitoringConsole
+ - type: NodeContainer
+ examinable: true
+ nodes:
+ hv:
+ !type:CableDeviceNode
+ nodeGroupID: HVPower
+ - type: ActivatableUI
+ key: enum.PowerMonitoringConsoleUiKey.Key
+ - type: UserInterface
+ interfaces:
+ - key: enum.PowerMonitoringConsoleUiKey.Key
+ type: PowerMonitoringConsoleBoundUserInterface
- type: entity
parent: ComputerBase