diff --git a/executor/__init__.py b/executor/__init__.py index c4f70e5b8..4e02a5cf0 100755 --- a/executor/__init__.py +++ b/executor/__init__.py @@ -40,7 +40,7 @@ class Executor(SmartPlugin): the update functions for the items """ - PLUGIN_VERSION = '1.1.1' + PLUGIN_VERSION = '1.2.0' def __init__(self, sh): """ @@ -142,4 +142,3 @@ def init_webinterface(self): description='') return True - diff --git a/executor/examples/check_device_presence.py b/executor/examples/check_device_presence.py new file mode 100644 index 000000000..c7dfcc743 --- /dev/null +++ b/executor/examples/check_device_presence.py @@ -0,0 +1,14 @@ +import os + +with os.popen('ip neigh show') as result: + # permanent, noarp, reachable, stale, none, incomplete, delay, probe, failed + ip = '192.168.10.56' + mac = "b4:b5:2f:ce:6d:29" + value = False + lines = str(result.read()).splitlines() + for line in lines: + if (ip in line or mac in line) and ("REACHABLE" in line or "STALE" in line): + value = True + break + #sh.devices.laptop.status(value)​ + print(f"set item to {value}") \ No newline at end of file diff --git a/executor/examples/check_items.py b/executor/examples/check_items.py new file mode 100644 index 000000000..e41eb3316 --- /dev/null +++ b/executor/examples/check_items.py @@ -0,0 +1,95 @@ +""" +given following items within a yaml: + + +MyItem: + MyChildItem: + type: num + initial_value: 12 + MyGrandchildItem: + type: str + initial_value: "foo" + +Within a logic it is possible to set the value of MyChildItem to 42 with +``sh.MyItem.MyChildItem(42)`` and retrieve the Items value with +``value = sh.MyItem.MyChildItem()`` + +Often beginners forget the parentheses and instead write +``sh.MyItem.MyChildItem = 42`` when they really intend to assign the value ``42`` +to the item or write ``value = sh.MyItem.MyChildItem`` when they really want to +retrieve the item's value. + +But using ``sh.MyItem.MyChildItem = 42`` destroys the structure here and makes +it impossible to retrieve the value of the child +``MyItem.MyChildItem.MyGrandchildItem`` +Alike, an instruction as ``value = sh.MyItem.MyChildItem`` will not assign the +value of ``sh.MyItem.MyChildItem`` but assign a reference to the item object +``sh.MyItem.MyChildItem`` + +It is not possible with Python to intercept an assignment to a variable or an +objects' attribute. The only thing one can do is search all items for a +mismatching item type. + +This logic checks all items returned by SmartHomeNG, and if it encounters one +which seems to be damaged like described before, it attempts to repair the +broken assignment. + +""" +from lib.item import Items +from lib.item.item import Item + +def repair_item(sh, item): + path = item.id() + path_elems = path.split('.') + ref = sh + + # traverse through object structure sh.path1.path2... + try: + for path_part in path_elems[:-1]: + ref = getattr(ref, path_part) + + setattr(ref, path_elems[-1], item) + print(f'Item reference repaired for {path}') + return True + except NameError: + print(f'Error: item traversal for {path} failed at part {path_part}. Item list not sorted?') + + return False + + +def get_item_type(sh, path): + expr = f'type(sh.{path})' + return str(eval(expr)) + + +def check_item(sh, path): + global get_item_type + + return get_item_type(sh, path) == "" + + +# to get access to the object instance: +items = Items.get_instance() + +# to access a method (eg. to get the list of Items): +# allitems = items.return_items() +problems_found = 0 +problems_fixed = 0 + +for one in items.return_items(ordered=True): + # get the items full path + path = one.property.path + try: + if not check_item(sh, path): + logger.error(f"Error: item {path} has type {get_item_type(sh, path)} but should be an Item Object") + problems_found += 1 + if repair_item(sh, one): + if check_item(sh, path): + problems_fixed += 1 + except ValueError as e: + logger.error(f'Error {e} while processing item {path}, parent defective? Items not sorted?') + +if problems_found: + logger.error(f"{problems_found} problematic item assignment{'' if problems_found == 1 else 's'} found, {problems_fixed} item assignment{'' if problems_fixed == 1 else 's'} fixed") +else: + logger.warning("no problems found") diff --git a/executor/examples/database_count.py b/executor/examples/database_count.py new file mode 100644 index 000000000..3d80771a5 --- /dev/null +++ b/executor/examples/database_count.py @@ -0,0 +1,9 @@ +from lib.item import Items +items = Items.get_instance() +myfiller = " " +allItems = items.return_items() +for myItem in allItems: + if not hasattr(myItem,'db'): + continue + mycount = myItem.db('countall', 0) + print (myItem.property.name + myfiller[0:len(myfiller)-len(myItem.property.name)]+ ' - Anzahl Datensätze :'+str(mycount)) diff --git a/executor/examples/database_series.py b/executor/examples/database_series.py new file mode 100644 index 000000000..88a4bcbe3 --- /dev/null +++ b/executor/examples/database_series.py @@ -0,0 +1,9 @@ +import json + +def myconverter(o): +import datetime +if isinstance(o, datetime.datetime): + return o.__str__() +data = sh..series('max','1d','now') +pretty = json.dumps(data, default = myconverter, indent = 2, separators=(',', ': ')) +print(pretty) diff --git a/executor/plugin.yaml b/executor/plugin.yaml index 3933b87be..3f26ce297 100755 --- a/executor/plugin.yaml +++ b/executor/plugin.yaml @@ -6,13 +6,13 @@ plugin: de: 'Ausführen von Python Statements im Kontext von SmartHomeNG v1.5 und höher' en: 'Execute Python statements in the context of SmartHomeNG v1.5 and up' maintainer: bmxp - tester: nobody # Who tests this plugin? + tester: onkelandy state: ready # change to ready when done with development keywords: Python eval exec code test documentation: https://www.smarthomeng.de/user/plugins/executor/user_doc.html support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1425152-support-thread-plugin-executor - version: 1.1.1 # Plugin version + version: 1.2.0 # Plugin version sh_minversion: 1.9 # minimum shNG version to use this plugin #sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) py_minversion: 3.8 # minimum Python version to use for this plugin, use f-strings for debug diff --git a/executor/user_doc.rst b/executor/user_doc.rst index 7c749a259..53238c335 100755 --- a/executor/user_doc.rst +++ b/executor/user_doc.rst @@ -16,7 +16,7 @@ executor Einführung ~~~~~~~~~~ -Das executor plugin kann genutzt werden, um **Python Code** (z.B. für **Logiken**) und **eval Ausdrücke** zu testen. +Das executor Plugin kann genutzt werden, um **Python Code** (z.B. für **Logiken**) zu testen. .. important:: @@ -35,8 +35,8 @@ Damit wird dem Plugin eine relative Pfadangabe unterhalb *var* angegeben wo Skri Webinterface ============ -Im Webinterface findet sich eine Listbox mit den auf dem Rechner gespeicherten Skripten. -Um das Skript in den Editor zu laden entweder ein Skript in der Liste einfach anklicken und auf *aus Datei laden* klicken oder +Im Webinterface findet sich eine Listbox mit den auf dem Rechner gespeicherten Skripten. +Um das Skript in den Editor zu laden, entweder ein Skript in der Liste einfach anklicken und auf *aus Datei laden* klicken oder direkt in der Liste einen Doppelklick auf die gewünschte Datei ausführen. Der Dateiname wird entsprechend der gewählten Datei gesetzt. Mit Klick auf *aktuellen Code speichern* wird der Code im konfigurierten @@ -46,7 +46,7 @@ Mit einem Klick auf *Code ausführen!* oder der Kombination Ctrl+Return wird der Das kann gerade bei Datenbank Abfragen recht lange dauern. Es kann keine Rückmeldung von SmartHomeNG abgefragt werden wie weit der Code derzeit ist. Das Ergebnis wird unten angezeigt. Solange kein Ergebnis vorliegt, steht im Ergebniskasten **... processing ...** -Mit einem Klick auf Datei löschen wird versucht die unter Dateiname angezeigte Datei ohne Rückfrage zu löschen. +Mit einem Klick auf *Datei löschen* wird versucht, die unter Dateiname angezeigte Datei ohne Rückfrage zu löschen. Anschliessend wird die Liste der Skripte aktualisiert. Beispiel Python Code @@ -55,7 +55,9 @@ Beispiel Python Code Sowohl ``logger`` als auch ``print`` funktionieren für die Ausgabe von Ergebnissen. Die Idee ist, dass Logiken mehr oder weniger 1:1 kopiert und getestet werden können. + Loggertest +---------- .. code-block:: python @@ -66,6 +68,7 @@ Loggertest Datenserien für ein Item ausgeben +--------------------------------- Abfragen von Daten aus dem database plugin für ein spezifisches Item: @@ -111,4 +114,20 @@ würde in folgendem Ergebnis münden: ] } -Damit die Nutzung + +Zählen der Datensätze in der Datenbank +-------------------------------------- + +Das folgende Snippet zeigt alle Datenbank-Items an und zählt die Einträge in der Datenbank. Vorsicht: Dies kann sehr lange dauern, wenn Sie eine große Anzahl von Einträgen mit Datenbankattributen haben. + +.. code-block:: python + + from lib.item import Items + items = Items.get_instance() + myfiller = " " + allItems = items.return_items() + for myItem in allItems: + if not hasattr(myItem,'db'): + continue + mycount = myItem.db('countall', 0) + print (myItem.property.name + myfiller[0:len(myfiller)-len(myItem.property.name)]+ ' - Anzahl Datensätze :'+str(mycount)) diff --git a/executor/webif/__init__.py b/executor/webif/__init__.py index a74caa780..512d899c9 100755 --- a/executor/webif/__init__.py +++ b/executor/webif/__init__.py @@ -48,7 +48,7 @@ import csv from jinja2 import Environment, FileSystemLoader -import sys +import sys class PrintCapture: """this class overwrites stdout and stderr temporarily to capture output""" @@ -197,11 +197,11 @@ def eval_statement(self, eline, path, reload=None): def exec_code(self, eline, reload=None): """ evaluate a whole python block in eline - + :return: result of the evaluation """ result = "" - stub_logger = Stub(warning=print, info=print, debug=print, error=print) + stub_logger = Stub(warning=print, info=print, debug=print, error=print, criticl=print, notice=print, dbghigh=print, dbgmed=print, dbglow=print) g = {} l = { 'sh': self.plugin.get_sh(), @@ -233,15 +233,19 @@ def get_code(self, filename=''): """loads and returns the given filename from the defined script path""" self.logger.debug(f"get_code called with {filename=}") try: - if self.plugin.executor_scripts is not None and filename != '': - filepath = os.path.join(self.plugin.executor_scripts,filename) - self.logger.debug(f"{filepath=}") + if (self.plugin.executor_scripts is not None and filename != '') or filename.startswith('examples/'): + if filename.startswith('examples/'): + filepath = os.path.join(self.plugin.get_plugin_dir(),filename) + self.logger.debug(f"Getting file from example path {filepath=}") + else: + filepath = os.path.join(self.plugin.executor_scripts,filename) + self.logger.debug(f"Getting file from script path {filepath=}") code_file = open(filepath) data = code_file.read() code_file.close() return data - except: - self.logger.error(f"{filepath} could not be read") + except Exception as e: + self.logger.error(f"{filepath} could not be read: {e}") return f"### {filename} could not be read ###" @cherrypy.expose @@ -283,20 +287,29 @@ def delete_file(self, filename=''): @cherrypy.expose def get_filelist(self): - """returns all filenames from the defined script path with suffix ``.py``""" - + """returns all filenames from the defined script path with suffix ``.py``, newest first""" + files = [] + files2 = [] + subdir = "{}/examples".format(self.plugin.get_plugin_dir()) + self.logger.debug(f"list files in plugin examples {subdir}") + mtime = lambda f: os.stat(os.path.join(subdir, f)).st_mtime + files = list(reversed(sorted(os.listdir(subdir), key=mtime))) + files = [f for f in files if os.path.isfile(os.path.join(subdir,f))] + files = ["examples/{}".format(f) for f in files if f.endswith(".py")] + #files = '\n'.join(f for f in files) + self.logger.debug(f"Examples Scripts {files}") if self.plugin.executor_scripts is not None: subdir = self.plugin.executor_scripts self.logger.debug(f"list files in {subdir}") - files = os.listdir(subdir) - files = [f for f in files if os.path.isfile(os.path.join(subdir,f))] - files = [f for f in files if f.endswith(".py")] - files = '\n'.join(f for f in files) - self.logger.debug(f"{files=}\n\n") - return files - - return '' - + files2 = list(reversed(sorted(os.listdir(subdir), key=mtime))) + files2 = [f for f in files2 if os.path.isfile(os.path.join(subdir,f))] + files2 = [f for f in files2 if f.endswith(".py")] + #files = '\n'.join(f for f in files) + self.logger.debug(f"User scripts {files2}") + + return json.dumps(files2 + files) + + @cherrypy.expose def get_autocomplete(self): _sh = self.plugin.get_sh() @@ -310,11 +323,11 @@ def get_autocomplete(self): if api is not None: for function in api: plugin_list.append("sh."+plugin_config_name + "." + function) - + myItems = _sh.return_items() itemList = [] for item in myItems: - itemList.append("sh."+str(item)+"()") + itemList.append("sh."+str(item.property.path)+"()") retValue = {'items':itemList,'plugins':plugin_list} - return (json.dumps(retValue)) \ No newline at end of file + return (json.dumps(retValue)) diff --git a/executor/webif/templates/index.html b/executor/webif/templates/index.html index d34350a03..bda07578b 100755 --- a/executor/webif/templates/index.html +++ b/executor/webif/templates/index.html @@ -3,12 +3,6 @@ {% set logo_frame = false %} {% block pluginscripts %} - -{% endblock pluginscripts %} - - -{% block content -%} - +{% endblock pluginscripts %} +{% block pluginstyles %} +{% endblock pluginstyles %} +{% block content -%}
@@ -358,12 +478,13 @@
{{ _('Instanz') }}: {{ p.get_instance_name( {% endif %}
{{ _('Plugin') }}     : {% if p.alive %}{{ _('Aktiv') }}{% else %}{{ _('Gestoppt') }}{% endif %}
-
+
+ + + +
@@ -381,13 +502,13 @@
{{ _('Plugin') }}     : {% if p.aliv
-
+