VOL-1395: Common shared libraries needed for Python based device adapters.

This is an initial check-in of code from the master branch.  Additional work
is expected on a few items to work with the new go-core and will be covered
by separate JIRAs and commits.

Change-Id: I0856ec6b79b8d3e49082c609eb9c7eedd75b1708
diff --git a/python/adapters/extensions/omci/state_machines/__init__.py b/python/adapters/extensions/omci/state_machines/__init__.py
new file mode 100644
index 0000000..b0fb0b2
--- /dev/null
+++ b/python/adapters/extensions/omci/state_machines/__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/python/adapters/extensions/omci/state_machines/alarm_sync.py b/python/adapters/extensions/omci/state_machines/alarm_sync.py
new file mode 100644
index 0000000..c7b7d64
--- /dev/null
+++ b/python/adapters/extensions/omci/state_machines/alarm_sync.py
@@ -0,0 +1,670 @@
+#
+# 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.
+#
+import structlog
+from datetime import datetime
+from transitions import Machine
+from twisted.internet import reactor
+from voltha.extensions.omci.omci_defs import ReasonCodes, EntityOperations
+from voltha.extensions.omci.omci_cc import OmciCCRxEvents, OMCI_CC, RX_RESPONSE_KEY
+from voltha.extensions.omci.omci_messages import OmciGetAllAlarmsResponse
+from voltha.extensions.omci.omci_frame import OmciFrame
+from voltha.extensions.omci.database.alarm_db_ext import AlarmDbExternal
+from voltha.extensions.omci.database.mib_db_api import ATTRIBUTES_KEY
+from voltha.extensions.omci.omci_entities import CircuitPack, PptpEthernetUni, OntG, AniG
+
+from common.event_bus import EventBusClient
+from voltha.protos.omci_alarm_db_pb2 import AlarmOpenOmciEventType
+
+RxEvent = OmciCCRxEvents
+RC = ReasonCodes
+OP = EntityOperations
+
+
+class AlarmSynchronizer(object):
+    """
+    OpenOMCI Alarm Synchronizer state machine
+    """
+    DEFAULT_STATES = ['disabled', 'starting', 'auditing', 'in_sync']
+
+    DEFAULT_TRANSITIONS = [
+        {'trigger': 'start', 'source': 'disabled', 'dest': 'starting'},
+
+        {'trigger': 'audit_alarm', 'source': 'starting', 'dest': 'auditing'},
+        {'trigger': 'sync_alarm', 'source': 'starting', 'dest': 'in_sync'},
+
+        {'trigger': 'success', 'source': 'auditing', 'dest': 'in_sync'},
+        {'trigger': 'audit_alarm', 'source': 'auditing', 'dest': 'auditing'},
+        {'trigger': 'failure', 'source': 'auditing', 'dest': 'auditing'},
+
+        {'trigger': 'audit_alarm', 'source': 'in_sync', 'dest': 'auditing'},
+
+        # Do wildcard 'stop' trigger last so it covers all previous states
+        {'trigger': 'stop', 'source': '*', 'dest': 'disabled'},
+    ]
+    DEFAULT_TIMEOUT_RETRY = 15     # Seconds to delay after task failure/timeout
+    DEFAULT_AUDIT_DELAY = 180      # Periodic tick to audit the ONU's alarm table
+
+    def __init__(self, agent, device_id, alarm_sync_tasks, db,
+                 advertise_events=False,
+                 states=DEFAULT_STATES,
+                 transitions=DEFAULT_TRANSITIONS,
+                 initial_state='disabled',
+                 timeout_delay=DEFAULT_TIMEOUT_RETRY,
+                 audit_delay=DEFAULT_AUDIT_DELAY):
+        """
+        Class initialization
+
+        :param agent: (OpenOmciAgent) Agent
+        :param device_id: (str) ONU Device ID
+        :param db: (MibDbApi) MIB/Alarm Database
+        :param advertise_events: (bool) Advertise events on OpenOMCI Event Bus
+        :param alarm_sync_tasks: (dict) Tasks to run
+        :param states: (list) List of valid states
+        :param transitions: (dict) Dictionary of triggers and state changes
+        :param initial_state: (str) Initial state machine state
+        :param timeout_delay: (int/float) Number of seconds after a timeout to attempt
+                                          a retry (goes back to starting state)
+        :param audit_delay: (int) Seconds between Alarm audits while in sync. Set to
+                                  zero to disable audit. An operator can request
+                                  an audit manually by calling 'self.audit_alarm'
+        """
+
+        self.log = structlog.get_logger(device_id=device_id)
+
+        self._agent = agent
+        self._device_id = device_id
+        self._device = None
+        self._database = db
+        self._timeout_delay = timeout_delay
+        self._audit_delay = audit_delay
+        self._resync_task = alarm_sync_tasks['alarm-resync']
+        self._advertise_events = advertise_events
+        self._alarm_manager = None
+        self._onu_id = None
+        self._uni_ports = list()
+        self._ani_ports = list()
+
+        self._deferred = None
+        self._current_task = None
+        self._task_deferred = None
+        self._last_alarm_sequence_value = 0
+        self._device_in_db = False
+
+        self._event_bus = EventBusClient()
+        self._omci_cc_subscriptions = {               # RxEvent.enum -> Subscription Object
+            RxEvent.Get_ALARM_Get: None,
+            RxEvent.Alarm_Notification: None
+        }
+        self._omci_cc_sub_mapping = {
+            RxEvent.Get_ALARM_Get: self.on_alarm_update_response,
+            RxEvent.Alarm_Notification: self.on_alarm_notification
+        }
+
+        # Statistics and attributes
+        # TODO: add any others if it will support problem diagnosis
+
+        # Set up state machine to manage states
+        self.machine = Machine(model=self, states=states,
+                               transitions=transitions,
+                               initial=initial_state,
+                               queued=True,
+                               name='{}-{}'.format(self.__class__.__name__,
+                                                   device_id))
+
+    def _cancel_deferred(self):
+        d1, self._deferred = self._deferred, None
+        d2, self._task_deferred = self._task_deferred, None
+
+        for d in [d1, d1]:
+            try:
+                if d is not None and not d.called:
+                    d.cancel()
+            except:
+                pass
+
+    def __str__(self):
+        return 'Alarm Synchronizer: Device ID: {}, State:{}'.format(self._device_id, self.state)
+
+    def delete(self):
+        """
+        Cleanup any state information
+        """
+        self.stop()
+        db, self._database = self._database, None
+
+        if db is not None:
+            db.remove(self._device_id)
+
+    @property
+    def device_id(self):
+        return self._device_id
+
+    @property
+    def last_alarm_sequence(self):
+        return self._last_alarm_sequence_value
+
+    def reset_alarm_sequence(self):
+        if self._last_alarm_sequence_value != 0:
+            self._last_alarm_sequence_value = 0
+
+    def increment_alarm_sequence(self):
+        self._last_alarm_sequence_value += 1
+        if self._last_alarm_sequence_value > 255:
+            self._last_alarm_sequence_value = 1
+
+    @property
+    def advertise_events(self):
+        return self._advertise_events
+
+    @advertise_events.setter
+    def advertise_events(self, value):
+        if not isinstance(value, bool):
+            raise TypeError('Advertise event is a boolean')
+        self._advertise_events = value
+
+    def advertise(self, event, info):
+        """Advertise an event on the OpenOMCI event bus"""
+        if self._advertise_events:
+            self._agent.advertise(event,
+                                  {
+                                      'state-machine': self.machine.name,
+                                      'info': info,
+                                      'time': str(datetime.utcnow())
+                                  })
+
+    def set_alarm_params(self, mgr=None, onu_id=None, uni_ports=None, ani_ports=None):
+        if mgr is not None:
+            self._alarm_manager = mgr
+
+        if onu_id is not None:
+            self._onu_id = onu_id
+
+        if uni_ports is not None:
+            assert isinstance(uni_ports, list)
+            self._uni_ports = uni_ports
+
+        if ani_ports is not None:
+            assert isinstance(ani_ports, list)
+            self._ani_ports = ani_ports
+
+    def on_enter_disabled(self):
+        """
+        State machine is being stopped
+        """
+        self.advertise(AlarmOpenOmciEventType.state_change, self.state)
+
+        self._cancel_deferred()
+
+        task, self._current_task = self._current_task, None
+        if task is not None:
+            task.stop()
+
+        # Drop Response and Autonomous notification subscriptions
+        for event, sub in self._omci_cc_subscriptions.iteritems():
+            if sub is not None:
+                self._omci_cc_subscriptions[event] = None
+                self._device.omci_cc.event_bus.unsubscribe(sub)
+
+    def _seed_database(self):
+        if not self._device_in_db:
+            try:
+                try:
+                    self._database.start()
+                    self._database.add(self._device_id)
+                    self.log.debug('seed-db-does-not-exist', device_id=self._device_id)
+
+                except KeyError:
+                    # Device already is in database
+                    self.log.debug('seed-db-exist', device_id=self._device_id)
+
+                self._device_in_db = True
+
+            except Exception as e:
+                self.log.exception('seed-database-failure', e=e)
+
+    def on_enter_starting(self):
+        """
+        Determine ONU status and start Alarm Synchronization tasks
+        """
+        self._device = self._agent.get_device(self._device_id)
+        self.advertise(AlarmOpenOmciEventType.state_change, self.state)
+
+        # Make sure root of external Alarm Database exists
+        self._seed_database()
+
+        # Set up Response and Autonomous notification subscriptions
+        try:
+            for event, sub in self._omci_cc_sub_mapping.iteritems():
+                if self._omci_cc_subscriptions[event] is None:
+                    self._omci_cc_subscriptions[event] = \
+                        self._device.omci_cc.event_bus.subscribe(
+                            topic=OMCI_CC.event_bus_topic(self._device_id, event),
+                            callback=sub)
+
+        except Exception as e:
+            self.log.exception('omci-cc-subscription-setup', e=e)
+
+        # Schedule first audit if enabled
+        if self._audit_delay > 0:
+            # Note using the shorter timeout delay here since this is the first
+            # audit after startup
+            self._deferred = reactor.callLater(self._timeout_delay, self.audit_alarm)
+        else:
+            self._deferred = reactor.callLater(0, self.sync_alarm)
+
+    def on_enter_in_sync(self):
+        """
+        Schedule a tick to occur to in the future to request an audit
+        """
+        self.advertise(AlarmOpenOmciEventType.state_change, self.state)
+
+        if self._audit_delay > 0:
+            # Note using the shorter timeout delay here since this is the first
+            # audit after startup
+            self._deferred = reactor.callLater(self._audit_delay, self.audit_alarm)
+
+    def on_enter_auditing(self):
+        """
+         Begin full Alarm data sync, Comparing the all alarms
+         """
+        self.advertise(AlarmOpenOmciEventType.state_change, self.state)
+
+        def success(results):
+            self.log.debug('alarm-diff-success')
+            self._current_task = None
+
+            # Any differences found between ONU and OpenOMCI Alarm tables?
+            if results is None:
+                self._device.alarm_db_in_sync = True
+                self._deferred = reactor.callLater(0, self.success)
+            else:
+                # Reconcile the alarm table and re-run audit
+                self.reconcile_alarm_table(results)
+                self._deferred = reactor.callLater(5, self.audit_alarm)
+
+        def failure(reason):
+            self.log.info('alarm-update-failure', reason=reason)
+            self._current_task = None
+            self._deferred = reactor.callLater(self._timeout_delay, self.failure)
+
+        self._current_task = self._resync_task(self._agent, self._device_id)
+        self._task_deferred = self._device.task_runner.queue_task(self._current_task)
+        self._task_deferred.addCallbacks(success, failure)
+
+    def reconcile_alarm_table(self, results):
+        self.log.debug('alarm-reconcile', state=self.state, results=results)
+
+        onu_only = results['onu-only']
+        olt_only = results['olt-only']
+        attr_diffs = results['attr-diffs']
+        onu_db = results['onu-db']
+        olt_db = results['olt-db']
+
+        if any(item is not None for item in (onu_only, olt_only, attr_diffs)):
+            self._device.alarm_db_in_sync = False
+
+        # Compare the differences.  During upload, if there are no alarms at all,
+        # then the ONU alarm table retrieved may be empty (instead of MEs with all
+        # bits cleared) depending upon the ONU's OMCI Stack.
+
+        if onu_only is not None:
+            self.process_onu_only_diffs(onu_only, onu_db)
+
+        if olt_only is not None:
+            self.process_olt_only_diffs(olt_only)
+
+        if attr_diffs is not None:
+            self.process_attr_diffs(attr_diffs, olt_db, onu_db)
+
+    def process_onu_only_diffs(self, onu_only, onu_db):
+        """
+        ONU only alarms will typically occur when doing the first audit as our
+        database is clear and we are seeding the alarm table. Save the entries
+        and if any are set, we need to raise that alarm.
+
+        :param onu_only: (list) Tuples with [0]=class ID, [1]=entity ID
+        :param onu_db: (dict) ONU Alarm database from the alarm audit upload
+        """
+        for cid_eid in onu_only:
+            class_id = cid_eid[0]
+            entity_id = cid_eid[1]
+            try:
+                bitmap = onu_db[class_id][entity_id][ATTRIBUTES_KEY][AlarmDbExternal.ALARM_BITMAP_KEY]
+                self.process_alarm_data(class_id, entity_id, bitmap, -1)
+
+            except KeyError as e:
+                self.log.error('alarm-not-found', class_id=class_id, entity_id=entity_id, e=e)
+
+    def process_olt_only_diffs(self, olt_only):
+        """
+        OLT only alarms may occur if the alarm(s) are no longer active on the ONU
+        and the notification was missed. Process this by sending a cleared bitmap
+        for any alarm in the OLT database only
+
+        :param olt_only: (list) Tuples with [0]=class ID, [1]=entity ID
+        """
+        for cid_eid in olt_only:
+            # First process the alarm clearing
+            self.process_alarm_data(cid_eid[0], cid_eid[1], 0, -1)
+            # Now remove from alarm DB so we match the ONU alarm table
+            self._database.delete(self._device_id, cid_eid[0], cid_eid[1])
+
+    def process_attr_diffs(self, attr_diffs, onu_db):
+        """
+        Mismatch in alarm settings. Note that the attribute should always be the
+        alarm bitmap attribute (long).  For differences, the ONU is always right
+
+        :param attr_diffs: (list(int,int,str)) [0]=class ID, [1]=entity ID, [1]=attr
+        :param olt_db: (dict) OLT Alarm database snapshot from the alarm audit
+        :param onu_db: (dict) ONU Alarm database from the alarm audit upload
+        """
+        for cid_eid_attr in attr_diffs:
+            class_id = cid_eid_attr[0]
+            entity_id = cid_eid_attr[1]
+
+            try:
+                assert AlarmDbExternal.ALARM_BITMAP_KEY == cid_eid_attr[2]
+                bitmap = onu_db[class_id][entity_id][ATTRIBUTES_KEY][AlarmDbExternal.ALARM_BITMAP_KEY]
+                self.process_alarm_data(class_id, entity_id, bitmap, -1)
+
+            except KeyError as e:
+                self.log.error('alarm-not-found', class_id=class_id, entity_id=entity_id, e=e)
+
+    def on_alarm_update_response(self, _topic, msg):
+        """
+        Process a Get All Alarms response
+
+        :param _topic: (str) OMCI-RX topic
+        :param msg: (dict) Dictionary with 'rx-response' and 'tx-request' (if any)
+        """
+        self.log.debug('on-alarm-update-response', state=self.state, msg=msg)
+
+        if self._omci_cc_subscriptions[RxEvent.Get_ALARM_Get]:
+            if self.state == 'disabled':
+                self.log.error('rx-in-invalid-state', state=self.state)
+                return
+
+            try:
+                response = msg.get(RX_RESPONSE_KEY)
+
+                if isinstance(response, OmciFrame) and \
+                        isinstance(response.fields.get('omci_message'), OmciGetAllAlarmsResponse):
+                    # ONU will reset its last alarm sequence number to 0 on receipt of the
+                    # Get All Alarms request
+                    self.log.debug('received-alarm-response')
+                    self.reset_alarm_sequence()
+
+            except Exception as e:
+                self.log.exception('upload-alarm-failure', e=e)
+
+    def on_alarm_notification(self, _topic, msg):
+        """
+        Process an alarm Notification
+
+        :param _topic: (str) OMCI-RX topic
+        :param msg: (dict) Dictionary with keys:
+                    TX_REQUEST_KEY  -> None (this is an autonomous msg)
+                    RX_RESPONSE_KEY -> OmciMessage (Alarm notification frame)
+        """
+        self.log.debug('on-alarm-update-response', state=self.state, msg=msg)
+
+        alarm_msg = msg.get(RX_RESPONSE_KEY)
+        if alarm_msg is not None:
+            omci_msg = alarm_msg.fields['omci_message'].fields
+            class_id = omci_msg['entity_class']
+            seq_no = omci_msg['alarm_sequence_number']
+
+            # Validate that this ME supports alarm notifications
+            if class_id not in self._device.me_map or \
+                    OP.AlarmNotification not in self._device.me_map[class_id].notifications or \
+                    len(self._device.me_map[class_id].alarms) == 0:
+                self.log.warn('invalid-alarm-notification', class_id=class_id)
+                return
+
+            self.process_alarm_data(class_id,
+                                    omci_msg['entity_id'],
+                                    omci_msg['alarm_bit_map'],
+                                    seq_no)
+
+    def process_alarm_data(self, class_id, entity_id, bitmap, msg_seq_no):
+        """
+        Process new alarm data
+
+        :param class_id: (int)  Class ID of alarm
+        :param entity_id: (int) Entity ID of alarm
+        :param bitmap: (long) Alarm bitmap value
+        :param msg_seq_no: (int) Alarm sequence number. -1 if generated during an audit
+        """
+        if msg_seq_no > 0:
+            # increment alarm number & compare to alarm # in message
+            # Signal early audit if no match and audits are enabled
+            self.increment_alarm_sequence()
+
+            if self.last_alarm_sequence != msg_seq_no and self._audit_delay > 0:
+                self._deferred = reactor.callLater(0, self.audit_alarm)
+
+        key = AlarmDbExternal.ALARM_BITMAP_KEY
+        prev_entry = self._database.query(self._device_id, class_id, entity_id)
+        try:
+            # Need to access the bit map structure which is nested in dict attributes
+            prev_bitmap = 0 if len(prev_entry) == 0 else long(prev_entry['attributes'][key])
+        except Exception as e:
+            self.log.exception('alarm-prev-entry-collection-failure', class_id=class_id,
+                               device_id=self._device_id, entity_id=entity_id, value=bitmap, e=e)
+        # Save current entry before going on
+        try:
+            self._database.set(self._device_id, class_id, entity_id, {key: bitmap})
+
+        except Exception as e:
+            self.log.exception('alarm-save-failure', class_id=class_id,
+                               device_id=self._device_id, entity_id=entity_id, value=bitmap, e=e)
+
+        if self._alarm_manager is not None:
+            # Generate a set of alarm number that are raised in current and previous
+            previously_raised = {alarm_no for alarm_no in xrange(224)
+                                 if prev_bitmap & (1L << (223-alarm_no)) != 0L}
+
+            currently_raised = {alarm_no for alarm_no in xrange(224)
+                                if bitmap & (1L << (223-alarm_no)) != 0L}
+
+            newly_cleared = previously_raised - currently_raised
+            newly_raised = currently_raised - previously_raised
+
+            # Generate the set/clear alarms now
+            for alarm_number in newly_cleared:
+                reactor.callLater(0, self.clear_alarm, class_id, entity_id, alarm_number)
+
+            for alarm_number in newly_raised:
+                reactor.callLater(0, self.raise_alarm, class_id, entity_id, alarm_number)
+
+    def get_alarm_description(self, class_id, alarm_number):
+        """
+        Get the alarm description, both as a printable-string and also a CamelCase value
+        """
+        if alarm_number in self._device.me_map[class_id].alarms:
+            description = self._device.me_map[class_id].alarms[alarm_number]
+        elif alarm_number <= 207:
+            description = 'Reserved alarm {}'.format(alarm_number)
+        else:
+            description = 'Vendor specific alarm {}'.format(alarm_number)
+
+        # For CamelCase, replace hyphens with spaces before camel casing the string
+        return description, description.replace('-', ' ').title().replace(' ', '')
+
+    def raise_alarm(self, class_id, entity_id, alarm_number):
+        """
+        Raise an alarm on the ONU
+
+        :param class_id: (int)  Class ID of the Alarm ME
+        :param entity_id: (int) Entity ID of the Alarm
+        :param alarm_number: (int) Alarm number (bit) that is alarmed
+        """
+        description, name = self.get_alarm_description(class_id, alarm_number)
+
+        self.log.warn('alarm-set', class_id=class_id, entity_id=entity_id,
+                      alarm_number=alarm_number, name=name, description=description)
+
+        if self._alarm_manager is not None:
+            alarm = self.omci_alarm_to_onu_alarm(class_id, entity_id, alarm_number)
+            if alarm is not None:
+                alarm.raise_alarm()
+
+    def clear_alarm(self, class_id, entity_id, alarm_number):
+        """
+        Lower/clear an alarm on the ONU
+
+        :param class_id: (int)  Class ID of the Alarm ME
+        :param entity_id: (int) Entity ID of the Alarm
+        :param alarm_number: (int) Alarm number (bit) that is alarmed
+        """
+        description, name = self.get_alarm_description(class_id, alarm_number)
+
+        self.log.info('alarm-cleared', class_id=class_id, entity_id=entity_id,
+                      alarm_number=alarm_number, name=name, description=description)
+
+        if self._alarm_manager is not None:
+            alarm = self.omci_alarm_to_onu_alarm(class_id, entity_id, alarm_number)
+            if alarm is not None:
+                alarm.clear_alarm()
+
+    def query_mib(self, class_id=None, instance_id=None):
+        """
+        Get Alarm database information.
+
+        This method can be used to request information from the database to the detailed
+        level requested
+
+        :param class_id:  (int) Managed Entity class ID
+        :param instance_id: (int) Managed Entity instance
+
+        :return: (dict) The value(s) requested. If class/inst/attribute is
+                        not found, an empty dictionary is returned
+        :raises DatabaseStateError: If the database is not enabled or does not exist
+        """
+        from voltha.extensions.omci.database.mib_db_api import DatabaseStateError
+
+        self.log.debug('query', class_id=class_id, instance_id=instance_id)
+        if self._database is None:
+            raise DatabaseStateError('Database does not yet exist')
+
+        return self._database.query(self._device_id, class_id=class_id, instance_id=instance_id)
+
+    def omci_alarm_to_onu_alarm(self, class_id, entity_id, alarm_number):
+        """
+        Map an OMCI Alarm Notification alarm to the proper ONU Alarm Library alarm
+
+        :param class_id: (int) ME Class ID
+        :param entity_id: (int) ME Class instance ID
+        :param alarm_number: (int) Alarm Number
+        :return: (AlarmBase) Alarm library alarm or None if not supported/found
+        """
+        from voltha.extensions.alarms.onu.onu_dying_gasp_alarm import OnuDyingGaspAlarm
+        from voltha.extensions.alarms.onu.onu_los_alarm import OnuLosAlarm
+        from voltha.extensions.alarms.onu.onu_equipment_alarm import OnuEquipmentAlarm
+        from voltha.extensions.alarms.onu.onu_selftest_failure_alarm import OnuSelfTestFailureAlarm
+        from voltha.extensions.alarms.onu.onu_laser_eol_alarm import OnuLaserEolAlarm
+        from voltha.extensions.alarms.onu.onu_laser_bias_current_alarm import OnuLaserBiasAlarm
+        from voltha.extensions.alarms.onu.onu_temp_yellow_alarm import OnuTempYellowAlarm
+        from voltha.extensions.alarms.onu.onu_temp_red_alarm import OnuTempRedAlarm
+        from voltha.extensions.alarms.onu.onu_voltage_yellow_alarm import OnuVoltageYellowAlarm
+        from voltha.extensions.alarms.onu.onu_voltage_red_alarm import OnuVoltageRedAlarm
+        from voltha.extensions.alarms.onu.onu_low_rx_optical_power_alarm import OnuLowRxOpticalAlarm
+        from voltha.extensions.alarms.onu.onu_high_rx_optical_power_alarm import OnuHighRxOpticalAlarm
+        from voltha.extensions.alarms.onu.onu_low_tx_optical_power_alarm import OnuLowTxOpticalAlarm
+        from voltha.extensions.alarms.onu.onu_high_tx_optical_power_alarm import OnuHighTxOpticalAlarm
+
+        mgr = self._alarm_manager
+        if class_id in (CircuitPack.class_id, PptpEthernetUni.class_id):
+            intf_id = self.select_uni_port(class_id, entity_id)
+
+        elif class_id in (AniG.class_id, OntG.class_id):
+            intf_id = self.select_ani_port(class_id, entity_id)
+
+        else:
+            self.log.error('unsupported-class-id', class_id=class_id, alarm_number=alarm_number)
+            return
+
+        alarm_map = {
+            (CircuitPack.class_id, 0): OnuEquipmentAlarm,
+            (CircuitPack.class_id, 2): OnuSelfTestFailureAlarm,
+            (CircuitPack.class_id, 3): OnuLaserEolAlarm,
+            (CircuitPack.class_id, 4): OnuTempYellowAlarm,
+            (CircuitPack.class_id, 5): OnuTempRedAlarm,
+
+            (PptpEthernetUni.class_id, 0): OnuLosAlarm,
+
+            (OntG.class_id, 0): OnuEquipmentAlarm,
+            (OntG.class_id, 6): OnuSelfTestFailureAlarm,
+            (OntG.class_id, 7): OnuDyingGaspAlarm,
+            (OntG.class_id, 8): OnuTempYellowAlarm,
+            (OntG.class_id, 9): OnuTempRedAlarm,
+            (OntG.class_id, 10): OnuVoltageYellowAlarm,
+            (OntG.class_id, 11): OnuVoltageRedAlarm,
+
+            (AniG.class_id, 0): OnuLowRxOpticalAlarm,
+            (AniG.class_id, 1): OnuHighRxOpticalAlarm,
+            (AniG.class_id, 4): OnuLowTxOpticalAlarm,
+            (AniG.class_id, 5): OnuHighTxOpticalAlarm,
+            (AniG.class_id, 6): OnuLaserBiasAlarm,
+        }
+        alarm_cls = alarm_map.get((class_id, alarm_number))
+
+        return alarm_cls(mgr, self._onu_id, intf_id) if alarm_cls is not None else None
+
+    def select_uni_port(self, class_id, entity_id):
+        """
+        Select the best possible UNI Port (logical) interface number for this ME class and
+        entity ID.
+
+        This base implementation will assume that a UNI Port object has been registered
+        on startup and supports both an 'entity_id' and also 'logical_port_number'
+        property.  See both the Adtran and BroadCom OpenOMCI ONU DA for an example
+        of this UNI port object.
+
+        :param class_id: (int)  ME Class ID for which the alarms belongs to
+        :param entity_id: (int) Instance ID
+
+        :return: (int) Logical Port number for the UNI port
+        """
+        # NOTE: Of the three class ID's supported in this version of code, only the CircuitPack,
+        #       and PptpEthernetUni MEs will map to the UNI port
+        assert class_id in (CircuitPack.class_id, PptpEthernetUni.class_id)
+
+        return next((uni.logical_port_number for uni in self._uni_ports if
+                     uni.entity_id == entity_id), None)
+
+    def select_ani_port(self, class_id, _entity_id):
+        """
+        Select the best possible ANI Port (physical) interface number for this ME class and
+        entity ID.
+
+        Currently the base implementation assumes only a single PON port and it will be
+        chosen.  A future implementation may want to have a PON Port object (similar to
+        the BroadCom Open OMCI and Adtran ONU's UNI Port object) that provides a match
+        for entity ID.  This does assume that the PON port object supports a property
+        of 'port_number' to return the physical port number.
+
+        :param class_id: (int)  ME Class ID for which the alarms belongs to
+        :param _entity_id: (int) Instance ID
+
+        :return: (int) Logical Port number for the UNI port
+        """
+        # NOTE: Of the three class ID's supported in this version of code, only the AniG
+        #       MEs will map to the ANI port. For some the OntG alarms (Dying Gasp) the
+        #       PON interface will also be selected.
+        assert class_id in (AniG.class_id, OntG.class_id)
+
+        return self._ani_ports[0].port_number if len(self._ani_ports) else None
diff --git a/python/adapters/extensions/omci/state_machines/image_agent.py b/python/adapters/extensions/omci/state_machines/image_agent.py
new file mode 100755
index 0000000..e6d5884
--- /dev/null
+++ b/python/adapters/extensions/omci/state_machines/image_agent.py
@@ -0,0 +1,1024 @@
+#
+# 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.
+#
+import os
+import structlog
+from datetime import datetime, timedelta
+from binascii import crc32, hexlify
+from transitions import Machine
+from transitions.extensions.nesting import HierarchicalMachine as HMachine
+from twisted.python import failure
+from twisted.internet import reactor
+from twisted.internet.defer import Deferred, CancelledError
+from common.event_bus import EventBusClient
+from voltha.protos.voltha_pb2 import ImageDownload
+from voltha.protos.omci_mib_db_pb2 import OpenOmciEventType
+from voltha.extensions.omci.omci_defs import EntityOperations, ReasonCodes, AttributeAccess, OmciSectionDataSize
+from voltha.extensions.omci.omci_entities import SoftwareImage
+from voltha.extensions.omci.omci_cc import DEFAULT_OMCI_TIMEOUT
+from voltha.extensions.omci.omci_messages import OmciEndSoftwareDownloadResponse, OmciActivateImageResponse
+
+###################################################################################
+##              OLT out-of-band download image procedure
+###################################################################################
+
+class ImageDownloadeSTM(object):
+    DEFAULT_STATES = ['disabled', 'downloading', 'validating', 'done']
+    DEFAULT_TRANSITIONS = [
+        {'trigger': 'start', 'source': 'disabled', 'dest': 'downloading'},
+        {'trigger': 'stop',  'source': ['downloading', 'validating', 'done'], 'dest': 'disabled'},
+        {'trigger': 'dw_success', 'source': 'downloading', 'dest': 'validating'},
+        {'trigger': 'dw_fail', 'source': 'downloading', 'dest': 'done'},
+        {'trigger': 'validate_success', 'source': 'validating', 'dest': 'done'},
+    ]
+    DEFAULT_TIMEOUT_RETRY = 1000      # Seconds to delay after task failure/timeout
+
+    # def __init__(self, omci_agent, dev_id, local_name, local_dir, remote_url, download_task, 
+    def __init__(self, omci_agent, image_download, 
+                     download_task_cls, 
+                     states=DEFAULT_STATES,
+                     transitions=DEFAULT_TRANSITIONS,
+                     initial_state='disabled',
+                     timeout_delay=DEFAULT_TIMEOUT_RETRY,
+                     advertise_events=True, clock=None):
+        """
+        :Param: omci_agent:	(OpenOMCIAgent)
+        :Param: image_dnld: (ImageDownload)
+                            ImageDownload.id  : device id
+                            ImageDownload.name: file name of the image 
+                            ImageDownload.url : URL to download the image from server
+                            ImageDownload.local_dir: local directory of the image file
+        """
+        self.log = structlog.get_logger(device_id=image_download.id)
+        self._agent = omci_agent
+        # self._imgdw = ImageDownload()
+        # self._imgdw.name = local_name
+        # self._imgdw.id   = dev_id
+        # self._imgdw.url  = remote_url
+        # self._imgdw.local_dir = local_dir
+        self._imgdw = image_download
+        # self._imgdw.state = ImageDownload.DOWNLOAD_UNKNOWN   # voltha_pb2
+
+        self._download_task_cls = download_task_cls
+        self._timeout_delay   = timeout_delay
+
+        self._current_task  = None
+        self._task_deferred = None
+        self._ret_deferred  = None
+        self._timeout_dc = None    # DelayedCall
+        self._advertise_events = advertise_events
+        self.reactor = clock if clock is not None else reactor 
+
+        self.log.debug("ImageDownloadeSTM", image_download=self._imgdw)
+        self.machine = Machine(model=self, states=states,
+                               transitions=transitions,
+                               initial=initial_state,
+                               queued=True,
+                               name='{}-{}'.format(self.__class__.__name__, self._imgdw.id))
+    # @property
+    # def name(self):
+    #     return self._imgdw.name
+
+    def _cancel_timeout(self):
+        d, self._timeout_dc = self._timeout_dc, None
+        if d is not None and not d.called:
+            d.cancel()
+
+    @property
+    def status(self):
+        return self._imgdw
+
+    @property
+    def deferred(self):
+        return self._ret_deferred
+        
+    def advertise(self, event, info):
+        """Advertise an event on the OpenOMCI event bus"""
+        if self._advertise_events:
+            self._agent.advertise(event,
+                                  {
+                                      'state-machine': self.machine.name,
+                                      'info': info,
+                                      'time': str(datetime.utcnow())
+                                  })
+
+    # def reset(self):
+    #     """
+    #     Reset all the state machine to intial state
+    #     It is used to clear failed result in last downloading
+    #     """
+    #     self.log.debug('reset download', image_download=self._imgdw)
+    #     if self._current_task is not None:
+    #         self._current_task.stop()
+            
+    #     self._cancel_deferred()
+        
+    #     if self._ret_deferred is not None:
+    #         self._ret_deferred.cancel()
+    #         self._ret_deferred = None
+
+    #     self.stop()
+    #     self._imgdw.state = ImageDownload.DOWNLOAD_UNKNOWN
+        
+    def get_file(self):
+        """
+          return a Deferred object
+          Caller will register a callback to the Deferred to get notified once the image is available
+        """
+        # self.log.debug('get_file', image_download=self._imgdw)
+        if self._ret_deferred is None or self._ret_deferred.called:
+            self._ret_deferred = Deferred()
+            
+        if self._imgdw.state == ImageDownload.DOWNLOAD_SUCCEEDED:
+            self.log.debug('Image Available')
+            self.reactor.callLater(0, self._ret_deferred.callback, self._imgdw)
+        elif self._imgdw.state == ImageDownload.DOWNLOAD_FAILED or self._imgdw.state == ImageDownload.DOWNLOAD_UNSUPPORTED:
+            self.log.debug('Image not exist')
+            self.reactor.callLater(0, self._ret_deferred.errback, failure.Failure(Exception('Image Download Failed ' + self._imgdw.name)))
+        elif self._imgdw.state == ImageDownload.DOWNLOAD_UNKNOWN or self._imgdw.state == ImageDownload.DOWNLOAD_REQUESTED:
+            self.log.debug('Start Image STM')
+            self._imgdw.state = ImageDownload.DOWNLOAD_STARTED
+            self.reactor.callLater(0, self.start)
+        else:
+            self.log.debug('NO action', state=self._imgdw.state)
+            
+        return self._ret_deferred
+            
+    def timeout(self):
+        self.log.debug('Image Download Timeout', download_task=self._current_task);
+        if self._current_task:
+            self.reactor.callLater(0, self._current_task.stop)
+        # if self._task_deferred is not None and not self._task_deferred.called:
+        #     self._task_deferred.cancel()
+            self._current_task = None
+        # else:
+        #     self.dw_fail()
+            
+    def on_enter_downloading(self):
+        self.log.debug("on_enter_downloading")
+        self.advertise(OpenOmciEventType.state_change, self.state)
+        def success(results):
+            self.log.debug('image-download-success', results=results)
+            self._imgdw.state = ImageDownload.DOWNLOAD_SUCCEEDED
+            self._imgdw.reason = ImageDownload.NO_ERROR
+            self._current_task = None
+            self._task_deferred = None
+            self.dw_success()
+
+        def failure(reason):
+            self.log.info('image-download-failure', reason=reason)
+            if self._imgdw.state == ImageDownload.DOWNLOAD_STARTED:
+                self._imgdw.state = ImageDownload.DOWNLOAD_FAILED
+            if isinstance(reason, CancelledError): 
+                self._imgdw.reason = ImageDownload.CANCELLED
+            self._current_task = None
+            self._task_deferred = None
+            self.dw_fail()
+
+        self._device = self._agent.get_device(self._imgdw.id)
+        self._current_task = self._download_task_cls(self._agent, self._imgdw, self.reactor)
+
+        self._task_deferred = self._device.task_runner.queue_task(self._current_task)
+        self._task_deferred.addCallbacks(success, failure)
+        self._imgdw.state = ImageDownload.DOWNLOAD_STARTED
+
+        if self._timeout_delay > 0:
+            self._timeout_dc = self.reactor.callLater(self._timeout_delay, self.timeout)
+
+    def on_enter_validating(self):
+        self.log.debug("on_enter_validating")
+        self.advertise(OpenOmciEventType.state_change, self.state)
+        self.validate_success()
+
+    def on_enter_done(self):
+        self.log.debug("on_enter_done")
+        self.advertise(OpenOmciEventType.state_change, self.state)
+        self._cancel_timeout()
+
+        d, self._ret_deferred = self._ret_deferred, None
+        if d is not None:
+            if self._imgdw.state == ImageDownload.DOWNLOAD_SUCCEEDED:
+                self.reactor.callLater(0, d.callback, self._imgdw)
+            else:  # failed
+                if self._imgdw.reason == ImageDownload.CANCELLED:
+                    self.reactor.callLater(0, d.cancel)
+                else:
+                    self.reactor.callLater(0, d.errback, failure.Failure(Exception('Image Download Failed ' + self._imgdw.name)))
+            
+    def on_enter_disabled(self):
+        self.log.debug("on_enter_disabled")
+        self.advertise(OpenOmciEventType.state_change, self.state)
+
+        self._cancel_timeout()
+        if self._current_task is not None:
+            self.reactor.callLater(0, self._current_task.stop)
+            self._current_task = None
+
+        if self._ret_deferred:
+            self.reactor.callLater(0, self._ret_deferred.cancel)
+            self._ret_deferred = None
+            
+        # remove local file fragments if download failed
+        file_path = self._imgdw.local_dir + '/' + self._imgdw.name
+        if self._imgdw.state != ImageDownload.DOWNLOAD_SUCCEEDED and os.path.exists(file_path):
+            os.remove(file_path)            
+        self._imgdw.state = ImageDownload.DOWNLOAD_UNKNOWN
+
+###################################################################################
+##              OMCI Software Image Download Procedure
+###################################################################################
+
+class OmciSoftwareImageDownloadSTM(object):
+    
+    OMCI_SWIMG_DOWNLOAD_TIMEOUT = 5400      # TODO: Seconds for the full downloading procedure to avoid errors that cause infinte downloading
+    OMCI_SWIMG_DOWNLOAD_WINDOW_SIZE = 32
+    OMCI_SWIMG_WINDOW_RETRY_MAX = 2
+    OMCI_SWIMG_ACTIVATE_RETRY_MAX = 2
+    OMCI_SWIMG_ACTIVATE_TRANSITIONS_TIMEOUT = 10      # Seconds to delay after task failure/timeout
+
+    # def __init__(self, omci_agent, dev_id, img_path, 
+    def __init__(self, image_id, omci_agent, image_dnld,
+                     window_size=OMCI_SWIMG_DOWNLOAD_WINDOW_SIZE,
+                     timeout_delay=OMCI_SWIMG_DOWNLOAD_TIMEOUT,
+                     advertise_events=True,
+                     clock=None):
+        """
+        omci_agent:	(OpenOMCIAgent)
+        image_dnld: (ImageDownload)
+                    ImageDownload.id  : device id
+                    ImageDownload.name: file name of the image 
+                    ImageDownload.url : URL to download the image from server
+                    ImageDownload.local_dir: local directory of the image file
+        window_size: window size of OMCI download procedure 
+        """
+        self.log = structlog.get_logger(device_id=image_dnld.id)
+        self._omci_agent = omci_agent
+        self._image_download = image_dnld
+        self._timeout = timeout_delay
+        self._timeout_dc = None
+        self._window_size = window_size
+        self.reactor = clock if clock is not None else reactor
+        self._offset = 0
+        # self._win_section = 0
+        self._win_retry = 0
+        self._device_id = image_dnld.id
+        self._device = omci_agent.get_device(image_dnld.id)
+        self.__init_state_machine()
+        self._ret_deferred = None
+        self._image_id = image_id    # Target software image entity ID
+        self._image_file = image_dnld.local_dir + '/' + image_dnld.name
+        self._image_obj = open(self._image_file, mode='rb')
+        self._image_size = os.path.getsize(self._image_file)
+        self._crc32 = 0
+        self._win_crc32 = 0
+        self._win_data = None
+        self._current_deferred = None
+        self._result = None    # ReasonCodes
+        self.crctable = []
+        self._crctable_init = False
+        self._actimg_retry_max = OmciSoftwareImageDownloadSTM.OMCI_SWIMG_ACTIVATE_RETRY_MAX
+        self._actimg_retry = 0
+        self.log.debug("DownloadSTM", image=self._image_file, image_size=self._image_size)
+ 
+    def __init_state_machine(self):
+
+        #### Download Window Sub State Machine ####
+        OMCI_DOWNLOAD_WINDOW_STATE = ['init_window', 'sending_sections', 'window_success', 'window_failed']
+        OMCI_DOWNLOAD_WINDOW_TRANSITIONS = [
+            {'trigger': 'send_sections',  'source': 'init_window',     'dest': 'sending_sections'},
+            # {'trigger': 'send_section_last',  'source': 'start_section', 'dest': 'last_section'  },
+            {'trigger': 'rx_ack_success', 'source': 'sending_sections', 'dest': 'window_success' },
+            {'trigger': 'rx_ack_failed',  'source': 'sending_sections', 'dest': 'window_failed'  },
+            # {'trigger': 'retry_window',   'source': 'window_failed', 'dest': 'start_section'  },
+            {'trigger': 'reset_window',   'source': '*',               'dest': 'init_window'    }
+        ]    
+        self.win_machine = HMachine(model=self, 
+                                    states=OMCI_DOWNLOAD_WINDOW_STATE,
+                                    transitions=OMCI_DOWNLOAD_WINDOW_TRANSITIONS,
+                                    initial='init_window',
+                                    queued=True,
+                                    name='{}-window_section_machine'.format(self.__class__.__name__))
+
+        #### Software Activation Sub State Machine ####
+        OMCI_SWIMG_ACTIVATE_STATES = ['init_act', 'activating', 'busy', 'rebooting', 'committing', 'done', 'failed']
+        OMCI_SWIMG_ACTIVATE_TRANSITIONS = [
+            {'trigger': 'activate', 'source': ['init_act', 'busy'], 'dest': 'activating'},
+            {'trigger': 'onu_busy', 'source': 'activating', 'dest': 'busy'},
+            {'trigger': 'reboot',   'source': 'activating', 'dest': 'rebooting'},
+            {'trigger': 'do_commit', 'source': ['activating', 'rebooting'], 'dest': 'committing'},
+            # {'trigger': 'commit_ok', 'source': 'committing', 'dest': 'done'},
+            {'trigger': 'reset_actimg', 'source': ['activating', 'rebooting', 'committing', 'failed'], 'dest': 'init_act'},
+            # {'trigger': 'actimg_fail', 'source': ['init_act', 'activating', 'rebooting', 'committing'], 'dest': 'failed'}
+        ]
+        
+        self.activate_machine = HMachine(model=self, 
+                                         states=OMCI_SWIMG_ACTIVATE_STATES,
+                                         transitions=OMCI_SWIMG_ACTIVATE_TRANSITIONS,
+                                         initial='init_act',
+                                         queued=True,
+                                         name='{}-activate_machine'.format(self.__class__.__name__))
+                                   
+        #### Main State Machine ####
+        OMCI_SWIMG_DOWNLOAD_STATES = [ 'init_image', 'starting_image', 'ending_image', 'endimg_busy', 'done_image',
+                                      {'name': 'dwin', 'children': self.win_machine}, 
+                                      {'name': 'actimg', 'children': self.activate_machine}
+                                     ]
+        OMCI_SWIMG_DOWNLOAD_TRANSITIONS = [
+            {'trigger': 'start_image',      'source': 'init_image',     'dest': 'starting_image' },
+            {'trigger': 'download_window',  'source': 'starting_image', 'dest': 'dwin_init_window' },
+            {'trigger': 'download_success', 'source': 'dwin',           'dest': 'ending_image'   },
+            {'trigger': 'onu_busy',         'source': 'ending_image',   'dest': 'endimg_busy'    },
+            {'trigger': 'retry_endimg',     'source': 'endimg_busy',    'dest': 'ending_image'   },
+            {'trigger': 'end_img_success',  'source': 'ending_image',   'dest': 'actimg_init_act'  },
+            {'trigger': 'activate_done',    'source': 'actimg',         'dest': 'done_image'     },
+            {'trigger': 'download_fail',    'source': '*',              'dest': 'done_image'     },
+            {'trigger': 'reset_image',      'source': '*',              'dest': 'init_image'     },
+        ]
+        
+        self.img_machine = HMachine(model=self, 
+                                   states=OMCI_SWIMG_DOWNLOAD_STATES,
+                                   transitions=OMCI_SWIMG_DOWNLOAD_TRANSITIONS,
+                                   initial='init_image',
+                                   queued=True,
+                                   name='{}-image_download_machine'.format(self.__class__.__name__))
+
+    # @property
+    # def image_filename(self):
+    #     return self._image_file
+
+    # @image_filename.setter
+    # def image_filename(self, value):
+    #     if self._image_fd is not None:
+    #         self._image_fd.close()
+    #     self._image_filename = value
+    #     self._image_fd = open(self._image_filename, mode='rb')
+    #     self._image_size = os.path.getsize(self._image_filename)
+    #    print("Set image file: " + self._image_filename + " size: " + str(self._image_size))
+
+    def __omci_start_download_resp_success(self, rx_frame):
+        self.log.debug("__omci_download_resp_success")
+        self.download_window()
+        return rx_frame
+
+    def __omci_start_download_resp_fail(self, fail):
+        self.log.debug("__omci_download_resp_fail", failure=fail)
+        self._result = ReasonCodes.ProcessingError
+        self.download_fail()
+
+    def __omci_end_download_resp_success(self, rx_frame):
+        self.log.debug("__omci_end_download_resp_success")
+        if rx_frame.fields['message_type'] == OmciEndSoftwareDownloadResponse.message_id: # 0x35
+            omci_data = rx_frame.fields['omci_message']
+            if omci_data.fields['result'] == 0: 
+                self.log.debug('OMCI End Image OK')
+                self._result = ReasonCodes.Success
+                self.end_img_success()
+            elif omci_data.fields['result'] == 6: # Device Busy
+                self.log.debug('OMCI End Image Busy')
+                self.onu_busy()
+            else:
+                self.log.debug('OMCI End Image Failed', reason=omci_data.fields['result'])
+        else:
+            self.log.debug('Receive Unexpected OMCI', message_type=rx_frame.fields['message_type'])
+
+    def __omci_end_download_resp_fail(self, fail):
+        self.log.debug("__omci_end_download_resp_fail", failure=fail)
+        self._result = ReasonCodes.ProcessingError
+        self.download_fail()
+    
+    def __omci_send_window_resp_success(self, rx_frame, cur_state, datasize):
+        # self.log.debug("__omci_send_window_resp_success", current_state=cur_state)
+        self._offset += datasize
+        self._image_download.downloaded_bytes += datasize
+        self.rx_ack_success()
+
+    def __omci_send_window_resp_fail(self, fail, cur_state):
+        self.log.debug("__omci_send_window_resp_fail", current_state=cur_state)
+        self.rx_ack_failed()
+
+    def __activate_resp_success(self, rx_frame):
+        self._current_deferred = None
+        if rx_frame.fields['message_type'] == OmciActivateImageResponse.message_id: # 0x36
+            omci_data = rx_frame.fields['omci_message']
+            if omci_data.fields['result'] == 0:
+                self.log.debug("Activate software image success, rebooting ONU ...", device_id=self._device.device_id,
+                                state=self._image_download.image_state)
+                standby_image_id = 0 if self._image_id else 1
+                self._omci_agent.database.set(self._device.device_id, SoftwareImage.class_id, self._image_id, 	{"is_active": 1})
+                self._omci_agent.database.set(self._device.device_id, SoftwareImage.class_id, standby_image_id, {"is_active": 0})
+                self.reboot()
+            elif omci_data.fields['result'] == 6: # Device Busy
+                self.log.debug('OMCI Activate Image Busy')
+                self.onu_busy()
+            else:
+                self.log.debug('OMCI Activate Image Failed', reason=omci_data['result'])
+        else:
+            self.log.debug('Receive Unexpected OMCI', message_type=rx_frame['message_type'])
+                
+    def __activate_fail(self, fail):
+        self.log.debug("Activate software image failed", faile=fail)
+        self._current_deferred = None
+        self._result = ReasonCodes.ProcessingError
+        self.activate_done()
+        
+    def __commit_success(self, rx_frame):
+        self.log.debug("Commit software success", device_id=self._device_id)
+        self._current_deferred = None
+        standby_image_id = 0 if self._image_id else 1
+        self._omci_agent.database.set(self._device_id, SoftwareImage.class_id, self._image_id, {"is_committed": 1})
+        self._omci_agent.database.set(self._device_id, SoftwareImage.class_id, standby_image_id, {"is_committed": 0})
+        self._image_download.image_state = ImageDownload.IMAGE_ACTIVE
+        self._result = ReasonCodes.Success
+        self.activate_done()
+
+    def __commit_fail(self, fail):
+        self.log.debug("Commit software image failed", faile=fail)
+        self._current_deferred = None
+        self._result = ReasonCodes.ProcessingError
+        self._image_download.image_state = ImageDownload.IMAGE_REVERT
+        self.activate_done()
+
+#    @property
+#    def image_id(self):
+#        return self._image_id
+
+#    @image_id.setter
+#    def image_id(self, value):
+#        self._image_id = value
+
+    @property
+    def status(self):
+        return self._image_download
+
+    def start(self):
+        self.log.debug("OmciSoftwareImageDownloadSTM.start", current_state=self.state)
+        if self._ret_deferred is None:
+            self._ret_deferred = Deferred()
+        if self.state == 'init_image':
+            self.reactor.callLater(0, self.start_image)
+        return self._ret_deferred
+
+    def stop(self):
+        self.log.debug("OmciSoftwareImageDownloadSTM.stop", current_state=self.state)
+        self._result = ReasonCodes.OperationCancelled
+        self.download_fail()
+        
+    def on_enter_init_image(self):
+        self.log.debug("on_enter_init_image")
+        self._image_obj.seek(0)
+        self._offset = 0
+        # self._win_section = 0
+        self._win_retry   = 0
+        
+    def on_enter_starting_image(self):
+        self.log.debug("on_enter_starting_image")
+        self._image_download.downloaded_bytes = 0
+        self._current_deferred = self._device.omci_cc.send_start_software_download(self._image_id, self._image_size, self._window_size)
+        self._current_deferred.addCallbacks(self.__omci_start_download_resp_success, self.__omci_start_download_resp_fail)
+                                            # callbackArgs=(self.state,), errbackArgs=(self.state,))
+
+    def on_enter_dwin_init_window(self):
+        # self.log.debug("on_enter_dwin_init_window", offset=self._offset, image_size=self._image_size)
+        if self._offset < self._image_size:
+            self.send_sections()
+
+    def on_enter_dwin_sending_sections(self):
+        # self.log.debug("on_enter_dwin_sending_sections", offset=self._offset)
+
+        if (self._offset + self._window_size * OmciSectionDataSize) <= self._image_size:
+            sections = self._window_size
+            mod = 0
+            datasize = self._window_size * OmciSectionDataSize
+        else:
+            datasize = self._image_size - self._offset
+            sections = datasize / OmciSectionDataSize
+            mod = datasize % OmciSectionDataSize
+            sections = sections + 1 if mod > 0 else sections
+
+        # self.log.debug("on_enter_dwin_sending_sections", offset=self._offset, datasize=datasize, sections=sections)
+        if self._win_retry == 0:
+            self._win_data = self._image_obj.read(datasize)
+            self._win_crc32 = self.crc32(self._crc32, self._win_data)
+            # self.log.debug("CRC32", crc32=self._win_crc32, offset=self._offset)
+        else:
+            self.log.debug("Retry download window with crc32", offset=self._offset)
+            
+        sent = 0
+        for i in range(0, sections):
+            if i < sections - 1:
+                # self.log.debug("section data", data=hexlify(data[(self._offset+sent):(self._offset+sent+OmciSectionDataSize)]))
+                self._device.omci_cc.send_download_section(self._image_id, i,
+                                                           self._win_data[sent:sent+OmciSectionDataSize])
+                sent += OmciSectionDataSize
+            else:
+                last_size = OmciSectionDataSize if mod == 0 else mod
+                self._current_deferred = self._device.omci_cc.send_download_section(self._image_id, i,
+                                                           self._win_data[sent:sent+last_size],
+                                                           timeout=DEFAULT_OMCI_TIMEOUT)
+                self._current_deferred.addCallbacks(self.__omci_send_window_resp_success, self.__omci_send_window_resp_fail,
+                                                    callbackArgs=(self.state, datasize), errbackArgs=(self.state,))
+                sent += last_size
+                assert sent==datasize
+
+    # def on_enter_dwin_last_section(self):
+    #     self._current_deferred = self._device.omci_cc.send_download_section, self._instance_id, self._win_section, data)
+    #     self._current_deferred.addCallbacks(self.__omci_resp_success, self.__omci_resp_fail,
+    #                                         callbackArgs=(self.state,), errbackArgs=(self.state,))
+
+    def on_enter_dwin_window_success(self):
+        # self.log.debug("on_enter_dwin_window_success")
+        self._crc32 = self._win_crc32 if self._win_crc32 != 0 else self._crc32
+        self._win_crc32 = 0
+        self._win_retry = 0
+        if self._offset < self._image_size:
+            self.reset_window()
+        else:
+            self.download_success()
+
+    def on_enter_dwin_window_failed(self):
+        self.log.debug("on_enter_dwin_window_fail: ", retry=self._win_retry)
+        if self._win_retry < self.OMCI_SWIMG_WINDOW_RETRY_MAX:
+            self._win_retry += 1
+            self.reset_window()
+        else:
+            self._result = ReasonCodes.ProcessingError
+            self.download_fail()
+
+    def on_enter_ending_image(self):
+        self.log.debug("on_enter_ending_image", crc32=self._crc32)
+        self._current_deferred = self._device.omci_cc.send_end_software_download(self._image_id, self._crc32, 
+                                                                                 self._image_size, timeout=18)
+        self._current_deferred.addCallbacks(self.__omci_end_download_resp_success, self.__omci_end_download_resp_fail)
+                                            # callbackArgs=(self.state,), errbackArgs=(self.state,))
+
+    def on_enter_endimg_busy(self):
+        self.log.debug("on_enter_endimg_busy")
+        self.reactor.callLater(3, self.retry_endimg)
+
+    def on_enter_actimg_init_act(self):
+        self.log.debug("on_enter_actimg_init_act", retry=self._actimg_retry, max_retry=self._actimg_retry_max)
+        # self._images[0] = self._omci_agent.database.query(self._device_id, SoftwareImage.class_id, 0, ["is_active", "is_committed", "is_valid"])
+        # self._images[1] = self._omci_agent.database.query(self._device_id, SoftwareImage.class_id, 1, ["is_active", "is_committed", "is_valid"])
+        # if (self._images[self._to_image]["is_active"] != 1 and self._images[self._to_image]["is_valid"] == 1):
+        if self._actimg_retry > self._actimg_retry_max:
+            self.log.debug("activate image failed: retry max", retries=self._actimg_retry)
+            self._result = ReasonCodes.ProcessingError
+            self.activate_done()
+        else:
+            self._image_download.image_state = ImageDownload.IMAGE_ACTIVATE
+            self.activate()
+            
+    def on_enter_actimg_activating(self):
+        self.log.debug("on_enter_actimg_activating")
+        img = self._omci_agent.database.query(self._device_id, SoftwareImage.class_id, 
+                                              self._image_id, ["is_active", "is_committed", "is_valid"])
+                                              
+        self.log.debug("on_enter_actimg_activating", instance=self._image_id, state=img)
+        if img["is_active"] == 0:
+            #if img["is_valid"] == 1:
+            self._current_deferred = self._device.omci_cc.send_active_image(self._image_id)
+            self._current_deferred.addCallbacks(self.__activate_resp_success, self.__activate_fail)
+            #else:
+            #    self.fail()
+        else:
+            self.do_commit()
+
+    def on_enter_actimg_busy(self):
+        self.log.debug("on_enter_actimg_busy")
+        self.reactor.callLater(3, self.activate)
+        
+    def __on_reboot_timeout(self):
+        self.log.debug("on_reboot_timeout")
+        self._timeout_dc = None
+        self._result = ReasonCodes.ProcessingError
+        self.activate_done()
+        
+    def on_enter_actimg_rebooting(self):
+        self.log.debug("on_enter_actimg_rebooting")
+        if self._timeout_dc == None:
+            self._timeout_dc = self.reactor.callLater(self._timeout, self.__on_reboot_timeout)
+
+    def on_exit_actimg_rebooting(self):
+        self.log.debug("on_exit_actimg_rebooting", timeout=self._timeout_dc)
+        if self._timeout_dc and self._timeout_dc.active:
+            self._timeout_dc.cancel()
+            self._timeout_dc = None
+    
+    def on_enter_actimg_committing(self):
+        # self.log.debug("on_enter_committing")
+        img = self._omci_agent.database.query(self._device_id, SoftwareImage.class_id, 
+                                              self._image_id, ["is_active", "is_committed", "is_valid"])
+        self.log.debug("on_enter_actimg_committing", instance=self._image_id, state=img)
+        if (img['is_active'] == 0):
+            self._actimg_retry += 1
+            self.log.debug("do retry", retry=self._actimg_retry)
+            self.reset_actimg()
+        else:
+            self._actimg_retry = 0
+            self._current_deferred = self._device.omci_cc.send_commit_image(self._image_id)
+            self._current_deferred.addCallbacks(self.__commit_success, self.__commit_fail)
+
+    def on_enter_done_image(self):
+        self.log.debug("on_enter_done_image", result=self._result)
+        if self._result == ReasonCodes.Success:
+            self.reactor.callLater(0, self._ret_deferred.callback, self._image_download) # (str(self._instance_id))
+        else:
+            self._ret_deferred.errback(failure.Failure(Exception('ONU Software Download Failed, instance ' + str(self._image_id))))
+
+    def __crc_GenTable32(self):
+        if self._crctable_init:
+            return
+            
+        #  x32 + x26 + x23 + x22 + x16 + x12 + x11 + x10 + x8 + x7 + x5 + x4 + x2 + x + 1   
+        pn32 = [0, 1, 2, 4, 5, 7, 8, 10, 11, 12, 16, 22, 23, 26]
+        poly = 0
+        for i in pn32:
+            poly |= (1 << i)
+
+        for i in range(0, 256):
+            _accum = (i << 24) & 0xFFFFFFFF
+            for j in range(0, 8):
+                if _accum & (1 << 31):
+                    _accum = (_accum << 1) ^ poly
+                else:
+                    _accum = (_accum << 1) & 0xFFFFFFFF
+            # self.crctable[i] = accum
+            self.crctable.append(_accum)
+        self._crctable_init = True
+            
+    def crc32(self, accum, data):
+        self.__crc_GenTable32()
+        _accum = ~accum & 0xFFFFFFFF
+        num = len(data)
+        for i in range(0, num):
+            _accum = self.crctable[((_accum >> 24) ^ ord(data[i])) & 0xFF] ^ ((_accum << 8) & 0xFFFFFFFF)
+
+        return ~_accum & 0xFFFFFFFF
+
+###################################################################################
+##              OMCI Software Image Activation/Committing Procedure
+###################################################################################
+'''
+class OmciSoftwareImageActivateSTM(object):
+    OMCI_SWIMG_ACTIVATE_STATES = ['starting', 'activating', 'busy', 'rebooting', 'committing', 'done', 'failed']
+    OMCI_SWIMG_ACTIVATE_TRANSITIONS = [
+        {'trigger': 'activate', 'source': ['starting', 'busy'], 'dest': 'activating'},
+        {'trigger': 'onu_busy', 'source': 'activating', 'dest': 'busy'},
+        {'trigger': 'reboot',   'source': 'activating', 'dest': 'rebooting'},
+        {'trigger': 'do_commit', 'source': ['activating', 'rebooting'], 'dest': 'committing'},
+        {'trigger': 'commit_ok', 'source': 'committing', 'dest': 'done'},
+        {'trigger': 'reset',    'source': ['activating', 'rebooting', 'committing', 'failed'], 'dest': 'starting'},
+        {'trigger': 'fail',     'source': ['starting', 'activating', 'rebooting', 'committing'], 'dest': 'failed'}
+    ]
+    OMCI_SWIMG_ACTIVATE_TRANSITIONS_TIMEOUT = 10      # Seconds to delay after task failure/timeout
+    OMCI_SWIMG_ACTIVATE_RETRY_MAX           = 2
+    def __init__(self, omci_agent, dev_id, target_img_entity_id, image_download,
+                     states=OMCI_SWIMG_ACTIVATE_STATES,
+                     transitions=OMCI_SWIMG_ACTIVATE_TRANSITIONS,
+                     initial_state='disabled',
+                     timeout_delay=OMCI_SWIMG_ACTIVATE_TRANSITIONS_TIMEOUT,
+                     advertise_events=True,
+                     clock=None):
+        self.log = structlog.get_logger(device_id=dev_id)
+        self._omci_agent = omci_agent
+        self._device_id  = dev_id
+        self._device = omci_agent.get_device(dev_id)
+        self._to_image = target_img_entity_id
+        self._from_image = 0 if self._to_image == 1 else 1
+        self._image_download = image_download
+        # self._images = dict()
+        self._timeout = timeout_delay
+        self._timeout_dc = None
+        self.reactor = clock if clock is not None else reactor
+        self._retry_max = OmciSoftwareImageActivateSTM.OMCI_SWIMG_ACTIVATE_RETRY_MAX
+        self._retry = 0
+        self._deferred = None
+        self.ret_deferred = None
+        self.machine = Machine(model=self, 
+                               states=states,
+                               transitions=transitions,
+                               initial='starting',
+                               queued=True,
+                               name='{}-image_activate_machine'.format(self.__class__.__name__))
+        self.log.debug("OmciSoftwareImageActivateSTM", target=self._to_image)
+
+    def __activate_resp_success(self, rx_frame):
+        if rx_frame.fields['message_type'] == 0x36:  # (OmciActivateImageResponse)
+            omci_data = rx_frame.fields['omci_message']
+            if omci_data.fields['result'] == 0:
+                self.log.debug("Activate software image success, rebooting ONU ...", device_id=self._device_id) 
+                self._omci_agent.database.set(self._device_id, SoftwareImage.class_id, self._to_image, 	{"is_active": 1})
+                self._omci_agent.database.set(self._device_id, SoftwareImage.class_id, self._from_image, {"is_active": 0})
+                self.reboot()
+            elif omci_data.fields['result'] == 6: # Device Busy
+                self.log.debug('OMCI Activate Image Busy')
+                self.onu_busy()
+            else:
+                self.log.debug('OMCI Activate Image Failed', reason=omci_data['result'])
+        else:
+            self.log.debug('Receive Unexpected OMCI', message_type=rx_frame['message_type'])
+                
+    def __activate_fail(self, fail):
+        self.log.debug("Activate software image failed", faile=fail)
+        
+    def __commit_success(self, rx_frame):
+        self.log.debug("Commit software success", device_id=self._device_id)
+        self._omci_agent.database.set(self._device_id, SoftwareImage.class_id, self._to_image, {"is_committed": 1})
+        self._omci_agent.database.set(self._device_id, SoftwareImage.class_id, self._from_image, {"is_committed": 0})
+        self.commit_ok()
+
+    def __commit_fail(self, fail):
+        self.log.debug("Commit software image failed", faile=fail)
+
+    @property
+    def status(self):
+        return self._image_download
+        
+    def start(self):
+        self.log.debug("Start switch software image", target=self._to_image)
+        # self._images[0] = self._omci_agent.database.query(self._device_id, SoftwareImage.class_id, 0, ["is_active", "is_committed", "is_valid"])
+        # self._images[1] = self._omci_agent.database.query(self._device_id, SoftwareImage.class_id, 1, ["is_active", "is_committed", "is_valid"])
+        # if (self._images[self._to_image]["is_active"] == 0 and self._images[self._to_image]["is_valid"] == 1):
+        self.ret_deferred = Deferred()
+        self._image_download.image_state = ImageDownload.IMAGE_ACTIVATE
+        self.reactor.callLater(0, self.activate)
+        return self.ret_deferred
+
+    def on_enter_starting(self):
+        # self.log.debug("on_enter_starting")
+        # self._images[0] = self._omci_agent.database.query(self._device_id, SoftwareImage.class_id, 0, ["is_active", "is_committed", "is_valid"])
+        # self._images[1] = self._omci_agent.database.query(self._device_id, SoftwareImage.class_id, 1, ["is_active", "is_committed", "is_valid"])
+        # if (self._images[self._to_image]["is_active"] != 1 and self._images[self._to_image]["is_valid"] == 1):
+        if self._retry > self._retry_max:
+            self.log.debug("failed: retry max", retries=self._retry)
+            self.fail()
+        else:
+            self.activate()
+            
+    def on_enter_activating(self):
+        img = self._omci_agent.database.query(self._device_id, SoftwareImage.class_id, 
+                                              self._to_image, ["is_active", "is_committed", "is_valid"])
+                                              
+        self.log.debug("on_enter_activating", instance=self._to_image, state=img)
+        if img["is_active"] == 0:
+            #if img["is_valid"] == 1:
+            self._deferred = self._device.omci_cc.send_active_image(self._to_image)
+            self._deferred.addCallbacks(self.__activate_resp_success, self.__activate_fail)
+            #else:
+            #    self.fail()
+        else:
+            self.do_commit()
+
+    def on_enter_busy(self):
+        self.log.debug("on_enter_busy")
+        self.reactor.callLater(3, self.activate)
+        
+    def on_enter_rebooting(self):
+        self.log.debug("on_enter_rebooting")
+        if self._timeout_dc == None:
+            self._timeout_dc = self.reactor.callLater(self._timeout, self.fail)
+
+    def on_exit_rebooting(self):
+        self.log.debug("on_exit_rebooting")
+        if self._timeout_dc and self._timeout_dc.active:
+            self._timeout_dc.cancel()
+            self._timeout_dc = None
+    
+    def on_enter_committing(self):
+        # self.log.debug("on_enter_committing")
+        img = self._omci_agent.database.query(self._device_id, SoftwareImage.class_id, 
+                                              self._to_image, ["is_active", "is_committed", "is_valid"])
+        self.log.debug("on_enter_committing", instance=self._to_image, state=img)
+        if (img['is_active'] == 0):
+            self._retry += 1
+            self.log.debug("do retry", retry=self._retry)
+            self.reset()
+        else:
+            self._retry = 0
+            self._deferred = self._device.omci_cc.send_commit_image(self._to_image)
+            self._deferred.addCallbacks(self.__commit_success, self.__commit_fail)
+
+    def on_enter_done(self):
+        self.log.debug("on_enter_done")
+        self._image_download.image_state = ImageDownload.IMAGE_ACTIVE
+        self.ret_deferred.callback(self._to_image)
+
+    def on_enter_failed(self):
+        self.log.debug("on_enter_failed")
+        self._image_download.image_state = ImageDownload.IMAGE_REVERT
+        self.ret_deferred.errback(failure.Failure(Exception('ONU Software Activating Failed, instance ' + str(self._to_image))))
+'''
+
+###################################################################################
+##              Image Agent for OLT/ONT software image handling
+###################################################################################
+class ImageAgent(object):
+    """
+        Image Agent supports multiple state machines running at the same time:
+    """
+
+    DEFAULT_LOCAL_ROOT = "/"
+    
+    # def __init__(self, omci_agent, dev_id, stm_cls, img_tasks, advertise_events=True):
+    def __init__(self, omci_agent, dev_id, 
+                     dwld_stm_cls, dwld_img_tasks, 
+                     upgrade_onu_stm_cls, upgrade_onu_tasks, 
+                     # image_activate_stm_cls, 
+                     advertise_events=True, local_dir=None, clock=None):
+        """
+        Class initialization
+
+        :param omci_agent: (OpenOmciAgent) Agent
+        :param dev_id    : (str) ONU Device ID
+        :param dwld_stm_cls          : (ImageDownloadeSTM) Image download state machine class
+        :param dwld_img_tasks        : (FileDownloadTask) file download task
+        :param upgrade_onu_stm_cls   : (OmciSoftwareImageDownloadSTM) ONU Image upgrade state machine class
+        :param upgrade_onu_tasks     : ({OmciSwImageUpgradeTask})
+        # :param image_activate_stm_cls: (OmciSoftwareImageActivateSTM)
+        """
+        
+        self.log = structlog.get_logger(device_id=dev_id)
+
+        self._omci_agent = omci_agent
+        self._device_id = dev_id
+        self._dwld_stm_cls  = dwld_stm_cls
+        # self._image_download_sm = None
+        self._images = dict()
+        self._download_task_cls = dwld_img_tasks['download-file'] 
+
+        self._omci_upgrade_sm_cls = upgrade_onu_stm_cls
+        self._omci_upgrade_task_cls = upgrade_onu_tasks['omci_upgrade_task']
+        self._omci_upgrade_task = None
+        self._omci_upgrade_deferred = None
+        
+        # self._omci_activate_img_sm_cls = image_activate_stm_cls
+        # self._omci_activate_img_sm = None
+        self.reactor = clock if clock is not None else reactor
+
+        self._advertise_events = advertise_events
+        # self._local_dir = None
+
+        self._device = None
+        # onu_dev = self._omci_agent.get_device(self._device_id)
+        # assert device
+        
+        # self._local_dir = DEFAULT_LOCAL_ROOT + onu_dev.adapter_agent.name
+        # self.log.debug("ImageAgent", local_dir=self._local_dir)
+        
+        
+    def __get_standby_image_instance(self):
+        instance_id = None
+        instance_0 = self._omci_agent.database.query(self._device_id, SoftwareImage.class_id, 0, ["is_active", "is_committed"])
+        if instance_0['is_active'] == 1:
+            instance_id = 1
+        else:
+            instance_1 = self._omci_agent.database.query(self._device_id, SoftwareImage.class_id, 1, ["is_active", "is_committed"])
+            if instance_1['is_active'] == 1:
+                instance_id = 0
+        return instance_id
+
+    def __clear_task(self, arg):
+        self.__omci_upgrade_task = None
+
+    # def get_image(self, name, local_dir, remote_url, timeout_delay=ImageDownloadeSTM.DEFAULT_TIMEOUT_RETRY):
+    def get_image(self, image_download, timeout_delay=ImageDownloadeSTM.DEFAULT_TIMEOUT_RETRY):
+
+        """
+         Get named image from servers
+        :param image_download: (voltha_pb2.ImageDownload)
+        :param timeout_delay : (number) timeout for download task
+        :
+        :Return a Deferred that will be triggered if the file is locally availabe or downloaded sucessfully
+        :  Caller will register callback and errback to the returned defer to get notified
+        """
+        self.log.debug("get_image", download=image_download)
+
+        # if self._local_dir is None:
+        #     onu_dev = self._omci_agent.get_device(self._device_id)
+        #     assert onu_dev
+        #     if image_download.local_dir is None:
+        #         self._local_dir = ImageAgent.DEFAULT_LOCAL_ROOT + onu_dev.adapter_agent.name
+        #     else:
+        #         self._local_dir = image_download.local_dir + '/' + onu_dev.adapter_agent.name
+            
+            # self.log.debug("ImageAgent", local_dir=self._local_dir)
+        #     image_download.local_dir = self._local_dir
+            
+        # if os.path.isfile(self._local_dir + '/' + image_download.name): # image file exists
+        #     d = Deferred()
+        #     self.reactor.callLater(0, d.callback, image_download)
+        #     self.log.debug("Image file exists")
+        #     return d
+
+        img_dnld_sm = self._images.get(image_download.name)
+        if img_dnld_sm is None:
+            img_dnld_sm = self._dwld_stm_cls(self._omci_agent, # self._device_id, name, local_dir, remote_url, 
+                                             image_download,
+                                             self._download_task_cls,
+                                             timeout_delay=timeout_delay,
+                                             clock=self.reactor
+                                            )
+            self._images[image_download.name] = img_dnld_sm
+
+        # if self._image_download_sm is None:
+        #     self._image_download_sm = self._dwld_stm_cls(self._omci_agent, # self._device_id, name, local_dir, remote_url, 
+        #                                                  image_download,
+        #                                                  self._download_task_cls,
+        #                                                  timeout_delay=timeout_delay,
+        #                                                  clock=self.reactor
+        #                                                 )
+        # else:
+        #     if self._image_download_sm.download_status.state != ImageDownload.DOWNLOAD_SUCCEEDED:
+        #         self._image_download_sm.reset()
+            
+        d = img_dnld_sm.get_file()
+        return d
+
+    def cancel_download_image(self, name):
+        img_dnld_sm = self._images.pop(name, None)
+        if img_dnld_sm is not None:
+            img_dnld_sm.stop()
+            
+            
+    def onu_omci_download(self, image_dnld_name):
+        """
+        Start upgrading ONU.
+        image_dnld: (ImageDownload)
+        : Return Defer instance to get called after upgrading success or failed. 
+        : Or return None if image does not exist
+        """
+        self.log.debug("onu_omci_download", image=image_dnld_name)
+
+        image_dnld_sm = self._images.get(image_dnld_name)
+        if image_dnld_sm is None:
+            return None
+            
+        self._device = self._omci_agent.get_device(image_dnld_sm.status.id) if self._device is None else self._device
+
+        # if restart:
+        #     self.cancel_upgrade_onu()            
+            
+        if self._omci_upgrade_task is None:
+            img_id = self.__get_standby_image_instance()
+            self.log.debug("start task", image_Id=img_id, task=self._omci_upgrade_sm_cls)
+            self._omci_upgrade_task = self._omci_upgrade_task_cls(img_id, 
+                                                                  self._omci_upgrade_sm_cls, 
+                                                                  self._omci_agent, 
+                                                                  image_dnld_sm.status, clock=self.reactor)
+            self.log.debug("task created but not started")
+            # self._device.task_runner.start()
+            self._omci_upgrade_deferred = self._device.task_runner.queue_task(self._omci_upgrade_task)
+            self._omci_upgrade_deferred.addBoth(self.__clear_task)
+        return self._omci_upgrade_deferred
+
+
+    def cancel_upgrade_onu(self):
+        self.log.debug("cancel_upgrade_onu")
+        if self._omci_upgrade_task is not None:
+            self.log.debug("cancel_upgrade_onu", running=self._omci_upgrade_task.running)
+            # if self._omci_upgrade_task.running:
+            self._omci_upgrade_task.stop()
+            self._omci_upgrade_task = None
+        if self._omci_upgrade_deferred is not None:
+           self.reactor.callLater(0, self._omci_upgrade_deferred.cancel)
+           self._omci_upgrade_deferred = None
+           
+
+    # def activate_onu_image(self, image_name):
+    #     self.log.debug("activate_onu_image", image=image_name)
+    #     img_dnld = self.get_image_status(image_name)
+    #     if img_dnld is None:
+    #         return None
+            
+    #     img_dnld.image_state = ImageDownload.IMAGE_INACTIVE    
+    #     if self._omci_activate_img_sm is None:
+    #         self._omci_activate_img_sm = self._omci_activate_img_sm_cls(self._omci_agent, self._device_id,
+    #                                                                     self.__get_standby_image_instance(), 
+    #                                                                     img_dnld, clock=self.reactor)
+    #         return self._omci_activate_img_sm.start()
+    #     else:
+    #         return None
+            
+    def onu_bootup(self):
+        if self._omci_upgrade_task is not None:
+            self._omci_upgrade_task.onu_bootup()
+
+    def get_image_status(self, image_name):
+        """
+          Return (ImageDownload)
+        """
+        sm = self._images.get(image_name)
+        return sm.status if sm is not None else None
+    
diff --git a/python/adapters/extensions/omci/state_machines/mib_sync.py b/python/adapters/extensions/omci/state_machines/mib_sync.py
new file mode 100644
index 0000000..d257257
--- /dev/null
+++ b/python/adapters/extensions/omci/state_machines/mib_sync.py
@@ -0,0 +1,942 @@
+#
+# 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.
+#
+import structlog
+from datetime import datetime, timedelta
+from transitions import Machine
+from twisted.internet import reactor
+from voltha.extensions.omci.omci_frame import OmciFrame
+from voltha.extensions.omci.database.mib_db_api import MDS_KEY
+from voltha.extensions.omci.omci_defs import EntityOperations, ReasonCodes, \
+    AttributeAccess
+from voltha.extensions.omci.omci_cc import OmciCCRxEvents, OMCI_CC, TX_REQUEST_KEY, \
+    RX_RESPONSE_KEY
+from voltha.extensions.omci.onu_device_entry import OnuDeviceEvents, OnuDeviceEntry, \
+    SUPPORTED_MESSAGE_ENTITY_KEY, SUPPORTED_MESSAGE_TYPES_KEY
+from voltha.extensions.omci.omci_entities import OntData
+from common.event_bus import EventBusClient
+from voltha.protos.omci_mib_db_pb2 import OpenOmciEventType
+
+RxEvent = OmciCCRxEvents
+DevEvent = OnuDeviceEvents
+OP = EntityOperations
+RC = ReasonCodes
+AA = AttributeAccess
+
+
+class MibSynchronizer(object):
+    """
+    OpenOMCI MIB Synchronizer state machine
+    """
+    DEFAULT_STATES = ['disabled', 'starting', 'uploading', 'examining_mds',
+                      'in_sync', 'out_of_sync', 'auditing', 'resynchronizing']
+
+    DEFAULT_TRANSITIONS = [
+        {'trigger': 'start', 'source': 'disabled', 'dest': 'starting'},
+
+        {'trigger': 'upload_mib', 'source': 'starting', 'dest': 'uploading'},
+        {'trigger': 'examine_mds', 'source': 'starting', 'dest': 'examining_mds'},
+
+        {'trigger': 'success', 'source': 'uploading', 'dest': 'in_sync'},
+
+        {'trigger': 'success', 'source': 'examining_mds', 'dest': 'in_sync'},
+        {'trigger': 'mismatch', 'source': 'examining_mds', 'dest': 'resynchronizing'},
+
+        {'trigger': 'audit_mib', 'source': 'in_sync', 'dest': 'auditing'},
+
+        {'trigger': 'success', 'source': 'out_of_sync', 'dest': 'in_sync'},
+        {'trigger': 'audit_mib', 'source': 'out_of_sync', 'dest': 'auditing'},
+
+        {'trigger': 'success', 'source': 'auditing', 'dest': 'in_sync'},
+        {'trigger': 'mismatch', 'source': 'auditing', 'dest': 'resynchronizing'},
+        {'trigger': 'force_resync', 'source': 'auditing', 'dest': 'resynchronizing'},
+
+        {'trigger': 'success', 'source': 'resynchronizing', 'dest': 'in_sync'},
+        {'trigger': 'diffs_found', 'source': 'resynchronizing', 'dest': 'out_of_sync'},
+
+        # Do wildcard 'timeout' trigger that sends us back to start
+        {'trigger': 'timeout', 'source': '*', 'dest': 'starting'},
+
+        # Do wildcard 'stop' trigger last so it covers all previous states
+        {'trigger': 'stop', 'source': '*', 'dest': 'disabled'},
+    ]
+    DEFAULT_TIMEOUT_RETRY = 5      # Seconds to delay after task failure/timeout
+    DEFAULT_AUDIT_DELAY = 60       # Periodic tick to audit the MIB Data Sync
+    DEFAULT_RESYNC_DELAY = 300     # Periodically force a resync
+
+    def __init__(self, agent, device_id, mib_sync_tasks, db,
+                 advertise_events=False,
+                 states=DEFAULT_STATES,
+                 transitions=DEFAULT_TRANSITIONS,
+                 initial_state='disabled',
+                 timeout_delay=DEFAULT_TIMEOUT_RETRY,
+                 audit_delay=DEFAULT_AUDIT_DELAY,
+                 resync_delay=DEFAULT_RESYNC_DELAY):
+        """
+        Class initialization
+
+        :param agent: (OpenOmciAgent) Agent
+        :param device_id: (str) ONU Device ID
+        :param db: (MibDbVolatileDict) MIB Database
+        :param advertise_events: (bool) Advertise events on OpenOMCI Event Bus
+        :param mib_sync_tasks: (dict) Tasks to run
+        :param states: (list) List of valid states
+        :param transitions: (dict) Dictionary of triggers and state changes
+        :param initial_state: (str) Initial state machine state
+        :param timeout_delay: (int/float) Number of seconds after a timeout to attempt
+                                          a retry (goes back to starting state)
+        :param audit_delay: (int) Seconds between MIB audits while in sync. Set to
+                                  zero to disable audit. An operator can request
+                                  an audit manually by calling 'self.audit_mib'
+        :param resync_delay: (int) Seconds in sync before performing a forced MIB
+                                   resynchronization
+        """
+        self.log = structlog.get_logger(device_id=device_id)
+
+        self._agent = agent
+        self._device_id = device_id
+        self._device = None
+        self._database = db
+        self._timeout_delay = timeout_delay
+        self._audit_delay = audit_delay
+        self._resync_delay = resync_delay
+
+        self._upload_task = mib_sync_tasks['mib-upload']
+        self._get_mds_task = mib_sync_tasks['get-mds']
+        self._audit_task = mib_sync_tasks['mib-audit']
+        self._resync_task = mib_sync_tasks['mib-resync']
+        self._reconcile_task = mib_sync_tasks['mib-reconcile']
+        self._advertise_events = advertise_events
+
+        self._deferred = None
+        self._current_task = None  # TODO: Support multiple running tasks after v.2.0 release
+        self._task_deferred = None
+        self._mib_data_sync = 0
+        self._last_mib_db_sync_value = None
+        self._device_in_db = False
+        self._next_resync = None
+
+        self._on_olt_only_diffs = None
+        self._on_onu_only_diffs = None
+        self._attr_diffs = None
+        self._audited_olt_db = None
+        self._audited_onu_db = None
+
+        self._event_bus = EventBusClient()
+        self._omci_cc_subscriptions = {               # RxEvent.enum -> Subscription Object
+            RxEvent.MIB_Reset: None,
+            RxEvent.AVC_Notification: None,
+            RxEvent.MIB_Upload: None,
+            RxEvent.MIB_Upload_Next: None,
+            RxEvent.Create: None,
+            RxEvent.Delete: None,
+            RxEvent.Set: None,
+        }
+        self._omci_cc_sub_mapping = {
+            RxEvent.MIB_Reset: self.on_mib_reset_response,
+            RxEvent.AVC_Notification: self.on_avc_notification,
+            RxEvent.MIB_Upload: self.on_mib_upload_response,
+            RxEvent.MIB_Upload_Next: self.on_mib_upload_next_response,
+            RxEvent.Create: self.on_create_response,
+            RxEvent.Delete: self.on_delete_response,
+            RxEvent.Set: self.on_set_response,
+        }
+        self._onu_dev_subscriptions = {               # DevEvent.enum -> Subscription Object
+            DevEvent.OmciCapabilitiesEvent: None
+        }
+        self._onu_dev_sub_mapping = {
+            DevEvent.OmciCapabilitiesEvent: self.on_capabilities_event
+        }
+
+        # Statistics and attributes
+        # TODO: add any others if it will support problem diagnosis
+
+        # Set up state machine to manage states
+        self.machine = Machine(model=self, states=states,
+                               transitions=transitions,
+                               initial=initial_state,
+                               queued=True,
+                               name='{}-{}'.format(self.__class__.__name__,
+                                                   device_id))
+        try:
+            import logging
+            logging.getLogger('transitions').setLevel(logging.WARNING)
+        except Exception as e:
+            self.log.exception('log-level-failed', e=e)
+
+    def _cancel_deferred(self):
+        d1, self._deferred = self._deferred, None
+        d2, self._task_deferred = self._task_deferred, None
+
+        for d in [d1, d1]:
+            try:
+                if d is not None and not d.called:
+                    d.cancel()
+            except:
+                pass
+
+    def __str__(self):
+        return 'MIBSynchronizer: Device ID: {}, State:{}'.format(self._device_id, self.state)
+
+    def delete(self):
+        """
+        Cleanup any state information
+        """
+        self.stop()
+        db, self._database = self._database, None
+
+        if db is not None:
+            db.remove(self._device_id)
+
+    @property
+    def device_id(self):
+        return self._device_id
+
+    @property
+    def mib_data_sync(self):
+        return self._mib_data_sync
+
+    def increment_mib_data_sync(self):
+        self._mib_data_sync += 1
+        if self._mib_data_sync > 255:
+            self._mib_data_sync = 0
+
+        if self._database is not None:
+            self._database.save_mib_data_sync(self._device_id,
+                                              self._mib_data_sync)
+
+    @property
+    def last_mib_db_sync(self):
+        return self._last_mib_db_sync_value
+
+    @last_mib_db_sync.setter
+    def last_mib_db_sync(self, value):
+        self._last_mib_db_sync_value = value
+        if self._database is not None:
+            self._database.save_last_sync(self.device_id, value)
+
+    @property
+    def is_new_onu(self):
+        """
+        Is this a new ONU (has never completed MIB synchronization)
+        :return: (bool) True if this ONU should be considered new
+        """
+        return self.last_mib_db_sync is None
+
+    @property
+    def advertise_events(self):
+        return self._advertise_events
+
+    @advertise_events.setter
+    def advertise_events(self, value):
+        if not isinstance(value, bool):
+            raise TypeError('Advertise event is a boolean')
+        self._advertise_events = value
+
+    def advertise(self, event, info):
+        """Advertise an event on the OpenOMCI event bus"""
+        if self._advertise_events:
+            self._agent.advertise(event,
+                                  {
+                                      'state-machine': self.machine.name,
+                                      'info': info,
+                                      'time': str(datetime.utcnow())
+                                  })
+
+    def on_enter_disabled(self):
+        """
+        State machine is being stopped
+        """
+        self.advertise(OpenOmciEventType.state_change, self.state)
+
+        self._cancel_deferred()
+        if self._device is not None:
+            self._device.mib_db_in_sync = False
+
+        task, self._current_task = self._current_task, None
+        if task is not None:
+            task.stop()
+
+        # Drop Response and Autonomous notification subscriptions
+        for event, sub in self._omci_cc_subscriptions.iteritems():
+            if sub is not None:
+                self._omci_cc_subscriptions[event] = None
+                self._device.omci_cc.event_bus.unsubscribe(sub)
+
+        for event, sub in self._onu_dev_subscriptions.iteritems():
+            if sub is not None:
+                self._onu_dev_subscriptions[event] = None
+                self._device.event_bus.unsubscribe(sub)
+
+        # TODO: Stop and remove any currently running or scheduled tasks
+        # TODO: Anything else?
+
+    def _seed_database(self):
+        if not self._device_in_db:
+            try:
+                try:
+                    self._database.start()
+                    self._database.add(self._device_id)
+                    self.log.debug('seed-db-does-not-exist', device_id=self._device_id)
+
+                except KeyError:
+                    # Device already is in database
+                    self.log.debug('seed-db-exist', device_id=self._device_id)
+                    self._mib_data_sync = self._database.get_mib_data_sync(self._device_id)
+                    self._last_mib_db_sync_value = self._database.get_last_sync(self._device_id)
+
+                self._device_in_db = True
+
+            except Exception as e:
+                self.log.exception('seed-database-failure', e=e)
+
+    def on_enter_starting(self):
+        """
+        Determine ONU status and start/re-start MIB Synchronization tasks
+        """
+        self._device = self._agent.get_device(self._device_id)
+        self.advertise(OpenOmciEventType.state_change, self.state)
+
+        # Make sure root of external MIB Database exists
+        self._seed_database()
+
+        # Set up Response and Autonomous notification subscriptions
+        try:
+            for event, sub in self._omci_cc_sub_mapping.iteritems():
+                if self._omci_cc_subscriptions[event] is None:
+                    self._omci_cc_subscriptions[event] = \
+                        self._device.omci_cc.event_bus.subscribe(
+                            topic=OMCI_CC.event_bus_topic(self._device_id, event),
+                            callback=sub)
+
+        except Exception as e:
+            self.log.exception('omci-cc-subscription-setup', e=e)
+
+        # Set up ONU device subscriptions
+        try:
+            for event, sub in self._onu_dev_sub_mapping.iteritems():
+                if self._onu_dev_subscriptions[event] is None:
+                    self._onu_dev_subscriptions[event] = \
+                        self._device.event_bus.subscribe(
+                                topic=OnuDeviceEntry.event_bus_topic(self._device_id, event),
+                                callback=sub)
+
+        except Exception as e:
+            self.log.exception('dev-subscription-setup', e=e)
+
+        # Clear any previous audit results
+        self._on_olt_only_diffs = None
+        self._on_onu_only_diffs = None
+        self._attr_diffs = None
+        self._audited_olt_db = None
+        self._audited_onu_db = None
+
+        # Determine if this ONU has ever synchronized
+        if self.is_new_onu:
+            # Start full MIB upload
+            self._deferred = reactor.callLater(0, self.upload_mib)
+
+        else:
+            # Examine the MIB Data Sync
+            self._deferred = reactor.callLater(0, self.examine_mds)
+
+    def on_enter_uploading(self):
+        """
+        Begin full MIB data upload, starting with a MIB RESET
+        """
+        self.advertise(OpenOmciEventType.state_change, self.state)
+
+        def success(results):
+            self.log.debug('mib-upload-success', results=results)
+            self._current_task = None
+            self._next_resync = datetime.utcnow() + timedelta(seconds=self._resync_delay)
+            self._deferred = reactor.callLater(0, self.success)
+
+        def failure(reason):
+            self.log.info('mib-upload-failure', reason=reason)
+            self._current_task = None
+            self._deferred = reactor.callLater(self._timeout_delay, self.timeout)
+
+        self._device.mib_db_in_sync = False
+        self._current_task = self._upload_task(self._agent, self._device_id)
+
+        self._task_deferred = self._device.task_runner.queue_task(self._current_task)
+        self._task_deferred.addCallbacks(success, failure)
+
+    def on_enter_examining_mds(self):
+        """
+        Create a simple task to fetch the MIB Data Sync value and
+        determine if the ONU value matches what is in the MIB database
+        """
+        self.advertise(OpenOmciEventType.state_change, self.state)
+
+        self._mib_data_sync = self._database.get_mib_data_sync(self._device_id) or 0
+
+        def success(onu_mds_value):
+            self.log.debug('examine-mds-success', onu_mds_value=onu_mds_value, olt_mds_value=self.mib_data_sync)
+            self._current_task = None
+
+            # Examine MDS value
+            if self.mib_data_sync == onu_mds_value:
+                self._next_resync = datetime.utcnow() + timedelta(seconds=self._resync_delay)
+                self._deferred = reactor.callLater(0, self.success)
+            else:
+                self._deferred = reactor.callLater(0, self.mismatch)
+
+        def failure(reason):
+            self.log.info('examine-mds-failure', reason=reason)
+            self._current_task = None
+            self._deferred = reactor.callLater(self._timeout_delay, self.timeout)
+
+        self._device.mib_db_in_sync = False
+        self._current_task = self._get_mds_task(self._agent, self._device_id)
+
+        self._task_deferred = self._device.task_runner.queue_task(self._current_task)
+        self._task_deferred.addCallbacks(success, failure)
+
+    def on_enter_in_sync(self):
+        """
+        The OLT/OpenOMCI MIB Database is in sync with the ONU MIB Database.
+        """
+        self.advertise(OpenOmciEventType.state_change, self.state)
+        self.last_mib_db_sync = datetime.utcnow()
+        self._device.mib_db_in_sync = True
+
+        if self._audit_delay > 0:
+            self._deferred = reactor.callLater(self._audit_delay, self.audit_mib)
+
+    def on_enter_out_of_sync(self):
+        """
+        The MIB in OpenOMCI and the ONU are out of sync.  This can happen if:
+
+           o the MIB_Data_Sync values are not equal, or
+           o the MIBs were compared and differences were found.
+
+        Schedule a task to reconcile the differences
+        """
+        self.advertise(OpenOmciEventType.state_change, self.state)
+
+        # We are only out-of-sync if there were differences.  If here due to MDS
+        # value differences, still run the reconcile so we up date the ONU's MDS
+        # value to match ours.
+
+        self._device.mib_db_in_sync = self._attr_diffs is None and \
+                                      self._on_onu_only_diffs is None and \
+                                      self._on_olt_only_diffs is None
+
+        def success(onu_mds_value):
+            self.log.debug('reconcile-success', mds_value=onu_mds_value)
+            self._current_task = None
+            self._next_resync = datetime.utcnow() + timedelta(seconds=self._resync_delay)
+            self._deferred = reactor.callLater(0, self.success)
+
+        def failure(reason):
+            self.log.info('reconcile-failure', reason=reason)
+            self._current_task = None
+            self._deferred = reactor.callLater(self._timeout_delay, self.timeout)
+
+        diff_collection = {
+            'onu-only': self._on_onu_only_diffs,
+            'olt-only': self._on_olt_only_diffs,
+            'attributes': self._attr_diffs,
+            'olt-db': self._audited_olt_db,
+            'onu-db': self._audited_onu_db
+        }
+        # Clear out results since reconciliation task will be handling them
+        self._on_olt_only_diffs = None
+        self._on_onu_only_diffs = None
+        self._attr_diffs = None
+        self._audited_olt_db = None
+        self._audited_onu_db = None
+
+        self._current_task = self._reconcile_task(self._agent, self._device_id, diff_collection)
+        self._task_deferred = self._device.task_runner.queue_task(self._current_task)
+        self._task_deferred.addCallbacks(success, failure)
+
+    def on_enter_auditing(self):
+        """
+        Perform a MIB Audit.  If our last MIB resync was too long in the
+        past, perform a resynchronization anyway
+        """
+        self.advertise(OpenOmciEventType.state_change, self.state)
+
+        if self._next_resync is None:
+            self.log.error('next-forced-resync-error', msg='Next Resync should always be valid at this point')
+            self._deferred = reactor.callLater(self._timeout_delay, self.timeout)
+
+        if datetime.utcnow() >= self._next_resync:
+            self._deferred = reactor.callLater(0, self.force_resync)
+        else:
+            def success(onu_mds_value):
+                self.log.debug('audit-success', onu_mds_value=onu_mds_value, olt_mds_value=self.mib_data_sync)
+                self._current_task = None
+
+                # Examine MDS value
+                if self.mib_data_sync == onu_mds_value:
+                    self._deferred = reactor.callLater(0, self.success)
+                else:
+                    self._device.mib_db_in_sync = False
+                    self._deferred = reactor.callLater(0, self.mismatch)
+
+            def failure(reason):
+                self.log.info('audit-failure', reason=reason)
+                self._current_task = None
+                self._deferred = reactor.callLater(self._timeout_delay, self.timeout)
+
+            self._current_task = self._audit_task(self._agent, self._device_id)
+            self._task_deferred = self._device.task_runner.queue_task(self._current_task)
+            self._task_deferred.addCallbacks(success, failure)
+
+    def on_enter_resynchronizing(self):
+        """
+        Perform a resynchronization of the MIB database
+
+        First calculate any differences
+        """
+        self.advertise(OpenOmciEventType.state_change, self.state)
+
+        def success(results):
+            self.log.debug('resync-success', results=results)
+
+            on_olt_only = results.get('on-olt-only')
+            on_onu_only = results.get('on-onu-only')
+            attr_diffs = results.get('attr-diffs')
+            olt_db = results.get('olt-db')
+            onu_db = results.get('onu-db')
+
+            self._current_task = None
+            self._on_olt_only_diffs = on_olt_only if on_olt_only and len(on_olt_only) else None
+            self._on_onu_only_diffs = on_onu_only if on_onu_only and len(on_onu_only) else None
+            self._attr_diffs = attr_diffs if attr_diffs and len(attr_diffs) else None
+            self._audited_olt_db = olt_db
+            self._audited_onu_db = onu_db
+
+            mds_equal = self.mib_data_sync == self._audited_onu_db[MDS_KEY]
+
+            if mds_equal and all(diff is None for diff in [self._on_olt_only_diffs,
+                                                           self._on_onu_only_diffs,
+                                                           self._attr_diffs]):
+                self._next_resync = datetime.utcnow() + timedelta(seconds=self._resync_delay)
+                self._deferred = reactor.callLater(0, self.success)
+            else:
+                self._deferred = reactor.callLater(0, self.diffs_found)
+
+        def failure(reason):
+            self.log.info('resync-failure', reason=reason)
+            self._current_task = None
+            self._deferred = reactor.callLater(self._timeout_delay, self.timeout)
+
+        self._current_task = self._resync_task(self._agent, self._device_id)
+        self._task_deferred = self._device.task_runner.queue_task(self._current_task)
+        self._task_deferred.addCallbacks(success, failure)
+
+    def on_mib_reset_response(self, _topic, msg):
+        """
+        Called upon receipt of a MIB Reset Response for this ONU
+
+        :param _topic: (str) OMCI-RX topic
+        :param msg: (dict) Dictionary with 'rx-response' and 'tx-request' (if any)
+        """
+        self.log.debug('on-mib-reset-response', state=self.state)
+        try:
+            response = msg[RX_RESPONSE_KEY]
+
+            # Check if expected in current mib_sync state
+            if self.state != 'uploading' or self._omci_cc_subscriptions[RxEvent.MIB_Reset] is None:
+                self.log.error('rx-in-invalid-state', state=self.state)
+
+            else:
+                now = datetime.utcnow()
+
+                if not isinstance(response, OmciFrame):
+                    raise TypeError('Response should be an OmciFrame')
+
+                omci_msg = response.fields['omci_message'].fields
+                status = omci_msg['success_code']
+
+                assert status == RC.Success, 'Unexpected MIB reset response status: {}'. \
+                    format(status)
+
+                self._device.mib_db_in_sync = False
+                self._mib_data_sync = 0
+                self._device._modified = now
+                self._database.on_mib_reset(self._device_id)
+
+        except KeyError:
+            pass            # NOP
+
+    def on_avc_notification(self, _topic, msg):
+        """
+        Process an Attribute Value Change Notification
+
+        :param _topic: (str) OMCI-RX topic
+        :param msg: (dict) Dictionary with 'rx-response' and 'tx-request' (if any)
+        """
+        self.log.debug('on-avc-notification', state=self.state)
+
+        if self._omci_cc_subscriptions[RxEvent.AVC_Notification]:
+            try:
+                notification = msg[RX_RESPONSE_KEY]
+
+                if self.state == 'disabled':
+                    self.log.error('rx-in-invalid-state', state=self.state)
+
+                # Inspect the notification
+                omci_msg = notification.fields['omci_message'].fields
+                class_id = omci_msg['entity_class']
+                instance_id = omci_msg['entity_id']
+                data = omci_msg['data']
+                attributes = [data.keys()]
+
+                # Look up ME Instance in Database. Not-found can occur if a MIB
+                # reset has occurred
+                info = self._database.query(self.device_id, class_id, instance_id, attributes)
+                # TODO: Add old/new info to log message
+                self.log.debug('avc-change', class_id=class_id, instance_id=instance_id)
+
+                # Save the changed data to the MIB.
+                self._database.set(self.device_id, class_id, instance_id, data)
+
+                # Autonomous creation and deletion of managed entities do not
+                # result in an increment of the MIB data sync value. However,
+                # AVC's in response to a change by the Operator do incur an
+                # increment of the MIB Data Sync.  If here during uploading,
+                # we issued a MIB-Reset which may generate AVC.  (TODO: Focus testing during hardening)
+                if self.state == 'uploading':
+                    self.increment_mib_data_sync()
+
+            except KeyError:
+                pass            # NOP
+
+    def on_mib_upload_response(self, _topic, msg):
+        """
+        Process a MIB Upload response
+
+        :param _topic: (str) OMCI-RX topic
+        :param msg: (dict) Dictionary with 'rx-response' and 'tx-request' (if any)
+        """
+        self.log.debug('on-mib-upload-next-response', state=self.state)
+
+        if self._omci_cc_subscriptions[RxEvent.MIB_Upload]:
+            # Check if expected in current mib_sync state
+            if self.state == 'resynchronizing':
+                # The resync task handles this
+                # TODO: Remove this subscription if we never do anything with the response
+                return
+
+            if self.state != 'uploading':
+                self.log.error('rx-in-invalid-state', state=self.state)
+
+    def on_mib_upload_next_response(self, _topic, msg):
+        """
+        Process a MIB Upload Next response
+
+        :param _topic: (str) OMCI-RX topic
+        :param msg: (dict) Dictionary with 'rx-response' and 'tx-request' (if any)
+        """
+        self.log.debug('on-mib-upload-next-response', state=self.state)
+
+        if self._omci_cc_subscriptions[RxEvent.MIB_Upload_Next]:
+            try:
+                if self.state == 'resynchronizing':
+                    # The resync task handles this
+                    return
+
+                # Check if expected in current mib_sync state
+                if self.state != 'uploading':
+                    self.log.error('rx-in-invalid-state', state=self.state)
+
+                else:
+                    response = msg[RX_RESPONSE_KEY]
+
+                    # Extract entity instance information
+                    omci_msg = response.fields['omci_message'].fields
+
+                    class_id = omci_msg['object_entity_class']
+                    entity_id = omci_msg['object_entity_id']
+
+                    # Filter out the 'mib_data_sync' from the database. We save that at
+                    # the device level and do not want it showing up during a re-sync
+                    # during data compares
+
+                    if class_id == OntData.class_id:
+                        return
+
+                    attributes = {k: v for k, v in omci_msg['object_data'].items()}
+
+                    # Save to the database
+                    self._database.set(self._device_id, class_id, entity_id, attributes)
+
+            except KeyError:
+                pass            # NOP
+            except Exception as e:
+                self.log.exception('upload-next', e=e)
+
+    def on_create_response(self, _topic, msg):
+        """
+        Process a Set response
+
+        :param _topic: (str) OMCI-RX topic
+        :param msg: (dict) Dictionary with 'rx-response' and 'tx-request' (if any)
+        """
+        self.log.debug('on-create-response', state=self.state)
+
+        if self._omci_cc_subscriptions[RxEvent.Create]:
+            if self.state in ['disabled', 'uploading']:
+                self.log.error('rx-in-invalid-state', state=self.state)
+                return
+            try:
+                request = msg[TX_REQUEST_KEY]
+                response = msg[RX_RESPONSE_KEY]
+                status = response.fields['omci_message'].fields['success_code']
+
+                if status != RC.Success and status != RC.InstanceExists:
+                    # TODO: Support offline ONTs in post VOLTHA v1.3.0
+                    omci_msg = response.fields['omci_message']
+                    self.log.warn('set-response-failure',
+                                  class_id=omci_msg.fields['entity_class'],
+                                  instance_id=omci_msg.fields['entity_id'],
+                                  status=omci_msg.fields['success_code'],
+                                  status_text=self._status_to_text(omci_msg.fields['success_code']),
+                                  parameter_error_attributes_mask=omci_msg.fields['parameter_error_attributes_mask'])
+                else:
+                    omci_msg = request.fields['omci_message'].fields
+                    class_id = omci_msg['entity_class']
+                    entity_id = omci_msg['entity_id']
+                    attributes = {k: v for k, v in omci_msg['data'].items()}
+
+                    # Save to the database
+                    created = self._database.set(self._device_id, class_id, entity_id, attributes)
+
+                    if created:
+                        self.increment_mib_data_sync()
+
+                    # If the ME contains set-by-create or writeable values that were
+                    # not specified in the create command, the ONU will have
+                    # initialized those fields
+
+                    if class_id in self._device.me_map:
+                        sbc_w_set = {attr.field.name for attr in self._device.me_map[class_id].attributes
+                                     if (AA.SBC in attr.access or AA.W in attr.access)
+                                     and attr.field.name != 'managed_entity_id'}
+
+                        missing = sbc_w_set - {k for k in attributes.iterkeys()}
+
+                        if len(missing):
+                            # Request the missing attributes
+                            self.update_sbc_w_items(class_id, entity_id, missing)
+
+            except KeyError as e:
+                pass            # NOP
+
+            except Exception as e:
+                self.log.exception('create', e=e)
+
+    def update_sbc_w_items(self, class_id, entity_id, missing_attributes):
+        """
+        Perform a get-request for Set-By-Create (SBC) or writable (w) attributes
+        that were not specified in the original Create request.
+
+        :param class_id: (int) Class ID
+        :param entity_id: (int) Instance ID
+        :param missing_attributes: (set) Missing SBC or Writable attribute
+        """
+        if len(missing_attributes) and class_id in self._device.me_map:
+            from voltha.extensions.omci.tasks.omci_get_request import OmciGetRequest
+
+            self.log.info('update-sbc-items', class_id=class_id, entity_id=entity_id,
+                          attributes=missing_attributes)
+
+            def success(results):
+                self._database.set(self._device_id, class_id, entity_id, results.attributes)
+
+            def failure(reason):
+                self.log.warn('update-sbc-w-failed', reason=reason, class_id=class_id,
+                              entity_id=entity_id, attributes=missing_attributes)
+
+            d = self._device.task_runner.queue_task(OmciGetRequest(self._agent, self._device_id,
+                                                                   self._device.me_map[class_id],
+                                                                   entity_id, missing_attributes,
+                                                                   allow_failure=True))
+            d.addCallbacks(success, failure)
+
+    def on_delete_response(self, _topic, msg):
+        """
+        Process a Delete response
+
+        :param _topic: (str) OMCI-RX topic
+        :param msg: (dict) Dictionary with 'rx-response' and 'tx-request' (if any)
+        """
+        self.log.debug('on-delete-response', state=self.state)
+
+        if self._omci_cc_subscriptions[RxEvent.Delete]:
+            if self.state in ['disabled', 'uploading']:
+                self.log.error('rx-in-invalid-state', state=self.state)
+                return
+            try:
+                request = msg[TX_REQUEST_KEY]
+                response = msg[RX_RESPONSE_KEY]
+
+                if response.fields['omci_message'].fields['success_code'] != RC.Success:
+                    # TODO: Support offline ONTs in post VOLTHA v1.3.0
+                    omci_msg = response.fields['omci_message']
+                    self.log.warn('set-response-failure',
+                                  class_id=omci_msg.fields['entity_class'],
+                                  instance_id=omci_msg.fields['entity_id'],
+                                  status=omci_msg.fields['success_code'],
+                                  status_text=self._status_to_text(omci_msg.fields['success_code']))
+                else:
+                    omci_msg = request.fields['omci_message'].fields
+                    class_id = omci_msg['entity_class']
+                    entity_id = omci_msg['entity_id']
+
+                    # Remove from the database
+                    deleted = self._database.delete(self._device_id, class_id, entity_id)
+
+                    if deleted:
+                        self.increment_mib_data_sync()
+
+            except KeyError as e:
+                pass            # NOP
+            except Exception as e:
+                self.log.exception('delete', e=e)
+
+    def on_set_response(self, _topic, msg):
+        """
+        Process a Set response
+
+        :param _topic: (str) OMCI-RX topic
+        :param msg: (dict) Dictionary with 'rx-response' and 'tx-request' (if any)
+        """
+        self.log.debug('on-set-response', state=self.state)
+
+        if self._omci_cc_subscriptions[RxEvent.Set]:
+            if self.state in ['disabled', 'uploading']:
+                self.log.error('rx-in-invalid-state', state=self.state)
+            try:
+                request = msg[TX_REQUEST_KEY]
+                response = msg[RX_RESPONSE_KEY]
+
+                if response.fields['omci_message'].fields['success_code'] != RC.Success:
+                    # TODO: Support offline ONTs in post VOLTHA v1.3.0
+                    omci_msg = response.fields['omci_message']
+                    self.log.warn('set-response-failure',
+                                  class_id=omci_msg.fields['entity_class'],
+                                  instance_id=omci_msg.fields['entity_id'],
+                                  status=omci_msg.fields['success_code'],
+                                  status_text=self._status_to_text(omci_msg.fields['success_code']),
+                                  unsupported_attribute_mask=omci_msg.fields['unsupported_attributes_mask'],
+                                  failed_attribute_mask=omci_msg.fields['failed_attributes_mask'])
+                else:
+                    omci_msg = request.fields['omci_message'].fields
+                    class_id = omci_msg['entity_class']
+                    entity_id = omci_msg['entity_id']
+                    attributes = {k: v for k, v in omci_msg['data'].items()}
+
+                    # Save to the database (Do not save 'sets' of the mib-data-sync however)
+                    if class_id != OntData.class_id:
+                        modified = self._database.set(self._device_id, class_id, entity_id, attributes)
+                        if modified:
+                            self.increment_mib_data_sync()
+
+            except KeyError as _e:
+                pass            # NOP
+            except Exception as e:
+                self.log.exception('set', e=e)
+
+    # TODO: Future -> Monitor Software download start, section, activate, and commit responses
+    #                 and increment MIB Data Sync per Table 11.2.2-1 of ITUT-T G.988 (11/2017)
+    #                 on page 515.  Eventually also monitor set-table responses once the
+    #                 extended message set is supported.
+    def on_capabilities_event(self, _topic, msg):
+        """
+        Process a OMCI capabilties event
+        :param _topic: (str) OnuDeviceEntry Capabilities event
+        :param msg: (dict) Message Entities & Message Types supported
+        """
+        self._database.update_supported_managed_entities(self.device_id,
+                                                         msg[SUPPORTED_MESSAGE_ENTITY_KEY])
+        self._database.update_supported_message_types(self.device_id,
+                                                      msg[SUPPORTED_MESSAGE_TYPES_KEY])
+
+    def _status_to_text(self, success_code):
+        return {
+                RC.Success: "Success",
+                RC.ProcessingError: "Processing Error",
+                RC.NotSupported: "Not Supported",
+                RC.ParameterError: "Paremeter Error",
+                RC.UnknownEntity: "Unknown Entity",
+                RC.UnknownInstance: "Unknown Instance",
+                RC.DeviceBusy: "Device Busy",
+                RC.InstanceExists: "Instance Exists"
+            }.get(success_code, 'Unknown status code: {}'.format(success_code))
+
+    def query_mib(self, class_id=None, instance_id=None, attributes=None):
+        """
+        Get MIB database information.
+
+        This method can be used to request information from the database to the detailed
+        level requested
+
+        :param class_id:  (int) Managed Entity class ID
+        :param instance_id: (int) Managed Entity instance
+        :param attributes: (list or str) Managed Entity instance's attributes
+
+        :return: (dict) The value(s) requested. If class/inst/attribute is
+                        not found, an empty dictionary is returned
+        :raises DatabaseStateError: If the database is not enabled or does not exist
+        """
+        from voltha.extensions.omci.database.mib_db_api import DatabaseStateError
+
+        self.log.debug('query', class_id=class_id,
+                       instance_id=instance_id, attributes=attributes)
+        if self._database is None:
+            raise DatabaseStateError('Database does not yet exist')
+
+        return self._database.query(self._device_id, class_id=class_id,
+                                    instance_id=instance_id,
+                                    attributes=attributes)
+
+    def mib_set(self, class_id, entity_id, attributes):
+        """
+        Set attributes of an existing ME Class instance
+
+        This method is primarily used by other state machines to save ME specific
+        information to the persistent database. Access by objects external to the
+        OpenOMCI library is discouraged.
+
+        :param class_id: (int) ME Class ID
+        :param entity_id: (int) ME Class entity ID
+        :param attributes: (dict) attribute -> value pairs to set
+        """
+        # It must exist first (but attributes can be new)
+        if isinstance(attributes, dict) and len(attributes) and\
+                self.query_mib(class_id, entity_id) is not None:
+            self._database.set(self._device_id, class_id, entity_id, attributes)
+
+    def mib_delete(self, class_id, entity_id):
+        """
+        Delete an existing ME Class instance
+
+        This method is primarily used by other state machines to delete an ME
+        from the MIB database
+
+        :param class_id: (int) ME Class ID
+        :param entity_id: (int) ME Class entity ID
+
+        :raises KeyError: If device does not exist
+        :raises DatabaseStateError: If the database is not enabled
+        """
+        self._database.delete(self._device_id, class_id, entity_id)
diff --git a/python/adapters/extensions/omci/state_machines/omci_onu_capabilities.py b/python/adapters/extensions/omci/state_machines/omci_onu_capabilities.py
new file mode 100644
index 0000000..c13739e
--- /dev/null
+++ b/python/adapters/extensions/omci/state_machines/omci_onu_capabilities.py
@@ -0,0 +1,262 @@
+#
+# 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.
+#
+import structlog
+from transitions import Machine
+from twisted.internet import reactor
+from voltha.extensions.omci.onu_device_entry import OnuDeviceEntry, OnuDeviceEvents, IN_SYNC_KEY
+from voltha.protos.omci_mib_db_pb2 import OpenOmciEventType
+
+
+class OnuOmciCapabilities(object):
+    """
+    OpenOMCI ONU OMCI Capabilities State machine
+    """
+    DEFAULT_STATES = ['disabled', 'out_of_sync', 'in_sync', 'idle']
+
+    DEFAULT_TRANSITIONS = [
+        {'trigger': 'start', 'source': 'disabled', 'dest': 'out_of_sync'},
+        {'trigger': 'synchronized', 'source': 'out_of_sync', 'dest': 'in_sync'},
+
+        {'trigger': 'success', 'source': 'in_sync', 'dest': 'idle'},
+        {'trigger': 'failure', 'source': 'in_sync', 'dest': 'out_of_sync'},
+
+        {'trigger': 'not_synchronized', 'source': 'idle', 'dest': 'out_of_sync'},
+
+        # Do wildcard 'stop' trigger last so it covers all previous states
+        {'trigger': 'stop', 'source': '*', 'dest': 'disabled'},
+    ]
+    DEFAULT_RETRY = 10      # Seconds to delay after task failure/timeout/poll
+
+    def __init__(self, agent, device_id, tasks,
+                 advertise_events=False,
+                 states=DEFAULT_STATES,
+                 transitions=DEFAULT_TRANSITIONS,
+                 initial_state='disabled',
+                 timeout_delay=DEFAULT_RETRY):
+        """
+        Class initialization
+
+        :param agent: (OpenOmciAgent) Agent
+        :param device_id: (str) ONU Device ID
+        :param tasks: (dict) Tasks to run
+        :param advertise_events: (bool) Advertise events on OpenOMCI Event Bus
+        :param states: (list) List of valid states
+        :param transitions: (dict) Dictionary of triggers and state changes
+        :param initial_state: (str) Initial state machine state
+        :param timeout_delay: (int/float) Number of seconds after a timeout or poll
+        """
+        self.log = structlog.get_logger(device_id=device_id)
+
+        self._agent = agent
+        self._device_id = device_id
+        self._device = None
+        self._timeout_delay = timeout_delay
+
+        self._get_capabilities_task = tasks['get-capabilities']
+        self._advertise_events = advertise_events
+
+        self._deferred = None
+        self._current_task = None
+        self._task_deferred = None
+        self._supported_entities = frozenset()
+        self._supported_msg_types = frozenset()
+
+        self._subscriptions = {               # RxEvent.enum -> Subscription Object
+            OnuDeviceEvents.MibDatabaseSyncEvent: None
+        }
+        self._sub_mapping = {
+            OnuDeviceEvents.MibDatabaseSyncEvent: self.on_mib_sync_event
+        }
+        # Statistics and attributes
+        # TODO: add any others if it will support problem diagnosis
+
+        # Set up state machine to manage states
+        self.machine = Machine(model=self, states=states,
+                               transitions=transitions,
+                               initial=initial_state,
+                               queued=True,
+                               name='{}-{}'.format(self.__class__.__name__,
+                                                   device_id))
+
+    def _cancel_deferred(self):
+        d1, self._deferred = self._deferred, None
+        d2, self._task_deferred = self._task_deferred, None
+
+        for d in [d1, d2]:
+            try:
+                if d is not None and not d.called:
+                    d.cancel()
+            except:
+                pass
+
+    def _cancel_tasks(self):
+        task, self._current_task = self._current_task, None
+        if task is not None:
+            task.stop()
+
+    def __str__(self):
+        return 'OnuOmciCapabilities: Device ID: {}, State:{}'.format(self._device_id, self.state)
+
+    def delete(self):
+        """
+        Cleanup any state information
+        """
+        self.stop()
+
+    @property
+    def device_id(self):
+        return self._device_id
+
+    @property
+    def supported_managed_entities(self):
+        """
+        Return a set of the Managed Entity class IDs supported on this ONU
+        None is returned if no MEs have been discovered
+
+        :return: (set of ints)
+        """
+        return self._supported_entities if len(self._supported_entities) else None
+
+    @property
+    def supported_message_types(self):
+        """
+        Return a set of the Message Types supported on this ONU
+        None is returned if no message types have been discovered
+
+        :return: (set of EntityOperations)
+        """
+        return self._supported_msg_types if len(self._supported_msg_types) else None
+
+    @property
+    def advertise_events(self):
+        return self._advertise_events
+
+    @advertise_events.setter
+    def advertise_events(self, value):
+        if not isinstance(value, bool):
+            raise TypeError('Advertise event is a boolean')
+        self._advertise_events = value
+
+    def advertise(self, event, info):
+        """Advertise an event on the OpenOMCI event bus"""
+        from datetime import datetime
+
+        if self._advertise_events:
+            self._agent.advertise(event,
+                                  {
+                                      'state-machine': self.machine.name,
+                                      'info': info,
+                                      'time': str(datetime.utcnow())
+                                  })
+
+    def on_enter_disabled(self):
+        """
+        State machine is being stopped
+        """
+        self.advertise(OpenOmciEventType.state_change, self.state)
+        self._cancel_deferred()
+        self._cancel_tasks()
+
+        self._supported_entities = frozenset()
+        self._supported_msg_types = frozenset()
+
+        # Drop Response and Autonomous notification subscriptions
+        for event, sub in self._subscriptions.iteritems():
+            if sub is not None:
+                self._subscriptions[event] = None
+                self._device.event_bus.unsubscribe(sub)
+
+    def on_enter_out_of_sync(self):
+        """
+        State machine has just started or the MIB database has transitioned
+        to an out-of-synchronization state
+        """
+        self.advertise(OpenOmciEventType.state_change, self.state)
+        self._cancel_deferred()
+        self._device = self._agent.get_device(self._device_id)
+
+        # Subscribe to events of interest
+        try:
+            for event, sub in self._sub_mapping.iteritems():
+                if self._subscriptions[event] is None:
+                    self._subscriptions[event] = \
+                        self._device.event_bus.subscribe(
+                            topic=OnuDeviceEntry.event_bus_topic(self._device_id,
+                                                                 event),
+                            callback=sub)
+
+        except Exception as e:
+            self.log.exception('subscription-setup', e=e)
+
+        # Periodically check/poll for in-sync in case subscription was missed or
+        # already in sync
+        self._deferred = reactor.callLater(0, self.check_in_sync)
+
+    def check_in_sync(self):
+        if self._device.mib_db_in_sync:
+            self.synchronized()
+        else:
+            self._deferred = reactor.callLater(self._timeout_delay,
+                                               self.check_in_sync)
+
+    def on_enter_in_sync(self):
+        """
+        State machine has just transitioned to an in-synchronization state
+        """
+        self.advertise(OpenOmciEventType.state_change, self.state)
+        self._cancel_deferred()
+
+        def success(results):
+            self.log.debug('capabilities-success', results=results)
+            self._supported_entities = self._current_task.supported_managed_entities
+            self._supported_msg_types = self._current_task.supported_message_types
+            self._current_task = None
+            self._deferred = reactor.callLater(0, self.success)
+
+        def failure(reason):
+            self.log.info('capabilities-failure', reason=reason)
+            self._supported_entities = frozenset()
+            self._supported_msg_types = frozenset()
+            self._current_task = None
+            self._deferred = reactor.callLater(self._timeout_delay, self.failure)
+
+        # Schedule a task to read the ONU's OMCI capabilities
+        self._current_task = self._get_capabilities_task(self._agent, self._device_id)
+        self._task_deferred = self._device.task_runner.queue_task(self._current_task)
+        self._task_deferred.addCallbacks(success, failure)
+
+    def on_enter_idle(self):
+        """
+        Notify any subscribers for a capabilities event and wait until
+        stopped or ONU MIB database goes out of sync
+        """
+        self.advertise(OpenOmciEventType.state_change, self.state)
+        self._cancel_deferred()
+        self._device.publish_omci_capabilities_event()
+
+    def on_mib_sync_event(self, _topic, msg):
+        """
+        Handle In-Sync/Out-of-Sync for the MIB database
+        :param _topic: (str) Subscription topic
+        :param msg: (dict) In-Sync event data
+        """
+        if self._subscriptions.get(OnuDeviceEvents.MibDatabaseSyncEvent) is None:
+            return
+
+        if msg[IN_SYNC_KEY]:
+            self.synchronized()
+        else:
+            self.not_synchronized()
diff --git a/python/adapters/extensions/omci/state_machines/performance_intervals.py b/python/adapters/extensions/omci/state_machines/performance_intervals.py
new file mode 100644
index 0000000..78cfa74
--- /dev/null
+++ b/python/adapters/extensions/omci/state_machines/performance_intervals.py
@@ -0,0 +1,904 @@
+#
+# Copyright 2018 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.
+#
+import structlog
+import arrow
+from transitions import Machine
+from datetime import datetime, timedelta
+from random import uniform, shuffle
+from twisted.internet import reactor
+from common.utils.indexpool import IndexPool
+from voltha.protos.omci_mib_db_pb2 import OpenOmciEventType
+from voltha.extensions.omci.omci_defs import EntityOperations, ReasonCodes
+from voltha.extensions.omci.omci_cc import OmciCCRxEvents, OMCI_CC, TX_REQUEST_KEY, \
+    RX_RESPONSE_KEY
+from voltha.extensions.omci.database.mib_db_api import ATTRIBUTES_KEY
+from voltha.extensions.omci.tasks.omci_get_request import OmciGetRequest
+from voltha.extensions.omci.omci_entities import MacBridgePortConfigurationData
+from voltha.extensions.omci.omci_entities import EthernetPMMonitoringHistoryData, \
+    FecPerformanceMonitoringHistoryData, \
+    XgPonTcPerformanceMonitoringHistoryData, \
+    XgPonDownstreamPerformanceMonitoringHistoryData, \
+    XgPonUpstreamPerformanceMonitoringHistoryData, \
+    EthernetFrameUpstreamPerformanceMonitoringHistoryData, \
+    EthernetFrameDownstreamPerformanceMonitoringHistoryData, \
+    EthernetFrameExtendedPerformanceMonitoring, \
+    EthernetFrameExtendedPerformanceMonitoring64Bit, AniG
+
+
+RxEvent = OmciCCRxEvents
+OP = EntityOperations
+RC = ReasonCodes
+
+
+class PerformanceIntervals(object):
+    """
+    OpenOMCI ONU Performance Monitoring Intervals State machine
+
+    This state machine focuses on L2 Internet Data Service and Classical
+    PM (for the v2.0 release).
+    """
+    DEFAULT_STATES = ['disabled', 'starting', 'synchronize_time', 'idle', 'create_pm_me',
+                      'collect_data', 'threshold_exceeded']
+
+    DEFAULT_TRANSITIONS = [
+        {'trigger': 'start', 'source': 'disabled', 'dest': 'starting'},
+        {'trigger': 'tick', 'source': 'starting', 'dest': 'synchronize_time'},
+
+        {'trigger': 'success', 'source': 'synchronize_time', 'dest': 'idle'},
+        {'trigger': 'failure', 'source': 'synchronize_time', 'dest': 'synchronize_time'},
+
+        {'trigger': 'tick', 'source': 'idle', 'dest': 'collect_data'},
+        {'trigger': 'add_me', 'source': 'idle', 'dest': 'create_pm_me'},
+        {'trigger': 'delete_me', 'source': 'idle', 'dest': 'delete_pm_me'},
+
+        # TODO: Can these be combined into one?
+        {'trigger': 'success', 'source': 'create_pm_me', 'dest': 'idle'},
+        {'trigger': 'failure', 'source': 'create_pm_me', 'dest': 'idle'},
+
+        # TODO: Can these be combined into one?
+        {'trigger': 'success', 'source': 'delete_pm_me', 'dest': 'idle'},
+        {'trigger': 'failure', 'source': 'delete_pm_me', 'dest': 'idle'},
+
+        # TODO: Can these be combined into one?
+        {'trigger': 'success', 'source': 'collect_data', 'dest': 'idle'},
+        {'trigger': 'failure', 'source': 'collect_data', 'dest': 'idle'},
+
+        # TODO: Add rebooted event transitions to disabled or synchronize_time
+        # TODO: Need to capture Threshold Crossing Alarms appropriately
+
+        # Do wildcard 'stop' trigger last so it covers all previous states
+        {'trigger': 'stop', 'source': '*', 'dest': 'disabled'},
+        {'trigger': 'reboot', 'source': '*', 'dest': 'rebooted'},
+    ]
+    DEFAULT_RETRY = 10               # Seconds to delay after task failure/timeout/poll
+    DEFAULT_TICK_DELAY = 15          # Seconds between checks for collection tick
+    DEFAULT_INTERVAL_SKEW = 10 * 60  # Seconds to skew past interval boundary
+    DEFAULT_COLLECT_ATTEMPTS = 3     # Maximum number of collection fetch attempts
+    DEFAULT_CREATE_ATTEMPTS = 15     # Maximum number of attempts to create a PM Managed Entities
+
+    def __init__(self, agent, device_id, tasks,
+                 advertise_events=False,
+                 states=DEFAULT_STATES,
+                 transitions=DEFAULT_TRANSITIONS,
+                 initial_state='disabled',
+                 timeout_delay=DEFAULT_RETRY,
+                 tick_delay=DEFAULT_TICK_DELAY,
+                 interval_skew=DEFAULT_INTERVAL_SKEW,
+                 collect_attempts=DEFAULT_COLLECT_ATTEMPTS,
+                 create_attempts=DEFAULT_CREATE_ATTEMPTS):
+        """
+        Class initialization
+
+        :param agent: (OpenOmciAgent) Agent
+        :param device_id: (str) ONU Device ID
+        :param tasks: (dict) Tasks to run
+        :param advertise_events: (bool) Advertise events on OpenOMCI Event Bus
+        :param states: (list) List of valid states
+        :param transitions: (dict) Dictionary of triggers and state changes
+        :param initial_state: (str) Initial state machine state
+        :param timeout_delay: (int/float) Number of seconds after a timeout to pause
+        :param tick_delay: (int/float) Collection poll check delay while idle
+        :param interval_skew: (int/float) Seconds to randomly skew the next interval
+                              collection to spread out requests for PM intervals
+        :param collect_attempts: (int) Max requests for a single PM interval before fail
+        :param create_attempts: (int) Max attempts to create PM Managed entities before stopping state machine
+        """
+        self.log = structlog.get_logger(device_id=device_id)
+
+        self._agent = agent
+        self._device_id = device_id
+        self._device = None
+        self._pm_config = None
+        self._timeout_delay = timeout_delay
+        self._tick_delay = tick_delay
+        self._interval_skew = interval_skew
+        self._collect_attempts = collect_attempts
+        self._create_attempts = create_attempts
+
+        self._sync_time_task = tasks['sync-time']
+        self._get_interval_task = tasks['collect-data']
+        self._create_pm_task = tasks['create-pm']
+        self._delete_pm_task = tasks['delete-pm']
+        self._advertise_events = advertise_events
+
+        self._omci_cc_subscriptions = {               # RxEvent.enum -> Subscription Object
+            RxEvent.MIB_Reset: None,
+            RxEvent.Create: None,
+            RxEvent.Delete: None
+        }
+        self._omci_cc_sub_mapping = {
+            RxEvent.MIB_Reset: self.on_mib_reset_response,
+            RxEvent.Create: self.on_create_response,
+            RxEvent.Delete: self.on_delete_response,
+        }
+        self._me_watch_list = {
+            MacBridgePortConfigurationData.class_id: {
+                'create-delete': self.add_remove_enet_frame_pm,
+                'instances': dict()  # BP entity_id -> (PM class_id, PM entity_id)
+            }
+        }
+        self._deferred = None
+        self._task_deferred = None
+        self._current_task = None
+        self._add_me_deferred = None
+        self._delete_me_deferred = None
+        self._next_interval = None
+        self._enet_entity_id = IndexPool(1024, 1)
+        self._add_pm_me_retry = 0
+
+        # (Class ID, Instance ID) -> Collect attempts remaining
+        self._pm_me_collect_retries = dict()
+        self._pm_me_extended_info = dict()
+        self._add_pm_me = dict()        # (pm cid, pm eid) -> (me cid, me eid, upstream)
+        self._del_pm_me = set()
+
+        # Pollable PM items
+        # Note that some items the KPI extracts are not listed below. These are the
+        # administrative states, operational states, and sensed ethernet type. The values
+        # in the MIB database should be accurate for these items.
+
+        self._ani_g_items = ["optical_signal_level", "transmit_optical_level"]
+        self._next_poll_time = datetime.utcnow()
+        self._poll_interval = 60                    # TODO: Fixed at once a minute
+
+        # Statistics and attributes
+        # TODO: add any others if it will support problem diagnosis
+
+        # Set up state machine to manage states
+        self.machine = Machine(model=self, states=states,
+                               transitions=transitions,
+                               initial=initial_state,
+                               queued=True,
+                               ignore_invalid_triggers=True,
+                               name='{}-{}'.format(self.__class__.__name__,
+                                                   device_id))
+        try:
+            import logging
+            logging.getLogger('transitions').setLevel(logging.WARNING)
+        except Exception as e:
+            self.log.exception('log-level-failed', e=e)
+
+
+    def _cancel_deferred(self):
+        d1, self._deferred = self._deferred, None
+        d2, self._task_deferred = self._task_deferred, None
+        d3, self._add_me_deferred = self._add_me_deferred, None
+        d4, self._delete_me_deferred = self._delete_me_deferred, None
+
+        for d in [d1, d2, d3, d4]:
+            try:
+                if d is not None and not d.called:
+                    d.cancel()
+            except:
+                pass
+
+    def _cancel_tasks(self):
+        task, self._current_task = self._current_task, None
+        if task is not None:
+            task.stop()
+
+    def __str__(self):
+        return 'PerformanceIntervals: Device ID: {}, State:{}'.format(self._device_id,
+                                                                      self.state)
+
+    def delete(self):
+        """
+        Cleanup any state information
+        """
+        self.stop()
+
+    @property
+    def device_id(self):
+        return self._device_id
+
+    @property
+    def advertise_events(self):
+        return self._advertise_events
+
+    @advertise_events.setter
+    def advertise_events(self, value):
+        if not isinstance(value, bool):
+            raise TypeError('Advertise event is a boolean')
+        self._advertise_events = value
+
+    def advertise(self, event, info):
+        """Advertise an event on the OpenOMCI event bus"""
+        if self._advertise_events:
+            self._agent.advertise(event,
+                                  {
+                                      'state-machine': self.machine.name,
+                                      'info': info,
+                                      'time': str(datetime.utcnow()),
+                                      'next': str(self._next_interval)
+                                  })
+
+    def set_pm_config(self, pm_config):
+        """
+        Set PM interval configuration
+
+        :param pm_config: (OnuPmIntervalMetrics) PM Interval configuration
+        :return:
+        """
+        self._pm_config = pm_config
+
+    def _me_is_supported(self, class_id):
+        """
+        Check to see if ONU supports this ME
+        :param class_id: (int) ME Class ID
+        :return: (bool) If ME is supported
+        """
+        #
+        supported = self._device.omci_capabilities.supported_managed_entities
+        return class_id in supported if supported is not None else False
+
+    def add_pm_me(self, pm_class_id, pm_entity_id, cid=0, eid=0, upstream=False):
+        """
+        Add a new Performance Monitoring ME.
+
+        The ME ID will be added to an internal list and will be added the next
+        time the idle state is reached. An 'add_pm_me' trigger will be raised in
+        case already in the Idle state.
+
+        :param pm_class_id: (int) ME Class ID (1..0xFFFE)
+        :param pm_entity_id: (int) Instance ID (1..0xFFFE)
+        :param cid: (int) Class ID of entity monitored, may be None
+        :param eid: (int) Instance ID of entity monitored, may be None
+        :param upstream: (bool): Flag indicating if PM is for upstream traffic
+        """
+        if not isinstance(pm_class_id, int):
+            raise TypeError('PM ME Instance ID is an integer')
+        if not 0 < pm_class_id < 0xFFFF:
+            raise ValueError('PM ME Instance ID must be 1..65534')
+
+        # Check to see if ONU supports this ME
+        if not self._me_is_supported(pm_class_id):
+            self.log.warn('unsupported-PM-me', class_id=pm_class_id)
+            return
+
+        key = (pm_class_id, pm_entity_id)
+        entry = (cid, eid, upstream)
+
+        if key not in self._pm_me_collect_retries and key not in self._add_pm_me:
+            self._add_pm_me[key] = entry
+
+            if self._add_me_deferred is None:
+                self._add_me_deferred = reactor.callLater(0, self.add_me)
+
+        if (pm_class_id, pm_entity_id) in self._del_pm_me:
+            self._del_pm_me.remove((pm_class_id, pm_entity_id))
+
+    def delete_pm_me(self, class_id, entity_id):
+        """
+        Remove a new Performance Monitoring ME.
+
+        The ME ID will be added to an internal list and will be removed the next
+        time the idle state is reached. An 'delete_pm_me' trigger will be raised in
+        case already in the Idle state.
+
+        :param class_id: (int) ME Class ID (1..0xFFFE)
+        :param entity_id: (int) Instance ID (1..0xFFFE)
+        """
+        if not isinstance(class_id, int):
+            raise TypeError('PM ME Class ID is an integer')
+        if not 0 < class_id < 0xFFFF:
+            raise ValueError('PM ME Class ID must be 1..65534')
+
+        # Check to see if ONU supports this ME
+        if not self._me_is_supported(class_id):
+            self.log.warn('unsupported-PM-me', class_id=class_id)
+            return
+
+        key = (class_id, entity_id)
+
+        if key in self._pm_me_collect_retries and key not in self._del_pm_me:
+            self._del_pm_me.add(key)
+
+            if self._delete_me_deferred is None:
+                self._delete_me_deferred = reactor.callLater(0, self.delete_me)
+
+        if key in self._add_pm_me:
+            self._add_pm_me.pop(key)
+
+    def on_enter_disabled(self):
+        """
+        State machine is being stopped
+        """
+        self.advertise(OpenOmciEventType.state_change, self.state)
+        self._cancel_deferred()
+        self._cancel_tasks()
+        self._next_interval = None
+
+        # Drop OMCI ME Response subscriptions
+        for event, sub in self._omci_cc_subscriptions.iteritems():
+            if sub is not None:
+                self._omci_cc_subscriptions[event] = None
+                self._device.omci_cc.event_bus.unsubscribe(sub)
+
+        # Manually remove ani ANI/PON and UNI PM interval MEs
+        config = self._device.configuration
+        anis = config.ani_g_entities
+        unis = config.uni_g_entities
+
+        if anis is not None:
+            for entity_id in anis.iterkeys():
+                self.delete_pm_me(FecPerformanceMonitoringHistoryData.class_id, entity_id)
+                self.delete_pm_me(XgPonTcPerformanceMonitoringHistoryData.class_id, entity_id)
+                self.delete_pm_me(XgPonDownstreamPerformanceMonitoringHistoryData.class_id, entity_id)
+                self.delete_pm_me(XgPonUpstreamPerformanceMonitoringHistoryData.class_id, entity_id)
+
+        if unis is not None:
+            for entity_id in config.uni_g_entities.iterkeys():
+                self.delete_pm_me(EthernetPMMonitoringHistoryData.class_id, entity_id)
+
+    def on_enter_starting(self):
+        """ Add the PON/ANI and UNI PM intervals"""
+        self.advertise(OpenOmciEventType.state_change, self.state)
+
+        self._device = self._agent.get_device(self._device_id)
+        self._cancel_deferred()
+
+        # Set up OMCI ME Response subscriptions
+        try:
+            for event, sub in self._omci_cc_sub_mapping.iteritems():
+                if self._omci_cc_subscriptions[event] is None:
+                    self._omci_cc_subscriptions[event] = \
+                        self._device.omci_cc.event_bus.subscribe(
+                            topic=OMCI_CC.event_bus_topic(self._device_id, event),
+                            callback=sub)
+
+        except Exception as e:
+            self.log.exception('omci-cc-subscription-setup', e=e)
+
+        try:
+            # Manually start some ANI/PON and UNI PM interval MEs
+            config = self._device.configuration
+            anis = config.ani_g_entities
+            unis = config.uni_g_entities
+
+            if anis is not None:
+                for entity_id in anis.iterkeys():
+                    self.add_pm_me(FecPerformanceMonitoringHistoryData.class_id,
+                                   entity_id)
+                    self.add_pm_me(XgPonTcPerformanceMonitoringHistoryData.class_id,
+                                   entity_id)
+                    self.add_pm_me(XgPonDownstreamPerformanceMonitoringHistoryData.class_id,
+                                   entity_id)
+                    self.add_pm_me(XgPonUpstreamPerformanceMonitoringHistoryData.class_id,
+                                   entity_id)
+
+            if unis is not None:
+                for entity_id in config.uni_g_entities.iterkeys():
+                    self.add_pm_me(EthernetPMMonitoringHistoryData.class_id, entity_id)
+
+            # Look for existing instances of dynamically created ME's that have PM
+            # associated with them and add them now
+            for class_id in self._me_watch_list.iterkeys():
+                instances = {k: v for k, v in
+                             self._device.query_mib(class_id=class_id).items()
+                             if isinstance(k, int)}
+
+                for entity_id, data in instances.items():
+                    method = self._me_watch_list[class_id]['create-delete']
+                    cid, eid = method(None, class_id, entity_id,
+                                      add=True, attributes=data[ATTRIBUTES_KEY])
+                    if cid > 0:
+                        # BP entity_id -> (PM class_id, PM entity_id)
+                        instances = self._me_watch_list[class_id]['instances']
+                        instances[entity_id] = (cid, eid)
+
+        except Exception as e:
+            self.log.exception('pm-me-setup', class_id=class_id, e=e)
+
+        # Got to synchronize_time state
+        self._deferred = reactor.callLater(0, self.tick)
+
+    def on_enter_synchronize_time(self):
+        """
+        State machine has just transitioned to the synchronize_time state
+        """
+        self.advertise(OpenOmciEventType.state_change, self.state)
+        self._cancel_deferred()
+
+        def success(_results):
+            self.log.debug('sync-time-success')
+            self._current_task = None
+            self._deferred = reactor.callLater(0, self.success)
+            # Calculate next interval time
+            self._next_interval = self.get_next_interval
+
+        def failure(reason):
+            self.log.info('sync-time-failure', reason=reason)
+            self._current_task = None
+            self._deferred = reactor.callLater(self._timeout_delay, self.failure)
+
+        # Schedule a task to set the ONU time
+        self._current_task = self._sync_time_task(self._agent, self._device_id)
+        self._task_deferred = self._device.task_runner.queue_task(self._current_task)
+        self._task_deferred.addCallbacks(success, failure)
+
+    def on_enter_idle(self):
+        """
+        State machine has just transitioned to the idle state
+
+        In this state, any added PM MEs that need to be created will be.
+        TODO: some non-interval PM stats (if there are any) are collected here
+        """
+        self.advertise(OpenOmciEventType.state_change, self.state)
+        self._cancel_deferred()
+
+        if len(self._del_pm_me) and self._delete_me_deferred is None:
+            self._delete_me_deferred = reactor.callLater(0, self.delete_me)
+
+        elif len(self._add_pm_me) and self._add_me_deferred is None:
+            self._add_me_deferred = reactor.callLater(0, self.add_me)
+
+        elif datetime.utcnow() >= self._next_poll_time:
+            def success(results):
+                self._device.timestamp = arrow.utcnow().float_timestamp
+                self._device.mib_synchronizer.mib_set(results.me_class.class_id,
+                                                      results.entity_id,
+                                                      results.attributes)
+                self._next_poll_time = datetime.utcnow() + timedelta(seconds=self._poll_interval)
+
+            def failure(reason):
+                self.log.info('poll-failure', reason=reason)
+                self._device.timestamp = None
+                return None
+
+            # Scan all ANI-G ports
+            ani_g_entities = self._device.configuration.ani_g_entities
+            ani_g_entities_ids = ani_g_entities.keys() if ani_g_entities is not None else None
+
+            if ani_g_entities_ids is not None and len(ani_g_entities_ids):
+                for entity_id in ani_g_entities_ids:
+                    task = OmciGetRequest(self._agent, self.device_id,
+                                          AniG, entity_id,
+                                          self._ani_g_items, allow_failure=True)
+                    self._task_deferred = self._device.task_runner.queue_task(task)
+                    self._task_deferred.addCallbacks(success, failure)
+            else:
+                self.log.warn('poll-pm-no-anis')
+                self._next_poll_time = datetime.utcnow() + timedelta(seconds=self._poll_interval)
+
+        # TODO: Compute a better mechanism than just polling here, perhaps based on
+        #       the next time to fetch data for 'any' interval
+        self._deferred = reactor.callLater(self._tick_delay, self.tick)
+
+    def on_enter_create_pm_me(self):
+        """
+        State machine has just transitioned to the create_pm_me state
+        """
+        self.advertise(OpenOmciEventType.state_change, self.state)
+        self._cancel_deferred()
+        self._cancel_tasks()
+        mes, self._add_pm_me = self._add_pm_me, dict()
+
+        def success(results):
+            self.log.debug('create-me-success', results=results)
+
+            # Check if already here. The create request could have received
+            # an already-exists status code which we consider successful
+            for pm, me in mes.items():
+                self._pm_me_collect_retries[pm] = self.pm_collected(pm)
+                self._pm_me_extended_info[pm] = me
+
+            self._current_task = None
+            self._deferred = reactor.callLater(0, self.success)
+
+        def failure(reason):
+            self.log.info('create-me-failure', reason=reason, retries=self._add_pm_me_retry)
+            self._current_task = None
+            if self._add_pm_me_retry <= self._create_attempts:
+              for pm, me in mes.items():
+                  self._add_pm_me[pm] = me
+              self._add_pm_me_retry += 1
+              self._deferred = reactor.callLater(self._timeout_delay, self.failure)
+            else:
+              # we cant seem to create any collection me, no point in doing anything
+              self.log.warn('unable-to-create-pm-me-disabling-collection', reason=reason, device_id=self._device_id)
+              self._deferred = reactor.callLater(self._timeout_delay, self.stop)
+
+        self._current_task = self._create_pm_task(self._agent, self._device_id, mes)
+        self._task_deferred = self._device.task_runner.queue_task(self._current_task)
+        self._task_deferred.addCallbacks(success, failure)
+
+    def on_enter_delete_pm_me(self):
+        """
+        State machine has just transitioned to the delete_pm_me state
+        """
+        self.advertise(OpenOmciEventType.state_change, self.state)
+        self._cancel_deferred()
+        self._cancel_tasks()
+
+        mes, self._del_pm_me = self._del_pm_me, set()
+
+        def success(results):
+            self.log.debug('delete-me-success', results=results)
+            self._current_task = None
+            for me in mes:
+                self._pm_me_collect_retries.pop(me)
+
+            self._deferred = reactor.callLater(0, self.success)
+
+        def failure(reason):
+            self.log.info('delete-me-failure', reason=reason)
+            self._current_task = None
+            for me in mes:
+                self._del_pm_me.add(me)
+
+            self._deferred = reactor.callLater(self._timeout_delay, self.failure)
+
+        self._current_task = self._delete_pm_task(self._agent, self._device_id, mes)
+        self._task_deferred = self._device.task_runner.queue_task(self._current_task)
+        self._task_deferred.addCallbacks(success, failure)
+
+    def on_enter_collect_data(self):
+        """
+        State machine has just transitioned to the collect_data state
+        """
+
+        if self._next_interval is not None and self._next_interval > datetime.utcnow():
+            self.log.debug('wait-next-interval')
+            # Not ready for next interval, transition back to idle and we should get
+            # called again after a short delay
+            reactor.callLater(0, self.success)
+            return
+
+        self.advertise(OpenOmciEventType.state_change, self.state)
+        self._cancel_deferred()
+        self._cancel_tasks()
+        keys = self._pm_me_collect_retries.keys()
+        shuffle(keys)
+
+        for key in keys:
+            class_id = key[0]
+            entity_id = key[1]
+
+            self.log.debug("in-enter-collect-data", data_key=key,
+                           retries=self._pm_me_collect_retries[key])
+
+            # Collect the data ?
+            if self._pm_me_collect_retries[key] > 0:
+                def success(results):
+                    self.log.debug('collect-success', results=results,
+                                   class_id=results.get('class_id'),
+                                   entity_id=results.get('entity_id'))
+                    self._current_task = None
+                    self._pm_me_collect_retries[key] = 0
+                    self._deferred = reactor.callLater(0, self.success)
+                    return results
+
+                def failure(reason):
+                    self.log.info('collect-failure', reason=reason)
+                    self._current_task = None
+                    self._pm_me_collect_retries[key] -= 1
+                    self._deferred = reactor.callLater(self._timeout_delay, self.failure)
+                    return reason   # Halt callback processing
+
+                # start the task
+                if key in self._pm_me_extended_info:
+                    self.log.debug('collect-extended-info-found', data_key=key,
+                                   extended_info=self._pm_me_extended_info[key])
+                    parent_class_id = self._pm_me_extended_info[key][0]
+                    parent_entity_id = self._pm_me_extended_info[key][1]
+                    upstream = self._pm_me_extended_info[key][2]
+                else:
+                    self.log.debug('collect-extended-info-not-found', data_key=key)
+                    parent_class_id = None
+                    parent_entity_id = None
+                    upstream = None
+
+                self._current_task = self._get_interval_task(self._agent, self._device_id,
+                                                             class_id, entity_id,
+                                                             parent_class_id=parent_class_id,
+                                                             parent_entity_id=parent_entity_id,
+                                                             upstream=upstream)
+                self._task_deferred = self._device.task_runner.queue_task(self._current_task)
+                self._task_deferred.addCallbacks(success, failure)
+                self._task_deferred.addCallback(self.publish_data)
+                return
+
+        # Here if all intervals have been collected (we are up to date)
+        self._next_interval = self.get_next_interval
+        self.log.debug('collect-calculate-next', next=self._next_interval)
+
+        self._pm_me_collect_retries = dict.fromkeys(self._pm_me_collect_retries, self._collect_attempts)
+        reactor.callLater(0, self.success)
+
+    def on_enter_threshold_exceeded(self):
+        """
+        State machine has just transitioned to the threshold_exceeded state
+        """
+        pass  # TODO: Not sure if we want this state. Need to get alarm synchronizer working first
+
+    @property
+    def get_next_interval(self):
+        """
+        Determine the time for the next interval collection for all of this
+        ONUs PM Intervals. Earliest fetch time is at least 1 minute into the
+        next interval.
+
+        :return: (datetime) UTC time to get the next interval
+        """
+        now = datetime.utcnow()
+
+        # Get delta seconds to at least 1 minute into next interval
+        next_delta_secs = (16 - (now.minute % 15)) * 60
+        next_interval = now + timedelta(seconds=next_delta_secs)
+
+        # NOTE: For debugging, uncomment next section to perform collection
+        #       right after initial code startup/mib-sync
+        if self._next_interval is None:
+            return now     # Do it now  (just for debugging purposes)
+
+        # Skew the next time up to the maximum specified
+        # TODO: May want to skew in a shorter range and select the minute
+        #       based off some device property value to make collection a
+        #       little more predictable on a per-ONU basis.
+        return next_interval + timedelta(seconds=uniform(0, self._interval_skew))
+
+    def pm_collected(self, key):
+        """
+        Query database and determine if PM data needs to be collected for this ME
+        """
+        class_id = key[0]
+        entity_id = key[1]
+
+        return self._collect_attempts        # TODO: Implement persistent storage
+
+    def publish_data(self, results):
+        """
+        Publish the PM interval results on the appropriate bus.  The results are
+        a dictionary with the following format.
+
+            'class-id':          (int) ME Class ID,
+            'entity-id':         (int) ME Entity ID,
+            'me-name':           (str) ME Class name,   # Mostly for debugging...
+            'interval-end-time': None,
+            'interval-utc-time': (DateTime) UTC time when retrieved from ONU,
+
+            Counters added here as they are retrieved with the format of
+            'counter-attribute-name': value (int)
+
+        :param results: (dict) PM results
+        """
+        self.log.debug('collect-publish', results=results)
+
+        if self._pm_config is not None:
+            self._pm_config.publish_metrics(results)
+
+        pass  # TODO: Save off last time interval fetched to persistent storage?
+
+    def on_mib_reset_response(self, _topic, msg):
+        """
+        Called upon receipt of a MIB Reset Response for this ONU
+
+        :param _topic: (str) OMCI-RX topic
+        :param msg: (dict) Dictionary with 'rx-response' and 'tx-request' (if any)
+        """
+        self.log.debug('on-mib-reset-response', state=self.state)
+        try:
+            response = msg[RX_RESPONSE_KEY]
+            omci_msg = response.fields['omci_message'].fields
+            status = omci_msg['success_code']
+
+            if status == RC.Success:
+                for class_id in self._me_watch_list.iterkeys():
+                    # BP entity_id -> (PM class_id, PM entity_id)
+                    instances = self._me_watch_list[class_id]['instances']
+                    for _, me_pair in instances.items():
+                        self._me_watch_list[class_id]['create-delete'](None, me_pair[0],
+                                                                       me_pair[1], add=False)
+                    self._me_watch_list[class_id]['instances'] = dict()
+
+        except KeyError:
+            pass            # NOP
+
+    def on_create_response(self, _topic, msg):
+        """
+        Called upon receipt of a Create Response for this ONU.
+
+        :param _topic: (str) OMCI-RX topic
+        :param msg: (dict) Dictionary with 'rx-response' and 'tx-request' (if any)
+        """
+        self.log.debug('on-create-response', state=self.state)
+
+        def valid_request(stat, c_id, e_id):
+            return self._omci_cc_subscriptions[RxEvent.Delete] is not None\
+                and stat in (RC.Success, RC.InstanceExists) \
+                and c_id in self._me_watch_list.keys() \
+                and e_id not in self._me_watch_list[c_id]['instances']
+
+        response = msg[RX_RESPONSE_KEY]
+        omci = response.fields['omci_message'].fields
+        class_id = omci['entity_class']
+        entity_id = omci['entity_id']
+        status = omci['success_code']
+
+        if valid_request(status, class_id, entity_id):
+            request = msg[TX_REQUEST_KEY]
+            method = self._me_watch_list[class_id]['create-delete']
+            cid, eid = method(request, class_id, entity_id, add=True)
+
+            if cid > 0:
+                # BP entity_id -> (PM class_id, PM entity_id)
+                instances = self._me_watch_list[class_id]['instances']
+                instances[entity_id] = (cid, eid)
+
+    def on_delete_response(self, _topic, msg):
+        """
+        Called upon receipt of a Delete Response for this ONU
+
+        :param _topic: (str) OMCI-RX topic
+        :param msg: (dict) Dictionary with 'rx-response' and 'tx-request' (if any)
+        """
+        self.log.debug('on-delete-response', state=self.state)
+
+        def valid_request(stat, cid, eid):
+            return self._omci_cc_subscriptions[RxEvent.Delete] is not None\
+                and stat in (RC.Success, RC.UnknownInstance) \
+                and cid in self._me_watch_list.keys() \
+                and eid in self._me_watch_list[cid]['instances']
+
+        response = msg[RX_RESPONSE_KEY]
+        omci = response.fields['omci_message'].fields
+        class_id = omci['entity_class']
+        entity_id = omci['entity_id']
+        status = omci['success_code']
+
+        if valid_request(status, class_id, entity_id):
+            request = msg[TX_REQUEST_KEY]
+            method = self._me_watch_list[class_id]['create-delete']
+
+            method(request, class_id, entity_id, add=False)
+            # BP entity_id -> (PM class_id, PM entity_id)
+            instances = self._me_watch_list[class_id]['instances']
+            del instances[entity_id]
+
+    def get_pm_entity_id_for_add(self, pm_cid, eid):
+        """
+        Select the Entity ID to use for a specific PM Class ID.  For extended
+        PM ME's, an entity id (>0) is allocated
+
+        :param pm_cid: (int) PM ME Class ID to create/get entry ID for
+        :param eid: (int) Reference class's entity ID. Used as PM entity ID for non-
+                    extended PM history PMs
+        :return: (int) Entity ID to use
+        """
+        if pm_cid in (EthernetFrameExtendedPerformanceMonitoring.class_id,
+                      EthernetFrameExtendedPerformanceMonitoring64Bit.class_id):
+            return self._enet_entity_id.get_next()
+        return eid
+
+    def release_pm_entity_id(self, pm_cid, eid):
+        if pm_cid in (EthernetFrameExtendedPerformanceMonitoring.class_id,
+                      EthernetFrameExtendedPerformanceMonitoring64Bit.class_id):
+            try:
+                self._enet_entity_id.release(eid)
+            except:
+                pass
+
+    def add_remove_enet_frame_pm(self, request, class_id, entity_id,
+                                 add=True,
+                                 attributes=None):
+        """
+        Add/remove PM for the dynamic MAC Port configuration data.
+
+        This can be called in a variety of ways:
+
+           o If from an Response event from OMCI_CC, the request will contain
+             the original create/delete request. The class_id and entity_id will
+             be the MAC Data Configuration Data class and instance ID.
+             add = True if create, False if delete
+
+           o If starting up (and the associated ME is already created), the MAC
+             Data Configuration Data class and instance ID, and attributes are
+             provided. request = None and add = True
+
+           o If cleaning up (stopping), the PM ME class_id, entity_id are provided.
+             request = None and add = False
+
+        :return: (int, int) PM ME class_id and entity_id for add/remove was performed.
+                            class and entity IDs are non-zero on success
+        """
+        pm_entity_id = 0
+        cid = 0
+        eid = 0
+        upstream = False
+
+        def tp_type_to_pm(tp):
+            # TODO: Support 64-bit extended Monitoring MEs.
+            # This will result in the need to maintain entity IDs of PMs differently
+            upstream_types = [  # EthernetFrameExtendedPerformanceMonitoring64Bit.class_id,
+                              EthernetFrameExtendedPerformanceMonitoring.class_id,
+                              EthernetFrameUpstreamPerformanceMonitoringHistoryData.class_id], True
+            downstream_types = [  # EthernetFrameExtendedPerformanceMonitoring64Bit.class_id,
+                                EthernetFrameExtendedPerformanceMonitoring.class_id,
+                                EthernetFrameDownstreamPerformanceMonitoringHistoryData.class_id], False
+            return {
+                1: downstream_types,
+                3: upstream_types,
+                5: downstream_types,
+                6: downstream_types,
+            }.get(tp, None)
+
+        if request is not None:
+            assert class_id == MacBridgePortConfigurationData.class_id
+
+            # Is this associated with the ANI or the UNI side of the bridge?
+            # For VOLTHA v2.0, only high-speed internet data service is
+            attributes = request.fields['omci_message'].fields['data']
+            pm_class_ids, upstream = tp_type_to_pm(attributes['tp_type'])
+            cid = request.fields['omci_message'].fields['entity_class']
+            eid = request.fields['omci_message'].fields['entity_id']
+            if not add:
+                instances = self._me_watch_list[cid]['instances']
+                _, pm_entity_id = instances.get(eid, (None, None))
+
+        elif add:
+            assert class_id == MacBridgePortConfigurationData.class_id
+            assert isinstance(attributes, dict)
+
+            # Is this associated with the ANI or the UNI side of the bridge?
+            pm_class_ids, upstream = tp_type_to_pm(attributes.get('tp_type'))
+            cid = class_id
+            eid = entity_id
+
+        else:
+            assert class_id in (EthernetFrameUpstreamPerformanceMonitoringHistoryData.class_id,
+                                EthernetFrameDownstreamPerformanceMonitoringHistoryData.class_id,
+                                EthernetFrameExtendedPerformanceMonitoring.class_id,
+                                EthernetFrameExtendedPerformanceMonitoring64Bit.class_id)
+            pm_class_ids = [class_id]
+
+        if pm_class_ids is None:
+            return False     # Unable to select a supported ME for this ONU
+
+        if add:
+            for pm_class_id in pm_class_ids:
+                if self._me_is_supported(pm_class_id):
+                    pm_entity_id = self.get_pm_entity_id_for_add(pm_class_id, eid)
+                    self.add_pm_me(pm_class_id, pm_entity_id, cid=cid, eid=eid,
+                                   upstream=upstream)
+                    return pm_class_id, pm_entity_id
+        else:
+            for pm_class_id in pm_class_ids:
+                if self._me_is_supported(pm_class_id):
+                    self.delete_pm_me(pm_class_id, pm_entity_id)
+                    self.release_pm_entity_id(pm_class_id, pm_entity_id)
+                    return pm_class_id, pm_entity_id
+
+        return 0, 0