VOL-607: OpenOMCI OMCI_CC and MEFrame class support
Fixed bug found during unit test create

Change-Id: Ib939681ebf81618af95b3597c3311526626aaebf
diff --git a/tests/utests/voltha/extensions/omci/test_me_frame.py b/tests/utests/voltha/extensions/omci/test_me_frame.py
new file mode 100644
index 0000000..07aa49d
--- /dev/null
+++ b/tests/utests/voltha/extensions/omci/test_me_frame.py
@@ -0,0 +1,45 @@
+#
+# Copyright 2017 the original author or authors.
+#
+# 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.
+#
+from unittest import TestCase, main
+
+from voltha.extensions.omci.me_frame import *
+
+# NOTE  NOTE  NOTE  NOTE  NOTE  NOTE  NOTE  NOTE  NOTE  NOTE  NOTE  NOTE  NOTE
+#
+# NOTE: This is a placeholder for OpenOMCI unit tests of the MEFrame class
+#       Initial (worthwhile) tests will be provided in VOL-607
+#
+# NOTE  NOTE  NOTE  NOTE  NOTE  NOTE  NOTE  NOTE  NOTE  NOTE  NOTE  NOTE  NOTE
+
+
+class TestMeFrameExample(TestCase):
+
+    def test_example_1(self):
+
+        self.assertTrue(True)
+        self.assertFalse(False)
+        self.assertEqual('123', '123')
+
+    def test_example_3(self):
+
+        self.assertTrue(True)
+        self.assertFalse(False)
+        self.assertEqual('123', '123')
+
+
+if __name__ == '__main__':
+    main()
+
diff --git a/tests/utests/voltha/extensions/omci/test_omci_cc.py b/tests/utests/voltha/extensions/omci/test_omci_cc.py
new file mode 100644
index 0000000..e68c61c
--- /dev/null
+++ b/tests/utests/voltha/extensions/omci/test_omci_cc.py
@@ -0,0 +1,47 @@
+#
+# Copyright 2017 the original author or authors.
+#
+# 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.
+#
+from unittest import TestCase, main
+
+from voltha.extensions.omci.omci_cc import *
+
+
+# NOTE  NOTE  NOTE  NOTE  NOTE  NOTE  NOTE  NOTE  NOTE  NOTE  NOTE  NOTE  NOTE
+#
+# NOTE: This is a placeholder for OpenOMCI unit tests of the OMCI_CC class
+#       Initial (worthwhile) tests will be provided in VOL-607. The VOL-607
+#       check-in will also likely include the start of a mock ONU device.
+#
+# NOTE  NOTE  NOTE  NOTE  NOTE  NOTE  NOTE  NOTE  NOTE  NOTE  NOTE  NOTE  NOTE
+
+
+class TestOmciCcExample(TestCase):
+
+    def test_example_1(self):
+
+        self.assertTrue(True)
+        self.assertFalse(False)
+        self.assertEqual('123', '123')
+
+    def test_example_3(self):
+
+        self.assertTrue(True)
+        self.assertFalse(False)
+        self.assertEqual('123', '123')
+
+
+if __name__ == '__main__':
+    main()
+
diff --git a/voltha/extensions/omci/me_frame.py b/voltha/extensions/omci/me_frame.py
new file mode 100644
index 0000000..1808d04
--- /dev/null
+++ b/voltha/extensions/omci/me_frame.py
@@ -0,0 +1,291 @@
+#
+# Copyright 2017 the original author or authors.
+#
+# 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.
+#
+"""
+OMCI Managed Entity Message support base class
+"""
+from voltha.extensions.omci.omci import *
+
+# abbreviations
+OP = EntityOperations
+AA = AttributeAccess
+
+
+class MEFrame(object):
+    """Base class to help simplify Frame Creation"""
+    def __init__(self, entity_class, entity_id, data):
+        assert issubclass(entity_class, EntityClass), \
+            "'{}' must be a subclass of MEFrame".format(entity_class)
+        self.check_type(entity_id, int)
+
+        if not 0 <= entity_id <= 0xFFFF:
+            raise ValueError('entity_id should be 0..65535')
+
+        self._class = entity_class
+        self._entity_id = entity_id
+        self.data = data
+
+    def __str__(self):
+        return '{}: Entity_ID: {}, Data: {}'.\
+            format(self.entity_class_name, self._entity_id, self.data)
+
+    @property
+    def entity_class(self):
+        """
+        The Entity Class for this ME
+        :return: (EntityClass) Entity class
+        """
+        return self._class
+
+    @property
+    def entity_class_name(self):
+        return self._class.__name__
+
+    @property
+    def entity_id(self):
+        """
+        The Entity ID for this ME frame
+        :return: (int) Entity ID (0..0xFFFF)
+        """
+        return self._entity_id
+
+    @staticmethod
+    def check_type(param, types):
+        if not isinstance(param, types):
+            raise TypeError("Parameter '{}' should be a {}".format(param, types))
+
+    def _check_operation(self, operation):
+        allowed = self.entity_class.mandatory_operations | self.entity_class.optional_operations
+        assert operation in allowed, "{} not allowed for '{}'".format(operation.name,
+                                                                      self.entity_class_name)
+
+    def _check_attributes(self, attributes, access):
+        keys = attributes.keys() if isinstance(attributes, dict) else attributes
+        for attr_name in keys:
+            # Bad attribute name (invalid or spelling error)?
+            index = self.entity_class.attribute_name_to_index_map.get(attr_name)
+            if index is None:
+                raise KeyError("Attribute '{}' is not valid for '{}'".
+                               format(attr_name, self.entity_class_name))
+            # Invalid access?
+            assert access in self.entity_class.attributes[index].access, \
+                "Access '{}' for attribute '{}' is not valid for '{}'".format(access.name,
+                                                                              attr_name,
+                                                                              self.entity_class_name)
+
+        if access.value in [AA.W.value, AA.SBC.value] and isinstance(attributes, dict):
+            for attr_name, value in attributes.iteritems():
+                index = self.entity_class.attribute_name_to_index_map.get(attr_name)
+                attribute = self.entity_class.attributes[index]
+                if not attribute.valid(value):
+                    raise ValueError("Invalid value '{}' for attribute '{}' of '{}".
+                                     format(value, attr_name, self.entity_class_name))
+
+    @staticmethod
+    def _attr_to_data(attributes):
+        """
+        Convert an object into the 'data' set or dictionary for get/set/create/delete
+        requests.
+
+        This method takes a 'string', 'list', or 'set' for get requests and
+        converts it to a 'set' of attributes.
+
+        For create/set requests a dictionary of attribute/value pairs is required
+
+        :param attributes: (basestring, list, set, dict) attributes. For gets
+                           a string, list, set, or dict can be provided. For create/set
+                           operations, a dictionary should be provided. For delete
+                           the attributes may be None since they are ignored.
+
+        :return: (set, dict) set for get/deletes, dict for create/set
+        """
+        if isinstance(attributes, basestring):
+            # data = [str(attributes)]
+            data = set()
+            data.add(str(attributes))
+
+        elif isinstance(attributes, list):
+            assert all(isinstance(attr, basestring) for attr in attributes),\
+                'attribute list must be strings'
+            data = {str(attr) for attr in attributes}
+            assert len(data) == len(attributes), 'Attributes were not unique'
+
+        elif isinstance(attributes, set):
+            assert all(isinstance(attr, basestring) for attr in attributes),\
+                'attribute set must be strings'
+            data = {str(attr) for attr in attributes}
+
+        elif isinstance(attributes, (dict, type(None))):
+            data = attributes
+
+        else:
+            raise TypeError("Unsupported attributes type '{}'".format(type(attributes)))
+
+        return data
+
+    def create(self):
+        """
+        Create a Create request frame for this ME
+        :return: (OmciFrame) OMCI Frame
+        """
+        assert hasattr(self.entity_class, 'class_id'), 'class_id required for Create actions'
+        assert hasattr(self, 'entity_id'), 'entity_id required for Create actions'
+        assert hasattr(self, 'data'), 'data required for Create actions'
+
+        data = getattr(self, 'data')
+        MEFrame.check_type(data, dict)
+        assert len(data) > 0, 'No attributes supplied'
+
+        self._check_operation(OP.Create)
+        self._check_attributes(data, AA.Writable)
+
+        return OmciFrame(
+            transaction_id=None,
+            message_type=OmciCreate.message_id,
+            omci_message=OmciCreate(
+                entity_class=getattr(self.entity_class, 'class_id'),
+                entity_id=getattr(self, 'entity_id'),
+                data=data
+            ))
+
+    def delete(self):
+        """
+        Create a Delete request frame for this ME
+        :return: (OmciFrame) OMCI Frame
+        """
+        self._check_operation(OP.Delete)
+
+        return OmciFrame(
+            transaction_id=None,
+            message_type=OmciGet.message_id,
+            omci_message=OmciGet(
+                entity_class=getattr(self.entity_class, 'class_id'),
+                entity_id=getattr(self, 'entity_id')
+            ))
+
+    def set(self):
+        """
+        Create a Set request frame for this ME
+        :return: (OmciFrame) OMCI Frame
+        """
+        assert hasattr(self, 'data'), 'data required for Set actions'
+        data = getattr(self, 'data')
+        MEFrame.check_type(data, dict)
+        assert len(data) > 0, 'No attributes supplied'
+
+        self._check_operation(OP.Set)
+        self._check_attributes(data, AA.Writable)
+
+        return OmciFrame(
+            transaction_id=None,
+            message_type=OmciSet.message_id,
+            omci_message=OmciSet(
+                entity_class=getattr(self.entity_class, 'class_id'),
+                entity_id=getattr(self, 'entity_id'),
+                attributes_mask=self.entity_class.mask_for(*data.keys()),
+                data=data
+            ))
+
+    def get(self):
+        """
+        Create a Get request frame for this ME
+        :return: (OmciFrame) OMCI Frame
+        """
+        assert hasattr(self, 'data'), 'data required for Get actions'
+        data = getattr(self, 'data')
+        MEFrame.check_type(data, (list, set, dict))
+        assert len(data) > 0, 'No attributes supplied'
+
+        mask_set = data.keys() if isinstance(data, dict) else data
+
+        self._check_operation(OP.Get)
+        self._check_attributes(mask_set, AA.Readable)
+
+        return OmciFrame(
+            transaction_id=None,
+            message_type=OmciGet.message_id,
+            omci_message=OmciGet(
+                entity_class=getattr(self.entity_class, 'class_id'),
+                entity_id=getattr(self, 'entity_id'),
+                attributes_mask=self.entity_class.mask_for(*mask_set)
+            ))
+
+    def reboot(self):
+        """
+        Create a Reboot request from for this ME
+        :return: (OmciFrame) OMCI Frame
+        """
+        self._check_operation(OP.Reboot)
+
+        return OmciFrame(
+            transaction_id=None,
+            message_type=OmciReboot.message_id,
+            omci_message=OmciReboot(
+                entity_class=getattr(self.entity_class, 'class_id'),
+                entity_id=getattr(self, 'entity_id')
+            ))
+
+    def mib_reset(self):
+        """
+        Create a MIB Reset request from for this ME
+        :return: (OmciFrame) OMCI Frame
+        """
+        self._check_operation(OP.MibReset)
+
+        return OmciFrame(
+            transaction_id=None,
+            message_type=OmciMibReset.message_id,
+            omci_message=OmciMibReset(
+                entity_class=getattr(self.entity_class, 'class_id'),
+                entity_id=getattr(self, 'entity_id')
+            ))
+
+    def mib_upload(self):
+        """
+        Create a MIB Upload request from for this ME
+        :return: (OmciFrame) OMCI Frame
+        """
+        self._check_operation(OP.MibUpload)
+
+        return OmciFrame(
+            transaction_id=None,
+            message_type=OmciMibUpload.message_id,
+            omci_message=OmciMibUpload(
+                entity_class=getattr(self.entity_class, 'class_id'),
+                entity_id=getattr(self, 'entity_id')
+            ))
+
+    def mib_upload_next(self):
+        """
+        Create a MIB Upload Next request from for this ME
+        :return: (OmciFrame) OMCI Frame
+        """
+        assert hasattr(self, 'data'), 'data required for Set actions'
+        data = getattr(self, 'data')
+        MEFrame.check_type(data, dict)
+        assert len(data) > 0, 'No attributes supplied'
+        assert 'mib_data_sync' in data, "'mib_data_sync' not in attributes list"
+
+        self._check_operation(OP.MibUploadNext)
+        self._check_attributes(data, AA.Writable)
+
+        return OmciFrame(
+            transaction_id=None,
+            message_type=OmciMibUploadNext.message_id,
+            omci_message=OmciMibUploadNext(
+                entity_class=getattr(self.entity_class, 'class_id'),
+                entity_id=getattr(self, 'entity_id'),
+                command_sequence_number=data['mib_data_sync']
+            ))
diff --git a/voltha/extensions/omci/omci_cc.py b/voltha/extensions/omci/omci_cc.py
new file mode 100644
index 0000000..8a5c55c
--- /dev/null
+++ b/voltha/extensions/omci/omci_cc.py
@@ -0,0 +1,519 @@
+#
+# Copyright 2017 the original author or authors.
+#
+# 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.
+#
+"""
+OMCI Message support
+"""
+
+import sys
+import arrow
+from twisted.internet import reactor, defer
+from twisted.internet.defer import DeferredQueue, TimeoutError, CancelledError, failure, fail
+from voltha.protos import third_party
+from common.frameio.frameio import hexify
+from voltha.extensions.omci.omci import *
+from voltha.extensions.omci.omci_me import OntGFrame, OntDataFrame
+
+_ = third_party
+
+_MAX_INCOMING_ALARM_MESSAGES = 256
+_MAX_INCOMING_AVC_MESSAGES = 256
+_MAX_INCOMING_TEST_RESULT_MESSAGES = 64
+
+DEFAULT_OMCI_TIMEOUT = 3            # Seconds
+MAX_OMCI_REQUEST_AGE = 60           # Seconds
+MAX_OMCI_TX_ID = 0xFFFF             # 2 Octets max
+
+# abbreviations
+OP = EntityOperations
+
+
+class OMCI_CC(object):
+    """ Handle OMCI Communication Channel specifics for Adtran ONUs"""
+
+    def __init__(self, adapter_agent, device_id, me_map=None,
+                 alarm_queue_limit=_MAX_INCOMING_ALARM_MESSAGES,
+                 avc_queue_limit=_MAX_INCOMING_ALARM_MESSAGES,
+                 test_results_queue_limit=_MAX_INCOMING_TEST_RESULT_MESSAGES):
+        self.log = structlog.get_logger(device_id=device_id)
+        self._adapter_agent = adapter_agent
+        self._device_id = device_id
+        self._proxy_address = None
+        self._tx_tid = 1
+        self._enabled = False
+        self._requests = dict()       # Tx ID -> (timestamp, deferred, tx_frame, timeout)
+        self._alarm_queue = DeferredQueue(size=alarm_queue_limit)
+        self._avc_queue = DeferredQueue(size=avc_queue_limit)
+        self._test_results_queue = DeferredQueue(size=test_results_queue_limit)
+        self._me_map = me_map
+
+        # Statistics
+        self._tx_frames = 0
+        self._rx_frames = 0
+        self._rx_unknown_tid = 0      # Rx OMCI with no Tx TID match
+        self._rx_onu_frames = 0       # Autonomously generated ONU frames
+        self._rx_alarm_overflow = 0   # Autonomously generated ONU alarms rx overflow
+        self._rx_avc_overflow = 0     # Autonomously generated ONU AVC rx overflow
+        self._rx_onu_discards = 0     # Autonomously generated ONU unknown message types
+        self._rx_timeouts = 0
+        self._tx_errors = 0           # Exceptions during tx request
+        self._consecutive_errors = 0  # Rx & Tx errors in a row, a good RX resets this to 0
+        self._reply_min = sys.maxint  # Fastest successful tx -> rx
+        self._reply_max = 0           # Longest successful tx -> rx
+        self._reply_sum = 0.0         # Total seconds for successful tx->rx (float for average)
+
+        # If a list of custom ME Entities classes were provided, insert them into
+        # main class_id to entity map.
+        # TODO: If this class becomes hidden from the ONU DA, move this to the OMCI State Machine runner
+
+    def __str__(self):
+        return "OMCISupport: {}".format(self._device_id)
+
+    @property
+    def enabled(self):
+        return self._enabled
+
+    @enabled.setter
+    def enabled(self, value):
+        """
+        Enable/disable the OMCI Communications Channel
+
+        :param value: (boolean) True to enable, False to disable
+        """
+        assert isinstance(value, bool), 'enabled is a boolean'
+
+        if self._enabled != value:
+            self._enabled = value
+            if self._enabled:
+                self._start()
+            else:
+                self._stop()
+
+    @property
+    def tx_frames(self):
+        return self._tx_frames
+
+    @property
+    def rx_frames(self):
+        return self._rx_frames
+
+    @property
+    def rx_unknown_tid(self):
+        return self._rx_unknown_tid         # Tx TID not found
+
+    @property
+    def rx_onu_frames(self):
+        return self._rx_onu_frames
+
+    @property
+    def rx_alarm_overflow(self):
+        return self._rx_alarm_overflow      # Alarm ONU autonomous overflows
+
+    @property
+    def rx_avc_overflow(self):
+        return self._rx_avc_overflow        # Attribute Value change autonomous overflows
+
+    @property
+    def rx_onu_discards(self):
+        return self._rx_onu_discards        # Attribute Value change autonomous overflows
+
+    @property
+    def rx_timeouts(self):
+        return self._rx_timeouts
+
+    @property
+    def tx_errors(self):
+        return self._tx_errors
+
+    @property
+    def consecutive_errors(self):
+        return self._consecutive_errors
+
+    @property
+    def reply_min(self):
+        return int(round(self._reply_min * 1000.0))     # Milliseconds
+
+    @property
+    def reply_max(self):
+        return int(round(self._reply_max * 1000.0))     # Milliseconds
+
+    @property
+    def reply_average(self):
+        avg = self._reply_sum / self._rx_frames if self._rx_frames > 0 else 0.0
+        return int(round(avg * 1000.0))     # Milliseconds
+
+    @property
+    def get_alarm_message(self):
+        """
+        Attempt to retrieve and remove an ONU Alarm Message from the ONU
+        autonomous message queue.
+
+        TODO: We may want to deprecate this, see TODO comment around line 399 in
+              the _request_success() method below
+
+        :return: a Deferred which fires with the next Alarm Frame available in
+                 the queue.
+        """
+        return self._alarm_queue.get()
+
+    @property
+    def get_avc_message(self):
+        """
+        Attempt to retrieve and remove an ONU Attribute Value Change (AVC)
+        Message from the ONU autonomous message queue.
+
+        TODO: We may want to deprecate this, see TODO comment around line 399 in
+              the _request_success() method below
+
+        :return: a Deferred which fires with the next AVC Frame available in
+                 the queue.
+        """
+        return self._avc_queue.get()
+
+    @property
+    def get_test_results(self):
+        """
+        Attempt to retrieve and remove an ONU Test Results Message from the
+        ONU autonomous message queue.
+
+        TODO: We may want to deprecate this, see TODO comment around line 399 in
+              the _request_success() method below
+
+        :return: a Deferred which fires with the next Test Results Frame is
+                 available in the queue.
+        """
+        return self._test_results_queue.get()
+
+    def _start(self):
+        """
+        Start the OMCI Communications Channel
+        """
+        assert self._enabled, 'Start should only be called if enabled'
+        #
+        # TODO: Perform any other common startup tasks here
+        #
+        self.flush()
+
+        device = self._adapter_agent.get_device(self._device_id)
+        self._proxy_address = device.proxy_address
+
+    def _stop(self):
+        """
+        Stop the OMCI Communications Channel
+        """
+        assert not self._enabled, 'Stop should only be called if disabled'
+        #
+        # TODO: Perform common shutdown tasks here
+        #
+        self.flush()
+        self._proxy_address = None
+
+        # TODO: What is best way to clean up any outstanding futures for these queues
+        self._alarm_queue = None
+        self._avc_queue = None
+        self._test_results_queue = None
+
+    def _receive_onu_message(self, rx_frame):
+        """ Autonomously generated ONU frame Rx handler"""
+        # TODO: Best way to handle autonomously generated ONU frames may be pub/sub method
+        from twisted.internet.defer import QueueOverflow
+        self.log.debug('rx-onu-frame', frame_type=type(rx_frame),
+                       frame=hexify(str(rx_frame)))
+
+        # TODO: Signal, via defer if Alarm Overflow or just an event?
+        msg_type = rx_frame.fields['message_type']
+
+        self._rx_onu_frames += 1
+
+        if msg_type == EntityOperations.AlarmNotification.value:
+            try:
+                self._alarm_queue.put((rx_frame, arrow.utcnow().float_timestamp))
+
+            except QueueOverflow:
+                self._rx_alarm_overflow += 1
+                self.log.warn('onu-rx-alarm-overflow', cnt=self._rx_alarm_overflow)
+
+        elif msg_type == EntityOperations.AttributeValueChange.value:
+            try:
+                self._alarm_queue.put((rx_frame, arrow.utcnow().float_timestamp))
+
+            except QueueOverflow:
+                self._rx_avc_overflow += 1
+                self.log.warn('onu-rx-avc-overflow', cnt=self._rx_avc_overflow)
+        else:
+            # TODO: Need to add test results message support
+
+            self.log.warn('onu-unsupported-autonomous-message', type=msg_type)
+            self._rx_onu_discards += 1
+
+    def receive_message(self, msg):
+        """
+        Receive and OMCI message from the proxy channel to the OLT.
+
+        Call this from your ONU Adapter on a new OMCI Rx on the proxy channel
+        """
+        if self.enabled:
+            try:
+                now = arrow.utcnow()
+                d = None
+
+                # NOTE: Since we may need to do an independent ME map on a per-ONU basis
+                #       save the current value of the entity_id_to_class_map, then
+                #       replace it with our custom one before decode, and then finally
+                #       restore it later. Tried other ways but really made the code messy.
+
+                saved_me_map = omci_entities.entity_id_to_class_map
+                omci_entities.entity_id_to_class_map = self._me_map
+
+                try:
+                    rx_frame = OmciFrame(msg)
+                    rx_tid = rx_frame.fields['transaction_id']
+
+                    if rx_tid == 0:
+                        return self._receive_onu_message(rx_frame)
+
+                    self._rx_frames += 1
+                    self._consecutive_errors = 0
+
+                except KeyError as e:
+                    # Unknown, Unsupported, or vendor-specific ME. Key is the unknown classID
+                    # TODO: Can we create a temporary one to hold it so upload does not always fail on new ME's?
+                    self.log.exception('frame-decode-key-error', msg=hexlify(msg), e=e)
+                    return
+
+                except Exception as e:
+                    self.log.exception('frame-decode', msg=hexlify(msg), e=e)
+                    return
+
+                finally:
+                    omci_entities.entity_id_to_class_map = saved_me_map     # Always restore it.
+
+                try:
+                    (ts, d, _, _) = self._requests.pop(rx_tid)
+
+                    ts_diff = now - arrow.Arrow.utcfromtimestamp(ts)
+                    secs = ts_diff.total_seconds()
+                    self._reply_sum += secs
+
+                    if secs < self._reply_min:
+                        self._reply_min = secs
+
+                    if secs > self._reply_max:
+                        self._reply_max = secs
+
+                    # TODO: Could also validate response type based on request action
+
+                except KeyError as e:
+                    # Possible late Rx on a message that timed-out
+                    self._rx_unknown_tid += 1
+                    self.log.warn('tx-message-missing', rx_id=rx_tid, msg=hexlify(msg))
+                    return
+
+                except Exception as e:
+                    self.log.exception('frame-match', msg=hexlify(msg), e=e)
+                    if d is not None:
+                        return d.errback(failure.Failure(e))
+                    return
+
+                d.callback(rx_frame)
+
+            except Exception as e:
+                self.log.exception('rx-msg', e=e)
+
+    def flush(self, max_age=0):
+        limit = arrow.utcnow().float_timestamp - max_age
+        old = [tid for tid, (ts, _, _, _) in self._requests.iteritems()
+               if ts <= limit]
+
+        for tid in old:
+            (_, d, _, _) = self._requests.pop(tid)
+            if d is not None and not d.called:
+                d.cancel()
+
+        self._requests = dict()
+
+        if max_age == 0:
+            # Flush autonomous messages (Alarms & AVCs)
+            while self._alarm_queue.pending:
+                _ = yield self._alarm_queue.get()
+
+            while self._avc_queue.pending:
+                _ = yield self._avc_queue.get()
+
+    def _get_tx_tid(self):
+        """
+        Get the next Transaction ID for a tx.  Note TID=0 is reserved
+        for autonomously generated messages from an ONU
+
+        :return: (int) TID
+        """
+        tx_tid, self._tx_tid = self._tx_tid, self._tx_tid + 1
+        if self._tx_tid > MAX_OMCI_TX_ID:
+            self._tx_tid = 1
+
+        return tx_tid
+
+    def _request_failure(self, value, tx_tid):
+        """
+        Handle a transmit failure and/or Rx timeout
+
+        :param value: (Failure) Twisted failure
+        :param tx_tid: (int) Associated Tx TID
+        """
+        if tx_tid in self._requests:
+            (_, _, _, timeout) = self._requests.pop(tx_tid)
+        else:
+            timeout = 0
+
+        if isinstance(value, failure.Failure):
+            value.trap(CancelledError)
+            self._rx_timeouts += 1
+            self._consecutive_errors += 1
+            self.log.info('timeout', tx_id=tx_tid, timeout=timeout)
+            value = failure.Failure(TimeoutError(timeout, "Deferred"))
+
+        return value
+
+    def _request_success(self, rx_frame):
+        """
+        Handle transmit success (a matching Rx was received)
+
+        :param rx_frame: (OmciFrame) OMCI response frame with matching TID
+        :return: (OmciFrame) OMCI response frame with matching TID
+        """
+        #
+        # TODO: Here we could update the MIB database if we did a set/create/delete
+        #       or perhaps a verify if a GET.  Also could increment mib counter
+        #
+        # TODO: A better way to perform this in VOLTHA v1.3 would be to provide
+        #       a pub/sub capability for external users/tasks to monitor responses
+        #       that could optionally take a filter. This would allow a MIB-Sync
+        #       task to easily watch all AVC notifications as well as Set/Create/Delete
+        #       operations and keep them serialized.  It may also be a better/easier
+        #       way to handle things if we containerize OpenOMCI.
+        #
+        try:
+            if isinstance(rx_frame.omci_message, OmciGetResponse):
+                pass    # TODO: Implement MIB check or remove
+
+            elif isinstance(rx_frame.omci_message, OmciSetResponse):
+                pass    # TODO: Implement MIB update
+
+            elif isinstance(rx_frame.omci_message, OmciCreateResponse):
+                pass    # TODO: Implement MIB update
+
+            elif isinstance(rx_frame.omci_message, OmciDeleteResponse):
+                pass    # TODO: Implement MIB update
+
+        except Exception as e:
+            self.log.exception('omci-message', e=e)
+            raise
+
+        return rx_frame
+
+    def send(self, frame, timeout=DEFAULT_OMCI_TIMEOUT):
+        """
+        Send the OMCI Frame to the ONU via the proxy_channel
+
+        :param frame: (OMCIFrame) Message to send
+        :param timeout: (int) Rx Timeout. 0=Forever
+        :return: (deferred) A deferred that fires when the response frame is received
+                            or if an error/timeout occurs
+        """
+        self.flush(max_age=MAX_OMCI_REQUEST_AGE)
+
+        assert timeout <= MAX_OMCI_REQUEST_AGE, \
+            'Maximum timeout is {} seconds'.format(MAX_OMCI_REQUEST_AGE)
+        assert isinstance(frame, OmciFrame), \
+            "Invalid frame class '{}'".format(type(frame))
+
+        if not self.enabled or self._proxy_address is None:
+            # TODO custom exceptions throughout this code would be helpful
+            return fail(result=failure.Failure(Exception('OMCI is not enabled')))
+
+        try:
+            tx_tid = frame.fields['transaction_id']
+            if tx_tid is None:
+                tx_tid = self._get_tx_tid()
+                frame.fields['transaction_id'] = tx_tid
+
+            assert tx_tid not in self._requests, 'TX TID {} is already exists'.format(tx_tid)
+            assert tx_tid >= 0, 'Invalid Tx TID: {}'.format(tx_tid)
+
+            ts = arrow.utcnow().float_timestamp
+            d = defer.Deferred()
+
+            # NOTE: Since we may need to do an independent ME map on a per-ONU basis
+            #       save the current value of the entity_id_to_class_map, then
+            #       replace it with our custom one before decode, and then finally
+            #       restore it later. Tried other ways but really made the code messy.
+
+            saved_me_map = omci_entities.entity_id_to_class_map
+            omci_entities.entity_id_to_class_map = self._me_map
+            try:
+                self._adapter_agent.send_proxied_message(self._proxy_address,
+                                                         hexify(str(frame)))
+            finally:
+                omci_entities.entity_id_to_class_map = saved_me_map
+
+            self._tx_frames += 1
+            self._requests[tx_tid] = (ts, d, frame, timeout)
+
+            d.addCallbacks(self._request_success, self._request_failure,
+                           errbackArgs=(tx_tid,))
+
+            if timeout > 0:
+                d.addTimeout(timeout, reactor)
+
+        except Exception as e:
+            self._tx_errors += 1
+            self._consecutive_errors += 1
+            self.log.exception('send-omci', e=e)
+            return fail(result=failure.Failure(e))
+
+        return d
+
+    ###################################################################################
+    # MIB Action shortcuts
+
+    def send_mib_reset(self, timeout=DEFAULT_OMCI_TIMEOUT):
+        """
+        Perform a MIB Reset
+        """
+        self.log.debug('send-mib-reset')
+
+        frame = OntDataFrame().mib_reset()
+        return self.send(frame, timeout)
+
+    def send_mib_upload(self, timeout=DEFAULT_OMCI_TIMEOUT):
+        self.log.debug('send-mib-upload')
+
+        frame = OntDataFrame().mib_upload()
+        return self.send(frame, timeout)
+
+    def send_mib_upload_next(self, seq_no, timeout=DEFAULT_OMCI_TIMEOUT):
+        self.log.debug('send-mib-upload-next')
+
+        frame = OntDataFrame(seq_no).mib_upload_next()
+        return self.send(frame, timeout)
+
+    def send_reboot(self, timeout=DEFAULT_OMCI_TIMEOUT):
+        """
+        Send an ONU Device reboot request (ONU-G ME).
+        """
+        self.log.debug('send-mib-reboot')
+
+        frame = OntGFrame().reboot()
+        return self.send(frame, timeout)
diff --git a/voltha/extensions/omci/omci_defs.py b/voltha/extensions/omci/omci_defs.py
index 4acf97d..5bd6841 100644
--- a/voltha/extensions/omci/omci_defs.py
+++ b/voltha/extensions/omci/omci_defs.py
@@ -13,7 +13,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 #
-from enum import Enum
+from enum import Enum, IntEnum
 from scapy.fields import PadField
 from scapy.packet import Raw
 
