Skip to content

Pattern 20: Scenario List

When to Use

Your integration needs to know which correlation scenarios exist in PQ — for example, to offer them in the external system's own configuration or to show a scenario name in its log. The framework keeps the list current for you.

Declare the capability in adapter-registration.yaml:

capabilities:
  scenarios: true

Without it, IScenarioCatalog is not registered and resolving it from DI fails.


IScenarioCatalog

Pq.Adapters.Framework.Capabilities.Scenarios.IScenarioCatalog is available through DI once the capability is declared. There is one instance per adapter process, shared by all devices.

Member Description
Task<IReadOnlyList<ScenarioInfo>> GetScenarios(CancellationToken) All scenarios, disabled ones included
event EventHandler? Changed Raised after the list was reloaded

ScenarioInfo is a record: Id (Guid), Name (string), Enabled (bool). The list contains every scenario; filter on Enabled yourself if you only want active ones.

Lifecycle

  • Lazy. Nothing happens until the first GetScenarios call. That call subscribes to scenario changes and loads the list from the PQ server.
  • Kept current. Whenever a scenario is created, renamed or otherwise changed, deleted, enabled or disabled, the framework reloads the whole list and raises Changed. Call GetScenarios again to read it.
  • Failure. If the first load fails, GetScenarios throws; the next call retries. If a reload fails, the previous list is kept and the next scenario change retries.
  • Limitation. A change made while the adapter is disconnected from the message bus is picked up with the next scenario change, not on reconnect.

Example

Push the list to the external system after connecting and after every change. Subscribe from a long-lived service (not from a Thing), and unsubscribe when it stops — the catalog lives for the whole process and would otherwise keep your handler alive.

public sealed class ScenarioPublisher(IScenarioCatalog catalog, Protocol protocol) : IDisposable
{
    public async Task Start(CancellationToken ct)
    {
        catalog.Changed += OnChanged;
        await Push(ct);
    }

    public void Dispose() => catalog.Changed -= OnChanged;

    private async void OnChanged(object? sender, EventArgs e)
    {
        try
        {
            await Push(CancellationToken.None);
        }
        catch (Exception ex)
        {
            // log: the next change pushes the full list again
        }
    }

    private async Task Push(CancellationToken ct)
    {
        var scenarios = await catalog.GetScenarios(ct);
        await protocol.SendScenarioList(scenarios.Where(s => s.Enabled), ct);
    }
}

Protocol.SendScenarioList stands for your own protocol call. Always send the full list: it is small, and a full list makes a missed or repeated change harmless.


Scenario Notify Command

To let PQ tell your external system that a scenario fired, support the framework command pq.command.scenario.notify. List it on the device type that represents the external system:

device_types:
  partner_system:
    commands:
      - pq.command.scenario.notify

In the scenario editor, the operator adds a Send command action, picks that device and the Notify Scenario command. There is nothing else to fill in: the server sets every parameter from the scenario that fired.

Parameter Type Content
ScenarioName string Scenario name as it is when the scenario fires
ScenarioId Guid? Scenario identifier
Persons EntityRef[]? Every distinct person named by the events that fired the scenario
Devices EntityRef[]? Every distinct device named by the events that fired the scenario

EntityRef (Pq.Domain.Api.Adapter) is a record: Id (Guid), Name (string). For a scheduled or manual run there are no firing events, so both lists are empty.

The command does not need capabilities: scenarios — that flag is only for the scenario list. Write the handler like any other:

public async Task<DeviceCommandResult> Notify(
    PartnerSystem system,
    Scenario.Notify command,
    Protocol protocol,
    CancellationToken ct)
{
    await protocol.SendScenarioFired(command.ScenarioName, command.Persons ?? [], command.Devices ?? [], ct);
    return DeviceCommandResult.Succeeded();
}

Protocol.SendScenarioFired stands for your own protocol call.