Skript zum zeitgesteuerten Shutdown mit Ferienüberprüfung

Code-Stücke können hier veröffentlicht werden.
Antworten
EmaNymton
User
Beiträge: 174
Registriert: Sonntag 30. Mai 2010, 14:07

Hi,
ich stand bei uns in der Schule vor dem Problem, dass wir Rechner für Info-Terminals zeitgesteuert hoch und runter fahren möchten. Da die Rechner über WLAN ins Netz gehen, fällt zumindest für das Hochfahren ein WoL-Lösung aus und da ich WLAN nicht ganz so traue auch eine serverseitige Lösung was das Runterfahren angeht.
Zusätzlich sollen die Rechner am Wochenende und in den Ferien nicht laufen. Nach ein wenig Recherche habe ich im Netz kein Tool gefunden, das diese Anforderungen erfüllt, also habe ich mich hingesetzt und ein kleines Python-Skript geschrieben, das die Aufgabe erledigt. Das Skript ist noch ausbaufähig über Rückmeldung/Verbesserungen würde ich mich freuen. Vielleicht kann es ja jemand auch noch gebrauchen.

Die Rechner werden jeden Tag um dieselbe Zeit per Bios hochgefahren. Das Runterfahren übernimmt dann das Skript. Da ich die Feriendaten als ics-Dateien von http://www.schulferien.org/iCal/vorliegen habe, verwende ich das Modul http://pypi.python.org/pypi/icalendar. Die ics-Dateien sollten im gleichen Verzeichnis liegen und müssen im Skript angegeben werden.

Das Skript sollte zum Windowsstart automatisch gestartet werden und entscheidet dann selbstständig, ob der Rechner direkt wieder runterfährt oder bis zu einer gewissen Zeit anbleibt, bei uns 18:00 Uhr.

Code: Alles auswählen

import subprocess
from icalendar import Calendar
from datetime import datetime

#settings, change for your own needs
shutdown_hour = 18
shutdown_minute = 0
seconds_till_shutdown = 30
ics_files = ['Ferien_Nordrhein_Westfalen_2012.ics',
             'Feiertage_Nordrhein_Westfalen_2012.ics']

# calc shutdown_time
today_time = datetime.today()
future = datetime(today_time.year,
                  today_time.month,
                  today_time.day+(today_time.hour >= shutdown_hour),
                  shutdown_hour,
                  shutdown_minute)

vacation = []
for ics_file in ics_files:
    with open(ics_file,'rb') as file:
        gcal = Calendar.from_string(file.read())
        for component in gcal.walk():
            if component.name == "VEVENT":
                start_date = datetime.strptime(str(component.get('dtstart')),
                                              '%Y%m%d')
                end_date = datetime.strptime(str(component.get('dtend')),
                                              '%Y%m%d')
                # check for one-day-vacation:
                if (end_date-start_date).days > 1:
                    vacation.append((start_date, end_date))
                else:
                    vacation.append((start_date, start_date))


def is_no_vacation(date_time):
    value = True
    for start_date, end_date in vacation:
       if date_time >= start_date and date_time <= end_date:
           value = False
    return value

if today_time.weekday() < 5 and is_no_vacation(today_time):
    seconds_till_shutdown = (future-today_time).seconds

print "shutdown in {} seconds".format(seconds_till_shutdown)
subprocess.call(["shutdown.exe", "-f", "-s", "-t", str(seconds_till_shutdown)])
BlackJack

@EmaNymton: Es funktioniert nicht mehr mit der neuesten Version von `icalendar` weil, wie auch schon in der vorherigen Version, gleich am Anfang in der Dokumentation darauf hingewiesen wird, dass die Umwandlung zwischen Zeichenketten und `icalendar`-Objekten nur noch mittels `from_ical()`/`to_ical()` passieren soll. `from_string()` gibt es nicht mehr und `str()` auf die Objekte angewendet funktioniert auch nicht mehr. Letzteres liefert nun auch die `repr()`-Darstellung.

