diff --git a/.gitignore b/.gitignore index 3b655c6854..de3ae3333c 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,14 @@ Thumbs.db # Don't check in built documentation /doc/user/build /doc/user/source/plugins_doc +!/doc/user/source/plugins_doc/plugins_footer.rst +!/doc/user/source/plugins_doc/plugins_gateway_header.rst +!/doc/user/source/plugins_doc/plugins_interface_header.rst +!/doc/user/source/plugins_doc/plugins_protocol_header.rst +!/doc/user/source/plugins_doc/plugins_system_header.rst +!/doc/user/source/plugins_doc/plugins_unclassified_header.rst +!/doc/user/source/plugins_doc/plugins_all_header.rst + /doc/developer/build /doc/developer/source/plugins_doc diff --git a/bin/shngversion.py b/bin/shngversion.py index ff4e2b44aa..8fcf38bb77 100644 --- a/bin/shngversion.py +++ b/bin/shngversion.py @@ -27,7 +27,7 @@ sys.path.append('..') import plugins.__init__ as plugin_vers - +from lib.utils import Version # Update auf 1.3d wg. neuer item features on_update, on_change # Update auf 1.3e wg. neuer logic features for visu_websocket @@ -72,11 +72,14 @@ # Update auf 1.8.2a wg. Kennzeichnung des Stands als "nach dem v1.8.2 Release" # Update auf 1.8.2b wg. Erweiterung des Item Loggings" # Update auf 1.8.2c wg. Wegen Anpassungen an mem-logging / lib.log -# Update auf 1.8.2d Unterstützung für User-Functions +# Update auf 1.8.2d wg. Unterstützung für User-Functions # Update auf 1.9.0 wg. Release +# Update auf 1.9a wg. Kennzeichnung des Stands als "nach dem v1.9.0 Release" + +# Update auf 1.9.1 wg. Release -shNG_version = '1.9.0' +shNG_version = '1.9.1' shNG_branch = 'master' # --------------------------------------------------------------------------------- @@ -115,17 +118,21 @@ def _get_git_data(sub='', printout=False): # --------------------------------------------------------------------------------- def get_shng_main_version(): - return shNG_version + return Version.format( shNG_version ) + +def get_shng_plugins_version(): + plgversion = get_plugins_version().split('-')[0] + return Version.format( plgversion ) def get_shng_version(): commit, commit_short, branch, describe = _get_git_data() VERSION = get_shng_main_version() if branch == 'master': - VERSION += '.'+branch+' ('+commit_short+')' + VERSION += '-'+branch+' ('+commit_short+')' elif branch == 'manual': - VERSION += '.'+shNG_branch+' ('+branch+')' + VERSION += '-'+shNG_branch+' ('+branch+')' else: - VERSION += '.'+commit_short+'.'+branch + VERSION += '-'+commit_short+'.'+branch return VERSION def get_shng_branch(): @@ -139,7 +146,7 @@ def get_shng_description(): def get_plugins_version(): commit, commit_short, branch, describe = _get_git_data('plugins') - VERSION = get_shng_main_version() + VERSION = Version.format( get_shng_main_version() ) try: PLUGINS_VERSION = plugin_vers.plugin_release() except: @@ -150,16 +157,16 @@ def get_plugins_version(): PLUGINS_SOURCE_BRANCH = '' if branch == 'master': - VERSION = PLUGINS_VERSION - VERSION += '.'+branch+' ('+commit_short+')' + VERSION = Version.format( PLUGINS_VERSION ) + VERSION += '-'+branch+' ('+commit_short+')' elif branch == 'manual': - VERSION = PLUGINS_VERSION + VERSION = Version.format( PLUGINS_VERSION ) if PLUGINS_SOURCE_BRANCH != '': - VERSION += '.'+PLUGINS_SOURCE_BRANCH + VERSION += '-'+PLUGINS_SOURCE_BRANCH VERSION += ' ('+branch+')' else: - VERSION = PLUGINS_VERSION - VERSION += '.'+commit_short+'.'+branch + VERSION = Version.format( PLUGINS_VERSION ) + VERSION += '-'+commit_short+'.'+branch return VERSION diff --git a/bin/smarthome.py b/bin/smarthome.py index cc9c1a186b..6c79ba7998 100644 --- a/bin/smarthome.py +++ b/bin/smarthome.py @@ -112,7 +112,7 @@ from lib.shpypi import Shpypi shpypi = Shpypi.get_instance() if shpypi is None: - shpypi = Shpypi(base=BASE) + shpypi = Shpypi(base=BASE, version=VERSION) core_reqs = shpypi.test_core_requirements(logging=False, pip3_command=args.pip3_command) if core_reqs == 0: @@ -120,8 +120,21 @@ python_bin = sys.executable if ' ' in python_bin: python_bin = '"'+python_bin+'"' - command = python_bin + ' ' + os.path.join(BASE, 'bin', 'smarthome.py') + # if we didn't change the working dir (yet), for example... + # command = [python_bin] + sys.argv + command = [python_bin, os.path.join(BASE, 'bin', 'smarthome.py')] + + # if started with parameter to stay in foreground, don't fork + if args.foreground or args.interactive or args.debug: + try: + # function call doesn't return; this process is replaced by the new one + os.execv(python_bin, [python_bin] + sys.argv) + except OSError as e: + print(f'Restart command {command} failed with error {e}') + exit(0) + try: + command.append('-r') p = subprocess.Popen(command, shell=True) except subprocess.SubprocessError as e: print("Restart command '{}' failed with error {}".format(command,e)) @@ -199,7 +212,10 @@ def _reload_logics(): lib.backup.make_backup_directories(BASE) - if args.interactive: + if args.restart: + time.sleep(5) + lib.daemon.kill(PIDFILE, 30) + elif args.interactive: MODE = 'interactive' import code import rlcompleter # noqa @@ -234,10 +250,6 @@ def _reload_logics(): elif args.stop: lib.daemon.kill(PIDFILE, 30) exit(0) - elif args.restart: - time.sleep(5) - lib.daemon.kill(PIDFILE, 30) - pass elif args.debug: MODE = 'debug' elif args.quiet: diff --git a/dev/sample_plugin/webif/templates/index.html b/dev/sample_plugin/webif/templates/index.html index 421d07c4f6..a18d4aa19c 100644 --- a/dev/sample_plugin/webif/templates/index.html +++ b/dev/sample_plugin/webif/templates/index.html @@ -5,6 +5,17 @@ {% set update_interval = 0 %} + +{% block pluginstyles %} + +{% endblock pluginstyles %} + @@ -14,10 +25,9 @@ if (dataSet === 'devices_info' || dataSet === null) { var objResponse = JSON.parse(response); myProto = document.getElementById(dataSet); - for (var device in objResponse) { + for (item in objResponse) { /* - shngInsertText (device+'_source', objResponse[device]['source']); - shngInsertText (device+'_powerState', objResponse[device]['powerState']); + shngInsertText (item+'_value', objResponse[item]['value'], 'maintable'); */ } } @@ -29,22 +39,23 @@ --> {% endblock pluginscripts %} @@ -113,30 +124,21 @@ {% block bodytab1 %}
{{ _('Hier kommt der Inhalt des Webinterfaces hin.') }} - - + + +
- + {% for item in p.get_items() %} {% if p.has_iattr(item.conf, '') %} - - + + {% endif %} {% endfor %} diff --git a/doc/build_doc_local.sh b/doc/build_doc_local.sh index b5d306cf93..fbafed7f58 100644 --- a/doc/build_doc_local.sh +++ b/doc/build_doc_local.sh @@ -102,8 +102,8 @@ if [ "${KEEP_REPO,,}" != "true" ]; then fi echo echo Sollten die build Verzeichnisse bereits existieren, werden die alten -echo gebauten Versionen während des Skriptes gelöscht. Der Account unter dem s -echo diese Skript ausgeführt wird, muss Rechte zum anlegen von Verzeichnissen +echo gebauten Versionen während des Skriptes gelöscht. Der Account unter dem +echo dieses Skript ausgeführt wird, muss Rechte zum anlegen von Verzeichnissen echo in \'$DIR\' haben. echo if [ "${GIT_CHECKOUT,,}" == "true" ]; then @@ -171,6 +171,8 @@ if [ "${DOC,,}" == "developer" ] || [ "${DOC,,}" == "all" ]; then make html || exit echo echo Bau der Entwickler-Dokumentation ist abgeschlossen! + echo + echo Die Startseite ist: \'${DIR}/user/build/html/index.html\' fi if [ "${DOC,,}" == "user" ] || [ "${DOC,,}" == "all" ]; then @@ -185,6 +187,8 @@ if [ "${DOC,,}" == "user" ] || [ "${DOC,,}" == "all" ]; then make html || exit echo echo Bau der Anwender-Dokumentation ist abgeschlossen! + echo + echo Die Startseite ist: \'${DIR}/user/build/html/index.html\' fi diff --git a/doc/build_plugin_config_files.py b/doc/build_plugin_config_files.py index a336786df8..0bc017e4ab 100644 --- a/doc/build_plugin_config_files.py +++ b/doc/build_plugin_config_files.py @@ -736,6 +736,7 @@ def write_configfile(plg, configfile_dir, language='de'): global docu_type docu_type = start_dir.split('/')[-1:][0] # developer / user + print('Creating the configuration documentation pages for the plugins') print('Start directory = '+start_dir) print('Documentation type = '+docu_type) print('Documentation language = '+language) diff --git a/doc/build_plugin_rst_files.py b/doc/build_plugin_rst_files.py index 568fbaf411..f58c6271f4 100644 --- a/doc/build_plugin_rst_files.py +++ b/doc/build_plugin_rst_files.py @@ -75,8 +75,9 @@ ['interface', 'Interface', 'Interface'], ['protocol', 'Protocol', 'Protokoll'], ['system', 'System', 'System'], + ['web', 'Web/Cloud', 'Web/Cloud'], [type_unclassified, 'Non classified', 'nicht klassifizierte'], - ['web', 'Web/Cloud', 'Web/Cloud'] + ['all', 'All plugins', 'Alle Plugins'] ] def bold(s): @@ -153,6 +154,16 @@ def get_version(section_dict, maxlen=8): return lines +def get_state(section_dict, maxlen=20): + state = section_dict.get('state', '') + + #import textwrap + #lines = textwrap.wrap(state, maxlen, break_long_words=False) + #if lines == []: + # lines.append('') + #return lines + return state + def get_maintainer(section_dict, maxlen=20): maint = section_dict.get('maintainer', '') @@ -230,6 +241,7 @@ def build_pluginlist( plugin_type='all' ): plg_dict['name'] = metaplugin.lower() plg_dict['version'] = get_version(section_dict) plg_dict['type'] = plgtype + plg_dict['state'] = get_state(section_dict) plg_dict['desc'] = get_description(section_dict, 85, language) plg_dict['maint'] = get_maintainer(section_dict, 15) plg_dict['test'] = get_tester(section_dict, 15) @@ -237,6 +249,7 @@ def build_pluginlist( plugin_type='all' ): plg_dict['sup'] = html_escape(section_dict.get('support', '')) else: plgtype = type_unclassified + plg_dict['type'] = plgtype else: plgtype = type_unclassified if plugin_type == type_unclassified: @@ -285,7 +298,7 @@ def build_pluginlist( plugin_type='all' ): return result -def write_rstfile(plgtype='All', plgtype_print='', heading=''): +def write_rstfile(plgtype='all', plgtype_print='', heading=''): """ Create a .rst file for each plugin category """ @@ -314,7 +327,10 @@ def write_rstfile(plgtype='All', plgtype_print='', heading=''): fh.write('\n') if (len(plglist) == 0): - fh.write('At the moments there are no plugins that have not been classified.\n') + if language == 'de': + fh.write('Zurzeit gibt es keine noch nicht klassifizierten Plugins.\n') + else: + fh.write('At the moments there are no plugins that have not been classified.\n') else: if not fh_dummy_used: fh_dummy = open(plugin_rst_dir + '/' + rst_dummyname, "w") @@ -372,9 +388,10 @@ def write_rstfile(plgtype='All', plgtype_print='', heading=''): fh.write('\n') fh.write(' +-'+ '-'*65 +'-+-' + '-'*8 +'-+-' + '-'*165 +'-+-----------------+-----------------+\n') if language == 'de': - fh.write(' | {p:<65.65} | Version | {b:<165.165} | Maintainer | Tester |\n'.format(p='Plugin', b='Beschreibung')) + fh.write(f" | {'Plugin (Konfiguration)':<65.65} | Version | {'Beschreibung':<165.165} | Maintainer | Tester |\n") else: - fh.write(' | {p:<65.65} | Version | {b:<165.165} | Maintainer | Tester |\n'.format(p='Plugin', b='Description')) + fh.write(f" | {'Plugin (Configuration)':<65.65} | Version | {'Description':<165.165} | Maintainer | Tester |\n") + #fh.write(' | {p:<65.65} | Version | {b:<165.165} | Maintainer | Tester |\n'.format(p='Plugin', b='Description')) fh.write(' +='+ '='*65 +'=+=' + '='*8 +'=+=' + '='*165 + '=+=================+=================+\n') for plg in plglist: plg_readme_link = ':doc:`'+plg['name']+' `' @@ -406,7 +423,31 @@ def write_rstfile(plgtype='All', plgtype_print='', heading=''): else: plg['sup'] = "`"+plg['name']+" support <"+plg['sup']+">`_" fh.write(' | {plg:<65.65} | {vers:<8.8} | - {desc:<163.163} | {maint:<15.15} | {test:<15.15} |\n'.format(plg='', vers='', desc=plg['sup'], maint='', test='')) + else: + fh.write(f" | {'':<65.65} | {'':<8.8} | {'.':<165.165} | {'':<15.15} | {'':<15.15} |\n") + + leerzeileausgegeben = False + if plgtype == 'all': + if language == 'de': + desc = 'Plugin Typ: **' + plg['type'] + '**' + else: + desc = 'Plugin type: **' + plg['type'] + '**' + leerzeileausgegeben = True + fh.write(f" | {'':<65.65} | {'':<8.8} | {'':<165.165} | {'':<15.15} | {'':<15.15} |\n") + fh.write(' | {plg:<65.65} | {vers:<8.8} | {desc:<165.165} | {maint:<15.15} | {test:<15.15} |\n'.format(plg='', vers='', desc=desc, maint='', test='')) + + if plg['state'] in ['deprecated', 'develop']: + if language == 'de': + desc = 'Plugin Status: **' + plg['state'] + '**' + else: + desc = 'Plugin Status: **' + plg['state'] + '**' + if not leerzeileausgegeben: + fh.write(f" | {'':<65.65} | {'':<8.8} | {'':<165.165} | {'':<15.15} | {'':<15.15} |\n") + fh.write(f" | {'':<65.65} | {'':<8.8} | {'':<165.165} | {'':<15.15} | {'':<15.15} |\n") + fh.write(' | {plg:<65.65} | {vers:<8.8} | {desc:<165.165} | {maint:<15.15} | {test:<15.15} |\n'.format(plg='', vers='', desc=desc, maint='', test='')) + fh.write(' +-'+ '-'*65 +'-+-' + '-'*8 + '-+-' + '-'*165 +'-+-----------------+-----------------+\n') + fh.write('\n') fh.write('\n') diff --git a/doc/requirements.txt b/doc/requirements.txt index 07c78bf22d..469f711c28 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,5 +1,5 @@ sphinx>=3.1,<4 -sphinx-rtd-theme +sphinx-rtd-theme>=1.0 # create tabs for content for alternative ways to use like | Linux | MacOS | Windows | # https://sphinx-tabs.readthedocs.io/en/latest/ diff --git a/doc/user/Makefile b/doc/user/Makefile index cac6dedc36..7f2b8e6222 100644 --- a/doc/user/Makefile +++ b/doc/user/Makefile @@ -50,6 +50,7 @@ html: @test ! -L "source/plugins" && echo "creating a link to the plugin directory" && ln -s ../../../plugins source/plugins || echo "link to plugins directory already exists" @test ! -L "source/modules" && echo "creating a link to the module directory" && ln -s ../../../modules source/modules || echo "link to modules directory already exists" @test ! -L "source/dev" && echo "creating a link to the dev directory" && ln -s ../../../dev source/dev || echo "link to dev directory already exists" + @test ! -L "source/logics" && echo "creating a link to the logics directory" && ln -s ../../../logics source/logics || echo "link to logics directory already exists" @echo @python3 -V @python3 ../build_plugin_config_files.py de @@ -60,6 +61,7 @@ html: @test -L "source/dev" && echo "removing link to the dev directory" && unlink source/dev @test -L "source/modules" && echo "removing link to the modules directory" && unlink source/modules @test -L "source/plugins" && echo "removing link to the plugin directory" && unlink source/plugins + @test -L "source/logics" && echo "removing link to the logics directory" && unlink source/logics @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." @@ -71,6 +73,7 @@ pdf: @test ! -L "source/plugins" && echo "creating a link to the plugin directory" && ln -s ../../../plugins source/plugins || echo "link to plugins directory already exists" @test ! -L "source/modules" && echo "creating a link to the module directory" && ln -s ../../../modules source/modules || echo "link to modules directory already exists" @test ! -L "source/dev" && echo "creating a link to the dev directory" && ln -s ../../../dev source/dev || echo "link to dev directory already exists" + @test ! -L "source/logics" && echo "creating a link to the logics directory" && ln -s ../../../logics source/logics || echo "link to logics directory already exists" @echo @python3 -V @python3 ../build_plugin_config_files.py de @@ -81,6 +84,7 @@ pdf: @test -L "source/dev" && echo "removing link to the dev directory" && unlink source/dev @test -L "source/modules" && echo "removing link to the modules directory" && unlink source/modules @test -L "source/plugins" && echo "removing link to the plugin directory" && unlink source/plugins + @test -L "source/logics" && echo "removing link to the logics directory" && unlink source/logics @echo @echo "Build finished. The PDF files are in build/pdf." diff --git a/doc/user/source/_static/img/all.png b/doc/user/source/_static/img/all.png new file mode 100644 index 0000000000..c3ad05a451 Binary files /dev/null and b/doc/user/source/_static/img/all.png differ diff --git a/doc/user/source/admin/logiken.rst b/doc/user/source/admin/logiken.rst index 5a08c19847..b8e3614c81 100644 --- a/doc/user/source/admin/logiken.rst +++ b/doc/user/source/admin/logiken.rst @@ -10,7 +10,7 @@ Liste der Logiken ================= Unter **Logiken** wird eine Liste der konfigurierten Logiken mit einer Reihe von Informationen angezeigt. -Rechts in der jeweiligen Zeile sind Icons über die +Rechts in der jeweiligen Zeile sind Buttons, über die - eine Logik ausgelöst (getriggert) werden kann, - eine Logik neu geladen werden kann, @@ -18,6 +18,14 @@ Rechts in der jeweiligen Zeile sind Icons über die - eine Logik entladen werden kann, - eine Logik inclusive Code und Parametern gelöscht werden kann +Die Liste der geladenen Logiken ist nach dem Namen der Logik alphabetisch sortiert. + +Nicht geladene Logiken werden am unteren Ende der Liste angezeigt. Für nicht geladene Logiken stehen +nur zwei Buttons zum Auslösen von Aktionen zur Verfügung, über die + +- eine Logik geladen werden kann, +- eine Logik inclusive Code und Parametern gelöscht werden kann + Oberhalb der Liste findet sich ein Button, der die Erstellung einer neuen Logik ermöglicht. Auf einem 2. Tab kann eine Liste der Systemlogiken eingesehen werden. diff --git a/doc/user/source/beispiele/eval.rst b/doc/user/source/beispiele/eval.rst index fe4e380519..580d59852b 100644 --- a/doc/user/source/beispiele/eval.rst +++ b/doc/user/source/beispiele/eval.rst @@ -15,7 +15,7 @@ Hier finden sich einige Beispiele für die Anwendung von **eval** und **eval_tri Bearbeiten von Werten --------------------- -Wird einem Item ein neuer Wert zugewiesen, wird dieser erst einmal als **value** zwischengespeichert. +Wird einem Item ein neuer Wert zugewiesen, wird dieser erst einmal als **value** zwischengespeichert. Ist ein **eval** vorhanden, wird dies erst ausgeführt, bevor dem Item der Wert final zugewiesen wird. Das kann man sich für Nachbearbeitungen zu nutze machen, bspw. wenn der zugewiesene Wert zu viele Nachkommastellen hat. In folgenden Beispiel wird auf eine Nachkommastelle gerundet. @@ -30,7 +30,7 @@ Nachkommastellen hat. In folgenden Beispiel wird auf eine Nachkommastelle gerund Wertermittung durch Datenbankabfrage ------------------------------------ -Mit Hilfe eines **eval** kann auch eine Datenbankabfrage (Plugin **database**) realisiert werden. +Mit Hilfe eines **eval** kann auch eine Datenbankabfrage (Plugin **database**) realisiert werden. Im folgenden Beispiel wird täglich um 0:01 Uhr das **eval** ausgeführt und der Wert ermittelt. .. code-block:: yaml @@ -213,13 +213,13 @@ Das bool Item ist True, wenn der Wert des passenden num-Items > 0 ansonsten Fals Basierend auf dem Wert eines numerischen andere Items setzen ------------------------------------------------------------ -Bei dem folgenden Beispiel werden basierend auf dem Wert des Items "Sollzustand" die +Bei dem folgenden Beispiel werden basierend auf dem Wert des Items "Sollzustand" die Items "FolgeA", "FolgeB", "FolgeC" und "FolgeD" gesetzt. -Änderungen des Items "Sollzustand" löst für die Folgeitems den **eval_trigger** aus -und übergibt seinen Wert als "value" and diese. Im **eval** wird nun die Bedingung +Änderungen des Items "Sollzustand" löst für die Folgeitems den **eval_trigger** aus +und übergibt seinen Wert als "value" and diese. Im **eval** wird nun die Bedingung basierend auf "value" geprüft, und das Item entsprechend gesetzt. -Für das Item "FolgeA" bedeutet es konkret: Ändert sich das Wert von "Sollzustand", -wird die Neuberechnung des Items "FolgeA" angestoßen und der Wert von "Sollzustand" +Für das Item "FolgeA" bedeutet es konkret: Ändert sich das Wert von "Sollzustand", +wird die Neuberechnung des Items "FolgeA" angestoßen und der Wert von "Sollzustand" wird als "value" mit übergeben. Das eval ergibt True, wenn "value" einer 2 entspricht, ansonsten False. .. code-block:: yaml @@ -254,7 +254,7 @@ Berechnung einer Zeitdauer in Sekunden von beliebigen datetime bis jetzt In diesem Beispiel wird die Dauer eines **autotimer** mit einem **eval** aus einem **datetime** Wertes eines Hilfsitem berechnet. Die Berechnung des Item "laufzeit_autotimer" wird durch Änderungen im Item "enddatetime_autotimer" -getriggert und berechnet die Zeitdauer in Sekunden zwischen dem Wert (datetime im ISO-format) +getriggert und berechnet die Zeitdauer in Sekunden zwischen dem Wert (datetime im ISO-format) des Items "enddatetime_autotimer" und jetzt. Dieser errechnete Wert wird dann als Dauer für den **autotimer** verwendet. @@ -280,7 +280,7 @@ Dieser errechnete Wert wird dann als Dauer für den **autotimer** verwendet. Ab SmartHomeNG v1.7 gibt es hierfür eine Funktion, die in eval Attributen und Logiken verwendet werden kann: **shtime.time_since()** ermöglicht auch die Rückgabe in anderen Einheiten/Formaten als Sekunden. - Zur Verwendung der Funktionen bitte im Abschnitt Logiken unter :doc:`Feiertage, Daten und Zeiten ` + Zur Verwendung der Funktionen bitte im Abschnitt Referenz/Logiken unter :doc:`Feiertage, Daten und Zeiten ` nachschauen. Dort sind auch eine Reihe weiterer hilfreicher Funktionen beschrieben. @@ -321,7 +321,7 @@ siehe auch `Thread im knx-user-forum `` - * ``crontab: `` - * ``crontab: `` - - Dabei sind je nach Variante folgende Werte zulässig: - - * Sekunde: ``0`` bis ``59`` - * Minute: ``0`` bis ``59`` - * Stunde: ``0`` bis ``23`` - * Tag: ``1`` bis ``31`` - * Monat: 1 bis 12 oder ``jan`` bis ``dec`` - * Wochentag ``0`` bis ``6`` oder ``mon``, ``tue``, ``wed``, ``thu``, ``fri``, ``sat``, ``sun`` - - Alle Parameter müssen durch ein Leerzeichen getrennt sein und innerhalb eines Parameters - darf kein zusätzliches Leerzeichen vorhanden sein, sonst kann der Parametersatz nicht ausgewertet werden. - - Im folgenden Beispiel wird jeden Tag um 23:59 ein Trigger erzeugt und der Wert 70 gesetzt. - - .. code-block:: yaml - - crontab: 59 23 * * = 70 - - Für jede dieser Zeiteinheiten (Minuten, Stunde, Tag, Wochentag) werden - folgende Muster unterstützt (Beispiel jeweils ohne Anführungszeichen verwenden): - - * eine einzelne Zahl, z.B. ``8`` → immer zur/zum 8. Sekunde/Minute/Stunde/Tag/Wochentag - * eine Liste von Zahlen, z.B. ``2,8,16`` → immer zur/zum 2., 8. und 16. Sekunde/Minute/Stunde/Tag/Monat/Wochentag - * ein Wertebereich, z.B. ``1-5`` → immer zwischen dem/der 1. und 5. Sekunde/Minute/Stunde/Tag/Monat/Wochentag - * einen Interval, z.B. ``\*\/4`` → immer alle 4 Sekunden/Minuten/Stunden/Tage/Wochentage - * einen Stern, z.B. ``*`` → jede Sekunde/Minute/Stunde/Tag/Monat/Wochentag - - .. tab:: Zeitpunkte bezogen auf Aufgang von Sonne oder Mond - - Nach dem Muster ``[H:M<](sunrise|sunset|moonrise|moonset)[+|-][offset][ )`` kann ein Triggerpunkt bezogen - auf Sonne oder Mond berechnet werden: - - * ``sunrise`` → immer zum Sonnenaufgang - * ``sunset`` → immer zum Sonnenuntergang - * ``sunrise`` und untere Begrenzung → ``06:00`` zusätzlich geladen werden. - - - -Vorhandene Objekte und Methoden -------------------------------- - -Ausser dem Smarthome Objekt ``sh`` gibt es weitere vordefinierte Objekte und Methoden innerhalb von Logiken - -logic -~~~~~ - -Dieses Objekt bietet Zugriff auf das aktuelle Logik Objekt. -Es ist möglich die Attribute wie ``crontab``, ``cycle``, etc. während der Laufzeit zu ändern. -Die Änderungen werden aber nicht in die ``logic.yaml`` geschrieben und sind nach einem -Neustart von SmartHomeNG verloren. - -``logic.alive`` - Der Code ``while logic.alive:`` erzeugt eine Endlos-Schleife die bis zum Beenden - von SmartHomeNG läuft. - -``logic.name``: - Liefert den Namen der Logik wie in ``logic.yaml`` definiert - -``logic.last\_time()``: - Diese Funktion liefert die letzte Ausführungszeit der Logik vor dem aktuellen Aufruf - -``logic.prio``: - Lesen und setzen der Priorität dieser Logik - -``logic.trigger()``: - Wie ``sh.trigger()``, aber triggert nur die aktuelle Logik. Diese Funktion ist nützlich - um die Logik zu einem späteren Zeitpunkt (noch einmal) auszuführen - -trigger -~~~~~~~ - -Das Dictionary ``trigger`` liefert Informationen zur Laufzeit der Logik -über das Ereignis, das zum Aufruf der Logik geführt hat. - -``trigger['by']``: - Ursprung - -``trigger['source']`` - Quelle - -``trigger['dest']``: - Ziel - -``trigger['value']``: - Wert - -logger und sh.log ------------------ - -Das ``logger`` Objekt wird zum Generieren von Log Mitteilungen verwendet. -Es stellt fünf unterschiedliche Logging Einstufungen (level) bereit: -``debug``, ``info``, ``warning``, ``error`` und ``critical``. - -Die Angabe ``logger.info('42')`` schreibt einen Eintrag ``42`` in den zugehörigen Logger. -Damit Logging Einträge auch in Logs auftauchen, muß in der ``logging.yaml`` -eine entsprechende Logging Einstellung vorhanden sein. -Die Grundeinstellung in der ``logging.yaml`` sorgt dafür, das die Einstufungen -``warning``, ``error`` und ``critical`` aufgezeichnet werden. - -.. attention:: - - Die Zeitangabe in den Log Mitteilungen beziehen sich immer auf die lokal eingestellte Zeit - -SmartHomeNG Methoden --------------------- - -sh.now und sh.utcnow -~~~~~~~~~~~~~~~~~~~~ - -Diese beiden Funktionen geben ein ``datetime`` Objekt zurück das die lokale Zeitzone berücksichtigt. -Es ist möglich mit anderen Zeitzonen zu rechnen. -``sh.tzinfo()`` und ``sh.utcinfo()`` geben die lokale Zeitzone und die UTC Zeitzone zurück. - -sh.sun -~~~~~~ - -Dieses Modul gibt Zugriff auf das Sonnenobjekt. -Vorbedingung ist die Definition von Längen- und Breitengrad in ``smarthome.yaml``. - -``sh.sun.pos([offset], [degree=False])`` - Gibt die aktuelle Position der Sonne zurück, optional einen Offset in Minuten and und - ob der Rückgabe in Grad anstatt in Rad erfolgen soll - - ``azimut, altitude = sh.sun.pos()``: - liefert die aktuelle Position der Sonne - - ``azimut, altitude = sh.sun.pos(degree=True)``: - liefert die aktuelle Position der Sonne in Grad - - ``azimut, altitude = sh.sun.pos(30)`` - liefert die Position, die die Sonne in 30 Minuten haben wird + #!/usr/bin/env python3 + # test_logic.py -``sh.sun.set([offset])``: - Gibt den nächsten Sonnenuntergang zurück, optional mit einem Offset in Grad. + logger.warning("Start der Logik") - ``sunset = sh.sun.set()``: - Liefert ein auf UTC basierendes ``datetime`` Objekt mit dem nächsten Sonnenuntergang + Hier steht Müll - ``sunset_tw = sh.sun.set(-6)``: - Liefert ein auf UTC basierendes ``datetime`` Objekt mit der Zeitangabe - des nächsten Sonnenuntergangs zuzüglich der Zeit bis die Dämmerung beendet ist. + logger.warning("Ende der Logik") -``sh.sun.rise([offset])``: - Gibt analog zu ``set`` den nächsten Sonnenaufgang zurück, optional mit einem Offset in Grad. +erzeugt beim laden/übersetzen folgenden Logeintrag: -sh.moon -~~~~~~~ +.. code-block:: text -Neben den drei Funktionen ``pos``, ``set`` und ``rise`` (wie beim Objekt ``sh.sun``) gibt es noch -zwei weitere Funktionen: + 2022-01-24 12:51:26 ERROR lib.logic Exception: invalid syntax (test_logic.py, line 6) + Traceback (most recent call last): + File "/usr/local/shng_dev/lib/logic.py", line 1034, in _generate_bytecode + self.bytecode = compile(code, self.pathname, 'exec') + File "/usr/local/shng_dev/logics/test_logic.py", line 6 + Hier steht Müll + ^ + SyntaxError: invalid syntax -``sh.moon.light(offset)``: - liefert einen Wert im Bereich [0...100] der hellen Oberfläche zur aktuellen Zeit plus einen Offset -``sh.moon.phase(offset)``: - Liefert die Mondphase als Ganzzahl Wert im Bereich [0...7]: - 0 = Neumond - 4 = Vollmond - 7 = abnehmender Halbmond - - -Scheduler ---------- - -sh.scheduler.trigger() / sh.trigger() -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Diese globale Funktion triggert eine gegebene Logik unter Angabe ihres Namens - -``sh.trigger(name [, by] [, source] [, value] [, dt])`` - ``name``: - der Name der zu triggernden Funktion - - ``by``: - Name einer aufrufenden Logik, der Vorgabewert ist ``Logic`` - - ``source``: - der Grund für das triggern - - ``value`` - eine Variable - - ``dt``: - ein datetime Objekt das die Triggerzeit angibt (lokale Zeitzone berücksichtigt) - -sh.scheduler.change() -~~~~~~~~~~~~~~~~~~~~~ - -Diese Methode ändert Optionen zur Laufzeit der Logiken. Beispiel: - -``sh.scheduler.change('alarmclock', active=False)`` deaktiviert die Logik ``alarmclock`` - -Zusätzlich zum ``active`` parameter können auch ``cron`` und ``cycle`` geändert werden. - -sh.tools Objekt ---------------- - -Das ``sh.tools`` Objekt stellt folgende nützliche Funktionen zur Verfügung: - -sh.tools.ping() +Laufzeit Fehler ~~~~~~~~~~~~~~~ -Sendet ein Ping an einen Computer und liefert das Ergebnis. Beispiel: - -``sh.office.laptop(sh.tools.ping('hostname'))`` - -setzt das Item ``office.laptop`` entsprechend der Rückmeldung ob ein Ping erfolgreich war oder nicht. - -sh.tools.dewpoint() -~~~~~~~~~~~~~~~~~~~ - -Berechnet den Taupunkt für eine gegebene Temperatur und Feuchtigkeit. Beispiel: - -``sh.office.dew(sh.tools.dewpoint(sh.office.temp(), sh.office.hum())`` +Nachdem eine Logik erfolgreich geladen wurde, können bei der Ausführung der Logik Laufzeit Fehler auftreten. +Falls ein Laufzeitfehler nicht innerhalb der Logik abgefangen wird, führt er zur Beendigung des aktuellen +Laufes der Logik und es wird ein entsprechender Fehler geloggt. Die Logik bleibt jedoch geladen und wird, wenn sie +getriggert wird, erneut ausgeführt. -setzt das Item ``office.dew`` auf das Ergebnis der Taupunktberechnung der Itemwerte von ``office.temp`` und ``office.hum`` +Beispiel: Folgende Test Logik -sh.tools.fetch\_url() -~~~~~~~~~~~~~~~~~~~~~ - -Liefert dem Inhalt einer Webseite als String oder ``False`` wenn ein Fehler auftritt. +.. code-block:: python -``sh.tools.fetch_url('https://www.regular.com')`` + #!/usr/bin/env python3 + # test_logic.py -Es ist möglich als Parameter den Benutzernamen und ein Password anzugeben um die Abfrage bei der zu authentifizieren. + logger.warning("Start der Logik") -``sh.tools.fetch_url('https://www.special.com', 'username', 'password')`` + var = int('abc') -Weiterhin kann ein Parameter für eine Zeitüberschreitung bestimmt werden: + logger.warning("Ende der Logik") -``sh.tools.fetch_url('https://www.regular.com', timeout=4)`` -bricht nach 4 Sekunden ohne Ergebnis ab +erzeugt zur Laufzeit folgenden Logeintrag: -sh.tools.dt2ts(dt) -~~~~~~~~~~~~~~~~~~ +.. code-block:: text -Wandelt ein datetime Objekt in einen Unix Zeitstempel um. + 2022-01-24 13:55:14 ERROR logics.test_logic In der Logik ist ein Fehler aufgetreten: + Logik 'test_logic', Datei '/usr/local/shng_dev/logics/test_logic.py', Zeile 6 + Hauptroutine der Logik, Exception: invalid literal for int() with base 10: 'abc' -sh.tools.dt2js(dt) -~~~~~~~~~~~~~~~~~~ -Wandelt ein datetime Objekt in einen json Zeitstempel um. +Template für eine neue Logik +---------------------------- +Eine neue Logik sollte möglichst auf der folgenden Vorlage ``logic.tpl`` aufbauen. +Die Template Datei ist im Verzeichnis ``logics/`` abgelegt: -sh.tools.rel2abs(temp, hum) -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. literalinclude:: /logics/logic.tpl + :caption: /logics/logic.tpl + :language: python -Wandelt einen relativen Feuchtigkeitswert in einen absoluten Feuchtigkeitswert um. +Um zum Beispiel eine Logik ``example`` zu erzeugen, muss zuerst die Datei ``logics/logic.tpl`` auf die +Datei ``logics/example.py`` kopiert werden und in der erstellten Datei der eigentliche Code der Logik erfasst werden. -Zugriffe auf Items ------------------- +Anschließend muss noch in der Konfigurationsdatei ``etc/logic.yaml`` die Konfiguration für die Logik festgelegt +werden. -Die Nutzung des ``sh`` Objektes für Items wird nicht weitergeführt. Es ist besser das Item API wie folgt zu nutzen: +Die Minimalkonfiguration sieht für das Beispiel folgendermaßen aus: -.. code:: python +.. code-block:: yaml - from lib.item import Items - items = Items.get_instance() + example: + filename: example.py -Mit dem ``items`` Objekt können nun die folgenden Funktionen verwendet werden: +Der Name der Logik wird durch den Namen des Abschnitts ``example`` in der Konfigurationsdatei festgelegt. Unter diesem +Namen wird die Logik innerhalb von SmartHomeNG angesprochen. Der Parameter ``filename`` legt fest, in welcher +Datei der Code für die Logik ``example`` gespeichert ist. Dafür kann im Prinzip ein beliebiger Dateiname gewählt +werden, solange die Extension ``.py`` ist. Um mögliche Verwirrungen zu vermeiden, wird jedoch empfohlen den +Namen der Logik und den Namen der Datei gleich zu wählen. Generell sollten Dateinamen nur aus Kleinbuchstaben +bestehen, da je nach verwendetem Betriebssystem das Dateisystem evtl. Case sensitiv ist und ``Example.py`` eine +andere Datei sein kann als ``example.py``. -items.return_item(path) -~~~~~~~~~~~~~~~~~~~~~~~ +Erst mit der Existenz dieser Konfiguration kann die Logik gestartet werden. Die Logik würde zwar noch nicht durch +ein normaler Ereignis innerhalb von SmartHomeNG getriggert, da keine Trigger definiert sind, die Logik kann jedoch +aus der Admin GUI getriggert werden. -Liefert ein Item Objekt für den angegebenen Pfad zurück. Beispiel: +Wenn die Logik zum Beispiel beim Start von SmartHomeNG getriggert werden soll, so muss noch der folgende ``crontab`` +Parameter angebeben werden: -``items.return_item('first_floor.bath')`` +.. code-block:: yaml -items.return_items() -~~~~~~~~~~~~~~~~~~~~ + example: + filename: example.py + crontab: init -Liefert alle Item Objekte zurück -.. code-block:: python +Verlassen einer Logik +--------------------- - for item in items.return_items(): - logger.info(item.id()) +Normalerweise wird der aktuelle Lauf einer Logik dadurch beendet, dass das Ende des Codes der Hauptroutine +erreicht wird. In einigen Fällen kann es jedoch sinnvoll sein, die Logik mitten aus dem Code heraus oder as einer +Funktion heraus zu beenden/zu verlassen. -items.match_items(regex) -~~~~~~~~~~~~~~~~~~~~~~~~ +Dafür kann eine spezielle Exception ausgelöst werden. Das geschieht mit dem Befehl ``raise LeaveLogic()``. +Details hierzu sind in der :ref:`Referenz ` zu finden. -Liefert alle Item Objekte deren Pfad mit einem regulären Ausdruck gefunden wird und die optional ein bestimmtes Attribut aufweisen. -.. code-block:: python +Beispiele +========= - for item in items.match_items('*.lights'): # selects all items ending with 'lights' - logger.info(item.id()) - - for item in items.match_items('*.lights:special'): # selects all items ending with 'lights' and attribute 'special' - logger.info(item.id()) - -items.find_items(configattribute) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Die folgende Beispielkonfiguration definiert 4 Logiken: -Abhängig von ``configattribute`` werden die folgenden Items zurückgegeben: +* Die erste Logik mit Namen ``InitSmartHomeNG`` befindet sich in der Datei ``logics/initsmarthomeng.py``. + Das Attribut ``crontab: init`` weist SmartHomeNG an diese Logik direkt nach dem Start auszuführen. +* Die zweite Logik mit dem Namen ``Hourly`` befindet sich in der Datei ``logics/time.py`` + und das Attribut ``cycle: 3600`` weist SmartHomeNG an die Logik alle 3600 Sekunden (jede Stunde) auszuführen. +* Die dritte Logik mit Namen ``Gate`` befindet sich in der Datei ``logics/gate.py`` und das Attribut + ``watch_item: gate.alarm`` weist SmartHomeNG an die Logik auszuführen, wenn der Wert des Items gate.alarm sich ändert. +* Die vierte Logik mit Namen ``disks`` befindet sich in der Datei ``logics/disks.py``. Der crontab Eintrag weist SmartHomeNG an + diese Logik alle 5 Minuten auszuführen. Der Parameter ``usage_warning`` ist ein benutzerdefinierter Parameter, der + in der Logik verwendet wird, um einen Schwellwert zu definieren. -.. table:: +.. code-block:: yaml + :caption: etc/logic.yaml - ====================== ========================================================= - Attribut Ergebnis - ====================== ========================================================= - ``attribute`` Nur Items bei denen keine Instanz ID angegeben ist - ``attribute@`` Items mit oder ohne Instanz ID - ``attribute@instance`` Items mit einem bestimmten Attribut und einer Instanz ID - ``@instance`` Items mit einer bestimmten Instanz ID - ====================== ========================================================= + InitSmarthomeNG: + filename: initsmarthomeng.py + crontab: init + Hourly: + filename: time.py + cycle: 3600 -.. code:: python + Gate: + filename: gate.py + watch_item: gate.alarm # monitor for changes - for item in items.find_items('my_special_attribute'): - logger.info(item.id()) + disks: + filename: disks.py + # 'crontab: run at start and every 5 minutes' + crontab: + - init + - '0,5,10,15,20,25,30,35,40,45,50,55 * * *' + usage_warning: 500 -find\_children(parentitem, configattribute): -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Liefert alle Kind Item Objekte eines Elternitems mit einem gegebenen ``configattribute``. -Die Suche nach dem ``configattribute`` wird genauso durchgeführt wie in ``find_items(configattribute)`` weiter oben. diff --git a/doc/user/source/entwicklung/plugins/webinterface_automatic_update.rst b/doc/user/source/entwicklung/plugins/webinterface_automatic_update.rst index 8c42472d71..453b252371 100644 --- a/doc/user/source/entwicklung/plugins/webinterface_automatic_update.rst +++ b/doc/user/source/entwicklung/plugins/webinterface_automatic_update.rst @@ -67,7 +67,7 @@ Die Klasse ``WebInterface`` im Plugin Code muss so erweitert werden, dass sie di Die optionale Möglichkeit einen ``dataSet`` anzugeben, ist für zukünftige Erweiterungen vorgesehen. -Darüber soll es möglich werden, Daten in unterschiedlichen Zyklen zu aktualisieren +Darüber soll es möglich werden, Daten in unterschiedlichen Zyklen zu aktualisieren (z.B. für Daten, deren Ermittlung eine längere Zeit in Anspruch nimmt). @@ -100,7 +100,7 @@ Bei Tabellen werden die einzelnen Datenzeilen beim Rendern durch die for-Schleif {% block **bodytab1** %}
-
{{ _('Item') }}{{ _('Wert') }}{{ _('Wert') }}
{{ item._path }}{{ item() }}{{ item._path }}{{ item() }}
+
@@ -149,13 +149,13 @@ dass die ID in Wertetabellen eindeutig sind, wird die for-Schleifenvariable (hie {% block **bodytab1** %}
-
{{ _('Item') }}
+
- - - - + + + + @@ -183,7 +183,7 @@ Das Webinterface ruft regelmäßig eine Methode des Plugins auf, um aktualisiert Wenn die Daten empfangen wurden, werden sie an die JavaScript-Funktion ``handleUpdatedData()`` der Webseite übergeben. Diese Funktion weist dann die neuen Daten den jeweiligen DOM-Elementen zu. -Die Funktion ``handleUpdatedData()`` ist im Block ``pluginscripts`` des HTML-Templates definiert. +Die Funktion ``handleUpdatedData()`` ist im Block ``pluginscripts`` des HTML-Templates definiert. Das folgende Beispiel weist die neuen Daten dem oben vorgestellten
{{ _('Item') }}{{ _('Typ') }}{{ _('knx_dpt') }}{{ _('Wert') }}{{ _('Item') }}{{ _('Typ') }}{{ _('knx_dpt') }}{{ _('Wert') }}
-Element des ``headtable`` zu: .. code-block:: html+jinja @@ -194,7 +194,7 @@ Das folgende Beispiel weist die neuen Daten dem oben vorgestellten -Element if (dataSet === 'devices_info' || dataSet === null) { var objResponse = JSON.parse(response); - shngInsertText('fromip', objResponse['fromip']) + shngInsertText('fromip', objResponse['fromip']); } } @@ -212,7 +212,9 @@ Das nächste Beispiel befüllt dazu analog die -Elemente der Zeilen in der T var objResponse = JSON.parse(response); for (var item in objResponse) { - shngInsertText(item+'_value', objResponse['item'][item]['value']) + shngInsertText(item+'_value', objResponse['item'][item]['value']); + // bei Tabellen mit datatables Funktion sollte die Zeile lauten: + // shngInsertText(item+'_value', objResponse['item'][item]['value'], 'maintable'); } } } @@ -220,6 +222,56 @@ Das nächste Beispiel befüllt dazu analog die -Elemente der Zeilen in der T {% endblock pluginscripts %} +Sortierbare Tabellen +-------------------- + +Wie erwähnt muss für das Aktivieren von sortier- und durchsuchbaren Tabellen der entsprechende Script-Block +wie in :doc:`Das Webinterface mit Inhalt füllen ` +unter Punkt 3 beschrieben eingefügt werden. Dabei ist auch zu beachten, dass der zu sortierenden +Tabelle eine entsprechende ID gegeben wird (im Beispiel oben ``#maintable``) und die Klasse ``display`` +ergänzt wird. + +Damit die neuen Daten auch von datatables.js erkannt und korrekt sortiert werden, ist es wichtig, +dem Aufruf ``shngInsertText`` die Tabellen-ID als dritten Parameter mitzugeben (im Beispiel 'maintable'). + +Standardmäßig werden die Spalten automatisch so skaliert, dass sich den Inhalten anpassen. Möchte man +bestimmten Spalten eine konkrete Breite vorgeben, sollte im Block ``pluginstyles`` entsprechender +Code eingefügt werden. Außerdem sind die ```` Tags natürlich mit den entsprechenden Klassen zu bestücken. +Zusätzlich/alternativ kann die Tabelle auch mit dem css Style ``table-layout: fixed;`` versehen werden, +um die dynamische Anpassung der Spaltenbreite gänzlich zu unterbinden. + +.. code-block:: css+jinja + + {% block pluginstyles %} + + {% endblock pluginstyles %} + + +Sollte der Inhalt einer Spalte erwartungsgemäß sehr breit sein, kann die Spalte stattdessen auch +als ausklappbare Informationszeile konfiguriert werden. Dann empfiehlt es sich auf jeden Fall, +wie oben angegeben, die Tabelle per CSS unsichtbar zu machen. Die datatables.js defaults sorgen dafür, +dass die Tabelle nach der kompletten Inititalisierung angezeigt wird. Dadurch wird ein +mögliches Flackern der Seite beim Aufbau verhindert. Die Deklaration der Tabelle im pluginscripts +Block hat dabei wie folgt auszusehen, wobei bei ``targets`` die interne Nummerierung der Spalten +anzugeben ist (0 wäre die erste Tabellenspalte, 2 die zweite, etc.). + +.. code-block:: html+jinja + + table = $('#maintable').DataTable( { + "columnDefs": [{ "targets": 0, "className": "none"}].concat($.fn.dataTable.defaults.columnDefs) + } ); + + Festlegen des Aktualisierungsintervalls --------------------------------------- @@ -234,4 +286,3 @@ Diese wird auf den gewünschten Wert in Millisekunden gesetzt. Dabei muss sicher .. warning:: Das Intervall darf nicht zu klein sein. Die Dauer **MUSS** länger sein als die notwendige Zeit zur Ausführung der Python-Methode ``get_data_html()``. - diff --git a/doc/user/source/entwicklung/plugins/webinterface_filling_webinterface.rst b/doc/user/source/entwicklung/plugins/webinterface_filling_webinterface.rst index 9fc0e2b688..a3dac70967 100644 --- a/doc/user/source/entwicklung/plugins/webinterface_filling_webinterface.rst +++ b/doc/user/source/entwicklung/plugins/webinterface_filling_webinterface.rst @@ -87,29 +87,29 @@ Die folgenden Schritte dienen dazu, das Webinterface mit Leben zu füllen: - 3. Folgender Script Code muss zwischen ``{% block pluginscripts %}`` und + 3. Folgender Scriptcode muss zwischen ``{% block pluginscripts %}`` und ``{% endblock pluginscripts %}`` eingefügt werden, um ein Filtern und Sortieren der Tabellen zu ermöglichen. - Der Code ``$('#maintable').DataTable( { "paging": false, fixedHeader: true } );`` + Der Code ``$('#maintable').DataTable( {} );`` muss für jede Tabelle, für die Filtern/Sortieren ermöglicht werden soll, kopiert werden. - Dabei ist sicher zu stellen, dass die ID (#maintable) jeweils richtig angepasst wird: + Dabei ist sicher zu stellen, dass die ID (#maintable) jeweils richtig angepasst wird. + Den entsprechenden ```` Tags sind die entsprechenden ids zu vergeben, außerdem sollte die + CSS Klasse ``display`` hinzugefügt werden. + Beispiel: ``
``. .. code-block:: HTML - + 4. Das Logo oben links auf der Seite wird automatisch durch das Logo des konfigurierten Plugin-Typs ersetzt. Wenn das Webinterface ein eigenes Logo mitbringen soll, muss das entsprechende Bild im Verzeichnis ``webif/static/img`` mit dem Namen ``plugin_logo`` abgelegt sein. Die zulässigen Dateiformate sind **.png**, **.jpg** oder **.svg**. Dabei sollte die Größe der Bilddatei die Größe des angezeigten Logos (derzeit ca. 180x150 Pixel) nicht überschreiten, um unnötige Datenübertragungen zu vermeiden. diff --git a/doc/user/source/index.rst b/doc/user/source/index.rst index 19069cb7fb..7a672571e2 100644 --- a/doc/user/source/index.rst +++ b/doc/user/source/index.rst @@ -52,9 +52,9 @@ oder im `Chat auf gitter.im `_ . beispiele/beispiele.rst tools/tools.rst fehlersuche - faq - referenz/referenz entwicklung/entwicklung + referenz/referenz + faq release/release genindex impressum diff --git a/doc/user/source/installation/komplettanleitung/02_smarthomeng.rst b/doc/user/source/installation/komplettanleitung/02_smarthomeng.rst index a1f346c6dd..722ad3e42f 100644 --- a/doc/user/source/installation/komplettanleitung/02_smarthomeng.rst +++ b/doc/user/source/installation/komplettanleitung/02_smarthomeng.rst @@ -25,16 +25,7 @@ Zunächst müssen einige zusätzlichen Pakete installiert werden: .. code-block:: bash - sudo apt-get -y install dialog python3 python3-dev python3-setuptools unzip git-core build-essential - sudo apt-get install python3-pip - - - Dann sollte evtl. noch Pythons Paketmanager PIP auf den neuesten Stand gebracht werden. Die installierte - Version von PIP sollte jedoch bereits ausreichend sein: - - .. code-block:: bash - - python3 -m pip install --upgrade pip + sudo apt-get -y install dialog python3 python3-dev python3-setuptools unzip git-core build-essential python3-pip SmartHomeNG Quellcode laden @@ -103,7 +94,7 @@ Weitere Python Bibliotheken installieren Dieser Mechanismus sorgt auch dafür, dass Pakete die von später konfigurierten Plugins benötigt werden, automatisch nachinstalliert werden. - .. tab:: SmartHomeNG vor v1.7 + .. tab:: SmartHomeNG vor v1.7 Für den ersten Start müssen noch einige Python Packages nachgeladen werden. Im Unterordner ``requirements`` befindet sich dafür eine Datei ``base.txt``. diff --git a/doc/user/source/konfiguration/konfigurationsdateien/holidays.rst b/doc/user/source/konfiguration/konfigurationsdateien/holidays.rst index b1bec1e26c..03c76a4319 100644 --- a/doc/user/source/konfiguration/konfigurationsdateien/holidays.rst +++ b/doc/user/source/konfiguration/konfigurationsdateien/holidays.rst @@ -94,6 +94,6 @@ etwa so aussehen: :language: yaml -Im Abschnitt **Logiken** ist auf der Seite :doc:`Feiertage, Daten und Zeiten ` +Im Abschnitt **Logiken** ist auf der Seite :doc:`Feiertage, Daten und Zeiten ` beschrieben, wie die hier konfigurierten Feiertage in Logiken und **eval** Attributen genutzt werden können. diff --git a/doc/user/source/konfiguration/logging.rst b/doc/user/source/konfiguration/logging.rst index a8a34bd973..0409c2e646 100644 --- a/doc/user/source/konfiguration/logging.rst +++ b/doc/user/source/konfiguration/logging.rst @@ -1,6 +1,6 @@ :tocdepth: 2 -..index:: Logging +.. index:: Logging ####### Logging @@ -10,22 +10,22 @@ Ein `Log `_ zeichnet Ergebnisse von Vorgänge und dient der Dokumentation. Anhand eines Logs kann man Programmfehlern auf die Spur kommen oder bestimmte Situationen können im Nachhinein untersucht werden. Je detaillierter ein Log geführt wird, desto einfacher ist die Untersuchung bestimmter Sachverhalte. -Je nachdem, was man untersuchen möchte, kann man mit einem **Logging Level** im Programm vorgeben wie ernst +Je nachdem, was man untersuchen möchte, kann man mit einem **Logging Level** im Programm vorgeben wie ernst oder wie wichtig ein bestimmter Logeintrag ist. -Innerhalb des Kerns von SmartHomeNG finden sich zum Beispiel Einträge im Programm mit dem Log Level **NOTICE** +Innerhalb des Kerns von SmartHomeNG finden sich zum Beispiel Einträge im Programm mit dem Log Level **NOTICE** die in einer Logdatei dann im Ergebnis so aufgezeichnet werden: ``2021-04-16 21:56:31 NOTICE lib.smarthome -------------------- Init SmartHomeNG 1.8.2c.4e0938c2.develop --------------------`` -Das ist als Information zu sehen um bei Problemen Hilfe zu erhalten. Es deutet hier nichts auf Fehler oder Probleme hin. +Das ist als Information zu sehen um bei Problemen Hilfe zu erhalten. Es deutet hier nichts auf Fehler oder Probleme hin. Ein anderer Logging Befehl im Core mit dem Log Level **WARNING** erzeugt hingehen folgendes: ``2021-04-16 21:56:32 WARNING lib.module Not loading module Mqtt from section 'mqtt': Module is disabled`` Das ist als Warnung gedacht um darauf hinzuweisen, das ein Module nicht geladen wird und in dieser Folge eventuell weitere Fehler oder Probleme auftauchen könnten. Steigerungen von Warnungen sind Log Level **ERROR** oder **CRITICAL**. -Während ein **ERROR** also ein Fehler durchaus bedeuten kann das SmartHomeNG weiterarbeiten kann, bedeutet ein **CRITICAL** -also ein kritischer Fehler das das Programm beendet werden muss. +Während ein **ERROR** also ein Fehler durchaus bedeuten kann das SmartHomeNG weiterarbeiten kann, bedeutet ein **CRITICAL** +also ein kritischer Fehler das das Programm beendet werden muss. Fehlt ein für den Kern von SmartHomeNG benötigtes Modul, so stell das einen kritischen Fehler dar. Die Log Level in der Übersicht, absteigend in der Bedeutung für den Programmablauf: @@ -51,7 +51,7 @@ Die Log Level in der Übersicht, absteigend in der Bedeutung für den Programmab - Warnung das etwas unerwartetes passiert ist aber trotzdem weitergearbeitet werden kann * - 20 - INFO - - Eine Ablaufinformation die nicht unbedingt wichtig ist + - Eine Ablaufinformation die nicht unbedingt wichtig ist * - DEBUG - 10 - Informationen für die Fehlersuche die normalerweise nicht benötigt werden @@ -68,14 +68,14 @@ verstanden werden sollen. Konfiguration des Loggings ========================== -Auf der Seite `Python Logging `_ +Auf der Seite `Python Logging `_ sind die Konfigurationsmöglichkeiten detailliert beschrieben. SmartHomeNG lädt beim Start die Konfiguration des Logging aus der Datei **etc/logging.yaml**. Ist diese Datei nicht vorhanden, -so versucht SmartHomeNG die Datei **etc/logging.yaml.default** zu kopieren nach **etc/logging.yaml** und dann daraus +so versucht SmartHomeNG die Datei **etc/logging.yaml.default** zu kopieren nach **etc/logging.yaml** und dann daraus die Konfiguration des Loggings zu laden. -Wenn bei der Konfiguration des Loggings etwas schief geht, kann also jederzeit die Datei **etc/logging.yaml** gelöscht oder +Wenn bei der Konfiguration des Loggings etwas schief geht, kann also jederzeit die Datei **etc/logging.yaml** gelöscht oder besser umbenannt werden und wird dann beim nächsten Neustart durch den Inhalt der **etc/logging.yaml.default** frisch bereitgestellt. Ein Beispiel für **etc/logging.yaml.default** im Folgenden: @@ -180,6 +180,9 @@ in der `__init__` Methode instanziert werden. Das ist inzwischen nicht mehr notw Klasse erzeugt den Logger inzwischen selbst. Ein **import logging** ist nicht mehr notwendig und die Initialisierung des Loggers in der `__init__` Methode sollte auch weggelassen werden. +Der Logger konn/muss in der ``etc/logging,yaml`` konfiguriert werden. Der Name des Loggers ist ``plugins.``. + + Für die Entwickler von Logiken: Verwendet man zur Instanziierung einen eigenen Namen (nicht empfohlen), wie z.B. @@ -198,36 +201,17 @@ muss in der config auch dieser Name verwendet werden. Ohne `plugin.` oder `logic DWD: level: DEBUG +Standardmäßig haben Logiken bereits einen Logger, der in der ``etc/logging,yaml`` konfiguriert werden kann/muss. +Der Name des Loggers ist ``logics.``, wobei der Name der Logik, der in der Konfiguration festgelegte +Name ist und nicht der Name des Python Skriptes. Logging der Veränderung von Items --------------------------------- -Die Veränderung von Item Werten kann am einfachsten geloggt werden, indem bei dem Item das Attribut **log_change** gesetzt -wird und auf einen entsprechenden Item Logger verweist. Der Item Logger muss in der etc/logging.yaml mit Level INFO oder -DEBUG definiert sein. - -.. code-block:: yaml - :caption: items/items.yaml - - test: - item: - log_change: - - -und - -.. code-block:: yaml - :caption: etc/logging.yaml - - ...- - - logger: - items_: - level: INFO - handlers: [shng_details_file] - - ... +Ab Version 1.5 von SmartHomeNG erfolgt die Konfiguration des Loggings der Veränderung von Item Werten über +Standard Item Attribute, wie es :doc:`bei den Standardattributen ` +beschrieben ist. Best Practices diff --git a/doc/user/source/konfiguration/logiken.rst b/doc/user/source/konfiguration/logiken.rst index 8fa285a1eb..31c0dd4457 100644 --- a/doc/user/source/konfiguration/logiken.rst +++ b/doc/user/source/konfiguration/logiken.rst @@ -1,8 +1,8 @@ .. index:: Logiken; Konfiguration -####### +======= Logiken -####### +======= Zur Konfiguration einer Logik wird in der Datei **../etc/logic.yaml** ein Abschnitt für die Logik angelegt. Unter dem Namen dieses Abschnitts wird die Logik an anderen Stellen referenziert. @@ -10,46 +10,8 @@ Logik angelegt. Unter dem Namen dieses Abschnitts wird die Logik an anderen Stel In diesem Abschnitt muss SmartHomeNG mitgeteilt werden, welche Code Datei ausgeführt werden soll und unter welchen Umständen. Dazu werden die im Folgenden beschriebenen Parameter genutzt. +.. include:: /referenz/logiken/logiken_standardparameter.rst -Standard Parameter ------------------- - -+------------------+-----------------------------------------------------------------------------------------------+ -| **Parameter** | **Beschreibung** | -+==================+===============================================================================================+ -| filename | Dateiname des Logik-Codes. Diese Datei muss im Verzeichnis **../logics** liegen. Dieser | -| | Parameter muss angegeben werden. | -+------------------+-----------------------------------------------------------------------------------------------+ -| watch_item | **Optional**: String oder Liste von Strings, die jeweils einen Item-Pfad repräsentieren. | -| | Eine Veränderung eines der hier aufgeführten Items führt dazu, dass die Logik ausgeführt | -| | wird. | -+------------------+-----------------------------------------------------------------------------------------------+ -| crontab | **Optional**: String oder Liste von Strings, die einen crontab Eintrag darstellen. Der Syntax | -| | des Parameters entspricht dem Syntax des **crontab** Attributes von Items. Details dazu | -| | stehen :doc:`hier ` . | -+------------------+-----------------------------------------------------------------------------------------------+ -| cycle | **Optional**: Angabe einer Zykluszeit, die angibt, in welchem Zeitabstand die Logik | -| | periodisch ausgelöst werden soll. Der Syntax des Parameters entspricht dem Syntax des | -| | **cycle** Attributes von Items. Details dazu stehen | -| | :doc:`hier ` . | -+------------------+-----------------------------------------------------------------------------------------------+ -| prio | **Optional**: Angabe einer Priorität für die Logik. Die Priorität kommt nur bei Logiken zum | -| | Einsatz, die ein Schedule haben, bei denen also der Paramter **crontab** oder **cycle** | -| | angegeben wurde. Die Priorität sollte zwischen **2** und **6** liegen. Falls der Parameter | -| | nicht angegeben wird, wird die Standardpriorität **3** verwendet. | -+------------------+-----------------------------------------------------------------------------------------------+ -| visu_acl | **Optional**: Dieser Parameter wird durch das Plugin **visu_websocket** | -| | implementiert. Wenn dieser Parameter auf **True** gesetzt wird, kann die Logik | -| | von einer Visualisierung aus (z.B. smartVISU) ausgelöst werden. | -+------------------+-----------------------------------------------------------------------------------------------+ -| | **Optional**: Es können weitere Parameter definiert werden, diese können aus der | -| | Logik heraus abgefragt werden und haben sonst keine Funktion. | -+------------------+-----------------------------------------------------------------------------------------------+ - -Falls keiner der optionalen Parameter **crontab**, **watch_item** oder **cycle** angegeben wird, wird -die Logik nicht automatisiert ausgeführt. Sie kann dann nur aus Plugins oder (falls konfiguriert) über -eine Visualisierung ausgelöst werden. - - -Details zur Erstellung von Logiken finden sich :doc:`hier <../logiken/logics>` . +Weitere Informationen und Details zur Erstellung von Logiken finden sich unter :doc:`Referenz/Logiken ` +und unter :doc:`Entwicklung/Logiken `. diff --git a/doc/user/source/konfiguration/userfunctions.rst b/doc/user/source/konfiguration/userfunctions.rst index a8389aa69c..dc05e21182 100644 --- a/doc/user/source/konfiguration/userfunctions.rst +++ b/doc/user/source/konfiguration/userfunctions.rst @@ -3,6 +3,11 @@ .. role:: greensup .. role:: redsup +.. index:: Konfiguration; Userfunctions +.. index:: Konfiguration; uf +.. index:: Konfiguration; Functions +.. index:: Userfunctions; Konfiguration + =========================== Userfunctions :redsup:`Neu` =========================== diff --git a/doc/user/source/logiken/logics.rst b/doc/user/source/logiken/logics.rst index b9ee90bd8b..902c7bad0f 100644 --- a/doc/user/source/logiken/logics.rst +++ b/doc/user/source/logiken/logics.rst @@ -12,595 +12,26 @@ Logiken ####### -Logiken für SmartHomeNG sind Python Skripte. Zur Erstellung von Logiken müssen Sie über -Kenntnisse der Programmiersprache Python verfügen. +Logiken dienen dazu, komplexere Zustände und Abläufe zu steuern, die über Item Attribute nicht abgebildet werden +können. Logiken für SmartHomeNG sind Python Skripte. Zur Erstellung von Logiken müssen Sie über Kenntnisse +der Programmiersprache Python verfügen. -Die Logik-Skripte müssen im Verzeichnis **../logics** der SmartHomeNG Installation abgelegt werden. - -===================== -Grundlegende Struktur -===================== - -Das wichtigste Objekt, das in Logiken verwendet wird, ist **sh**. Dies ist das Smarthome-Objekt. -Es enthält jedes Detail über die laufende SmartHomeNG Instanz. Mit diesem Objekt ist es möglich auf -alle Items, Plugins und Grundfunktionen von SmartHomeNG zuzugreifen. - -Zugriff auf Items und Werte -=========================== - -Um den Wert eines Items zu erhalten, rufen Sie zum Beispiel den Namen auf: sh.path.item(). -Um einen neuen Wert zu setzen, geben Sie ihn einfach als Argument an: sh.path.item(neuer_wert). - -.. attention:: - - Zuweisung von Item-Werten: - - Es ist sehr wichtig, immer mit Klammern **()** auf die Items zuzugreifen! Wenn das Item direkt - zugewiesen wird, z.B. mit sh.path.item = Wert, dann wird das item-Objekt in SmartHomeNG überschrieben. - - In diesem Fall kann die mitgelieferte Logik **check_items.py** verwendet werden, um auf Vorhandensein - entsprechend beschädigter Items zu prüfen und diese wiederherzustellen. Alternativ werden die Items nach - einem Neustart von SmartHomeNG neu erstellt. - - -Alternativ kann auch über die Item-Properties auf den Wert zugegriffen werden: sh.path.item.propery.value -gibt den Wert zurück, mit sh.path.item.property.value = Wert kann der Wert zugewiesen werden. Diese Variante -lässt sich wie eine normale Variablenzuweisung nutzen. - -Beispiel -======== - -Eine Logik sieht prinzipiell folgendermaßen aus: - -.. code-block:: python - :caption: /usr/local/smarthome/logics/testlogik1.py - - #!/usr/bin/env python3 - # testlogik1.py - - #Code der Logik: - - # Das Deckenlicht im Büro einschalten, falls es nicht eingeschaltet ist - if not sh.buero.deckenlicht(): - sh.buero.deckenlicht('on') - - -=============================== -Verfügbare Objekte und Methoden -=============================== - - -.. toctree:: - :maxdepth: 3 - :hidden: - :titlesonly: - - objekteundmethoden_logging - objekteundmethoden_feiertage_datum_zeit - objekteundmethoden_sonne_mond - objekteundmethoden_item_methoden - objekteundmethoden_scheduler - objekteundmethoden_tools - - -Neben dem **sh** Objekt, gibt es andere wichtige vordefinierte Objekte: - - -Das logic Objekt -================ - -Dieses Objekt bietet Zugriff auf das aktuelle Logikobjekt. Es ist möglich, während der Laufzeit -logische Attribute (crontab, cycle, ...) abzufragen und ändern. Diese Änderungen gehen nach dem Neustart -von SmartHomeNG verloren. - -Definierte Methoden des Logikobjekts: - -+-------------------+--------------------------------------------------------------------------------------------------------+ -| Methode | Erläuterung | -+===================+========================================================================================================+ -| logic.id() | Diese Methode liefert dem Namen der Logik wie in **../etc/logic.yaml** angegeben. | -+-------------------+--------------------------------------------------------------------------------------------------------+ -| logic.last_run() | Diese Methode liefert den letzten Lauf dieser Logik (vor aktuellen Lauf). | -+-------------------+--------------------------------------------------------------------------------------------------------+ -| logic.disable() | Konfigurierte Logiken sind standardmäßig aktiv und werden entsprechend der Konfiguration ausgeführt. | -| | Diese Methode deaktiviert die Logik, sodass deren Ausführung unterbunden wird. (Ab SmartHomeNG v1.3) | -+-------------------+--------------------------------------------------------------------------------------------------------+ -| logic.enable() | Eine bereits deaktivierte Logik kann mit dieser Methode wieder aktiviert werden. (Ab SmartHomeNG v1.3) | -+-------------------+--------------------------------------------------------------------------------------------------------+ - - -Vordefinierte Attribute des Logikobjekts: - -+---------------------------+--------------------------------------------------------------------------------------------------------+ -| Attribut | Erläuterung | -+===========================+========================================================================================================+ -| trigger[] | Ein Python-Dictionary, welches im Folgenden beschreiben wird. | -+---------------------------+--------------------------------------------------------------------------------------------------------+ -| logic.name | Das Attribut logic.name liefert das selbe Ergebnis wie die Methode logic.id() | -+---------------------------+--------------------------------------------------------------------------------------------------------+ -| logic.crontab | Das Attribut liefert das aktuelle **crontab** Setting dieser Logik. | -+---------------------------+--------------------------------------------------------------------------------------------------------+ -| logic.cycle | Das Attribut liefert das aktuelle **cycle** Setting dieser Logik. | -+---------------------------+--------------------------------------------------------------------------------------------------------+ -| logic.prio | Das Attribut liefert das aktuelle **prio** Setting dieser Logik. | -+---------------------------+--------------------------------------------------------------------------------------------------------+ -| logic.filename | Das Attribut liefert den Dateinamen des Python Skripts dieser Logik. | -+---------------------------+--------------------------------------------------------------------------------------------------------+ -| logic. | Liefert den konfigurierten Parameter oder den Wert einer in einem vorherigen Lauf dieser | -| | Logik persistieren Variablen. | -+---------------------------+--------------------------------------------------------------------------------------------------------+ - - -trigger Dict -============ - -trigger ist ein Python-Dictionary, welches als Laufzeitumgebung einige Informationen über das -Ereignis liefert, das die Logik ausgelöst hat. - -Das Dictionary enthält folgende Informationen: - -+---------------------------+--------------------------------------------------------------------------------------------------------+ -| Attribut/Funktion | Erläuterung | -+===========================+========================================================================================================+ -| trigger['by'] | Auslöser ('Scheduler', Item', etc.) | -+---------------------------+--------------------------------------------------------------------------------------------------------+ -| trigger['source'] | enthält den Pfad des Items, welches die Logik getriggert hat. | -+---------------------------+--------------------------------------------------------------------------------------------------------+ -| trigger['source_details'] | Falls eine Logik aus einem Item heraus getriggert wurde (also trigger['by'] == Item ist), enthält | -| | trigger['source_details'] weitere Details zum Auslöser (Beispiel: 'knx:1.1.241:ga=3/3/5') | -+---------------------------+--------------------------------------------------------------------------------------------------------+ -| trigger['dest'] | | -+---------------------------+--------------------------------------------------------------------------------------------------------+ -| trigger['value'] | enthält den Wert des Items, dass die Logik getriggert hat. | -+---------------------------+--------------------------------------------------------------------------------------------------------+ - - -Das logics Objekt -================= - -Zugriff auf das Logics-API über das logics Objekt: - -+---------------------------------+---------------------------------------------------------------------------------------------------------+ -| Methode | Erläuterung | -+=================================+=========================================================================================================+ -| logics. | ermöglicht den Zugriff auf das Logics API, welches in der Developer Dokumentation beschrieben ist. | -| | Im folgenden sind einige Beispiele aufgeführt: | -+---------------------------------+---------------------------------------------------------------------------------------------------------+ -| logics.scheduler_add() | Hinzufügen eines Scheduler Eintrages für den logics-Namensraum. Der Syntax entspricht der | -| | scheduler.add() Methode. | -+---------------------------------+---------------------------------------------------------------------------------------------------------+ -| logics.scheduler_change() | Ändern eines Scheduler Eintrages im logics-Namensraum. Der Syntax entspricht der scheduler.change() | -| | Methode. | -+---------------------------------+---------------------------------------------------------------------------------------------------------+ -| logics.scheduler_remove() | Löschen eines Scheduler Eintrages im logics-Namensraum. Der Syntax entspricht der scheduler_remove() | -+---------------------------------+---------------------------------------------------------------------------------------------------------+ -| logics.trigger_logic() | Triggern einer im Logik | -+---------------------------------+---------------------------------------------------------------------------------------------------------+ -| logics.set_config_section_key() | Setzt den Wert eines Schlüssels für eine angegebene Logik (Abschnitt) permanent in ../etc/logic.yaml | -+---------------------------------+---------------------------------------------------------------------------------------------------------+ - -Der vollständige Syntax der Methoden kann der `Entwickler Dokumentation `_ entnommen werden. - - -Geladene Python Module -====================== - -Im Logik Environment sind diverse Python Module bereits geladen: - -- sys -- os -- time -- datetime -- ephem -- random -- Queue -- subprocess - - -============= -Konfiguration -============= - -Details zur Konfiguration von Logiken finden sich :doc:`hier <../konfiguration/logiken>` . - - -.. index:: Logiken; Persistente Variablen - -===================== -Persistente Variablen -===================== - -Normale Variablen innerhalb von Logiken sind nur für den jeweiligen Lauf gültig. Es ist jedoch -in einigen Fällen notwendig, Werte zwischen verschiedenen Läufen einer Logik zu übergeben. - -Solche persistente Variablen sind in Logiken von SmartHomeNG möglich, es sind jedoch einige -Dinge zu beachten: - -- Diese Variablen sind über mehrere Läufe von Logiken persistent. Allerdings gilt das nur während - des Laufs von SmartHomeNG. Bei einem Neustart von SmartHomeNG gehen diese Werte verloren. - Der Wert einer solchen Variablen geht auch verloren, wenn die Logik über das Backend während - der Laufzeit von SmartHomeNG gespeichert und neu geladen wird. -- Beim 1. Lauf einer Logik nach dem Start von SmartHomeNG existieren diese Variablen nicht. Der - erste Zugriff innerhalb einer Logik muss deshalb in einen **if not hasattr():** Ausdruck - eingebunden werden. -- Diese Variablen sind **lokal zur Logik**. Sie stehen außerhalb der Logik, die sie definiert hat, - nicht zur Verfügung. Wenn die Daten auch außerhalb der Logik verwendet werden, müssen sie in - Items abgelegt werden. (Ein Sonderfall sind Werte, die zwar außerhalb der definierenden Logik - verwendet werden sollen, aber nur in Logiken. Hier gibt es eine weitere Möglichkeit, die - weiter unten beschrieben wird). -- Es dürfen einige Namen nicht verwendet werden, da sie interne Variablen des Logik Objektes - überschreiben würden. - -Die Persistenz dieser Variablen gilt nur während des Laufs von SmartHomeNG. Mit der Beendigung von -SmartHomeNG gehen die gespeicherten Werte verloren. - - -Einrichtung -=========== - -Normale Variablen sind lokal zum Lauf der der Logik. Eine Variable **myvar** die während eines -Laufes der Logik einen Wert zugewiesen bekommt, ist beim Beginn des nächsten Laufes nicht -definiert. - -Um eine Variable so zu definieren, dass sie die Zeit bis zum nächsten Lauf der Logik überdauert, -muss sie als Attribut zur Logik definiert werden. Also statt: - -.. code-block:: python - - myvar = 'my Value' - -muss die Variable folgendermaßen definiert werden: - -.. code-block:: python - - logic.myvar = 'my Value' - - -Die Variable **logic.myvar** übersteht die Zeit bis zum nächsten Lauf der Logik und sie steht -nur in der Logik zur Verfügung, die sie auch definiert hat. - - -Existenz einer Variablen sicherstellen -====================================== - -Wenn auf eine Variable zugegriffen wird bevor sie definiert wird, führt das zu einer Exception -und der Rest der Logik wird nicht ausgeführt. Beim ersten Lauf einer Logik nach einem Neustart -von SmartHomeNG existiert jedoch keine Variable aus vorangegangenen Läufen. Sie muss erstmal -definiert werden. Das kann zum Beispiel in der folgenden Form erfolgen, in der die Variable -**logic.myvar** mit dem Wert **None** initialisiert wird, falls sie nicht existiert - -.. code-block:: python - :caption: Sicherstellen, dass die Variable existiert - - if not hasattr(logic, 'myvar'): - logic.myvar = None - - -Nutzung selbst definierter Parameter -==================================== - -Es ist möglich eigene Parameter in der Datei **../etc/logic.yaml** zu definieren. Diese Parameter -stehen in der Logik unter **logic.** zur Verfügung. Diese Parameter können als -eine bereits initialisierte Variable verstanden/genutzt werden. Sie können in der Logik nicht -nur gelesen, sondern auch verändert werden. Diese Änderung geht wie beschrieben bei einem -Neustart von SmartHomeNG verloren. - - -.. attention:: - - Einschränkungen bei Variablennamen: - - Variablennamen dürfen nicht gleich einem Namen der Attribute sein, die im Abschnitt - **Verfügbare Objekte und Methoden** beschrieben sind. - - -Einrichtung (Logik übergreifend) -================================ - -Statt eine persistende lokale Variable einzurichten: - -.. code-block:: python - - :caption: persistent, lokal zu definierenden Logik - - logic.myvar = 'my Value' - - -kann eine Variable Logik-übergreifend eingerichtet werden. Dann ist als Präfix statt *logic.* -der Präfix *logics.* zu verwenden: - -.. code-block:: python - - :caption: persistent, für alle Logiken zugreifbar - - logics.myvar = 'my Value' - - -Analog zur lokalen persistenten Variable muss die Existenz folgendermaßen sichergestellt werden: - -.. code-block:: python - :caption: Sicherstellen, dass die Variable existiert - - if not hasattr(logics, 'myvar'): - logics.myvar = None - - -Unterschiede zu lokalen persistenten Variablen ----------------------------------------------- - -Eine einmal initialisierte Logik-übergreifende persistente Variable behält ihren Wert bis -zum Neustart von SmartHomeNG. - -.. attention:: - - Da die Logik-übergreifende Variable ihren Wert auch behält, wenn die Logik die sie initialisiert hat - neu geladen wird, kann es zu unerwarteten Ergebnissen kommen, da sich die Logik nun evtl. bei einem - Neustart der Logik anders verhält, als beim Neustart von SmartHomeNG! - - - -.. index:: Funktionen; Logiken -.. index:: Logiken; Funktionen - -====================== -Nutzung von Funktionen -====================== - -Bei der Nutzung von Funktionen in Logiken ist eine Besonderheit zu beachten: - -Eine Logik verhält sich nicht wie ein Python Modul! Variablen und Funktionen die auf Ebene der Logik definiert werden, -sind keine globalen Objekte. Sie stehen in Funktionen die innerhalb der Logik definiert werden nicht zur Verfügung. -Daher müssen Variablen und Funktionen die innerhalb von Funktionen genutzt werden, der Funktion explizit bekannt gemacht +Die Logik-Skripte müssen im Verzeichnis **../logics** der SmartHomeNG Installation abgelegt werden. Damit die Logiken +getriggert und ausgeführt werden, müssen sie zusätzlich in der Konfigurationsdatei **../etc/logic.yaml** konfiguriert werden. -Dafür müssen Funktionen und Variablen der Funktion als Parameter übergeben werden. Das kann geschehen, indem die -Übergabe für jede Variable/Funktion einzeln erfolgt oder sie können in einem Objekt übergeben werden (was die zu -bevorzugende Methode ist. - -Dazu kann das Objekt **logic** genutzt werden, welches SmartHomeNG zur Verfüfung stellt um Variablen zu implementieren, -die den Lauf der Logik "überleben" und beim nächsten Lauf dieser Logik wieder zur Verfügung stehen. Das **logic** -Objekt ist privat. Das bedeutet, jede Logik hat ihr eigenes **logic** Objekt. - -Funktionen müssen dazu in der Definition den zusätzlichen Parameter **logic** enthalten. Das sollte zur besseren -Handhabung der letzte Parameter sein. Da dieser Parameter innerhalb der Logik immer mit dem selben Wert übergeben wird, -kann der Wert auch gleich als Standard-Wert in der Funktionsdefinition mit angegeben werden. Dann braucht er in den -Aufrufen der Funktion nicht angegeben zu werden. - -Das folgende Beispiel verdeutlicht das Vorgehen: - -.. code-block:: python - - # Variablen, die in Funktionen genutzt werden sollen, müssen dem logic Objekt zugewiesen werden - logic.sh = sh - logic.myvar1 = 5 - logic.myvar2 = False - - # Funktionen definieren und anschließend dem logic Objekt zuweisen - def func1(wert, logic=logic): - logger.warning("Funktion 1: wert = {}".format(wert)) - logic.func1 = func1 - - def func2(logic=logic): - logger.warning("Funktion 2") - logic.func1(2) - logic.func2 = func2 - - - # Main Routine der Logik - logic.func1(1) - logic.func2() - -Um aus Funktionen heraus auf das **sh** Objekt zugreifen zu können, sollte auch dieses (wie im obigen Beispiel) als -Variable im **logic** Objekt abgelegt werden. - - -.. index:: Logiken; Klassen - -================================ -Klassen in Logiken :redsup:`Neu` -================================ - -In Logiken können auch Klassen definiert werden. Damit diese Klassen in Funktionen zur Verfügung stehen, -muss auch hier (wie bei Funktionen) die Klasse dem Logik Objekt zugewiesen werden (letzte Zeile im folgenden Beispiel): - -.. code-block:: python - - class triggervalue(): - - def __init__(self, init_value=None, trigger_item=None, logic=None): - self.trigger_item = trigger_item - self.value = init_value - self.changed = False - self._last_value = init_value - if self.trigger_item is not None: - self.value = logic.items.return_item(trigger_item)() - self.changed = (logic.trigger_dict['source'] == trigger_item) - self._last_value = None - - def set(self, newvalue): - if self.trigger_item is None: - self._last_value = self.value - self.value = newvalue - self.changed = self.value != self._last_value - return self.changed - return self.changed - logic.triggervalue = triggervalue - -Wenn in einer Klasse auf Elemente des **logic** Objektes zugegriffen werden soll (wie in dem obigen Beispiel), -muss **logic** beim Erstellen einer Instanz mit übergeben werden: - -.. code-block:: python - - logic.freigabe_sued = logic.triggervalue(trigger_item='beschattung.beschattungsautomatik.sued', logic=logic) - +.. attention:: + **Die Informationen zu Logiken haben in der Dokumentation einen neuen Platz gefunden**: -.. index:: mqtt; Logiken -.. index:: Logiken; mqtt - -=========================== -Nutzung von MQTT in Logiken -=========================== - -Die Nutzung des MQTT Protokolls in Logiken wird durch das mqtt Modul von SmartHomeNG möglich, welches ab Version 1.7 -zur Verfügung steht. Zur Nutzung muss das mqtt Modul geladen und konfiguriert sein und der konfigurierte MQTT Broker -muss laufen. - - -Das mqtt Objekt -=============== - -Um MQTT in Logiken zu nutzen, steht ein Objekt **mqtt** zur Verfügung. Falls das mqtt Modul geladen/konfiguriert ist -oder keine Verbindung zum Broker besteht, ist **mqtt** **None**. In Logiken kann folgendermaßen geprüft werden, ob -MQTT Unterstützung besteht: - -.. code-block:: python - :caption: Test ob MQTT Unterstützung besteht - - if mqtt is None: - # no MQTT support available - logger.warning("MQTT module is not loaded or not yet initialized") - - -Das **mqtt** Objekt stellt folgende Funktionen bereit, um MQTT Messages zu versenden oder zu empfangen: - - -mqtt.publish_topic() --------------------- - -Eine MQTT Message kann versendet werden, indem die Funktion folgendermaßen aufgerufen wird: - -.. code-block:: python - :caption: publish_topic() - - mqtt.publish_topic(source_logic, topic, payload) - - -+-------------------------+------------------------------------------------------------------------------------------------------+ -| Parameter | Bemerkung | -+=========================+======================================================================================================+ -| source_logic | Name der Logik, welche die Funktion **publish_topic** aufruft. Hier kann einfach die Variable | -| | **logic.name** genutzt werden. | -+-------------------------+------------------------------------------------------------------------------------------------------+ -| topic | Topic der zu veröffentlichenden MQTT Message als **str**. | -+-------------------------+------------------------------------------------------------------------------------------------------+ -| payload | Payload der zu veröffentlichenden MQTT Message. Die Variable kann jedem Python Datentyp sein. | -+-------------------------+------------------------------------------------------------------------------------------------------+ - - -mqtt.subscribe_topic() ----------------------- - -Messages die ein bestimmtes MQTT Topic enthalten, können folgendermaßen abonniert werden: - -.. code-block:: python - :caption: subscribe_topic() - - mqtt.subscribe_topic(source_logic, topic, callback, qos=None, payload_type='str', bool_values=None) - - -+-------------------------+------------------------------------------------------------------------------------------------------+ -| Parameter | Bemerkung | -+=========================+======================================================================================================+ -| source_logic | Name der Logik, welche die Funktion **subscribe_topic** aufruft. Hier kann einfach die Variable | -| | **logic.name** genutzt werden. | -+-------------------------+------------------------------------------------------------------------------------------------------+ -| topic | Topic welches aboniert werden soll. | -+-------------------------+------------------------------------------------------------------------------------------------------+ -| callback | Name der Logik, welche bei Empfang des abonnierten Topics getriggert werden soll. Das kann die | -| | aufrufende Logik sein oder eine andere Logik. Wenn die aufrufende Logik getriggert werden soll, kann | -| | hier einfach die Variable **logic.name** genutzt werden. | -+-------------------------+------------------------------------------------------------------------------------------------------+ -| qos | **Optional**: Quality-of-Service, falls ein von der Standardeinstellung abweichender qos genutzt | -| | werden soll. Anderenfalls kann dieser Parameter weggelassen werden. | -+-------------------------+------------------------------------------------------------------------------------------------------+ -| payload_type | **Optional**: Datentyp in welchen die Payload umgewandelt werden soll. Wenn dieser Parameter nicht | -| | angegeben wird, wird der Datentyp **str** verwendet. Angegeben werden können folgende SmartHomeNG | -| | Datentypen: 'str', 'num', 'bool', 'list', 'dict', 'scene' oder 'bytes' - **bytes** bedeutet, dass | -| | keine Typenwandlung vorgenommen wird. | -+-------------------------+------------------------------------------------------------------------------------------------------+ -| bool_values | **Optional**: Legt fest, welche Werte des Payload als **bool** Values interpretiert werden sollen. | -| | Falls der Parameter angegeben wird, muss eine Liste mit **zwei** Werten angegeben werden, wobei der | -| | erste Wert für **False** steht und der zweiter Wert für **True** | -+-------------------------+------------------------------------------------------------------------------------------------------+ - -Wenn eine als **callback** angegebene Logik getriggert wird, ist das **trigger** dict mit folgenden Werten belegt: - -- **trigger['source']** - 'mqtt' - Konstante -- **trigger['by']** - - Dadurch kann bestimmt werden, wie die payload zu behandeln ist, falls eine Logik die Callbacks mehrer topics erhält -- **trigger['value']** - , wobei der Datentyp der payload dem entspricht, was in mqtt.subscribe_topic() als payload_type angegeben wurde - -Es können mehrere Logiken das selbe Topic abonnieren. Alle Logiken die das Topic abonniert haben, werden beim Eintreffen -einer passenden MQTT Message getriggert. - - -mqtt.unsubscribe_topic() ------------------------- - -Ein bestehendes Abonnement für Messages die ein bestimmtes MQTT Topic enthalten, kann folgendermaßen beendet werden: - -.. code-block:: python - :caption: unsubscribe_topic() - - mqtt.unsubscribe_topic(source_logic, topic) - - -+-------------------------+------------------------------------------------------------------------------------------------------+ -| Parameter | Bemerkung | -+=========================+======================================================================================================+ -| source_logic | Name der Logik, welche die Funktion **subscribe_topic()** für den Topic aufgerufen hatte. | -+-------------------------+------------------------------------------------------------------------------------------------------+ -| topic | Topic dessen Subscription beendet werden soll. | -+-------------------------+------------------------------------------------------------------------------------------------------+ - - - -Beispiel Logik -============== - -Hier ist eine Beispiel Logik, die sowohl Subscriptions ausführt, als auch die Callbacks behandelt: - -.. code-block:: python - :caption: Beispiel Logik **mqtt_demo** - - #!/usr/bin/env python3 - # logics/mqtt_demo.py - - def logic_publish_topic(logger, mqtt, logic, topic, payload): - logger.info("Function '{}()' - called by '{}()' in logic '{}'".format(inspect.stack()[0][3], inspect.stack()[1][3], logic.name)) - if mqtt.publish_topic(logic.name, topic, payload): - logger.info("Function '{}()' - test-topic was published".format(inspect.stack()[0][3], inspect.stack()[1][3])) - else: - logger.warning("Function '{}()' - test-topic was NOT published".format(inspect.stack()[0][3], inspect.stack()[1][3])) - - def logic_subscribe_topic(logger, mqtt, logic, topic, payload_type='str'): - logger.info("Function '{}()' - called by '{}()' in logic '{}'".format(inspect.stack()[0][3], inspect.stack()[1][3], logic.name)) - mqtt.subscribe_topic(logic.name, topic, None, payload_type, logic.name) + Weitere Informationen und Details zur Erstellung von Logiken finden sich nun in der Dokumentation + unter :doc:`Referenz/Logiken ` + und unter :doc:`Entwicklung/Logiken `. - # logic main-code starts here - logger.info("Triggered: trigger['source'] = {}, trigger[by] = {}, trigger[value] = {}".format(trigger['source'], trigger['by'], trigger['value']) ) - if mqtt is None: - # no MQTT support available - logger.warning("MQTT module is not loaded or not yet initialized") - elif trigger['source'] == 'mqtt': - # callback received - topic = trigger['by'] - payload = trigger['value'] - logger.info("MQTT received topic '{}': payload = '{}' - type(payload) = {})".format(topic, payload, type(payload))) - else: - mydict = {'txt': 'Test payload 2', 'num': 5} - # logger, mqtt and logic are handed over to functions, because only this way they are accessable in a logic's function - logic_publish_topic(logger, mqtt, logic, 'test_mqtt/topic', 'Test payload') - logic_publish_topic(logger, mqtt, logic, 'test_mqtt/topic2', mydict) - logic_subscribe_topic(logger, mqtt, logic, 'test_mqtt/sub') - logic_subscribe_topic(logger, mqtt, logic, 'test_mqtt/sub2', 'dict') +Details zur Konfiguration von Logiken finden sich nach wie vor auch unter :doc:`Konfiguration/Logiken <../konfiguration/logiken>` . -.. hint:: - Den **logger** müsste man nicht unbedingt an die Funktionen in der Logik übergeben, aber dann würden im Log die - Einträge aus Funktionen innerhalb der Logik im Logfile als Modul nicht **logics.mqtt_demo** angeben, sondern - **scheduler**. diff --git a/doc/user/source/logiken/objekteundmethoden_item_methoden.rst b/doc/user/source/logiken/objekteundmethoden_item_methoden.rst deleted file mode 100644 index 033cc85ee7..0000000000 --- a/doc/user/source/logiken/objekteundmethoden_item_methoden.rst +++ /dev/null @@ -1,90 +0,0 @@ - -.. role:: bluesup -.. role:: redsup - -============= -Item Methoden -============= - -Die grundsätzlichen Methoden, die jedes Item hat, sind unter **Items** beschrieben. Darüber -hinaus stehen die nachfolgend beschrieben Methoden zum Handling von Items zur Verfügung. - -Die Nutzung des SmartHomeNG Objektes ``sh`` ist dabei als veraltet anzusehen. Die richtige -Methode ist die Nutzung des Item API. Das ``items`` Objekt kann wie folgt -genutzt werden: - -.. code:: python - - from lib.item import Items - items = Items.get_instance() - -Nun können folgende Funktionen aufgerufen werden: - -return_item(path) -================= - -Liefert das Item Objekt für den angegebenen Pfad zurück. - -.. code:: python - - items.return_item('erdgeschoss.flur') - - -return_items() -============== - -Liefert sämtliche definierten Item Objekte zurück. - -.. code:: python - - for item in items.return_items(): - logger.info(item.id()) - - -match_items(regex) -================== - -Liefert alle Items zurück, die der Regular Expression, dem Pfad und dem optionalen Attribut entsprechen. - -.. code:: python - - for item in items.match_items('*.licht'): - # Selektiere alle Items, deren Pfad mit 'licht' endet - logger.info(item.id()) - - for item in items.match_items('*.licht:special'): - # Selektiere alle Items, deren Pfad mit 'licht' endet und die das Attribut 'special' haben - logger.info(item.id()) - - -find_items(configattribute) -=========================== - -In Abhängigkeit von ``configattribute`` werden wie folgt Items zurückgeliefert: - -.. table:: - - ==================== ========================================= - Attribut Ergebnis - ==================== ========================================= - ``Attribut`` Nur Items ohne Angabe von Instanz Kennung - ``Attribut@`` Items mit und ohne Instanz Kennung - ``Attribut@Instanz`` Items mit exakter Übereinstimmung von Attribut und Instanz Kennung - ``@Instanz`` Items die mit dieser Instanz Kennung definiert worden sind - ==================== ========================================= - - -.. code:: python - - for item in items.find_items('my_special_attribute'): - logger.info(item.id()) - - -find_children(parentitem, configattribute) -========================================== - -Diese Funktion liefert ausgehend vom Item ``parentitem`` alle Items mit passendem -``configattribute``. Dabei wird die Suche nach dem Attribut exakt so ausgeführt wie -in der Funktion ``find_items(configattribute)`` zuvor beschrieben. - - diff --git a/doc/user/source/logiken/objekteundmethoden_logging.rst b/doc/user/source/logiken/objekteundmethoden_logging.rst deleted file mode 100644 index feb26b0b57..0000000000 --- a/doc/user/source/logiken/objekteundmethoden_logging.rst +++ /dev/null @@ -1,56 +0,0 @@ - -.. index:: Logiken; Logging -.. index:: Logging; Logiken - -.. role:: bluesup -.. role:: redsup - - -Logging in Logiken :redsup:`Neu` -================================ - -Logiken verfügen über einen vordefinierten Logger. Dieser Logger ist im ``logic`` Objekt hinterlegt und kann -z.B. über - -.. code-block:: python - - logic.logger.info("Logtext") - -angesprochen werden. Die Ausgabe und er Loglevel können in ../etc/logging.yaml konfiguriert werden: - -.. code-block:: yaml - - loggers: - - # ============================== - # Loggers for SmartHomeNG logics - # ------------------------------ - logics: - handlers: [shng_details_file] - level: WARNING - - logics.: - handlers: [q21] - level: INFO - - -Wobei der Logger **logics** den Loglevel und Handler für alle Logiken festlegt, für die keine besonderen -Konfigurationen vorgenommen werden. - -**logics.** Legt den Loglevel und den Handler für eine einzelne Logik fest. - -.. note:: - - In SmartHomeNG v1.7.1 und davor, gab es diesen vordefinierten Logger noch nicht, - so dass in den Logiken der ein Logger explizit definiert werden musste: - - .. code-block:: python - - import logging - logger = logging.getLogger(__name__) - - ... - - logger.info("Logtext") - - diff --git a/doc/user/source/logiken/objekteundmethoden_scheduler.rst b/doc/user/source/logiken/objekteundmethoden_scheduler.rst deleted file mode 100644 index f02ccc7f68..0000000000 --- a/doc/user/source/logiken/objekteundmethoden_scheduler.rst +++ /dev/null @@ -1,42 +0,0 @@ -Scheduler Methoden ------------------- - -sh.scheduler.trigger() / sh.trigger() -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Diese globale Funktion triggert eine Logik durch die Angabe ihres Namens. - -.. code-block:: python - - sh.trigger(name [, by] [, source] [, value] [, dt]) - -- `name` (die Angabe von ist Pflicht) definiert die zu triggernde Logik. -- `by` Name der aufrufenden Logik -- `source` der eigentliche Grund für das Auslösen der Logik -- `value` Wert (des auslösenden Items) -- `dt` datetime Objekt (berücksichtigt Zeitzone), welches die Trigger-Zeit bestimmt. - - -.. note:: - - Ab SmartHomeNG v1.4 darauf achten, dass eine Trennung der - Namensräume für die Trigger stattgefunden hat. Wenn sh.trigger genutzt wird, muss dem Namen - **logics.** vorangestellt werden. - - Also: statt ```sh.trigger('my_logic')`` --> ```sh.sh.trigger('logics.my_logic')``` - - - -sh.scheduler.change() -^^^^^^^^^^^^^^^^^^^^^ - -Diese Methode ändert einige Laufzeit Optionen der Logik. - -.. code-block:: python - - sh.scheduler.change('alarmclock', active=False) - -disabled die Logik ‘alarmclock’. Außer dem active Flag, ist es möglich `cron` und `cycle` zu ändern. - - - diff --git a/doc/user/source/logiken/objekteundmethoden_sonne_mond.rst b/doc/user/source/logiken/objekteundmethoden_sonne_mond.rst deleted file mode 100644 index da05ded838..0000000000 --- a/doc/user/source/logiken/objekteundmethoden_sonne_mond.rst +++ /dev/null @@ -1,41 +0,0 @@ -Zeit, Sonne und Mond -==================== - - -sh.sun ------- - -Dieses Objekt bietet Zugriff auf Parameter der Sonne. Um dieses Objekt zu verwenden, ist es -erforderlich, den Breitengrad (latitude, z.B. lat: 53.5989481) und den -Längengrad (longitude z.B. lon: 10.0459898), -sowie die Höhe über dem Meeresspiegel (elevation z.B.: elev: 20) -in der Datei **../etc/smarthome.yaml** anzugeben. - -.. code-block:: python - :caption: Beispiele zur Sonnenstandsberechnung - - # sh.sun.pos(offset) hierbei gibt offset die Differenz in Zeit-Minuten zur aktuellen Zeit an - azimut, altitude = sh.sun.pos() # liefert die aktuelle Position der Sonne - azimut, altitude = sh.sun.pos(30) # liefert die Position, welche die Sonne in 30 Minuten haben wird - - # sh.sun.set(offset) hierbei gibt offset die Differenz in Grad zum nächsten Sonnenuntergang an - sunset = sh.sun.set() # liefert den utc-basierten Zeitpunkt des nächsten Sonnenuntergangs - sunset_tw = sh.sun.set(-6) # liefert den utc-basierten Zeitpunkt zu dem die Sonne 6° unter dem Horizont - # steht. (Ende der bürgerlichen Abenddämmerung) - - # sh.sun.rise(offset) hierbei gibt offset die Differenz in Grad zum nächsten Sonnenaufganges an - sunrise = sh.sun.rise() # liefert den utc-basierten Zeitpunkt des nächsten Sonnenaufganges - sunrise_tw = sh.sun.rise(-6) # liefert den utc-basierten Zeitpunkt zu dem die Sonne wieder 6° unter - # dem Horizont steht. (Beginn der nächsten bürgerlichen Morgendämmerung) - - -sh.moon -------- - -Neben den drei Funktionen (sh.moon.pos, sh.moon.set, sh.moon.rise) stehen zwei weitere zur Verfügung. - -**sh.moon.light(offset)** liefert einen Wert von 0 bis 100 der beleuchteten Fläche zur aktuellen Zeit + Offset. -**sh.moon.phase(offset)** gibt die Mondphase als ganze Zahl (0 bis 7) zurück, wobei: 0 = Neumond, 4 = Vollmond, 6 = abnehmender Halbmond - - - diff --git a/doc/user/source/logiken/objekteundmethoden_tools.rst b/doc/user/source/logiken/objekteundmethoden_tools.rst deleted file mode 100644 index e13aeaae3b..0000000000 --- a/doc/user/source/logiken/objekteundmethoden_tools.rst +++ /dev/null @@ -1,76 +0,0 @@ -sh.tools Methoden ------------------ - -Das sh.tools Objekt stellt einige nützliche Funktionen zur Verfügung: - - -sh.tools.ping() -^^^^^^^^^^^^^^^ - -Sendet ein Ping an einen Computer und liefert True zurück, falls der Computer antwortet. - -.. code-block:: python - - sh.office.laptop(sh.tools.ping('hostname')) - -speichert das Ergebnis des Pings von 'hostname' in das Item office.laptop: - - -sh.tools.dewpoint() -^^^^^^^^^^^^^^^^^^^ - -Errechnet der Taupukt für die gegebene Temperatur und Luftfeuchtigkeit. - -.. code-block:: python - - sh.office.dew(sh.tools.dewpoint(sh.office.temp(), sh.office.hum()) - - -sh.tools.fetch_url() -^^^^^^^^^^^^^^^^^^^^ - -Liefert den Inhalt einer Website als String oder ``False``, falls das Lesen der Website fehlschlägt. - - -.. code-block:: python - - sh.tools.fetch_url('https://www.regular.com') - -Es ist möglich ‘username’ und ‘password’ anzugeben, um sich bei der Website zu authentifizieren: - -.. code-block:: python - - sh.tools.fetch_url('https://www.special.com', 'username', 'password') - -Es ist auch möglich das Standard Timeout von 2 Sekunden für den Aufruf zu ändern: - -.. code-block:: python - - sh.tools.fetch_url('https://www.regular.com', timeout=4) - - -sh.tools.dt2ts(dt) -^^^^^^^^^^^^^^^^^^ - -Converts an datetime object to a unix timestamp. - - -sh.tools.dt2js(dt) -^^^^^^^^^^^^^^^^^^ - -Konvertiert ein datetime Objekt zu einem Json Timestamp. - - -sh.tools.rel2abs(temp, hum) -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Konvertiert die relative Luftfeuchtigkeit zur absoluten Luftfeuchtigkeit - - -sh.tools.runtime() -^^^^^^^^^^^^^^^^^^ - -Liefert die Laufzeit von SmartHomeNG zurück. - - - diff --git a/doc/user/source/plugins_all.rst b/doc/user/source/plugins_all.rst index 894ddab446..3e570b2672 100644 --- a/doc/user/source/plugins_all.rst +++ b/doc/user/source/plugins_all.rst @@ -15,11 +15,11 @@ in der Navigationsleiste. Diese Seiten werden vollständig automatisch aus den Plugins des Repositories generiert. Deshalb: - + - kann nur der Autor des jeweiligen Plugins sicherstellen, dass keine ungewünschten Mischungen der Sprache (Deutsch/Englisch) vorkommen. - kann keine Dokumentation für Plugins außerhalb des SmartHomeNG Plugin-Repositories in die Dokumentation aufgenommen werden. - - Hinweise auf weitere Plugins, die sich nicht im SmartHomeNG Repository befinden, sind auf der + + Hinweise auf weitere Plugins, die sich nicht im SmartHomeNG Repository befinden, sind auf der entsprechenden `Wiki Seite `_ zu finden. Die Plugins sind Kategorien unterteilt, die von der Interaktion mit bzw. Ansteuerung von externen Geräten oder Diensten ableiten. @@ -44,6 +44,7 @@ Die Plugins und die jeweiligen Beschreibungen sind auf den folgenden Seiten aufg /plugins_doc/plugins_protocol /plugins_doc/plugins_web /plugins_doc/plugins_unclassified + /plugins_doc/plugins_all Wenn ein Plugin Autor die Informationen auf diesen Seiten aktualisieren möchte, muss er nur diff --git a/doc/user/source/plugins_doc/plugins_all_header.rst b/doc/user/source/plugins_doc/plugins_all_header.rst new file mode 100644 index 0000000000..4ab4e2d745 --- /dev/null +++ b/doc/user/source/plugins_doc/plugins_all_header.rst @@ -0,0 +1,25 @@ + +============ +Alle Plugins +============ + +.. image:: /_static/img/all.png + :width: 200px + :height: 200px + :scale: 50 % + :alt: all plugins + :align: left + +.. |br| raw:: html + +
+ +Dieser Abschnitt enthält eine Liste aller Plugins, unabhängig vom jewiligen Plugin-Typ. + + +|br| +|br| +|br| +|br| +|br| +|br| diff --git a/doc/user/source/plugins_doc/plugins_system_header.rst b/doc/user/source/plugins_doc/plugins_system_header.rst index 45344e4a3b..67e6a79c30 100644 --- a/doc/user/source/plugins_doc/plugins_system_header.rst +++ b/doc/user/source/plugins_doc/plugins_system_header.rst @@ -21,8 +21,6 @@ die zentralen Funktionen von SmartHomeNG. Diese Plugins sind zum Teil auch enger von SmartHomeNG gekoppelt und müssen daher in der Version genauer zum Core passen als andere Plugins. -Besonders das **backend** Plugin, das **cli** Plugin und das **visu_websocket** Plugin müssen -genau zur Core Version von SmartHomeNG passen. |br| |br| diff --git a/doc/user/source/entwicklung/APIs.rst b/doc/user/source/referenz/APIs.rst similarity index 93% rename from doc/user/source/entwicklung/APIs.rst rename to doc/user/source/referenz/APIs.rst index 56cc89ec27..62213e9d56 100644 --- a/doc/user/source/entwicklung/APIs.rst +++ b/doc/user/source/referenz/APIs.rst @@ -2,7 +2,7 @@ APIs von SmartHomeNG ==================== Es gibt einige APIs um auf die verschiedenen Objekte (oder Objekt Hierarchien) von SmartHomeNG zuzugreifen. -Diese Programmier Interfaces können nützlich sein, wenn man Plugins entwickelt. +Diese Programmier Interfaces können nützlich sein, wenn man Plugins oder auch Logiken entwickelt. Die APIs sind als Libraries implementiert und im Ordner ../lib gespeichert. Die Beschreibung ihrer Funktionalität ist hier beschrieben: diff --git a/doc/user/source/referenz/items/funktionen.rst b/doc/user/source/referenz/items/funktionen.rst index c89ecacbea..c7fb2c1d11 100644 --- a/doc/user/source/referenz/items/funktionen.rst +++ b/doc/user/source/referenz/items/funktionen.rst @@ -47,45 +47,69 @@ genutzt werden können. Weiterhin werden die folgenden Funktionen unterstützt, die jedoch **ab SmartHomeNG v1.6** nicht mehr genutzt werden sollen. -Bitte stattdessen die entsprechenden in SmartHomeNG v1.6 eingeführten Properties benutzen. +Bitte stattdessen die entsprechenden in SmartHomeNG v1.6 eingeführten :doc:`Properties ` benutzen. +------------------------+------------------------------------------------------------------------------+ | **Funktion** | **Beschreibung** | +========================+==============================================================================+ | id() | Liefert den item-Pfad des Items zurück. Aufruf: sh.item.id() | +------------------------+------------------------------------------------------------------------------+ +| () | Liefert den aktuellen Wert des Items zurück. Aufruf: sh.item() | ++------------------------+------------------------------------------------------------------------------+ +| prev_value() | Liefert den Wert des Items zurück, den es vor der letzten Änderung hatte. | ++------------------------+------------------------------------------------------------------------------+ | last_update() | Liefert ein *datetime* Objekt mit dem Zeitpunkt des letzten Updates des | | | Items zurück. Im Gegensatz zu **last_change()** wird dieser Zeitstempel auch | | | verändert, wenn sich bei einem Update der Wert des Items nicht ändert. | +------------------------+------------------------------------------------------------------------------+ +| prev_update() | Liefert ein *datetime* Objekt mit dem Zeitpunkt des vorletzten Updates des | +| | Items zurück. Im Gegensatz zu **prev_change()** wird dieser Zeitstempel auch | +| | verändert, wenn sich bei einem Update der Wert des Items nicht ändert. | ++------------------------+------------------------------------------------------------------------------+ | last_change() | Liefert ein *datetime* Objekt mit dem Zeitpunkt der letzten Änderung des | | | Items zurück. | +------------------------+------------------------------------------------------------------------------+ -| age() | Liefert das Alter des Items seit der letzten Änderung des Wertes in Sekunden | -| | zurück. | +| prev_change() | Liefert ein *datetime* Objekt mit dem Zeitpunkt der vorletzten Änderung des | +| | Items zurück. | ++------------------------+------------------------------------------------------------------------------+ +| last_trigger() | Liefert ein *datetime* Objekt mit dem Zeitpunkt der letzten eval Triggerung | +| | des Items zurück. Hat das Item kein eval Attribut oder wurde es nicht | +| | getriggert, wird der Zeitpunkt des letzten Updates übermittelt. | +| | (Neu **ab SmartHomeNG v1.9.1**) | ++------------------------+------------------------------------------------------------------------------+ +| prev_trigger() | Liefert ein *datetime* Objekt mit dem Zeitpunkt der vorletzten eval | +| | Triggerung des Items zurück. (Neu **ab SmartHomeNG v1.9.1**) | +------------------------+------------------------------------------------------------------------------+ | update_age() | Liefert das Alter des Items seit dem letzten Update in Sekunden zurück. Das | | | Update Age wird auch gesetzt, wenn sich bei einem Update der Wert des Items | | | nicht ändert. (Neu **ab SmartHomeNG v1.4**) | +------------------------+------------------------------------------------------------------------------+ -| changed_by() | Liefert einen String zurück, der auf das Objekt hinweist, welches das Item | -| | zuletzt geändert hat. | +| prev_update_age() | Liefert das Alter des vorangegangenen Updates in Sekunden zurück. | +------------------------+------------------------------------------------------------------------------+ -| prev_value() | Liefert den Wert des Items zurück, den es vor der letzten Änderung hatte. | +| age() | Liefert das Alter des Items seit der letzten Änderung des Wertes in Sekunden | +| | zurück. | +------------------------+------------------------------------------------------------------------------+ -| prev_update() | Liefert ein *datetime* Objekt mit dem Zeitpunkt des vorletzten Updates des | -| | Items zurück. Im Gegensatz zu **prev_change()** wird dieser Zeitstempel auch | -| | verändert, wenn sich bei einem Update der Wert des Items nicht ändert. | +| prev_age() | Liefert das Alter des vorangegangenen geänderten Wertes in Sekunden zurück. | +------------------------+------------------------------------------------------------------------------+ -| prev_change() | Liefert ein *datetime* Objekt mit dem Zeitpunkt der vorletzten Änderung des | -| | Items zurück. | +| trigger_age() | Liefert das Alter der letzten Eval Triggerung in Sekunden zurück. | +| | (Neu **ab SmartHomeNG v1.9.1**) | +------------------------+------------------------------------------------------------------------------+ -| prev_update_age() | Liefert das Alter des vor-vorangegangenen Wertes in Sekunden zurück. | +| prev_trigger_age() | Liefert das Alter der vorletzten Eval Triggerung in Sekunden zurück. | +| | (Neu **ab SmartHomeNG v1.9.1**) | +------------------------+------------------------------------------------------------------------------+ -| prev_age() | Liefert das Alter des vorangegangenen Wertes in Sekunden zurück. | +| changed_by() | Liefert einen String zurück, der auf das Objekt hinweist, welches das Item | +| | zuletzt geändert hat. | ++------------------------+------------------------------------------------------------------------------+ +| updated_by() | Liefert einen String zurück, der auf das Objekt hinweist, durch welches | +| | das Item zuletzt ein Update erfahren hat. | ++------------------------+------------------------------------------------------------------------------+ +| triggered_by() | Liefert einen String zurück, der auf das Objekt hinweist, durch welches | +| | eine eventuell vorhandene eval Expression getriggert wurde. | +| | (Neu **ab SmartHomeNG v1.9.1**) | +------------------------+------------------------------------------------------------------------------+ + Beispiele für die Nutzung von Funktionen ---------------------------------------- @@ -113,4 +137,10 @@ Die folgende Beispiel Logik nutzt einige der oben beschriebenen Funktionen: # will in- or decrement the living room light to 100 by a stepping of ``1`` and a timedelta of ``2.5`` seconds. sh.living.light.fade(100, 1, 2.5) +Der folgende Beispiel eval Ausdruck sorgt dafür, dass ein Item den zugewiesenen Wert nur dann übernimmt, +wenn die Wertänderung bzw. das Anstoßen der eval Funktion über das Admin Interface erfolgt ist und das +letzte Update vor der aktuellen Triggerung über 10 Sekunden zurück liegt. + +.. code-block:: python + eval: value if sh..self.triggered_by().startswith('admin') and sh..self.update_age() > 10 else None diff --git a/doc/user/source/referenz/items/items.rst b/doc/user/source/referenz/items/items.rst index 6ed5f96664..8b3957a7fd 100644 --- a/doc/user/source/referenz/items/items.rst +++ b/doc/user/source/referenz/items/items.rst @@ -2,8 +2,10 @@ .. index:: Referenz; Items .. Index:: Items; Referenz -.. role:: bluesup .. role:: redsup +.. role:: bluesup +.. role:: greensup +.. role:: blacksup ===== @@ -22,3 +24,4 @@ In den folgenden Abschnitten sind Informationen zu den Items in SmartHomeNG zu f properties funktionen systemitems + methoden diff --git a/doc/user/source/referenz/items/methoden.rst b/doc/user/source/referenz/items/methoden.rst new file mode 100644 index 0000000000..e31d009c8d --- /dev/null +++ b/doc/user/source/referenz/items/methoden.rst @@ -0,0 +1,94 @@ + +.. role:: redsup +.. role:: bluesup +.. role:: greensup +.. role:: blacksup + +Methoden zum Zugriff auf Items +============================== + +Die im folgenden beschriebenen Methoden waren ursprünglich Teil des smarthome Objektes ``sh``. +Die Nutzung des ``sh`` Objektes für Items wird nicht weitergeführt. Es ist besser das Item API wie folgt zu nutzen: + +.. code:: python + + from lib.item import Items + items = Items.get_instance() + +.. note:: + + Bei Ausführung einer Logik ist das ``items`` Objekt bereits initialisiert und kann ohne die oben beschriebene + Initialisierung dirket genutzt werden. + + +Mit dem ``items`` Objekt können nun die folgenden Funktionen verwendet werden: + + +return_item(path) +----------------- + +Liefert ein Item Objekt für den angegebenen Pfad zurück. Beispiel: + +.. code-block:: python + + logger.info(items.return_item('first_floor.bath')) + + +return_items() +-------------- + +Liefert alle Item Objekte zurück + +.. code-block:: python + + for item in items.return_items(): + logger.info(item.id()) + + +match_items(regex) +------------------ + +Liefert alle Item Objekte deren Pfad mit einem regulären Ausdruck gefunden wird und die optional ein bestimmtes Attribut aufweisen. + +.. code-block:: python + + for item in items.match_items('*.lights'): # selects all items ending with 'lights' + logger.info(item.id()) + + for item in items.match_items('*.lights:special'): # selects all items ending with 'lights' and attribute 'special' + logger.info(item.id()) + + +find_items(configattribute) +--------------------------- + +Abhängig von ``configattribute`` werden die folgenden Items zurückgegeben: + +.. table:: + + ====================== ========================================================= + Attribut Ergebnis + ====================== ========================================================= + ``attribute`` Nur Items bei denen keine Instanz ID angegeben ist + ``attribute@`` Items mit oder ohne Instanz ID + ``attribute@instance`` Items mit einem bestimmten Attribut und einer Instanz ID + ``@instance`` Items mit einer bestimmten Instanz ID + ====================== ========================================================= + + +.. code:: python + + for item in items.find_items('my_special_attribute'): + logger.info(item.id()) + + +find_children(parentitem, configattribute) +------------------------------------------ + +Liefert alle Kind Item Objekte eines Elternitems mit einem gegebenen ``configattribute``. +Die Suche nach dem ``configattribute`` wird genauso durchgeführt wie in ``find_items(configattribute)`` weiter oben. + +.. code:: python + + for item in items.find_children('my_special_attribute'): + logger.info(item.id()) diff --git a/doc/user/source/referenz/items/properties.rst b/doc/user/source/referenz/items/properties.rst index f3788b75c6..dc6a8bab15 100644 --- a/doc/user/source/referenz/items/properties.rst +++ b/doc/user/source/referenz/items/properties.rst @@ -13,6 +13,7 @@ genutzt werden können. Alle Properties sind zumindest lesend (r/o) zugreifbar. auch beschrieben (r/w) werden. Properties sind **ab SmartHomeNG v1.6** verfügbar. +Die Properties ``last/prev_trigger(_by/_age)`` sind **ab SmartHomeNG v1.9.1** verfügbar. Properties werden in Logiken und eval-Ausdrücken folgendermaßen abgerufen: @@ -61,6 +62,15 @@ Werte für Properties, die auch geschrieben werden können (z.B. in Logiken), we | last_change_by | r/o | str | Liefert einen String zurück, der auf das Objekt hinweist, welches das Item | | | | | zuletzt geändert hat. | +----------------------+------------+----------+------------------------------------------------------------------------------+ +| last_trigger | r/o | datetime | Liefert ein *datetime* Objekt mit dem Zeitpunkt der letzten eval Triggerung | +| | | | des Items zurück. Hat das Item kein eval Attribut oder wurde es nicht | +| | | | getriggert, wird der Zeitpunkt des letzten Updates übermittelt. | ++----------------------+------------+----------+------------------------------------------------------------------------------+ +| last_trigger_age | r/o | float | Liefert das Alter der letzten Eval Triggerung in Sekunden zurück. | ++----------------------+------------+----------+------------------------------------------------------------------------------+ +| last_trigger_by | r/o | str | Liefert einen String zurück, der auf das Objekt hinweist, durch welches | +| | | | eine eventuell vorhandene eval Expression getriggert wurde. | ++----------------------+------------+----------+------------------------------------------------------------------------------+ | last_update | r/o | datetime | Liefert ein *datetime* Objekt mit dem Zeitpunkt des letzten Updates des | | | | | Items zurück. Im Gegensatz zu **last_change()** wird dieser Zeitstempel auch | | | | | verändert, wenn sich bei einem Update der Wert des Items nicht ändert. | @@ -95,19 +105,27 @@ Werte für Properties, die auch geschrieben werden können (z.B. in Logiken), we | prev_change | r/o | datetime | Liefert ein *datetime* Objekt mit dem Zeitpunkt der vorletzten Änderung des | | | | | Items zurück. | +----------------------+------------+----------+------------------------------------------------------------------------------+ -| prev_change_age | r/o | float | Liefert das Alter des vorangegangenen Wertes in Sekunden zurück. | +| prev_change_age | r/o | float | Liefert das Alter des vorangegangenen (geänderten) Wertes in Sekunden zurück.| +----------------------+------------+----------+------------------------------------------------------------------------------+ | prev_change_by | r/o | str | Liefert einen String zurück, der auf das Objekt hinweist, welches das Item | -| | | | das vorletzte mal geändert hat. | +| | | | das vorletzte Mal geändert hat. | ++----------------------+------------+----------+------------------------------------------------------------------------------+ +| prev_trigger | r/o | datetime | Liefert ein *datetime* Objekt mit dem Zeitpunkt der vorletzten eval | +| | | | Triggerung des Items zurück. | ++----------------------+------------+----------+------------------------------------------------------------------------------+ +| prev_trigger_age | r/o | float | Liefert das Alter der vorletzten Eval Triggerung in Sekunden zurück. | ++----------------------+------------+----------+------------------------------------------------------------------------------+ +| prev_trigger_by | r/o | str | Liefert einen String zurück, der auf das Objekt hinweist, durch welches | +| | | | eine eventuell vorhandene eval Expression das vorletzte Mal ausgelöst wurde. | +----------------------+------------+----------+------------------------------------------------------------------------------+ | prev_update | r/o | datetime | Liefert ein *datetime* Objekt mit dem Zeitpunkt des vorletzten Updates des | | | | | Items zurück. Im Gegensatz zu **prev_change()** wird dieser Zeitstempel auch | | | | | verändert, wenn sich bei einem Update der Wert des Items nicht ändert. | +----------------------+------------+----------+------------------------------------------------------------------------------+ -| prev_update_age | r/o | float | Liefert das Alter des vor-vorangegangenen Wertes in Sekunden zurück. | +| prev_update_age | r/o | float | Liefert das Alter des vorangegangenen Updates in Sekunden zurück. | +----------------------+------------+----------+------------------------------------------------------------------------------+ | prev_update_by | r/o | str | Liefert einen String zurück, der auf das Objekt hinweist, durch welches das | -| | | | Item das vorletzte mal ein Update erfahren hat. | +| | | | Item das vorletzte Mal ein Update erfahren hat. | +----------------------+------------+----------+------------------------------------------------------------------------------+ | prev_value | r/o | str | Liefert den Wert des Items zurück, den es vor der vorletzten Änderung hatte. | +----------------------+------------+----------+------------------------------------------------------------------------------+ @@ -129,3 +147,11 @@ Werte für Properties, die auch geschrieben werden können (z.B. in Logiken), we | | | | **schaltaktor** zugegriffen werden: **schaltaktor.property.knx_send** | | | | | Dyn. Properties sind erst in SmartHomeNG Releases nach v1.6.1 implementiert. | +----------------------+------------+----------+------------------------------------------------------------------------------+ + +Der folgende Beispiel eval Ausdruck sorgt dafür, dass ein Item den zugewiesenen Wert nur dann übernimmt, +wenn die Wertänderung bzw. das Anstoßen der eval Funktion über das Admin Interface erfolgt ist und das +letzte Update vor der aktuellen Triggerung über 10 Sekunden zurück liegt. + +.. code-block:: python + + eval: value if sh..self.property.last_trigger_by == 'admin' and sh..self.property.last_update_age > 10 else None diff --git a/doc/user/source/referenz/items/standard_attribute/eval.rst b/doc/user/source/referenz/items/standard_attribute/eval.rst index 7df35d8287..254448e5d3 100644 --- a/doc/user/source/referenz/items/standard_attribute/eval.rst +++ b/doc/user/source/referenz/items/standard_attribute/eval.rst @@ -101,8 +101,8 @@ zusätzliche Funktionen in eval Ausdrücken Bereits SmartHomeNG v1.3 wird das Python Modul `math `__ bereitgestellt und es können entsprechende Funktionen genutzt werden. Außerdem sind seit SmarthomeNG v1.7 die -:doc:`Items-API ` als **items** (z.B. items.return_item('bla')) und das -:doc:`shtime Modul ` mittels **shtime** (z.B. shtime.now()) verfügbar. +:doc:`Items-API ` als **items** (z.B. items.return_item('bla')) und das +:doc:`shtime Modul ` mittels **shtime** (z.B. shtime.now()) verfügbar. Beispiel: @@ -127,7 +127,7 @@ auf andere Items beziehen. .. tip:: - Im Abschnitt **Logiken** ist auf der Seite :doc:`Feiertage, Daten und Zeiten ` + Im Abschnitt **Logiken** ist auf der Seite :doc:`Feiertage, Daten und Zeiten ` beschrieben, welche Feiertags- und Datums-Funktionen in Logiken benutzt werden können. Diese Funktionen können auch in eval Attributen genutzt werden können. @@ -255,3 +255,35 @@ den Ausdruck **sh..self.prev_value()** zugegriffen werden. **None** bewirkt, dass das Item unverändert bleibt, und somit auch keine Trigger ausgelöst werden. Diese Art, per ``None`` Werte nicht zuzuweisen, funktioniert **nur** bei ``eval``; bei anderen Attributen wie z.B. ``cycle`` kann dies nicht genutzt werden. + + +Abfrage des Auslösers eines **eval** Ausdrucks +---------------------------------------------- + +Zu dem Zeitpunkt, wo ein **eval** Ausdruck ausgeführt wird, bezieht sich das Property ``last_update_by`` **nicht** auf +die aktuellste Auslösung des Items, sondern auf das letzte Update davor. Möchte man nun aber abfragen, durch welches +Item, Plugin oder durch welche Logik die Ausführung des eval Ausdrucks angestoßen wurde, +muss das seit SmartHomeNG 1.9.1 implementierte Property ``last_trigger_by`` genutzt werden. + +Dabei findet folgender zeitlicher Ablauf statt: + + - Aktualisieren des Itemwerts oder Veränderung eines Triggeritems (eval_trigger) + - Setzen und Aktualisieren der ``last_trigger_by`` und dazu passenden Properties + - Ausführen des eval Ausdrucks + - Ist das Ergebnis des eval Ausdrucks **None**, passiert nichts weiter. Das Item wird nicht aktualisiert, + ``last_update_by`` ändert sich ebenfalls nicht. + - Ist das Ergebnis des eval Ausdrucks ein erlaubter Wert, wird dieser ins Item geschrieben. + ``last_update_by`` und/oder ``last_change_by`` werden entsprechend aktualisiert. + +Details zu den verschiedenen Abfragemöglichkeiten sind unter :doc:`Properties ` zu finden. + +.. attention:: + + Möchte man in einem **eval** Ausdruck abfragen, wodurch der Ausdruck ausgelöst wurde, muss **property.last_trigger_by** + genutzt werden. Das **property.last_update_by** wird erst danach (oder bei None gar nicht) aktualisiert. + +.. tip:: + + War die Auflösung eines **eval** Ausdrucks nicht erfolgreich (weil z.B. die Syntax falsch ist oder ein Item nicht + gefunden werden konnte), wird beim ``last_trigger_by`` den Caller- und Source-Angaben noch ein None (als "dest") hinten + angehängt. Der Wert beinhaltet somit ``::None``. diff --git a/doc/user/source/entwicklung/libraries_plugins_logics.rst b/doc/user/source/referenz/libraries_plugins_logics.rst similarity index 100% rename from doc/user/source/entwicklung/libraries_plugins_logics.rst rename to doc/user/source/referenz/libraries_plugins_logics.rst diff --git a/doc/user/source/referenz/logging/logging.rst b/doc/user/source/referenz/logging/logging.rst index f7d9266842..47e2485d9b 100644 --- a/doc/user/source/referenz/logging/logging.rst +++ b/doc/user/source/referenz/logging/logging.rst @@ -6,9 +6,9 @@ .. role:: redsup -===================== -Logging :redsup:`Neu` -===================== +======= +Logging +======= SmartHomeNG nutzt das Logging-Modul von Python. In den folgenden Abschnitten sind Informationen zum Logging in SmartHomeNG zu finden. @@ -26,5 +26,7 @@ Die vollständige zum Python Logging Modul ist unter logging_handler logging_formatter logging_filter + logging_textformatierung + diff --git a/doc/user/source/referenz/logging/logging_textformatierung.rst b/doc/user/source/referenz/logging/logging_textformatierung.rst new file mode 100644 index 0000000000..30de7c2cd8 --- /dev/null +++ b/doc/user/source/referenz/logging/logging_textformatierung.rst @@ -0,0 +1,80 @@ + +.. index:: Referenz; Logging, Text Formatierung +.. Index:: Logging, Text Formatierung; Referenz + +.. role:: bluesup +.. role:: redsup + + +============================== +Text Formatierung beim Logging +============================== + +Bei der Erstellung von Logiken, User Functions oder Plugins ist es sinnvoll bestimmte Vorkommnisse oder Ereignisse +in Logdateien festzuhalten. Die anderen Abschnitte zum Logging in der Referenz befassen sich mit der Konfiguration +des Loggings. Hier folgen einige Vorschläge zum Vorgehen bei der Formatierung der eigentlichen Log Message. + +Die Log Message muss dem Logger als **ein** String übergeben werden. Direkt im Log Aufruf kann man den String zwar +in der folgenden Weise zusammen bauen, das ist jedoch nicht zu empfehlen: + +.. code-block:: python + + menge = 24 + preis = 19.98 + logger.info("Artikelmenge: "+str(menge) + ", Preis: " + str(preis) + ", Gesamt: " + str(menge * preis)) + + +In älteren Python Versionen waren unterschiedliche Methoden zur Formatierung üblich. Die veraltete Methode im +Stil von **printf** und **sprintf**: + +.. code-block:: python + + menge = 24 + preis = 19.98 + logger.info("Artikelmenge: %3d, Preis: %6.2f, Gesamt: %8.2f" % (menge, preis, menge * preis)) + + +Der "pythonische Weg" war in **älteren** Python Versionen die Nutzung von "**format**": + +.. code-block:: python + + menge = 24 + preis = 19.98 + logger.info("Artikelmenge: {}, Preis: {}, Gesamt: {}".format(menge, preis, menge * preis)) + +Auch hier war eine Formatierung der Variableninhalte möglich: + + +.. code-block:: python + + menge = 24 + preis = 19.98 + logger.info("Artikelmenge: {:3d}, Preis: {:6.2f}, Gesamt: {:8.2f}".format(menge, preis, menge * preis)) + + +Seit Python 3.6 gibt es eine neue Möglichkeit der Formatierung, die sogenannten **f-Strings**. Diese Methode erhöht +die Lesbarkeit des Python Codes deutlich, besonders bei Verwendung vieler Variablen in einer Message. + +.. tip:: + + Da inzwischen die von SmartHomeNG unterstützte Python Version >= 3.6 ist, können **f-Strings** uneingeschränkt in + SmartHomeNG genutzt werden. + + Diese Art der Formatierung wird ausdrücklich empfohlen. Es ist jedoch nicht notwendig, in älterem Code alle + älteren Formatierungs-Optionen zu ersetzen. + +.. code-block:: python + + menge = 24 + preis = 19.98 + logger.info(f"Artikelmenge: {menge}, Preis: {preis}, Gesamt: {menge * preis}") + + +Auch hier ist die Formatierung der Variableninhalte möglich: + +.. code-block:: python + + menge = 24 + preis = 19.98 + logger.info(f"Artikelmenge: {menge:3d}, Preis: {preis:6.2f}, Gesamt: {menge * preis:8.2f}") + diff --git a/doc/user/source/referenz/logiken/logiken.rst b/doc/user/source/referenz/logiken/logiken.rst new file mode 100644 index 0000000000..4a2fcc37e0 --- /dev/null +++ b/doc/user/source/referenz/logiken/logiken.rst @@ -0,0 +1,48 @@ +:tocdepth: 5 + +.. role:: redsup +.. role:: bluesup +.. role:: greensup +.. role:: blacksup + + +.. index:: Referenz; Logiken +.. Index:: Logiken; Referenz + +##################### +Logiken :redsup:`Neu` +##################### + +Logiken dienen dazu, komplexere Zustände und Abläufe zu steuern, die über Item Attribute nicht abgebildet werden +können. Logiken für SmartHomeNG sind Python Skripte. Zur Erstellung von Logiken müssen Sie über Kenntnisse +der Programmiersprache Python verfügen. + +In diesem Abschnitt wird die grundlegende Struktur einer Logik, sowie deren Konfiguration beschrieben. Weiterhin +werden die definierten Objekte und Methoden, sowie die zur Verfügung stehenden Pythom Module erläutert. Im weiteren +wird beschrieben, wie auf Items zugegriffen wird. + +Das mqtt Modul bietet einen Support für Logiken, der auch in diesem Abschnitt dokumentiert ist. + +Außerdem werden die Prinzipien der Nutzung von Plugin Funktionen beschrieben (falls das jeweilige Plugin Funktionen +definiert hat). + +.. toctree:: + :maxdepth: 5 + :hidden: + + logiken_grundstruktur + logiken_konfiguration + logiken_logic_objekt + logiken_logging + + logiken_python_module + logiken_smarthomeng_methoden + logiken_plugin_funktionen + + logiken_persistente_vars + logiken_items + logiken_funktionen + logiken_mqtt + logiken_exceptions + + diff --git a/doc/user/source/referenz/logiken/logiken_exceptions.rst b/doc/user/source/referenz/logiken/logiken_exceptions.rst new file mode 100644 index 0000000000..aa7f51a3f1 --- /dev/null +++ b/doc/user/source/referenz/logiken/logiken_exceptions.rst @@ -0,0 +1,80 @@ + +.. role:: redsup +.. role:: bluesup +.. role:: greensup +.. role:: blacksup + +.. index:: Struktur; Logiken +.. index:: Logiken; Struktur + +========== +Exceptions +========== + +Laufzeit Fehler +=============== + +Nachdem eine Logik erfolgreich geladen wurde, können bei der Ausführung der Logik Laufzeit Fehler auftreten. +Falls ein Laufzeitfehler nicht innerhalb der Logik abgefangen wird, führt er zur Beendigung des aktuellen +Laufes der Logik und es wird ein entsprechender Fehler geloggt. Die Logik bleibt jedoch geladen und wird, wenn sie +getriggert wird, erneut ausgeführt. + +Beispiel: Folgende Test Logik + +.. code-block:: python + + #!/usr/bin/env python3 + # test_logic.py + + logger.warning("Start der Logik") + + var = int('abc') + + logger.warning("Ende der Logik") + + +erzeugt zur Laufzeit folgenden Logeintrag: + +.. code-block:: text + + 2022-01-24 13:55:14 ERROR logics.test_logic In der Logik ist ein Fehler aufgetreten: + Logik 'test_logic', Datei '/usr/local/shng_dev/logics/test_logic.py', Zeile 6 + Hauptroutine der Logik, Exception: invalid literal for int() with base 10: 'abc' + + +.. index:: Logiken; LeaveLogic +.. _logik_exceptions_leavelogic: + +Exception LeaveLogic +==================== + +Normalerweise wird der aktuelle Lauf einer Logik dadurch beendet, dass das Ende des Codes der Hauptroutine +erreicht wird. + +In einigen Fällen kann es jedoch sinnvoll sein, die Logik mitten aus dem Code heraus oder as einer Funktion heraus +zu beenden/zu verlassen. + +Dafür kann eine spezielle Exception ausgelöst werden. Das geschieht mit dem Befehl ``raise LeaveLogic()``: + +.. code-block:: python + + ... + + if logik_beenden: + raise LeaveLogic() + + ... + +Optional kann der Exception ein Grund als Parameter mitgegeben werden. + +.. code-block:: python + + ... + + if logik_beenden: + raise LeaveLogic('Die Logik sollte beendet werden') + + ... + +Falls ein Grund angegeben wird, erfolgt ein Logeintrag mit dem Level INFO, der das Verlassen und diesen Grund loggt. + diff --git a/doc/user/source/referenz/logiken/logiken_funktionen.rst b/doc/user/source/referenz/logiken/logiken_funktionen.rst new file mode 100644 index 0000000000..991a010d7f --- /dev/null +++ b/doc/user/source/referenz/logiken/logiken_funktionen.rst @@ -0,0 +1,104 @@ + +:tocdepth: 5 + +================================= +Funktionen und Klassen in Logiken +================================= + + +.. index:: Funktionen; Logiken +.. index:: Logiken; Funktionen + +Funktionen in Logiken +===================== + +Bei der Nutzung von Funktionen in Logiken ist eine Besonderheit zu beachten: + +Eine Logik verhält sich nicht wie ein Python Modul! Variablen und Funktionen die auf Ebene der Logik definiert werden, +sind keine globalen Objekte. Sie stehen in Funktionen die innerhalb der Logik definiert werden nicht zur Verfügung. +Daher müssen Variablen und Funktionen die innerhalb von Funktionen genutzt werden, der Funktion explizit bekannt gemacht +werden. + +Dafür müssen Funktionen und Variablen der Funktion als Parameter übergeben werden. Das kann geschehen, indem die +Übergabe für jede Variable/Funktion einzeln erfolgt oder sie können in einem Objekt übergeben werden (was die zu +bevorzugende Methode ist. + +Dazu kann das Objekt **logic** genutzt werden, welches SmartHomeNG zur Verfüfung stellt um Variablen zu implementieren, +die den Lauf der Logik "überleben" und beim nächsten Lauf dieser Logik wieder zur Verfügung stehen. Das **logic** +Objekt ist privat. Das bedeutet, jede Logik hat ihr eigenes **logic** Objekt. + +Funktionen müssen dazu in der Definition den zusätzlichen Parameter **logic** enthalten. Das sollte zur besseren +Handhabung der letzte Parameter sein. Da dieser Parameter innerhalb der Logik immer mit dem selben Wert übergeben wird, +kann der Wert auch gleich als Standard-Wert in der Funktionsdefinition mit angegeben werden. Dann braucht er in den +Aufrufen der Funktion nicht angegeben zu werden. + +Das folgende Beispiel verdeutlicht das Vorgehen: + +.. code-block:: python + + # Variablen, die in Funktionen genutzt werden sollen, müssen dem logic Objekt zugewiesen werden + logic.sh = sh + logic.myvar1 = 5 + logic.myvar2 = False + + # Funktionen definieren und anschließend dem logic Objekt zuweisen + def func1(wert, logic=logic): + logger.warning("Funktion 1: wert = {}".format(wert)) + logic.func1 = func1 + + def func2(logic=logic): + logger.warning("Funktion 2") + logic.func1(2) + logic.func2 = func2 + + + # Main Routine der Logik + logic.func1(1) + logic.func2() + +Um aus Funktionen heraus auf das **sh** Objekt zugreifen zu können, sollte auch dieses (wie im obigen Beispiel) als +Variable im **logic** Objekt abgelegt werden. + + +.. index:: Klassen; Logiken +.. index:: Logiken; Klassen + +Klassen in Logiken +================== + +In Logiken können auch Klassen definiert werden. Damit diese Klassen in Funktionen zur Verfügung stehen, +muss auch hier (wie bei Funktionen) die Klasse dem Logik Objekt zugewiesen werden (letzte Zeile im folgenden Beispiel): + +.. code-block:: python + + class triggervalue(): + + def __init__(self, init_value=None, trigger_item=None, logic=None): + self.trigger_item = trigger_item + self.value = init_value + self.changed = False + self._last_value = init_value + if self.trigger_item is not None: + self.value = logic.items.return_item(trigger_item)() + self.changed = (logic.trigger_dict['source'] == trigger_item) + self._last_value = None + + def set(self, newvalue): + if self.trigger_item is None: + self._last_value = self.value + self.value = newvalue + self.changed = self.value != self._last_value + return self.changed + return self.changed + logic.triggervalue = triggervalue + +Wenn in einer Klasse auf Elemente des **logic** Objektes zugegriffen werden soll (wie in dem obigen Beispiel), +muss **logic** beim Erstellen einer Instanz mit übergeben werden: + +.. code-block:: python + + logic.freigabe_sued = logic.triggervalue(trigger_item='beschattung.beschattungsautomatik.sued', logic=logic) + + + + diff --git a/doc/user/source/referenz/logiken/logiken_grundstruktur.rst b/doc/user/source/referenz/logiken/logiken_grundstruktur.rst new file mode 100644 index 0000000000..a53959e536 --- /dev/null +++ b/doc/user/source/referenz/logiken/logiken_grundstruktur.rst @@ -0,0 +1,42 @@ + +.. role:: redsup +.. role:: bluesup +.. role:: greensup +.. role:: blacksup + +.. index:: Struktur; Logiken +.. index:: Logiken; Struktur + +========================= +Grundstruktur einer Logik +========================= + +Eine Logik besteht aus einem Python Skript sowie einer Reihe von Parametern die den Aufruf (das Triggern) der Logik +steuern. Eine Logik kan während der Laufzeit von SmartHomeNG geladen und entladen werden. Dadurch sind Veränderungen +ein einer Logik möglich, ohne SmartHomeNG neu starten zu müssen. + +Das eigentliche Python Skript einer Logik muss im Verzeichnis ``../logics`` der SmartHomeNG Installation abgelegt +werden. Damit die Logik getriggert und ausgeführt wird, muss sie zusätzlich in der Konfigurationsdatei +``../etc/logic.yaml`` konfiguriert werden. + +Zur Erstellung einer neuen Logik sollte die Template Datei ``logics/logic.tpl`` als Vorlage genutzt werden. + +Die einfachste Methode zu Erstellung einer neuen Logik stellt die Verwendung der Admin GUI dar. Dort kann sowohl +das Skript erstellt werden, as auch die Parameter zur Konfiguration erfasst werden. Die Admin GUI ermöglicht auch +im Anschluß das Laden einer neuen Logik. + +Einfache Logiken bestehen nur aus einer Hauptroutine. Es ist jedoch möglich, Funktionen und Klassen innerhalb einer +Logik zu defninieren, um bei umfangreichen Logiken den Code verständlich und wartbar zu halten. + +Eine einfache Logik, die das Wohnzimmerlicht einschaltet, wenn sie getriggert wird, kann zum Beispiel so aussehen: + +.. code-block:: python + + #!/usr/bin/env python + # put on the light in the living room, if it is not on + if not sh.living_room.light(): + sh.living_room.light('on') + +Auf Items muß unter Nutzung von Klammern ``()`` zugegriffen werden, da es sich um eine Item Methode handelt und +nicht um eine Variable. + diff --git a/doc/user/source/referenz/logiken/logiken_items.rst b/doc/user/source/referenz/logiken/logiken_items.rst new file mode 100644 index 0000000000..f73589f641 --- /dev/null +++ b/doc/user/source/referenz/logiken/logiken_items.rst @@ -0,0 +1,61 @@ +:tocdepth: 5 + +.. index:: Zugriff auf Items; Logiken +.. index:: Logiken; Zugriff auf Items + + +================= +Zugriff auf Items +================= + +.. index:: Item Wert; Logiken +.. index:: Logiken; Item Wert + +Zugriff auf den Wert eines Items +================================ + +Um Zugriff auf Items zu erhalten, müssen Sie über die zentrale Instanz **sh** des Smarthome Objektes angesprochen +werden. Um zum Beispiel den Wert des Items **path.item** zu erhalten, muss der Aufruf **sh.path.item()** lauten. +Der Wert wird über eine Methode des Items aufgerufen, weshalb der Name des Item Objektes um **()** ergänzt werden +muss. Um dem Item einen neuen Wert zuzuweisen, wird dieser einfach als Argument angegeben: **sh.path.item(neuer_wert)**. + +.. attention:: + + Zuweisung von Item-Werten: + + Es ist sehr wichtig, immer mit Klammern **()** auf die Items zuzugreifen! Wenn (fehlerhafter weise) das Item direkt + zugewiesen wird, z.B. mit **sh.path.item = Wert**, dann wird das item-Objekt in SmartHomeNG überschrieben. + Anschließend ist **sh.path.item** eine normale Variable und kein Item Objekt mehr. + + In diesem Fall kann die mitgelieferte Logik **check_items.py** verwendet werden, um auf Vorhandensein + entsprechend beschädigter Items zu prüfen und diese wiederherzustellen. Alternativ werden die Items nach + einem Neustart von SmartHomeNG neu erstellt. + + +Alternativ kann auch über die Item-Properties auf den Wert zugegriffen werden: **sh.path.item.propery.value** +gibt den Wert zurück, mit **sh.path.item.property.value = Wert** kann der Wert zugewiesen werden. Diese Variante +lässt sich wie eine normale Variablenzuweisung nutzen. + +Beispiel +-------- + +Eine Logik sieht prinzipiell folgendermaßen aus: + +.. code-block:: python + :caption: /usr/local/smarthome/logics/testlogik1.py + + #!/usr/bin/env python3 + # testlogik1.py + + #Code der Logik: + + # Das Deckenlicht im Büro einschalten, falls es nicht eingeschaltet ist + if not sh.buero.deckenlicht(): + sh.buero.deckenlicht('on') + + + +.. include:: /referenz/items/properties.rst + +.. include:: /referenz/items/methoden.rst + diff --git a/doc/user/source/referenz/logiken/logiken_konfiguration.rst b/doc/user/source/referenz/logiken/logiken_konfiguration.rst new file mode 100644 index 0000000000..6ef4bccda5 --- /dev/null +++ b/doc/user/source/referenz/logiken/logiken_konfiguration.rst @@ -0,0 +1,228 @@ +:tocdepth: 5 + +.. role:: bluesup + +.. index:: Konfiguration; Logiken +.. index:: Logiken; Konfiguration + + +=================== +Logik Konfiguration +=================== + +Zur Konfiguration einer Logik wird in der Datei **../etc/logic.yaml** ein Abschnitt für die +Logik angelegt. Unter dem Namen dieses Abschnitts wird die Logik an anderen Stellen referenziert. + +In diesem Abschnitt muss SmartHomeNG mitgeteilt werden, welche Code Datei ausgeführt werden soll +und unter welchen Umständen. Dazu werden die im Folgenden beschriebenen Parameter genutzt. + + +.. include:: logiken_standardparameter.rst + + +Die folgenden Parameter können genutzt werden um eine Logik und ihre Ausführungsumstände festzulegen: + +.. _logik_parameter_watch_item: + +watch_item +~~~~~~~~~~ + +Die Liste der angegebenen Items wird auf Änderungen überwacht + +.. code-block:: yaml + :caption: etc/logic.yaml + + logicnamehere: + watch_item: + - house.alarm + - garage.alarm + + +Jede Änderung bei den Items **house.alarm** oder **garage.alarm** löst die Ausführung der angegebenen Logik aus. +Es ist möglich einen Stern * für einen Pfadbestandteil zu setzen, ähnlich eines regulären Ausdrucks: + +.. code-block:: yaml + + watch_item: '*.door' + +Eine Änderung von **garage.door** oder auch **house.door** wird die Ausführung der Logik auslösen aber **nicht** +eine Änderung von **house.hallway.door** + + +.. _logik_parameter_cycle: + +cycle +~~~~~ + +Sorgt für eine zyklische Ausführung der Logik + +.. code-block:: yaml + + cycle: 60 + + +Optional kann ein Argument übergeben werden + +.. code-block:: yaml + + cycle: 60 = 100 + + +Dadurch wird die Logik alle 60 Sekunden ausgeführt und der Wert 100 an die Logik übergeben. +Innerhalb der Logik kann auf den Wert über ``trigger['value']`` zugegriffen werden + + +.. _logik_parameter_crontab: + +crontab :bluesup:`Update` +~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. Der Inhalt der Beschreibung von crontab wurde aus referenz/items/standard_attribute/crontab.rst 1:1 kopiert + +Es gibt drei verschiedene Parametersätze für ein Crontab Attribut: + +.. tabs:: + + .. tab:: init + Das Item wird zum Start von SmarthomeNG aktualisiert und triggert + dadurch unter Umständen eine zugewiesene Logik: + + .. code-block:: yaml + + crontab: init + + Hier kann auch zusätzlich ein Offset angegeben werden um den + tatsächlichen Zeitpunkt zu verschieben: + + .. code-block:: yaml + + crontab: init+10 # 10 Sekunden nach Start + + .. tab:: Zeitpunkte + + Das Item soll zu bestimmten Zeitpunkten aktualisiert werden. + Die Schreibweise ist an Linux Crontab angelehnt, entspricht diesem aber nicht genau. + Es gibt je nach Parameteranzahl 3 Varianten: + + * ``crontab: `` + * ``crontab: `` + * ``crontab: `` + + Dabei sind je nach Variante folgende Werte zulässig: + + * Sekunde: ``0`` bis ``59`` + * Minute: ``0`` bis ``59`` + * Stunde: ``0`` bis ``23`` + * Tag: ``1`` bis ``31`` + * Monat: 1 bis 12 oder ``jan`` bis ``dec`` + * Wochentag ``0`` bis ``6`` oder ``mon``, ``tue``, ``wed``, ``thu``, ``fri``, ``sat``, ``sun`` + + Alle Parameter müssen durch ein Leerzeichen getrennt sein und innerhalb eines Parameters + darf kein zusätzliches Leerzeichen vorhanden sein, sonst kann der Parametersatz nicht ausgewertet werden. + + Im folgenden Beispiel wird jeden Tag um 23:59 ein Trigger erzeugt und der Wert 70 gesetzt. + + .. code-block:: yaml + + crontab: 59 23 * * = 70 + + Für jede dieser Zeiteinheiten (Minuten, Stunde, Tag, Wochentag) werden + folgende Muster unterstützt (Beispiel jeweils ohne Anführungszeichen verwenden): + + * eine einzelne Zahl, z.B. ``8`` → immer zur/zum 8. Sekunde/Minute/Stunde/Tag/Wochentag + * eine Liste von Zahlen, z.B. ``2,8,16`` → immer zur/zum 2., 8. und 16. Sekunde/Minute/Stunde/Tag/Monat/Wochentag + * ein Wertebereich, z.B. ``1-5`` → immer zwischen dem/der 1. und 5. Sekunde/Minute/Stunde/Tag/Monat/Wochentag + * einen Interval, z.B. ``\*\/4`` → immer alle 4 Sekunden/Minuten/Stunden/Tage/Wochentage + * einen Stern, z.B. ``*`` → jede Sekunde/Minute/Stunde/Tag/Monat/Wochentag + + .. tab:: Zeitpunkte bezogen auf Aufgang von Sonne oder Mond + + Nach dem Muster ``[H:M<](sunrise|sunset|moonrise|moonset)[+|-][offset][ )`` kann ein Triggerpunkt bezogen + auf Sonne oder Mond berechnet werden: + + * ``sunrise`` → immer zum Sonnenaufgang + * ``sunset`` → immer zum Sonnenuntergang + * ``sunrise`` und untere Begrenzung → ``06:00` +und :doc:`Entwicklung/Logiken `. \ No newline at end of file diff --git a/doc/user/source/referenz/logiken/logiken_logging.rst b/doc/user/source/referenz/logiken/logiken_logging.rst new file mode 100644 index 0000000000..6aa118e0da --- /dev/null +++ b/doc/user/source/referenz/logiken/logiken_logging.rst @@ -0,0 +1,85 @@ + +.. index:: Logging; Logging in Logiken +.. index:: Logiken; Logging in Logiken + +.. role:: bluesup +.. role:: redsup + + +================== +Logging in Logiken +================== + +Logger Konfiguration +==================== + +Damit aus Logiken heraus geloggt werden kann, muss dafür in der Logging-Konfigurationsdatei ``etc/logging.yaml`` +ein entsprechender Logger konfiguriert sein. + +Es sollte auf jeden Fall ein Standardlogger für alle Logiken konfiguriert sein, damit Warnungen und Fehler geloggt +werden können. Dazu ist in der Standardkonfiguration von SmartHomeNG der notwendige Logger bereits folgendermaßen +konfiguriert: + +.. code-block:: yaml + :caption: etc/logging.yaml + + logger: + + ... + + logics: + handlers: [shng_details_file] + level: WARNING + +Wenn aus allen Logiken heraus auch INFOs und DEBUG Informationen geloggt werden sollen, kann hier der ``level`` +entsprechend angepasst werden. + +Ein besserer Weg ist es jedoch, für die jeweilige Logik aus der heraus weitere Informationen geloggt werden sollen, +einen eigenen Logger anzulegen. Dadurch kann das Logging für jede Logik unabhängig von den anderen Logiken festgelegt +werden. Dazu muss ein weiterer Logger angelegt werden, der den Namen der Logik enthält ``logics.``. +Dabei ist der Logikname **nicht** der Name der Python Skript Datei, sondern der Name des Abschnitts in der +Konfigurationsdatei ``etc/logics.yaml``. + +Für eine Logik mit dem Namen ``example``, sieht das beispielsweise folgendermaßen aus: + +.. code-block:: yaml + :caption: etc/logging.yaml + + logger: + + ... + + logics: + handlers: [shng_details_file] + level: WARNING + + logics.example: + level: INFO + +Als Handler wird dabei der bereits im Logger ``logics`` definierte Handler verwendet. Es können bei Bedarf im +Logger der einzelnen Logik zusätzliche handler angegeben werden. Dabei muss darauf geachtet werden, dass der im +Logger ``logics`` definierte Handler nicht erneut angegeben wird, sa sonst die Logausgaben doppelt erfolgen. + + + +Logging im Code +=============== + +Nachdem das Logging für die Logik/Logiken wie oben beschrieben konfiguriert wurde, können aus dem Code der Logik +heraus folgendermaßen Logeinträge erstellt werden: + +.. code-block:: python + + logger.error("Logtext der mit dem Level ERROR geloggt wird") + + logger.warning("Logtext der mit dem Level WARNING geloggt wird") + + logger.info("Logtext der mit dem Level INFO geloggt wird") + + logger.debug("Logtext der mit dem Level DEBUG geloggt wird") + + +Zusätzlich zu den Logeinträgen die in der Logik explizit erzeugt werden, wird vor Aufruf der Logik ein Logeintrag +erzeugt, der anzeigt, was die Ausführung der Logik getriggert hat. Dieser Logeintrag wird im Level DEBUG geschrieben. +Er erscheint in den Logdateien also nur, wenn der Loglevel für die entsprechende Logik auf DEBUG gesetzt ist. + diff --git a/doc/user/source/referenz/logiken/logiken_logic_objekt.rst b/doc/user/source/referenz/logiken/logiken_logic_objekt.rst new file mode 100644 index 0000000000..1c2edc5911 --- /dev/null +++ b/doc/user/source/referenz/logiken/logiken_logic_objekt.rst @@ -0,0 +1,123 @@ + +.. role:: redsup +.. role:: bluesup +.. role:: greensup +.. role:: blacksup + +==================================== +Die Objekte **logic** und **logics** +==================================== + +.. index:: logic Objekt; Logiken +.. index:: Logiken; logic Objekt + +Das ``logic`` Objekt +==================== + +Dieses Objekt bietet Zugriff auf das aktuelle Logikobjekt. Es ist möglich, während der Laufzeit die meisten +logischen Attribute (wie crontab, cycle, ...) abzufragen und ändern. Diese Änderungen gehen nach dem Neustart +von SmartHomeNG verloren, da sie nicht in die Konfigurationsdatei ``etc/logic.yaml`` geschrieben werden. + +Ein Zugriff auf das Objekt ``logic`` ist nur aus der Logik selbst zur Laufzeit möglich. + + +Definierte Methoden des Logikobjekts: + ++----------------------+--------------------------------------------------------------------------------------------------------+ +| Methode | Erläuterung | ++======================+========================================================================================================+ +| ``logic.id()`` | Diese Methode liefert dem Namen der Logik wie in **../etc/logic.yaml** angegeben. | ++----------------------+--------------------------------------------------------------------------------------------------------+ +| ``logic.last_run()`` | Diese Methode liefert den letzten Lauf dieser Logik (vor aktuellen Lauf). | ++----------------------+--------------------------------------------------------------------------------------------------------+ +| ``logic.disable()`` | Konfigurierte Logiken sind standardmäßig aktiv und werden entsprechend der Konfiguration ausgeführt. | +| | Diese Methode deaktiviert die Logik, sodass deren Ausführung unterbunden wird. (Ab SmartHomeNG v1.3) | ++----------------------+--------------------------------------------------------------------------------------------------------+ +| ``logic.enable()`` | Eine bereits deaktivierte Logik kann mit dieser Methode wieder aktiviert werden. (Ab SmartHomeNG v1.3) | ++----------------------+--------------------------------------------------------------------------------------------------------+ + + +Vordefinierte Attribute des Logikobjekts: + ++--------------------------------+-----------------------------------------------------------------------------------------+ +| Attribut | Erläuterung | ++================================+=========================================================================================+ +| ``logic.name`` | Das Attribut logic.name liefert das selbe Ergebnis wie die Methode logic.id() | ++--------------------------------+-----------------------------------------------------------------------------------------+ +| ``logic.crontab`` | Das Attribut liefert das aktuelle **crontab** Setting dieser Logik. | ++--------------------------------+-----------------------------------------------------------------------------------------+ +| ``logic.cycle`` | Das Attribut liefert das aktuelle **cycle** Setting dieser Logik. | ++--------------------------------+-----------------------------------------------------------------------------------------+ +| ``logic.prio`` | Das Attribut liefert das aktuelle **prio** Setting dieser Logik. | ++--------------------------------+-----------------------------------------------------------------------------------------+ +| ``logic.filename`` | Das Attribut liefert den Dateinamen des Python Skripts dieser Logik. | ++--------------------------------+-----------------------------------------------------------------------------------------+ +| ``logic.`` | Liefert den konfigurierten Parameter oder den Wert einer | +| | in einem vorherigen Lauf dieser Logik persistieren Variabl | ++--------------------------------+-----------------------------------------------------------------------------------------+ + +Zusätzlich gibt es noch das Dictionary ``trigger``, welches Infromationen dazu enthält, wodurch der aktuelle Lauf +der Logik getriggert wurde. + + + +.. index:: trigger dict; Logiken +.. index:: Logiken; trigger dict + +Das ``trigger`` Dictionary +-------------------------- + +Das ``trigger`` dict ist ein Python-Dictionary, welches als Laufzeitumgebung einige Informationen über das +Ereignis liefert, das die Logik ausgelöst hat. + +Das Dictionary enthält folgende Informationen: + ++-------------------------------+--------------------------------------------------------------------------------------------------------+ +| Attribut/Funktion | Erläuterung | ++===============================+========================================================================================================+ +| ``trigger['by']`` | Auslöser (``Scheduler``, ``Item``, etc.) | ++-------------------------------+--------------------------------------------------------------------------------------------------------+ +| ``trigger['source']`` | enthält beim Auslöser ``Item`` den Pfad des Items, welches die Logik getriggert hat und beim | +| | Auslöser ``Scheduler`` die Information ob es ein ``cron`` oder ein ``cycle`` Eintrag war. | ++-------------------------------+--------------------------------------------------------------------------------------------------------+ +| ``trigger['source_details']`` | Falls eine Logik aus einem Item heraus getriggert wurde (also trigger['by'] == Item ist), enthält | +| | ``trigger['source_details']`` weitere Details zum Auslöser (Beispiel: 'knx:1.1.241:ga=3/3/5') | +| | Falls der Auslöser der Scheduler war, enthält ``trigger['source_details']`` die Zyklus Teit oder den | +| | auslösenden ``crontab`` Eintrag. | ++-------------------------------+--------------------------------------------------------------------------------------------------------+ +| ``trigger['dest']`` | | ++-------------------------------+--------------------------------------------------------------------------------------------------------+ +| ``trigger['value']`` | enthält den Wert des Items, dass die Logik getriggert hat. | ++-------------------------------+--------------------------------------------------------------------------------------------------------+ + + +.. index:: logics Objekt; Logiken +.. index:: Logiken; logics Objekt + +Das ``logics`` Objekt +===================== + +Zugriff auf das Logics-API über das logics Objekt: + ++---------------------------------+---------------------------------------------------------------------------------------------------------+ +| Methode | Erläuterung | ++=================================+=========================================================================================================+ +| logics. | ermöglicht den Zugriff auf das Logics API, welches unter **APIs von SmartHomeNG** im Abschnitt | +| | **Entwicklung** beschrieben ist. Im folgenden sind einige Beispiele aufgeführt. | ++---------------------------------+---------------------------------------------------------------------------------------------------------+ +| logics.scheduler_add() | Hinzufügen eines Scheduler Eintrages für den logics-Namensraum. Der Syntax entspricht der | +| | scheduler.add() Methode. | ++---------------------------------+---------------------------------------------------------------------------------------------------------+ +| logics.scheduler_change() | Ändern eines Scheduler Eintrages im logics-Namensraum. Der Syntax entspricht der scheduler.change() | +| | Methode. | ++---------------------------------+---------------------------------------------------------------------------------------------------------+ +| logics.scheduler_remove() | Löschen eines Scheduler Eintrages im logics-Namensraum. Der Syntax entspricht der scheduler_remove() | ++---------------------------------+---------------------------------------------------------------------------------------------------------+ +| logics.trigger() | Triggern einer im Logik | ++---------------------------------+---------------------------------------------------------------------------------------------------------+ +| logics.set_config_section_key() | Setzt den Wert eines Schlüssels für eine angegebene Logik (Abschnitt) permanent in ../etc/logic.yaml | ++---------------------------------+---------------------------------------------------------------------------------------------------------+ + +Der vollständige Syntax der Methoden kann im Abschnitt :doc:`Entwicklung/APIs von SmartHomeNG ` +dem **Logics-API** entnommen werden. + diff --git a/doc/user/source/referenz/logiken/logiken_mqtt.rst b/doc/user/source/referenz/logiken/logiken_mqtt.rst new file mode 100644 index 0000000000..2d9640a4f7 --- /dev/null +++ b/doc/user/source/referenz/logiken/logiken_mqtt.rst @@ -0,0 +1,171 @@ + +.. index:: mqtt; Logiken +.. index:: Logiken; mqtt + +=========================== +Nutzung von MQTT in Logiken +=========================== + +Die Nutzung des MQTT Protokolls in Logiken wird durch das mqtt Modul von SmartHomeNG möglich, welches ab Version 1.7 +zur Verfügung steht. Zur Nutzung muss das mqtt Modul geladen und konfiguriert sein und der konfigurierte MQTT Broker +muss laufen. + + +Das mqtt Objekt +=============== + +Um MQTT in Logiken zu nutzen, steht ein Objekt **mqtt** zur Verfügung. Falls das mqtt Modul geladen/konfiguriert ist +oder keine Verbindung zum Broker besteht, ist **mqtt** **None**. In Logiken kann folgendermaßen geprüft werden, ob +MQTT Unterstützung besteht: + +.. code-block:: python + :caption: Test ob MQTT Unterstützung besteht + + if mqtt is None: + # no MQTT support available + logger.warning("MQTT module is not loaded or not yet initialized") + + +Das **mqtt** Objekt stellt eine Reihe von Methoden bereit, um MQTT Messages zu versenden oder zu empfangen. +Aus diese Methoden kann natürlich nur zugegriffen werden, wenn es das mqtt Objet gibt (also mqtt nicht None ist). +Die definierten Methoden sind folgende: + + +mqtt.publish_topic() +-------------------- + +Eine MQTT Message kann versendet werden, indem die Funktion folgendermaßen aufgerufen wird: + +.. code-block:: python + :caption: publish_topic() + + mqtt.publish_topic(source_logic, topic, payload) + + ++-------------------------+------------------------------------------------------------------------------------------------------+ +| Parameter | Bemerkung | ++=========================+======================================================================================================+ +| source_logic | Name der Logik, welche die Funktion **publish_topic** aufruft. Hier kann einfach die Variable | +| | **logic.name** genutzt werden. | ++-------------------------+------------------------------------------------------------------------------------------------------+ +| topic | Topic der zu veröffentlichenden MQTT Message als **str**. | ++-------------------------+------------------------------------------------------------------------------------------------------+ +| payload | Payload der zu veröffentlichenden MQTT Message. Die Variable kann jedem Python Datentyp sein. | ++-------------------------+------------------------------------------------------------------------------------------------------+ + + +mqtt.subscribe_topic() +---------------------- + +Messages die ein bestimmtes MQTT Topic enthalten, können folgendermaßen abonniert werden: + +.. code-block:: python + :caption: subscribe_topic() + + mqtt.subscribe_topic(source_logic, topic, callback, qos=None, payload_type='str', bool_values=None) + + ++-------------------------+------------------------------------------------------------------------------------------------------+ +| Parameter | Bemerkung | ++=========================+======================================================================================================+ +| source_logic | Name der Logik, welche die Funktion **subscribe_topic** aufruft. Hier kann einfach die Variable | +| | **logic.name** genutzt werden. | ++-------------------------+------------------------------------------------------------------------------------------------------+ +| topic | Topic welches aboniert werden soll. | ++-------------------------+------------------------------------------------------------------------------------------------------+ +| callback | Name der Logik, welche bei Empfang des abonnierten Topics getriggert werden soll. Das kann die | +| | aufrufende Logik sein oder eine andere Logik. Wenn die aufrufende Logik getriggert werden soll, kann | +| | hier einfach die Variable **logic.name** genutzt werden. | ++-------------------------+------------------------------------------------------------------------------------------------------+ +| qos | **Optional**: Quality-of-Service, falls ein von der Standardeinstellung abweichender qos genutzt | +| | werden soll. Anderenfalls kann dieser Parameter weggelassen werden. | ++-------------------------+------------------------------------------------------------------------------------------------------+ +| payload_type | **Optional**: Datentyp in welchen die Payload umgewandelt werden soll. Wenn dieser Parameter nicht | +| | angegeben wird, wird der Datentyp **str** verwendet. Angegeben werden können folgende SmartHomeNG | +| | Datentypen: 'str', 'num', 'bool', 'list', 'dict', 'scene' oder 'bytes' - **bytes** bedeutet, dass | +| | keine Typenwandlung vorgenommen wird. | ++-------------------------+------------------------------------------------------------------------------------------------------+ +| bool_values | **Optional**: Legt fest, welche Werte des Payload als **bool** Values interpretiert werden sollen. | +| | Falls der Parameter angegeben wird, muss eine Liste mit **zwei** Werten angegeben werden, wobei der | +| | erste Wert für **False** steht und der zweiter Wert für **True** | ++-------------------------+------------------------------------------------------------------------------------------------------+ + +Wenn eine als **callback** angegebene Logik getriggert wird, ist das **trigger** dict mit folgenden Werten belegt: + +- **trigger['source']** - 'mqtt' - Konstante +- **trigger['by']** - - Dadurch kann bestimmt werden, wie die payload zu behandeln ist, falls eine Logik die Callbacks mehrer topics erhält +- **trigger['value']** - , wobei der Datentyp der payload dem entspricht, was in mqtt.subscribe_topic() als payload_type angegeben wurde + +Es können mehrere Logiken das selbe Topic abonnieren. Alle Logiken die das Topic abonniert haben, werden beim Eintreffen +einer passenden MQTT Message getriggert. + + +mqtt.unsubscribe_topic() +------------------------ + +Ein bestehendes Abonnement für Messages die ein bestimmtes MQTT Topic enthalten, kann folgendermaßen beendet werden: + +.. code-block:: python + :caption: unsubscribe_topic() + + mqtt.unsubscribe_topic(source_logic, topic) + + ++-------------------------+------------------------------------------------------------------------------------------------------+ +| Parameter | Bemerkung | ++=========================+======================================================================================================+ +| source_logic | Name der Logik, welche die Funktion **subscribe_topic()** für den Topic aufgerufen hatte. | ++-------------------------+------------------------------------------------------------------------------------------------------+ +| topic | Topic dessen Subscription beendet werden soll. | ++-------------------------+------------------------------------------------------------------------------------------------------+ + + + +Beispiel Logik +============== + +Hier ist eine Beispiel Logik, die sowohl Subscriptions ausführt, als auch die Callbacks behandelt: + +.. code-block:: python + :caption: Beispiel Logik **mqtt_demo** + + #!/usr/bin/env python3 + # logics/mqtt_demo.py + + def logic_publish_topic(logger, mqtt, logic, topic, payload): + logger.info("Function '{}()' - called by '{}()' in logic '{}'".format(inspect.stack()[0][3], inspect.stack()[1][3], logic.name)) + if mqtt.publish_topic(logic.name, topic, payload): + logger.info("Function '{}()' - test-topic was published".format(inspect.stack()[0][3], inspect.stack()[1][3])) + else: + logger.warning("Function '{}()' - test-topic was NOT published".format(inspect.stack()[0][3], inspect.stack()[1][3])) + + def logic_subscribe_topic(logger, mqtt, logic, topic, payload_type='str'): + logger.info("Function '{}()' - called by '{}()' in logic '{}'".format(inspect.stack()[0][3], inspect.stack()[1][3], logic.name)) + mqtt.subscribe_topic(logic.name, topic, None, payload_type, logic.name) + + + # logic main-code starts here + logger.info("Triggered: trigger['source'] = {}, trigger[by] = {}, trigger[value] = {}".format(trigger['source'], trigger['by'], trigger['value']) ) + if mqtt is None: + # no MQTT support available + logger.warning("MQTT module is not loaded or not yet initialized") + elif trigger['source'] == 'mqtt': + # callback received + topic = trigger['by'] + payload = trigger['value'] + logger.info("MQTT received topic '{}': payload = '{}' - type(payload) = {})".format(topic, payload, type(payload))) + else: + mydict = {'txt': 'Test payload 2', 'num': 5} + # logger, mqtt and logic are handed over to functions, because only this way they are accessable in a logic's function + logic_publish_topic(logger, mqtt, logic, 'test_mqtt/topic', 'Test payload') + logic_publish_topic(logger, mqtt, logic, 'test_mqtt/topic2', mydict) + logic_subscribe_topic(logger, mqtt, logic, 'test_mqtt/sub') + logic_subscribe_topic(logger, mqtt, logic, 'test_mqtt/sub2', 'dict') + + +.. hint:: + + Den **logger** müsste man nicht unbedingt an die Funktionen in der Logik übergeben, aber dann würden im Log die + Einträge aus Funktionen innerhalb der Logik im Logfile als Modul nicht **logics.mqtt_demo** angeben, sondern + **scheduler**. + diff --git a/doc/user/source/referenz/logiken/logiken_persistente_vars.rst b/doc/user/source/referenz/logiken/logiken_persistente_vars.rst new file mode 100644 index 0000000000..808ad8413a --- /dev/null +++ b/doc/user/source/referenz/logiken/logiken_persistente_vars.rst @@ -0,0 +1,140 @@ + +.. index:: Persistente Variablen; Logiken +.. index:: Logiken; Persistente Variablen + +.. role:: redsup +.. role:: bluesup +.. role:: greensup +.. role:: blacksup + +===================== +Persistente Variablen +===================== + +Normale Variablen innerhalb von Logiken sind nur für den jeweiligen Lauf gültig. Es ist jedoch +in einigen Fällen notwendig, Werte zwischen verschiedenen Läufen einer Logik zu übergeben. + +Solche persistente Variablen sind in Logiken von SmartHomeNG möglich, es sind jedoch einige +Dinge zu beachten: + +- Diese Variablen sind über mehrere Läufe von Logiken persistent. Allerdings gilt das nur während + des Laufs von SmartHomeNG. Bei einem Neustart von SmartHomeNG gehen diese Werte verloren. + Der Wert einer solchen Variablen geht auch verloren, wenn die Logik über das Backend während + der Laufzeit von SmartHomeNG gespeichert und neu geladen wird. +- Beim 1. Lauf einer Logik nach dem Start von SmartHomeNG existieren diese Variablen nicht. Der + erste Zugriff innerhalb einer Logik muss deshalb in einen **if not hasattr():** Ausdruck + eingebunden werden. +- Diese Variablen sind **lokal zur Logik**. Sie stehen außerhalb der Logik, die sie definiert hat, + nicht zur Verfügung. Wenn die Daten auch außerhalb der Logik verwendet werden, müssen sie in + Items abgelegt werden. (Ein Sonderfall sind Werte, die zwar außerhalb der definierenden Logik + verwendet werden sollen, aber nur in Logiken. Hier gibt es eine weitere Möglichkeit, die + weiter unten beschrieben wird). +- Es dürfen einige Namen nicht verwendet werden, da sie interne Variablen des Logik Objektes + überschreiben würden. + +Die Persistenz dieser Variablen gilt nur während des Laufs von SmartHomeNG. Mit der Beendigung von +SmartHomeNG gehen die gespeicherten Werte verloren. + + +Einrichtung +=========== + +Normale Variablen sind lokal zum Lauf der der Logik. Eine Variable **myvar** die während eines +Laufes der Logik einen Wert zugewiesen bekommt, ist beim Beginn des nächsten Laufes nicht +definiert. + +Um eine Variable so zu definieren, dass sie die Zeit bis zum nächsten Lauf der Logik überdauert, +muss sie als Attribut zur Logik definiert werden. Also statt: + +.. code-block:: python + + myvar = 'my Value' + +muss die Variable folgendermaßen definiert werden: + +.. code-block:: python + + logic.myvar = 'my Value' + + +Die Variable **logic.myvar** übersteht die Zeit bis zum nächsten Lauf der Logik und sie steht +nur in der Logik zur Verfügung, die sie auch definiert hat. + + +Existenz einer Variablen sicherstellen +====================================== + +Wenn auf eine Variable zugegriffen wird bevor sie definiert wird, führt das zu einer Exception +und der Rest der Logik wird nicht ausgeführt. Beim ersten Lauf einer Logik nach einem Neustart +von SmartHomeNG existiert jedoch keine Variable aus vorangegangenen Läufen. Sie muss erstmal +definiert werden. Das kann zum Beispiel in der folgenden Form erfolgen, in der die Variable +**logic.myvar** mit dem Wert **None** initialisiert wird, falls sie nicht existiert + +.. code-block:: python + :caption: Sicherstellen, dass die Variable existiert + + if not hasattr(logic, 'myvar'): + logic.myvar = None + + +Nutzung selbst definierter Parameter +==================================== + +Es ist möglich eigene Parameter in der Datei **../etc/logic.yaml** zu definieren. Diese Parameter +stehen in der Logik unter **logic.** zur Verfügung. Diese Parameter können als +eine bereits initialisierte Variable verstanden/genutzt werden. Sie können in der Logik nicht +nur gelesen, sondern auch verändert werden. Diese Änderung geht wie beschrieben bei einem +Neustart von SmartHomeNG verloren. + + +.. attention:: + + Einschränkungen bei Variablennamen: + + Variablennamen dürfen nicht gleich einem Namen der Attribute sein, die im Abschnitt + **Verfügbare Objekte und Methoden** beschrieben sind. + + +Einrichtung (Logik übergreifend) +================================ + +Statt eine persistende lokale Variable einzurichten: + +.. code-block:: python + + :caption: persistent, lokal zu definierenden Logik + + logic.myvar = 'my Value' + + +kann eine Variable Logik-übergreifend eingerichtet werden. Dann ist als Präfix statt *logic.* +der Präfix *logics.* zu verwenden: + +.. code-block:: python + + :caption: persistent, für alle Logiken zugreifbar + + logics.myvar = 'my Value' + + +Analog zur lokalen persistenten Variable muss die Existenz folgendermaßen sichergestellt werden: + +.. code-block:: python + :caption: Sicherstellen, dass die Variable existiert + + if not hasattr(logics, 'myvar'): + logics.myvar = None + + +Unterschiede zu lokalen persistenten Variablen +---------------------------------------------- + +Eine einmal initialisierte Logik-übergreifende persistente Variable behält ihren Wert bis +zum Neustart von SmartHomeNG. + +.. attention:: + + Da die Logik-übergreifende Variable ihren Wert auch behält, wenn die Logik die sie initialisiert hat + neu geladen wird, kann es zu unerwarteten Ergebnissen kommen, da sich die Logik nun evtl. bei einem + Neustart der Logik anders verhält, als beim Neustart von SmartHomeNG! + diff --git a/doc/user/source/referenz/logiken/logiken_plugin_funktionen.rst b/doc/user/source/referenz/logiken/logiken_plugin_funktionen.rst new file mode 100644 index 0000000000..ab11a1b05d --- /dev/null +++ b/doc/user/source/referenz/logiken/logiken_plugin_funktionen.rst @@ -0,0 +1,63 @@ + +:tocdepth: 5 + +.. role:: redsup +.. role:: bluesup +.. role:: greensup +.. role:: blacksup + + +.. index:: Logiken; Plugin Funktionen + +============================= +Nutzung von Plugin Funktionen +============================= + +Eine Reihe von Plugins stellt öffentliche Funktionen zur Verfügung, die in Logiken genutzt werden können. +Diese Funktionen sind in der Konfigurationsdokumentation des jeweiligen Plugins beschrieben. + +Im folgenden wird die Nutzung am Beispiel der Funktion ``groupwrite()`` des **knx** Plugins erläutert. + +Dokumentation der Plugin Funktion(en) +===================================== + +Um die Dokumentation der Funktionen eins Plugins zu finden, bzw. herauszufinden, ob ein Plugin öffentliche +Funktionen anbietet, klickt man in der Navigation der Anwender Dokumentation auf **Plugins** und anschließend auf +den Unterpunkt für den Plugin Typ (für das **knx** Plugin auf **Gateway Plugins**). In der angezeigten Tabelle +von Plugins klickt man anschließend in der Spalte **Plugin** auf den Namen (also **knx**). Dadurch wird die +Konfigurations-Dokuemntation (Parameter, Item Attribute, etc.) des Plugins angezeigt. + +Auf der Seite den Abschnitt **Plugins Functions** suchen. Für das **knx** Plugin sind hier mehrere Funktionen +aufgeführt, unter anderem auch die Funktion ``groupwrite(ga, data, dpt)`` mit der Beschreibung ihrer Parameter. + + +Aufruf der Plugin Funktion(en) +============================== + +Funktionen von Plugins greifen intern auf Daten der laufenden Plugin Instanz zu. Es kann also in SmartHomeNG +mehrere Funktionen gleichen Namens geben, wenn man mehrere Instanzen eines Plugins konfiguriert hat. Deshalb kann +man nicht über den Namen des Plugins auf die Funktion zugreifen, sondern über den Konfiguration-Namen der jeweiligen +Instanz, also den Namen der Abschnitts in der Konfigurationsdatei ``etc/plugin.yaml``. + +Der Aufruf der Funktion ``groupwrite(ga, data, dpt)`` hätte folgenden Sytax: + +.. code-block:: python + + sh..groupwrite(ga, data, dpt) + +Wenn das knx Plugin in der Datei ``etc/plugin.yaml`` folgendermaßen konfiguriert ist: + +.. code-block:: yaml + + local_knxd: + plugin_name: knx + use_project_file: true + +und auf die Gruppenadresse **1/2/1** der 8-Bit Wert (DPT **5**) **123** geschrieben werden soll, lautet der Aufruf +der Pluginfunktion so: + +.. code-block:: python + + sh.local_knxd.groupwrite('1/2/1', 123, 5) + + diff --git a/doc/user/source/referenz/logiken/logiken_python_module.rst b/doc/user/source/referenz/logiken/logiken_python_module.rst new file mode 100644 index 0000000000..b4b4971b11 --- /dev/null +++ b/doc/user/source/referenz/logiken/logiken_python_module.rst @@ -0,0 +1,48 @@ +:tocdepth: 5 + +.. role:: redsup +.. role:: bluesup +.. role:: greensup +.. role:: blacksup + +.. index:: Python Module; Logiken +.. index:: Logiken; Python Module + + +============================== +Bereits geladene Python Module +============================== + +Im Logik Environment sind diverse Python Module bereits geladen. Auf die Funktionen der folgenden Python Packages +kann also zugegriffen werden, ohne sie vorher explizit importieren zu müssen. + +Standard Python Packages: + ++-----------------+------------------------------------------------+ +| Package | Beschreibung | ++=================+================================================+ +| ``sys`` | System-specific parameters and functions | ++-----------------+------------------------------------------------+ +| ``os`` | Miscellaneous operating system interfaces | ++-----------------+------------------------------------------------+ +| ``time`` | Time access and conversions | ++-----------------+------------------------------------------------+ +| ``datetime`` | Basic date and time types | ++-----------------+------------------------------------------------+ +| ``random`` | Generate pseudo-random numbers | ++-----------------+------------------------------------------------+ +| ``queue`` | A synchronized queue class | ++-----------------+------------------------------------------------+ +| ``subprocess`` | Subprocess management | ++-----------------+------------------------------------------------+ + + +weitere Packages von pypi.org: + ++-----------------+------------------------------------------------+ +| Package | Beschreibung | ++=================+================================================+ +| ``ephem`` | Compute positions of the planets and stars | ++-----------------+------------------------------------------------+ + + diff --git a/doc/user/source/referenz/logiken/logiken_smarthomeng_methoden.rst b/doc/user/source/referenz/logiken/logiken_smarthomeng_methoden.rst new file mode 100644 index 0000000000..ea51441d36 --- /dev/null +++ b/doc/user/source/referenz/logiken/logiken_smarthomeng_methoden.rst @@ -0,0 +1,80 @@ +:tocdepth: 5 + +.. index:: SmartHomeNG Methoden; Logiken +.. index:: Logiken; SmartHomeNG Methoden + + +==================== +SmartHomeNG Methoden +==================== + +.. include:: /referenz/smarthomeng/feiertage_datum_zeit.rst + + +.. include:: /referenz/smarthomeng/methoden_sonne_mond.rst + + + +sh.tools Objekt +=============== + +Das ``sh.tools`` Objekt stellt folgende nützliche Funktionen zur Verfügung: + +sh.tools.ping() +--------------- + +Sendet ein Ping an einen Computer und liefert das Ergebnis. Beispiel: + +``sh.office.laptop(sh.tools.ping('hostname'))`` + +setzt das Item ``office.laptop`` entsprechend der Rückmeldung ob ein Ping erfolgreich war oder nicht. + +sh.tools.dewpoint() +------------------- + +Berechnet den Taupunkt für eine gegebene Temperatur und Feuchtigkeit. Beispiel: + +``sh.office.dew(sh.tools.dewpoint(sh.office.temp(), sh.office.hum())`` + +setzt das Item ``office.dew`` auf das Ergebnis der Taupunktberechnung der Itemwerte von ``office.temp`` und ``office.hum`` + +sh.tools.fetch_url() +-------------------- + +Liefert dem Inhalt einer Webseite als String oder ``False`` wenn ein Fehler auftritt. + +``sh.tools.fetch_url('https://www.regular.com')`` + +Es ist möglich als Parameter den Benutzernamen und ein Password anzugeben um die Abfrage bei der zu authentifizieren. + +``sh.tools.fetch_url('https://www.special.com', 'username', 'password')`` + +Weiterhin kann ein Parameter für eine Zeitüberschreitung bestimmt werden: + +``sh.tools.fetch_url('https://www.regular.com', timeout=4)`` + +bricht nach 4 Sekunden ohne Ergebnis ab + +sh.tools.dt2ts(dt) +------------------ + +Wandelt ein datetime Objekt in einen Unix Zeitstempel um. + +sh.tools.dt2js(dt) +------------------ + +Wandelt ein datetime Objekt in einen json Zeitstempel um. + + +sh.tools.rel2abs(temp, hum) +--------------------------- + +Wandelt einen relativen Feuchtigkeitswert in einen absoluten Feuchtigkeitswert um. + + +sh.tools.runtime() +------------------ + +Liefert die Laufzeit von SmartHomeNG zurück. + + diff --git a/doc/user/source/referenz/logiken/logiken_standardparameter.rst b/doc/user/source/referenz/logiken/logiken_standardparameter.rst new file mode 100644 index 0000000000..44f1b1b830 --- /dev/null +++ b/doc/user/source/referenz/logiken/logiken_standardparameter.rst @@ -0,0 +1,50 @@ + +.. index:: Logiken; Standardparameter +.. index:: Standardparameter; Logiken + + +Standardparameter +================= + +Die folgenden Parameter können für eine Logik in der Konfigurationsdatei im Verzeichnis ../etc angegeben werden. + ++------------------+-----------------------------------------------------------------------------------------------+ +| **Parameter** | **Beschreibung** | ++==================+===============================================================================================+ +| filename | Dateiname des Logik-Codes. Diese Datei muss im Verzeichnis **../logics** liegen. Dieser | +| | Parameter muss angegeben werden. | ++------------------+-----------------------------------------------------------------------------------------------+ +| watch_item | **Optional**: String oder Liste von Strings, die jeweils einen Item-Pfad repräsentieren. | +| | Eine Veränderung eines der hier aufgeführten Items führt dazu, dass die Logik ausgeführt | +| | wird. Details dazu stehen :ref:`hier ` . | ++------------------+-----------------------------------------------------------------------------------------------+ +| crontab | **Optional**: String oder Liste von Strings, die einen crontab Eintrag darstellen. Der Syntax | +| | des Parameters entspricht dem Syntax des **crontab** Attributes von Items. Details dazu | +| | stehen :ref:`hier ` . | ++------------------+-----------------------------------------------------------------------------------------------+ +| cycle | **Optional**: Angabe einer Zykluszeit, die angibt, in welchem Zeitabstand die Logik | +| | periodisch ausgelöst werden soll. Der Syntax des Parameters entspricht dem Syntax des | +| | **cycle** Attributes von Items. Details dazu stehen | +| | :ref:`hier ` . | ++------------------+-----------------------------------------------------------------------------------------------+ +| prio | **Optional**: Angabe einer Priorität für die Logik. Die Priorität kommt nur bei Logiken zum | +| | Einsatz, die ein Schedule haben, bei denen also der Paramter **crontab** oder **cycle** | +| | angegeben wurde. Die Priorität sollte zwischen **1** und **5** liegen. Falls der Parameter | +| | nicht angegeben wird, wird die Standardpriorität **3** verwendet. | ++------------------+-----------------------------------------------------------------------------------------------+ +| visu_acl | **Optional**: Dieser Parameter wird durch das Plugin **visu_websocket** | +| | implementiert. Wenn dieser Parameter auf **True** gesetzt wird, kann die Logik | +| | von einer Visualisierung aus (z.B. smartVISU) ausgelöst werden. | ++------------------+-----------------------------------------------------------------------------------------------+ +| enabled | **Optional**: ``enabled`` kann auf ``False`` gesetzt werden um die Ausführung der Logik | +| | auszusetzen. Der Ausführungsstatus der Logik kann über das CLI-Plugin oder das Admin | +| | Interface gesetzt werden. | ++------------------+-----------------------------------------------------------------------------------------------+ +| | **Optional**: Es können weitere Parameter definiert werden, diese können aus der | +| | Logik heraus abgefragt werden und haben sonst keine Funktion. | ++------------------+-----------------------------------------------------------------------------------------------+ + +Falls keiner der optionalen Parameter **crontab**, **watch_item** oder **cycle** angegeben wird, wird +die Logik nicht automatisiert ausgeführt. Sie kann dann nur aus Plugins oder (falls konfiguriert) über +eine Visualisierung ausgelöst werden. + diff --git a/doc/user/source/referenz/module/module_websocket.rst b/doc/user/source/referenz/module/module_websocket.rst index 9f1be88f59..97ab055a13 100644 --- a/doc/user/source/referenz/module/module_websocket.rst +++ b/doc/user/source/referenz/module/module_websocket.rst @@ -4,8 +4,8 @@ .. role:: redsup .. role:: bluesup -Module websocket :redsup:`new` -============================== +Module websocket +================ Dieses Modul implementiert die Möglichkeit mit SmartHomeNG über das Websocket Protokoll zu kommunizieren. diff --git a/doc/user/source/referenz/plugins/plugins.rst b/doc/user/source/referenz/plugins/plugins.rst index 0d50d5471e..549ead3269 100644 --- a/doc/user/source/referenz/plugins/plugins.rst +++ b/doc/user/source/referenz/plugins/plugins.rst @@ -2,8 +2,10 @@ .. index:: Referenz; Plugins .. Index:: Plugins; Referenz -.. role:: bluesup .. role:: redsup +.. role:: bluesup +.. role:: greensup +.. role:: blacksup ======= diff --git a/doc/user/source/referenz/referenz.rst b/doc/user/source/referenz/referenz.rst index 04595ce216..9467820160 100644 --- a/doc/user/source/referenz/referenz.rst +++ b/doc/user/source/referenz/referenz.rst @@ -1,3 +1,4 @@ +:tocdepth: 5 .. index:: Referenz @@ -14,14 +15,17 @@ können. .. toctree:: - :maxdepth: 4 + :maxdepth: 5 :hidden: :titlesonly: smarthomeng/smarthomeng items/items + logiken/logiken module/module plugins/plugins metadata/metadata logging/logging userfunctions/userfunctions + libraries_plugins_logics + APIs diff --git a/doc/user/source/logiken/objekteundmethoden_feiertage_datum_zeit.rst b/doc/user/source/referenz/smarthomeng/feiertage_datum_zeit.rst similarity index 100% rename from doc/user/source/logiken/objekteundmethoden_feiertage_datum_zeit.rst rename to doc/user/source/referenz/smarthomeng/feiertage_datum_zeit.rst diff --git a/doc/user/source/referenz/smarthomeng/methoden_sonne_mond.rst b/doc/user/source/referenz/smarthomeng/methoden_sonne_mond.rst new file mode 100644 index 0000000000..0e03260a48 --- /dev/null +++ b/doc/user/source/referenz/smarthomeng/methoden_sonne_mond.rst @@ -0,0 +1,154 @@ + + +Astronomie (Sonne und Mond) +=========================== + +sh.sun +------ + +Dieses Objekt bietet Zugriff auf Parameter der Sonne. Um dieses Objekt zu verwenden, ist es erforderlich, +den Breitengrad (latitude, z.B. lat: 53.5989481) und den Längengrad (longitude z.B. lon: 10.0459898), +sowie die Höhe über dem Meeresspiegel (elevation z.B.: elev: 20) in der Datei **../etc/smarthome.yaml** anzugeben. + + +sh.sun.pos() +~~~~~~~~~~~~ + +Die Methode ``sh.sun.pos()`` liefert die Position der Sonne und hat zwei optionale Parameter: + +- Der erste Parameter ``offset`` ist der zeitliche Offset zu jetzt in Minuten +- Der zweite Parameter ``degree`` gibt an, ob das Ergebnis im Bogenmaß oder in Grad erfolgen soll. + +.. code-block:: python + :caption: Beispiele zur Sonnenstandsberechnung + + # sh.sun.pos(offset) hierbei gibt offset die Differenz in Zeit-Minuten zur aktuellen Zeit an + + azimut, altitude = sh.sun.pos() # liefert die aktuelle Position der Sonne, Ergbnis im Bogenmaß + azimut, altitude = sh.sun.pos(30) # liefert die Position, welche die Sonne in 30 Minuten haben wird im Bogenmaß + + azimut, altitude = sh.sun.pos(degree=True) # liefert die aktuelle Position der Sonne in Grad + azimut, altitude = sh.sun.pos(-45, True) # liefert die Position, welche die Sonne vor 45 Minuten hatte in Grad + + +sh.sun.set() +~~~~~~~~~~~~ + +Die Methode ``sh.sun.set()`` hat liefert den Zeitpunkt des nächsten Sonnenuntergangs und hat einen optionale Parameter: + +- Der Parameter ``offset`` ist der Offset zum Sonnenuntergang in Grad + (eine negative Zahl gibt an, wieviel Grad die Sonne unter dem Horizont stehen soll) + +Das Ergebnis ist ein auf UTC basierendes ``datetime`` Objekt + +.. code-block:: python + :caption: Beispiele zur Sonnenuntergangsberechnung + + # sh.sun.set(offset) hierbei gibt offset die Differenz in Grad zum nächsten Sonnenuntergang an + + sunset = sh.sun.set() # liefert den utc-basierten Zeitpunkt des nächsten Sonnenuntergangs + sunset_tw = sh.sun.set(-6) # liefert den utc-basierten Zeitpunkt zu dem die Sonne 6° unter dem Horizont + # steht. (Ende der bürgerlichen Abenddämmerung) + + +sh.sun.rise() +~~~~~~~~~~~~~ + +Die Methode ``sh.sun.rise()`` hat liefert den Zeitpunkt des nächsten Sonnenuntergangs und hat einen optionale Parameter: + +- Der Parameter ``offset`` ist der Offset zum Sonnenuntergang in Grad + (eine negative Zahl gibt an, wieviel Grad die Sonne unter dem Horizont stehen soll) + +Das Ergebnis ist ein auf UTC basierendes ``datetime`` Objekt + +.. code-block:: python + :caption: Beispiele zur Sonnenaufgangsberechnung + + # sh.sun.rise(offset) hierbei gibt offset die Differenz in Grad zum nächsten Sonnenaufganges an + + sunrise = sh.sun.rise() # liefert den utc-basierten Zeitpunkt des nächsten Sonnenaufganges + sunrise_tw = sh.sun.rise(-6) # liefert den utc-basierten Zeitpunkt zu dem die Sonne wieder 6° unter + # dem Horizont steht. (Beginn der nächsten bürgerlichen Morgendämmerung) + + +sh.moon +------- + +Neben den drei Funktionen ``pos``, ``set`` und ``rise`` (wie beim Objekt ``sh.sun``) gibt es noch +zwei weitere Funktionen: + +Neben den drei Funktionen ``sh.moon.pos()``, ``sh.moon.set()`` und ``sh.moon.rise()`` (analog zum ``sh.sun`` Objekt) +stehen zwei weitere Funktionen (``sh.moon.light()`` und ``sh.moon.phase()``) zur Verfügung. + + +sh.moon.pos() +~~~~~~~~~~~~~ + +Die Methode ``sh.moon.pos()`` liefert die Position des Mondes und hat zwei optionale Parameter: + +- Der erste Parameter ``offset`` ist der zeitliche Offset zu jetzt in Minuten +- Der zweite Parameter ``degree`` gibt an, ob das Ergebnis im Bogenmaß oder in Grad erfolgen soll. + +.. code-block:: python + :caption: Beispiele zur Mondstandsberechnung + + # sh.moon.pos(offset) hierbei gibt offset die Differenz in Zeit-Minuten zur aktuellen Zeit an + + azimut, altitude = sh.moon.pos() # liefert die aktuelle Position des Mondes, Ergbnis im Bogenmaß + azimut, altitude = sh.moon.pos(30) # liefert die Position, welche der Mond in 30 Minuten haben wird im Bogenmaß + + azimut, altitude = sh.moon.pos(degree=True) # liefert die aktuelle Position des Mondes in Grad + azimut, altitude = sh.moon.pos(-45, True) # liefert die Position, welche der Mond vor 45 Minuten hatte in Grad + + +sh.moon.set() +~~~~~~~~~~~~~ + +Die Methode ``sh.moon.set()`` hat liefert den Zeitpunkt des nächsten Monduntergangs und hat einen optionale Parameter: + +- Der Parameter ``offset`` ist der Offset zum Monduntergang in Grad + (eine negative Zahl gibt an, wieviel Grad der Mond unter dem Horizont stehen soll) + +Das Ergebnis ist ein auf UTC basierendes ``datetime`` Objekt + +.. code-block:: python + :caption: Beispiele zur Monduntergangsberechnung + + # sh.moon.set(offset) hierbei gibt offset die Differenz in Grad zum nächsten Monduntergang an + + moonset = sh.moon.set() # liefert den utc-basierten Zeitpunkt des nächsten Monduntergangs + moonset_tw = sh.moon.set(-6) # liefert den utc-basierten Zeitpunkt zu dem der Mond 6° unter dem Horizont steht + + +sh.moon.rise() +~~~~~~~~~~~~~~ + +Die Methode ``sh.moon.rise()`` hat liefert den Zeitpunkt des nächsten Monduntergangs und hat einen optionale Parameter: + +- Der Parameter ``offset`` ist der Offset zum Monduntergang in Grad + (eine negative Zahl gibt an, wieviel Grad der Mond unter dem Horizont stehen soll) + +Das Ergebnis ist ein auf UTC basierendes ``datetime`` Objekt + +.. code-block:: python + :caption: Beispiele zur Mondaufgangsberechnung + + # sh.moon.rise(offset) hierbei gibt offset die Differenz in Grad zum nächsten Mondaufganges an + + moonrise = sh.moon.rise() # liefert den utc-basierten Zeitpunkt des nächsten Mondaufganges + moonrise_tw = sh.moon.rise(-6) # liefert den utc-basierten Zeitpunkt zu dem der Mond wieder 6° unter + # dem Horizont steht. + + +sh.moon.light() +~~~~~~~~~~~~~~~ + +``sh.moon.light(offset)`` liefert einen Wert von 0 bis 100 der beleuchteten Fläche zur aktuellen Zeit + Offset. + + +sh.moon.phase() +~~~~~~~~~~~~~~~ + +``sh.moon.phase(offset)`` gibt die Mondphase als ganze Zahl (0 bis 7) zurück, +wobei: 0 = Neumond, 4 = Vollmond, 6 = abnehmender Halbmond + diff --git a/doc/user/source/release/1_9_x.rst b/doc/user/source/release/1_9_x.rst new file mode 100644 index 0000000000..986969f38e --- /dev/null +++ b/doc/user/source/release/1_9_x.rst @@ -0,0 +1,386 @@ +============================ +Release 1.9.1 - 13. Feb 2022 +============================ + +Es gibt eine Menge neuer Features im Core von SmartHomeNG und den Plugins. + +.. note :: + + Diese Release Notes sind ein Arbeitsstand. + + - Berücksichtigt sind Commits im smarthome Repository bis incl. 12. Februar 2022 + (. . .) + - Berücksichtigt sind Commits im plugins Repository bis incl. 12. Februar 2022 + (. . .) + + +Überblick +========= + +Dieses ist neues Release für SmartHomeNG. Die Änderungen gegenüber dem Release v1.9.x sind im +folgenden in diesen Release Notes beschrieben. + + +Unterstützte Python Versionen +----------------------------- + +Die älteste offiziell unterstützte Python Version für SmartHomeNG Release 1.8 ist Python 3.6. +(Siehe auch *Hard- u. Software Anforderungen* im Abschnitt *Installation* zu unterstützten Python Versionen) + +.. + Das bedeutet nicht unbedingt, dass SmartHomeNG ab Release 1.8 nicht mehr unter älteren Python Versionen läuft, + sondern das SmartHomeNG nicht mehr mit älteren Python Versionen getestet wird und das gemeldete Fehler mit älteren + Python Versionen nicht mehr zu Buxfixen führen. + + Es werden jedoch zunehmend Features eingesetzt, die erst ab Python 3.6 zur Verfügung stehen. + So ist Python 3.6 die minimale Vorraussetzung zur Nutzung des neuen Websocket Moduls. + + +Minimum Python Version +---------------------- + +Die absolute Minimum Python Version in der SmartHomeNG startet wurde auf v3.6 angehoben, da Python 3.5 im +September 2020 End-of-Life (End of security fixes) gegangen ist. Bei einer Neuinstallation wird jedoch empfohlen +auf einer der neueren Python Versionen (3.7 oder 3.8) aufzusetzen. + +.. important:: + + Mit dem kommenden Release 1.8 werden die unterstützten Python Versionen + :doc:`(wie hier beschrieben) ` auf **Python 3.6, 3.7, 3.8** angehoben. Python 3.6 + hat eine Reihe sehr interessanter Features und Verbesserungen gebracht, die dann in SmartHomeNG genutzt + werden können. + + Sollten solche neuen Features in den Core Einzug halten, wird die **Absolute Minimum Python Version** auf 3.6 + angehoben werden. Sollten die Features nur in Plugins genutzt werden, so können nur solche Plugins nicht genutzt + werden, wenn eine ältere Python Version als 3.6 eingesetzt wird. + + +Änderungen am Core +================== + +Updates to the CORE +------------------- + +* Fixed initialization for cases where logging.yaml contains instructions that thow an exception + when SmartHomeNG is not fully configured (requirements installed) +* If logging.yaml gives an exception, logging.yaml.default is used +* Changed restart handling, when in debug- or foreground-mode + + +* Logics: + + * Logics are now prevented from beeing executed before SmartHomeNG is in Run-Mode (the initialization has finished) + even if triggerd (by e.g. item initialization) during initialization phase + +* Libs: + + * lib.item: + + * introduce new property last_trigger(_by/_age) and prev_trigger(_by/_age) and according functions. + This way it is possible to figure out what has triggered the eval expression. The property might be + especially useful inside the eval expression itself but can also be used to figure out what, when + (or if) an eval got triggered (without being updated, because maybe the eval didn't succeed or the + value has not changed) + * Get relative item path: Fix issue where last character of expression part got truncated and therefore + eval expression might get invalid + * Fix for eval expressions, if crontab attribute is given with an init value, which is to be used in a + numerical calculation + * Fix for non numeric values on attribute crontab: init = xyz + + * lib.metadata: + + * Added attribute prefix 'my\_' for user defined attributes (for logics, etc.) + + * lib.network: + + * Fix for udp server eventloop too late + * Pass timeout parameter + * Add port to connect message + * Fix validate_inet_addr return for invalid ip + * Fix for clean shutdown if not connected + * Added __str__ methods to simplyfy logging + * Modified startup/connect logging + * Modified __running initialization + * Added more debug logging, modified connect_on_send behaviour to respect autoreconnect setting + * Added autoconnect parameter + + * lib.orb: + + * Set requirement to ephem max version 4.1.0 because 4.1.3 has a bug or breaking change + * Fix never up errors with ephem + + * lib.scheduler: + + * Trigger dict in logics contains now the source (cron, cycle) for scheduler triggers + * Fixed bug where value wasn't set correctly on cron triggers + * Separated code to start a logic task to is's own method + * Implemented 'raise LeaveLogic()' for logics + * Small modification for executing logics + * Added more details to 'update_by' and 'change_by' on items + * Bugfix for 'str' object has no attribute 'get' (line 643) + + * lib.shpypi: + + * Small fixes + + * lib.smarthome: + + * Change getbasedir for user function to avoid deprecated warning + + * lib.triggertimes: + + * Fix a triggertimes with empty params or wrong parameter set size + * Enhance debug message 'eventtime is earlier than starttime' + * Fix a bug in relation to sunset with offset + + +* Modules: + + * admin: + + * eval syntax checker sets 'value' to the 'path relative to' item, if it is specified + + * http: + + * Added datatables for use in webinterfaces of plugins + * datatables: Fix table width in tabs + * datatables: Improve defaults and fix some issues + * datatables: update to 1.11.4 + + * mqtt: Fixed _get_caller_type for logics (see issue #450) + + * websocket: + + * Removed "untested" warning from trigger-logic command + * Loglevel for 'no close frame received or sent' reduced to INFO + + +Änderungen bei Plugins +====================== + +New Plugins +----------- + +For details of the changes of the individual plugins, please refer to the documentation of the respective plugin. + +* : ... + + + +Plugin Updates +-------------- + +* avdevice: + + * Fix web interface logging (introduces with last commit), remove very old versions + +* asterisk: + + * Check connected before calling close in stop + +* avm: + + * Created _pv_1_5_12 before merging PR + * Connect to Smarthome Devices via AHA-Interface (tested with Fritz 440, Comet Dect) + (therefore Plugin AVM smarthome can be depreciated) + * Implemented call deflections (tested with Fritzbox 7530) + * Renamed parameter "index" to "avm_tam_index" + * Code cleanup (add new functions to minimize code repetitions) + * Improved error handling + * Updated WebIF + * Updated documentation + * Bumped version to 1.6.1 + +* casambi: Resend commands on transmission error (e.g. due to sigpipe error) once + +* database: + + * Prevent multiple threads for handling database_maxage + +* dlms: + + * Enhance returned result + * Fixed a bug where no response was returned + * Parameter baudrate will be stored to config now + * Change parameter name for speed + +* enocean: + + * Added support for EEP A5_30_01 and A5_30_03, e.g. water leak sensor + * Extended plugin webinterface + * Suggest next free device ID (offset) value during teach-in process + * New feature "enable/disable logging of unknown devices" via plugin.yaml or WebIf + * Added tooltips to WebIf + +* garminconnect: + + * Fix for login issues, using PyPi package with latest version (4.1.22) now. Still not 100% perfect, due + to the package implementation. But at least working again + * Added call to login method before each data retreival to prevent session timeouts + * Removed broken logout + +* gpio: + + * Fix error handling and log on startup + * Slight improvements in webif concerning datatables + * Update web interface (datatables, realtime values), convert debug messages to f-strings + +* knx: + + * Only execute update_item if plugin is running + * Bumped version to 1.8.0 + * Fix connected bug for init ga + +* modbus_tcp: + + * slaveUnit zu Items hinzugefügt + * slaveUnit-Bug _regToRead-key (adress.unit) + * Bugfix in web interface + * Fixes in example.yaml + * Feature to write to registers + * Bumped version to 1.0.6 + +* mpd: + + * Fix connection related bug, remove scheduler at stop + +* openweathermap: + + * Add struct for airpollution + * Bumped to 1.8.4 + * Changed default update cycle to 900 sec to prevent getting locked out by server, if for example + smartVISU is using the same API key + +* philips_tv: + + * Minor improvements for webinterface + +* rpi1wire: + + * Udpdate WebIF Tables with new functions + * Error handling improved + * Udpdate WebIF Tables with new functions + +* smartvisu: + + * Copy info block.html from _templates dir (for sv 3.2 and up) + +* snap7_logo: + + * Unspecified updates + * Update to 1.6.0 + +* sonos: + + * Fix for use with python 3.9 upwards (Thread::isAlive deprecated) + * Update SoCo base framework to version 0.26.1 + +* stateengine: + + * Add gitignore to webif/visualizations folder to make sure folder is created and files in folder are ignored + * Minor webif update + * Update web interface in real-time and include datatables + +* tasmota: + + * Implemented support if TW light sources + * Implemented ZIGBEE support; + * Updated Docu + * Updated WebIF + * Bumped version to 1.2.0 + +* telegram: + + * Fix web interface (using default template) + * Webif support datatables + * Fix a bug for /info item values + +* uzsu: + + * Use sun and moon objects of sh and new orb.py + * Remove uzsu\_ from scheduler name + * Simplify requirements file: scipy 1.1.0 if python >= 3.7 + * Major web interface update. Clean code, use automatic update of values, implement datatables, etc. + * Removed old uzsu_sun residuum + +* viessmann: + + * Fixed log call + +* withings_health: + + * Updated requirements so that with 2.4.0 of pypi package auth is working again (hopefully) + + +Outdated Plugins +---------------- + +The following plugins were already marked in version v1.6 as *deprecated*. This means that the plugins +are still working, but are not developed further anymore and are removed from the release of SmartHomeNG +in the next release. User of these plugins should switch to corresponding succeeding plugins. + +* System Plugins + + * backend - use the administration interface instead + * sqlite_visu2_8 - switch to the **database** plugin + +* Web Plugins + + * wunderground - the free API is not provided anymore by Wunderground + + +The following plugins are marked as *deprecated* with SmartHomeNG v1.7, because neither user nor tester have been found: + +* Gateway Plugins + + * ecmd + * elro + * iaqstick + * snom + * tellstick + +* Interface Plugins + + * easymeter + * smawb + * vr100 + +* Web Plugins + + * nma + +Moreover, the previous mqtt plugin was renamed to mqtt1 and marked as *deprecated*, because the new mqtt +plugin takes over the functionality. This plugin is based on the mqtt module and the recent core. + + +Retired Plugins +--------------- + +The following plugins have been retired. They had been deprecated in one of the preceding releases of SmartHomeNG. +They have been removed from the plugins repository, but they can still be found on github. Now they reside in +the **plugin_archive** repository from where they can be downloaded if they are still needed. + +* ... + + +Weitere Änderungen +================== + +Tools +----- + +* plugin_metadata_checker.py: + + * Added two checks for plugin name (length and lowercase) + * Fixed check for mandatory value given and default value given + + +Documentation +------------- + +* Added "updated_by()" function to reference page +* Consolidated logic documentation to reference section +* Added Plugins/All plugins page to navigation + + + diff --git a/doc/user/source/release/1_x_x.rst.vorlage b/doc/user/source/release/1_x_x.rst.vorlage index ca1231d676..b6ab411e7b 100644 --- a/doc/user/source/release/1_x_x.rst.vorlage +++ b/doc/user/source/release/1_x_x.rst.vorlage @@ -1,5 +1,5 @@ ========================== -Release 1.x - tt. mmm 2021 +Release 1.x - tt. mmm 2022 ========================== Es gibt eine Menge neuer Features im Core von SmartHomeNG und den Plugins. @@ -17,7 +17,7 @@ Es gibt eine Menge neuer Features im Core von SmartHomeNG und den Plugins. Überblick ========= -Dieses ist neues Release für SmartHomeNG. Die Änderungen gegenüber dem Release v1.8.x sind im +Dieses ist neues Release für SmartHomeNG. Die Änderungen gegenüber dem Release v1.9.x sind im folgenden in diesen Release Notes beschrieben. @@ -77,7 +77,10 @@ Updates in the CORE * ... -* lib....: +* Libs: + + * lib. ...: + * Modules: * ...: @@ -88,6 +91,10 @@ Updates in the CORE * ... +* tests: + + * ... + Änderungen bei Plugins ====================== @@ -118,24 +125,11 @@ in the next release. User of these plugins should switch to corresponding succee * System Plugins - * sqlite - switch to the **database** plugin + * backend - use the administration interface instead * sqlite_visu2_8 - switch to the **database** plugin -* Gateway Plugins - - * tellstick - classic Plugin, not used according to survey in knx-user-forum - -* Interface Plugins - - * netio230b - classic plugin, not used according to survey in knx-user-forum - * smawb - classic plugin, not used according to survey in knx-user-forum - * Web Plugins - * alexa - switch to the **alexa4p3** plugin - * boxcar - classic Plugin, not used according to survey in knx-user-forum - * mail - switch to the **mailsend** and **mailrcv** plugin - * openenergymonitor - classic plugin, not used according to survey in knx-user-forum * wunderground - the free API is not provided anymore by Wunderground @@ -152,13 +146,11 @@ The following plugins are marked as *deprecated* with SmartHomeNG v1.7, because * Interface Plugins * easymeter - * netio230b * smawb * vr100 * Web Plugins - * boxcar * nma Moreover, the previous mqtt plugin was renamed to mqtt1 and marked as *deprecated*, because the new mqtt @@ -170,7 +162,7 @@ Retired Plugins The following plugins have been retired. They had been deprecated in one of the preceding releases of SmartHomeNG. They have been removed from the plugins repository, but they can still be found on github. Now they reside in -the plugin_archive repository from where they can be downloaded if they are still needed. +the **plugin_archive** repository from where they can be downloaded if they are still needed. * ... diff --git a/etc/logging.yaml.default b/etc/logging.yaml.default index 8c62130600..28ca6746f1 100644 --- a/etc/logging.yaml.default +++ b/etc/logging.yaml.default @@ -51,10 +51,10 @@ filters: handlers: shng_warnings_file: - # This handler writes only entries with level WARNING and above + # This handler writes only entries with level NOTICE/WARNING and above # to the file. This allows for a quick check if all is running smooth. # - # To keep the warnings file easy readable, the level should always be WARNING! + # To keep the warnings file easy readable, the level should always be NOTICE! # # The TimedRotatingFileHandler seperates the logentries by day and # keeps the entries of the last seven days in seperate files. @@ -62,7 +62,7 @@ handlers: #class: logging.handlers.TimedRotatingFileHandler (): lib.log.ShngTimedRotatingFileHandler formatter: shng_simple - level: WARNING + level: NOTICE utc: false when: midnight backupCount: 7 @@ -141,29 +141,29 @@ loggers: lib: # Default logger for SmartHomeNG libraries handlers: [shng_details_file] - level: WARNING + level: NOTICE lib.smarthome: # Add all logging handlers that should receive the initial log lines after a startup # (example below) but leave out the logging handlers that are defined in the root-logger # (otherwise log entries will be doubled). # - # 2020-12-29 11:35:34 WARNING lib.smarthome -------------------- Init SmartHomeNG 1.8.0 -------------------- - # 2020-12-29 11:35:34 WARNING lib.smarthome Running in Python interpreter 'v3.8.3 final' in virtual environment - # 2020-12-29 11:35:34 WARNING lib.smarthome - on Linux-4.9.0-6-amd64-x86_64-with-glibc2.17 (pid=24407) - # 2020-12-29 11:35:35 WARNING lib.smarthome - Nutze Feiertage für Land 'DE', Provinz 'HH', 1 benutzerdefinierte(r) Feiertag(e) definiert - # 2020-12-29 11:36:54 WARNING lib.smarthome -------------------- SmartHomeNG initialization finished -------------------- + # 2020-12-29 11:35:34 NOTICE lib.smarthome -------------------- Init SmartHomeNG 1.8.0 -------------------- + # 2020-12-29 11:35:34 NOTICE lib.smarthome Running in Python interpreter 'v3.8.3 final' in virtual environment + # 2020-12-29 11:35:34 NOTICE lib.smarthome - on Linux-4.9.0-6-amd64-x86_64-with-glibc2.17 (pid=24407) + # 2020-12-29 11:35:35 NOTICE lib.smarthome - Nutze Feiertage für Land 'DE', Provinz 'HH', 1 benutzerdefinierte(r) Feiertag(e) definiert + # 2020-12-29 11:36:54 NOTICE lib.smarthome -------------------- SmartHomeNG initialization finished -------------------- # # logging to shng_details_file is already enabled in logger lib: #handlers: [shng_develop_file] # set to WARNING LEVEL to prevent unwanted log entries and add ONLY start and stop log-messages to further logfiles - level: WARNING + level: NOTICE modules: # Default logger for SmartHomeNG modules handlers: [shng_details_file] - level: WARNING + level: NOTICE plugins: # Default logger for SmartHomeNG plugins @@ -212,7 +212,7 @@ loggers: # # Logging items with configuration: log_change: new_device # # to a different file # # - # handlers: [items_file] + # handlers: [shng_items_file] # level: DEBUG # ------------------------------------------ diff --git a/lib/db.py b/lib/db.py index fa5e317d59..5f1598662f 100644 --- a/lib/db.py +++ b/lib/db.py @@ -49,6 +49,8 @@ class Database(): 'lock()' - acquire the database lock (prevent simultaneous reads/writes) 'release()' - release the database lock 'verify()' - check database connection and reconnect if required + 'connected()' - check if database is connected + The SQL statements executed may have placeholders and parameters which are passed to the execution methods listed above. The following DB-API diff --git a/lib/item/item.py b/lib/item/item.py index cf4ebcff36..991819b362 100644 --- a/lib/item/item.py +++ b/lib/item/item.py @@ -111,6 +111,7 @@ def __init__(self, smarthome, parent, path, config, items_instance=None): self.cast = cast_bool self.__changed_by = 'Init:None' self.__updated_by = self.__changed_by + self.__triggered_by = 'N/A' self.__children = [] self.conf = {} self._crontab = None @@ -143,10 +144,13 @@ def __init__(self, smarthome, parent, path, config, items_instance=None): self._items_to_trigger = [] self.__last_change = self.shtime.now() self.__last_update = self.__last_change + self.__last_trigger = self.__last_change self.__prev_change = self.__last_change self.__prev_update = self.__prev_change + self.__prev_trigger = self.__prev_change self.__prev_change_by = 'N/A' self.__prev_update_by = self.__prev_change_by + self.__prev_trigger_by = self.__prev_change_by self._lock = threading.Condition() self.__logics_to_trigger = [] self._name = path @@ -400,6 +404,7 @@ def __init__(self, smarthome, parent, path, config, items_instance=None): self.__last_change, self._value = cache_read(self._cache, self.shtime.tzinfo()) self.__prev_change = self.__last_change self.__updated_by = self.__changed_by + self.__triggered_by = 'N/A' self.__last_update = self.__last_change self.__prev_update = self.__prev_change @@ -648,6 +653,16 @@ def _get_last_update_age(self): delta = self.shtime.now() - self.__last_update return delta.total_seconds() + def _get_last_trigger(self): + return self.__last_trigger + + def _get_last_trigger_age(self): + delta = self.shtime.now() - self.__last_trigger + return delta.total_seconds() + + def _get_last_trigger_by(self): + return self.__triggered_by + def _get_last_value(self): return self.__last_value @@ -679,6 +694,18 @@ def _get_prev_update_by(self): def _get_prev_value(self): return self.__prev_value + def _get_prev_trigger(self): + return self.__prev_trigger + + def _get_prev_trigger_age(self): + + delta = self.__last_trigger - self.__prev_trigger + if delta.total_seconds() < 0.0001: + return 0.0 + return delta.total_seconds() + + def _get_prev_trigger_by(self): + return self. __prev_trigger_by """ @@ -748,6 +775,23 @@ def update_age(self): """ return self.property.last_update_age + def last_trigger(self): + """ + Timestamp of last trigger of item's eval expression (if available) + + :return: Timestamp of last update + """ + return self.property.last_trigger + + def trigger_age(self): + """ + Trigger-age of the item's last eval trigger. Returns the time in seconds since the eval has been triggered + + :return: Update-age of the value + :rtype: int + """ + return self.property.last_trigger_age + def prev_change(self): """ Timestamp of the previous (next-to-last) change of item's value @@ -783,6 +827,23 @@ def prev_update_age(self): """ return self.property.prev_update_age + def prev_trigger(self): + """ + Timestamp of previous (next-to-last) trigger of item's eval + + :return: Timestamp of previous update + """ + return self.property.prev_trigger + + def prev_trigger_age(self): + """ + Trigger-age of the item's previous eval trigger. Returns the time in seconds of the previous eval trigger + + :return: Update-age of the previous value + :rtype: int + """ + return self.property.prev_trigger_age + def prev_value(self): """ Next-to-last value of the item @@ -809,6 +870,14 @@ def updated_by(self): """ return self.property.last_update_by + def triggered_by(self): + """ + Returns an indication, which plugin, logic or event triggered the item's eval + + :return: Updater of item's value + :rtype: str + """ + return self.property.last_trigger_by """ Following are methods to handle relative item paths @@ -926,7 +995,7 @@ def __checkfortags(evalstr, begintag, endtag): while (rest.find(begintag+'.') != -1): pref += rest[:rest.find(begintag+'.')+len(begintag)] rest = rest[rest.find(begintag+'.')+len(begintag):] - if endtag == '': + if endtag == '' or rest.find(endtag) == -1: rel = rest rest = '' else: @@ -1188,7 +1257,7 @@ def __run_eval(self, value=None, caller='Eval', source=None, dest=None): cond = eval(self._trigger_condition) logger.warning("Item {}: Condition result '{}' evaluating trigger condition {}".format(self._path, cond, self._trigger_condition)) except Exception as e: - log_msg = "Item {}: Xroblem evaluating trigger condition '{}': {}".format(self._path, self._trigger_condition, e) + log_msg = "Item {}: Problem evaluating trigger condition '{}': {}".format(self._path, self._trigger_condition, e) if (self._sh.shng_status['code'] != 20) and (caller != 'Init'): logger.debug(log_msg) else: @@ -1209,9 +1278,27 @@ def __run_eval(self, value=None, caller='Eval', source=None, dest=None): # uf.import_user_modules() - Modules were loaded during initialization phase of shng try: - #logger.warning("Item {}: Evaluating item value {}".format(self._path, self._eval)) - value = eval(self._eval) + self.__prev_trigger_by = self.__triggered_by + self.__triggered_by = "{0}:{1}".format(caller, source) + self.__prev_trigger = self.__last_trigger + self.__last_trigger = self.shtime.now() + + logger.debug("Item {}: Eval triggered by: {}. Evaluating item with value {}. Eval expression: {}".format(self._path, self.__triggered_by, value, self._eval)) + #value = eval(self._eval) + + # ms if contab: init = x is set, x is transfered as a string, for that case re-try eval with x converted to float + try: + value = eval(self._eval) + except: + #value = float(value) + value = self._value = self.cast(value) + value = eval(self._eval) + # ms end + except Exception as e: + # adding "None" as the "destination" information at end of triggered_by + # This helps figuring out whether an eval expression was successfully evaluated or not. + self.__triggered_by = "{0}:{1}:None".format(caller, source) log_msg = "Item {}: problem evaluating '{}': {}".format(self._path, self._eval, e) if (self._sh.shng_status['code'] != 20) and (caller != 'Init'): logger.debug(log_msg + " (status_code={}/caller={})".format(self._sh.shng_status['code'], caller)) @@ -1429,6 +1516,7 @@ def _set_value(self, value, caller, source=None, dest=None, prev_change=None, la self.__prev_update_by = self.__updated_by self.__changed_by = "{0}:{1}".format(caller, source) self.__updated_by = "{0}:{1}".format(caller, source) + self.__triggered_by = "{0}:{1}".format(caller, source) if caller != "fader": # log every item change to standard logger, if level is DEBUG @@ -1557,7 +1645,7 @@ def get_method_triggers(self): return self.__methods_to_trigger - def timer(self, time, value, auto=False, compat=ATTRIB_COMPAT_DEFAULT, source=None): + def timer(self, time, value, auto=False, compat=ATTRIB_COMPAT_DEFAULT, caller=None, source=None): time = self._cast_duration(time) value = self._castvalue_to_itemtype(value, compat) if auto: diff --git a/lib/item/property.py b/lib/item/property.py index 1793af37d4..2b6f940649 100644 --- a/lib/item/property.py +++ b/lib/item/property.py @@ -311,6 +311,57 @@ def last_update_by(self, value): self._ro_error() return + @property + def last_trigger(self): + """ + Read-Only Property: last_trigger + + Available in SmartHomeNG v1.9.1 and above + + :return: path of the item + :rtype: str + """ + return self._item._get_last_trigger() + + @last_trigger.setter + def last_trigger(self, value): + self._ro_error() + return + + @property + def last_trigger_age(self): + """ + Read-Only Property: last_trigger_age + + Available in SmartHomeNG v1.9.1 and above + + :return: path of the item + :rtype: str + """ + return self._item._get_last_trigger_age() + + @last_trigger_age.setter + def last_trigger_age(self, value): + self._ro_error() + return + + @property + def last_trigger_by(self): + """ + Read-Only Property: last_trigger_by + + Available in SmartHomeNG v1.9.1 and above + + :return: path of the item + :rtype: str + """ + return self._item._get_last_trigger_by() + + @last_trigger_by.setter + def last_trigger_by(self, value): + self._ro_error() + return + @property def last_value(self): """ @@ -566,6 +617,57 @@ def prev_update_by(self, value): self._ro_error() return + @property + def prev_trigger(self): + """ + Read-Only Property: prev_trigger + + Available in SmartHomeNG v1.9.1 and above + + :return: path of the item + :rtype: str + """ + return self._item._get_prev_trigger() + + @prev_trigger.setter + def prev_trigger(self, value): + self._ro_error() + return + + @property + def prev_trigger_age(self): + """ + Read-Only Property: prev_trigger_age + + Available in SmartHomeNG v1.9.1 and above + + :return: path of the item + :rtype: str + """ + return self._item._get_prev_trigger_age() + + @prev_trigger_age.setter + def prev_trigger_age(self, value): + self._ro_error() + return + + @property + def prev_trigger_by(self): + """ + Read-Only Property: prev_trigger_by + + Available in SmartHomeNG v1.9.1 and above + + :return: path of the item + :rtype: str + """ + return self._item._get_prev_trigger_by() + + @prev_trigger_by.setter + def prev_trigger_by(self, value): + self._ro_error() + return + @property def prev_value(self): """ diff --git a/lib/log.py b/lib/log.py index da4562f15c..32c04b9b41 100644 --- a/lib/log.py +++ b/lib/log.py @@ -53,7 +53,7 @@ def __init__(self, sh): return - def configure_logging(self, config_dict): + def configure_logging(self, config_dict, config_filename='logging.yaml'): if config_dict == None: print() @@ -87,17 +87,17 @@ def configure_logging(self, config_dict): except Exception as e: #self._logger_main.error(f"Invalid logging configuration in file 'logging.yaml' - Exception: {e}") print() - print("ERROR: Invalid logging configuration in file 'logging.yaml'") + print(f"ERROR: Invalid logging configuration in file '{config_filename}'") print(f" Exception: {e}") print() - exit(1) + return False #self.logger.notice(f"Logs.configure_logging: Level NOTICE = {notice_level} / root_handler_level={root_handler_level}") # Initialize MemLog Handler to output root log entries to smartVISU self.initMemLog() - return + return True def add_logging_level(self, description, value): diff --git a/lib/metadata.py b/lib/metadata.py index 6c0daaf198..f33f74bed1 100644 --- a/lib/metadata.py +++ b/lib/metadata.py @@ -25,6 +25,7 @@ import collections from lib.utils import Utils +from lib.utils import Version import lib.shyaml as shyaml from lib.constants import (YAML_FILE, FOO, META_DATA_TYPES, META_DATA_DEFAULTS) @@ -169,6 +170,15 @@ def __init__(self, sh, addon_name, addon_type, classpath=''): # build dict for checking of item attributes and their values if self.itemprefixdefinitions is not None: + # dummy 'my_' prefix for user's attributes for logics, etc. (add only, if list is still empty) + if all_itemprefixdefinitions == {}: + prefix_name = 'my_' + all_itemprefixdefinitions[prefix_name] = {'type': 'foo', 'description': {'de': 'Attribute für verschiedene Tests', 'en': 'Attributes for various tests'}, 'listtype': ['foo'], 'listlen': 0, '_addon_name': 'priv_develop', '_addon_type': 'plugin', '_name': 'my_', '_type': 'prefix'} + all_itemprefixdefinitions[prefix_name]['_addon_name'] = 'lib_metadata' + all_itemprefixdefinitions[prefix_name]['_addon_type'] = 'plugin' + all_itemprefixdefinitions[prefix_name]['_name'] = prefix_name + all_itemprefixdefinitions[prefix_name]['_type'] = 'prefix' + # add all prefixes loaded from metadate of the plugin for prefix_name in self.itemprefixdefinitions: all_itemprefixdefinitions[prefix_name] = self.itemprefixdefinitions[prefix_name] all_itemprefixdefinitions[prefix_name]['_addon_name'] = self._addon_name @@ -410,37 +420,20 @@ def test_shngcompatibility(self): :return: True if the SmartHomeNG version is in the supported range :rtype: bool """ - l = str(self._sh.version).split('.') - shng_version = l[0]+'.'+l[1] - if len(l) > 2: - shng_version += '.'+l[2] - l = str(self.get_string('sh_minversion')).split('.') - min_shngversion = l[0] - if len(l) > 1: - min_shngversion += '.'+l[1] - if len(l) > 2: - min_shngversion += '.'+l[2] - - l = str(self.get_string('sh_maxversion')).split('.') - max_shngversion = l[0] - if len(l) > 1: - max_shngversion += '.'+l[1] - if len(l) > 2: - max_shngversion += '.'+l[2] - - mod_version = self.get_string('version') + shng_version = Version.format(self._sh.version.split('-')[0]) + min_shngversion = Version.format(str(self.get_string('sh_minversion'))) + max_shngversion = Version.format(str(self.get_string('sh_maxversion'))) + mod_version = Version.format(self.get_string('version')) if min_shngversion != '': - #r = self._compare_versions(min_shngversion, shng_version, '>', (min_shngversion > shng_version)) # if min_shngversion > shng_version: - if self._compare_versions(min_shngversion, shng_version, '>', (min_shngversion > shng_version)): - logger.error("{0} '{1}': The version {3} of SmartHomeNG is too old for this {0}. It requires at least version v{2}. The {0} was not loaded.".format(self._addon_type, self._addon_name, min_shngversion, shng_version)) + if Version.compare(min_shngversion, shng_version, '>'): + logger.error(f"{self._addon_type} '{self._addon_name}' {mod_version}: SmartHomeNG {shng_version} is too old for this {self._addon_type}. It requires at least version {Version.format(min_shngversion)}. The {self._addon_type} was not loaded.") return False if max_shngversion != '': - #self._compare_versions(max_shngversion, shng_version, '<', (max_shngversion < shng_version)) # if max_shngversion < shng_version: - if self._compare_versions(max_shngversion, shng_version, '<', (max_shngversion < shng_version)): - logger.error("{0} '{1}': The version {3} of SmartHomeNG is too new for this {0}. It requires a version up to v{2}. The {0} was not loaded.".format(self._addon_type, self._addon_name, max_shngversion, shng_version)) + if Version.compare(max_shngversion, shng_version, '<'): + logger.error(f"{self._addon_type} '{self._addon_name}' {mod_version}: SmartHomeNG {shng_version} is too new for this {self._addon_type}. It requires a version up to {Version.format(max_shngversion)}. The {self._addon_type} was not loaded.") return False return True @@ -453,29 +446,31 @@ def test_pythoncompatibility(self): :rtype: bool """ l = sys.version_info - py_version = str(l[0])+'.'+str(l[1]) - - l = str(self.get_string('py_minversion')).split('.') - min_pyversion = l[0] - if len(l) > 1: - min_pyversion += '.'+l[1] - l = str(self.get_string('py_maxversion')).split('.') - max_pyversion = l[0] - if len(l) > 1: - max_pyversion += '.'+l[1] - mod_version = self.get_string('version') + py_version = Version.format(str(l[0])+'.'+str(l[1])+'.'+str(l[2])) + min_pyversion = Version.format(str(self.get_string('py_minversion'))) + max = str(self.get_string('py_maxversion')) + if len(max.split('.')) == 2: + # if given max version has only two parts, make it the max for that version: 3.8 -> 3.8.999 + max += '.999' + max_pyversion = Version.format(str(max)) + mod_version = Version.format(self.get_string('version')) + + #if min_pyversion != '' or max_pyversion != '': + # logger.notice(f"{self._addon_type} '{self._addon_name}' {mod_version}: Python Version: {py_version}, min: {min_pyversion}, max: {max_pyversion}") if min_pyversion != '': #self._compare_versions(min_pyversion, py_version, '>', (min_pyversion > py_version)) # if min_pyversion > py_version: - if self._compare_versions(min_pyversion, py_version, '>', (min_pyversion > py_version)): - logger.error("{0} '{1}': The Python version {3} is too old for this {0}. It requires at least version v{2}. The {0} was not loaded.".format(self._addon_type, self._addon_name, min_pyversion, py_version)) + #if self._compare_versions(min_pyversion, py_version, '>', (min_pyversion > py_version)): + if Version.compare(min_pyversion, py_version, '>'): + logger.error(f"{self._addon_type} '{self._addon_name}' {mod_version}: The Python version {py_version} is too old for this {self._addon_type}. It requires at least version {min_pyversion}. The {self._addon_type} was not loaded.") return False if max_pyversion != '': #self._compare_versions(max_pyversion, py_version, '<', (max_pyversion < py_version)) # if max_pyversion < py_version: - if self._compare_versions(max_pyversion, py_version, '<', (max_pyversion < py_version)): - logger.error("{0} '{1}': The Python version {3} is too new for this {0}. It requires a version up to v{2}. The {0} was not loaded.".format(self._addon_type, self._addon_name, max_pyversion, py_version)) + #if self._compare_versions(max_pyversion, py_version, '<', (max_pyversion < py_version)): + if Version.compare(max_pyversion, py_version, '<'): + logger.error(f"{self._addon_type} '{self._addon_name}' {mod_version}: The Python version {py_version} is too new for this {self._addon_type}. It requires a version up to {max_pyversion}. The {self._addon_type} was not loaded.") return False return True diff --git a/lib/network.py b/lib/network.py index 00458aaed1..9cb3576885 100644 --- a/lib/network.py +++ b/lib/network.py @@ -47,6 +47,7 @@ import subprocess import threading import time +from contextlib import suppress from . import aioudp @@ -171,7 +172,7 @@ def validate_inet_addr(addr, port): :type addr: str :param port: port number under test :type port: num - :return: (ip_address, port, family) or (None, undef, undef) if error occurs + :return: (ip_address, port, family) or (None, 0, None) if error occurs :rtype: tuple """ logger = logging.getLogger(__name__) @@ -198,6 +199,8 @@ def validate_inet_addr(addr, port): # Unable to resolve hostname logger.error(f'Cannot resolve {addr} to a valid ip address (v4 or v6): {e}') ip = None + port = 0 + family = None return (ip, port, family) @@ -222,7 +225,6 @@ def clean_uri(uri, mode='show'): """ # find login data pattern = re.compile('http([s]?)://([^:]+:[^@]+@)') - # possible replacement modes replacement = { 'strip': 'http\\g<1>://', @@ -512,6 +514,7 @@ def data_received_callback(Tcp_client_instance, message) :param retry_cycle: Time between cycles if :param:autoreconnect is True :param binary: Switch between binary and text mode. Text will be encoded / decoded using encoding parameter. :param terminator: Terminator to use to split received data into chunks (split lines for example). If integer then split into n bytes. Default is None means process chunks as received. + :param autoconnect: automatically connect on send. Copies autoreconnect if None :type host: str :type port: int @@ -522,9 +525,10 @@ def data_received_callback(Tcp_client_instance, message) :type retry_cycle: int :type binary: bool :type terminator: int | bytes | str + :type autoconnect: bool """ - def __init__(self, host, port, name=None, autoreconnect=True, connect_retries=5, connect_cycle=5, retry_cycle=30, binary=False, terminator=False): + def __init__(self, host, port, name=None, autoreconnect=True, connect_retries=5, connect_cycle=5, retry_cycle=30, binary=False, terminator=False, timeout=1, autoconnect=None): self.logger = logging.getLogger(__name__) # public properties @@ -535,12 +539,15 @@ def __init__(self, host, port, name=None, autoreconnect=True, connect_retries=5, self._host = host self._port = port self._autoreconnect = autoreconnect + self._autoconnect = autoconnect + if self._autoconnect is None: + self._autoconnect = self._autoreconnect self._is_connected = False self._is_receiving = False self._connect_retries = connect_retries self._connect_cycle = connect_cycle self._retry_cycle = retry_cycle - self._timeout = 1 + self._timeout = timeout self._hostip = None self._family = socket.AF_INET @@ -558,9 +565,9 @@ def __init__(self, host, port, name=None, autoreconnect=True, connect_retries=5, self.__connect_threadlock = threading.Lock() self.__receive_thread = None self.__receive_threadlock = threading.Lock() - self.__running = True + self.__running = False - #self.logger.setLevel(logging.DEBUG) # Das sollte hier NICHT gesetzt werden, sondern in etc/logging.yaml im Logger lib.network konfiguriert werden! + # self.logger.setLevel(logging.DEBUG) # Das sollte hier NICHT gesetzt werden, sondern in etc/logging.yaml im Logger lib.network konfiguriert werden! self._host = host self._port = port @@ -594,17 +601,21 @@ def connect(self): :return: False if an error prevented us from launching a connection thread. True if a connection thread has been started. :rtype: bool """ + if self._is_connected: # return false if already connected + self.logger.debug(f'Already connected to {self._host}:{self._port}, ignoring new request') + return False + if self._hostip is None: # return False if no valid ip to connect to - self.logger.error(f'No valid IP address to connect to {self._host}') + self.logger.error(f'No valid IP address to connect to {self._host}:{self._port}') self._is_connected = False return False - if self._is_connected: # return false if already connected - self.logger.error(f'Already connected to {self._host}, ignoring new request') - return False - self.__connect_thread = threading.Thread(target=self._connect_thread_worker, name='TCP_Connect') - self.__connect_thread.daemon = True - self.__connect_thread.start() + self.logger.debug(f'Starting connect to {self._host}:{self._port}') + if not self.__connect_thread: + self.__connect_thread = threading.Thread(target=self._connect_thread_worker, name='TCP_Connect') + self.__connect_thread.daemon = True + if not self.__running: + self.__connect_thread.start() return True def connected(self): @@ -629,6 +640,16 @@ def send(self, message): except Exception: self.logger.warning(f'Error encoding message for client {self.name}') return False + + # automatically (re)connect on send attempt + if not self._is_connected: + if self._autoconnect: + self.logger.debug(f'autoconnecting to host {self._host} on send attempt, message is {message}') + self.connect() + else: + self.logger.warning(f'trying to send {message}, but not connected to host {self._host} and autoconnect not active. Aborting.') + return False + try: if self._is_connected: bytes_sent = self._socket.send(message) @@ -636,8 +657,13 @@ def send(self, message): self.logger.warning(f'Error sending message {message} to host {self._host}: message truncated, sent {bytes_sent} of {len(message)} bytes') else: return False - except BrokenPipeError: - self.logger.warning(f'Detected disconnect from {self._host}, send failed.') + + except (BrokenPipeError, TimeoutError) as e: + if e.errno == 60: + # timeout + self.logger.warning(f'Detected timeout on {self._host}, disconnecting, send failed.') + else: + self.logger.warning(f'Detected disconnect from {self._host}, send failed.') self._is_connected = False if self._disconnected_callback: self._disconnected_callback(self) @@ -657,13 +683,14 @@ def _connect_thread_worker(self): Thread worker to handle connection. """ if not self.__connect_threadlock.acquire(blocking=False): - self.logger.warning(f'Connection attempt already in progress for {self._host}, ignoring new request') + self.logger.info(f'Connection attempt already in progress for {self._host}:{self._port}, ignoring new request') return if self._is_connected: - self.logger.error(f'Already connected to {self._host}, ignoring new request') + self.logger.info(f'Already connected to {self._host}:{self._port}, ignoring new request') return - self.logger.debug(f'Starting connection cycle for {self._host}') + self.logger.debug(f'Starting connection cycle for {self._host}:{self._port}') self._connect_counter = 0 + self.__running = True while self.__running and not self._is_connected: # Try a full connect cycle while not self._is_connected and self._connect_counter < self._connect_retries and self.__running: @@ -719,6 +746,7 @@ def __receive_thread_worker(self): """ Thread worker to handle receiving. """ + self.logger.debug(f'started receive thread for host {self._host}') waitobj = IOWait() waitobj.watch(self._socket, read=True) __buffer = b'' @@ -732,10 +760,15 @@ def __receive_thread_worker(self): events = waitobj.wait(1000) # BMX for fileno, read, write in events: # BMX if read: - msg = self._socket.recv(4096) + timeout = False + try: + msg = self._socket.recv(4096) + except TimeoutError: + msg = None + timeout = True # Check if incoming message is not empty if msg: - # TODO: doing this breaks line separation if multiple lines + # TODO: doing this breaks line separation if multiple lines # are read at a time, the next loop can't split it # because line endings are missing # find out reason for this operation... @@ -777,8 +810,12 @@ def __receive_thread_worker(self): else: if self.__running: - # default state, peer closed connection - self.logger.warning(f'Connection closed by peer {self._host}') + if timeout: + # TimeoutError exception caught + self.logger.warning(f'Connection timed out on peer {self._host}, disconnecting.') + else: + # default state, peer closed connection + self.logger.warning(f'Connection closed by peer {self._host}') self._is_connected = False waitobj.unwatch(self._socket) if self._disconnected_callback is not None: @@ -803,11 +840,11 @@ def __receive_thread_worker(self): self._is_receiving = False return else: - self._log_exception(ex, f'lib.network receive thread died with error: {ex}. Go tell...') + self._log_exception(ex, f'lib.network receive thread died with unexpected error: {ex}. Go tell...') self._is_receiving = False - - def _log_exception( self, ex, msg): - self.logger.error(msg + ' If stack trace is necessary, enable debug log') + + def _log_exception(self, ex, msg): + self.logger.error(msg + ' -- If stack trace is necessary, enable/check debug log') if self.logger.isEnabledFor(logging.DEBUG): @@ -845,14 +882,21 @@ def close(self): """ Close the current client socket. """ - self.logger.info(f'Closing connection to {self._host} on TCP port {self._port}') self.__running = False - self._socket.shutdown(socket.SHUT_RD) + self.logger.info(f'Closing connection to {self._host} on TCP port {self._port}') + if self._is_connected: + self._socket.shutdown(socket.SHUT_RD) if self.__connect_thread is not None and self.__connect_thread.is_alive(): self.__connect_thread.join() if self.__receive_thread is not None and self.__receive_thread.is_alive(): self.__receive_thread.join() + def __str__(self): + if self.name: + return self.name + else: + return super().__str__() + class ConnectionClient(object): """ @@ -904,10 +948,8 @@ async def __drain_writer(self): """ Ensure drain() is called. """ - try: + with suppress(ConnectionResetError): await self.writer.drain() - except ConnectionResetError: - pass def send(self, message): """ @@ -984,6 +1026,12 @@ def _iac_to_string(self, msg): string += chr(char) return string.rstrip() + def __str__(self): + if self.name: + return self.name + else: + return super().__str__() + class Tcp_server(object): """ @@ -1222,13 +1270,17 @@ def close(self): self.__loop.call_soon_threadsafe(self.__loop.stop) while self.__loop.is_running(): pass - try: + with suppress(AttributeError): # thread can disappear between first and second condition test if self.__listening_thread and self.__listening_thread.is_alive(): self.__listening_thread.join() - except AttributeError: # thread can disappear between first and second condition test - pass self.__loop.close() + def __str__(self): + if self.name: + return self.name + else: + return super().__str__() + class Udp_server(object): """ @@ -1268,8 +1320,10 @@ def __init__(self, port, host='', name=None): self._close_timeout = 2 # private properties - self.__loop = None self.__coroutine = None + self.__loop = asyncio.new_event_loop() + asyncio.set_event_loop(self.__loop) + self.__server = aioudp.aioUDPServer() self.__listening_thread = None self.__running = True @@ -1299,8 +1353,6 @@ def start(self): return False try: self.logger.info(f'Starting up UDP server socket {self.__our_socket}') - self.__loop = asyncio.new_event_loop() - asyncio.set_event_loop(self.__loop) self.__coroutine = self.__start_server() self.__loop.run_until_complete(self.__coroutine) @@ -1358,11 +1410,9 @@ def close(self): self.__loop.stop() time.sleep(0.5) - try: + with suppress(AttributeError): # thread can disappear between first and second condition test if self.__listening_thread and self.__listening_thread.is_alive(): self.__listening_thread.join() - except AttributeError: # thread can disappear between first and second condition test - pass self.__loop.close() async def __start_server(self): @@ -1418,4 +1468,10 @@ async def __handle_connection(self, data, addr): except UnicodeError: self.logger.debug(f'Received undecodable bytes from {host}:{port}') else: - self.logger.debug(f'Received empty datagram from {host}:{port}') \ No newline at end of file + self.logger.debug(f'Received empty datagram from {host}:{port}') + + def __str__(self): + if self.name: + return self.name + else: + return super().__str__() diff --git a/lib/orb.py b/lib/orb.py index 1a14fa7637..d51abf8bf9 100644 --- a/lib/orb.py +++ b/lib/orb.py @@ -1,229 +1,293 @@ -#!/usr/bin/env python3 -# -# vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab -######################################################################### -# Copyright 2011-2014 Marcus Popp marcus@popp.mx -# -######################################################################### -# This file is part of SmartHomeNG. https://github.com/smarthomeNG// -# -# SmartHomeNG is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# SmartHomeNG is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with SmartHomeNG. If not, see . -########################################################################## - -import logging -import datetime -import math - -logger = logging.getLogger(__name__) - -try: - import ephem -except ImportError as e: - ephem = None # noqa - - -import dateutil.relativedelta -from dateutil.tz import tzutc - - -class Orb(): - - def __init__(self, orb, lon, lat, elev=False): - """ - :param orb: either 'sun' or 'moon' - :param lon: longitude of observer in degrees - :param lat: latitude of observer in degrees - :param elev: elevation of observer in meters - """ - if ephem is None: - logger.warning("Could not find/use ephem!") - return - self._obs = ephem.Observer() - """ - PyEphem: An `Observer` instance allows to compute the positions of - celestial bodies as seen from a particular position on the Earth's surface. - Following attributes can be set after creation (used defaults are given): - `date` - the moment the `Observer` is created - `lat` - zero degrees latitude - `lon` - zero degrees longitude - `elevation` - 0 meters above sea level - `horizon` - 0 degrees - `epoch` - J2000 - `temp` - 15 degrees Celsius - `pressure` - 1010 mBar -""" - self._obs.long = str(lon) - self._obs.lat = str(lat) - if elev: - self._obs.elevation = float(elev) - if orb == 'sun': - self._orb = ephem.Sun() - elif orb == 'moon': - self._orb = ephem.Moon() - self.phase = self._phase - self.light = self._light - - def _avoid_neverup(self, dt, date_utc, doff): - # avoid NeverUp Error if degree offset is too high - # Get times for noon and midnight - originaldoff = doff - midnight = self.midnight(0, 0, dt=dt) - noon = self.noon(0, 0, dt=dt) - # If the altitudes are calculated from previous or next day, set the correct day for the obeserver query - noon = noon if noon >= date_utc else \ - self.noon(0, 0, dt=date_utc + dateutil.relativedelta.relativedelta(days=1)) - midnight = midnight if midnight >= date_utc else \ - self.midnight(0, 0, dt=date_utc - dateutil.relativedelta.relativedelta(days=1)) - # Get lowest and highest altitudes of the relevant day/night - max_altitude = self.pos(offset=None, degree=True, dt=midnight)[1] if doff <= 0 else \ - self.pos(offset=None, degree=True, dt=noon)[1] - # Set observation date back to original queried date - self._obs.date = date_utc - # Limit degree offset to the highest or lowest possible for the given date - doff = max(doff, max_altitude + 0.00001) if doff < 0 else min(doff, max_altitude - 0.00001) if doff > 0 else doff - if not originaldoff == doff: - logger.info("Had to truncate the degree offset to {} as the sun never goes " - "below/above the given value {}.".format(doff, originaldoff)) - return doff - - def noon(self, doff=0, moff=0, dt=None): - if dt is not None: - self._obs.date = dt - dt.utcoffset() - date_utc = (self._obs.date.datetime()).replace(tzinfo=tzutc()) - else: - self._obs.date = datetime.datetime.utcnow() - dateutil.relativedelta.relativedelta(minutes=moff) + dateutil.relativedelta.relativedelta(seconds=2) - date_utc = (self._obs.date.datetime()).replace(tzinfo=tzutc()) - if not doff == 0: - doff = self._avoid_neverup(dt, date_utc, doff) - self._obs.horizon = str(doff) - next_transit = self._obs.next_transit(self._orb).datetime() - next_transit = next_transit + dateutil.relativedelta.relativedelta(minutes=moff) - return next_transit.replace(tzinfo=tzutc()) - - def midnight(self, doff=0, moff=0, dt=None): - if dt is not None: - self._obs.date = dt - dt.utcoffset() - date_utc = (self._obs.date.datetime()).replace(tzinfo=tzutc()) - else: - self._obs.date = datetime.datetime.utcnow() - dateutil.relativedelta.relativedelta(minutes=moff) + dateutil.relativedelta.relativedelta(seconds=2) - date_utc = (self._obs.date.datetime()).replace(tzinfo=tzutc()) - if not doff == 0: - doff = self._avoid_neverup(dt, date_utc, doff) - self._obs.horizon = str(doff) - next_antitransit = self._obs.next_antitransit(self._orb).datetime() - next_antitransit = next_antitransit + dateutil.relativedelta.relativedelta(minutes=moff) - return next_antitransit.replace(tzinfo=tzutc()) - - def rise(self, doff=0, moff=0, center=True, dt=None): - """ - Computes the rise of either sun or moon - :param doff: degrees offset for the observers horizon - :param moff: minutes offset from time of rise (either before or after) - :param center: if True then the centerpoint of either sun or moon will be considered to make the transit otherwise the upper limb will be considered - :param dt: start time for the search for a rise, if not given the current time will be used - :return: - """ - # workaround if rise is 0.001 seconds in the past - if dt is not None: - self._obs.date = dt - dt.utcoffset() - date_utc = (self._obs.date.datetime()).replace(tzinfo=tzutc()) - else: - self._obs.date = datetime.datetime.utcnow() - dateutil.relativedelta.relativedelta(minutes=moff) + dateutil.relativedelta.relativedelta(seconds=2) - date_utc = (self._obs.date.datetime()).replace(tzinfo=tzutc()) - if not doff == 0: - doff = self._avoid_neverup(dt, date_utc, doff) - self._obs.horizon = str(doff) - if not doff == 0: - next_rising = self._obs.next_rising(self._orb, use_center=center).datetime() - else: - next_rising = self._obs.next_rising(self._orb).datetime() - next_rising = next_rising + dateutil.relativedelta.relativedelta(minutes=moff) - return next_rising.replace(tzinfo=tzutc()) - - def set(self, doff=0, moff=0, center=True, dt=None): - """ - Computes the setting of either sun or moon - :param doff: degrees offset for the observers horizon - :param moff: minutes offset from time of setting (either before or after) - :param center: if True then the centerpoint of either sun or moon will be considered to make the transit otherwise the upper limb will be considered - :param dt: start time for the search for a setting, if not given the current time will be used - :return: - """ - # workaround if set is 0.001 seconds in the past - if dt is not None: - self._obs.date = dt - dt.utcoffset() - date_utc = (self._obs.date.datetime()).replace(tzinfo=tzutc()) - else: - self._obs.date = datetime.datetime.utcnow() - dateutil.relativedelta.relativedelta(minutes=moff) + dateutil.relativedelta.relativedelta(seconds=2) - date_utc = (self._obs.date.datetime()).replace(tzinfo=tzutc()) - # avoid NeverUp error - if not doff == 0: - doff = self._avoid_neverup(dt, date_utc, doff) - self._obs.horizon = str(doff) - if not doff == 0: - next_setting = self._obs.next_setting(self._orb, use_center=center).datetime() - else: - next_setting = self._obs.next_setting(self._orb).datetime() - next_setting = next_setting + dateutil.relativedelta.relativedelta(minutes=moff) - return next_setting.replace(tzinfo=tzutc()) - - def pos(self, offset=None, degree=False, dt=None): - """ - Calculates the position of either sun or moon - :param offset: given in minutes, shifts the time of calculation by some minutes back or forth - :param degree: if True: return the position of either sun or moon from the observer as degrees, otherwise as radians - :param dt: time for which the position needs to be calculated - :return: a tuple with azimuth and elevation - """ - if dt is None: - date = datetime.datetime.utcnow() - else: - date = dt.replace(tzinfo=tzutc()) - if offset: - date += dateutil.relativedelta.relativedelta(minutes=offset) - self._obs.date = date - self._orb.compute(self._obs) - if degree: - return (math.degrees(self._orb.az), math.degrees(self._orb.alt)) - else: - return (self._orb.az, self._orb.alt) - - def _light(self, offset=None): - """ - Applies only for moon, returns fraction of lunar surface illuminated when viewed from earth - for the current time plus an offset - :param offset: an offset given in minutes - """ - date = datetime.datetime.utcnow() - if offset: - date += dateutil.relativedelta.relativedelta(minutes=offset) - self._obs.date = date - self._orb.compute(self._obs) - return int(round(self._orb.moon_phase * 100)) - - def _phase(self, offset=None): - """ - Applies only for moon, returns the moon phase related to a cycle of approx. 29.5 days - for the current time plus an offset - :param offset: an offset given in minutes - """ - date = datetime.datetime.utcnow() - cycle = 29.530588861 - if offset: - date += dateutil.relativedelta.relativedelta(minutes=offset) - self._obs.date = date - self._orb.compute(self._obs) - last = ephem.previous_new_moon(self._obs.date) - frac = (self._obs.date - last) / cycle - return int(round(frac * 8)) +#!/usr/bin/env python3 +# +# vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab +######################################################################### +# Copyright 2011-2014 Marcus Popp marcus@popp.mx +# Copyright 2021-2022 Bernd Meiners Bernd.Meiners@mail.de +######################################################################### +# This file is part of SmartHomeNG. https://github.com/smarthomeNG// +# +# SmartHomeNG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# SmartHomeNG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with SmartHomeNG. If not, see . +########################################################################## + +import logging +import datetime +import math + +logger = logging.getLogger(__name__) + +try: + import ephem +except ImportError as e: + ephem = None # noqa + +import dateutil.relativedelta +from dateutil.tz import tzutc + +""" +This library contains a class Orb for calculating sun or moon related events. +Currently it uses ephem for calculation of the sky bound events. +""" + +class Orb(): + """ + Save an observers location and the name of a celestial body for future use + + The Methods internally use PyEphem for computation + + An `Observer` instance allows to compute the positions of + celestial bodies as seen from a particular position on the Earth's surface. + Following attributes can be set after creation (used defaults are given): + + `date` - the moment the `Observer` is created + `lat` - zero degrees latitude + `lon` - zero degrees longitude + `elevation` - 0 meters above sea level + `horizon` - 0 degrees + `epoch` - J2000 + `temp` - 15 degrees Celsius + `pressure` - 1010 mBar + """ + + def __init__(self, orb, lon, lat, elev=False): + """ + Save location and celestial body + + :param orb: either 'sun' or 'moon' + :param lon: longitude of observer in degrees + :param lat: latitude of observer in degrees + :param elev: elevation of observer in meters + """ + if ephem is None: + logger.warning("Could not find/use ephem!") + return + + self.orb = orb + self.lat = lat + self.lon = lon + self.elev = elev + + def get_observer_and_orb(self): + """ + Return a tuple of an instance of an observer with location information + and a celestial body + Both returned objects are uniquely created to prevent errors in computation + + See also this thread at `Stackoverflow `_ + dated back to 2015 where the creator of pyephem writes: + + > Second answer: As long as each thread has its own Moon and Observer objects, it should be able to do its own computations without ruining those of any other threads. + + :return: tuple of observer and celestial body + """ + + observer = ephem.Observer() + # ephem expects lat and lon as strings + observer.long = str(self.lon) + observer.lat = str(self.lat) + if self.elev: + observer.elevation = float(self.elev) + + if self.orb == 'sun': + orb = ephem.Sun() + elif self.orb == 'moon': + orb = ephem.Moon() + self.phase = self._phase + self.light = self._light + + return observer,orb + + def _avoid_neverup(self, dt, date_utc, doff): + """ + When specifying an offset for e.g. a sunset or a sunrise it might well be that the + offset is too high to be ever reached for a specific location and time + Therefore this function will limit this offset and return it to the calling function + + :param dt: starting point for calculation + :type dt: datetime + :param date_utc: a datetime with utc time + :type date_utc: datetime + :param doff: offset in degrees + :type doff: float + :return: corrected offset in degrees + :rtype: float + """ + originaldoff = doff + + # Get times for noon and midnight + midnight = self.midnight(0, 0, dt=dt) + noon = self.noon(0, 0, dt=dt) + + # If the altitudes are calculated from previous or next day, set the correct day for the observer query + noon = noon if noon >= date_utc else \ + self.noon(0, 0, dt=date_utc + dateutil.relativedelta.relativedelta(days=1)) + midnight = midnight if midnight >= date_utc else \ + self.midnight(0, 0, dt=date_utc - dateutil.relativedelta.relativedelta(days=1)) + # Get lowest and highest altitudes of the relevant day/night + max_altitude = self.pos(offset=None, degree=True, dt=midnight)[1] if doff <= 0 else \ + self.pos(offset=None, degree=True, dt=noon)[1] + + # Limit degree offset to the highest or lowest possible for the given date + doff = max(doff, max_altitude + 0.00001) if doff < 0 else min(doff, max_altitude - 0.00001) if doff > 0 else doff + if not originaldoff == doff: + logger.notice(f"offset {originaldoff} truncated to {doff}") + return doff + + def noon(self, doff=0, moff=0, dt=None): + observer, orb = self.get_observer_and_orb() + if dt is not None: + observer.date = dt - dt.utcoffset() + date_utc = (observer.date.datetime()).replace(tzinfo=tzutc()) + else: + observer.date = datetime.datetime.utcnow() - dateutil.relativedelta.relativedelta(minutes=moff) + dateutil.relativedelta.relativedelta(seconds=2) + date_utc = (observer.date.datetime()).replace(tzinfo=tzutc()) + if not doff == 0: + doff = self._avoid_neverup(dt, date_utc, doff) + observer.horizon = str(doff) + next_transit = observer.next_transit(orb).datetime() + next_transit = next_transit + dateutil.relativedelta.relativedelta(minutes=moff) + next_transit = next_transit.replace(tzinfo=tzutc()) + logger.debug(f"ephem: noon for {self.orb} with doff={doff}, moff={moff}, dt={dt} will be {next_transit}") + return next_transit + + def midnight(self, doff=0, moff=0, dt=None): + observer, orb = self.get_observer_and_orb() + if dt is not None: + observer.date = dt - dt.utcoffset() + date_utc = (observer.date.datetime()).replace(tzinfo=tzutc()) + else: + observer.date = datetime.datetime.utcnow() - dateutil.relativedelta.relativedelta(minutes=moff) + dateutil.relativedelta.relativedelta(seconds=2) + date_utc = (observer.date.datetime()).replace(tzinfo=tzutc()) + if not doff == 0: + doff = self._avoid_neverup(dt, date_utc, doff) + observer.horizon = str(doff) + next_antitransit = observer.next_antitransit(orb).datetime() + next_antitransit = next_antitransit + dateutil.relativedelta.relativedelta(minutes=moff) + next_antitransit = next_antitransit.replace(tzinfo=tzutc()) + logger.debug(f"ephem: midnight for {self.orb} with doff={doff}, moff={moff}, dt={dt} will be {next_antitransit}") + return next_antitransit + + def rise(self, doff=0, moff=0, center=True, dt=None): + """ + Computes the rise of either sun or moon + :param doff: degrees offset for the observers horizon + :param moff: minutes offset from time of rise (either before or after) + :param center: if True then the centerpoint of either sun or moon will be considered to make the transit otherwise the upper limb will be considered + :param dt: start time for the search for a rise, if not given the current time will be used + :return: + """ + observer, orb = self.get_observer_and_orb() + # workaround if rise is 0.001 seconds in the past + if dt is not None: + observer.date = dt - dt.utcoffset() + date_utc = (observer.date.datetime()).replace(tzinfo=tzutc()) + else: + observer.date = datetime.datetime.utcnow() - dateutil.relativedelta.relativedelta(minutes=moff) + dateutil.relativedelta.relativedelta(seconds=2) + date_utc = (observer.date.datetime()).replace(tzinfo=tzutc()) + if not doff == 0: + doff = self._avoid_neverup(dt, date_utc, doff) + observer.horizon = str(doff) + if not doff == 0: + next_rising = observer.next_rising(orb, use_center=center).datetime() + else: + next_rising = observer.next_rising(orb).datetime() + next_rising = next_rising + dateutil.relativedelta.relativedelta(minutes=moff) + next_rising = next_rising.replace(tzinfo=tzutc()) + logger.debug(f"ephem: next_rising for {self.orb} with doff={doff}, moff={moff}, center={center}, dt={dt} will be {next_rising}") + return next_rising + + def set(self, doff=0, moff=0, center=True, dt=None): + """ + Computes the setting of either sun or moon + :param doff: degrees offset for the observers horizon + :param moff: minutes offset from time of setting (either before or after) + :param center: if True then the centerpoint of either sun or moon will be considered to make the transit otherwise the upper limb will be considered + :param dt: start time for the search for a setting, if not given the current time will be used + :return: + """ + observer, orb = self.get_observer_and_orb() + # workaround if set is 0.001 seconds in the past + if dt is not None: + observer.date = dt - dt.utcoffset() + date_utc = (observer.date.datetime()).replace(tzinfo=tzutc()) + else: + observer.date = datetime.datetime.utcnow() - dateutil.relativedelta.relativedelta(minutes=moff) + dateutil.relativedelta.relativedelta(seconds=2) + date_utc = (observer.date.datetime()).replace(tzinfo=tzutc()) + # avoid NeverUp error + if not doff == 0: + doff = self._avoid_neverup(dt, date_utc, doff) + observer.horizon = str(doff) + if not doff == 0: + next_setting = observer.next_setting(orb, use_center=center).datetime() + else: + next_setting = observer.next_setting(orb).datetime() + next_setting = next_setting + dateutil.relativedelta.relativedelta(minutes=moff) + next_setting = next_setting.replace(tzinfo=tzutc()) + logger.debug(f"ephem: next_setting for {self.orb} with doff={doff}, moff={moff}, center={center}, dt={dt} will be {next_setting}") + return next_setting + + def pos(self, offset=None, degree=False, dt=None): + """ + Calculates the position of either sun or moon + :param offset: given in minutes, shifts the time of calculation by some minutes back or forth + :param degree: if True: return the position of either sun or moon from the observer as degrees, otherwise as radians + :param dt: time for which the position needs to be calculated + :return: a tuple with azimuth and elevation + """ + observer, orb = self.get_observer_and_orb() + if dt is None: + date = datetime.datetime.utcnow() + else: + date = dt.replace(tzinfo=tzutc()) + if offset: + date += dateutil.relativedelta.relativedelta(minutes=offset) + observer.date = date + orb.compute(observer) + if degree: + return (math.degrees(orb.az), math.degrees(orb.alt)) + else: + return (orb.az, orb.alt) + + def _light(self, offset=None): + """ + Applies only for moon, returns fraction of lunar surface illuminated when viewed from earth + for the current time plus an offset + :param offset: an offset given in minutes + """ + observer, orb = self.get_observer_and_orb() + date = datetime.datetime.utcnow() + if offset: + date += dateutil.relativedelta.relativedelta(minutes=offset) + observer.date = date + orb.compute(observer) + light = int(round(orb.moon_phase * 100)) + return light + + def _phase(self, offset=None): + """ + Applies only for moon, returns the moon phase related to a cycle of approx. 29.5 days + for the current time plus an offset + :param offset: an offset given in minutes + """ + observer, orb = self.get_observer_and_orb() + date = datetime.datetime.utcnow() + cycle = 29.530588861 + if offset: + date += dateutil.relativedelta.relativedelta(minutes=offset) + observer.date = date + orb.compute(observer) + last = ephem.previous_new_moon(observer.date) + frac = (observer.date - last) / cycle + phase = int(round(frac * 8)) + return phase diff --git a/lib/plugin.py b/lib/plugin.py index e1ac87abae..c5e773540e 100644 --- a/lib/plugin.py +++ b/lib/plugin.py @@ -132,6 +132,7 @@ def __init__(self, smarthome, configfile): if item_attribute_prefixes is not None: attribute_prefixes_keys = list(item_attribute_prefixes.keys()) for attribute_prefix in attribute_prefixes_keys: + #logger.warning(f"plugin_name={plugin_name}, attribute_prefix={attribute_prefix}, item_attribute_prefixes[attribute_prefix]={item_attribute_prefixes[attribute_prefix]}") self._sh.items.add_plugin_attribute_prefix(plugin_name, attribute_prefix, item_attribute_prefixes[attribute_prefix]) # Test if plugin defines item structs diff --git a/lib/requirements.txt b/lib/requirements.txt index d522e48d99..8654e972b7 100644 --- a/lib/requirements.txt +++ b/lib/requirements.txt @@ -1,5 +1,5 @@ # lib.orb: -ephem>=3.7 +ephem>=3.7,<=4.1.0 # lib.shtime: python-dateutil>=2.5.3 diff --git a/lib/scheduler.py b/lib/scheduler.py index 03c4c86855..2f7d2fccfd 100644 --- a/lib/scheduler.py +++ b/lib/scheduler.py @@ -62,6 +62,7 @@ from lib.triggertimes import TriggerTimes +class LeaveLogic(Exception): pass # declare a label for 'raise LeaveLogic' class _PriorityQueue: """ @@ -285,7 +286,8 @@ def run(self): if task['next'] is not None: if task['next'] < now: self._runc.acquire() - self._runq.insert(task['prio'], (name, task['obj'], 'Scheduler', None, None, task['value'])) + # insert priority and a tuple of (name, obj, by, source, dest, value) # ms + self._runq.insert(task['prio'], (name, task['obj'], 'Scheduler', task.get('source', None), None, task['value'])) self._runc.notify() self._runc.release() task['next'] = None @@ -375,7 +377,11 @@ def check_caller(self, name, from_smartplugin=False): :param from_smartplugin: :return: returns either the name or name combined with instance name """ - stack = inspect.stack() + try: + stack = inspect.stack() + except Exception as e: + logger.exception(f"check_caller('{name}'): Exception in inspect.stack(): {e}") + try: obj = stack[2][0].f_locals["self"] if isinstance(obj, SmartPlugin): @@ -415,9 +421,11 @@ def add(self, name, obj, prio=3, cron=None, cycle=None, value=None, offset=None, self.items = Items.get_instance() self._lock.acquire() try: + source = '??' if isinstance(cron, str): cron = [cron, ] if isinstance(cron, list): + details = None _cron = {} for entry in cron: desc, __, _value = entry.partition('=') @@ -427,6 +435,7 @@ def add(self, name, obj, prio=3, cron=None, cycle=None, value=None, offset=None, else: _value = _value.strip() if desc.lower().startswith('init'): + details = desc offset = 5 # default init offset desc, op, seconds = desc.partition('+') if op: @@ -439,12 +448,15 @@ def add(self, name, obj, prio=3, cron=None, cycle=None, value=None, offset=None, next = self.shtime.now() + datetime.timedelta(seconds=offset) else: _cron[desc] = _value + source = {'source': 'cron', 'details': details} if _cron == {}: cron = None else: cron = _cron + if isinstance(cycle, int): - cycle = {cycle: None} + source = {'source': 'cycle1', 'details': cycle} + cycle = {cycle: cycle} elif isinstance(cycle, str): cycle, __, _value = cycle.partition('=') try: @@ -455,10 +467,11 @@ def add(self, name, obj, prio=3, cron=None, cycle=None, value=None, offset=None, if _value != '': _value = _value.strip() else: - _value = None + _value = cycle cycle = {cycle: _value} + source = {'source': 'cycle', 'details': _value} if cycle is not None and offset is None: # spread cycle jobs - offset = random.randint(10, 15) + offset = random.randint(10, 15) # change name for multi instance plugins if obj.__class__.__name__ == 'method': if isinstance(obj.__self__, SmartPlugin): @@ -467,7 +480,7 @@ def add(self, name, obj, prio=3, cron=None, cycle=None, value=None, offset=None, if not from_smartplugin: name = name +'_'+ obj.__self__.get_instance_name() logger.debug("Scheduler: Name changed by adding plugin instance name to: " + name) - self._scheduler[name] = {'prio': prio, 'obj': obj, 'cron': cron, 'cycle': cycle, 'value': value, 'next': next, 'active': True} + self._scheduler[name] = {'prio': prio, 'obj': obj, 'source': source, 'cron': cron, 'cycle': cycle, 'value': value, 'next': next, 'active': True} if next is None: self._next_time(name, offset) finally: @@ -561,10 +574,15 @@ def _next_time(self, name, offset=None): now = now.replace(microsecond=0) if job['cycle'] is not None: cycle = list(job['cycle'].keys())[0] - value = job['cycle'][cycle] + #value = job['cycle'][cycle] + # set value only, if it is an item scheduler + if job['obj'].__class__.__name__ == 'Item': + value = job['cycle'][cycle] if offset is None: offset = cycle next_time = now + datetime.timedelta(seconds=offset) + #job['source'] = 'cycle' + job['source'] = {'source': 'cycle', 'details': str(cycle)} if job['cron'] is not None: for entry in job['cron']: if entry == 'None': @@ -573,9 +591,12 @@ def _next_time(self, name, offset=None): if next_time is not None: if ct < next_time: next_time = ct + #job['source'] = 'cron' # ms + job['source'] = {'source': 'cron', 'details': str(entry)} value = job['cron'][entry] else: next_time = ct + job['source'] = {'source': 'cron', 'details': str(entry)} value = job['cron'][entry] self._scheduler[name]['next'] = next_time self._scheduler[name]['value'] = value @@ -610,60 +631,26 @@ def _worker(self): self._runc.release() self._task(name, obj, by, source, dest, value) + def _task(self, name, obj, by, source, dest, value): threading.current_thread().name = name logger = logging.getLogger(name) if obj.__class__.__name__ == 'Logic': - source_details = None - if isinstance(source, dict): - source_details = source.get('details', '') - source = source.get('item', '') - trigger = {'by': by, 'source': source, 'source_details': source_details, 'dest': dest, 'value': value} # noqa - - #following variables are assigned to be available during logic execution - sh = self._sh # noqa - shtime = self.shtime - items = self.items - - # set the logic environment here (for use within functions in logics): - logic = obj # noqa - logic.sh = sh - logic.logger = logger - logic.shtime = shtime - logic.items = items - logic.trigger_dict = trigger # logic.trigger has naming conflict with method logic.trigger of lib.item - - logics = obj._logics - - if not self.mqtt: - if _lib_modules_found: - self.mqtt = Modules.get_instance().get_module('mqtt') - mqtt = self.mqtt - logic.mqtt = mqtt + self._execute_logic_task(obj, by, source, dest, value) - try: - if logic.enabled: - exec(obj.bytecode) - # store timestamp of last run - obj.set_last_run() - for method in logic.get_method_triggers(): - try: - method(logic, by, source, dest) - except Exception as e: - logger.exception("Logic: Trigger {} for {} failed: {}".format(method, logic.name, e)) - except SystemExit: - # ignore exit() call from logic. - pass - except Exception as e: - tb = sys.exc_info()[2] - tb = traceback.extract_tb(tb)[-1] - logger.exception("Logic: {0}, File: {1}, Line: {2}, Method: {3}, Exception: {4}".format(name, tb[0], tb[1], tb[2], e)) elif obj.__class__.__name__ == 'Item': try: + if isinstance(source, str): + scheduler_source = source + else: + scheduler_source = source.get('source', '') + if scheduler_source != '': + scheduler_source = ':'+scheduler_source+':'+source.get('details','') if value is not None: - obj(value, caller="Scheduler") + obj(value, caller=("Scheduler"+scheduler_source)) except Exception as e: - logger.exception("Item {0} exception: {1}".format(name, e)) + logger.exception(f"Item {name} exception: {e}") + else: # method try: if value is None: @@ -671,5 +658,82 @@ def _task(self, name, obj, by, source, dest, value): else: obj(**value) except Exception as e: - logger.exception("Method {0} exception: {1}".format(name, e)) + logger.exception(f"Method {name} exception: {e}") + threading.current_thread().name = 'idle' + + + def _execute_logic_task(self, logic, by, source, dest, value): + """ + Execute a logic from _task method + + :param logic: + :return: + """ + name = 'logics.' + logic.name + logger = logging.getLogger(name) + source_details = None + if isinstance(source, dict): + source_details = source.get('details', '') + src = source.get('item', '') + if src == '': + # get source ('cron' or 'cycle') + src = source.get('source', '') + source = src + trigger = {'by': by, 'source': source, 'source_details': source_details, 'dest': dest, 'value': value} # noqa + + # following variables are assigned to be available during logic execution + sh = self._sh # noqa + shtime = self.shtime + items = self.items + + # set the logic environment here (for use within functions in logics): + #logic = obj # noqa + logic.sh = sh + logic.logger = logger + logic.shtime = shtime + logic.items = items + logic.trigger_dict = trigger # logic.trigger has naming conflict with method logic.trigger of lib.item + + logics = logic._logics + + if not self.mqtt: + if _lib_modules_found: + self.mqtt = Modules.get_instance().get_module('mqtt') + mqtt = self.mqtt + logic.mqtt = mqtt + + try: + if logic.enabled: + if sh.shng_status['code'] < 20: + logger.warning(f"Logik ignoriert, SmartHomeNG ist noch nicht vollständig initialisiert - Logik wurde getriggert durch {trigger}") + else: + logger.debug(f"Getriggert durch: {trigger}") + exec(logic.bytecode) + # store timestamp of last run + logic.set_last_run() + for method in logic.get_method_triggers(): + try: + method(logic, by, source, dest) + except Exception as e: + logger.exception("Logic: Trigger {} for {} failed: {}".format(method, logic.name, e)) + except LeaveLogic as e: + # 'LeaveLogic' is no error + if str(e) != '': + logger.info(f"Die Logik '{logic.name}' wurde verlassen. Grund: {e}") + except SystemExit: + # ignore exit() call from logic. + pass + except Exception as e: + tb = sys.exc_info()[2] + tb = traceback.extract_tb(tb)[-1] + if tb[2] == '': + logic_method = 'Hauptroutine der Logik' + else: + logic_method = 'function ' + tb[2] + '()' + logger.error(f"In der Logik ist ein Fehler aufgetreten:\n Logik '{logic.name}', Datei '{tb[0]}', Zeile {tb[1]}\n {logic_method}, Exception: {e}") + #logger.exception(f"In der Logik ist ein Fehler aufgetreten:\n Logik '{logic.name}', Datei '{tb[0]}', Zeile {tb[1]}\n {logic_method}, Exception: '{e}'\n ") + + return + + diff --git a/lib/shpypi.py b/lib/shpypi.py index c4b6105126..afb72f5974 100644 --- a/lib/shpypi.py +++ b/lib/shpypi.py @@ -46,7 +46,9 @@ except: pass +import lib.shyaml as shyaml from lib.utils import Utils +from lib.utils import Version from lib.constants import (YAML_FILE) @@ -55,7 +57,7 @@ class Shpypi: - def __init__(self, sh=None, base=None): + def __init__(self, sh=None, base=None, version=None, for_tests=False): """ :param smarthome: @@ -63,6 +65,7 @@ def __init__(self, sh=None, base=None): NOTE: During initialization (and initial calls to some methods, the logging interface is not yet initialized!!! """ self.logger = logging.getLogger(__name__) + self.for_tests = for_tests global _shpypi_instance if _shpypi_instance is not None: @@ -72,7 +75,7 @@ def __init__(self, sh=None, base=None): self.logger.critical("A second 'shpypi' object has been created. There should only be ONE instance of class 'Shpypi'!!! Called from: {} {} ({})".format(calframe[1][1], calframe[1][2], calframe[1][3])) _shpypi_instance = self - self.req_files = Requirements_files() + self.req_files = Requirements_files(version, self.for_tests) self.scheduler_crontab = ['init', '7 3 * *'] @@ -229,7 +232,7 @@ def test_requirements(self, filepath, logging=True, hard_requirement=True): def test_core_requirements(self, logging=True, pip3_command=None): # build an actual requirements file for core+modules - # req_files = Requirements_files() + # req_files = Requirements_files(self.sh) self.req_files.create_requirementsfile('base') self.req_files.create_requirementsfile('all') self.req_files.create_requirementsfile('core') @@ -263,7 +266,7 @@ def test_base_requirements(self, sh=None): if sh is not None: self.sh = sh # build an actual requirements file for core+modules - # req_files = Requirements_files() + # req_files = Requirements_files(self.sh) self.req_files.create_requirementsfile('base') # test if the requirements of the base.txt file are met @@ -281,7 +284,7 @@ def test_base_requirements(self, sh=None): def test_conf_plugins_requirements(self, plugin_conf_basename, plugins_dir): # import lib.shyaml here, so test_base_requirements() can be run even if ruamel.yaml package is not installed - import lib.shyaml as shyaml +# import lib.shyaml as shyaml if not os.path.isfile(plugin_conf_basename+ YAML_FILE): self.logger.warning("Requirments for configured plugins were not checked because the plugin configuration is not in YAML format") @@ -312,9 +315,13 @@ def test_conf_plugins_requirements(self, plugin_conf_basename, plugins_dir): self._conf_plugin_filelist = [] for plugin in req_dict: - self._conf_plugin_filelist.append(req_dict[plugin]) + use_it, msg = self.req_files._test_plugin_versions(req_dict[plugin]) + if use_it: + self._conf_plugin_filelist.append(req_dict[plugin]) + else: + self.logger.info(f"Not appended: ({msg}) conf_all: {req_dict[plugin]}") - #req_files = Requirements_files() + #req_files = Requirements_files(self.sh) self.req_files.set_conf_plugin_files(self._conf_plugin_filelist) self.req_files.create_requirementsfile('conf_all') @@ -402,6 +409,7 @@ def install_requirements(self, req_type, logging=True, pip3_command=None): if logging: self.logger.warning("Running in a virtualenv environment - installing " + req_type_display + " requirements only to actual virtualenv, please wait...") else: + print() print("Running in a virtualenv environment,") print("installing "+req_type_display+" requirements only to actual virtualenv, please wait...") stdout, stderr = Utils.execute_subprocess('pip3 install -r requirements/'+req_type+'.txt') @@ -1060,11 +1068,17 @@ class Requirements_files(): _plugin_files = [] _core_files = [] # to be a list in the future - def __init__(self): + def __init__(self, version='0.0.0', for_tests=False): self.logger = logging.getLogger(__name__) + self.for_tests = for_tests self._conf_plugin_files = [] + + self.shng_version = Version.format(version.split('-')[0]) + l = sys.version_info + self.py_version = Version.format(str(l[0])+'.'+str(l[1])+'.'+str(l[2])) + self.sh_basedir = os.sep.join(os.path.realpath(__file__).split(os.sep)[:-2]) self.logger.debug("Requirements_files is using '{}' as base directory".format(self.sh_basedir)) return @@ -1107,7 +1121,7 @@ def _build_packagelist(self, requirements): # build list of package requirement dicts packagelist = [] - self.logger.debug("Req_files: _build_packagelist: requirements = '{}'".format(requirements)) + self.logger.debug("_build_packagelist: Req_files: requirements = '{}'".format(requirements)) for key in requirements: packaged = {} wrk = re.split('<|>|=', key) @@ -1158,11 +1172,57 @@ def _build_packagelist(self, requirements): return packagelist + def _test_plugin_versions(self, filename): + + metaname = filename[:-16] + 'plugin.yaml' + meta = shyaml.yaml_load(metaname, ignore_notfound=True) + if meta is None: + #self.logger.warning(f"{metaname} not found") + return False, '' + else: + sh_minversion = Version.format(meta['plugin'].get('sh_minversion', '')) + sh_maxversion = Version.format(meta['plugin'].get('sh_maxversion', '')) + py_minversion = Version.format(meta['plugin'].get('py_minversion', '')) + #py_maxversion = Version.format(meta['plugin'].get('py_maxversion', '')) + py_maxversion = meta['plugin'].get('py_maxversion', '') #w/o format to enable max 3.8 -> 3.8.999 + if sh_minversion != '': + self.logger.debug(f"sh_minversion={sh_minversion}, self.shng_version={self.shng_version}") + if Version.compare(sh_minversion, self.shng_version, '>'): + self.logger.debug(f"meta data: {metaname} - meta: sh_minversion={sh_minversion}, sh_maxversion={sh_maxversion}") + return False, 'SmartHomeNG min version' + if sh_maxversion != '': + self.logger.debug(f"sh_maxversion={sh_maxversion}, self.shng_version={self.shng_version}") + if Version.compare(sh_maxversion, self.shng_version, '<'): + self.logger.debug(f"meta data: {metaname} - meta: sh_minversion={sh_minversion}, sh_maxversion={sh_maxversion}") + return False, 'SmartHomeNG max version' + + if py_minversion != '': + self.logger.debug(f"py_minversion={py_minversion}, self.py_version={self.py_version}") + if Version.compare(py_minversion, self.py_version, '>'): + self.logger.debug(f"meta data: {metaname} - meta: py_minversion={py_minversion}, py_maxversion={py_maxversion}") + return False, 'Python min version' + if py_maxversion != '': + max = str(py_maxversion) + if len(max.split('.')) == 2: + # if given max version has only two parts, make it the max for that version: 3.8 -> 3.8.999 + max += '.999' + py_maxversion = Version.format(str(max)) + + self.logger.debug(f"py_maxversion={py_maxversion}, self.py_version={self.py_version}") + if Version.compare(py_maxversion, self.py_version, '<'): + self.logger.debug(f"meta data: {metaname} - meta: py_minversion={py_minversion}, py_maxversion={py_maxversion}") + return False, 'Python max version' + #if sh_minversion != '' or sh_maxversion != '' or py_minversion != '' or py_maxversion != '': + # self.logger.warning(f"meta data: {metaname} - meta: sh_minversion={sh_minversion}, sh_maxversion={sh_maxversion}, py_minversion={py_minversion}, py_maxversion={py_maxversion}") + return True, '' + + def _get_filelist(self, selection): """ returns a list of files with all paths for a requirements.txt within a certain selection subpath. Currently selection is one of 'modules', 'lib', 'plugins' + When testing with travis the self.sh_basedir will be something like "/home/travis/build//smarthome" On a native Linux without specialities this is normally "/usr/local/smarthome" When inspecting the plugins then all requirements.txt for previous plugin versions should be omitted from the list. @@ -1176,15 +1236,24 @@ def _get_filelist(self, selection): #self.logger.debug("level = {}: root = {}".format(level, root)) if (selection == "plugins" and "_pv" not in root) or (level < basedir_level + 3): for filename in fnmatch.filter(filenames, 'requirements.txt'): - # print("level = {}: root = {}".format(level, root)) - file_list.append(os.path.join(root, filename)) - self.logger.debug("found '{}'".format(os.path.join(root, filename))) + use_it = True + msg = '' + if (selection == "plugins"): + if self.for_tests: + use_it, msg = self._test_plugin_versions(os.path.join(root, filename)) + if use_it: + # print("level = {}: root = {}".format(level, root)) + file_list.append(os.path.join(root, filename)) + self.logger.debug("found '{}'".format(os.path.join(root, filename))) + else: + self.logger.info(f"Not appended: ({msg}) all: {root}") self.logger.debug("_get_filelist found '{}'".format(file_list)) return file_list def _build_filelists(self, selection): + self.logger.debug(f"_build_filelists: selection={selection}") # global _module_files, _plugin_files, _core_files self._module_files = [] self._plugin_files = [] @@ -1194,7 +1263,7 @@ def _build_filelists(self, selection): # build list of all modules with requirements self._module_files = self._get_filelist('modules') - if selection in ['plugins','all']: + if selection in ['plugins', 'all']: # build list of all plugins with requirements self._plugin_files = self._get_filelist('plugins') @@ -1230,18 +1299,22 @@ def __read_requirementfiles(self): requirements = {} # Read requirements for core + #self.logger.warning(f"__read_requirementfiles: self._core_files={self._core_files}") for fname in self._core_files: self._read_requirementfile(fname, requirements, 'SmartHomeNG-') # Read requirements for modules + #self.logger.warning(f"__read_requirementfiles: self._module_files={self._module_files}") for fname in self._module_files: self._read_requirementfile(fname, requirements, 'SmartHomeNG-module ') # Read requirements for plugins + #self.logger.warning(f"__read_requirementfiles: self._plugin_files={self._plugin_files}") for fname in self._plugin_files: self._read_requirementfile(fname, requirements, 'plugin ') # Read requirements for configured plugins + #self.logger.warning(f"__read_requirementfiles: self._conf_plugin_files={self._conf_plugin_files}") for fname in self._conf_plugin_files: self._read_requirementfile(fname, requirements, 'configured plugin ') @@ -1366,6 +1439,7 @@ def _write_resultfile(self, selection, packagelist_consolidated, requirements): def set_conf_plugin_files(self, conf_plugin_filelist): self._conf_plugin_files = conf_plugin_filelist + def create_requirementsfile(self, selection): """ Ths method creates a requirements-file for one of the following selections: diff --git a/lib/shtime.py b/lib/shtime.py index b9a15482f4..0dfaf9b400 100644 --- a/lib/shtime.py +++ b/lib/shtime.py @@ -124,8 +124,12 @@ def translate(self, txt, vars=None): def set_tz(self, tz): """ set timezone info from name of timezone + + :param tz: Name of the timezone (like 'Europe/Berlin') + :type: tz: str """ tzinfo = dateutil.tz.gettz(tz) + self.logger.info(f"set_tz: tz={tz} -> tzinfo={tzinfo}") if tzinfo is not None: # TZ = tzinfo self._tz = tz @@ -136,12 +140,16 @@ def set_tz(self, tz): else: self.logger.warning(self.translate("Problem parsing timezone '{tz}' - Using UTC").format(tz=tz)) self._timezone = pytz.timezone("UTC") + self.logger.info(f"self.set_tz: -> self._timezone={self._timezone}") + self.logger.info(f"self.set_tz: -> self._tzinfo={self._tzinfo}") return def set_tzinfo(self, tzinfo): """ Set the timezone info + + :param tzinfo: """ self._tzinfo = tzinfo return @@ -168,7 +176,7 @@ def tz(self): """ Returns the the actual local timezone - :return: Timezone + :return: Name of the timezone (like 'Europe/Berlin', or 'UTC' if not set) :rtype: str """ @@ -190,7 +198,7 @@ def tzname(self): """ Returns the name about the actual local timezone (e.g. CET) - :return: Timezone info + :return: timezone string (like: 'CET') :rtype: str """ @@ -201,7 +209,7 @@ def tznameST(self): """ Returns the name for Standard Time in the local timezone (e.g. CET) - :return: Timezone info + :return: Timezone info (like: 'CET') :rtype: str """ @@ -213,7 +221,7 @@ def tznameDST(self): """ Returns the name for Daylight Saving Time (DST) in the local timezone (e.g. CEST) - :return: Timezone info + :return: Timezone info (like: 'CEST') :rtype: str """ diff --git a/lib/smarthome.py b/lib/smarthome.py index 504bf4e931..c67f709c5b 100644 --- a/lib/smarthome.py +++ b/lib/smarthome.py @@ -196,6 +196,9 @@ def __init__(self, MODE, extern_conf_dir=''): self._logger_main = logging.getLogger(__name__) self.logs = lib.log.Logs(self) # initialize object for memory logs + # keep for checking on restart command + self._mode = MODE + self.initialize_vars() self.initialize_dir_vars() self.create_directories() @@ -261,9 +264,11 @@ def __init__(self, MODE, extern_conf_dir=''): ############################################################# # Setting (local) tz if set in smarthome.yaml + # (to ensure propper logging) if hasattr(self, '_tz'): self.shtime.set_tz(self._tz) - del(self._tz) + # self._tz is deleted later (work in progress) + #del(self._tz) ############################################################# # test if needed Python packages are installed @@ -285,6 +290,12 @@ def __init__(self, MODE, extern_conf_dir=''): self.shng_status = {'code': 1, 'text': 'Initalizing: Logging initalized'} + if hasattr(self, '_tz'): + # set _tz again (now with logging enabled), + # so that shtime.set_tz can produce log output + self.shtime.set_tz(self._tz) + del(self._tz) + ############################################################# # Fork process and write pidfile if MODE == 'default': @@ -510,7 +521,17 @@ def init_logging(self, conf_basename='', MODE='default'): conf_basename = self._log_conf_basename conf_dict = lib.shyaml.yaml_load(conf_basename + YAML_FILE, True) - self.logs.configure_logging(conf_dict) + if not self.logs.configure_logging(conf_dict): + #conf_basename = self._log_conf_basename + YAML_FILE + '.default' + print(f" Trying default logging configuration from:") + print(f" {conf_basename + YAML_FILE + '.default'}") + print() + conf_dict = lib.shyaml.yaml_load(conf_basename + YAML_FILE + '.default', True) + if not self.logs.configure_logging(conf_dict, 'logging.yaml.default'): + print("ABORTING") + print() + exit(1) + print("Starting with default logging configuration") if MODE == 'interactive': # remove default stream handler logging.getLogger().disabled = True @@ -569,7 +590,7 @@ def start(self): ############################################################# # Init and import user-functions ############################################################# - uf.init_lib(self.getBaseDir()) + uf.init_lib(self.get_basedir()) ############################################################# # Init Item-Wrapper @@ -697,7 +718,13 @@ def stop(self, signum=None, frame=None): def restart(self, source=''): """ This method is used to restart the python interpreter and SmartHomeNG + + If SmartHomeNG was started in one of the foreground modes (-f, -i, -d), + just quit and let the user restart manually. """ + if self._mode in ['foreground', 'debug', 'interactive']: + self.stop() + if self.shng_status['code'] == 30: self._logger.warning("Another RESTART is issued, while SmartHomeNG is restarting. Reason: "+source) else: @@ -1200,4 +1227,3 @@ def runtime(self): self._deprecated_warning('Shtime-API') return self.shtime.runtime() - diff --git a/lib/triggertimes.py b/lib/triggertimes.py index 9faa04045c..0a05bcaf40 100644 --- a/lib/triggertimes.py +++ b/lib/triggertimes.py @@ -4,7 +4,7 @@ # Copyright 2016-2020 Martin Sinn m.sinn@gmx.de # Copyright 2016 Christian Straßburg c.strassburg@gmx.de # Copyright 2012-2013 Marcus Popp marcus@popp.mx -# Copyright 2019-2021 Bernd Meiners Bernd.Meiners@mail.de +# Copyright 2019-2022 Bernd Meiners Bernd.Meiners@mail.de ######################################################################### # This file is part of SmartHomeNG. # @@ -33,9 +33,8 @@ from dateutil.relativedelta import MO, TU, WE, TH, FR, SA, SU from dateutil.tz import tzutc -#print("lib.triggertimes is being imported") from lib.shtime import Shtime -shtime = None +shtime = Shtime.get_instance() logger = logging.getLogger(__name__) @@ -341,13 +340,28 @@ def __init__(self, triggertime): self.next_event = None # store last result self.max_calc_time = 0 # keep track of maximum calculation time + # prevent + self.hour = None + self.hour_range = None + self.minute = None + self.minute_range = None + self.second = None + self.second_range = None + self.day = None + self.day_range = None + self.wday = None + self.weekday_range = None + self.month = None + self.month_range = None + self.parameter_count = 0 + self._is_valid = False self.parse_triggertime() def parse_triggertime(self): """parse the crontab string for details and store them to the class variables for later use""" logger.debug(f'Enter Crontab.parse_triggertime({self._triggertime})') - self._is_valid = False + with self._lock: triggertime = self._triggertime # replace @yearly etc. with correct preset @@ -358,11 +372,14 @@ def parse_triggertime(self): try: parameter_set = triggertime.strip().split() except: - logger.error(f"crontab entry '{triggertime}' can not be split up into 4 parts for minute, hour, day and weekday") + logger.error(f"crontab entry '{triggertime}' can not be split up into parts") return False self.parameter_count = len(parameter_set) - if self.parameter_count == 4: + if self.parameter_count < 4: + logger.error(f"crontab entry '{triggertime}' has fewer than 4 parts and is invalid") + return False + elif self.parameter_count == 4: logger.debug(f'old smarthome.py style parameter set {triggertime} given') self.minute, self.hour, self.day, self.wday = parameter_set[0],parameter_set[1],parameter_set[2],parameter_set[3] self.month='*' @@ -374,6 +391,9 @@ def parse_triggertime(self): elif self.parameter_count == 6: logger.debug(f'new SmartHomeNG style parameter set {triggertime} given') self.second, self.minute, self.hour, self.day, self.month, self.wday = parameter_set[0],parameter_set[1],parameter_set[2],parameter_set[3], parameter_set[4], parameter_set[5] + else: + logger.error(f"crontab entry '{triggertime}' has more than 6 parts and is invalid") + return False if self.parameter_count > 4: # replace abbreviated months like 'jan' with their number like '1' @@ -418,6 +438,9 @@ def get_next(self, starttime: datetime): :return: found date and time of next occurence or a time way up in the future :rtype: datetime """ + if not self._is_valid: + return get_invalid_time() + with self._lock: tik = time.perf_counter() if self.next_event is None: @@ -749,7 +772,8 @@ def split_offset(skyevent): @staticmethod def keep_in_range(value, minvalue, maxvalue): - assert minvalue <= maxvalue + if minvalue > maxvalue: + logger.error(f"minvalue={minvalue} is greater than maxvalue={maxvalue}") if value < minvalue: value = minvalue logger.warning(f"{value}<{minvalue} --> {value}={minvalue}") @@ -869,10 +893,11 @@ def get_next(self, starttime: datetime): days = 0 searchtime = starttime #logger.debug(f'looking for the next event after {starttime}') - searchtime = searchtime.replace(microsecond=0) + datetime.timedelta(seconds=1) # smallest amount higher than given time + searchtime = searchtime + datetime.timedelta(microseconds=1) # smallest amount higher than given time while True: - #logger.warning(f"{searchtime}") - days = abs((starttime-searchtime).days) + #logger.warning(f"searchtime: {searchtime}") + #logger.warning(f"difference {searchtime-starttime}") + days = abs((searchtime-starttime).days) if days > days_max_count: logger.error(f'No matches after {days} examined days, giving up') return get_invalid_time() @@ -896,9 +921,8 @@ def get_next(self, starttime: datetime): # time in next_time will be in utctime. So we need to adjust it if eventtime.tzinfo == tzutc(): eventtime = eventtime.astimezone(Skytime.sh.shtime.tzinfo()) - logger.debug(f"starting with {starttime} the next {self.event}({self.doff},{self.moff}) is {eventtime}") else: - logger.warning("searchtime.tzinfo was not given as utc!") + logger.error("searchtime.tzinfo was not given as utc!") else: logger.error(f'No function found to get next skyevent time for {self._triggertime}') return get_invalid_time() @@ -908,18 +932,25 @@ def get_next(self, starttime: datetime): # - searchtime must be smaller than eventtime and # - eventtime might be one or more day(s) later + logger.debug(f"starting with {starttime} the next {self.event}({self.doff},{self.moff}) is {eventtime}") + # if the dates differ then it must be certain that the new date adheres to the # constraints of the day range. if eventtime.date() > searchtime.date(): logger.debug(f"eventtime ({eventtime.date()}) is at least a day later than current searchtime ({searchtime}), skip to eventtime's early morning") - searchtime = eventtime.replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=Skytime.sh.shtime.tzinfo()) + searchtime = eventtime.replace(hour=0, minute=0, second=0, microsecond=0) continue # need to start over for a matching date - # eventtime and searchtime have the same date + if eventtime.date() < searchtime.date(): + logger.debug(f"eventtime ({eventtime.date()}) is at least a day earlier than current searchtime ({searchtime}), skip to searchtime's early morning") + searchtime = searchtime.replace(hour=0, minute=0, second=0, microsecond=0) + datetime.timedelta(days=1) + continue + + # eventtime and searchtime have the same day # now check time limits if given if self.h_min is not None and self.m_min is not None: try: - dmin = eventtime.replace(hour=self.h_min, minute=self.m_min, second=0, microsecond=0, tzinfo=Skytime.sh.shtime.tzinfo()) + dmin = eventtime.replace(hour=self.h_min, minute=self.m_min, second=0, microsecond=0) except Exception: logger.error('Wrong syntax: {self._triggertime}. Should be [H:M<](skyevent)[+|-][offset][ 4: + del versl[-1] + return versl + + @classmethod + def to_list(cls, vers): + """ + Split version number to list and get rid of non-numeric parts + + :param vers: + + :return: version as list + :rtype: list + """ + if len(vers) == 0: + vers = '0' + if vers[0].lower() == 'v': + vers = vers[1:] + + # create list with [major,minor,revision,build] + vsplit = vers.split('.') + vsplit = cls.check_list(vsplit) + + # get rid of non numeric parts + vlist = [] + build = 0 + if vsplit == '': + return '' + for v in vsplit: + if v[-1].isalpha(): + build += ord(v[-1].lower()) - 96 + v = v[:-1] + vi = 0 + try: + vi = int(v) + except: + pass + vlist.append(vi) + vlist[3] += build + return vlist + + @classmethod + def to_string(cls, versl): + + if versl == [0, 0, 0, 0]: + return '' + import copy + versl2 = copy.deepcopy(versl) + cls.check_list(versl2) + if versl2 == '': + return '' + + if versl2[3] == 0: + del versl2[3] + versls = [str(int) for int in versl2] + vers = ".".join(versls) + + return 'v' + vers + + @classmethod + def format(cls, vers): + + return cls.to_string(cls.to_list(str(vers))) + + @classmethod + def compare(cls, v1, v2, operator): + """ + Compare two version numbers and return if the condition is met + + :param v1: + :param v2: + :param operator: + :type v1: str or list of int + :type v2: str or list of int + :type operator: str + + :return: true if condition is met + :rtype: bool + """ + if isinstance(v1, str): + v1 = cls.to_list(v1) + if isinstance(v2, str): + v2 = cls.to_list(v2) + + result = False + if v1 == v2 and operator in ['>=', '==', '<=']: + result = True + if v1 < v2 and operator in ['<', '<=']: + result = True + if v1 > v2 and operator in ['>', '>=']: + result = True + # logger.warning(f"_compare_versions: v1={v1}, v2={v2}, operator='{operator}', result={result}") + return result + +#-------------------------------------------------------------------------------------------- def get_python_version(): PYTHON_VERSION = str(sys.version_info[0]) + '.' + str(sys.version_info[1]) + '.' + str(sys.version_info[2]) + ' ' + str(sys.version_info[3]) diff --git a/logics/.gitignore b/logics/.gitignore index 276cef36bf..f850825a77 100644 --- a/logics/.gitignore +++ b/logics/.gitignore @@ -3,3 +3,4 @@ # except .gitignore !.gitignore !example_*.* +!logic.tpl diff --git a/logics/logic.tpl b/logics/logic.tpl new file mode 100644 index 0000000000..280e34fa01 --- /dev/null +++ b/logics/logic.tpl @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +# +# This file contains a logic for use with SmartHomeNG +# +# Name of the logic: example_logic.py +# + +# This logic performs the following function(s): +# +# ... +# + +# The following triggers should be defined in ../etc/logic.yaml: +# +# watch_item = | | ... +# crontab = init = Init +# cycle = 600 +# + diff --git a/modules/admin/api_services.py b/modules/admin/api_services.py index 219c002bb4..5c8db9403d 100755 --- a/modules/admin/api_services.py +++ b/modules/admin/api_services.py @@ -113,6 +113,7 @@ def eval_syntax_checker(self, eval_code, relative_to): if rel_to_item is not None: expanded_code = rel_to_item.get_stringwithabsolutepathes(eval_code, 'sh.', '(') expanded_code = rel_to_item.get_stringwithabsolutepathes(expanded_code, 'sh.', '.property') + value = rel_to_item() # item value for use in eval else: expanded_code = "Error: Item {} does not exist!".format(relative_to) try: diff --git a/modules/http/webif/gstatic/css/smarthomeng.css b/modules/http/webif/gstatic/css/smarthomeng.css index f37adc4358..4880eedc6e 100644 --- a/modules/http/webif/gstatic/css/smarthomeng.css +++ b/modules/http/webif/gstatic/css/smarthomeng.css @@ -31,6 +31,15 @@ plusIconlist-group-item.py-2.node-tree.node-selected { background-color: #ecf3f8; } +table .truncate { + overflow: hidden; + text-overflow: ellipsis; +} + +table .notruncate { + overflow: visible; +} + .minusIcon { position: relative; display: inline-block; @@ -247,4 +256,3 @@ a.text-shng:focus, a.text-shng:hover { color: #4d4d4d; font-weight: bold; } - diff --git a/modules/http/webif/gstatic/datatables/datatables.defaults.js b/modules/http/webif/gstatic/datatables/datatables.defaults.js new file mode 100644 index 0000000000..135072cb2f --- /dev/null +++ b/modules/http/webif/gstatic/datatables/datatables.defaults.js @@ -0,0 +1,62 @@ +/* + * This file contains a binding function with some useful datatables options + * It can be implemented in the web interface page of a plugin by using + * + */ +$(window).bind('datatables_defaults', function() { + // jQuery fix possible performance issue on scrolling + // https://stackoverflow.com/questions/39152877/ + jQuery.event.special.touchstart = { + setup: function( _, ns, handle ) { + this.addEventListener("touchstart", handle, { passive: !ns.includes("noPreventDefault") }); + } + }; + jQuery.event.special.touchmove = { + setup: function( _, ns, handle ) { + this.addEventListener('touchmove', handle, { passive: !ns.includes('noPreventDefault') }); + } + }; + try + { + + // Set datatable useful defaults + $.extend( $.fn.dataTable.defaults, { + lengthMenu: [ [25, 50, 100, -1], [25, 50, 100, "All"] ], // pagination menu + pageLength: 100, // default to "all" + pagingType: "full_numbers", // include first and last in pagination menu + colReorder: true, // enable colomn reorder by drag and drop + columnDefs: [{ targets: '_all', className: 'truncate' }], + fixedHeader: {header: true, // header will always be visible on top of page when scrolling + headerOffset: $('#webif-navbar').outerHeight() + $('#webif-tabs').outerHeight()}, + autoWidth: false, + initComplete: function () {$(this).show();}, // show table (only) after init + responsive: {details: {renderer: $.fn.dataTable.Responsive.renderer.listHiddenNodes()}}, //makes it possible to update columns even if they are not shown as columns (but as collapsable items) + fnDrawCallback: function(oSettings) { // hide pagination if not needed + if (oSettings._iDisplayLength > oSettings.fnRecordsDisplay() || oSettings._iDisplayLength == -1) { + $(oSettings.nTableWrapper).find('.dataTables_paginate').hide(); + } else { + $(oSettings.nTableWrapper).find('.dataTables_paginate').show(); + } + $.fn.dataTable.tables({ visible: true, api: true }).fixedHeader.enable( false ); + $.fn.dataTable.tables({ visible: true, api: true }).fixedHeader.enable( true ); + $.fn.dataTable.tables({ visible: true, api: true }).fixedHeader.adjust(); + } + }); + // Set date format for correct sorting of columns containing date strings + $.fn.dataTable.moment('DD.MM.YYYY HH:mm:ss'); + $.fn.dataTable.moment('YYYY-MM-DD HH:mm:ss'); + $('a[data-toggle="tab"]').on('shown.bs.tab', function(e){ + $.fn.dataTable.tables({ visible: true, api: true }).columns.adjust(); + $.fn.dataTable.tables({ visible: true, api: true }).fixedHeader.adjust(); + $.fn.dataTable.tables({ visible: true, api: true }).responsive.recalc(); + }); + } + catch (e) + { + console.log("Datatable JS not loaded, showing standard table without reorder option" + e) + } +}); diff --git a/modules/http/webif/gstatic/datatables/datatables.min.css b/modules/http/webif/gstatic/datatables/datatables.min.css index 4720d1866d..d86236f76e 100644 --- a/modules/http/webif/gstatic/datatables/datatables.min.css +++ b/modules/http/webif/gstatic/datatables/datatables.min.css @@ -4,10 +4,19 @@ * * To rebuild or modify this file with the latest versions of the included * software please visit: - * https://datatables.net/download/#dt/dt-1.11.0 + * https://datatables.net/download/#dt/dt-1.11.4/cr-1.5.5/fh-3.2.1/r-2.2.9 * * Included libraries: - * DataTables 1.11.0 + * DataTables 1.11.4, ColReorder 1.5.5, FixedHeader 3.2.1, Responsive 2.2.9 */ -td.dt-control{background:url("https://www.datatables.net/examples/resources/details_open.png") no-repeat center center;cursor:pointer}tr.dt-hasChild td.dt-control{background:url("https://www.datatables.net/examples/resources/details_close.png") no-repeat center center}table.dataTable th.dt-left,table.dataTable td.dt-left{text-align:left}table.dataTable th.dt-center,table.dataTable td.dt-center,table.dataTable td.dataTables_empty{text-align:center}table.dataTable th.dt-right,table.dataTable td.dt-right{text-align:right}table.dataTable th.dt-justify,table.dataTable td.dt-justify{text-align:justify}table.dataTable th.dt-nowrap,table.dataTable td.dt-nowrap{white-space:nowrap}table.dataTable thead th.dt-head-left,table.dataTable thead td.dt-head-left,table.dataTable tfoot th.dt-head-left,table.dataTable tfoot td.dt-head-left{text-align:left}table.dataTable thead th.dt-head-center,table.dataTable thead td.dt-head-center,table.dataTable tfoot th.dt-head-center,table.dataTable tfoot td.dt-head-center{text-align:center}table.dataTable thead th.dt-head-right,table.dataTable thead td.dt-head-right,table.dataTable tfoot th.dt-head-right,table.dataTable tfoot td.dt-head-right{text-align:right}table.dataTable thead th.dt-head-justify,table.dataTable thead td.dt-head-justify,table.dataTable tfoot th.dt-head-justify,table.dataTable tfoot td.dt-head-justify{text-align:justify}table.dataTable thead th.dt-head-nowrap,table.dataTable thead td.dt-head-nowrap,table.dataTable tfoot th.dt-head-nowrap,table.dataTable tfoot td.dt-head-nowrap{white-space:nowrap}table.dataTable tbody th.dt-body-left,table.dataTable tbody td.dt-body-left{text-align:left}table.dataTable tbody th.dt-body-center,table.dataTable tbody td.dt-body-center{text-align:center}table.dataTable tbody th.dt-body-right,table.dataTable tbody td.dt-body-right{text-align:right}table.dataTable tbody th.dt-body-justify,table.dataTable tbody td.dt-body-justify{text-align:justify}table.dataTable tbody th.dt-body-nowrap,table.dataTable tbody td.dt-body-nowrap{white-space:nowrap}table.dataTable{width:100%;margin:0 auto;clear:both;border-collapse:separate;border-spacing:0}table.dataTable thead th,table.dataTable tfoot th{font-weight:bold}table.dataTable thead th,table.dataTable thead td{padding:10px 18px;border-bottom:1px solid #111}table.dataTable thead th:active,table.dataTable thead td:active{outline:none}table.dataTable tfoot th,table.dataTable tfoot td{padding:10px 18px 6px 18px;border-top:1px solid #111}table.dataTable thead .sorting,table.dataTable thead .sorting_asc,table.dataTable thead .sorting_desc,table.dataTable thead .sorting_asc_disabled,table.dataTable thead .sorting_desc_disabled{cursor:pointer;*cursor:hand;background-repeat:no-repeat;background-position:center right}table.dataTable thead .sorting{background-image:url("img/sort_both.png")}table.dataTable thead .sorting_asc{background-image:url("img/sort_asc.png") !important}table.dataTable thead .sorting_desc{background-image:url("img/sort_desc.png") !important}table.dataTable thead .sorting_asc_disabled{background-image:url("img/sort_asc_disabled.png")}table.dataTable thead .sorting_desc_disabled{background-image:url("img/sort_desc_disabled.png")}table.dataTable tbody tr{background-color:#fff}table.dataTable tbody tr.selected{background-color:#b0bed9}table.dataTable tbody th,table.dataTable tbody td{padding:8px 10px}table.dataTable.row-border tbody th,table.dataTable.row-border tbody td,table.dataTable.display tbody th,table.dataTable.display tbody td{border-top:1px solid #ddd}table.dataTable.row-border tbody tr:first-child th,table.dataTable.row-border tbody tr:first-child td,table.dataTable.display tbody tr:first-child th,table.dataTable.display tbody tr:first-child td{border-top:none}table.dataTable.cell-border tbody th,table.dataTable.cell-border tbody td{border-top:1px solid #ddd;border-right:1px solid #ddd}table.dataTable.cell-border tbody tr th:first-child,table.dataTable.cell-border tbody tr td:first-child{border-left:1px solid #ddd}table.dataTable.cell-border tbody tr:first-child th,table.dataTable.cell-border tbody tr:first-child td{border-top:none}table.dataTable.stripe tbody tr.odd,table.dataTable.display tbody tr.odd{background-color:#f9f9f9}table.dataTable.stripe tbody tr.odd.selected,table.dataTable.display tbody tr.odd.selected{background-color:#acbad4}table.dataTable.hover tbody tr:hover,table.dataTable.display tbody tr:hover{background-color:#f6f6f6}table.dataTable.hover tbody tr:hover.selected,table.dataTable.display tbody tr:hover.selected{background-color:#aab7d1}table.dataTable.order-column tbody tr>.sorting_1,table.dataTable.order-column tbody tr>.sorting_2,table.dataTable.order-column tbody tr>.sorting_3,table.dataTable.display tbody tr>.sorting_1,table.dataTable.display tbody tr>.sorting_2,table.dataTable.display tbody tr>.sorting_3{background-color:#fafafa}table.dataTable.order-column tbody tr.selected>.sorting_1,table.dataTable.order-column tbody tr.selected>.sorting_2,table.dataTable.order-column tbody tr.selected>.sorting_3,table.dataTable.display tbody tr.selected>.sorting_1,table.dataTable.display tbody tr.selected>.sorting_2,table.dataTable.display tbody tr.selected>.sorting_3{background-color:#acbad5}table.dataTable.display tbody tr.odd>.sorting_1,table.dataTable.order-column.stripe tbody tr.odd>.sorting_1{background-color:#f1f1f1}table.dataTable.display tbody tr.odd>.sorting_2,table.dataTable.order-column.stripe tbody tr.odd>.sorting_2{background-color:#f3f3f3}table.dataTable.display tbody tr.odd>.sorting_3,table.dataTable.order-column.stripe tbody tr.odd>.sorting_3{background-color:whitesmoke}table.dataTable.display tbody tr.odd.selected>.sorting_1,table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_1{background-color:#a6b4cd}table.dataTable.display tbody tr.odd.selected>.sorting_2,table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_2{background-color:#a8b5cf}table.dataTable.display tbody tr.odd.selected>.sorting_3,table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_3{background-color:#a9b7d1}table.dataTable.display tbody tr.even>.sorting_1,table.dataTable.order-column.stripe tbody tr.even>.sorting_1{background-color:#fafafa}table.dataTable.display tbody tr.even>.sorting_2,table.dataTable.order-column.stripe tbody tr.even>.sorting_2{background-color:#fcfcfc}table.dataTable.display tbody tr.even>.sorting_3,table.dataTable.order-column.stripe tbody tr.even>.sorting_3{background-color:#fefefe}table.dataTable.display tbody tr.even.selected>.sorting_1,table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_1{background-color:#acbad5}table.dataTable.display tbody tr.even.selected>.sorting_2,table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_2{background-color:#aebcd6}table.dataTable.display tbody tr.even.selected>.sorting_3,table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_3{background-color:#afbdd8}table.dataTable.display tbody tr:hover>.sorting_1,table.dataTable.order-column.hover tbody tr:hover>.sorting_1{background-color:#eaeaea}table.dataTable.display tbody tr:hover>.sorting_2,table.dataTable.order-column.hover tbody tr:hover>.sorting_2{background-color:#ececec}table.dataTable.display tbody tr:hover>.sorting_3,table.dataTable.order-column.hover tbody tr:hover>.sorting_3{background-color:#efefef}table.dataTable.display tbody tr:hover.selected>.sorting_1,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_1{background-color:#a2aec7}table.dataTable.display tbody tr:hover.selected>.sorting_2,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_2{background-color:#a3b0c9}table.dataTable.display tbody tr:hover.selected>.sorting_3,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_3{background-color:#a5b2cb}table.dataTable.no-footer{border-bottom:1px solid #111}table.dataTable.nowrap th,table.dataTable.nowrap td{white-space:nowrap}table.dataTable.compact thead th,table.dataTable.compact thead td{padding:4px 17px}table.dataTable.compact tfoot th,table.dataTable.compact tfoot td{padding:4px}table.dataTable.compact tbody th,table.dataTable.compact tbody td{padding:4px}table.dataTable th,table.dataTable td{box-sizing:content-box}.dataTables_wrapper{position:relative;clear:both}.dataTables_wrapper .dataTables_length{float:left}.dataTables_wrapper .dataTables_length select{border:1px solid #aaa;border-radius:3px;padding:5px;background-color:transparent;padding:4px}.dataTables_wrapper .dataTables_filter{float:right;text-align:right}.dataTables_wrapper .dataTables_filter input{border:1px solid #aaa;border-radius:3px;padding:5px;background-color:transparent;margin-left:3px}.dataTables_wrapper .dataTables_info{clear:both;float:left;padding-top:.755em}.dataTables_wrapper .dataTables_paginate{float:right;text-align:right;padding-top:.25em}.dataTables_wrapper .dataTables_paginate .paginate_button{box-sizing:border-box;display:inline-block;min-width:1.5em;padding:.5em 1em;margin-left:2px;text-align:center;text-decoration:none !important;cursor:pointer;*cursor:hand;color:#333 !important;border:1px solid transparent;border-radius:2px}.dataTables_wrapper .dataTables_paginate .paginate_button.current,.dataTables_wrapper .dataTables_paginate .paginate_button.current:hover{color:#333 !important;border:1px solid #979797;background-color:white;background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, white), color-stop(100%, #dcdcdc));background:-webkit-linear-gradient(top, white 0%, #dcdcdc 100%);background:-moz-linear-gradient(top, white 0%, #dcdcdc 100%);background:-ms-linear-gradient(top, white 0%, #dcdcdc 100%);background:-o-linear-gradient(top, white 0%, #dcdcdc 100%);background:linear-gradient(to bottom, white 0%, #dcdcdc 100%)}.dataTables_wrapper .dataTables_paginate .paginate_button.disabled,.dataTables_wrapper .dataTables_paginate .paginate_button.disabled:hover,.dataTables_wrapper .dataTables_paginate .paginate_button.disabled:active{cursor:default;color:#666 !important;border:1px solid transparent;background:transparent;box-shadow:none}.dataTables_wrapper .dataTables_paginate .paginate_button:hover{color:white !important;border:1px solid #111;background-color:#585858;background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #585858), color-stop(100%, #111));background:-webkit-linear-gradient(top, #585858 0%, #111 100%);background:-moz-linear-gradient(top, #585858 0%, #111 100%);background:-ms-linear-gradient(top, #585858 0%, #111 100%);background:-o-linear-gradient(top, #585858 0%, #111 100%);background:linear-gradient(to bottom, #585858 0%, #111 100%)}.dataTables_wrapper .dataTables_paginate .paginate_button:active{outline:none;background-color:#2b2b2b;background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #2b2b2b), color-stop(100%, #0c0c0c));background:-webkit-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:-moz-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:-ms-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:-o-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:linear-gradient(to bottom, #2b2b2b 0%, #0c0c0c 100%);box-shadow:inset 0 0 3px #111}.dataTables_wrapper .dataTables_paginate .ellipsis{padding:0 1em}.dataTables_wrapper .dataTables_processing{position:absolute;top:50%;left:50%;width:100%;height:40px;margin-left:-50%;margin-top:-25px;padding-top:20px;text-align:center;font-size:1.2em;background-color:white;background:-webkit-gradient(linear, left top, right top, color-stop(0%, rgba(255, 255, 255, 0)), color-stop(25%, rgba(255, 255, 255, 0.9)), color-stop(75%, rgba(255, 255, 255, 0.9)), color-stop(100%, rgba(255, 255, 255, 0)));background:-webkit-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);background:-moz-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);background:-ms-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);background:-o-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);background:linear-gradient(to right, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%)}.dataTables_wrapper .dataTables_length,.dataTables_wrapper .dataTables_filter,.dataTables_wrapper .dataTables_info,.dataTables_wrapper .dataTables_processing,.dataTables_wrapper .dataTables_paginate{color:#333}.dataTables_wrapper .dataTables_scroll{clear:both}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody{*margin-top:-1px;-webkit-overflow-scrolling:touch}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>th,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>td,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>th,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>td{vertical-align:middle}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>th>div.dataTables_sizing,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>td>div.dataTables_sizing,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>th>div.dataTables_sizing,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>td>div.dataTables_sizing{height:0;overflow:hidden;margin:0 !important;padding:0 !important}.dataTables_wrapper.no-footer .dataTables_scrollBody{border-bottom:1px solid #111}.dataTables_wrapper.no-footer div.dataTables_scrollHead table.dataTable,.dataTables_wrapper.no-footer div.dataTables_scrollBody>table{border-bottom:none}.dataTables_wrapper:after{visibility:hidden;display:block;content:"";clear:both;height:0}@media screen and (max-width: 767px){.dataTables_wrapper .dataTables_info,.dataTables_wrapper .dataTables_paginate{float:none;text-align:center}.dataTables_wrapper .dataTables_paginate{margin-top:.5em}}@media screen and (max-width: 640px){.dataTables_wrapper .dataTables_length,.dataTables_wrapper .dataTables_filter{float:none;text-align:center}.dataTables_wrapper .dataTables_filter{margin-top:.5em}} +table.dataTable th.dt-left,table.dataTable td.dt-left{text-align:left}table.dataTable th.dt-center,table.dataTable td.dt-center,table.dataTable td.dataTables_empty{text-align:center}table.dataTable th.dt-right,table.dataTable td.dt-right{text-align:right}table.dataTable th.dt-justify,table.dataTable td.dt-justify{text-align:justify}table.dataTable th.dt-nowrap,table.dataTable td.dt-nowrap{white-space:nowrap}table.dataTable thead th.dt-head-left,table.dataTable thead td.dt-head-left,table.dataTable tfoot th.dt-head-left,table.dataTable tfoot td.dt-head-left{text-align:left}table.dataTable thead th.dt-head-center,table.dataTable thead td.dt-head-center,table.dataTable tfoot th.dt-head-center,table.dataTable tfoot td.dt-head-center{text-align:center}table.dataTable thead th.dt-head-right,table.dataTable thead td.dt-head-right,table.dataTable tfoot th.dt-head-right,table.dataTable tfoot td.dt-head-right{text-align:right}table.dataTable thead th.dt-head-justify,table.dataTable thead td.dt-head-justify,table.dataTable tfoot th.dt-head-justify,table.dataTable tfoot td.dt-head-justify{text-align:justify}table.dataTable thead th.dt-head-nowrap,table.dataTable thead td.dt-head-nowrap,table.dataTable tfoot th.dt-head-nowrap,table.dataTable tfoot td.dt-head-nowrap{white-space:nowrap}table.dataTable tbody th.dt-body-left,table.dataTable tbody td.dt-body-left{text-align:left}table.dataTable tbody th.dt-body-center,table.dataTable tbody td.dt-body-center{text-align:center}table.dataTable tbody th.dt-body-right,table.dataTable tbody td.dt-body-right{text-align:right}table.dataTable tbody th.dt-body-justify,table.dataTable tbody td.dt-body-justify{text-align:justify}table.dataTable tbody th.dt-body-nowrap,table.dataTable tbody td.dt-body-nowrap{white-space:nowrap}table.dataTable td.dt-control{text-align:center;cursor:pointer}table.dataTable td.dt-control:before{height:1em;width:1em;margin-top:-9px;display:inline-block;color:white;border:.15em solid white;border-radius:1em;box-shadow:0 0 .2em #444;box-sizing:content-box;text-align:center;text-indent:0 !important;font-family:"Courier New",Courier,monospace;line-height:1em;content:"+";background-color:#31b131}table.dataTable tr.dt-hasChild td.dt-control:before{content:"-";background-color:#d33333}table.dataTable{width:100%;margin:0 auto;clear:both;border-collapse:separate;border-spacing:0}table.dataTable thead th,table.dataTable tfoot th{font-weight:bold}table.dataTable thead th,table.dataTable thead td{padding:10px 18px;border-bottom:1px solid #111}table.dataTable thead th:active,table.dataTable thead td:active{outline:none}table.dataTable tfoot th,table.dataTable tfoot td{padding:10px 18px 6px 18px;border-top:1px solid #111}table.dataTable thead .sorting,table.dataTable thead .sorting_asc,table.dataTable thead .sorting_desc,table.dataTable thead .sorting_asc_disabled,table.dataTable thead .sorting_desc_disabled{cursor:pointer;*cursor:hand;background-repeat:no-repeat;background-position:center right}table.dataTable thead .sorting{background-image:url("img/sort_both.png")}table.dataTable thead .sorting_asc{background-image:url("img/sort_asc.png") !important}table.dataTable thead .sorting_desc{background-image:url("img/sort_desc.png") !important}table.dataTable thead .sorting_asc_disabled{background-image:url("img/sort_asc_disabled.png")}table.dataTable thead .sorting_desc_disabled{background-image:url("img/sort_desc_disabled.png")}table.dataTable tbody tr{background-color:#fff}table.dataTable tbody tr.selected{background-color:#b0bed9}table.dataTable tbody th,table.dataTable tbody td{padding:8px 10px}table.dataTable.row-border tbody th,table.dataTable.row-border tbody td,table.dataTable.display tbody th,table.dataTable.display tbody td{border-top:1px solid #ddd}table.dataTable.row-border tbody tr:first-child th,table.dataTable.row-border tbody tr:first-child td,table.dataTable.display tbody tr:first-child th,table.dataTable.display tbody tr:first-child td{border-top:none}table.dataTable.cell-border tbody th,table.dataTable.cell-border tbody td{border-top:1px solid #ddd;border-right:1px solid #ddd}table.dataTable.cell-border tbody tr th:first-child,table.dataTable.cell-border tbody tr td:first-child{border-left:1px solid #ddd}table.dataTable.cell-border tbody tr:first-child th,table.dataTable.cell-border tbody tr:first-child td{border-top:none}table.dataTable.stripe tbody tr.odd,table.dataTable.display tbody tr.odd{background-color:#f9f9f9}table.dataTable.stripe tbody tr.odd.selected,table.dataTable.display tbody tr.odd.selected{background-color:#acbad4}table.dataTable.hover tbody tr:hover,table.dataTable.display tbody tr:hover{background-color:#f6f6f6}table.dataTable.hover tbody tr:hover.selected,table.dataTable.display tbody tr:hover.selected{background-color:#aab7d1}table.dataTable.order-column tbody tr>.sorting_1,table.dataTable.order-column tbody tr>.sorting_2,table.dataTable.order-column tbody tr>.sorting_3,table.dataTable.display tbody tr>.sorting_1,table.dataTable.display tbody tr>.sorting_2,table.dataTable.display tbody tr>.sorting_3{background-color:#fafafa}table.dataTable.order-column tbody tr.selected>.sorting_1,table.dataTable.order-column tbody tr.selected>.sorting_2,table.dataTable.order-column tbody tr.selected>.sorting_3,table.dataTable.display tbody tr.selected>.sorting_1,table.dataTable.display tbody tr.selected>.sorting_2,table.dataTable.display tbody tr.selected>.sorting_3{background-color:#acbad5}table.dataTable.display tbody tr.odd>.sorting_1,table.dataTable.order-column.stripe tbody tr.odd>.sorting_1{background-color:#f1f1f1}table.dataTable.display tbody tr.odd>.sorting_2,table.dataTable.order-column.stripe tbody tr.odd>.sorting_2{background-color:#f3f3f3}table.dataTable.display tbody tr.odd>.sorting_3,table.dataTable.order-column.stripe tbody tr.odd>.sorting_3{background-color:whitesmoke}table.dataTable.display tbody tr.odd.selected>.sorting_1,table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_1{background-color:#a6b4cd}table.dataTable.display tbody tr.odd.selected>.sorting_2,table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_2{background-color:#a8b5cf}table.dataTable.display tbody tr.odd.selected>.sorting_3,table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_3{background-color:#a9b7d1}table.dataTable.display tbody tr.even>.sorting_1,table.dataTable.order-column.stripe tbody tr.even>.sorting_1{background-color:#fafafa}table.dataTable.display tbody tr.even>.sorting_2,table.dataTable.order-column.stripe tbody tr.even>.sorting_2{background-color:#fcfcfc}table.dataTable.display tbody tr.even>.sorting_3,table.dataTable.order-column.stripe tbody tr.even>.sorting_3{background-color:#fefefe}table.dataTable.display tbody tr.even.selected>.sorting_1,table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_1{background-color:#acbad5}table.dataTable.display tbody tr.even.selected>.sorting_2,table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_2{background-color:#aebcd6}table.dataTable.display tbody tr.even.selected>.sorting_3,table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_3{background-color:#afbdd8}table.dataTable.display tbody tr:hover>.sorting_1,table.dataTable.order-column.hover tbody tr:hover>.sorting_1{background-color:#eaeaea}table.dataTable.display tbody tr:hover>.sorting_2,table.dataTable.order-column.hover tbody tr:hover>.sorting_2{background-color:#ececec}table.dataTable.display tbody tr:hover>.sorting_3,table.dataTable.order-column.hover tbody tr:hover>.sorting_3{background-color:#efefef}table.dataTable.display tbody tr:hover.selected>.sorting_1,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_1{background-color:#a2aec7}table.dataTable.display tbody tr:hover.selected>.sorting_2,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_2{background-color:#a3b0c9}table.dataTable.display tbody tr:hover.selected>.sorting_3,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_3{background-color:#a5b2cb}table.dataTable.no-footer{border-bottom:1px solid #111}table.dataTable.nowrap th,table.dataTable.nowrap td{white-space:nowrap}table.dataTable.compact thead th,table.dataTable.compact thead td{padding:4px 17px}table.dataTable.compact tfoot th,table.dataTable.compact tfoot td{padding:4px}table.dataTable.compact tbody th,table.dataTable.compact tbody td{padding:4px}table.dataTable th,table.dataTable td{box-sizing:content-box}.dataTables_wrapper{position:relative;clear:both}.dataTables_wrapper .dataTables_length{float:left}.dataTables_wrapper .dataTables_length select{border:1px solid #aaa;border-radius:3px;padding:5px;background-color:transparent;padding:4px}.dataTables_wrapper .dataTables_filter{float:right;text-align:right}.dataTables_wrapper .dataTables_filter input{border:1px solid #aaa;border-radius:3px;padding:5px;background-color:transparent;margin-left:3px}.dataTables_wrapper .dataTables_info{clear:both;float:left;padding-top:.755em}.dataTables_wrapper .dataTables_paginate{float:right;text-align:right;padding-top:.25em}.dataTables_wrapper .dataTables_paginate .paginate_button{box-sizing:border-box;display:inline-block;min-width:1.5em;padding:.5em 1em;margin-left:2px;text-align:center;text-decoration:none !important;cursor:pointer;*cursor:hand;color:#333 !important;border:1px solid transparent;border-radius:2px}.dataTables_wrapper .dataTables_paginate .paginate_button.current,.dataTables_wrapper .dataTables_paginate .paginate_button.current:hover{color:#333 !important;border:1px solid #979797;background-color:white;background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, white), color-stop(100%, #dcdcdc));background:-webkit-linear-gradient(top, white 0%, #dcdcdc 100%);background:-moz-linear-gradient(top, white 0%, #dcdcdc 100%);background:-ms-linear-gradient(top, white 0%, #dcdcdc 100%);background:-o-linear-gradient(top, white 0%, #dcdcdc 100%);background:linear-gradient(to bottom, white 0%, #dcdcdc 100%)}.dataTables_wrapper .dataTables_paginate .paginate_button.disabled,.dataTables_wrapper .dataTables_paginate .paginate_button.disabled:hover,.dataTables_wrapper .dataTables_paginate .paginate_button.disabled:active{cursor:default;color:#666 !important;border:1px solid transparent;background:transparent;box-shadow:none}.dataTables_wrapper .dataTables_paginate .paginate_button:hover{color:white !important;border:1px solid #111;background-color:#585858;background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #585858), color-stop(100%, #111));background:-webkit-linear-gradient(top, #585858 0%, #111 100%);background:-moz-linear-gradient(top, #585858 0%, #111 100%);background:-ms-linear-gradient(top, #585858 0%, #111 100%);background:-o-linear-gradient(top, #585858 0%, #111 100%);background:linear-gradient(to bottom, #585858 0%, #111 100%)}.dataTables_wrapper .dataTables_paginate .paginate_button:active{outline:none;background-color:#2b2b2b;background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #2b2b2b), color-stop(100%, #0c0c0c));background:-webkit-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:-moz-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:-ms-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:-o-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:linear-gradient(to bottom, #2b2b2b 0%, #0c0c0c 100%);box-shadow:inset 0 0 3px #111}.dataTables_wrapper .dataTables_paginate .ellipsis{padding:0 1em}.dataTables_wrapper .dataTables_processing{position:absolute;top:50%;left:50%;width:100%;height:40px;margin-left:-50%;margin-top:-25px;padding-top:20px;text-align:center;font-size:1.2em;background-color:white;background:-webkit-gradient(linear, left top, right top, color-stop(0%, rgba(255, 255, 255, 0)), color-stop(25%, rgba(255, 255, 255, 0.9)), color-stop(75%, rgba(255, 255, 255, 0.9)), color-stop(100%, rgba(255, 255, 255, 0)));background:-webkit-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);background:-moz-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);background:-ms-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);background:-o-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);background:linear-gradient(to right, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%)}.dataTables_wrapper .dataTables_length,.dataTables_wrapper .dataTables_filter,.dataTables_wrapper .dataTables_info,.dataTables_wrapper .dataTables_processing,.dataTables_wrapper .dataTables_paginate{color:#333}.dataTables_wrapper .dataTables_scroll{clear:both}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody{*margin-top:-1px;-webkit-overflow-scrolling:touch}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>th,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>td,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>th,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>td{vertical-align:middle}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>th>div.dataTables_sizing,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>td>div.dataTables_sizing,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>th>div.dataTables_sizing,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>td>div.dataTables_sizing{height:0;overflow:hidden;margin:0 !important;padding:0 !important}.dataTables_wrapper.no-footer .dataTables_scrollBody{border-bottom:1px solid #111}.dataTables_wrapper.no-footer div.dataTables_scrollHead table.dataTable,.dataTables_wrapper.no-footer div.dataTables_scrollBody>table{border-bottom:none}.dataTables_wrapper:after{visibility:hidden;display:block;content:"";clear:both;height:0}@media screen and (max-width: 767px){.dataTables_wrapper .dataTables_info,.dataTables_wrapper .dataTables_paginate{float:none;text-align:center}.dataTables_wrapper .dataTables_paginate{margin-top:.5em}}@media screen and (max-width: 640px){.dataTables_wrapper .dataTables_length,.dataTables_wrapper .dataTables_filter{float:none;text-align:center}.dataTables_wrapper .dataTables_filter{margin-top:.5em}} + + +table.DTCR_clonedTable.dataTable{position:absolute !important;background-color:rgba(255, 255, 255, 0.7);z-index:202}div.DTCR_pointer{width:1px;background-color:#0259c4;z-index:201} + + +table.fixedHeader-floating{background-color:white}table.fixedHeader-floating.no-footer{border-bottom-width:0}table.fixedHeader-locked{position:absolute !important;background-color:white}@media print{table.fixedHeader-floating{display:none}} + + +table.dataTable.dtr-inline.collapsed>tbody>tr>td.child,table.dataTable.dtr-inline.collapsed>tbody>tr>th.child,table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty{cursor:default !important}table.dataTable.dtr-inline.collapsed>tbody>tr>td.child:before,table.dataTable.dtr-inline.collapsed>tbody>tr>th.child:before,table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty:before{display:none !important}table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control,table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control{position:relative;padding-left:30px;cursor:pointer}table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control:before,table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control:before{top:50%;left:5px;height:1em;width:1em;margin-top:-9px;display:block;position:absolute;color:white;border:.15em solid white;border-radius:1em;box-shadow:0 0 .2em #444;box-sizing:content-box;text-align:center;text-indent:0 !important;font-family:"Courier New",Courier,monospace;line-height:1em;content:"+";background-color:#31b131}table.dataTable.dtr-inline.collapsed>tbody>tr.parent>td.dtr-control:before,table.dataTable.dtr-inline.collapsed>tbody>tr.parent>th.dtr-control:before{content:"-";background-color:#d33333}table.dataTable.dtr-inline.collapsed.compact>tbody>tr>td.dtr-control,table.dataTable.dtr-inline.collapsed.compact>tbody>tr>th.dtr-control{padding-left:27px}table.dataTable.dtr-inline.collapsed.compact>tbody>tr>td.dtr-control:before,table.dataTable.dtr-inline.collapsed.compact>tbody>tr>th.dtr-control:before{left:4px;height:14px;width:14px;border-radius:14px;line-height:14px;text-indent:3px}table.dataTable.dtr-column>tbody>tr>td.dtr-control,table.dataTable.dtr-column>tbody>tr>th.dtr-control,table.dataTable.dtr-column>tbody>tr>td.control,table.dataTable.dtr-column>tbody>tr>th.control{position:relative;cursor:pointer}table.dataTable.dtr-column>tbody>tr>td.dtr-control:before,table.dataTable.dtr-column>tbody>tr>th.dtr-control:before,table.dataTable.dtr-column>tbody>tr>td.control:before,table.dataTable.dtr-column>tbody>tr>th.control:before{top:50%;left:50%;height:.8em;width:.8em;margin-top:-0.5em;margin-left:-0.5em;display:block;position:absolute;color:white;border:.15em solid white;border-radius:1em;box-shadow:0 0 .2em #444;box-sizing:content-box;text-align:center;text-indent:0 !important;font-family:"Courier New",Courier,monospace;line-height:1em;content:"+";background-color:#31b131}table.dataTable.dtr-column>tbody>tr.parent td.dtr-control:before,table.dataTable.dtr-column>tbody>tr.parent th.dtr-control:before,table.dataTable.dtr-column>tbody>tr.parent td.control:before,table.dataTable.dtr-column>tbody>tr.parent th.control:before{content:"-";background-color:#d33333}table.dataTable>tbody>tr.child{padding:.5em 1em}table.dataTable>tbody>tr.child:hover{background:transparent !important}table.dataTable>tbody>tr.child ul.dtr-details{display:inline-block;list-style-type:none;margin:0;padding:0}table.dataTable>tbody>tr.child ul.dtr-details>li{border-bottom:1px solid #efefef;padding:.5em 0}table.dataTable>tbody>tr.child ul.dtr-details>li:first-child{padding-top:0}table.dataTable>tbody>tr.child ul.dtr-details>li:last-child{border-bottom:none}table.dataTable>tbody>tr.child span.dtr-title{display:inline-block;min-width:75px;font-weight:bold}div.dtr-modal{position:fixed;box-sizing:border-box;top:0;left:0;height:100%;width:100%;z-index:100;padding:10em 1em}div.dtr-modal div.dtr-modal-display{position:absolute;top:0;left:0;bottom:0;right:0;width:50%;height:50%;overflow:auto;margin:auto;z-index:102;overflow:auto;background-color:#f5f5f7;border:1px solid black;border-radius:.5em;box-shadow:0 12px 30px rgba(0, 0, 0, 0.6)}div.dtr-modal div.dtr-modal-content{position:relative;padding:1em}div.dtr-modal div.dtr-modal-close{position:absolute;top:6px;right:6px;width:22px;height:22px;border:1px solid #eaeaea;background-color:#f9f9f9;text-align:center;border-radius:3px;cursor:pointer;z-index:12}div.dtr-modal div.dtr-modal-close:hover{background-color:#eaeaea}div.dtr-modal div.dtr-modal-background{position:fixed;top:0;left:0;right:0;bottom:0;z-index:101;background:rgba(0, 0, 0, 0.6)}@media screen and (max-width: 767px){div.dtr-modal div.dtr-modal-display{width:95%}} diff --git a/modules/http/webif/gstatic/datatables/datatables.min.js b/modules/http/webif/gstatic/datatables/datatables.min.js index 51b23cad18..6d7ff7955f 100644 --- a/modules/http/webif/gstatic/datatables/datatables.min.js +++ b/modules/http/webif/gstatic/datatables/datatables.min.js @@ -4,10 +4,10 @@ * * To rebuild or modify this file with the latest versions of the included * software please visit: - * https://datatables.net/download/#dt/dt-1.11.0 + * https://datatables.net/download/#dt/dt-1.11.4/cr-1.5.5/fh-3.2.1/r-2.2.9 * * Included libraries: - * DataTables 1.11.0 + * DataTables 1.11.4, ColReorder 1.5.5, FixedHeader 3.2.1, Responsive 2.2.9 */ /*! @@ -21,7 +21,7 @@ or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details. For details please refer to: http://www.datatables.net - DataTables 1.11.0 + DataTables 1.11.4 ©2008-2021 SpryMedia Ltd - datatables.net/license */ var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.findInternal=function(l,z,A){l instanceof String&&(l=String(l));for(var q=l.length,E=0;E").css({position:"fixed",top:0,left:-1*l(z).scrollLeft(),height:1, +b[2].toLowerCase()),d[c]=e,"o"===b[1]&&E(a[e]))});a._hungarianMap=d}function P(a,b,c){a._hungarianMap||E(a);var d;l.each(b,function(e,h){d=a._hungarianMap[e];d===q||!c&&b[d]!==q||("o"===d.charAt(0)?(b[d]||(b[d]={}),l.extend(!0,b[d],b[e]),P(a[d],b[d],c)):b[d]=b[e])})}function ma(a){var b=u.defaults.oLanguage,c=b.sDecimal;c&&Xa(c);if(a){var d=a.sZeroRecords;!a.sEmptyTable&&d&&"No data available in table"===b.sEmptyTable&&X(a,a,"sZeroRecords","sEmptyTable");!a.sLoadingRecords&&d&&"Loading..."===b.sLoadingRecords&& +X(a,a,"sZeroRecords","sLoadingRecords");a.sInfoThousands&&(a.sThousands=a.sInfoThousands);(a=a.sDecimal)&&c!==a&&Xa(a)}}function zb(a){S(a,"ordering","bSort");S(a,"orderMulti","bSortMulti");S(a,"orderClasses","bSortClasses");S(a,"orderCellsTop","bSortCellsTop");S(a,"order","aaSorting");S(a,"orderFixed","aaSortingFixed");S(a,"paging","bPaginate");S(a,"pagingType","sPaginationType");S(a,"pageLength","iDisplayLength");S(a,"searching","bFilter");"boolean"===typeof a.sScrollX&&(a.sScrollX=a.sScrollX?"100%": +"");"boolean"===typeof a.scrollX&&(a.scrollX=a.scrollX?"100%":"");if(a=a.aoSearchCols)for(var b=0,c=a.length;b").css({position:"fixed",top:0,left:-1*l(z).scrollLeft(),height:1, width:1,overflow:"hidden"}).append(l("
").css({position:"absolute",top:1,left:1,width:100,overflow:"scroll"}).append(l("
").css({width:"100%",height:10}))).appendTo("body"),d=c.children(),e=d.children();b.barWidth=d[0].offsetWidth-d[0].clientWidth;b.bScrollOversize=100===e[0].offsetWidth&&100!==d[0].clientWidth;b.bScrollbarLeft=1!==Math.round(e.offset().left);b.bBounding=c[0].getBoundingClientRect().width?!0:!1;c.remove()}l.extend(a.oBrowser,u.__browser);a.oScroll.iBarWidth=u.__browser.barWidth} -function Bb(a,b,c,d,e,h){var g=!1;if(c!==q){var f=c;g=!0}for(;d!==e;)a.hasOwnProperty(d)&&(f=g?b(f,a[d],d,a):a[d],g=!0,d+=h);return f}function Xa(a,b){var c=u.defaults.column,d=a.aoColumns.length;c=l.extend({},u.models.oColumn,c,{nTh:b?b:A.createElement("th"),sTitle:c.sTitle?c.sTitle:b?b.innerHTML:"",aDataSort:c.aDataSort?c.aDataSort:[d],mData:c.mData?c.mData:d,idx:d});a.aoColumns.push(c);c=a.aoPreSearchCols;c[d]=l.extend({},u.models.oSearch,c[d]);Ga(a,d,l(b).data())}function Ga(a,b,c){b=a.aoColumns[b]; -var d=a.oClasses,e=l(b.nTh);if(!b.sWidthOrig){b.sWidthOrig=e.attr("width")||null;var h=(e.attr("style")||"").match(/width:\s*(\d+[pxem%]+)/);h&&(b.sWidthOrig=h[1])}c!==q&&null!==c&&(zb(c),P(u.defaults.column,c,!0),c.mDataProp===q||c.mData||(c.mData=c.mDataProp),c.sType&&(b._sManualType=c.sType),c.className&&!c.sClass&&(c.sClass=c.className),c.sClass&&e.addClass(c.sClass),l.extend(b,c),X(b,c,"sWidth","sWidthOrig"),c.iDataSort!==q&&(b.aDataSort=[c.iDataSort]),X(b,c,"aDataSort"));var g=b.mData,f=na(g), -k=b.mRender?na(b.mRender):null;c=function(n){return"string"===typeof n&&-1!==n.indexOf("@")};b._bAttrSrc=l.isPlainObject(g)&&(c(g.sort)||c(g.type)||c(g.filter));b._setter=null;b.fnGetData=function(n,m,p){var t=f(n,m,q,p);return k&&m?k(t,m,n,p):t};b.fnSetData=function(n,m,p){return ha(g)(n,m,p)};"number"!==typeof g&&(a._rowReadObject=!0);a.oFeatures.bSort||(b.bSortable=!1,e.addClass(d.sSortableNone));a=-1!==l.inArray("asc",b.asSorting);c=-1!==l.inArray("desc",b.asSorting);b.bSortable&&(a||c)?a&&!c? -(b.sSortingClass=d.sSortableAsc,b.sSortingClassJUI=d.sSortJUIAscAllowed):!a&&c?(b.sSortingClass=d.sSortableDesc,b.sSortingClassJUI=d.sSortJUIDescAllowed):(b.sSortingClass=d.sSortable,b.sSortingClassJUI=d.sSortJUI):(b.sSortingClass=d.sSortableNone,b.sSortingClassJUI="")}function ta(a){if(!1!==a.oFeatures.bAutoWidth){var b=a.aoColumns;Ya(a);for(var c=0,d=b.length;cn[m])d(f.length+n[m],k);else if("string"===typeof n[m]){var p=0;for(g=f.length;pb&&a[e]--; - -1!=d&&c===q&&a.splice(d,1)}function wa(a,b,c,d){var e=a.aoData[b],h,g=function(k,n){for(;k.childNodes.length;)k.removeChild(k.firstChild);k.innerHTML=T(a,b,n,"display")};if("dom"!==c&&(c&&"auto"!==c||"dom"!==e.src)){var f=e.anCells;if(f)if(d!==q)g(f[d],d);else for(c=0,h=f.length;c").appendTo(d));var k=0;for(b=f.length;k=a.fnRecordsDisplay()?0:f,a.iInitDisplayStart=-1);f=a._iDisplayStart;var m=a.fnDisplayEnd();if(a.bDeferLoading)a.bDeferLoading=!1,a.iDraw++,V(a,!1);else if(!k)a.iDraw++;else if(!a.bDestroying&&!b){Fb(a);return}if(0!==n.length)for(b=k?a.aoData.length:m,g=k?0:f;g",{"class":h?e[0]:""}).append(l("
").insertAfter(H));r.nTBody=ea[0];H=t.children("tfoot");0===H.length&&0").appendTo(t));0===H.length||0===H.children().length?t.addClass(C.sNoFooter):0/g,sc=/^\d{2,4}[\.\/\-]\d{1,2}[\.\/\-]\d{1,2}([T ]{1}\d{1,2}[:\.]\d{2}([\.:]\d{2})?)?$/,tc=/(\/|\.|\*|\+|\?|\||\(|\)|\[|\]|\{|\}|\\|\$|\^|\-)/g,qb=/['\u00A0,$£€¥%\u2009\u202F\u20BD\u20a9\u20BArfkɃΞ]/gi,Z=function(a){return a&&!0!==a&&"-"!==a?!1:!0},gc= -function(a){var b=parseInt(a,10);return!isNaN(b)&&isFinite(a)?b:null},hc=function(a,b){rb[b]||(rb[b]=new RegExp(ib(b),"g"));return"string"===typeof a&&"."!==b?a.replace(/\./g,"").replace(rb[b],"."):a},sb=function(a,b,c){var d="string"===typeof a;if(Z(a))return!0;b&&d&&(a=hc(a,b));c&&d&&(a=a.replace(qb,""));return!isNaN(parseFloat(a))&&isFinite(a)},ic=function(a,b,c){return Z(a)?!0:Z(a)||"string"===typeof a?sb(a.replace(Ua,""),b,c)?!0:null:null},U=function(a,b,c){var d=[],e=0,h=a.length;if(c!==q)for(;e< -h;e++)a[e]&&a[e][b]&&d.push(a[e][b][c]);else for(;ea.length)){var b=a.slice().sort();for(var c=b[0],d=1,e=b.length;d< -e;d++){if(b[d]===c){b=!1;break a}c=b[d]}}b=!0}if(b)return a.slice();b=[];e=a.length;var h,g=0;d=0;a:for(;d")[0],qc=Qa.textContent!==q,rc=/<.*?>/g,gb=u.util.throttle,lc=[],N=Array.prototype,uc=function(a){var b,c=u.settings,d=l.map(c,function(h,g){return h.nTable});if(a){if(a.nTable&&a.oApi)return[a];if(a.nodeName&&"table"===a.nodeName.toLowerCase()){var e=l.inArray(a,d);return-1!==e?[c[e]]:null}if(a&&"function"===typeof a.settings)return a.settings().toArray();"string"===typeof a?b=l(a):a instanceof l&&(b=a)}else return[];if(b)return b.map(function(h){e= -l.inArray(this,d);return-1!==e?c[e]:null}).toArray()};var B=function(a,b){if(!(this instanceof B))return new B(a,b);var c=[],d=function(g){(g=uc(g))&&c.push.apply(c,g)};if(Array.isArray(a))for(var e=0,h=a.length;ea?new B(b[a],this[a]):null},filter:function(a){var b=[];if(N.filter)b=N.filter.call(this,a,this);else for(var c=0,d=this.length;c").addClass(f),l("td",k).addClass(f).html(g)[0].colSpan= -oa(a),e.push(k[0]))};h(c,d);b._details&&b._details.detach();b._details=l(e);b._detailsShow&&b._details.insertAfter(b.nTr)},wb=function(a,b){var c=a.context;c.length&&(a=c[0].aoData[b!==q?b:a[0]])&&a._details&&(a._details.remove(),a._detailsShow=q,a._details=q,l(a.nTr).removeClass("dt-hasChild"),qa(c[0]))},oc=function(a,b){var c=a.context;if(c.length&&a.length){var d=c[0].aoData[a[0]];d._details&&((d._detailsShow=b)?(d._details.insertAfter(d.nTr),l(d.nTr).addClass("dt-hasChild")):(d._details.detach(), -l(d.nTr).removeClass("dt-hasChild")),F(c[0],null,"childRow",[b,a.row(a[0])]),xc(c[0]),qa(c[0]))}},xc=function(a){var b=new B(a),c=a.aoData;b.off("draw.dt.DT_details column-visibility.dt.DT_details destroy.dt.DT_details");0f){var m=l.map(d,function(p,t){return p.bVisible?t:null});return[m[m.length+f]]}return[ua(a,f)];case "name":return l.map(e,function(p,t){return p===n[1]?t:null});default:return[]}if(g.nodeName&&g._DT_CellIndex)return[g._DT_CellIndex.column]; -f=l(h).filter(g).map(function(){return l.inArray(this,h)}).toArray();if(f.length||!g.nodeName)return f;f=l(g).closest("*[data-dt-column]");return f.length?[f.data("dt-column")]:[]},a,c)};y("columns()",function(a,b){a===q?a="":l.isPlainObject(a)&&(b=a,a="");b=ub(b);var c=this.iterator("table",function(d){return zc(d,a,b)},1);c.selector.cols=a;c.selector.opts=b;return c});J("columns().header()","column().header()",function(a,b){return this.iterator("column",function(c,d){return c.aoColumns[d].nTh}, -1)});J("columns().footer()","column().footer()",function(a,b){return this.iterator("column",function(c,d){return c.aoColumns[d].nTf},1)});J("columns().data()","column().data()",function(){return this.iterator("column-rows",pc,1)});J("columns().dataSrc()","column().dataSrc()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].mData},1)});J("columns().cache()","column().cache()",function(a){return this.iterator("column-rows",function(b,c,d,e,h){return Ea(b.aoData,h,"search"=== -a?"_aFilterData":"_aSortData",c)},1)});J("columns().nodes()","column().nodes()",function(){return this.iterator("column-rows",function(a,b,c,d,e){return Ea(a.aoData,e,"anCells",b)},1)});J("columns().visible()","column().visible()",function(a,b){var c=this,d=this.iterator("column",function(e,h){if(a===q)return e.aoColumns[h].bVisible;var g=e.aoColumns,f=g[h],k=e.aoData,n;if(a!==q&&f.bVisible!==a){if(a){var m=l.inArray(!0,U(g,"bVisible"),h+1);g=0;for(n=k.length;gd;return!0};u.isDataTable=u.fnIsDataTable=function(a){var b= -l(a).get(0),c=!1;if(a instanceof u.Api)return!0;l.each(u.settings,function(d,e){d=e.nScrollHead?l("table",e.nScrollHead)[0]:null;var h=e.nScrollFoot?l("table",e.nScrollFoot)[0]:null;if(e.nTable===b||d===b||h===b)c=!0});return c};u.tables=u.fnTables=function(a){var b=!1;l.isPlainObject(a)&&(b=a.api,a=a.visible);var c=l.map(u.settings,function(d){if(!a||a&&l(d.nTable).is(":visible"))return d.nTable});return b?new B(c):c};u.camelToHungarian=P;y("$()",function(a,b){b=this.rows(b).nodes();b=l(b);return l([].concat(b.filter(a).toArray(), -b.find(a).toArray()))});l.each(["on","one","off"],function(a,b){y(b+"()",function(){var c=Array.prototype.slice.call(arguments);c[0]=l.map(c[0].split(/\s/),function(e){return e.match(/\.dt\b/)?e:e+".dt"}).join(" ");var d=l(this.tables().nodes());d[b].apply(d,c);return this})});y("clear()",function(){return this.iterator("table",function(a){Ka(a)})});y("settings()",function(){return new B(this.context,this.context)});y("init()",function(){var a=this.context;return a.length?a[0].oInit:null});y("data()", -function(){return this.iterator("table",function(a){return U(a.aoData,"_aData")}).flatten()});y("destroy()",function(a){a=a||!1;return this.iterator("table",function(b){var c=b.nTableWrapper.parentNode,d=b.oClasses,e=b.nTable,h=b.nTBody,g=b.nTHead,f=b.nTFoot,k=l(e);h=l(h);var n=l(b.nTableWrapper),m=l.map(b.aoData,function(t){return t.nTr}),p;b.bDestroying=!0;F(b,"aoDestroyCallback","destroy",[b]);a||(new B(b)).columns().visible(!0);n.off(".DT").find(":not(tbody *)").off(".DT");l(z).off(".DT-"+b.sInstance); -e!=g.parentNode&&(k.children("thead").detach(),k.append(g));f&&e!=f.parentNode&&(k.children("tfoot").detach(),k.append(f));b.aaSorting=[];b.aaSortingFixed=[];Ra(b);l(m).removeClass(b.asStripeClasses.join(" "));l("th, td",g).removeClass(d.sSortable+" "+d.sSortableAsc+" "+d.sSortableDesc+" "+d.sSortableNone);h.children().detach();h.append(m);g=a?"remove":"detach";k[g]();n[g]();!a&&c&&(c.insertBefore(e,b.nTableReinsertBefore),k.css("width",b.sDestroyWidth).removeClass(d.sTable),(p=b.asDestroyStripes.length)&& -h.children().each(function(t){l(this).addClass(b.asDestroyStripes[t%p])}));c=l.inArray(b,u.settings);-1!==c&&u.settings.splice(c,1)})});l.each(["column","row","cell"],function(a,b){y(b+"s().every()",function(c){var d=this.selector.opts,e=this;return this.iterator(b,function(h,g,f,k,n){c.call(e[b](g,"cell"===b?f:d,"cell"===b?d:q),g,f,k,n)})})});y("i18n()",function(a,b,c){var d=this.context[0];a=na(a)(d.oLanguage);a===q&&(a=b);c!==q&&l.isPlainObject(a)&&(a=a[c]!==q?a[c]:a._);return a.replace("%d",c)}); -u.version="1.11.0";u.settings=[];u.models={};u.models.oSearch={bCaseInsensitive:!0,sSearch:"",bRegex:!1,bSmart:!0,"return":!1};u.models.oRow={nTr:null,anCells:null,_aData:[],_aSortData:null,_aFilterData:null,_sFilterRow:null,_sRowStripe:"",src:null,idx:-1};u.models.oColumn={idx:null,aDataSort:null,asSorting:null,bSearchable:null,bSortable:null,bVisible:null,_sManualType:null,_bAttrSrc:!1,fnCreatedCell:null,fnGetData:null,fnSetData:null,mData:null,mRender:null,nTh:null,nTf:null,sClass:null,sContentPadding:null, -sDefaultContent:null,sName:null,sSortDataType:"std",sSortingClass:null,sSortingClassJUI:null,sTitle:null,sType:null,sWidth:null,sWidthOrig:null};u.defaults={aaData:null,aaSorting:[[0,"asc"]],aaSortingFixed:[],ajax:null,aLengthMenu:[10,25,50,100],aoColumns:null,aoColumnDefs:null,aoSearchCols:[],asStripeClasses:null,bAutoWidth:!0,bDeferRender:!1,bDestroy:!1,bFilter:!0,bInfo:!0,bLengthChange:!0,bPaginate:!0,bProcessing:!1,bRetrieve:!1,bScrollCollapse:!1,bServerSide:!1,bSort:!0,bSortMulti:!0,bSortCellsTop:!1, -bSortClasses:!0,bStateSave:!1,fnCreatedRow:null,fnDrawCallback:null,fnFooterCallback:null,fnFormatNumber:function(a){return a.toString().replace(/\B(?=(\d{3})+(?!\d))/g,this.oLanguage.sThousands)},fnHeaderCallback:null,fnInfoCallback:null,fnInitComplete:null,fnPreDrawCallback:null,fnRowCallback:null,fnServerData:null,fnServerParams:null,fnStateLoadCallback:function(a){try{return JSON.parse((-1===a.iStateDuration?sessionStorage:localStorage).getItem("DataTables_"+a.sInstance+"_"+location.pathname))}catch(b){return{}}}, -fnStateLoadParams:null,fnStateLoaded:null,fnStateSaveCallback:function(a,b){try{(-1===a.iStateDuration?sessionStorage:localStorage).setItem("DataTables_"+a.sInstance+"_"+location.pathname,JSON.stringify(b))}catch(c){}},fnStateSaveParams:null,iStateDuration:7200,iDeferLoading:null,iDisplayLength:10,iDisplayStart:0,iTabIndex:0,oClasses:{},oLanguage:{oAria:{sSortAscending:": activate to sort column ascending",sSortDescending:": activate to sort column descending"},oPaginate:{sFirst:"First",sLast:"Last", -sNext:"Next",sPrevious:"Previous"},sEmptyTable:"No data available in table",sInfo:"Showing _START_ to _END_ of _TOTAL_ entries",sInfoEmpty:"Showing 0 to 0 of 0 entries",sInfoFiltered:"(filtered from _MAX_ total entries)",sInfoPostFix:"",sDecimal:"",sThousands:",",sLengthMenu:"Show _MENU_ entries",sLoadingRecords:"Loading...",sProcessing:"Processing...",sSearch:"Search:",sSearchPlaceholder:"",sUrl:"",sZeroRecords:"No matching records found"},oSearch:l.extend({},u.models.oSearch),sAjaxDataProp:"data", -sAjaxSource:null,sDom:"lfrtip",searchDelay:null,sPaginationType:"simple_numbers",sScrollX:"",sScrollXInner:"",sScrollY:"",sServerMethod:"GET",renderer:null,rowId:"DT_RowId"};E(u.defaults);u.defaults.column={aDataSort:null,iDataSort:-1,asSorting:["asc","desc"],bSearchable:!0,bSortable:!0,bVisible:!0,fnCreatedCell:null,mData:null,mRender:null,sCellType:"td",sClass:"",sContentPadding:"",sDefaultContent:null,sName:"",sSortDataType:"std",sTitle:null,sType:null,sWidth:null};E(u.defaults.column);u.models.oSettings= -{oFeatures:{bAutoWidth:null,bDeferRender:null,bFilter:null,bInfo:null,bLengthChange:null,bPaginate:null,bProcessing:null,bServerSide:null,bSort:null,bSortMulti:null,bSortClasses:null,bStateSave:null},oScroll:{bCollapse:null,iBarWidth:0,sX:null,sXInner:null,sY:null},oLanguage:{fnInfoCallback:null},oBrowser:{bScrollOversize:!1,bScrollbarLeft:!1,bBounding:!1,barWidth:0},ajax:null,aanFeatures:[],aoData:[],aiDisplay:[],aiDisplayMaster:[],aIds:{},aoColumns:[],aoHeader:[],aoFooter:[],oPreviousSearch:{}, -aoPreSearchCols:[],aaSorting:null,aaSortingFixed:[],asStripeClasses:null,asDestroyStripes:[],sDestroyWidth:0,aoRowCallback:[],aoHeaderCallback:[],aoFooterCallback:[],aoDrawCallback:[],aoRowCreatedCallback:[],aoPreDrawCallback:[],aoInitComplete:[],aoStateSaveParams:[],aoStateLoadParams:[],aoStateLoaded:[],sTableId:"",nTable:null,nTHead:null,nTFoot:null,nTBody:null,nTableWrapper:null,bDeferLoading:!1,bInitialised:!1,aoOpenRows:[],sDom:null,searchDelay:null,sPaginationType:"two_button",iStateDuration:0, -aoStateSave:[],aoStateLoad:[],oSavedState:null,oLoadedState:null,sAjaxSource:null,sAjaxDataProp:null,jqXHR:null,json:q,oAjaxData:q,fnServerData:null,aoServerParams:[],sServerMethod:null,fnFormatNumber:null,aLengthMenu:null,iDraw:0,bDrawing:!1,iDrawError:-1,_iDisplayLength:10,_iDisplayStart:0,_iRecordsTotal:0,_iRecordsDisplay:0,oClasses:{},bFiltered:!1,bSorted:!1,bSortCellsTop:null,oInit:null,aoDestroyCallback:[],fnRecordsTotal:function(){return"ssp"==Q(this)?1*this._iRecordsTotal:this.aiDisplayMaster.length}, -fnRecordsDisplay:function(){return"ssp"==Q(this)?1*this._iRecordsDisplay:this.aiDisplay.length},fnDisplayEnd:function(){var a=this._iDisplayLength,b=this._iDisplayStart,c=b+a,d=this.aiDisplay.length,e=this.oFeatures,h=e.bPaginate;return e.bServerSide?!1===h||-1===a?b+d:Math.min(b+a,this._iRecordsDisplay):!h||c>d||-1===a?d:c},oInstance:null,sInstance:null,iTabIndex:0,nScrollHead:null,nScrollFoot:null,aLastSort:[],oPlugins:{},rowIdFn:null,rowId:null};u.ext=M={buttons:{},classes:{},build:"dt/dt-1.11.0", -errMode:"alert",feature:[],search:[],selector:{cell:[],column:[],row:[]},internal:{},legacy:{ajax:null},pager:{},renderer:{pageButton:{},header:{}},order:{},type:{detect:[],search:{},order:{}},_unique:0,fnVersionCheck:u.fnVersionCheck,iApiIndex:0,oJUIClasses:{},sVersion:u.version};l.extend(M,{afnFiltering:M.search,aTypes:M.type.detect,ofnSearch:M.type.search,oSort:M.type.order,afnSortData:M.order,aoFeatures:M.feature,oApi:M.internal,oStdClasses:M.classes,oPagination:M.pager});l.extend(u.ext.classes, -{sTable:"dataTable",sNoFooter:"no-footer",sPageButton:"paginate_button",sPageButtonActive:"current",sPageButtonDisabled:"disabled",sStripeOdd:"odd",sStripeEven:"even",sRowEmpty:"dataTables_empty",sWrapper:"dataTables_wrapper",sFilter:"dataTables_filter",sInfo:"dataTables_info",sPaging:"dataTables_paginate paging_",sLength:"dataTables_length",sProcessing:"dataTables_processing",sSortAsc:"sorting_asc",sSortDesc:"sorting_desc",sSortable:"sorting",sSortableAsc:"sorting_desc_disabled",sSortableDesc:"sorting_asc_disabled", -sSortableNone:"sorting_disabled",sSortColumn:"sorting_",sFilterInput:"",sLengthSelect:"",sScrollWrapper:"dataTables_scroll",sScrollHead:"dataTables_scrollHead",sScrollHeadInner:"dataTables_scrollHeadInner",sScrollBody:"dataTables_scrollBody",sScrollFoot:"dataTables_scrollFoot",sScrollFootInner:"dataTables_scrollFootInner",sHeaderTH:"",sFooterTH:"",sSortJUIAsc:"",sSortJUIDesc:"",sSortJUI:"",sSortJUIAscAllowed:"",sSortJUIDescAllowed:"",sSortJUIWrapper:"",sSortIcon:"",sJUIHeader:"",sJUIFooter:""});var dc= -u.ext.pager;l.extend(dc,{simple:function(a,b){return["previous","next"]},full:function(a,b){return["first","previous","next","last"]},numbers:function(a,b){return[Da(a,b)]},simple_numbers:function(a,b){return["previous",Da(a,b),"next"]},full_numbers:function(a,b){return["first","previous",Da(a,b),"next","last"]},first_last_numbers:function(a,b){return["first",Da(a,b),"last"]},_numbers:Da,numbers_length:7});l.extend(!0,u.ext.renderer,{pageButton:{_:function(a,b,c,d,e,h){var g=a.oClasses,f=a.oLanguage.oPaginate, -k=a.oLanguage.oAria.paginate||{},n,m,p=0,t=function(x,w){var r,C=g.sPageButtonDisabled,G=function(I){lb(a,I.data.action,!0)};var aa=0;for(r=w.length;aa").appendTo(x);t(O,L)}else{n=null;m=L;O=a.iTabIndex;switch(L){case "ellipsis":x.append('');break;case "first":n=f.sFirst;0===e&&(O=-1,m+=" "+C);break;case "previous":n=f.sPrevious;0===e&&(O=-1,m+=" "+C);break;case "next":n=f.sNext;if(0=== -h||e===h-1)O=-1,m+=" "+C;break;case "last":n=f.sLast;if(0===h||e===h-1)O=-1,m+=" "+C;break;default:n=a.fnFormatNumber(L+1),m=e===L?g.sPageButtonActive:""}null!==n&&(O=l("",{"class":g.sPageButton+" "+m,"aria-controls":a.sTableId,"aria-label":k[L],"data-dt-idx":p,tabindex:O,id:0===c&&"string"===typeof L?a.sTableId+"_"+L:null}).html(n).appendTo(x),ob(O,{action:L},G),p++)}}};try{var v=l(b).find(A.activeElement).data("dt-idx")}catch(x){}t(l(b).empty(),d);v!==q&&l(b).find("[data-dt-idx="+v+"]").trigger("focus")}}}); -l.extend(u.ext.type.detect,[function(a,b){b=b.oLanguage.sDecimal;return sb(a,b)?"num"+b:null},function(a,b){if(a&&!(a instanceof Date)&&!sc.test(a))return null;b=Date.parse(a);return null!==b&&!isNaN(b)||Z(a)?"date":null},function(a,b){b=b.oLanguage.sDecimal;return sb(a,b,!0)?"num-fmt"+b:null},function(a,b){b=b.oLanguage.sDecimal;return ic(a,b)?"html-num"+b:null},function(a,b){b=b.oLanguage.sDecimal;return ic(a,b,!0)?"html-num-fmt"+b:null},function(a,b){return Z(a)||"string"===typeof a&&-1!==a.indexOf("<")? -"html":null}]);l.extend(u.ext.type.search,{html:function(a){return Z(a)?a:"string"===typeof a?a.replace(fc," ").replace(Ua,""):""},string:function(a){return Z(a)?a:"string"===typeof a?a.replace(fc," "):a}});var Ta=function(a,b,c,d){if(0!==a&&(!a||"-"===a))return-Infinity;b&&(a=hc(a,b));a.replace&&(c&&(a=a.replace(c,"")),d&&(a=a.replace(d,"")));return 1*a};l.extend(M.type.order,{"date-pre":function(a){a=Date.parse(a);return isNaN(a)?-Infinity:a},"html-pre":function(a){return Z(a)?"":a.replace?a.replace(/<.*?>/g, -"").toLowerCase():a+""},"string-pre":function(a){return Z(a)?"":"string"===typeof a?a.toLowerCase():a.toString?a.toString():""},"string-asc":function(a,b){return ab?1:0},"string-desc":function(a,b){return ab?-1:0}});Wa("");l.extend(!0,u.ext.renderer,{header:{_:function(a,b,c,d){l(a.nTable).on("order.dt.DT",function(e,h,g,f){a===h&&(e=c.idx,b.removeClass(d.sSortAsc+" "+d.sSortDesc).addClass("asc"==f[e]?d.sSortAsc:"desc"==f[e]?d.sSortDesc:c.sSortingClass))})},jqueryui:function(a,b,c, -d){l("
").addClass(d.sSortJUIWrapper).append(b.contents()).append(l("").addClass(d.sSortIcon+" "+c.sSortingClassJUI)).appendTo(b);l(a.nTable).on("order.dt.DT",function(e,h,g,f){a===h&&(e=c.idx,b.removeClass(d.sSortAsc+" "+d.sSortDesc).addClass("asc"==f[e]?d.sSortAsc:"desc"==f[e]?d.sSortDesc:c.sSortingClass),b.find("span."+d.sSortIcon).removeClass(d.sSortJUIAsc+" "+d.sSortJUIDesc+" "+d.sSortJUI+" "+d.sSortJUIAscAllowed+" "+d.sSortJUIDescAllowed).addClass("asc"==f[e]?d.sSortJUIAsc:"desc"== -f[e]?d.sSortJUIDesc:c.sSortingClassJUI))})}}});var xb=function(a){return"string"===typeof a?a.replace(/&/g,"&").replace(//g,">").replace(/"/g,"""):a};u.render={number:function(a,b,c,d,e){return{display:function(h){if("number"!==typeof h&&"string"!==typeof h)return h;var g=0>h?"-":"",f=parseFloat(h);if(isNaN(f))return xb(h);f=f.toFixed(c);h=Math.abs(f);f=parseInt(h,10);h=c?b+(h-f).toFixed(c).substring(2):"";0===f&&0===parseFloat(h)&&(g="");return g+(d||"")+f.toString().replace(/\B(?=(\d{3})+(?!\d))/g, -a)+h+(e||"")}}},text:function(){return{display:xb,filter:xb}}};l.extend(u.ext.internal,{_fnExternApiFunc:ec,_fnBuildAjax:Oa,_fnAjaxUpdate:Fb,_fnAjaxParameters:Ob,_fnAjaxUpdateDraw:Pb,_fnAjaxDataSrc:Aa,_fnAddColumn:Xa,_fnColumnOptions:Ga,_fnAdjustColumnSizing:ta,_fnVisibleToColumnIndex:ua,_fnColumnIndexToVisible:va,_fnVisbleColumns:oa,_fnGetColumns:Ia,_fnColumnTypes:Za,_fnApplyColumnDefs:Cb,_fnHungarianMap:E,_fnCamelToHungarian:P,_fnLanguageCompat:ma,_fnBrowserDetect:Ab,_fnAddData:ia,_fnAddTr:Ja,_fnNodeToDataIndex:function(a, -b){return b._DT_RowIndex!==q?b._DT_RowIndex:null},_fnNodeToColumnIndex:function(a,b,c){return l.inArray(c,a.aoData[b].anCells)},_fnGetCellData:T,_fnSetCellData:Db,_fnSplitObjNotation:bb,_fnGetObjectDataFn:na,_fnSetObjectDataFn:ha,_fnGetDataMaster:cb,_fnClearTable:Ka,_fnDeleteIndex:La,_fnInvalidate:wa,_fnGetRowElements:ab,_fnCreateTr:$a,_fnBuildHead:Eb,_fnDrawHead:ya,_fnDraw:ja,_fnReDraw:ka,_fnAddOptionsHtml:Hb,_fnDetectHeader:xa,_fnGetUniqueThs:Na,_fnFeatureHtmlFilter:Jb,_fnFilterComplete:za,_fnFilterCustom:Sb, -_fnFilterColumn:Rb,_fnFilter:Qb,_fnFilterCreateSearch:hb,_fnEscapeRegex:ib,_fnFilterData:Tb,_fnFeatureHtmlInfo:Mb,_fnUpdateInfo:Wb,_fnInfoMacros:Xb,_fnInitialise:Ba,_fnInitComplete:Pa,_fnLengthChange:jb,_fnFeatureHtmlLength:Ib,_fnFeatureHtmlPaginate:Nb,_fnPageChange:lb,_fnFeatureHtmlProcessing:Kb,_fnProcessingDisplay:V,_fnFeatureHtmlTable:Lb,_fnScrollDraw:Ha,_fnApplyToChildren:ca,_fnCalculateColumnWidths:Ya,_fnThrottle:gb,_fnConvertToWidth:Yb,_fnGetWidestNode:Zb,_fnGetMaxLenString:$b,_fnStringToCss:K, -_fnSortFlatten:pa,_fnSort:Gb,_fnSortAria:bc,_fnSortListener:nb,_fnSortAttachListener:eb,_fnSortingClasses:Ra,_fnSortData:ac,_fnSaveState:qa,_fnLoadState:cc,_fnSettingsFromNode:Sa,_fnLog:da,_fnMap:X,_fnBindAction:ob,_fnCallbackReg:R,_fnCallbackFire:F,_fnLengthOverflow:kb,_fnRenderer:fb,_fnDataSource:Q,_fnRowAttributes:db,_fnExtend:pb,_fnCalculateEnd:function(){}});l.fn.dataTable=u;u.$=l;l.fn.dataTableSettings=u.settings;l.fn.dataTableExt=u.ext;l.fn.DataTable=function(a){return l(this).dataTable(a).api()}; -l.each(u,function(a,b){l.fn.DataTable[a]=b});return u}); +function Cb(a,b,c,d,e,h){var f=!1;if(c!==q){var g=c;f=!0}for(;d!==e;)a.hasOwnProperty(d)&&(g=f?b(g,a[d],d,a):a[d],f=!0,d+=h);return g}function Ya(a,b){var c=u.defaults.column,d=a.aoColumns.length;c=l.extend({},u.models.oColumn,c,{nTh:b?b:A.createElement("th"),sTitle:c.sTitle?c.sTitle:b?b.innerHTML:"",aDataSort:c.aDataSort?c.aDataSort:[d],mData:c.mData?c.mData:d,idx:d});a.aoColumns.push(c);c=a.aoPreSearchCols;c[d]=l.extend({},u.models.oSearch,c[d]);Ga(a,d,l(b).data())}function Ga(a,b,c){b=a.aoColumns[b]; +var d=a.oClasses,e=l(b.nTh);if(!b.sWidthOrig){b.sWidthOrig=e.attr("width")||null;var h=(e.attr("style")||"").match(/width:\s*(\d+[pxem%]+)/);h&&(b.sWidthOrig=h[1])}c!==q&&null!==c&&(Ab(c),P(u.defaults.column,c,!0),c.mDataProp===q||c.mData||(c.mData=c.mDataProp),c.sType&&(b._sManualType=c.sType),c.className&&!c.sClass&&(c.sClass=c.className),c.sClass&&e.addClass(c.sClass),l.extend(b,c),X(b,c,"sWidth","sWidthOrig"),c.iDataSort!==q&&(b.aDataSort=[c.iDataSort]),X(b,c,"aDataSort"));var f=b.mData,g=na(f), +k=b.mRender?na(b.mRender):null;c=function(m){return"string"===typeof m&&-1!==m.indexOf("@")};b._bAttrSrc=l.isPlainObject(f)&&(c(f.sort)||c(f.type)||c(f.filter));b._setter=null;b.fnGetData=function(m,n,p){var t=g(m,n,q,p);return k&&n?k(t,n,m,p):t};b.fnSetData=function(m,n,p){return ha(f)(m,n,p)};"number"!==typeof f&&(a._rowReadObject=!0);a.oFeatures.bSort||(b.bSortable=!1,e.addClass(d.sSortableNone));a=-1!==l.inArray("asc",b.asSorting);c=-1!==l.inArray("desc",b.asSorting);b.bSortable&&(a||c)?a&&!c? +(b.sSortingClass=d.sSortableAsc,b.sSortingClassJUI=d.sSortJUIAscAllowed):!a&&c?(b.sSortingClass=d.sSortableDesc,b.sSortingClassJUI=d.sSortJUIDescAllowed):(b.sSortingClass=d.sSortable,b.sSortingClassJUI=d.sSortJUI):(b.sSortingClass=d.sSortableNone,b.sSortingClassJUI="")}function ta(a){if(!1!==a.oFeatures.bAutoWidth){var b=a.aoColumns;Za(a);for(var c=0,d=b.length;cm[n])d(g.length+m[n],k);else if("string"===typeof m[n]){var p=0;for(f=g.length;pb&&a[e]--; -1!=d&&c===q&&a.splice(d,1)}function wa(a,b,c,d){var e=a.aoData[b],h,f=function(k,m){for(;k.childNodes.length;)k.removeChild(k.firstChild);k.innerHTML=T(a,b,m,"display")};if("dom"!==c&&(c&&"auto"!==c||"dom"!==e.src)){var g=e.anCells;if(g)if(d!==q)f(g[d],d);else for(c=0,h=g.length;c").appendTo(d));var k=0;for(b=g.length;k=a.fnRecordsDisplay()?0:d,a.iInitDisplayStart=-1);c=F(a,"aoPreDrawCallback","preDraw",[a]);if(-1!==l.inArray(!1,c))V(a,!1);else{c=[];var e=0;d=a.asStripeClasses;var h=d.length,f=a.oLanguage,g="ssp"==Q(a),k=a.aiDisplay,m=a._iDisplayStart,n=a.fnDisplayEnd();a.bDrawing=!0;if(a.bDeferLoading)a.bDeferLoading=!1,a.iDraw++,V(a,!1);else if(!g)a.iDraw++;else if(!a.bDestroying&&!b){Gb(a);return}if(0!==k.length)for(b=g?a.aoData.length:n,f=g?0:m;f",{"class":h?d[0]:""}).append(l("
").insertAfter(H));r.nTBody=ea[0];H=t.children("tfoot");0===H.length&&0").appendTo(t));0===H.length||0===H.children().length?t.addClass(C.sNoFooter):0/g,uc=/^\d{2,4}[\.\/\-]\d{1,2}[\.\/\-]\d{1,2}([T ]{1}\d{1,2}[:\.]\d{2}([\.:]\d{2})?)?$/,vc=/(\/|\.|\*|\+|\?|\||\(|\)|\[|\]|\{|\}|\\|\$|\^|\-)/g,rb=/['\u00A0,$£€¥%\u2009\u202F\u20BD\u20a9\u20BArfkɃΞ]/gi,Z=function(a){return a&&!0!==a&&"-"!==a?!1:!0},hc= +function(a){var b=parseInt(a,10);return!isNaN(b)&&isFinite(a)?b:null},ic=function(a,b){sb[b]||(sb[b]=new RegExp(jb(b),"g"));return"string"===typeof a&&"."!==b?a.replace(/\./g,"").replace(sb[b],"."):a},tb=function(a,b,c){var d="string"===typeof a;if(Z(a))return!0;b&&d&&(a=ic(a,b));c&&d&&(a=a.replace(rb,""));return!isNaN(parseFloat(a))&&isFinite(a)},jc=function(a,b,c){return Z(a)?!0:Z(a)||"string"===typeof a?tb(a.replace(Va,""),b,c)?!0:null:null},U=function(a,b,c){var d=[],e=0,h=a.length;if(c!==q)for(;e< +h;e++)a[e]&&a[e][b]&&d.push(a[e][b][c]);else for(;ea.length)){var b=a.slice().sort();for(var c=b[0],d=1,e=b.length;d< +e;d++){if(b[d]===c){b=!1;break a}c=b[d]}}b=!0}if(b)return a.slice();b=[];e=a.length;var h,f=0;d=0;a:for(;d")[0],sc=Qa.textContent!==q,tc=/<.*?>/g,hb=u.util.throttle,nc=[],N=Array.prototype,wc=function(a){var b,c=u.settings,d=l.map(c,function(h,f){return h.nTable});if(a){if(a.nTable&&a.oApi)return[a];if(a.nodeName&&"table"===a.nodeName.toLowerCase()){var e= +l.inArray(a,d);return-1!==e?[c[e]]:null}if(a&&"function"===typeof a.settings)return a.settings().toArray();"string"===typeof a?b=l(a):a instanceof l&&(b=a)}else return[];if(b)return b.map(function(h){e=l.inArray(this,d);return-1!==e?c[e]:null}).toArray()};var B=function(a,b){if(!(this instanceof B))return new B(a,b);var c=[],d=function(f){(f=wc(f))&&c.push.apply(c,f)};if(Array.isArray(a))for(var e=0,h=a.length;ea?new B(b[a],this[a]):null},filter:function(a){var b=[];if(N.filter)b=N.filter.call(this,a,this);else for(var c=0,d=this.length;c").addClass(g),l("td",k).addClass(g).html(f)[0].colSpan=oa(a),e.push(k[0]))};h(c,d);b._details&&b._details.detach();b._details=l(e);b._detailsShow&&b._details.insertAfter(b.nTr)},xb=function(a,b){var c=a.context;c.length&&(a=c[0].aoData[b!==q?b:a[0]])&&a._details&&(a._details.remove(),a._detailsShow=q,a._details= +q,l(a.nTr).removeClass("dt-hasChild"),qa(c[0]))},qc=function(a,b){var c=a.context;if(c.length&&a.length){var d=c[0].aoData[a[0]];d._details&&((d._detailsShow=b)?(d._details.insertAfter(d.nTr),l(d.nTr).addClass("dt-hasChild")):(d._details.detach(),l(d.nTr).removeClass("dt-hasChild")),F(c[0],null,"childRow",[b,a.row(a[0])]),zc(c[0]),qa(c[0]))}},zc=function(a){var b=new B(a),c=a.aoData;b.off("draw.dt.DT_details column-visibility.dt.DT_details destroy.dt.DT_details");0g){var n=l.map(d,function(p,t){return p.bVisible?t:null});return[n[n.length+g]]}return[ua(a,g)];case "name":return l.map(e,function(p,t){return p===m[1]?t:null});default:return[]}if(f.nodeName&&f._DT_CellIndex)return[f._DT_CellIndex.column];g=l(h).filter(f).map(function(){return l.inArray(this,h)}).toArray();if(g.length||!f.nodeName)return g;g=l(f).closest("*[data-dt-column]");return g.length?[g.data("dt-column")]:[]},a,c)};y("columns()",function(a,b){a===q?a="":l.isPlainObject(a)&&(b=a, +a="");b=vb(b);var c=this.iterator("table",function(d){return Bc(d,a,b)},1);c.selector.cols=a;c.selector.opts=b;return c});J("columns().header()","column().header()",function(a,b){return this.iterator("column",function(c,d){return c.aoColumns[d].nTh},1)});J("columns().footer()","column().footer()",function(a,b){return this.iterator("column",function(c,d){return c.aoColumns[d].nTf},1)});J("columns().data()","column().data()",function(){return this.iterator("column-rows",rc,1)});J("columns().dataSrc()", +"column().dataSrc()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].mData},1)});J("columns().cache()","column().cache()",function(a){return this.iterator("column-rows",function(b,c,d,e,h){return Ea(b.aoData,h,"search"===a?"_aFilterData":"_aSortData",c)},1)});J("columns().nodes()","column().nodes()",function(){return this.iterator("column-rows",function(a,b,c,d,e){return Ea(a.aoData,e,"anCells",b)},1)});J("columns().visible()","column().visible()",function(a,b){var c= +this,d=this.iterator("column",function(e,h){if(a===q)return e.aoColumns[h].bVisible;var f=e.aoColumns,g=f[h],k=e.aoData,m;if(a!==q&&g.bVisible!==a){if(a){var n=l.inArray(!0,U(f,"bVisible"),h+1);f=0;for(m=k.length;fd;return!0};u.isDataTable=u.fnIsDataTable=function(a){var b=l(a).get(0),c=!1;if(a instanceof u.Api)return!0;l.each(u.settings,function(d,e){d=e.nScrollHead?l("table",e.nScrollHead)[0]:null;var h=e.nScrollFoot?l("table",e.nScrollFoot)[0]:null;if(e.nTable===b||d===b||h===b)c=!0});return c};u.tables=u.fnTables=function(a){var b= +!1;l.isPlainObject(a)&&(b=a.api,a=a.visible);var c=l.map(u.settings,function(d){if(!a||a&&l(d.nTable).is(":visible"))return d.nTable});return b?new B(c):c};u.camelToHungarian=P;y("$()",function(a,b){b=this.rows(b).nodes();b=l(b);return l([].concat(b.filter(a).toArray(),b.find(a).toArray()))});l.each(["on","one","off"],function(a,b){y(b+"()",function(){var c=Array.prototype.slice.call(arguments);c[0]=l.map(c[0].split(/\s/),function(e){return e.match(/\.dt\b/)?e:e+".dt"}).join(" ");var d=l(this.tables().nodes()); +d[b].apply(d,c);return this})});y("clear()",function(){return this.iterator("table",function(a){Ka(a)})});y("settings()",function(){return new B(this.context,this.context)});y("init()",function(){var a=this.context;return a.length?a[0].oInit:null});y("data()",function(){return this.iterator("table",function(a){return U(a.aoData,"_aData")}).flatten()});y("destroy()",function(a){a=a||!1;return this.iterator("table",function(b){var c=b.nTableWrapper.parentNode,d=b.oClasses,e=b.nTable,h=b.nTBody,f=b.nTHead, +g=b.nTFoot,k=l(e);h=l(h);var m=l(b.nTableWrapper),n=l.map(b.aoData,function(t){return t.nTr}),p;b.bDestroying=!0;F(b,"aoDestroyCallback","destroy",[b]);a||(new B(b)).columns().visible(!0);m.off(".DT").find(":not(tbody *)").off(".DT");l(z).off(".DT-"+b.sInstance);e!=f.parentNode&&(k.children("thead").detach(),k.append(f));g&&e!=g.parentNode&&(k.children("tfoot").detach(),k.append(g));b.aaSorting=[];b.aaSortingFixed=[];Sa(b);l(n).removeClass(b.asStripeClasses.join(" "));l("th, td",f).removeClass(d.sSortable+ +" "+d.sSortableAsc+" "+d.sSortableDesc+" "+d.sSortableNone);h.children().detach();h.append(n);f=a?"remove":"detach";k[f]();m[f]();!a&&c&&(c.insertBefore(e,b.nTableReinsertBefore),k.css("width",b.sDestroyWidth).removeClass(d.sTable),(p=b.asDestroyStripes.length)&&h.children().each(function(t){l(this).addClass(b.asDestroyStripes[t%p])}));c=l.inArray(b,u.settings);-1!==c&&u.settings.splice(c,1)})});l.each(["column","row","cell"],function(a,b){y(b+"s().every()",function(c){var d=this.selector.opts,e= +this;return this.iterator(b,function(h,f,g,k,m){c.call(e[b](f,"cell"===b?g:d,"cell"===b?d:q),f,g,k,m)})})});y("i18n()",function(a,b,c){var d=this.context[0];a=na(a)(d.oLanguage);a===q&&(a=b);c!==q&&l.isPlainObject(a)&&(a=a[c]!==q?a[c]:a._);return a.replace("%d",c)});u.version="1.11.4";u.settings=[];u.models={};u.models.oSearch={bCaseInsensitive:!0,sSearch:"",bRegex:!1,bSmart:!0,"return":!1};u.models.oRow={nTr:null,anCells:null,_aData:[],_aSortData:null,_aFilterData:null,_sFilterRow:null,_sRowStripe:"", +src:null,idx:-1};u.models.oColumn={idx:null,aDataSort:null,asSorting:null,bSearchable:null,bSortable:null,bVisible:null,_sManualType:null,_bAttrSrc:!1,fnCreatedCell:null,fnGetData:null,fnSetData:null,mData:null,mRender:null,nTh:null,nTf:null,sClass:null,sContentPadding:null,sDefaultContent:null,sName:null,sSortDataType:"std",sSortingClass:null,sSortingClassJUI:null,sTitle:null,sType:null,sWidth:null,sWidthOrig:null};u.defaults={aaData:null,aaSorting:[[0,"asc"]],aaSortingFixed:[],ajax:null,aLengthMenu:[10, +25,50,100],aoColumns:null,aoColumnDefs:null,aoSearchCols:[],asStripeClasses:null,bAutoWidth:!0,bDeferRender:!1,bDestroy:!1,bFilter:!0,bInfo:!0,bLengthChange:!0,bPaginate:!0,bProcessing:!1,bRetrieve:!1,bScrollCollapse:!1,bServerSide:!1,bSort:!0,bSortMulti:!0,bSortCellsTop:!1,bSortClasses:!0,bStateSave:!1,fnCreatedRow:null,fnDrawCallback:null,fnFooterCallback:null,fnFormatNumber:function(a){return a.toString().replace(/\B(?=(\d{3})+(?!\d))/g,this.oLanguage.sThousands)},fnHeaderCallback:null,fnInfoCallback:null, +fnInitComplete:null,fnPreDrawCallback:null,fnRowCallback:null,fnServerData:null,fnServerParams:null,fnStateLoadCallback:function(a){try{return JSON.parse((-1===a.iStateDuration?sessionStorage:localStorage).getItem("DataTables_"+a.sInstance+"_"+location.pathname))}catch(b){return{}}},fnStateLoadParams:null,fnStateLoaded:null,fnStateSaveCallback:function(a,b){try{(-1===a.iStateDuration?sessionStorage:localStorage).setItem("DataTables_"+a.sInstance+"_"+location.pathname,JSON.stringify(b))}catch(c){}}, +fnStateSaveParams:null,iStateDuration:7200,iDeferLoading:null,iDisplayLength:10,iDisplayStart:0,iTabIndex:0,oClasses:{},oLanguage:{oAria:{sSortAscending:": activate to sort column ascending",sSortDescending:": activate to sort column descending"},oPaginate:{sFirst:"First",sLast:"Last",sNext:"Next",sPrevious:"Previous"},sEmptyTable:"No data available in table",sInfo:"Showing _START_ to _END_ of _TOTAL_ entries",sInfoEmpty:"Showing 0 to 0 of 0 entries",sInfoFiltered:"(filtered from _MAX_ total entries)", +sInfoPostFix:"",sDecimal:"",sThousands:",",sLengthMenu:"Show _MENU_ entries",sLoadingRecords:"Loading...",sProcessing:"Processing...",sSearch:"Search:",sSearchPlaceholder:"",sUrl:"",sZeroRecords:"No matching records found"},oSearch:l.extend({},u.models.oSearch),sAjaxDataProp:"data",sAjaxSource:null,sDom:"lfrtip",searchDelay:null,sPaginationType:"simple_numbers",sScrollX:"",sScrollXInner:"",sScrollY:"",sServerMethod:"GET",renderer:null,rowId:"DT_RowId"};E(u.defaults);u.defaults.column={aDataSort:null, +iDataSort:-1,asSorting:["asc","desc"],bSearchable:!0,bSortable:!0,bVisible:!0,fnCreatedCell:null,mData:null,mRender:null,sCellType:"td",sClass:"",sContentPadding:"",sDefaultContent:null,sName:"",sSortDataType:"std",sTitle:null,sType:null,sWidth:null};E(u.defaults.column);u.models.oSettings={oFeatures:{bAutoWidth:null,bDeferRender:null,bFilter:null,bInfo:null,bLengthChange:null,bPaginate:null,bProcessing:null,bServerSide:null,bSort:null,bSortMulti:null,bSortClasses:null,bStateSave:null},oScroll:{bCollapse:null, +iBarWidth:0,sX:null,sXInner:null,sY:null},oLanguage:{fnInfoCallback:null},oBrowser:{bScrollOversize:!1,bScrollbarLeft:!1,bBounding:!1,barWidth:0},ajax:null,aanFeatures:[],aoData:[],aiDisplay:[],aiDisplayMaster:[],aIds:{},aoColumns:[],aoHeader:[],aoFooter:[],oPreviousSearch:{},aoPreSearchCols:[],aaSorting:null,aaSortingFixed:[],asStripeClasses:null,asDestroyStripes:[],sDestroyWidth:0,aoRowCallback:[],aoHeaderCallback:[],aoFooterCallback:[],aoDrawCallback:[],aoRowCreatedCallback:[],aoPreDrawCallback:[], +aoInitComplete:[],aoStateSaveParams:[],aoStateLoadParams:[],aoStateLoaded:[],sTableId:"",nTable:null,nTHead:null,nTFoot:null,nTBody:null,nTableWrapper:null,bDeferLoading:!1,bInitialised:!1,aoOpenRows:[],sDom:null,searchDelay:null,sPaginationType:"two_button",iStateDuration:0,aoStateSave:[],aoStateLoad:[],oSavedState:null,oLoadedState:null,sAjaxSource:null,sAjaxDataProp:null,jqXHR:null,json:q,oAjaxData:q,fnServerData:null,aoServerParams:[],sServerMethod:null,fnFormatNumber:null,aLengthMenu:null,iDraw:0, +bDrawing:!1,iDrawError:-1,_iDisplayLength:10,_iDisplayStart:0,_iRecordsTotal:0,_iRecordsDisplay:0,oClasses:{},bFiltered:!1,bSorted:!1,bSortCellsTop:null,oInit:null,aoDestroyCallback:[],fnRecordsTotal:function(){return"ssp"==Q(this)?1*this._iRecordsTotal:this.aiDisplayMaster.length},fnRecordsDisplay:function(){return"ssp"==Q(this)?1*this._iRecordsDisplay:this.aiDisplay.length},fnDisplayEnd:function(){var a=this._iDisplayLength,b=this._iDisplayStart,c=b+a,d=this.aiDisplay.length,e=this.oFeatures,h= +e.bPaginate;return e.bServerSide?!1===h||-1===a?b+d:Math.min(b+a,this._iRecordsDisplay):!h||c>d||-1===a?d:c},oInstance:null,sInstance:null,iTabIndex:0,nScrollHead:null,nScrollFoot:null,aLastSort:[],oPlugins:{},rowIdFn:null,rowId:null};u.ext=M={buttons:{},classes:{},build:"dt/dt-1.11.4/cr-1.5.5/fh-3.2.1/r-2.2.9",errMode:"alert",feature:[],search:[],selector:{cell:[],column:[],row:[]},internal:{},legacy:{ajax:null},pager:{},renderer:{pageButton:{},header:{}},order:{},type:{detect:[],search:{},order:{}},_unique:0,fnVersionCheck:u.fnVersionCheck, +iApiIndex:0,oJUIClasses:{},sVersion:u.version};l.extend(M,{afnFiltering:M.search,aTypes:M.type.detect,ofnSearch:M.type.search,oSort:M.type.order,afnSortData:M.order,aoFeatures:M.feature,oApi:M.internal,oStdClasses:M.classes,oPagination:M.pager});l.extend(u.ext.classes,{sTable:"dataTable",sNoFooter:"no-footer",sPageButton:"paginate_button",sPageButtonActive:"current",sPageButtonDisabled:"disabled",sStripeOdd:"odd",sStripeEven:"even",sRowEmpty:"dataTables_empty",sWrapper:"dataTables_wrapper",sFilter:"dataTables_filter", +sInfo:"dataTables_info",sPaging:"dataTables_paginate paging_",sLength:"dataTables_length",sProcessing:"dataTables_processing",sSortAsc:"sorting_asc",sSortDesc:"sorting_desc",sSortable:"sorting",sSortableAsc:"sorting_desc_disabled",sSortableDesc:"sorting_asc_disabled",sSortableNone:"sorting_disabled",sSortColumn:"sorting_",sFilterInput:"",sLengthSelect:"",sScrollWrapper:"dataTables_scroll",sScrollHead:"dataTables_scrollHead",sScrollHeadInner:"dataTables_scrollHeadInner",sScrollBody:"dataTables_scrollBody", +sScrollFoot:"dataTables_scrollFoot",sScrollFootInner:"dataTables_scrollFootInner",sHeaderTH:"",sFooterTH:"",sSortJUIAsc:"",sSortJUIDesc:"",sSortJUI:"",sSortJUIAscAllowed:"",sSortJUIDescAllowed:"",sSortJUIWrapper:"",sSortIcon:"",sJUIHeader:"",sJUIFooter:""});var ec=u.ext.pager;l.extend(ec,{simple:function(a,b){return["previous","next"]},full:function(a,b){return["first","previous","next","last"]},numbers:function(a,b){return[Da(a,b)]},simple_numbers:function(a,b){return["previous",Da(a,b),"next"]}, +full_numbers:function(a,b){return["first","previous",Da(a,b),"next","last"]},first_last_numbers:function(a,b){return["first",Da(a,b),"last"]},_numbers:Da,numbers_length:7});l.extend(!0,u.ext.renderer,{pageButton:{_:function(a,b,c,d,e,h){var f=a.oClasses,g=a.oLanguage.oPaginate,k=a.oLanguage.oAria.paginate||{},m,n,p=0,t=function(x,w){var r,C=f.sPageButtonDisabled,G=function(I){Ra(a,I.data.action,!0)};var aa=0;for(r=w.length;aa").appendTo(x); +t(O,L)}else{m=null;n=L;O=a.iTabIndex;switch(L){case "ellipsis":x.append('');break;case "first":m=g.sFirst;0===e&&(O=-1,n+=" "+C);break;case "previous":m=g.sPrevious;0===e&&(O=-1,n+=" "+C);break;case "next":m=g.sNext;if(0===h||e===h-1)O=-1,n+=" "+C;break;case "last":m=g.sLast;if(0===h||e===h-1)O=-1,n+=" "+C;break;default:m=a.fnFormatNumber(L+1),n=e===L?f.sPageButtonActive:""}null!==m&&(O=l("",{"class":f.sPageButton+" "+n,"aria-controls":a.sTableId,"aria-label":k[L], +"data-dt-idx":p,tabindex:O,id:0===c&&"string"===typeof L?a.sTableId+"_"+L:null}).html(m).appendTo(x),ob(O,{action:L},G),p++)}}};try{var v=l(b).find(A.activeElement).data("dt-idx")}catch(x){}t(l(b).empty(),d);v!==q&&l(b).find("[data-dt-idx="+v+"]").trigger("focus")}}});l.extend(u.ext.type.detect,[function(a,b){b=b.oLanguage.sDecimal;return tb(a,b)?"num"+b:null},function(a,b){if(a&&!(a instanceof Date)&&!uc.test(a))return null;b=Date.parse(a);return null!==b&&!isNaN(b)||Z(a)?"date":null},function(a, +b){b=b.oLanguage.sDecimal;return tb(a,b,!0)?"num-fmt"+b:null},function(a,b){b=b.oLanguage.sDecimal;return jc(a,b)?"html-num"+b:null},function(a,b){b=b.oLanguage.sDecimal;return jc(a,b,!0)?"html-num-fmt"+b:null},function(a,b){return Z(a)||"string"===typeof a&&-1!==a.indexOf("<")?"html":null}]);l.extend(u.ext.type.search,{html:function(a){return Z(a)?a:"string"===typeof a?a.replace(gc," ").replace(Va,""):""},string:function(a){return Z(a)?a:"string"===typeof a?a.replace(gc," "):a}});var Ua=function(a, +b,c,d){if(0!==a&&(!a||"-"===a))return-Infinity;b&&(a=ic(a,b));a.replace&&(c&&(a=a.replace(c,"")),d&&(a=a.replace(d,"")));return 1*a};l.extend(M.type.order,{"date-pre":function(a){a=Date.parse(a);return isNaN(a)?-Infinity:a},"html-pre":function(a){return Z(a)?"":a.replace?a.replace(/<.*?>/g,"").toLowerCase():a+""},"string-pre":function(a){return Z(a)?"":"string"===typeof a?a.toLowerCase():a.toString?a.toString():""},"string-asc":function(a,b){return ab?1:0},"string-desc":function(a,b){return a< +b?1:a>b?-1:0}});Xa("");l.extend(!0,u.ext.renderer,{header:{_:function(a,b,c,d){l(a.nTable).on("order.dt.DT",function(e,h,f,g){a===h&&(e=c.idx,b.removeClass(d.sSortAsc+" "+d.sSortDesc).addClass("asc"==g[e]?d.sSortAsc:"desc"==g[e]?d.sSortDesc:c.sSortingClass))})},jqueryui:function(a,b,c,d){l("
").addClass(d.sSortJUIWrapper).append(b.contents()).append(l("").addClass(d.sSortIcon+" "+c.sSortingClassJUI)).appendTo(b);l(a.nTable).on("order.dt.DT",function(e,h,f,g){a===h&&(e=c.idx,b.removeClass(d.sSortAsc+ +" "+d.sSortDesc).addClass("asc"==g[e]?d.sSortAsc:"desc"==g[e]?d.sSortDesc:c.sSortingClass),b.find("span."+d.sSortIcon).removeClass(d.sSortJUIAsc+" "+d.sSortJUIDesc+" "+d.sSortJUI+" "+d.sSortJUIAscAllowed+" "+d.sSortJUIDescAllowed).addClass("asc"==g[e]?d.sSortJUIAsc:"desc"==g[e]?d.sSortJUIDesc:c.sSortingClassJUI))})}}});var yb=function(a){Array.isArray(a)&&(a=a.join(","));return"string"===typeof a?a.replace(/&/g,"&").replace(//g,">").replace(/"/g,"""):a};u.render= +{number:function(a,b,c,d,e){return{display:function(h){if("number"!==typeof h&&"string"!==typeof h)return h;var f=0>h?"-":"",g=parseFloat(h);if(isNaN(g))return yb(h);g=g.toFixed(c);h=Math.abs(g);g=parseInt(h,10);h=c?b+(h-g).toFixed(c).substring(2):"";0===g&&0===parseFloat(h)&&(f="");return f+(d||"")+g.toString().replace(/\B(?=(\d{3})+(?!\d))/g,a)+h+(e||"")}}},text:function(){return{display:yb,filter:yb}}};l.extend(u.ext.internal,{_fnExternApiFunc:fc,_fnBuildAjax:Oa,_fnAjaxUpdate:Gb,_fnAjaxParameters:Pb, +_fnAjaxUpdateDraw:Qb,_fnAjaxDataSrc:Aa,_fnAddColumn:Ya,_fnColumnOptions:Ga,_fnAdjustColumnSizing:ta,_fnVisibleToColumnIndex:ua,_fnColumnIndexToVisible:va,_fnVisbleColumns:oa,_fnGetColumns:Ia,_fnColumnTypes:$a,_fnApplyColumnDefs:Db,_fnHungarianMap:E,_fnCamelToHungarian:P,_fnLanguageCompat:ma,_fnBrowserDetect:Bb,_fnAddData:ia,_fnAddTr:Ja,_fnNodeToDataIndex:function(a,b){return b._DT_RowIndex!==q?b._DT_RowIndex:null},_fnNodeToColumnIndex:function(a,b,c){return l.inArray(c,a.aoData[b].anCells)},_fnGetCellData:T, +_fnSetCellData:Eb,_fnSplitObjNotation:cb,_fnGetObjectDataFn:na,_fnSetObjectDataFn:ha,_fnGetDataMaster:db,_fnClearTable:Ka,_fnDeleteIndex:La,_fnInvalidate:wa,_fnGetRowElements:bb,_fnCreateTr:ab,_fnBuildHead:Fb,_fnDrawHead:ya,_fnDraw:ja,_fnReDraw:ka,_fnAddOptionsHtml:Ib,_fnDetectHeader:xa,_fnGetUniqueThs:Na,_fnFeatureHtmlFilter:Kb,_fnFilterComplete:za,_fnFilterCustom:Tb,_fnFilterColumn:Sb,_fnFilter:Rb,_fnFilterCreateSearch:ib,_fnEscapeRegex:jb,_fnFilterData:Ub,_fnFeatureHtmlInfo:Nb,_fnUpdateInfo:Xb, +_fnInfoMacros:Yb,_fnInitialise:Ba,_fnInitComplete:Pa,_fnLengthChange:kb,_fnFeatureHtmlLength:Jb,_fnFeatureHtmlPaginate:Ob,_fnPageChange:Ra,_fnFeatureHtmlProcessing:Lb,_fnProcessingDisplay:V,_fnFeatureHtmlTable:Mb,_fnScrollDraw:Ha,_fnApplyToChildren:ca,_fnCalculateColumnWidths:Za,_fnThrottle:hb,_fnConvertToWidth:Zb,_fnGetWidestNode:$b,_fnGetMaxLenString:ac,_fnStringToCss:K,_fnSortFlatten:pa,_fnSort:Hb,_fnSortAria:cc,_fnSortListener:nb,_fnSortAttachListener:fb,_fnSortingClasses:Sa,_fnSortData:bc,_fnSaveState:qa, +_fnLoadState:dc,_fnImplementState:pb,_fnSettingsFromNode:Ta,_fnLog:da,_fnMap:X,_fnBindAction:ob,_fnCallbackReg:R,_fnCallbackFire:F,_fnLengthOverflow:lb,_fnRenderer:gb,_fnDataSource:Q,_fnRowAttributes:eb,_fnExtend:qb,_fnCalculateEnd:function(){}});l.fn.dataTable=u;u.$=l;l.fn.dataTableSettings=u.settings;l.fn.dataTableExt=u.ext;l.fn.DataTable=function(a){return l(this).dataTable(a).api()};l.each(u,function(a,b){l.fn.DataTable[a]=b});return u}); /*! @@ -205,3 +206,178 @@ l.each(u,function(a,b){l.fn.DataTable[a]=b});return u}); (function(c){"function"===typeof define&&define.amd?define(["jquery","datatables.net"],function(a){return c(a,window,document)}):"object"===typeof exports?module.exports=function(a,b){a||(a=window);b&&b.fn.dataTable||(b=require("datatables.net")(a,b).$);return c(b,a,a.document)}:c(jQuery,window,document)})(function(c,a,b,d){return c.fn.dataTable}); +/*! + Copyright 2010-2021 SpryMedia Ltd. + + This source file is free software, available under the following license: + MIT license - http://datatables.net/license/mit + + This source file is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details. + + For details please refer to: http://www.datatables.net + ColReorder 1.5.5 + ©2010-2021 SpryMedia Ltd - datatables.net/license +*/ +(function(e){"function"===typeof define&&define.amd?define(["jquery","datatables.net"],function(u){return e(u,window,document)}):"object"===typeof exports?module.exports=function(u,t){u||(u=window);t&&t.fn.dataTable||(t=require("datatables.net")(u,t).$);return e(t,u,u.document)}:e(jQuery,window,document)})(function(e,u,t,z){function y(a){for(var b=[],c=0,f=a.length;cb||b>=k)this.oApi._fnLog(a,1,"ColReorder 'from' index is out of bounds: "+b);else if(0>c||c>=k)this.oApi._fnLog(a,1,"ColReorder 'to' index is out of bounds: "+ +c);else{var m=[];var d=0;for(g=k;dthis.s.fixed-1&&fMath.pow(Math.pow(this._fnCursorPosition(a,"pageX")-this.s.mouse.startX,2)+Math.pow(this._fnCursorPosition(a,"pageY")-this.s.mouse.startY,2),.5))return;this._fnCreateDragNode()}this.dom.drag.css({left:this._fnCursorPosition(a,"pageX")-this.s.mouse.offsetX,top:this._fnCursorPosition(a,"pageY")-this.s.mouse.offsetY});var c=this.s.mouse.toIndex;a=this._fnCursorPosition(a,"pageX");for(var f=function(d){for(;0<= +d;){d--;if(0>=d)return null;if(b.s.aoTargets[d+1].x!==b.s.aoTargets[d].x)return b.s.aoTargets[d]}},h=function(){for(var d=0;dp){m= +k;break}}m?(this.dom.pointer.css("left",m.x),this.s.mouse.toIndex=m.to):(this.dom.pointer.css("left",g().x),this.s.mouse.toIndex=g().to);this.s.init.bRealtime&&c!==this.s.mouse.toIndex&&(this.s.dt.oInstance.fnColReorder(this.s.mouse.fromIndex,this.s.mouse.toIndex),this.s.mouse.fromIndex=this.s.mouse.toIndex,""===this.s.dt.oScroll.sX&&""===this.s.dt.oScroll.sY||this.s.dt.oInstance.fnAdjustColumnSizing(!1),this._fnRegions())},_fnMouseUp:function(a){e(t).off(".ColReorder");null!==this.dom.drag&&(this.dom.drag.remove(), +this.dom.pointer.remove(),this.dom.drag=null,this.dom.pointer=null,this.s.dt.oInstance.fnColReorder(this.s.mouse.fromIndex,this.s.mouse.toIndex,!0),this._fnSetColumnIndexes(),""===this.s.dt.oScroll.sX&&""===this.s.dt.oScroll.sY||this.s.dt.oInstance.fnAdjustColumnSizing(!1),this.s.dt.oInstance.oApi._fnSaveState(this.s.dt),null!==this.s.reorderCallback&&this.s.reorderCallback.call(this))},_fnRegions:function(){var a=this.s.dt.aoColumns,b=this._fnIsLtr();this.s.aoTargets.splice(0,this.s.aoTargets.length); +var c=e(this.s.dt.nTable).offset().left,f=[];e.each(a,function(l,k){if(k.bVisible&&"none"!==k.nTh.style.display){k=e(k.nTh);var p=k.offset().left;b&&(p+=k.outerWidth());f.push({index:l,bound:p});c=p}else f.push({index:l,bound:c})});var h=f[0];a=e(a[h.index].nTh).outerWidth();this.s.aoTargets.push({to:0,x:h.bound-a});for(h=0;h
").addClass("DTCR_pointer").css({position:"absolute",top:a?e(e(this.s.dt.nScrollBody).parent()).offset().top:e(this.s.dt.nTable).offset().top,height:a?e(e(this.s.dt.nScrollBody).parent()).height():e(this.s.dt.nTable).height()}).appendTo("body")},_fnSetColumnIndexes:function(){e.each(this.s.dt.aoColumns,function(a,b){e(b.nTh).attr("data-column-index",a)})},_fnCursorPosition:function(a,b){return-1!==a.type.indexOf("touch")?a.originalEvent.touches[0][b]:a[b]},_fnIsLtr:function(){return"rtl"!== +e(this.s.dt.nTable).css("direction")}});r.defaults={aiOrder:null,bEnable:!0,bRealtime:!0,iFixedColumnsLeft:0,iFixedColumnsRight:0,fnReorderCallback:null};r.version="1.5.5";e.fn.dataTable.ColReorder=r;e.fn.DataTable.ColReorder=r;"function"==typeof e.fn.dataTable&&"function"==typeof e.fn.dataTableExt.fnVersionCheck&&e.fn.dataTableExt.fnVersionCheck("1.10.8")?e.fn.dataTableExt.aoFeatures.push({fnInit:function(a){var b=a.oInstance;a._colReorder?b.oApi._fnLog(a,1,"ColReorder attempted to initialise twice. Ignoring second"): +(b=a.oInit,new r(a,b.colReorder||b.oColReorder||{}));return null},cFeature:"R",sFeature:"ColReorder"}):alert("Warning: ColReorder requires DataTables 1.10.8 or greater - www.datatables.net/download");e(t).on("preInit.dt.colReorder",function(a,b){if("dt"===a.namespace){a=b.oInit.colReorder;var c=D.defaults.colReorder;if(a||c)c=e.extend({},a,c),!1!==a&&new r(b,c)}});e.fn.dataTable.Api.register("colReorder.reset()",function(){return this.iterator("table",function(a){a._colReorder.fnReset()})});e.fn.dataTable.Api.register("colReorder.order()", +function(a,b){return a?this.iterator("table",function(c){c._colReorder.fnOrder(a,b)}):this.context.length?this.context[0]._colReorder.fnOrder():null});e.fn.dataTable.Api.register("colReorder.transpose()",function(a,b){return this.context.length&&this.context[0]._colReorder?this.context[0]._colReorder.fnTranspose(a,b):a});e.fn.dataTable.Api.register("colReorder.move()",function(a,b,c,f){this.context.length&&(this.context[0]._colReorder.s.dt.oInstance.fnColReorder(a,b,c,f),this.context[0]._colReorder._fnSetColumnIndexes()); +return this});e.fn.dataTable.Api.register("colReorder.enable()",function(a){return this.iterator("table",function(b){b._colReorder&&b._colReorder.fnEnable(a)})});e.fn.dataTable.Api.register("colReorder.disable()",function(){return this.iterator("table",function(a){a._colReorder&&a._colReorder.fnDisable()})});return r}); + + +/*! + Copyright 2009-2021 SpryMedia Ltd. + + This source file is free software, available under the following license: + MIT license - http://datatables.net/license/mit + + This source file is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details. + + For details please refer to: http://www.datatables.net + FixedHeader 3.2.1 + ©2009-2021 SpryMedia Ltd - datatables.net/license +*/ +var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.findInternal=function(b,g,h){b instanceof String&&(b=String(b));for(var l=b.length,k=0;k'), +placeholder:null},footer:{host:null,floating:null,floatingParent:b('
'),placeholder:null}};this.dom.header.host=this.dom.thead.parent();this.dom.footer.host=this.dom.tfoot.parent();a=a.settings()[0];if(a._fixedHeader)throw"FixedHeader already initialised on table "+a.nTable.id;a._fixedHeader=this;this._constructor()};b.extend(t.prototype,{destroy:function(){this.s.dt.off(".dtfc");b(g).off(this.s.namespace);this.c.header&&this._modeChange("in-place","header",!0);this.c.footer&& +this.dom.tfoot.length&&this._modeChange("in-place","footer",!0)},enable:function(a,c){this.s.enable=a;if(c||c===l)this._positions(),this._scroll(!0)},enabled:function(){return this.s.enable},headerOffset:function(a){a!==l&&(this.c.headerOffset=a,this.update());return this.c.headerOffset},footerOffset:function(a){a!==l&&(this.c.footerOffset=a,this.update());return this.c.footerOffset},update:function(a){var c=this.s.dt.table().node();b(c).is(":visible")?this.enable(!0,!1):this.enable(!1,!1);0!==b(c).children("thead").length&& +(this._positions(),this._scroll(a!==l?a:!0))},_constructor:function(){var a=this,c=this.s.dt;b(g).on("scroll"+this.s.namespace,function(){a._scroll()}).on("resize"+this.s.namespace,k.util.throttle(function(){a.s.position.windowHeight=b(g).height();a.update()},50));var d=b(".fh-fixedHeader");!this.c.headerOffset&&d.length&&(this.c.headerOffset=d.outerHeight());d=b(".fh-fixedFooter");!this.c.footerOffset&&d.length&&(this.c.footerOffset=d.outerHeight());c.on("column-reorder.dt.dtfc column-visibility.dt.dtfc column-sizing.dt.dtfc responsive-display.dt.dtfc", +function(f,e){a.update()}).on("draw.dt.dtfc",function(f,e){a.update(e===c.settings()[0]?!1:!0)});c.on("destroy.dtfc",function(){a.destroy()});this._positions();this._scroll()},_clone:function(a,c){var d=this,f=this.s.dt,e=this.dom[a],p="header"===a?this.dom.thead:this.dom.tfoot;if("footer"!==a||!this._scrollEnabled())if(!c&&e.floating)e.floating.removeClass("fixedHeader-floating fixedHeader-locked");else{e.floating&&(null!==e.placeholder&&e.placeholder.remove(),this._unsize(a),e.floating.children().detach(), +e.floating.remove());c=b(f.table().node());var n=b(c.parent()),q=this._scrollEnabled();e.floating=b(f.table().node().cloneNode(!1)).attr("aria-hidden","true").css({"table-layout":"fixed",top:0,left:0}).removeAttr("id").append(p);e.floatingParent.css({width:n.width(),overflow:"hidden",height:"fit-content",position:"fixed",left:q?c.offset().left+n.scrollLeft():0}).css("header"===a?{top:this.c.headerOffset,bottom:""}:{top:"",bottom:this.c.footerOffset}).addClass("footer"===a?"dtfh-floatingparentfoot": +"dtfh-floatingparenthead").append(e.floating).appendTo("body");this._stickyPosition(e.floating,"-");a=function(){var r=n.scrollLeft();d.s.scrollLeft={footer:r,header:r};e.floatingParent.scrollLeft(d.s.scrollLeft.header)};a();n.scroll(a);e.placeholder=p.clone(!1);e.placeholder.find("*[id]").removeAttr("id");e.host.prepend(e.placeholder);this._matchWidths(e.placeholder,e.floating)}},_stickyPosition:function(a,c){if(this._scrollEnabled()){var d=this,f="rtl"===b(d.s.dt.table().node()).css("direction"); +a.find("th").each(function(){if("sticky"===b(this).css("position")){var e=b(this).css("right"),p=b(this).css("left");"auto"===e||f?"auto"!==p&&f&&(e=+p.replace(/px/g,"")+("-"===c?-1:1)*d.s.dt.settings()[0].oBrowser.barWidth,b(this).css("left",0y?e.tfootHeight:z+e.tfootHeight-y:d+ +this.c.headerOffset+e.theadHeight-m;m="header"===c?"top":"bottom";d=this.c[c+"Offset"]-(0y&&n+this.c.headerOffsete||this.dom.header.floatingParent===l?a=!0:this.dom.header.floatingParent.css({top:this.c.headerOffset, +position:"fixed"}).append(this.dom.header.floating)):q="below":q="in-place",(a||q!==this.s.headerMode)&&this._modeChange(q,"header",a),this._horizontal("header",p));var w={offset:{top:0,left:0},height:0},x={offset:{top:0,left:0},height:0};this.c.footer&&this.dom.tfoot.length&&(this.s.enable?!m.visible||m.tfootBottom+this.c.footerOffset<=r?m="in-place":e+m.tfootHeight+this.c.footerOffset>r&&y+this.c.footerOffsetn&&(c=n-f.top,r=r+(c>-w.height?c:0)-(w.offset.top+(c<-w.height?w.height:0)+x.height),0>r&&(r=0),d.outerHeight(r),Math.round(d.outerHeight())>=Math.round(r)?b(this.dom.tfoot.parent()).addClass("fixedHeader-floating"):b(this.dom.tfoot.parent()).removeClass("fixedHeader-floating"))); +this.dom.header.floating&&this.dom.header.floatingParent.css("left",z-p);this.dom.footer.floating&&this.dom.footer.floatingParent.css("left",z-p);this.s.dt.settings()[0]._fixedColumns!==l&&(d=function(A,C,u){u===l&&(u=b("div.dtfc-"+A+"-"+C+"-blocker"),u=0===u.length?null:u.clone().appendTo("body").css("z-index",1));null!==u&&u.css({top:"top"===C?w.offset.top:x.offset.top,left:"right"===A?z+B-u.width():z});return u},this.dom.header.rightBlocker=d("right","top",this.dom.header.rightBlocker),this.dom.header.leftBlocker= +d("left","top",this.dom.header.leftBlocker),this.dom.footer.rightBlocker=d("right","bottom",this.dom.footer.rightBlocker),this.dom.footer.leftBlocker=d("left","bottom",this.dom.footer.leftBlocker))},_scrollEnabled:function(){var a=this.s.dt.settings()[0].oScroll;return""!==a.sY||""!==a.sX?!0:!1}});t.version="3.2.1";t.defaults={header:!0,footer:!1,headerOffset:0,footerOffset:0};b.fn.dataTable.FixedHeader=t;b.fn.DataTable.FixedHeader=t;b(h).on("init.dt.dtfh",function(a,c,d){"dt"===a.namespace&&(a=c.oInit.fixedHeader, +d=k.defaults.fixedHeader,!a&&!d||c._fixedHeader||(d=b.extend({},d,a),!1!==a&&new t(c,d)))});k.Api.register("fixedHeader()",function(){});k.Api.register("fixedHeader.adjust()",function(){return this.iterator("table",function(a){(a=a._fixedHeader)&&a.update()})});k.Api.register("fixedHeader.enable()",function(a){return this.iterator("table",function(c){c=c._fixedHeader;a=a!==l?a:!0;c&&a!==c.enabled()&&c.enable(a)})});k.Api.register("fixedHeader.enabled()",function(){if(this.context.length){var a=this.context[0]._fixedHeader; +if(a)return a.enabled()}return!1});k.Api.register("fixedHeader.disable()",function(){return this.iterator("table",function(a){(a=a._fixedHeader)&&a.enabled()&&a.enable(!1)})});b.each(["header","footer"],function(a,c){k.Api.register("fixedHeader."+c+"Offset()",function(d){var f=this.context;return d===l?f.length&&f[0]._fixedHeader?f[0]._fixedHeader[c+"Offset"]():l:this.iterator("table",function(e){if(e=e._fixedHeader)e[c+"Offset"](d)})})});return t}); + + +/*! + Copyright 2014-2021 SpryMedia Ltd. + + This source file is free software, available under the following license: + MIT license - http://datatables.net/license/mit + + This source file is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details. + + For details please refer to: http://www.datatables.net + Responsive 2.2.9 + 2014-2021 SpryMedia Ltd - datatables.net/license +*/ +var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.findInternal=function(b,k,m){b instanceof String&&(b=String(b));for(var n=b.length,p=0;ptd, >th", +g).each(function(e){e=c.column.index("toData",e);!1===a.s.current[e]&&b(this).css("display","none")})});c.on("destroy.dtr",function(){c.off(".dtr");b(c.table().body()).off(".dtr");b(k).off("resize.dtr orientationchange.dtr");c.cells(".dtr-control").nodes().to$().removeClass("dtr-control");b.each(a.s.current,function(g,l){!1===l&&a._setColumnVis(g,!0)})});this.c.breakpoints.sort(function(g,l){return g.widthl.width?-1:0});this._classLogic();this._resizeAuto();d=this.c.details;!1!== +d.type&&(a._detailsInit(),c.on("column-visibility.dtr",function(){a._timer&&clearTimeout(a._timer);a._timer=setTimeout(function(){a._timer=null;a._classLogic();a._resizeAuto();a._resize(!0);a._redrawChildren()},100)}),c.on("draw.dtr",function(){a._redrawChildren()}),b(c.table().node()).addClass("dtr-"+d.type));c.on("column-reorder.dtr",function(g,l,h){a._classLogic();a._resizeAuto();a._resize(!0)});c.on("column-sizing.dtr",function(){a._resizeAuto();a._resize()});c.on("preXhr.dtr",function(){var g= +[];c.rows().every(function(){this.child.isShown()&&g.push(this.id(!0))});c.one("draw.dtr",function(){a._resizeAuto();a._resize();c.rows(g).every(function(){a._detailsDisplay(this,!1)})})});c.on("draw.dtr",function(){a._controlClass()}).on("init.dtr",function(g,l,h){"dt"===g.namespace&&(a._resizeAuto(),a._resize(),b.inArray(!1,a.s.current)&&c.columns.adjust())});this._resize()},_columnsVisiblity:function(a){var c=this.s.dt,d=this.s.columns,f,g=d.map(function(t,v){return{columnIdx:v,priority:t.priority}}).sort(function(t, +v){return t.priority!==v.priority?t.priority-v.priority:t.columnIdx-v.columnIdx}),l=b.map(d,function(t,v){return!1===c.column(v).visible()?"not-visible":t.auto&&null===t.minWidth?!1:!0===t.auto?"-":-1!==b.inArray(a,t.includeIn)}),h=0;var e=0;for(f=l.length;eh-d[q].minWidth?(r=!0,l[q]=!1):l[q]=!0,h-=d[q].minWidth)}g=!1;e=0;for(f=d.length;e=q&&g(h,c[e].name);else if("not-"===r)for(e= +0,r=c.length;ef?c.columns().eq(0).length+f:f;if(c.cell(this).index().column!==l)return}l=c.row(b(this).closest("tr"));"click"===g.type?a._detailsDisplay(l,!1):"mousedown"===g.type?b(this).css("outline","none"):"mouseup"===g.type&&b(this).trigger("blur").css("outline","")}})}, +_detailsObj:function(a){var c=this,d=this.s.dt;return b.map(this.s.columns,function(f,g){if(!f.never&&!f.control)return f=d.settings()[0].aoColumns[g],{className:f.sClass,columnIndex:g,data:d.cell(a,g).render(c.c.orthogonal),hidden:d.column(g).visible()&&!c.s.current[g],rowIndex:a,title:null!==f.sTitle?f.sTitle:b(d.column(g).header()).text()}})},_find:function(a){for(var c=this.c.breakpoints,d=0,f=c.length;d").append(h).appendTo(g)}b("
").append(l).appendTo(f);"inline"===this.c.details.type&&b(d).addClass("dtr-inline collapsed");b(d).find("[name]").removeAttr("name");b(d).css("position","relative");d=b("
").css({width:1,height:1,overflow:"hidden",clear:"both"}).append(d);d.insertBefore(a.table().node());l.each(function(e){e=a.column.index("fromVisible",e);c[e].minWidth=this.offsetWidth||0});d.remove()}},_responsiveOnlyHidden:function(){var a=this.s.dt;return b.map(this.s.current, +function(c,d){return!1===a.column(d).visible()?!0:c})},_setColumnVis:function(a,c){var d=this.s.dt;c=c?"":"none";b(d.column(a).header()).css("display",c);b(d.column(a).footer()).css("display",c);d.column(a).nodes().to$().css("display",c);b.isEmptyObject(A)||d.cells(null,a).indexes().each(function(f){y(d,f.row,f.column)})},_tabIndexes:function(){var a=this.s.dt,c=a.cells({page:"current"}).nodes().to$(),d=a.settings()[0],f=this.c.details.target;c.filter("[data-dtr-keyboard]").removeData("[data-dtr-keyboard]"); +"number"===typeof f?a.cells(null,f,{page:"current"}).nodes().to$().attr("tabIndex",d.iTabIndex).data("dtr-keyboard",1):("td:first-child, th:first-child"===f&&(f=">td:first-child, >th:first-child"),b(f,a.rows({page:"current"}).nodes()).attr("tabIndex",d.iTabIndex).data("dtr-keyboard",1))}});u.breakpoints=[{name:"desktop",width:Infinity},{name:"tablet-l",width:1024},{name:"tablet-p",width:768},{name:"mobile-l",width:480},{name:"mobile-p",width:320}];u.display={childRow:function(a,c,d){if(c){if(b(a.node()).hasClass("parent"))return a.child(d(), +"child").show(),!0}else{if(a.child.isShown())return a.child(!1),b(a.node()).removeClass("parent"),!1;a.child(d(),"child").show();b(a.node()).addClass("parent");return!0}},childRowImmediate:function(a,c,d){if(!c&&a.child.isShown()||!a.responsive.hasHidden())return a.child(!1),b(a.node()).removeClass("parent"),!1;a.child(d(),"child").show();b(a.node()).addClass("parent");return!0},modal:function(a){return function(c,d,f){if(d)b("div.dtr-modal-content").empty().append(f());else{var g=function(){l.remove(); +b(m).off("keypress.dtr")},l=b('
').append(b('
').append(b('
').append(f())).append(b('
×
').click(function(){g()}))).append(b('
').click(function(){g()})).appendTo("body");b(m).on("keyup.dtr",function(h){27===h.keyCode&&(h.stopPropagation(),g())})}a&&a.header&&b("div.dtr-modal-content").prepend("

"+a.header(c)+"

")}}};var A={};u.renderer= +{listHiddenNodes:function(){return function(a,c,d){var f=b('
    '),g=!1;b.each(d,function(l,h){h.hidden&&(b("
  • '+h.title+"
  • ").append(b('').append(p(a,h.rowIndex,h.columnIndex))).appendTo(f),g=!0)});return g?f:!1}},listHidden:function(){return function(a, +c,d){return(a=b.map(d,function(f){var g=f.className?'class="'+f.className+'"':"";return f.hidden?"
  • '+f.title+' '+f.data+"
  • ":""}).join(""))?b('
      ').append(a):!1}},tableAll:function(a){a=b.extend({tableClass:""},a);return function(c,d,f){c=b.map(f,function(g){return"
"}).join("");return b('
",{valign:"top",colSpan:oa(a),"class":a.oClasses.sRowEmpty}).html(d))[0];F(a,"aoHeaderCallback","header",[l(a.nTHead).children("tr")[0],cb(a),f,m,n]);F(a,"aoFooterCallback","footer",[l(a.nTFoot).children("tr")[0],cb(a),f,m,n]);e=l(a.nTBody);e.children().detach(); -e.append(l(c));F(a,"aoDrawCallback","draw",[a]);a.bSorted=!1;a.bFiltered=!1;a.bDrawing=!1}}function ka(a,b){var c=a.oFeatures,d=c.bFilter;c.bSort&&Gb(a);d?za(a,a.oPreviousSearch):a.aiDisplay=a.aiDisplayMaster.slice();!0!==b&&(a._iDisplayStart=0);a._drawHold=b;ja(a);a._drawHold=!1}function Hb(a){var b=a.oClasses,c=l(a.nTable);c=l("
").insertBefore(c);var d=a.oFeatures,e=l("
",{id:a.sTableId+"_wrapper","class":b.sWrapper+(a.nTFoot?"":" "+b.sNoFooter)});a.nHolding=c[0];a.nTableWrapper=e[0]; -a.nTableReinsertBefore=a.nTable.nextSibling;for(var h=a.sDom.split(""),g,f,k,n,m,p,t=0;t")[0];n=h[t+1];if("'"==n||'"'==n){m="";for(p=2;h[t+p]!=n;)m+=h[t+p],p++;"H"==m?m=b.sJUIHeader:"F"==m&&(m=b.sJUIFooter);-1!=m.indexOf(".")?(n=m.split("."),k.id=n[0].substr(1,n[0].length-1),k.className=n[1]):"#"==m.charAt(0)?k.id=m.substr(1,m.length-1):k.className=m;t+=p}e.append(k);e=l(k)}else if(">"==f)e=e.parent();else if("l"==f&&d.bPaginate&&d.bLengthChange)g= -Ib(a);else if("f"==f&&d.bFilter)g=Jb(a);else if("r"==f&&d.bProcessing)g=Kb(a);else if("t"==f)g=Lb(a);else if("i"==f&&d.bInfo)g=Mb(a);else if("p"==f&&d.bPaginate)g=Nb(a);else if(0!==u.ext.feature.length)for(k=u.ext.feature,p=0,n=k.length;p',f=d.sSearch;f=f.match(/_INPUT_/)?f.replace("_INPUT_",g):f+g;b=l("
",{id:h.f?null:c+"_filter","class":b.sFilter}).append(l("
").addClass(b.sLength); -a.aanFeatures.l||(k[0].id=c+"_length");k.children().append(a.oLanguage.sLengthMenu.replace("_MENU_",e[0].outerHTML));l("select",k).val(a._iDisplayLength).on("change.DT",function(n){jb(a,l(this).val());ja(a)});l(a.nTable).on("length.dt.DT",function(n,m,p){a===m&&l("select",k).val(p)});return k[0]}function Nb(a){var b=a.sPaginationType,c=u.ext.pager[b],d="function"===typeof c,e=function(g){ja(g)};b=l("
").addClass(a.oClasses.sPaging+b)[0];var h=a.aanFeatures;d||c.fnInit(a,b,e);h.p||(b.id=a.sTableId+ -"_paginate",a.aoDrawCallback.push({fn:function(g){if(d){var f=g._iDisplayStart,k=g._iDisplayLength,n=g.fnRecordsDisplay(),m=-1===k;f=m?0:Math.ceil(f/k);k=m?1:Math.ceil(n/k);n=c(f,k);var p;m=0;for(p=h.p.length;mh&&(d=0)):"first"==b?d=0:"previous"==b?(d=0<=e?d-e:0,0>d&&(d=0)): -"next"==b?d+e",{id:a.aanFeatures.r?null:a.sTableId+"_processing","class":a.oClasses.sProcessing}).html(a.oLanguage.sProcessing).insertBefore(a.nTable)[0]}function V(a,b){a.oFeatures.bProcessing&&l(a.aanFeatures.r).css("display",b?"block":"none");F(a,null,"processing",[a,b])}function Lb(a){var b=l(a.nTable), -c=a.oScroll;if(""===c.sX&&""===c.sY)return a.nTable;var d=c.sX,e=c.sY,h=a.oClasses,g=b.children("caption"),f=g.length?g[0]._captionSide:null,k=l(b[0].cloneNode(!1)),n=l(b[0].cloneNode(!1)),m=b.children("tfoot");m.length||(m=null);k=l("
",{"class":h.sScrollWrapper}).append(l("
",{"class":h.sScrollHead}).css({overflow:"hidden",position:"relative",border:0,width:d?d?K(d):null:"100%"}).append(l("
",{"class":h.sScrollHeadInner}).css({"box-sizing":"content-box",width:c.sXInner||"100%"}).append(k.removeAttr("id").css("margin-left", -0).append("top"===f?g:null).append(b.children("thead"))))).append(l("
",{"class":h.sScrollBody}).css({position:"relative",overflow:"auto",width:d?K(d):null}).append(b));m&&k.append(l("
",{"class":h.sScrollFoot}).css({overflow:"hidden",border:0,width:d?d?K(d):null:"100%"}).append(l("
",{"class":h.sScrollFootInner}).append(n.removeAttr("id").css("margin-left",0).append("bottom"===f?g:null).append(b.children("tfoot")))));b=k.children();var p=b[0];h=b[1];var t=m?b[2]:null;if(d)l(h).on("scroll.DT", -function(v){v=this.scrollLeft;p.scrollLeft=v;m&&(t.scrollLeft=v)});l(h).css("max-height",e);c.bCollapse||l(h).css("height",e);a.nScrollHead=p;a.nScrollBody=h;a.nScrollFoot=t;a.aoDrawCallback.push({fn:Ha,sName:"scrolling"});return k[0]}function Ha(a){var b=a.oScroll,c=b.sX,d=b.sXInner,e=b.sY;b=b.iBarWidth;var h=l(a.nScrollHead),g=h[0].style,f=h.children("div"),k=f[0].style,n=f.children("table");f=a.nScrollBody;var m=l(f),p=f.style,t=l(a.nScrollFoot).children("div"),v=t.children("table"),x=l(a.nTHead), -w=l(a.nTable),r=w[0],C=r.style,G=a.nTFoot?l(a.nTFoot):null,aa=a.oBrowser,L=aa.bScrollOversize;U(a.aoColumns,"nTh");var O=[],I=[],H=[],ea=[],Y,Ca=function(D){D=D.style;D.paddingTop="0";D.paddingBottom="0";D.borderTopWidth="0";D.borderBottomWidth="0";D.height=0};var fa=f.scrollHeight>f.clientHeight;if(a.scrollBarVis!==fa&&a.scrollBarVis!==q)a.scrollBarVis=fa,ta(a);else{a.scrollBarVis=fa;w.children("thead, tfoot").remove();if(G){var ba=G.clone().prependTo(w);var la=G.find("tr");ba=ba.find("tr")}var mb= -x.clone().prependTo(w);x=x.find("tr");fa=mb.find("tr");mb.find("th, td").removeAttr("tabindex");c||(p.width="100%",h[0].style.width="100%");l.each(Na(a,mb),function(D,W){Y=ua(a,D);W.style.width=a.aoColumns[Y].sWidth});G&&ca(function(D){D.style.width=""},ba);h=w.outerWidth();""===c?(C.width="100%",L&&(w.find("tbody").height()>f.offsetHeight||"scroll"==m.css("overflow-y"))&&(C.width=K(w.outerWidth()-b)),h=w.outerWidth()):""!==d&&(C.width=K(d),h=w.outerWidth());ca(Ca,fa);ca(function(D){var W=z.getComputedStyle? -z.getComputedStyle(D).width:K(l(D).width());H.push(D.innerHTML);O.push(W)},fa);ca(function(D,W){D.style.width=O[W]},x);l(fa).height(0);G&&(ca(Ca,ba),ca(function(D){ea.push(D.innerHTML);I.push(K(l(D).css("width")))},ba),ca(function(D,W){D.style.width=I[W]},la),l(ba).height(0));ca(function(D,W){D.innerHTML='
'+H[W]+"
";D.childNodes[0].style.height="0";D.childNodes[0].style.overflow="hidden";D.style.width=O[W]},fa);G&&ca(function(D,W){D.innerHTML='
'+ -ea[W]+"
";D.childNodes[0].style.height="0";D.childNodes[0].style.overflow="hidden";D.style.width=I[W]},ba);w.outerWidth()f.offsetHeight||"scroll"==m.css("overflow-y")?h+b:h,L&&(f.scrollHeight>f.offsetHeight||"scroll"==m.css("overflow-y"))&&(C.width=K(la-b)),""!==c&&""===d||da(a,1,"Possible column misalignment",6)):la="100%";p.width=K(la);g.width=K(la);G&&(a.nScrollFoot.style.width=K(la));!e&&L&&(p.height=K(r.offsetHeight+b));c=w.outerWidth();n[0].style.width=K(c);k.width= -K(c);d=w.height()>f.clientHeight||"scroll"==m.css("overflow-y");e="padding"+(aa.bScrollbarLeft?"Left":"Right");k[e]=d?b+"px":"0px";G&&(v[0].style.width=K(c),t[0].style.width=K(c),t[0].style[e]=d?b+"px":"0px");w.children("colgroup").insertBefore(w.children("thead"));m.trigger("scroll");!a.bSorted&&!a.bFiltered||a._drawHold||(f.scrollTop=0)}}function ca(a,b,c){for(var d=0,e=0,h=b.length,g,f;e").appendTo(f.find("tbody"));f.find("thead, tfoot").remove();f.append(l(a.nTHead).clone()).append(l(a.nTFoot).clone());f.find("tfoot th, tfoot td").css("width","");n=Na(a,f.find("thead")[0]);for(v=0;v").css({width:w.sWidthOrig,margin:0,padding:0,border:0,height:1}));if(a.aoData.length)for(v= -0;v").css(h||e?{position:"absolute",top:0,left:0,height:1,right:0,overflow:"hidden"}:{}).append(f).appendTo(p);h&&g?f.width(g):h?(f.css("width","auto"),f.removeAttr("width"),f.width()").css("width",K(a)).appendTo(b||A.body);b=a[0].offsetWidth;a.remove();return b}function Zb(a,b){var c=$b(a,b);if(0>c)return null;var d=a.aoData[c];return d.nTr?d.anCells[b]:l("
").html(T(a,c,b,"display"))[0]}function $b(a,b){for(var c, -d=-1,e=-1,h=0,g=a.aoData.length;hd&&(d=c.length,e=h);return e}function K(a){return null===a?"0px":"number"==typeof a?0>a?"0px":a+"px":a.match(/\d$/)?a+"px":a}function pa(a){var b=[],c=a.aoColumns;var d=a.aaSortingFixed;var e=l.isPlainObject(d);var h=[];var g=function(m){m.length&&!Array.isArray(m[0])?h.push(m):l.merge(h,m)};Array.isArray(d)&&g(d);e&&d.pre&&g(d.pre);g(a.aaSorting);e&&d.post&&g(d.post);for(a=0;aG?1:0;if(0!==C)return"asc"===r.dir?C:-C}C=c[m];G=c[p];return CG?1:0}):g.sort(function(m,p){var t,v=f.length,x=e[m]._aSortData,w=e[p]._aSortData;for(t=0;tG?1:0})}a.bSorted=!0}function bc(a){var b=a.aoColumns, -c=pa(a);a=a.oLanguage.oAria;for(var d=0,e=b.length;d/g,"");var k=h.nTh;k.removeAttribute("aria-sort");h.bSortable&&(0e?e+1:3))}e=0;for(h=d.length;ee?e+1:3))}a.aLastSort=d}function ac(a,b){var c=a.aoColumns[b],d=u.ext.order[c.sSortDataType], -e;d&&(e=d.call(a.oInstance,a,b,va(a,b)));for(var h,g=u.ext.type.order[c.sType+"-pre"],f=0,k=a.aoData.length;f=h.length?[0,m[1]]:m)}));f.search!==q&&l.extend(a.oPreviousSearch,Vb(f.search));if(f.columns)for(d=0,e=f.columns.length;d=c&&(b=c-d);b-=b%d;if(-1===d||0>b)b=0;a._iDisplayStart=b}function fb(a,b){a=a.renderer;var c=u.ext.renderer[b];return l.isPlainObject(a)&&a[b]?c[a[b]]||c._:"string"===typeof a?c[a]||c._:c._}function Q(a){return a.oFeatures.bServerSide? -"ssp":a.ajax||a.sAjaxSource?"ajax":"dom"}function Da(a,b){var c=dc.numbers_length,d=Math.floor(c/2);b<=c?a=ra(0,b):a<=d?(a=ra(0,c-2),a.push("ellipsis"),a.push(b-1)):(a>=b-1-d?a=ra(b-(c-2),b):(a=ra(a-d+2,a+d-1),a.push("ellipsis"),a.push(b-1)),a.splice(0,0,"ellipsis"),a.splice(0,0,0));a.DT_el="span";return a}function Wa(a){l.each({num:function(b){return Ta(b,a)},"num-fmt":function(b){return Ta(b,a,qb)},"html-num":function(b){return Ta(b,a,Ua)},"html-num-fmt":function(b){return Ta(b,a,Ua,qb)}},function(b, -c){M.type.order[b+a+"-pre"]=c;b.match(/^html\-/)&&(M.type.search[b+a]=M.type.search.html)})}function ec(a){return function(){var b=[Sa(this[u.ext.iApiIndex])].concat(Array.prototype.slice.call(arguments));return u.ext.internal[a].apply(this,b)}}var u=function(a,b){if(this instanceof u)return l(a).DataTable(b);b=a;this.$=function(g,f){return this.api(!0).$(g,f)};this._=function(g,f){return this.api(!0).rows(g,f).data()};this.api=function(g){return g?new B(Sa(this[M.iApiIndex])):new B(this)};this.fnAddData= -function(g,f){var k=this.api(!0);g=Array.isArray(g)&&(Array.isArray(g[0])||l.isPlainObject(g[0]))?k.rows.add(g):k.row.add(g);(f===q||f)&&k.draw();return g.flatten().toArray()};this.fnAdjustColumnSizing=function(g){var f=this.api(!0).columns.adjust(),k=f.settings()[0],n=k.oScroll;g===q||g?f.draw(!1):(""!==n.sX||""!==n.sY)&&Ha(k)};this.fnClearTable=function(g){var f=this.api(!0).clear();(g===q||g)&&f.draw()};this.fnClose=function(g){this.api(!0).row(g).child.hide()};this.fnDeleteRow=function(g,f,k){var n= -this.api(!0);g=n.rows(g);var m=g.settings()[0],p=m.aoData[g[0][0]];g.remove();f&&f.call(this,m,p);(k===q||k)&&n.draw();return p};this.fnDestroy=function(g){this.api(!0).destroy(g)};this.fnDraw=function(g){this.api(!0).draw(g)};this.fnFilter=function(g,f,k,n,m,p){m=this.api(!0);null===f||f===q?m.search(g,k,n,p):m.column(f).search(g,k,n,p);m.draw()};this.fnGetData=function(g,f){var k=this.api(!0);if(g!==q){var n=g.nodeName?g.nodeName.toLowerCase():"";return f!==q||"td"==n||"th"==n?k.cell(g,f).data(): -k.row(g).data()||null}return k.data().toArray()};this.fnGetNodes=function(g){var f=this.api(!0);return g!==q?f.row(g).node():f.rows().nodes().flatten().toArray()};this.fnGetPosition=function(g){var f=this.api(!0),k=g.nodeName.toUpperCase();return"TR"==k?f.row(g).index():"TD"==k||"TH"==k?(g=f.cell(g).index(),[g.row,g.columnVisible,g.column]):null};this.fnIsOpen=function(g){return this.api(!0).row(g).child.isShown()};this.fnOpen=function(g,f,k){return this.api(!0).row(g).child(f,k).show().child()[0]}; -this.fnPageChange=function(g,f){g=this.api(!0).page(g);(f===q||f)&&g.draw(!1)};this.fnSetColumnVis=function(g,f,k){g=this.api(!0).column(g).visible(f);(k===q||k)&&g.columns.adjust().draw()};this.fnSettings=function(){return Sa(this[M.iApiIndex])};this.fnSort=function(g){this.api(!0).order(g).draw()};this.fnSortListener=function(g,f,k){this.api(!0).order.listener(g,f,k)};this.fnUpdate=function(g,f,k,n,m){var p=this.api(!0);k===q||null===k?p.row(f).data(g):p.cell(f,k).data(g);(m===q||m)&&p.columns.adjust(); -(n===q||n)&&p.draw();return 0};this.fnVersionCheck=M.fnVersionCheck;var c=this,d=b===q,e=this.length;d&&(b={});this.oApi=this.internal=M.internal;for(var h in u.ext.internal)h&&(this[h]=ec(h));this.each(function(){var g={},f=1").appendTo(t));r.nTHead=H[0];var ea=t.children("tbody");0===ea.length&&(ea=l("
",{valign:"top",colSpan:oa(a),"class":a.oClasses.sRowEmpty}).html(e))[0];F(a,"aoHeaderCallback","header",[l(a.nTHead).children("tr")[0],db(a),m,n,k]);F(a,"aoFooterCallback", +"footer",[l(a.nTFoot).children("tr")[0],db(a),m,n,k]);d=l(a.nTBody);d.children().detach();d.append(l(c));F(a,"aoDrawCallback","draw",[a]);a.bSorted=!1;a.bFiltered=!1;a.bDrawing=!1}}function ka(a,b){var c=a.oFeatures,d=c.bFilter;c.bSort&&Hb(a);d?za(a,a.oPreviousSearch):a.aiDisplay=a.aiDisplayMaster.slice();!0!==b&&(a._iDisplayStart=0);a._drawHold=b;ja(a);a._drawHold=!1}function Ib(a){var b=a.oClasses,c=l(a.nTable);c=l("
").insertBefore(c);var d=a.oFeatures,e=l("
",{id:a.sTableId+"_wrapper", +"class":b.sWrapper+(a.nTFoot?"":" "+b.sNoFooter)});a.nHolding=c[0];a.nTableWrapper=e[0];a.nTableReinsertBefore=a.nTable.nextSibling;for(var h=a.sDom.split(""),f,g,k,m,n,p,t=0;t")[0];m=h[t+1];if("'"==m||'"'==m){n="";for(p=2;h[t+p]!=m;)n+=h[t+p],p++;"H"==n?n=b.sJUIHeader:"F"==n&&(n=b.sJUIFooter);-1!=n.indexOf(".")?(m=n.split("."),k.id=m[0].substr(1,m[0].length-1),k.className=m[1]):"#"==n.charAt(0)?k.id=n.substr(1,n.length-1):k.className=n;t+=p}e.append(k); +e=l(k)}else if(">"==g)e=e.parent();else if("l"==g&&d.bPaginate&&d.bLengthChange)f=Jb(a);else if("f"==g&&d.bFilter)f=Kb(a);else if("r"==g&&d.bProcessing)f=Lb(a);else if("t"==g)f=Mb(a);else if("i"==g&&d.bInfo)f=Nb(a);else if("p"==g&&d.bPaginate)f=Ob(a);else if(0!==u.ext.feature.length)for(k=u.ext.feature,p=0,m=k.length;p',g=d.sSearch;g=g.match(/_INPUT_/)?g.replace("_INPUT_",f):g+f;b=l("
",{id:h.f?null:c+"_filter","class":b.sFilter}).append(l("
").addClass(b.sLength);a.aanFeatures.l||(k[0].id=c+"_length");k.children().append(a.oLanguage.sLengthMenu.replace("_MENU_",e[0].outerHTML));l("select",k).val(a._iDisplayLength).on("change.DT",function(m){kb(a,l(this).val());ja(a)});l(a.nTable).on("length.dt.DT",function(m,n,p){a===n&&l("select",k).val(p)});return k[0]}function Ob(a){var b=a.sPaginationType,c=u.ext.pager[b],d="function"===typeof c,e=function(f){ja(f)};b=l("
").addClass(a.oClasses.sPaging+b)[0]; +var h=a.aanFeatures;d||c.fnInit(a,b,e);h.p||(b.id=a.sTableId+"_paginate",a.aoDrawCallback.push({fn:function(f){if(d){var g=f._iDisplayStart,k=f._iDisplayLength,m=f.fnRecordsDisplay(),n=-1===k;g=n?0:Math.ceil(g/k);k=n?1:Math.ceil(m/k);m=c(g,k);var p;n=0;for(p=h.p.length;nh&& +(d=0)):"first"==b?d=0:"previous"==b?(d=0<=e?d-e:0,0>d&&(d=0)):"next"==b?d+e",{id:a.aanFeatures.r?null:a.sTableId+"_processing","class":a.oClasses.sProcessing}).html(a.oLanguage.sProcessing).insertBefore(a.nTable)[0]}function V(a,b){a.oFeatures.bProcessing&&l(a.aanFeatures.r).css("display",b?"block":"none"); +F(a,null,"processing",[a,b])}function Mb(a){var b=l(a.nTable),c=a.oScroll;if(""===c.sX&&""===c.sY)return a.nTable;var d=c.sX,e=c.sY,h=a.oClasses,f=b.children("caption"),g=f.length?f[0]._captionSide:null,k=l(b[0].cloneNode(!1)),m=l(b[0].cloneNode(!1)),n=b.children("tfoot");n.length||(n=null);k=l("
",{"class":h.sScrollWrapper}).append(l("
",{"class":h.sScrollHead}).css({overflow:"hidden",position:"relative",border:0,width:d?d?K(d):null:"100%"}).append(l("
",{"class":h.sScrollHeadInner}).css({"box-sizing":"content-box", +width:c.sXInner||"100%"}).append(k.removeAttr("id").css("margin-left",0).append("top"===g?f:null).append(b.children("thead"))))).append(l("
",{"class":h.sScrollBody}).css({position:"relative",overflow:"auto",width:d?K(d):null}).append(b));n&&k.append(l("
",{"class":h.sScrollFoot}).css({overflow:"hidden",border:0,width:d?d?K(d):null:"100%"}).append(l("
",{"class":h.sScrollFootInner}).append(m.removeAttr("id").css("margin-left",0).append("bottom"===g?f:null).append(b.children("tfoot"))))); +b=k.children();var p=b[0];h=b[1];var t=n?b[2]:null;if(d)l(h).on("scroll.DT",function(v){v=this.scrollLeft;p.scrollLeft=v;n&&(t.scrollLeft=v)});l(h).css("max-height",e);c.bCollapse||l(h).css("height",e);a.nScrollHead=p;a.nScrollBody=h;a.nScrollFoot=t;a.aoDrawCallback.push({fn:Ha,sName:"scrolling"});return k[0]}function Ha(a){var b=a.oScroll,c=b.sX,d=b.sXInner,e=b.sY;b=b.iBarWidth;var h=l(a.nScrollHead),f=h[0].style,g=h.children("div"),k=g[0].style,m=g.children("table");g=a.nScrollBody;var n=l(g),p= +g.style,t=l(a.nScrollFoot).children("div"),v=t.children("table"),x=l(a.nTHead),w=l(a.nTable),r=w[0],C=r.style,G=a.nTFoot?l(a.nTFoot):null,aa=a.oBrowser,L=aa.bScrollOversize;U(a.aoColumns,"nTh");var O=[],I=[],H=[],ea=[],Y,Ca=function(D){D=D.style;D.paddingTop="0";D.paddingBottom="0";D.borderTopWidth="0";D.borderBottomWidth="0";D.height=0};var fa=g.scrollHeight>g.clientHeight;if(a.scrollBarVis!==fa&&a.scrollBarVis!==q)a.scrollBarVis=fa,ta(a);else{a.scrollBarVis=fa;w.children("thead, tfoot").remove(); +if(G){var ba=G.clone().prependTo(w);var la=G.find("tr");ba=ba.find("tr")}var mb=x.clone().prependTo(w);x=x.find("tr");fa=mb.find("tr");mb.find("th, td").removeAttr("tabindex");c||(p.width="100%",h[0].style.width="100%");l.each(Na(a,mb),function(D,W){Y=ua(a,D);W.style.width=a.aoColumns[Y].sWidth});G&&ca(function(D){D.style.width=""},ba);h=w.outerWidth();""===c?(C.width="100%",L&&(w.find("tbody").height()>g.offsetHeight||"scroll"==n.css("overflow-y"))&&(C.width=K(w.outerWidth()-b)),h=w.outerWidth()): +""!==d&&(C.width=K(d),h=w.outerWidth());ca(Ca,fa);ca(function(D){var W=z.getComputedStyle?z.getComputedStyle(D).width:K(l(D).width());H.push(D.innerHTML);O.push(W)},fa);ca(function(D,W){D.style.width=O[W]},x);l(fa).height(0);G&&(ca(Ca,ba),ca(function(D){ea.push(D.innerHTML);I.push(K(l(D).css("width")))},ba),ca(function(D,W){D.style.width=I[W]},la),l(ba).height(0));ca(function(D,W){D.innerHTML='
'+H[W]+"
";D.childNodes[0].style.height="0";D.childNodes[0].style.overflow= +"hidden";D.style.width=O[W]},fa);G&&ca(function(D,W){D.innerHTML='
'+ea[W]+"
";D.childNodes[0].style.height="0";D.childNodes[0].style.overflow="hidden";D.style.width=I[W]},ba);Math.round(w.outerWidth())g.offsetHeight||"scroll"==n.css("overflow-y")?h+b:h,L&&(g.scrollHeight>g.offsetHeight||"scroll"==n.css("overflow-y"))&&(C.width=K(la-b)),""!==c&&""===d||da(a,1,"Possible column misalignment",6)):la="100%";p.width=K(la);f.width=K(la); +G&&(a.nScrollFoot.style.width=K(la));!e&&L&&(p.height=K(r.offsetHeight+b));c=w.outerWidth();m[0].style.width=K(c);k.width=K(c);d=w.height()>g.clientHeight||"scroll"==n.css("overflow-y");e="padding"+(aa.bScrollbarLeft?"Left":"Right");k[e]=d?b+"px":"0px";G&&(v[0].style.width=K(c),t[0].style.width=K(c),t[0].style[e]=d?b+"px":"0px");w.children("colgroup").insertBefore(w.children("thead"));n.trigger("scroll");!a.bSorted&&!a.bFiltered||a._drawHold||(g.scrollTop=0)}}function ca(a,b,c){for(var d=0,e=0,h= +b.length,f,g;e").appendTo(g.find("tbody"));g.find("thead, tfoot").remove();g.append(l(a.nTHead).clone()).append(l(a.nTFoot).clone());g.find("tfoot th, tfoot td").css("width","");m=Na(a,g.find("thead")[0]);for(v=0;v").css({width:w.sWidthOrig,margin:0,padding:0,border:0,height:1}));if(a.aoData.length)for(v=0;v").css(h||e?{position:"absolute",top:0,left:0,height:1,right:0,overflow:"hidden"}:{}).append(g).appendTo(p);h&&f?g.width(f):h?(g.css("width","auto"),g.removeAttr("width"),g.width()").css("width",K(a)).appendTo(b||A.body);b=a[0].offsetWidth;a.remove();return b}function $b(a,b){var c= +ac(a,b);if(0>c)return null;var d=a.aoData[c];return d.nTr?d.anCells[b]:l("
").html(T(a,c,b,"display"))[0]}function ac(a,b){for(var c,d=-1,e=-1,h=0,f=a.aoData.length;hd&&(d=c.length,e=h);return e}function K(a){return null===a?"0px":"number"==typeof a?0>a?"0px":a+"px":a.match(/\d$/)?a+"px":a}function pa(a){var b=[],c=a.aoColumns;var d=a.aaSortingFixed;var e=l.isPlainObject(d);var h=[];var f=function(n){n.length&& +!Array.isArray(n[0])?h.push(n):l.merge(h,n)};Array.isArray(d)&&f(d);e&&d.pre&&f(d.pre);f(a.aaSorting);e&&d.post&&f(d.post);for(a=0;aG?1:0;if(0!==C)return"asc"===r.dir?C:-C}C=c[n];G=c[p];return CG?1:0}):f.sort(function(n,p){var t,v=g.length,x=e[n]._aSortData,w=e[p]._aSortData;for(t=0;tG?1:0})}a.bSorted=!0}function cc(a){var b=a.aoColumns,c=pa(a);a=a.oLanguage.oAria;for(var d=0,e=b.length;d/g,"");var k=h.nTh;k.removeAttribute("aria-sort");h.bSortable&&(0e?e+1:3))}e= +0;for(h=d.length;ee?e+1:3))}a.aLastSort=d}function bc(a,b){var c=a.aoColumns[b],d=u.ext.order[c.sSortDataType],e;d&&(e=d.call(a.oInstance,a,b,va(a,b)));for(var h,f=u.ext.type.order[c.sType+"-pre"],g=0,k=a.aoData.length;g=e.length?[0,m[1]]:m)}));b.search!==q&&l.extend(a.oPreviousSearch,Wb(b.search));if(b.columns){f=0;for(d=b.columns.length;f=c&&(b=c-d);b-=b%d;if(-1===d||0>b)b=0;a._iDisplayStart=b}function gb(a,b){a=a.renderer;var c=u.ext.renderer[b];return l.isPlainObject(a)&&a[b]?c[a[b]]||c._:"string"===typeof a?c[a]||c._:c._}function Q(a){return a.oFeatures.bServerSide? +"ssp":a.ajax||a.sAjaxSource?"ajax":"dom"}function Da(a,b){var c=ec.numbers_length,d=Math.floor(c/2);b<=c?a=ra(0,b):a<=d?(a=ra(0,c-2),a.push("ellipsis"),a.push(b-1)):(a>=b-1-d?a=ra(b-(c-2),b):(a=ra(a-d+2,a+d-1),a.push("ellipsis"),a.push(b-1)),a.splice(0,0,"ellipsis"),a.splice(0,0,0));a.DT_el="span";return a}function Xa(a){l.each({num:function(b){return Ua(b,a)},"num-fmt":function(b){return Ua(b,a,rb)},"html-num":function(b){return Ua(b,a,Va)},"html-num-fmt":function(b){return Ua(b,a,Va,rb)}},function(b, +c){M.type.order[b+a+"-pre"]=c;b.match(/^html\-/)&&(M.type.search[b+a]=M.type.search.html)})}function fc(a){return function(){var b=[Ta(this[u.ext.iApiIndex])].concat(Array.prototype.slice.call(arguments));return u.ext.internal[a].apply(this,b)}}var u=function(a,b){if(this instanceof u)return l(a).DataTable(b);b=a;this.$=function(f,g){return this.api(!0).$(f,g)};this._=function(f,g){return this.api(!0).rows(f,g).data()};this.api=function(f){return f?new B(Ta(this[M.iApiIndex])):new B(this)};this.fnAddData= +function(f,g){var k=this.api(!0);f=Array.isArray(f)&&(Array.isArray(f[0])||l.isPlainObject(f[0]))?k.rows.add(f):k.row.add(f);(g===q||g)&&k.draw();return f.flatten().toArray()};this.fnAdjustColumnSizing=function(f){var g=this.api(!0).columns.adjust(),k=g.settings()[0],m=k.oScroll;f===q||f?g.draw(!1):(""!==m.sX||""!==m.sY)&&Ha(k)};this.fnClearTable=function(f){var g=this.api(!0).clear();(f===q||f)&&g.draw()};this.fnClose=function(f){this.api(!0).row(f).child.hide()};this.fnDeleteRow=function(f,g,k){var m= +this.api(!0);f=m.rows(f);var n=f.settings()[0],p=n.aoData[f[0][0]];f.remove();g&&g.call(this,n,p);(k===q||k)&&m.draw();return p};this.fnDestroy=function(f){this.api(!0).destroy(f)};this.fnDraw=function(f){this.api(!0).draw(f)};this.fnFilter=function(f,g,k,m,n,p){n=this.api(!0);null===g||g===q?n.search(f,k,m,p):n.column(g).search(f,k,m,p);n.draw()};this.fnGetData=function(f,g){var k=this.api(!0);if(f!==q){var m=f.nodeName?f.nodeName.toLowerCase():"";return g!==q||"td"==m||"th"==m?k.cell(f,g).data(): +k.row(f).data()||null}return k.data().toArray()};this.fnGetNodes=function(f){var g=this.api(!0);return f!==q?g.row(f).node():g.rows().nodes().flatten().toArray()};this.fnGetPosition=function(f){var g=this.api(!0),k=f.nodeName.toUpperCase();return"TR"==k?g.row(f).index():"TD"==k||"TH"==k?(f=g.cell(f).index(),[f.row,f.columnVisible,f.column]):null};this.fnIsOpen=function(f){return this.api(!0).row(f).child.isShown()};this.fnOpen=function(f,g,k){return this.api(!0).row(f).child(g,k).show().child()[0]}; +this.fnPageChange=function(f,g){f=this.api(!0).page(f);(g===q||g)&&f.draw(!1)};this.fnSetColumnVis=function(f,g,k){f=this.api(!0).column(f).visible(g);(k===q||k)&&f.columns.adjust().draw()};this.fnSettings=function(){return Ta(this[M.iApiIndex])};this.fnSort=function(f){this.api(!0).order(f).draw()};this.fnSortListener=function(f,g,k){this.api(!0).order.listener(f,g,k)};this.fnUpdate=function(f,g,k,m,n){var p=this.api(!0);k===q||null===k?p.row(g).data(f):p.cell(g,k).data(f);(n===q||n)&&p.columns.adjust(); +(m===q||m)&&p.draw();return 0};this.fnVersionCheck=M.fnVersionCheck;var c=this,d=b===q,e=this.length;d&&(b={});this.oApi=this.internal=M.internal;for(var h in u.ext.internal)h&&(this[h]=fc(h));this.each(function(){var f={},g=1").appendTo(t));r.nTHead=H[0];var ea=t.children("tbody");0===ea.length&&(ea=l("
'+g.title+": "+g.data+"
').append(c)}}};u.defaults={breakpoints:u.breakpoints,auto:!0,details:{display:u.display.childRow,renderer:u.renderer.listHidden(),target:0,type:"inline"},orthogonal:"display"};var C=b.fn.dataTable.Api;C.register("responsive()",function(){return this});C.register("responsive.index()", +function(a){a=b(a);return{column:a.data("dtr-index"),row:a.parent().data("dtr-index")}});C.register("responsive.rebuild()",function(){return this.iterator("table",function(a){a._responsive&&a._responsive._classLogic()})});C.register("responsive.recalc()",function(){return this.iterator("table",function(a){a._responsive&&(a._responsive._resizeAuto(),a._responsive._resize())})});C.register("responsive.hasHidden()",function(){var a=this.context[0];return a._responsive?-1!==b.inArray(!1,a._responsive._responsiveOnlyHidden()): +!1});C.registerPlural("columns().responsiveHidden()","column().responsiveHidden()",function(){return this.iterator("column",function(a,c){return a._responsive?a._responsive._responsiveOnlyHidden()[c]:!1},1)});u.version="2.2.9";b.fn.dataTable.Responsive=u;b.fn.DataTable.Responsive=u;b(m).on("preInit.dt.dtr",function(a,c,d){"dt"===a.namespace&&(b(c.nTable).hasClass("responsive")||b(c.nTable).hasClass("dt-responsive")||c.oInit.responsive||z.defaults.responsive)&&(a=c.oInit.responsive,!1!==a&&new u(c, +b.isPlainObject(a)?a:{}))});return u}); + + +/*! DataTables styling wrapper for Responsive + * ©2018 SpryMedia Ltd - datatables.net/license + */ + +(function( factory ){ + if ( typeof define === 'function' && define.amd ) { + // AMD + define( ['jquery', 'datatables.net-dt', 'datatables.net-responsive'], function ( $ ) { + return factory( $, window, document ); + } ); + } + else if ( typeof exports === 'object' ) { + // CommonJS + module.exports = function (root, $) { + if ( ! root ) { + root = window; + } + + if ( ! $ || ! $.fn.dataTable ) { + $ = require('datatables.net-dt')(root, $).$; + } + + if ( ! $.fn.dataTable.Responsive ) { + require('datatables.net-responsive')(root, $); + } + + return factory( $, root, root.document ); + }; + } + else { + // Browser + factory( jQuery, window, document ); + } +}(function( $, window, document, undefined ) { + +return $.fn.dataTable; + +})); + diff --git a/modules/http/webif/gstatic/datatables/datetime-moment.min.js b/modules/http/webif/gstatic/datatables/datetime-moment.min.js new file mode 100644 index 0000000000..7f19751c0e --- /dev/null +++ b/modules/http/webif/gstatic/datatables/datetime-moment.min.js @@ -0,0 +1 @@ +!function(n){"function"==typeof define&&define.amd?define(["jquery","moment","datatables.net"],n):n(jQuery,moment)}(function(n,e){function t(n){return"string"==typeof n&&(n=(n=n.replace(/(<.*?>)|(\r?\n|\r)/g,"")).trim()),n}n.fn.dataTable.moment=function(r,i,o){var a=n.fn.dataTable.ext.type;a.detect.unshift(function(n){return""===(n=t(n))||null===n?"moment-"+r:e(n,r,i,!0).isValid()?"moment-"+r:null}),a.order["moment-"+r+"-pre"]=function(n){return n=t(n),e(n,r,i,!0).isValid()?parseInt(e(n,r,i,!0).format("x"),10):o?-1/0:1/0}}}); \ No newline at end of file diff --git a/modules/http/webif/gstatic/datatables/moment.min.js b/modules/http/webif/gstatic/datatables/moment.min.js new file mode 100644 index 0000000000..57cd2d4f3d --- /dev/null +++ b/modules/http/webif/gstatic/datatables/moment.min.js @@ -0,0 +1,2 @@ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.moment=t()}(this,function(){"use strict";var e,i;function f(){return e.apply(null,arguments)}function o(e){return e instanceof Array||"[object Array]"===Object.prototype.toString.call(e)}function u(e){return null!=e&&"[object Object]"===Object.prototype.toString.call(e)}function m(e,t){return Object.prototype.hasOwnProperty.call(e,t)}function l(e){if(Object.getOwnPropertyNames)return 0===Object.getOwnPropertyNames(e).length;for(var t in e)if(m(e,t))return;return 1}function r(e){return void 0===e}function h(e){return"number"==typeof e||"[object Number]"===Object.prototype.toString.call(e)}function a(e){return e instanceof Date||"[object Date]"===Object.prototype.toString.call(e)}function d(e,t){for(var n=[],s=0;s>>0,s=0;sFe(e)?(r=e+1,a-Fe(e)):(r=e,a);return{year:r,dayOfYear:o}}function Ae(e,t,n){var s,i,r=Ge(e.year(),t,n),a=Math.floor((e.dayOfYear()-r-1)/7)+1;return a<1?s=a+je(i=e.year()-1,t,n):a>je(e.year(),t,n)?(s=a-je(e.year(),t,n),i=e.year()+1):(i=e.year(),s=a),{week:s,year:i}}function je(e,t,n){var s=Ge(e,t,n),i=Ge(e+1,t,n);return(Fe(e)-s+i)/7}C("w",["ww",2],"wo","week"),C("W",["WW",2],"Wo","isoWeek"),L("week","w"),L("isoWeek","W"),A("week",5),A("isoWeek",5),ce("w",te),ce("ww",te,Q),ce("W",te),ce("WW",te,Q),ge(["w","ww","W","WW"],function(e,t,n,s){t[s.substr(0,1)]=Z(e)});function Ie(e,t){return e.slice(t,7).concat(e.slice(0,t))}C("d",0,"do","day"),C("dd",0,0,function(e){return this.localeData().weekdaysMin(this,e)}),C("ddd",0,0,function(e){return this.localeData().weekdaysShort(this,e)}),C("dddd",0,0,function(e){return this.localeData().weekdays(this,e)}),C("e",0,0,"weekday"),C("E",0,0,"isoWeekday"),L("day","d"),L("weekday","e"),L("isoWeekday","E"),A("day",11),A("weekday",11),A("isoWeekday",11),ce("d",te),ce("e",te),ce("E",te),ce("dd",function(e,t){return t.weekdaysMinRegex(e)}),ce("ddd",function(e,t){return t.weekdaysShortRegex(e)}),ce("dddd",function(e,t){return t.weekdaysRegex(e)}),ge(["dd","ddd","dddd"],function(e,t,n,s){var i=n._locale.weekdaysParse(e,s,n._strict);null!=i?t.d=i:y(n).invalidWeekday=e}),ge(["d","e","E"],function(e,t,n,s){t[s]=Z(e)});var Ze="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),ze="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),$e="Su_Mo_Tu_We_Th_Fr_Sa".split("_"),qe=de,Be=de,Je=de;function Qe(){function e(e,t){return t.length-e.length}for(var t,n,s,i,r=[],a=[],o=[],u=[],l=0;l<7;l++)t=_([2e3,1]).day(l),n=me(this.weekdaysMin(t,"")),s=me(this.weekdaysShort(t,"")),i=me(this.weekdays(t,"")),r.push(n),a.push(s),o.push(i),u.push(n),u.push(s),u.push(i);r.sort(e),a.sort(e),o.sort(e),u.sort(e),this._weekdaysRegex=new RegExp("^("+u.join("|")+")","i"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp("^("+o.join("|")+")","i"),this._weekdaysShortStrictRegex=new RegExp("^("+a.join("|")+")","i"),this._weekdaysMinStrictRegex=new RegExp("^("+r.join("|")+")","i")}function Xe(){return this.hours()%12||12}function Ke(e,t){C(e,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),t)})}function et(e,t){return t._meridiemParse}C("H",["HH",2],0,"hour"),C("h",["hh",2],0,Xe),C("k",["kk",2],0,function(){return this.hours()||24}),C("hmm",0,0,function(){return""+Xe.apply(this)+T(this.minutes(),2)}),C("hmmss",0,0,function(){return""+Xe.apply(this)+T(this.minutes(),2)+T(this.seconds(),2)}),C("Hmm",0,0,function(){return""+this.hours()+T(this.minutes(),2)}),C("Hmmss",0,0,function(){return""+this.hours()+T(this.minutes(),2)+T(this.seconds(),2)}),Ke("a",!0),Ke("A",!1),L("hour","h"),A("hour",13),ce("a",et),ce("A",et),ce("H",te),ce("h",te),ce("k",te),ce("HH",te,Q),ce("hh",te,Q),ce("kk",te,Q),ce("hmm",ne),ce("hmmss",se),ce("Hmm",ne),ce("Hmmss",se),ye(["H","HH"],Me),ye(["k","kk"],function(e,t,n){var s=Z(e);t[Me]=24===s?0:s}),ye(["a","A"],function(e,t,n){n._isPm=n._locale.isPM(e),n._meridiem=e}),ye(["h","hh"],function(e,t,n){t[Me]=Z(e),y(n).bigHour=!0}),ye("hmm",function(e,t,n){var s=e.length-2;t[Me]=Z(e.substr(0,s)),t[De]=Z(e.substr(s)),y(n).bigHour=!0}),ye("hmmss",function(e,t,n){var s=e.length-4,i=e.length-2;t[Me]=Z(e.substr(0,s)),t[De]=Z(e.substr(s,2)),t[Se]=Z(e.substr(i)),y(n).bigHour=!0}),ye("Hmm",function(e,t,n){var s=e.length-2;t[Me]=Z(e.substr(0,s)),t[De]=Z(e.substr(s))}),ye("Hmmss",function(e,t,n){var s=e.length-4,i=e.length-2;t[Me]=Z(e.substr(0,s)),t[De]=Z(e.substr(s,2)),t[Se]=Z(e.substr(i))});var tt=z("Hours",!0);var nt,st={calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},longDateFormat:{LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},invalidDate:"Invalid date",ordinal:"%d",dayOfMonthOrdinalParse:/\d{1,2}/,relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",w:"a week",ww:"%d weeks",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},months:Te,monthsShort:Ne,week:{dow:0,doy:6},weekdays:Ze,weekdaysMin:$e,weekdaysShort:ze,meridiemParse:/[ap]\.?m?\.?/i},it={},rt={};function at(e){return e?e.toLowerCase().replace("_","-"):e}function ot(e){for(var t,n,s,i,r=0;r=t&&function(e,t){for(var n=Math.min(e.length,t.length),s=0;s=t-1)break;t--}r++}return nt}function ut(t){var e;if(void 0===it[t]&&"undefined"!=typeof module&&module&&module.exports)try{e=nt._abbr,require("./locale/"+t),lt(e)}catch(e){it[t]=null}return it[t]}function lt(e,t){var n;return e&&((n=r(t)?dt(e):ht(e,t))?nt=n:"undefined"!=typeof console&&console.warn&&console.warn("Locale "+e+" not found. Did you forget to load it?")),nt._abbr}function ht(e,t){if(null===t)return delete it[e],null;var n,s=st;if(t.abbr=e,null!=it[e])Y("defineLocaleOverride","use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale See http://momentjs.com/guides/#/warnings/define-locale/ for more info."),s=it[e]._config;else if(null!=t.parentLocale)if(null!=it[t.parentLocale])s=it[t.parentLocale]._config;else{if(null==(n=ut(t.parentLocale)))return rt[t.parentLocale]||(rt[t.parentLocale]=[]),rt[t.parentLocale].push({name:e,config:t}),null;s=n._config}return it[e]=new x(b(s,t)),rt[e]&&rt[e].forEach(function(e){ht(e.name,e.config)}),lt(e),it[e]}function dt(e){var t;if(e&&e._locale&&e._locale._abbr&&(e=e._locale._abbr),!e)return nt;if(!o(e)){if(t=ut(e))return t;e=[e]}return ot(e)}function ct(e){var t,n=e._a;return n&&-2===y(e).overflow&&(t=n[ve]<0||11xe(n[pe],n[ve])?ke:n[Me]<0||24je(n,r,a)?y(e)._overflowWeeks=!0:null!=u?y(e)._overflowWeekday=!0:(o=Ee(n,s,i,r,a),e._a[pe]=o.year,e._dayOfYear=o.dayOfYear)}(e),null!=e._dayOfYear&&(r=St(e._a[pe],s[pe]),(e._dayOfYear>Fe(r)||0===e._dayOfYear)&&(y(e)._overflowDayOfYear=!0),n=Ve(r,0,e._dayOfYear),e._a[ve]=n.getUTCMonth(),e._a[ke]=n.getUTCDate()),t=0;t<3&&null==e._a[t];++t)e._a[t]=u[t]=s[t];for(;t<7;t++)e._a[t]=u[t]=null==e._a[t]?2===t?1:0:e._a[t];24===e._a[Me]&&0===e._a[De]&&0===e._a[Se]&&0===e._a[Ye]&&(e._nextDay=!0,e._a[Me]=0),e._d=(e._useUTC?Ve:function(e,t,n,s,i,r,a){var o;return e<100&&0<=e?(o=new Date(e+400,t,n,s,i,r,a),isFinite(o.getFullYear())&&o.setFullYear(e)):o=new Date(e,t,n,s,i,r,a),o}).apply(null,u),i=e._useUTC?e._d.getUTCDay():e._d.getDay(),null!=e._tzm&&e._d.setUTCMinutes(e._d.getUTCMinutes()-e._tzm),e._nextDay&&(e._a[Me]=24),e._w&&void 0!==e._w.d&&e._w.d!==i&&(y(e).weekdayMismatch=!0)}}function Ot(e){if(e._f!==f.ISO_8601)if(e._f!==f.RFC_2822){e._a=[],y(e).empty=!0;for(var t,n,s,i,r,a,o,u=""+e._i,l=u.length,h=0,d=H(e._f,e._locale).match(N)||[],c=0;cn.valueOf():n.valueOf()"}),pn.toJSON=function(){return this.isValid()?this.toISOString():null},pn.toString=function(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},pn.unix=function(){return Math.floor(this.valueOf()/1e3)},pn.valueOf=function(){return this._d.valueOf()-6e4*(this._offset||0)},pn.creationData=function(){return{input:this._i,format:this._f,locale:this._locale,isUTC:this._isUTC,strict:this._strict}},pn.eraName=function(){for(var e,t=this.localeData().eras(),n=0,s=t.length;nthis.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()},pn.isLocal=function(){return!!this.isValid()&&!this._isUTC},pn.isUtcOffset=function(){return!!this.isValid()&&this._isUTC},pn.isUtc=At,pn.isUTC=At,pn.zoneAbbr=function(){return this._isUTC?"UTC":""},pn.zoneName=function(){return this._isUTC?"Coordinated Universal Time":""},pn.dates=n("dates accessor is deprecated. Use date instead.",fn),pn.months=n("months accessor is deprecated. Use month instead",Ue),pn.years=n("years accessor is deprecated. Use year instead",Le),pn.zone=n("moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/",function(e,t){return null!=e?("string"!=typeof e&&(e=-e),this.utcOffset(e,t),this):-this.utcOffset()}),pn.isDSTShifted=n("isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information",function(){if(!r(this._isDSTShifted))return this._isDSTShifted;var e,t={};return v(t,this),(t=bt(t))._a?(e=(t._isUTC?_:Tt)(t._a),this._isDSTShifted=this.isValid()&&0 - + @@ -35,12 +35,15 @@ - - + + + + {% block pluginstyles %} + {% endblock pluginstyles %} {%- endblock styles %} - {% block scripts %} + {% block scripts %} @@ -65,7 +68,10 @@ - + + + + diff --git a/modules/http/webif/gtemplates/base_plugin.html b/modules/http/webif/gtemplates/base_plugin.html old mode 100644 new mode 100755 index c8abe5359e..089bc115cb --- a/modules/http/webif/gtemplates/base_plugin.html +++ b/modules/http/webif/gtemplates/base_plugin.html @@ -16,10 +16,10 @@ {% block content %} {% if scroll_heading is not defined %} -
+
{% endif %} -
+
@@ -57,7 +57,7 @@
{{ _('Plugin') }}     : {% if p.alive %}{{_('Aktiv')}}{% else %}{{ {% block headtable %} {% endblock %} -
+
{% block buttons %} @@ -120,7 +120,7 @@
{{ _('Plugin') }}     : {% if p.alive %}{{_('Aktiv')}}{% else %}{{ {% endif %} -
+