← Back to team overview

openlp-core team mailing list archive

[Merge] lp:~alisonken1/openlp/pjlink2g into lp:openlp

 

Ken Roberts has proposed merging lp:~alisonken1/openlp/pjlink2g into lp:openlp.

Commit message:
PJLink2 update 

Requested reviews:
  OpenLP Core (openlp-core)

For more details, see:
https://code.launchpad.net/~alisonken1/openlp/pjlink2g/+merge/328634

- Break PJLink class into base class and process commands class
- Restructure class methods
- Break projector PJLink tests into pjlink_base and pjlink_commands
- Restructure test methods
- Remove unused test imports
- Rename several tests
- Remove extraneous test (test_projector_return_ok)
- Added tests for process_erst reply

--------------------------------
lp:~alisonken1/openlp/pjlink2g (revision 2756)
[SUCCESS] https://ci.openlp.io/job/Branch-01-Pull/2117/
[SUCCESS] https://ci.openlp.io/job/Branch-02-Functional-Tests/2027/
[SUCCESS] https://ci.openlp.io/job/Branch-03-Interface-Tests/1935/
[SUCCESS] https://ci.openlp.io/job/Branch-04a-Code_Analysis/1312/
[SUCCESS] https://ci.openlp.io/job/Branch-04b-Test_Coverage/1155/
[SUCCESS] https://ci.openlp.io/job/Branch-04c-Code_Analysis2/285/
[SUCCESS] https://ci.openlp.io/job/Branch-05-AppVeyor-Tests/130/

-- 
Your team OpenLP Core is requested to review the proposed merge of lp:~alisonken1/openlp/pjlink2g into lp:openlp.
=== modified file 'openlp/core/lib/__init__.py'
--- openlp/core/lib/__init__.py	2017-05-20 05:51:58 +0000
+++ openlp/core/lib/__init__.py	2017-08-06 07:33:29 +0000
@@ -621,5 +621,5 @@
 from .renderer import Renderer
 from .mediamanageritem import MediaManagerItem
 from .projector.db import ProjectorDB, Projector
-from .projector.pjlink1 import PJLink
+from .projector.pjlink import PJLink
 from .projector.constants import PJLINK_PORT, ERROR_MSG, ERROR_STRING