@@ -102,3 +102,16 @@
     TestResult = 27
     GetCurrentData = 28
     SetTable = 29       # Defined in Extended Message Set Only
+
+
+class ReasonCodes(IntEnum):
+    # OMCI Result and reason codes
+    Success = 0,            # Command processed successfully
+    ProcessingError = 1,    # Command processing error
+    NotSupported = 2,       # Command not supported
+    ParameterError = 3,     # Parameter error
+    UnknownEntity = 4,      # Unknown managed entity
+    UnknownInstance = 5,    # Unknown managed entity instance
+    DeviceBusy = 6,         # Device busy
+    InstanceExists = 7,     # Instance Exists
+    AttributeFailure = 9,   # Attribute(s) failed or unknown
diff --git a/voltha/extensions/omci/omci_me.py b/voltha/extensions/omci/omci_me.py
new file mode 100644
index 0000000..427c1e7
--- /dev/null
+++ b/voltha/extensions/omci/omci_me.py
@@ -0,0 +1,664 @@
+#
+# Copyright 2017 the original author or authors.
+#
+# 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.
+#
+"""
+OMCI Managed Entity Frame support
+"""
+from voltha.extensions.omci.omci import *
+from voltha.extensions.omci.me_frame import MEFrame
+
+
+class CardholderFrame(MEFrame):
+    """
+    This managed entity represents fixed equipment slot configuration
+    for the ONU
+    """
+    def __init__(self, single, slot_number, attributes):
+        """
+        :param single:(bool) True if the ONU is a single piece of integrated equipment,
+                             False if the ONU contains pluggable equipment modules
+        :param slot_number: (int) slot number (0..254)
+
+        :param attributes: (basestring, list, set, dict) attributes. For gets
+                           a string, list, or set can be provided. For create/set
+                           operations, a dictionary should be provided, for
+                           deletes None may be specified.
+        """
+        # Validate
+        MEFrame.check_type(single, bool)
+        MEFrame.check_type(slot_number, int)
+        if not 0 <= slot_number <= 254:
+            raise ValueError('slot_number should be 0..254')
+
+        entity_id = 256 + slot_number if single else slot_number
+
+        super(CardholderFrame, self).__init__(Cardholder, entity_id,
+                                              MEFrame._attr_to_data(attributes))
+
+
+class CircuitPackFrame(MEFrame):
+    """
+    This managed entity models a real or virtual circuit pack that is equipped in
+    a real or virtual ONU slot.
+    """
+    def __init__(self, entity_id, attributes):
+        """
+        :param entity_id: (int) This attribute uniquely identifies each instance of
+                                this managed entity. Its value is the same as that
+                                of the cardholder managed entity containing this
+                                circuit pack instance. (0..65535)
+
+        :param attributes: (basestring, list, set, dict) attributes. For gets
+                           a string, list, or set can be provided. For create/set
+                           operations, a dictionary should be provided, for
+                           deletes None may be specified.
+        """
+        super(CircuitPackFrame, self).__init__(CircuitPack, entity_id,
+                                               MEFrame._attr_to_data(attributes))
+
+
+class ExtendedVlanTaggingOperationConfigurationDataFrame(MEFrame):
+    """
+    This managed entity organizes data associated with VLAN tagging. Regardless
+    of its point of attachment, the specified tagging operations refer to the
+     upstream direction.
+    """
+    def __init__(self, entity_id, attributes):
+        """
+        :param entity_id: (int) This attribute uniquely identifies each instance of
+                                this managed entity. Its value is the same as that
+                                of the cardholder managed entity containing this
+                                circuit pack instance. (0..65535)
+
+        :param attributes: (basestring, list, set, dict) attributes. For gets
+                           a string, list, or set can be provided. For create/set
+                           operations, a dictionary should be provided, for
+                           deletes None may be specified.
+        """
+        super(ExtendedVlanTaggingOperationConfigurationDataFrame,
+              self).__init__(ExtendedVlanTaggingOperationConfigurationData,
+                             entity_id,
+                             MEFrame._attr_to_data(attributes))
+
+
+class IpHostConfigDataFrame(MEFrame):
+    """
+    The IP host config data configures IPv4 based services offered on the ONU.
+    """
+    def __init__(self, entity_id, attributes):
+        """
+        :param entity_id: (int) This attribute uniquely identifies each instance of
+                                this managed entity. (0..65535)
+
+        :param attributes: (basestring, list, set, dict) attributes. For gets
+                           a string, list, or set can be provided. For create/set
+                           operations, a dictionary should be provided, for
+                           deletes None may be specified.
+        """
+        super(IpHostConfigDataFrame, self).__init__(IpHostConfigData,
+                                                    entity_id,
+                                                    MEFrame._attr_to_data(attributes))
+
+
+class GalEthernetProfileFrame(MEFrame):
+    """
+    This managed entity organizes data that describe the GTC adaptation layer
+    processing functions of the ONU for Ethernet services.
+    """
+    def __init__(self, entity_id, max_gem_payload_size=None):
+        """
+        :param entity_id: (int) This attribute uniquely identifies each instance of
+                                this managed entity. (0..65535)
+
+        :param max_gem_payload_size: (int) This attribute defines the maximum payload
+                                     size generated in the associated GEM interworking
+                                     termination point managed entity. (0..65535
+        """
+        MEFrame.check_type(max_gem_payload_size, (int, type(None)))
+        if max_gem_payload_size is not None and not 0 <= max_gem_payload_size <= 0xFFFF:  # TODO: verify min/max
+            raise ValueError('max_gem_payload_size should be 0..0xFFFF')
+
+        data = None if max_gem_payload_size is None else\
+            {
+                'max_gem_payload_size': max_gem_payload_size
+            }
+        super(GalEthernetProfileFrame, self).__init__(GalEthernetProfile,
+                                                      entity_id,
+                                                      data)
+
+
+class GemInterworkingTpFrame(MEFrame):
+    """
+    An instance of this managed entity represents a point in the ONU where the
+    interworking of a bearer service (usually Ethernet) to the GEM layer takes
+    place.
+    """
+    def __init__(self, entity_id,
+                 gem_port_network_ctp_pointer=None,
+                 interworking_option=None,
+                 service_profile_pointer=None,
+                 interworking_tp_pointer=None,
+                 pptp_counter=None,
+                 gal_profile_pointer=None,
+                 attributes=None):
+        """
+        :param entity_id: (int) This attribute uniquely identifies each instance of
+                                this managed entity. (0..65535)
+
+        :param gem_port_network_ctp_pointer: (int) This attribute points to an instance of
+                                                 the GEM port network CTP. (0..65535)
+
+        :param interworking_option: (int) This attribute identifies the type
+                                of non-GEM function that is being interworked.
+                                The options are:
+                                    0 Circuit-emulated TDM
+                                    1 MAC bridged LAN
+                                    2 Reserved
+                                    3 Reserved
+                                    4 Video return path
+                                    5 IEEE 802.1p mapper
+                                    6 Downstream broadcast
+                                    7 MPLS PW TDM service
+
+        :param service_profile_pointer: (int) This attribute points to an instance of
+                                              a service profile.
+                            CES service profile                 if interworking option = 0
+                            MAC bridge service profile          if interworking option = 1
+                            Video return path service profile   if interworking option = 4
+                            IEEE 802.1p mapper service profile  if interworking option = 5
+                            Null pointer                        if interworking option = 6
+                            CES service profile                 if interworking option = 7
+
+        :param interworking_tp_pointer: (int) This attribute is used for the circuit
+                                              emulation service and IEEE 802.1p mapper
+                                              service without a MAC bridge.
+
+        :param gal_profile_pointer: (int) This attribute points to an instance of
+                                              a service profile.
+
+        :param attributes: (basestring, list, set, dict) additional ME attributes.
+                           not specifically specified as a parameter. For gets
+                           a string, list, or set can be provided. For create/set
+                           operations, a dictionary should be provided, for
+                           deletes None may be specified..
+        """
+        # Validate
+        self.check_type(gem_port_network_ctp_pointer, (int, type(None)))
+        self.check_type(interworking_option, (int, type(None)))
+        self.check_type(service_profile_pointer, (int, type(None)))
+        self.check_type(interworking_tp_pointer,(int, type(None)))
+        self.check_type(pptp_counter,(int, type(None)))
+        self.check_type(gal_profile_pointer, (int, type(None)))
+
+        if gem_port_network_ctp_pointer is not None and not 0 <= gem_port_network_ctp_pointer <= 0xFFFE:  # TODO: Verify max
+            raise ValueError('gem_port_network_ctp_pointer should be 0..0xFFFE')
+
+        if interworking_option is not None and not 0 <= interworking_option <= 7:
+            raise ValueError('interworking_option should be 0..7')
+
+        if service_profile_pointer is not None and not 0 <= service_profile_pointer <= 0xFFFE:  # TODO: Verify max
+            raise ValueError('service_profile_pointer should be 0..0xFFFE')
+
+        if interworking_tp_pointer is not None and not 0 <= interworking_tp_pointer <= 0xFFFE:  # TODO: Verify max
+            raise ValueError('interworking_tp_pointer should be 0..0xFFFE')
+
+        if pptp_counter is not None and not 0 <= pptp_counter <= 255:  # TODO: Verify max
+            raise ValueError('pptp_counter should be 0..255')
+
+        if gal_profile_pointer is not None and not 0 <= gal_profile_pointer <= 0xFFFE:  # TODO: Verify max
+            raise ValueError('gal_profile_pointer should be 0..0xFFFE')
+
+        data = MEFrame._attr_to_data(attributes)
+
+        if gem_port_network_ctp_pointer is not None or \
+                interworking_option is not None or \
+                service_profile_pointer is not None or \
+                interworking_tp_pointer is not None or \
+                gal_profile_pointer is not None:
+
+            data = data or dict()
+
+            if gem_port_network_ctp_pointer is not None:
+                data['gem_port_network_ctp_pointer'] = gem_port_network_ctp_pointer
+
+            if interworking_option is not None:
+                data['interworking_option'] = interworking_option
+
+            if service_profile_pointer is not None:
+                data['service_profile_pointer'] = service_profile_pointer
+
+            if interworking_tp_pointer is not None:
+                data['interworking_tp_pointer'] = interworking_tp_pointer
+
+            if gal_profile_pointer is not None:
+                data['gal_profile_pointer'] = gal_profile_pointer
+
+        super(GemInterworkingTpFrame, self).__init__(GemInterworkingTp,
+                                                     entity_id,
+                                                     data)
+
+
+class GemPortNetworkCtpFrame(MEFrame):
+    """
+    This managed entity represents the termination of a GEM port on an ONU.
+    """
+    def __init__(self, entity_id, port_id=None, tcont_id=None,
+                 direction=None, upstream_tm=None, attributes=None):
+        """
+        :param entity_id: (int) This attribute uniquely identifies each instance of
+                                this managed entity. (0..65535)
+
+        :param port_id: (int) This attribute is the port-ID of the GEM port associated
+                              with this CTP
+
+        :param tcont_id: (int) This attribute points to a T-CONT instance
+
+        :param direction: (string) Data direction.  Valid values are:
+                                   'upstream'       - UNI-to-ANI
+                                   'downstream'     - ANI-to-UNI
+                                   'bi-directional' - guess :-)
+
+        :param upstream_tm: (int) If the traffic management option attribute in
+                                  the ONU-G ME is 0 (priority controlled) or 2
+                                  (priority and rate controlled), this pointer
+                                  specifies the priority queue ME serving this GEM
+                                  port network CTP. If the traffic management
+                                  option attribute is 1 (rate controlled), this
+                                  attribute redundantly points to the T-CONT serving
+                                  this GEM port network CTP.
+
+        :param attributes: (basestring, list, set, dict) additional ME attributes.
+                           not specifically specified as a parameter. For gets
+                           a string, list, or set can be provided. For create/set
+                           operations, a dictionary should be provided, for
+                           deletes None may be specified.
+        """
+        _directions = {"upstream": 1, "downstream": 2, "bi-directional": 3}
+
+        # Validate
+        self.check_type(port_id, (int, type(None)))
+        self.check_type(tcont_id, (int, type(None)))
+        self.check_type(direction, (basestring, type(None)))
+        self.check_type(upstream_tm, (int, type(None)))
+
+        if port_id is not None and not 0 <= port_id <= 0xFFFE:  # TODO: Verify max
+            raise ValueError('port_id should be 0..0xFFFE')
+
+        if tcont_id is not None and not 0 <= tcont_id <= 0xFFFE:  # TODO: Verify max
+            raise ValueError('tcont_id should be 0..0xFFFE')
+
+        if direction is not None and str(direction).lower() not in _directions:
+            raise ValueError('direction should one of {}'.format(_directions.keys()))
+
+        if upstream_tm is not None and not 0 <= upstream_tm <= 0xFFFE:  # TODO: Verify max
+            raise ValueError('upstream_tm should be 0..0xFFFE')
+
+        data = MEFrame._attr_to_data(attributes)
+
+        if port_id is not None or tcont_id is not None or\
+                direction is not None or upstream_tm is not None:
+
+            data = data or dict()
+
+            if port_id is not None:
+                data['port_id'] = port_id
+            if tcont_id is not None:
+                data['tcont_pointer'] = tcont_id
+            if direction is not None:
+                data['direction'] = _directions[str(direction).lower()]
+            if upstream_tm is not None:
+                data['traffic_management_pointer_upstream'] = upstream_tm
+
+        super(GemPortNetworkCtpFrame, self).__init__(GemPortNetworkCtp,
+                                                     entity_id,
+                                                     data)
+
+
+class Ieee8021pMapperServiceProfileFrame(MEFrame):
+    """
+    This managed entity associates the priorities of IEEE 802.1p [IEEE
+    802.1D] priority tagged frames with specific connections.
+    """
+    def __init__(self, entity_id, tp_pointer=None, interwork_tp_pointers=None):
+        """
+        :param entity_id: (int) This attribute uniquely identifies each instance of
+                                this managed entity. (0..65535)
+
+        :param tp_pointer: (int) This attribute points to an instance of the
+                                 associated termination point. (0..65535)
+
+        :param interwork_tp_pointers: (list) List of 1 to 8 interworking termination
+                                   point IDs. The first entry is assigned
+                                   got p-bit priority 0. If less than 8 IDs
+                                   are provided, the last ID is used for
+                                   the remaining items.
+        """
+        if tp_pointer is None and interwork_tp_pointers is None:
+            data = dict(
+                    tp_pointer=OmciNullPointer,
+                    interwork_tp_pointer_for_p_bit_priority_0=OmciNullPointer,
+                    interwork_tp_pointer_for_p_bit_priority_1=OmciNullPointer,
+                    interwork_tp_pointer_for_p_bit_priority_2=OmciNullPointer,
+                    interwork_tp_pointer_for_p_bit_priority_3=OmciNullPointer,
+                    interwork_tp_pointer_for_p_bit_priority_4=OmciNullPointer,
+                    interwork_tp_pointer_for_p_bit_priority_5=OmciNullPointer,
+                    interwork_tp_pointer_for_p_bit_priority_6=OmciNullPointer,
+                    interwork_tp_pointer_for_p_bit_priority_7=OmciNullPointer
+                )
+        else:
+            self.check_type(tp_pointer, (list, type(None)))
+            self.check_type(interwork_tp_pointers, (list, type(None)))
+
+            data = dict()
+
+            if tp_pointer is not None:
+                data['tp_pointer'] = tp_pointer
+
+            if interwork_tp_pointers is not None:
+                assert all(isinstance(tp, int) and 0 <= tp <= 0xFFFF
+                           for tp in interwork_tp_pointers),\
+                    'Interworking TP IDs must be 0..0xFFFF'
+                assert 1 <= len(interwork_tp_pointers) <= 8, \
+                    'Invalid number of Interworking TP IDs. Must be 1..8'
+
+                data = dict()
+                for pbit in range(0, len(interwork_tp_pointers)):
+                    data['interwork_tp_pointer_for_p_bit_priority_{}'.format(pbit)] = \
+                        interwork_tp_pointers[pbit]
+
+                for pbit in range(len(interwork_tp_pointers), 8):
+                    data['interwork_tp_pointer_for_p_bit_priority_{}'.format(pbit)] = \
+                        interwork_tp_pointers[len(interwork_tp_pointers) - 1]
+
+        super(Ieee8021pMapperServiceProfileFrame, self).__init__(Ieee8021pMapperServiceProfile,
+                                                                 entity_id,
+                                                                 data)
+
+
+class MacBridgePortConfigurationDataFrame(MEFrame):
+    """
+    This managed entity represents the ONU as equipment.
+    """
+    def __init__(self, entity_id, bridge_id_pointer=None, port_num=None,
+                 tp_type=None, tp_pointer=None, attributes=None):
+        """
+        :param entity_id: (int) This attribute uniquely identifies each instance of
+                                this managed entity. (0..65535)
+
+        :param bridge_id_pointer: (int) This attribute points to an instance of the
+                                        MAC bridge service profile. (0..65535)
+
+        :param port_num: (int) This attribute is the bridge port number. (0..255)
+
+        :param tp_type: (int) This attribute identifies the type of termination point
+                              associated with this MAC bridge port. Valid values are:
+                        1  Physical path termination point Ethernet UNI
+                        2  Interworking VCC termination point
+                        3  IEEE 802.1p mapper service profile
+                        4  IP host config data or IPv6 host config data
+                        5  GEM interworking termination point
+                        6  Multicast GEM interworking termination point
+                        7  Physical path termination point xDSL UNI part 1
+                        8  Physical path termination point VDSL UNI
+                        9  Ethernet flow termination point
+                        10 Reserved
+                        11 Virtual Ethernet interface point
+                        12 Physical path termination point MoCA UNI
+
+        :param tp_pointer: (int) This attribute points to the termination point
+                                 associated with this MAC bridge por. (0..65535)
+
+        :param attributes: (basestring, list, set, dict) additional ME attributes.
+                           not specifically specified as a parameter. For gets
+                           a string, list, or set can be provided. For create/set
+                           operations, a dictionary should be provided, for
+                           deletes None may be specified.
+        """
+        # Validate
+        self.check_type(bridge_id_pointer, (int, type(None)))
+        self.check_type(port_num, (int, type(None)))
+        self.check_type(tp_type, (int, type(None)))
+        self.check_type(tp_pointer, (int, type(None)))
+
+        if bridge_id_pointer is not None and not 0 <= bridge_id_pointer <= 0xFFFE:  # TODO: Verify max
+            raise ValueError('bridge_id_pointer should be 0..0xFFFE')
+
+        if port_num is not None and not 0 <= port_num <= 255:
+            raise ValueError('port_num should be 0..255')       # TODO: Verify min,max
+
+        if tp_type is not None and not 1 <= tp_type <= 12:
+            raise ValueError('service_profile_pointer should be 1..12')
+
+        if tp_pointer is not None and not 0 <= tp_pointer <= 0xFFFE:  # TODO: Verify max
+            raise ValueError('interworking_tp_pointer should be 0..0xFFFE')
+
+        data = MEFrame._attr_to_data(attributes)
+
+        if bridge_id_pointer is not None or \
+                port_num is not None or \
+                tp_type is not None or \
+                tp_pointer is not None:
+
+            data = data or dict()
+
+            if bridge_id_pointer is not None:
+                data['bridge_id_pointer'] = bridge_id_pointer
+
+            if port_num is not None:
+                data['port_num'] = port_num
+
+            if tp_type is not None:
+                data['tp_type'] = tp_type
+
+            if tp_pointer is not None:
+                data['tp_pointer'] = tp_pointer
+
+        super(MacBridgePortConfigurationDataFrame, self).\
+            __init__(MacBridgePortConfigurationData, entity_id, data)
+
+
+class MacBridgeServiceProfileFrame(MEFrame):
+    """
+    This managed entity models a MAC bridge in its entirety; any number
+    of ports may be associated with the bridge through pointers to the
+    MAC bridge service profile managed entity.
+    """
+    def __init__(self, entity_id, attributes=None):
+        """
+        :param entity_id: (int) This attribute uniquely identifies each instance of
+                                this managed entity. (0..65535)
+
+        :param attributes: (basestring, list, set, dict) attributes. For gets
+                           a string, list, or set can be provided. For create/set
+                           operations, a dictionary should be provided, for
+                           deletes None may be specified.
+        """
+        super(MacBridgeServiceProfileFrame, self).__init__(MacBridgeServiceProfile,
+                                                           entity_id,
+                                                           MEFrame._attr_to_data(attributes))
+
+
+class OntGFrame(MEFrame):
+    """
+    This managed entity represents the ONU as equipment.
+    """
+    def __init__(self, attributes=None):
+        """
+        :param attributes: (basestring, list, set, dict) attributes. For gets
+                           a string, list, or set can be provided. For create/set
+                           operations, a dictionary should be provided, for
+                           deletes None may be specified.
+        """
+        super(OntGFrame, self).__init__(OntG, 0,
+                                        MEFrame._attr_to_data(attributes))
+
+
+class Ont2GFrame(MEFrame):
+    """
+    This managed entity contains additional attributes associated with a PON ONU.
+    """
+    def __init__(self, attributes=None):
+        """
+        :param attributes: (basestring, list, set, dict) attributes. For gets
+                           a string, list, or set can be provided. For create/set
+                           operations, a dictionary should be provided, for
+                           deletes None may be specified.
+        """
+        # Only one managed entity instance (Entity ID=0)
+        super(Ont2GFrame, self).__init__(Ont2G, 0,
+                                         MEFrame._attr_to_data(attributes))
+
+
+class PptpEthernetUniFrame(MEFrame):
+    """
+    This managed entity represents the point at an Ethernet UNI where the physical path
+    terminates and Ethernet physical level functions are performed.
+    """
+    def __init__(self, entity_id, attributes=None):
+        """
+        :param entity_id: (int) This attribute uniquely identifies each instance of
+                                this managed entity. (0..65535)
+
+        :param attributes: (basestring, list, set, dict) attributes. For gets
+                           a string, list, or set can be provided. For create/set
+                           operations, a dictionary should be provided, for
+                           deletes None may be specified.
+        """
+        super(PptpEthernetUniFrame, self).__init__(PptpEthernetUni, entity_id,
+                                                   MEFrame._attr_to_data(attributes))
+
+
+class SoftwareImageFrame(MEFrame):
+    """
+    This managed entity models an executable software image stored in the ONU.
+    """
+    def __init__(self, entity_id):
+        """
+        :param entity_id: (int) This attribute uniquely identifies each instance of
+                                this managed entity. (0..65535)
+        """
+        super(SoftwareImageFrame, self).__init__(SoftwareImage, entity_id, None)
+
+
+class TcontFrame(MEFrame):
+    """
+    An instance of the traffic container managed entity T-CONT represents a
+    logical connection group associated with a G-PON PLOAM layer alloc-ID.
+    """
+    def __init__(self, entity_id, alloc_id=None, policy=None):
+        """
+        :param entity_id: (int) This attribute uniquely identifies each instance of
+                                this managed entity. (0..65535)
+
+        :param alloc_id: (int) This attribute links the T-CONT with the alloc-ID
+                               assigned by the OLT in the assign_alloc-ID PLOAM
+                               message (0..0xFFF)
+
+        :param policy: (int) This attribute indicates the T-CONT's traffic scheduling
+                             policy. Valid values:
+                                0 - Null
+                                1 - Strict priority
+                                2 - WRR - Weighted round robin
+        """
+        # Validate
+        self.check_type(alloc_id, (int, type(None)))
+        self.check_type(policy, (int, type(None)))
+
+        if alloc_id is not None and not 0 <= alloc_id <= 0xFFF:
+            raise ValueError('alloc_id should be 0..0xFFF')
+
+        if policy is not None and not 0 <= policy <= 2:
+            raise ValueError('policy should be 0..2')
+
+        if alloc_id is None and policy is None:
+            data = None
+        else:
+            data = dict()
+
+            if alloc_id is not None:
+                data['alloc_id'] = alloc_id
+
+            if policy is not None:
+                data['policy'] = policy
+
+        super(TcontFrame, self).__init__(Tcont, entity_id, data)
+
+
+class VlanTaggingFilterDataFrame(MEFrame):
+    """
+    An instance of this managed entity represents a point in the ONU where the
+    interworking of a bearer service (usually Ethernet) to the GEM layer takes
+    place.
+    """
+    def __init__(self, entity_id, vlan_tcis=None, forward_operation=None):
+        """
+        :param entity_id: (int) This attribute uniquely identifies each instance of
+                                this managed entity. (0..65535)
+
+        :param vlan_tcis: (list) This attribute is a list of provisioned TCI values
+                                 for the bridge port. (0..0xFFFF)
+
+        :param forward_operation: (int) What to do.  See ITU spec for more information
+
+        """
+        # Validate
+        self.check_type(vlan_tcis, (list, type(None)))
+        self.check_type(forward_operation, (int, type(None)))
+
+        if forward_operation is not None and not 0 <= forward_operation <= 0x21:
+            raise ValueError('forward_operation should be 0..0x21')
+
+        if vlan_tcis is None and forward_operation is None:
+            data = None
+
+        else:
+            data = dict()
+
+            if vlan_tcis is not None:
+                assert all(isinstance(tci, int) and 0 <= tci <= 0xFFFF
+                           for tci in vlan_tcis), "VLAN TCI's are 0..0xFFFF"
+                assert 1 <= len(vlan_tcis) <= 12, 'Number of VLAN TCI values is 1..12'
+
+                for index in range(0, len(vlan_tcis)):
+                    data['vlan_filter_{}'.format(index)] = vlan_tcis[index]
+
+                data['number_of_entries'] = len(vlan_tcis)
+
+            if forward_operation is not None:
+                assert 0 <= forward_operation <= 0x21, 'forwarding_operation must be 0x00..0x21'
+                data['forward_operation'] = forward_operation
+
+        super(VlanTaggingFilterDataFrame, self).__init__(VlanTaggingFilterData,
+                                                         entity_id,
+                                                         data)
+
+
+class OntDataFrame(MEFrame):
+    """
+    This managed entity models the MIB itself
+    """
+    def __init__(self, mib_data_sync=None):
+        """
+        :param mib_data_sync: (int) This attribute is used to check the alignment
+                                    of the MIB of the ONU with the corresponding MIB
+                                    in the OLT. (0..255)
+        """
+        self.check_type(mib_data_sync, (int, type(None)))
+        if mib_data_sync is not None and not 0 <= mib_data_sync <= 255:
+            raise ValueError('mib_data_sync should be 0..255')
+
+        data = {'mib_data_sync': mib_data_sync} if mib_data_sync is not None else None
+
+        super(OntDataFrame, self).__init__(OntData, 0, data)
diff --git a/voltha/extensions/omci/omci_messages.py b/voltha/extensions/omci/omci_messages.py
index 65e291a..2dee1e3 100644
--- a/voltha/extensions/omci/omci_messages.py
+++ b/voltha/extensions/omci/omci_messages.py
@@ -14,13 +14,12 @@
 # limitations under the License.
 #
 import structlog
