Fix db.Manager upgrade/PJlink2 update D

This commit is contained in:
Ken Roberts 2017-05-27 11:21:24 -07:00
parent 533bef159a
commit 3ea37800b7
12 changed files with 235 additions and 84 deletions

View File

@ -144,6 +144,7 @@ def upgrade_db(url, upgrade):
:param url: The url of the database to upgrade.
:param upgrade: The python module that contains the upgrade instructions.
"""
log.debug('Checking upgrades for DB {db}'.format(db=url))
session, metadata = init_db(url)
class Metadata(BaseModel):
@ -160,17 +161,15 @@ def upgrade_db(url, upgrade):
metadata_table.create(checkfirst=True)
mapper(Metadata, metadata_table)
version_meta = session.query(Metadata).get('version')
if version_meta is None:
# Tables have just been created - fill the version field with the most recent version
if session.query(Metadata).get('dbversion'):
version = 0
else:
version = upgrade.__version__
if version_meta:
version = int(version_meta.value)
else:
# Due to issues with other checks, if the version is not set in the DB then default to 0
# and let the upgrade function handle the checks
version = 0
version_meta = Metadata.populate(key='version', value=version)
session.add(version_meta)
session.commit()
else:
version = int(version_meta.value)
if version > upgrade.__version__:
session.remove()
return version, upgrade.__version__

View File

@ -59,33 +59,61 @@ TIMEOUT = 30.0
PJLINK_MAX_PACKET = 136
# NOTE: Change format to account for some commands are both class 1 and 2
PJLINK_VALID_CMD = {
'ACKN': ['2', ], # UDP Reply to 'SRCH'
'AVMT': ['1', ], # Shutter option
'CLSS': ['1', ], # PJLink class support query
'ERST': ['1', '2'], # Error status option
'FILT': ['2', ], # Get current filter usage time
'FREZ': ['2', ], # Set freeze/unfreeze picture being projected
'INF1': ['1', ], # Manufacturer name query
'INF2': ['1', ], # Product name query
'INFO': ['1', ], # Other information query
'INNM': ['2', ], # Get Video source input terminal name
'INPT': ['1', ], # Video sources option
'INST': ['1', ], # Input sources available query
'IRES': ['2', ], # Get Video source resolution
'LAMP': ['1', ], # Lamp(s) query (Includes fans)
'LKUP': ['2', ], # UPD Linkup status notification
'MVOL': ['2', ], # Set microphone volume
'NAME': ['1', ], # Projector name query
'PJLINK': ['1', ], # Initial connection
'POWR': ['1', ], # Power option
'RFIL': ['2', ], # Get replacement air filter model number
'RLMP': ['2', ], # Get lamp replacement model number
'RRES': ['2', ], # Get projector recommended video resolution
'SNUM': ['2', ], # Get projector serial number
'SRCH': ['2', ], # UDP broadcast search for available projectors on local network
'SVER': ['2', ], # Get projector software version
'SVOL': ['2', ] # Set speaker volume
'ACKN': {'version': ['2', ], # UDP Reply to 'SRCH'
'description': 'Acknowledge a PJLink SRCH command - returns MAC address.'},
'AVMT': {'version': ['1', ],
'description': 'Blank/unblank video and/or mute audio.'},
'CLSS': {'version': ['1', ],
'description': 'Query projector PJLink class support.'},
'ERST': {'version': ['1', '2'],
'description': 'Query error status from projector. '
'Returns fan/lamp/temp/cover/filter/other error status.'},
'FILT': {'version': ['2', ], # Assume (!) time in hours
'description': 'Query number of hours on filter.'},
'FREZ': {'version': ['2', ],
'description': 'Freeze or unfreeze current image being projected.'},
'INF1': {'version': ['1', ],
'description': 'Query projector manufacturer name.'},
'INF2': {'version': ['1', ],
'description': 'Query projector product name.'},
'INFO': {'version': ['1', ],
'description': 'Query projector for other information set by manufacturer.'},
'INNM': {'version': ['2', ],
'description': 'Query specified input source name'},
'INPT': {'version': ['1', ],
'description': 'Switch to specified video source.'},
'INST': {'version': ['1', ],
'description': 'Query available input sources.'},
'IRES': {'version:': ['2', ],
'description': 'Query current input resolution.'},
'LAMP': {'version': ['1', ],
'description': 'Query lamp time and on/off status. Multiple lamps supported.'},
'LKUP': {'version': ['2', ],
'description': 'UDP Status notify. Returns MAC address.'},
'MVOL': {'version': ['2', ],
'description': 'Adjust microphone volume by 1 step.'},
'NAME': {'version': ['1', ],
'description': 'Query customer-set projector name.'},
'PJLINK': {'version': ['1', ],
'description': 'Initial connection with authentication/no authentication request.'},
'POWR': {'version': ['1', ],
'description': 'Turn lamp on or off/standby.'},
'RFIL': {'version': ['2', ],
'description': 'Query replacement air filter model number.'},
'RLMP': {'version': ['2', ],
'description': 'Query replacement lamp model number.'},
'RRES': {'version': ['2', ],
'description': 'Query recommended resolution.'},
'SNUM': {'version': ['2', ],
'description': 'Query projector serial number.'},
'SRCH': {'version': ['2', ],
'description': 'UDP broadcast search request for available projectors.'},
'SVER': {'version': ['2', ],
'description': 'Query projector software version number.'},
'SVOL': {'version': ['2', ],
'description': 'Adjust speaker volume by 1 step.'}
}
# Error and status codes
S_OK = E_OK = 0 # E_OK included since I sometimes forget
# Error codes. Start at 200 so we don't duplicate system error codes.

View File

@ -44,6 +44,7 @@ from sqlalchemy.orm import relationship
from openlp.core.lib.db import Manager, init_db, init_url
from openlp.core.lib.projector.constants import PJLINK_DEFAULT_CODES
from openlp.core.lib.projector import upgrade
Base = declarative_base(MetaData())
@ -243,7 +244,9 @@ class ProjectorDB(Manager):
"""
def __init__(self, *args, **kwargs):
log.debug('ProjectorDB().__init__(args="{arg}", kwargs="{kwarg}")'.format(arg=args, kwarg=kwargs))
super().__init__(plugin_name='projector', init_schema=self.init_schema)
super(ProjectorDB, self).__init__(plugin_name='projector',
init_schema=self.init_schema,
upgrade_mod=upgrade)
log.debug('ProjectorDB() Initialized using db url {db}'.format(db=self.db_url))
log.debug('Session: {session}'.format(session=self.session))

