From a99b5f1340172d122a8cfdc2c390e6e2bff48829 Mon Sep 17 00:00:00 2001 From: Dag Wieers Date: Thu, 21 Nov 2019 23:49:42 +0100 Subject: [PATCH] [screensaver.turnoff] v0.10.2 - Create separate entrypoint - Improve stability - Add more unit tests - Add sanity tests, unit tests and coverage support - Use JSON-RPC for all built-ins - Improvements for Python 3 - Support Odroid-C2 display method - Support RPi touchscreen display method - Improve mute and unmuting audio using JSON-RPC - Fix translations - Fix an issue when stopping the screensaver --- screensaver.turnoff/{LICENSE.txt => LICENSE} | 0 screensaver.turnoff/README.md | 83 +++- screensaver.turnoff/addon.xml | 50 ++- screensaver.turnoff/default.py | 8 + .../resource.language.en_gb/strings.po | 194 +++++---- screensaver.turnoff/resources/media/icon.png | Bin 0 -> 4593 bytes screensaver.turnoff/resources/settings.xml | 46 ++- .../720p/{screensaver-turnoff.xml => gui.xml} | 0 screensaver.turnoff/screensaver.py | 384 ++++++++++++------ 9 files changed, 541 insertions(+), 224 deletions(-) rename screensaver.turnoff/{LICENSE.txt => LICENSE} (100%) create mode 100644 screensaver.turnoff/default.py create mode 100644 screensaver.turnoff/resources/media/icon.png rename screensaver.turnoff/resources/skins/default/720p/{screensaver-turnoff.xml => gui.xml} (100%) diff --git a/screensaver.turnoff/LICENSE.txt b/screensaver.turnoff/LICENSE similarity index 100% rename from screensaver.turnoff/LICENSE.txt rename to screensaver.turnoff/LICENSE diff --git a/screensaver.turnoff/README.md b/screensaver.turnoff/README.md index f165479d8f..2397176499 100644 --- a/screensaver.turnoff/README.md +++ b/screensaver.turnoff/README.md @@ -1,3 +1,9 @@ +[![GitHub release](https://img.shields.io/github/release/dagwieers/screensaver.turnoff.svg)](https://github.com/dagwieers/screensaver.turnoff/releases) +[![Build Status](https://travis-ci.org/dagwieers/screensaver.turnoff.svg?branch=master)](https://travis-ci.org/dagwieers/screensaver.turnoff) +[![Codecov status](https://img.shields.io/codecov/c/github/dagwieers/screensaver.turnoff/master)](https://codecov.io/gh/dagwieers/screensaver.turnoff/branch/master) +[![License: GPLv2](https://img.shields.io/badge/License-GPLv2-yellow.svg)](https://opensource.org/licenses/GPL-2.0) +[![Contributors](https://img.shields.io/github/contributors/dagwieers/screensaver.turnoff.svg)](https://github.com/dagwieers/screensaver.turnoff/graphs/contributors) + # Kodi screensaver that turns your screen off to save power This Kodi screensaver turns your TV, projector or monitor off, so it can actually "save your screen". @@ -5,7 +11,7 @@ This Kodi screensaver turns your TV, projector or monitor off, so it can actuall ## How does it work ? -It supports the following methods: +It supports the following display methods: - **CEC (built-in)** - The screensaver forces the display to go into Standby using internal CEC controls. @@ -28,9 +34,80 @@ It supports the following methods: - **CEC on Android (kernel)** - The screensaver immediately forces the display off using kernel CEC controls and turns off device. +- **Backlight on Raspberry Pi (kernel)** + - The screensaver turns off the backlight of the display. This only works on Raspberry Pi. + +- **Backlight on Odroid C2 (kernel)** + - The screensaver turns off the backlight of the display. This only works on Odroid C2. + -Optionally it also can put your system to sleep or power it off. +Optionally it also can put your system to sleep or power it off using one of the following methods: + +- **Suspend (built-in)** +- **Hibernate (built-in)** +- **Quit (built-in)** +- **Shutdown (built-in)** +- **Reboot (built-in)** +- **Powerdown (built-in)** Or log off your user or mute audio. -One can press the `HOME` key to deactivate the screensaver, depending on the method used and the state of the display it may turn your display back on. +One can press the `HOME` key to deactivate the screensaver, depending on the method used and the state of the display/system it may turn your display and system back on. + + +## Related +A collection of related links: + +- [Hardware hack for turning RPi on over CEC](https://forum.kodi.tv/showthread.php?tid=174315&pid=2651811#pid2651811) + + +## Reporting issues +You can report issues at [our GitHub project](https://github.com/dagwieers/screensaver.turnoff). + + +## Releases +### v0.10.2 (2019-11-21) +- Create separate entrypoint +- Improve stability +- Add more unit tests + +### v0.10.1 (2019-10-30) +- Add sanity tests, unit tests and coverage support +- Use JSON-RPC for all built-ins +- Improvements for Python 3 +- Support Odroid-C2 display method + +### v0.10.0 (2019-03-13) +- Support RPi touchscreen display method +- Improve mute and unmuting audio using JSON-RPC + +### v0.9.2 (2018-06-07) +- Fix translations +- Fix an issue when stopping the screensaver + +### v0.9.1 (2018-04-14) +- Improve documentation +- Don't log when no action was taken +- Fix sanity issues + +### v0.9.0 (2018-04-13) +- Improve help in add-on settings +- Improve add-on logging +- Add icon and title to pop-ups + +### v0.8.1 (2018-04-12) +- Renamed add-on from 'No Signal' to 'Turn Off' +- Support vbetool and xrandr display methods +- Support Android CEC display method +- Support built-in and Android power methods +- Support Mute built-in +- Show a pop-up when errors are detected + +### v0.8.0 (2018-04-12) +- Support built-in DPMS and CEC display methods +- Improve add-on settings + +### v0.7.4 (2018-04-12) +- Support RPi and X11 xset DPMS display methods +- Support System.LogOff built-in +- Initial release diff --git a/screensaver.turnoff/addon.xml b/screensaver.turnoff/addon.xml index 8acb6940b8..2378e85d3f 100644 --- a/screensaver.turnoff/addon.xml +++ b/screensaver.turnoff/addon.xml @@ -1,13 +1,49 @@ - + + + + + all - Screensaver that turns your screen off to save power - The Turn Off screensaver turns your TV, projector or monitor off, like any old fashioned screensaver is intended to do. - http://github.com/dagwieers/plugin.screensaver.off - GPL2+ + Screensaver that turns your display off to save power + +The Turn Off screensaver turns your TV, projector or screen off, like any old fashioned screensaver is intended to do. + +Next to managing your display, it can also manage your device power state, log your profile off or mute audio to avoid sounds through your A/V receiver. + + GPL-2.0-or-later dag@wieers.com + https://kodi.wiki/view/Add-on:Turn_Off + https://github.com/dagwieers/screensaver.turnoff + https://forum.kodi.tv/showthread.php?tid=331076 + +v0.10.2 (2019-11-21) +- Create separate entrypoint +- Improve stability +- Add more unit tests + +v0.10.1 (2019-10-30) +- Add sanity tests, unit tests and coverage support +- Use JSON-RPC for all built-ins +- Improvements for Python 3 +- Support Odroid-C2 display method + +v0.10.0 (2019-03-13) +- Support RPi touchscreen display method +- Improve mute and unmuting audio using JSON-RPC + +v0.9.2 (2018-06-07) +- Fix translations +- Fix an issue when stopping the screensaver + +v0.9.1 (2018-04-14) +- Improve documentation +- Don't log when no action was taken +- Fix sanity issues + + + resources/media/icon.png + - - diff --git a/screensaver.turnoff/default.py b/screensaver.turnoff/default.py new file mode 100644 index 0000000000..b6f5c63a6c --- /dev/null +++ b/screensaver.turnoff/default.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2018, Dag Wieers (@dagwieers) +# GNU General Public License v2.0 (see COPYING or https://www.gnu.org/licenses/gpl-2.0.txt) +''' This Kodi addon turns off display devices when Kodi goes into screensaver-mode ''' + +from __future__ import absolute_import, division, unicode_literals +import screensaver +screensaver.run() diff --git a/screensaver.turnoff/resources/language/resource.language.en_gb/strings.po b/screensaver.turnoff/resources/language/resource.language.en_gb/strings.po index 1da8bad7e1..75edd1f1de 100644 --- a/screensaver.turnoff/resources/language/resource.language.en_gb/strings.po +++ b/screensaver.turnoff/resources/language/resource.language.en_gb/strings.po @@ -6,149 +6,197 @@ msgstr "" "POT-Creation-Date: 2018-04-13 03:00+0000\n" "PO-Revision-Date: 2018-04-13 03:00+0000\n" "Last-Translator: Dag Wieers \n" -"Language-Team: en\n" +"Language-Team: nl\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Language: en\n" +"Language: nl\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -msgctxt "#33100" +msgctxt "#32100" msgid "Display" -msgstr "Display" +msgstr "" -msgctxt "#33101" +msgctxt "#32101" msgid "Pick a method for turning off your display" -msgstr "Pick a method for turning off your display" +msgstr "" -msgctxt "#33102" +msgctxt "#32102" msgid "Display method" -msgstr "Display method" +msgstr "" -msgctxt "#33103" +msgctxt "#32103" msgid "Not all 'Display methods' work on every system. Carefully test." -msgstr "Not all 'Display methods' work on every system. Carefully test." +msgstr "" -msgctxt "#33104" +msgctxt "#32104" msgid "- CEC methods require CEC device support. Check vendor manual." -msgstr "- CEC methods require CEC device support. Check vendor manual." +msgstr "" -msgctxt "#33105" -msgid "- No Signal method disables video signal. Device may go to sleep or turn off." -msgstr "- No Signal method disables video signal. Device may go to sleep or turn off." +msgctxt "#32105" +msgid "- No Signal method disables video signal to display device." +msgstr "" -msgctxt "#33106" +msgctxt "#32106" msgid "- DPMS methods make use of VESA DPMS capabilities of your monitor." -msgstr "- DPMS methods make use of VESA DPMS capabilities of your monitor." +msgstr "" -msgctxt "#33107" +msgctxt "#32107" msgid "Press HOME or BACK key on remote to return from screen saver." -msgstr "Press HOME or BACK key on remote to return from screen saver." +msgstr "" -msgctxt "#33108" +msgctxt "#32108" msgid "NOTE: Not every device can be powered on automatically." -msgstr "NOTE: Not every device can be powered on automatically." +msgstr "" -msgctxt "#33110" +msgctxt "#32110" msgid "Do nothing" -msgstr "Do nothing" +msgstr "" -msgctxt "#33111" +msgctxt "#32111" msgid "CEC (built-in)" -msgstr "CEC (built-in)" +msgstr "" -msgctxt "#33112" +msgctxt "#32112" msgid "No Signal on Raspberry Pi (using vcgencmd)" -msgstr "No Signal on Raspberry Pi (using vcgencmd)" +msgstr "" -msgctxt "#33113" +msgctxt "#32113" msgid "DPMS (built-in)" -msgstr "DPMS (built-in)" +msgstr "" -msgctxt "#33114" +msgctxt "#32114" msgid "DPMS (using xset)" -msgstr "DPMS (using xset)" +msgstr "" -msgctxt "#33115" +msgctxt "#32115" msgid "DPMS (using vbetool)" -msgstr "DPMS (using vbetool)" +msgstr "" -msgctxt "#33116" +msgctxt "#32116" msgid "DPMS (using xrandr)" -msgstr "DPMS (using xrandr)" +msgstr "" -msgctxt "#33117" +msgctxt "#32117" msgid "CEC on Android (kernel)" -msgstr "CEC on Android (kernel)" +msgstr "" + +msgctxt "#32118" +msgid "Backlight on Raspberry Pi (kernel)" +msgstr "" -msgctxt "#33200" +msgctxt "#32119" +msgid "Backlight on Odroid C2 (kernel)" +msgstr "" + +msgctxt "#32200" msgid "Power" -msgstr "Power" +msgstr "" -msgctxt "#33201" +msgctxt "#32201" msgid "Pick a method for turning off your system" -msgstr "Pick a method for turning off your system" +msgstr "" -msgctxt "#33202" +msgctxt "#32202" msgid "Power method" -msgstr "Power method" +msgstr "" -msgctxt "#33203" +msgctxt "#32203" msgid "Don't change this unless you know exactly what you are doing." -msgstr "Don't change this unless you know exactly what you are doing." +msgstr "" -msgctxt "#33210" +msgctxt "#32210" msgid "Do nothing" -msgstr "Do nothing" +msgstr "" -msgctxt "#33211" +msgctxt "#32211" msgid "Suspend (built-in)" -msgstr "Suspend (built-in)" +msgstr "" -msgctxt "#33212" +msgctxt "#32212" msgid "Hibernate (built-in)" -msgstr "Hibernate (built-in)" +msgstr "" -msgctxt "#33213" +msgctxt "#32213" msgid "Quit (built-in)" -msgstr "Quit (built-in)" +msgstr "" -msgctxt "#33214" +msgctxt "#32214" msgid "Shutdown action (built-in)" -msgstr "Shutdown action (built-in)" +msgstr "" -msgctxt "#33215" +msgctxt "#32215" msgid "Reboot (built-in)" -msgstr "Reboot (built-in)" +msgstr "" -msgctxt "#33216" +msgctxt "#32216" msgid "Powerdown (built-in)" -msgstr "Powerdown (built-in)" +msgstr "" -msgctxt "#33217" +msgctxt "#32217" msgid "Android POWER key event (using input)" -msgstr "Android POWER key event (using input)" +msgstr "" -msgctxt "#33300" +msgctxt "#32300" msgid "Options" -msgstr "Options" +msgstr "" -msgctxt "#33301" +msgctxt "#32301" msgid "When activating screensaver..." -msgstr "When activating screensaver..." +msgstr "" -msgctxt "#33311" +msgctxt "#32311" msgid "Log off user" -msgstr "Log off user" +msgstr "" -msgctxt "#33312" +msgctxt "#32312" msgid "Useful in case you have parental controls enabled." -msgstr "Useful in case you have parental controls enabled." +msgstr "" -msgctxt "#33321" +msgctxt "#32321" msgid "Mute audio" -msgstr "Mute audio" +msgstr "" -msgctxt "#33322" +msgctxt "#32322" msgid "Ensure no audio is produced inadvertently, e.g. when using A/V receiver." -msgstr "Ensure no audio is produced inadvertently, e.g. when using A/V receiver." +msgstr "" + +msgctxt "#32400" +msgid "Expert" +msgstr "" + +msgctxt "#32401" +msgid "Test drive this screensaver configuration" +msgstr "" + +msgctxt "#32411" +msgid "Enable Screensaver" +msgstr "" + +msgctxt "#32412" +msgid "Activate Screensaver" +msgstr "" + +msgctxt "#32420" +msgid "Logging" +msgstr "" + +msgctxt "#32421" +msgid "Log level" +msgstr "" + +msgctxt "#32430" +msgid "Quiet" +msgstr "" + +msgctxt "#32431" +msgid "Info" +msgstr "" + +msgctxt "#32432" +msgid "Verbose" +msgstr "" + +msgctxt "#32433" +msgid "Debug" +msgstr "" diff --git a/screensaver.turnoff/resources/media/icon.png b/screensaver.turnoff/resources/media/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c2b9ff72e05048e373002d5e54315c488353bdc3 GIT binary patch literal 4593 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 A1^@s6 literal 0 HcmV?d00001 diff --git a/screensaver.turnoff/resources/settings.xml b/screensaver.turnoff/resources/settings.xml index a47beb6c7d..fc2d839409 100644 --- a/screensaver.turnoff/resources/settings.xml +++ b/screensaver.turnoff/resources/settings.xml @@ -1,25 +1,33 @@ - - - - - - - - - + + + + + + + + + - - - - + + + + - - - - - - + + + + + + + + + + + + + + diff --git a/screensaver.turnoff/resources/skins/default/720p/screensaver-turnoff.xml b/screensaver.turnoff/resources/skins/default/720p/gui.xml similarity index 100% rename from screensaver.turnoff/resources/skins/default/720p/screensaver-turnoff.xml rename to screensaver.turnoff/resources/skins/default/720p/gui.xml diff --git a/screensaver.turnoff/screensaver.py b/screensaver.turnoff/screensaver.py index 0f49c0b416..97cd494998 100644 --- a/screensaver.turnoff/screensaver.py +++ b/screensaver.turnoff/screensaver.py @@ -1,164 +1,304 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2018, Dag Wieers (@dagwieers) +# GNU General Public License v2.0 (see COPYING or https://www.gnu.org/licenses/gpl-2.0.txt) +''' This Kodi addon turns off display devices when Kodi goes into screensaver-mode ''' + +from __future__ import absolute_import, division, unicode_literals import sys +import atexit import subprocess -import xbmc -import xbmcaddon -import xbmcgui +from xbmc import executebuiltin, executeJSONRPC, getCondVisibility, log as xlog, Monitor +from xbmcaddon import Addon +from xbmcgui import Dialog, WindowXMLDialog + +DEBUG_LOGGING = 0 + +# NOTE: The below order relates to resources/settings.xml +DISPLAY_METHODS = [ + dict(name='do-nothing', title='Do nothing', + function='log', + args_off=[2, 'Do nothing to power off display'], + args_on=[2, 'Do nothing to power back on display']), + dict(name='cec-builtin', title='CEC (buil-in)', + function='run_builtin', + args_off=['CECStandby'], + args_on=['CECActivateSource']), + dict(name='no-signal-rpi', title='No Signal on Raspberry Pi (using vcgencmd)', + function='run_command', + args_off=['vcgencmd', 'display_power', '0'], + args_on=['vcgencmd', 'display_power', '1']), + dict(name='dpms-builtin', title='DPMS (built-in)', + function='run_builtin', + args_off=['ToggleDPMS'], + args_on=['ToggleDPMS']), + dict(name='dpms-xset', title='DPMS (using xset)', + function='run_command', + args_off=['xset', 'dpms', 'force', 'off'], + args_on=['xset', 'dpms', 'force', 'on']), + dict(name='dpms-vbetool', title='DPMS (using vbetool)', + function='run_command', + args_off=['vbetool', 'dpms', 'off'], + args_on=['vbetool', 'dpms', 'on']), + # TODO: This needs more outside testing + dict(name='dpms-xrandr', title='DPMS (using xrandr)', + function='run_command', + args_off=['xrandr', '--output CRT-0', 'off'], + args_on=['xrandr', '--output CRT-0', 'on']), + # TODO: This needs more outside testing + dict(name='cec-android', title='CEC on Android (kernel)', + function='run_command', + args_off=['su', '-c', 'echo 0 >/sys/devices/virtual/graphics/fb0/cec'], + args_on=['su', '-c', 'echo 1 >/sys/devices/virtual/graphics/fb0/cec']), + # NOTE: Contrary to what one might think, 1 means off and 0 means on + dict(name='backlight-rpi', title='Backlight on Raspberry Pi (kernel)', + function='run_command', + args_off=['su', '-c', 'echo 1 >/sys/class/backlight/rpi_backlight/bl_power'], + args_on=['su', '-c', 'echo 0 >/sys/class/backlight/rpi_backlight/bl_power']), + dict(name='backlight-odroid-c2', title='Backlight on Odroid C2 (kernel)', + function='run_command', + args_off=['su', '-c', 'echo 0 >/sys/class/amhdmitx/amhdmitx0/phy'], + args_on=['su', '-c', 'echo 1 >/sys/class/amhdmitx/amhdmitx0/phy']), +] + +POWER_METHODS = [ + dict(name='do-nothing', title='Do nothing', + function='log', kwargs_off=dict(level=2, msg='Do nothing to power off system')), + dict(name='suspend-builtin', title='Suspend (built-in)', + function='jsonrpc', kwargs_off=dict(method='System.Suspend')), + dict(name='hibernate-builtin', title='Hibernate (built-in)', + function='jsonrpc', kwargs_off=dict(method='System.Hibernate')), + dict(name='quit-builtin', title='Quit (built-in)', + function='jsonrpc', kwargs_off=dict(method='Application.Quit')), + dict(name='shutdown-builtin', title='ShutDown action (built-in)', + function='jsonrpc', kwargs_off=dict(method='System.Shutdown')), + dict(name='reboot-builtin', title='Reboot (built-in)', + function='jsonrpc', kwargs_off=dict(method='System.Reboot')), + dict(name='powerdown-builtin', title='Powerdown (built-in)', + function='jsonrpc', kwargs_off=dict(method='System.Powerdown')), +] + + +class SafeDict(dict): + ''' A safe dictionary implementation that does not break down on missing keys ''' + def __missing__(self, key): + ''' Replace missing keys with the original placeholder ''' + return '{' + key + '}' + + +def from_unicode(text, encoding='utf-8'): + ''' Force unicode to text ''' + if sys.version_info.major == 2 and isinstance(text, unicode): # noqa: F821; pylint: disable=undefined-variable + return text.encode(encoding) + return text + + +def to_unicode(text, encoding='utf-8'): + ''' Force text to unicode ''' + return text.decode(encoding) if isinstance(text, bytes) else text + +def log(level=1, msg='', **kwargs): + ''' Log info messages to Kodi ''' + max_log_level = int(get_setting('max_log_level', 0)) + if not DEBUG_LOGGING and not (level <= max_log_level and max_log_level != 0): + return + from string import Formatter + if kwargs: + msg = Formatter().vformat(msg, (), SafeDict(**kwargs)) + msg = '[{addon}] {msg}'.format(addon=ADDON_ID, msg=msg) + xlog(from_unicode(msg), level % 3 if DEBUG_LOGGING else 2) -def log_error(msg='', level=xbmc.LOGERROR): - xbmc.log(msg='[%s] %s' % (addon_name, msg), level=level) +def log_error(msg, **kwargs): + ''' Log error messages to Kodi ''' + from string import Formatter + if kwargs: + msg = Formatter().vformat(msg, (), SafeDict(**kwargs)) + msg = '[{addon}] {msg}'.format(addon=ADDON_ID, msg=msg) + xlog(from_unicode(msg), 4) -def log_notice(msg='', level=xbmc.LOGNOTICE): - xbmc.log(msg='[%s] %s' % (addon_name, msg), level=level) + +def jsonrpc(**kwargs): + ''' Perform JSONRPC calls ''' + from json import dumps, loads + if 'id' not in kwargs: + kwargs.update(id=1) + if 'jsonrpc' not in kwargs: + kwargs.update(jsonrpc='2.0') + result = loads(executeJSONRPC(dumps(kwargs))) + log(3, msg="Sending JSON-RPC payload: '{payload}' returns '{result}'", payload=kwargs, result=result) + return result + + +def get_setting(setting_id, default=None): + ''' Get an add-on setting ''' + value = to_unicode(ADDON.getSetting(setting_id)) + if value == '' and default is not None: + return default + return value + + +def get_global_setting(setting): + ''' Get a Kodi setting ''' + result = jsonrpc(method='Settings.GetSettingValue', params=dict(setting=setting)) + return result.get('result', {}).get('value') def popup(heading='', msg='', delay=10000, icon=''): + ''' Bring up a pop-up with a meaningful error ''' if not heading: - heading = '%s screensaver failed' % addon_name + heading = 'Addon {addon} failed'.format(addon=ADDON_ID) if not icon: - icon = addon_icon - xbmcgui.Dialog().notification(heading, msg, icon, delay) + icon = ADDON_ICON + Dialog().notification(heading, msg, icon, delay) + + +def set_mute(toggle=True): + ''' Set mute using Kodi JSON-RPC interface ''' + jsonrpc(method='Application.SetMute', params=dict(mute=toggle)) + + +def activate_window(window='home'): + ''' Set mute using Kodi JSON-RPC interface ''' +# result = jsonrpc(method='GUI.ActivateWindow', params=dict(window=window, parameters=['Home'])) + jsonrpc(method='GUI.ActivateWindow', params=dict(window=window)) def run_builtin(builtin): - log_notice(msg="Executing builtin '%s'" % builtin) - try: - xbmc.executebuiltin(builtin) - except Exception as e: - log_error(msg="Exception executing builtin '%s': %s" % (builtin, e)) - popup(msg="Exception executing builtin '%s': %s" % (builtin, e)) + ''' Run Kodi builtins while catching exceptions ''' + log(2, msg="Executing builtin '{builtin}'", builtin=builtin) + executebuiltin(builtin, True) -def run_command(command, shell=False): +def run_command(*command, **kwargs): + ''' Run commands on the OS while catching exceptions ''' # TODO: Add options for running using su or sudo try: - cmd = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=shell) + cmd = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, **kwargs) (out, err) = cmd.communicate() if cmd.returncode == 0: - log_notice(msg="Running command '%s' returned rc=%s" % (' '.join(command), cmd.returncode)) + log(2, msg="Running command '{command}' returned rc={rc}", command=' '.join(command), rc=cmd.returncode) else: - log_error(msg="Running command '%s' failed with rc=%s" % (' '.join(command), cmd.returncode)) + log_error(msg="Running command '{command}' failed with rc={rc}", command=' '.join(command), rc=cmd.returncode) if err: - log_error(msg="Command '%s' returned on stderr: %s" % (command[0], err)) + log_error(msg="Command '{command}' returned on stderr: {stderr}", command=command[0], stderr=err) if out: - log_error(msg="Command '%s' returned on stdout: %s " % (command[0], out)) + log_error(msg="Command '{command}' returned on stdout: {stdout} ", command=command[0], stdout=out) popup(msg="%s\n%s" % (out, err)) sys.exit(1) - except Exception as e: - log_error(msg="Exception running '%s': %s" % (command[0], e)) - popup(msg="Exception running '%s': %s" % (command[0], e)) + except OSError as exc: + log_error(msg="Exception running '{command}': {exc}", command=command[0], exc=exc) + popup(msg="Exception running '%s': %s" % (command[0], exc)) sys.exit(2) -class Screensaver(xbmcgui.WindowXMLDialog): +def func(function, *args, **kwargs): + ''' Execute a global function with arguments ''' + return globals()[function](*args, **kwargs) + - class Monitor(xbmc.Monitor): +class TurnOffDialog(WindowXMLDialog, object): + ''' The TurnOffScreensaver class managing the XML gui ''' - def __init__(self, callback): - self._callback = callback + def __init__(self, *args): # pylint: disable=super-init-not-called,unused-argument + ''' Initialize dialog ''' + self.display = None + self.logoff = None + self.monitor = None + self.mute = None + self.power = None + atexit.register(self.exit) - def onScreensaverDeactivated(self): - self._callback() + def onInit(self): # pylint: disable=invalid-name + ''' Perform this when the screensaver is started ''' + self.logoff = get_setting('logoff', 'false') + self.mute = get_setting('mute', 'true') - def __init__(self, *args, **kwargs): - pass + display_method = int(get_setting('display_method', 0)) + self.display = DISPLAY_METHODS[display_method] - def onInit(self): - self._monitor = self.Monitor(self.exit) + power_method = int(get_setting('power_method', 0)) + self.power = POWER_METHODS[power_method] + + log(3, msg='display_method={display}, power_method={power}, logoff={logoff}, mute={mute}', + display=self.display.get('name'), power=self.power.get('name'), logoff=self.logoff, mute=self.mute) + + # Turn off display + if self.display.get('name') != 'do-nothing': + log(1, msg="Turn display off using method '{name}'", **self.display) + func(self.display.get('function'), *self.display.get('args_off')) + + # FIXME: Screensaver always seems to lock when started, requires unlock and re-login + # Log off user + if self.logoff == 'true': + log(1, msg='Log off user') +# run_builtin('System.LogOff') + activate_window('loginscreen') +# run_builtin('ActivateWindow(loginscreen)') +# run_builtin('ActivateWindowAndFocus(loginscreen,return)') + + # Mute audio + if self.mute == 'true': + log(1, msg='Mute audio') + set_mute(toggle=True) + + self.monitor = TurnOffMonitor(action=self.resume) + self.monitor.waitForAbort(1) # Power off system - if power_method != 0: - log_notice(msg='Turn system off using method %s' % power_method) - if power_method == '1': # Suspend (built-in) - run_builtin('Suspend') - elif power_method == '2': # Hibernate (built-in) - run_builtin('Hibernate') - elif power_method == '3': # Quit (built-in) - run_builtin('Quit') - elif power_method == '4': # ShutDown action (built-in) - run_builtin('ShutDown') - elif power_method == '5': # Reboot (built-in) - run_builtin('Reboot') - elif power_method == '6': # PowerDown (built-in) - run_builtin('PowerDown') - elif power_method == '7': # Android POWER key event (using input) - run_command(['su', '-c', 'input keyevent KEYCODE_POWER'], shell=True) - - def onAction(self, action): - self.exit() + if self.power.get('name') != 'do-nothing': + log(1, msg="Turn system off using method '{name}'", **self.power) + func(self.power.get('function'), **self.power.get('kwargs_off', {})) - def exit(self): + def resume(self): + ''' Perform this when the Screensaver is stopped ''' # Unmute audio - if mute == 'true': - run_builtin('Mute') + if self.mute == 'true': + log(1, msg='Unmute audio') + set_mute(toggle=False) # Turn on display - if display_method != 0: - log_notice(msg='Turn display signal back on using method %s' % display_method) - if display_method == '1': # CEC (built-in) - run_builtin('CECActivateSource') - elif display_method == '2': # No Signal on Raspberry Pi (using vcgencmd) - run_command(['vcgencmd', 'display_power', '1']) - elif display_method == '3': # DPMS (built-in) - run_builtin('ToggleDPMS') - elif display_method == '4': # DPMS (using xset) - run_command(['xset', 'dpms', 'force', 'on']) - elif display_method == '5': # DPMS (using vbetool) - run_command(['vbetool', 'dpms', 'on']) - elif display_method == '6': # DPMS (using xrandr) - # NOTE: This needs more outside testing - run_command(['xrandr', '--output CRT-0', 'on']) - elif display_method == '7': # CEC on Android (kernel) - # NOTE: This needs more outside testing - run_command(['su', '-c', 'echo 1 >/sys/devices/virtual/graphics/fb0/cec'], shell=True) - - del self._monitor + if self.display.get('name') != 'do-nothing': + log(1, msg="Turn display back on using method '{name}'", **self.display) + func(self.display.get('function'), *self.display.get('args_on')) + + # Clean up everything + self.exit() + + def exit(self): + ''' Clean up function ''' + self.monitor = None self.close() -if __name__ == '__main__': - addon = xbmcaddon.Addon() - - addon_name = addon.getAddonInfo('name') - addon_path = addon.getAddonInfo('path') - addon_icon = addon.getAddonInfo('icon') - display_method = addon.getSetting('display_method') - power_method = addon.getSetting('power_method') - logoff = addon.getSetting('logoff') - mute = addon.getSetting('mute') - - # Turn off display - if display_method != 0: - log_notice(msg='Turn display signal off using method %s' % display_method) - if display_method == '1': # CEC (built-in) - run_builtin('CECStandby') - elif display_method == '2': # No Signal on Raspberry Pi (using vcgencmd) - run_command(['vcgencmd', 'display_power', '0']) - elif display_method == '3': # DPMS (built-in) - run_builtin('ToggleDPMS') - elif display_method == '4': # DPMS (using xset) - run_command(['xset', 'dpms', 'force', 'off']) - elif display_method == '5': # DPMS (using vbetool) - run_command(['vbetool', 'dpms', 'off']) - elif display_method == '6': # DPMS (using xrandr) - # NOTE: This needs more outside testing - run_command(['xrandr', '--output CRT-0', 'off']) - elif display_method == '7': # CEC on Android (kernel) - # NOTE: This needs more outside testing - run_command(['su', '-c', 'echo 0 >/sys/devices/virtual/graphics/fb0/cec'], shell=True) - - - # FIXME: Screensaver always seems to log off when logged in ? - # Log off user - if logoff == 'true': - run_builtin('System.Logoff()') - - # Mute audio - if mute == 'true': - run_builtin('Mute') - - # Do not start screensaver when command fails - screensaver = Screensaver('screensaver-turnoff.xml', addon_path, 'default') - screensaver.doModal() - del screensaver + +class TurnOffMonitor(Monitor, object): + ''' This is the monitor to exit TurnOffScreensaver ''' + + def __init__(self, **kwargs): # pylint: disable=super-init-not-called + ''' Initialize monitor ''' + self.action = kwargs.get('action') + + def onScreensaverDeactivated(self): # pylint: disable=invalid-name + ''' Perform cleanup function ''' + self.action() + + +def run(): + ''' Runs the screensaver ''' + + # If player has media, avoid running + if getCondVisibility("Player.HasMedia"): + log(1, msg='Screensaver not started because player has media.') + return + + TurnOffDialog('gui.xml', ADDON_PATH, 'default').doModal() + + +ADDON = Addon() +ADDON_NAME = to_unicode(ADDON.getAddonInfo('name')) +ADDON_ID = to_unicode(ADDON.getAddonInfo('id')) +ADDON_PATH = to_unicode(ADDON.getAddonInfo('path')) +ADDON_ICON = to_unicode(ADDON.getAddonInfo('icon')) + +DEBUG_LOGGING = get_global_setting('debug.showloginfo')