diff --git a/components/register/all.go b/components/register/all.go index 1d5620d2b28..b66fdbbcbf4 100644 --- a/components/register/all.go +++ b/components/register/all.go @@ -14,9 +14,11 @@ import ( _ "go.viam.com/rdk/components/input/register" _ "go.viam.com/rdk/components/motor/register" _ "go.viam.com/rdk/components/movementsensor/register" + // register APIs without implementations directly. _ "go.viam.com/rdk/components/posetracker" _ "go.viam.com/rdk/components/powersensor/register" _ "go.viam.com/rdk/components/sensor/register" _ "go.viam.com/rdk/components/servo/register" + _ "go.viam.com/rdk/components/switch/register" ) diff --git a/components/switch/client.go b/components/switch/client.go new file mode 100644 index 00000000000..d9f226a471d --- /dev/null +++ b/components/switch/client.go @@ -0,0 +1,88 @@ +// Package switch_component contains a gRPC based switch client. +package switch_component + +import ( + "context" + + pb "go.viam.com/api/component/switch/v1" + "go.viam.com/utils/protoutils" + "go.viam.com/utils/rpc" + + "go.viam.com/rdk/logging" + rprotoutils "go.viam.com/rdk/protoutils" + "go.viam.com/rdk/resource" +) + +// client implements SwitchServiceClient. +type client struct { + resource.Named + resource.TriviallyReconfigurable + resource.TriviallyCloseable + name string + client pb.SwitchServiceClient + logger logging.Logger +} + +// NewClientFromConn constructs a new Client from connection passed in. +func NewClientFromConn( + ctx context.Context, + conn rpc.ClientConn, + remoteName string, + name resource.Name, + logger logging.Logger, +) (Switch, error) { + c := pb.NewSwitchServiceClient(conn) + return &client{ + Named: name.PrependRemote(remoteName).AsNamed(), + name: name.ShortName(), + client: c, + logger: logger, + }, nil +} + +func (c *client) SetPosition(ctx context.Context, position uint32, extra map[string]interface{}) error { + ext, err := protoutils.StructToStructPb(extra) + if err != nil { + return err + } + _, err = c.client.SetPosition(ctx, &pb.SetPositionRequest{ + Name: c.name, + Position: position, + Extra: ext, + }) + return err +} + +func (c *client) GetPosition(ctx context.Context, extra map[string]interface{}) (uint32, error) { + ext, err := protoutils.StructToStructPb(extra) + if err != nil { + return 0, err + } + resp, err := c.client.GetPosition(ctx, &pb.GetPositionRequest{ + Name: c.name, + Extra: ext, + }) + if err != nil { + return 0, err + } + return resp.Position, nil +} + +func (c *client) GetNumberOfPositions(ctx context.Context, extra map[string]interface{}) (int, error) { + ext, err := protoutils.StructToStructPb(extra) + if err != nil { + return 0, err + } + resp, err := c.client.GetNumberOfPositions(ctx, &pb.GetNumberOfPositionsRequest{ + Name: c.name, + Extra: ext, + }) + if err != nil { + return 0, err + } + return int(resp.NumberOfPositions), nil +} + +func (c *client) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { + return rprotoutils.DoFromResourceClient(ctx, c.client, c.name, cmd) +} diff --git a/components/switch/client_test.go b/components/switch/client_test.go new file mode 100644 index 00000000000..bfeceb88380 --- /dev/null +++ b/components/switch/client_test.go @@ -0,0 +1,146 @@ +package switch_component_test + +import ( + "context" + "net" + "testing" + + "go.viam.com/test" + "go.viam.com/utils/rpc" + + switch_component "go.viam.com/rdk/components/switch" + viamgrpc "go.viam.com/rdk/grpc" + "go.viam.com/rdk/logging" + "go.viam.com/rdk/resource" + "go.viam.com/rdk/testutils" + "go.viam.com/rdk/testutils/inject" +) + +const ( + testSwitchName = "switch1" + failSwitchName = "switch2" + missingSwitchName = "missing" +) + +func TestClient(t *testing.T) { + logger := logging.NewTestLogger(t) + listener1, err := net.Listen("tcp", "localhost:0") + test.That(t, err, test.ShouldBeNil) + rpcServer, err := rpc.NewServer(logger, rpc.WithUnauthenticated()) + test.That(t, err, test.ShouldBeNil) + + var switchName string + var extraOptions map[string]interface{} + + injectSwitch := &inject.Switch{} + injectSwitch.SetPositionFunc = func(ctx context.Context, position uint32, extra map[string]interface{}) error { + extraOptions = extra + switchName = testSwitchName + return nil + } + injectSwitch.GetPositionFunc = func(ctx context.Context, extra map[string]interface{}) (uint32, error) { + extraOptions = extra + return 0, nil + } + injectSwitch.GetNumberOfPositionsFunc = func(ctx context.Context, extra map[string]interface{}) (int, error) { + extraOptions = extra + return 2, nil + } + + injectSwitch2 := &inject.Switch{} + injectSwitch2.SetPositionFunc = func(ctx context.Context, position uint32, extra map[string]interface{}) error { + switchName = failSwitchName + return errCantSetPosition + } + injectSwitch2.GetPositionFunc = func(ctx context.Context, extra map[string]interface{}) (uint32, error) { + return 0, errCantGetPosition + } + injectSwitch2.GetNumberOfPositionsFunc = func(ctx context.Context, extra map[string]interface{}) (int, error) { + return 0, errCantGetNumberOfPositions + } + + switchSvc, err := resource.NewAPIResourceCollection( + switch_component.API, + map[resource.Name]switch_component.Switch{ + switch_component.Named(testSwitchName): injectSwitch, + switch_component.Named(failSwitchName): injectSwitch2, + }) + test.That(t, err, test.ShouldBeNil) + resourceAPI, ok, err := resource.LookupAPIRegistration[switch_component.Switch](switch_component.API) + test.That(t, err, test.ShouldBeNil) + test.That(t, ok, test.ShouldBeTrue) + test.That(t, resourceAPI.RegisterRPCService(context.Background(), rpcServer, switchSvc), test.ShouldBeNil) + + injectSwitch.DoFunc = testutils.EchoFunc + + go rpcServer.Serve(listener1) + defer rpcServer.Stop() + + // failing + t.Run("Failing client", func(t *testing.T) { + cancelCtx, cancel := context.WithCancel(context.Background()) + cancel() + _, err := viamgrpc.Dial(cancelCtx, listener1.Addr().String(), logger) + test.That(t, err, test.ShouldNotBeNil) + test.That(t, err, test.ShouldBeError, context.Canceled) + }) + + // working + t.Run("switch client 1", func(t *testing.T) { + conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) + test.That(t, err, test.ShouldBeNil) + client1, err := switch_component.NewClientFromConn(context.Background(), conn, "", switch_component.Named(testSwitchName), logger) + test.That(t, err, test.ShouldBeNil) + + // DoCommand + resp, err := client1.DoCommand(context.Background(), testutils.TestCommand) + test.That(t, err, test.ShouldBeNil) + test.That(t, resp["command"], test.ShouldEqual, testutils.TestCommand["command"]) + test.That(t, resp["data"], test.ShouldEqual, testutils.TestCommand["data"]) + + extra := map[string]interface{}{"foo": "SetPosition"} + err = client1.SetPosition(context.Background(), 0, extra) + test.That(t, err, test.ShouldBeNil) + test.That(t, extraOptions, test.ShouldResemble, extra) + test.That(t, switchName, test.ShouldEqual, testSwitchName) + + extra = map[string]interface{}{"foo": "GetPosition"} + pos, err := client1.GetPosition(context.Background(), extra) + test.That(t, err, test.ShouldBeNil) + test.That(t, extraOptions, test.ShouldResemble, extra) + test.That(t, pos, test.ShouldEqual, 0) + + extra = map[string]interface{}{"foo": "GetNumberOfPositions"} + count, err := client1.GetNumberOfPositions(context.Background(), extra) + test.That(t, err, test.ShouldBeNil) + test.That(t, extraOptions, test.ShouldResemble, extra) + test.That(t, count, test.ShouldEqual, 2) + + test.That(t, client1.Close(context.Background()), test.ShouldBeNil) + test.That(t, conn.Close(), test.ShouldBeNil) + }) + + t.Run("switch client 2", func(t *testing.T) { + conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) + test.That(t, err, test.ShouldBeNil) + client2, err := resourceAPI.RPCClient(context.Background(), conn, "", switch_component.Named(failSwitchName), logger) + test.That(t, err, test.ShouldBeNil) + + extra := map[string]interface{}{} + err = client2.SetPosition(context.Background(), 0, extra) + test.That(t, err, test.ShouldNotBeNil) + test.That(t, err.Error(), test.ShouldContainSubstring, errCantSetPosition.Error()) + test.That(t, switchName, test.ShouldEqual, failSwitchName) + + _, err = client2.GetPosition(context.Background(), extra) + test.That(t, err, test.ShouldNotBeNil) + test.That(t, err.Error(), test.ShouldContainSubstring, errCantGetPosition.Error()) + + _, err = client2.GetNumberOfPositions(context.Background(), extra) + test.That(t, err, test.ShouldNotBeNil) + test.That(t, err.Error(), test.ShouldContainSubstring, errCantGetNumberOfPositions.Error()) + + test.That(t, client2.Close(context.Background()), test.ShouldBeNil) + test.That(t, conn.Close(), test.ShouldBeNil) + }) +} diff --git a/components/switch/fake/switch.go b/components/switch/fake/switch.go new file mode 100644 index 00000000000..fe89bc4649b --- /dev/null +++ b/components/switch/fake/switch.go @@ -0,0 +1,108 @@ +// Package fake implements fake switches with different position counts. +package fake + +import ( + "context" + "fmt" + "sync" + + switch_component "go.viam.com/rdk/components/switch" + "go.viam.com/rdk/logging" + "go.viam.com/rdk/resource" +) + +var ( + model2Way = resource.DefaultModelFamily.WithModel("fake-2way") + model3Way = resource.DefaultModelFamily.WithModel("fake-3way") + model10Way = resource.DefaultModelFamily.WithModel("fake-10way") +) + +// Config is the config for a fake switch. +type Config struct { + resource.TriviallyValidateConfig +} + +func init() { + // Register all three switch models + resource.RegisterComponent(switch_component.API, model2Way, resource.Registration[switch_component.Switch, *Config]{ + Constructor: func(ctx context.Context, deps resource.Dependencies, conf resource.Config, logger logging.Logger) (switch_component.Switch, error) { + return NewSwitch(ctx, deps, conf, logger, 2) + }, + }) + resource.RegisterComponent(switch_component.API, model3Way, resource.Registration[switch_component.Switch, *Config]{ + Constructor: func(ctx context.Context, deps resource.Dependencies, conf resource.Config, logger logging.Logger) (switch_component.Switch, error) { + return NewSwitch(ctx, deps, conf, logger, 3) + }, + }) + resource.RegisterComponent(switch_component.API, model10Way, resource.Registration[switch_component.Switch, *Config]{ + Constructor: func(ctx context.Context, deps resource.Dependencies, conf resource.Config, logger logging.Logger) (switch_component.Switch, error) { + return NewSwitch(ctx, deps, conf, logger, 10) + }, + }) +} + +// Switch is a fake switch that can be set to different positions. +type Switch struct { + resource.Named + resource.TriviallyCloseable + mu sync.Mutex + logger logging.Logger + position uint32 + positionCount int +} + +// NewSwitch instantiates a new switch of the fake model type. +func NewSwitch( + ctx context.Context, + deps resource.Dependencies, + conf resource.Config, + logger logging.Logger, + positionCount int, +) (switch_component.Switch, error) { + s := &Switch{ + Named: conf.ResourceName().AsNamed(), + logger: logger, + position: 0, + positionCount: positionCount, + } + if err := s.Reconfigure(ctx, deps, conf); err != nil { + return nil, err + } + return s, nil +} + +// Reconfigure reconfigures the switch atomically and in place. +func (s *Switch) Reconfigure(_ context.Context, _ resource.Dependencies, conf resource.Config) error { + s.mu.Lock() + defer s.mu.Unlock() + return nil +} + +// SetPosition sets the switch to the specified position. +func (s *Switch) SetPosition(ctx context.Context, position uint32, extra map[string]interface{}) error { + s.mu.Lock() + defer s.mu.Unlock() + + if position >= uint32(s.positionCount) { + return fmt.Errorf("switch component %v position %d is invalid (valid range: 0-%d)", s.Name(), position, s.positionCount-1) + } + s.position = position + return nil +} + +// GetPosition returns the current position of the switch. +func (s *Switch) GetPosition(ctx context.Context, extra map[string]interface{}) (uint32, error) { + s.mu.Lock() + defer s.mu.Unlock() + return s.position, nil +} + +// GetNumberOfPositions returns the total number of valid positions for this switch. +func (s *Switch) GetNumberOfPositions(ctx context.Context, extra map[string]interface{}) (int, error) { + return s.positionCount, nil +} + +// DoCommand executes a command on the switch. +func (s *Switch) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { + return cmd, nil +} diff --git a/components/switch/register/register.go b/components/switch/register/register.go new file mode 100644 index 00000000000..9edd92b9be2 --- /dev/null +++ b/components/switch/register/register.go @@ -0,0 +1,7 @@ +// Package register registers all relevant switches and also API specific functions +package register + +import ( + // for switches. + _ "go.viam.com/rdk/components/switch/fake" +) diff --git a/components/switch/server.go b/components/switch/server.go new file mode 100644 index 00000000000..d1c753367e8 --- /dev/null +++ b/components/switch/server.go @@ -0,0 +1,63 @@ +// Package switch_component contains a gRPC based switch service server. +package switch_component + +import ( + "context" + "fmt" + + pb "go.viam.com/api/component/switch/v1" + + "go.viam.com/rdk/resource" +) + +// ErrInvalidPosition is the returned error if switch position is invalid. +var ErrInvalidPosition = func(switchName string, position, maxPosition int) error { + return fmt.Errorf("switch component %v position %d is invalid (max: %d)", switchName, position, maxPosition) +} + +// serviceServer implements the SwitchService from switch.proto. +type serviceServer struct { + pb.UnimplementedSwitchServiceServer + coll resource.APIResourceCollection[Switch] +} + +// NewRPCServiceServer constructs a switch gRPC service server. +// It is intentionally untyped to prevent use outside of tests. +func NewRPCServiceServer(coll resource.APIResourceCollection[Switch]) interface{} { + return &serviceServer{coll: coll} +} + +// SetPosition sets the position of a switch of the underlying robot. +func (s *serviceServer) SetPosition(ctx context.Context, req *pb.SetPositionRequest) (*pb.SetPositionResponse, error) { + sw, err := s.coll.Resource(req.Name) + if err != nil { + return nil, err + } + return &pb.SetPositionResponse{}, sw.SetPosition(ctx, req.Position, req.Extra.AsMap()) +} + +// GetPosition gets the current position of a switch of the underlying robot. +func (s *serviceServer) GetPosition(ctx context.Context, req *pb.GetPositionRequest) (*pb.GetPositionResponse, error) { + sw, err := s.coll.Resource(req.Name) + if err != nil { + return nil, err + } + position, err := sw.GetPosition(ctx, req.Extra.AsMap()) + if err != nil { + return nil, err + } + return &pb.GetPositionResponse{Position: uint32(position)}, nil +} + +// GetNumberOfPositions gets the total number of positions for a switch of the underlying robot. +func (s *serviceServer) GetNumberOfPositions(ctx context.Context, req *pb.GetNumberOfPositionsRequest) (*pb.GetNumberOfPositionsResponse, error) { + sw, err := s.coll.Resource(req.Name) + if err != nil { + return nil, err + } + count, err := sw.GetNumberOfPositions(ctx, req.Extra.AsMap()) + if err != nil { + return nil, err + } + return &pb.GetNumberOfPositionsResponse{NumberOfPositions: uint32(count)}, nil +} diff --git a/components/switch/server_test.go b/components/switch/server_test.go new file mode 100644 index 00000000000..8fc19380fcc --- /dev/null +++ b/components/switch/server_test.go @@ -0,0 +1,126 @@ +package switch_component_test + +import ( + "context" + "errors" + "testing" + + pb "go.viam.com/api/component/switch/v1" + "go.viam.com/test" + "go.viam.com/utils/protoutils" + + switch_component "go.viam.com/rdk/components/switch" + "go.viam.com/rdk/resource" + "go.viam.com/rdk/testutils/inject" +) + +var ( + errCantSetPosition = errors.New("can't set position") + errCantGetPosition = errors.New("can't get position") + errCantGetNumberOfPositions = errors.New("can't get number of positions") + errSwitchNotFound = errors.New("not found") +) + +const testSwitchName2 = "switch3" + +func newServer() (pb.SwitchServiceServer, *inject.Switch, *inject.Switch, error) { + injectSwitch := &inject.Switch{} + injectSwitch2 := &inject.Switch{} + switches := map[resource.Name]switch_component.Switch{ + switch_component.Named(testSwitchName): injectSwitch, + switch_component.Named(testSwitchName2): injectSwitch2, + } + switchSvc, err := resource.NewAPIResourceCollection(switch_component.API, switches) + if err != nil { + return nil, nil, nil, err + } + return switch_component.NewRPCServiceServer(switchSvc).(pb.SwitchServiceServer), injectSwitch, injectSwitch2, nil +} + +func TestServer(t *testing.T) { + switchServer, injectSwitch, injectSwitch2, err := newServer() + test.That(t, err, test.ShouldBeNil) + + var switchName string + var extraOptions map[string]interface{} + + injectSwitch.SetPositionFunc = func(ctx context.Context, position uint32, extra map[string]interface{}) error { + extraOptions = extra + switchName = testSwitchName + return nil + } + injectSwitch.GetPositionFunc = func(ctx context.Context, extra map[string]interface{}) (uint32, error) { + extraOptions = extra + return 0, nil + } + injectSwitch.GetNumberOfPositionsFunc = func(ctx context.Context, extra map[string]interface{}) (int, error) { + extraOptions = extra + return 2, nil + } + + injectSwitch2.SetPositionFunc = func(ctx context.Context, position uint32, extra map[string]interface{}) error { + switchName = testSwitchName2 + return errCantSetPosition + } + injectSwitch2.GetPositionFunc = func(ctx context.Context, extra map[string]interface{}) (uint32, error) { + return 0, errCantGetPosition + } + injectSwitch2.GetNumberOfPositionsFunc = func(ctx context.Context, extra map[string]interface{}) (int, error) { + return 0, errCantGetNumberOfPositions + } + + t.Run("set position", func(t *testing.T) { + _, err := switchServer.SetPosition(context.Background(), &pb.SetPositionRequest{Name: missingSwitchName}) + test.That(t, err, test.ShouldNotBeNil) + test.That(t, err.Error(), test.ShouldContainSubstring, errSwitchNotFound.Error()) + + extra := map[string]interface{}{"foo": "SetPosition"} + ext, err := protoutils.StructToStructPb(extra) + test.That(t, err, test.ShouldBeNil) + _, err = switchServer.SetPosition(context.Background(), &pb.SetPositionRequest{Name: testSwitchName, Position: 0, Extra: ext}) + test.That(t, err, test.ShouldBeNil) + test.That(t, switchName, test.ShouldEqual, testSwitchName) + test.That(t, extraOptions, test.ShouldResemble, extra) + + _, err = switchServer.SetPosition(context.Background(), &pb.SetPositionRequest{Name: testSwitchName2}) + test.That(t, err, test.ShouldNotBeNil) + test.That(t, err.Error(), test.ShouldContainSubstring, errCantSetPosition.Error()) + test.That(t, switchName, test.ShouldEqual, testSwitchName2) + }) + + t.Run("get position", func(t *testing.T) { + _, err := switchServer.GetPosition(context.Background(), &pb.GetPositionRequest{Name: missingSwitchName}) + test.That(t, err, test.ShouldNotBeNil) + test.That(t, err.Error(), test.ShouldContainSubstring, errSwitchNotFound.Error()) + + extra := map[string]interface{}{"foo": "GetPosition"} + ext, err := protoutils.StructToStructPb(extra) + test.That(t, err, test.ShouldBeNil) + resp, err := switchServer.GetPosition(context.Background(), &pb.GetPositionRequest{Name: testSwitchName, Extra: ext}) + test.That(t, err, test.ShouldBeNil) + test.That(t, resp.Position, test.ShouldEqual, 0) + test.That(t, extraOptions, test.ShouldResemble, extra) + + _, err = switchServer.GetPosition(context.Background(), &pb.GetPositionRequest{Name: testSwitchName2}) + test.That(t, err, test.ShouldNotBeNil) + test.That(t, err.Error(), test.ShouldContainSubstring, errCantGetPosition.Error()) + }) + + t.Run("get number of positions", func(t *testing.T) { + _, err := switchServer.GetNumberOfPositions(context.Background(), &pb.GetNumberOfPositionsRequest{Name: missingSwitchName}) + test.That(t, err, test.ShouldNotBeNil) + test.That(t, err.Error(), test.ShouldContainSubstring, errSwitchNotFound.Error()) + + extra := map[string]interface{}{"foo": "GetNumberOfPositions"} + ext, err := protoutils.StructToStructPb(extra) + test.That(t, err, test.ShouldBeNil) + resp, err := switchServer.GetNumberOfPositions(context.Background(), &pb.GetNumberOfPositionsRequest{Name: testSwitchName, Extra: ext}) + test.That(t, err, test.ShouldBeNil) + test.That(t, resp.NumberOfPositions, test.ShouldEqual, 2) + test.That(t, extraOptions, test.ShouldResemble, extra) + + _, err = switchServer.GetNumberOfPositions(context.Background(), &pb.GetNumberOfPositionsRequest{Name: testSwitchName2}) + test.That(t, err, test.ShouldNotBeNil) + test.That(t, err.Error(), test.ShouldContainSubstring, errCantGetNumberOfPositions.Error()) + }) +} diff --git a/components/switch/switch.go b/components/switch/switch.go new file mode 100644 index 00000000000..b8a864b93c6 --- /dev/null +++ b/components/switch/switch.go @@ -0,0 +1,57 @@ +// Package switch_component defines a multi-position switch. +package switch_component + +import ( + "context" + + pb "go.viam.com/api/component/switch/v1" + + "go.viam.com/rdk/resource" + "go.viam.com/rdk/robot" +) + +func init() { + resource.RegisterAPI(API, resource.APIRegistration[Switch]{ + RPCServiceServerConstructor: NewRPCServiceServer, + RPCServiceHandler: pb.RegisterSwitchServiceHandlerFromEndpoint, + RPCServiceDesc: &pb.SwitchService_ServiceDesc, + RPCClient: NewClientFromConn, + }) +} + +// SubtypeName is a constant that identifies the component resource API string. +const SubtypeName = "switch" + +// API is a variable that identifies the component resource API. +var API = resource.APINamespaceRDK.WithComponentType(SubtypeName) + +// Named is a helper for getting the named switch's typed resource name. +func Named(name string) resource.Name { + return resource.NewName(API, name) +} + +// A Switch represents a physical multi-position switch. +type Switch interface { + resource.Resource + + // SetPosition sets the switch to the specified position. + // Position must be within the valid range for the switch type. + // This will block until done or a new operation cancels this one. + SetPosition(ctx context.Context, position uint32, extra map[string]interface{}) error + + // GetPosition returns the current position of the switch. + GetPosition(ctx context.Context, extra map[string]interface{}) (uint32, error) + + // GetNumberOfPositions returns the total number of valid positions for this switch. + GetNumberOfPositions(ctx context.Context, extra map[string]interface{}) (int, error) +} + +// FromRobot is a helper for getting the named Switch from the given Robot. +func FromRobot(r robot.Robot, name string) (Switch, error) { + return robot.ResourceFromRobot[Switch](r, Named(name)) +} + +// NamesFromRobot is a helper for getting all switch names from the given Robot. +func NamesFromRobot(r robot.Robot) []string { + return robot.NamesByAPI(r, API) +} diff --git a/testutils/inject/switch.go b/testutils/inject/switch.go new file mode 100644 index 00000000000..963ee46b549 --- /dev/null +++ b/testutils/inject/switch.go @@ -0,0 +1,48 @@ +package inject + +import ( + "context" + + switch_component "go.viam.com/rdk/components/switch" + "go.viam.com/rdk/resource" +) + +// Switch is an injected switch. +type Switch struct { + switch_component.Switch + name resource.Name + DoFunc func(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) + SetPositionFunc func(ctx context.Context, position uint32, extra map[string]interface{}) error + GetPositionFunc func(ctx context.Context, extra map[string]interface{}) (uint32, error) + GetNumberOfPositionsFunc func(ctx context.Context, extra map[string]interface{}) (int, error) +} + +// NewSwitch returns a new injected switch. +func NewSwitch(name string) *Switch { + return &Switch{name: switch_component.Named(name)} +} + +// Name returns the name of the resource. +func (s *Switch) Name() resource.Name { + return s.name +} + +// DoCommand executes a command on the switch. +func (s *Switch) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { + return s.DoFunc(ctx, cmd) +} + +// SetPosition sets the switch position. +func (s *Switch) SetPosition(ctx context.Context, position uint32, extra map[string]interface{}) error { + return s.SetPositionFunc(ctx, position, extra) +} + +// GetPosition gets the current switch position. +func (s *Switch) GetPosition(ctx context.Context, extra map[string]interface{}) (uint32, error) { + return s.GetPositionFunc(ctx, extra) +} + +// GetNumberOfPositions gets the total number of positions for the switch. +func (s *Switch) GetNumberOfPositions(ctx context.Context, extra map[string]interface{}) (int, error) { + return s.GetNumberOfPositionsFunc(ctx, extra) +}