pax_global_header00006660000000000000000000000064136615713470014527gustar00rootroot0000000000000052 comment=1932aaf7c18113e6281927f4ee2d30c6b8593639 benoitlouy-pypjlink-1932aaf/000077500000000000000000000000001366157134700161665ustar00rootroot00000000000000benoitlouy-pypjlink-1932aaf/.gitignore000066400000000000000000000001021366157134700201470ustar00rootroot00000000000000*.pyc __pycache__ pypjlink.egg-info/ /.tox/ /dist/ /build/ /venv/ benoitlouy-pypjlink-1932aaf/.travis.yml000066400000000000000000000002421366157134700202750ustar00rootroot00000000000000language: python python: - "3.5" - "3.6" - "3.7" - "3.7-dev" - "3.8" - "3.8-dev" install: - pip install -r test_requirements.txt script: - pytest benoitlouy-pypjlink-1932aaf/CHANGELOG000066400000000000000000000004531366157134700174020ustar00rootroot00000000000000================== pypjlink changelog ================== 1.1.1 (2016-03-01) ================== * added manual page * password argument of authenticate() method can be a string, or omitted if the projector doesn't require authentication 1.1 (2016-02-02) ================ * Python 3 support benoitlouy-pypjlink-1932aaf/LICENSE000066400000000000000000000261361366157134700172030ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. benoitlouy-pypjlink-1932aaf/README.md000066400000000000000000000005251366157134700174470ustar00rootroot00000000000000# pypjlink2 Implementation of the PJLink Class 1 protocol to control projectors. Fork of pypjlink which is no longer maintained, with support for Optoma projectors ## Usage ```python from pypjlink import Projector with Projector.from_address('projector_host') as projector: projector.authenticate() projector.set_power('on') ``` benoitlouy-pypjlink-1932aaf/docs/000077500000000000000000000000001366157134700171165ustar00rootroot00000000000000benoitlouy-pypjlink-1932aaf/docs/pjlink.1000066400000000000000000000035161366157134700204740ustar00rootroot00000000000000.TH PJLINK 1 "February 2016" "pjlink" "User Commands" .SH NAME pjlink \- communicate with projectors through pjlink protocol .SH SYNOPSIS .B pjlink \-h .br .B pjlink [\-p PROJECTOR] COMMAND [COMMAND_ARG [COMMAND_ARG ...]] .SH DESCRIPTION Pjlink is a tool to interoperate with data projectors through the industry standard pjlink protocol. .SH OPTIONS .TP \fB\-h\fR, \fB\-\-help\fR Show help message and exit .TP \fB\-p\fR PROJECTOR, \fB\-\-projector\fR=PROJECTOR Hostname and port where the projector is listening. .br Must be in the form HOST[:PORT]. If PORT is omitted it defaults to 4352. .SS Commands .TP \fBpower\fR [ on | off ] Without argument, get projector power status. .br With argument, set projector power accordingly. .TP \fBinput\fR [ SOURCE_TYPE INPUT_NUMBER ] .br With no arguments, query projector about active input source. .br With arguments, set active input source to number INPUT_NUMBER of type SOURCE_TYPE. .br SOURCE_TYPE muse be one of: VIDEO, DIGITAL, NETWORK, RGB, STORAGE. .TP \fBinputs\fR .br Query projector about available input sources. .TP \fBinfo\fR .br Query projector for general info. .TP \fBlamps\fR .br Query projector about lamps lighting hour. .TP \fBerrors\fR .br Query projector about detected errors. .TP \fBmute\fR [ video | audio | all] .br Without arguments, get project audio and video mute status. With argument, mute video and/or audio. .TP \fBunmute\fR [ video | audio | all] .br Without arguments, get project audio and video mute status. With argument, unmute video and/or audio. .SH "SEE ALSO" Pjlink protocol: .br http://pjlink.jbmia.or.jp/english/data/5-1_PJLink_eng_20131210.pdf .SH "REPORTING BUGS" Bugs tracking is on github: .br https://github.com/gaetano-guerriero/pypjlink .SH AUTHOR Pjlink was originally written by Peter Ward. This version is mantained by Gaetano Guerriero. benoitlouy-pypjlink-1932aaf/pypjlink/000077500000000000000000000000001366157134700200265ustar00rootroot00000000000000benoitlouy-pypjlink-1932aaf/pypjlink/__init__.py000066400000000000000000000001721366157134700221370ustar00rootroot00000000000000# -*- coding: utf-8 -*- version = '1.2.1' from pypjlink.projector import ( Projector, MUTE_VIDEO, MUTE_AUDIO, ) benoitlouy-pypjlink-1932aaf/pypjlink/cli.py000066400000000000000000000114471366157134700211560ustar00rootroot00000000000000import argparse try: from ConfigParser import ( NoSectionError, SafeConfigParser as ConfigParser ) except ImportError: from configparser import ( NoSectionError, SafeConfigParser as ConfigParser ) from getpass import getpass from os import path import sys import appdirs from pypjlink import Projector from pypjlink import projector from pypjlink.cliutils import make_command, print_error def cmd_power(p, state=None): if state is None: print(p.get_power()) else: p.set_power(state) def cmd_input(p, source, number): if source is None: source, number = p.get_input() print(source, number) else: p.set_input(source, number) def cmd_inputs(p): for source, number in p.get_inputs(): print('%s-%s' % (source, number)) def cmd_mute_state(p): video, audio = p.get_mute() print('video:', 'muted' if video else 'unmuted') print('audio:', 'muted' if audio else 'unmuted') def cmd_mute(p, what): if what is None: return cmd_mute_state(p) what = { 'video': projector.MUTE_VIDEO, 'audio': projector.MUTE_AUDIO, 'all': projector.MUTE_VIDEO | projector.MUTE_AUDIO, }[what] p.set_mute(what, True) def cmd_unmute(p, what): if what is None: return cmd_mute_state(p) what = { 'video': projector.MUTE_VIDEO, 'audio': projector.MUTE_AUDIO, 'all': projector.MUTE_VIDEO | projector.MUTE_AUDIO, }[what] p.set_mute(what, False) def cmd_info(p): info = [ ('Name', p.get_name()), ('Manufacturer', p.get_manufacturer()), ('Product Name', p.get_product_name()), ('Other Info', p.get_other_info()) ] for key, value in info: print('%s: %s' % (key, value)) def cmd_lamps(p): for i, (time, state) in enumerate(p.get_lamps(), 1): print('Lamp %d: %s (%d hours)' % ( i, 'on' if state else 'off', time, )) def cmd_errors(p): for what, state in p.get_errors().items(): print('%s: %s' % (what, state)) def make_parser(): parser = argparse.ArgumentParser() parser.add_argument('-p', '--projector') parser.add_argument('-e', '--encoding', default='utf-8') sub = parser.add_subparsers(dest='command', title='command') sub.required = True power = make_command(sub, 'power', cmd_power) power.add_argument('state', nargs='?', choices=('on', 'off')) inpt = make_command(sub, 'input', cmd_input) inpt.add_argument('source', nargs='?', choices=projector.SOURCE_TYPES) inpt.add_argument('number', nargs='?', choices='123456789', default='1') make_command(sub, 'inputs', cmd_inputs) mute = make_command(sub, 'mute', cmd_mute) mute.add_argument('what', nargs='?', choices=('video', 'audio', 'all')) unmute = make_command(sub, 'unmute', cmd_unmute) unmute.add_argument('what', nargs='?', choices=('video', 'audio', 'all')) make_command(sub, 'info', cmd_info) make_command(sub, 'lamps', cmd_lamps) make_command(sub, 'errors', cmd_errors) return parser def resolve_projector(projector): password = None # host:port if projector is not None and ':' in projector: host, port = projector.rsplit(':', 1) port = int(port) # maybe defined in config else: appdir = appdirs.user_data_dir('pjlink') conf_file = path.join(appdir, 'pjlink.conf') try: config = ConfigParser({'port': '4352', 'password': ''}) with open(conf_file, 'r') as f: config.readfp(f) section = projector if projector is None: section = 'default' host = config.get(section, 'host') port = config.getint(section, 'port') password = config.get(section, 'password') or None except (NoSectionError, IOError): if projector is None: raise KeyError('No default projector defined in %s' % conf_file) # no config file, or no projector defined for this host # thus, treat the projector as a hostname w/o port host = projector port = 4352 return host, port, password def main(): parser = make_parser() args = parser.parse_args() kwargs = dict(args._get_kwargs()) func = kwargs.pop('__func__') kwargs.pop('command', None) projector = kwargs.pop('projector') host, port, password = resolve_projector(projector) encoding = kwargs.pop('encoding') if not password: password = getpass with Projector.from_address(host, port, encoding) as proj: rv = proj.authenticate(password) if rv is False: print_error('Incorrect password.') return func(proj, **kwargs) if __name__ == '__main__': main() benoitlouy-pypjlink-1932aaf/pypjlink/cliutils.py000066400000000000000000000061441366157134700222350ustar00rootroot00000000000000import getpass import sys def prompt(name, default=None): """ Grab user input from command line. :param name: prompt text :param default: default value if no input provided. """ prompt = name + (default and ' [%s]' % default or '') prompt += name.endswith('?') and ' ' or ': ' while True: rv = raw_input(prompt) if rv: return rv if default is not None: return default def prompt_pass(name, default=None): """ Grabs hidden (password) input from command line. :param name: prompt text :param default: default value if no input provided. """ prompt = name + (default and ' [%s]' % default or '') prompt += name.endswith('?') and ' ' or ': ' while True: rv = getpass.getpass(prompt) if rv: return rv if default is not None: return default def prompt_bool(name, default=False, yes_choices=None, no_choices=None): """ Grabs user input from command line and converts to boolean value. :param name: prompt text :param default: default value if no input provided. :param yes_choices: default 'y', 'yes', '1', 'on', 'true', 't' :param no_choices: default 'n', 'no', '0', 'off', 'false', 'f' """ yes_choices = yes_choices or ('y', 'yes', '1', 'on', 'true', 't') no_choices = no_choices or ('n', 'no', '0', 'off', 'false', 'f') while True: rv = prompt(name, default and yes_choices[0] or no_choices[0]) if not rv: return default if rv.lower() in yes_choices: return True elif rv.lower() in no_choices: return False def prompt_choices( name, choices, default=None, resolve=lambda s: s.lower(), no_choice=('none',) ): """ Grabs user input from command line from set of provided choices. :param name: prompt text :param choices: list or tuple of available choices. Choices may be single strings or (key, value) tuples. :param default: default value if no input provided. :param no_choice: acceptable list of strings for "null choice" """ _choices = [] options = [] for choice in choices: if isinstance(choice, basestring): options.append(choice) else: options.append("%s [%s]" % (choice[1], choice[0])) choice = choice[0] _choices.append(choice) while True: rv = prompt(name + '? - (%s)' % ', '.join(options), default) if not rv: return default rv = resolve(rv) if rv in no_choice: return None if rv in _choices: return rv def make_command(group, name, function): parser = group.add_parser(name, help=function.__doc__) parser.set_defaults(__func__=function) return parser def make_command_group(parent_group, name): parser = parent_group.add_parser(name) sub = parser.add_subparsers(title='subcommands') return sub def print_error(error): if sys.version_info.major == 2: print >> sys.stderr, error else: sys.stderr.write(error + '\n') benoitlouy-pypjlink-1932aaf/pypjlink/projector.py000066400000000000000000000153411366157134700224130ustar00rootroot00000000000000# -*- coding: utf-8 -*- from __future__ import unicode_literals import hashlib import socket import sys from pypjlink import protocol class ProjectorError(Exception): pass reverse_dict = lambda d: dict(zip(d.values(), d.keys())) POWER_STATES = { 'off': '0', 'on': '1', 'cooling': '2', 'warm-up': '3', } POWER_STATES_REV = reverse_dict(POWER_STATES) SOURCE_TYPES = { 'RGB': '1', 'VIDEO': '2', 'DIGITAL': '3', 'STORAGE': '4', 'NETWORK': '5', } SOURCE_TYPES_REV = reverse_dict(SOURCE_TYPES) MUTE_VIDEO = 1 MUTE_AUDIO = 2 MUTE_STATES_REV = { '11': (True, False), '20': (False, False), '21': (False, True), '31': (True, True), '30': (False, False), } ERROR_STATES_REV = { '0': 'ok', '1': 'warning', '2': 'error', } class Projector(object): def __init__(self, f, encoding): self.f = f self.encoding = encoding def __enter__(self): return self def __exit__(self, exception_type, exception_value, traceback): self.close() def close(self): self.f.close() @classmethod def from_address(cls, address, port=4352, encoding='utf-8', timeout = 2): """build a Projector from a ip address""" sock = socket.socket() sock.settimeout(timeout) sock.connect((address, port)) # in python 3 I need to specify newline, otherwise read hangs # in "PJLINK 0\r" # I expect socket file to return byte strings in python 2 and # unicode strings in python 3 if sys.version_info.major == 2: f = sock.makefile() else: f = sock.makefile(mode='rw', newline='\r', encoding=encoding) sock.close() return cls(f, encoding) def authenticate(self, password=None): # I'm just implementing the authentication scheme designed in the # protocol. Don't take this as any kind of assurance that it's secure. data = protocol.read(self.f, 9, self.encoding) assert data[:7].upper() == 'PJLINK ' security = data[7] if security == '0': return None data += protocol.read(self.f, 9, self.encoding) assert security == '1' assert data[8] == ' ' salt = data[9:17] assert data[17] == '\r' if password is None: raise RuntimeError('projector needs a password') if callable(password): password = password() pass_data = (salt + password).encode('utf-8') pass_data_md5 = hashlib.md5(pass_data).hexdigest() # we *must* send a command to complete the procedure, # so we just get the power state. cmd_data = protocol.to_binary('POWR', '?') self.f.write(pass_data_md5 + cmd_data) self.f.flush() # read the response, see if it's a failed auth data = protocol.read(self.f, 7, self.encoding) if data.upper() == 'PJLINK ': # should be a failed auth if we get that data += protocol.read(self.f, 5, self.encoding) assert data == 'PJLINK ERRA\r' # it definitely is return False # good auth, so we should get a reply to the command we sent body, param = protocol.parse_response(self.f, self.encoding, data) # make sure we got a sensible response back assert body == 'POWR' if param in protocol.ERRORS: raise ProjectorError(protocol.ERRORS[param]) # but we don't care about the value if we did return True def get(self, body): success, response = protocol.send_command(self.f, body, '?', self.encoding) if not success: raise ProjectorError(response) return response def set(self, body, param): success, response = protocol.send_command(self.f, body, param, self.encoding) if not success: raise ProjectorError(response) assert response == 'OK' # Power def get_power(self): param = self.get('POWR') return POWER_STATES_REV[param] def set_power(self, status, force=False): if not force: assert status in ('off', 'on') self.set('POWR', POWER_STATES[status]) # Input def get_input(self): param = self.get('INPT') source, number = param source = SOURCE_TYPES_REV[source] number = int(number) return (source, number) def set_input(self, source, number): source = SOURCE_TYPES[source] number = str(number) assert number in '123456789' self.set('INPT', source + number) # A/V mute def get_mute(self): param = self.get('AVMT') return MUTE_STATES_REV[param] def set_mute(self, what, state): assert what in (MUTE_VIDEO, MUTE_AUDIO, MUTE_VIDEO | MUTE_AUDIO) what = str(what) assert what in '123' state = '1' if state else '0' self.set('AVMT', what + state) # Errors def get_errors(self): param = self.get('ERST') errors = 'fan lamp temperature cover filter other'.split() assert len(param) == len(errors) return dict((key, ERROR_STATES_REV[value]) for key, value in zip(errors, param)) # Lamps def get_lamps(self): param = self.get('LAMP') assert len(param) <= 65 values = param.split(' ') assert len(values) <= 16 and len(values) % 2 == 0 lamps = [] for time, state in zip(values[::2], values[1::2]): time = int(time) state = bool(int(state)) lamps.append((time, state)) assert len(lamps) <= 8 return lamps # Input list def get_inputs(self): param = self.get('INST') assert len(param) <= 95 values = param.split(' ') assert len(values) <= 50 inputs = [] for value in values: source, number = value source = SOURCE_TYPES_REV[source] assert number in '123456789' number = int(number) inputs.append((source, number)) return inputs # Projector info def get_name(self): param = self.get('NAME') assert len(param) <= 64 return param def get_manufacturer(self): param = self.get('INF1') assert len(param) <= 32 # stupidly, this is not defined as utf-8 in the spec. :( return param def get_product_name(self): param = self.get('INF2') assert len(param) <= 32 # stupidly, this is not defined as utf-8 in the spec. :( return param def get_other_info(self): param = self.get('INFO') assert len(param) <= 32 return param # TODO: def get_class(self): self.get('CLSS') # once we know that class 2 is, and how to deal with it benoitlouy-pypjlink-1932aaf/pypjlink/protocol.py000066400000000000000000000034151366157134700222440ustar00rootroot00000000000000# -*- coding: utf-8 -*- import sys def read_until(f, term, encoding): data = [] c = f.read(1) while c != term: data.append(c) c = f.read(1) data = ''.join(data) if sys.version_info.major == 2: data = data.decode(encoding) return data def to_binary(body, param, sep=' '): assert body.isupper() assert len(body) == 4 assert len(param) <= 128 return '%1' + body + sep + param + '\r' def parse_response(f, encoding, data=''): if len(data) < 7: data += read(f, 2 + 4 + 1 - len(data), encoding) header = data[0] assert header == '%' version = data[1] # only class 1 is currently defined assert version == '1' body = data[2:6] # commands are case-insensitive, but let's turn them upper case anyway # this will avoid the rest of our code from making this mistake # FIXME: AFAIR this takes the current locale into consideration, it shouldn't. body = body.upper() sep = data[6] assert sep == '=' param = read_until(f, '\r', encoding) return (body, param) # python 3 socket makefile is already unicode in text mode, i do the same on # python 2 if sys.version_info.major == 2: def read(f, n, encoding): return f.read(n).decode(encoding) else: def read(f, n, encoding): return f.read(n) ERRORS = { 'ERR1': 'undefined command', 'ERR2': 'out of parameter', 'ERR3': 'unavailable time', 'ERR4': 'projector failure', } def send_command(f, req_body, req_param, encoding): data = to_binary(req_body, req_param) f.write(data) f.flush() resp_body, resp_param = parse_response(f, encoding) assert resp_body == req_body if resp_param in ERRORS: return False, ERRORS[resp_param] return True, resp_param benoitlouy-pypjlink-1932aaf/requirements.txt000066400000000000000000000000171366157134700214500ustar00rootroot00000000000000appdirs==1.4.0 benoitlouy-pypjlink-1932aaf/setup.py000077500000000000000000000022421366157134700177030ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- from setuptools import find_packages, setup from pypjlink import version with open('README.md', 'r') as fh: long_description = fh.read() print(long_description) setup( name='pypjlink2', version=version, author=('Peter Ward , ' 'Gaetano Guerriero , ' 'Benoit Louy '), author_email='pypjlink@mm.st', url='https://github.com/benoitlouy/pypjlink', description='PJLink is a standard for controlling data projectors.', long_description=long_description, long_description_content_type='text/markdown', classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Console', 'License :: OSI Approved :: Apache Software License', 'Natural Language :: English', 'Topic :: Multimedia :: Video :: Display', 'Topic :: Utilities', ], install_requires=['appdirs'], packages=find_packages(exclude=['tests']), entry_points={ 'console_scripts': [ 'pjlink = pypjlink.cli:main', ], }, test_suite='tests', ) benoitlouy-pypjlink-1932aaf/test_requirements.txt000066400000000000000000000000621366157134700225070ustar00rootroot00000000000000pytest==5.2.0 mock==2.0.0; python_version < '3.4' benoitlouy-pypjlink-1932aaf/tests/000077500000000000000000000000001366157134700173305ustar00rootroot00000000000000benoitlouy-pypjlink-1932aaf/tests/__init__.py000066400000000000000000000000001366157134700214270ustar00rootroot00000000000000benoitlouy-pypjlink-1932aaf/tests/test_projector.py000066400000000000000000000106171366157134700227550ustar00rootroot00000000000000# -*- coding: utf-8 -*- from __future__ import unicode_literals import hashlib import unittest from pypjlink.projector import Projector, POWER_STATES from .utils import MockProjSocket NO_AUTH_RESPONSE = 'PJLINK 0\r' class AuthenticationTC(unittest.TestCase): def test_no_auth(self): """test detection of no auth projector""" power_response = '%1POWR=1\r' with MockProjSocket(NO_AUTH_RESPONSE + power_response) as mock_stream: proj = Projector.from_address('projhost') self.assertFalse(proj.authenticate(lambda: '')) # since projector said no auth required, I shouldn't have written # anything to it self.assertFalse(mock_stream.write.called) def test_auth(self): """test authentication""" auth_response = 'PJLINK 1 00112233\r' power_response = '%1POWR=1\r' with MockProjSocket(auth_response + power_response) as mock_stream: proj = Projector.from_address('projhost') self.assertTrue(proj.authenticate(lambda: 'p')) # test write of authentication self.assertEqual( mock_stream.written, self._md5_auth_code('00112233', 'p') + '%1POWR ?\r') def test_wrong_auth(self): """test failed authentication""" response = ( 'PJLINK 1 00112233\r' 'PJLINK ERRA\r' ) with MockProjSocket(response) as mock_stream: proj = Projector.from_address('projhost') self.assertFalse(proj.authenticate(lambda: 'p')) self.assertEqual( mock_stream.written, self._md5_auth_code('00112233', 'p') + '%1POWR ?\r') def test_string_password(self): """since 1.1.1, password can be a string in authenticate() method""" auth_response = 'PJLINK 1 00112244\r' power_response = '%1POWR=1\r' with MockProjSocket(auth_response + power_response) as mock_stream: proj = Projector.from_address('projhost') self.assertTrue(proj.authenticate('ps')) # test write of authentication self.assertEqual( mock_stream.written, self._md5_auth_code('00112244', 'ps') + '%1POWR ?\r') def test_no_password_no_auth(self): """since 1.1.1, password can be omitted from authenticate() method if projector doesn't need a password""" power_response = '%1POWR=1\r' with MockProjSocket(NO_AUTH_RESPONSE + power_response) as mock_stream: proj = Projector.from_address('projhost') self.assertFalse(proj.authenticate()) self.assertFalse(mock_stream.write.called) def test_no_password_auth_required(self): """if projector needs a password but is missing in authenticate() there should be an error""" auth_response = 'PJLINK 1 00112255\r' power_response = '%1POWR=1\r' with MockProjSocket(auth_response + power_response) as mock_stream: proj = Projector.from_address('projhost') with self.assertRaises(RuntimeError): proj.authenticate() self.assertFalse(mock_stream.write.called) def _md5_auth_code(self, salt, password): return hashlib.md5((salt + password).encode('utf-8')).hexdigest() class NameTC(unittest.TestCase): def test_unicode(self): """test utf-8 chars in projector name""" response = NO_AUTH_RESPONSE + '%1NAME=à€\r' with MockProjSocket(response) as mock_stream: proj = Projector.from_address('projhost') proj.authenticate(lambda: '') name = proj.get_name() self.assertEqual(name, 'à€') class PowerTC(unittest.TestCase): def test_get(self): response = NO_AUTH_RESPONSE + '%1POWR={}\r'.format( POWER_STATES['cooling']) with MockProjSocket(response) as mock_stream: proj = Projector.from_address('projhost') proj.authenticate(lambda: '') self.assertEqual(proj.get_power(), 'cooling') def test_set(self): response = NO_AUTH_RESPONSE + '%1POWR=OK\r' with MockProjSocket(response) as mock_stream: proj = Projector.from_address('projhost') proj.authenticate(lambda: '') proj.set_power('off') self.assertEqual( mock_stream.written, '%1POWR {}\r'.format(POWER_STATES['off'])) benoitlouy-pypjlink-1932aaf/tests/utils.py000066400000000000000000000023251366157134700210440ustar00rootroot00000000000000# -*- coding: utf-8 -*- from io import StringIO, BytesIO try: from unittest import mock except ImportError: import mock class MockProjSocket(object): """A context manager that mocks socket creation inside projector module Read from the mock returns response string passed in constructor. Write into the mock appends data in .written string attribute, besides calling .write mock method """ mock_target = 'pypjlink.projector.socket.socket' def __init__(self, response): self._response = response self._mock_sock = None def __enter__(self): mock_socket_func = mock.patch(self.mock_target).__enter__() self._mock_sock = mock_socket_func.return_value stream = self._mock_sock.makefile.return_value import sys if sys.version_info.major == 2: buf = BytesIO(self._response.encode('utf-8')) else: buf = StringIO(self._response) stream.read = buf.read stream.written = '' def _write(data): stream.written += data stream.write.side_effect = _write return stream def __exit__ (self, exc_type, exc_value, traceback): self._mock_sock.__exit__() benoitlouy-pypjlink-1932aaf/tox.ini000066400000000000000000000002201366157134700174730ustar00rootroot00000000000000[tox] envlist = py34, py35, py36, py37, py38 [testenv] deps= -rrequirements.txt commands= python -m unittest discover pypjlink