Automatische Zeitbuchung

Jedes mal am Ende des Monats: “Zaubara, hast du schon deine Zeit und Leistungserfassung gemacht?” Und ich denk ma nur: “Oida, schon wieder der Schas.” 

Wieder einen halben Tag verschwendet nur um ein grauenhaft schlechtes Online Tool zu bedienen.

“I choose a lazy person to do a hard job. Because a lazy person will find an easy way to do it.”

– Bill Gates

Die Idee

Ein kleines Tool schreiben, das mir jeden Tag auf das Knopferl “Hallo ich bin jetzt da und arbeite” drückt. 
Wenn das Pensum erfüllt ist, soll das Programm auf “Auf wiederschaun, i bin weg” drücken. 
Zum Glück gibts das auf der Web-Oberfläche. Verwendet nur niemand.

Aber an welchen Tagen soll es klicken?
Wie lang soll es warten bis es sich wieder ausloggt?
Und wann soll es Homeoffice schreiben und wann ist Office Tag?
Was ist mit Urlaub oder Feiertagen?

Das entscheidet dieses Flowchart:

Zum Glück gibts auf der Web-Oberfläche auch eine Sollzeit für den heutigen Tag! Die kann man auslesen und sich danach richten.
Steht da 0 Stunden? Passt, ausloggen und zurücklehnen.
Steht da was anderes? Log dich ein. Schau dir die aktuelle Uhrzeit an, addiere die Sollzeit dazu und schreib die Zeit auf. Komm um diese Uhrzeit wieder um dich auszuloggen. Easy.

Python Code

Gibts auf Github: https://github.com/thazaubara/i-work-hard

Im Grunde läuft das Programm alle 10 Minuten auf meinem Server durch. Je nach Status entscheidet es sich für einloggen, ausloggen oder einfach warten. Man muss ja nicht immer die Web-Oberfläche abfragen, spart Ressourcen. Dafür muss man aber mitschreiben um den aktuellen Status zu kennen. Ah ja und Montag und Donnerstag ist Office Tag, da klick ma was anderes!

Ubuntu Server

24/7 Online. Python file läuft alle 10 Minuten als cronjob durch.

24/7 Online. Python file läuft alle 10 Minuten als cronjob durch.

Steht dann auch hübsch im syslog drinnen:

zaubara@ubuntu-server:~/scripts/i-work-hard$ tail -f /var/log/syslog | grep 'I WORK HARD' 
Dec  4 09:00:22 ubuntu-server zaubara: I WORK HARD at 04.12.2023 09:00 -> Normal booking booked.
Dec  4 09:00:22 ubuntu-server zaubara: I WORK HARD at 04.12.2023 09:00 -> Logged out. Quitting Driver.
Dec  4 17:30:21 ubuntu-server zaubara: I WORK HARD at 04.12.2023 17:30 -> Go home.
Dec  4 17:30:21 ubuntu-server zaubara: I WORK HARD at 04.12.2023 17:30 -> Logged out. Quitting Driver.
Dec  5 09:00:26 ubuntu-server zaubara: I WORK HARD at 05.12.2023 09:00 -> Homeoffice booked.
Dec  5 09:00:26 ubuntu-server zaubara: I WORK HARD at 05.12.2023 09:00 -> Logged out. Quitting Driver.
Dec  5 17:30:20 ubuntu-server zaubara: I WORK HARD at 05.12.2023 17:30 -> Go home.
Dec  5 17:30:20 ubuntu-server zaubara: I WORK HARD at 05.12.2023 17:30 -> Logged out. Quitting Driver.
Dec  6 09:00:22 ubuntu-server zaubara: I WORK HARD at 06.12.2023 09:00 -> Homeoffice booked.
Dec  6 09:00:22 ubuntu-server zaubara: I WORK HARD at 06.12.2023 09:00 -> Logged out. Quitting Driver.
Dec  6 17:30:21 ubuntu-server zaubara: I WORK HARD at 06.12.2023 17:30 -> Go home.
Dec  6 17:30:21 ubuntu-server zaubara: I WORK HARD at 06.12.2023 17:30 -> Logged out. Quitting Driver.
Dec  7 09:00:23 ubuntu-server zaubara: I WORK HARD at 07.12.2023 09:00 -> Normal booking booked.
Dec  7 09:00:23 ubuntu-server zaubara: I WORK HARD at 07.12.2023 09:00 -> Logged out. Quitting Driver.
Dec  7 17:30:20 ubuntu-server zaubara: I WORK HARD at 07.12.2023 17:30 -> Go home.
Dec  7 17:30:20 ubuntu-server zaubara: I WORK HARD at 07.12.2023 17:30 -> Logged out. Quitting Driver.

Selenium

