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
+
+
+ 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()