View File

@ -186,10 +186,15 @@ class PJLink(QtNetwork.QTcpSocket):
self.pjlink_name = None
self.manufacturer = None
self.model = None
self.serial_no = None
self.sw_version = 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
@ -451,14 +456,14 @@ class PJLink(QtNetwork.QTcpSocket):
return
data_split = data.split('=')
try:
(prefix, class_, 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], 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()))
self.change_status(E_INVALID_DATA)
self.receive_data_signal()
return
if not (cmd in PJLINK_VALID_CMD and class_ in PJLINK_VALID_CMD[cmd]):
if cmd not in PJLINK_VALID_CMD:
log.warning('({ip}) get_data(): Invalid packet - unknown command "{data}"'.format(ip=self.ip, data=cmd))
self.receive_data_signal()
return
@ -507,14 +512,25 @@ class PJLink(QtNetwork.QTcpSocket):
log.warning('({ip}) send_command(): Not connected - returning'.format(ip=self.ip))
self.send_queue = []
return
if cmd not in PJLINK_VALID_CMD:
log.error('({ip}) send_command(): Invalid command requested - ignoring.'.format(ip=self.ip))
return
self.projectorNetwork.emit(S_NETWORK_SENDING)
log.debug('({ip}) send_command(): Building cmd="{command}" opts="{data}"{salt}'.format(ip=self.ip,
command=cmd,
data=opts,
salt='' if salt is None
else ' with hash'))
# TODO: Check for class of command rather than default to projector PJLink class
header = PJLINK_HEADER.format(linkclass=self.pjlink_class)
cmd_ver = PJLINK_VALID_CMD[cmd]['version']
if self.pjlink_class in cmd_ver:
header = PJLINK_HEADER.format(linkclass=self.pjlink_class)
elif len(cmd_ver) == 1 and (int(cmd_ver[0]) < int(self.pjlink_class)):
# Typically a class 1 only command
header = PJLINK_HEADER.format(linkclass=cmd_ver[0])
else:
# NOTE: Once we get to version 3 then think about looping
log.error('({ip}): send_command(): PJLink class check issue? aborting'.format(ip=self.ip))
return
out = '{salt}{header}{command} {options}{suffix}'.format(salt="" if salt is None else salt,
header=header,
command=cmd,
@ -589,10 +605,13 @@ class PJLink(QtNetwork.QTcpSocket):
cmd=cmd,
data=data))
# Check if we have a future command not available yet
if cmd in self.pjlink_future:
self._not_implemented(cmd)
if cmd not in PJLINK_VALID_CMD:
log.error('({ip}) Unknown command received - ignoring'.format(ip=self.ip))
return
if data in PJLINK_ERRORS:
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':
@ -624,14 +643,11 @@ class PJLink(QtNetwork.QTcpSocket):
self.send_busy = False
self.projectorReceivedData.emit()
return
if cmd in self.pjlink_functions:
log.debug('({ip}) Calling function for {cmd}'.format(ip=self.ip, cmd=cmd))
self.pjlink_functions[cmd](data)
else:
log.warning('({ip}) Invalid command {data}'.format(ip=self.ip, data=cmd))
# Command checks already passed
log.debug('({ip}) Calling function for {cmd}'.format(ip=self.ip, cmd=cmd))
self.send_busy = False
self.projectorReceivedData.emit()
self.pjlink_functions[cmd](data)
def process_lamp(self, data):
"""

View File

@ -0,0 +1,73 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2017 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 #
###############################################################################
"""
The :mod:`upgrade` module provides a way for the database and schema that is the
backend for the Songs plugin
"""
import logging
# Not all imports used at this time, but keep for future upgrades
from sqlalchemy import 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__)
# Initial projector DB was unversioned
__version__ = 2
log.debug('Projector DB upgrade module loading')
def upgrade_1(session, metadata):
"""
Version 1 upgrade - old db might/might not be versioned.
"""
pass
def upgrade_2(session, metadata):
"""
Version 2 upgrade.
Update Projector() table to include new data defined in PJLink version 2 changes
serial_no: Column(String(30))
sw_version: Column(String(30))
model_filter: Column(String(30))
model_lamp: Column(String(30))
:param session: DB session instance
:param metadata: Metadata of current DB
"""
new_op = get_upgrade_op(session)
if 'serial_no' not in [t.name for t in metadata.tables.values()]:
log.debug("Upgrading projector DB to version '2'")
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")

View File

@ -46,16 +46,16 @@ class PathEdit(QtWidgets.QWidget):
:param parent: The parent of the widget. This is just passed to the super method.
:type parent: QWidget or None
:param dialog_caption: Used to customise the caption in the QFileDialog.
:param dialog_caption: str
:param default_path: The default path. This is set as the path when the revert button is clicked
:type default_path: str
:param show_revert: Used to determin if the 'revert button' should be visible.
:type show_revert: bool
:return: None
:rtype: None
"""
@ -72,7 +72,7 @@ class PathEdit(QtWidgets.QWidget):
Set up the widget
:param show_revert: Show or hide the revert button
:type show_revert: bool
:return: None
:rtype: None
"""

View File

@ -662,6 +662,20 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, UiProjecto
message = '%s<b>%s</b>: %s<br />' % (message,
translate('OpenLP.ProjectorManager', 'Current source input is'),
projector.link.source)
if projector.link.pjlink_class == '2':
# Information only available for PJLink Class 2 projectors
message += '<b>{title}</b>: {data}<br /><br />'.format(title=translate('OpenLP.ProjectorManager',
'Serial Number'),
data=projector.serial_no)
message += '<b>{title}</b>: {data}<br /><br />'.format(title=translate('OpenLP.ProjectorManager',
'Software Version'),
data=projector.sw_version)
message += '<b>{title}</b>: {data}<br /><br />'.format(title=translate('OpenLP.ProjectorManager',
'Lamp type'),
data=projector.model_lamp)
message += '<b>{title}</b>: {data}<br /><br />'.format(title=translate('OpenLP.ProjectorManager',
'Filter type'),
data=projector.model_filter)
count = 1
for item in projector.link.lamp:
message += '<b>{title} {count}</b> {status} '.format(title=translate('OpenLP.ProjectorManager',

View File

@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################

View File

@ -69,7 +69,7 @@ class Ui_SongUsageDetailDialog(object):
self.file_horizontal_layout.setSpacing(8)
self.file_horizontal_layout.setContentsMargins(8, 8, 8, 8)
self.file_horizontal_layout.setObjectName('file_horizontal_layout')
self.report_path_edit = PathEdit(self.file_group_box, path_type = PathType.Directories, show_revert=False)
self.report_path_edit = PathEdit(self.file_group_box, path_type=PathType.Directories, show_revert=False)
self.file_horizontal_layout.addWidget(self.report_path_edit)
self.vertical_layout.addWidget(self.file_group_box)
self.button_box = create_button_box(song_usage_detail_dialog, 'button_box', ['cancel', 'ok'])

View File

@ -384,21 +384,6 @@ class TestPJLink(TestCase):
self.assertEquals("{test}".format(test=mock_send_command.call_args),
"call(data='{hash}%1CLSS ?\\r')".format(hash=TEST_HASH))
@patch.object(pjlink_test, '_not_implemented')
def not_implemented_test(self, mock_not_implemented):
"""
Test PJLink._not_implemented method being called
"""
# GIVEN: test object
pjlink = pjlink_test
test_cmd = 'TESTMEONLY'
# WHEN: A future command is called that is not implemented yet
pjlink.process_command(test_cmd, "Garbage data for test only")
# THEN: PJLink.__not_implemented should have been called with test_cmd
mock_not_implemented.assert_called_with(test_cmd)
@patch.object(pjlink_test, 'disconnect_from_host')
def socket_abort_test(self, mock_disconnect):
"""

