Skip to content

Commit

Permalink
Merge pull request #776 from onkelandy/executor
Browse files Browse the repository at this point in the history
Executor Plugin: Improvements and fixes
  • Loading branch information
onkelandy authored Jul 13, 2023
2 parents b6097ee + e23bf29 commit 8e31d75
Show file tree
Hide file tree
Showing 9 changed files with 375 additions and 96 deletions.
3 changes: 1 addition & 2 deletions executor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down Expand Up @@ -142,4 +142,3 @@ def init_webinterface(self):
description='')

return True

14 changes: 14 additions & 0 deletions executor/examples/check_device_presence.py
Original file line number Diff line number Diff line change
@@ -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}")
95 changes: 95 additions & 0 deletions executor/examples/check_items.py
Original file line number Diff line number Diff line change
@@ -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) == "<class 'lib.item.item.Item'>"


# 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")
9 changes: 9 additions & 0 deletions executor/examples/database_count.py
Original file line number Diff line number Diff line change
@@ -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))
9 changes: 9 additions & 0 deletions executor/examples/database_series.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import json

def myconverter(o):
import datetime
if isinstance(o, datetime.datetime):
return o.__str__()
data = sh.<your item here>.series('max','1d','now')
pretty = json.dumps(data, default = myconverter, indent = 2, separators=(',', ': '))
print(pretty)
4 changes: 2 additions & 2 deletions executor/plugin.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 24 additions & 5 deletions executor/user_doc.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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::

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -66,6 +68,7 @@ Loggertest
Datenserien für ein Item ausgeben
---------------------------------

Abfragen von Daten aus dem database plugin für ein spezifisches Item:

Expand Down Expand Up @@ -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))
57 changes: 35 additions & 22 deletions executor/webif/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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))
return (json.dumps(retValue))
Loading

0 comments on commit 8e31d75

Please sign in to comment.