Writing a plugin
A .NET class library that references rPDU2MQTT.Core and implements IIntegration plus any capabilities. Copy the DLL into plugins/.
Example: HelloWorld, a destination that writes every reading and tier to a file.
The project
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<!-- Ship only your own DLL; the host provides Core. -->
<ProjectReference Include="path/to/rPDU2MQTT.Core.csproj" Private="false" ExcludeAssets="runtime" />
</ItemGroup>
</Project>
rPDU2MQTT.Core contains the contracts, config model, flow engine and helpers. No ASP.NET or MQTT client dependency.
The identity
public sealed class MyPlugin : IIntegration
{
public string Id => "influx"; // lowercase, stable
public string DisplayName => "InfluxDB"; // nav label, status card
public IntegrationGroup Group => IntegrationGroup.Destinations;
public bool Enabled(Config cfg) => settings.Enabled; // read live
// Optional: a non-null reason disables the plugin and is reported.
public string? Misconfigured(Config cfg) => ...;
}
The capabilities
Implement any combination. EmonCMS, for example, is a destination, a history provider and a configuration publisher.
| Interface | For |
|---|---|
IMeasurementDestination |
Receive readings and the flow hierarchy on each poll. |
IMeasurementHistory |
Answer what a node read at a past instant. |
IConfigurationPublisher |
Push structure to the far end — entities, feeds, dashboards — and sweep what you no longer own. |
INodeProvider |
Offer nodes the operator could adopt (discovery only; never write config). |
IValueSourcePlugin |
Supply live values for nodes bound to your source type. |
IDeviceSourcePlugin |
Poll hardware into a snapshot — this is how a second PDU vendor is supported. |
IDeviceControlPlugin |
Switch its outlets, when the hardware can. |
IIntegrationApi |
Actions beyond the standard ones. |
IStatusProvider |
Decide what your own health means, when the default is not specific enough. |
IConfigurablePlugin |
Carry your own settings section. |
Receiving data
public Task SendAsync(ExportPass pass, CancellationToken ct)
{
foreach (var r in pass.Readings) // every PDU measurement
Send(r.NodeId, r.Type, r.Value, r.Units);
foreach (var t in pass.TiersFor(Tags(cfg))) // flow tiers, filtered by tag
Send(t.Node.Id, t.Metric, t.Value, t.Units);
return Task.CompletedTask;
}
ExportPassis built once per poll and shared by every destination.- An exception is recorded against the plugin id; other destinations still run.
- A tier with no value is absent from
Tiers. LeaderGateddefaults totrue: one process in a cluster calls you. Setfalsefor per-process output (Prometheus does).
Your settings
public sealed class MySettings
{
[DefaultValue(false)]
[Description("Send readings to InfluxDB.")] // shown under the field in the GUI
public bool Enabled { get; set; }
[Description("Base URL, e.g. http://influx:8086.")]
public string? Url { get; set; }
}
// on the plugin:
public Type ConfigType => typeof(MySettings);
public void ApplyConfig(object s) => settings = (MySettings)s;
The GUI generates a settings page from the class: typed inputs, defaults, descriptions.
Stored under Plugins:, keyed by id:
Plugins:
influx:
Enabled: true
Url: http://influx:8086
Your actions
Standard actions come from the implemented interfaces:
| Action | Comes from |
|---|---|
probe |
IIntegration — always present |
publish, sweep |
IConfigurationPublisher |
Custom actions:
public IReadOnlyList<IntegrationAction> Actions =>
[
new("backfill", "Backfill", "Re-send the last hour.", ActionEffect.Write,
async (ctx, ct) => new { ok = true, sent = await Backfill(ctx.Int("hours", 1), ct) }),
];
- Each action is at
POST /api/integrations/{id}/{action}and gets a button on the settings page. ActionEffect.Destructivemakes the GUI confirm first.- Query and form values arrive on the action context.
Supplying values, or being a device
Two capabilities read into the bridge:
// A node binds { Type: "mything", Metric: "realpower", Settings: { … } } and you supply the value.
public string SourceType => "mything";
public string SourceTypeLabel => "My Thing";
public Task ReconcileAsync(Config cfg, IReadOnlyList<SourceBinding> bindings, CancellationToken ct)
{
// Called with this source type's bindings, and again whenever the configuration changes.
foreach (var b in bindings) values[b.NodeId + "|" + b.Key()] = Read(b.Setting("Address"));
return Task.CompletedTask;
}
public bool TryGetValue(string nodeId, string metric, out double value) => values.TryGetValue(…);
// Or poll hardware. The snapshot goes through the same pipeline as a Vertiv PDU's.
public string InstanceId => "mydevice";
public Task<PduData?> PollAsync(Config cfg, CancellationToken ct) => …;
// Optional: switching its outlets.
public bool Supports(string action) => action is "on" or "off";
public Task<string> ControlOutletAsync(Config cfg, string deviceId, int outlet, string action, CancellationToken ct) => …;
- Return
nullwhen there is nothing to report. An empty snapshot reads as every outlet at zero. - Return the result the hardware reports, not the requested state.
- The host provides the poll timer, leader gate and device lock.
Installing it
dotnet build
cp bin/Debug/net10.0/MyPlugin.dll <bridge>/plugins/
Restart. The log shows Plugin loaded: MyPlugin.dll — influx. RPDU2MQTT_PLUGINS sets another directory.
- Each plugin has its own load context.
- A plugin that fails to load is reported and skipped.
Limits
- The Kubernetes CRD does not validate plugin settings. They are stored in the open
Pluginsmap. - No custom editors. Plugins get the generated settings page and action buttons.