Various improvements to PI group handling

- Moved group translation logic to core service
- Removed dependency on KRYO
- Fixed bug where tratments with PI instructions where not supported if
	an interpreter was present
- Fixed bug where action profile name was not found during protobuf
	encoding (always perform P4Info lookup by name and alias)
- Improved reading of members by issuing one big request for all
	groups

Change-Id: Ifcf8380b09293e70be15cf4999bd2845caf5d01e
diff --git a/apps/p4runtime-test/src/test/java/org/onosproject/p4runtime/test/P4RuntimeTest.java b/apps/p4runtime-test/src/test/java/org/onosproject/p4runtime/test/P4RuntimeTest.java
index 45a8716..da1e36f 100644
--- a/apps/p4runtime-test/src/test/java/org/onosproject/p4runtime/test/P4RuntimeTest.java
+++ b/apps/p4runtime-test/src/test/java/org/onosproject/p4runtime/test/P4RuntimeTest.java
@@ -270,7 +270,7 @@
                 .withId(groupId)
                 .addMembers(members)
                 .build();
-        CompletableFuture<Boolean> success = client.writeActionGroupMembers(actionGroup, members,
+        CompletableFuture<Boolean> success = client.writeActionGroupMembers(actionGroup,
                                                                             P4RuntimeClient.WriteOperationType.INSERT,
                                                                             bmv2DefaultPipeconf);
         assert (success.get());
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiFlowRuleTranslationService.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiFlowRuleTranslationService.java
deleted file mode 100644
index 4948ebb..0000000
--- a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiFlowRuleTranslationService.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright 2017-present Open Networking Foundation
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.onosproject.net.pi.runtime;
-
-import com.google.common.annotations.Beta;
-import org.onosproject.net.flow.FlowRule;
-import org.onosproject.net.pi.model.PiPipeconf;
-
-/**
- * A service to translate ONOS flow rules to table entries of a protocol-independent pipeline.
- */
-@Beta
-public interface PiFlowRuleTranslationService {
-
-    /**
-     * Returns a table entry equivalent to the given flow rule for the given protocol-independent
-     * pipeline configuration.
-     *
-     * @param rule     a flow rule
-     * @param pipeconf a pipeline configuration
-     * @return a table entry
-     * @throws PiFlowRuleTranslationException if the flow rule cannot be translated
-     */
-    PiTableEntry translate(FlowRule rule, PiPipeconf pipeconf)
-            throws PiFlowRuleTranslationException;
-
-    /**
-     * Signals that an error was encountered while translating flow rule.
-     */
-    class PiFlowRuleTranslationException extends Exception {
-
-        /**
-         * Creates a new exception with the given message.
-         *
-         * @param message a message
-         */
-        public PiFlowRuleTranslationException(String message) {
-            super(message);
-        }
-    }
-}
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiGroupKey.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiGroupKey.java
new file mode 100644
index 0000000..598f3e7
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiGroupKey.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.net.pi.runtime;
+
+import com.google.common.base.Objects;
+import org.onosproject.net.group.GroupKey;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Implementation of GroupKey for the case of a protocol-independent pipeline.
+ */
+public final class PiGroupKey implements GroupKey {
+
+    private final PiTableId tableId;
+    private final PiActionProfileId piActionProfileId;
+    private final int groupId;
+
+    /**
+     * Returns a new group key for the given table ID, action profile ID, and group ID.
+     *
+     * @param tableId           table ID
+     * @param piActionProfileId action profile ID
+     * @param groupId           group ID
+     */
+    public PiGroupKey(PiTableId tableId, PiActionProfileId piActionProfileId, int groupId) {
+        this.tableId = checkNotNull(tableId);
+        this.piActionProfileId = checkNotNull(piActionProfileId);
+        this.groupId = groupId;
+    }
+
+    /**
+     * Returns the table ID defined by this key.
+     *
+     * @return table ID
+     */
+    public PiTableId tableId() {
+        return tableId;
+    }
+
+    /**
+     * Returns the group ID defined by this key.
+     *
+     * @return group ID
+     */
+    public int groupId() {
+        return groupId;
+    }
+
+    /**
+     * Returns the action profile ID defined by this key.
+     *
+     * @return action profile ID
+     */
+    public PiActionProfileId actionProfileId() {
+        return piActionProfileId;
+    }
+
+    @Override
+    public byte[] key() {
+        return toString().getBytes();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (!(o instanceof PiGroupKey)) {
+            return false;
+        }
+        PiGroupKey that = (PiGroupKey) o;
+        return groupId == that.groupId &&
+                Objects.equal(tableId, that.tableId) &&
+                Objects.equal(piActionProfileId, that.piActionProfileId);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(tableId, piActionProfileId, groupId);
+    }
+
+    @Override
+    public String toString() {
+        return tableId.id() + "-" + piActionProfileId.id() + "-" + String.valueOf(groupId);
+    }
+}
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiTranslationService.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiTranslationService.java
new file mode 100644
index 0000000..a0c1101
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiTranslationService.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.net.pi.runtime;
+
+import com.google.common.annotations.Beta;
+import org.onosproject.net.flow.FlowRule;
+import org.onosproject.net.group.Group;
+import org.onosproject.net.pi.model.PiPipeconf;
+
+/**
+ * A service to translate ONOS entities to protocol-independent ones.
+ */
+@Beta
+public interface PiTranslationService {
+
+    /**
+     * Returns a PI table entry equivalent to the given flow rule for the given protocol-independent pipeline
+     * configuration.
+     *
+     * @param rule     a flow rule
+     * @param pipeconf a pipeline configuration
+     * @return a table entry
+     * @throws PiTranslationException if the flow rule cannot be translated
+     */
+    PiTableEntry translateFlowRule(FlowRule rule, PiPipeconf pipeconf)
+            throws PiTranslationException;
+
+    /**
+     * Returns a PI action group equivalent to the given group for the given protocol-independent pipeline
+     * configuration.
+     *
+     * @param group    a group
+     * @param pipeconf a pipeline configuration
+     * @return a PI action group
+     * @throws PiTranslationException if the group cannot be translated
+     */
+    PiActionGroup translateGroup(Group group, PiPipeconf pipeconf)
+            throws PiTranslationException;
+
+    /**
+     * Signals that an error was encountered while translating an entity.
+     */
+    class PiTranslationException extends Exception {
+
+        /**
+         * Creates a new exception with the given message.
+         *
+         * @param message a message
+         */
+        public PiTranslationException(String message) {
+            super(message);
+        }
+    }
+}
diff --git a/core/net/src/main/java/org/onosproject/net/pi/impl/CriterionTranslatorHelper.java b/core/net/src/main/java/org/onosproject/net/pi/impl/CriterionTranslatorHelper.java
index df73a65..4e0fd16 100644
--- a/core/net/src/main/java/org/onosproject/net/pi/impl/CriterionTranslatorHelper.java
+++ b/core/net/src/main/java/org/onosproject/net/pi/impl/CriterionTranslatorHelper.java
@@ -92,7 +92,7 @@
 import static java.lang.String.format;
 import static org.onlab.util.ImmutableByteSequence.ByteSequenceTrimException;
 import static org.onosproject.net.pi.impl.CriterionTranslator.CriterionTranslatorException;
-import static org.onosproject.net.pi.runtime.PiFlowRuleTranslationService.PiFlowRuleTranslationException;
+import static org.onosproject.net.pi.runtime.PiTranslationService.PiTranslationException;
 
 /**
  * Helper class to translate criterion instances to PI field matches.
@@ -145,14 +145,14 @@
      * @param matchType match type
      * @param bitWidth  size of the field match in bits
      * @return a PI field match
-     * @throws PiFlowRuleTranslationException if the criterion cannot be translated (see exception message)
+     * @throws PiTranslationException if the criterion cannot be translated (see exception message)
      */
     static PiFieldMatch translateCriterion(Criterion criterion, PiHeaderFieldId fieldId, PiMatchType matchType,
                                            int bitWidth)
-            throws PiFlowRuleTranslationException {
+            throws PiTranslationException {
 
         if (!TRANSLATORS.containsKey(criterion.getClass())) {
-            throw new PiFlowRuleTranslationException(format(
+            throw new PiTranslationException(format(
                     "Translation of criterion class %s is not implemented.",
                     criterion.getClass().getSimpleName()));
         }
@@ -171,15 +171,15 @@
                     Pair<ImmutableByteSequence, Integer> lp = translator.lpmMatch();
                     return new PiLpmFieldMatch(fieldId, lp.getLeft(), lp.getRight());
                 default:
-                    throw new PiFlowRuleTranslationException(format(
+                    throw new PiTranslationException(format(
                             "Translation of criterion %s (%s class) to match type %s is not implemented.",
                             criterion.type().name(), criterion.getClass().getSimpleName(), matchType.name()));
             }
         } catch (ByteSequenceTrimException e) {
-            throw new PiFlowRuleTranslationException(format(
+            throw new PiTranslationException(format(
                     "Size mismatch for criterion %s: %s", criterion.type(), e.getMessage()));
         } catch (CriterionTranslatorException e) {
-            throw new PiFlowRuleTranslationException(format(
+            throw new PiTranslationException(format(
                     "Unable to translate criterion %s: %s", criterion.type(), e.getMessage()));
         }
     }
diff --git a/core/net/src/main/java/org/onosproject/net/pi/impl/PiFlowRuleTranslator.java b/core/net/src/main/java/org/onosproject/net/pi/impl/PiFlowRuleTranslator.java
index fbfd9b7..c0fd846 100644
--- a/core/net/src/main/java/org/onosproject/net/pi/impl/PiFlowRuleTranslator.java
+++ b/core/net/src/main/java/org/onosproject/net/pi/impl/PiFlowRuleTranslator.java
@@ -21,7 +21,6 @@
 import org.onlab.util.ImmutableByteSequence;
 import org.onosproject.net.Device;
 import org.onosproject.net.flow.FlowRule;
-import org.onosproject.net.flow.IndexTableId;
 import org.onosproject.net.flow.TrafficSelector;
 import org.onosproject.net.flow.TrafficTreatment;
 import org.onosproject.net.flow.criteria.Criterion;
@@ -61,7 +60,9 @@
 import static org.onlab.util.ImmutableByteSequence.fit;
 import static org.onosproject.net.flow.criteria.Criterion.Type.PROTOCOL_INDEPENDENT;
 import static org.onosproject.net.pi.impl.CriterionTranslatorHelper.translateCriterion;
-import static org.onosproject.net.pi.runtime.PiFlowRuleTranslationService.PiFlowRuleTranslationException;
+import static org.onosproject.net.pi.impl.PiUtils.getInterpreterOrNull;
+import static org.onosproject.net.pi.impl.PiUtils.translateTableId;
+import static org.onosproject.net.pi.runtime.PiTranslationService.PiTranslationException;
 
 /**
  * Implementation of flow rule translation logic.
@@ -69,77 +70,43 @@
 final class PiFlowRuleTranslator {
 
     public static final int MAX_PI_PRIORITY = (int) Math.pow(2, 24);
-    private static final Logger log = LoggerFactory.getLogger(PiFlowRuleTranslationServiceImpl.class);
+    private static final Logger log = LoggerFactory.getLogger(PiFlowRuleTranslator.class);
 
     private PiFlowRuleTranslator() {
         // Hide constructor.
     }
 
-    static PiTableEntry translateFlowRule(FlowRule rule, PiPipeconf pipeconf, Device device)
-            throws PiFlowRuleTranslationException {
+    /**
+     * Returns a PI table entry equivalent to the given flow rule, for the given pipeconf and device.
+     *
+     * @param rule     flow rule
+     * @param pipeconf pipeconf
+     * @param device   device
+     * @return PI table entry
+     * @throws PiTranslationException if the flow rule cannot be translated
+     */
+    static PiTableEntry translate(FlowRule rule, PiPipeconf pipeconf, Device device)
+            throws PiTranslationException {
 
         PiPipelineModel pipelineModel = pipeconf.pipelineModel();
 
         // Retrieve interpreter, if any.
-        final PiPipelineInterpreter interpreter;
+        final PiPipelineInterpreter interpreter = getInterpreterOrNull(device, pipeconf);
+        // Get table model.
+        final PiTableId piTableId = translateTableId(rule.table(), interpreter);
+        final PiTableModel tableModel = getTableModel(piTableId, pipelineModel);
+        // Translate selector.
+        final Collection<PiFieldMatch> fieldMatches = translateFieldMatches(interpreter, rule.selector(), tableModel);
+        // Translate treatment.
+        final PiTableAction piTableAction = translateTreatment(rule.treatment(), interpreter, piTableId, pipelineModel);
 
-        if (device != null) {
-            interpreter = device.is(PiPipelineInterpreter.class) ? device.as(PiPipelineInterpreter.class) : null;
-        } else {
-            // The case of device == null should be admitted only during unit testing.
-            // In any other case, the interpreter should be constructed using the device.as() method to make sure that
-            // behaviour's handler/data attributes are correctly populated.
-            // FIXME: modify test class PiFlowRuleTranslatorTest to avoid passing null device
-            // I.e. we need to create a device object that supports is/as method for obtaining the interpreter.
-            log.warn("translateFlowRule() called with device == null, is this a unit test?");
-            try {
-                interpreter = (PiPipelineInterpreter) pipeconf.implementation(PiPipelineInterpreter.class)
-                        .orElse(null)
-                        .newInstance();
-            } catch (InstantiationException | IllegalAccessException e) {
-                throw new RuntimeException(format("Unable to instantiate interpreter of pipeconf %s", pipeconf.id()));
-            }
-        }
-
-        PiTableId piTableId;
-        switch (rule.table().type()) {
-            case PIPELINE_INDEPENDENT:
-                piTableId = (PiTableId) rule.table();
-                break;
-            case INDEX:
-                IndexTableId indexId = (IndexTableId) rule.table();
-                if (interpreter == null) {
-                    throw new PiFlowRuleTranslationException(format(
-                            "Unable to map table ID '%d' from index to PI: missing interpreter", indexId.id()));
-                } else if (!interpreter.mapFlowRuleTableId(indexId.id()).isPresent()) {
-                    throw new PiFlowRuleTranslationException(format(
-                            "Unable to map table ID '%d' from index to PI: missing ID in interpreter", indexId.id()));
-                } else {
-                    piTableId = interpreter.mapFlowRuleTableId(indexId.id()).get();
-                }
-                break;
-            default:
-                throw new PiFlowRuleTranslationException(format(
-                        "Unrecognized table ID type %s", rule.table().type().name()));
-        }
-
-        PiTableModel table = pipelineModel.table(piTableId.toString())
-                .orElseThrow(() -> new PiFlowRuleTranslationException(format(
-                        "Not such a table in pipeline model: %s", piTableId)));
-
-        /* Translate selector */
-        Collection<PiFieldMatch> fieldMatches = buildFieldMatches(interpreter, rule.selector(), table);
-
-        /* Translate treatment */
-        PiTableAction piTableAction = buildAction(rule.treatment(), interpreter, piTableId);
-        piTableAction = typeCheckAction(piTableAction, table);
-
-        PiTableEntry.Builder tableEntryBuilder = PiTableEntry.builder();
+        // Build PI entry.
+        final PiTableEntry.Builder tableEntryBuilder = PiTableEntry.builder();
 
         // In the P4 world 0 is the highest priority, in ONOS the lowest one.
         // FIXME: move priority conversion to the driver, where different constraints might apply
         // e.g. less bits for encoding priority in TCAM-based implementations.
-        int newPriority;
+        final int newPriority;
         if (rule.priority() > MAX_PI_PRIORITY) {
             log.warn("Flow rule priority too big, setting translated priority to max value {}: {}",
                      MAX_PI_PRIORITY, rule);
@@ -157,11 +124,11 @@
                 .withAction(piTableAction);
 
         if (!rule.isPermanent()) {
-            if (table.supportsAging()) {
+            if (tableModel.supportsAging()) {
                 tableEntryBuilder.withTimeout((double) rule.timeout());
             } else {
                 log.warn("Flow rule is temporary, but table '{}' doesn't support " +
-                                 "aging, translating to permanent.", table.name());
+                                 "aging, translating to permanent.", tableModel.name());
             }
 
         }
@@ -169,12 +136,40 @@
         return tableEntryBuilder.build();
     }
 
+
+    /**
+     * Returns a PI action equivalent to the given treatment, optionally using the given interpreter. This method also
+     * checks that the produced PI table action is suitable for the given table ID and pipeline model. If suitable, the
+     * returned action instance will have parameters well-sized, according to the table model.
+     *
+     * @param treatment     traffic treatment
+     * @param interpreter   interpreter
+     * @param tableId       PI table ID
+     * @param pipelineModel pipeline model
+     * @return PI table action
+     * @throws PiTranslationException if the treatment cannot be translated or if the PI action is not suitable for the
+     *                                given pipeline model
+     */
+    static PiTableAction translateTreatment(TrafficTreatment treatment, PiPipelineInterpreter interpreter,
+                                            PiTableId tableId, PiPipelineModel pipelineModel)
+            throws PiTranslationException {
+        PiTableModel tableModel = getTableModel(tableId, pipelineModel);
+        return typeCheckAction(buildAction(treatment, interpreter, tableId), tableModel);
+    }
+
+    private static PiTableModel getTableModel(PiTableId piTableId, PiPipelineModel pipelineModel)
+            throws PiTranslationException {
+        return pipelineModel.table(piTableId.toString())
+                .orElseThrow(() -> new PiTranslationException(format(
+                        "Not such a table in pipeline model: %s", piTableId)));
+    }
+
     /**
      * Builds a PI action out of the given treatment, optionally using the given interpreter.
      */
     private static PiTableAction buildAction(TrafficTreatment treatment, PiPipelineInterpreter interpreter,
-                                        PiTableId tableId)
-            throws PiFlowRuleTranslationException {
+                                             PiTableId tableId)
+            throws PiTranslationException {
 
         PiTableAction piTableAction = null;
 
@@ -184,7 +179,7 @@
                 if (treatment.allInstructions().size() == 1) {
                     piTableAction = ((PiInstruction) inst).action();
                 } else {
-                    throw new PiFlowRuleTranslationException(format(
+                    throw new PiTranslationException(format(
                             "Unable to translate treatment, found multiple instructions " +
                                     "of which one is protocol-independent: %s", treatment));
                 }
@@ -196,14 +191,14 @@
             try {
                 piTableAction = interpreter.mapTreatment(treatment, tableId);
             } catch (PiPipelineInterpreter.PiInterpreterException e) {
-                throw new PiFlowRuleTranslationException(
+                throw new PiTranslationException(
                         "Interpreter was unable to translate treatment. " + e.getMessage());
             }
         }
 
         if (piTableAction == null) {
             // No PiInstruction, no interpreter. It's time to give up.
-            throw new PiFlowRuleTranslationException(
+            throw new PiTranslationException(
                     "Unable to translate treatment, neither an interpreter or a "
                             + "protocol-independent instruction were provided.");
         }
@@ -211,13 +206,8 @@
         return piTableAction;
     }
 
-    /**
-     * Checks that the given PI table action is suitable for the given table
-     * model and returns a new action instance with parameters well-sized,
-     * according to the table model. If not suitable, throws an exception explaining why.
-     */
     private static PiTableAction typeCheckAction(PiTableAction piTableAction, PiTableModel table)
-            throws PiFlowRuleTranslationException {
+            throws PiTranslationException {
         switch (piTableAction.type()) {
             case ACTION:
                 return checkPiAction((PiAction) piTableAction, table);
@@ -229,15 +219,15 @@
     }
 
     private static PiTableAction checkPiAction(PiAction piAction, PiTableModel table)
-            throws PiFlowRuleTranslationException  {
+            throws PiTranslationException {
         // Table supports this action?
         PiActionModel actionModel = table.action(piAction.id().name()).orElseThrow(
-                () -> new PiFlowRuleTranslationException(format("Not such action '%s' for table '%s'",
-                                                                piAction.id(), table.name())));
+                () -> new PiTranslationException(format("Not such action '%s' for table '%s'",
+                                                        piAction.id(), table.name())));
 
         // Is the number of runtime parameters correct?
         if (actionModel.params().size() != piAction.parameters().size()) {
-            throw new PiFlowRuleTranslationException(format(
+            throw new PiTranslationException(format(
                     "Wrong number of runtime parameters for action '%s', expected %d but found %d",
                     actionModel.name(), actionModel.params().size(), piAction.parameters().size()));
         }
@@ -247,13 +237,13 @@
         PiAction.Builder newActionBuilder = PiAction.builder().withId(piAction.id());
         for (PiActionParam param : piAction.parameters()) {
             PiActionParamModel paramModel = actionModel.param(param.id().name())
-                    .orElseThrow(() -> new PiFlowRuleTranslationException(format(
+                    .orElseThrow(() -> new PiTranslationException(format(
                             "Not such parameter '%s' for action '%s'", param.id(), actionModel.name())));
             try {
                 newActionBuilder.withParameter(new PiActionParam(param.id(),
                                                                  fit(param.value(), paramModel.bitWidth())));
             } catch (ByteSequenceTrimException e) {
-                throw new PiFlowRuleTranslationException(format(
+                throw new PiTranslationException(format(
                         "Size mismatch for parameter '%s' of action '%s': %s",
                         param.id(), piAction.id(), e.getMessage()));
             }
@@ -266,9 +256,9 @@
      * Builds a collection of PI field matches out of the given selector, optionally using the given interpreter. The
      * field matches returned are guaranteed to be compatible for the given table model.
      */
-    private static Collection<PiFieldMatch> buildFieldMatches(PiPipelineInterpreter interpreter,
-                                                              TrafficSelector selector, PiTableModel tableModel)
-            throws PiFlowRuleTranslationException {
+    private static Collection<PiFieldMatch> translateFieldMatches(PiPipelineInterpreter interpreter,
+                                                                  TrafficSelector selector, PiTableModel tableModel)
+            throws PiTranslationException {
 
         Map<PiHeaderFieldId, PiFieldMatch> fieldMatches = Maps.newHashMap();
 
@@ -325,7 +315,7 @@
                         break;
                     // FIXME: Can we handle the case of RANGE or VALID match?
                     default:
-                        throw new PiFlowRuleTranslationException(format(
+                        throw new PiTranslationException(format(
                                 "No value found for required match field '%s'", fieldId));
                 }
                 // Next field.
@@ -339,7 +329,7 @@
                 try {
                     fieldMatch = translateCriterion(criterion, fieldId, fieldModel.matchType(), bitWidth);
                     translatedCriteria.add(criterion);
-                } catch (PiFlowRuleTranslationException ex) {
+                } catch (PiTranslationException ex) {
                     // Ignore exception if the same field was found in PiCriterion.
                     if (piCriterionFields.containsKey(fieldId)) {
                         ignoredCriteria.add(criterion);
@@ -355,7 +345,7 @@
                     // Field was already translated from other criterion.
                     // Throw exception only if we are trying to match on different values of the same field...
                     if (!fieldMatch.equals(piCriterionFields.get(fieldId))) {
-                        throw new PiFlowRuleTranslationException(format(
+                        throw new PiTranslationException(format(
                                 "Duplicate match field '%s': instance translated from criterion '%s' is different to " +
                                         "what found in PiCriterion.", fieldId, criterion.type()));
                     }
@@ -377,7 +367,7 @@
                 .filter(c -> !translatedCriteria.contains(c) && !ignoredCriteria.contains(c))
                 .forEach(c -> skippedCriteriaJoiner.add(c.type().name()));
         if (skippedCriteriaJoiner.length() > 0) {
-            throw new PiFlowRuleTranslationException(format(
+            throw new PiTranslationException(format(
                     "The following criteria cannot be translated for table '%s': %s",
                     tableModel.name(), skippedCriteriaJoiner.toString()));
         }
@@ -388,7 +378,7 @@
                 .filter(k -> !usedPiCriterionFields.contains(k) && !ignoredPiCriterionFields.contains(k))
                 .forEach(k -> skippedPiFieldsJoiner.add(k.id()));
         if (skippedPiFieldsJoiner.length() > 0) {
-            throw new PiFlowRuleTranslationException(format(
+            throw new PiTranslationException(format(
                     "The following PiCriterion field matches are not supported in table '%s': %s",
                     tableModel.name(), skippedPiFieldsJoiner.toString()));
         }
@@ -397,11 +387,11 @@
     }
 
     private static PiFieldMatch typeCheckFieldMatch(PiFieldMatch fieldMatch, PiTableMatchFieldModel fieldModel)
-            throws PiFlowRuleTranslationException {
+            throws PiTranslationException {
 
         // Check parameter type and size
         if (!fieldModel.matchType().equals(fieldMatch.type())) {
-            throw new PiFlowRuleTranslationException(format(
+            throw new PiTranslationException(format(
                     "Wrong match type for field '%s', expected %s, but found %s",
                     fieldMatch.fieldId(), fieldModel.matchType().name(), fieldMatch.type().name()));
         }
@@ -427,7 +417,7 @@
                 case LPM:
                     PiLpmFieldMatch lpmfield = (PiLpmFieldMatch) fieldMatch;
                     if (lpmfield.prefixLength() > modelBitWidth) {
-                        throw new PiFlowRuleTranslationException(format(
+                        throw new PiTranslationException(format(
                                 "Invalid prefix length for LPM field '%s', found %d but field has bit-width %d",
                                 fieldMatch.fieldId(), lpmfield.prefixLength(), modelBitWidth));
                     }
@@ -446,7 +436,7 @@
                             "Unrecognized match type " + fieldModel.matchType().name());
             }
         } catch (ByteSequenceTrimException e) {
-            throw new PiFlowRuleTranslationException(format(
+            throw new PiTranslationException(format(
                     "Size mismatch for field %s: %s", fieldMatch.fieldId(), e.getMessage()));
         }
     }
diff --git a/core/net/src/main/java/org/onosproject/net/pi/impl/PiGroupTranslator.java b/core/net/src/main/java/org/onosproject/net/pi/impl/PiGroupTranslator.java
new file mode 100644
index 0000000..7b798c0
--- /dev/null
+++ b/core/net/src/main/java/org/onosproject/net/pi/impl/PiGroupTranslator.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.net.pi.impl;
+
+import org.onosproject.net.Device;
+import org.onosproject.net.group.Group;
+import org.onosproject.net.group.GroupBucket;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.model.PiPipelineInterpreter;
+import org.onosproject.net.pi.runtime.PiAction;
+import org.onosproject.net.pi.runtime.PiActionGroup;
+import org.onosproject.net.pi.runtime.PiActionGroupId;
+import org.onosproject.net.pi.runtime.PiActionGroupMember;
+import org.onosproject.net.pi.runtime.PiActionGroupMemberId;
+import org.onosproject.net.pi.runtime.PiGroupKey;
+import org.onosproject.net.pi.runtime.PiTableAction;
+import org.onosproject.net.pi.runtime.PiTranslationService.PiTranslationException;
+
+import java.nio.ByteBuffer;
+
+import static java.lang.String.format;
+import static org.onosproject.net.pi.impl.PiFlowRuleTranslator.translateTreatment;
+import static org.onosproject.net.pi.impl.PiUtils.getInterpreterOrNull;
+import static org.onosproject.net.pi.runtime.PiTableAction.Type.ACTION;
+
+/**
+ * Implementation of group translation logic.
+ */
+final class PiGroupTranslator {
+
+    private PiGroupTranslator() {
+        // Hides constructor.
+    }
+
+    /**
+     * Returns a PI action group equivalent to the given group, for the given pipeconf and device.
+     *
+     * @param group    group
+     * @param pipeconf pipeconf
+     * @param device   device
+     * @return PI action group
+     * @throws PiTranslationException if the group cannot be translated
+     */
+    static PiActionGroup translate(Group group, PiPipeconf pipeconf, Device device) throws PiTranslationException {
+
+        final PiPipelineInterpreter interpreter = getInterpreterOrNull(device, pipeconf);
+
+        final PiActionGroup.Builder piActionGroupBuilder = PiActionGroup.builder()
+                .withId(PiActionGroupId.of(group.id().id()));
+
+        switch (group.type()) {
+            case SELECT:
+                piActionGroupBuilder.withType(PiActionGroup.Type.SELECT);
+                break;
+            default:
+                throw new PiTranslationException(format("Group type %s not supported", group.type()));
+        }
+
+        if (!(group.appCookie() instanceof PiGroupKey)) {
+            throw new PiTranslationException("Group app cookie is not PI (class should be PiGroupKey)");
+        }
+        final PiGroupKey groupKey = (PiGroupKey) group.appCookie();
+
+        piActionGroupBuilder.withActionProfileId(groupKey.actionProfileId());
+
+        // Translate group buckets to PI group members
+        short bucketIdx = 0;
+        for (GroupBucket bucket : group.buckets().buckets()) {
+            /*
+            FIXME: the way member IDs are computed can cause collisions!
+            Problem:
+            In P4Runtime action group members, i.e. action buckets, are associated to a numeric ID chosen
+            at member insertion time. This ID must be unique for the whole action profile (i.e. the group table in
+            OpenFlow). In ONOS, GroupBucket doesn't specify any ID.
+
+            Solutions:
+            - Change GroupBucket API to force application wanting to perform group operations to specify a member id.
+            - Maintain state to dynamically allocate/deallocate member IDs, e.g. in a dedicated service, or in a
+            P4Runtime Group Provider.
+
+            Hack:
+            Statically derive member ID by combining groupId and position of the bucket in the list.
+             */
+            ByteBuffer bb = ByteBuffer.allocate(4)
+                    .putShort((short) (group.id().id() & 0xffff))
+                    .putShort(bucketIdx);
+            bb.rewind();
+            int memberId = bb.getInt();
+            bucketIdx++;
+
+            final PiTableAction tableAction = translateTreatment(bucket.treatment(), interpreter, groupKey.tableId(),
+                                                                 pipeconf.pipelineModel());
+
+            if (tableAction.type() != ACTION) {
+                throw new PiTranslationException(format(
+                        "PI table action of type %s is not supported in groups", tableAction.type()));
+            }
+
+            piActionGroupBuilder.addMember(PiActionGroupMember.builder()
+                                                   .withId(PiActionGroupMemberId.of(memberId))
+                                                   .withAction((PiAction) tableAction)
+                                                   .withWeight(bucket.weight())
+                                                   .build());
+        }
+
+        return piActionGroupBuilder.build();
+    }
+}
diff --git a/core/net/src/main/java/org/onosproject/net/pi/impl/PiFlowRuleTranslationServiceImpl.java b/core/net/src/main/java/org/onosproject/net/pi/impl/PiTranslationServiceImpl.java
similarity index 62%
rename from core/net/src/main/java/org/onosproject/net/pi/impl/PiFlowRuleTranslationServiceImpl.java
rename to core/net/src/main/java/org/onosproject/net/pi/impl/PiTranslationServiceImpl.java
index c55a57c..68a2d20 100644
--- a/core/net/src/main/java/org/onosproject/net/pi/impl/PiFlowRuleTranslationServiceImpl.java
+++ b/core/net/src/main/java/org/onosproject/net/pi/impl/PiTranslationServiceImpl.java
@@ -23,26 +23,27 @@
 import org.apache.felix.scr.annotations.ReferenceCardinality;
 import org.apache.felix.scr.annotations.Service;
 import org.onosproject.net.Device;
+import org.onosproject.net.DeviceId;
 import org.onosproject.net.device.DeviceService;
 import org.onosproject.net.flow.FlowRule;
+import org.onosproject.net.group.Group;
 import org.onosproject.net.pi.model.PiPipeconf;
-import org.onosproject.net.pi.runtime.PiFlowRuleTranslationService;
+import org.onosproject.net.pi.runtime.PiActionGroup;
 import org.onosproject.net.pi.runtime.PiTableEntry;
+import org.onosproject.net.pi.runtime.PiTranslationService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import static org.onosproject.net.pi.impl.PiFlowRuleTranslator.translateFlowRule;
-
 /**
- * Implementation of the protocol-independent flow rule translation service.
+ * Implementation of the protocol-independent translation service.
  */
 @Component(immediate = true)
 @Service
-public class PiFlowRuleTranslationServiceImpl implements PiFlowRuleTranslationService {
+public class PiTranslationServiceImpl implements PiTranslationService {
 
     private final Logger log = LoggerFactory.getLogger(this.getClass());
 
-    // TODO: implement cache to speed up translation of flow rules.
+    // TODO: implement cache to speed up translation.
 
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     protected DeviceService deviceService;
@@ -58,15 +59,21 @@
     }
 
     @Override
-    public PiTableEntry translate(FlowRule rule, PiPipeconf pipeconf)
-            throws PiFlowRuleTranslationException {
+    public PiTableEntry translateFlowRule(FlowRule rule, PiPipeconf pipeconf) throws PiTranslationException {
+        return PiFlowRuleTranslator.translate(rule, pipeconf, getDevice(rule.deviceId()));
+    }
 
-        final Device device = deviceService.getDevice(rule.deviceId());
+    @Override
+    public PiActionGroup translateGroup(Group group, PiPipeconf pipeconf) throws PiTranslationException {
+        return PiGroupTranslator.translate(group, pipeconf, getDevice(group.deviceId()));
+    }
+
+    private Device getDevice(DeviceId deviceId) throws PiTranslationException {
+        final Device device = deviceService.getDevice(deviceId);
         if (device == null) {
-            throw new PiFlowRuleTranslationException("Unable to get device " + rule.deviceId());
+            throw new PiTranslationException("Unable to get device " + deviceId);
         }
-
-        return translateFlowRule(rule, pipeconf, device);
+        return device;
     }
 }
 
diff --git a/core/net/src/main/java/org/onosproject/net/pi/impl/PiUtils.java b/core/net/src/main/java/org/onosproject/net/pi/impl/PiUtils.java
new file mode 100644
index 0000000..aab8b40
--- /dev/null
+++ b/core/net/src/main/java/org/onosproject/net/pi/impl/PiUtils.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.net.pi.impl;
+
+import org.onosproject.net.Device;
+import org.onosproject.net.flow.IndexTableId;
+import org.onosproject.net.flow.TableId;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.model.PiPipelineInterpreter;
+import org.onosproject.net.pi.runtime.PiTableId;
+import org.onosproject.net.pi.runtime.PiTranslationService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static java.lang.String.format;
+
+/**
+ * PI utility class.
+ */
+final class PiUtils {
+
+    private static final Logger log = LoggerFactory.getLogger(PiUtils.class);
+
+    private PiUtils() {
+        // Hides constructor.
+    }
+
+    static PiPipelineInterpreter getInterpreterOrNull(Device device, PiPipeconf pipeconf) {
+        if (device != null) {
+            return device.is(PiPipelineInterpreter.class) ? device.as(PiPipelineInterpreter.class) : null;
+        } else {
+            // The case of device == null should be admitted only during unit testing.
+            // In any other case, the interpreter should be constructed using the device.as() method to make sure that
+            // behaviour's handler/data attributes are correctly populated.
+            // FIXME: modify test class PiFlowRuleTranslatorTest to avoid passing null device
+            // I.e. we need to create a device object that supports is/as method for obtaining the interpreter.
+            log.warn("getInterpreterOrNull() called with device == null, is this a unit test?");
+            try {
+                return (PiPipelineInterpreter) pipeconf.implementation(PiPipelineInterpreter.class)
+                        .orElse(null)
+                        .newInstance();
+            } catch (InstantiationException | IllegalAccessException e) {
+                throw new RuntimeException(format("Unable to instantiate interpreter of pipeconf %s", pipeconf.id()));
+            }
+        }
+    }
+
+    static PiTableId translateTableId(TableId tableId, PiPipelineInterpreter interpreter)
+            throws PiTranslationService.PiTranslationException {
+        switch (tableId.type()) {
+            case PIPELINE_INDEPENDENT:
+                return (PiTableId) tableId;
+            case INDEX:
+                IndexTableId indexId = (IndexTableId) tableId;
+                if (interpreter == null) {
+                    throw new PiTranslationService.PiTranslationException(format(
+                            "Unable to map table ID '%d' from index to PI: missing interpreter", indexId.id()));
+                } else if (!interpreter.mapFlowRuleTableId(indexId.id()).isPresent()) {
+                    throw new PiTranslationService.PiTranslationException(format(
+                            "Unable to map table ID '%d' from index to PI: missing ID in interpreter", indexId.id()));
+                } else {
+                    return interpreter.mapFlowRuleTableId(indexId.id()).get();
+                }
+            default:
+                throw new PiTranslationService.PiTranslationException(format(
+                        "Unrecognized table ID type %s", tableId.type().name()));
+        }
+    }
+}
diff --git a/core/net/src/test/java/org/onosproject/net/pi/impl/PiFlowRuleTranslatorTest.java b/core/net/src/test/java/org/onosproject/net/pi/impl/PiFlowRuleTranslatorTest.java
deleted file mode 100644
index 6874292..0000000
--- a/core/net/src/test/java/org/onosproject/net/pi/impl/PiFlowRuleTranslatorTest.java
+++ /dev/null
@@ -1,164 +0,0 @@
-/*
- * Copyright 2017-present Open Networking Foundation
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.onosproject.net.pi.impl;
-
-import com.google.common.testing.EqualsTester;
-import org.junit.Before;
-import org.junit.Test;
-import org.onlab.packet.MacAddress;
-import org.onosproject.bmv2.model.Bmv2PipelineModelParser;
-import org.onosproject.core.ApplicationId;
-import org.onosproject.core.DefaultApplicationId;
-import org.onosproject.net.DeviceId;
-import org.onosproject.net.PortNumber;
-import org.onosproject.net.flow.DefaultFlowRule;
-import org.onosproject.net.flow.DefaultTrafficSelector;
-import org.onosproject.net.flow.DefaultTrafficTreatment;
-import org.onosproject.net.flow.FlowRule;
-import org.onosproject.net.flow.TrafficSelector;
-import org.onosproject.net.flow.TrafficTreatment;
-import org.onosproject.net.pi.model.DefaultPiPipeconf;
-import org.onosproject.net.pi.model.PiPipeconf;
-import org.onosproject.net.pi.model.PiPipeconfId;
-import org.onosproject.net.pi.model.PiPipelineInterpreter;
-import org.onosproject.net.pi.runtime.PiTableEntry;
-import org.onosproject.net.pi.runtime.PiTernaryFieldMatch;
-
-import java.util.Optional;
-import java.util.Random;
-
-import static org.hamcrest.CoreMatchers.equalTo;
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.onosproject.net.pi.impl.MockInterpreter.*;
-import static org.onosproject.net.pi.impl.PiFlowRuleTranslator.MAX_PI_PRIORITY;
-
-/**
- * Tests for {@link PiFlowRuleTranslator}.
- */
-@SuppressWarnings("ConstantConditions")
-public class PiFlowRuleTranslatorTest {
-
-    private static final String BMV2_JSON_PATH = "/org/onosproject/net/pi/impl/default.json";
-    private static final short IN_PORT_MASK = 0x01ff; // 9-bit mask
-    private static final short ETH_TYPE_MASK = (short) 0xffff;
-
-    private Random random = new Random();
-    private PiPipeconf pipeconf;
-
-    @Before
-    public void setUp() throws Exception {
-        pipeconf = DefaultPiPipeconf.builder()
-                .withId(new PiPipeconfId("mock-pipeconf"))
-                .withPipelineModel(Bmv2PipelineModelParser.parse(this.getClass().getResource(BMV2_JSON_PATH)))
-                .addBehaviour(PiPipelineInterpreter.class, MockInterpreter.class)
-                .build();
-    }
-
-    @Test
-    public void testTranslate() throws Exception {
-
-        DeviceId deviceId = DeviceId.NONE;
-        ApplicationId appId = new DefaultApplicationId(1, "test");
-        int tableId = 0;
-        MacAddress ethDstMac = MacAddress.valueOf(random.nextLong());
-        MacAddress ethSrcMac = MacAddress.valueOf(random.nextLong());
-        short ethType = (short) (0x0000FFFF & random.nextInt());
-        short outPort = (short) random.nextInt(65);
-        short inPort = (short) random.nextInt(65);
-        int timeout = random.nextInt(100);
-        int priority = random.nextInt(100);
-
-        TrafficSelector matchInPort1 = DefaultTrafficSelector
-                .builder()
-                .matchInPort(PortNumber.portNumber(inPort))
-                .matchEthDst(ethDstMac)
-                .matchEthSrc(ethSrcMac)
-                .matchEthType(ethType)
-                .build();
-
-        TrafficTreatment outPort2 = DefaultTrafficTreatment
-                .builder()
-                .setOutput(PortNumber.portNumber(outPort))
-                .build();
-
-        FlowRule rule1 = DefaultFlowRule.builder()
-                .forDevice(deviceId)
-                .forTable(tableId)
-                .fromApp(appId)
-                .withSelector(matchInPort1)
-                .withTreatment(outPort2)
-                .makeTemporary(timeout)
-                .withPriority(priority)
-                .build();
-
-        FlowRule rule2 = DefaultFlowRule.builder()
-                .forDevice(deviceId)
-                .forTable(tableId)
-                .fromApp(appId)
-                .withSelector(matchInPort1)
-                .withTreatment(outPort2)
-                .makeTemporary(timeout)
-                .withPriority(priority)
-                .build();
-
-        PiTableEntry entry1 = PiFlowRuleTranslator.translateFlowRule(rule1, pipeconf, null);
-        PiTableEntry entry2 = PiFlowRuleTranslator.translateFlowRule(rule1, pipeconf, null);
-
-        // check equality, i.e. same rules must produce same entries
-        new EqualsTester()
-                .addEqualityGroup(rule1, rule2)
-                .addEqualityGroup(entry1, entry2)
-                .testEquals();
-
-        int numMatchParams = pipeconf.pipelineModel().table(TABLE0).get().matchFields().size();
-        // parse values stored in entry1
-        PiTernaryFieldMatch inPortParam = (PiTernaryFieldMatch) entry1.matchKey().fieldMatch(IN_PORT_ID).get();
-        PiTernaryFieldMatch ethDstParam = (PiTernaryFieldMatch) entry1.matchKey().fieldMatch(ETH_DST_ID).get();
-        PiTernaryFieldMatch ethSrcParam = (PiTernaryFieldMatch) entry1.matchKey().fieldMatch(ETH_SRC_ID).get();
-        PiTernaryFieldMatch ethTypeParam = (PiTernaryFieldMatch) entry1.matchKey().fieldMatch(ETH_TYPE_ID).get();
-        Optional<Double> expectedTimeout = pipeconf.pipelineModel().table(TABLE0).get().supportsAging()
-                ? Optional.of((double) rule1.timeout()) : Optional.empty();
-
-        // check that the number of parameters in the entry is the same as the number of table keys
-        assertThat("Incorrect number of match parameters",
-                   entry1.matchKey().fieldMatches().size(), is(equalTo(numMatchParams)));
-
-        // check that values stored in entry are the same used for the flow rule
-        assertThat("Incorrect inPort match param value",
-                   inPortParam.value().asReadOnlyBuffer().getShort(), is(equalTo(inPort)));
-        assertThat("Incorrect inPort match param mask",
-                   inPortParam.mask().asReadOnlyBuffer().getShort(), is(equalTo(IN_PORT_MASK)));
-        assertThat("Incorrect ethDestMac match param value",
-                   ethDstParam.value().asArray(), is(equalTo(ethDstMac.toBytes())));
-        assertThat("Incorrect ethDestMac match param mask",
-                   ethDstParam.mask().asArray(), is(equalTo(MacAddress.BROADCAST.toBytes())));
-        assertThat("Incorrect ethSrcMac match param value",
-                   ethSrcParam.value().asArray(), is(equalTo(ethSrcMac.toBytes())));
-        assertThat("Incorrect ethSrcMac match param mask",
-                   ethSrcParam.mask().asArray(), is(equalTo(MacAddress.BROADCAST.toBytes())));
-        assertThat("Incorrect ethType match param value",
-                   ethTypeParam.value().asReadOnlyBuffer().getShort(), is(equalTo(ethType)));
-        assertThat("Incorrect ethType match param mask",
-                   ethTypeParam.mask().asReadOnlyBuffer().getShort(), is(equalTo(ETH_TYPE_MASK)));
-        assertThat("Incorrect priority value",
-                   entry1.priority().get(), is(equalTo(MAX_PI_PRIORITY - rule1.priority())));
-        assertThat("Incorrect timeout value",
-                   entry1.timeout(), is(equalTo(expectedTimeout)));
-
-    }
-}
diff --git a/core/net/src/test/java/org/onosproject/net/pi/impl/PiTranslatorServiceTest.java b/core/net/src/test/java/org/onosproject/net/pi/impl/PiTranslatorServiceTest.java
new file mode 100644
index 0000000..2e6fa5f
--- /dev/null
+++ b/core/net/src/test/java/org/onosproject/net/pi/impl/PiTranslatorServiceTest.java
@@ -0,0 +1,267 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.net.pi.impl;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.testing.EqualsTester;
+import org.junit.Before;
+import org.junit.Test;
+import org.onlab.packet.MacAddress;
+import org.onlab.util.ImmutableByteSequence;
+import org.onosproject.TestApplicationId;
+import org.onosproject.bmv2.model.Bmv2PipelineModelParser;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.DefaultApplicationId;
+import org.onosproject.core.GroupId;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.flow.DefaultFlowRule;
+import org.onosproject.net.flow.DefaultTrafficSelector;
+import org.onosproject.net.flow.DefaultTrafficTreatment;
+import org.onosproject.net.flow.FlowRule;
+import org.onosproject.net.flow.TrafficSelector;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.flow.instructions.Instructions;
+import org.onosproject.net.group.DefaultGroup;
+import org.onosproject.net.group.DefaultGroupBucket;
+import org.onosproject.net.group.DefaultGroupDescription;
+import org.onosproject.net.group.Group;
+import org.onosproject.net.group.GroupBucket;
+import org.onosproject.net.group.GroupBuckets;
+import org.onosproject.net.group.GroupDescription;
+import org.onosproject.net.pi.model.DefaultPiPipeconf;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.model.PiPipeconfId;
+import org.onosproject.net.pi.model.PiPipelineInterpreter;
+import org.onosproject.net.pi.runtime.PiAction;
+import org.onosproject.net.pi.runtime.PiActionGroup;
+import org.onosproject.net.pi.runtime.PiActionGroupMember;
+import org.onosproject.net.pi.runtime.PiActionGroupMemberId;
+import org.onosproject.net.pi.runtime.PiActionId;
+import org.onosproject.net.pi.runtime.PiActionParam;
+import org.onosproject.net.pi.runtime.PiActionParamId;
+import org.onosproject.net.pi.runtime.PiActionProfileId;
+import org.onosproject.net.pi.runtime.PiGroupKey;
+import org.onosproject.net.pi.runtime.PiTableAction;
+import org.onosproject.net.pi.runtime.PiTableEntry;
+import org.onosproject.net.pi.runtime.PiTableId;
+import org.onosproject.net.pi.runtime.PiTernaryFieldMatch;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+import java.util.Random;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.onlab.util.ImmutableByteSequence.copyFrom;
+import static org.onlab.util.ImmutableByteSequence.fit;
+import static org.onosproject.net.group.GroupDescription.Type.SELECT;
+import static org.onosproject.net.pi.impl.MockInterpreter.*;
+import static org.onosproject.net.pi.impl.PiFlowRuleTranslator.MAX_PI_PRIORITY;
+
+/**
+ * Tests for {@link PiFlowRuleTranslator}.
+ */
+@SuppressWarnings("ConstantConditions")
+public class PiTranslatorServiceTest {
+
+    private static final String BMV2_JSON_PATH = "/org/onosproject/net/pi/impl/default.json";
+    private static final short IN_PORT_MASK = 0x01ff; // 9-bit mask
+    private static final short ETH_TYPE_MASK = (short) 0xffff;
+    private static final DeviceId DEVICE_ID = DeviceId.deviceId("device:dummy:1");
+    private static final ApplicationId APP_ID = TestApplicationId.create("dummy");
+    private static final PiTableId ECMP_TABLE_ID = PiTableId.of("ecmp");
+    private static final PiActionProfileId ACT_PROF_ID = PiActionProfileId.of("ecmp_selector");
+    private static final GroupId GROUP_ID = GroupId.valueOf(1);
+    private static final PiActionId EGRESS_PORT_ACTION_ID = PiActionId.of("set_egress_port");
+    private static final int PORT_BITWIDTH = 9;
+    private static final PiActionParamId PORT_PARAM_ID = PiActionParamId.of("port");
+    private static final List<GroupBucket> BUCKET_LIST = ImmutableList.of(outputBucket(1),
+                                                                          outputBucket(2),
+                                                                          outputBucket(3)
+    );
+    private static final PiGroupKey GROUP_KEY = new PiGroupKey(ECMP_TABLE_ID, ACT_PROF_ID, GROUP_ID.id());
+    private static final GroupBuckets BUCKETS = new GroupBuckets(BUCKET_LIST);
+    private static final GroupDescription GROUP_DESC =
+            new DefaultGroupDescription(DEVICE_ID, SELECT, BUCKETS, GROUP_KEY, GROUP_ID.id(), APP_ID);
+    private static final Group GROUP = new DefaultGroup(GROUP_ID, GROUP_DESC);
+    private static final int DEFAULT_MEMBER_WEIGHT = 1;
+    private static final int BASE_MEM_ID = 65535;
+    private Collection<PiActionGroupMember> expectedMembers;
+
+    private Random random = new Random();
+    private PiPipeconf pipeconf;
+
+    @Before
+    public void setUp() throws Exception {
+        pipeconf = DefaultPiPipeconf.builder()
+                .withId(new PiPipeconfId("mock-pipeconf"))
+                .withPipelineModel(Bmv2PipelineModelParser.parse(this.getClass().getResource(BMV2_JSON_PATH)))
+                .addBehaviour(PiPipelineInterpreter.class, MockInterpreter.class)
+                .build();
+
+        expectedMembers = ImmutableSet.of(outputMember(1),
+                                          outputMember(2),
+                                          outputMember(3));
+    }
+
+    @Test
+    public void testTranslateFlowRules() throws Exception {
+
+        ApplicationId appId = new DefaultApplicationId(1, "test");
+        int tableId = 0;
+        MacAddress ethDstMac = MacAddress.valueOf(random.nextLong());
+        MacAddress ethSrcMac = MacAddress.valueOf(random.nextLong());
+        short ethType = (short) (0x0000FFFF & random.nextInt());
+        short outPort = (short) random.nextInt(65);
+        short inPort = (short) random.nextInt(65);
+        int timeout = random.nextInt(100);
+        int priority = random.nextInt(100);
+
+        TrafficSelector matchInPort1 = DefaultTrafficSelector
+                .builder()
+                .matchInPort(PortNumber.portNumber(inPort))
+                .matchEthDst(ethDstMac)
+                .matchEthSrc(ethSrcMac)
+                .matchEthType(ethType)
+                .build();
+
+        TrafficTreatment outPort2 = DefaultTrafficTreatment
+                .builder()
+                .setOutput(PortNumber.portNumber(outPort))
+                .build();
+
+        FlowRule rule1 = DefaultFlowRule.builder()
+                .forDevice(DEVICE_ID)
+                .forTable(tableId)
+                .fromApp(appId)
+                .withSelector(matchInPort1)
+                .withTreatment(outPort2)
+                .makeTemporary(timeout)
+                .withPriority(priority)
+                .build();
+
+        FlowRule rule2 = DefaultFlowRule.builder()
+                .forDevice(DEVICE_ID)
+                .forTable(tableId)
+                .fromApp(appId)
+                .withSelector(matchInPort1)
+                .withTreatment(outPort2)
+                .makeTemporary(timeout)
+                .withPriority(priority)
+                .build();
+
+        PiTableEntry entry1 = PiFlowRuleTranslator.translate(rule1, pipeconf, null);
+        PiTableEntry entry2 = PiFlowRuleTranslator.translate(rule1, pipeconf, null);
+
+        // check equality, i.e. same rules must produce same entries
+        new EqualsTester()
+                .addEqualityGroup(rule1, rule2)
+                .addEqualityGroup(entry1, entry2)
+                .testEquals();
+
+        int numMatchParams = pipeconf.pipelineModel().table(TABLE0).get().matchFields().size();
+        // parse values stored in entry1
+        PiTernaryFieldMatch inPortParam = (PiTernaryFieldMatch) entry1.matchKey().fieldMatch(IN_PORT_ID).get();
+        PiTernaryFieldMatch ethDstParam = (PiTernaryFieldMatch) entry1.matchKey().fieldMatch(ETH_DST_ID).get();
+        PiTernaryFieldMatch ethSrcParam = (PiTernaryFieldMatch) entry1.matchKey().fieldMatch(ETH_SRC_ID).get();
+        PiTernaryFieldMatch ethTypeParam = (PiTernaryFieldMatch) entry1.matchKey().fieldMatch(ETH_TYPE_ID).get();
+        Optional<Double> expectedTimeout = pipeconf.pipelineModel().table(TABLE0).get().supportsAging()
+                ? Optional.of((double) rule1.timeout()) : Optional.empty();
+
+        // check that the number of parameters in the entry is the same as the number of table keys
+        assertThat("Incorrect number of match parameters",
+                   entry1.matchKey().fieldMatches().size(), is(equalTo(numMatchParams)));
+
+        // check that values stored in entry are the same used for the flow rule
+        assertThat("Incorrect inPort match param value",
+                   inPortParam.value().asReadOnlyBuffer().getShort(), is(equalTo(inPort)));
+        assertThat("Incorrect inPort match param mask",
+                   inPortParam.mask().asReadOnlyBuffer().getShort(), is(equalTo(IN_PORT_MASK)));
+        assertThat("Incorrect ethDestMac match param value",
+                   ethDstParam.value().asArray(), is(equalTo(ethDstMac.toBytes())));
+        assertThat("Incorrect ethDestMac match param mask",
+                   ethDstParam.mask().asArray(), is(equalTo(MacAddress.BROADCAST.toBytes())));
+        assertThat("Incorrect ethSrcMac match param value",
+                   ethSrcParam.value().asArray(), is(equalTo(ethSrcMac.toBytes())));
+        assertThat("Incorrect ethSrcMac match param mask",
+                   ethSrcParam.mask().asArray(), is(equalTo(MacAddress.BROADCAST.toBytes())));
+        assertThat("Incorrect ethType match param value",
+                   ethTypeParam.value().asReadOnlyBuffer().getShort(), is(equalTo(ethType)));
+        assertThat("Incorrect ethType match param mask",
+                   ethTypeParam.mask().asReadOnlyBuffer().getShort(), is(equalTo(ETH_TYPE_MASK)));
+        assertThat("Incorrect priority value",
+                   entry1.priority().get(), is(equalTo(MAX_PI_PRIORITY - rule1.priority())));
+        assertThat("Incorrect timeout value",
+                   entry1.timeout(), is(equalTo(expectedTimeout)));
+
+    }
+
+    private static GroupBucket outputBucket(int portNum) {
+        ImmutableByteSequence paramVal = copyFrom(portNum);
+        PiActionParam param = new PiActionParam(PiActionParamId.of(PORT_PARAM_ID.name()), paramVal);
+        PiTableAction action = PiAction.builder().withId(EGRESS_PORT_ACTION_ID).withParameter(param).build();
+        TrafficTreatment treatment = DefaultTrafficTreatment.builder()
+                .add(Instructions.piTableAction(action))
+                .build();
+        return DefaultGroupBucket.createSelectGroupBucket(treatment);
+    }
+
+    private static PiActionGroupMember outputMember(int portNum)
+            throws ImmutableByteSequence.ByteSequenceTrimException {
+        PiActionParam param = new PiActionParam(PORT_PARAM_ID, fit(copyFrom(portNum), PORT_BITWIDTH));
+        PiAction piAction = PiAction.builder()
+                .withId(EGRESS_PORT_ACTION_ID)
+                .withParameter(param).build();
+        return PiActionGroupMember.builder()
+                .withAction(piAction)
+                .withId(PiActionGroupMemberId.of(BASE_MEM_ID + portNum))
+                .withWeight(DEFAULT_MEMBER_WEIGHT)
+                .build();
+    }
+
+    /**
+     * Test add group with buckets.
+     */
+    @Test
+    public void testTranslateGroups() throws Exception {
+
+        PiActionGroup piGroup1 = PiGroupTranslator.translate(GROUP, pipeconf, null);
+        PiActionGroup piGroup2 = PiGroupTranslator.translate(GROUP, pipeconf, null);
+
+        new EqualsTester()
+                .addEqualityGroup(piGroup1, piGroup2)
+                .testEquals();
+
+        assertThat("Group ID must be equal",
+                   piGroup1.id().id(), is(equalTo(GROUP_ID.id())));
+        assertThat("Group type must be SELECT",
+                   piGroup1.type(), is(equalTo(PiActionGroup.Type.SELECT)));
+        assertThat("Action profile ID must be equal",
+                   piGroup1.actionProfileId(), is(equalTo(ACT_PROF_ID)));
+
+        // members installed
+        Collection<PiActionGroupMember> members = piGroup1.members();
+        assertThat("The number of group members must be equal",
+                   piGroup1.members().size(), is(expectedMembers.size()));
+        assertThat("Group members must be equal",
+                   members.containsAll(expectedMembers) && expectedMembers.containsAll(members));
+    }
+}
diff --git a/core/store/serializers/src/main/java/org/onosproject/store/serializers/KryoNamespaces.java b/core/store/serializers/src/main/java/org/onosproject/store/serializers/KryoNamespaces.java
index 54dd14c..2de7dcc 100644
--- a/core/store/serializers/src/main/java/org/onosproject/store/serializers/KryoNamespaces.java
+++ b/core/store/serializers/src/main/java/org/onosproject/store/serializers/KryoNamespaces.java
@@ -213,6 +213,7 @@
 import org.onosproject.net.pi.runtime.PiExactFieldMatch;
 import org.onosproject.net.pi.runtime.PiFieldMatch;
 import org.onosproject.net.pi.runtime.PiActionProfileId;
+import org.onosproject.net.pi.runtime.PiGroupKey;
 import org.onosproject.net.pi.runtime.PiHeaderFieldId;
 import org.onosproject.net.pi.runtime.PiLpmFieldMatch;
 import org.onosproject.net.pi.runtime.PiMatchKey;
@@ -607,6 +608,7 @@
                     PiActionParamId.class,
                     PiExactFieldMatch.class,
                     PiFieldMatch.class,
+                    PiGroupKey.class,
                     PiHeaderFieldId.class,
                     PiLpmFieldMatch.class,
                     PiMatchKey.class,
diff --git a/drivers/p4runtime/BUCK b/drivers/p4runtime/BUCK
index 5c50f12..b8167e8 100644
--- a/drivers/p4runtime/BUCK
+++ b/drivers/p4runtime/BUCK
@@ -5,22 +5,14 @@
     '//protocols/p4runtime/api:onos-protocols-p4runtime-api',
     '//incubator/grpc-dependencies:grpc-core-repkg-' + GRPC_VER,
     '//lib:grpc-netty-' + GRPC_VER,
-    '//core/store/serializers:onos-core-serializers',
-    '//lib:KRYO',
 ]
 
 BUNDLES = [
     ':onos-drivers-p4runtime',
 ]
 
-TEST_DEPS = [
-    '//lib:TEST_ADAPTERS',
-    '//core/api:onos-api-tests',
-]
-
-osgi_jar_with_tests (
+osgi_jar (
     deps = COMPILE_DEPS,
-    test_deps = TEST_DEPS,
 )
 
 onos_app (
diff --git a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/AbstractP4RuntimeHandlerBehaviour.java b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/AbstractP4RuntimeHandlerBehaviour.java
index 8a5a2dd..35de9ed 100644
--- a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/AbstractP4RuntimeHandlerBehaviour.java
+++ b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/AbstractP4RuntimeHandlerBehaviour.java
@@ -24,6 +24,7 @@
 import org.onosproject.net.driver.AbstractHandlerBehaviour;
 import org.onosproject.net.pi.model.PiPipeconf;
 import org.onosproject.net.pi.runtime.PiPipeconfService;
+import org.onosproject.net.pi.runtime.PiTranslationService;
 import org.onosproject.p4runtime.api.P4RuntimeClient;
 import org.onosproject.p4runtime.api.P4RuntimeController;
 import org.slf4j.Logger;
@@ -47,6 +48,7 @@
     protected P4RuntimeController controller;
     protected PiPipeconf pipeconf;
     protected P4RuntimeClient client;
+    protected PiTranslationService piTranslationService;
 
     /**
      * Initializes this behaviour attributes. Returns true if the operation was successful, false otherwise. This method
@@ -80,6 +82,8 @@
         }
         pipeconf = piPipeconfService.getPipeconf(piPipeconfService.ofDevice(deviceId).get()).get();
 
+        piTranslationService = handler().get(PiTranslationService.class);
+
         return true;
     }
 
diff --git a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeFlowRuleProgrammable.java b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeFlowRuleProgrammable.java
index c2cc0b5..c9fe49d83 100644
--- a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeFlowRuleProgrammable.java
+++ b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeFlowRuleProgrammable.java
@@ -31,7 +31,7 @@
 import org.onosproject.net.pi.runtime.PiCounterCellId;
 import org.onosproject.net.pi.runtime.PiCounterId;
 import org.onosproject.net.pi.runtime.PiDirectCounterCellId;
-import org.onosproject.net.pi.runtime.PiFlowRuleTranslationService;
+import org.onosproject.net.pi.runtime.PiTranslationService;
 import org.onosproject.net.pi.runtime.PiTableEntry;
 import org.onosproject.net.pi.runtime.PiTableId;
 import org.onosproject.p4runtime.api.P4RuntimeClient.WriteOperationType;
@@ -95,7 +95,6 @@
 
     private PiPipelineModel pipelineModel;
     private PiPipelineInterpreter interpreter;
-    private PiFlowRuleTranslationService piFlowRuleTranslationService;
 
     @Override
     protected boolean setupBehaviour() {
@@ -110,7 +109,6 @@
         }
         interpreter = device.as(PiPipelineInterpreter.class);
         pipelineModel = pipeconf.pipelineModel();
-        piFlowRuleTranslationService = handler().get(PiFlowRuleTranslationService.class);
         return true;
     }
 
@@ -249,8 +247,8 @@
             PiTableEntry piTableEntry;
 
             try {
-                piTableEntry = piFlowRuleTranslationService.translate(rule, pipeconf);
-            } catch (PiFlowRuleTranslationService.PiFlowRuleTranslationException e) {
+                piTableEntry = piTranslationService.translateFlowRule(rule, pipeconf);
+            } catch (PiTranslationService.PiTranslationException e) {
                 log.warn("Unable to translate flow rule: {} - {}", e.getMessage(), rule);
                 continue; // next rule
             }
diff --git a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeGroupProgrammable.java b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeGroupProgrammable.java
index e108123..b9f9766 100644
--- a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeGroupProgrammable.java
+++ b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeGroupProgrammable.java
@@ -16,42 +16,28 @@
 
 package org.onosproject.drivers.p4runtime;
 
-import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
-import org.onlab.util.KryoNamespace;
 import org.onosproject.core.GroupId;
 import org.onosproject.net.Device;
 import org.onosproject.net.DeviceId;
 import org.onosproject.net.device.DeviceService;
-import org.onosproject.net.flow.TrafficTreatment;
-import org.onosproject.net.flow.instructions.Instruction;
-import org.onosproject.net.flow.instructions.PiInstruction;
 import org.onosproject.net.group.Group;
-import org.onosproject.net.group.GroupBucket;
 import org.onosproject.net.group.GroupOperation;
 import org.onosproject.net.group.GroupOperations;
 import org.onosproject.net.group.GroupProgrammable;
 import org.onosproject.net.group.GroupStore;
-import org.onosproject.net.pi.model.PiPipelineInterpreter;
-import org.onosproject.net.pi.runtime.PiActionProfileId;
-import org.onosproject.net.pi.runtime.PiAction;
 import org.onosproject.net.pi.runtime.PiActionGroup;
 import org.onosproject.net.pi.runtime.PiActionGroupId;
-import org.onosproject.net.pi.runtime.PiActionGroupMember;
-import org.onosproject.net.pi.runtime.PiActionGroupMemberId;
-import org.onosproject.net.pi.runtime.PiTableAction;
-import org.onosproject.net.pi.runtime.PiTableId;
+import org.onosproject.net.pi.runtime.PiActionProfileId;
+import org.onosproject.net.pi.runtime.PiTranslationService;
 import org.onosproject.p4runtime.api.P4RuntimeClient;
 import org.onosproject.p4runtime.api.P4RuntimeGroupReference;
 import org.onosproject.p4runtime.api.P4RuntimeGroupWrapper;
-import org.onosproject.store.serializers.KryoNamespaces;
 import org.slf4j.Logger;
 
-import java.nio.ByteBuffer;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.List;
 import java.util.Map;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
@@ -70,11 +56,6 @@
     private static final String ACT_GRP = "action group";
     private static final String INSERT = "insert";
     private static final Logger log = getLogger(P4RuntimeGroupProgrammable.class);
-    private static final int GROUP_ID_MASK = 0xffff;
-    public static final KryoNamespace KRYO = new KryoNamespace.Builder()
-            .register(KryoNamespaces.API)
-            .register(DefaultP4RuntimeGroupCookie.class)
-            .build("P4RuntimeGroupProgrammable");
 
     /*
      * About action groups in P4runtime:
@@ -102,28 +83,12 @@
     // TODO: replace with distribute store
     private static final Map<P4RuntimeGroupReference, P4RuntimeGroupWrapper> GROUP_STORE = Maps.newConcurrentMap();
 
-    private PiPipelineInterpreter interpreter;
-
-    protected boolean init() {
-        if (!setupBehaviour()) {
-            return false;
-        }
-        Device device = deviceService.getDevice(deviceId);
-        // Need an interpreter to map the bucket treatment to a PI action
-        if (!device.is(PiPipelineInterpreter.class)) {
-            log.warn("Can't find interpreter for device {}", device.id());
-        } else {
-            interpreter = device.as(PiPipelineInterpreter.class);
-        }
-        return true;
-    }
-
     @Override
     public void performGroupOperation(DeviceId deviceId, GroupOperations groupOps) {
-        if (!init()) {
-            // Ignore group operation of not initialized.
+        if (!setupBehaviour()) {
             return;
         }
+
         Device device = handler().get(DeviceService.class).getDevice(deviceId);
 
         for (GroupOperation groupOp : groupOps.operations()) {
@@ -136,71 +101,38 @@
         GroupStore groupStore = handler().get(GroupStore.class);
         Group group = groupStore.getGroup(device.id(), groupId);
 
-        // Most of this logic can go in a core service, e.g. PiGroupTranslationService
-        // From a P4Runtime perspective, we need first to insert members, then the group.
-        PiActionGroupId piActionGroupId = PiActionGroupId.of(groupOp.groupId().id());
-
-        PiActionGroup.Builder piActionGroupBuilder = PiActionGroup.builder()
-                .withId(piActionGroupId);
-
-        switch (group.type()) {
-            case SELECT:
-                piActionGroupBuilder.withType(PiActionGroup.Type.SELECT);
-                break;
-            default:
-                log.warn("Group type {} not supported, ignore group {}.", group.type(), groupId);
-                return;
-        }
-        /*
-            Problem:
-            In P4Runtime, action profiles (i.e. group tables) are specific to one or more tables.
-            Mapping of treatments depends on the target table. How do we derive the target table from here?
-
-            Solution:
-            - Add table information into app cookie and put into group description
-         */
-        // TODO: notify group service if we get deserialize error
-        DefaultP4RuntimeGroupCookie defaultP4RuntimeGroupCookie = KRYO.deserialize(group.appCookie().key());
-        PiTableId piTableId = defaultP4RuntimeGroupCookie.tableId();
-        PiActionProfileId piActionProfileId = defaultP4RuntimeGroupCookie.actionProfileId();
-        piActionGroupBuilder.withActionProfileId(piActionProfileId);
-
-        List<PiActionGroupMember> members = buildMembers(group, piActionGroupId, piTableId);
-        if (members == null) {
-            log.warn("Can't build members for group {} on {}", group, device.id());
+        PiActionGroup piActionGroup;
+        try {
+            piActionGroup = piTranslationService.translateGroup(group, pipeconf);
+        } catch (PiTranslationService.PiTranslationException e) {
+            log.warn("Unable translate group, aborting group operation {}: {}", groupOp.opType(), e.getMessage());
             return;
         }
 
-        piActionGroupBuilder.addMembers(members);
-        PiActionGroup piActionGroup = piActionGroupBuilder.build();
+        P4RuntimeGroupReference groupRef = new P4RuntimeGroupReference(deviceId, piActionGroup.actionProfileId(),
+                                                                       piActionGroup.id());
 
-        P4RuntimeGroupReference groupRef =
-                new P4RuntimeGroupReference(deviceId, piActionProfileId, piActionGroupId);
         Lock lock = GROUP_LOCKS.computeIfAbsent(groupRef, k -> new ReentrantLock());
         lock.lock();
 
-
         try {
             P4RuntimeGroupWrapper oldGroupWrapper = GROUP_STORE.get(groupRef);
-            P4RuntimeGroupWrapper newGroupWrapper =
-                    new P4RuntimeGroupWrapper(piActionGroup, group, System.currentTimeMillis());
-            boolean success;
+            P4RuntimeGroupWrapper newGroupWrapper = new P4RuntimeGroupWrapper(piActionGroup, group,
+                                                                              System.currentTimeMillis());
             switch (groupOp.opType()) {
                 case ADD:
                 case MODIFY:
-                    success = writeGroupToDevice(oldGroupWrapper, piActionGroup, members);
-                    if (success) {
+                    if (writeGroupToDevice(oldGroupWrapper, piActionGroup)) {
                         GROUP_STORE.put(groupRef, newGroupWrapper);
                     }
                     break;
                 case DELETE:
-                    success = deleteGroupFromDevice(piActionGroup, members);
-                    if (success) {
+                    if (deleteGroupFromDevice(piActionGroup)) {
                         GROUP_STORE.remove(groupRef);
                     }
                     break;
                 default:
-                    throw new UnsupportedOperationException();
+                    log.warn("Group operation {} not supported", groupOp.opType());
             }
         } finally {
             lock.unlock();
@@ -211,13 +143,10 @@
      * Installs action group and members to device via client interface.
      *
      * @param oldGroupWrapper old group wrapper for the group; null if not exists
-     * @param piActionGroup the action group to be installed
-     * @param members members of the action group
+     * @param piActionGroup   the action group to be installed
      * @return true if install success; false otherwise
      */
-    private boolean writeGroupToDevice(P4RuntimeGroupWrapper oldGroupWrapper,
-                                       PiActionGroup piActionGroup,
-                                       Collection<PiActionGroupMember> members) {
+    private boolean writeGroupToDevice(P4RuntimeGroupWrapper oldGroupWrapper, PiActionGroup piActionGroup) {
         boolean success = true;
         CompletableFuture<Boolean> writeSuccess;
         if (checkStoreBeforeUpdate && oldGroupWrapper != null &&
@@ -226,11 +155,9 @@
             return true;
         }
         if (deleteBeforeUpdate && oldGroupWrapper != null) {
-            success = deleteGroupFromDevice(oldGroupWrapper.piActionGroup(),
-                                            oldGroupWrapper.piActionGroup().members());
+            success = deleteGroupFromDevice(oldGroupWrapper.piActionGroup());
         }
         writeSuccess = client.writeActionGroupMembers(piActionGroup,
-                                                      members,
                                                       P4RuntimeClient.WriteOperationType.INSERT,
                                                       pipeconf);
         success = success && completeSuccess(writeSuccess, ACT_GRP_MEMS, INSERT);
@@ -242,18 +169,16 @@
         return success;
     }
 
-    private boolean deleteGroupFromDevice(PiActionGroup piActionGroup,
-                                          Collection<PiActionGroupMember> members) {
+    private boolean deleteGroupFromDevice(PiActionGroup piActionGroup) {
         boolean success;
         CompletableFuture<Boolean> writeSuccess;
         writeSuccess = client.writeActionGroup(piActionGroup,
-                                P4RuntimeClient.WriteOperationType.DELETE,
-                                pipeconf);
+                                               P4RuntimeClient.WriteOperationType.DELETE,
+                                               pipeconf);
         success = completeSuccess(writeSuccess, ACT_GRP, DELETE);
         writeSuccess = client.writeActionGroupMembers(piActionGroup,
-                                       members,
-                                       P4RuntimeClient.WriteOperationType.DELETE,
-                                       pipeconf);
+                                                      P4RuntimeClient.WriteOperationType.DELETE,
+                                                      pipeconf);
         success = success && completeSuccess(writeSuccess, ACT_GRP_MEMS, DELETE);
         return success;
     }
@@ -268,98 +193,16 @@
         }
     }
 
-    /**
-     * Build pi action group members from group.
-     *
-     * @param group the group
-     * @param piActionGroupId the PI action group id of the group
-     * @param piTableId the PI table related to the group
-     * @return list of PI action group members; null if can't build member list
-     */
-    private List<PiActionGroupMember> buildMembers(Group group, PiActionGroupId piActionGroupId, PiTableId piTableId) {
-        GroupId groupId = group.id();
-        ImmutableList.Builder<PiActionGroupMember> membersBuilder = ImmutableList.builder();
-
-        int bucketIdx = 0;
-        for (GroupBucket bucket : group.buckets().buckets()) {
-            /*
-            Problem:
-            In P4Runtime action group members, i.e. action buckets, are associated to a numeric ID chosen
-            at member insertion time. This ID must be unique for the whole action profile (i.e. the group table in
-            OpenFlow). In ONOS, GroupBucket doesn't specify any ID.
-
-            Solutions:
-            - Change GroupBucket API to force application wanting to perform group operations to specify a member id.
-            - Maintain state to dynamically allocate/deallocate member IDs, e.g. in a dedicated service, or in a
-            P4Runtime Group Provider.
-
-            Hack:
-            Statically derive member ID by combining groupId and position of the bucket in the list.
-             */
-            ByteBuffer bb = ByteBuffer.allocate(4)
-                    .putShort((short) (piActionGroupId.id() & GROUP_ID_MASK))
-                    .putShort((short) bucketIdx);
-            bb.rewind();
-            int memberId = bb.getInt();
-
-            bucketIdx++;
-            PiAction action;
-            if (interpreter != null) {
-                // if we have interpreter, use interpreter
-                try {
-                    action = interpreter.mapTreatment(bucket.treatment(), piTableId);
-                } catch (PiPipelineInterpreter.PiInterpreterException e) {
-                    log.warn("Can't map treatment {} to action due to {}, ignore group {}",
-                             bucket.treatment(), e.getMessage(), groupId);
-                    return null;
-                }
-            } else {
-                // if we don't have interpreter, accept PiInstruction only
-                TrafficTreatment treatment = bucket.treatment();
-
-                if (treatment.allInstructions().size() > 1) {
-                    log.warn("Treatment {} has multiple instructions, ignore group {}",
-                             treatment, groupId);
-                    return null;
-                }
-                Instruction instruction = treatment.allInstructions().get(0);
-                if (instruction.type() != Instruction.Type.PROTOCOL_INDEPENDENT) {
-                    log.warn("Instruction {} is not a PROTOCOL_INDEPENDENT type, ignore group {}",
-                             instruction, groupId);
-                    return null;
-                }
-
-                PiInstruction piInstruction = (PiInstruction) instruction;
-                if (piInstruction.action().type() != PiTableAction.Type.ACTION) {
-                    log.warn("Action {} is not an ACTION type, ignore group {}",
-                             piInstruction.action(), groupId);
-                    return null;
-                }
-                action = (PiAction) piInstruction.action();
-            }
-
-            PiActionGroupMember member = PiActionGroupMember.builder()
-                    .withId(PiActionGroupMemberId.of(memberId))
-                    .withAction(action)
-                    .withWeight(bucket.weight())
-                    .build();
-
-            membersBuilder.add(member);
-        }
-        return membersBuilder.build();
-    }
-
     @Override
     public Collection<Group> getGroups() {
-        if (!init()) {
-            return Collections.emptySet();
+        if (!setupBehaviour()) {
+            return Collections.emptyList();
         }
 
         Collection<Group> result = Sets.newHashSet();
         Collection<PiActionProfileId> piActionProfileIds = Sets.newHashSet();
 
-        // Collection action profile Ids
-        // TODO: find better way to get all action profile ids....
+        // TODO: find better way to get all action profile ids. e.g. by providing them in the interpreter
         GROUP_STORE.forEach((groupRef, wrapper) -> piActionProfileIds.add(groupRef.actionProfileId()));
 
         AtomicBoolean success = new AtomicBoolean(true);
@@ -409,32 +252,4 @@
         }
     }
 
-    /**
-     * P4Runtime app cookie for group.
-     */
-    public static class DefaultP4RuntimeGroupCookie {
-        private PiTableId tableId;
-        private PiActionProfileId piActionProfileId;
-        private Integer groupId;
-
-        public DefaultP4RuntimeGroupCookie(PiTableId tableId,
-                                           PiActionProfileId piActionProfileId,
-                                           Integer groupId) {
-            this.tableId = tableId;
-            this.piActionProfileId = piActionProfileId;
-            this.groupId = groupId;
-        }
-
-        public PiTableId tableId() {
-            return tableId;
-        }
-
-        public PiActionProfileId actionProfileId() {
-            return piActionProfileId;
-        }
-
-        public Integer groupId() {
-            return groupId;
-        }
-    }
 }
diff --git a/drivers/p4runtime/src/test/java/org/onosproject/drivers/p4runtime/P4runtimeGroupProgrammableTest.java b/drivers/p4runtime/src/test/java/org/onosproject/drivers/p4runtime/P4runtimeGroupProgrammableTest.java
deleted file mode 100644
index c334218..0000000
--- a/drivers/p4runtime/src/test/java/org/onosproject/drivers/p4runtime/P4runtimeGroupProgrammableTest.java
+++ /dev/null
@@ -1,291 +0,0 @@
-/*
- * Copyright 2017-present Open Networking Foundation
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.onosproject.drivers.p4runtime;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Lists;
-import org.easymock.Capture;
-import org.easymock.EasyMock;
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Test;
-import org.onlab.util.ImmutableByteSequence;
-import org.onosproject.TestApplicationId;
-import org.onosproject.core.ApplicationId;
-import org.onosproject.core.GroupId;
-import org.onosproject.drivers.p4runtime.P4RuntimeGroupProgrammable.DefaultP4RuntimeGroupCookie;
-import org.onosproject.net.Device;
-import org.onosproject.net.DeviceId;
-import org.onosproject.net.device.DeviceService;
-import org.onosproject.net.driver.DriverData;
-import org.onosproject.net.driver.DriverHandler;
-import org.onosproject.net.flow.DefaultTrafficTreatment;
-import org.onosproject.net.flow.TrafficTreatment;
-import org.onosproject.net.flow.instructions.Instructions;
-import org.onosproject.net.group.DefaultGroup;
-import org.onosproject.net.group.DefaultGroupBucket;
-import org.onosproject.net.group.DefaultGroupDescription;
-import org.onosproject.net.group.DefaultGroupKey;
-import org.onosproject.net.group.Group;
-import org.onosproject.net.group.GroupBucket;
-import org.onosproject.net.group.GroupBuckets;
-import org.onosproject.net.group.GroupDescription;
-import org.onosproject.net.group.GroupKey;
-import org.onosproject.net.group.GroupOperation;
-import org.onosproject.net.group.GroupOperations;
-import org.onosproject.net.group.GroupStore;
-import org.onosproject.net.pi.model.DefaultPiPipeconf;
-import org.onosproject.net.pi.model.PiPipeconf;
-import org.onosproject.net.pi.model.PiPipeconfId;
-import org.onosproject.net.pi.model.PiPipelineInterpreter;
-import org.onosproject.net.pi.model.PiPipelineModel;
-import org.onosproject.net.pi.runtime.PiActionProfileId;
-import org.onosproject.net.pi.runtime.PiAction;
-import org.onosproject.net.pi.runtime.PiActionGroup;
-import org.onosproject.net.pi.runtime.PiActionGroupMember;
-import org.onosproject.net.pi.runtime.PiActionGroupMemberId;
-import org.onosproject.net.pi.runtime.PiActionId;
-import org.onosproject.net.pi.runtime.PiActionParam;
-import org.onosproject.net.pi.runtime.PiActionParamId;
-import org.onosproject.net.pi.runtime.PiPipeconfService;
-import org.onosproject.net.pi.runtime.PiTableAction;
-import org.onosproject.net.pi.runtime.PiTableId;
-import org.onosproject.p4runtime.api.P4RuntimeClient;
-import org.onosproject.p4runtime.api.P4RuntimeController;
-
-import java.net.URL;
-import java.util.Collection;
-import java.util.List;
-import java.util.Optional;
-import java.util.concurrent.CompletableFuture;
-
-import static org.easymock.EasyMock.*;
-import static org.junit.Assert.assertEquals;
-import static org.onosproject.net.group.GroupDescription.Type.SELECT;
-import static org.onosproject.net.pi.model.PiPipeconf.ExtensionType.P4_INFO_TEXT;
-import static org.onosproject.p4runtime.api.P4RuntimeClient.WriteOperationType.DELETE;
-import static org.onosproject.p4runtime.api.P4RuntimeClient.WriteOperationType.INSERT;
-
-public class P4runtimeGroupProgrammableTest {
-    private static final String P4INFO_PATH = "/default.p4info";
-    private static final DeviceId DEVICE_ID = DeviceId.deviceId("device:p4runtime:1");
-    private static final PiPipeconfId PIPECONF_ID = new PiPipeconfId("p4runtime-mock-pipeconf");
-    private static final PiPipeconf PIPECONF = buildPipeconf();
-    private static final PiTableId ECMP_TABLE_ID = PiTableId.of("ecmp");
-    private static final PiActionProfileId ACT_PROF_ID = PiActionProfileId.of("ecmp_selector");
-    private static final ApplicationId APP_ID = TestApplicationId.create("P4runtimeGroupProgrammableTest");
-    private static final GroupId GROUP_ID = GroupId.valueOf(1);
-    private static final PiActionId EGRESS_PORT_ACTION_ID = PiActionId.of("set_egress_port");
-    private static final PiActionParamId PORT_PARAM_ID = PiActionParamId.of("port");
-    private static final List<GroupBucket> BUCKET_LIST = ImmutableList.of(
-            outputBucket(1),
-            outputBucket(2),
-            outputBucket(3)
-    );
-    private static final DefaultP4RuntimeGroupCookie COOKIE =
-            new DefaultP4RuntimeGroupCookie(ECMP_TABLE_ID, ACT_PROF_ID, GROUP_ID.id());
-    private static final GroupKey GROUP_KEY =
-            new DefaultGroupKey(P4RuntimeGroupProgrammable.KRYO.serialize(COOKIE));
-    private static final GroupBuckets BUCKETS = new GroupBuckets(BUCKET_LIST);
-    private static final GroupDescription GROUP_DESC =
-            new DefaultGroupDescription(DEVICE_ID,
-                                        SELECT,
-                                        BUCKETS,
-                                        GROUP_KEY,
-                                        GROUP_ID.id(),
-                                        APP_ID);
-    private static final Group GROUP = new DefaultGroup(GROUP_ID, GROUP_DESC);
-    private static final int DEFAULT_MEMBER_WEIGHT = 1;
-    private static final int BASE_MEM_ID = 65535;
-    private static final Collection<PiActionGroupMember> EXPECTED_MEMBERS =
-            ImmutableSet.of(
-                    outputMember(1),
-                    outputMember(2),
-                    outputMember(3)
-            );
-
-    private P4RuntimeGroupProgrammable programmable;
-    private DriverHandler driverHandler;
-    private DriverData driverData;
-    private P4RuntimeController controller;
-    private P4RuntimeClient client;
-    private PiPipeconfService piPipeconfService;
-    private DeviceService deviceService;
-    private Device device;
-    private GroupStore groupStore;
-
-    private static PiPipeconf buildPipeconf() {
-        final URL p4InfoUrl = P4runtimeGroupProgrammableTest.class.getResource(P4INFO_PATH);
-        return DefaultPiPipeconf.builder()
-                .withId(PIPECONF_ID)
-                .withPipelineModel(niceMock(PiPipelineModel.class))
-                .addExtension(P4_INFO_TEXT, p4InfoUrl)
-                .build();
-    }
-
-    private static GroupBucket outputBucket(int portNum) {
-        ImmutableByteSequence paramVal = ImmutableByteSequence.copyFrom(portNum);
-        PiActionParam param = new PiActionParam(PiActionParamId.of(PORT_PARAM_ID.name()), paramVal);
-        PiTableAction action = PiAction.builder().withId(EGRESS_PORT_ACTION_ID).withParameter(param).build();
-
-        TrafficTreatment treatment = DefaultTrafficTreatment.builder()
-                .add(Instructions.piTableAction(action))
-                .build();
-
-        return DefaultGroupBucket.createSelectGroupBucket(treatment);
-    }
-
-    private static PiActionGroupMember outputMember(int portNum) {
-        PiActionParam param = new PiActionParam(PORT_PARAM_ID,
-                                                ImmutableByteSequence.copyFrom(portNum));
-        PiAction piAction = PiAction.builder()
-                .withId(EGRESS_PORT_ACTION_ID)
-                .withParameter(param).build();
-
-        return PiActionGroupMember.builder()
-                .withAction(piAction)
-                .withId(PiActionGroupMemberId.of(BASE_MEM_ID + portNum))
-                .withWeight(DEFAULT_MEMBER_WEIGHT)
-                .build();
-    }
-
-    @Before
-    public void setup() {
-        driverHandler = EasyMock.niceMock(DriverHandler.class);
-        driverData = EasyMock.niceMock(DriverData.class);
-        controller = EasyMock.niceMock(P4RuntimeController.class);
-        client = EasyMock.niceMock(P4RuntimeClient.class);
-        piPipeconfService = EasyMock.niceMock(PiPipeconfService.class);
-        deviceService = EasyMock.niceMock(DeviceService.class);
-        device = EasyMock.niceMock(Device.class);
-        groupStore = EasyMock.niceMock(GroupStore.class);
-
-        expect(controller.hasClient(DEVICE_ID)).andReturn(true).anyTimes();
-        expect(controller.getClient(DEVICE_ID)).andReturn(client).anyTimes();
-        expect(device.is(PiPipelineInterpreter.class)).andReturn(true).anyTimes();
-        expect(device.id()).andReturn(DEVICE_ID).anyTimes();
-        expect(deviceService.getDevice(DEVICE_ID)).andReturn(device).anyTimes();
-        expect(driverData.deviceId()).andReturn(DEVICE_ID).anyTimes();
-        expect(groupStore.getGroup(DEVICE_ID, GROUP_ID)).andReturn(GROUP).anyTimes();
-        expect(piPipeconfService.ofDevice(DEVICE_ID)).andReturn(Optional.of(PIPECONF_ID)).anyTimes();
-        expect(piPipeconfService.getPipeconf(PIPECONF_ID)).andReturn(Optional.of(PIPECONF)).anyTimes();
-        expect(driverHandler.data()).andReturn(driverData).anyTimes();
-        expect(driverHandler.get(P4RuntimeController.class)).andReturn(controller).anyTimes();
-        expect(driverHandler.get(PiPipeconfService.class)).andReturn(piPipeconfService).anyTimes();
-        expect(driverHandler.get(DeviceService.class)).andReturn(deviceService).anyTimes();
-        expect(driverHandler.get(GroupStore.class)).andReturn(groupStore).anyTimes();
-
-        programmable = new P4RuntimeGroupProgrammable();
-        programmable.setHandler(driverHandler);
-        programmable.setData(driverData);
-        EasyMock.replay(driverHandler, driverData, controller, piPipeconfService,
-                        deviceService, device, groupStore);
-    }
-
-    /**
-     * Test init function.
-     */
-    @Test
-    public void testInit() {
-        programmable.init();
-    }
-
-    /**
-     * Test add group with buckets.
-     */
-    @Test
-    public void testAddGroup() {
-        List<GroupOperation> ops = Lists.newArrayList();
-        ops.add(GroupOperation.createAddGroupOperation(GROUP_ID, SELECT, BUCKETS));
-        GroupOperations groupOps = new GroupOperations(ops);
-        CompletableFuture<Boolean> completeTrue = new CompletableFuture<>();
-        completeTrue.complete(true);
-
-        Capture<PiActionGroup> groupCapture1 = EasyMock.newCapture();
-        expect(client.writeActionGroup(EasyMock.capture(groupCapture1), EasyMock.eq(INSERT), EasyMock.eq(PIPECONF)))
-                .andReturn(completeTrue).anyTimes();
-
-        Capture<PiActionGroup> groupCapture2 = EasyMock.newCapture();
-        Capture<Collection<PiActionGroupMember>> membersCapture = EasyMock.newCapture();
-        expect(client.writeActionGroupMembers(EasyMock.capture(groupCapture2),
-                                                       EasyMock.capture(membersCapture),
-                                                       EasyMock.eq(INSERT),
-                                                       EasyMock.eq(PIPECONF)))
-                .andReturn(completeTrue).anyTimes();
-
-        EasyMock.replay(client);
-        programmable.performGroupOperation(DEVICE_ID, groupOps);
-
-        // verify group installed by group programmable
-        PiActionGroup group1 = groupCapture1.getValue();
-        PiActionGroup group2 = groupCapture2.getValue();
-        assertEquals("Groups should be equal", group1, group2);
-        assertEquals(GROUP_ID.id(), group1.id().id());
-        assertEquals(PiActionGroup.Type.SELECT, group1.type());
-        assertEquals(ACT_PROF_ID, group1.actionProfileId());
-
-        // members installed
-        Collection<PiActionGroupMember> members = group1.members();
-        assertEquals(3, members.size());
-
-        Assert.assertTrue(EXPECTED_MEMBERS.containsAll(members));
-        Assert.assertTrue(members.containsAll(EXPECTED_MEMBERS));
-    }
-
-    /**
-     * Test remove group with buckets.
-     */
-    @Test
-    public void testDelGroup() {
-        List<GroupOperation> ops = Lists.newArrayList();
-        ops.add(GroupOperation.createDeleteGroupOperation(GROUP_ID, SELECT));
-        GroupOperations groupOps = new GroupOperations(ops);
-        CompletableFuture<Boolean> completeTrue = new CompletableFuture<>();
-        completeTrue.complete(true);
-
-        Capture<PiActionGroup> groupCapture1 = EasyMock.newCapture();
-        expect(client.writeActionGroup(EasyMock.capture(groupCapture1), EasyMock.eq(DELETE), EasyMock.eq(PIPECONF)))
-                .andReturn(completeTrue).anyTimes();
-
-        Capture<PiActionGroup> groupCapture2 = EasyMock.newCapture();
-        Capture<Collection<PiActionGroupMember>> membersCapture = EasyMock.newCapture();
-        expect(client.writeActionGroupMembers(EasyMock.capture(groupCapture2),
-                                                       EasyMock.capture(membersCapture),
-                                                       EasyMock.eq(DELETE),
-                                                       EasyMock.eq(PIPECONF)))
-                .andReturn(completeTrue).anyTimes();
-
-        EasyMock.replay(client);
-        programmable.performGroupOperation(DEVICE_ID, groupOps);
-
-        // verify group installed by group programmable
-        PiActionGroup group1 = groupCapture1.getValue();
-        PiActionGroup group2 = groupCapture2.getValue();
-        assertEquals("Groups should be equal", group1, group2);
-        assertEquals(GROUP_ID.id(), group1.id().id());
-        assertEquals(PiActionGroup.Type.SELECT, group1.type());
-        assertEquals(ACT_PROF_ID, group1.actionProfileId());
-
-        // members installed
-        Collection<PiActionGroupMember> members = group1.members();
-        assertEquals(3, members.size());
-
-        Assert.assertTrue(EXPECTED_MEMBERS.containsAll(members));
-        Assert.assertTrue(members.containsAll(EXPECTED_MEMBERS));
-    }
-}
diff --git a/drivers/p4runtime/src/test/resources/default.p4info b/drivers/p4runtime/src/test/resources/default.p4info
deleted file mode 120000
index 8f71cbe..0000000
--- a/drivers/p4runtime/src/test/resources/default.p4info
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../tools/test/p4src/p4-16/p4c-out/default.p4info
\ No newline at end of file
diff --git a/protocols/p4runtime/api/src/main/java/org/onosproject/p4runtime/api/P4RuntimeClient.java b/protocols/p4runtime/api/src/main/java/org/onosproject/p4runtime/api/P4RuntimeClient.java
index 3b7f3d3..b2743ac 100644
--- a/protocols/p4runtime/api/src/main/java/org/onosproject/p4runtime/api/P4RuntimeClient.java
+++ b/protocols/p4runtime/api/src/main/java/org/onosproject/p4runtime/api/P4RuntimeClient.java
@@ -18,12 +18,11 @@
 
 import com.google.common.annotations.Beta;
 import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiActionGroup;
 import org.onosproject.net.pi.runtime.PiActionProfileId;
 import org.onosproject.net.pi.runtime.PiCounterCellData;
 import org.onosproject.net.pi.runtime.PiCounterCellId;
 import org.onosproject.net.pi.runtime.PiCounterId;
-import org.onosproject.net.pi.runtime.PiActionGroup;
-import org.onosproject.net.pi.runtime.PiActionGroupMember;
 import org.onosproject.net.pi.runtime.PiPacketOperation;
 import org.onosproject.net.pi.runtime.PiTableEntry;
 import org.onosproject.net.pi.runtime.PiTableId;
@@ -121,13 +120,11 @@
      * Performs the given write operation for the given action group members and pipeconf.
      *
      * @param group action group
-     * @param members the collection of action group members
      * @param opType write operation type
      * @param pipeconf the pipeconf currently deployed on the device
      * @return true if the operation was successful, false otherwise
      */
     CompletableFuture<Boolean> writeActionGroupMembers(PiActionGroup group,
-                                                       Collection<PiActionGroupMember> members,
                                                        WriteOperationType opType,
                                                        PiPipeconf pipeconf);
 
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/ActionProfileGroupEncoder.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/ActionProfileGroupEncoder.java
index 472bf8e..c3783c1 100644
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/ActionProfileGroupEncoder.java
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/ActionProfileGroupEncoder.java
@@ -18,9 +18,9 @@
 
 import com.google.common.collect.Maps;
 import org.onosproject.net.pi.model.PiPipeconf;
-import org.onosproject.net.pi.runtime.PiActionProfileId;
 import org.onosproject.net.pi.runtime.PiActionGroup;
 import org.onosproject.net.pi.runtime.PiActionGroupId;
+import org.onosproject.net.pi.runtime.PiActionProfileId;
 import p4.P4RuntimeOuterClass.ActionProfileGroup;
 import p4.P4RuntimeOuterClass.ActionProfileGroup.Member;
 import p4.P4RuntimeOuterClass.ActionProfileMember;
@@ -57,12 +57,10 @@
         }
 
         PiActionProfileId piActionProfileId = piActionGroup.actionProfileId();
-        int actionProfileId;
-        P4InfoOuterClass.ActionProfile actionProfile =
-                browser.actionProfiles().getByName(piActionProfileId.id());
-        actionProfileId = actionProfile.getPreamble().getId();
-        ActionProfileGroup.Builder actionProfileGroupBuilder =
-                ActionProfileGroup.newBuilder()
+        P4InfoOuterClass.ActionProfile actionProfile = browser.actionProfiles()
+                .getByNameOrAlias(piActionProfileId.id());
+        int actionProfileId = actionProfile.getPreamble().getId();
+        ActionProfileGroup.Builder actionProfileGroupBuilder = ActionProfileGroup.newBuilder()
                         .setGroupId(piActionGroup.id().id())
                         .setActionProfileId(actionProfileId);
 
@@ -71,8 +69,7 @@
                 actionProfileGroupBuilder.setType(ActionProfileGroup.Type.SELECT);
                 break;
             default:
-                throw new EncodeException(format("Unsupported pi action group type %s",
-                                                 piActionGroup.type()));
+                throw new EncodeException(format("PI action group type %s not supported", piActionGroup.type()));
         }
 
         piActionGroup.members().forEach(m -> {
@@ -84,6 +81,8 @@
             actionProfileGroupBuilder.addMembers(member);
         });
 
+        actionProfileGroupBuilder.setMaxSize(piActionGroup.members().size());
+
         return actionProfileGroupBuilder.build();
     }
 
@@ -98,8 +97,8 @@
      * @throws EncodeException if can't find P4Info from pipeconf
      */
     static PiActionGroup decode(ActionProfileGroup actionProfileGroup,
-                                        Collection<ActionProfileMember> members,
-                                        PiPipeconf pipeconf)
+                                Collection<ActionProfileMember> members,
+                                PiPipeconf pipeconf)
             throws P4InfoBrowser.NotFoundException, EncodeException {
         P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
         if (browser == null) {
@@ -110,15 +109,19 @@
         P4InfoOuterClass.ActionProfile actionProfile = browser.actionProfiles()
                 .getById(actionProfileGroup.getActionProfileId());
         PiActionProfileId piActionProfileId = PiActionProfileId.of(actionProfile.getPreamble().getName());
-        piActionGroupBuilder.withActionProfileId(piActionProfileId)
+
+        piActionGroupBuilder
+                .withActionProfileId(piActionProfileId)
                 .withId(PiActionGroupId.of(actionProfileGroup.getGroupId()));
 
         switch (actionProfileGroup.getType()) {
+            case UNSPECIFIED:
+                // FIXME: PI returns unspecified for select groups. Remove this case when PI bug will be fixed.
             case SELECT:
                 piActionGroupBuilder.withType(PiActionGroup.Type.SELECT);
                 break;
             default:
-                throw new EncodeException(format("Unsupported action profile type %s",
+                throw new EncodeException(format("Action profile type %s is not supported",
                                                  actionProfileGroup.getType()));
         }
 
@@ -134,9 +137,12 @@
         });
 
         for (ActionProfileMember member : members) {
+            if (!memberWeights.containsKey(member.getMemberId())) {
+                // Not a member of this group, ignore.
+                continue;
+            }
             int weight = memberWeights.get(member.getMemberId());
-            piActionGroupBuilder
-                    .addMember(ActionProfileMemberEncoder.decode(member, weight, pipeconf));
+            piActionGroupBuilder.addMember(ActionProfileMemberEncoder.decode(member, weight, pipeconf));
         }
 
         return piActionGroupBuilder.build();
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/ActionProfileMemberEncoder.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/ActionProfileMemberEncoder.java
index 2f08a59..878ede6 100644
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/ActionProfileMemberEncoder.java
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/ActionProfileMemberEncoder.java
@@ -73,7 +73,7 @@
 
         // action profile id
         P4InfoOuterClass.ActionProfile actionProfile =
-                browser.actionProfiles().getByName(group.actionProfileId().id());
+                browser.actionProfiles().getByNameOrAlias(group.actionProfileId().id());
 
         int actionProfileId = actionProfile.getPreamble().getId();
         actionProfileMemberBuilder.setActionProfileId(actionProfileId);
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4InfoBrowser.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4InfoBrowser.java
index 609d2d2..177c381 100644
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4InfoBrowser.java
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4InfoBrowser.java
@@ -303,20 +303,6 @@
         }
 
         /**
-         * Returns the entity identified by the given name, if present, otherwise, throws an exception.
-         *
-         * @param name entity name
-         * @return entity message
-         * @throws NotFoundException if the entity cannot be found
-         */
-        T getByName(String name) throws NotFoundException {
-            if (!hasName(name)) {
-                throw new NotFoundException(entityName, name);
-            }
-            return names.get(name);
-        }
-
-        /**
          * Returns the entity identified by the given name or alias, if present, otherwise, throws an exception.
          *
          * @param name entity name or alias
@@ -344,20 +330,6 @@
         }
 
         /**
-         * Returns the entity identified by the given alias, if present, otherwise, throws an exception.
-         *
-         * @param alias entity alias
-         * @return entity message
-         * @throws NotFoundException if the entity cannot be found
-         */
-        T getByAlias(String alias) throws NotFoundException {
-            if (!hasName(alias)) {
-                throw new NotFoundException(entityName, alias);
-            }
-            return aliases.get(alias);
-        }
-
-        /**
          * Returns true if the P4Info defines an entity with such id, false otherwise.
          *
          * @param id entity id
@@ -388,7 +360,7 @@
     public static final class NotFoundException extends Exception {
 
         NotFoundException(String entityName, String key) {
-            super(format("No such %s in P4Info with name/alias '%s'", entityName, key));
+            super(format("No such %s in P4Info with %name or alias '%s'", entityName, key));
         }
 
         NotFoundException(String entityName, int id) {
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4RuntimeClientImpl.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4RuntimeClientImpl.java
index 7f22e79..a579e36 100644
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4RuntimeClientImpl.java
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4RuntimeClientImpl.java
@@ -16,10 +16,10 @@
 
 package org.onosproject.p4runtime.ctl;
 
+import com.google.common.collect.HashMultimap;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
-import com.google.common.collect.HashMultimap;
 import com.google.common.collect.Multimap;
 import com.google.common.collect.Sets;
 import com.google.protobuf.ByteString;
@@ -32,14 +32,14 @@
 import org.onlab.util.Tools;
 import org.onosproject.net.DeviceId;
 import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiActionGroup;
+import org.onosproject.net.pi.runtime.PiActionGroupMember;
 import org.onosproject.net.pi.runtime.PiActionProfileId;
 import org.onosproject.net.pi.runtime.PiCounterCellData;
 import org.onosproject.net.pi.runtime.PiCounterCellId;
 import org.onosproject.net.pi.runtime.PiCounterId;
 import org.onosproject.net.pi.runtime.PiDirectCounterCellId;
 import org.onosproject.net.pi.runtime.PiIndirectCounterCellId;
-import org.onosproject.net.pi.runtime.PiActionGroup;
-import org.onosproject.net.pi.runtime.PiActionGroupMember;
 import org.onosproject.net.pi.runtime.PiPacketOperation;
 import org.onosproject.net.pi.runtime.PiPipeconfService;
 import org.onosproject.net.pi.runtime.PiTableEntry;
@@ -62,7 +62,6 @@
 import p4.P4RuntimeOuterClass.TableEntry;
 import p4.P4RuntimeOuterClass.Update;
 import p4.P4RuntimeOuterClass.WriteRequest;
-import p4.config.P4InfoOuterClass;
 import p4.config.P4InfoOuterClass.P4Info;
 import p4.tmp.P4Config;
 
@@ -73,14 +72,13 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.Executor;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicLong;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
 import java.util.function.Supplier;
@@ -90,9 +88,7 @@
 import static org.onlab.util.Tools.groupedThreads;
 import static org.onosproject.net.pi.model.PiPipeconf.ExtensionType;
 import static org.slf4j.LoggerFactory.getLogger;
-import static p4.P4RuntimeOuterClass.Entity.EntityCase.ACTION_PROFILE_GROUP;
-import static p4.P4RuntimeOuterClass.Entity.EntityCase.ACTION_PROFILE_MEMBER;
-import static p4.P4RuntimeOuterClass.Entity.EntityCase.TABLE_ENTRY;
+import static p4.P4RuntimeOuterClass.Entity.EntityCase.*;
 import static p4.P4RuntimeOuterClass.PacketOut;
 import static p4.P4RuntimeOuterClass.SetForwardingPipelineConfigRequest.Action.VERIFY_AND_COMMIT;
 
@@ -123,9 +119,9 @@
     /**
      * Default constructor.
      *
-     * @param deviceId the ONOS device id
+     * @param deviceId   the ONOS device id
      * @param p4DeviceId the P4 device id
-     * @param channel gRPC channel
+     * @param channel    gRPC channel
      * @param controller runtime client controller
      */
     P4RuntimeClientImpl(DeviceId deviceId, long p4DeviceId, ManagedChannel channel,
@@ -239,10 +235,9 @@
 
     @Override
     public CompletableFuture<Boolean> writeActionGroupMembers(PiActionGroup group,
-                                                              Collection<PiActionGroupMember> members,
                                                               WriteOperationType opType,
                                                               PiPipeconf pipeconf) {
-        return supplyInContext(() -> doWriteActionGroupMembers(group, members, opType, pipeconf),
+        return supplyInContext(() -> doWriteActionGroupMembers(group, opType, pipeconf),
                                "writeActionGroupMembers-" + opType.name());
     }
 
@@ -394,7 +389,7 @@
         P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
         int tableId;
         try {
-            tableId = browser.tables().getByName(piTableId.id()).getPreamble().getId();
+            tableId = browser.tables().getByNameOrAlias(piTableId.id()).getPreamble().getId();
         } catch (P4InfoBrowser.NotFoundException e) {
             log.warn("Unable to dump table: {}", e.getMessage());
             return Collections.emptyList();
@@ -509,23 +504,19 @@
         return CounterEntryCodec.decodeCounterEntities(entities, counterIdMap, pipeconf);
     }
 
-    private boolean doWriteActionGroupMembers(PiActionGroup group, Collection<PiActionGroupMember> members,
-                                              WriteOperationType opType, PiPipeconf pipeconf) {
-        WriteRequest.Builder writeRequestBuilder = WriteRequest.newBuilder();
+    private boolean doWriteActionGroupMembers(PiActionGroup group, WriteOperationType opType, PiPipeconf pipeconf) {
 
-        Collection<ActionProfileMember> actionProfileMembers = Lists.newArrayList();
+        final Collection<ActionProfileMember> actionProfileMembers = Lists.newArrayList();
         try {
-            for (PiActionGroupMember member : members) {
-                actionProfileMembers.add(
-                        ActionProfileMemberEncoder.encode(group, member, pipeconf)
-                );
+            for (PiActionGroupMember member : group.members()) {
+                actionProfileMembers.add(ActionProfileMemberEncoder.encode(group, member, pipeconf));
             }
         } catch (EncodeException | P4InfoBrowser.NotFoundException e) {
-            log.warn("Can't encode group member {} due to {}", members, e.getMessage());
+            log.warn("Unable to write ({}) group members: {}", opType, e.getMessage());
             return false;
         }
 
-        Collection<Update> updateMsgs = actionProfileMembers.stream()
+        final Collection<Update> updateMsgs = actionProfileMembers.stream()
                 .map(actionProfileMember ->
                              Update.newBuilder()
                                      .setEntity(Entity.newBuilder()
@@ -536,18 +527,19 @@
                 .collect(Collectors.toList());
 
         if (updateMsgs.size() == 0) {
-            // Nothing to update
+            // Nothing to update.
             return true;
         }
 
-        writeRequestBuilder
+        WriteRequest writeRequestMsg = WriteRequest.newBuilder()
                 .setDeviceId(p4DeviceId)
-                .addAllUpdates(updateMsgs);
+                .addAllUpdates(updateMsgs)
+                .build();
         try {
-            blockingStub.write(writeRequestBuilder.build());
+            blockingStub.write(writeRequestMsg);
             return true;
         } catch (StatusRuntimeException e) {
-            log.warn("Unable to write table entries ({}): {}", opType, e.getMessage());
+            log.warn("Unable to write ({}) group members: {}", opType, e.getMessage());
             return false;
         }
     }
@@ -555,141 +547,149 @@
     private Collection<PiActionGroup> doDumpGroups(PiActionProfileId piActionProfileId, PiPipeconf pipeconf) {
         log.debug("Dumping groups from action profile {} from {} (pipeconf {})...",
                   piActionProfileId.id(), deviceId, pipeconf.id());
-        P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
+
+        final P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
         if (browser == null) {
-            log.warn("Unable to get a P4Info browser for pipeconf {}, skipping dump action profile {}",
-                     pipeconf, piActionProfileId);
+            log.warn("Unable to get a P4Info browser for pipeconf {}, aborting dump action profile", pipeconf);
             return Collections.emptySet();
         }
 
-        int actionProfileId;
+        final int actionProfileId;
         try {
-            P4InfoOuterClass.ActionProfile actionProfile =
-                    browser.actionProfiles().getByName(piActionProfileId.id());
-            actionProfileId = actionProfile.getPreamble().getId();
+            actionProfileId = browser
+                    .actionProfiles()
+                    .getByNameOrAlias(piActionProfileId.id())
+                    .getPreamble()
+                    .getId();
         } catch (P4InfoBrowser.NotFoundException e) {
-            log.warn("Can't find action profile {} from p4info", piActionProfileId);
+            log.warn("Unable to dump groups: {}", e.getMessage());
             return Collections.emptySet();
         }
 
-        ActionProfileGroup actionProfileGroup =
-                ActionProfileGroup.newBuilder()
-                        .setActionProfileId(actionProfileId)
-                        .build();
-
-        ReadRequest requestMsg = ReadRequest.newBuilder()
+        // Prepare read request to read all groups from the given action profile.
+        final ReadRequest groupRequestMsg = ReadRequest.newBuilder()
                 .setDeviceId(p4DeviceId)
                 .addEntities(Entity.newBuilder()
-                                     .setActionProfileGroup(actionProfileGroup)
+                                     .setActionProfileGroup(
+                                             ActionProfileGroup.newBuilder()
+                                                     .setActionProfileId(actionProfileId)
+                                                     .build())
                                      .build())
                 .build();
 
-        Iterator<ReadResponse> responses;
+        // Read groups.
+        final Iterator<ReadResponse> groupResponses;
         try {
-            responses = blockingStub.read(requestMsg);
+            groupResponses = blockingStub.read(groupRequestMsg);
         } catch (StatusRuntimeException e) {
-            log.warn("Unable to read action profile {} due to {}", piActionProfileId, e.getMessage());
+            log.warn("Unable dump groups from action profile '{}': {}", piActionProfileId.id(), e.getMessage());
             return Collections.emptySet();
         }
 
-        List<ActionProfileGroup> actionProfileGroups =
-                Tools.stream(() -> responses)
-                        .map(ReadResponse::getEntitiesList)
-                        .flatMap(List::stream)
-                        .filter(entity -> entity.getEntityCase() == ACTION_PROFILE_GROUP)
-                        .map(Entity::getActionProfileGroup)
-                        .collect(Collectors.toList());
+        final List<ActionProfileGroup> groupMsgs = Tools.stream(() -> groupResponses)
+                .map(ReadResponse::getEntitiesList)
+                .flatMap(List::stream)
+                .filter(entity -> entity.getEntityCase() == ACTION_PROFILE_GROUP)
+                .map(Entity::getActionProfileGroup)
+                .collect(Collectors.toList());
 
         log.debug("Retrieved {} groups from action profile {} on {}...",
-                  actionProfileGroups.size(), piActionProfileId.id(), deviceId);
+                  groupMsgs.size(), piActionProfileId.id(), deviceId);
 
-        // group id -> members
-        Multimap<Integer, ActionProfileMember> actionProfileMemberMap = HashMultimap.create();
-        AtomicLong memberCount = new AtomicLong(0);
-        AtomicBoolean success = new AtomicBoolean(true);
-        actionProfileGroups.forEach(actProfGrp -> {
-            actProfGrp.getMembersList().forEach(member -> {
-                ActionProfileMember actProfMember =
-                        ActionProfileMember.newBuilder()
-                                .setActionProfileId(actProfGrp.getActionProfileId())
-                                .setMemberId(member.getMemberId())
-                                .build();
-                Entity entity = Entity.newBuilder()
-                        .setActionProfileMember(actProfMember)
-                        .build();
+        // Returned groups contain only a minimal description of their members.
+        // We need to issue a new request to get the full description of each member.
 
-                ReadRequest reqMsg = ReadRequest.newBuilder().setDeviceId(p4DeviceId)
-                        .addEntities(entity)
-                        .build();
+        // Keep a map of all member IDs for each group ID, will need it later.
+        final Multimap<Integer, Integer> groupIdToMemberIdsMap = HashMultimap.create();
+        groupMsgs.forEach(g -> groupIdToMemberIdsMap.putAll(
+                g.getGroupId(),
+                g.getMembersList().stream()
+                        .map(ActionProfileGroup.Member::getMemberId)
+                        .collect(Collectors.toList())));
 
-                Iterator<ReadResponse> resps;
-                try {
-                    resps = blockingStub.read(reqMsg);
-                } catch (StatusRuntimeException e) {
-                    log.warn("Unable to read member {} from action profile {} due to {}",
-                             member, piActionProfileId, e.getMessage());
-                    success.set(false);
-                    return;
-                }
-                Tools.stream(() -> resps)
-                        .map(ReadResponse::getEntitiesList)
-                        .flatMap(List::stream)
-                        .filter(e -> e.getEntityCase() == ACTION_PROFILE_MEMBER)
-                        .map(Entity::getActionProfileMember)
-                        .forEach(m -> {
-                            actionProfileMemberMap.put(actProfGrp.getGroupId(), m);
-                            memberCount.incrementAndGet();
-                        });
-            });
-        });
+        // Prepare one big read request to read all members in one shot.
+        final Set<Entity> entityMsgs = groupMsgs.stream()
+                .flatMap(g -> g.getMembersList().stream())
+                .map(ActionProfileGroup.Member::getMemberId)
+                // Prevent issuing many read requests for the same member.
+                .distinct()
+                .map(id -> ActionProfileMember.newBuilder()
+                        .setActionProfileId(actionProfileId)
+                        .setMemberId(id)
+                        .build())
+                .map(m -> Entity.newBuilder()
+                        .setActionProfileMember(m)
+                        .build())
+                .collect(Collectors.toSet());
+        final ReadRequest memberRequestMsg = ReadRequest.newBuilder().setDeviceId(p4DeviceId)
+                .addAllEntities(entityMsgs)
+                .build();
 
-        if (!success.get()) {
-            // Can't read members
-            return Collections.emptySet();
-        }
-        log.info("Retrieved {} group members from action profile {} on {}...",
-                 memberCount.get(), piActionProfileId.id(), deviceId);
-
-        Collection<PiActionGroup> piActionGroups = Sets.newHashSet();
-
-        for (ActionProfileGroup apg : actionProfileGroups) {
-            try {
-                Collection<ActionProfileMember> members = actionProfileMemberMap.get(apg.getGroupId());
-                PiActionGroup decodedGroup =
-                        ActionProfileGroupEncoder.decode(apg, members, pipeconf);
-                piActionGroups.add(decodedGroup);
-            } catch (EncodeException | P4InfoBrowser.NotFoundException e) {
-                log.warn("Can't decode group {} due to {}", apg, e.getMessage());
-                return Collections.emptySet();
-            }
+        // Read members.
+        final Iterator<ReadResponse> memberResponses;
+        try {
+            memberResponses = blockingStub.read(memberRequestMsg);
+        } catch (StatusRuntimeException e) {
+            log.warn("Unable to read members from action profile {}: {}", piActionProfileId, e.getMessage());
+            return Collections.emptyList();
         }
 
-        return piActionGroups;
+        final Multimap<Integer, ActionProfileMember> groupIdToMembersMap = HashMultimap.create();
+        Tools.stream(() -> memberResponses)
+                .map(ReadResponse::getEntitiesList)
+                .flatMap(List::stream)
+                .filter(e -> e.getEntityCase() == ACTION_PROFILE_MEMBER)
+                .map(Entity::getActionProfileMember)
+                .forEach(member -> groupIdToMemberIdsMap.asMap()
+                        // Get all group IDs that contain this member.
+                        .entrySet()
+                        .stream()
+                        .filter(entry -> entry.getValue().contains(member.getMemberId()))
+                        .map(Map.Entry::getKey)
+                        .forEach(gid -> groupIdToMembersMap.put(gid, member)));
+
+        log.debug("Retrieved {} group members from action profile {} on {}...",
+                  groupIdToMembersMap.size(), piActionProfileId.id(), deviceId);
+
+        return groupMsgs.stream()
+                .map(groupMsg -> {
+                    try {
+                        return ActionProfileGroupEncoder.decode(groupMsg,
+                                                                groupIdToMembersMap.get(groupMsg.getGroupId()),
+                                                                pipeconf);
+                    } catch (P4InfoBrowser.NotFoundException | EncodeException e) {
+                        log.warn("Unable to decode group: {}\n {}", e.getMessage(), groupMsg);
+                        return null;
+                    }
+                })
+                .filter(Objects::nonNull)
+                .collect(Collectors.toList());
     }
 
     private boolean doWriteActionGroup(PiActionGroup group, WriteOperationType opType, PiPipeconf pipeconf) {
-        WriteRequest.Builder writeRequestBuilder = WriteRequest.newBuilder();
-        ActionProfileGroup actionProfileGroup;
+
+        final ActionProfileGroup actionProfileGroup;
         try {
             actionProfileGroup = ActionProfileGroupEncoder.encode(group, pipeconf);
         } catch (EncodeException | P4InfoBrowser.NotFoundException e) {
-            log.warn("Can't encode group {} due to {}", e.getMessage());
+            log.warn("Unable to encode group: {}", e.getMessage());
             return false;
         }
-        Update updateMessage = Update.newBuilder()
-                .setEntity(Entity.newBuilder()
-                                   .setActionProfileGroup(actionProfileGroup)
-                                   .build())
-                .setType(UPDATE_TYPES.get(opType))
-                .build();
-        writeRequestBuilder
+
+        final WriteRequest writeRequestMsg = WriteRequest.newBuilder()
                 .setDeviceId(p4DeviceId)
-                .addUpdates(updateMessage);
+                .addUpdates(Update.newBuilder()
+                                    .setEntity(Entity.newBuilder()
+                                                       .setActionProfileGroup(actionProfileGroup)
+                                                       .build())
+                                    .setType(UPDATE_TYPES.get(opType))
+                                    .build())
+                .build();
         try {
-            blockingStub.write(writeRequestBuilder.build());
+            blockingStub.write(writeRequestMsg);
             return true;
         } catch (StatusRuntimeException e) {
-            log.warn("Unable to write table entries ({}): {}", opType, e.getMessage());
+            log.warn("Unable to write groups ({}): {}", opType, e.getMessage());
             return false;
         }
     }
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/PacketIOCodec.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/PacketIOCodec.java
index 20219f3..2ffe31b 100644
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/PacketIOCodec.java
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/PacketIOCodec.java
@@ -33,9 +33,7 @@
 import static org.onlab.util.ImmutableByteSequence.copyFrom;
 import static org.onosproject.p4runtime.ctl.P4InfoBrowser.NotFoundException;
 import static org.slf4j.LoggerFactory.getLogger;
-import static p4.P4RuntimeOuterClass.PacketIn;
-import static p4.P4RuntimeOuterClass.PacketMetadata;
-import static p4.P4RuntimeOuterClass.PacketOut;
+import static p4.P4RuntimeOuterClass.*;
 
 /**
  * Encoder of packet metadata, from ONOS Pi* format, to P4Runtime protobuf messages, and vice versa.
@@ -75,7 +73,7 @@
 
         //Get the packet out controller packet metadata
         P4InfoOuterClass.ControllerPacketMetadata controllerPacketMetadata =
-                browser.controllerPacketMetadatas().getByName(PACKET_OUT);
+                browser.controllerPacketMetadatas().getByNameOrAlias(PACKET_OUT);
         PacketOut.Builder packetOutBuilder = PacketOut.newBuilder();
 
         //outer controller packet metadata id
@@ -96,7 +94,7 @@
             try {
                 //get each metadata id
                 int metadataId = browser.packetMetadatas(controllerPacketMetadataId)
-                        .getByName(metadata.id().name()).getId();
+                        .getByNameOrAlias(metadata.id().name()).getId();
 
                 //Add the metadata id and it's data the packet out
                 return PacketMetadata.newBuilder()
@@ -129,7 +127,7 @@
 
         List<PiPacketMetadata> packetMetadatas;
         try {
-            int controllerPacketMetadataId = browser.controllerPacketMetadatas().getByName(PACKET_IN)
+            int controllerPacketMetadataId = browser.controllerPacketMetadatas().getByNameOrAlias(PACKET_IN)
                                                 .getPreamble().getId();
             packetMetadatas = decodePacketMetadataIn(packetIn.getMetadataList(), browser,
                                                                             controllerPacketMetadataId);
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/TableEntryEncoder.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/TableEntryEncoder.java
index 8278b3e..3890848 100644
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/TableEntryEncoder.java
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/TableEntryEncoder.java
@@ -39,10 +39,10 @@
 import org.onosproject.net.pi.runtime.PiTernaryFieldMatch;
 import org.onosproject.net.pi.runtime.PiValidFieldMatch;
 import org.slf4j.Logger;
+import p4.P4RuntimeOuterClass.Action;
 import p4.P4RuntimeOuterClass.FieldMatch;
 import p4.P4RuntimeOuterClass.TableAction;
 import p4.P4RuntimeOuterClass.TableEntry;
-import p4.P4RuntimeOuterClass.Action;
 import p4.config.P4InfoOuterClass;
 
 import java.util.Collection;
@@ -197,7 +197,7 @@
         TableEntry.Builder tableEntryMsgBuilder = TableEntry.newBuilder();
 
         //FIXME this throws some kind of NPE
-        P4InfoOuterClass.Table tableInfo = browser.tables().getByName(tableId.id());
+        P4InfoOuterClass.Table tableInfo = browser.tables().getByNameOrAlias(tableId.id());
 
         // Table id.
         tableEntryMsgBuilder.setTableId(tableInfo.getPreamble().getId());
@@ -216,7 +216,7 @@
         TableEntry.Builder tableEntryMsgBuilder = TableEntry.newBuilder();
 
         //FIXME this throws some kind of NPE
-        P4InfoOuterClass.Table tableInfo = browser.tables().getByName(piTableEntry.table().id());
+        P4InfoOuterClass.Table tableInfo = browser.tables().getByNameOrAlias(piTableEntry.table().id());
 
         // Table id.
         tableEntryMsgBuilder.setTableId(tableInfo.getPreamble().getId());
@@ -469,13 +469,13 @@
     static Action encodePiAction(PiAction piAction, P4InfoBrowser browser)
             throws P4InfoBrowser.NotFoundException, EncodeException {
 
-        int actionId = browser.actions().getByName(piAction.id().name()).getPreamble().getId();
+        int actionId = browser.actions().getByNameOrAlias(piAction.id().name()).getPreamble().getId();
 
         Action.Builder actionMsgBuilder =
                 Action.newBuilder().setActionId(actionId);
 
         for (PiActionParam p : piAction.parameters()) {
-            P4InfoOuterClass.Action.Param paramInfo = browser.actionParams(actionId).getByName(p.id().name());
+            P4InfoOuterClass.Action.Param paramInfo = browser.actionParams(actionId).getByNameOrAlias(p.id().name());
             ByteString paramValue = ByteString.copyFrom(p.value().asReadOnlyBuffer());
             assertSize(format("param '%s' of action '%s'", p.id(), piAction.id()),
                        paramValue, paramInfo.getBitwidth());
diff --git a/protocols/p4runtime/ctl/src/test/java/org/onosproject/p4runtime/ctl/P4RuntimeGroupTest.java b/protocols/p4runtime/ctl/src/test/java/org/onosproject/p4runtime/ctl/P4RuntimeGroupTest.java
index 8932a16..1c9c4ab 100644
--- a/protocols/p4runtime/ctl/src/test/java/org/onosproject/p4runtime/ctl/P4RuntimeGroupTest.java
+++ b/protocols/p4runtime/ctl/src/test/java/org/onosproject/p4runtime/ctl/P4RuntimeGroupTest.java
@@ -36,7 +36,6 @@
 import org.onosproject.net.pi.model.PiPipeconf;
 import org.onosproject.net.pi.model.PiPipeconfId;
 import org.onosproject.net.pi.model.PiPipelineModel;
-import org.onosproject.net.pi.runtime.PiActionProfileId;
 import org.onosproject.net.pi.runtime.PiAction;
 import org.onosproject.net.pi.runtime.PiActionGroup;
 import org.onosproject.net.pi.runtime.PiActionGroupId;
@@ -45,6 +44,7 @@
 import org.onosproject.net.pi.runtime.PiActionId;
 import org.onosproject.net.pi.runtime.PiActionParam;
 import org.onosproject.net.pi.runtime.PiActionParamId;
+import org.onosproject.net.pi.runtime.PiActionProfileId;
 import p4.P4RuntimeOuterClass.ActionProfileGroup;
 import p4.P4RuntimeOuterClass.ActionProfileMember;
 import p4.P4RuntimeOuterClass.Entity;
@@ -57,15 +57,15 @@
 import java.util.List;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
 
-import static org.easymock.EasyMock.*;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
+import static org.easymock.EasyMock.niceMock;
+import static org.junit.Assert.*;
 import static org.onosproject.net.pi.model.PiPipeconf.ExtensionType.P4_INFO_TEXT;
 import static org.onosproject.net.pi.runtime.PiActionGroup.Type.SELECT;
 import static org.onosproject.p4runtime.api.P4RuntimeClient.WriteOperationType.INSERT;
-import static p4.P4RuntimeOuterClass.*;
+import static p4.P4RuntimeOuterClass.Action;
+import static p4.P4RuntimeOuterClass.ReadResponse;
 
 /**
  * Tests for P4 Runtime Action Profile Group support.
@@ -98,7 +98,7 @@
     private static final int P4_DEVICE_ID = 1;
     private static final int SET_EGRESS_PORT_ID = 16794308;
     private static final String GRPC_SERVER_NAME = "P4RuntimeGroupTest";
-    private static final long DEFAULT_TIMEOUT_TIME = 5;
+    private static final long DEFAULT_TIMEOUT_TIME = 10;
 
     private P4RuntimeClientImpl client;
     private P4RuntimeControllerImpl controller;
@@ -187,7 +187,7 @@
     @Test
     public void testInsertPiActionMembers() throws Exception {
         CompletableFuture<Void> complete = p4RuntimeServerImpl.expectRequests(1);
-        client.writeActionGroupMembers(GROUP, GROUP_MEMBERS, INSERT, PIPECONF);
+        client.writeActionGroupMembers(GROUP, INSERT, PIPECONF);
         complete.get(DEFAULT_TIMEOUT_TIME, TimeUnit.SECONDS);
         WriteRequest result = p4RuntimeServerImpl.getWriteReqs().get(0);
         assertEquals(1, result.getDeviceId());
@@ -240,7 +240,6 @@
                     .addParams(param)
                     .build();
 
-
             ActionProfileMember actProfMember =
                     ActionProfileMember.newBuilder()
                             .setMemberId(id)
@@ -255,14 +254,14 @@
                               .build()
         );
 
-        members.forEach(m -> {
-            responses.add(ReadResponse.newBuilder()
-                                  .addEntities(Entity.newBuilder().setActionProfileMember(m))
-                                  .build());
-        });
+        responses.add(ReadResponse.newBuilder()
+                              .addAllEntities(members.stream()
+                                                      .map(m -> Entity.newBuilder().setActionProfileMember(m).build())
+                                                      .collect(Collectors.toList()))
+                              .build());
 
         p4RuntimeServerImpl.willReturnReadResult(responses);
-        CompletableFuture<Void> complete = p4RuntimeServerImpl.expectRequests(4);
+        CompletableFuture<Void> complete = p4RuntimeServerImpl.expectRequests(2);
         CompletableFuture<Collection<PiActionGroup>> groupsComplete = client.dumpGroups(ACT_PROF_ID, PIPECONF);
         complete.get(DEFAULT_TIMEOUT_TIME, TimeUnit.SECONDS);
 
diff --git a/protocols/p4runtime/ctl/src/test/java/org/onosproject/p4runtime/ctl/TableEntryEncoderTest.java b/protocols/p4runtime/ctl/src/test/java/org/onosproject/p4runtime/ctl/TableEntryEncoderTest.java
index b3e9a1e..b434b4d 100644
--- a/protocols/p4runtime/ctl/src/test/java/org/onosproject/p4runtime/ctl/TableEntryEncoderTest.java
+++ b/protocols/p4runtime/ctl/src/test/java/org/onosproject/p4runtime/ctl/TableEntryEncoderTest.java
@@ -44,9 +44,7 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.hasSize;
 import static org.hamcrest.Matchers.is;
-import static org.onlab.util.ImmutableByteSequence.copyFrom;
-import static org.onlab.util.ImmutableByteSequence.fit;
-import static org.onlab.util.ImmutableByteSequence.ofOnes;
+import static org.onlab.util.ImmutableByteSequence.*;
 import static org.onosproject.net.pi.model.PiPipeconf.ExtensionType.P4_INFO_TEXT;
 import static org.onosproject.p4runtime.ctl.TableEntryEncoder.decode;
 import static org.onosproject.p4runtime.ctl.TableEntryEncoder.encode;
@@ -114,8 +112,8 @@
         assertThat(browser.tables().hasName(TABLE_0), is(true));
         assertThat(browser.actions().hasName(SET_EGRESS_PORT), is(true));
 
-        int tableId = browser.tables().getByName(TABLE_0).getPreamble().getId();
-        int actionId = browser.actions().getByName(SET_EGRESS_PORT).getPreamble().getId();
+        int tableId = browser.tables().getByNameOrAlias(TABLE_0).getPreamble().getId();
+        int actionId = browser.actions().getByNameOrAlias(SET_EGRESS_PORT).getPreamble().getId();
 
         assertThat(browser.matchFields(tableId).hasName(STANDARD_METADATA + "." + INGRESS_PORT), is(true));
         assertThat(browser.actionParams(actionId).hasName(PORT), is(true));
@@ -141,7 +139,7 @@
                 .testEquals();
 
         // Table ID.
-        int p4InfoTableId = browser.tables().getByName(tableId.id()).getPreamble().getId();
+        int p4InfoTableId = browser.tables().getByNameOrAlias(tableId.id()).getPreamble().getId();
         int encodedTableId = tableEntryMsg.getTableId();
         assertThat(encodedTableId, is(p4InfoTableId));
 
@@ -152,12 +150,12 @@
         Action actionMsg = tableEntryMsg.getAction().getAction();
 
         // Action ID.
-        int p4InfoActionId = browser.actions().getByName(outActionId.name()).getPreamble().getId();
+        int p4InfoActionId = browser.actions().getByNameOrAlias(outActionId.name()).getPreamble().getId();
         int encodedActionId = actionMsg.getActionId();
         assertThat(encodedActionId, is(p4InfoActionId));
 
         // Action param ID.
-        int p4InfoActionParamId = browser.actionParams(p4InfoActionId).getByName(portParamId.name()).getId();
+        int p4InfoActionParamId = browser.actionParams(p4InfoActionId).getByNameOrAlias(portParamId.name()).getId();
         int encodedActionParamId = actionMsg.getParams(0).getParamId();
         assertThat(encodedActionParamId, is(p4InfoActionParamId));