From 830385e2a01ba73357af0f66dd807ecfe4df421b Mon Sep 17 00:00:00 2001 From: Fredrik Eriksson Date: Thu, 17 Nov 2022 08:09:05 +0100 Subject: [PATCH] [service.projcontrol] 1.4.0 --- service.projcontrol/LICENSE | 35 ++ service.projcontrol/README.rst | 115 +++++++ service.projcontrol/addon.xml | 56 ++++ service.projcontrol/icon.png | Bin 0 -> 37366 bytes service.projcontrol/icon_license.txt | 201 ++++++++++++ service.projcontrol/lib/__init__.py | 14 + service.projcontrol/lib/acer.py | 306 ++++++++++++++++++ service.projcontrol/lib/benq.py | 183 +++++++++++ service.projcontrol/lib/commands.py | 158 +++++++++ service.projcontrol/lib/epson.py | 208 ++++++++++++ service.projcontrol/lib/errors.py | 15 + service.projcontrol/lib/helpers.py | 46 +++ service.projcontrol/lib/infocus.py | 202 ++++++++++++ service.projcontrol/lib/monitor.py | 108 +++++++ service.projcontrol/lib/server.py | 94 ++++++ service.projcontrol/lib/service.py | 81 +++++ .../resource.language.de_de/strings.po | 174 ++++++++++ .../resource.language.en_gb/strings.po | 178 ++++++++++ .../resource.language.nb_no/strings.po | 174 ++++++++++ .../resource.language.sv_se/strings.po | 174 ++++++++++ service.projcontrol/resources/settings.xml | 257 +++++++++++++++ service.projcontrol/service.py | 10 + 22 files changed, 2789 insertions(+) create mode 100644 service.projcontrol/LICENSE create mode 100644 service.projcontrol/README.rst create mode 100644 service.projcontrol/addon.xml create mode 100644 service.projcontrol/icon.png create mode 100644 service.projcontrol/icon_license.txt create mode 100644 service.projcontrol/lib/__init__.py create mode 100644 service.projcontrol/lib/acer.py create mode 100644 service.projcontrol/lib/benq.py create mode 100644 service.projcontrol/lib/commands.py create mode 100644 service.projcontrol/lib/epson.py create mode 100644 service.projcontrol/lib/errors.py create mode 100644 service.projcontrol/lib/helpers.py create mode 100644 service.projcontrol/lib/infocus.py create mode 100644 service.projcontrol/lib/monitor.py create mode 100644 service.projcontrol/lib/server.py create mode 100644 service.projcontrol/lib/service.py create mode 100644 service.projcontrol/resources/language/resource.language.de_de/strings.po create mode 100644 service.projcontrol/resources/language/resource.language.en_gb/strings.po create mode 100644 service.projcontrol/resources/language/resource.language.nb_no/strings.po create mode 100644 service.projcontrol/resources/language/resource.language.sv_se/strings.po create mode 100644 service.projcontrol/resources/settings.xml create mode 100644 service.projcontrol/service.py diff --git a/service.projcontrol/LICENSE b/service.projcontrol/LICENSE new file mode 100644 index 0000000000..1499e3f6c7 --- /dev/null +++ b/service.projcontrol/LICENSE @@ -0,0 +1,35 @@ +Copyright (c) 2015, Fredrik Eriksson +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of kodi_projcontrol nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +--- + +Note that icon.png is taken from Googles Noto Emoji Objects Icons, and is +licensed under a different license. Icon has been modified from the original; +it has been resized and transparent areas has been colored black. +See icon_license.txt for icon license terms. diff --git a/service.projcontrol/README.rst b/service.projcontrol/README.rst new file mode 100644 index 0000000000..79bdcfdf38 --- /dev/null +++ b/service.projcontrol/README.rst @@ -0,0 +1,115 @@ +Projector Control for Kodi +========================== +Service add-on to Kodi for controling projectors with an optional RESTful API. This is intended to be used on stand-alone media centers running Kodi. + +Features +-------- +* Power on, off and set input on the projector when kodi starts/exits and/or screensaver activates/deactivates +* Automatically update library when projector is shut down +* Do regular library updates as long as the projector is shut down +* Power on, off or toggle projector using a REST API +* Change input source of the projector using a REST API + +Requirements +------------ +* py-serial +* bottle +* A supported projector connected over serial interface +* Kodi installation (only tested on Linux) + +Supported Projectors +-------------------- +It should be a trivial task to add support for more projectors with serial connections. However I can't test any new implementation +without having a projector of that model. While I wouldn't mind if you send me +projectors of different brands and models, you will probably find it cheaper to learn a little python and implement it yourself. +PR:s are always welcome. + +That said; if you have a projector that you want support for, please create a github +issue at https://github.com/fredrik-eriksson/kodi_projcontrol. At minimum the following information is required to implement +support for a projector: + +* connection settings (baudrate, bytesize, parity and stopbits) +* command syntax +* response syntax (for both get and set commands) +* how to verify projector is accepting commands (if possible) +* how to detect and handle error-responses + +Below is a list of currently supported projectors + +Epson +##### +* TW3200 +* PowerLite 820p + +InFocus +####### +* IN72/IN74/IN76 + +Acer +#### +* X1373WH +* V7500 + +BenQ +#### +* M535 series + +Usage +----- +Copy repository to your Kodi addon directory (usually ~/.kodi/addons) and rename it to 'service.projcontrol'. + +REST API +-------- +Note if you use the REST API: the api provides absolutely no security; never enable it on untrusted network. + +After configuring and enabling the REST API from Kodi you can test it using curl + +.. code-block:: shell + + # Check power status and input source + $ curl http://10.37.37.13:6661/power + { + "power": true, + "source": "HDMI1" + } + + # Controlling power with POST request. Valid commands are "on", "off" or "toggle" + $ curl -i -H "Content-Type: application/json" -X POST -d '"off"' http://10.37.37.13:6661/power + HTTP/1.0 200 OK + Content-Type: application/json + Content-Length: 21 + Server: Werkzeug/0.9.6 Python/2.7.9 + Date: Mon, 09 Nov 2015 18:54:03 GMT + + { + "success": true + } + + # Check valid input sources + $ curl http://10.37.37.13:6661/source + { + "sources": [ + "PC", + "HDMI1", + "Component - YCbCr", + "HDMI2", + "Component - YPbPr", + "Video", + "S-Video", + "Component", + "Component - Auto", + "RCA" + ] + } + + # Set input source + $ curl -i -H "Content-Type: application/json" -X POST -d '"HDMI1"' http://10.37.37.13:6661/source + HTTP/1.0 200 OK + Content-Type: application/json + Content-Length: 21 + Server: Werkzeug/0.9.6 Python/2.7.9 + Date: Mon, 09 Nov 2015 18:54:03 GMT + + { + "success": true + } diff --git a/service.projcontrol/addon.xml b/service.projcontrol/addon.xml new file mode 100644 index 0000000000..1d20d7a23a --- /dev/null +++ b/service.projcontrol/addon.xml @@ -0,0 +1,56 @@ + + + + + + + + + + all + Control your projector from Kodi + Hantera din projektor med Kodi + Styr prosjektøren din med Kodi + Kontrolliere deinen Beamer mit Kodi + From Kodi, control a projector connected via a serial port. See https://github.com/fredrik-eriksson/kodi_projcontrol for supported projectors and features. + Hantera en projektor ansluten via serieport, via Kodi. Se https://github.com/fredrik-eriksson/kodi_projcontrol (Endast på engelska) för vilka projektorer och funktioner som stöds. + Styr en prosjektør som er koblet til Kodi via seriellport. Se https://github.com/fredrik-eriksson/kodi_projcontrol (kun på engelsk) over oversikt over hvilke prosjektører og funksjoner som støttes. + Kontrolliere deinen mit einem seriellen Port verbundenen Beamer mit Kodi. Siehe unter https://github.com/fredrik-eriksson/kodi_projcontrol (nur in englisch) nach unterstützten Beamer und Funktionen. + + +v1.4.0 - 2022-11-17 +- Kodi 19 support +- Add support for BenQ projectors +- Add support for Acer projectors + +v1.3.0 - 2018-08-16 +- replaced flask dependency with bottle +- some restructuring +- other changes required for inclusion in official Kodi repository + +v1.2.0 - 2018-07-09 +- Made strings localizeable (is that a word?) and added Swedish translation +- Made API service optional +- Add support to turn on/off projector at Kodi startup and shutdown +- Add support to turn on/off projector at screensaver (de)activation +- Add support for InFocus projectors IN72/IN74/IN76 + +v1.1.0 - 2015-11-09 +- Replace twisted with flask +- Use py-serial to configure serial port +- fixed regular library updates + +v1.0.0 - 2015-06-27 +- Initial release + + BSD-3-Clause + https://github.com/fredrik-eriksson/kodi_projcontrol + + icon.png + + + + diff --git a/service.projcontrol/icon.png b/service.projcontrol/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..96d8910d9fddcc7b6f3f800c1e74fefdca2d3fa4 GIT binary patch literal 37366 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� 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 wX=Zrk07*qoM6N<$f~u)^2><{9 literal 0 HcmV?d00001 diff --git a/service.projcontrol/icon_license.txt b/service.projcontrol/icon_license.txt new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/service.projcontrol/icon_license.txt @@ -0,0 +1,201 @@ + 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. diff --git a/service.projcontrol/lib/__init__.py b/service.projcontrol/lib/__init__.py new file mode 100644 index 0000000000..faeb592851 --- /dev/null +++ b/service.projcontrol/lib/__init__.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015 Fredrik Eriksson +# This file is covered by the BSD-3-Clause license, read LICENSE for details. + +CMD_PWR_ON="poweron" +CMD_PWR_OFF="poweroff" +CMD_PWR_QUERY="powerquery" + +CMD_SRC_QUERY="sourcequery" +CMD_SRC_SET="sourceset" + +CMD_BRT_QUERY="brightnessquery" +CMD_BRT_SET="brightnessset" + diff --git a/service.projcontrol/lib/acer.py b/service.projcontrol/lib/acer.py new file mode 100644 index 0000000000..70bb79043e --- /dev/null +++ b/service.projcontrol/lib/acer.py @@ -0,0 +1,306 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015,2018 Fredrik Eriksson +# 2018 Petter Reinholdtsen +# 2022 Michael Spreng +# This file is covered by the MIT license, read LICENSE for details. + +"""Module for communicating with Acer projectors supporting RS232 +serial interface. + +Parameter Rs232 : 9600 / 8 / N / 1 + +1 OKOKOKOKOK\r Power On +2 * 0 IR 001\r Power On +3 * 0 IR 002\r Power Off +4 * 0 IR 004\r Keystone +5 * 0 IR 006\r Mute +6 * 0 IR 007\r Freeze +7 * 0 IR 008\r Menu +8 * 0 IR 009\r Up +9 * 0 IR 010\r Down +10 * 0 IR 011\r Right +11 * 0 IR 012\r Left +12 * 0 IR 013\r Enter +13 * 0 IR 014\r Re-Sync +14 * 0 IR 015\r Source Analog RGB for D-sub +15 * 0 IR 016\r Source Digital RGB +16 * 0 IR 017\r Source PbPr for D-sub +17 * 0 IR 018\r Source S-Video +18 * 0 IR 019\r Source Composite Video +19 * 0 IR 020\r Source Component Video +20 * 0 IR 021\r Aspect ratio 16:9 +21 * 0 IR 022\r Aspect ratio 4:3 +22 * 0 IR 023\r Volume + +23 * 0 IR 024\r Volume – +24 * 0 IR 025\r Brightness +25 * 0 IR 026\r Contrast +26 * 0 IR 027\r Color Temperature +27 * 0 IR 028\r Source Analog RGB for DVI Port +28 * 0 IR 029\r Source Analog YPbPr for DVI Port +29 * 0 IR 030\r Hide +30 * 0 IR 031\r Source +31 * 0 IR 032\r Video: Color saturation adjustment +32 * 0 IR 033\r Video: Hue adjustment +33 * 0 IR 034\r Video: Sharpness adjustment +34 * 0 IR 035\r Query Model name +35 * 0 IR 036\r Query Native display resolution +36 * 0 IR 037\r Query company name +37 * 0 IR 040\r Aspect ratioL.Box +38 * 0 IR 041\r Aspect ratio 1:1 +39 * 0 IR 042\r Keystone Up +40 * 0 IR 043\r Keystone Down +41 * 0 IR 044\r Keystone Left +42 * 0 IR 045\r Keystone Right +43 * 0 IR 046\r Zoom +44 * 0 IR 047\r e-Key +45 * 0 IR 048\r Color RGB +46 * 0 IR 049\r Language +47 * 0 IR 050\r Source HDMI + + * 0 Src ?\r Get current source +Answer: Src 0 no signal on currently selected input (does not tell which one is selected) + Src 1 VGA + Src 8 HDMI + + * 0 Lamp\r Lamp operation hours +Answer: 0001 four digit number (hours) + +OK: in case command was understood, projector answers with *000 +Error: in case of error, projector answers with *001 +""" + +import os +import time +import re +import select + +import serial + +import lib.commands +import lib.errors +from lib.helpers import log + +# List of all valid models and their input sources +# Remember to add new models to the settings.xml-file as well +_valid_sources_ = { + "generic/X1373WH": { + "VGA": ("Src 1", "015"), + "S-Video": ("Src ?", "018"), # don't know response + "Composite": ("Src ?", "019"), # don't know response + "HDMI": ("Src 8", "050"), + }, + "V7500": { + "VGA - RGB": ("Src 1", "015"), + "VGA - PbPr": ("Src ?", "017"), # don't know response + "Composite": ("Src ?", "019"), # don't know response + "Component": ("Src ?", "020"), # don't know response + "HDMI": ("Src 8", "050"), + } + } + +# map the generic commands to ESC/VP21 commands +_command_mapping_ = { + lib.CMD_PWR_ON: "* 0 IR 001", + lib.CMD_PWR_OFF: "* 0 IR 002", + lib.CMD_PWR_QUERY: "* 0 IR 037", + + lib.CMD_SRC_QUERY: "* 0 Src ?", + lib.CMD_SRC_SET: "* 0 IR {source_id}", + } + +_serial_options_ = { + "baudrate": 9600, + "bytesize": serial.EIGHTBITS, + "parity": serial.PARITY_NONE, + "stopbits": serial.STOPBITS_ONE +} + +def get_valid_sources(model): + """Return all valid source strings for this model""" + if model in _valid_sources_: + return list(_valid_sources_[model].keys()) + return None + +def get_serial_options(): + return _serial_options_ + +def get_source_id(model, source): + """Return the "real" source ID based on projector model and human readable + source string""" + if model in _valid_sources_ and source in _valid_sources_[model]: + return _valid_sources_[model][source] + return None + +class ProjectorInstance: + + def __init__(self, model, ser, timeout=5): + """Class for managing Acer projectors + + :param model: projector model + :param ser: open Serial port for the serial console + :param timeout: time to wait for response from projector + """ + self.serial = ser + self.timeout = timeout + self.model = model + res = self._verify_connection() + if not res: + raise lib.errors.ProjectorError( + "Could not verify ready-state of projector" + #"Verify returned {}".format(res) + ) + + + def _verify_connection(self): + """Verify that the projecor is ready to receive commands. Use the + name command to see if we get a valid response. + + """ + + return True + + # projector is quite slow to react to rs232 commands. + # no good command found for checking so often + + log("start verify with: '{}'".format(_command_mapping_[lib.CMD_PWR_QUERY])) + res = self._send_command(_command_mapping_[lib.CMD_PWR_QUERY], for_verify=True) + if res == "*001": + # in case the projector is off + return True + elif res: + # in case the projector is on, it will also send the name. Discard it: + self._read_response() + return True + return False + + def _read_response(self): + """Read response from projector""" + read = "" + res = "" + time.sleep(0.5) + while not read.endswith("\r"): + r, w, x = select.select([self.serial.fileno()], [], [], self.timeout) + if len(r) == 0: + raise lib.errors.ProjectorError( + "Timeout when reading response from projector" + ) + for f in r: + try: + read = os.read(f, 1).decode('utf-8') + res += read + except OSError as e: + raise lib.errors.ProjectorError( + "Error when reading response from projector: {}".format(e), + ) + return None + + part = res.strip('\r') + log("projector responded: '{}'".format(part)) + return part + + + def _send_command(self, cmd_str, for_verify = False): + """Send command to the projector. + + :param cmd_str: Full raw command string to send to the projector + """ + log("sending command '{}'".format(cmd_str)) + ret = None + try: + self.serial.write("{}\r\n".format(cmd_str).encode('utf-8')) + except OSError as e: + raise lib.errors.ProjectorError( + "Error when Sending command '{}' to projector: {}".\ + format(cmd_str, e) + ) + return ret + + ret = self._read_response() + + if for_verify: + return ret + else: + return ret == "*000" + + def _power_on(self): + if self._power_query(): + log("PWR_ON: Projector already turned on") + return True + else: + res = self._send_command("* 0 IR 001") + # wait 10 seconds. The projector needs some time to start up. + # For some time commands are ignored and it acts like it is still off. + # So wait long enough until things ares settled. + time.sleep(10) + return res + + def _power_off(self): + return self._send_command("* 0 IR 002") + + def _power_query(self): + res = self._send_command("* 0 IR 037") + # If turned on, projector returns Name Acer. Consume that part as well. + if res: + self._read_response() + return res + + def _source_query(self): + res = self._send_command("* 0 Src ?") + if not res: + raise lib.errors.InvalidCommandError("Get source command failed") + res = self._read_response() + log("query source returned {}".format(res)) + return res + + def _source_set(self, source_id): + source = self._source_query() + if source == source_id[0]: + log("SRC_SET: Correct source already set") + return True + cmd_str = "* 0 IR {}".format(source_id[1]) + res = self._send_command(cmd_str) + # Switching the source takes quite some time. During that time + # the source_query command returns "Src 0" for no signal. + # Wait long enough, so it will return the correct source after + # this command has completed + time.sleep(10) + # for debugging: check which source is now active + self._source_query() + return res + + def send_command(self, command, source_id = "undefined", **kwargs): + """Send command to the projector. + + :param command: A valid command from lib + :param **kwargs: Optional parameters to the command. For Acer the + valid keyword is "source_id" on CMD_SRC_SET + + :return: True or False on CMD_PWR_QUERY, a source string on + CMD_SRC_QUER, otherwise None. + """ + + if command == lib.CMD_PWR_ON: + res = self._power_on() + elif command == lib.CMD_PWR_OFF: + res = self._power_off() + elif command == lib.CMD_PWR_QUERY: + res = self._power_query() + elif command == lib.CMD_SRC_SET: + res = self._source_set(source_id) + elif command == lib.CMD_SRC_QUERY: + internal = self._source_query() + res = "" + for source in _valid_sources_[self.model]: + if _valid_sources_[self.model][source][0] == internal: + res = source + break + if res == "": + raise lib.errors.InvalidCommandError( + "Command get source returned unexpected result {}".format(internal) + ) + else: + raise lib.errors.InvalidCommandError( + "Command {} not supported".format(command) + ) + + return res diff --git a/service.projcontrol/lib/benq.py b/service.projcontrol/lib/benq.py new file mode 100644 index 0000000000..dd3965e0f4 --- /dev/null +++ b/service.projcontrol/lib/benq.py @@ -0,0 +1,183 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015,2018 Fredrik Eriksson +# 2018 Petter Reinholdtsen +# This file is covered by the MIT license, read LICENSE for details. + +"""Module for communicating with BenQ projectors supporting RS232 +serial interface. + +Protocol description fetched on 2020-07-04 from +https://benqesupport.blob.core.windows.net/esupport/Projector/Control%20Protocols/MH535/RS232%20Control%20Guide_0_Windows10_Windows7_Windows8.pdf + +""" + +import os +import re +import select + +import serial + +import lib.commands +import lib.errors +from lib.helpers import log + +# List of all valid models and their input sources +# Remember to add new models to the settings.xml-file as well +_valid_sources_ = { + "M535 series": { + "COMPUTER/YPbPr": "RGB", + "COMPUTER 2/YPbPr2": "RGB2", + "HDMI(MHL)": "hdmi", + "HDMI 2(MHL2)": "hdmi2", + "Composite": "vid", + "S-Video": "svid", + } + } + +# map the generic commands to ESC/VP21 commands +_command_mapping_ = { + lib.CMD_PWR_ON: "*pow=on#", + lib.CMD_PWR_OFF: "*pow=off#", + lib.CMD_PWR_QUERY: "*pow=?#", + + lib.CMD_SRC_QUERY: "*sour=?#", + lib.CMD_SRC_SET: "*sour={source_id}#", + } + +_serial_options_ = { + "baudrate": 115200, + "bytesize": serial.EIGHTBITS, + "parity": serial.PARITY_NONE, + "stopbits": serial.STOPBITS_ONE +} + +def get_valid_sources(model): + """Return all valid source strings for this model""" + if model in _valid_sources_: + return list(_valid_sources_[model].keys()) + return None + +def get_serial_options(): + return _serial_options_ + +def get_source_id(model, source): + """Return the "real" source ID based on projector model and human readable + source string""" + if model in _valid_sources_ and source in _valid_sources_[model]: + return _valid_sources_[model][source] + return None + +class ProjectorInstance: + + def __init__(self, model, ser, timeout=5): + """Class for managing BenQ projectors + + :param model: BenQ model + :param ser: open Serial port for the serial console + :param timeout: time to wait for response from projector + """ + self.serial = ser + self.timeout = timeout + self.model = model + res = self._verify_connection() + if not res: + raise lib.errors.ProjectorError( + "Could not verify ready-state of projector" + #"Verify returned {}".format(res) + ) + + + def _verify_connection(self): + """Verify that the projecor is ready to receive commands. Use the + *pow=?# command to see if we get a valid response. + """ + res = self._send_command("*pow=?#") + return res is not None + + def _read_response(self): + """Read response from projector""" + read = "" + res = "" + # Match either *pow=off# or *pow=on# + while not re.match(r'>', res): + r, w, x = select.select([self.serial.fileno()], [], [], self.timeout) + if len(r) == 0: + raise lib.errors.ProjectorError( + "Timeout when reading response from projector" + ) + for f in r: + try: + read = os.read(f, 256).decode('utf-8') + res += read + except OSError as e: + raise lib.errors.ProjectorError( + "Error when reading response from projector: {}".format(e), + ) + return None + + part = res.split('\r', 1) + log("projector responded: '{}'".format(part[1])) + return part[0] + + + def _send_command(self, cmd_str): + """Send command to the projector. + + :param cmd_str: Full raw command string to send to the projector + """ + ret = None + try: + self.serial.write("\r{}\r".format(cmd_str).encode('utf-8')) + except OSError as e: + raise lib.errors.ProjectorError( + "Error when Sending command '{}' to projector: {}".\ + format(cmd_str, e) + ) + return ret + + if cmd_str.endswith('?#'): + ret = self._read_response() + if ret == 'Illegal format': + log("Projector responded with Error!") + return None + log("Command sent successfully") + ret = ret.split('=', 1)[1] + if ret == "on#": + ret = True + elif ret == "off#": + ret = False + elif ret in [ + _valid_sources_[self.model][x] for x in + _valid_sources_[self.model] + ]: + ret = [ + x for x in + _valid_sources_[self.model] if + _valid_sources_[self.model][x] == ret][0] + + return ret + + def send_command(self, command, **kwargs): + """Send command to the projector. + + :param command: A valid command from lib + :param **kwargs: Optional parameters to the command. For BenQ the + valid keyword is "source_id" on CMD_SRC_SET + + :return: True or False on CMD_PWR_QUERY, a source string on + CMD_SRC_QUERY, otherwise None. + """ + if not command in _command_mapping_: + raise lib.errors.InvalidCommandError( + "Command {} not supported".format(command) + ) + + if command == lib.CMD_SRC_SET: + cmd_str = _command_mapping_[command].format(**kwargs) + else: + cmd_str = _command_mapping_[command] + + log("sending command '{}'".format(cmd_str)) + res = self._send_command(cmd_str) + log("send_command returned {}".format(res)) + return res diff --git a/service.projcontrol/lib/commands.py b/service.projcontrol/lib/commands.py new file mode 100644 index 0000000000..592cf92722 --- /dev/null +++ b/service.projcontrol/lib/commands.py @@ -0,0 +1,158 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015,2018 Fredrik Eriksson +# This file is covered by the BSD-3-Clause license, read LICENSE for details. + +"""High level commands that can be used on the projectors""" +import threading +import os + +import serial + +import xbmc +import xbmcaddon + +import lib +import lib.epson +import lib.infocus +import lib.benq +import lib.acer +import lib.errors +import lib.helpers + +__addon__ = xbmcaddon.Addon() +__cmd_lock__ = threading.Lock() + +def _get_proj_module_(): + manufacturer = __addon__.getSetting("manufacturer") + if manufacturer == "Epson": + return lib.epson + if manufacturer == "InFocus": + return lib.infocus + if manufacturer == "BenQ": + return lib.benq + if manufacturer == "Acer": + return lib.acer + else: + raise lib.errors.ConfigurationError("Manufacturer {} is not supported".format(manufacturer)) + +def _get_configured_model_(): + manufacturer = __addon__.getSetting("manufacturer") + if manufacturer == "Epson": + model = __addon__.getSetting("epson_model") + elif manufacturer == "InFocus": + model = __addon__.getSetting("infocus_model") + elif manufacturer == "BenQ": + model = __addon__.getSetting("benq_model") + elif manufacturer == "Acer": + model = __addon__.getSetting("acer_model") + else: + raise lib.errors.ConfigurationError("Manufacturer {} is not supported".format(manufacturer)) + return model + + +def open_proj(): + """Open the serial device, only intended to be used from do_cmd() + + :return: a file descriptor or None + """ + try: + mod = _get_proj_module_() + except lib.errors.ConfigurationError as e: + lib.helpers.display_error_message(32203) + return None + + kwargs = mod.get_serial_options() + + try: + s = serial.Serial( __addon__.getSetting("device"), **kwargs) + return s + except (OSError, serial.SerialException) as e: + lib.helpers.display_error_message(32204) + return None + +def do_cmd(command, **kwargs): + """Execute a command to the projector and return any output. + + :param command: one of the commands from lib + :param **kwargs: optional arguments to command + + :return: output from projector or None + """ + res = None + with __cmd_lock__: + ser = open_proj() + if ser: + try: + mod = _get_proj_module_() + model = _get_configured_model_() + proj = mod.ProjectorInstance( + model, + ser, + int(__addon__.getSetting("timeout"))) + except lib.errors.ProjectorError as pe: + lib.helpers.display_error_message(32205) + lib.helpers.log("Failed to open projector: {}".format(pe)) + ser.close() + return res + + try: + res = proj.send_command(command, **kwargs) + except lib.errors.ProjectorError as pe: + lib.helpers.display_error_message(32206) + lib.helpers.log("Failed to send command to projector: {}".format(pe)) + ser.close() + lib.helpers.log("do_cmd returns: {}".format(res)) + return res + +def start(): + """Start the projector""" + do_cmd(lib.CMD_PWR_ON) + if __addon__.getSetting("set_input") == "true": + set_source(__addon__.getSetting("input_source")) + +def stop(final_shutdown=False): + """Shut down the projector""" + do_cmd(lib.CMD_PWR_OFF) + if __addon__.getSetting("lib_update") == "true" and not final_shutdown: + if __addon__.getSetting("update_music") == "true": + xbmc.executebuiltin('UpdateLibrary(music)') + if __addon__.getSetting("update_video") == "true": + xbmc.executebuiltin('UpdateLibrary(video)') + +def toggle_power(): + """Toggle the power to the projector""" + if do_cmd(lib.CMD_PWR_QUERY): + stop() + else: + start() + +def report(): + """Report current power status and used source. + + :return: a dict containing 'power' and 'source' entries. + """ + + pwr = do_cmd(lib.CMD_PWR_QUERY) + src = do_cmd(lib.CMD_SRC_QUERY) + return {"power": pwr, "source": src} + +def set_source(source): + """Set input source for projector. To get a list of valid source strings, + use GET on /source or call get_available_sources(). + + :param source: valid input source string + """ + mod = _get_proj_module_() + model = _get_configured_model_() + src_id = mod.get_source_id(model, source) + if not src_id: + lib.helpers.display_error_message(32207, ": {}".format(source)) + return False + do_cmd(lib.CMD_SRC_SET, source_id=src_id) + return True + +def get_available_sources(): + """Return a list valid sources for the configured projector.""" + mod = _get_proj_module_() + model = _get_configured_model_() + return mod.get_valid_sources(model) diff --git a/service.projcontrol/lib/epson.py b/service.projcontrol/lib/epson.py new file mode 100644 index 0000000000..1a0b6a5589 --- /dev/null +++ b/service.projcontrol/lib/epson.py @@ -0,0 +1,208 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015,2018 Fredrik Eriksson +# This file is covered by the BSD-3-Clause license, read LICENSE for details. + +"""Module for communicating with Epson projectors supporting the ESC/VP21 +protocol over RS232 serial interface. + +Protocol description fetched on 2015-06-26 from +https://files.support.epson.com/pdf/pltw1_/pltw1_cm.pdf +""" + +import os +import select + +import serial + +import lib.commands +import lib.errors + +from lib.helpers import log + +# List of all valid models and their input sources +# Remember to add new models to the settings.xml-file as well +_valid_sources_ = { + "TW3200": { + "Component": "10", + "Component - YCbCr": "14", + "Component - YPbPr": "15", + "Component - Auto": "1F", + "PC": "20", + "HDMI1": "30", + "HDMI2": "A0", + "Video": "40", + "RCA": "41", + "S-Video": "42" + }, + "PowerLite 820p": { + "Computer1/Analog RGB": "11", + "Computer1/Digital RGB": "12", + "Computer1/RGB-Video": "13", + "Computer2/Component - Analog RGB": "21", + "Computer2/Component - RGB-Video": "22", + "Computer2/Component - YCbCr": "23", + "Computer2/Component - YPbPr": "24", + "Video": "41", + "S-Video": "42", + } + } + +# map the generic commands to ESC/VP21 commands +_command_mapping_ = { + lib.CMD_PWR_ON: "PWR ON", + lib.CMD_PWR_OFF: "PWR OFF", + lib.CMD_PWR_QUERY: "PWR?", + + lib.CMD_SRC_QUERY: "SOURCE?", + lib.CMD_SRC_SET: "SOURCE {source_id}" + } + +_serial_options_ = { + "baudrate": 9600, + "bytesize": serial.EIGHTBITS, + "parity": serial.PARITY_NONE, + "stopbits": serial.STOPBITS_ONE +} + +def get_valid_sources(model): + """Return all valid source strings for this model""" + if model in _valid_sources_: + return list(_valid_sources_[model].keys()) + return None + +def get_serial_options(): + return _serial_options_ + +def get_source_id(model, source): + """Return the "real" source ID based on projector model and human readable + source string""" + if model in _valid_sources_ and source in _valid_sources_[model]: + return _valid_sources_[model][source] + return None + +class ProjectorInstance: + + def __init__(self, model, ser, timeout=5): + """Class for managing Epson projectors + + :param model: Epson model + :param ser: open Serial port for the serial console + :param timeout: time to wait for response from projector + """ + self.serial = ser + self.timeout = timeout + self.model = model + res = self._verify_connection() + if not res: + raise lib.errors.ProjectorError( + "Could not verify ready-state of projector" + #"Verify returned {}".format(res) + ) + + + def _verify_connection(self): + """Verify that the projecor is ready to receive commands. The projector + is ready when it returns with a colon when sending carriage return to + it. + """ + self._send_command("\r") + res = "" + while res is not None: + res = self._read_response() + if res.endswith(":") : + return True + self._send_command("\r") + return False + + def _read_response(self): + """Read response from projector""" + read = "" + res = "" + while not read.endswith(":"): + r, w, x = select.select([self.serial.fileno()], [], [], self.timeout) + if len(r) == 0: + raise lib.errors.ProjectorError( + "Timeout when reading response from projector" + ) + for f in r: + try: + read = os.read(f, 256).decode('utf-8') + res += read + except OSError as e: + raise lib.errors.ProjectorError( + "Error when reading response from projector: {}".format(e), + ) + return None + + part = res.split('\r', 1) + log("projector responded: '{}'".format(part[0])) + return part[0] + + + def _send_command(self, cmd_str): + """Send command to the projector. + + :param cmd_str: Full raw command string to send to the projector + """ + ret = None + try: + self.serial.write("{}\r".format(cmd_str).encode('utf-8')) + except OSError as e: + raise lib.errors.ProjectorError( + "Error when Sending command '{}' to projector: {}".\ + format(cmd_str, e) + ) + return ret + + if cmd_str.endswith('?'): + ret = self._read_response() + while "=" not in ret and ret != 'ERR': + ret = self._read_response() + if ret == 'ERR': + log("Projector responded with Error!") + return None + log("Command sent successfully") + ret = ret.split('=', 1)[1] + if ret == "01": + ret = True + elif ret == "00": + ret = False + elif ret in [ + _valid_sources_[self.model][x] for x in + _valid_sources_[self.model] + ]: + ret = [ + x for x in + _valid_sources_[self.model] if + _valid_sources_[self.model][x] == ret][0] + + return ret + + def send_command(self, command, **kwargs): + """Send command to the projector. + + :param command: A valid command from lib + :param **kwargs: Optional parameters to the command. For Epson the only + valid keyword is "source_id" on CMD_SRC_SET. + + :return: True or False on CMD_PWR_QUERY, a source string on + CMD_SRC_QUERY, otherwise None. + """ + if not command in _command_mapping_: + raise lib.errors.InvalidCommandError( + "Command {} not supported".format(command) + ) + + if command == lib.CMD_SRC_SET: + cmd_str = _command_mapping_[command].format(**kwargs) + else: + cmd_str = _command_mapping_[command] + + log("sending command '{}'".format(cmd_str)) + res = self._send_command(cmd_str) + log("send_command returned {}".format(res)) + return res + + + + diff --git a/service.projcontrol/lib/errors.py b/service.projcontrol/lib/errors.py new file mode 100644 index 0000000000..3e175e1bcf --- /dev/null +++ b/service.projcontrol/lib/errors.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015,2018 Fredrik Eriksson +# This file is covered by the BSD-3-Clause license, read LICENSE for details. + +class ProjectorError(Exception): + """Exception for failures in projector communication""" + pass + +class InvalidCommandError(Exception): + """Exception for invalid input""" + pass + +class ConfigurationError(Exception): + """Exception for invalid configuration""" + pass diff --git a/service.projcontrol/lib/helpers.py b/service.projcontrol/lib/helpers.py new file mode 100644 index 0000000000..1e81e55568 --- /dev/null +++ b/service.projcontrol/lib/helpers.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015 Fredrik Eriksson +# This file is covered by the BSD-3-Clause license, read LICENSE for details. + +import xbmc +import xbmcaddon +import xbmcgui + +__addon__ = xbmcaddon.Addon() + +def display_error_message( + message_id, + append="", + title=__addon__.getLocalizedString(32100), + type_=xbmcgui.NOTIFICATION_ERROR, + time=1000, + sound=True): + """Display an error message in the Kodi interface""" + display_message( + message_id, + append, + title, + type_, + time, + sound) + +def display_message( + message_id, + append="", + title=__addon__.getLocalizedString(32101), + type_=xbmcgui.NOTIFICATION_INFO, + time=5000, + sound=False): + """Display an informational message in the Kodi interface""" + + dialog = xbmcgui.Dialog() + dialog.notification( + title, + "{}{}".format(__addon__.getLocalizedString(message_id), append), + type_, + time, + sound) + +def log(message): + xbmc.log("projcontrol: {}".format(message), level=xbmc.LOGDEBUG) + diff --git a/service.projcontrol/lib/infocus.py b/service.projcontrol/lib/infocus.py new file mode 100644 index 0000000000..7dd22f52c7 --- /dev/null +++ b/service.projcontrol/lib/infocus.py @@ -0,0 +1,202 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015,2018 Fredrik Eriksson +# 2018 Petter Reinholdtsen +# This file is covered by the MIT license, read LICENSE for details. + +"""Module for communicating with Infocus projectors supporting RS232 +serial interface. + +The protocol specification is in the appendix in the projector user +guide. + +""" + +import os +import re +import select + +import serial + +import lib.commands +import lib.errors +from lib.helpers import log + +# List of all valid models and their input sources +# Remember to add new models to the settings.xml-file as well +_valid_sources_ = { + "IN72/IN74/IN76": { + "HDMI": "0", + "MI-DA": "1", + "Component": "2", + "S-Video": "3", + "Composite": "4", + "SCART RGB": "5", + } + } + +# map the generic commands to ESC/VP21 commands +_command_mapping_ = { + lib.CMD_PWR_ON: "(PWR1)", + lib.CMD_PWR_OFF: "(PWR0)", + lib.CMD_PWR_QUERY: "(PWR?)", + + lib.CMD_SRC_QUERY: "(SRC?)", + lib.CMD_SRC_SET: "(SRC{source_id})", + + lib.CMD_BRT_QUERY: "(BRT?)", + lib.CMD_BRT_SET: "(BRT{level})", + } + +_boolean_commands = ( + '(ASC?)', + '(PWR?)', + ) + +_serial_options_ = { + "baudrate": 19200, + "bytesize": serial.EIGHTBITS, + "parity": serial.PARITY_NONE, + "stopbits": serial.STOPBITS_ONE +} + +def get_valid_sources(model): + """Return all valid source strings for this model""" + if model in _valid_sources_: + return list(_valid_sources_[model].keys()) + return None + +def get_serial_options(): + return _serial_options_ + +def get_source_id(model, source): + """Return the "real" source ID based on projector model and human readable + source string""" + if model in _valid_sources_ and source in _valid_sources_[model]: + return _valid_sources_[model][source] + return None + +class ProjectorInstance: + + def __init__(self, model, ser, timeout=5): + """Class for managing InFocus projectors + + :param model: InFocus model + :param ser: open Serial port for the serial console + :param timeout: time to wait for response from projector + """ + self.serial = ser + self.timeout = timeout + self.model = model + res = self._verify_connection() + if not res: + raise lib.errors.ProjectorError( + "Could not verify ready-state of projector" + #"Verify returned {}".format(res) + ) + + + def _verify_connection(self): + """Verify that the projecor is ready to receive commands. Use the + (LMP?) command to see if we get a valid response. + + """ + res = self._send_command("(LMP?)") + return res is not None + + def _read_response(self): + """Read response from projector""" + read = "" + res = "" + # Match either (PWR0) or (LMP?)(0-65535,2344) + while not re.match(r'(\([^?]*\)|\(.*\?\)\([-0-9]*,[0-9]*\))', res): + r, w, x = select.select([self.serial.fileno()], [], [], self.timeout) + if len(r) == 0: + raise lib.errors.ProjectorError( + "Timeout when reading response from projector" + ) + for f in r: + try: + read = os.read(f, 256).decode('utf-8') + res += read + except OSError as e: + raise lib.errors.ProjectorError( + "Error when reading response from projector: {}".format(e), + ) + return None + + part = res.split('\n', 1) + log("projector responded: '{}'".format(part[0])) + return part[0] + + + def _send_command(self, cmd_str): + """Send command to the projector. + + :param cmd_str: Full raw command string to send to the projector + """ + ret = None + try: + self.serial.write("{}\n".format(cmd_str).encode('utf-8')) + except OSError as e: + raise lib.errors.ProjectorError( + "Error when Sending command '{}' to projector: {}".\ + format(cmd_str, e) + ) + return ret + + ret = self._read_response() + while ")" not in ret and ret != '?': + ret = self._read_response() + if ret == '?': + log("Error, command not understood by projector!") + return None + log("Command sent successfully!") + if cmd_str.endswith('?)'): + r = re.match('\(.+\)\(([-\d]+),(\d+)\)', ret) + ret = r.group(2) + if cmd_str in _boolean_commands: + if int(ret) == 1: + ret = True + elif int(ret) == 0: + ret = False + else: + log("Error, unable to parse boolean value!") + return None + elif ret in [ + _valid_sources_[self.model][x] for x in + _valid_sources_[self.model] + ]: + ret = [ + x for x in + _valid_sources_[self.model] if + _valid_sources_[self.model][x] == ret][0] + + return ret + else: + return None + + def send_command(self, command, **kwargs): + """Send command to the projector. + + :param command: A valid command from lib + :param **kwargs: Optional parameters to the command. For InFocus the + valid keyword is "source_id" on CMD_SRC_SET and + "level" on CMD_BRT_SET. + + :return: True or False on CMD_PWR_QUERY, a source string on + CMD_SRC_QUERY, an integer on CMD_BRT_QUERY, otherwise None. + """ + if not command in _command_mapping_: + raise lib.errors.InvalidCommandError( + "Command {} not supported".format(command) + ) + + if command == lib.CMD_SRC_SET or command == lib.CMD_BRT_SET: + cmd_str = _command_mapping_[command].format(**kwargs) + else: + cmd_str = _command_mapping_[command] + + log("sending command '{}'".format(cmd_str)) + res = self._send_command(cmd_str) + log("send_command returned {}".format(res)) + return res diff --git a/service.projcontrol/lib/monitor.py b/service.projcontrol/lib/monitor.py new file mode 100644 index 0000000000..a66eeafef9 --- /dev/null +++ b/service.projcontrol/lib/monitor.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2018 Fredrik Eriksson +# This file is covered by the BSD-3-Clause license, read LICENSE for details. + +import datetime +import threading + +import xbmc + +import lib.commands +import lib.helpers +import lib.service + +class ProjectorMonitor(xbmc.Monitor): + """Subclass of xbmc.Monitor that restarts the twisted web server on + configuration changes, and starting library updates if configured. + """ + + def __init__(self, *args, **kwargs): + self._update_lock_ = threading.Lock() + self._ongoing_updates_ = set() + self._update_timer_ = None + self._ss_activation_timer_ = None + self._last_power_command_ = datetime.datetime.fromtimestamp(0) + self._addon_ = lib.service.refresh_addon() + if self._addon_.getSetting("at_start") == "true": + lib.commands.start() + + + def update_libraries(self): + """Called by the timer to start a new library update if the projector + is still offline and configuration is set to allow regular library + updates. + """ + power_status = lib.commands.report()["power"] + if not power_status \ + and self._addon_.getSetting("lib_update") == "true" \ + and self._addon_.getSetting("update_again") == "true": + if self._addon_.getSetting("update_music") == "true": + xbmc.executebuiltin('UpdateLibrary(music)') + if self._addon_.getSetting("update_video") == "true": + xbmc.executebuiltin('UpdateLibrary(video)') + + def cleanup(self): + """Remove any lingering timer before exit""" + if self._ss_activation_timer_: + self._ss_activation_timer_.cancel() + + with self._update_lock_: + if self._update_timer_: + self._update_timer_.cancel() + self._update_timer_ = None + + def onScreensaverActivated(self): + if self._addon_.getSetting("at_ss_start") == "true": + delay = int(self._addon_.getSetting("at_ss_start_delay")) + lib.helpers.log("Screensaver activated, scheduling projector shutdown") + self._ss_activation_timer_ = threading.Timer(delay, lib.commands.stop) + self._last_power_command_ = datetime.datetime.now() + datetime.timedelta(seconds=delay) + self._ss_activation_timer_.start() + + def onScreensaverDeactivated(self): + if self._ss_activation_timer_: + lib.helpers.log("Screensaver deactivated, aborting any scheduled projector shutdown") + self._ss_activation_timer_.cancel() + + if self._addon_.getSetting("at_ss_shutdown") == "true": + min_turnaround = int(self._addon_.getSetting("min_turnaround")) + time_since_stop = datetime.datetime.now() - self._last_power_command_ + if time_since_stop.days == 0 and time_since_stop.seconds < min_turnaround: + lib.helpers.log("Screensaver deactivated too soon, will sleep a while before starting projector") + self.waitForAbort(min_turnaround-time_since_stop.seconds) + lib.helpers.log("Screensaver deactivated, starting projector") + lib.commands.start() + + def onSettingsChanged(self): + self._addon_ = lib.service.refresh_addon() + + if self._addon_.getSetting("enabled") == "true": + lib.service.restart_server() + else: + lib.service.stop_server() + + def onCleanStarted(self, library): + self.onScanStarted(library) + + def onCleanFinished(self, library): + self.onScanFinished(library) + + def onScanStarted(self, library): + self.cleanup() + with self._update_lock_: + self._ongoing_updates_.add(library) + return library + + def onScanFinished(self, library): + self.cleanup() + with self._update_lock_: + self._ongoing_updates_.discard(library) + if len(self._ongoing_updates_) == 0 \ + and self._addon_.getSetting("lib_update") == "true" \ + and self._addon_.getSetting("update_again") == "true": + self._update_timer_ = threading.Timer( + int(self._addon_.getSetting("update_again_at"))*60, + self.update_libraries) + self._update_timer_.start() + return library + diff --git a/service.projcontrol/lib/server.py b/service.projcontrol/lib/server.py new file mode 100644 index 0000000000..b949339a06 --- /dev/null +++ b/service.projcontrol/lib/server.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015,2018 Fredrik Eriksson +# This file is covered by the BSD-3-Clause license, read LICENSE for details. + +import json +import logging + +import bottle +import wsgiref.simple_server + +import lib.commands + + +class StoppableWSGIRefServer(bottle.ServerAdapter): + server = None + + def run(self, handler): + self.server = wsgiref.simple_server.make_server(self.host, self.port, handler, **self.options) + self.server.serve_forever() + + def stop(self): + self.server.shutdown() + + +app = bottle.Bottle() + +@app.get('/') +def start(): + bottle.response.content_type = "application/json" + return json.dumps([ "power", "source"]) + +@app.get('/power') +def power(): + bottle.response.content_type = "application/json" + return json.dumps(lib.commands.report()) + +@app.post('/power') +def power_req(): + bottle.response.content_type = "application/json" + ret = {'success': False} + + try: + data = bottle.request.json + except ValueError as e: + return json.dumps(ret) + + if data == 'on': + lib.commands.start() + ret['success'] = True + elif data == 'off': + lib.commands.stop() + ret['success'] = True + elif data == 'toggle': + lib.commands.toggle_power() + ret['success'] = True + return json.dumps(ret) + +@app.get('/source') +def source(): + bottle.response.content_type = "application/json" + valid_sources = lib.commands.get_available_sources() + return json.dumps({'sources': valid_sources}) + +@app.post('/source') +def source_req(): + bottle.response.content_type = "application/json" + valid_sources = lib.commands.get_available_sources() + ret = {'success': False} + try: + data = bottle.request.json + except ValueError as e: + return json.dumps(ret) + + if data in valid_sources: + ret['success'] = lib.commands.set_source(data) + return json.dumps(ret) + +_server_ = None + +def init_server(port, address): + """Start the bottle web server. + + :param port: port to listen on + :param address: address to bind to + """ + global _server_ + if _server_: + stop_server() + + _server_ = StoppableWSGIRefServer(host=address, port=port) + app.run(server=_server_) + +def stop_server(): + _server_.stop() diff --git a/service.projcontrol/lib/service.py b/service.projcontrol/lib/service.py new file mode 100644 index 0000000000..bbe90fe1c3 --- /dev/null +++ b/service.projcontrol/lib/service.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2018 Fredrik Eriksson +# This file is covered by the BSD-3-Clause license, read LICENSE for details. + +import threading + +import xbmc +import xbmcaddon + +import lib.helpers +import lib.monitor + +try: + import lib.server + __server_available__ = True +except ImportError: + __server_available__ = False + +__addon__ = xbmcaddon.Addon() +__server__ = None + + +def server_available(): + if not __server_available__ and __addon__.getSetting("enabled") == "true": + lib.helpers.display_error_message(32200) + return __server_available__ + +def restart_server(): + """Restart the REST API. + """ + if not server_available(): + return + + global __server__ + stop_server() + + if __addon__.getSetting("enabled") != "true": + return + + + port = int(__addon__.getSetting("port")) + address = __addon__.getSetting("address") + __server__ = threading.Thread(target=lib.server.init_server, args=(port, address)) + __server__.start() + # wait one second and make sure the server has started + xbmc.sleep(1000) + if not __server__.is_alive(): + __server__.join() + lib.helpers.display_error_message(32201) + __server__ = None + else: + lib.helpers.display_message(32300, " {}:{}".format(address,port)) + +def refresh_addon(): + global __addon__ + __addon__ = xbmcaddon.Addon() + return __addon__ + +def stop_server(): + """Stop the REST API.""" + if not server_available(): + return + + global __server__ + if __server__: + lib.server.stop_server() + __server__.join() + lib.helpers.display_message(32301) + __server__ = None + +def run(): + monitor = lib.monitor.ProjectorMonitor() + restart_server() + + monitor.waitForAbort() + + lib.helpers.log("Shutting down addon") + stop_server() + monitor.cleanup() + if __addon__.getSetting("at_shutdown") == "true": + lib.commands.stop(final_shutdown=True) diff --git a/service.projcontrol/resources/language/resource.language.de_de/strings.po b/service.projcontrol/resources/language/resource.language.de_de/strings.po new file mode 100644 index 0000000000..add99196cf --- /dev/null +++ b/service.projcontrol/resources/language/resource.language.de_de/strings.po @@ -0,0 +1,174 @@ +# Kodi Media Center language file +# Addon Name: Projector Control +# Addon id: service.projcontrol +# Addon Provider: feffe +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: English (http://www.transifex.com/projects/p/xbmc-addons/language/en/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: en\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + + +# Configuration options 32001-32099 + +msgctxt "#32001" +msgid "Projector Details" +msgstr "Beamerdetails" + +msgctxt "#32002" +msgid "Serial Device" +msgstr "Serielles Gerät" + +msgctxt "#32003" +msgid "Response Read Timeout" +msgstr "Antwortzeitüberschreitung" + +msgctxt "#32004" +msgid "Manufacturer" +msgstr "Hersteller" + +msgctxt "#32005" +msgid "Model" +msgstr "Modell" + +# empty strings #32006-32009 + +msgctxt "#32010" +msgid "Projector Control" +msgstr "Beamersteuerung" + +msgctxt "#32011" +msgid "Start projector when Kodi starts" +msgstr "Starte Beamer wenn Kodi startet" + +msgctxt "#32012" +msgid "Shutdown projector when Kodi exits" +msgstr "Fahre Beamer herunter bei verlassen von Kodi" + +msgctxt "#32013" +msgid "Shutdown projector when screensaver activates" +msgstr "Fahre Beamer herunter wenn Bildschirmschoner aktiviert wird" + +msgctxt "#32014" +msgid "Delay after screensaver activation before projector shutdown" +msgstr "Verzögerung nach Bildschirmschoneraktivierung bevor Beamer herunterfährt" + +msgctxt "#32015" +msgid "Start projector when screensaver deactivates" +msgstr "Starte Beamer wenn Bildschirmschoner deaktiviert wird" + +msgctxt "#32016" +msgid "Minimum interval between projector shutdown and startup" +msgstr "Minimumintervall zwischen Beamerabschaltung und Hochfahren" + +msgctxt "#32017" +msgid "Set input source on projector after projector starts" +msgstr "Setze Eingangsquelle bei Beamer nachdem Beamer startet" + +msgctxt "#32018" +msgid "Input source" +msgstr "Eingangsquelle" + +# empty string #32019 + +msgctxt "#32020" +msgid "Library Updates" +msgstr "Bibliothekaktualisierungen" + +msgctxt "#32021" +msgid "Update library when projector shuts down" +msgstr "Aktualisiere Bibliothek wenn Beamer herunterfährt" + +msgctxt "#32022" +msgid "Update Music Library" +msgstr "Aktualisiere Musikbibliothek" + +msgctxt "#32023" +msgid "Update Video Library" +msgstr "Aktualisiere Videobibliothek" + +msgctxt "#32024" +msgid "Regular updates" +msgstr "Regelmässige Aktualisierungen" + +msgctxt "#32025" +msgid "Update every (Minutes)" +msgstr "Aktualisiere jede (Minuten)" + +# empty strings #32026-32029 + +msgctxt "#32030" +msgid "REST API" +msgstr "REST API" + +msgctxt "#32031" +msgid "Enable REST API" +msgstr "Aktiviere REST API" + +msgctxt "#32032" +msgid "Listen Address" +msgstr "Liste Adressen" + +msgctxt "#32033" +msgid "Listen Port" +msgstr "Liste Schnittstellen" + + + +msgctxt "#32100" +msgid "Projector Command Failed" +msgstr "Beamerbefehl fehlgeschlagen" + +msgctxt "#32101" +msgid "Report From Projector" +msgstr "Meldung von Beamer" + + +# Errors + +msgctxt "#32200" +msgid "REST API not available, see https://github.com/fredrik-eriksson/kodi_projcontrol for possible reasons" +msgstr "REST API nicht verfügbar, schaue unter https://github.com/fredrik-eriksson/kodi_projcontrol für mögliche Gründe" + +msgctxt "#32201" +msgid "Failed to start API web server, Try to disable and reenable addon" +msgstr "Start von API web server fehlgeschlagen, Versuche das Addon zu de- und reaktivieren" + +msgctxt "#32203" +msgid "Configuration error: unsupported manufacturer" +msgstr "Konfigurationsfehler: Nicht unterstützter Hersteller" + +msgctxt "#32204" +msgid "Failed to open projector serial device" +msgstr "Öffnen des seriellen Gerätes fehlgeschlagen" + +msgctxt "#32205" +msgid "Failed to send command to projector" +msgstr "Befehl zu Beamer senden fehlgeschlagen" + +msgctxt "#32206" +msgid "Error when sending command to projector" +msgstr "Fehler wenn Befehl zu Beamer gesendet wird" + +msgctxt "#32207" +msgid "Configuration error: invalid source for this model" +msgstr "Konfigurationsfehler: ungültige Quelle für dieses Modell" + + +# Informational + +msgctxt "#32300" +msgid "Started projector API at" +msgstr "Beamer API gestartet am" + +msgctxt "#32301" +msgid "Projector API stopped" +msgstr "Beamer API gestoppt" diff --git a/service.projcontrol/resources/language/resource.language.en_gb/strings.po b/service.projcontrol/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 0000000000..38a1633ada --- /dev/null +++ b/service.projcontrol/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,178 @@ +# Kodi Media Center language file +# Addon Name: Projector Control +# Addon id: service.projcontrol +# Addon Provider: feffe +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: English (http://www.transifex.com/projects/p/xbmc-addons/language/en/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: en\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + + +# Configuration options 32001-32099 + +msgctxt "#32001" +msgid "Projector Details" +msgstr "" + +msgctxt "#32002" +msgid "Serial Device" +msgstr "" + +msgctxt "#32003" +msgid "Response Read Timeout" +msgstr "" + +msgctxt "#32004" +msgid "Manufacturer" +msgstr "" + +msgctxt "#32005" +msgid "Model" +msgstr "" + +# empty strings #32006-32009 + +msgctxt "#32010" +msgid "Projector Control" +msgstr "" + +msgctxt "#32011" +msgid "Start projector when Kodi starts" +msgstr "" + +msgctxt "#32012" +msgid "Shutdown projector when Kodi exits" +msgstr "" + +msgctxt "#32013" +msgid "Shutdown projector when screensaver activates" +msgstr "" + +msgctxt "#32014" +msgid "Delay after screensaver activation before projector shutdown" +msgstr "" + +msgctxt "#32015" +msgid "Start projector when screensaver deactivates" +msgstr "" + +msgctxt "#32016" +msgid "Minimum interval between projector shutdown and startup" +msgstr "" + +msgctxt "#32017" +msgid "Set input source on projector after projector starts" +msgstr "" + +msgctxt "#32018" +msgid "Input source" +msgstr "" + +# empty string #32019 + +msgctxt "#32020" +msgid "Library Updates" +msgstr "" + +msgctxt "#32021" +msgid "Update library when projector shuts down" +msgstr "" + +msgctxt "#32022" +msgid "Update Music Library" +msgstr "" + +msgctxt "#32023" +msgid "Update Video Library" +msgstr "" + +msgctxt "#32024" +msgid "Regular updates" +msgstr "" + +msgctxt "#32025" +msgid "Update every (Minutes)" +msgstr "" + +# empty strings #32026-32029 + +msgctxt "#32030" +msgid "REST API" +msgstr "" + +msgctxt "#32031" +msgid "Enable REST API" +msgstr "" + +msgctxt "#32032" +msgid "Listen Address" +msgstr "" + +msgctxt "#32033" +msgid "Listen Port" +msgstr "" + + + +msgctxt "#32100" +msgid "Projector Command Failed" +msgstr "" + +msgctxt "#32101" +msgid "Report From Projector" +msgstr "" + + +# Errors + +msgctxt "#32200" +msgid "REST API not available, see https://github.com/fredrik-eriksson/kodi_projcontrol for possible reasons" +msgstr "" + +msgctxt "#32201" +msgid "Failed to start API web server, Try to disable and reenable addon" +msgstr "" + +msgctxt "#32202" +msgid "Failed to start projector web server, Try to disable and reenable addon" +msgstr "" + +msgctxt "#32203" +msgid "Configuration error: unsupported manufacturer" +msgstr "" + +msgctxt "#32204" +msgid "Failed to open projector serial device" +msgstr "" + +msgctxt "#32205" +msgid "Failed to send command to projector" +msgstr "" + +msgctxt "#32206" +msgid "Error when sending command to projector" +msgstr "" + +msgctxt "#32207" +msgid "Configuration error: invalid source for this model" +msgstr "" + + +# Informational + +msgctxt "#32300" +msgid "Started projector API at" +msgstr "" + +msgctxt "#32301" +msgid "Projector API stopped" +msgstr "" diff --git a/service.projcontrol/resources/language/resource.language.nb_no/strings.po b/service.projcontrol/resources/language/resource.language.nb_no/strings.po new file mode 100644 index 0000000000..e8cefb1e76 --- /dev/null +++ b/service.projcontrol/resources/language/resource.language.nb_no/strings.po @@ -0,0 +1,174 @@ +# Kodi Media Center language file +# Addon Name: Projector Control +# Addon id: service.projcontrol +# Addon Provider: feffe +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Petter Reinholdtsen\n" +"Language-Team: Norwegian Bokmål\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: nb\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + + +# Configuration options 32001-32099 + +msgctxt "#32001" +msgid "Projector Details" +msgstr "Prosjektøregenskaper" + +msgctxt "#32002" +msgid "Serial Device" +msgstr "Seriellport" + +msgctxt "#32003" +msgid "Response Read Timeout" +msgstr "Svarutløpstid" + +msgctxt "#32004" +msgid "Manufacturer" +msgstr "Produsent" + +msgctxt "#32005" +msgid "Model" +msgstr "Model" + +# empty strings #32006-32009 + +msgctxt "#32010" +msgid "Projector Control" +msgstr "Prosjektørhåndtering" + +msgctxt "#32011" +msgid "Start projector when Kodi starts" +msgstr "Start prosjektøren når Kodi starter" + +msgctxt "#32012" +msgid "Shutdown projector when Kodi exits" +msgstr "Slå av prosjektøren når Kodi avslutter" + +msgctxt "#32013" +msgid "Shutdown projector when screensaver activates" +msgstr "Slå av prosjektøren når skjermsparerer aktiveres" + +msgctxt "#32014" +msgid "Delay after screensaver activation before projector shutdown" +msgstr "Tidsforsinkelse før prosjektøren slås av når skjermspareren starter" + +msgctxt "#32015" +msgid "Start projector when screensaver deactivates" +msgstr "Start prosjektøren når skjermspareren avsluttes" + +msgctxt "#32016" +msgid "Minimum interval between projector shutdown and startup" +msgstr "Miste tidsintervall mellom prosjektavsetning og oppstart" + +msgctxt "#32017" +msgid "Set input source on projector after projector starts" +msgstr "Sett videokilde på prosjektøren når den startes" + +msgctxt "#32018" +msgid "Input source" +msgstr "Videokilde" + +# empty string #32019 + +msgctxt "#32020" +msgid "Library Updates" +msgstr "Mediebiblioteksoppdateringer" + +msgctxt "#32021" +msgid "Update library when projector shuts down" +msgstr "Oppdater mediebibliotek når prosjektøren slås av" + +msgctxt "#32022" +msgid "Update Music Library" +msgstr "Oppdater musikkbiblioteket" + +msgctxt "#32023" +msgid "Update Video Library" +msgstr "Oppdater videobiblioteket" + +msgctxt "#32024" +msgid "Regular updates" +msgstr "Repeterende oppdateringer" + +msgctxt "#32025" +msgid "Update every (Minutes)" +msgstr "Oppdater hver (minutter)" + +# empty strings #32026-32029 + +msgctxt "#32030" +msgid "REST API" +msgstr "REST API" + +msgctxt "#32031" +msgid "Enable REST API" +msgstr "Aktiver REST API" + +msgctxt "#32032" +msgid "Listen Address" +msgstr "Adress å lytte på" + +msgctxt "#32033" +msgid "Listen Port" +msgstr "Port å lytte på" + + + +msgctxt "#32100" +msgid "Projector Command Failed" +msgstr "Prosjektørkommando feilet" + +msgctxt "#32101" +msgid "Report From Projector" +msgstr "Rapport fra prosjektør" + + +# Errors + +msgctxt "#32200" +msgid "REST API not available, see https://github.com/fredrik-eriksson/kodi_projcontrol for possible reasons" +msgstr "REST-APIet er utilgjengelig, se https://github.com/fredrik-eriksson/kodi_projcontrol for mulige årsaker" + +msgctxt "#32201" +msgid "Failed to start API web server, Try to disable and reenable addon" +msgstr "Klarte ikke starte API-nettjeneren, forsøk å deaktivere og reaktivere tillegget" + +msgctxt "#32203" +msgid "Configuration error: unsupported manufacturer" +msgstr "Oppsettfeil: ukjent produsent" + +msgctxt "#32204" +msgid "Failed to open projector serial device" +msgstr "Klarte ikke åpne prosjektørens seriellport" + +msgctxt "#32205" +msgid "Failed to send command to projector" +msgstr "Klarte ikke sende kommandoer til prosjektøren" + +msgctxt "#32206" +msgid "Error when sending command to projector" +msgstr "Feil under sending av kommandoer til prosjektøren" + +msgctxt "#32207" +msgid "Configuration error: invalid source for this model" +msgstr "Oppsettfeil: ugyldig kilde for denne modellen" + + +# Informational + +msgctxt "#32300" +msgid "Started projector API at" +msgstr "Startet prosjektør-API på" + +msgctxt "#32301" +msgid "Projector API stopped" +msgstr "Stoppet prosjektør-API" diff --git a/service.projcontrol/resources/language/resource.language.sv_se/strings.po b/service.projcontrol/resources/language/resource.language.sv_se/strings.po new file mode 100644 index 0000000000..6d1973984e --- /dev/null +++ b/service.projcontrol/resources/language/resource.language.sv_se/strings.po @@ -0,0 +1,174 @@ +# Kodi Media Center language file +# Addon Name: Projector Control +# Addon id: service.projcontrol +# Addon Provider: feffe +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: English (http://www.transifex.com/projects/p/xbmc-addons/language/en/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: en\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + + +# Configuration options 32001-32099 + +msgctxt "#32001" +msgid "Projector Details" +msgstr "Projektoregenskaper" + +msgctxt "#32002" +msgid "Serial Device" +msgstr "Seriellport" + +msgctxt "#32003" +msgid "Response Read Timeout" +msgstr "Svarstimeout" + +msgctxt "#32004" +msgid "Manufacturer" +msgstr "Tillverkare" + +msgctxt "#32005" +msgid "Model" +msgstr "Model" + +# empty strings #32006-32009 + +msgctxt "#32010" +msgid "Projector Control" +msgstr "Projektorhantering" + +msgctxt "#32011" +msgid "Start projector when Kodi starts" +msgstr "Starta projektorn när Kodi startar" + +msgctxt "#32012" +msgid "Shutdown projector when Kodi exits" +msgstr "Stäng av projektorn när Kodi avslutas" + +msgctxt "#32013" +msgid "Shutdown projector when screensaver activates" +msgstr "Stäng av projektorn när skärmsläckaren aktiveras" + +msgctxt "#32014" +msgid "Delay after screensaver activation before projector shutdown" +msgstr "Fördröjning innan projektorn stängs av när skärmsläckaren startar" + +msgctxt "#32015" +msgid "Start projector when screensaver deactivates" +msgstr "Starta projektorn när skärmsläckaren avslutas" + +msgctxt "#32016" +msgid "Minimum interval between projector shutdown and startup" +msgstr "Minsta interval mellan projektoravstängning och uppstart" + +msgctxt "#32017" +msgid "Set input source on projector after projector starts" +msgstr "Set videokälla på projektorn när den startat" + +msgctxt "#32018" +msgid "Input source" +msgstr "Videokälla" + +# empty string #32019 + +msgctxt "#32020" +msgid "Library Updates" +msgstr "Uppdatering av mediabibliotek" + +msgctxt "#32021" +msgid "Update library when projector shuts down" +msgstr "Uppdatera mediabibliotek när projektorn stängs av" + +msgctxt "#32022" +msgid "Update Music Library" +msgstr "Uppdatera musikbiblioteket" + +msgctxt "#32023" +msgid "Update Video Library" +msgstr "Uppdatera videobiblioteket" + +msgctxt "#32024" +msgid "Regular updates" +msgstr "Återkommande uppdateringar" + +msgctxt "#32025" +msgid "Update every (Minutes)" +msgstr "Uppdatera varje (minuter)" + +# empty strings #32026-32029 + +msgctxt "#32030" +msgid "REST API" +msgstr "REST API" + +msgctxt "#32031" +msgid "Enable REST API" +msgstr "Aktivera REST API" + +msgctxt "#32032" +msgid "Listen Address" +msgstr "Adress att lyssna på" + +msgctxt "#32033" +msgid "Listen Port" +msgstr "Port att lyssna på" + + + +msgctxt "#32100" +msgid "Projector Command Failed" +msgstr "Projektorkommando misslyckades" + +msgctxt "#32101" +msgid "Report From Projector" +msgstr "Rapport från projektor" + + +# Errors + +msgctxt "#32200" +msgid "REST API not available, see https://github.com/fredrik-eriksson/kodi_projcontrol for possible reasons" +msgstr "Det går inte att aktivera REST API, se https://github.com/fredrik-eriksson/kodi_projcontrol för möjliga orsaker" + +msgctxt "#32201" +msgid "Failed to start API web server, Try to disable and reenable addon" +msgstr "Misslyckades med att starta API-server, prova att avaktivera och aktivera tillägget" + +msgctxt "#32203" +msgid "Configuration error: unsupported manufacturer" +msgstr "Konfigurationsfel: okänd tillverkare" + +msgctxt "#32204" +msgid "Failed to open projector serial device" +msgstr "Misslyckades med att öppna serieportsanslutningen" + +msgctxt "#32205" +msgid "Failed to send command to projector" +msgstr "Misslyckades med att skicka kommando till projektorn" + +msgctxt "#32206" +msgid "Error when sending command to projector" +msgstr "Fel i kommunikation med projektorn" + +msgctxt "#32207" +msgid "Configuration error: invalid source for this model" +msgstr "Konfigurationsfel: ogiltig källa för projektormodellen" + + +# Informational + +msgctxt "#32300" +msgid "Started projector API at" +msgstr "Projektor-API startat på" + +msgctxt "#32301" +msgid "Projector API stopped" +msgstr "Projektor-API avstängt" diff --git a/service.projcontrol/resources/settings.xml b/service.projcontrol/resources/settings.xml new file mode 100644 index 0000000000..6a2508e794 --- /dev/null +++ b/service.projcontrol/resources/settings.xml @@ -0,0 +1,257 @@ + + +
+ + + + 0 + /dev/ttyUSB0 + + 32002 + + + + 1 + 5 + + 32003 + + + + 0 + Epson + + + + + + + + + + 32004 + + + + 0 + TW3200 + + + + + + + + + Epson + + + + 32005 + + + + 0 + IN72/IN74/IN76 + + + + + + + + InFocus + + + + 32005 + + + + 0 + M535 series + + + + + + + + BenQ + + + + 32005 + + + + 0 + generic/X1373WH + + + + + + + + + Acer + + + + 32005 + + + + + + + + 0 + false + + + + 0 + false + + + + 0 + false + + + + 1 + 0 + + + true + + + + 32014 + + + + 0 + false + + + + 1 + 90 + + + true + + + + 32016 + + + + 0 + false + + + + 0 + HDMI1 + + + true + + + + 32018 + + + + + + + + 0 + false + + + + 0 + false + + + true + + + + + + 0 + false + + + true + + + + + + 0 + false + + + true + + + + + + 0 + 60 + + + true + + + + 32025 + + + + + + + + 2 + false + + + + 2 + 0.0.0.0 + + + true + + + + 32032 + + + + 2 + 6661 + + + true + + + + 32033 + + + + +
+
diff --git a/service.projcontrol/service.py b/service.projcontrol/service.py new file mode 100644 index 0000000000..f23e0a9d5e --- /dev/null +++ b/service.projcontrol/service.py @@ -0,0 +1,10 @@ +#!/usr/bin/python3 + +# -*- coding: utf-8 -*- +# Copyright (c) 2015,2018 Fredrik Eriksson +# This file is covered by the BSD-3-Clause license, read LICENSE for details. + +import lib.service + +if __name__ == '__main__': + lib.service.run()