VOL-1397: Adtran-OLT - Initial containerization commit
 - Need to move VERSION to base directory

Change-Id: I9d62d0607a011ce642e379fd92b35ec48b300070
diff --git a/adapters/adtran_olt/codec/__init__.py b/adapters/adtran_olt/codec/__init__.py
new file mode 100644
index 0000000..b0fb0b2
--- /dev/null
+++ b/adapters/adtran_olt/codec/__init__.py
@@ -0,0 +1,13 @@
+# 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.
diff --git a/adapters/adtran_olt/codec/ietf_interfaces.py b/adapters/adtran_olt/codec/ietf_interfaces.py
new file mode 100644
index 0000000..0bdf691
--- /dev/null
+++ b/adapters/adtran_olt/codec/ietf_interfaces.py
@@ -0,0 +1,328 @@
+# CCopyright 2017-present Adtran, Inc.
+#
+# 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 twisted.internet.defer import inlineCallbacks, returnValue
+import xmltodict
+import structlog
+from pyvoltha.protos.openflow_13_pb2 import OFPPF_1GB_FD, OFPPF_10GB_FD, OFPPF_40GB_FD, OFPPF_100GB_FD
+from pyvoltha.protos.openflow_13_pb2 import OFPPF_FIBER, OFPPF_COPPER
+from pyvoltha.protos.openflow_13_pb2 import OFPPS_LIVE, OFPPC_PORT_DOWN, OFPPS_LINK_DOWN, OFPPF_OTHER
+from pyvoltha.protos.common_pb2 import OperStatus, AdminState
+
+log = structlog.get_logger()
+
+_ietf_interfaces_config_rpc = """
+    <filter xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
+      <interfaces xmlns="urn:ietf:params:xml:ns:yang:ietf-interfaces">
+        <interface/>
+      </interfaces>
+    </filter>
+"""
+
+_ietf_interfaces_state_rpc = """
+    <filter xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
+      <interfaces-state xmlns="urn:ietf:params:xml:ns:yang:ietf-interfaces">
+        <interface>
+          <name/>
+          <type/>
+          <admin-status/>
+          <oper-status/>
+          <last-change/>
+          <phys-address/>
+          <speed/>
+        </interface>
+      </interfaces-state>
+    </filter>
+"""
+
+_allowed_with_default_types = ['report-all', 'report-all-tagged', 'trim', 'explicit']
+
+# TODO: Centralize the item below as a function in a core util module
+
+
+def _with_defaults(default_type=None):
+    if default_type is None:
+        return ""
+
+    assert(default_type in _allowed_with_default_types)
+    return """
+    <with-defaults xmlns="urn:ietf:params:xml:ns:yang:ietf-netconf-with-defaults">
+        {}</with-defaults>""".format(default_type)
+
+
+class IetfInterfacesConfig(object):
+    def __init__(self, session):
+        self._session = session
+
+    @inlineCallbacks
+    def get_config(self, source='running', with_defaults=None):
+
+        filter = _ietf_interfaces_config_rpc + _with_defaults(with_defaults)
+
+        request = self._session.get(source, filter=filter)
+        rpc_reply = yield request
+        returnValue(rpc_reply)
+
+    def get_interfaces(self, rpc_reply, interface_type=None):
+        """
+        Get the physical entities of a particular type
+        :param rpc_reply: Reply from previous get or request
+        :param interface_type: (String or List) The type of interface (case-insensitive)
+        :return: list) of OrderDict interface entries
+        """
+        result_dict = xmltodict.parse(rpc_reply.data_xml)
+
+        entries = result_dict['data']['interfaces']
+
+        if interface_type is None:
+            return entries
+
+        # for entry in entries:
+        #     import pprint
+        #     log.info(pprint.PrettyPrinter(indent=2).pformat(entry))
+
+        def _matches(entry, value):
+            if 'type' in entry and '#text' in entry['type']:
+                text_val = entry['type']['#text'].lower()
+                if isinstance(value, list):
+                    return any(v.lower() in text_val for v in value)
+                return value.lower() in text_val
+            return False
+
+        return [entry for entry in entries if _matches(entry, interface_type)]
+
+
+class IetfInterfacesState(object):
+    def __init__(self, session):
+        self._session = session
+
+    @inlineCallbacks
+    def get_state(self):
+        try:
+            request = self._session.get(_ietf_interfaces_state_rpc)
+            rpc_reply = yield request
+            returnValue(rpc_reply)
+
+        except Exception as e:
+            log.exception('get_state', e=e)
+            raise
+
+    @staticmethod
+    def get_interfaces(self, rpc_reply, key='type', key_value=None):
+        """
+        Get the physical entities of a particular type
+        :param key_value: (String or List) The type of interface (case-insensitive)
+        :return: list) of OrderDict interface entries
+        """
+        result_dict = xmltodict.parse(rpc_reply.data_xml)
+        entries = result_dict['data']['interfaces-state']['interface']
+
+        if key_value is None:
+            return entries
+
+        for entry in entries:
+            import pprint
+            log.info(pprint.PrettyPrinter(indent=2).pformat(entry))
+
+        def _matches(entry, key, value):
+            if key in entry and '#text' in entry[key]:
+                text_val = entry[key]['#text'].lower()
+                if isinstance(value, list):
+                    return any(v.lower() in text_val for v in value)
+                return value.lower() in text_val
+            return False
+
+        return [entry for entry in entries if _matches(entry, key, key_value)]
+
+    @staticmethod
+    def _get_admin_state(entry):
+        state_map = {
+            'up': AdminState.ENABLED,
+            'down': AdminState.DISABLED,
+            'testing': AdminState.DISABLED
+        }
+        return state_map.get(entry.get('admin-status', 'down'),
+                             AdminState.UNKNOWN)
+
+    @staticmethod
+    def _get_oper_status(entry):
+        state_map = {
+            'up': OperStatus.ACTIVE,
+            'down': OperStatus.FAILED,
+            'testing': OperStatus.TESTING,
+            'unknown': OperStatus.UNKNOWN,
+            'dormant': OperStatus.DISCOVERED,
+            'not-present': OperStatus.UNKNOWN,
+            'lower-layer-down': OperStatus.FAILED
+        }
+        return state_map.get(entry.get('oper-status', 'down'),
+                             OperStatus.UNKNOWN)
+
+    @staticmethod
+    def _get_mac_addr(entry):
+        mac_addr = entry.get('phys-address', None)
+        if mac_addr is None:
+            import random
+            # TODO: Get with qumram team about phys addr
+            mac_addr = '08:00:{}{}:{}{}:{}{}:00'.format(random.randint(0, 9),
+                                                        random.randint(0, 9),
+                                                        random.randint(0, 9),
+                                                        random.randint(0, 9),
+                                                        random.randint(0, 9),
+                                                        random.randint(0, 9))
+        return mac_addr
+
+    @staticmethod
+    def _get_speed_value(entry):
+        speed = entry.get('speed') or IetfInterfacesState._get_speed_via_name(entry.get('name'))
+        if isinstance(speed, str):
+            return long(speed)
+        return speed
+
+    @staticmethod
+    def _get_speed_via_name(name):
+        speed_map = {
+            'terabit':         1000000000000,
+            'hundred-gigabit':  100000000000,
+            'fourty-gigabit':    40000000000,
+            'ten-gigabit':       10000000000,
+            'gigabit':            1000000000,
+        }
+        for n,v in speed_map.iteritems():
+            if n in name.lower():
+                return v
+        return 0
+
+    @staticmethod
+    def _get_of_state(entry):
+        # If port up and ready: OFPPS_LIVE
+        # If port config bit is down: OFPPC_PORT_DOWN
+        # If port state bit is down: OFPPS_LINK_DOWN
+        # if IetfInterfacesState._get_admin_state(entry) == AdminState.ENABLED:
+        #     return OFPPS_LIVE \
+        #         if IetfInterfacesState._get_oper_status(entry) == OperStatus.ACTIVE \
+        #         else OFPPS_LINK_DOWN
+        #
+        # return OFPPC_PORT_DOWN
+        # TODO: Update of openflow port state is not supported, so always say we are alive
+        return OFPPS_LIVE
+
+    @staticmethod
+    def _get_of_capabilities(entry):
+        # The capabilities field is a bitmap that uses a combination of the following flags :
+        # Capabilities supported by the datapath
+        # enum ofp_capabilities {
+        #    OFPC_FLOW_STATS = 1 << 0,   /* Flow statistics. */
+        #    OFPC_TABLE_STATS = 1 << 1,  /* Table statistics. */
+        #    OFPC_PORT_STATS = 1 << 2,   /* Port statistics. */
+        #    OFPC_GROUP_STATS = 1 << 3,  /* Group statistics. */
+        #    OFPC_IP_REASM = 1 << 5,     /* Can reassemble IP fragments. */
+        #    OFPC_QUEUE_STATS = 1 << 6,  /* Queue statistics. */
+        #    OFPC_PORT_BLOCKED = 1 << 8, /* Switch will block looping ports. */
+        #    OFPC_BUNDLES = 1 << 9,      /* Switch supports bundles. */
+        #    OFPC_FLOW_MONITORING = 1 << 10, /* Switch supports flow monitoring. */
+        # }
+        # enum ofp_port_features {
+        #     OFPPF_10MB_HD = 1 << 0, /* 10 Mb half-duplex rate support. */
+        #     OFPPF_10MB_FD = 1 << 1, /* 10 Mb full-duplex rate support. */
+        #     OFPPF_100MB_HD = 1 << 2, /* 100 Mb half-duplex rate support. */
+        #     OFPPF_100MB_FD = 1 << 3, /* 100 Mb full-duplex rate support. */
+        #     OFPPF_1GB_HD = 1 << 4, /* 1 Gb half-duplex rate support. */
+        #     OFPPF_1GB_FD = 1 << 5, /* 1 Gb full-duplex rate support. */
+        #     OFPPF_10GB_FD = 1 << 6, /* 10 Gb full-duplex rate support. */
+        #     OFPPF_40GB_FD = 1 << 7, /* 40 Gb full-duplex rate support. */
+        #     OFPPF_100GB_FD = 1 << 8, /* 100 Gb full-duplex rate support. */
+        #     OFPPF_1TB_FD = 1 << 9, /* 1 Tb full-duplex rate support. */
+        #     OFPPF_OTHER = 1 << 10, /* Other rate, not in the list. */
+        #     OFPPF_COPPER = 1 << 11, /* Copper medium. */
+        #     OFPPF_FIBER = 1 << 12, /* Fiber medium. */
+        #     OFPPF_AUTONEG = 1 << 13, /* Auto-negotiation. */
+        #     OFPPF_PAUSE = 1 << 14, /* Pause. */
+        #     OFPPF_PAUSE_ASYM = 1 << 15 /* Asymmetric pause. */
+        # }
+        # TODO: Look into adtran-physical-entities and decode xSFP type any other settings
+        return IetfInterfacesState._get_of_speed(entry) | OFPPF_FIBER
+
+    @staticmethod
+    def _get_of_speed(entry):
+        speed = IetfInterfacesState._get_speed_value(entry)
+        speed_map = {
+            1000000000: OFPPF_1GB_FD,
+            10000000000: OFPPF_10GB_FD,
+            40000000000: OFPPF_40GB_FD,
+            100000000000: OFPPF_100GB_FD,
+        }
+        # return speed_map.get(speed, OFPPF_OTHER)
+        # TODO: For now, force 100 GB
+        return OFPPF_100GB_FD
+
+    @staticmethod
+    def _get_port_number(name, if_index):
+        import re
+
+        formats = [
+            'xpon \d/{1,2}\d',                          # OLT version 3 (Feb 2018++)
+            'Hundred-Gigabit-Ethernet \d/\d/{1,2}\d',   # OLT version 2
+            'XPON \d/\d/{1,2}\d',                       # OLT version 2
+            'hundred-gigabit-ethernet \d/{1,2}\d',      # OLT version 1
+            'channel-termination {1,2}\d',              # OLT version 1
+        ]
+        p2 = re.compile('\d+')
+
+        for regex in formats:
+            p = re.compile(regex, re.IGNORECASE)
+            match = p.match(name)
+            if match is not None:
+                return int(p2.findall(name)[-1])
+
+    @staticmethod
+    def get_port_entries(rpc_reply, port_type):
+        """
+        Get the port entries that make up the northbound and
+        southbound interfaces
+
+        :param rpc_reply:
+        :param port_type:
+        :return:
+        """
+        ports = dict()
+        result_dict = xmltodict.parse(rpc_reply.data_xml)
+        entries = result_dict['data']['interfaces-state']['interface']
+        if not isinstance(entries, list):
+            entries = [entries]
+        port_entries = [entry for entry in entries if 'name' in entry and
+                        port_type.lower() in entry['name'].lower()]
+
+        for entry in port_entries:
+            port = {
+                'port_no': IetfInterfacesState._get_port_number(entry.get('name'),
+                                                                entry.get('ifindex')),
+                'name': entry.get('name', 'unknown'),
+                'ifIndex': entry.get('ifIndex'),
+                # 'label': None,
+                'mac_address': IetfInterfacesState._get_mac_addr(entry),
+                'admin_state': IetfInterfacesState._get_admin_state(entry),
+                'oper_status': IetfInterfacesState._get_oper_status(entry),
+                'ofp_state': IetfInterfacesState._get_of_state(entry),
+                'ofp_capabilities': IetfInterfacesState._get_of_capabilities(entry),
+                'current_speed': IetfInterfacesState._get_of_speed(entry),
+                'max_speed': IetfInterfacesState._get_of_speed(entry),
+            }
+            port_no = port['port_no']
+            if port_no not in ports:
+                ports[port_no] = port
+            else:
+                ports[port_no].update(port)
+
+        return ports
diff --git a/adapters/adtran_olt/codec/olt_config.py b/adapters/adtran_olt/codec/olt_config.py
new file mode 100644
index 0000000..473e2f6
--- /dev/null
+++ b/adapters/adtran_olt/codec/olt_config.py
@@ -0,0 +1,329 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# 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.
+
+import structlog
+
+log = structlog.get_logger()
+
+
+class OltConfig(object):
+    """
+    Class to wrap decode of olt container (config) from the ADTRAN
+    gpon-olt-hw.yang YANG model
+    """
+    def __init__(self, packet):
+        self._packet = packet
+        self._pons = None
+
+    def __str__(self):
+        return "OltConfig: {}".format(self.olt_id)
+
+    @property
+    def olt_id(self):
+        """Unique OLT identifier"""
+        return self._packet.get('olt-id', '')
+
+    @property
+    def debug_output(self):
+        """least important level that will output everything"""
+        return self._packet.get('debug-output', 'warning')
+
+    @property
+    def pons(self):
+        if self._pons is None:
+            self._pons = OltConfig.Pon.decode(self._packet.get('pon', None))
+        return self._pons
+
+    class Pon(object):
+        """
+        Provides decode of PON list from within
+        """
+        def __init__(self, packet):
+            assert 'pon-id' in packet, 'pon-id not found'
+            self._packet = packet
+            self._onus = None
+
+        def __str__(self):
+            return "OltConfig.Pon: pon-id: {}".format(self.pon_id)
+
+        @staticmethod
+        def decode(pon_list):
+            pons = {}
+
+            if pon_list is not None:
+                for pon_data in pon_list:
+                    pon = OltConfig.Pon(pon_data)
+                    assert pon.pon_id not in pons
+                    pons[pon.pon_id] = pon
+
+            return pons
+
+        @property
+        def pon_id(self):
+            """PON identifier"""
+            return self._packet['pon-id']
+
+        @property
+        def enabled(self):
+            """The desired state of the interface"""
+            return self._packet.get('enabled', False)
+
+        @property
+        def downstream_fec_enable(self):
+            """Enables downstream Forward Error Correction"""
+            return self._packet.get('downstream-fec-enable', False)
+
+        @property
+        def upstream_fec_enable(self):
+            """Enables upstream Forward Error Correction"""
+            return self._packet.get('upstream-fec-enable', False)
+
+        @property
+        def deployment_range(self):
+            """Maximum deployment distance (meters)"""
+            return self._packet.get('deployment-range', 25000)
+
+        @property
+        def onus(self):
+            if self._onus is None:
+                self._onus = OltConfig.Pon.Onu.decode(self._packet.get('onus', None))
+            return self._onus
+
+        class Onu(object):
+            """
+            Provides decode of onu list for a PON port
+            """
+            def __init__(self, packet):
+                assert 'onu-id' in packet, 'onu-id not found'
+                self._packet = packet
+                self._tconts = None
+                self._tconts_dict = None
+                self._gem_ports = None
+                self._gem_ports_dict = None
+
+            def __str__(self):
+                return "OltConfig.Pon.Onu: onu-id: {}".format(self.onu_id)
+
+            @staticmethod
+            def decode(onu_dict):
+                onus = {}
+
+                if onu_dict is not None:
+                    if 'onu' in onu_dict:
+                        for onu_data in onu_dict['onu']:
+                            onu = OltConfig.Pon.Onu(onu_data)
+                            assert onu.onu_id not in onus
+                            onus[onu.onu_id] = onu
+                    elif len(onu_dict) > 0 and 'onu-id' in onu_dict[0]:
+                        onu = OltConfig.Pon.Onu(onu_dict[0])
+                        assert onu.onu_id not in onus
+                        onus[onu.onu_id] = onu
+
+                return onus
+
+            @property
+            def onu_id(self):
+                """The ID used to identify the ONU"""
+                return self._packet['onu-id']
+
+            @property
+            def serial_number_64(self):
+                """The serial number (base-64) is unique for each ONU"""
+                return self._packet.get('serial-number', '')
+
+            @property
+            def password(self):
+                """ONU Password"""
+                return self._packet.get('password', bytes(0))
+
+            @property
+            def enable(self):
+                """If true, places the ONU in service"""
+                return self._packet.get('enable', False)
+
+            @property
+            def tconts(self):
+                if self._tconts is None:
+                    self._tconts = OltConfig.Pon.Onu.TCont.decode(self._packet.get('t-conts', None))
+                return self._tconts
+
+            @property
+            def tconts_dict(self):               # TODO: Remove if not used
+                if self._tconts_dict is None:
+                    self._tconts_dict = {tcont.alloc_id: tcont for tcont in self.tconts}
+                return self._tconts_dict
+
+            @property
+            def gem_ports(self):
+                if self._gem_ports is None:
+                    self._gem_ports = OltConfig.Pon.Onu.GemPort.decode(self._packet.get('gem-ports', None))
+                return self._gem_ports
+
+            @property
+            def gem_ports_dict(self):               # TODO: Remove if not used
+                if self._gem_ports_dict is None:
+                    self._gem_ports_dict = {gem.gem_id: gem for gem in self.gem_ports}
+                return self._gem_ports_dict
+
+            class TCont(object):
+                """
+                Provides decode of onu list for the T-CONT container
+                """
+                def __init__(self, packet):
+                    assert 'alloc-id' in packet, 'alloc-id not found'
+                    self._packet = packet
+                    self._traffic_descriptor = None
+                    self._best_effort = None
+
+                def __str__(self):
+                    return "OltConfig.Pon.Onu.TCont: alloc-id: {}".format(self.alloc_id)
+
+                @staticmethod
+                def decode(tcont_container):
+                    tconts = {}
+
+                    if tcont_container is not None:
+                        for tcont_data in tcont_container.get('t-cont', []):
+                            tcont = OltConfig.Pon.Onu.TCont(tcont_data)
+                            assert tcont.alloc_id not in tconts
+                            tconts[tcont.alloc_id] = tcont
+
+                    return tconts
+
+                @property
+                def alloc_id(self):
+                    """The ID used to identify the T-CONT"""
+                    return self._packet['alloc-id']
+
+                @property
+                def traffic_descriptor(self):
+                    """
+                    Each Alloc-ID is provisioned with a traffic descriptor that specifies
+                    the three bandwidth component parameters: fixed bandwidth, assured
+                    bandwidth, and maximum bandwidth, as well as the ternary eligibility
+                    indicator for additional bandwidth assignment
+                    """
+                    if self._traffic_descriptor is None and 'traffic-descriptor' in self._packet:
+                        self._traffic_descriptor = OltConfig.Pon.Onu.TCont.\
+                            TrafficDescriptor(self._packet['traffic-descriptor'])
+                    return self._traffic_descriptor
+
+                class TrafficDescriptor(object):
+                    def __init__(self, packet):
+                        self._packet = packet
+
+                    def __str__(self):
+                        return "OltConfig.Pon.Onu.TCont.TrafficDescriptor: {}/{}/{}".\
+                            format(self.fixed_bandwidth, self.assured_bandwidth,
+                                   self.maximum_bandwidth)
+
+                    @property
+                    def fixed_bandwidth(self):
+                        try:
+                            return int(self._packet.get('fixed-bandwidth', 0))
+                        except:
+                            return 0
+
+                    @property
+                    def assured_bandwidth(self):
+                        try:
+                            return int(self._packet.get('assured-bandwidth', 0))
+                        except:
+                            return 0
+
+                    @property
+                    def maximum_bandwidth(self):
+                        try:
+                            return int(self._packet.get('maximum-bandwidth', 0))
+                        except:
+                            return 0
+
+                    @property
+                    def additional_bandwidth_eligibility(self):
+                        return self._packet.get('additional-bandwidth-eligibility', 'none')
+
+                @property
+                def best_effort(self):
+                    if self._best_effort is None:
+                        self._best_effort = OltConfig.Pon.Onu.TCont.BestEffort.decode(
+                            self._packet.get('best-effort', None))
+                    return self._best_effort
+
+                class BestEffort(object):
+                    def __init__(self, packet):
+                        self._packet = packet
+
+                    def __str__(self):
+                        return "OltConfig.Pon.Onu.TCont.BestEffort: {}".format(self.bandwidth)
+
+                    @property
+                    def bandwidth(self):
+                        return self._packet['bandwidth']
+
+                    @property
+                    def priority(self):
+                        return self._packet['priority']
+
+                    @property
+                    def weight(self):
+                        return self._packet['weight']
+
+            class GemPort(object):
+                """
+                Provides decode of onu list for the gem-ports container
+                """
+                def __init__(self, packet):
+                    assert 'port-id' in packet, 'port-id not found'
+                    self._packet = packet
+
+                def __str__(self):
+                    return "OltConfig.Pon.Onu.GemPort: port-id: {}/{}".\
+                        format(self.port_id, self.alloc_id)
+
+                @staticmethod
+                def decode(gem_port_container):
+                    gem_ports = {}
+
+                    if gem_port_container is not None:
+                        for gem_port_data in gem_port_container.get('gem-port', []):
+                            gem_port = OltConfig.Pon.Onu.GemPort(gem_port_data)
+                            assert gem_port.port_id not in gem_ports
+                            gem_ports[gem_port.port_id] = gem_port
+
+                    return gem_ports
+
+                @property
+                def port_id(self):
+                    """The ID used to identify the GEM Port"""
+                    return self._packet['port-id']
+
+                @property
+                def gem_id(self):
+                    """The ID used to identify the GEM Port"""
+                    return self.port_id
+
+                @property
+                def alloc_id(self):
+                    """The Alloc-ID of the T-CONT to which this GEM port is mapped"""
+                    return self._packet['alloc-id']
+
+                @property
+                def omci_transport(self):
+                    """If true, this GEM port is used to transport the OMCI virtual connection"""
+                    return self._packet.get('omci-transport', False)
+
+                @property
+                def encryption(self):
+                    """If true, enable encryption using the advanced encryption standard(AES)"""
+                    return self._packet.get('encryption', False)
diff --git a/adapters/adtran_olt/codec/olt_state.py b/adapters/adtran_olt/codec/olt_state.py
new file mode 100644
index 0000000..74413a4
--- /dev/null
+++ b/adapters/adtran_olt/codec/olt_state.py
@@ -0,0 +1,290 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# 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.
+#
+import structlog
+
+log = structlog.get_logger()
+
+
+class OltState(object):
+    """
+    Class to wrap decode of olt-state container from the ADTRAN
+    gpon-olt-hw.yang YANG model
+    """
+
+    def __init__(self, packet):
+        self._packet = packet
+        self._pons = None
+
+    def __str__(self):
+        return "OltState: {}".format(self.software_version)
+
+    @property
+    def software_version(self):
+        """The software version of olt driver"""
+        return self._packet.get('software-version', '')
+
+    @property
+    def pons(self):
+        if self._pons is None:
+            self._pons = OltState.Pon.decode(self._packet.get('pon', None))
+        return self._pons
+
+    #############################################################
+    # Act like a container for simple access into PON list
+
+    def __len__(self):
+        return len(self.pons)
+
+    def __getitem__(self, key):
+        if not isinstance(key, int):
+            raise TypeError('Key should be of type int')
+        if key not in self.pons:
+            raise KeyError("key '{}' not found".format(key))
+
+        return self.pons[key]
+
+    def __iter__(self):
+        raise NotImplementedError('TODO: Not yet implemented')
+
+    def __contains__(self, item):
+        if not isinstance(item, int):
+            raise TypeError('Item should be of type int')
+        return item in self.pons
+
+    # TODO: Look at generator support and if it is useful
+
+    class Pon(object):
+        """
+        Provides decode of PON list from within
+        """
+        def __init__(self, packet):
+            assert 'pon-id' in packet
+            self._packet = packet
+            self._onus = None
+            self._gems = None
+
+        def __str__(self):
+            return "OltState.Pon: pon-id: {}".format(self.pon_id)
+
+        @staticmethod
+        def decode(pon_list):
+            # log.debug('Decoding PON List:{}{}'.format(os.linesep,
+            #                                           pprint.PrettyPrinter().pformat(pon_list)))
+            pons = {}
+            for pon_data in pon_list:
+                pon = OltState.Pon(pon_data)
+                assert pon.pon_id not in pons
+                pons[pon.pon_id] = pon
+
+            return pons
+
+        @property
+        def pon_id(self):
+            """PON identifier"""
+            return self._packet['pon-id']
+
+        @property
+        def downstream_wavelength(self):
+            """The wavelength, in nanometers, being used in the downstream direction"""
+            return self._packet.get('downstream-wavelength', 0)
+
+        @property
+        def upstream_wavelength(self):
+            """The wavelength, in nanometers, being used in the upstream direction"""
+            return self._packet.get('upstream-wavelength', 0)
+
+        @property
+        def downstream_channel_id(self):
+            """Downstream wavelength channel identifier associated with this PON."""
+            return self._packet.get('downstream-channel-id', 0)
+
+        @property
+        def rx_packets(self):
+            """Sum all of the RX Packets of GEM ports that are not base TCONT's"""
+            return int(self._packet.get('rx-packets', 0))
+
+        @property
+        def tx_packets(self):
+            """Sum all of the TX Packets of GEM ports that are not base TCONT's"""
+            return int(self._packet.get('tx-packets', 0))
+
+        @property
+        def rx_bytes(self):
+            """Sum all of the RX Octets of GEM ports that are not base TCONT's"""
+            return int(self._packet.get('rx-bytes', 0))
+
+        @property
+        def tx_bytes(self):
+            """Sum all of the TX Octets of GEM ports that are not base TCONT's"""
+            return int(self._packet.get('tx-bytes', 0))
+
+        @property
+        def tx_bip_errors(self):
+            """Sum the TX ONU bip errors to get TX BIP's per PON"""
+            return int(self._packet.get('tx-bip-errors', 0))
+
+        @property
+        def wm_tuned_out_onus(self):
+            """
+            bit array indicates the list of tuned out ONU's that are in wavelength
+            mobility protecting state.
+                onu-bit-octects:
+                  type binary { length "4 .. 1024"; }
+                  description  each bit position indicates corresponding ONU's status
+                               (true or false) whether that ONU's is in
+                               wavelength mobility protecting state or not
+                               For 128 ONTs per PON, the size of this
+                               array will be 16. onu-bit-octects[0] and MSB bit in that byte
+                               represents ONU 0 etc.
+            """
+            return self._packet.get('wm-tuned-out-onus', bytes(0))
+
+        @property
+        def ont_los(self):
+            """List of configured ONTs that have been previously discovered and are in a los of signal state"""
+            return self._packet.get('ont-los', [])
+
+        @property
+        def discovered_onu(self):
+            """
+            Immutable Set of each Optical Network Unit(ONU) that has been activated via discovery
+                key/value: serial-number (string)
+            """
+            return frozenset([sn['serial-number'] for sn in self._packet.get('discovered-onu', [])
+                              if 'serial-number' in sn and sn['serial-number'] != 'AAAAAAAAAAA='])
+
+        @property
+        def gems(self):
+            """This list is not in the proposed BBF model, the stats are part of ietf-interfaces"""
+            if self._gems is None:
+                self._gems = OltState.Pon.Gem.decode(self._packet.get('gem', []))
+            return self._gems
+
+        @property
+        def onus(self):
+            """
+            The map of each Optical Network Unit(ONU).  Key: ONU ID (int)
+            """
+            if self._onus is None:
+                self._onus = OltState.Pon.Onu.decode(self._packet.get('onu', []))
+            return self._onus
+
+        class Onu(object):
+            """
+            Provides decode of onu list for a PON port
+            """
+            def __init__(self, packet):
+                assert 'onu-id' in packet, 'onu-id not found in packet'
+                self._packet = packet
+
+            def __str__(self):
+                return "OltState.Pon.Onu: onu-id: {}".format(self.onu_id)
+
+            @staticmethod
+            def decode(onu_list):
+                # log.debug('onus:{}{}'.format(os.linesep,
+                #                              pprint.PrettyPrinter().pformat(onu_list)))
+                onus = {}
+                for onu_data in onu_list:
+                    onu = OltState.Pon.Onu(onu_data)
+                    assert onu.onu_id not in onus
+                    onus[onu.onu_id] = onu
+
+                return onus
+
+            @property
+            def onu_id(self):
+                """The ID used to identify the ONU"""
+                return self._packet['onu-id']
+
+            @property
+            def oper_status(self):
+                """The operational state of each ONU"""
+                return self._packet.get('oper-status', 'unknown')
+
+            @property
+            def reported_password(self):
+                """The password reported by the ONU (binary)"""
+                return self._packet.get('reported-password', bytes(0))
+
+            @property
+            def rssi(self):
+                """The received signal strength indication of the ONU"""
+                return self._packet.get('rssi', -9999)
+
+            @property
+            def equalization_delay(self):
+                """Equalization delay (bits)"""
+                return self._packet.get('equalization-delay', 0)
+
+            @property
+            def fiber_length(self):
+                """Distance to ONU"""
+                return self._packet.get('fiber-length', 0)
+
+        class Gem(object):
+            """
+            Provides decode of onu list for a PON port
+            """
+            def __init__(self, packet):
+                assert 'onu-id' in packet, 'onu-id not found in packet'
+                assert 'port-id' in packet, 'port-id not found in packet'
+                assert 'alloc-id' in packet, 'alloc-id not found in packet'
+                self._packet = packet
+
+            def __str__(self):
+                return "OltState.Pon.Gem: onu-id: {}, gem-id".\
+                    format(self.onu_id, self.gem_id)
+
+            @staticmethod
+            def decode(gem_list):
+                # log.debug('gems:{}{}'.format(os.linesep,
+                #                              pprint.PrettyPrinter().pformat(gem_list)))
+                gems = {}
+                for gem_data in gem_list:
+                    gem = OltState.Pon.Gem(gem_data)
+                    assert gem.gem_id not in gems
+                    gems[gem.gem_id] = gem
+
+                return gems
+
+            @property
+            def onu_id(self):
+                """The ID used to identify the ONU"""
+                return self._packet['onu-id']
+
+            @property
+            def alloc_id(self):
+                return self._packet['alloc-id']
+
+            @property
+            def gem_id(self):
+                return self._packet['port-id']
+
+            @property
+            def tx_packets(self):
+                return int(self._packet.get('tx-packets', 0))
+
+            @property
+            def tx_bytes(self):
+                return int(self._packet.get('tx-bytes', 0))
+
+            @property
+            def rx_packets(self):
+                return int(self._packet.get('rx-packets', 0))
+
+            @property
+            def rx_bytes(self):
+                return int(self._packet.get('rx-bytes', 0))
diff --git a/adapters/adtran_olt/codec/physical_entities_state.py b/adapters/adtran_olt/codec/physical_entities_state.py
new file mode 100644
index 0000000..47187c9
--- /dev/null
+++ b/adapters/adtran_olt/codec/physical_entities_state.py
@@ -0,0 +1,80 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# 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 adapters.adtran_common.net.adtran_netconf import adtran_module_url
+from twisted.internet.defer import inlineCallbacks, returnValue
+import xmltodict
+import structlog
+
+log = structlog.get_logger()
+
+_phys_entities_rpc = """
+    <filter xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
+      <physical-entities-state xmlns="{}">
+        <physical-entity/>
+      </physical-entities-state>
+    </filter>
+    """.format(adtran_module_url('adtran-physical-entities'))
+
+
+class PhysicalEntitiesState(object):
+    def __init__(self, session):
+        self._session = session
+        self._rpc_reply = None
+
+    @inlineCallbacks
+    def get_state(self):
+        self._rpc_reply = None
+        request = self._session.get(_phys_entities_rpc)
+        self._rpc_reply = yield request
+        returnValue(self._rpc_reply)
+
+    @property
+    def physical_entities(self):
+        """
+        :return: (list) of OrderDict physical entities
+        """
+        if self._rpc_reply is None:
+            # TODO: Support auto-get?
+            return None
+
+        result_dict = xmltodict.parse(self._rpc_reply.data_xml)
+        return result_dict['data']['physical-entities-state']['physical-entity']
+
+    def get_physical_entities(self, classification=None):
+        """
+        Get the physical entities of a particular type
+        :param classification: (String or List) The classification or general hardware type of the
+                                                component identified by this physical entity
+                                                (case-insensitive)
+        :return: (list) of OrderDict physical entities
+        """
+        entries = self.physical_entities
+
+        if classification is None:
+            return entries
+
+        # for entry in entries:
+        #     import pprint
+        #     log.info(pprint.PrettyPrinter(indent=2).pformat(entry))
+
+        def _matches(entry, value):
+            if 'classification' in entry and '#text' in entry['classification']:
+                text_val = entry['classification']['#text'].lower()
+                if isinstance(value, list):
+                    return any(v.lower() in text_val for v in value)
+                return value.lower() in text_val
+            return False
+
+        return [entry for entry in entries if _matches(entry, classification)]