Add bulk MXAccess subscription commands

This commit is contained in:
Joseph Doherty
2026-04-26 22:29:27 -04:00
parent daff16cfd2
commit 3d11ac3316
31 changed files with 14346 additions and 969 deletions
@@ -137,6 +137,44 @@ public sealed class MxGatewayClientSessionTests
Assert.Equal(56, request.Command.Write2.UserId);
}
[Fact]
public async Task SubscribeBulkAsync_BuildsOneBulkCommandAndReturnsPerItemResults()
{
FakeGatewayTransport transport = CreateTransport();
transport.AddInvokeReply(new MxCommandReply
{
SessionId = "session-fixture",
Kind = MxCommandKind.SubscribeBulk,
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
SubscribeBulk = new BulkSubscribeReply
{
Results =
{
new SubscribeResult
{
ServerHandle = 12,
TagAddress = "Area001.Pump001.Speed",
ItemHandle = 34,
WasSuccessful = true,
},
},
},
});
await using MxGatewayClient client = CreateClient(transport);
MxGatewaySession session = await client.OpenSessionAsync();
IReadOnlyList<SubscribeResult> results = await session.SubscribeBulkAsync(
12,
["Area001.Pump001.Speed"]);
SubscribeResult result = Assert.Single(results);
Assert.Equal(34, result.ItemHandle);
MxCommandRequest request = Assert.Single(transport.InvokeCalls).Request;
Assert.Equal(MxCommandKind.SubscribeBulk, request.Command.Kind);
Assert.Equal(12, request.Command.SubscribeBulk.ServerHandle);
Assert.Equal(["Area001.Pump001.Speed"], request.Command.SubscribeBulk.TagAddresses);
}
[Fact]
public async Task StreamEventsAsync_YieldsEventsInGatewayOrder()
{
@@ -175,6 +175,194 @@ public sealed class MxGatewaySession : IAsyncDisposable
cancellationToken);
}
public async Task UnAdviseAsync(
int serverHandle,
int itemHandle,
CancellationToken cancellationToken = default)
{
MxCommandReply reply = await UnAdviseRawAsync(serverHandle, itemHandle, cancellationToken)
.ConfigureAwait(false);
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
}
public Task<MxCommandReply> UnAdviseRawAsync(
int serverHandle,
int itemHandle,
CancellationToken cancellationToken = default)
{
return InvokeCommandAsync(
new MxCommand
{
Kind = MxCommandKind.UnAdvise,
UnAdvise = new UnAdviseCommand
{
ServerHandle = serverHandle,
ItemHandle = itemHandle,
},
},
cancellationToken);
}
public async Task RemoveItemAsync(
int serverHandle,
int itemHandle,
CancellationToken cancellationToken = default)
{
MxCommandReply reply = await RemoveItemRawAsync(serverHandle, itemHandle, cancellationToken)
.ConfigureAwait(false);
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
}
public Task<MxCommandReply> RemoveItemRawAsync(
int serverHandle,
int itemHandle,
CancellationToken cancellationToken = default)
{
return InvokeCommandAsync(
new MxCommand
{
Kind = MxCommandKind.RemoveItem,
RemoveItem = new RemoveItemCommand
{
ServerHandle = serverHandle,
ItemHandle = itemHandle,
},
},
cancellationToken);
}
public async Task<IReadOnlyList<SubscribeResult>> AddItemBulkAsync(
int serverHandle,
IReadOnlyList<string> tagAddresses,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(tagAddresses);
AddItemBulkCommand command = new() { ServerHandle = serverHandle };
command.TagAddresses.Add(tagAddresses);
MxCommandReply reply = await InvokeCommandAsync(
new MxCommand
{
Kind = MxCommandKind.AddItemBulk,
AddItemBulk = command,
},
cancellationToken)
.ConfigureAwait(false);
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
return reply.AddItemBulk?.Results.ToArray() ?? [];
}
public async Task<IReadOnlyList<SubscribeResult>> AdviseItemBulkAsync(
int serverHandle,
IReadOnlyList<int> itemHandles,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(itemHandles);
AdviseItemBulkCommand command = new() { ServerHandle = serverHandle };
command.ItemHandles.Add(itemHandles);
MxCommandReply reply = await InvokeCommandAsync(
new MxCommand
{
Kind = MxCommandKind.AdviseItemBulk,
AdviseItemBulk = command,
},
cancellationToken)
.ConfigureAwait(false);
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
return reply.AdviseItemBulk?.Results.ToArray() ?? [];
}
public async Task<IReadOnlyList<SubscribeResult>> RemoveItemBulkAsync(
int serverHandle,
IReadOnlyList<int> itemHandles,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(itemHandles);
RemoveItemBulkCommand command = new() { ServerHandle = serverHandle };
command.ItemHandles.Add(itemHandles);
MxCommandReply reply = await InvokeCommandAsync(
new MxCommand
{
Kind = MxCommandKind.RemoveItemBulk,
RemoveItemBulk = command,
},
cancellationToken)
.ConfigureAwait(false);
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
return reply.RemoveItemBulk?.Results.ToArray() ?? [];
}
public async Task<IReadOnlyList<SubscribeResult>> UnAdviseItemBulkAsync(
int serverHandle,
IReadOnlyList<int> itemHandles,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(itemHandles);
UnAdviseItemBulkCommand command = new() { ServerHandle = serverHandle };
command.ItemHandles.Add(itemHandles);
MxCommandReply reply = await InvokeCommandAsync(
new MxCommand
{
Kind = MxCommandKind.UnAdviseItemBulk,
UnAdviseItemBulk = command,
},
cancellationToken)
.ConfigureAwait(false);
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
return reply.UnAdviseItemBulk?.Results.ToArray() ?? [];
}
public async Task<IReadOnlyList<SubscribeResult>> SubscribeBulkAsync(
int serverHandle,
IReadOnlyList<string> tagAddresses,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(tagAddresses);
SubscribeBulkCommand command = new() { ServerHandle = serverHandle };
command.TagAddresses.Add(tagAddresses);
MxCommandReply reply = await InvokeCommandAsync(
new MxCommand
{
Kind = MxCommandKind.SubscribeBulk,
SubscribeBulk = command,
},
cancellationToken)
.ConfigureAwait(false);
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
return reply.SubscribeBulk?.Results.ToArray() ?? [];
}
public async Task<IReadOnlyList<SubscribeResult>> UnsubscribeBulkAsync(
int serverHandle,
IReadOnlyList<int> itemHandles,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(itemHandles);
UnsubscribeBulkCommand command = new() { ServerHandle = serverHandle };
command.ItemHandles.Add(itemHandles);
MxCommandReply reply = await InvokeCommandAsync(
new MxCommand
{
Kind = MxCommandKind.UnsubscribeBulk,
UnsubscribeBulk = command,
},
cancellationToken)
.ConfigureAwait(false);
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
return reply.UnsubscribeBulk?.Results.ToArray() ?? [];
}
public async Task WriteAsync(
int serverHandle,
int itemHandle,
@@ -297,4 +485,5 @@ public sealed class MxGatewaySession : IAsyncDisposable
},
cancellationToken);
}
}
File diff suppressed because it is too large Load Diff
@@ -117,6 +117,49 @@ func TestSessionHelpersBuildCommandsAndExposeRawReply(t *testing.T) {
}
}
func TestSubscribeBulkBuildsOneBulkCommandAndReturnsResults(t *testing.T) {
fake := &fakeGatewayServer{
invokeReply: &pb.MxCommandReply{
SessionId: "session-1",
Kind: pb.MxCommandKind_MX_COMMAND_KIND_SUBSCRIBE_BULK,
ProtocolStatus: &pb.ProtocolStatus{
Code: pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_OK,
},
Payload: &pb.MxCommandReply_SubscribeBulk{
SubscribeBulk: &pb.BulkSubscribeReply{
Results: []*pb.SubscribeResult{
{
ServerHandle: 12,
TagAddress: "Area001.Pump001.Speed",
ItemHandle: 34,
WasSuccessful: true,
},
},
},
},
},
}
client, cleanup := newBufconnClient(t, fake)
defer cleanup()
session := NewSessionForID(client, "session-1")
results, err := session.SubscribeBulk(context.Background(), 12, []string{"Area001.Pump001.Speed"})
if err != nil {
t.Fatalf("SubscribeBulk() error = %v", err)
}
if len(results) != 1 || results[0].GetItemHandle() != 34 {
t.Fatalf("results = %#v, want item handle 34", results)
}
req := fake.invokeRequest
if req.GetCommand().GetKind() != pb.MxCommandKind_MX_COMMAND_KIND_SUBSCRIBE_BULK {
t.Fatalf("command kind = %s", req.GetCommand().GetKind())
}
if got := req.GetCommand().GetSubscribeBulk().GetTagAddresses(); len(got) != 1 || got[0] != "Area001.Pump001.Speed" {
t.Fatalf("tag addresses = %#v", got)
}
}
func TestInvokeReturnsTypedMxAccessErrorWithRawReply(t *testing.T) {
hresult := int32(-2147467259)
fake := &fakeGatewayServer{
+158
View File
@@ -104,6 +104,25 @@ func (s *Session) Unregister(ctx context.Context, serverHandle int32) error {
return err
}
// RemoveItem invokes MXAccess RemoveItem.
func (s *Session) RemoveItem(ctx context.Context, serverHandle, itemHandle int32) error {
_, err := s.RemoveItemRaw(ctx, serverHandle, itemHandle)
return err
}
// RemoveItemRaw invokes MXAccess RemoveItem and returns the raw reply.
func (s *Session) RemoveItemRaw(ctx context.Context, serverHandle, itemHandle int32) (*MxCommandReply, error) {
return s.invokeCommand(ctx, &pb.MxCommand{
Kind: pb.MxCommandKind_MX_COMMAND_KIND_REMOVE_ITEM,
Payload: &pb.MxCommand_RemoveItem{
RemoveItem: &pb.RemoveItemCommand{
ServerHandle: serverHandle,
ItemHandle: itemHandle,
},
},
})
}
// AddItem invokes MXAccess AddItem and returns the item handle.
func (s *Session) AddItem(ctx context.Context, serverHandle int32, itemDefinition string) (int32, error) {
reply, err := s.AddItemRaw(ctx, serverHandle, itemDefinition)
@@ -182,6 +201,145 @@ func (s *Session) AdviseRaw(ctx context.Context, serverHandle, itemHandle int32)
})
}
// UnAdvise invokes MXAccess UnAdvise.
func (s *Session) UnAdvise(ctx context.Context, serverHandle, itemHandle int32) error {
_, err := s.UnAdviseRaw(ctx, serverHandle, itemHandle)
return err
}
// UnAdviseRaw invokes MXAccess UnAdvise and returns the raw reply.
func (s *Session) UnAdviseRaw(ctx context.Context, serverHandle, itemHandle int32) (*MxCommandReply, error) {
return s.invokeCommand(ctx, &pb.MxCommand{
Kind: pb.MxCommandKind_MX_COMMAND_KIND_UN_ADVISE,
Payload: &pb.MxCommand_UnAdvise{
UnAdvise: &pb.UnAdviseCommand{
ServerHandle: serverHandle,
ItemHandle: itemHandle,
},
},
})
}
// AddItemBulk invokes MXAccess AddItem for each tag inside one gateway command.
func (s *Session) AddItemBulk(ctx context.Context, serverHandle int32, tagAddresses []string) ([]*SubscribeResult, error) {
if tagAddresses == nil {
return nil, errors.New("mxgateway: tag addresses are required")
}
reply, err := s.invokeCommand(ctx, &pb.MxCommand{
Kind: pb.MxCommandKind_MX_COMMAND_KIND_ADD_ITEM_BULK,
Payload: &pb.MxCommand_AddItemBulk{
AddItemBulk: &pb.AddItemBulkCommand{
ServerHandle: serverHandle,
TagAddresses: tagAddresses,
},
},
})
if err != nil {
return nil, err
}
return reply.GetAddItemBulk().GetResults(), nil
}
// AdviseItemBulk invokes MXAccess Advise for each item handle inside one gateway command.
func (s *Session) AdviseItemBulk(ctx context.Context, serverHandle int32, itemHandles []int32) ([]*SubscribeResult, error) {
if itemHandles == nil {
return nil, errors.New("mxgateway: item handles are required")
}
reply, err := s.invokeCommand(ctx, &pb.MxCommand{
Kind: pb.MxCommandKind_MX_COMMAND_KIND_ADVISE_ITEM_BULK,
Payload: &pb.MxCommand_AdviseItemBulk{
AdviseItemBulk: &pb.AdviseItemBulkCommand{
ServerHandle: serverHandle,
ItemHandles: itemHandles,
},
},
})
if err != nil {
return nil, err
}
return reply.GetAdviseItemBulk().GetResults(), nil
}
// RemoveItemBulk invokes MXAccess RemoveItem for each item handle inside one gateway command.
func (s *Session) RemoveItemBulk(ctx context.Context, serverHandle int32, itemHandles []int32) ([]*SubscribeResult, error) {
if itemHandles == nil {
return nil, errors.New("mxgateway: item handles are required")
}
reply, err := s.invokeCommand(ctx, &pb.MxCommand{
Kind: pb.MxCommandKind_MX_COMMAND_KIND_REMOVE_ITEM_BULK,
Payload: &pb.MxCommand_RemoveItemBulk{
RemoveItemBulk: &pb.RemoveItemBulkCommand{
ServerHandle: serverHandle,
ItemHandles: itemHandles,
},
},
})
if err != nil {
return nil, err
}
return reply.GetRemoveItemBulk().GetResults(), nil
}
// UnAdviseItemBulk invokes MXAccess UnAdvise for each item handle inside one gateway command.
func (s *Session) UnAdviseItemBulk(ctx context.Context, serverHandle int32, itemHandles []int32) ([]*SubscribeResult, error) {
if itemHandles == nil {
return nil, errors.New("mxgateway: item handles are required")
}
reply, err := s.invokeCommand(ctx, &pb.MxCommand{
Kind: pb.MxCommandKind_MX_COMMAND_KIND_UN_ADVISE_ITEM_BULK,
Payload: &pb.MxCommand_UnAdviseItemBulk{
UnAdviseItemBulk: &pb.UnAdviseItemBulkCommand{
ServerHandle: serverHandle,
ItemHandles: itemHandles,
},
},
})
if err != nil {
return nil, err
}
return reply.GetUnAdviseItemBulk().GetResults(), nil
}
// SubscribeBulk invokes AddItem and Advise for each tag inside one gateway command.
func (s *Session) SubscribeBulk(ctx context.Context, serverHandle int32, tagAddresses []string) ([]*SubscribeResult, error) {
if tagAddresses == nil {
return nil, errors.New("mxgateway: tag addresses are required")
}
reply, err := s.invokeCommand(ctx, &pb.MxCommand{
Kind: pb.MxCommandKind_MX_COMMAND_KIND_SUBSCRIBE_BULK,
Payload: &pb.MxCommand_SubscribeBulk{
SubscribeBulk: &pb.SubscribeBulkCommand{
ServerHandle: serverHandle,
TagAddresses: tagAddresses,
},
},
})
if err != nil {
return nil, err
}
return reply.GetSubscribeBulk().GetResults(), nil
}
// UnsubscribeBulk invokes UnAdvise and RemoveItem for each item handle inside one gateway command.
func (s *Session) UnsubscribeBulk(ctx context.Context, serverHandle int32, itemHandles []int32) ([]*SubscribeResult, error) {
if itemHandles == nil {
return nil, errors.New("mxgateway: item handles are required")
}
reply, err := s.invokeCommand(ctx, &pb.MxCommand{
Kind: pb.MxCommandKind_MX_COMMAND_KIND_UNSUBSCRIBE_BULK,
Payload: &pb.MxCommand_UnsubscribeBulk{
UnsubscribeBulk: &pb.UnsubscribeBulkCommand{
ServerHandle: serverHandle,
ItemHandles: itemHandles,
},
},
})
if err != nil {
return nil, err
}
return reply.GetUnsubscribeBulk().GetResults(), nil
}
// Write invokes MXAccess Write.
func (s *Session) Write(ctx context.Context, serverHandle, itemHandle int32, value *MxValue, userID int32) error {
_, err := s.WriteRaw(ctx, serverHandle, itemHandle, value, userID)
+49 -31
View File
@@ -12,30 +12,40 @@ type RawEventStream = pb.MxAccessGateway_StreamEventsClient
// Generated protobuf aliases keep raw contract access available from the public
// mxgateway package while generated code remains under internal/generated.
type (
OpenSessionRequest = pb.OpenSessionRequest
OpenSessionReply = pb.OpenSessionReply
CloseSessionRequest = pb.CloseSessionRequest
CloseSessionReply = pb.CloseSessionReply
StreamEventsRequest = pb.StreamEventsRequest
MxCommandRequest = pb.MxCommandRequest
MxCommandReply = pb.MxCommandReply
MxCommand = pb.MxCommand
MxEvent = pb.MxEvent
MxValue = pb.MxValue
Value = pb.MxValue
MxArray = pb.MxArray
MxStatusProxy = pb.MxStatusProxy
ProtocolStatus = pb.ProtocolStatus
RegisterCommand = pb.RegisterCommand
UnregisterCommand = pb.UnregisterCommand
AddItemCommand = pb.AddItemCommand
AddItem2Command = pb.AddItem2Command
AdviseCommand = pb.AdviseCommand
WriteCommand = pb.WriteCommand
Write2Command = pb.Write2Command
RegisterReply = pb.RegisterReply
AddItemReply = pb.AddItemReply
AddItem2Reply = pb.AddItem2Reply
OpenSessionRequest = pb.OpenSessionRequest
OpenSessionReply = pb.OpenSessionReply
CloseSessionRequest = pb.CloseSessionRequest
CloseSessionReply = pb.CloseSessionReply
StreamEventsRequest = pb.StreamEventsRequest
MxCommandRequest = pb.MxCommandRequest
MxCommandReply = pb.MxCommandReply
MxCommand = pb.MxCommand
MxEvent = pb.MxEvent
MxValue = pb.MxValue
Value = pb.MxValue
MxArray = pb.MxArray
MxStatusProxy = pb.MxStatusProxy
ProtocolStatus = pb.ProtocolStatus
RegisterCommand = pb.RegisterCommand
UnregisterCommand = pb.UnregisterCommand
AddItemCommand = pb.AddItemCommand
AddItem2Command = pb.AddItem2Command
RemoveItemCommand = pb.RemoveItemCommand
AdviseCommand = pb.AdviseCommand
UnAdviseCommand = pb.UnAdviseCommand
AddItemBulkCommand = pb.AddItemBulkCommand
AdviseItemBulkCommand = pb.AdviseItemBulkCommand
RemoveItemBulkCommand = pb.RemoveItemBulkCommand
UnAdviseItemBulkCommand = pb.UnAdviseItemBulkCommand
SubscribeBulkCommand = pb.SubscribeBulkCommand
UnsubscribeBulkCommand = pb.UnsubscribeBulkCommand
WriteCommand = pb.WriteCommand
Write2Command = pb.Write2Command
RegisterReply = pb.RegisterReply
AddItemReply = pb.AddItemReply
AddItem2Reply = pb.AddItem2Reply
SubscribeResult = pb.SubscribeResult
BulkSubscribeReply = pb.BulkSubscribeReply
)
type (
@@ -49,13 +59,21 @@ type (
)
const (
CommandKindRegister = pb.MxCommandKind_MX_COMMAND_KIND_REGISTER
CommandKindUnregister = pb.MxCommandKind_MX_COMMAND_KIND_UNREGISTER
CommandKindAddItem = pb.MxCommandKind_MX_COMMAND_KIND_ADD_ITEM
CommandKindAddItem2 = pb.MxCommandKind_MX_COMMAND_KIND_ADD_ITEM2
CommandKindAdvise = pb.MxCommandKind_MX_COMMAND_KIND_ADVISE
CommandKindWrite = pb.MxCommandKind_MX_COMMAND_KIND_WRITE
CommandKindWrite2 = pb.MxCommandKind_MX_COMMAND_KIND_WRITE2
CommandKindRegister = pb.MxCommandKind_MX_COMMAND_KIND_REGISTER
CommandKindUnregister = pb.MxCommandKind_MX_COMMAND_KIND_UNREGISTER
CommandKindAddItem = pb.MxCommandKind_MX_COMMAND_KIND_ADD_ITEM
CommandKindAddItem2 = pb.MxCommandKind_MX_COMMAND_KIND_ADD_ITEM2
CommandKindRemoveItem = pb.MxCommandKind_MX_COMMAND_KIND_REMOVE_ITEM
CommandKindAdvise = pb.MxCommandKind_MX_COMMAND_KIND_ADVISE
CommandKindUnAdvise = pb.MxCommandKind_MX_COMMAND_KIND_UN_ADVISE
CommandKindAddItemBulk = pb.MxCommandKind_MX_COMMAND_KIND_ADD_ITEM_BULK
CommandKindAdviseItemBulk = pb.MxCommandKind_MX_COMMAND_KIND_ADVISE_ITEM_BULK
CommandKindRemoveItemBulk = pb.MxCommandKind_MX_COMMAND_KIND_REMOVE_ITEM_BULK
CommandKindUnAdviseItemBulk = pb.MxCommandKind_MX_COMMAND_KIND_UN_ADVISE_ITEM_BULK
CommandKindSubscribeBulk = pb.MxCommandKind_MX_COMMAND_KIND_SUBSCRIBE_BULK
CommandKindUnsubscribeBulk = pb.MxCommandKind_MX_COMMAND_KIND_UNSUBSCRIBE_BULK
CommandKindWrite = pb.MxCommandKind_MX_COMMAND_KIND_WRITE
CommandKindWrite2 = pb.MxCommandKind_MX_COMMAND_KIND_WRITE2
DataTypeUnknown = pb.MxDataType_MX_DATA_TYPE_UNKNOWN
DataTypeBoolean = pb.MxDataType_MX_DATA_TYPE_BOOLEAN
@@ -2,9 +2,12 @@ package com.dohertylan.mxgateway.client;
import java.security.SecureRandom;
import java.util.HexFormat;
import java.util.List;
import java.util.Objects;
import mxaccess_gateway.v1.MxaccessGateway.AddItem2Command;
import mxaccess_gateway.v1.MxaccessGateway.AddItemBulkCommand;
import mxaccess_gateway.v1.MxaccessGateway.AddItemCommand;
import mxaccess_gateway.v1.MxaccessGateway.AdviseItemBulkCommand;
import mxaccess_gateway.v1.MxaccessGateway.AdviseCommand;
import mxaccess_gateway.v1.MxaccessGateway.CloseSessionReply;
import mxaccess_gateway.v1.MxaccessGateway.CloseSessionRequest;
@@ -15,7 +18,14 @@ import mxaccess_gateway.v1.MxaccessGateway.MxCommandRequest;
import mxaccess_gateway.v1.MxaccessGateway.MxValue;
import mxaccess_gateway.v1.MxaccessGateway.OpenSessionReply;
import mxaccess_gateway.v1.MxaccessGateway.RegisterCommand;
import mxaccess_gateway.v1.MxaccessGateway.RemoveItemBulkCommand;
import mxaccess_gateway.v1.MxaccessGateway.RemoveItemCommand;
import mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest;
import mxaccess_gateway.v1.MxaccessGateway.SubscribeBulkCommand;
import mxaccess_gateway.v1.MxaccessGateway.SubscribeResult;
import mxaccess_gateway.v1.MxaccessGateway.UnAdviseCommand;
import mxaccess_gateway.v1.MxaccessGateway.UnAdviseItemBulkCommand;
import mxaccess_gateway.v1.MxaccessGateway.UnsubscribeBulkCommand;
import mxaccess_gateway.v1.MxaccessGateway.UnregisterCommand;
import mxaccess_gateway.v1.MxaccessGateway.Write2Command;
import mxaccess_gateway.v1.MxaccessGateway.WriteCommand;
@@ -117,6 +127,19 @@ public final class MxGatewaySession implements AutoCloseable {
.build());
}
public void removeItem(int serverHandle, int itemHandle) {
removeItemRaw(serverHandle, itemHandle);
}
public MxCommandReply removeItemRaw(int serverHandle, int itemHandle) {
return invokeCommand(MxCommand.newBuilder()
.setKind(MxCommandKind.MX_COMMAND_KIND_REMOVE_ITEM)
.setRemoveItem(RemoveItemCommand.newBuilder()
.setServerHandle(serverHandle)
.setItemHandle(itemHandle))
.build());
}
public void advise(int serverHandle, int itemHandle) {
adviseRaw(serverHandle, itemHandle);
}
@@ -130,6 +153,85 @@ public final class MxGatewaySession implements AutoCloseable {
.build());
}
public void unAdvise(int serverHandle, int itemHandle) {
unAdviseRaw(serverHandle, itemHandle);
}
public MxCommandReply unAdviseRaw(int serverHandle, int itemHandle) {
return invokeCommand(MxCommand.newBuilder()
.setKind(MxCommandKind.MX_COMMAND_KIND_UN_ADVISE)
.setUnAdvise(UnAdviseCommand.newBuilder()
.setServerHandle(serverHandle)
.setItemHandle(itemHandle))
.build());
}
public List<SubscribeResult> addItemBulk(int serverHandle, List<String> tagAddresses) {
Objects.requireNonNull(tagAddresses, "tagAddresses");
MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
.setKind(MxCommandKind.MX_COMMAND_KIND_ADD_ITEM_BULK)
.setAddItemBulk(AddItemBulkCommand.newBuilder()
.setServerHandle(serverHandle)
.addAllTagAddresses(tagAddresses))
.build());
return reply.getAddItemBulk().getResultsList();
}
public List<SubscribeResult> adviseItemBulk(int serverHandle, List<Integer> itemHandles) {
Objects.requireNonNull(itemHandles, "itemHandles");
MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
.setKind(MxCommandKind.MX_COMMAND_KIND_ADVISE_ITEM_BULK)
.setAdviseItemBulk(AdviseItemBulkCommand.newBuilder()
.setServerHandle(serverHandle)
.addAllItemHandles(itemHandles))
.build());
return reply.getAdviseItemBulk().getResultsList();
}
public List<SubscribeResult> removeItemBulk(int serverHandle, List<Integer> itemHandles) {
Objects.requireNonNull(itemHandles, "itemHandles");
MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
.setKind(MxCommandKind.MX_COMMAND_KIND_REMOVE_ITEM_BULK)
.setRemoveItemBulk(RemoveItemBulkCommand.newBuilder()
.setServerHandle(serverHandle)
.addAllItemHandles(itemHandles))
.build());
return reply.getRemoveItemBulk().getResultsList();
}
public List<SubscribeResult> unAdviseItemBulk(int serverHandle, List<Integer> itemHandles) {
Objects.requireNonNull(itemHandles, "itemHandles");
MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
.setKind(MxCommandKind.MX_COMMAND_KIND_UN_ADVISE_ITEM_BULK)
.setUnAdviseItemBulk(UnAdviseItemBulkCommand.newBuilder()
.setServerHandle(serverHandle)
.addAllItemHandles(itemHandles))
.build());
return reply.getUnAdviseItemBulk().getResultsList();
}
public List<SubscribeResult> subscribeBulk(int serverHandle, List<String> tagAddresses) {
Objects.requireNonNull(tagAddresses, "tagAddresses");
MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
.setKind(MxCommandKind.MX_COMMAND_KIND_SUBSCRIBE_BULK)
.setSubscribeBulk(SubscribeBulkCommand.newBuilder()
.setServerHandle(serverHandle)
.addAllTagAddresses(tagAddresses))
.build());
return reply.getSubscribeBulk().getResultsList();
}
public List<SubscribeResult> unsubscribeBulk(int serverHandle, List<Integer> itemHandles) {
Objects.requireNonNull(itemHandles, "itemHandles");
MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
.setKind(MxCommandKind.MX_COMMAND_KIND_UNSUBSCRIBE_BULK)
.setUnsubscribeBulk(UnsubscribeBulkCommand.newBuilder()
.setServerHandle(serverHandle)
.addAllItemHandles(itemHandles))
.build());
return reply.getUnsubscribeBulk().getResultsList();
}
public void write(int serverHandle, int itemHandle, MxValue value, int userId) {
writeRaw(serverHandle, itemHandle, value, userId);
}
@@ -17,12 +17,14 @@ import io.grpc.inprocess.InProcessServerBuilder;
import io.grpc.stub.ServerCallStreamObserver;
import io.grpc.stub.StreamObserver;
import java.time.Duration;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import mxaccess_gateway.v1.MxAccessGatewayGrpc;
import mxaccess_gateway.v1.MxaccessGateway.AddItemReply;
import mxaccess_gateway.v1.MxaccessGateway.BulkSubscribeReply;
import mxaccess_gateway.v1.MxaccessGateway.CloseSessionReply;
import mxaccess_gateway.v1.MxaccessGateway.CloseSessionRequest;
import mxaccess_gateway.v1.MxaccessGateway.MxCommandKind;
@@ -36,6 +38,7 @@ import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatusCode;
import mxaccess_gateway.v1.MxaccessGateway.RegisterReply;
import mxaccess_gateway.v1.MxaccessGateway.SessionState;
import mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest;
import mxaccess_gateway.v1.MxaccessGateway.SubscribeResult;
import org.junit.jupiter.api.Test;
final class MxGatewayClientSessionTests {
@@ -112,6 +115,42 @@ final class MxGatewayClientSessionTests {
}
}
@Test
void subscribeBulkBuildsOneBulkCommandAndReturnsResults() throws Exception {
AtomicReference<MxCommandRequest> commandRequest = new AtomicReference<>();
TestGatewayService service = new TestGatewayService() {
@Override
public void invoke(MxCommandRequest request, StreamObserver<MxCommandReply> responseObserver) {
commandRequest.set(request);
responseObserver.onNext(MxCommandReply.newBuilder()
.setSessionId(request.getSessionId())
.setKind(request.getCommand().getKind())
.setProtocolStatus(ok())
.setSubscribeBulk(BulkSubscribeReply.newBuilder()
.addResults(SubscribeResult.newBuilder()
.setServerHandle(12)
.setTagAddress("Area001.Pump001.Speed")
.setItemHandle(34)
.setWasSuccessful(true)))
.build());
responseObserver.onCompleted();
}
};
try (InProcessGateway gateway = InProcessGateway.start(service, new AtomicReference<>());
MxGatewayClient client = gateway.client("", Duration.ofSeconds(5))) {
MxGatewaySession session = MxGatewaySession.forSessionId(client, "existing-session");
List<SubscribeResult> results = session.subscribeBulk(12, List.of("Area001.Pump001.Speed"));
assertEquals(34, results.get(0).getItemHandle());
assertEquals(MxCommandKind.MX_COMMAND_KIND_SUBSCRIBE_BULK, commandRequest.get().getCommand().getKind());
assertEquals(
List.of("Area001.Pump001.Speed"),
commandRequest.get().getCommand().getSubscribeBulk().getTagAddressesList());
}
}
@Test
void streamCancellationCancelsServerCall() throws Exception {
CountDownLatch cancelled = new CountDownLatch(1);
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+163 -1
View File
@@ -2,7 +2,7 @@
from __future__ import annotations
from collections.abc import AsyncIterator
from collections.abc import AsyncIterator, Sequence
from .errors import ensure_mxaccess_success
from .generated import mxaccess_gateway_pb2 as pb
@@ -89,6 +89,24 @@ class Session:
correlation_id=correlation_id,
)
async def remove_item(
self,
server_handle: int,
item_handle: int,
*,
correlation_id: str = "",
) -> None:
await self.invoke(
pb.MxCommand(
kind=pb.MX_COMMAND_KIND_REMOVE_ITEM,
remove_item=pb.RemoveItemCommand(
server_handle=server_handle,
item_handle=item_handle,
),
),
correlation_id=correlation_id,
)
async def add_item(
self,
server_handle: int,
@@ -147,6 +165,150 @@ class Session:
correlation_id=correlation_id,
)
async def unadvise(
self,
server_handle: int,
item_handle: int,
*,
correlation_id: str = "",
) -> None:
await self.invoke(
pb.MxCommand(
kind=pb.MX_COMMAND_KIND_UN_ADVISE,
un_advise=pb.UnAdviseCommand(
server_handle=server_handle,
item_handle=item_handle,
),
),
correlation_id=correlation_id,
)
async def add_item_bulk(
self,
server_handle: int,
tag_addresses: Sequence[str],
*,
correlation_id: str = "",
) -> list[pb.SubscribeResult]:
if tag_addresses is None:
raise TypeError("tag_addresses is required")
reply = await self.invoke(
pb.MxCommand(
kind=pb.MX_COMMAND_KIND_ADD_ITEM_BULK,
add_item_bulk=pb.AddItemBulkCommand(
server_handle=server_handle,
tag_addresses=tag_addresses,
),
),
correlation_id=correlation_id,
)
return list(reply.add_item_bulk.results)
async def advise_item_bulk(
self,
server_handle: int,
item_handles: Sequence[int],
*,
correlation_id: str = "",
) -> list[pb.SubscribeResult]:
if item_handles is None:
raise TypeError("item_handles is required")
reply = await self.invoke(
pb.MxCommand(
kind=pb.MX_COMMAND_KIND_ADVISE_ITEM_BULK,
advise_item_bulk=pb.AdviseItemBulkCommand(
server_handle=server_handle,
item_handles=item_handles,
),
),
correlation_id=correlation_id,
)
return list(reply.advise_item_bulk.results)
async def remove_item_bulk(
self,
server_handle: int,
item_handles: Sequence[int],
*,
correlation_id: str = "",
) -> list[pb.SubscribeResult]:
if item_handles is None:
raise TypeError("item_handles is required")
reply = await self.invoke(
pb.MxCommand(
kind=pb.MX_COMMAND_KIND_REMOVE_ITEM_BULK,
remove_item_bulk=pb.RemoveItemBulkCommand(
server_handle=server_handle,
item_handles=item_handles,
),
),
correlation_id=correlation_id,
)
return list(reply.remove_item_bulk.results)
async def unadvise_item_bulk(
self,
server_handle: int,
item_handles: Sequence[int],
*,
correlation_id: str = "",
) -> list[pb.SubscribeResult]:
if item_handles is None:
raise TypeError("item_handles is required")
reply = await self.invoke(
pb.MxCommand(
kind=pb.MX_COMMAND_KIND_UN_ADVISE_ITEM_BULK,
un_advise_item_bulk=pb.UnAdviseItemBulkCommand(
server_handle=server_handle,
item_handles=item_handles,
),
),
correlation_id=correlation_id,
)
return list(reply.un_advise_item_bulk.results)
async def subscribe_bulk(
self,
server_handle: int,
tag_addresses: Sequence[str],
*,
correlation_id: str = "",
) -> list[pb.SubscribeResult]:
if tag_addresses is None:
raise TypeError("tag_addresses is required")
reply = await self.invoke(
pb.MxCommand(
kind=pb.MX_COMMAND_KIND_SUBSCRIBE_BULK,
subscribe_bulk=pb.SubscribeBulkCommand(
server_handle=server_handle,
tag_addresses=tag_addresses,
),
),
correlation_id=correlation_id,
)
return list(reply.subscribe_bulk.results)
async def unsubscribe_bulk(
self,
server_handle: int,
item_handles: Sequence[int],
*,
correlation_id: str = "",
) -> list[pb.SubscribeResult]:
if item_handles is None:
raise TypeError("item_handles is required")
reply = await self.invoke(
pb.MxCommand(
kind=pb.MX_COMMAND_KIND_UNSUBSCRIBE_BULK,
unsubscribe_bulk=pb.UnsubscribeBulkCommand(
server_handle=server_handle,
item_handles=item_handles,
),
),
correlation_id=correlation_id,
)
return list(reply.unsubscribe_bulk.results)
async def write(
self,
server_handle: int,
@@ -58,6 +58,41 @@ async def test_mxaccess_error_preserves_raw_reply() -> None:
assert captured.value.raw_reply is failure_reply
@pytest.mark.asyncio
async def test_subscribe_bulk_sends_one_bulk_command_and_returns_results() -> None:
stub = FakeGatewayStub()
bulk_reply = pb.MxCommandReply(
session_id="session-1",
kind=pb.MX_COMMAND_KIND_SUBSCRIBE_BULK,
protocol_status=pb.ProtocolStatus(code=pb.PROTOCOL_STATUS_CODE_OK),
subscribe_bulk=pb.BulkSubscribeReply(
results=[
pb.SubscribeResult(
server_handle=12,
tag_address="Area001.Pump001.Speed",
item_handle=34,
was_successful=True,
),
],
),
)
stub.invoke.replies = [bulk_reply]
client = await GatewayClient.connect(
ClientOptions(endpoint="fake", api_key="mxgw_test_secret", plaintext=True),
stub=stub,
)
session = await client.open_session()
results = await session.subscribe_bulk(12, ["Area001.Pump001.Speed"])
assert results[0].item_handle == 34
assert len(stub.invoke.requests) == 1
assert stub.invoke.requests[0].command.kind == pb.MX_COMMAND_KIND_SUBSCRIBE_BULK
assert list(stub.invoke.requests[0].command.subscribe_bulk.tag_addresses) == [
"Area001.Pump001.Speed",
]
@pytest.mark.asyncio
async def test_stream_events_cancels_underlying_call_when_closed() -> None:
stream = FakeStream(
+172 -3
View File
@@ -3,9 +3,11 @@ use crate::error::Error;
use crate::generated::mxaccess_gateway::v1::mx_command::Payload;
use crate::generated::mxaccess_gateway::v1::mx_command_reply;
use crate::generated::mxaccess_gateway::v1::{
AddItem2Command, AddItemCommand, AdviseCommand, CloseSessionRequest, MxCommand, MxCommandKind,
MxCommandReply, MxCommandRequest, MxValue as ProtoMxValue, OpenSessionRequest, RegisterCommand,
StreamEventsRequest, Write2Command, WriteCommand,
AddItem2Command, AddItemBulkCommand, AddItemCommand, AdviseCommand, AdviseItemBulkCommand,
CloseSessionRequest, MxCommand, MxCommandKind, MxCommandReply, MxCommandRequest,
MxValue as ProtoMxValue, OpenSessionRequest, RegisterCommand, RemoveItemBulkCommand,
RemoveItemCommand, StreamEventsRequest, SubscribeBulkCommand, SubscribeResult, UnAdviseCommand,
UnAdviseItemBulkCommand, UnsubscribeBulkCommand, Write2Command, WriteCommand,
};
use crate::value::MxValue;
@@ -94,6 +96,18 @@ impl Session {
Ok(add_item2_handle(&reply))
}
pub async fn remove_item(&self, server_handle: i32, item_handle: i32) -> Result<(), Error> {
self.invoke(
MxCommandKind::RemoveItem,
Payload::RemoveItem(RemoveItemCommand {
server_handle,
item_handle,
}),
)
.await?;
Ok(())
}
pub async fn advise(&self, server_handle: i32, item_handle: i32) -> Result<(), Error> {
self.invoke(
MxCommandKind::Advise,
@@ -106,6 +120,126 @@ impl Session {
Ok(())
}
pub async fn un_advise(&self, server_handle: i32, item_handle: i32) -> Result<(), Error> {
self.invoke(
MxCommandKind::UnAdvise,
Payload::UnAdvise(UnAdviseCommand {
server_handle,
item_handle,
}),
)
.await?;
Ok(())
}
pub async fn add_item_bulk(
&self,
server_handle: i32,
tag_addresses: Vec<String>,
) -> Result<Vec<SubscribeResult>, Error> {
let reply = self
.invoke(
MxCommandKind::AddItemBulk,
Payload::AddItemBulk(AddItemBulkCommand {
server_handle,
tag_addresses,
}),
)
.await?;
Ok(bulk_results(reply, BulkReplyKind::AddItemBulk))
}
pub async fn advise_item_bulk(
&self,
server_handle: i32,
item_handles: Vec<i32>,
) -> Result<Vec<SubscribeResult>, Error> {
let reply = self
.invoke(
MxCommandKind::AdviseItemBulk,
Payload::AdviseItemBulk(AdviseItemBulkCommand {
server_handle,
item_handles,
}),
)
.await?;
Ok(bulk_results(reply, BulkReplyKind::AdviseItemBulk))
}
pub async fn remove_item_bulk(
&self,
server_handle: i32,
item_handles: Vec<i32>,
) -> Result<Vec<SubscribeResult>, Error> {
let reply = self
.invoke(
MxCommandKind::RemoveItemBulk,
Payload::RemoveItemBulk(RemoveItemBulkCommand {
server_handle,
item_handles,
}),
)
.await?;
Ok(bulk_results(reply, BulkReplyKind::RemoveItemBulk))
}
pub async fn un_advise_item_bulk(
&self,
server_handle: i32,
item_handles: Vec<i32>,
) -> Result<Vec<SubscribeResult>, Error> {
let reply = self
.invoke(
MxCommandKind::UnAdviseItemBulk,
Payload::UnAdviseItemBulk(UnAdviseItemBulkCommand {
server_handle,
item_handles,
}),
)
.await?;
Ok(bulk_results(reply, BulkReplyKind::UnAdviseItemBulk))
}
pub async fn subscribe_bulk(
&self,
server_handle: i32,
tag_addresses: Vec<String>,
) -> Result<Vec<SubscribeResult>, Error> {
let reply = self
.invoke(
MxCommandKind::SubscribeBulk,
Payload::SubscribeBulk(SubscribeBulkCommand {
server_handle,
tag_addresses,
}),
)
.await?;
Ok(bulk_results(reply, BulkReplyKind::SubscribeBulk))
}
pub async fn unsubscribe_bulk(
&self,
server_handle: i32,
item_handles: Vec<i32>,
) -> Result<Vec<SubscribeResult>, Error> {
let reply = self
.invoke(
MxCommandKind::UnsubscribeBulk,
Payload::UnsubscribeBulk(UnsubscribeBulkCommand {
server_handle,
item_handles,
}),
)
.await?;
Ok(bulk_results(reply, BulkReplyKind::UnsubscribeBulk))
}
pub async fn write(
&self,
server_handle: i32,
@@ -226,6 +360,41 @@ fn add_item2_handle(reply: &MxCommandReply) -> i32 {
}
}
enum BulkReplyKind {
AddItemBulk,
AdviseItemBulk,
RemoveItemBulk,
UnAdviseItemBulk,
SubscribeBulk,
UnsubscribeBulk,
}
fn bulk_results(reply: MxCommandReply, kind: BulkReplyKind) -> Vec<SubscribeResult> {
match (reply.payload, kind) {
(Some(mx_command_reply::Payload::AddItemBulk(reply)), BulkReplyKind::AddItemBulk) => {
reply.results
}
(Some(mx_command_reply::Payload::AdviseItemBulk(reply)), BulkReplyKind::AdviseItemBulk) => {
reply.results
}
(Some(mx_command_reply::Payload::RemoveItemBulk(reply)), BulkReplyKind::RemoveItemBulk) => {
reply.results
}
(
Some(mx_command_reply::Payload::UnAdviseItemBulk(reply)),
BulkReplyKind::UnAdviseItemBulk,
) => reply.results,
(Some(mx_command_reply::Payload::SubscribeBulk(reply)), BulkReplyKind::SubscribeBulk) => {
reply.results
}
(
Some(mx_command_reply::Payload::UnsubscribeBulk(reply)),
BulkReplyKind::UnsubscribeBulk,
) => reply.results,
_ => Vec::new(),
}
}
fn int32_reply_value(value: &ProtoMxValue) -> Option<i32> {
match value.kind.as_ref()? {
crate::generated::mxaccess_gateway::v1::mx_value::Kind::Int32Value(value) => Some(*value),
+44 -4
View File
@@ -14,10 +14,10 @@ use mxgateway_client::generated::mxaccess_gateway::v1::mx_access_gateway_server:
use mxgateway_client::generated::mxaccess_gateway::v1::mx_command_reply;
use mxgateway_client::generated::mxaccess_gateway::v1::mx_value::Kind;
use mxgateway_client::generated::mxaccess_gateway::v1::{
AddItemReply, CloseSessionReply, CloseSessionRequest, MxCommandKind, MxCommandReply,
MxDataType, MxEvent, MxEventFamily, MxStatusCategory, MxStatusProxy, MxStatusSource, MxValue,
OpenSessionReply, OpenSessionRequest, ProtocolStatus, ProtocolStatusCode, SessionState,
StreamEventsRequest,
AddItemReply, BulkSubscribeReply, CloseSessionReply, CloseSessionRequest, MxCommandKind,
MxCommandReply, MxDataType, MxEvent, MxEventFamily, MxStatusCategory, MxStatusProxy,
MxStatusSource, MxValue, OpenSessionReply, OpenSessionRequest, ProtocolStatus,
ProtocolStatusCode, SessionState, StreamEventsRequest, SubscribeResult,
};
use mxgateway_client::{
ApiKey, ClientOptions, CommandError, Error, GatewayClient, MxStatus, MxValue as ClientMxValue,
@@ -87,6 +87,25 @@ async fn session_helpers_build_commands_and_preserve_command_errors() {
assert_eq!(error.reply().statuses.len(), 2);
}
#[tokio::test]
async fn subscribe_bulk_builds_one_bulk_command_and_returns_results() {
let state = Arc::new(FakeState::default());
let endpoint = spawn_fake_gateway(state.clone()).await;
let client = GatewayClient::connect(ClientOptions::new(endpoint))
.await
.unwrap();
let session = client.session("session-fixture");
let results = session
.subscribe_bulk(12, vec!["Area001.Pump001.Speed".to_owned()])
.await
.unwrap();
assert_eq!(results[0].item_handle, 34);
let last_command = state.last_command_kind.lock().await;
assert_eq!(*last_command, Some(MxCommandKind::SubscribeBulk as i32));
}
#[tokio::test]
async fn event_stream_preserves_order_and_drop_cancels_server_stream() {
let state = Arc::new(FakeState::default());
@@ -268,6 +287,27 @@ impl MxAccessGateway for FakeGateway {
return Ok(Response::new(mxaccess_failure_reply()));
}
if kind == MxCommandKind::SubscribeBulk as i32 {
return Ok(Response::new(MxCommandReply {
session_id: request.session_id,
correlation_id: "fake-correlation".to_owned(),
kind,
protocol_status: Some(ok_status("command ok")),
payload: Some(mx_command_reply::Payload::SubscribeBulk(
BulkSubscribeReply {
results: vec![SubscribeResult {
server_handle: 12,
tag_address: "Area001.Pump001.Speed".to_owned(),
item_handle: 34,
was_successful: true,
error_message: String::new(),
}],
},
)),
..MxCommandReply::default()
}));
}
Ok(Response::new(MxCommandReply {
session_id: request.session_id,
correlation_id: "fake-correlation".to_owned(),