-from enum import Enum
 from scapy.fields import ByteField, StrFixedLenField, ConditionalField, Field
 from scapy.fields import ShortField, BitField
 from scapy.packet import Packet
 
 from voltha.extensions.omci.omci_defs import AttributeAccess
-from voltha.extensions.omci.omci_entities import entity_id_to_class_map
+import voltha.extensions.omci.omci_entities as omci_entities
 
 
 log = structlog.get_logger()
@@ -36,27 +35,27 @@
 
     def addfield(self, pkt, s, val):
         class_id = getattr(pkt, self._entity_class)
-        entity_class = entity_id_to_class_map.get(class_id)
+        entity_class = omci_entities.entity_id_to_class_map.get(class_id)
         for attribute in entity_class.attributes:
-            if AttributeAccess.SetByCreate not in attribute._access:
+            if AttributeAccess.SetByCreate not in attribute.access:
                 continue
-            if attribute._fld.name == 'managed_entity_id':
+            if attribute.field.name == 'managed_entity_id':
                 continue
-            fld = attribute._fld
+            fld = attribute.field
             s = fld.addfield(pkt, s, val.get(fld.name, fld.default))
         return s
 
     def getfield(self, pkt, s):
         """Extract an internal value from a string"""
         class_id = getattr(pkt, self._entity_class)
