[Linux, Gnome] Retreive the lock or unlock state of your linux computer

Hello there.
I’m working on my room occupancy project, and because i rarely move when i’m at my computer, i wanted to get data on when i’m using it.

First i used a simple ping, but the horrible trust is that i leave my computer running most of the time.

So i went instead for something who could tell if i’m actively using it. I found that Gnome send dbus signal when the screensaver is enable, and when that happens my computer auto-lock

So i’ve written a dirty python script, set it up as a systemd user service, and it work very well !
I though i would share it here, but if anyone made anything with the same goal, it’s surely better !
Don’t get it wrong i’m not a dev :upside_down_face:

Python Script
$ cat .local/bin/ha_desktop_status.py 
#! python3

# Include Python stuff
import logging
import signal
# Include dbus stuff
import dbus
from dbus.mainloop.glib import DBusGMainLoop
from gi.repository import GLib
# Include Home-Assistant Stuff
from requests import post
from requests import get
from requests import HTTPError
from timeloop import Timeloop
from datetime import timedelta

# Define Global Variable
DBUS_SCREENSAVER_INTERFACE = "org.gnome.ScreenSaver"
HA_URL = "https://home-assistant.domain.com"
HA_TOKEN = "FILL ME WITH YOUR LONGLIVED TOKEN"
HA_ENTITY = "input_boolean.computer_name_used"

LAST_STATE = True
BACKGROUP_UPDATE_INTERVAL = 5

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("HA_Desktop_Status")


# Listen for Term signal from Systemd
def signal_handler():
    signal.signal(signal.SIGTERM, exit_gracefully)


def exit_gracefully(signum=0, frame=None):
    logger.info('Exiting now')
    tl.stop()
    loop.quit()
    ha_update_status(False)
    exit(0)


# Listen for Dbus Screensaver Event
def set_dbus_loop():
    # Create Session bus
    DBusGMainLoop(set_as_default=True)
    session_bus = dbus.SessionBus()
    # register for Gnome Screensaver signal
    session_bus.add_signal_receiver(dbus_lock_handler,
                                    dbus_interface=DBUS_SCREENSAVER_INTERFACE)
    # Create loop and start it
    loop = GLib.MainLoop()
    return loop


# React to Dbus Screensaver Event
def dbus_lock_handler(is_lock):
    global LAST_STATE
    if is_lock:
        logger.info('The computer is now locked')
        LAST_STATE = False
    else:
        logger.info('The computer is now unloked')
        LAST_STATE = True
    ha_update_status(LAST_STATE)


# Home assistant API Handler
def ha_call(endpoint, data=None):
    url = "{}{}".format(HA_URL, endpoint)
    headers = {
        "Authorization": "Bearer {}".format(HA_TOKEN),
        "content-type": "application/json",
    }
    if data:
        response = post(url, data, headers=headers, timeout=5)
    else:
        response = get(url, headers=headers, timeout=5)
    if response.ok:
        return response
    else:
        raise HTTPError("{}: {}".format(response.status_code,
                                        response.reason))


# Set the status of my computer state on Homeassistant
def ha_update_status(status):
    if status:
        service = "input_boolean.turn_on"
    else:
        service = "input_boolean.turn_off"
    endpoint = "/api/services/{}".format("/".join(service.split('.')))
    data = '{{"entity_id": "{}"}}'.format(HA_ENTITY)
    try:
        ha_call(endpoint, data)
    except TimeoutError as err:
        logger.warning("{}: The server have timeout, we will try again at the next loop".format(err))


tl = Timeloop()


# Setup loop to push last state again, just in case
@tl.job(interval=timedelta(minutes=BACKGROUP_UPDATE_INTERVAL))
def update_loop():
    ha_update_status(LAST_STATE)
    logger.info('State re-sended : {}'.format(LAST_STATE))


if __name__ == '__main__':
    try:
        signal_handler()
        ha_update_status(True)
        tl.start()
        loop = set_dbus_loop()
        loop.run()
    except KeyboardInterrupt:
        exit_gracefully()

Systemd service
$ systemctl --user cat ha_desktop_status.service 
# /home/user/.config/systemd/user/ha_desktop_status.service
[Unit]
Description=HomeAssistant Computer Status
After=graphical-session.target
After=network-online.target

