diff --git a/openlp/core/lib/projector/db.py b/openlp/core/lib/projector/db.py index 89b807f21..a0c7c009e 100644 --- a/openlp/core/lib/projector/db.py +++ b/openlp/core/lib/projector/db.py @@ -303,7 +303,7 @@ class ProjectorDB(Manager): :param ip: Host IP/Name :returns: Projector() instance """ - log.debug('get_projector_by_ip(ip="%s")' % ip) + log.debug('get_projector_by_ip(ip="{ip}")'.format(ip=ip)) projector = self.get_object_filtered(Projector, Projector.ip == ip) if projector is None: # Not found diff --git a/openlp/core/lib/projector/pjlink1.py b/openlp/core/lib/projector/pjlink1.py index eb2753cbf..d71ce7c99 100644 --- a/openlp/core/lib/projector/pjlink1.py +++ b/openlp/core/lib/projector/pjlink1.py @@ -44,6 +44,8 @@ log.debug('pjlink1 loaded') __all__ = ['PJLink'] +import re + from codecs import decode from PyQt5 import QtCore, QtNetwork @@ -53,9 +55,12 @@ from openlp.core.lib.projector.constants import CONNECTION_ERRORS, CR, ERROR_MSG E_AUTHENTICATION, E_CONNECTION_REFUSED, E_GENERAL, E_INVALID_DATA, E_NETWORK, E_NOT_CONNECTED, \ E_PARAMETER, E_PROJECTOR, E_SOCKET_TIMEOUT, E_UNAVAILABLE, E_UNDEFINED, PJLINK_ERRORS, \ PJLINK_ERST_STATUS, PJLINK_MAX_PACKET, PJLINK_PORT, PJLINK_POWR_STATUS, PJLINK_VALID_CMD, \ - PJLINK_DEFAULT_CODES, STATUS_STRING, S_CONNECTED, S_CONNECTING, S_NETWORK_RECEIVED, S_NETWORK_SENDING, \ + STATUS_STRING, S_CONNECTED, S_CONNECTING, S_NETWORK_RECEIVED, S_NETWORK_SENDING, \ S_NOT_CONNECTED, S_OFF, S_OK, S_ON, S_STATUS +# Possible future imports +# from openlp.core.lib.projector.constants import PJLINK_DEFAULT_CODES + # Shortcuts SocketError = QtNetwork.QAbstractSocket.SocketError SocketSTate = QtNetwork.QAbstractSocket.SocketState @@ -113,8 +118,13 @@ class PJLink(QtNetwork.QTcpSocket): 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') @@ -158,7 +168,9 @@ class PJLink(QtNetwork.QTcpSocket): 'LAMP': self.process_lamp, 'NAME': self.process_name, 'PJLINK': self.check_login, - 'POWR': self.process_powr + 'POWR': self.process_powr, + 'SNUM': self.process_snum, + 'SVER': self.process_sver } def reset_information(self): @@ -166,12 +178,16 @@ class PJLink(QtNetwork.QTcpSocket): 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 @@ -188,7 +204,6 @@ class PJLink(QtNetwork.QTcpSocket): if hasattr(self, 'socket_timer'): log.debug('({ip}): Calling socket_timer.stop()'.format(ip=self.ip)) self.socket_timer.stop() - self.send_queue = [] self.send_busy = False def thread_started(self): @@ -249,7 +264,10 @@ class PJLink(QtNetwork.QTcpSocket): # Restart timer self.timer.start() # These commands may change during connetion - for command in ['POWR', 'ERST', 'LAMP', 'AVMT', 'INPT']: + check_list = ['POWR', 'ERST', 'LAMP', 'AVMT', 'INPT'] + if self.pjlink_class == '2': + check_list.extend(['FILT', 'FREZ']) + for command in check_list: self.send_command(command, queue=True) # The following commands do not change, so only check them once if self.power == S_ON and self.source_available is None: @@ -262,6 +280,38 @@ class PJLink(QtNetwork.QTcpSocket): self.send_command('INF2', queue=True) if self.pjlink_name is None: self.send_command('NAME', queue=True) + if self.pjlink_class == '2': + # Class 2 specific checks + if self.serial_no is None: + self.send_command('SNUM', queue=True) + if self.sw_version is None: + self.send_command('SVER', queue=True) + if self.model_filter is None: + self.send_command('RFIL', queue=True) + 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): """ @@ -341,7 +391,7 @@ class PJLink(QtNetwork.QTcpSocket): data = decode(read, 'utf-8') # Possibility of extraneous data on input when reading. # Clean out extraneous characters in buffer. - dontcare = self.readLine(self.max_size) + dontcare = self.readLine(self.max_size) # noqa: F841 log.debug('({ip}) check_login() read "{data}"'.format(ip=self.ip, data=data.strip())) # At this point, we should only have the initial login prompt with # possible authentication @@ -440,7 +490,8 @@ class PJLink(QtNetwork.QTcpSocket): return data_split = data.split('=') try: - (prefix, version, cmd, data) = (data_split[0][0], data_split[0][1], data_split[0][2:], data_split[1]) + (prefix, version, cmd, data) = (data_split[0][0], data_split[0][1], # noqa: F841 + data_split[0][2:], data_split[1]) except ValueError as e: log.warning('({ip}) get_data(): Invalid packet - expected header + command + data'.format(ip=self.ip)) log.warning('({ip}) get_data(): Received data: "{data}"'.format(ip=self.ip, data=data_in.strip())) @@ -729,11 +780,22 @@ class PJLink(QtNetwork.QTcpSocket): :param data: Class that projector supports. """ # bug 1550891: Projector returns non-standard class response: - # : Expected: %1CLSS=1 - # : Received: %1CLSS=Class 1 + # : Expected: '%1CLSS=1' + # : Received: '%1CLSS=Class 1' (Optoma) + # : Received: '%1CLSS=Version1' (BenQ) if len(data) > 1: - # Split non-standard information from response - clss = data.split()[-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 @@ -845,6 +907,42 @@ class PJLink(QtNetwork.QTcpSocket): 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. diff --git a/openlp/core/lib/projector/upgrade.py b/openlp/core/lib/projector/upgrade.py index 913d54d2d..178ab2be9 100644 --- a/openlp/core/lib/projector/upgrade.py +++ b/openlp/core/lib/projector/upgrade.py @@ -25,16 +25,18 @@ backend for the projector setup. """ import logging -# Not all imports used at this time, but keep for future upgrades -from sqlalchemy import Table, Column, types, inspect -from sqlalchemy.exc import NoSuchTableError +from sqlalchemy import Table, Column, types from sqlalchemy.sql.expression import null -from openlp.core.common.db import drop_columns from openlp.core.lib.db import get_upgrade_op log = logging.getLogger(__name__) +# Possible future imports +# from sqlalchemy.exc import NoSuchTableError +# from sqlalchemy import inspect +# from openlp.core.common.db import drop_columns + # Initial projector DB was unversioned __version__ = 2 diff --git a/setup.cfg b/setup.cfg index 0ecc03ae8..e7e2651c0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,4 +1,14 @@ +# E402 module level import not at top of file +# E722 do not use bare except, specify exception instead +# F841 local variable '' is assigned to but never used + [pep8] exclude=resources.py,vlc.py max-line-length = 120 ignore = E402,E722 + +[flake8] +exclude=resources.py,vlc.py +max-line-length = 120 +ignore = E402,E722 + diff --git a/tests/functional/openlp_core_lib/test_projector_constants.py b/tests/functional/openlp_core_lib/test_projector_constants.py index 019c18888..61a93eb10 100644 --- a/tests/functional/openlp_core_lib/test_projector_constants.py +++ b/tests/functional/openlp_core_lib/test_projector_constants.py @@ -22,7 +22,7 @@ """ Package to test the openlp.core.lib.projector.constants package. """ -from unittest import TestCase, skip +from unittest import TestCase, skip # noqa: F401 class TestProjectorConstants(TestCase): diff --git a/tests/functional/openlp_core_lib/test_projectordb.py b/tests/functional/openlp_core_lib/test_projector_db.py similarity index 99% rename from tests/functional/openlp_core_lib/test_projectordb.py rename to tests/functional/openlp_core_lib/test_projector_db.py index d4ff4e75c..fd2852acf 100644 --- a/tests/functional/openlp_core_lib/test_projectordb.py +++ b/tests/functional/openlp_core_lib/test_projector_db.py @@ -29,8 +29,8 @@ import os import shutil from tempfile import mkdtemp -from unittest import TestCase, skip -from unittest.mock import MagicMock, patch +from unittest import TestCase, skip # noqa: F401 +from unittest.mock import MagicMock, patch # noqa: F401 from openlp.core.lib.projector import upgrade from openlp.core.lib.db import upgrade_db @@ -413,7 +413,7 @@ class TestProjectorDB(TestCase): Test add_projector() fail """ # GIVEN: Test entry in the database - ignore_result = self.projector.add_projector(Projector(**TEST1_DATA)) + ignore_result = self.projector.add_projector(Projector(**TEST1_DATA)) # noqa: F841 # WHEN: Attempt to add same projector entry results = self.projector.add_projector(Projector(**TEST1_DATA)) @@ -439,7 +439,7 @@ class TestProjectorDB(TestCase): Test update_projector() when entry not in database """ # GIVEN: Projector entry in database - ignore_result = self.projector.add_projector(Projector(**TEST1_DATA)) + ignore_result = self.projector.add_projector(Projector(**TEST1_DATA)) # noqa: F841 projector = Projector(**TEST2_DATA) # WHEN: Attempt to update data with a different ID diff --git a/tests/functional/openlp_core_lib/test_projector_pjlink1.py b/tests/functional/openlp_core_lib/test_projector_pjlink1.py index e5fb7566f..3ff47d85a 100644 --- a/tests/functional/openlp_core_lib/test_projector_pjlink1.py +++ b/tests/functional/openlp_core_lib/test_projector_pjlink1.py @@ -59,9 +59,101 @@ class TestPJLink(TestCase): self.assertTrue(mock_qmd5_hash.called_with(TEST_PIN, "Connection request should have been called with TEST_PIN")) - def test_projector_class(self): + def test_projector_process_rfil_save(self): """ - Test class version from projector + 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.assertEquals(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.assertEquals(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.assertEquals(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 @@ -73,9 +165,23 @@ class TestPJLink(TestCase): self.assertEquals(pjlink.pjlink_class, '1', 'Projector should have returned class=1') - def test_non_standard_class_reply(self): + def test_projector_clss_two(self): """ - Bugfix 1550891: CLSS request returns non-standard 'Class N' reply + 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.assertEquals(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 @@ -85,7 +191,7 @@ class TestPJLink(TestCase): # THEN: Projector class should be set with proper value self.assertEquals(pjlink.pjlink_class, '1', - 'Non-standard class reply should have set proper class') + 'Non-standard class reply should have set class=1') @patch.object(pjlink_test, 'change_status') def test_status_change(self, mock_change_status):