Das beste Web-Testing Framework. Bis jetzt. 
Hier das Getting Started: https://selenium-python.readthedocs.io/installation.html

Einfach erklärt: 
“Such dir das Element mit dem xpath so-und-so und klick drauf”
“Such dir das Element mit der HTML id password und schreib die-eine-variable rein”
“Such dir das eine div und nimm das dritte element aus der Liste und kopiere den Wert”

Offizielle Doku hier: https://selenium-python.readthedocs.io/locating-elements.html

Also los gehts. Rechtsklick auf die Seite. Inspect Element.

Id raus kopieren und unter anderem folgende Zeilen schreiben:

# CLICK THE POST TOUCH BUTTON
WebDriverWait(driver, 10).until(EC.visibility_of_element_located((By.ID, "TileButtonCID31513-btnWrap"))).click()

“Suche das Element TileButton-soundso, wenn nicht da, lass dir bis zu 10 Sekunden Zeit und versuchs nochmal. Wenn gefunden, klick drauf”. Sorry 4 Spaghetti Code, bei 15 von diesen Statements ists dann doch einfacher so zu lesen.

Lieber safe than sorry. Klickt man falsch, stürzt die Kiste ab und man hat einen von fünf möglichen Logins verkackt. Bei 5 von 5 muss man ca nen halben Tag warten bis wieder freigeschalten wird.

Mit solchen Statements geht man dann von Seite zu Seite, bis man bei den Elementen angekommen ist, die gewünscht sind. 

  • Seite aufrufen.
  • Username einfügen
  • Password einfügen
  • Login Button klicken
  • “Time” Button klicken
  • “Post Touch” button klicken
  • “Working Time Today” auslesen und aufschreiben
  • Je nach Status:
    • Klick “Posting Type” -> “Homeoffice”
    • Klick “Posting Type” -> “Normalbuchung”
    • Klick “Going”
  • Ausloggen
  • Fertig

Status File

Damit nicht jedes mal Selenium gestartet werden muss um nachzuschauen wie lang noch gearbeitet werden muss, schreiben wir einfach mit.

Wichtige Infos:

  • Aktuelles Datum
  • Aktueller Tag
    wobei, nicht notwenig, ist aber lesbarer
  • Normalbuchung oder Homeoffice?
    Tage an denen nicht gearbeitet wird, werden auch nicht mitgeschrieben
  • Startzeitpunkt
    eh kloa
  • Endzeitpunkt
    wann wird voraussichtlich wieder nachgeschaut wie lang ich noch hackeln muss?
  • Ist der Tag beendet?
    eh kloa
  • Wann wurde wirklich ausgeloggt?
    Wann wirklich ausgeloggt wurde. 

Das kommt in ein Simples Textfile, das alle 10 Minuten vom Script ausgelesen wird. JSON Format. Das Langzeitgedächtnis von dem Programm.

[    
...
    {
        "date": "06.12.2023",
        "day": "Wednesday",
        "action": "homeoffice",
        "start": "09:00",
        "end": "17:30",
        "finished": "yes",
        "logout_time": "17:30"
    },
    {
        "date": "07.12.2023",
        "day": "Thursday",
        "action": "normalbuchung",
        "start": "09:00",
        "end": "17:30",
        "finished": "yes",
        "logout_time": "17:30"
    }
]

Die eine Funktion

Herz des Ganzen ist eigentlich dieser Teil vom Code. Der Rest bezieht sich auf irgendwelche Date-checks oder File-mitschreib-Geschichten.

Info: sleep() ist eine Funktion von mir, nicht per se time.sleep(). Wartet einfach eine Sekunde und gibt mir mögliche Breakpoints zum Debuggen. Weil BMD ewig langsam ist.