Das mit `str()` betrifft `start_date` und `end_date` im Skript. Wobei die API von `icalendar` an der Stelle sowieso sehr schlecht genutzt wird. Statt die Datumskomponente in eine Zeichenkette umzuwandeln und die dann in ein Datumsobjekt zu parsen, hätte man sich von der Datumskomponente direkt das Datumsobjekt als Attribut holen können. Das Testen auf 'VEVENT' müsste man auch nicht mit einem eigenen ``if`` machen — `Calendar.walk()` kann selbst schon filtern.

Man sollte Code auf Modulebene vermeiden, der nicht nur „Konstanten” definiert, also konstante Werte, Funktionen, und so weiter, damit man das Modul auch ohne das es als Programm läuft, importieren kann. Zum Beispiel um einzelne Funktionen zu testen. Ausserdem vermeidet man so von vornherein unsaubere Funktionen die auf Werten operieren, die sie nicht als Argumente übergeben bekommen haben.

Namen für konstante Werte auf Modulebene werden per Konvention komplett gross geschrieben.

Die Berechnung des Zeitpunktes zum Herunterfahren ist fehlerhaft. Wenn man einen Tag dazu addieren möchte muss man auch auf Monats und Jahresenden achten und nicht einfach nur die Tages-Komponente um eins erhöhen.

Abkürzungen bei Namen sollte man vermeiden wenn sie nicht allgemein bekannt sind. Ich verstehe zum Beispiel nicht warum `gcal` so heisst wie es heisst? Wofür steht das `g`? Und wenn Du das jetzt noch weisst, ist das auch in ein oder zwei Jahren noch so?

Die Sonderbehandlung für eintägige Ferien verstehe ich nicht — in den Kalenderdateien ist das Startdatum immer das des ersten Ferientages und das Enddatum immer das des ersten Tages *nach* den Ferien. Das gilt sowohl für eintägige Ferien/Feiertage als auch für längere Ferien. Die Sonderbehandlung macht IMHO keinen Sinn. Dein Programm lässt mehrtägige Ferien einen Tag zu lange laufen. Beispiel für 2012 im Januar: da entsteht folgendes Tupel: ``(datetime.date(2012, 1, 1), datetime.date(2012, 1, 7)``. Du testest mit ``date_time <= end_date`` und das ergibt am 1.7. für deine `is_no_vacation()` den Wert `False` also einen freien Tag obwohl das der erste Tag *nach* den Ferien ist. Das fällt bloss nicht auf weil es ein Samstag ist und damit ins Wochenende fällt.

Solch negativ formulierten Testfunktionen sollte man übrigens vermeiden. Da muss man immer einmal mehr „um die Ecke” denken wenn man die verstehen will. Man kann die Funktion auch deutlich kompakter forumulieren.

Ungetestet:

Code: Alles auswählen

#!/usr/bin/env python
import subprocess
from icalendar import Calendar
from datetime import datetime as DateTime, time as Time, timedelta as TimeDelta

#settings, change for your own needs
SHUTDOWN_TIME = Time(18, 0)
MINIMUM_SECONDS_TILL_SHUTDOWN = 30
ICS_FILENAMES = [
    'Ferien_Nordrhein_Westfalen_2012.ics',
    'Feiertage_Nordrhein_Westfalen_2012.ics',
]


def is_weekend(date):
    return date.weekday() >= 5


def is_vacation(vacations, date):
    return any(start <= date < end for start, end in vacations)


def main():
    now = DateTime.now()
    shutdown_time = DateTime.combine(now.date(), SHUTDOWN_TIME)
    if now.hour >= SHUTDOWN_TIME.hour:
        shutdown_time += TimeDelta(day=1)

    vacations = []
    for ics_filename in ICS_FILENAMES:
        with open(ics_filename, 'rb') as calendar_file:
            calendar = Calendar.from_ical(calendar_file.read())
        vacations.extend(
            (e['dtstart'].dt, e['dtend'].dt) for e in calendar.walk('VEVENT')
        )

    seconds_till_shutdown = (
        MINIMUM_SECONDS_TILL_SHUTDOWN
        if is_weekend(now) or is_vacation(vacations, now)
        else (shutdown_time - now).seconds
    )

    print 'shutdown in {0} seconds'.format(seconds_till_shutdown)
    subprocess.call(
        ['shutdown.exe', '-f', '-s', '-t', str(seconds_till_shutdown)]
    )


if __name__ == '__main__':
    main()
EmaNymton
User
Beiträge: 174
Registriert: Sonntag 30. Mai 2010, 14:07

Danke für deine ausführliche Antwort!

Die Methode from_ical() hatte ich versucht, aber wurde mir als unbekannt angezeigt. Hab jetzt nochmal die aktuelle Version von icalender installiert und jetzt geht's auch mit from_ical().

Hab gerade mal dein Skript kurz durchlaufen lassen und bis auf die Kleinigkeit, dass in den Tupeln in vacations date-Objekte stehen und du in is_vacation(vacations, now) datetime- und date-Objekte vergleichst, macht es das, was es soll. Ich habe jetzt beim generieren eben datetime-Objekte mittels

Code: Alles auswählen

vacations.extend(
            (DateTime.combine(e['dtstart'].dt,Time.min),
             DateTime.combine(e['dtend'].dt,Time.min)) 
                    for e in calendar.walk('VEVENT')
        )
erzeugt und alles ist gut.

Die Sache mit den Ferien und Feiertagen fand ich auch sehr merkwürdig, aber es scheint in der Tat so zu sein, dass die ical-Dateien bei Ferien den ersten und den letzten Ferientag beinhalten, bei Feiertagen aber eben als Starttag den Feiertag selbst und als Endtag den nächsten Tag. Das hatte mich zu dem "Workaround" geführt, da ich sonst eben den letzten Ferientag mit dem echt kleiner Vergleich nicht berücksichtige.

Bspl. Herbstferien NRW 2012
DTSTART;VALUE=DATE:20121008
DTEND;VALUE=DATE:20121021
der 21.10. ist der Sonntag und nicht der Montag nach den Ferien, oder hab ich da jetzt was übersehen?

Ich hatte mich bei der Methodenwahl eben an der Prüfung orientiert ob ein Wochentag ist und keine Ferien, da dann der Rechner länger anbleiben soll.
Du formulierst die Bedienung eben so, wann der Rechner direkt wieder runtergefahren werden soll. Die Kompaktheit bekomme ich doch auch so hin, oder?

Code: Alles auswählen

seconds_till_shutdown = (
        (shutdown_time - now).seconds
        if is_not_weekend(now) and is_not_vacation(vacations, now)
        else  MINIMUM_SECONDS_TILL_SHUTDOWN
    )
Aber prinzipiell hast du recht, vielleicht denke ich aber einfach auch zu viel um die Ecke ;)

