diff --git a/service.projcontrol/LICENSE b/service.projcontrol/LICENSE new file mode 100644 index 000000000..1499e3f6c --- /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 000000000..79bdcfdf3 --- /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 000000000..9b2c4b2af --- /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 000000000..96d8910d9 Binary files /dev/null and b/service.projcontrol/icon.png differ diff --git a/service.projcontrol/icon_license.txt b/service.projcontrol/icon_license.txt new file mode 100644 index 000000000..261eeb9e9 --- /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 000000000..faeb59285 --- /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 000000000..70bb79043 --- /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 000000000..dd3965e0f --- /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 000000000..592cf9272 --- /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 000000000..1a0b6a558 --- /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 000000000..3e175e1bc --- /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 000000000..1e81e5556 --- /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 000000000..7dd22f52c --- /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 000000000..a66eeafef --- /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 000000000..b949339a0 --- /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 000000000..bbe90fe1c --- /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 000000000..add99196c --- /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 000000000..38a1633ad --- /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 000000000..e8cefb1e7 --- /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 000000000..6d1973984 --- /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 000000000..3ef7d3943 --- /dev/null +++ b/service.projcontrol/resources/settings.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/service.projcontrol/service.py b/service.projcontrol/service.py new file mode 100644 index 000000000..f23e0a9d5 --- /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()