def do_bmd_stuff(action, headless=True):
    options = webdriver.ChromeOptions()
    options.add_argument("user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36")
    options.add_argument("--window-size=1300,800")

    if headless:
        options.add_argument("--start-maximized")
        options.add_argument("--headless")

    driver = webdriver.Chrome(options=options)
    width = driver.get_window_size().get("width")
    height = driver.get_window_size().get("height")
    print(f"driver set to {width}x{height}")

    # GO TO SITE
    base_url = "https://*****/bmdweb2"
    print(f"Loading {BMD_URL}")
    driver.get(BMD_URL)

    try:
        # FILL CREDENTIALS
        #txt_username = WebDriverWait(driver, 10).until(EC.visibility_of_element_located((By.ID, "txtuser - inputEl")))
        txt_username = WebDriverWait(driver, 10).until(EC.visibility_of_element_located((By.ID, "txtuser-inputEl")))
        txt_username.send_keys(BMD_USER)
        txt_password = driver.find_element(By.ID, "txtpass-inputEl")
        txt_password.send_keys(BMD_PASS)

        # CLICK THE LOGIN BUTTON
        loginbutton = driver.find_element(By.ID, "loginbutton-btnEl")
        loginbutton.click()

        # CLICK THE TIME BUTTON#
        sleep()
        WebDriverWait(driver, 10).until(EC.visibility_of_element_located((By.ID, "TileButtonPKG564-btnWrap"))).click()
        print("Login Successful.")

        # CLICK THE POST TOUCH BUTTON
        sleep()
        WebDriverWait(driver, 10).until(EC.visibility_of_element_located((By.ID, "TileButtonCID31513-btnWrap"))).click()

        # GET WORKING TIME TODAY
        day_debit = WebDriverWait(driver, 10).until(EC.visibility_of_element_located((By.ID, "AttrFieldTextFieldContainer7612734691278121CID4089024UID184-inputEl"))).get_attribute("value")
        day_so_far = WebDriverWait(driver, 10).until(EC.visibility_of_element_located((By.ID, "AttrFieldTextFieldContainer7612734691278121CID4089025UID185-inputEl"))).get_attribute("value")

        day_debit_float = 0.0
        day_so_far_float = 0.0

        try:
            hours, minutes = map(int, day_debit.split(':'))
            day_debit_float = hours + minutes / 60.0
            hours, minutes = map(int, day_so_far.split(':'))
            day_so_far_float = hours + minutes / 60.0
        except:
            print("Error parsing time. Exiting.")
            sys.exit(1)

        # print(f"Day debit: {day_debit_float}")

        if action == action_homeoffice:
            # CLICK POSTING TYPE
            sleep()
            WebDriverWait(driver, 10).until(EC.visibility_of_element_located((By.ID, "ButtonFrameBtn162-btnEl"))).click()
            # CLICK "HOMEOFFICE"
            sleep()
            WebDriverWait(driver, 10).until(EC.visibility_of_element_located((By.ID, "ButtonFrameBtn287-btnEl"))).click()
            # CLICK SAVE BUTTON
            sleep()
            WebDriverWait(driver, 10).until(EC.visibility_of_element_located((By.ID, "ButtonFrameBtn179-btnEl"))).click()
            print_log("Homeoffice booked.")
        elif action == action_normalbuchung:
            # CLICK POSTING TYPE
            sleep()
            WebDriverWait(driver, 10).until(EC.visibility_of_element_located((By.ID, "ButtonFrameBtn162-btnEl"))).click()
            # CLICK "NORMAL BOOKING"
            sleep()
            WebDriverWait(driver, 10).until(EC.visibility_of_element_located((By.ID, "ButtonFrameBtn281-btnEl"))).click()
            # CLICK SAVE BUTTON
            sleep()
            WebDriverWait(driver, 10).until(EC.visibility_of_element_located((By.ID, "ButtonFrameBtn179-btnEl"))).click()
            print_log("Normal booking booked.")
        elif action == action_logout:
            # CLICK GOING
            sleep()
            WebDriverWait(driver, 10).until(EC.visibility_of_element_located((By.ID, "ButtonFrameBtn159-btnEl"))).click()
            # CLICK SAVE BUTTON
            sleep()
            WebDriverWait(driver, 10).until(EC.visibility_of_element_located((By.ID, "ButtonFrameBtn179-btnEl"))).click()
            print_log("Go home.")
        elif action == action_check_time:
            print(f"Just checked Time.")
            pass
        else:
            print("Unknown action. Returning.")

        # TODO Logout. bc -> The max. number of 5 zulässigen Datenbankverbindungen pro Benutzer wurde überschritten!
        # CLICK USER
        sleep()
        WebDriverWait(driver, 10).until(EC.visibility_of_element_located((By.ID, "NavBtnCMDUser77-btnInnerEl"))).click()
        # CLICK LOGOOUT
        sleep()
        WebDriverWait(driver, 10).until(EC.visibility_of_element_located((By.ID, "NavBar68ItemCMDLogout-itemEl"))).click()
        print_log("Logged out. Quitting Driver.")

        driver.quit()
        return day_debit_float, day_so_far_float

    except Exception as e:
        print_log(f"Error in Script. Exiting.")
        print(e)
        sys.exit(1)

Weitere Ideen

  • Zufallsgenerator für etwas ungenauere Zeitbuchungen. Damits nicht so auffällig ist wenn sich das mal jemand anschaut.
  • Telegram Bot, der mit Logs ausspuckt
  • Telegram Bot, der das Script bei Bedarf steuert
  • Vollautomatische Leistungserfassung zusätzlich. Für 0 Klicks im Monat.

Danach ist das Problem eh in Luft aufgelöst. Viel Spaß beim nachbauen 😛