mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-20 16:52:07 +08:00
5d26681ac3
# Description `MachineryMeasurementValue` gets an additive init-only `NodeId`; the positional constructor is unchanged. `ReadMeteringPointAsync` fills it, and so do `ReadMainMeteringPointAsync` and `ReadSubMetersAsync`, which use it. The values are now read by the NodeIds the browse returned, in one batched Read, instead of translating each browse name back to a NodeId first. That is one round trip less, and value and NodeId come from the same node. `docs/Machinery.md` shows how to subscribe to the measurements. Tests: - New end-to-end test `MeteringPointMeasurementsCarryTheNodeIdOfTheirVariableAsync` against `MachineryIntegrationServer`, covering the main metering point and the sub-meter. It reads `BrowseName` and `Value` behind every returned NodeId. It fails on master. - `Opc.Ua.Machinery.Tests` 165/165 on net10.0; all target frameworks build with 0 warnings. - Fork CI (full matrix incl. macOS) and CodeQL: green on `afa00065a` (the same change on master `ce88955f7`) — [CI run 36826789991](https://github.com/biancode/UA-.NETStandard/actions/runs/36826789991) (189 jobs), [CodeQL run 36826793071](https://github.com/biancode/UA-.NETStandard/actions/runs/36826793071). Rebasing onto master with #4607 needed one conflict resolution in `MachineryEndToEndTests.cs` (both new tests kept); `Opc.Ua.Machinery.Tests` passes 165/165 on net10.0 locally afterwards and CodeQL is green on that state ([run 36979020583](https://github.com/biancode/UA-.NETStandard/actions/runs/36979020583)). The run on the current head is in progress: [CI run 37016200389](https://github.com/biancode/UA-.NETStandard/actions/runs/37016200389). ## Related Issues - Fixes #4641 ## Checklist - [x] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [x] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [ ] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. (Affected suites on net10.0 and net8.0 locally on macOS; .NET Framework is covered by the fork CI's Windows net48 legs.) - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received.
255 lines
11 KiB
C#
255 lines
11 KiB
C#
/* ========================================================================
|
|
* Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved.
|
|
*
|
|
* OPC Foundation MIT License 1.00
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person
|
|
* obtaining a copy of this software and associated documentation
|
|
* files (the "Software"), to deal in the Software without
|
|
* restriction, including without limitation the rights to use,
|
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the
|
|
* Software is furnished to do so, subject to the following
|
|
* conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be
|
|
* included in all copies or substantial portions of the Software.
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
* OTHER DEALINGS IN THE SOFTWARE.
|
|
*
|
|
* The complete license agreement can be found here:
|
|
* http://opcfoundation.org/License/MIT/1.00/
|
|
* ======================================================================*/
|
|
|
|
using System.Collections.Generic;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using EcmBrowseNames = Opc.Ua.ECM.BrowseNames;
|
|
using MachineryBrowseNames = Opc.Ua.Machinery.BrowseNames;
|
|
|
|
namespace Opc.Ua.Machinery.Client
|
|
{
|
|
public sealed partial class MachineryClient
|
|
{
|
|
/// <summary>
|
|
/// Resolves a machinery item's <c>Monitoring/Consumption</c> folder,
|
|
/// the entry point OPC 40001-4 puts energy information under, or
|
|
/// <see cref="NodeId.Null"/> when the item publishes none.
|
|
/// </summary>
|
|
/// <param name="machineryItem">The machine or component to inspect.</param>
|
|
/// <param name="cancellationToken">Cancels the operation.</param>
|
|
public ValueTask<NodeId> ResolveConsumptionAsync(
|
|
NodeId machineryItem,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ushort machineryNamespaceIndex = NamespaceIndexOf(
|
|
Opc.Ua.Machinery.Namespaces.Machinery);
|
|
return ResolvePathAsync(
|
|
machineryItem,
|
|
cancellationToken,
|
|
new QualifiedName(MachineryBrowseNames.Monitoring, machineryNamespaceIndex),
|
|
new QualifiedName(MachineryBrowseNames.Consumption, machineryNamespaceIndex));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Enumerates the OPC 40001-4 resource folders a machinery item
|
|
/// publishes — one per energy resource it meters.
|
|
/// </summary>
|
|
/// <param name="machineryItem">The machine or component to inspect.</param>
|
|
/// <param name="cancellationToken">Cancels the operation.</param>
|
|
public async IAsyncEnumerable<MachineEntry> EnumerateEnergyResourcesAsync(
|
|
NodeId machineryItem,
|
|
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
|
{
|
|
NodeId consumption = await ResolveConsumptionAsync(machineryItem, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
await foreach (MachineEntry entry in EnumerateChildObjectsAsync(
|
|
consumption,
|
|
cancellationToken).ConfigureAwait(false))
|
|
{
|
|
yield return entry;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Enumerates every metering point of an OPC 40001-4 resource folder:
|
|
/// <c>Main</c> and the further points the resource publishes next to
|
|
/// it.
|
|
/// </summary>
|
|
/// <param name="resource">The resource folder to inspect.</param>
|
|
/// <param name="cancellationToken">Cancels the operation.</param>
|
|
public IAsyncEnumerable<MachineEntry> EnumerateMeteringPointsAsync(
|
|
NodeId resource,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
return EnumerateChildObjectsAsync(resource, cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Enumerates the sub-meters of a metering point — the points it
|
|
/// <c>Contains</c>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// OPC 40001-4 §8.1 runs <c>Contains</c> from a metering point to a
|
|
/// point whose measurements are part of it, typically from <c>Main</c>
|
|
/// to the points below the same resource. The reference is followed
|
|
/// forward and with its subtypes, so a vendor refinement of it is
|
|
/// found too; a point that contains nothing yields nothing.
|
|
/// </remarks>
|
|
/// <param name="meteringPoint">The containing metering point.</param>
|
|
/// <param name="cancellationToken">Cancels the operation.</param>
|
|
public IAsyncEnumerable<MachineEntry> EnumerateSubMetersAsync(
|
|
NodeId meteringPoint,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var contains = new NodeId(
|
|
Opc.Ua.Machinery.Energy.ReferenceTypes.Contains,
|
|
NamespaceIndexOf(Opc.Ua.Machinery.Energy.Namespaces.MachineryEnergy));
|
|
return BrowseChildrenAsync(
|
|
meteringPoint,
|
|
NodeClass.Object,
|
|
contains,
|
|
cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads every sub-meter a metering point <c>Contains</c>, the same
|
|
/// way <see cref="ReadMeteringPointAsync"/> reads one point.
|
|
/// </summary>
|
|
/// <param name="meteringPoint">The containing metering point.</param>
|
|
/// <param name="cancellationToken">Cancels the operation.</param>
|
|
public async IAsyncEnumerable<MachineryMeteringPoint> ReadSubMetersAsync(
|
|
NodeId meteringPoint,
|
|
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
|
{
|
|
await foreach (MachineEntry subMeter in EnumerateSubMetersAsync(
|
|
meteringPoint,
|
|
cancellationToken).ConfigureAwait(false))
|
|
{
|
|
MachineryMeteringPoint? point = await ReadMeteringPointAsync(
|
|
subMeter.NodeId,
|
|
cancellationToken).ConfigureAwait(false);
|
|
if (point != null)
|
|
{
|
|
yield return point;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads the <c>Main</c> metering point of a resource, or
|
|
/// <see langword="null"/> when the resource publishes none.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// OPC 40001-4 requires a <c>Main</c> point per resource folder, so a
|
|
/// null here means the server is not conformant rather than that the
|
|
/// feature is absent.
|
|
/// </remarks>
|
|
/// <param name="resource">The resource folder to read.</param>
|
|
/// <param name="cancellationToken">Cancels the operation.</param>
|
|
public async ValueTask<MachineryMeteringPoint?> ReadMainMeteringPointAsync(
|
|
NodeId resource,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ushort energyNamespaceIndex = NamespaceIndexOf(
|
|
Opc.Ua.Machinery.Energy.Namespaces.MachineryEnergy);
|
|
NodeId main = await ResolveChildAsync(
|
|
resource,
|
|
new QualifiedName(
|
|
Opc.Ua.Machinery.Energy.BrowseNames.Main,
|
|
energyNamespaceIndex),
|
|
cancellationToken).ConfigureAwait(false);
|
|
return main.IsNull
|
|
? null
|
|
: await ReadMeteringPointAsync(main, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads one OPC 34100 metering point: its application tag and every
|
|
/// measurement value below it.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Each measurement carries the NodeId of its variable, so a client
|
|
/// that wants live values subscribes to it without browsing the
|
|
/// point again. A measurement whose value cannot be read is left out.
|
|
/// </remarks>
|
|
/// <param name="meteringPoint">The metering point to read.</param>
|
|
/// <param name="cancellationToken">Cancels the operation.</param>
|
|
public async ValueTask<MachineryMeteringPoint?> ReadMeteringPointAsync(
|
|
NodeId meteringPoint,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (meteringPoint.IsNull)
|
|
{
|
|
return null;
|
|
}
|
|
ushort ecmNamespaceIndex = NamespaceIndexOf(Opc.Ua.ECM.Namespaces.ECM);
|
|
var applicationTagName = new QualifiedName(
|
|
EcmBrowseNames.ApplicationTag,
|
|
ecmNamespaceIndex);
|
|
|
|
Dictionary<QualifiedName, Variant> values = await ReadChildValuesAsync(
|
|
meteringPoint,
|
|
[applicationTagName],
|
|
Opc.Ua.ReferenceTypeIds.HierarchicalReferences,
|
|
cancellationToken).ConfigureAwait(false);
|
|
|
|
var variables = new List<MachineEntry>();
|
|
var measurements = new List<MachineryMeasurementValue>();
|
|
await foreach (MachineEntry child in EnumerateChildVariablesAsync(
|
|
meteringPoint,
|
|
cancellationToken).ConfigureAwait(false))
|
|
{
|
|
if (child.BrowseName != applicationTagName)
|
|
{
|
|
variables.Add(child);
|
|
}
|
|
}
|
|
if (variables.Count > 0)
|
|
{
|
|
// The browse already returned the NodeIds, so the values are
|
|
// read by them in one batched Read.
|
|
var reads = new ReadValueId[variables.Count];
|
|
for (int ii = 0; ii < variables.Count; ii++)
|
|
{
|
|
reads[ii] = new ReadValueId
|
|
{
|
|
NodeId = variables[ii].NodeId,
|
|
AttributeId = Attributes.Value
|
|
};
|
|
}
|
|
ReadResponse read = await Session.ReadAsync(
|
|
requestHeader: null,
|
|
maxAge: 0,
|
|
timestampsToReturn: TimestampsToReturn.Neither,
|
|
nodesToRead: reads.ToArrayOf(),
|
|
ct: cancellationToken).ConfigureAwait(false);
|
|
for (int ii = 0; ii < read.Results.Count && ii < variables.Count; ii++)
|
|
{
|
|
if (StatusCode.IsGood(read.Results[ii].StatusCode))
|
|
{
|
|
measurements.Add(new MachineryMeasurementValue(
|
|
variables[ii].BrowseName,
|
|
read.Results[ii].WrappedValue)
|
|
{
|
|
NodeId = variables[ii].NodeId
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return new MachineryMeteringPoint(
|
|
meteringPoint,
|
|
AsString(values, applicationTagName),
|
|
measurements.ToArrayOf());
|
|
}
|
|
}
|
|
}
|