Danke jedenfalls für deine Korrekturen, werde ich so übernehmen!
BlackJack

@EmaNymton: Ich hätte es ja umgekehrt gemacht: Statt bei den `vacations` um „Nullzeiten” zu erweitern, beim Aufruf von `is_(not)_vacation()` mit ``now.date()` die Zeitinformation von `now` weglassen.

Ich sehe da keinen Unterschied zwischen Feier- und Ferientagen. Wie kommst Du darauf das die Herbstferien in NRW 2012 bis zum 21.10. gehen? Die Webseite sagt sie gehen bis zum Samstag den 20.10.: http://www.schulferien.org/NRW/nrw.html

Das sagt übrigens auch die Seite des Ministeriums für Schule und Weiterbildung des Landes Nordrhein-Westfalen: http://www.schulministerium.nrw.de/BP/S ... index.html

Ich denke es macht auch wenig Sinn in einem VEVENT für ganze Tage als Start- und Enddatum den gleichen Tag zu haben oder das Datum eines Endtages der noch zum Ereignis gehört. Denn nur das Datum bedeutet ja 00:00:00 für die Uhrzeitanteil. Da enden die Ereignisse aber nicht, denn die dauern ja noch den gesamten Tag an. Das Ende gibt den ersten Zeitpunkt an, an dem das Ereignis nicht mehr stattfindet. Am 20.10. um 23:59:59.999999999999… sind immer noch Ferien. Erst am 21.10. um 00:00:00 sind sie definitiv zu ende.
EmaNymton
User
Beiträge: 174
Registriert: Sonntag 30. Mai 2010, 14:07

Ok, du hast Recht, ich bin irgendwie immer davon ausgegangen, dass die Ferien bis Sonntag gehen. Liegt vielleicht daran, dass ich sonst Sonntags immer arbeite ;)
Antworten