-        entity_class = entity_id_to_class_map.get(class_id)
+        entity_class = omci_entities.entity_id_to_class_map.get(class_id)
         data = {}
         for attribute in entity_class.attributes:
-            if AttributeAccess.SetByCreate not in attribute._access:
+            if AttributeAccess.SetByCreate not in attribute.access:
                 continue
-            if attribute._fld.name == 'managed_entity_id':
+            if attribute.field.name == 'managed_entity_id':
                 continue
-            fld = attribute._fld
+            fld = attribute.field
             s, value = fld.getfield(pkt, s)
             data[fld.name] = value
         return s, data
@@ -75,10 +74,10 @@
     def addfield(self, pkt, s, val):
         class_id = getattr(pkt, self._entity_class)
         attribute_mask = getattr(pkt, self._attributes_mask)
-        entity_class = entity_id_to_class_map.get(class_id)
+        entity_class = omci_entities.entity_id_to_class_map.get(class_id)
         indices = entity_class.attribute_indices_from_mask(attribute_mask)
         for index in indices:
-            fld = entity_class.attributes[index]._fld
+            fld = entity_class.attributes[index].field
             s = fld.addfield(pkt, s, val[fld.name])
         return s
 
@@ -86,22 +85,22 @@
         """Extract an internal value from a string"""
         class_id = getattr(pkt, self._entity_class)
         attribute_mask = getattr(pkt, self._attributes_mask)
-        entity_class = entity_id_to_class_map[class_id]
+        entity_class = omci_entities.entity_id_to_class_map[class_id]
         indices = entity_class.attribute_indices_from_mask(attribute_mask)
         data = {}
         for index in indices:
             try:
-                fld = entity_class.attributes[index]._fld
+                fld = entity_class.attributes[index].field
             except IndexError, e:
-                log.error("Cannot decode attribute {} for entity class {}".format(
-                        index, entity_class))
+                log.error("attribute-decode-failure", attribute_index=index,
+                          entity_class=entity_class, e=e)
                 continue
             try:
                 s, value = fld.getfield(pkt, s)
             except Exception, e:
                 raise
             data[fld.name] = value
-        return  s, data
+        return s, data
 
 
 class OmciMessage(Packet):