Determine when monitor is turned on or off via python dbus

I run kde on my tv and want to turn the tv on and off via my cec device when kde sends the signal to turn the monitor on or off, as currently this doesn’t work on my tv.

I have found this for determining when kde tries to sleep:

from datetime import datetime
import dbus
#import gobject
from dbus.mainloop.glib import DBusGMainLoop
from gi.repository import GLib

def handle_sleep(*args):
    print("%s    PrepareForSleep%s" % (datetime.now().ctime(), args))

DBusGMainLoop(set_as_default=True)     # integrate into gobject main loop
bus = dbus.SystemBus()                 # connect to system wide dbus
bus.add_signal_receiver(               # define the signal to listen to
    handle_sleep,                      # callback function
    'PrepareForSleep',                 # signal name
    'org.freedesktop.login1.Manager',  # interface
    'org.freedesktop.login1'           # bus name
)

loop = GLib.MainLoop() #loop = gobject.MainLoop()
loop.run()

but I want to know how to intercept the monitor on/off state instead of the sleep state.
Can any one help me out with this?

edit:
The reason I want to watch the monitor state is because I don’t want my pc hooked up to the tv to actually ever go to sleep as I use it as my server. But I want to screen to be able to be turned on and off as if it had gone to sleep.

I’m not aware of a D-Bus API for this. But it looks like you should be able to write a KWin script that can connect to the aboutToTurnOff() signal of an Output object (i.e. a display). In case your CEC interface can’t be handled from within that KWin script, you can make it send some custom D-Bus to somewhere else.

Thanks for the info! dbus isn’t important, so that sounds like it might just do what I need!

@jpetso I’ve gotten most of the way there with a KWin script.
I am stuck trying to execute a shell command via my KWin script.

I’m trying to execute the shell command: echo ‘standby 0’ | cec-client -s
which sends the signal to the CEC device to turn my TV off, here is my dbus call to try to do that:

callDBus(‘org.kde.krunner’, ‘/org/kde/krunner’, ‘org.kde.KDBusService.CommandLine’, [‘/usr/bin/konsole’], ‘/home/’, “echo ‘standby 0’ | cec-client -s”);

but it does not work. Do you know how I can execute a shell command via KWin script?

Don’t have my source code with me, but I figure that might be because krunner executes the command directly rather than through a shell. So it wouldn’t have pipes available.

Try "bash -c \"echo 'standby 0' | cec-client -s\"" instead for the last argument and see if that works? If it doesn’t, we’d have to dig a little.

@jpetso I’ve tried adding an error function which is now spitting out some warning/error in the console:

callDBus('org.kde.krunner', '/org/kde/krunner', 'org.kde.KDBusService.CommandLine', ['/usr/bin/konsole'], '/home/', "bash -c \"echo 'standby 0' | cec-client -s\"", (res) => {
        print('krunner result 4:', res)
    });

Gives me an error:

kwin_scripting: Received D-Bus message is error: "Invalid method name: /usr/bin/konsole"

So I tried:

callDBus('org.kde.krunner', '/org/kde/krunner', 'org.kde.KDBusService.CommandLine', ['konsole'], '/home/', "bash -c \"echo 'standby 0' | cec-client -s\"", (res) => {
        print('krunner result 4:', res)
    });

but this gives me the error:

kwin_scripting: Received D-Bus message is error: "No such interface 'org.kde.KDBusService.CommandLine' at object path '/org/kde/krunner'"

I"m not sure what to try now.

Edit: Doing more research it seems krunner is not meant to run shell scripts, so I fired off my own custom dbus event with some code like this:

callDBus('org.cec_kwin', '/org/cec_kwin', 'org.cec_kwin.Command', 'aboutToTurnOff', [], (res) => {
        print('krunner result 6:', res)
    });

Then I have a shell script which listens for the appropriate dbus event:

interface=org.cec_kwin.Command

dbus-monitor --profile "interface='$interface'" |
while read -r line; do
    echo $line | grep aboutToTurnOff && ./aboutToTurnOff.sh
    echo $line | grep wakeUp && ./wakeUp.sh
done

This seems to work only once then gives an error:

kwin_scripting: Received D-Bus message is error: "The name is not activatable"

At which point my kwin script doesn’t seem to work :-/

The method needs to be a separate argument in callDBus e.g

callDBus("org.kde.krunner", "/org/kde/krunner", "org.kde.KDBusService", "CommandLine", args, "", {});

But that doesn’t work from kwin for some reason, and from python-dbus it just opens krunner with the command in the input but doesn’t actually run it until you press enter.

Also freedesktop notifications fails string "No such method 'Notify' in interface 'org.freedesktop.Notifications' at object path '/org/freedesktop/Notifications' (signature 'sisssava{sv}i')" but works fine from python?? I was calling it like this:

callDBus("org.freedesktop.Notifications", "/org/freedesktop/Notifications", "org.freedesktop.Notifications", "Notify", "App", replace_id, icon, heading, content, [], hints, 3000);

What you could do instead is have a dbus service that runs any command (possibly unsafe??) when you call it or have it hardcoded in a method:

"""dbus run command example service
"""

import subprocess
import dbus
import dbus.service
import dbus.mainloop.glib
from gi.repository import GLib


class Service(dbus.service.Object):
    def __init__(self):
        self._loop = GLib.MainLoop()

    def run(self):
        dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
        bus_name = dbus.service.BusName("com.example.runcommand", dbus.SessionBus())
        dbus.service.Object.__init__(self, bus_name, "/com/example/runcommand")

        print("Service running...")
        self._loop.run()
        print("Service stopped")

    @dbus.service.method("com.example.runcommand", in_signature="s", out_signature="i")
    def run_command(self, m):
        print(f"Running command '{m}'")
        command = m.split()
        result = subprocess.run(
            command,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
            text=True,
            check=True,
        )

        print(f"exit code: '{result.returncode}'")
        print(f"stdout: '{result.stdout.strip()}'")
        return result.returncode

    @dbus.service.method("com.example.runcommand", in_signature="", out_signature="i")
    def notify_send(self):
        command = ["notify-send", "Hello", "World"]
        command_str = " ".join(command)
        print(f"Running command '{command_str}'")
        result = subprocess.run(
            command,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
            text=True,
            check=True,
        )

        print(f"exit code: '{result.returncode}'")
        print(f"stdout: '{result.stdout.strip()}'")
        return result.returncode

    @dbus.service.method("com.example.runcommand", in_signature="", out_signature="")
    def quit(self):
        print("Shutting down")
        self._loop.quit()


if __name__ == "__main__":
    Service().run()

Then from KWin call it like this:

callDBus("com.example.runcommand", "/com/example/runcommand", "com.example.runcommand", "run_command", "notify-send Hello World");

Wow thanks for the suggestions, they are very much appreciated! I will try your idea out when I get some time next week.

1 Like

okay so this is almost working using luiscanegra’s idea of having a python script listening for the dbus service.

I have the code in github: /bit-shift-io/scripts/tree/master/cec_kwin (as you can’t post url’s on here).

But here is my main.js:

function callDbusMethod(methodName, args) {
    print("callDbusMethod: ", methodName, " args:", args)
    callDBus("io.bitshift.dbus_service", "/io/bitshift/dbus_service", "io.bitshift.dbus_service", methodName, args);
}

function aboutToTurnOff() {
    callDbusMethod("aboutToTurnOff");
}

function wakeUp() {
    callDbusMethod("wakeUp");
}

let screens = workspace.screens
print("starting cec_kwin.", screens.length, " screens found.")

if (screens.length) {
    let firstScreen = screens[0]
    firstScreen.aboutToTurnOff.connect(aboutToTurnOff)
    firstScreen.wakeUp.connect(wakeUp)
}

This works the first time, I see a aboutToTurnOff and wakeUp call, but the next time the monitor goes to sleep it seems the functions connected to the signals are not fired.

Is this a bug in kde6 or have I done something wrong?

For me it works every time, just tried adding your kwin script, I changed it to call my python script run_command method and it shows the notify-send notification when the monitors turn off/on.

Also works when done manually with qdbus org.kde.kglobalaccel /component/org_kde_powerdevil invokeShortcut "Turn Off Screen"