[Service]
Type=simple
Environment=PYTHONUNBUFFERED=1
ExecStart=/usr/bin/python3  %h/.local/bin/ha_desktop_status.py

Restart=on-failure

[Install]
WantedBy=graphical-session.target

I hop no one will use those as drop in, because it might not last long, but i hope it give idea to people :slight_smile:

6 Likes

Interesting idea! I tried to run this and when I locked the screen I got this error:

INFO:HA_Desktop_Status:The computer is now locked
ERROR:dbus.connection:Exception in handler for D-Bus signal:
Traceback (most recent call last):
  File "/home/mhirsch/ha_gnome_lock/lib64/python3.8/site-packages/dbus/connection.py", line 232, in maybe_handle_message
    self._handler(*args, **kwargs)
TypeError: dbus_lock_handler() missing 1 required positional argument: 'is_lock'

Any idea about what could be causing that? I assume I’m using different library versions than you. Here’s what I’ve got installed in my venv

$ pip3 list
Package     Version  
----------- ---------
certifi     2020.12.5
chardet     3.0.4    
dbus-python 1.2.16   
idna        2.10     
pip         19.3.1   
pycairo     1.20.0   
PyGObject   3.38.0   
requests    2.25.0   
setuptools  41.6.0   
timeloop    1.0.2    
urllib3     1.26.2   

edit: the script is actually working, but must be getting an additional callback without the is_lock parameter being set. Probably worth noting that you have to create the input_boolean helper manually. Some sensors seem to get created when you call them but this didn’t work for me until I created it.

Yeaaa, i shouldn’t have used an input boolean but a binary_sensor. That way it would have made it by itself…

To be improved obviously but i have so many stuff in progress that i don’t take time to go back and improve for now.

On the error you ar having, i haven’t seen it but i need to update my distro.
If I encounter it and fix it I’ll let you know :slight_smile:

Thanks to @vlycop. It’s a good idea!

A little modification of script, to use mqtt

#! python3

import logging
import dbus
from dbus.mainloop.glib import DBusGMainLoop
from gi.repository import GLib
import random
from paho.mqtt import client as mqtt_client

MQTT_BROKER = "192.168.14.2"
MQTT_PORT = 1883
MQTT_TOPIC = "dct/active"
MQTT_USERNAME = "user"
MQTT_PASSWORD = "password"
MQTT_CLIENT_ID = f'python-mqtt-{random.randint(0, 1000)}'

LAST_STATE = True
BACKGROUP_UPDATE_INTERVAL = 5

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("HA_Desktop_Status")

client = mqtt_client.Client(MQTT_CLIENT_ID)
client.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD)
client.connect(MQTT_BROKER, MQTT_PORT)

def exit_gracefully(signum=0, frame=None):
    logger.info('Exiting now')
    loop.quit()
    ha_update_status(True)
    exit(0)

def set_dbus_loop():
    DBusGMainLoop(set_as_default=True)
    session_bus = dbus.SessionBus()
    session_bus.add_signal_receiver(dbus_lock_handler,
                                    dbus_interface="org.gnome.ScreenSaver",
                                    signal_name="ActiveChanged")
    loop = GLib.MainLoop()
    return loop

def dbus_lock_handler(is_locked):
    global LAST_STATE
    if is_locked:
        logger.info('The computer is now locked')
        LAST_STATE = True
    else:
        logger.info('The computer is now unlocked')
        LAST_STATE = False
    ha_update_status(LAST_STATE)

def ha_update_status(locked):
    if not client.is_connected:
        client.reconnect()
    if locked:
        msg = '{"state": "OFF"}'
    else:
        msg = '{"state": "ON"}'
    result = client.publish(MQTT_TOPIC, msg)
    if result[0] == 0:
        logger.info(f"Send `{msg}` to topic `{MQTT_TOPIC}`")
    else:
        logger.info(f"Failed to send message to topic {MQTT_TOPIC}")

if __name__ == '__main__':
    try:
        ha_update_status(False)
        loop = set_dbus_loop()
        loop.run()
    except KeyboardInterrupt:
        exit_gracefully()

And, in homeassistant:

binary_sensor:
  - platform: mqtt
    name: dct active
    state_topic: dct/active
    value_template: "{{ value_json.state }}"

Enjoy! :slightly_smiling_face:

4 Likes

This is exactly what I was looking for. Thank you and @vlycop for posting!