=== modified file 'openlp/core/lib/projector/constants.py'
--- openlp/core/lib/projector/constants.py	2017-06-09 12:12:39 +0000
+++ openlp/core/lib/projector/constants.py	2017-08-06 07:33:29 +0000
@@ -154,7 +154,7 @@
              },
     'SRCH': {'version': ['2', ],
              'description': translate('OpenLP.PJLinkConstants',
-                                      'UDP broadcast search request for available projectors.')
+                                      'UDP broadcast search request for available projectors. Reply is ACKN.')
              },
     'SVER': {'version': ['2', ],
              'description': translate('OpenLP.PJLinkConstants',

=== renamed file 'openlp/core/lib/projector/pjlink1.py' => 'openlp/core/lib/projector/pjlink.py'
--- openlp/core/lib/projector/pjlink1.py	2017-07-20 15:31:50 +0000
+++ openlp/core/lib/projector/pjlink.py	2017-08-06 07:33:29 +0000
@@ -20,14 +20,17 @@
 # Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
 ###############################################################################
 """
-    :mod:`openlp.core.lib.projector.pjlink1` module
+    :mod:`openlp.core.lib.projector.pjlink` module
     Provides the necessary functions for connecting to a PJLink-capable projector.
 
-    See PJLink Class 1 Specifications for details.
-    http://pjlink.jbmia.or.jp/english/dl.html
-
-        Section 5-1 PJLink Specifications
-
+    PJLink Class 1 Specifications.
+    http://pjlink.jbmia.or.jp/english/dl_class1.html
+        Section 5-1 PJLink Specifications
+        Section 5-5 Guidelines for Input Terminals
+
+    PJLink Class 2 Specifications.
+    http://pjlink.jbmia.or.jp/english/dl_class2.html
+        Section 5-1 PJLink Specifications
         Section 5-5 Guidelines for Input Terminals
 
     NOTE:
@@ -40,7 +43,7 @@
 import logging
 log = logging.getLogger(__name__)
 
-log.debug('pjlink1 loaded')
+log.debug('pjlink loaded')
 
 __all__ = ['PJLink']
 
@@ -69,7 +72,407 @@
 PJLINK_SUFFIX = CR
 
 
-class PJLink(QtNetwork.QTcpSocket):
+class PJLinkCommands(object):
+    """
+    Process replies from PJLink projector.
+    """
+
+    def __init__(self, *args, **kwargs):
+        """
+        Setup for the process commands
+        """
+        log.debug('PJlinkCommands(args={args} kwargs={kwargs})'.format(args=args, kwargs=kwargs))
+        super().__init__()
+        # Map command to function
+        self.pjlink_functions = {
+            'AVMT': self.process_avmt,
+            'CLSS': self.process_clss,
+            'ERST': self.process_erst,
+            'INFO': self.process_info,
+            'INF1': self.process_inf1,
+            'INF2': self.process_inf2,
+            'INPT': self.process_inpt,
+            'INST': self.process_inst,
+            'LAMP': self.process_lamp,
+            'NAME': self.process_name,
+            'PJLINK': self.check_login,
+            'POWR': self.process_powr,
+            'SNUM': self.process_snum,
+            'SVER': self.process_sver,
+            'RFIL': self.process_rfil,
+            'RLMP': self.process_rlmp
+        }
+
+    def reset_information(self):
+        """
+        Initialize instance variables. Also used to reset projector-specific information to default.
+        """
+        log.debug('({ip}) reset_information() connect status is {state}'.format(ip=self.ip, state=self.state()))
+        self.fan = None  # ERST
+        self.filter_time = None  # FILT
+        self.lamp = None  # LAMP
+        self.mac_adx_received = None  # ACKN
+        self.manufacturer = None  # INF1
+        self.model = None  # INF2
+        self.model_filter = None  # RFIL
+        self.model_lamp = None  # RLMP
+        self.mute = None  # AVMT
+        self.other_info = None  # INFO
+        self.pjlink_class = PJLINK_CLASS  # Default class
+        self.pjlink_name = None  # NAME
+        self.power = S_OFF  # POWR
+        self.serial_no = None  # SNUM
+        self.serial_no_received = None
+        self.sw_version = None  # SVER
+        self.sw_version_received = None
+        self.shutter = None  # AVMT
+        self.source_available = None  # INST
+        self.source = None  # INPT
+        # These should be part of PJLink() class, but set here for convenience
+        if hasattr(self, 'timer'):
+            log.debug('({ip}): Calling timer.stop()'.format(ip=self.ip))
+            self.timer.stop()
+        if hasattr(self, 'socket_timer'):
+            log.debug('({ip}): Calling socket_timer.stop()'.format(ip=self.ip))
+            self.socket_timer.stop()
+        self.send_busy = False
+        self.send_queue = []
+
+    def process_command(self, cmd, data):
+        """
+        Verifies any return error code. Calls the appropriate command handler.
+
+        :param cmd: Command to process
+        :param data: Data being processed
+        """
+        log.debug('({ip}) Processing command "{cmd}" with data "{data}"'.format(ip=self.ip,
+                                                                                cmd=cmd,
+                                                                                data=data))
+        # Check if we have a future command not available yet
+        if cmd not in PJLINK_VALID_CMD:
+            log.error("({ip}) Ignoring command='{cmd}' (Invalid/Unknown)".format(ip=self.ip, cmd=cmd))
+            return
+        elif cmd not in self.pjlink_functions:
+            log.warn("({ip}) Unable to process command='{cmd}' (Future option)".format(ip=self.ip, cmd=cmd))
+            return
+        elif data in PJLINK_ERRORS:
+            # Oops - projector error
+            log.error('({ip}) Projector returned error "{data}"'.format(ip=self.ip, data=data))
+            if data.upper() == 'ERRA':
+                # Authentication error
+                self.disconnect_from_host()
+                self.change_status(E_AUTHENTICATION)
+                log.debug('({ip}) emitting projectorAuthentication() signal'.format(ip=self.ip))
+                self.projectorAuthentication.emit(self.name)
+            elif data.upper() == 'ERR1':
+                # Undefined command
+                self.change_status(E_UNDEFINED, '{error}: "{data}"'.format(error=ERROR_MSG[E_UNDEFINED],
+                                                                           data=cmd))
+            elif data.upper() == 'ERR2':
+                # Invalid parameter
+                self.change_status(E_PARAMETER)
+            elif data.upper() == 'ERR3':
+                # Projector busy
+                self.change_status(E_UNAVAILABLE)
+            elif data.upper() == 'ERR4':
+                # Projector/display error
+                self.change_status(E_PROJECTOR)
+            self.receive_data_signal()
+            return
+        # Command succeeded - no extra information
+        elif data.upper() == 'OK':
+            log.debug('({ip}) Command returned OK'.format(ip=self.ip))
+            # A command returned successfully
+            self.receive_data_signal()
+            return
+        # Command checks already passed
+        log.debug('({ip}) Calling function for {cmd}'.format(ip=self.ip, cmd=cmd))
+        self.receive_data_signal()
+        self.pjlink_functions[cmd](data)
+
+    def process_avmt(self, data):
+        """
+        Process shutter and speaker status. See PJLink specification for format.
+        Update self.mute (audio) and self.shutter (video shutter).
+
+        :param data: Shutter and audio status
+        """
+        shutter = self.shutter
+        mute = self.mute
+        if data == '11':
+            shutter = True
+            mute = False
+        elif data == '21':
+            shutter = False
+            mute = True
+        elif data == '30':
+            shutter = False
+            mute = False
+        elif data == '31':
+            shutter = True
+            mute = True
+        else:
+            log.warning('({ip}) Unknown shutter response: {data}'.format(ip=self.ip, data=data))
+        update_icons = shutter != self.shutter
+        update_icons = update_icons or mute != self.mute
+        self.shutter = shutter
+        self.mute = mute
+        if update_icons:
+            self.projectorUpdateIcons.emit()
+        return
+
+    def process_clss(self, data):
+        """
+        PJLink class that this projector supports. See PJLink specification for format.
+        Updates self.class.
+
+        :param data: Class that projector supports.
+        """
+        # bug 1550891: Projector returns non-standard class response:
+        #            : Expected: '%1CLSS=1'
+        #            : Received: '%1CLSS=Class 1'  (Optoma)
+        #            : Received: '%1CLSS=Version1'  (BenQ)
+        if len(data) > 1:
+            log.warn("({ip}) Non-standard CLSS reply: '{data}'".format(ip=self.ip, data=data))
+            # Due to stupid projectors not following standards (Optoma, BenQ comes to mind),
+            # AND the different responses that can be received, the semi-permanent way to
+            # fix the class reply is to just remove all non-digit characters.
+            try:
+                clss = re.findall('\d', data)[0]  # Should only be the first match
+            except IndexError:
+                log.error("({ip}) No numbers found in class version reply - defaulting to class '1'".format(ip=self.ip))
+                clss = '1'
+        elif not data.isdigit():
+            log.error("({ip}) NAN class version reply - defaulting to class '1'".format(ip=self.ip))
+            clss = '1'
+        else:
+            clss = data
+        self.pjlink_class = clss
+        log.debug('({ip}) Setting pjlink_class for this projector to "{data}"'.format(ip=self.ip,
+                                                                                      data=self.pjlink_class))
+        return
+
+    def process_erst(self, data):
+        """
+        Error status. See PJLink Specifications for format.
+        Updates self.projector_errors
+
+\        :param data: Error status
+        """
+        try:
+            datacheck = int(data)
+        except ValueError:
+            # Bad data - ignore
+            return
+        if datacheck == 0:
+            self.projector_errors = None
+        else:
+            self.projector_errors = {}
+            # Fan
+            if data[0] != '0':
+                self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Fan')] = \
+                    PJLINK_ERST_STATUS[data[0]]
+            # Lamp
+            if data[1] != '0':
+                self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Lamp')] =  \
+                    PJLINK_ERST_STATUS[data[1]]
+            # Temp
+            if data[2] != '0':
+                self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Temperature')] =  \
+                    PJLINK_ERST_STATUS[data[2]]
+            # Cover
+            if data[3] != '0':
+                self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Cover')] =  \
+                    PJLINK_ERST_STATUS[data[3]]
+            # Filter
+            if data[4] != '0':
+                self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Filter')] =  \
+                    PJLINK_ERST_STATUS[data[4]]
+            # Other
+            if data[5] != '0':
+                self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Other')] =  \
+                    PJLINK_ERST_STATUS[data[5]]
+        return
+
+    def process_inf1(self, data):
+        """
+        Manufacturer name set in projector.
+        Updates self.manufacturer
+
+        :param data: Projector manufacturer
+        """
+        self.manufacturer = data
+        log.debug('({ip}) Setting projector manufacturer data to "{data}"'.format(ip=self.ip, data=self.manufacturer))
+        return
+
+    def process_inf2(self, data):
+        """
+        Projector Model set in projector.
+        Updates self.model.
+
+        :param data: Model name
+        """
+        self.model = data
+        log.debug('({ip}) Setting projector model to "{data}"'.format(ip=self.ip, data=self.model))
+        return
+
+    def process_info(self, data):
+        """
+        Any extra info set in projector.
+        Updates self.other_info.
+
+        :param data: Projector other info
+        """
+        self.other_info = data
+        log.debug('({ip}) Setting projector other_info to "{data}"'.format(ip=self.ip, data=self.other_info))
+        return
+
+    def process_inpt(self, data):
+        """
+        Current source input selected. See PJLink specification for format.
+        Update self.source
+
+        :param data: Currently selected source
+        """
+        self.source = data
+        log.info('({ip}) Setting data source to "{data}"'.format(ip=self.ip, data=self.source))
+        return
+
+    def process_inst(self, data):
+        """
+        Available source inputs. See PJLink specification for format.
+        Updates self.source_available
+
+        :param data: Sources list
+        """
+        sources = []
+        check = data.split()
+        for source in check:
+            sources.append(source)
+        sources.sort()
+        self.source_available = sources
+        self.projectorUpdateIcons.emit()
+        log.debug('({ip}) Setting projector sources_available to "{data}"'.format(ip=self.ip,
+                                                                                  data=self.source_available))
+        return
+
+    def process_lamp(self, data):
+        """
+        Lamp(s) status. See PJLink Specifications for format.
+        Data may have more than 1 lamp to process.
+        Update self.lamp dictionary with lamp status.
+
+        :param data: Lamp(s) status.
+        """
+        lamps = []
+        data_dict = data.split()
+        while data_dict:
+            try:
+                fill = {'Hours': int(data_dict[0]), 'On': False if data_dict[1] == '0' else True}
+            except ValueError:
+                # In case of invalid entry
+                log.warning('({ip}) process_lamp(): Invalid data "{data}"'.format(ip=self.ip, data=data))
+                return
+            lamps.append(fill)
+            data_dict.pop(0)  # Remove lamp hours
+            data_dict.pop(0)  # Remove lamp on/off
+        self.lamp = lamps
+        return
+
+    def process_name(self, data):
+        """
+        Projector name set in projector.
+        Updates self.pjlink_name
+
+        :param data: Projector name
+        """
+        self.pjlink_name = data
+        log.debug('({ip}) Setting projector PJLink name to "{data}"'.format(ip=self.ip, data=self.pjlink_name))
+        return
+
+    def process_powr(self, data):
+        """
+        Power status. See PJLink specification for format.
+        Update self.power with status. Update icons if change from previous setting.
+
+        :param data: Power status
+        """
+        log.debug('({ip}: Processing POWR command'.format(ip=self.ip))
+        if data in PJLINK_POWR_STATUS:
+            power = PJLINK_POWR_STATUS[data]
+            update_icons = self.power != power
+            self.power = power
+            self.change_status(PJLINK_POWR_STATUS[data])
+            if update_icons:
+                self.projectorUpdateIcons.emit()
+                # Update the input sources available
+                if power == S_ON:
+                    self.send_command('INST')
+        else:
+            # Log unknown status response
+            log.warning('({ip}) Unknown power response: {data}'.format(ip=self.ip, data=data))
+        return
+
+    def process_rfil(self, data):
+        """
+        Process replacement filter type
+        """
+        if self.model_filter is None:
+            self.model_filter = data
+        else:
+            log.warn("({ip}) Filter model already set".format(ip=self.ip))
+            log.warn("({ip}) Saved model: '{old}'".format(ip=self.ip, old=self.model_filter))
+            log.warn("({ip}) New model: '{new}'".format(ip=self.ip, new=data))
+
+    def process_rlmp(self, data):
+        """
+        Process replacement lamp type
+        """
+        if self.model_lamp is None:
+            self.model_lamp = data
+        else:
+            log.warn("({ip}) Lamp model already set".format(ip=self.ip))
+            log.warn("({ip}) Saved lamp: '{old}'".format(ip=self.ip, old=self.model_lamp))
+            log.warn("({ip}) New lamp: '{new}'".format(ip=self.ip, new=data))
+
+    def process_snum(self, data):
+        """
+        Serial number of projector.
+
+        :param data: Serial number from projector.
+        """
+        if self.serial_no is None:
+            log.debug("({ip}) Setting projector serial number to '{data}'".format(ip=self.ip, data=data))
+            self.serial_no = data
+            self.db_update = False
+        else:
+            # Compare serial numbers and see if we got the same projector
+            if self.serial_no != data:
+                log.warn("({ip}) Projector serial number does not match saved serial number".format(ip=self.ip))
+                log.warn("({ip}) Saved:    '{old}'".format(ip=self.ip, old=self.serial_no))
+                log.warn("({ip}) Received: '{new}'".format(ip=self.ip, new=data))
+                log.warn("({ip}) NOT saving serial number".format(ip=self.ip))
+                self.serial_no_received = data
+
+    def process_sver(self, data):
+        """
+        Software version of projector
+        """
+        if self.sw_version is None:
+            log.debug("({ip}) Setting projector software version to '{data}'".format(ip=self.ip, data=data))
+            self.sw_version = data
+            self.db_update = True
+        else:
+            # Compare software version and see if we got the same projector
+            if self.serial_no != data:
+                log.warn("({ip}) Projector software version does not match saved software version".format(ip=self.ip))
+                log.warn("({ip}) Saved:    '{old}'".format(ip=self.ip, old=self.sw_version))
+                log.warn("({ip}) Received: '{new}'".format(ip=self.ip, new=data))
+                log.warn("({ip}) NOT saving serial number".format(ip=self.ip))
+                self.sw_version_received = data
+
+
+class PJLink(PJLinkCommands, QtNetwork.QTcpSocket):
     """
     Socket service for connecting to a PJLink-capable projector.
     """
@@ -84,17 +487,18 @@
 
     # New commands available in PJLink Class 2
     pjlink_udp_commands = [
-        'ACKN',
+        'ACKN',  # Class 2
         'ERST',  # Class 1 or 2
         'INPT',  # Class 1 or 2
-        'LKUP',
+        'LKUP',  # Class 2
         'POWR',  # Class 1 or 2
-        'SRCH'
+        'SRCH'   # Class 2
     ]
 
-    def __init__(self, name=None, ip=None, port=PJLINK_PORT, pin=None, *args, **kwargs):
+    def __init__(self, port=PJLINK_PORT, *args, **kwargs):
         """
         Setup for instance.
+        Options should be in kwargs except for port which does have a default.
 
         :param name: Display name
         :param ip: IP address to connect to
@@ -109,23 +513,16 @@
         :param socket_timeout: Time (in seconds) to abort the connection if no response
         """
         log.debug('PJlink(args={args} kwargs={kwargs})'.format(args=args, kwargs=kwargs))
-        self.name = name
-        self.ip = ip
-        self.port = port
-        self.pin = pin
         super().__init__()
-        self.model_lamp = None
-        self.model_filter = None
-        self.mac_adx = kwargs.get('mac_adx')
-        self.serial_no = None
-        self.serial_no_received = None  # Used only if saved serial number is different than received serial number
-        self.dbid = None
-        self.db_update = False  # Use to check if db needs to be updated prior to exiting
-        self.location = None
-        self.notes = None
         self.dbid = kwargs.get('dbid')
+        self.ip = kwargs.get('ip')
         self.location = kwargs.get('location')
+        self.mac_adx = kwargs.get('mac_adx')
+        self.name = kwargs.get('name')
         self.notes = kwargs.get('notes')
+        self.pin = kwargs.get('pin')
+        self.port = port
+        self.db_update = False  # Use to check if db needs to be updated prior to exiting
         # Poll time 20 seconds unless called with something else
         self.poll_time = 20000 if 'poll_time' not in kwargs else kwargs['poll_time'] * 1000
         # Timeout 5 seconds unless called with something else
@@ -141,8 +538,6 @@
         # Add enough space to input buffer for extraneous \n \r
         self.max_size = PJLINK_MAX_PACKET + 2
         self.setReadBufferSize(self.max_size)
-        # PJLink information
-        self.pjlink_class = '1'  # Default class
         self.reset_information()
         # Set from ProjectorManager.add_projector()
         self.widget = None  # QListBox entry
@@ -151,58 +546,6 @@
         self.send_busy = False
         # Socket timer for some possible brain-dead projectors or network cable pulled
         self.socket_timer = None
-        # Map command to function
-        self.pjlink_functions = {
-            'AVMT': self.process_avmt,
-            'CLSS': self.process_clss,
-            'ERST': self.process_erst,
-            'INFO': self.process_info,
-            'INF1': self.process_inf1,
-            'INF2': self.process_inf2,
-            'INPT': self.process_inpt,
-            'INST': self.process_inst,
-            'LAMP': self.process_lamp,
-            'NAME': self.process_name,
-            'PJLINK': self.check_login,
-            'POWR': self.process_powr,
-            'SNUM': self.process_snum,
-            'SVER': self.process_sver,
-            'RFIL': self.process_rfil,
-            'RLMP': self.process_rlmp
-        }
-
-    def reset_information(self):
-        """
-        Reset projector-specific information to default
-        """
-        log.debug('({ip}) reset_information() connect status is {state}'.format(ip=self.ip, state=self.state()))
-        self.send_queue = []
-        self.power = S_OFF
-        self.pjlink_name = None
-        self.manufacturer = None
-        self.model = None
-        self.serial_no = None
-        self.serial_no_received = None
-        self.sw_version = None
-        self.sw_version_received = None
-        self.mac_adx = None
-        self.shutter = None
-        self.mute = None
-        self.lamp = None
-        self.model_lamp = None
-        self.fan = None
-        self.filter_time = None
-        self.model_filter = None
-        self.source_available = None
-        self.source = None
-        self.other_info = None
-        if hasattr(self, 'timer'):
-            log.debug('({ip}): Calling timer.stop()'.format(ip=self.ip))
-            self.timer.stop()
-        if hasattr(self, 'socket_timer'):
-            log.debug('({ip}): Calling socket_timer.stop()'.format(ip=self.ip))
-            self.socket_timer.stop()
-        self.send_busy = False
 
     def thread_started(self):
         """
@@ -290,28 +633,6 @@
             if self.model_lamp is None:
                 self.send_command('RLMP', queue=True)
 
-    def process_rfil(self, data):
-        """
-        Process replacement filter type
-        """
-        if self.model_filter is None:
-            self.model_filter = data
-        else:
-            log.warn("({ip}) Filter model already set".format(ip=self.ip))
-            log.warn("({ip}) Saved model: '{old}'".format(ip=self.ip, old=self.model_filter))
-            log.warn("({ip}) New model: '{new}'".format(ip=self.ip, new=data))
-
-    def process_rlmp(self, data):
-        """
-        Process replacement lamp type
-        """
-        if self.model_lamp is None:
-            self.model_lamp = data
-        else:
-            log.warn("({ip}) Lamp model already set".format(ip=self.ip))
-            log.warn("({ip}) Saved lamp: '{old}'".format(ip=self.ip, old=self.model_lamp))
-            log.warn("({ip}) New lamp: '{new}'".format(ip=self.ip, new=data))
-
     def _get_status(self, status):
         """
         Helper to retrieve status/error codes and convert to strings.
@@ -474,6 +795,7 @@
             self.send_busy = False
             return
         read = self.readLine(self.max_size)
+        log.debug("({ip}) get_data(): '{buff}'".format(ip=self.ip, buff=read))
         if read == -1:
             # No data available
             log.debug('({ip}) get_data(): No data available (-1)'.format(ip=self.ip))
@@ -626,317 +948,6 @@
             self.change_status(E_NETWORK,
                                translate('OpenLP.PJLink', 'Error while sending data to projector'))
 
-    def process_command(self, cmd, data):
-        """
-        Verifies any return error code. Calls the appropriate command handler.
-
-        :param cmd: Command to process
-        :param data: Data being processed
-        """
-        log.debug('({ip}) Processing command "{cmd}" with data "{data}"'.format(ip=self.ip,
-                                                                                cmd=cmd,
-                                                                                data=data))
-        # Check if we have a future command not available yet
-        if cmd not in PJLINK_VALID_CMD:
-            log.error('({ip}) Unknown command received - ignoring'.format(ip=self.ip))
-            return
-        elif cmd not in self.pjlink_functions:
-            log.warn('({ip}) Future command received - unable to process yet'.format(ip=self.ip))
-            return
-        elif data in PJLINK_ERRORS:
-            # Oops - projector error
-            log.error('({ip}) Projector returned error "{data}"'.format(ip=self.ip, data=data))
-            if data.upper() == 'ERRA':
-                # Authentication error
-                self.disconnect_from_host()
-                self.change_status(E_AUTHENTICATION)
-                log.debug('({ip}) emitting projectorAuthentication() signal'.format(ip=self.ip))
-                self.projectorAuthentication.emit(self.name)
-            elif data.upper() == 'ERR1':
-                # Undefined command
-                self.change_status(E_UNDEFINED, '{error}: "{data}"'.format(error=ERROR_MSG[E_UNDEFINED],
-                                                                           data=cmd))
-            elif data.upper() == 'ERR2':
-                # Invalid parameter
-                self.change_status(E_PARAMETER)
-            elif data.upper() == 'ERR3':
-                # Projector busy
-                self.change_status(E_UNAVAILABLE)
-            elif data.upper() == 'ERR4':
-                # Projector/display error
-                self.change_status(E_PROJECTOR)
-            self.receive_data_signal()
-            return
-        # Command succeeded - no extra information
-        elif data.upper() == 'OK':
-            log.debug('({ip}) Command returned OK'.format(ip=self.ip))
-            # A command returned successfully
-            self.receive_data_signal()
-            return
-        # Command checks already passed
-        log.debug('({ip}) Calling function for {cmd}'.format(ip=self.ip, cmd=cmd))
-        self.receive_data_signal()
-        self.pjlink_functions[cmd](data)
-
-    def process_lamp(self, data):
-        """
-        Lamp(s) status. See PJLink Specifications for format.
-        Data may have more than 1 lamp to process.
-        Update self.lamp dictionary with lamp status.
-
-        :param data: Lamp(s) status.
-        """
-        lamps = []
-        data_dict = data.split()
-        while data_dict:
-            try:
-                fill = {'Hours': int(data_dict[0]), 'On': False if data_dict[1] == '0' else True}
-            except ValueError:
-                # In case of invalid entry
-                log.warning('({ip}) process_lamp(): Invalid data "{data}"'.format(ip=self.ip, data=data))
-                return
-            lamps.append(fill)
-            data_dict.pop(0)  # Remove lamp hours
-            data_dict.pop(0)  # Remove lamp on/off
-        self.lamp = lamps
-        return
-
-    def process_powr(self, data):
-        """
-        Power status. See PJLink specification for format.
-        Update self.power with status. Update icons if change from previous setting.
-
-        :param data: Power status
-        """
-        log.debug('({ip}: Processing POWR command'.format(ip=self.ip))
-        if data in PJLINK_POWR_STATUS:
-            power = PJLINK_POWR_STATUS[data]
-            update_icons = self.power != power
-            self.power = power
-            self.change_status(PJLINK_POWR_STATUS[data])
-            if update_icons:
-                self.projectorUpdateIcons.emit()
-                # Update the input sources available
-                if power == S_ON:
-                    self.send_command('INST')
-        else:
-            # Log unknown status response
-            log.warning('({ip}) Unknown power response: {data}'.format(ip=self.ip, data=data))
-        return
-
-    def process_avmt(self, data):
-        """
-        Process shutter and speaker status. See PJLink specification for format.
-        Update self.mute (audio) and self.shutter (video shutter).
-
-        :param data: Shutter and audio status
-        """
-        shutter = self.shutter
-        mute = self.mute
-        if data == '11':
-            shutter = True
-            mute = False
-        elif data == '21':
-            shutter = False
-            mute = True
-        elif data == '30':
-            shutter = False
-            mute = False
-        elif data == '31':
-            shutter = True
-            mute = True
-        else:
-            log.warning('({ip}) Unknown shutter response: {data}'.format(ip=self.ip, data=data))
-        update_icons = shutter != self.shutter
-        update_icons = update_icons or mute != self.mute
-        self.shutter = shutter
-        self.mute = mute
-        if update_icons:
-            self.projectorUpdateIcons.emit()
-        return
-
-    def process_inpt(self, data):
-        """
-        Current source input selected. See PJLink specification for format.
-        Update self.source
-
-        :param data: Currently selected source
-        """
-        self.source = data
-        log.info('({ip}) Setting data source to "{data}"'.format(ip=self.ip, data=self.source))
-        return
-
-    def process_clss(self, data):
-        """
-        PJLink class that this projector supports. See PJLink specification for format.
-        Updates self.class.
-
-        :param data: Class that projector supports.
-        """
-        # bug 1550891: Projector returns non-standard class response:
-        #            : Expected: '%1CLSS=1'
-        #            : Received: '%1CLSS=Class 1'  (Optoma)
-        #            : Received: '%1CLSS=Version1'  (BenQ)
-        if len(data) > 1:
-            log.warn("({ip}) Non-standard CLSS reply: '{data}'".format(ip=self.ip, data=data))
-            # Due to stupid projectors not following standards (Optoma, BenQ comes to mind),
-            # AND the different responses that can be received, the semi-permanent way to
-            # fix the class reply is to just remove all non-digit characters.
-            try:
-                clss = re.findall('\d', data)[0]  # Should only be the first match
-            except IndexError:
-                log.error("({ip}) No numbers found in class version reply - defaulting to class '1'".format(ip=self.ip))
-                clss = '1'
-        elif not data.isdigit():
-            log.error("({ip}) NAN class version reply - defaulting to class '1'".format(ip=self.ip))
-            clss = '1'
-        else:
-            clss = data
-        self.pjlink_class = clss
-        log.debug('({ip}) Setting pjlink_class for this projector to "{data}"'.format(ip=self.ip,
-                                                                                      data=self.pjlink_class))
-        return
-
-    def process_name(self, data):
-        """
-        Projector name set in projector.
-        Updates self.pjlink_name
-
-        :param data: Projector name
-        """
-        self.pjlink_name = data
-        log.debug('({ip}) Setting projector PJLink name to "{data}"'.format(ip=self.ip, data=self.pjlink_name))
-        return
-
-    def process_inf1(self, data):
-        """
-        Manufacturer name set in projector.
-        Updates self.manufacturer
-
-        :param data: Projector manufacturer
-        """
-        self.manufacturer = data
-        log.debug('({ip}) Setting projector manufacturer data to "{data}"'.format(ip=self.ip, data=self.manufacturer))
-        return
-
-    def process_inf2(self, data):
-        """
-        Projector Model set in projector.
-        Updates self.model.
-
-        :param data: Model name
-        """
-        self.model = data
-        log.debug('({ip}) Setting projector model to "{data}"'.format(ip=self.ip, data=self.model))
-        return
-
-    def process_info(self, data):
-        """
-        Any extra info set in projector.
-        Updates self.other_info.
-
-        :param data: Projector other info
-        """
-        self.other_info = data
-        log.debug('({ip}) Setting projector other_info to "{data}"'.format(ip=self.ip, data=self.other_info))
-        return
-
-    def process_inst(self, data):
-        """
-        Available source inputs. See PJLink specification for format.
-        Updates self.source_available
-
-        :param data: Sources list
-        """
-        sources = []
-        check = data.split()
-        for source in check:
-            sources.append(source)
-        sources.sort()
-        self.source_available = sources
-        self.projectorUpdateIcons.emit()
-        log.debug('({ip}) Setting projector sources_available to "{data}"'.format(ip=self.ip,
-                                                                                  data=self.source_available))
-        return
-
-    def process_erst(self, data):
-        """
-        Error status. See PJLink Specifications for format.
-        Updates self.projector_errors
-
-        :param data: Error status
-        """
-        try:
-            datacheck = int(data)
-        except ValueError:
-            # Bad data - ignore
-            return
-        if datacheck == 0:
-            self.projector_errors = None
-        else:
-            self.projector_errors = {}
-            # Fan
-            if data[0] != '0':
-                self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Fan')] = \
-                    PJLINK_ERST_STATUS[data[0]]
-            # Lamp
-            if data[1] != '0':
-                self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Lamp')] =  \
-                    PJLINK_ERST_STATUS[data[1]]
-            # Temp
-            if data[2] != '0':
-                self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Temperature')] =  \
-                    PJLINK_ERST_STATUS[data[2]]
-            # Cover
-            if data[3] != '0':
-                self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Cover')] =  \
-                    PJLINK_ERST_STATUS[data[3]]
-            # Filter
-            if data[4] != '0':
-                self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Filter')] =  \
-                    PJLINK_ERST_STATUS[data[4]]
-            # Other
-            if data[5] != '0':
-                self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Other')] =  \
-                    PJLINK_ERST_STATUS[data[5]]
-        return
-
-    def process_snum(self, data):
-        """
-        Serial number of projector.
-
-        :param data: Serial number from projector.
-        """
-        if self.serial_no is None:
-            log.debug("({ip}) Setting projector serial number to '{data}'".format(ip=self.ip, data=data))
-            self.serial_no = data
-            self.db_update = False
-        else:
-            # Compare serial numbers and see if we got the same projector
-            if self.serial_no != data:
-                log.warn("({ip}) Projector serial number does not match saved serial number".format(ip=self.ip))
-                log.warn("({ip}) Saved:    '{old}'".format(ip=self.ip, old=self.serial_no))
-                log.warn("({ip}) Received: '{new}'".format(ip=self.ip, new=data))
-                log.warn("({ip}) NOT saving serial number".format(ip=self.ip))
-                self.serial_no_received = data
-
-    def process_sver(self, data):
-        """
-        Software version of projector
-        """
-        if self.sw_version is None:
-            log.debug("({ip}) Setting projector software version to '{data}'".format(ip=self.ip, data=data))
-            self.sw_version = data
-            self.db_update = True
-        else:
-            # Compare software version and see if we got the same projector
-            if self.serial_no != data:
-                log.warn("({ip}) Projector software version does not match saved software version".format(ip=self.ip))
-                log.warn("({ip}) Saved:    '{old}'".format(ip=self.ip, old=self.sw_version))
-                log.warn("({ip}) Received: '{new}'".format(ip=self.ip, new=data))
-                log.warn("({ip}) NOT saving serial number".format(ip=self.ip))
-                self.sw_version_received = data
-
     def connect_to_host(self):
         """
         Initiate connection to projector.
@@ -1098,11 +1109,3 @@
         self.send_busy = False
         self.projectorReceivedData.emit()
         return
-
-    def _not_implemented(self, cmd):
-        """
-        Log when a future PJLink command has not been implemented yet.
-        """
-        log.warn("({ip}) Future command '{cmd}' has not been implemented yet".format(ip=self.ip,
-                                                                                     cmd=cmd))
-        return

=== modified file 'openlp/core/lib/projector/upgrade.py'
--- openlp/core/lib/projector/upgrade.py	2017-07-07 23:43:50 +0000
+++ openlp/core/lib/projector/upgrade.py	2017-08-06 07:33:29 +0000
@@ -42,7 +42,7 @@
     """
     Version 1 upgrade - old db might/might not be versioned.
     """
-    log.debug('Skipping upgrade_1 of projector DB - not used')
+    log.debug('Skipping projector DB upgrade to version 1 - not used')
 
 
 def upgrade_2(session, metadata):
@@ -60,14 +60,14 @@
     :param session: DB session instance
     :param metadata: Metadata of current DB
     """
+    log.debug('Checking projector DB upgrade to version 2')
     projector_table = Table('projector', metadata, autoload=True)
-    if 'mac_adx' not in [col.name for col in projector_table.c.values()]:
-        log.debug("Upgrading projector DB to version '2'")
+    upgrade_db = 'mac_adx' not in [col.name for col in projector_table.c.values()]
+    if upgrade_db:
         new_op = get_upgrade_op(session)
         new_op.add_column('projector', Column('mac_adx', types.String(18), server_default=null()))
         new_op.add_column('projector', Column('serial_no', types.String(30), server_default=null()))
         new_op.add_column('projector', Column('sw_version', types.String(30), server_default=null()))
         new_op.add_column('projector', Column('model_filter', types.String(30), server_default=null()))
         new_op.add_column('projector', Column('model_lamp', types.String(30), server_default=null()))
-    else:
-        log.warn("Skipping upgrade_2 of projector DB")
+    log.debug('{status} projector DB upgrade to version 2'.format(status='Updated' if upgrade_db else 'Skipping'))

=== modified file 'openlp/core/ui/projector/manager.py'
--- openlp/core/ui/projector/manager.py	2017-07-07 23:43:50 +0000
+++ openlp/core/ui/projector/manager.py	2017-08-06 07:33:29 +0000
@@ -38,7 +38,7 @@
     E_NETWORK, E_NOT_CONNECTED, E_UNKNOWN_SOCKET_ERROR, STATUS_STRING, S_CONNECTED, S_CONNECTING, S_COOLDOWN, \
     S_INITIALIZE, S_NOT_CONNECTED, S_OFF, S_ON, S_STANDBY, S_WARMUP
 from openlp.core.lib.projector.db import ProjectorDB
-from openlp.core.lib.projector.pjlink1 import PJLink
+from openlp.core.lib.projector.pjlink import PJLink
 from openlp.core.lib.projector.pjlink2 import PJLinkUDP
 from openlp.core.ui.projector.editform import ProjectorEditForm
 from openlp.core.ui.projector.sourceselectform import SourceSelectTabs, SourceSelectSingle

=== modified file 'tests/functional/openlp_core_lib/test_projector_constants.py'
--- tests/functional/openlp_core_lib/test_projector_constants.py	2017-06-29 02:58:08 +0000
+++ tests/functional/openlp_core_lib/test_projector_constants.py	2017-08-06 07:33:29 +0000
@@ -22,7 +22,7 @@
 """
 Package to test the openlp.core.lib.projector.constants package.
 """
-from unittest import TestCase, skip
+from unittest import TestCase
 
 
 class TestProjectorConstants(TestCase):
@@ -40,4 +40,4 @@
         from openlp.core.lib.projector.constants import PJLINK_DEFAULT_CODES
 
         # THEN: Verify dictionary was build correctly
-        self.assertEquals(PJLINK_DEFAULT_CODES, TEST_VIDEO_CODES, 'PJLink video strings should match')
+        self.assertEqual(PJLINK_DEFAULT_CODES, TEST_VIDEO_CODES, 'PJLink video strings should match')

=== modified file 'tests/functional/openlp_core_lib/test_projector_db.py'
--- tests/functional/openlp_core_lib/test_projector_db.py	2017-06-29 02:58:08 +0000
+++ tests/functional/openlp_core_lib/test_projector_db.py	2017-08-06 07:33:29 +0000
@@ -29,8 +29,8 @@
 import shutil
 from tempfile import mkdtemp
 
-from unittest import TestCase, skip
-from unittest.mock import MagicMock, patch
+from unittest import TestCase
+from unittest.mock import patch
 
 from openlp.core.lib.projector import upgrade
 from openlp.core.lib.db import upgrade_db
@@ -413,7 +413,7 @@
         Test add_projector() fail
         """
         # GIVEN: Test entry in the database
-        ignore_result = self.projector.add_projector(Projector(**TEST1_DATA))
+        self.projector.add_projector(Projector(**TEST1_DATA))
 
         # WHEN: Attempt to add same projector entry
         results = self.projector.add_projector(Projector(**TEST1_DATA))
@@ -439,7 +439,7 @@
         Test update_projector() when entry not in database
         """
         # GIVEN: Projector entry in database
-        ignore_result = self.projector.add_projector(Projector(**TEST1_DATA))
+        self.projector.add_projector(Projector(**TEST1_DATA))
         projector = Projector(**TEST2_DATA)
 
         # WHEN: Attempt to update data with a different ID

=== renamed file 'tests/functional/openlp_core_lib/test_projector_pjlink1.py' => 'tests/functional/openlp_core_lib/test_projector_pjlink_base.py'
--- tests/functional/openlp_core_lib/test_projector_pjlink1.py	2017-06-29 02:58:08 +0000
+++ tests/functional/openlp_core_lib/test_projector_pjlink_base.py	2017-08-06 07:33:29 +0000
@@ -20,21 +20,20 @@
 # Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
 ###############################################################################
 """
-Package to test the openlp.core.lib.projector.pjlink1 package.
+Package to test the openlp.core.lib.projector.pjlink base package.
 """
 from unittest import TestCase
 from unittest.mock import call, patch, MagicMock
 
-from openlp.core.lib.projector.pjlink1 import PJLink
-from openlp.core.lib.projector.constants import E_PARAMETER, ERROR_STRING, S_OFF, S_STANDBY, S_ON, \
-    PJLINK_POWR_STATUS, S_CONNECTED
+from openlp.core.lib.projector.pjlink import PJLink
+from openlp.core.lib.projector.constants import E_PARAMETER, ERROR_STRING, S_ON, S_CONNECTED
 
 from tests.resources.projector.data import TEST_PIN, TEST_SALT, TEST_CONNECT_AUTHENTICATE, TEST_HASH
 
 pjlink_test = PJLink(name='test', ip='127.0.0.1', pin=TEST_PIN, no_poll=True)
 
 
-class TestPJLink(TestCase):
+class TestPJLinkBase(TestCase):
     """
     Tests for the PJLink module
     """
@@ -42,7 +41,10 @@
     @patch.object(pjlink_test, 'send_command')
     @patch.object(pjlink_test, 'waitForReadyRead')
     @patch('openlp.core.common.qmd5_hash')
-    def test_authenticated_connection_call(self, mock_qmd5_hash, mock_waitForReadyRead, mock_send_command,
+    def test_authenticated_connection_call(self,
+                                           mock_qmd5_hash,
+                                           mock_waitForReadyRead,
+                                           mock_send_command,
                                            mock_readyRead):
         """
         Ticket 92187: Fix for projector connect with PJLink authentication exception.
@@ -59,140 +61,6 @@
         self.assertTrue(mock_qmd5_hash.called_with(TEST_PIN,
                                                    "Connection request should have been called with TEST_PIN"))
 
-    def test_projector_process_rfil_save(self):
-        """
-        Test saving filter type
-        """
-        # GIVEN: Test object
-        pjlink = pjlink_test
-        pjlink.model_filter = None
-        filter_model = 'Filter Type Test'
-
-        # WHEN: Filter model is received
-        pjlink.process_rfil(data=filter_model)
-
-        # THEN: Filter model number should be saved
-        self.assertEqual(pjlink.model_filter, filter_model, 'Filter type should have been saved')
-
-    def test_projector_process_rfil_nosave(self):
-        """
-        Test saving filter type previously saved
-        """
-        # GIVEN: Test object
-        pjlink = pjlink_test
-        pjlink.model_filter = 'Old filter type'
-        filter_model = 'Filter Type Test'
-
-        # WHEN: Filter model is received
-        pjlink.process_rfil(data=filter_model)
-
-        # THEN: Filter model number should be saved
-        self.assertNotEquals(pjlink.model_filter, filter_model, 'Filter type should NOT have been saved')
-
-    def test_projector_process_rlmp_save(self):
-        """
-        Test saving lamp type
-        """
-        # GIVEN: Test object
-        pjlink = pjlink_test
-        pjlink.model_lamp = None
-        lamp_model = 'Lamp Type Test'
-
-        # WHEN: Filter model is received
-        pjlink.process_rlmp(data=lamp_model)
-
-        # THEN: Filter model number should be saved
-        self.assertEqual(pjlink.model_lamp, lamp_model, 'Lamp type should have been saved')
-
-    def test_projector_process_rlmp_nosave(self):
-        """
-        Test saving lamp type previously saved
-        """
-        # GIVEN: Test object
-        pjlink = pjlink_test
-        pjlink.model_lamp = 'Old lamp type'
-        lamp_model = 'Filter Type Test'
-
-        # WHEN: Filter model is received
-        pjlink.process_rlmp(data=lamp_model)
-
-        # THEN: Filter model number should be saved
-        self.assertNotEquals(pjlink.model_lamp, lamp_model, 'Lamp type should NOT have been saved')
-
-    def test_projector_process_snum_set(self):
-        """
-        Test saving serial number from projector
-        """
-        # GIVEN: Test object
-        pjlink = pjlink_test
-        pjlink.serial_no = None
-        test_number = 'Test Serial Number'
-
-        # WHEN: No serial number is set and we receive serial number command
-        pjlink.process_snum(data=test_number)
-
-        # THEN: Serial number should be set
-        self.assertEqual(pjlink.serial_no, test_number,
-                         'Projector serial number should have been set')
-
-    def test_projector_process_snum_different(self):
-        """
-        Test projector serial number different than saved serial number
-        """
-        # GIVEN: Test object
-        pjlink = pjlink_test
-        pjlink.serial_no = 'Previous serial number'
-        test_number = 'Test Serial Number'
-
-        # WHEN: No serial number is set and we receive serial number command
-        pjlink.process_snum(data=test_number)
-
-        # THEN: Serial number should be set
-        self.assertNotEquals(pjlink.serial_no, test_number,
-                             'Projector serial number should NOT have been set')
-
-    def test_projector_clss_one(self):
-        """
-        Test class 1 sent from projector
-        """
-        # GIVEN: Test object
-        pjlink = pjlink_test
-
-        # WHEN: Process class response
-        pjlink.process_clss('1')
-
-        # THEN: Projector class should be set to 1
-        self.assertEqual(pjlink.pjlink_class, '1',
-                         'Projector should have returned class=1')
-
-    def test_projector_clss_two(self):
-        """
-        Test class 2 sent from projector
-        """
-        # GIVEN: Test object
-        pjlink = pjlink_test
-
-        # WHEN: Process class response
-        pjlink.process_clss('2')
-
-        # THEN: Projector class should be set to 1
-        self.assertEqual(pjlink.pjlink_class, '2',
-                         'Projector should have returned class=2')
-
-    def test_bug_1550891_non_standard_class_reply(self):
-        """
-        Bugfix 1550891: CLSS request returns non-standard reply
-        """
-        # GIVEN: Test object
-        pjlink = pjlink_test
-
-        # WHEN: Process non-standard reply
-        pjlink.process_clss('Class 1')
-
-        # THEN: Projector class should be set with proper value
-        self.assertEqual(pjlink.pjlink_class, '1',
-                         'Non-standard class reply should have set class=1')
-
     @patch.object(pjlink_test, 'change_status')
     def test_status_change(self, mock_change_status):
         """
@@ -210,242 +78,13 @@
                                        'change_status should have been called with "{}"'.format(
                                            ERROR_STRING[E_PARAMETER]))
 
-    @patch.object(pjlink_test, 'process_inpt')
-    def test_projector_return_ok(self, mock_process_inpt):
-        """
-        Test projector calls process_inpt command when process_command is called with INPT option
-        """
-        # GIVEN: Test object
-        pjlink = pjlink_test
-
-        # WHEN: process_command is called with INST command and 31 input:
-        pjlink.process_command('INPT', '31')
-
-        # THEN: process_inpt method should have been called with 31
-        mock_process_inpt.called_with('31',
-                                      "process_inpt should have been called with 31")
-
-    @patch.object(pjlink_test, 'projectorReceivedData')
-    def test_projector_process_lamp(self, mock_projectorReceivedData):
-        """
-        Test status lamp on/off and hours
-        """
-        # GIVEN: Test object
-        pjlink = pjlink_test
-
-        # WHEN: Call process_command with lamp data
-        pjlink.process_command('LAMP', '22222 1')
-
-        # THEN: Lamp should have been set with status=ON and hours=22222
-        self.assertEqual(pjlink.lamp[0]['On'], True,
-                         'Lamp power status should have been set to TRUE')
-        self.assertEqual(pjlink.lamp[0]['Hours'], 22222,
-                         'Lamp hours should have been set to 22222')
-
-    @patch.object(pjlink_test, 'projectorReceivedData')
-    def test_projector_process_multiple_lamp(self, mock_projectorReceivedData):
-        """
-        Test status multiple lamp on/off and hours
-        """
-        # GIVEN: Test object
-        pjlink = pjlink_test
-
-        # WHEN: Call process_command with lamp data
-        pjlink.process_command('LAMP', '11111 1 22222 0 33333 1')
-
-        # THEN: Lamp should have been set with proper lamp status
-        self.assertEqual(len(pjlink.lamp), 3,
-                         'Projector should have 3 lamps specified')
-        self.assertEqual(pjlink.lamp[0]['On'], True,
-                         'Lamp 1 power status should have been set to TRUE')
-        self.assertEqual(pjlink.lamp[0]['Hours'], 11111,
-                         'Lamp 1 hours should have been set to 11111')
-        self.assertEqual(pjlink.lamp[1]['On'], False,
-                         'Lamp 2 power status should have been set to FALSE')
-        self.assertEqual(pjlink.lamp[1]['Hours'], 22222,
-                         'Lamp 2 hours should have been set to 22222')
-        self.assertEqual(pjlink.lamp[2]['On'], True,
-                         'Lamp 3 power status should have been set to TRUE')
-        self.assertEqual(pjlink.lamp[2]['Hours'], 33333,
-                         'Lamp 3 hours should have been set to 33333')
-
-    @patch.object(pjlink_test, 'projectorReceivedData')
-    @patch.object(pjlink_test, 'projectorUpdateIcons')
-    @patch.object(pjlink_test, 'send_command')
-    @patch.object(pjlink_test, 'change_status')
-    def test_projector_process_power_on(self, mock_change_status,
-                                        mock_send_command,
-                                        mock_UpdateIcons,
-                                        mock_ReceivedData):
-        """
-        Test status power to ON
-        """
-        # GIVEN: Test object and preset
-        pjlink = pjlink_test
-        pjlink.power = S_STANDBY
-
-        # WHEN: Call process_command with turn power on command
-        pjlink.process_command('POWR', PJLINK_POWR_STATUS[S_ON])
-
-        # THEN: Power should be set to ON
-        self.assertEqual(pjlink.power, S_ON, 'Power should have been set to ON')
-        mock_send_command.assert_called_once_with('INST')
-        self.assertEqual(mock_UpdateIcons.emit.called, True, 'projectorUpdateIcons should have been called')
-
-    @patch.object(pjlink_test, 'projectorReceivedData')
-    @patch.object(pjlink_test, 'projectorUpdateIcons')
-    @patch.object(pjlink_test, 'send_command')
-    @patch.object(pjlink_test, 'change_status')
-    def test_projector_process_power_off(self, mock_change_status,
-                                         mock_send_command,
-                                         mock_UpdateIcons,
-                                         mock_ReceivedData):
-        """
-        Test status power to STANDBY
-        """
-        # GIVEN: Test object and preset
-        pjlink = pjlink_test
-        pjlink.power = S_ON
-
-        # WHEN: Call process_command with turn power on command
-        pjlink.process_command('POWR', PJLINK_POWR_STATUS[S_STANDBY])
-
-        # THEN: Power should be set to STANDBY
-        self.assertEqual(pjlink.power, S_STANDBY, 'Power should have been set to STANDBY')
-        self.assertEqual(mock_send_command.called, False, 'send_command should not have been called')
-        self.assertEqual(mock_UpdateIcons.emit.called, True, 'projectorUpdateIcons should have been called')
-
-    @patch.object(pjlink_test, 'projectorUpdateIcons')
-    def test_projector_process_avmt_closed_unmuted(self, mock_projectorReceivedData):
-        """
-        Test avmt status shutter closed and audio muted
-        """
-        # GIVEN: Test object
-        pjlink = pjlink_test
-        pjlink.shutter = False
-        pjlink.mute = True
-
-        # WHEN: Called with setting shutter closed and mute off
-        pjlink.process_avmt('11')
-
-        # THEN: Shutter should be True and mute should be False
-        self.assertTrue(pjlink.shutter, 'Shutter should have been set to closed')
-        self.assertFalse(pjlink.mute, 'Audio should be off')
-
-    @patch.object(pjlink_test, 'projectorUpdateIcons')
-    def test_projector_process_avmt_open_muted(self, mock_projectorReceivedData):
-        """
-        Test avmt status shutter open and mute on
-        """
-        # GIVEN: Test object
-        pjlink = pjlink_test
-        pjlink.shutter = True
-        pjlink.mute = False
-
-        # WHEN: Called with setting shutter closed and mute on
-        pjlink.process_avmt('21')
-
-        # THEN: Shutter should be closed and mute should be True
-        self.assertFalse(pjlink.shutter, 'Shutter should have been set to closed')
-        self.assertTrue(pjlink.mute, 'Audio should be off')
-
-    @patch.object(pjlink_test, 'projectorUpdateIcons')
-    def test_projector_process_avmt_open_unmuted(self, mock_projectorReceivedData):
-        """
-        Test avmt status shutter open and mute off off
-        """
-        # GIVEN: Test object
-        pjlink = pjlink_test
-        pjlink.shutter = True
-        pjlink.mute = True
-
-        # WHEN: Called with setting shutter to closed and mute on
-        pjlink.process_avmt('30')
-
-        # THEN: Shutter should be closed and mute should be True
-        self.assertFalse(pjlink.shutter, 'Shutter should have been set to open')
-        self.assertFalse(pjlink.mute, 'Audio should be on')
-
-    @patch.object(pjlink_test, 'projectorUpdateIcons')
-    def test_projector_process_avmt_closed_muted(self, mock_projectorReceivedData):
-        """
-        Test avmt status shutter closed and mute off
-        """
-        # GIVEN: Test object
-        pjlink = pjlink_test
-        pjlink.shutter = False
-        pjlink.mute = False
-
-        # WHEN: Called with setting shutter to closed and mute on
-        pjlink.process_avmt('31')
-
-        # THEN: Shutter should be closed and mute should be True
-        self.assertTrue(pjlink.shutter, 'Shutter should have been set to closed')
-        self.assertTrue(pjlink.mute, 'Audio should be on')
-
-    def test_projector_process_input(self):
-        """
-        Test input source status shows current input
-        """
-        # GIVEN: Test object
-        pjlink = pjlink_test
-        pjlink.source = '0'
-
-        # WHEN: Called with input source
-        pjlink.process_inpt('1')
-
-        # THEN: Input selected should reflect current input
-        self.assertEqual(pjlink.source, '1', 'Input source should be set to "1"')
-
-    def test_projector_reset_information(self):
-        """
-        Test reset_information() resets all information and stops timers
-        """
-        # GIVEN: Test object and test data
-        pjlink = pjlink_test
-        pjlink.power = S_ON
-        pjlink.pjlink_name = 'OPENLPTEST'
-        pjlink.manufacturer = 'PJLINK'
-        pjlink.model = '1'
-        pjlink.shutter = True
-        pjlink.mute = True
-        pjlink.lamp = True
-        pjlink.fan = True
-        pjlink.source_available = True
-        pjlink.other_info = 'ANOTHER TEST'
-        pjlink.send_queue = True
-        pjlink.send_busy = True
-        pjlink.timer = MagicMock()
-        pjlink.socket_timer = MagicMock()
-
-        # WHEN: reset_information() is called
-        with patch.object(pjlink.timer, 'stop') as mock_timer:
-            with patch.object(pjlink.socket_timer, 'stop') as mock_socket_timer:
-                pjlink.reset_information()
-
-        # THEN: All information should be reset and timers stopped
-        self.assertEqual(pjlink.power, S_OFF, 'Projector power should be OFF')
-        self.assertIsNone(pjlink.pjlink_name, 'Projector pjlink_name should be None')
-        self.assertIsNone(pjlink.manufacturer, 'Projector manufacturer should be None')
-        self.assertIsNone(pjlink.model, 'Projector model should be None')
-        self.assertIsNone(pjlink.shutter, 'Projector shutter should be None')
-        self.assertIsNone(pjlink.mute, 'Projector shuttter should be None')
-        self.assertIsNone(pjlink.lamp, 'Projector lamp should be None')
-        self.assertIsNone(pjlink.fan, 'Projector fan should be None')
-        self.assertIsNone(pjlink.source_available, 'Projector source_available should be None')
-        self.assertIsNone(pjlink.source, 'Projector source should be None')
-        self.assertIsNone(pjlink.other_info, 'Projector other_info should be None')
-        self.assertEqual(pjlink.send_queue, [], 'Projector send_queue should be an empty list')
-        self.assertFalse(pjlink.send_busy, 'Projector send_busy should be False')
-        self.assertTrue(mock_timer.called, 'Projector timer.stop()  should have been called')
-        self.assertTrue(mock_socket_timer.called, 'Projector socket_timer.stop() should have been called')
-
     @patch.object(pjlink_test, 'send_command')
     @patch.object(pjlink_test, 'waitForReadyRead')
     @patch.object(pjlink_test, 'projectorAuthentication')
     @patch.object(pjlink_test, 'timer')
     @patch.object(pjlink_test, 'socket_timer')
-    def test_bug_1593882_no_pin_authenticated_connection(self, mock_socket_timer,
+    def test_bug_1593882_no_pin_authenticated_connection(self,
+                                                         mock_socket_timer,
                                                          mock_timer,
                                                          mock_authentication,
                                                          mock_ready_read,
@@ -469,7 +108,8 @@
     @patch.object(pjlink_test, '_send_command')
     @patch.object(pjlink_test, 'timer')
     @patch.object(pjlink_test, 'socket_timer')
-    def test_bug_1593883_pjlink_authentication(self, mock_socket_timer,
+    def test_bug_1593883_pjlink_authentication(self,
+                                               mock_socket_timer,
                                                mock_timer,
                                                mock_send_command,
                                                mock_state,
@@ -491,7 +131,7 @@
                          "call(data='{hash}%1CLSS ?\\r')".format(hash=TEST_HASH))
 
     @patch.object(pjlink_test, 'disconnect_from_host')
-    def socket_abort_test(self, mock_disconnect):
+    def test_socket_abort(self, mock_disconnect):
         """
         Test PJLink.socket_abort calls disconnect_from_host
         """
@@ -504,7 +144,7 @@
         # THEN: disconnect_from_host should be called
         self.assertTrue(mock_disconnect.called, 'Should have called disconnect_from_host')
 
-    def poll_loop_not_connected_test(self):
+    def test_poll_loop_not_connected(self):
         """
         Test PJLink.poll_loop not connected return
         """
@@ -522,7 +162,7 @@
         self.assertFalse(pjlink.timer.called, 'Should have returned without calling any other method')
 
     @patch.object(pjlink_test, 'send_command')
-    def poll_loop_start_test(self, mock_send_command):
+    def test_poll_loop_start(self, mock_send_command):
         """
         Test PJLink.poll_loop makes correct calls
         """

=== added file 'tests/functional/openlp_core_lib/test_projector_pjlink_commands.py'
--- tests/functional/openlp_core_lib/test_projector_pjlink_commands.py	1970-01-01 00:00:00 +0000
+++ tests/functional/openlp_core_lib/test_projector_pjlink_commands.py	2017-08-06 07:33:29 +0000
@@ -0,0 +1,468 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2015 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+Package to test the openlp.core.lib.projector.pjlink commands package.
+"""
+from unittest import TestCase
+from unittest.mock import patch, MagicMock
+
+from openlp.core.lib.projector.pjlink import PJLink
+from openlp.core.lib.projector.constants import PJLINK_ERST_STATUS, PJLINK_POWR_STATUS, \
+    S_OFF, S_STANDBY, S_ON
+
+from tests.resources.projector.data import TEST_PIN
+
+pjlink_test = PJLink(name='test', ip='127.0.0.1', pin=TEST_PIN, no_poll=True)
+
+# ERST status codes
+ERST_OK, ERST_WARN, ERST_ERR = '0', '1', '2'
+
+
+class TestPJLinkCommands(TestCase):
+    """
+    Tests for the PJLink module
+    """
+    def test_projector_reset_information(self):
+        """
+        Test reset_information() resets all information and stops timers
+        """
+        # GIVEN: Test object and test data
+        pjlink = pjlink_test
+        pjlink.power = S_ON
+        pjlink.pjlink_name = 'OPENLPTEST'
+        pjlink.manufacturer = 'PJLINK'
+        pjlink.model = '1'
+        pjlink.shutter = True
+        pjlink.mute = True
+        pjlink.lamp = True
+        pjlink.fan = True
+        pjlink.source_available = True
+        pjlink.other_info = 'ANOTHER TEST'
+        pjlink.send_queue = True
+        pjlink.send_busy = True
+        pjlink.timer = MagicMock()
+        pjlink.socket_timer = MagicMock()
+
+        # WHEN: reset_information() is called
+        with patch.object(pjlink.timer, 'stop') as mock_timer:
+            with patch.object(pjlink.socket_timer, 'stop') as mock_socket_timer:
+                pjlink.reset_information()
+
+        # THEN: All information should be reset and timers stopped
+        self.assertEqual(pjlink.power, S_OFF, 'Projector power should be OFF')
+        self.assertIsNone(pjlink.pjlink_name, 'Projector pjlink_name should be None')
+        self.assertIsNone(pjlink.manufacturer, 'Projector manufacturer should be None')
+        self.assertIsNone(pjlink.model, 'Projector model should be None')
+        self.assertIsNone(pjlink.shutter, 'Projector shutter should be None')
+        self.assertIsNone(pjlink.mute, 'Projector shuttter should be None')
+        self.assertIsNone(pjlink.lamp, 'Projector lamp should be None')
+        self.assertIsNone(pjlink.fan, 'Projector fan should be None')
+        self.assertIsNone(pjlink.source_available, 'Projector source_available should be None')
+        self.assertIsNone(pjlink.source, 'Projector source should be None')
+        self.assertIsNone(pjlink.other_info, 'Projector other_info should be None')
+        self.assertEqual(pjlink.send_queue, [], 'Projector send_queue should be an empty list')
+        self.assertFalse(pjlink.send_busy, 'Projector send_busy should be False')
+        self.assertTrue(mock_timer.called, 'Projector timer.stop()  should have been called')
+        self.assertTrue(mock_socket_timer.called, 'Projector socket_timer.stop() should have been called')
+
+    @patch.object(pjlink_test, 'projectorUpdateIcons')
+    def test_projector_process_avmt_closed_muted(self, mock_projectorReceivedData):
+        """
+        Test avmt status shutter closed and mute off
+        """
+        # GIVEN: Test object
+        pjlink = pjlink_test
+        pjlink.shutter = False
+        pjlink.mute = False
+
+        # WHEN: Called with setting shutter to closed and mute on
+        pjlink.process_avmt('31')
+
+        # THEN: Shutter should be closed and mute should be True
+        self.assertTrue(pjlink.shutter, 'Shutter should have been set to closed')
+        self.assertTrue(pjlink.mute, 'Audio should be on')
+
+    @patch.object(pjlink_test, 'projectorUpdateIcons')
+    def test_projector_process_avmt_closed_unmuted(self, mock_projectorReceivedData):
+        """
+        Test avmt status shutter closed and audio muted
+        """
+        # GIVEN: Test object
+        pjlink = pjlink_test
+        pjlink.shutter = False
+        pjlink.mute = True
+
+        # WHEN: Called with setting shutter closed and mute off
+        pjlink.process_avmt('11')
+
+        # THEN: Shutter should be True and mute should be False
+        self.assertTrue(pjlink.shutter, 'Shutter should have been set to closed')
+        self.assertFalse(pjlink.mute, 'Audio should be off')
+
+    @patch.object(pjlink_test, 'projectorUpdateIcons')
+    def test_projector_process_avmt_open_muted(self, mock_projectorReceivedData):
+        """
+        Test avmt status shutter open and mute on
+        """
+        # GIVEN: Test object
+        pjlink = pjlink_test
+        pjlink.shutter = True
+        pjlink.mute = False
+
+        # WHEN: Called with setting shutter closed and mute on
+        pjlink.process_avmt('21')
+
+        # THEN: Shutter should be closed and mute should be True
+        self.assertFalse(pjlink.shutter, 'Shutter should have been set to closed')
+        self.assertTrue(pjlink.mute, 'Audio should be off')
+
+    @patch.object(pjlink_test, 'projectorUpdateIcons')
+    def test_projector_process_avmt_open_unmuted(self, mock_projectorReceivedData):
+        """
+        Test avmt status shutter open and mute off off
+        """
+        # GIVEN: Test object
+        pjlink = pjlink_test
+        pjlink.shutter = True
+        pjlink.mute = True
+
+        # WHEN: Called with setting shutter to closed and mute on
+        pjlink.process_avmt('30')
+
+        # THEN: Shutter should be closed and mute should be True
+        self.assertFalse(pjlink.shutter, 'Shutter should have been set to open')
+        self.assertFalse(pjlink.mute, 'Audio should be on')
+
+    def test_projector_process_clss_one(self):
+        """
+        Test class 1 sent from projector
+        """
+        # GIVEN: Test object
+        pjlink = pjlink_test
+
+        # WHEN: Process class response
+        pjlink.process_clss('1')
+
+        # THEN: Projector class should be set to 1
+        self.assertEqual(pjlink.pjlink_class, '1',
+                         'Projector should have returned class=1')
+
+    def test_projector_process_clss_two(self):
+        """
+        Test class 2 sent from projector
+        """
+        # GIVEN: Test object
+        pjlink = pjlink_test
+
+        # WHEN: Process class response
+        pjlink.process_clss('2')
+
+        # THEN: Projector class should be set to 1
+        self.assertEqual(pjlink.pjlink_class, '2',
+                         'Projector should have returned class=2')
+
+    def test_projector_process_clss_nonstandard_reply(self):
+        """
+        Bugfix 1550891: CLSS request returns non-standard reply
+        """
+        # GIVEN: Test object
+        pjlink = pjlink_test
+
+        # WHEN: Process non-standard reply
+        pjlink.process_clss('Class 1')
+
+        # THEN: Projector class should be set with proper value
+        self.assertEqual(pjlink.pjlink_class, '1',
+                         'Non-standard class reply should have set class=1')
+
+    def test_projector_process_erst_all_ok(self):
+        """
+        Test test_projector_process_erst_all_ok
+        """
+        # GIVEN: Test object
+        pjlink = pjlink_test
+        chk_test = ERST_OK
+
+        # WHEN: process_erst with no errors
+        pjlink.process_erst('{fan}{lamp}{temp}{cover}{filter}{other}'.format(fan=chk_test,
+                                                                             lamp=chk_test,
+                                                                             temp=chk_test,
+                                                                             cover=chk_test,
+                                                                             filter=chk_test,
+                                                                             other=chk_test))
+
+        # PJLink instance errors should be None
+        self.assertIsNone(pjlink.projector_errors, 'projector_errors should have been set to None')
+
+    def test_projector_process_erst_all_warn(self):
+        """
+        Test test_projector_process_erst_all_warn
+        """
+        # GIVEN: Test object
+        pjlink = pjlink_test
+        chk_test = ERST_WARN
+        chk_code = PJLINK_ERST_STATUS[chk_test]
+        chk_value = {'Fan': chk_code,
+                     'Lamp': chk_code,
+                     'Temperature': chk_code,
+                     'Cover': chk_code,
+                     'Filter': chk_code,
+                     'Other': chk_code
+                     }
+
+        # WHEN: process_erst with status set to WARN
+        pjlink.process_erst('{fan}{lamp}{temp}{cover}{filter}{other}'.format(fan=chk_test,
+                                                                             lamp=chk_test,
+                                                                             temp=chk_test,
+                                                                             cover=chk_test,
+                                                                             filter=chk_test,
+                                                                             other=chk_test))
+
+        # PJLink instance errors should match chk_value
+        self.assertEqual(pjlink.projector_errors, chk_value,
+                         'projector_errors should have been set to all {err}'.format(err=chk_code))
+
+    def test_projector_process_erst_all_error(self):
+        """
+        Test test_projector_process_erst_all_error
+        """
+        # GIVEN: Test object
+        pjlink = pjlink_test
+        chk_test = ERST_ERR
+        chk_code = PJLINK_ERST_STATUS[chk_test]
+        chk_value = {'Fan': chk_code,
+                     'Lamp': chk_code,
+                     'Temperature': chk_code,
+                     'Cover': chk_code,
+                     'Filter': chk_code,
+                     'Other': chk_code
+                     }
+
+        # WHEN: process_erst with status set to ERROR
+        pjlink.process_erst('{fan}{lamp}{temp}{cover}{filter}{other}'.format(fan=chk_test,
+                                                                             lamp=chk_test,
+                                                                             temp=chk_test,
+                                                                             cover=chk_test,
+                                                                             filter=chk_test,
+                                                                             other=chk_test))
+
+        # PJLink instance errors should be set to chk_value
+        self.assertEqual(pjlink.projector_errors, chk_value,
+                         'projector_errors should have been set to all {err}'.format(err=chk_code))
+
+    def test_projector_process_inpt(self):
+        """
+        Test input source status shows current input
+        """
+        # GIVEN: Test object
+        pjlink = pjlink_test
+        pjlink.source = '0'
+
+        # WHEN: Called with input source
+        pjlink.process_inpt('1')
+
+        # THEN: Input selected should reflect current input
+        self.assertEqual(pjlink.source, '1', 'Input source should be set to "1"')
+
+    @patch.object(pjlink_test, 'projectorReceivedData')
+    def test_projector_process_lamp_single(self, mock_projectorReceivedData):
+        """
+        Test status lamp on/off and hours
+        """
+        # GIVEN: Test object
+        pjlink = pjlink_test
+
+        # WHEN: Call process_command with lamp data
+        pjlink.process_command('LAMP', '22222 1')
+
+        # THEN: Lamp should have been set with status=ON and hours=22222
+        self.assertEqual(pjlink.lamp[0]['On'], True,
+                         'Lamp power status should have been set to TRUE')
+        self.assertEqual(pjlink.lamp[0]['Hours'], 22222,
+                         'Lamp hours should have been set to 22222')
+
+    @patch.object(pjlink_test, 'projectorReceivedData')
+    def test_projector_process_lamp_multiple(self, mock_projectorReceivedData):
+        """
+        Test status multiple lamp on/off and hours
+        """
+        # GIVEN: Test object
+        pjlink = pjlink_test
+
+        # WHEN: Call process_command with lamp data
+        pjlink.process_command('LAMP', '11111 1 22222 0 33333 1')
+
+        # THEN: Lamp should have been set with proper lamp status
+        self.assertEqual(len(pjlink.lamp), 3,
+                         'Projector should have 3 lamps specified')
+        self.assertEqual(pjlink.lamp[0]['On'], True,
+                         'Lamp 1 power status should have been set to TRUE')
+        self.assertEqual(pjlink.lamp[0]['Hours'], 11111,
+                         'Lamp 1 hours should have been set to 11111')
+        self.assertEqual(pjlink.lamp[1]['On'], False,
+                         'Lamp 2 power status should have been set to FALSE')
+        self.assertEqual(pjlink.lamp[1]['Hours'], 22222,
+                         'Lamp 2 hours should have been set to 22222')
+        self.assertEqual(pjlink.lamp[2]['On'], True,
+                         'Lamp 3 power status should have been set to TRUE')
+        self.assertEqual(pjlink.lamp[2]['Hours'], 33333,
+                         'Lamp 3 hours should have been set to 33333')
+
+    @patch.object(pjlink_test, 'projectorReceivedData')
+    @patch.object(pjlink_test, 'projectorUpdateIcons')
+    @patch.object(pjlink_test, 'send_command')
+    @patch.object(pjlink_test, 'change_status')
+    def test_projector_process_powr_on(self,
+                                       mock_change_status,
+                                       mock_send_command,
+                                       mock_UpdateIcons,
+                                       mock_ReceivedData):
+        """
+        Test status power to ON
+        """
+        # GIVEN: Test object and preset
+        pjlink = pjlink_test
+        pjlink.power = S_STANDBY
+
+        # WHEN: Call process_command with turn power on command
+        pjlink.process_command('POWR', PJLINK_POWR_STATUS[S_ON])
+
+        # THEN: Power should be set to ON
+        self.assertEqual(pjlink.power, S_ON, 'Power should have been set to ON')
+        mock_send_command.assert_called_once_with('INST')
+        self.assertEqual(mock_UpdateIcons.emit.called, True, 'projectorUpdateIcons should have been called')
+
+    @patch.object(pjlink_test, 'projectorReceivedData')
+    @patch.object(pjlink_test, 'projectorUpdateIcons')
+    @patch.object(pjlink_test, 'send_command')
+    @patch.object(pjlink_test, 'change_status')
+    def test_projector_process_powr_off(self,
+                                        mock_change_status,
+                                        mock_send_command,
+                                        mock_UpdateIcons,
+                                        mock_ReceivedData):
+        """
+        Test status power to STANDBY
+        """
+        # GIVEN: Test object and preset
+        pjlink = pjlink_test
+        pjlink.power = S_ON
+
+        # WHEN: Call process_command with turn power on command
+        pjlink.process_command('POWR', PJLINK_POWR_STATUS[S_STANDBY])
+
+        # THEN: Power should be set to STANDBY
+        self.assertEqual(pjlink.power, S_STANDBY, 'Power should have been set to STANDBY')
+        self.assertEqual(mock_send_command.called, False, 'send_command should not have been called')
+        self.assertEqual(mock_UpdateIcons.emit.called, True, 'projectorUpdateIcons should have been called')
+
+    def test_projector_process_rfil_save(self):
+        """
+        Test saving filter type
+        """
+        # GIVEN: Test object
+        pjlink = pjlink_test
+        pjlink.model_filter = None
+        filter_model = 'Filter Type Test'
+
+        # WHEN: Filter model is received
+        pjlink.process_rfil(data=filter_model)
+
+        # THEN: Filter model number should be saved
+        self.assertEqual(pjlink.model_filter, filter_model, 'Filter type should have been saved')
+
+    def test_projector_process_rfil_nosave(self):
+        """
+        Test saving filter type previously saved
+        """
+        # GIVEN: Test object
+        pjlink = pjlink_test
+        pjlink.model_filter = 'Old filter type'
+        filter_model = 'Filter Type Test'
+
+        # WHEN: Filter model is received
+        pjlink.process_rfil(data=filter_model)
+
+        # THEN: Filter model number should be saved
+        self.assertNotEquals(pjlink.model_filter, filter_model, 'Filter type should NOT have been saved')
+
+    def test_projector_process_rlmp_save(self):
+        """
+        Test saving lamp type
+        """
+        # GIVEN: Test object
+        pjlink = pjlink_test
+        pjlink.model_lamp = None
+        lamp_model = 'Lamp Type Test'
+
+        # WHEN: Filter model is received
+        pjlink.process_rlmp(data=lamp_model)
+
+        # THEN: Filter model number should be saved
+        self.assertEqual(pjlink.model_lamp, lamp_model, 'Lamp type should have been saved')
+
+    def test_projector_process_rlmp_nosave(self):
+        """
+        Test saving lamp type previously saved
+        """
+        # GIVEN: Test object
+        pjlink = pjlink_test
+        pjlink.model_lamp = 'Old lamp type'
+        lamp_model = 'Filter Type Test'
+
+        # WHEN: Filter model is received
+        pjlink.process_rlmp(data=lamp_model)
+
+        # THEN: Filter model number should be saved
+        self.assertNotEquals(pjlink.model_lamp, lamp_model, 'Lamp type should NOT have been saved')
+
+    def test_projector_process_snum_set(self):
+        """
+        Test saving serial number from projector
+        """
+        # GIVEN: Test object
+        pjlink = pjlink_test
+        pjlink.serial_no = None
+        test_number = 'Test Serial Number'
+
+        # WHEN: No serial number is set and we receive serial number command
+        pjlink.process_snum(data=test_number)
+
+        # THEN: Serial number should be set
+        self.assertEqual(pjlink.serial_no, test_number,
+                         'Projector serial number should have been set')
+
+    def test_projector_process_snum_different(self):
+        """
+        Test projector serial number different than saved serial number
+        """
+        # GIVEN: Test object
+        pjlink = pjlink_test
+        pjlink.serial_no = 'Previous serial number'
+        test_number = 'Test Serial Number'
+
+        # WHEN: No serial number is set and we receive serial number command
+        pjlink.process_snum(data=test_number)
+
+        # THEN: Serial number should be set
+        self.assertNotEquals(pjlink.serial_no, test_number,
+                             'Projector serial number should NOT have been set')


References