Are you on kde plasma 6?

Yes, I am running Plasma 6.0.2 on Arch

The first thing that comes to mind is, does KWin remove a screen from workspace.screens and re-add a different one? I don’t think it should if you get the wakeUp signal as intended, but perhaps worth checking whether the workspace.screensChanged signal gets emitted at any point.

You were correct jpetso! It turns out when the CEC device turns the screen back on, KDE would fire the screensChanged signal.

I now have a working solution! Thanks for your help everyone.

To wrap up, my code is on github: /bit-shift-io/scripts/tree/master/cec_kwin (as you can’t post url’s on here).

My final kwin script looks like this:

function callDbusMethod(methodName, args) {
    print("[cec_kwin] callDbusMethod: ", methodName, " args:", args)
    callDBus("io.bitshift.dbus_service", "/io/bitshift/dbus_service", "io.bitshift.dbus_service", methodName, args, function (r) {
        print("[cec_kwin]", methodName, "successfully called. Returned:", r)
    })
}

function aboutToTurnOff() {
    callDbusMethod("aboutToTurnOff")
}

function wakeUp() {
    callDbusMethod("wakeUp")
}

function screensChanged() {
    let screens = workspace.screens
    print("[cec_kwin] screensChanged.", screens.length, "screens found.")

    if (screens.length) {
        let firstScreen = screens[0]
        firstScreen.aboutToTurnOff.connect(aboutToTurnOff)
        firstScreen.wakeUp.connect(wakeUp)
    }
}


print("[cec_kwin] Starting.")
workspace.screensChanged.connect(screensChanged)
screensChanged()

with my python dbus service looking like this:

#!/usr/bin/env python
#-*- coding: utf-8 -*-

"""
dbus service that listens for method calls sent from the kwin script. This will then try to take some action, 
typically attempt to execute a script sh script by the same name as the kwin script method.
"""

import subprocess
import dbus
import dbus.service
import dbus.mainloop.glib
from gi.repository import GLib
import os

def script_path(): 
    return os.path.dirname(os.path.realpath(__file__))


class Service(dbus.service.Object):
    def __init__(self):
        self._loop = GLib.MainLoop()

    def run(self):
        dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
        bus_name = dbus.service.BusName("io.bitshift.dbus_service", dbus.SessionBus())
        dbus.service.Object.__init__(self, bus_name, "/io/bitshift/dbus_service")

        print("Service running from " + script_path() + "...")
        self._loop.run()
        print("Service stopped")

    @dbus.service.method("io.bitshift.dbus_service", in_signature="", out_signature="")
    def aboutToTurnOff(self):
        print(f"aboutToTurnOff")
        self.run_command(script_path() + "/aboutToTurnOff.sh")

    @dbus.service.method("io.bitshift.dbus_service", in_signature="", out_signature="")
    def wakeUp(self):
        print(f"wakeUp")
        self.run_command(script_path() + "/wakeUp.sh")

    @dbus.service.method("io.bitshift.dbus_service", in_signature="s", out_signature="i")
    def run_command(self, m):
        print(f"Running command '{m}'")
        command = m.split()
        result = subprocess.run(
            command,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
            text=True,
            check=True,
        )

        print(f"exit code: '{result.returncode}'")
        print(f"stdout: '{result.stdout.strip()}'")
        return result.returncode

    # @dbus.service.method("io.bitshift.dbus_service", in_signature="", out_signature="i")
    # def notify_send(self):
    #     command = ["notify-send", "Hello", "World"]
    #     command_str = " ".join(command)
    #     print(f"Running command '{command_str}'")
    #     result = subprocess.run(
    #         command,
    #         stdout=subprocess.PIPE,
    #         stderr=subprocess.STDOUT,
    #         text=True,
    #         check=True,
    #     )

    #     print(f"exit code: '{result.returncode}'")
    #     print(f"stdout: '{result.stdout.strip()}'")
    #     return result.returncode

    @dbus.service.method("io.bitshift.dbus_service", in_signature="", out_signature="")
    def quit(self):
        print("Shutting down")
        self._loop.quit()


if __name__ == "__main__":
    Service().run()

I have the python script running as a systemd service.

2 Likes