View File

@ -27,11 +27,15 @@ PREREQUISITE: add_record() and get_all() functions validated.
"""
import os
import shutil
from unittest import TestCase
from tempfile import mkdtemp
from unittest import TestCase, skip
from unittest.mock import MagicMock, patch
from openlp.core.lib.projector.db import Manufacturer, Model, Projector, ProjectorDB, ProjectorSource, Source
from openlp.core.lib.projector import upgrade
from openlp.core.lib.db import upgrade_db
from openlp.core.lib.projector.constants import PJLINK_PORT
from openlp.core.lib.projector.db import Manufacturer, Model, Projector, ProjectorDB, ProjectorSource, Source
from tests.resources.projector.data import TEST_DB_PJLINK1, TEST_DB, TEST1_DATA, TEST2_DATA, TEST3_DATA
from tests.utils.constants import TEST_RESOURCES_PATH
@ -85,6 +89,42 @@ def add_records(projector_db, test):
return added
class TestProjectorDBUpdate(TestCase):
"""
Test case for upgrading Projector DB.
NOTE: Separate class so I don't have to look for upgrade tests.
"""
def setUp(self):
"""
Setup for tests
"""
self.tmp_folder = mkdtemp(prefix='openlp_')
def tearDown(self):
"""
Clean up after tests
"""
# Ignore errors since windows can have problems with locked files
shutil.rmtree(self.tmp_folder, ignore_errors=True)
def test_upgrade_old_projector_db(self):
"""
Test that we can upgrade an old song db to the current schema
"""
# GIVEN: An old song db
old_db = os.path.join(TEST_RESOURCES_PATH, "projector", TEST_DB_PJLINK1)
tmp_db = os.path.join(self.tmp_folder, TEST_DB)
shutil.copyfile(old_db, tmp_db)
db_url = 'sqlite:///{db}'.format(db=tmp_db)
# WHEN: upgrading the db
updated_to_version, latest_version = upgrade_db(db_url, upgrade)
# THEN: the song db should have been upgraded to the latest version
self.assertEqual(updated_to_version, latest_version,
'The projector DB should have been upgrade to the latest version')
class TestProjectorDB(TestCase):
"""
Test case for ProjectorDB
@ -94,7 +134,9 @@ class TestProjectorDB(TestCase):
"""
Set up anything necessary for all tests
"""
mocked_init_url.return_value = 'sqlite:///{db}'.format(db=TEST_DB)
self.tmp_folder = mkdtemp(prefix='openlp_')
tmpdb_url = 'sqlite:///{db}'.format(db=os.path.join(self.tmp_folder, TEST_DB))
mocked_init_url.return_value = tmpdb_url
self.projector = ProjectorDB()
def tearDown(self):
@ -103,15 +145,8 @@ class TestProjectorDB(TestCase):
"""
self.projector.session.close()
self.projector = None
retries = 0
while retries < 5:
try:
if os.path.exists(TEST_DB):
os.unlink(TEST_DB)
break
except:
time.sleep(1)
retries += 1
# Ignore errors since windows can have problems with locked files
shutil.rmtree(self.tmp_folder, ignore_errors=True)
def test_find_record_by_ip(self):
"""

View File

@ -29,7 +29,7 @@ from tempfile import gettempdir
# Test data
TEST_DB_PJLINK1 = 'projector_pjlink1.sqlite'
TEST_DB = os.path.join(gettempdir(), 'openlp-test-projectordb.sql')
TEST_DB = 'openlp-test-projectordb.sqlite'
TEST_SALT = '498e4a67'
@ -39,8 +39,6 @@ TEST_HASH = '5d8409bc1c3fa39749434aa3a5c38682'
TEST_CONNECT_AUTHENTICATE = 'PJLink 1 {salt}'.format(salt=TEST_SALT)
TEST_DB = os.path.join(gettempdir(), 'openlp-test-projectordb.sql')
TEST1_DATA = dict(ip='111.111.111.111',
port='1111',
pin='1111',