diff --git a/.gitignore b/.gitignore index bc08e4ed4..6a46b1f3c 100755 --- a/.gitignore +++ b/.gitignore @@ -25,7 +25,10 @@ ehthumbs.db Thumbs.db # don't upload private plugins -/priv_* +/priv_*/ + +# don't upload plugins loaded from develop to a release installation +/*_dev/ # Pycharm settings /.idea diff --git a/README.md b/README.md index ca9d05965..a5c05bb3c 100755 --- a/README.md +++ b/README.md @@ -3,20 +3,18 @@ ![Github Tag](https://img.shields.io/github/tag/smarthomeNG/smarthome.svg) ![Made with Python](https://img.shields.io/badge/made%20with-python-blue.svg) [![Join the chat at https://gitter.im/smarthomeNG/smarthome](https://badges.gitter.im/smarthomeNG/smarthome.svg)](https://gitter.im/smarthomeNG/smarthome?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -[![Build Status on TravisCI](https://travis-ci.com/smarthomeNG/plugins.svg?branch=master)](https://travis-ci.com/smarthomeNG/plugins) + Die Plugins für SmartHomeNG erweitern die Möglichkeiten des Gesamtsystems in dem sie Zugriff auf verschiedene Schnittstellen bereitstellen. -Auf der ([Webseite des Projektes](https://www.smarthomeNG.de)) kann eine [Benutzerdokumentation](https://www.smarthomeNG.de) eingesehen werden. +Es existiert eine [Benutzerdokumentation](https://smarthomeng.github.io/smarthome/) in der Kern und Plugins dokumentiert sind. -Ein [Wiki](https://github.com/smarthomeNG/smarthome/wiki) existiert zumeist in deutscher Sprache. +Ein [Wiki](https://github.com/smarthomeNG/smarthome/wiki) existiert zumeist in deutscher Sprache, wird aber sehr selten erweitert oder aktualisiert. ## Aktueller Status der Entwicklung -[![Aktuelle Entwicklung](https://travis-ci.com/smarthomeNG/plugins.svg?branch=develop)](https://travis-ci.com/smarthomeNG/plugins) - - +Es gibt eine stetig aktualisierte [Dokumentation für Entwickler](https://smarthomeng.github.io/dev_doc/). ## Other languages -It is possible to read the documentation with [Google's translation service](https://translate.google.com/translate?hl=&sl=de&tl=en&u=https://www.smarthomeng.de/dev/user/) in other languages as well. \ No newline at end of file +It is possible to read the documentation with [Google's translation service](https://translate.google.com/translate?hl=&sl=de&tl=en&u=https://smarthomeng.github.io/smarthome/) in other languages as well. diff --git a/apcups/README.md b/apcups/README.md deleted file mode 100755 index 4be911631..000000000 --- a/apcups/README.md +++ /dev/null @@ -1,259 +0,0 @@ -# APC UPS - -## Requirements - -A running **apcupsd** with configured netserver (NIS). The plugin retrieves the information via network from the netserver. No local apcupsd is required. -The apcupsd package must be installed also on the local system (running daemon is not required). The plugin uses the **apcaccess** helper tool from the package. - -If running the daemon locally and using the netserver, then the ``/etc/apcupsd/apcupsd.conf`` should contain additionally the following entries: - -``` -NETSERVER on -NISPORT 3551 -NISIP 127.0.0.1 -``` - -## Supported Hardware - -Should work on all APC UPS devices. Tested only on a "smartUPS". - -## Configuration - -### plugin.yaml - -Add the following lines to activate the plugin: - -```yaml -ApcUps: - plugin_name: apcups - host: localhost - port: 3551 -``` - -Description of the attributes: - -* __host__: ip address of the NIS (optional, default: localhost) -* __port__: port of the NIS (optional, default: 3551) -* __cycle__: time to update the items with values from apcaccess - -### items.yaml - -There is only one attribute: **apcups** - -For a list of values for this attribute call "apcaccess" on the command line. This command will give back a text block containing a list of ``statusname : value`` entries like the following: - -``` -APC : 001,050,1127 -DATE : 2017-11-02 07:59:15 +0100 -HOSTNAME : sh11 -VERSION : 3.14.12 (29 March 2014) debian -UPSNAME : UPS_IDEN -CABLE : Ethernet Link -DRIVER : PCNET UPS Driver -UPSMODE : Stand Alone -STARTTIME: 2017-11-02 07:59:11 +0100 -MODEL : Smart-UPS 1400 RM -STATUS : ONLINE -LINEV : 227.5 Volts -LOADPCT : 31.2 Percent -BCHARGE : 100.0 Percent -TIMELEFT : 30.0 Minutes -MBATTCHG : 10 Percent -MINTIMEL : 5 Minutes -MAXTIME : 0 Seconds -MAXLINEV : 227.5 Volts -MINLINEV : 226.0 Volts -OUTPUTV : 227.5 Volts -SENSE : High -DWAKE : 0 Seconds -DSHUTD : 120 Seconds -DLOWBATT : 2 Minutes -LOTRANS : 208.0 Volts -HITRANS : 253.0 Volts -RETPCT : 0.0 Percent -ITEMP : 25.6 C -ALARMDEL : Low Battery -BATTV : 27.7 Volts -LINEFREQ : 49.8 Hz -LASTXFER : Line voltage notch or spike -NUMXFERS : 0 -TONBATT : 0 Seconds -CUMONBATT: 0 Seconds -XOFFBATT : N/A -SELFTEST : NO -STESTI : 336 -STATFLAG : 0x05000008 -REG1 : 0x00 -REG2 : 0x00 -REG3 : 0x00 -MANDATE : 08/16/00 -SERIALNO : GS0034003173 -BATTDATE : 06/20/15 -NOMOUTV : 230 Volts -NOMBATTV : 24.0 Volts -EXTBATTS : 0 -FIRMWARE : 162.3.I -END APC : 2017-11-02 08:00:39 +0100 -``` - -The plugin will check the items type. If the type is a string, then the value will we returned as a string (e.g. status "ONLINE"). -If it is of type num, then the item will be set to a float. To convert to a float, the returned string will be cut after first space and the converted (e.g. 235 Volt = 235). - -### Example - -The example below will read the keys **LINEV**, **STATUS** and **TIMELEFT** and returns their values. - -```yaml -# items/apcups.yaml -serverroom: - - apcups: - - linev: - visu_acl: ro - type: num - apcups: linev - - status: - # will be 'ONLINE', 'ONBATT', or in case of a problem simply empty - visu_acl: ro - type: str - apcups: status - - timeleft: - visu_acl: ro - type: num - apcups: timeleft -``` - -**type** depends on the values. - -### Status Report Fields - -Due to a look into this http://apcupsd.org/manual/manual.html#configuration-examples file the meaning of the above variables are as follows - -``` -APC - Header record indicating the STATUS format revision level, the number of records that follow the APC statement, and the number of bytes that follow the record. -DATE - The date and time that the information was last obtained from the UPS. -HOSTNAME - The name of the machine that collected the UPS data. -UPSNAME - The name of the UPS as stored in the EEPROM or in the UPSNAME directive in the configuration file. -VERSION - The apcupsd release number, build date, and platform. -CABLE - The cable as specified in the configuration file (UPSCABLE). -MODEL - The UPS model as derived from information from the UPS. -UPSMODE - The mode in which apcupsd is operating as specified in the configuration file (UPSMODE) -STARTTIME - The time/date that apcupsd was started. -STATUS - The current status of the UPS (ONLINE, ONBATT, etc.) -LINEV - The current line voltage as returned by the UPS. -LOADPCT - The percentage of load capacity as estimated by the UPS. -BCHARGE - The percentage charge on the batteries. -TIMELEFT - The remaining runtime left on batteries as estimated by the UPS. -MBATTCHG - If the battery charge percentage (BCHARGE) drops below this value, apcupsd will shutdown your system. Value is set in the configuration file (BATTERYLEVEL) -MINTIMEL - apcupsd will shutdown your system if the remaining runtime equals or is below this point. Value is set in the configuration file (MINUTES) -MAXTIME - apcupsd will shutdown your system if the time on batteries exceeds this value. A value of zero disables the feature. Value is set in the configuration file (TIMEOUT) -MAXLINEV - The maximum line voltage since the UPS was started, as reported by the UPS -MINLINEV - The minimum line voltage since the UPS was started, as returned by the UPS -OUTPUTV - The voltage the UPS is supplying to your equipment -SENSE - The sensitivity level of the UPS to line voltage fluctuations. -DWAKE - The amount of time the UPS will wait before restoring power to your equipment after a power off condition when the power is restored. -DSHUTD - The grace delay that the UPS gives after receiving a power down command from apcupsd before it powers off your equipment. -DLOWBATT - The remaining runtime below which the UPS sends the low battery signal. At this point apcupsd will force an immediate emergency shutdown. -LOTRANS - The line voltage below which the UPS will switch to batteries. -HITRANS - The line voltage above which the UPS will switch to batteries. -RETPCT - The percentage charge that the batteries must have after a power off condition before the UPS will restore power to your equipment. -ITEMP - Internal UPS temperature as supplied by the UPS. -ALARMDEL - The delay period for the UPS alarm. -BATTV - Battery voltage as supplied by the UPS. -LINEFREQ - Line frequency in hertz as given by the UPS. -LASTXFER - The reason for the last transfer to batteries. -NUMXFERS - The number of transfers to batteries since apcupsd startup. -XONBATT - Time and date of last transfer to batteries, or N/A. -TONBATT - Time in seconds currently on batteries, or 0. -CUMONBATT - Total (cumulative) time on batteries in seconds since apcupsd startup. -XOFFBATT - Time and date of last transfer from batteries, or N/A. -SELFTEST - - The results of the last self test, and may have the following values: - - OK: self test indicates good battery - BT: self test failed due to insufficient battery capacity - NG: self test failed due to overload - NO: No results (i.e. no self test performed in the last 5 minutes) - -STESTI - The interval in hours between automatic self tests. -STATFLAG - Status flag. English version is given by STATUS. -DIPSW - The current dip switch settings on UPSes that have them. -REG1 - The value from the UPS fault register 1. -REG2 - The value from the UPS fault register 2. -REG3 - The value from the UPS fault register 3. -MANDATE - The date the UPS was manufactured. -SERIALNO - The UPS serial number. -BATTDATE - The date that batteries were last replaced. -NOMOUTV - The output voltage that the UPS will attempt to supply when on battery power. -NOMINV - The input voltage that the UPS is configured to expect. -NOMBATTV - The nominal battery voltage. -NOMPOWER - The maximum power in Watts that the UPS is designed to supply. -HUMIDITY - The humidity as measured by the UPS. -AMBTEMP - The ambient temperature as measured by the UPS. -EXTBATTS - The number of external batteries as defined by the user. A correct number here helps the UPS compute the remaining runtime more accurately. -BADBATTS - The number of bad battery packs. -FIRMWARE - The firmware revision number as reported by the UPS. -APCMODEL - The old APC model identification code. -END APC - The time and date that the STATUS record was written. -``` diff --git a/apcups/__init__.py b/apcups/__init__.py index 8328fa6e2..6fc987aef 100755 --- a/apcups/__init__.py +++ b/apcups/__init__.py @@ -20,6 +20,7 @@ ######################################################################### import logging +import threading import subprocess # we scrape apcaccess output from lib.model.smartplugin import SmartPlugin @@ -28,7 +29,7 @@ ITEM_TAG = ['apcups'] class APCUPS(SmartPlugin): - PLUGIN_VERSION = "1.3.3" + PLUGIN_VERSION = "1.4.0" def __init__(self, sh): # Call init code of parent class (SmartPlugin) @@ -42,49 +43,63 @@ def __init__(self, sh): self._port = self.get_parameter_value('port') self._cycle = self.get_parameter_value('cycle') self._items = {} + self._lock = threading.Lock() + # the command goes here + self._command = f"/sbin/apcaccess status {self._host}:{ self._port}" + self._last_readout = "" def run(self): self.alive = True - self.scheduler_add('APCups', self.update_status, cycle=self._cycle) + self.scheduler_add(self.get_shortname(), self.update_status, cycle=self._cycle) def stop(self): self.alive = False + self.scheduler_remove(self.get_shortname()) + def parse_item(self, item): if self.has_iattr(item.conf, ITEM_TAG[0]): apcups_key = (self.get_iattr_value(item.conf, ITEM_TAG[0])).lower() self._items[apcups_key]=item - self.logger.debug("item {0} added with apcupd_key {1}".format(item,apcups_key)) - return self.update_item - else: - return None + self.logger.debug(f"item {item} added with apcupd_key {apcups_key}") + # no callback for any item needed as this plugin is readonly + return None def update_item(self, item, caller=None, source=None, dest=None): - if caller != 'plugin': - self.logger.debug("update item: {0}".format(item.id())) + pass def update_status(self): """ Start **apcaccess** on a shell, capture the output and parse it. The items attribut parameter will be matched against the shell output """ - command = '/sbin/apcaccess status {0}:{1}'.format(self._host, self._port) # the command goes here - output = subprocess.check_output(command.split(), shell=False) - # decode byte string to string - output = output.decode() - for line in output.split('\n'): - (key,spl,val) = line.partition(': ') - key = key.rstrip().lower() - val = val.strip() + + if not self._lock.acquire(timeout=1): + return + try: + self._command = f"/sbin/apcaccess status {self._host}:{self._port}" # the command goes here + output = subprocess.check_output(self._command.split(), shell=False) + # decode byte string to string + output = output.decode() + # save for webinterface + self._last_readout = output + for line in output.split('\n'): + (key,spl,val) = line.partition(': ') + key = key.rstrip().lower() + val = val.strip() - if key in self._items: - self.logger.debug("update item {0} with {1}".format(self._items[key],val)) - item = self._items[key] - self.logger.debug("Item type {0}".format(item.type())) - if item.type() == 'str': - item (val, 'apcups') - else: - val = val.split(' ', 1)[0] # ignore anything after 1st space - item (float(val), 'apcups') - return + if key in self._items: + self.logger.debug(f"update item {self._items[key]} with {val}") + item = self._items[key] + self.logger.debug(f"Item type {item.type()}") + if item.type() == 'str': + item (val, self.get_shortname()) + else: + val = val.split(' ', 1)[0] # ignore anything after 1st space + item (float(val), self.get_shortname()) + return + except Exception as e: + self.logger.error(f"Problem {e} reading output from call to {self._command}") + finally: + self._lock.release() diff --git a/apcups/plugin.yaml b/apcups/plugin.yaml index b31891b4c..e8ba61906 100755 --- a/apcups/plugin.yaml +++ b/apcups/plugin.yaml @@ -5,15 +5,15 @@ plugin: description: de: 'Unterstützung für smartUPS Geräte der Firma APC' en: 'Support for smartUPS devices sold by APC' - maintainer: cmalo + maintainer: bmx tester: Sandman60 state: ready - #keywords: iot xyz + keywords: ups uninterruptible power supply #documentation: https://github.com/smarthomeNG/smarthome/wiki/CLI-Plugin # url of documentation (wiki) page #support: https://knx-user-forum.de/forum/supportforen/smarthome-py - version: 1.3.3 # Plugin version - sh_minversion: 1.3 # minimum shNG version to use this plugin + version: 1.4.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) multi_instance: True # plugin supports multi instance restartable: unknown @@ -38,7 +38,8 @@ parameters: cycle: type: int - valid_min: 0 + default: 60 + valid_min: 1 description: de: 'Zyklus-Zeit zum Update der Items mit Werten von APCaccess' en: time to update the items with values from apcaccess diff --git a/apcups/user_doc.rst b/apcups/user_doc.rst new file mode 100644 index 000000000..e8c22464f --- /dev/null +++ b/apcups/user_doc.rst @@ -0,0 +1,306 @@ +.. index:: Plugins; apcups +.. index:: apcups + + +====== +apcups +====== + + +.. image:: webif/static/img/plugin_logo.png + :alt: plugin logo + :width: 300px + :height: 300px + :scale: 50 % + :align: left + + +Anforderungen +============= + +Ein laufender ``apcupsd`` mit einem konfigurierten Netserver (NIS) ist notwendig. Dieser kann lokal oder remote laufen. +Das Plugin fragt Laufzeitdaten vom apcupsd über das Hilfsprogramm ``apcaccess`` ab. Aus diesem Grund muss das ``apcups`` Package auch lokal installiert sein. +Wenn der Daemon lokal installiert ist, sollte die Datei ``/etc/apcupsd/apcupsd.conf`` noch folgende Informationen beinhalten: + +:: + + NETSERVER on + NISPORT 3551 + NISIP 127.0.0.1 + +Unterstützte Geräte +------------------- + +Sollte mit allen APC UPS Geräten funktionieren, die den apcupsd unterstützen. Getestet wurde nur mit einer **smartUPS**. + + +Konfiguration +============= + +Die Plugin Parameter und die Informationen zur Item-spezifischen Konfiguration des Plugins sind +unter :doc:`/plugins_doc/config/apcups` beschrieben. + + +plugin.yaml +----------- + +Zu den Informationen, welche Parameter in der ../etc/plugin.yaml konfiguriert werden können bzw. müssen, bitte +bitte die Dokumentation :doc:`Dokumentation ` lesen, die aus +den Metadaten der plugin.yaml erzeugt wurde (siehe oben). + +items.yaml +---------- + +Zu den Informationen, welche Attribute in der Item Konfiguration verwendet werden können bzw. müssen, bitte +bitte die Dokumentation :doc:`Dokumentation ` lesen, die aus +den Metadaten der plugin.yaml erzeugt wurde (siehe oben). + +Es gibt nur ein einziges Attribut ``apcups``. Die Namen für den Statusabruf können über den Befehl ``apcaccess`` auf der Kommandozeile +abgerufen werden. Damit wird eine Liste der Art ``Statusname : Wert`` angezeigt. + +Der Wert der dem ``apcups`` Item Attribut als Parameter übergeben wird ist dieser Statusname. Dem Item wird dann dieser Wert zugewiesen. + +:: + + APC : 001,050,1127 + DATE : 2017-11-02 07:59:15 +0100 + HOSTNAME : sh11 + VERSION : 3.14.12 (29 March 2014) debian + UPSNAME : UPS_IDEN + CABLE : Ethernet Link + DRIVER : PCNET UPS Driver + UPSMODE : Stand Alone + STARTTIME: 2017-11-02 07:59:11 +0100 + MODEL : Smart-UPS 1400 RM + STATUS : ONLINE + LINEV : 227.5 Volts + LOADPCT : 31.2 Percent + BCHARGE : 100.0 Percent + TIMELEFT : 30.0 Minutes + MBATTCHG : 10 Percent + MINTIMEL : 5 Minutes + MAXTIME : 0 Seconds + MAXLINEV : 227.5 Volts + MINLINEV : 226.0 Volts + OUTPUTV : 227.5 Volts + SENSE : High + DWAKE : 0 Seconds + DSHUTD : 120 Seconds + DLOWBATT : 2 Minutes + LOTRANS : 208.0 Volts + HITRANS : 253.0 Volts + RETPCT : 0.0 Percent + ITEMP : 25.6 C + ALARMDEL : Low Battery + BATTV : 27.7 Volts + LINEFREQ : 49.8 Hz + LASTXFER : Line voltage notch or spike + NUMXFERS : 0 + TONBATT : 0 Seconds + CUMONBATT: 0 Seconds + XOFFBATT : N/A + SELFTEST : NO + STESTI : 336 + STATFLAG : 0x05000008 + REG1 : 0x00 + REG2 : 0x00 + REG3 : 0x00 + MANDATE : 08/16/00 + SERIALNO : GS0034003173 + BATTDATE : 06/20/15 + NOMOUTV : 230 Volts + NOMBATTV : 24.0 Volts + EXTBATTS : 0 + FIRMWARE : 162.3.I + END APC : 2017-11-02 08:00:39 +0100 + +Das Plugin führt eine automatische Typumwandlung durch entsprechend dem verwendeten Item Typ. +Bei der Umwandlung in einen numerischen Wert wird nach dem ersten Leerzeichen abgeschnitten und dann konvertiert. +Aus ``235 Volt`` wird also ``235`` + +logic.yaml +---------- + +Zu den Informationen, welche Konfigurationsmöglichkeiten für Logiken bestehen, bitte +bitte die Dokumentation :doc:`Dokumentation ` lesen, die aus +den Metadaten der plugin.yaml erzeugt wurde (siehe oben). + +Funktionen +---------- + +Zu den Informationen, welche Funktionen das Plugin bereitstellt (z.B. zur Nutzung in Logiken), bitte +bitte die Dokumentation :doc:`Dokumentation ` lesen, die aus +den Metadaten der plugin.yaml erzeugt wurde (siehe oben). + + +Beispiele +========= + +Schlüssel auslesen +~~~~~~~~~~~~~~~~~~ + +Das folgende Beispiel liest die Schlüssel **LINEV**, **STATUS** und +**TIMELEFT** und gibt deren Werte zurück. + +.. code:: yaml + + # items/apcups.yaml + serverroom: + + apcups: + + linev: + visu_acl: ro + type: num + apcups: linev + + status: + # will be 'ONLINE', 'ONBATT', or in case of a problem simply empty + visu_acl: ro + type: str + apcups: status + + timeleft: + visu_acl: ro + type: num + apcups: timeleft + +**type** hängt von den Werten ab. + +Status Report Fields +~~~~~~~~~~~~~~~~~~~~ + +Laut `APC `_) +ist die Bedeutung der Variablen wie folgt: + +:: + + APC + Header-Datensatz, der den Revisionsstand des STATUS-Formats, die Anzahl der Datensätze, die auf die APC-Anweisung folgen, und die Anzahl der Bytes, die auf den Datensatz folgen, angibt. + DATE + Das Datum und die Uhrzeit, zu der die Informationen zuletzt von der USV abgerufen wurden. + HOSTNAME + Der Name des Rechners, der die USV-Daten erfasst hat. + UPSNAME + Der Name der USV, wie er im EEPROM oder in der Direktive UPSNAME in der Konfigurationsdatei gespeichert ist. + VERSION + Die apcupsd-Versionsnummer, das Erstellungsdatum und die Plattform. + KABEL + Das Kabel, wie in der Konfigurationsdatei angegeben (UPSCABLE). + MODELL + Das USV-Modell, das aus den Informationen der USV abgeleitet wurde. + UPSMODE + Der Modus, in dem apcupsd arbeitet, wie in der Konfigurationsdatei angegeben (UPSMODE) + STARTTIME + Die Uhrzeit/das Datum, zu der/dem apcupsd gestartet wurde. + STATUS + Der aktuelle Status der USV (ONLINE, ONBATT, etc.) + LINEV + Die aktuelle Netzspannung, wie sie von der USV zurückgegeben wird. + LOADPCT + Der von der USV geschätzte Prozentsatz der Lastkapazität. + BCHARGE + Die prozentuale Ladung der Batterien. + TIMELEFT + Die von der USV geschätzte Restlaufzeit der Batterien. + MBATTCHG + Wenn der Prozentsatz der Batterieladung (BCHARGE) unter diesen Wert fällt, schaltet apcupsd Ihr System ab. Der Wert wird in der Konfigurationsdatei (BATTERYLEVEL) festgelegt. + MINTIMEL + apcupsd fährt Ihr System herunter, wenn die verbleibende Laufzeit diesen Wert erreicht oder unterschreitet. Der Wert wird in der Konfigurationsdatei festgelegt (MINUTES) + MAXTIME + apcupsd schaltet Ihr System ab, wenn die Akkulaufzeit diesen Wert überschreitet. Ein Wert von Null deaktiviert die Funktion. Der Wert wird in der Konfigurationsdatei festgelegt (TIMEOUT) + MAXLINEV + Die maximale Netzspannung seit dem Start der USV, wie von der USV gemeldet + MINLINEV + Die minimale Netzspannung seit dem Start der USV, wie von der USV zurückgemeldet + OUTPUTV + Die Spannung, die die USV an Ihre Geräte liefert + SENSE + Der Empfindlichkeitsgrad der USV gegenüber Schwankungen der Netzspannung. + DWAKE + Die Zeit, die die USV wartet, bevor sie die Stromversorgung Ihrer Geräte nach einem Stromausfall wiederherstellt, wenn die Stromversorgung wiederhergestellt ist. + DSHUTD + Die Wartezeit, die die USV nach Erhalt eines Ausschaltbefehls von apcupsd einhält, bevor sie Ihre Geräte ausschaltet. + DLOWBATT + Die verbleibende Laufzeit, bei deren Unterschreitung die USV das Signal für eine schwache Batterie sendet. An diesem Punkt erzwingt apcupsd eine sofortige Notabschaltung. + LOTRANS + Die Netzspannung, unterhalb derer die USV auf Batterien umschaltet. + HITRANS + Die Netzspannung, oberhalb derer die USV auf Batterien umschaltet. + RETPCT + Die prozentuale Ladung, die die Batterien nach einem Stromausfall haben müssen, bevor die USV die Stromversorgung Ihrer Geräte wiederherstellt. + ITEMP + Interne USV-Temperatur, wie von der USV geliefert. + ALARMDEL + Die Verzögerungszeit für den USV-Alarm. + BATTV + Batteriespannung, wie sie von der USV geliefert wird. + LINEFREQ + Netzfrequenz in Hertz, wie von der USV angegeben. + LASTXFER + Der Grund für die letzte Übertragung an die Batterien. + NUMXFERS + Die Anzahl der Übertragungen an die Batterien seit dem Start von apcupsd. + XONBATT + Uhrzeit und Datum der letzten Übertragung in die Batterien oder N/A. + TONBATT + Zeit in Sekunden, die derzeit auf Batterien übertragen wird, oder 0. + CUMONBATT + Gesamte (kumulative) Zeit auf den Batterien in Sekunden seit dem Start von apcupsd. + XOFFBATT + Zeit und Datum der letzten Übertragung von den Batterien oder N/A. + SELFTEST + Die Ergebnisse des letzten Selbsttests und können die folgenden Werte haben: + + OK: Selbsttest zeigt gute Batterie an + BT: Selbsttest wegen unzureichender Batteriekapazität fehlgeschlagen + NG: Selbsttest aufgrund von Überlastung fehlgeschlagen + NO: Keine Ergebnisse (d.h. in den letzten 5 Minuten wurde kein Selbsttest durchgeführt) + + STESTI + Das Intervall in Stunden zwischen den automatischen Selbsttests. + STATFLAG + Statusflagge. Die englische Version wird durch STATUS angegeben. + DIPSW + Die aktuellen Dip-Schalter-Einstellungen bei USVs, die über solche verfügen. + REG1 + Der Wert aus dem USV-Fehlerregister 1. + REG2 + Der Wert aus dem USV-Fehlerregister 2. + REG3 + Der Wert aus dem USV-Fehlerregister 3. + MANDATE + Das Datum, an dem die USV hergestellt wurde. + SERIENNUMMER + Die Seriennummer der USV. + BATTDATUM + Das Datum, an dem die Batterien zuletzt ausgetauscht wurden. + NOMOUTV + Die Ausgangsspannung, die die USV versucht zu liefern, wenn sie mit Batterien betrieben wird. + NOMINV + Die Eingangsspannung, für die die USV konfiguriert ist. + NOMBATTV + Die Nennspannung der Batterie. + NOMPOWER + Die maximale Leistung in Watt, für die die USV ausgelegt ist. + FEUCHTIGKEIT + Die von der USV gemessene Luftfeuchtigkeit. + AMBTEMP + Die von der USV gemessene Umgebungstemperatur. + EXTBATTEN + Die Anzahl der externen Batterien, wie vom Benutzer definiert. Eine korrekte Zahl hier hilft der USV, die verbleibende Laufzeit genauer zu berechnen. + BADBATTS + Die Anzahl der defekten Akkus. + FIRMWARE + Die Firmware-Revisionsnummer, wie von der USV gemeldet. + APCMODEL + Der alte APC-Modellidentifikationscode. + END APC + Die Uhrzeit und das Datum, an dem der STATUS-Datensatz geschrieben wurde. + +| + +Web Interface +============= + +Aktuell hat das Plugin kein Webinterface. diff --git a/apcups/webif/static/img/plugin_logo.png b/apcups/webif/static/img/plugin_logo.png new file mode 100644 index 000000000..8764febba Binary files /dev/null and b/apcups/webif/static/img/plugin_logo.png differ diff --git a/asterisk/README.md b/asterisk/README.md deleted file mode 100755 index 199a098aa..000000000 --- a/asterisk/README.md +++ /dev/null @@ -1,102 +0,0 @@ -# Asterisk - -## Requirements - -A running asterisk daemon with a configured Asterisk Manager Interface (AMI) is necessary. -In manager.config its required to enable at least: -``read = system,call,user,cdr`` and ``write = system,call,orginate`` - -## Configuration - -### plugin.yaml - -The plugin needs the username and password of the AMI and a IP and port address if asterisk does not run on localhost. - -```yaml -ast: - plugin_name: asterisk - username: admin - password: secret - host: 127.0.0.1 # default - port: 5038 # default - -``` - -### items.yaml - -#### ast_dev - -It is possible to specify the ``ast_dev`` attribute to an bool item in items.yaml. -The argument could be a number or string and correspond to the asterisk device configuration. -E.g. ``2222`` for the following device in asterisk ``sip.conf``: - -``` -[2222] -secret=very -context=internal -``` - -#### ast_box -The mailbox number of this phone. It will be set to the number of new messages in this mailbox. - -#### ast_db -Specify the database entry which will be updated at an item change. - -In items.yaml: - -```yaml -office: - - fon: - type: bool - ast_dev: 2222 - ast_db: active/office - - box: - type: num - ast_box: 22 -``` - -Calling the '2222' from sip client or making a call from it, item ``office.fon`` will be set to True. -After finishing the call, it will be set to False. - - -### logic.yaml - -It is possible to specify the `ast_userevent` keyword to every logic in logic.yaml. - -``` -logic1: - ast_userevent: Call - -logic2: - ast_userevent: Action -``` - -In the asterisk extensions.conf ``exten => _X.,n,UserEvent(Call,Source: ${CALLERID(num)},Value: ${CALLERID(name)})`` would trigger 'logic1' every time, this UserEvent is sent. - -A specified destination for the logic will be triggered e.g. ``exten => _X.,n,UserEvent(Call,Source: ${CALLERID(num)},Destination: Office,Value: ${CALLERID(name)})`` - - -### Functions - - -#### call(source, dest, context, callerid=None) - -`sh.ast.call('SIP/200', '240', 'door')` would initate a call from the SIP extention '200' to the extention '240' with the 'door' context. Optional a callerid for the call is usable. - -#### db_write(key, value) - -``sh.ast.db_write('dnd/office', 1)`` would set the asterisk db entry ``dnd/office`` to ``1``. - -#### db_read(key) - -``dnd = sh.ast.db_read('dnd/office')`` would set ``dnd`` to the value of the asterisk db entry ``dnd/office``. - -#### mailbox_count(mailbox, context='default') - -``mbc = sh.ast.mailbox_count('2222')`` would set ``mbc`` to a tuple ``(old_messages, new_messages)``. - -#### hangup(device) - -``sh.ast.hangup('30')`` would close all connections from or to the device ``30``. diff --git a/asterisk/__init__.py b/asterisk/__init__.py index a7425724e..b415b3427 100755 --- a/asterisk/__init__.py +++ b/asterisk/__init__.py @@ -31,7 +31,7 @@ class Asterisk(SmartPlugin): - PLUGIN_VERSION = "1.4.0" + PLUGIN_VERSION = "1.4.2" DB = 'ast_db' DEV = 'ast_dev' @@ -77,6 +77,92 @@ def __init__(self, sh): self._trigger_logics = {} self._log_in = lib.log.Log(self.get_sh(), 'env.asterisk.log.in', ['start', 'name', 'number', 'duration', 'direction']) + def run(self): + """ + Run method for the plugin + """ + self.logger.debug("Run method called") + if self._client.connect(): + self.alive = True + else: + self.logger.error(f'Connection to {self.host}:{self.port} not possible, plugin not starting') + + def stop(self): + """ + Stop method for the plugin + """ + self.logger.debug("Stop method called") + if self._client.connected(): + self._client.close() + self.alive = False + self._reply_lock.acquire() + self._reply_lock.notify() + self._reply_lock.release() + + + def parse_item(self, item): + """ + Default plugin parse_item method. Is called when the plugin is initialized. + The plugin can, corresponding to its attribute keywords, decide what to do with + the item in future, like adding it to an internal array for future reference + :param item: The item to process. + :return: If the plugin needs to be informed of an items change you should return a call back function + like the function update_item down below. An example when this is needed is the knx plugin + where parse_item returns the update_item function when the attribute knx_send is found. + This means that when the items value is about to be updated, the call back function is called + with the item, caller, source and dest as arguments and in case of the knx plugin the value + can be sent to the knx with a knx write function within the knx plugin. + """ + if self.has_iattr(item.conf, Asterisk.DEV): + self._devices[self.get_iattr_value(item.conf, Asterisk.DEV)] = item + if self.has_iattr(item.conf, Asterisk.BOX): + self._mailboxes[self.get_iattr_value(item.conf, Asterisk.BOX)] = item + if self.has_iattr(item.conf, Asterisk.DB): + return self.update_item + + + def parse_logic(self, logic): + """ + Default plugin parse_logic method + """ + if Asterisk.USEREVENT in logic.conf: + event = logic.conf[Asterisk.USEREVENT] + if event not in self._trigger_logics: + self._trigger_logics[event] = [logic] + else: + self._trigger_logics[event].append(logic) + + + def update_item(self, item, caller=None, source=None, dest=None): + """ + Item has been updated + + This method is called, if the value of an item has been updated by SmartHomeNG. + It should write the changed value out to the device (hardware/interface) that + is managed by this plugin. + + :param item: item to be updated towards the plugin + :param caller: if given it represents the callers name + :param source: if given it represents the source + :param dest: if given it represents the dest + """ + if self.alive and caller != self.get_shortname(): + self.logger.debug("Update item: {}, item has been changed outside this plugin".format(item.id())) + if self.has_iattr(item.conf, Asterisk.DB): + value = item() + if isinstance(value, bool): + value = int(item()) + self.db_write(self.get_iattr_value(item.conf, Asterisk.DB), value) + + + def handle_connect(self, client): + self._command(self._init_cmd, reply=False) + for mb in self._mailboxes: + mbc = self.mailbox_count(mb) + if mbc is not None: + self._mailboxes[mb](mbc[1]) + + def _command(self, d, reply=True): """ This function sends a command to the Asterisk Server @@ -150,8 +236,28 @@ def hangup(self, hang): if device == hang: self._command({'Action': 'Hangup', 'Channel': channel}, reply=False) - def found_terminator(self, data): - data = data.decode() + def found_terminator(self, client, data): + """called then terminator is found + + :param client: tcp client which is used for the connection + :param str data: the response from asterisk server + :return : None + """ + if isinstance(data, bytes): + data = data.decode() + + """ + the response will be normally like ``key: value`` + exception is start of communication which presents e.g. + ``` + Asterisk Call Manager/5.0.4 + Response: Success + Message: Authentication accepted + ``` + The following code puts everything into the dict ``event`` + It only implements a very basic inspection of the response + """ + self.logger.debug(f"data to inspect: {data}") event = {} for line in data.splitlines(): key, sep, value = line.partition(': ') @@ -246,84 +352,3 @@ def _get_device(self, channel): channel, s, d = channel.rpartition('-') a, b, channel = channel.partition('/') return channel - - def parse_item(self, item): - """ - Default plugin parse_item method. Is called when the plugin is initialized. - The plugin can, corresponding to its attribute keywords, decide what to do with - the item in future, like adding it to an internal array for future reference - :param item: The item to process. - :return: If the plugin needs to be informed of an items change you should return a call back function - like the function update_item down below. An example when this is needed is the knx plugin - where parse_item returns the update_item function when the attribute knx_send is found. - This means that when the items value is about to be updated, the call back function is called - with the item, caller, source and dest as arguments and in case of the knx plugin the value - can be sent to the knx with a knx write function within the knx plugin. - """ - if self.has_iattr(item.conf, Asterisk.DEV): - self._devices[self.get_iattr_value(item.conf, Asterisk.DEV)] = item - if self.has_iattr(item.conf, Asterisk.BOX): - self._mailboxes[self.get_iattr_value(item.conf, Asterisk.BOX)] = item - if self.has_iattr(item.conf, Asterisk.DB): - return self.update_item - - def update_item(self, item, caller=None, source=None, dest=None): - """ - Item has been updated - - This method is called, if the value of an item has been updated by SmartHomeNG. - It should write the changed value out to the device (hardware/interface) that - is managed by this plugin. - - :param item: item to be updated towards the plugin - :param caller: if given it represents the callers name - :param source: if given it represents the source - :param dest: if given it represents the dest - """ - if self.alive and caller != self.get_shortname(): - self.logger.debug("Update item: {}, item has been changed outside this plugin".format(item.id())) - if self.has_iattr(item.conf, Asterisk.DB): - value = item() - if isinstance(value, bool): - value = int(item()) - self.db_write(self.get_iattr_value(item.conf, Asterisk.DB), value) - - def parse_logic(self, logic): - """ - Default plugin parse_logic method - """ - if Asterisk.USEREVENT in logic.conf: - event = logic.conf[Asterisk.USEREVENT] - if event not in self._trigger_logics: - self._trigger_logics[event] = [logic] - else: - self._trigger_logics[event].append(logic) - - def run(self): - """ - Run method for the plugin - """ - self.logger.debug("Run method called") - if self._client.connect(): - self.alive = True - else: - self.logger.error(f'Connection to {self.host}:{self.port} not possible, plugin not starting') - - def handle_connect(self): - self._command(self._init_cmd, reply=False) - for mb in self._mailboxes: - mbc = self.mailbox_count(mb) - if mbc is not None: - self._mailboxes[mb](mbc[1]) - - def stop(self): - """ - Stop method for the plugin - """ - self.logger.debug("Stop method called") - if self._client.connected(): - self._client.close() - self.alive = False - self._reply_lock.acquire() - self._reply_lock.notify() - self._reply_lock.release() diff --git a/asterisk/plugin.yaml b/asterisk/plugin.yaml index 18d1020ed..7a8772e9f 100755 --- a/asterisk/plugin.yaml +++ b/asterisk/plugin.yaml @@ -5,13 +5,13 @@ plugin: description: de: 'Ansteuerung einer Asterisk Telefonanlage' en: 'Control of an Asterisk PBX' - maintainer: 'Nobody' + maintainer: 'bmxp' tester: 'Nobody' # Who tests this plugin? keywords: PBX Asterisk VOIP ISDN state: ready documentation: https://www.smarthomeng.de/user/plugins/asterisk/README.html support: https://knx-user-forum.de/forum/supportforen/smarthome-py/ - version: 1.4.0 # Plugin version + version: 1.4.2 # Plugin version sh_minversion: 1.9.0 # minimum shNG version to use this plugin # sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) # py_minversion: 3.6 # minimum Python version to use for this plugin diff --git a/asterisk/user_doc.rst b/asterisk/user_doc.rst new file mode 100644 index 000000000..cde4ecd90 --- /dev/null +++ b/asterisk/user_doc.rst @@ -0,0 +1,182 @@ +.. index:: Plugins; asterisk +.. index:: asterisk + + +======== +asterisk +======== + + +.. image:: webif/static/img/plugin_logo.svg + :alt: plugin logo + :width: 300px + :height: 300px + :scale: 50 % + :align: left + +Ansteuerung einer **Asterisk** Telefonanlage + + +Anforderungen +============= + +... + +Notwendige Software +------------------- + +Für den Betrieb wird vorausgesetzt das eine Version von Asterisk installiert ist und der entsprechende Daemon läuft. +Es ist ebenfalls notwendig, das das Asterisk Manager Interface (AMI) installiert und funktionell ist. +In der ``manager.config`` muss mindestens +``read = system,call,user,cdr`` und ``write = system,call,orginate`` +eingerichtet sein. + +Unterstützte Geräte +------------------- + + + + +Konfiguration +============= + +Die Plugin Parameter und die Informationen zur Item-spezifischen Konfiguration des Plugins sind +unter :doc:`/plugins_doc/config/asterisk` beschrieben. + + +plugin.yaml +----------- + +Zu den Informationen, welche Parameter in der ../etc/plugin.yaml konfiguriert werden können bzw. müssen, bitte +bitte die Dokumentation :doc:`Dokumentation ` lesen, die aus +den Metadaten der plugin.yaml erzeugt wurde (siehe oben). + + + +items.yaml +---------- + +Zu den Informationen, welche Attribute in der Item Konfiguration verwendet werden können bzw. müssen, bitte +bitte die Dokumentation :doc:`Dokumentation ` lesen, die aus +den Metadaten der plugin.yaml erzeugt wurde (siehe oben). + +.. code:: yaml + + buero: + telefon: + type: bool + ast_dev: 2222 + ast_db: active/buero + + box: + type: num + ast_box: 22 + + +Bei einem Anruf bei ``2222`` von einem SIP client oder +bei Anruf von dem entsprechenden Gerät wird das Item ``buero.telefon`` auf ``True`` gesetzt. +Wird der Anruf beendet, so wird das Item ``buero.telefon`` auf ``False`` gesetzt. + +ast_dev +~~~~~~~ + +Dieses keyword kann bei einem Item mit Typ Bool verwendet werden. +Der Parameter für dieses keyword ist ein String, der identisch zu einer Gerätebenennung in der sip.conf ist. +Im unteren Beispiel also ``device22``: + +.. code:: ini + + [device22] + secret=very + context=internal + +ast_db +~~~~~~ + +Gibt den Databank Eintrag an der bei einer Item Änderung aktualisiert wird + +ast_box +~~~~~~~ + +Hier wird die Mailbox Nummer des Telefones eingegeben. Die Anzahl der neuen Nachrichten wird dann an dieses Item übermittelt +logic.yaml +---------- + +Zu den Informationen, welche Konfigurationsmöglichkeiten für Logiken bestehen, bitte +bitte die Dokumentation :doc:`Dokumentation ` lesen, die aus +den Metadaten der plugin.yaml erzeugt wurde (siehe oben). + +Beispiel für eine Logik: + +..code:: yaml + + logic1: + ast_userevent: Call + + logic2: + ast_userevent: Action + +Für jede Logik kann in der ``logic.yaml`` das keyword ``ast_userevent`` angegeben werden. + +In der Asterisk ``extensions.conf`` muss dann angegeben werden +``exten => _X.,n,UserEvent(Call,Source: ${CALLERID(num)},Value: ${CALLERID(name)})`` + +Damit würde die ``logic1`` jedesmal getriggert, wenn das UserEvent geschickt wird. + +A specified destination for the logic will be triggered e.g. +``exten => _X.,n,UserEvent(Call,Source: ${CALLERID(num)},Destination: Office,Value: ${CALLERID(name)})`` + + +Funktionen +---------- + +Zu den Informationen, welche Funktionen das Plugin bereitstellt (z.B. zur Nutzung in Logiken), bitte +bitte die Dokumentation :doc:`Dokumentation ` lesen, die aus +den Metadaten der plugin.yaml erzeugt wurde (siehe oben). + +call(source, dest, context, callerid=None) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``sh.ast.call('SIP/200', '240', 'door')`` +würde einen Anruf von der SIP extension ``200`` zur extention ``240`` starten mit dem ``door`` context. +Optional kann eine callerid mit angegeben werden + + +db_write(key, value) +~~~~~~~~~~~~~~~~~~~~ + +``sh.ast.db_write('dnd/office', 1)`` würde den Asterisk Datenbank Eintrag ``dnd/office`` auf ``1`` setzen + +db_read(key) +~~~~~~~~~~~~ + +``dnd = sh.ast.db_read('dnd/office')`` würde ``dnd`` auf den Wert des Asterisk Datenbank Eintrages ``dnd/office`` setzen. + +mailbox_count(mailbox, context='default') +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``mbc = sh.ast.mailbox_count('2222')`` würde ``mbc`` auf ein Wertetupel ``(old_messages, new_messages)`` setzen. + +hangup(device) +~~~~~~~~~~~~~~ + +``sh.ast.hangup('30')`` would close all connections from or to the device ``30``. + + +.. todo ist noch die Implementation des + Web Interface + ============= + + Das Plugin hat aktuell kein Webinterface + + Tab 1: + ---------------------- + + + + .. image:: assets/webif_tab1.jpg + :class: screenshot + + +######################################################################### +# This file is part of SmartHomeNG. +# https://www.smarthomeNG.de +# https://knx-user-forum.de/forum/supportforen/smarthome-py +# +# This file implements the web interface for the Sample plugin. +# +# 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 datetime +import time +import os +import json + +from lib.item import Items +from lib.model.smartplugin import SmartPluginWebIf + + +# ------------------------------------------ +# Webinterface of the plugin +# ------------------------------------------ + +import cherrypy +import csv +from jinja2 import Environment, FileSystemLoader + + +class WebInterface(SmartPluginWebIf): + + def __init__(self, webif_dir, plugin): + """ + Initialization of instance of class WebInterface + + :param webif_dir: directory where the webinterface of the plugin resides + :param plugin: instance of the plugin + :type webif_dir: str + :type plugin: object + """ + self.logger = plugin.logger + self.webif_dir = webif_dir + self.plugin = plugin + self.items = Items.get_instance() + + self.tplenv = self.init_template_environment() + + + @cherrypy.expose + def index(self, reload=None): + """ + Build index.html for cherrypy + + Render the template and return the html file to be delivered to the browser + + :return: contents of the template after being rendered + """ + pagelength = self.plugin.get_parameter_value('webif_pagelength') + tmpl = self.tplenv.get_template('index.html') + # add values to be passed to the Jinja2 template eg: tmpl.render(p=self.plugin, interface=interface, ...) + return tmpl.render(p=self.plugin, + webif_pagelength=pagelength, + items=sorted(self.items.return_items(), key=lambda k: str.lower(k['_path'])), + item_count=0) + + + @cherrypy.expose + def get_data_html(self, dataSet=None): + """ + Return data to update the webpage + + For the standard update mechanism of the web interface, the dataSet to return the data for is None + + :param dataSet: Dataset for which the data should be returned (standard: None) + :return: dict with the data needed to update the web page. + """ + # if dataSets are used, define them here + if dataSet == 'overview': + # get the new data from the plugin variable called _webdata + data = self.plugin._webdata + try: + data = json.dumps(data) + return data + except Exception as e: + self.logger.error(f"get_data_html exception: {e}") + if dataSet is None: + # get the new data + data = {} + + # data['item'] = {} + # for i in self.plugin.items: + # data['item'][i]['value'] = self.plugin.getitemvalue(i) + # + # return it as json the the web page + # try: + # return json.dumps(data) + # except Exception as e: + # self.logger.error("get_data_html exception: {}".format(e)) + return {} diff --git a/asterisk/webif/static/img/plugin_logo.svg b/asterisk/webif/static/img/plugin_logo.svg new file mode 100644 index 000000000..0ad939fa0 --- /dev/null +++ b/asterisk/webif/static/img/plugin_logo.svg @@ -0,0 +1,36 @@ + + + + + + + + + + diff --git a/unifi/webif/static/img/readme.txt b/asterisk/webif/static/img/readme.txt old mode 100755 new mode 100644 similarity index 100% rename from unifi/webif/static/img/readme.txt rename to asterisk/webif/static/img/readme.txt diff --git a/asterisk/webif/templates/index.html b/asterisk/webif/templates/index.html new file mode 100644 index 000000000..29d022b1f --- /dev/null +++ b/asterisk/webif/templates/index.html @@ -0,0 +1,262 @@ +{% extends "base_plugin.html" %} + +{% set logo_frame = false %} + + +{% set update_interval = 0 %} + +{% set update_interval = (200 * (log_array | length)) %} + + +{% set dataSet = 'devices_info' %} + + +{% set update_params = item_id %} + + +{% set buttons = false %} + + +{% set autorefresh_buttons = false %} + + +{% set reload_button = false %} + + +{% set close_button = false %} + + +{% set row_count = true %} + + +{% set initial_update = true %} + + +{% block pluginstyles %} + +{% endblock pluginstyles %} + + +{% block pluginscripts %} + + + + +{% endblock pluginscripts %} + + +{% block headtable %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Prompt 1{% if 1 == 2 %}{{ _('Ja') }}{% else %}{{ _('Nein') }}{% endif %}Prompt 4{{ _('Wert 4') }}
Prompt 2{{ _('Wert 2') }}Prompt 5-
Prompt 3-Prompt 6-
+{% endblock headtable %} + + + +{% block buttons %} +{% if 1==2 %} +
+ + +
+{% endif %} +{% endblock %} + + +{% set tabcount = 4 %} + + + +{% if item_count==0 %} + {% set start_tab = 2 %} +{% endif %} + + + +{% set tab1title = "" ~ p.get_shortname() ~ " Items (" ~ item_count ~ ")" %} +{% block bodytab1 %} + +
+ {{ _('Hier kommen bei Bedarf Informationen des Webinterfaces oberhalb der Tabelle hin.') }} (optional) +
+ + + + + + + + + + + + + {% for item in items %} + {% if p.has_iattr(item.conf, '') %} + + + + + + + {% endif %} + {% endfor %} + +
{{ _('Item') }}{{ _('Wert') }}
{{ item._path }}{{ item() }}
+ +
+ Etwaige Informationen unterhalb der Tabelle (optional) +
+ +{% endblock bodytab1 %} + + + +{% set tab2title = "" ~ p.get_shortname() ~ " Geräte (" ~ device_count ~ ")" %} +{% block bodytab2 %} +{% endblock bodytab2 %} + + + +{% block bodytab3 %} +{% endblock bodytab3 %} + + + +{% block bodytab4 %} +{% endblock bodytab4 %} diff --git a/avdevice/README.md b/avdevice/README.md deleted file mode 100755 index f36e753dd..000000000 --- a/avdevice/README.md +++ /dev/null @@ -1,71 +0,0 @@ -# AV Device - -## Requirements -If you want to connect to your device via RS232 (recommended) you need to install: -Serial Python module - -Install it with: -sudo pip3 install serial --upgrade - -## Supported Hardware - -Hopefully several different AV devices based on TCP or Serial RS232 connections -Tested with Pioneer (< 2016 models) and Denon AV receivers, Epson projector Oppo Bluray player - -## Changelog -### v1.6.3 -* updated webinterface implementation, realtime values, fixes - -### v1.6.2 -* implement lineending send and response parameters -* fix struct visu_acl -* show item path for dependson item instead of name -* fix handling of negative number values - -### v1.6.1 -* use property.path instead of id() -* fix folder for command files reading - -### v1.6.0 -* Implement struct features -* fixed dependency check - -### v1.5.0 -* Minor code re-write using smartplugin methods and logging -* added config file for Denon AVR1100 -* fixed Denon example -* added Web Interface -* some bug fixes - -### v1.3.6 -* Major code re-write using multiple modules and classes, minimizing complexity -* Extended "translate" functionality with wildcards -* Implemented optional waiting time between multiple commands -* Improved Keep Command handling -* Several bug fixes and tests - -### v1.3.5 -* Implemented possibility to "translate" values -* Improved Wildcard handling -* Improved code -* Added Oppo support -* Improved response and queue handling - -### v1.3.4 -* Tested full Denon support -* Implemented Dependencies -* Implemented rudimentary Wildcard handling -* Implemented Initialization commands -* Improved Queue handling and CPU usage -* Bug fixes - -### v1.3.3 -* Added Denon support -* Added option to provide min-value in config file -* Improved response handling -* Implemented possibility to reload config files -* Improved verbose logging -* Bug fixes - -### v1.3.2 -* Added and tested full Denon support diff --git a/avdevice/plugin.yaml b/avdevice/plugin.yaml index 8afaf1461..29e05e7a0 100755 --- a/avdevice/plugin.yaml +++ b/avdevice/plugin.yaml @@ -3,8 +3,8 @@ plugin: # Global plugin attributes type: interface # plugin type (gateway, interface, protocol, system, web) description: - de: 'Steuerung eines AV Gerätes über TCP/IP oder RS232 Schnittstelle' - en: 'Controlling AV devices via TCP/IP or RS232' + de: 'Steuerung eines AV Gerätes über TCP/IP oder RS232 Schnittstelle. Ersetzt durch verschiedene SmartDevice Plugins.' + en: 'Controlling AV devices via TCP/IP or RS232. Replaced by multiple SmartDevice plugins.' description_long: de: 'Steuerung eines AV Gerätes über TCP/IP oder RS232 Schnittstelle. Das Plugin unterstützt eine Vielzahl von AV-Geräten und wurde mit folgenden Geräten getestet: @@ -12,6 +12,7 @@ plugin: - Denon AV Receiver > 2016 - Epson Projektor < 2010 - Oppo UHD Player + Zu diesen Geräten gibt es eigene neue Plugins, die auf dem SmartDevice Framework aufbauen. ' en: 'Controlling AV devices via TCP/IP or RS232 The plugin supports a variety of AV devices and was tested with the following models: @@ -19,13 +20,14 @@ plugin: - Denon AV Receiver > 2016 - Epson Projektor < 2010 - Oppo UHD Player + For these devices there are newer separate plugins based on the SmartDevice framework. ' requirements: de: 'pyserial Python Modul' en: 'pyserial python module' maintainer: onkelandy tester: Foxi352 # Who tests this plugin? - state: ready + state: deprecated keywords: av denon pioneer epson oppo player amp receiver projector rs232 telnet tcpip remote control support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1097870-neues-plugin-av-device-f%C3%BCr-yamaha-pioneer-denon-etc diff --git a/avdevice/user_doc_en.rst b/avdevice/user_doc_en.rst deleted file mode 100755 index 922e79049..000000000 --- a/avdevice/user_doc_en.rst +++ /dev/null @@ -1,536 +0,0 @@ -.. index:: Plugins; avdevice -.. index:: avdevice - -avdevice -######## - -Configuration -============= - -.. important:: - - You can find the configuration information in the user documentation. - -.. code-block:: yaml - - # etc/plugin.yaml - avdevice: - class_name: AVDevice - class_path: plugins.avdevice - model: sc-lx86 - #instance: pioneer_one - tcp_ip: 10.0.0.130 - #tcp_port: 23 - #tcp_timeout: 1 - rs232_port: /dev/ttyUSB1 - #rs232_baudrate: 9600 - #rs232_timeout: 0.1 - #ignoreresponse: 'RGB,RGC,RGD,GBH,GHH,VTA,AUA,AUB' - #forcebuffer: 'GEH01020, GEH04022, GEH05024' - #inputignoredisplay: '' - #dependson_item: '' - #dependson_value: True - #errorresponse: E02, E04, E06 - #resetonerror: False - #depend0_power0: False - #depend0_volume0: False - #sendretries: 10 - #resendwait: 1.0 - #reconnectretries: 13 - #reconnectcycle: 10 - #secondstokeep: 50 - #responsebuffer: 5 - #autoreconnect: false - #update_exclude: '' - -Items -===== - -avdevice_zone[0-4]@[instance]: [function] ------------------------------------------ - -Specifiy the zone number and instance. If you don’t use zones you can -either use ``avdevice`` or ``avdevice_zone0`` as attributes. - -The command has to correspond to a command in the relevant text -configuration file in the avdevice plugin folder named the same as the -``model`` configured in plugin.yaml. It is important to set the correct -type for each item. The Pioneer RS232 codeset expects bool and int types -only. For example to set the listening mode to "pure direct", the item -has to be int and you set it to the value "8". If you want to use the -``translation-feature`` you should set the item to ``foo``. This feature is -explained later. - -Full item examples are included as separate yaml files for Pioneer and -Denon devices. The examples include the tested items/commands and -allow easy copy/paste. - -Special attribute values (statusupdate and reload) are described in the user documentation. - - -avdevice_zone[0-4]_speakers@[instance]: [function] --------------------------------------------------- - -Specifiy the zone number and instance. This attribute is used to switch between -Speaker A, B and AB layout. Speakers Items are special and -should be set up the way mentioned in the following example. 1 and 2 -correspond to the value the speaker command expects (for example for -Pioneer receivers < 2016). - -.. code-block:: yaml - - # items/my.yaml - Pioneer: - type: foo - - Speakers: - type: num - visu_acl: rw - avdevice_zone1: speakers - - SpeakerA: - type: bool - visu_acl: rw - avdevice_zone1_speakers: 1 - - SpeakerB: - type: bool - visu_acl: rw - avdevice_zone1_speakers: 2 - - -avdevice_zone[0-4]_depend@[instance]: [function] ------------------------------------------------- - -Specifiy the zone number and instance. The depend attribute lets you -specifiy for each item if it depends on another item/function. If you -define such a dependency several things will happen: - -- The item only gets updated/changed if the dependency is fullfilled - -- Query command of the item will get removed from the queue if the dependency is not fullfilled - -- Query command of the item will (only) get added if one of the master items gets changed and the dependency is fullfilled. - -- After connecting to the device the query command of an item only gets added if you add ``init`` to the dependency configuration. - -You can use multiple depend items and attributes even for different -zones. You can even define ``and/or`` for the dependencies by adding up to -four different groups (a, b, c, d) after the value seperated by a comma -``,``. - -You can not only define a ``master item`` but also a ``master value`` and -several standard python comparison operators. - -If you don’t set an operator and value, ``==`` and ``True`` is assumed. If -you don’t set a group, group ``a`` is assumed. This means, if you add -several dependent function without a group, the functions will get -evaluated as ``or`` and dependency is fullfilled as soon as one of the -functions/items corresponds to the given value. - -The example below shows the following dependencies: - -- The disctype will always be queried after connecting to the device (as long as you have specified a query command in the command-file) - -- Audio language and encoding will be queried after connecting to the device or as so on as the item with the ``play`` function (Oppo.Play) is True - -- The track will get updated/queried if these dependencies are fullfilled: (play is True or status is play) AND verbose is set to 2 AND audiotype is either PCM or PCM 44.1/16 - -- The trackname will get updated/queried if these dependencies are fullfilled: (play is True or status is play) AND verbose is set to 2 AND audiotype is either PCM or PCM 44.1/16 AND disctpye is one of these three values: DVD-AUDIO, CDDA, DATA-DISC - - -.. code-block:: yaml - - # items/my.yaml - Oppo: - type: foo - - Power: - visu_acl: rw - type: bool - avdevice@oppo: power - - Verbose: - visu_acl: rw - type: num - cache: 'false' - enforce_updates: 'yes' - avdevice@oppo: verbose - - Status: - visu_acl: rw - type: str - cache: 'False' - enforce_updates: 'yes' - avdevice@oppo: status - on_change: - - ..Pause = True if value == 'PAUSE' else False - - ..Stop = True if not (value == 'PLAY' or value == 'PAUSE' or value == 'INVALID') else False - - ..Play = True if value == 'PLAY' else False - - Play: - visu_acl: rw - type: bool - enforce_updates: 'yes' - avdevice@oppo: play - - Disctype: - visu_acl: rw - type: str - cache: 'False' - enforce_updates: 'yes' - avdevice@oppo: disctype - avdevice_depend@oppo: init - - Audio: - type: foo - - Language: - visu_acl: rw - type: str - cache: 'False' - enforce_updates: 'yes' - avdevice@oppo: audiolanguage - avdevice_depend@oppo: - - play - - init - - Encoding: - visu_acl: rw - type: str - cache: 'False' - enforce_updates: 'yes' - avdevice@oppo: audiotype - avdevice_depend@oppo: - - play - - init - - Track: - visu_acl: rw - type: num - cache: 'False' - enforce_updates: 'yes' - avdevice@oppo: audiotrack - avdevice_depend@oppo: - - play = True, a - - status = PLAY, a - - verbose = 2, b - - audiotype = PCM, c - - audiotype = PCM 44.1/16, c - - Trackname: - visu_acl: rw - type: str - avdevice@oppo: trackname - avdevice_depend@oppo: - - disctype = DVD-AUDIO, a - - disctype = CDDA, a - - disctype = DATA-DISC, a - - play = True, b - - status = PLAY, b - - audiotype = PCM, c - - audiotype = PCM 44.1/16, c - - verbose = 2, d - -avdevice_zone[0-4]_init@[instance]: [function] ----------------------------------------------- - -Specifiy the zone number and instance. -The init attribute lets you set a specific command to a specific value as soon as the device is connected. For example if you want to always set the verbose level to 2 as soon as the plugin connects to it (at startup and after turning on the power socket or reconnecting the cable) you can define an additional item with the attribute "avdevice_init". The value of that item (Oppo.Verbose.Init) gets appended to the command linked to the verbose item (Oppo.Verbose). - -You can use multiple init items and attributes even for different zones. - -.. code-block:: yaml - - # items/my.yaml - Oppo: - type: foo - Verbose: - type: bool - visu_acl: rw - avdevice_zone1: verbose - - Init: - visu_acl: rw - type: bool - cache: 'true' - value: 2 - avdevice_zone1_init: verbose - - Pioneer: - type: foo - - Zone1: - type: foo - - Mute: - type: bool - visu_acl: rw - avdevice_zone1: mute - - Init: - visu_acl: rw - type: bool - cache: 'true' - value: True - avdevice_zone1_init: mute - - Zone2: - type: foo - - Mute: - type: bool - visu_acl: rw - avdevice_zone2: mute - - Init: - visu_acl: rw - type: bool - cache: 'true' - value: True - avdevice_zone2_init: mute - - -Commands -======== - -Configure your commands depending on your model and manufacturer. You -have to name the file the same as configured in the plugin.yaml as -“model”. E.g. if you’ve configured ``model: vsx-923`` you name the file -``vsx-923.txt`` - -Each line holds one specific command that should be sent to the device. -You also specify the zone, the query command, response command, etc. You -can comment out lines by placing a ``#`` in front of the line. You can also -comment a whole block by using ``’’’`` at the beginning and end of a block. - -- ``zone``: Number of zone. Has to correspond to the attribute in - item.yaml. E.g. for zone 1 use “avdevice_zone1: command”. Zone 0 - holds special commands like navigating in the menu, display reponse, - information about currently playing songs, etc. - -- ``function``: name of the function. You can name it whatever you - like. You reference this value in the item using avdevice_zoneX: - function. - -- ``functiontype``: for boolean functions use “on” or “off”. For - commands setting a specific value like source, input mode, volume, - etc. use “set”. To increase or decrease a value use the corresponding - “increase” or “decrease”. For everything else leave empty! - -- ``send``: the command to be sent, e.g. power off is “PF” for Pioneer - receivers. You can use a pipe “\|” if more than one command should be - sent. Add an integer or float to specify a pause in seconds between - the commands, like “PO\|2\|PO”. That might be necessary for power on - commands via RS232, e.g. for Pioneer receivers to power on “PO|PO” - forces the plugin to send the “PO” command twice. Use stars “\*” to - specify the format of the value to be sent. Let’s say your device - expects the value for volume as 3 digits, a “\*\*\*VL” ensures that - even setting the volume to “5” sends the command as “005VL” - -- ``query``: Query command. This is usually useful after setting up the - connection or turning on the power. This command gets also used if - the plugin doesn’t receive the correct answer after sending a - command. It is recommended to leave this value empty for all - functions except on, off and set. - -- ``response``: The expected response after sending a command. Use - “none” if you don’t want to wait for the correct response. Use “\*” the same way - as with the send command. You can even specify multiple response - possibilities separated by “\|”. - -- ``readwrite``: R for read only, W for write only, RW for Read and - Write. E.g. display values are read only whereas turning the volume - up might be a write operation only. Setting this correctly ensures a - fast and reliable plugin operation - -- ``invertresponse``: some devices are stupid enough to reply with a - “0” for “on” and “1” for “off”. E.g. a Pioneer receiver responds with - “PWR0” if the device is turned on. Configure with “yes” if your - device is quite stupid, too. - -- ``minvalue``: You can define the minimum value for setting a specific - function. This might be most relevant for setting the volume or - bass/trebble values. If you configure this with “-3” and set the bass - to “-5” (via Visu or CLI) the value will get clamped by the plugin - and set to “-3”. - -- ``maxvalue``: You can define the maximum value for setting a specific - function. This might be most relevant for setting the volume. If you - configure this with “100” and set the volume to “240” (via Visu or - CLI) the value will get clamped by the plugin and set to “100”. - -- ``responsetype``: Defines the type of the response value and can be - set to “bool”, “num” or “str” or a mixture of them (separated by a - pipe “\|” or comma “,”). Most response types are set automatically on - startup but you can force a specific type using this value. It is - recommended to use the values suggested in the txt files that come - with the plugin. - -- ``translationfile``: If you want to translate a specific value/code - to something else, define the name of a txt file in the translation folder - here that holds the information on how to translate which value. This feature - is described later in more detail. - -.. code-block:: none - - # plugins/avdevice/pioneer.txt - ZONE; FUNCTION; FUNCTIONTYPE; SEND; QUERY; RESPONSE; READWRITE; INVERTRESPONSE; MINVALUE; MAXVALUE; RESPONSETYPE; TRANSLATIONFILE - 1; power; on; PO|PO; ?P; PWR*; RW; yes - 1; power; off; PF; ?P; PWR*; RW; yes - 1; volume+; increase; VU; ; VOL; W - 1; volume-; decrease; VD; ; VOL; W - 1; volume; set; ***VL; ?V; VOL***; RW; ; 80; 185 - 1; input; set; **FN; ?F; FN**; RW - 1; speakers; set; *SPK; ?SPK; SPK*; RW - ''' - #commented out from here - 2; power; on; APO|APO; ?AP; APR*; RW; yes - 2; power; off; APF; ?AP; APR*; RW; yes - 0; title; ; ; ; GEH01020; R - 0; station; ; ; ; GEH04022; R - 0; genre; ; ; ; GEH05024; R - #commented out until here - ''' - 0; display; ; ?FL; ?FL; FL******************************; R - 1; input; set; **FN; ?F; FN**; RW; ; ; ; ; pioneer_input - 1; mode; set; ****SR; ?S; SR****; RW; ; ; ; num; pioneer_SR - 1; playingmode; ; ?L; ?L; LM****; R; ; ; ; str,int; pioneer_LM - #0; test; ; ; ; noidea; R (commented out) - - -Struct Templates -================ - -Since smarthomeNG 1.6 you can use templates provided by the plugin: - -- general: Display, menu, cursor, statusupdate, reload config, etc. -- speaker_selection: speaker A, B or both -- individual_volume: set the volume of each speaker individually -- sound_settings: listening Mode, bass, trebble, dynamic compression, etc. -- video_settings: aspect Ratio, monitorout, etc. -- zone1, zone2, zone3: several relevant functions like source, volume, etc. - -The templates might include too many items or items your device does not support. As long as there is no command in the models/model.txt file, the items are just ignored. So no problem! - - -Translations -============ - -You could create a file called denon_volume.txt and link it -in your model.txt file to convert 3 digit volume to a float. Denon -receivers handle e.g. 50.5 as 505. If you want to use value limits or -visualize the volume correctly in your VISU you should use the following -translation file: - -.. code-block:: none - - # plugins/avdevice/denon_volume.txt - CODE; TRANSLATION - ***; **.* - -Pioneer receivers use numbers to define input source or listening mode -what is very cryptic and not very user friendly. Therefore you should -use the relevant files in the plugins folder like pioneer_input. That -file looks something like this: - -.. code-block:: none - - # plugins/avdevice/pioneer_input.txt - CODE; TRANSLATION - 00; PHONO - 01; CD - 02; TUNER - -Now, when the plugin receives FN01 as a response, the response gets -converted to “CD”. Vice versa you can even update your item to “CD” and -the plugin will send “01FN” as a command. It is advised to define the -according item as ``type: foo`` so you can either use a number or string, -just the way you like. - - -Wildcards -========= - -For the model.txt file you can use question marks as a wild card if the -response of the device includes information for several different items. -This is the case with a lot of responses from Oppo bluray players. - -Use a “?” for “any single character”, use “??” for “two characters of -any value” and so on. If the length of the wildcard can differ, use a -“?{str}” meaning that the plugin expects a string of any given length. - -The definition for audiotype in the example means that the expected -response consists of: “@QAT OK” in the beginning followed by a single -character followed by a “/” and another single character again. After -that is the relevant part of the response, the value of the item, -defined by exactly three digits/characters. Behind that is a blank and -any value consisting of five characters or digits. - -The example definition for audiotrack means that the response can be: -“@UAT” followed by any word/number without a specific length, followed -by a blank and the real value consisting of two characters. The response -could also start with “@QTK OK” followed by the relevant value -consisting of exactly one digit/character. After that there will be a -“/” and any character/digit. It is important to add the “/?” in the end -because the plugin also compares the length of the response with the -expected length (calculated from the response in the command-file). It -is not relevant, if you use a {str} in your response because then the -length can not be determined. - -This feature is still under development. Feel free to experiment with it -and post your experience in the knx-forum. - -.. code-block:: none - - # plugins/avdevice/oppo-udp203.txt - ZONE; FUNCTION; FUNCTIONTYPE; SEND; QUERY; RESPONSE; READWRITE; INVERTRESPONSE; MINVALUE; MAXVALUE; RESPONSETYPE; TRANSLATIONFILE - 0; audiotype; ; ; #QAT; @QAT OK ?/? *** ?????; R; ; ; ; str - 0; audiotrack; ; #AUD; #QTK; @UAT ?{str} **|@QTK OK */?; RW; ; ; ; num - - -Webinterface -============ - -Use the web interface to see which item using the plugin is set to which value. -Furthermore you can see a history of the commands and queries being sent by the -plugin. You can also use the web interface to reload your configuration file. - -.. image:: avdevice_webif.png - :height: 1618px - :width: 3338px - :scale: 25% - :alt: Web Interface - :align: center - -Troubleshooting -=============== -1.) Have a look at the webinterface. You'll figure out the item ids and values -as well as a history of the commands. - -2.) Have a look at the smarthome logfile. If you can’t figure out the -reason for your problem, change the verbose level in logging.yaml. You -can use level 10 (=DEBUG), 9 (VERBOSE1) and 8 (VERBOSE2) as debugging -levels. - -3.) Concerning send and response entries in the text file, make sure the -number of stars correspond to the way your device wants to receive the -command or sends the response. Example 1: Your Pioneer receiver expects -the value for the volume as three digits. So the command needs three -stars. If you now set the item to a value with only two digits, like 90, -the plugin converts the command automatically to have a leading 0. -Example 2: Your Denon receiver responds with values like ON, OFF or -STANDBY to power commands. Replace every character with a star! ON = 2 -stars, OFF = 3 stars, etc. Example 3: Sending or receiving strings of -different length like “CD”, “GAME”, etc. should be set up with one star -only. Alternatively you can use "\*{str}". Set the responsetype -accordingly! - -4.) Set the response type in the textfile to the correct value. The -plugin tries to anticipate the correct value but that doesn’t always -work. The sleep timer of Denon devices is a wonderfully sick example: -You can set values between 1 and 120 to set the timer in minutes. If you -want to turn it off, the receiver expects the value “OFF” instead of a -zero. The plugin fixes that problem if you set the responsetype to -bool|num. As soon as you set the item to 0, it magically converts that -value to “OFF” and the other way around when receiving “OFF”. diff --git a/avdevice/webif/templates/index.html b/avdevice/webif/templates/index.html index d86af636b..75fd9d722 100755 --- a/avdevice/webif/templates/index.html +++ b/avdevice/webif/templates/index.html @@ -18,33 +18,24 @@ - -/* - * The combined file was created by the DataTables downloader builder: - * https://datatables.net/download - * - * To rebuild or modify this file with the latest versions of the included - * software please visit: - * https://datatables.net/download/#dt/dt-1.10.21/fh-3.1.7/r-2.2.5 - * - * Included libraries: - * DataTables 1.10.21, FixedHeader 3.1.7, Responsive 2.2.5 - */ - - - {% endblock pluginscripts %} @@ -66,27 +73,8 @@ {% endblock headtable %} - - -{% block buttons %} -{% if 1==2 %} -
- -
-{% endif %} -{% endblock %} - - {% set tabcount = 3 %} - - {% set item_count = p._items|length %} {% if item_count==0 %} {% set start_tab = 1 %} @@ -101,7 +89,7 @@ {% set tab1title = "" ~ _("Letzte Auslesung") ~ "" %} {% block bodytab1 %}
-
{{ p._lastresultstr }}
+
{{ p._lastresultstr }}
{% endblock bodytab1 %} @@ -111,28 +99,19 @@ --> {% set tab2title = "" ~ item_count ~ " " ~ _("Items definiert") ~ "" %} {% block bodytab2 %} -
-
- - - - - - - - + +
{{ _('Item') }}{{ _('jsonread_filter') }}{{ _('Wert') }}
{% for item in p._items %} - - - - + + + + {% endfor %}
{{ item._path }}{{ p.get_iattr_value(item.conf, 'jsonread_filter') }}{{ item() }}
{{ item._path }}{{ p.get_iattr_value(item.conf, 'jsonread_filter') }}{{ item() }}
-
-
+ {% endblock bodytab2 %} {% block pluginscripts %} {% endblock pluginscripts %} {% block headtable %} - - @@ -152,16 +130,16 @@ Set the tab title --> {% set tab1title = "" ~ p.get_shortname() ~ " Items (" ~ item_count ~ ")" %} -{% set tab2title = "" ~ p.get_shortname() ~ " Obis Data" %} +{% set tab2title = "" ~ p.get_shortname() ~ " verfügbare Obis Daten" %} {% if '1-0:1.8.0*255' in p.values %} - {% set tab3title = "" ~ p.get_shortname() ~ " Zählerstatus" %} + {% set tab3title = "" ~ p.get_shortname() ~ " Zählerstatus" %} {% else %} - {% set tab3title = "hidden" %} + {% set tab3title = "hidden" %} {% endif %} {% if maintenance %} - {% set tab4title = "" ~ p.get_shortname() ~ " Maintenance" %} + {% set tab4title = "" ~ p.get_shortname() ~ " Maintenance" %} {% else %} - {% set tab4title = "hidden" %} + {% set tab4title = "hidden" %} {% endif %} {% block bodytab1 %} -
-
- - - - - - - - - - - - {% for item in items %} - - - - - - - - - {% endfor %} - -
{{ _('Item') }}{{ _('Attribut') }}{{_('Typ')}}{{_('Wert')}}{{_('Letztes Update')}}{{_('Letzter Change')}}
{{ item._path }}{{ p.get_iattr_value(item.conf, 'sml_obis') }}{{ item._type }}.{{ item._value }}{{ item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') }}{{ item.property.last_change.strftime('%d.%m.%Y %H:%M:%S') }}
- - + + + + + + + + + + + + + {% for item in items %} + + + + + + + + + {% endfor %} + +
{{ _('Item') }}{{ _('Attribut') }}{{_('Typ')}}{{_('Wert')}}{{_('Letztes Update')}}{{_('Letzter Change')}}
{{ item._path }}{{ p.get_iattr_value(item.conf, 'sml_obis') }}{{ item._type }}.{{ item._value }}{{ item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') }}{{ item.property.last_change.strftime('%d.%m.%Y %H:%M:%S') }}
{% endblock bodytab1 %} {% block bodytab2 %} -
-
- OBIS Raw data - - - - - - - - - - {% for entry in p.values %} - - - - - - {% endfor %} - -
{{ _('OBIS Codes') }}{{ _('Data') }}{{ _('Unit') }}
{{ entry }}{{ p.values[entry]['name'] }}{{ p.values[entry]['unit'] }}
+
+ OBIS Raw data
+ + + + + + + + + + {% for entry in p.values %} + + + + + + {% endfor %} + +
{{ _('OBIS Codes') }}{{ _('Data') }}{{ _('Unit') }}
{{ entry }}{{ p.values[entry]['name'] }}{{ p.values[entry]['unit'] }}
{% endblock bodytab2 %} {% block bodytab3 %} -
- {% if '1-0:1.8.0*255' in p.values %} - - - - - - - - - {% if 'statRun' in p.values['1-0:1.8.0*255'] %} - - - - - {% endif %} - {% if 'statFraudMagnet' in p.values['1-0:1.8.0*255'] %} - - - - - {% endif %} - {% if 'statFraudCover' in p.values['1-0:1.8.0*255'] %} - - - - - {% endif %} - {% if 'statEnergyTotal' in p.values['1-0:1.8.0*255'] %} - - - - - {% endif %} - {% if 'statEnergyL1' in p.values['1-0:1.8.0*255'] %} - - - - - {% endif %} - {% if 'statEnergyL2' in p.values['1-0:1.8.0*255'] %} - - - - - {% endif %} - {% if 'statEnergyL3' in p.values['1-0:1.8.0*255'] %} - - - - - {% endif %} - {% if 'statVoltageL1' in p.values['1-0:1.8.0*255'] %} - - - - - {% endif %} - {% if 'statVoltageL2' in p.values['1-0:1.8.0*255'] %} - - - - - {% endif %} - {% if 'statVoltageL3' in p.values['1-0:1.8.0*255'] %} - - - - - {% endif %} - {% if 'statRotaryField' in p.values['1-0:1.8.0*255'] %} - - - - - {% endif %} - {% if 'statBackstop' in p.values['1-0:1.8.0*255'] %} - - - - - {% endif %} - {% if 'statCalFault' in p.values['1-0:1.8.0*255'] %} - - - - - {% endif %} - -
{{ _('Status') }}{{ _('Value') }}
{{ _('Zähler in Betrieb') }}{% if p.values['1-0:1.8.0*255']['statRun'] %}{{ _('Ja') }}{% else %}{{ _('Nein') }}{% endif %}
{{ _('magnetische Manipulation') }}{% if p.values['1-0:1.8.0*255']['statFraudMagnet'] %}{{ _('Ja') }}{% else %}{{ _('Nein') }}{% endif %}
{{ _('Manipulation der Abdeckung') }}{% if p.values['1-0:1.8.0*255']['statFraudCover'] %}{{ _('Ja') }}{% else %}{{ _('Nein') }}{% endif %}
{{ _('Stromfluss gesamt') }}{% if p.values['1-0:1.8.0*255']['statEnergyTotal'] %}{{ _('-A') }}{% else %}{{ _('+A') }}{% endif %}
{{ _('Stromfluss L1') }}{% if p.values['1-0:1.8.0*255']['statEnergyL1'] %}{{ _('-A') }}{% else %}{{ _('+A') }}{% endif %}
{{ _('Stromfluss L2') }}{% if p.values['1-0:1.8.0*255']['statEnergyL2'] %}{{ _('-A') }}{% else %}{{ _('+A') }}{% endif %}
{{ _('Stromfluss L3') }}{% if p.values['1-0:1.8.0*255']['statEnergyL3'] %}{{ _('-A') }}{% else %}{{ _('+A') }}{% endif %}
{{ _('Spannung an L1') }}{% if p.values['1-0:1.8.0*255']['statVoltageL1'] %}{{ _('OK') }}{% else %}{{ _('NOK') }}{% endif %}
{{ _('Spannung an L2') }}{% if p.values['1-0:1.8.0*255']['statVoltageL2'] %}{{ _('OK') }}{% else %}{{ _('NOK') }}{% endif %}
{{ _('Spannung an L3') }}{% if p.values['1-0:1.8.0*255']['statVoltageL3'] %}{{ _('OK') }}{% else %}{{ _('NOK') }}{% endif %}
{{ _('Drehfeld') }}{% if p.values['1-0:1.8.0*255']['statRotaryField'] %}{{ _('NOK') }}{% else %}{{ _('OK') }}{% endif %}
{{ _('Backstop') }}{% if p.values['1-0:1.8.0*255']['statBackstop'] %}{{ _('Active') }}{% else %}{{ _('Nein') }}{% endif %}
{{ _('Fataler Fehler') }}{% if p.values['1-0:1.8.0*255']['statCalFault'] %}{{ _('FAULT') }}{% else %}{{ _('Keiner') }}{% endif %}
- {% endif %} -
+{% if '1-0:1.8.0*255' in p.values %} + + + + + + + + + {% if 'statRun' in p.values['1-0:1.8.0*255'] %} + + + + + {% endif %} + {% if 'statFraudMagnet' in p.values['1-0:1.8.0*255'] %} + + + + + {% endif %} + {% if 'statFraudCover' in p.values['1-0:1.8.0*255'] %} + + + + + {% endif %} + {% if 'statEnergyTotal' in p.values['1-0:1.8.0*255'] %} + + + + + {% endif %} + {% if 'statEnergyL1' in p.values['1-0:1.8.0*255'] %} + + + + + {% endif %} + {% if 'statEnergyL2' in p.values['1-0:1.8.0*255'] %} + + + + + {% endif %} + {% if 'statEnergyL3' in p.values['1-0:1.8.0*255'] %} + + + + + {% endif %} + {% if 'statVoltageL1' in p.values['1-0:1.8.0*255'] %} + + + + + {% endif %} + {% if 'statVoltageL2' in p.values['1-0:1.8.0*255'] %} + + + + + {% endif %} + {% if 'statVoltageL3' in p.values['1-0:1.8.0*255'] %} + + + + + {% endif %} + {% if 'statRotaryField' in p.values['1-0:1.8.0*255'] %} + + + + + {% endif %} + {% if 'statBackstop' in p.values['1-0:1.8.0*255'] %} + + + + + {% endif %} + {% if 'statCalFault' in p.values['1-0:1.8.0*255'] %} + + + + + {% endif %} + +
{{ _('Status') }}{{ _('Value') }}
{{ _('Zähler in Betrieb') }}{% if p.values['1-0:1.8.0*255']['statRun'] %}{{ _('Ja') }}{% else %}{{ _('Nein') }}{% endif %}
{{ _('magnetische Manipulation') }}{% if p.values['1-0:1.8.0*255']['statFraudMagnet'] %}{{ _('Ja') }}{% else %}{{ _('Nein') }}{% endif %}
{{ _('Manipulation der Abdeckung') }}{% if p.values['1-0:1.8.0*255']['statFraudCover'] %}{{ _('Ja') }}{% else %}{{ _('Nein') }}{% endif %}
{{ _('Stromfluss gesamt') }}{% if p.values['1-0:1.8.0*255']['statEnergyTotal'] %}{{ _('-A') }}{% else %}{{ _('+A') }}{% endif %}
{{ _('Stromfluss L1') }}{% if p.values['1-0:1.8.0*255']['statEnergyL1'] %}{{ _('-A') }}{% else %}{{ _('+A') }}{% endif %}
{{ _('Stromfluss L2') }}{% if p.values['1-0:1.8.0*255']['statEnergyL2'] %}{{ _('-A') }}{% else %}{{ _('+A') }}{% endif %}
{{ _('Stromfluss L3') }}{% if p.values['1-0:1.8.0*255']['statEnergyL3'] %}{{ _('-A') }}{% else %}{{ _('+A') }}{% endif %}
{{ _('Spannung an L1') }}{% if p.values['1-0:1.8.0*255']['statVoltageL1'] %}{{ _('OK') }}{% else %}{{ _('NOK') }}{% endif %}
{{ _('Spannung an L2') }}{% if p.values['1-0:1.8.0*255']['statVoltageL2'] %}{{ _('OK') }}{% else %}{{ _('NOK') }}{% endif %}
{{ _('Spannung an L3') }}{% if p.values['1-0:1.8.0*255']['statVoltageL3'] %}{{ _('OK') }}{% else %}{{ _('NOK') }}{% endif %}
{{ _('Drehfeld') }}{% if p.values['1-0:1.8.0*255']['statRotaryField'] %}{{ _('NOK') }}{% else %}{{ _('OK') }}{% endif %}
{{ _('Backstop') }}{% if p.values['1-0:1.8.0*255']['statBackstop'] %}{{ _('Active') }}{% else %}{{ _('Nein') }}{% endif %}
{{ _('Fataler Fehler') }}{% if p.values['1-0:1.8.0*255']['statCalFault'] %}{{ _('FAULT') }}{% else %}{{ _('Keiner') }}{% endif %}
+{% endif %} {% endblock bodytab3 %} {% block bodytab4 %} -
- - - - - - - - - - - - - - - - - - - - - - - - - -
{{ _('dict/list') }}{{ _('count') }}{{ _('content') }}
{{ _('_items') }}{{ len(p._items) }}{{ p._items }}
{{ _('_item_dict') }}{{ len(p._item_dict) }}{{ p._item_dict }}
{{ _('values') }}{{ len(p.values) }}{{ p.values }}
-
+ + + + + + + + + + + + + + + + + + + + + + + + + +
{{ _('dict/list') }}{{ _('count') }}{{ _('content') }}
{{ _('_items') }}{{ len(p._items) }}{{ p._items }}
{{ _('_item_dict') }}{{ len(p._item_dict) }}{{ p._item_dict }}
{{ _('values') }}{{ len(p.values) }}{{ p.values }}
{% endblock bodytab4 %} diff --git a/solarforecast/__init__.py b/solarforecast/__init__.py index 065bf8cdb..53b8752b6 100755 --- a/solarforecast/__init__.py +++ b/solarforecast/__init__.py @@ -32,7 +32,7 @@ class Solarforecast(SmartPlugin): - PLUGIN_VERSION = '1.9.1' + PLUGIN_VERSION = '1.9.2' def __init__(self, sh): """ @@ -130,13 +130,14 @@ def poll_backend(self): statusCode = sessionrequest_response.status_code if statusCode == 200: + self.logger.debug("Sending session request command successful") pass - #self.logger.debug("Sending session request command successful") else: self.logger.error(f"Server error: {statusCode}") return responseJson = sessionrequest_response.json() + self.logger.debug(f"Json response: {responseJson}") # Decode Json data: wattHoursToday = None @@ -150,27 +151,27 @@ def poll_backend(self): resultJson = responseJson['result'] if 'watt_hours_day' in resultJson: wattHoursJson = resultJson['watt_hours_day'] -# self.logger.debug(f"wattHourJson: {wattHoursJson}") + # self.logger.debug(f"wattHourJson: {wattHoursJson}") if str(today) in wattHoursJson: wattHoursToday = float(wattHoursJson[str(today)]) if str(tomorrow) in wattHoursJson: wattHoursTomorrow = float(wattHoursJson[str(tomorrow)]) -# self.logger.debug(f"Ertrag today {wattHoursToday/1000} kWh, tomorrow: {wattHoursTomorrow/1000} kwH") +# self.logger.debug(f"Ertrag today {wattHoursToday/1000} kWh, tomorrow: {wattHoursTomorrow/1000} kwH") for attribute, matchStringItems in self._items.items(): -# if not self.alive: -# return + if not self.alive: + return # self.logger.warning("DEBUG: attribute: {0}, matchStringItems: {1}".format(attribute, matchStringItems)) value = None - if attribute == 'power_today': + if attribute == 'energy_today': value = wattHoursToday - elif attribute == 'power_tomorrow': + elif attribute == 'energy_tomorrow': value = wattHoursTomorrow elif attribute == 'date_today': value = str(today) @@ -181,7 +182,7 @@ def poll_backend(self): if value is not None: for sameMatchStringItem in matchStringItems: sameMatchStringItem(value, self.get_shortname() ) -# self.logger.debug('_update: Value "{0}" written to item {1}'.format(value, sameMatchStringItem)) + self.logger.debug('_update: Value "{0}" written to item {1}'.format(value, sameMatchStringItem)) pass def get_items(self): diff --git a/solarforecast/plugin.yaml b/solarforecast/plugin.yaml index 15ba12618..d4bcf7d26 100755 --- a/solarforecast/plugin.yaml +++ b/solarforecast/plugin.yaml @@ -6,20 +6,19 @@ plugin: de: 'Plugin zur Anbindung an eine Web-basierte Solaretragsvorhersage' en: 'Plugin to connect to a web-based solar forecast service' maintainer: Alexander Schwithal (aschwith) - tester: henfri + tester: henfri, Haiphong state: develop # change to ready when done with development keywords: solar.forecast, solar # documentation: support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1842817-support-thread-f%C3%BCr-das-solarforecast-plugin - version: 1.9.1 # Plugin version + version: 1.9.2 # Plugin version sh_minversion: 1.8.0 # minimum shNG version to use this plugin # sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) - multi_instance: False # plugin supports multi instance + multi_instance: True # plugin supports multi instance restartable: unknown classname: Solarforecast # class containing the plugin - parameters: latitude: type: num @@ -33,19 +32,19 @@ parameters: mandatory: False default: 0.0 description: - de: '(Optional) Laengengrad der Solaranlage in dezimalen Grad. ' + de: '(Optional) Längengrad der Solaranlage in dezimalen Grad. ' en: '(Optional) Longitude of solar system in decimal degree. Otherwise taken from smarthome config.' declination: type: num mandatory: True description: - de: 'Declinationswinkel Solaranlage in Grad' + de: 'Deklinationswinkel Solaranlage in Grad' en: 'Panel declination in degree' azimuth: type: num mandatory: True description: - de: 'Azimutwinkel der Panelausrichtung. 0 Grad enstspricht Sueden' + de: 'Azimutwinkel der Panelausrichtung. 0 Grad entspricht Süden' en: 'Azimuth angle of panel direction. 0 degree is equivalent to southern direction' kwp: type: num @@ -62,7 +61,7 @@ parameters: de: ['solar.forecast'] en: ['solar.forecast'] description: - de: 'Webservice fuer Vorhersage' + de: 'Webservice für Vorhersage' en: 'Webservice for forecast' @@ -72,18 +71,18 @@ item_attributes: type: str description: de: 'Solarforecast Attribute: - Vorhersage Leistung morgen - Vorhersage Leistung heute + Vorhersage Energie in Wh morgen + Vorhersage Energie in Wh heute Datum morgen Datum heute' en: 'Solarforecast attributes: - Forecast power tomorrow - Forecast power today + Forecast energy in Wh tomorrow + Forecast energy in Wh today Date tomorrow Date today' valid_list: - - power_tomorrow - - power_today + - energy_tomorrow + - energy_today - date_tomorrow - date_today diff --git a/solarforecast/user_doc.rst b/solarforecast/user_doc.rst index 26100f5c5..5d0d2868f 100755 --- a/solarforecast/user_doc.rst +++ b/solarforecast/user_doc.rst @@ -12,7 +12,7 @@ solarforecast :scale: 50 % :align: left -Dieses Plugin unterstützt Solare.forecast Vorhersagen von Solaretrag (Leistung). +Dieses Plugin unterstützt Solare.forecast Vorhersagen von Solaretrag (Energieertrag). Für weitere Informationen empfiehlt sich die Lektüre der offiziellen `Solar.forecast API Dokumentation `_ @@ -31,7 +31,7 @@ The plugin does not need a license key in public mode. An API key can be optaine Beispiele ========= -Beispiel für jeweils zwei Items mit vorhergesagtem Leistungsertrag für heute und morgen. +Beispiel für jeweils zwei Items mit vorhergesagtem Energieertrag für heute und morgen. .. code:: yaml @@ -40,7 +40,8 @@ Beispiel für jeweils zwei Items mit vorhergesagtem Leistungsertrag für heute u today: type: num visu_acl: ro - solarforecast_attribute: power_today + name: Forecast energy for today in Wh + solarforecast_attribute: energy_today date: type: str @@ -50,14 +51,14 @@ Beispiel für jeweils zwei Items mit vorhergesagtem Leistungsertrag für heute u tomorrow: type: num visu_acl: ro - solarforecast_attribute: power_tomorrow + name: Forecast energy for tomorrow in Wh + solarforecast_attribute: energy_tomorrow date: type: str visu_acl: ro solarforecast_attribute: date_tomorrow - Web Interface ============= diff --git a/sonos/__init__.py b/sonos/__init__.py index 0654fde61..1a04ce5d3 100755 --- a/sonos/__init__.py +++ b/sonos/__init__.py @@ -331,7 +331,7 @@ def __init__(self, uid, logger, plugin_shortname): self.logger = logger self.plugin_shortname = plugin_shortname self.uid_items = [] - self._uid = "" + self._uid = uid self._soco = None self._events = None self._zone_group = [] @@ -2271,7 +2271,7 @@ def play_tunein(self, station_name: str, start: bool = True) -> None: if not self.is_coordinator: sonos_speaker[self.coordinator].play_tunein(station_name, start) else: - result, msg = self._play_radio(station_name=station_name, music_service='TuneIn', start=start) + result, msg = self._play_tunein(station_name=station_name, music_service='TuneIn', start=start) if not result: self.logger.warning(msg) return False @@ -2298,7 +2298,7 @@ def play_sonos_radio(self, station_name: str, start: bool = True) -> None: return False return True - def _play_radio(self, station_name: str, music_service: str = 'TuneIn', start: bool = True) -> tuple: + def _play_tunein(self, station_name: str, music_service: str = 'TuneIn', start: bool = True) -> tuple: """ Old legacy radio select function. The function is not based on the SoCo library. Plays a radio station by a given radio name. If more than one radio station are found, the first result will be @@ -2314,9 +2314,10 @@ def _play_radio(self, station_name: str, music_service: str = 'TuneIn', start: b # if a patch is applied. # ------------------------------------------------------------------------------------------------------------ # - + self.logger.warning(f"DEBUG: _play_tunein start") if not self._check_property(): return False, "Property check failed" + if not self.is_coordinator: sonos_speaker[self.coordinator].play_tunein(station_name, start) else: @@ -2354,6 +2355,8 @@ def _play_radio(self, station_name: str, music_service: str = 'TuneIn', start: b return False, "response should contain either the key 'searchResult' or 'getMetadataResult'" for result_type in ('mediaCollection', 'mediaMetadata'): + self.logger.warning(f"DEBUG loop: result is {result_type}") + # Upper case the first letter (used for the class_key) result_type_proper = result_type[0].upper() + result_type[1:] raw_items = response.get(result_type, []) @@ -2365,6 +2368,9 @@ def _play_radio(self, station_name: str, music_service: str = 'TuneIn', start: b # Form the class_key, which is a unique string for this type, # formed by concatenating the result type with the item type. Turns # into e.g: MediaMetadataTrack + + self.logger.warning(f"DEBUG loop 2: raw_item is {raw_item}") + class_key = result_type_proper + raw_item['itemType'].title() cls = get_class(class_key) #from plugins.sonos.soco.music_services.token_store import JsonFileTokenStore @@ -2373,6 +2379,7 @@ def _play_radio(self, station_name: str, music_service: str = 'TuneIn', start: b #cls.from_music_service(MusicService(service_name='TuneIn', token_store=JsonFileTokenStore()), raw_item)) if not items: + self.logger.warning(f"DEBUG _play radio: No matching items found") exit(0) item_id = items[0].metadata['id'] @@ -2385,14 +2392,16 @@ def _play_radio(self, station_name: str, music_service: str = 'TuneIn', start: b self.soco.avTransport.SetAVTransportURI([('InstanceID', 0), ('CurrentURI', uri), ('CurrentURIMetaData', meta)]) if start: + self.logger.warning(f"DEBUG _play radio: Starting play") self.soco.play() + + self.logger.warning(f"DEBUG _play radio: finished function") return True, "" - def _play_radio_dontuse(self, station_name: str, music_service: str = 'TuneIn', start: bool = True) -> tuple: + def _play_radio(self, station_name: str, music_service: str = 'TuneIn', start: bool = True) -> tuple: """ - WARNING: THIS FUNCTION IS NOT WORKING FOR SOME RADIO STATIONS, e.g. Plays a radio station by a given radio name at a given music service. If more than one radio station are found, - the first result will be played. + the first result will be played. This is the recommended function for selection of radio stations. :param music_service: music service name Default: TuneIn :param station_name: radio station name :param start: Start playing after setting the radio stream? Default: True @@ -2414,7 +2423,7 @@ def _play_radio_dontuse(self, station_name: str, music_service: str = 'TuneIn', ' """ - + self.logger.dbghigh(f"_play_radio called with station_name= {station_name}") # get all music services all_music_services_names = MusicService.get_all_music_services_names() @@ -2425,16 +2434,14 @@ def _play_radio_dontuse(self, station_name: str, music_service: str = 'TuneIn', # get music service instance music_service = MusicService(music_service) - # adapt station_name for optimal search results - if " " in station_name: - station_name_for_search = station_name.split(" ", 1)[0] - elif station_name[-1].isdigit(): - station_name_for_search = station_name[:-1] - else: - station_name_for_search = station_name - # do search - search_result = music_service.search(category='stations', term=station_name_for_search, index=0, count=100) + search_result = music_service.search(category='stations', term=station_name, index=0, count=100) + + # Debug output of whole search list: + k=1 + for station in search_result: + self.logger.dbghigh(f"Entry {k}: {station.title}") + k = k + 1 # get station object from search result the_station = None @@ -2445,11 +2452,11 @@ def _play_radio_dontuse(self, station_name: str, music_service: str = 'TuneIn', the_station = station break - # Fuzzy match + # Fuzzy match with lower station name: if not the_station: - station_name = station_name.lower() + station_name_lower = station_name.lower() for station in search_result: - if station_name in station.title.lower(): + if station_name_lower in station.title.lower(): self.logger.info(f"Fuzzy match '{station.title}' found") the_station = station break @@ -2458,9 +2465,27 @@ def _play_radio_dontuse(self, station_name: str, music_service: str = 'TuneIn', if not the_station: last_char = len(station_name) - 1 if station_name[last_char].isdigit(): - station_name = f"{station_name[0:last_char]} {station_name[last_char:]}" + #old_station_name = f"{station_name[0:last_char]} {station_name[last_char:]}" + #self.logger.dbghigh(f"Debug: Old Very fuzzy query: {old_station_name}") + + # Split Digits and Radio Name: + digitString = '' + radioNameString = '' + splitIndex = last_char + for i in reversed(range(len(station_name))): + if station_name[i].isdigit() or station_name[i] == '.' or station_name[i] == ',': + digitString = station_name[i] + digitString + else: + splitIndex = i + break + radioNameString = station_name[0:splitIndex ] + self.logger.dbghigh(f"Debug: RadioName: {radioNameString}, digitString: {digitString}") + + new_station_name = f"{radioNameString.lower()} {digitString}" + self.logger.dbghigh(f"New very fuzzy query: {new_station_name}") + for station in search_result: - if station_name in station.title.lower(): + if new_station_name in station.title.lower(): self.logger.info(f"Very fuzzy match '{station.title}' found") the_station = station break @@ -2969,7 +2994,7 @@ class Sonos(SmartPlugin): """ Main class of the Plugin. Does all plugin specific stuff """ - PLUGIN_VERSION = "1.8.3" + PLUGIN_VERSION = "1.8.4" def __init__(self, sh): """Initializes the plugin.""" @@ -3734,17 +3759,31 @@ def _is_speaker_up(self, uid: str, ip_address: str) -> bool: return False except subprocess.TimeoutExpired: self.logger.debug(f"Ping {ip_address} process timed out") - return False - + return False + def _get_zone_name_from_uid(self, uid: str) -> str: """ Return zone/speaker name per uid """ for zone in self.zones: + if (zone._uid is None) or (uid is None): + return 'unknown' if zone._uid.lower() == uid.lower(): return zone._player_name + @property + def get_rechable_zones(self): + valid_zones = [] + for zone in self.zones: + try: + uid = zone.uid + except: + pass + else: + valid_zones.append(zone) + return valid_zones + @property def sonos_speaker(self): """ diff --git a/sonos/plugin.yaml b/sonos/plugin.yaml index 4fca62837..0e7feeb79 100755 --- a/sonos/plugin.yaml +++ b/sonos/plugin.yaml @@ -12,7 +12,7 @@ plugin: documentation: https://github.com/smarthomeNG/plugins/blob/master/sonos/README.md support: https://knx-user-forum.de/forum/supportforen/smarthome-py/25151-sonos-anbindung - version: 1.8.3 # Plugin version + version: 1.8.4 # Plugin version sh_minversion: 1.5.1 # minimum shNG version to use this plugin py_minversion: 3.8 # minimum Python version to use for this plugin multi_instance: False # plugin supports multi instance @@ -85,28 +85,6 @@ parameters: de: "(optional) Verlängert die Dauer von Snippet Audio Dateien um einen festen Offset in Sekunden." en: "(optional) Extend snippet duration by a fixed offset specified in seconds" - webif_pagelength: - type: int - default: 0 - valid_list: - - -1 - - 0 - - 25 - - 50 - - 100 - description: - de: 'Anzahl an Items, die standardmäßig in einer Web Interface Tabelle pro Seite angezeigt werden. - 0 = automatisch, -1 = alle' - en: 'Amount of items being listed in a web interface table per page by default. - 0 = automatic, -1 = all' - description_long: - de: 'Anzahl an Items, die standardmäßig in einer Web Interface Tabelle pro Seite angezeigt werden.\n - Bei 0 wird die Tabelle automatisch an die Höhe des Browserfensters angepasst.\n - Bei -1 werden alle Tabelleneinträge auf einer Seite angezeigt.' - en: 'Amount of items being listed in a web interface table per page by default.\n - 0 adjusts the table height automatically based on the height of the browser windows.\n - -1 shows all table entries on one page.' - item_attributes: sonos_uid: type: str diff --git a/sonos/user_doc.rst b/sonos/user_doc.rst index a7fbaa178..02a043314 100755 --- a/sonos/user_doc.rst +++ b/sonos/user_doc.rst @@ -389,14 +389,15 @@ Unteritem ``tts_fade_in``: Wird ein untergeordnetes Item vom Typ Boolean mit Attribut ``sonos_attrib: tts_fade_in`` definiert, wird die Lautstärke nach dem Abspielen der Nachricht von 0 auf das gewünschte Level schrittweise angehoben und eingeblendet. -play_tunein / play_sonos_radio +play_sonos_radio / play_tunein ------------------------------ ``write`` Spielt einen Radiosender anhand eines Namens. Das Item ist vom Typ String. Sonos sucht dazu in einer Datenbank nach potentiellen Radiostationen, die dem Namen entsprechen. Wird mehr als ein zum Suchbegriff passender Radiosender gefunden, wird der erste Treffer verwendet. -Der Befehl ist ein Gruppenbefehl und wird für alle Speaker einer Gruppe angewendet. +Der Befehl ist ein Gruppenbefehl und wird für alle Speaker einer Gruppe angewendet. Empfohlen wird die Nutzung der Funktion play_sonos_radio. +Die alte Funktion play_tunein existiert noch, sollte aber nicht mehr verwendet werden. Unteritem ``start_after``: Wird ein untergeordnetes Item vom Typ Boolean mit Attribut ``sonos_attrib: start_after`` definiert, wird das diff --git a/sonos/webif/templates/index.html b/sonos/webif/templates/index.html index bf0adb97b..e0cc340ad 100755 --- a/sonos/webif/templates/index.html +++ b/sonos/webif/templates/index.html @@ -252,7 +252,7 @@ - {% for zone in p.zones %} + {% for zone in p.get_rechable_zones %} {{ zone._player_name }} diff --git a/stateengine/StateEngineAction.py b/stateengine/StateEngineAction.py index 407957ffa..9b2ae9a9e 100755 --- a/stateengine/StateEngineAction.py +++ b/stateengine/StateEngineAction.py @@ -34,9 +34,13 @@ class SeActionBase(StateEngineTools.SeItemChild): def name(self): return self._name + @property + def function(self): + return self._function + @property def action_status(self): - return self.__action_status + return self._action_status # Cast function for delay # value: value to cast @@ -75,42 +79,80 @@ def __init__(self, abitem, name: str): self.__mode = StateEngineValue.SeValue(self._abitem, "mode", True, "str") self.__order = StateEngineValue.SeValue(self._abitem, "order", False, "num") self._scheduler_name = None - self.__function = None + self._function = None self.__template = None - self.__action_status = {} + self._action_status = {} + self._retrigger_issue = None + self._suspend_issue = None self.__queue = abitem.queue def update_delay(self, value): - self.__delay.set(value) - self.__delay.set_cast(SeActionBase.__cast_delay) + _issue_list = [] + _, _, _issue = self.__delay.set(value) + if _issue: + _issue = {self._name: {'issue': _issue, 'attribute': 'delay', + 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + _issue_list.append(_issue) + _issue = self.__delay.set_cast(SeActionBase.__cast_delay) + if _issue: + _issue = {self._name: {'issue': _issue, 'attribute': 'delay', + 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + _issue_list.append(_issue) + _issue_list = StateEngineTools.flatten_list(_issue_list) + return _issue_list def update_instanteval(self, value): if self.__instanteval is None: self.__instanteval = StateEngineValue.SeValue(self._abitem, "instanteval", False, "bool") - self.__instanteval.set(value) + _, _, _issue = self.__instanteval.set(value) + _issue = {self._name: {'issue': _issue, 'attribute': 'instanteval', + 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + return _issue def update_repeat(self, value): if self.__repeat is None: self.__repeat = StateEngineValue.SeValue(self._abitem, "repeat", False, "bool") - self.__repeat.set(value) + _, _, _issue = self.__repeat.set(value) + _issue = {self._name: {'issue': _issue, 'attribute': 'repeat', + 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + return _issue def update_order(self, value): - self.__order.set(value) + _, _, _issue = self.__order.set(value) + _issue = {self._name: {'issue': _issue, 'attribute': 'order', + 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + return _issue - def update_conditionsets(self, value): - self.conditionset.set(value) + def update_conditionset(self, value): + _, _, _issue = self.conditionset.set(value) + _issue = {self._name: {'issue': _issue, 'attribute': 'conditionset', + 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + return _issue - def update_previousconditionsets(self, value): - self.previousconditionset.set(value) + def update_previousconditionset(self, value): + _, _, _issue = self.previousconditionset.set(value) + _issue = {self._name: {'issue': _issue, 'attribute': 'previousconditionset', + 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + return _issue - def update_previousstate_conditionsets(self, value): - self.previousstate_conditionset.set(value) + def update_previousstate_conditionset(self, value): + _, _, _issue = self.previousstate_conditionset.set(value) + _issue = {self._name: {'issue': _issue, 'attribute': 'previousstate_conditionset', + 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + return _issue - def update_modes(self, value): - self.__mode.set(value) + def update_mode(self, value): + _value, _, _issue = self.__mode.set(value) + _issue = {self._name: {'issue': _issue, 'attribute': 'mode', + 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + return _value[0], _issue def get_order(self): - return self.__order.get(1) + order = self.__order.get(1) + if not isinstance(order, int): + self._log_warning("Order is currently {} but must be an integer. Setting it to 1.", order) + order = 1 + return order def update_webif_actionstatus(self, state, name, success, issue=None): try: @@ -151,7 +193,7 @@ def update_webif_actionstatus(self, state, name, success, issue=None): # Write action to logger def write_to_logger(self): - self._log_debug("name: {}", self._name) + self._log_info("function: {}", self._function) self.__delay.write_to_logger() if self.__repeat is not None: self.__repeat.write_to_logger() @@ -181,6 +223,148 @@ def set_source(self, current_condition, previous_condition, previousstate_condit source = ", ".join(source) return source + # If se_item_ starts with eval: the eval expression is getting evaluated + # check_item: the eval entry as a string + # check_value: current value of an action, will get newly cast based on eval (optional) + # check_mindelta: current mindelta of an action, will get newly cast based on eval (optional) + # returns: evaluated expression + # newly evaluated value + # newly evaluated mindelta + # Any issue that might have occured as a dict + def check_getitem_fromeval(self, check_item, check_value=None, check_mindelta=None): + _issue = {self._name: {'issue': None, 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + if isinstance(check_item, str): + item = None + #self._log_develop("Get item from eval on {} {}", self._function, check_item) + if "stateengine_eval" in check_item or "se_eval" in check_item: + # noinspection PyUnusedLocal + stateengine_eval = se_eval = StateEngineEval.SeEval(self._abitem) + try: + item = check_item.replace('sh', 'self._sh') + item = item.replace('shtime', 'self._shtime') + if item.startswith("eval:"): + _text = "If you define an item by se_eval_ you should use a "\ + "plain eval expression without a preceeding eval. "\ + "Please update your config of {}" + _issue = { + self._name: {'issue': _text.format(check_item), 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + self._log_warning(_text, check_item) + _, _, item = item.partition(":") + elif re.match(r'^.*:', item): + _text = "se_eval/item attributes have to be plain eval expression. Please update your config of {}" + _issue = { + self._name: {'issue': _text.format(check_item), + 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + self._log_warning(_text, check_item) + _, _, item = item.partition(":") + item = eval(item) + if item is not None: + check_item, _issue = self._abitem.return_item(item) + _issue = { + self._name: {'issue': _issue, 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + if check_value: + check_value.set_cast(check_item.cast) + if check_mindelta: + check_mindelta.set_cast(check_item.cast) + self._scheduler_name = "{}-SeItemDelayTimer".format(check_item.property.path) + if self._abitem.id == check_item.property.path: + self._caller += '_self' + #self._log_develop("Got item from eval on {} {}", self._function, check_item) + else: + self._log_develop("Got no item from eval on {} with initial item {}", self._function, self.__item) + except Exception as ex: + _issue = {self._name: {'issue': ex, 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + # raise Exception("Problem evaluating item '{}' from eval: {}".format(check_item, ex)) + self._log_error("Problem evaluating item '{}' from eval: {}", check_item, ex) + check_item = None + if item is None: + _issue = {self._name: {'issue': ['Item {} from eval not existing'.format(check_item)], + 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + # raise Exception("Problem evaluating item '{}' from eval. It does not exist.".format(check_item)) + self._log_error("Problem evaluating item '{}' from eval. It does not exist", check_item) + check_item = None + elif check_item is None: + _issue = {self._name: {'issue': ['Item is None'], + 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + return check_item, check_value, check_mindelta, _issue + + def check_complete(self, item_state, check_item, check_status, check_mindelta, check_value, action_type, evals_items=None): + _issue = {self._name: {'issue': None, + 'issueorigin': [{'state': item_state.property.path, 'action': self._function}]}} + self._log_develop("Check item {} status {} value {} evals_items {}", check_item, check_status, check_value, evals_items) + try: + _name = evals_items.get(self.name) + if _name is not None: + _item = _name.get('item') + _eval = str(_name.get('eval')) + _selfitem = check_item if check_item not in (None, "None") else None + _item = _item if _item not in (None, "None") else None + _eval = _eval if _eval not in (None, "None") else None + check_item = _selfitem or _eval + if check_item is None: + _returnitem, _returnissue = self._abitem.return_item(_item) + check_item = _returnitem + else: + _returnissue = None + _issue = {self._name: {'issue': _returnissue, + 'issueorigin': [{'state': item_state.property.path, 'action': self._function}]}} + self._log_debug("Check item {} status {} value {} _returnissue {}", check_item, check_status, check_value, + _returnissue) + except Exception as ex: + self._log_info("No valid item info for action {}, trying to get differently. Problem: {}", self._name, ex) + # missing item in action: Try to find it. + if check_item is None: + item = StateEngineTools.find_attribute(self._sh, item_state, "se_item_" + self._name) + if item is not None: + check_item, _issue = self._abitem.return_item(item) + _issue = {self._name: {'issue': _issue, + 'issueorigin': [{'state': item_state.property.path, 'action': self._function}]}} + else: + item = StateEngineTools.find_attribute(self._sh, item_state, "se_eval_" + self._name) + if item is not None: + check_item = str(item) + + if check_item is None and _issue[self._name].get('issue') is None: + _issue = {self._name: {'issue': ['Item not defined in rules section'], + 'issueorigin': [{'state': item_state.property.path, 'action': self._function}]}} + # missing status in action: Try to find it. + if check_status is None: + status = StateEngineTools.find_attribute(self._sh, item_state, "se_status_" + self._name) + if status is not None: + check_status, _issue = self._abitem.return_item(status) + _issue = {self._name: {'issue': _issue, + 'issueorigin': [{'state': item_state.property.path, 'action': self._function}]}} + elif check_status is not None: + check_status = str(status) + + if check_mindelta.is_empty(): + mindelta = StateEngineTools.find_attribute(self._sh, item_state, "se_mindelta_" + self._name) + if mindelta is not None: + check_mindelta.set(mindelta) + + if check_status is not None: + check_value.set_cast(check_status.cast) + check_mindelta.set_cast(check_status.cast) + self._scheduler_name = "{}-SeItemDelayTimer".format(check_status.property.path) + if self._abitem.id == check_status.property.path: + self._caller += '_self' + elif check_status is None: + if isinstance(check_item, str): + pass + elif check_item is not None: + check_value.set_cast(check_item.cast) + check_mindelta.set_cast(check_item.cast) + self._scheduler_name = "{}-SeItemDelayTimer".format(check_item.property.path) + if self._abitem.id == check_item.property.path: + self._caller += '_self' + if _issue[self._name].get('issue') not in [[], [None], None]: + self._log_develop("Issue with {} action {}", action_type, _issue) + else: + _issue = {self._name: {'issue': None, + 'issueorigin': [{'state': item_state.property.path, 'action': self._function}]}} + + return check_item, check_status, check_mindelta, check_value, _issue + # Execute action (considering delay, etc) # is_repeat: Indicate if this is a repeated action without changing the state # item_allow_repeat: Is repeating actions generally allowed for the item? @@ -247,7 +431,16 @@ def _update_repeat_webif(value: bool): self._log_decrease_indent(50) self._log_increase_indent() self._log_info("Action '{0}': Preparing", self._name) + self._log_increase_indent() + try: + self._getitem_fromeval() + self._log_decrease_indent() + _validitem = True + except Exception as ex: + _validitem = False + self._log_decrease_indent() if not self._can_execute(state): + self._log_decrease_indent() return conditions_met = 0 condition_necessary = 0 @@ -284,14 +477,6 @@ def _update_repeat_webif(value: bool): else: repeat_text = "" self._log_increase_indent() - try: - self._getitem_fromeval() - self._log_decrease_indent() - _validitem = True - except Exception as ex: - _validitem = False - self._log_error("Action '{0}': Ignored because {1}", self._name, ex) - self._log_decrease_indent() if _validitem: delay = 0 if self.__delay.is_empty() else self.__delay.get() plan_next = self._se_plugin.scheduler_return_next(self._scheduler_name) @@ -373,6 +558,7 @@ def _waitforexecute(self, state, actionname: str, namevar: str = "", repeat_text else: value = None self._abitem.add_scheduler_entry(self._scheduler_name) + self.update_webif_actionstatus(state, self._name, 'Scheduled') self._se_plugin.scheduler_add(self._scheduler_name, self._delayed_execute, value={'actionname': actionname, 'namevar': self._name, 'repeat_text': repeat_text, 'value': value, @@ -381,12 +567,12 @@ def _waitforexecute(self, state, actionname: str, namevar: str = "", repeat_text 'previousstate_condition': previousstate_condition, 'state': state}, next=next_run) - def _delayed_execute(self, actionname: str, namevar: str = "", repeat_text: str = "", value=None, current_condition=None, previous_condition=None, previousstate_condition=None, state=None): + def _delayed_execute(self, actionname: str, namevar: str = "", repeat_text: str = "", value=None, current_condition=None, previous_condition=None, previousstate_condition=None, state=None, caller=None): if state: - self._log_debug("Putting delayed action '{}' from state '{}' into queue.", namevar, state) + self._log_debug("Putting delayed action '{}' from state '{}' into queue. Caller: {}", namevar, state, caller) self.__queue.put(["delayedaction", self, actionname, namevar, repeat_text, value, current_condition, previous_condition, previousstate_condition, state]) else: - self._log_debug("Putting delayed action '{}' into queue.", namevar) + self._log_debug("Putting delayed action '{}' into queue. Caller: {}", namevar, caller) self.__queue.put(["delayedaction", self, actionname, namevar, repeat_text, value, current_condition, previous_condition, previousstate_condition]) if not self._abitem.update_lock.locked(): self._log_debug("Running queue") @@ -412,116 +598,51 @@ def __init__(self, abitem, name: str): self.__delta = 0 self.__value = StateEngineValue.SeValue(self._abitem, "value") self.__mindelta = StateEngineValue.SeValue(self._abitem, "mindelta") - self.__function = "set" + self._function = "set" def __repr__(self): return "SeAction Set {}".format(self._name) def _getitem_fromeval(self): - if isinstance(self.__item, str): - item = None - if "stateengine_eval" in self.__item or "se_eval" in self.__item: - # noinspection PyUnusedLocal - stateengine_eval = se_eval = StateEngineEval.SeEval(self._abitem) - try: - item = self.__item.replace('sh', 'self._sh') - item = item.replace('shtime', 'self._shtime') - item = eval(item) - if item is not None: - self.__item, _issue = self._abitem.return_item(item) - _issue = {self._name: {'issue': _issue, 'issueorigin': [{'state': 'unknown', 'action': 'unknown'}]}} - self.__value.set_cast(self.__item.cast) - self.__mindelta.set_cast(self.__item.cast) - self._scheduler_name = "{}-SeItemDelayTimer".format(self.__item.property.path) - if self._abitem.id == self.__item.property.path: - self._caller += '_self' - except Exception as ex: - _issue = {self._name: {'issue': ex, 'issueorigin': [{'state': 'unknown', 'action': 'unknown'}]}} - raise Exception("Problem evaluating item '{}' from eval: {}".format(self.__item, ex)) - finally: - self.__action_status = _issue - if item is None: - _issue = {self._name: {'issue': 'Item {} from eval not existing'.format(self.__item), 'issueorigin': [{'state': 'unknown', 'action': 'unknown'}]}} - self.__action_status = _issue - raise Exception("Problem evaluating item '{}' from eval. It does not exist.".format(self.__item)) + if self.__item is None: + return + self.__item, self.__value, self.__mindelta, _issue = self.check_getitem_fromeval(self.__item, self.__value, + self.__mindelta) + if self.__item is None: + self._action_status = _issue + raise Exception("Problem evaluating item '{}' from eval.".format(self.__item)) # set the action based on a set_(action_name) attribute # value: Value of the set_(action_name) attribute def update(self, value): _, _, _issue = self.__value.set(value) - _issue = {self._name: {'issue': _issue, 'issueorigin': [{'state': 'unknown', 'action': 'set initital'}]}} + _issue = {self._name: {'issue': _issue, 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} return _issue # Complete action # item_state: state item to read from def complete(self, item_state, evals_items=None): - _issue = {self._name: {'issue': None, 'issueorigin': [{'state': item_state.property.path, 'action': 'set initital'}]}} - try: - _name = evals_items.get(self.name) - if _name is not None: - _item = _name.get('item') - _eval = str(_name.get('eval')) - _selfitem = self.__item if self.__item is not None and self.__item != "None" else None - _item = _item if _item is not None and _item != "None" else None - _eval = _eval if _eval is not None and _eval != "None" else None - self.__item = _selfitem or self._abitem.return_item(_item)[0] or _eval - _issue = {self._name: {'issue': self._abitem.return_item(_item)[1], 'issueorigin': [{'state': item_state.property.path, 'action': 'set (first try to get item)'}]}} - except Exception as ex: - self._log_error("No valid item info for action {}, trying to get differently. Problem: {}", self.name, ex) - # missing item in action: Try to find it. - if self.__item is None: - item = StateEngineTools.find_attribute(self._sh, item_state, "se_item_" + self._name) - - if item is not None: - self.__item, _issue = self._abitem.return_item(item) - _issue = {self._name: {'issue': _issue, 'issueorigin': [{'state': item_state.property.path, 'action': 'set'}]}} - else: - item = StateEngineTools.find_attribute(self._sh, item_state, "se_eval_" + self._name) - if item is not None: - self.__item = str(item) - - if self.__item is None and _issue[self._name].get('issue') is None: - _issue = {self._name: {'issue': 'Item not defined in rules section', 'issueorigin': [{'state': item_state.property.path, 'action': 'set'}]}} - # missing status in action: Try to find it. - if self.__status is None: - status = StateEngineTools.find_attribute(self._sh, item_state, "se_status_" + self._name) - - if status is not None: - self.__status, _issue = self._abitem.return_item(status) - _issue = {self._name: {'issue': 'Item not defined in rules section', 'issueorigin': [{'state': item_state.property.path, 'action': 'set'}]}} - - if self.__mindelta.is_empty(): - mindelta = StateEngineTools.find_attribute(self._sh, item_state, "se_mindelta_" + self._name) - if mindelta is not None: - self.__mindelta.set(mindelta) - - if self.__status is not None: - self.__value.set_cast(self.__status.cast) - self.__mindelta.set_cast(self.__status.cast) - self._scheduler_name = "{}-SeItemDelayTimer".format(self.__status.property.path) - if self._abitem.id == self.__status.property.path: - self._caller += '_self' - elif self.__status is None: - if isinstance(self.__item, str): - pass - elif self.__item is not None: - self.__value.set_cast(self.__item.cast) - self.__mindelta.set_cast(self.__item.cast) - self._scheduler_name = "{}-SeItemDelayTimer".format(self.__item.property.path) - if self._abitem.id == self.__item.property.path: - self._caller += '_self' - if _issue[self._name].get('issue') is not None: - self.__action_status = _issue - self._log_develop("Issue with set action {}", _issue) + self.__item, self.__status, self.__mindelta, self.__value, _issue = self.check_complete( + item_state, self.__item, self.__status, self.__mindelta, self.__value, "set", evals_items) + self._action_status = _issue return _issue # Write action to logger def write_to_logger(self): SeActionBase.write_to_logger(self) if isinstance(self.__item, str): - self._log_debug("item from eval: {0}", self.__item) + try: + self._log_debug("item from eval: {0}", self.__item) + self._log_increase_indent() + current, _, _, _ = self.check_getitem_fromeval(self.__item) + self._log_debug("Currently eval results in {}", current) + self._log_decrease_indent() + except Exception as ex: + self._log_warning("Issue while getting item from eval {}", ex) elif self.__item is not None: self._log_debug("item: {0}", self.__item.property.path) + else: + self._log_debug("item is not defined! Check log file.") if self.__status is not None: self._log_debug("status: {0}", self.__status.property.path) self.__mindelta.write_to_logger() @@ -591,22 +712,39 @@ def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, item(value, caller=self._caller, source=source) def get(self): + orig_item = self.__item + try: + self._getitem_fromeval() + except Exception as ex: + self._log_warning("Issue while getting item from eval {}", ex) + item_from_eval = orig_item if orig_item != self.__item else False try: - _item = str(self.__item.property.path) + if self.__item is not None: + item = str(self.__item.property.path) + else: + item = None + except Exception as ex: + item = None + try: + val = self.__value.get() + if val is not None: + value = str(val) + else: + value = None except Exception: - _item = str(self.__item) - _mindelta = self.__mindelta.get() - if _mindelta is None: - result = {'function': str(self.__function), 'item': _item, - 'value': str(self.__value.get()), 'conditionset': str(self.conditionset.get()), + value = None + mindelta = self.__mindelta.get() + if mindelta is None: + result = {'function': str(self._function), 'item': item, 'item_from_eval': item_from_eval, + 'value': value, 'conditionset': str(self.conditionset.get()), 'previousconditionset': str(self.previousconditionset.get()), 'previousstate_conditionset': str(self.previousstate_conditionset.get()), 'actionstatus': {}} else: - result = {'function': str(self.__function), 'item': _item, - 'value': str(self.__value.get()), 'conditionset': str(self.conditionset.get()), + result = {'function': str(self._function), 'item': item, 'item_from_eval': item_from_eval, + 'value': value, 'conditionset': str(self.conditionset.get()), 'previousconditionset': str(self.previousconditionset.get()), 'previousstate_conditionset': str(self.previousstate_conditionset.get()), 'actionstatus': {}, - 'delta': str(self.__delta), 'mindelta': str(_mindelta)} + 'delta': str(self.__delta), 'mindelta': str(mindelta)} return result @@ -618,7 +756,7 @@ class SeActionSetByattr(SeActionBase): def __init__(self, abitem, name: str): super().__init__(abitem, name) self.__byattr = None - self.__function = "set by attribute" + self._function = "set by attribute" def __repr__(self): return "SeAction SetByAttr {}".format(self._name) @@ -628,7 +766,7 @@ def __repr__(self): def update(self, value): self.__byattr = value _issue = {self._name: {'issue': None, 'attribute': self.__byattr, - 'issueorigin': [{'state': 'unknown', 'action': 'unknown'}]}} + 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} return _issue # Complete action @@ -636,12 +774,11 @@ def update(self, value): def complete(self, item_state, evals_items=None): self._scheduler_name = "{}-SeByAttrDelayTimer".format(self.__byattr) _issue = {self._name: {'issue': None, 'attribute': self.__byattr, - 'issueorigin': [{'state': 'unknown', 'action': 'unknown'}]}} + 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} return _issue # Write action to logger def write_to_logger(self): - self._log_debug("function: {}", self.__function) SeActionBase.write_to_logger(self) if self.__byattr is not None: self._log_debug("set by attribute: {0}", self.__byattr) @@ -652,13 +789,14 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s if returnvalue: return value self._log_info("{0}: Setting values by attribute '{1}'.{2}", actionname, self.__byattr, repeat_text) + self.update_webif_actionstatus(state, self._name, 'True') source = self.set_source(current_condition, previous_condition, previousstate_condition) for item in self.itemsApi.find_items(self.__byattr): self._log_info("\t{0} = {1}", item.property.path, item.conf[self.__byattr]) item(item.conf[self.__byattr], caller=self._caller, source=source) def get(self): - result = {'function': str(self.__function), 'byattr': str(self.__byattr), + result = {'function': str(self._function), 'byattr': str(self.__byattr), 'conditionset': str(self.conditionset.get()), 'previousconditionset': str(self.previousconditionset.get()), 'previousstate_conditionset': str(self.previousstate_conditionset.get()), 'actionstatus': {}} return result @@ -673,7 +811,7 @@ def __init__(self, abitem, name: str): super().__init__(abitem, name) self.__logic = None self.__value = StateEngineValue.SeValue(self._abitem, "value") - self.__function = "trigger" + self._function = "trigger" def __repr__(self): return "SeAction Trigger {}".format(self._name) @@ -686,7 +824,7 @@ def update(self, value): value = None if value == "" else value _, _, _issue = self.__value.set(value) _issue = {self._name: {'issue': _issue, 'logic': self.__logic, - 'issueorigin': [{'state': 'unknown', 'action': 'unknown'}]}} + 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} return _issue # Complete action @@ -694,12 +832,11 @@ def update(self, value): def complete(self, item_state, evals_items=None): self._scheduler_name = "{}-SeLogicDelayTimer".format(self.__logic) _issue = {self._name: {'issue': None, 'logic': self.__logic, - 'issueorigin': [{'state': 'unknown', 'action': 'unknown'}]}} + 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} return _issue # Write action to logger def write_to_logger(self): - self._log_debug("function: {}", self.__function) SeActionBase.write_to_logger(self) if self.__logic is not None: self._log_debug("trigger logic: {0}", self.__logic) @@ -717,13 +854,22 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s if returnvalue: return value + self.update_webif_actionstatus(state, self._name, 'True') self._log_info("{0}: Triggering logic '{1}' using value '{2}'.{3}", actionname, self.__logic, value, repeat_text) add_logics = 'logics.{}'.format(self.__logic) if not self.__logic.startswith('logics.') else self.__logic self._sh.trigger(add_logics, by=self._caller, source=self._name, value=value) def get(self): - result = {'function': str(self.__function), 'logic': str(self.__logic), - 'value': str(self.__value.get()), + try: + val = self.__value.get() + if val is not None: + value = str(val) + else: + value = None + except Exception: + value = None + result = {'function': str(self._function), 'logic': str(self.__logic), + 'value': value, 'conditionset': str(self.conditionset.get()), 'previousconditionset': str(self.previousconditionset.get()), 'previousstate_conditionset': str(self.previousstate_conditionset.get()), 'actionstatus': {}} return result @@ -736,7 +882,7 @@ class SeActionRun(SeActionBase): def __init__(self, abitem, name: str): super().__init__(abitem, name) self.__eval = None - self.__function = "run" + self._function = "run" def __repr__(self): return "SeAction Run {}".format(self._name) @@ -752,7 +898,7 @@ def update(self, value): if func == "eval": self.__eval = value _issue = {self._name: {'issue': None, 'eval': StateEngineTools.get_eval_name(self.__eval), - 'issueorigin': [{'state': 'unknown', 'action': 'unknown'}]}} + 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} return _issue # Complete action @@ -760,12 +906,11 @@ def update(self, value): def complete(self, item_state, evals_items=None): self._scheduler_name = "{}-SeRunDelayTimer".format(StateEngineTools.get_eval_name(self.__eval)) _issue = {self._name: {'issue': None, 'eval': StateEngineTools.get_eval_name(self.__eval), - 'issueorigin': [{'state': 'unknown', 'action': 'unknown'}]}} + 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} return _issue # Write action to logger def write_to_logger(self): - self._log_debug("function: {}", self.__function) SeActionBase.write_to_logger(self) if self.__eval is not None: self._log_debug("eval: {0}", StateEngineTools.get_eval_name(self.__eval)) @@ -792,10 +937,12 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s if previousstate_condition: self._log_debug("Running eval {0} based on previous state's conditionset {1}", self.__eval, previousstate_condition) eval(self.__eval) + self.update_webif_actionstatus(state, self._name, 'True') self._log_decrease_indent() except Exception as ex: self._log_decrease_indent() text = "{0}: Problem evaluating '{1}': {2}." + self.update_webif_actionstatus(state, self._name, 'False', 'Problem evaluating: {}'.format(ex)) self._log_error(text.format(actionname, StateEngineTools.get_eval_name(self.__eval), ex)) else: try: @@ -809,14 +956,16 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s if previousstate_condition: self._log_debug("Running eval {0} based on previous state's conditionset {1}", self.__eval, previousstate_condition) self.__eval() + self.update_webif_actionstatus(state, self._name, 'True') self._log_decrease_indent() except Exception as ex: self._log_decrease_indent() + self.update_webif_actionstatus(state, self._name, 'False', 'Problem calling: {}'.format(ex)) text = "{0}: Problem calling '{0}': {1}." self._log_error(text.format(actionname, StateEngineTools.get_eval_name(self.__eval), ex)) def get(self): - result = {'function': str(self.__function), 'eval': str(self.__eval), + result = {'function': str(self._function), 'eval': str(self.__eval), 'conditionset': str(self.conditionset.get()), 'previousconditionset': str(self.previousconditionset.get()), 'previousstate_conditionset': str(self.previousstate_conditionset.get()), 'actionstatus': {}} return result @@ -830,9 +979,10 @@ class SeActionForceItem(SeActionBase): def __init__(self, abitem, name: str): super().__init__(abitem, name) self.__item = None + self.__status = None self.__value = StateEngineValue.SeValue(self._abitem, "value") self.__mindelta = StateEngineValue.SeValue(self._abitem, "mindelta") - self.__function = "force set" + self._function = "force set" def __repr__(self): return "SeAction Force {}".format(self._name) @@ -841,67 +991,33 @@ def __repr__(self): # value: Value of the set_(action_name) attribute def update(self, value): _, _, _issue = self.__value.set(value) - _issue = {self._name: {'issue': _issue, 'issueorigin': [{'state': 'unknown', 'action': 'force initital'}]}} + _issue = {self._name: {'issue': _issue, 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} return _issue # Complete action # item_state: state item to read from def complete(self, item_state, evals_items=None): - _issue = {self._name: {'issue': None, 'issueorigin': [{'state': item_state.property.path, 'action': 'force initital'}]}} - # missing item in action: Try to find it. - if self.__item is None: - item = StateEngineTools.find_attribute(self._sh, item_state, "se_item_" + self._name) - if item is not None: - self.__item, _issue = self._abitem.return_item(item) - _issue = {self._name: {'issue': 'Item not defined in rules section', 'issueorigin': [{'state': item_state.property.path, 'action': 'force'}]}} - else: - item = StateEngineTools.find_attribute(self._sh, item_state, "se_eval_" + self._name) - self.__item = str(item) - - # missing status in action: Try to find it. - if self.__status is None: - status = StateEngineTools.find_attribute(self._sh, item_state, "se_status_" + self._name) - - if status is not None: - self.__status, _issue = self._abitem.return_item(status) - _issue = {self._name: {'issue': 'Item not defined in rules section', 'issueorigin': [{'state': item_state.property.path, 'action': 'force'}]}} - - if self.__mindelta.is_empty(): - mindelta = StateEngineTools.find_attribute(self._sh, item_state, "se_mindelta_" + self._name) - if mindelta is not None: - self.__mindelta.set(mindelta) - - if self.__item is None and _issue[self._name].get('issue') is None: - _issue = {self._name: {'issue': 'Item not found', 'issueorigin': [{'state': item_state.property.path, 'action': 'force'}]}} - if self.__status is not None: - self.__value.set_cast(self.__status.cast) - self.__mindelta.set_cast(self.__status.cast) - self._scheduler_name = "{}-SeItemDelayTimer".format(self.__status.property.path) - if self._abitem.id == self.__status.property.path: - self._caller += '_self' - elif self.__status is None: - if isinstance(self.__item, str): - pass - elif self.__item is not None: - self.__value.set_cast(self.__item.cast) - self.__mindelta.set_cast(self.__item.cast) - self._scheduler_name = "{}-SeItemDelayTimer".format(self.__item.property.path) - if self._abitem.id == self.__item.property.path: - self._caller += '_self' - if _issue[self._name].get('issue') is not None: - self.__action_status = _issue - self._log_develop("Issue with force action {}", _issue) + self.__item, self.__status, self.__mindelta, self.__value, _issue = self.check_complete( + item_state, self.__item, self.__status, self.__mindelta, self.__value, "force", evals_items) + self._action_status = _issue return _issue # Write action to logger def write_to_logger(self): - self._log_debug("function: {}", self.__function) - self._log_debug("value: {}", self.__value) SeActionBase.write_to_logger(self) if isinstance(self.__item, str): - self._log_debug("item from eval: {0}", self.__item) + try: + self._log_debug("item from eval: {0}", self.__item) + self._log_increase_indent() + current, _, _, _ = self.check_getitem_fromeval(self.__item) + self._log_debug("Currently eval results in {}", current) + self._log_decrease_indent() + except Exception as ex: + self._log_warning("Issue while getting item from eval {}", ex) elif self.__item is not None: self._log_debug("item: {0}", self.__item.property.path) + else: + self._log_debug("item is not defined! Check log file.") if self.__status is not None: self._log_debug("status: {0}", self.__status.property.path) self.__mindelta.write_to_logger() @@ -927,32 +1043,13 @@ def _can_execute(self, state): return True def _getitem_fromeval(self): - if isinstance(self.__item, str): - _issue = {self._name: {'issue': None, 'issueorigin': [{'state': 'unknown', 'action': 'unknown'}]}} - if "stateengine_eval" in self.__item or "se_eval" in self.__item: - # noinspection PyUnusedLocal - stateengine_eval = se_eval = StateEngineEval.SeEval(self._abitem) - try: - item = self.__item.replace('sh', 'self._sh') - item = item.replace('shtime', 'self._shtime') - item = eval(item) - if item is not None: - self.__item, _issue = self._abitem.return_item(item) - _issue = {self._name: {'issue': _issue, 'issueorigin': [{'state': 'unknown', 'action': 'unknown'}]}} - self.__value.set_cast(self.__item.cast) - self.__mindelta.set_cast(self.__item.cast) - self._scheduler_name = "{}-SeItemDelayTimer".format(self.__item.property.path) - if self._abitem.id == self.__item.property.path: - self._caller += '_self' - else: - self._log_error("Problem evaluating item '{}' from eval. It is None.", item) - - except Exception as ex: - _issue = {self._name: {'issue': 'Problem evaluating item {} from eval'.format(self.__item), 'issueorigin': [{'state': 'unknown', 'action': 'unknown'}]}} - self._log_error("Problem evaluating item '{}' from eval: {}.", self.__item, ex) - finally: - self.__action_status = _issue - return _issue + if self.__item is None: + return + self.__item, self.__value, self.__mindelta, _issue = self.check_getitem_fromeval(self.__item, self.__value, + self.__mindelta) + if self.__item is None: + self._action_status = _issue + raise Exception("Problem evaluating item '{}' from eval.".format(self.__item)) # Really execute the action (needs to be implemented in derived classes) # noinspection PyProtectedMember @@ -964,6 +1061,7 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s if value is None: self._log_debug("{0}: Value is None", actionname) + self.update_webif_actionstatus(state, self._name, 'False', 'Value is None') return if returnvalue: @@ -975,6 +1073,7 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s # noinspection PyCallingNonCallable delta = float(abs(self.__item() - value)) if delta < mindelta: + self.update_webif_actionstatus(state, self._name, 'False') text = "{0}: Not setting '{1}' to '{2}' because delta '{3:.2}' is lower than mindelta '{4}'" self._log_debug(text, actionname, self.__item.property.path, value, delta, mindelta) return @@ -1004,15 +1103,33 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s self._log_debug("{0}: New value differs from old value, no force required.", actionname) self._log_decrease_indent() self._log_debug("{0}: Set '{1}' to '{2}'.{3}", actionname, self.__item.property.path, value, repeat_text) + self.update_webif_actionstatus(state, self._name, 'True') # noinspection PyCallingNonCallable self.__item(value, caller=self._caller, source=source) def get(self): + orig_item = self.__item + try: + self._getitem_fromeval() + except Exception as ex: + self._log_warning("Issue while getting item from eval {}", ex) + item_from_eval = orig_item if orig_item != self.__item else False + try: + if self.__item is not None: + item = str(self.__item.property.path) + else: + item = None + except Exception: + item = None try: - item = str(self.__item.property.path) + val = self.__value.get() + if val is not None: + value = str(val) + else: + value = None except Exception: - item = str(self.__item) - result = {'function': str(self.__function), 'item': item, 'value': str(self.__value.get()), + value = None + result = {'function': str(self._function), 'item': item, 'item_from_eval': item_from_eval, 'value': value, 'conditionset': str(self.conditionset.get()), 'previousconditionset': str(self.previousconditionset.get()), 'previousstate_conditionset': str(self.previousstate_conditionset.get()), 'actionstatus': {}} return result @@ -1027,7 +1144,7 @@ def __init__(self, abitem, name: str): super().__init__(abitem, name) self.__special = None self.__value = None - self.__function = "special" + self._function = "special" def __repr__(self): return "SeAction Special {}".format(self._name) @@ -1043,7 +1160,7 @@ def update(self, value): else: raise ValueError("Action {0}: Unknown special value '{1}'!".format(self._name, special)) self.__special = special - _issue = {self._name: {'issue': None, 'special': self.__value, 'issueorigin': [{'state': 'unknown', 'action': 'special'}]}} + _issue = {self._name: {'issue': None, 'special': self.__value, 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} return _issue # Complete action @@ -1054,12 +1171,11 @@ def complete(self, item_state, evals_items=None): else: item = self.__value.property.path self._scheduler_name = "{}_{}-SeSpecialDelayTimer".format(self.__special, item) - _issue = {self._name: {'issue': None, 'special': item, 'issueorigin': [{'state': 'unknown', 'action': 'special'}]}} + _issue = {self._name: {'issue': None, 'special': item, 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} return _issue # Write action to logger def write_to_logger(self): - self._log_debug("function: {}", self.__function) SeActionBase.write_to_logger(self) self._log_debug("Special Action: {0}", self.__special) if isinstance(self.__value, list): @@ -1081,61 +1197,84 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s self._log_increase_indent() if self.__special == "suspend": self.suspend_execute(state, current_condition, previous_condition, previousstate_condition) + if self._suspend_issue in ["", [], None, [None]]: + self.update_webif_actionstatus(state, self._name, 'True') + else: + self.update_webif_actionstatus(state, self._name, 'False', self._suspend_issue) self._log_decrease_indent() elif self.__special == "retrigger": + if self._retrigger_issue in ["", [], None, [None]]: + self.update_webif_actionstatus(state, self._name, 'True') + else: + self.update_webif_actionstatus(state, self._name, 'False', self._retrigger_issue) # noinspection PyCallingNonCallable self._abitem.update_state(self.__value, self._caller) #self.__value(True, caller=self._caller) self._log_decrease_indent() else: self._log_decrease_indent() + self.update_webif_actionstatus(state, self._name, 'False', 'Unknown special value {}'.format(self.__special)) raise ValueError("{0}: Unknown special value '{1}'!".format(actionname, self.__special)) self._log_debug("Special action {0}: done", self.__special) def suspend_get_value(self, value): - _issue = {self._name: {'issue': None, 'issueorigin': [{'state': 'suspend', 'action': 'suspend initital'}]}} + _issue = {self._name: {'issue': None, 'issueorigin': [{'state': 'suspend', 'action': 'suspend'}]}} if value is None: - _issue = {self._name: {'issue': 'Special action suspend requires arguments', 'issueorigin': [{'state': 'suspend', 'action': 'suspend'}]}} - self.__action_status = _issue - raise ValueError("Action {0}: Special action 'suspend' requires arguments!".format(self._name)) + text = 'Special action suspend requires arguments' + _issue = {self._name: {'issue': text, 'issueorigin': [{'state': 'suspend', 'action': 'suspend'}]}} + self._action_status = _issue + self._suspend_issue = text + raise ValueError("Action {0}: {1}".format(self._name, text)) suspend, manual = StateEngineTools.partition_strip(value, ",") if suspend is None or manual is None: - _issue = {self._name: {'issue': 'Special action suspend requires two arguments', 'issueorigin': [{'state': 'suspend', 'action': 'suspend'}]}} - self.__action_status = _issue - raise ValueError("Action {0}: Special action 'suspend' requires two arguments (separated by a comma)!".format(self._name)) + text = "Special action 'suspend' requires two arguments (separated by a comma)!" + _issue = {self._name: {'issue': text, 'issueorigin': [{'state': 'suspend', 'action': 'suspend'}]}} + self._action_status = _issue + self._suspend_issue = text + raise ValueError("Action {0}: {1}".format(self._name, text)) suspend_item, _issue = self._abitem.return_item(suspend) _issue = {self._name: {'issue': _issue, 'issueorigin': [{'state': 'suspend', 'action': 'suspend'}]}} if suspend_item is None: - _issue = {self._name: {'issue': 'Suspend item not found', 'issueorigin': [{'state': 'suspend', 'action': 'suspend'}]}} - self.__action_status = _issue - raise ValueError("Action {0}: Suspend item '{1}' not found!".format(self._name, suspend)) + text = "Suspend item '{}' not found!".format(suspend) + _issue = {self._name: {'issue': text, 'issueorigin': [{'state': 'suspend', 'action': 'suspend'}]}} + self._action_status = _issue + self._suspend_issue = text + raise ValueError("Action {0}: {1}".format(self._name, text)) manual_item, _issue = self._abitem.return_item(manual) + self._suspend_issue = _issue _issue = {self._name: {'issue': _issue, 'issueorigin': [{'state': 'suspend', 'action': 'suspend'}]}} if manual_item is None: - _issue = {self._name: {'issue': 'Manual item {} not found'.format(manual), 'issueorigin': [{'state': 'suspend', 'action': 'suspend'}]}} - self.__action_status = _issue - raise ValueError("Action {0}: Manual item '{1}' not found!".format(self._name, manual)) - self.__action_status = _issue + text = 'Manual item {} not found'.format(manual) + _issue = {self._name: {'issue': text, 'issueorigin': [{'state': 'suspend', 'action': 'suspend'}]}} + self._action_status = _issue + self._suspend_issue = text + raise ValueError("Action {0}: {1}".format(self._name, text)) + self._action_status = _issue return [suspend_item, manual_item.property.path] def retrigger_get_value(self, value): if value is None: - _issue = {self._name: {'issue': 'Special action retrigger requires item', 'issueorigin': [{'state': 'retrigger', 'action': 'retrigger'}]}} - self.__action_status = _issue - raise ValueError("Action {0}: Special action 'retrigger' requires item".format(self._name)) + text = 'Special action retrigger requires item' + _issue = {self._name: {'issue': text, 'issueorigin': [{'state': 'retrigger', 'action': 'retrigger'}]}} + self._action_status = _issue + self._retrigger_issue = text + raise ValueError("Action {0}: {1}".format(self._name, text)) se_item, __ = StateEngineTools.partition_strip(value, ",") se_item, _issue = self._abitem.return_item(se_item) + self._retrigger_issue = _issue _issue = {self._name: {'issue': _issue, 'issueorigin': [{'state': 'retrigger', 'action': 'retrigger'}]}} - self.__action_status = _issue + self._action_status = _issue if se_item is None: - _issue = {self._name: {'issue': 'Retrigger item {} not found'.format(se_item), 'issueorigin': [{'state': 'retrigger', 'action': 'retrigger'}]}} - self.__action_status = _issue - raise ValueError("Action {0}: Retrigger item '{1}' not found!".format(self._name, se_item)) + text = 'Retrigger item {} not found'.format(se_item) + _issue = {self._name: {'issue': text, 'issueorigin': [{'state': 'retrigger', 'action': 'retrigger'}]}} + self._action_status = _issue + self._retrigger_issue = text + raise ValueError("Action {0}: {1}".format(self._name, text)) return se_item def suspend_execute(self, state=None, current_condition=None, previous_condition=None, previousstate_condition=None): @@ -1158,7 +1297,7 @@ def suspend_execute(self, state=None, current_condition=None, previous_condition suspend_remaining = int(suspend_time - suspend_over + 0.5) # adding 0.5 causes round up ... self._abitem.set_variable("item.suspend_remaining", suspend_remaining) self._log_debug("Updated variable 'item.suspend_remaining' to {0}", suspend_remaining) - self.__action_status = _issue + self._action_status = _issue def get(self): try: @@ -1171,7 +1310,7 @@ def get(self): value_result[i] = val.property.path except Exception: pass - result = {'function': str(self.__function), 'special': str(self.__special), + result = {'function': str(self._function), 'special': str(self.__special), 'value': str(value_result), 'conditionset': str(self.conditionset.get()), 'previousconditionset': str(self.previousconditionset.get()), 'previousstate_conditionset': str(self.previousstate_conditionset.get()), 'actionstatus': {}} @@ -1185,13 +1324,12 @@ class SeActionAddItem(SeActionSetItem): # name: Name of action def __init__(self, abitem, name: str): super().__init__(abitem, name) - self.__function = "add to list" + self._function = "add to list" def __repr__(self): return "SeAction Add {}".format(self._name) def write_to_logger(self): - self._log_debug("function: {}", self.__function) SeActionSetItem.write_to_logger(self) SeActionBase.write_to_logger(self) @@ -1206,11 +1344,22 @@ def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, def get(self): try: - item = str(self.__item.property.path) + if self.__item is not None: + item = str(self.__item.property.path) + else: + item = None + except Exception: + item = None + try: + val = self.__value.get() + if val is not None: + value = str(val) + else: + value = None except Exception: - item = str(self.__item) - result = {'function': str(self.__function), 'item': item, - 'value': str(self.__value.get()), 'conditionset': str(self.conditionset.get()), + value = None + result = {'function': str(self._function), 'item': item, + 'value': value, 'conditionset': str(self.conditionset.get()), 'previousconditionset': str(self.previousconditionset.get()), 'previousstate_conditionset': str(self.previousstate_conditionset.get()), 'actionstatus': {}} return result @@ -1223,13 +1372,12 @@ class SeActionRemoveFirstItem(SeActionSetItem): # name: Name of action def __init__(self, abitem, name: str): super().__init__(abitem, name) - self.__function = "remove first from list" + self._function = "remove first from list" def __repr__(self): return "SeAction RemoveFirst {}".format(self._name) def write_to_logger(self): - self._log_debug("function: {}", self.__function) SeActionSetItem.write_to_logger(self) SeActionBase.write_to_logger(self) @@ -1250,11 +1398,22 @@ def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, def get(self): try: - item = str(self.__item.property.path) + if self.__item is not None: + item = str(self.__item.property.path) + else: + item = None + except Exception: + item = None + try: + val = self.__value.get() + if val is not None: + value = str(val) + else: + value = None except Exception: - item = str(self.__item) - result = {'function': str(self.__function), 'item': item, - 'value': str(self.__value.get()), 'conditionset': str(self.conditionset.get()), + value = None + result = {'function': str(self._function), 'item': item, + 'value': value, 'conditionset': str(self.conditionset.get()), 'previousconditionset': str(self.previousconditionset.get()), 'previousstate_conditionset': str(self.previousstate_conditionset.get()), 'actionstatus': {}} return result @@ -1267,13 +1426,12 @@ class SeActionRemoveLastItem(SeActionSetItem): # name: Name of action def __init__(self, abitem, name: str): super().__init__(abitem, name) - self.__function = "remove last from list" + self._function = "remove last from list" def __repr__(self): return "SeAction RemoveLast {}".format(self._name) def write_to_logger(self): - self._log_debug("function: {}", self.__function) SeActionSetItem.write_to_logger(self) SeActionBase.write_to_logger(self) @@ -1296,11 +1454,22 @@ def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, def get(self): try: - item = str(self.__item.property.path) + if self.__item is not None: + item = str(self.__item.property.path) + else: + item = None + except Exception: + item = None + try: + val = self.__value.get() + if val is not None: + value = str(val) + else: + value = None except Exception: - item = str(self.__item) - result = {'function': str(self.__function), 'item': item, - 'value': str(self.__value.get()), 'conditionset': str(self.conditionset.get()), + value = None + result = {'function': str(self._function), 'item': item, + 'value': value, 'conditionset': str(self.conditionset.get()), 'previousconditionset': str(self.previousconditionset.get()), 'previousstate_conditionset': str(self.previousstate_conditionset.get()), 'actionstatus': {}} return result @@ -1313,13 +1482,12 @@ class SeActionRemoveAllItem(SeActionSetItem): # name: Name of action def __init__(self, abitem, name: str): super().__init__(abitem, name) - self.__function = "remove all from list" + self._function = "remove all from list" def __repr__(self): return "SeAction RemoveAll {}".format(self._name) def write_to_logger(self): - self._log_debug("function: {}", self.__function) SeActionSetItem.write_to_logger(self) SeActionBase.write_to_logger(self) @@ -1340,11 +1508,22 @@ def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, def get(self): try: - item = str(self.__item.property.path) + if self.__item is not None: + item = str(self.__item.property.path) + else: + item = None + except Exception: + item = None + try: + val = self.__value.get() + if val is not None: + value = str(val) + else: + value = None except Exception: - item = str(self.__item) - result = {'function': str(self.__function), 'item': item, - 'value': str(self.__value.get()), 'conditionset': str(self.conditionset.get()), + value = None + result = {'function': str(self._function), 'item': item, + 'value': value, 'conditionset': str(self.conditionset.get()), 'previousconditionset': str(self.previousconditionset.get()), 'previousstate_conditionset': str(self.previousstate_conditionset.get()), 'actionstatus': {}} return result diff --git a/stateengine/StateEngineActions.py b/stateengine/StateEngineActions.py index 5e134b467..6aa9976f2 100755 --- a/stateengine/StateEngineActions.py +++ b/stateengine/StateEngineActions.py @@ -83,85 +83,155 @@ def update(self, attribute, value): # If we do not have the action yet (delay-attribute before action-attribute), ... self.__unassigned_delays[name] = value else: - self.__actions[name].update_delay(value) - return + _issue = self.__actions[name].update_delay(value) + return _count, _issue elif func == "se_instanteval": # set instant calculation if name not in self.__actions: # If we do not have the action yet (repeat-attribute before action-attribute), ... self.__unassigned_instantevals[name] = value else: - self.__actions[name].update_instanteval(value) - return + _issue = self.__actions[name].update_instanteval(value) + return _count, _issue elif func == "se_repeat": # set repeat if name not in self.__actions: # If we do not have the action yet (repeat-attribute before action-attribute), ... self.__unassigned_repeats[name] = value else: - self.__actions[name].update_repeat(value) - return + _issue = self.__actions[name].update_repeat(value) + return _count, _issue elif func == "se_conditionset": # set conditionset if name not in self.__actions: # If we do not have the action yet (conditionset-attribute before action-attribute), ... self.__unassigned_conditionsets[name] = value else: - self.__actions[name].update_conditionsets(value) - return + _issue = self.__actions[name].update_conditionset(value) + return _count, _issue elif func == "se_previousconditionset": # set conditionset if name not in self.__actions: # If we do not have the action yet (conditionset-attribute before action-attribute), ... self.__unassigned_previousconditionsets[name] = value else: - self.__actions[name].update_previousconditionsets(value) - return + _issue = self.__actions[name].update_previousconditionset(value) + return _count, _issue elif func == "se_previousstate_conditionset": # set conditionset if name not in self.__actions: # If we do not have the action yet (conditionset-attribute before action-attribute), ... self.__unassigned_previousstate_conditionsets[name] = value else: - self.__actions[name].update_previousstate_conditionsets(value) - return + _issue = self.__actions[name].update_previousstate_conditionset(value) + return _count, _issue elif func == "se_mode": # set remove mode + _issue_list = [] if name not in self.__actions: # If we do not have the action yet (mode-attribute before action-attribute), ... self.__unassigned_modes[name] = value else: - self.__actions[name].update_modes(value) - return + _val, _issue = self.__actions[name].update_mode(value) + if _issue: + _issue_list.append(_issue) + _issue, _action = self.__check_mode_setting(name, _val, self.__actions[name].function, self.__actions[name]) + if _issue: + _issue_list.append(_issue) + if _action: + self.__actions[name] = _action + return _count, _issue_list elif func == "se_order": # set order if name not in self.__actions: # If we do not have the action yet (order-attribute before action-attribute), ... self.__unassigned_orders[name] = value else: - self.__actions[name].update_order(value) - return + _issue = self.__actions[name].update_order(value) + return _count, _issue elif func == "se_action": # and name not in self.__actions: _issue = self.__handle_combined_action_attribute(name, value) _count += 1 - elif self.__ensure_action_exists(func, name): - # update action - _issue = self.__actions[name].update(value) - _count += 1 + else: + _issue_list = [] + _ensure_action, _issue = self.__ensure_action_exists(func, name) + if _issue: + _issue_list.append(_issue) + if _ensure_action: + # update action + _issue = self.__actions[name].update(value) + if _issue: + _issue_list.append(_issue) + _count += 1 + _issue = StateEngineTools.flatten_list(_issue_list) except ValueError as ex: if name in self.__actions: del self.__actions[name] - _issue = {name: {'issue': ex, 'issueorigin': [{'state': 'unknown', 'action': 'unknown'}]}} + _issue = {name: {'issue': ex, 'issueorigin': [{'state': 'unknown', 'action': self.__actions[name].function}]}} self._log_warning("Ignoring action {0} because: {1}", attribute, ex) return _count, _issue + def __check_force_setting(self, name, value, function): + _issue = None + _returnfunction = function + if value is not None: + # Parameter force is supported only for type "set" and type "force" + if function not in ["set", "force"]: + _issue = { + name: {'issue': ['Parameter force not supported for this function'], + 'attribute': 'force', 'issueorigin': [{'state': 'unknown', 'action': function}]}} + _issue = "Parameter 'force' not supported for this function" + self._log_warning("Attribute 'se_action_{0}': Parameter 'force' not supported " + "for function '{1}'", name, function) + elif value and function == "set": + # Convert type "set" with force=True to type "force" + self._log_info("Attribute 'se_action_{0}': Parameter 'function' changed from 'set' to 'force', " + "because parameter 'force' is 'True'!", name) + _returnfunction = "force" + elif not value and function == "force": + # Convert type "force" with force=False to type "set" + self._log_info("Attribute 'se_action_{0}': Parameter 'function' changed from 'force' to 'set', " + "because parameter 'force' is 'False'!", name) + _returnfunction = "set" + return _issue, _returnfunction + def __check_mode_setting(self, name, value, function, action): + if value is not None: + possible_mode_list = ['first', 'last', 'all'] + _issue = None + # Parameter mode is supported only for type "remove" + if not "remove" in function: + _issue = {name: {'issue': ['Parameter mode not supported for this function'], 'attribute': 'mode', + 'issueorigin': [{'state': 'unknown', 'action': function}]}} + self._log_warning("Attribute 'se_action_{0}': Parameter 'mode' not supported for function '{1}'", + name, function) + elif function in ["remove", "remove all from list"]: + # Convert type "remove" with mode to specific remove type + if value in possible_mode_list: + if value == "all": + action = StateEngineAction.SeActionRemoveAllItem(self._abitem, name) + elif value == "first": + action = StateEngineAction.SeActionRemoveFirstItem(self._abitem, name) + elif value == "last": + action = StateEngineAction.SeActionRemoveLastItem(self._abitem, name) + self._log_info("Attribute 'se_action_{0}': Function 'remove' changed to '{1}'", name, value) + else: + _issue = { + name: {'issue': ['Parameter {} not allowed for mode!'.format(value)], 'attribute': 'mode', + 'issueorigin': [{'state': 'unknown', 'action': function}]}} + self._log_warning( + "Attribute 'se_action_{0}': Parameter '{1}' for 'mode' is wrong - can only be {2}", + name, value, possible_mode_list) + return _issue, action + return None, None + # ensure that action exists and create if missing # func: action function # name: action name def __ensure_action_exists(self, func, name): # Check if action exists + _issue = None if name in self.__actions: - return True + return True, _issue # Create action depending on function if func == "se_set": @@ -185,41 +255,61 @@ def __ensure_action_exists(self, func, name): elif func == "se_removelast": action = StateEngineAction.SeActionRemoveLastItem(self._abitem, name) else: - return False + return False, _issue + _issue_list = [] if name in self.__unassigned_delays: - action.update_delay(self.__unassigned_delays[name]) + _issue = action.update_delay(self.__unassigned_delays[name]) + if _issue: + _issue_list.append(_issue) del self.__unassigned_delays[name] if name in self.__unassigned_instantevals: - action.update_instanteval(self.__unassigned_instantevals[name]) + _issue = action.update_instanteval(self.__unassigned_instantevals[name]) + if _issue: + _issue_list.append(_issue) del self.__unassigned_instantevals[name] if name in self.__unassigned_repeats: - action.update_repeat(self.__unassigned_repeats[name]) + _issue = action.update_repeat(self.__unassigned_repeats[name]) + if _issue: + _issue_list.append(_issue) del self.__unassigned_repeats[name] if name in self.__unassigned_modes: - action.update_modes(self.__unassigned_modes[name]) + _val, _issue = action.update_mode(self.__unassigned_modes[name]) + if _issue: + _issue_list.append(_issue) + _issue, action = self.__check_mode_setting(name, _val, func.replace("se_", ""), action) + if _issue: + _issue_list.append(_issue) del self.__unassigned_modes[name] if name in self.__unassigned_orders: - action.update_order(self.__unassigned_orders[name]) + _issue = action.update_order(self.__unassigned_orders[name]) + if _issue: + _issue_list.append(_issue) del self.__unassigned_orders[name] if name in self.__unassigned_conditionsets: - action.update_conditionsets(self.__unassigned_conditionsets[name]) + _issue = action.update_conditionset(self.__unassigned_conditionsets[name]) + if _issue: + _issue_list.append(_issue) del self.__unassigned_conditionsets[name] if name in self.__unassigned_previousconditionsets: - action.update_previousconditionsets(self.__unassigned_previousconditionsets[name]) + _issue = action.update_previousconditionset(self.__unassigned_previousconditionsets[name]) + if _issue: + _issue_list.append(_issue) del self.__unassigned_previousconditionsets[name] if name in self.__unassigned_previousstate_conditionsets: - action.update_previousconditionsets(self.__unassigned_previousstate_conditionsets[name]) + _issue = action.update_previousconditionset(self.__unassigned_previousstate_conditionsets[name]) + if _issue: + _issue_list.append(_issue) del self.__unassigned_previousstate_conditionsets[name] self.__actions[name] = action - return True + return True, _issue_list def __handle_combined_action_attribute(self, name, value_list): # value_list needs to be string or list @@ -245,7 +335,7 @@ def __handle_combined_action_attribute(self, name, value_list): else: parameter[key] = val parameter['action'] = name - + _issue_list = [] # function given and valid? if parameter['function'] is None: raise ValueError("Attribute 'se_action_{0}: Parameter 'function' must be set!".format(name)) @@ -254,65 +344,55 @@ def __handle_combined_action_attribute(self, name, value_list): raise ValueError("Attribute 'se_action_{0}: Invalid value '{1}' for parameter " "'function'!".format(name, parameter['function'])) - # handle force - if parameter['force'] is not None: - # Parameter force is supported only for type "set" and type "force" - if parameter['function'] != "set" and parameter['function'] != "force": - self._log_warning("Attribute 'se_action_{0}': Parameter 'force' not supported " - "for function '{1}'", name, parameter['function']) - elif parameter['force'] and parameter['function'] == "set": - # Convert type "set" with force=True to type "force" - self._log_info("Attribute 'se_action_{0}': Parameter 'function' changed from 'set' to 'force', " - "because parameter 'force' is 'True'!", name) - parameter['function'] = "force" - elif not parameter['force'] and parameter['function'] == "force": - # Convert type "force" with force=False to type "set" - self._log_info("Attribute 'se_action_{0}': Parameter 'function' changed from 'force' to 'set', " - "because parameter 'force' is 'False'!", name) - parameter['function'] = "set" - - possible_mode_list = ['first', 'last', 'all'] - if parameter['mode'] is not None: - # Parameter mode is supported only for type "remove" - if parameter['function'] != "remove": - self._log_warning("Attribute 'se_action_{0}': Parameter 'mode' not supported for function '{1}'", - name, parameter['function']) - elif parameter['mode'] and parameter['function'] == "remove": - # Convert type "remove" with mode to specific remove type - if parameter['mode'] in possible_mode_list: - parameter['function'] = "remove{}".format(parameter['mode']) - self._log_info("Attribute 'se_action_{0}': Function 'remove' changed to '{1}'", name, parameter['function']) - else: - parameter['function'] = "remove" - self._log_info("Attribute 'se_action_{0}': Parameter '{1}' for 'mode' is wrong - can only be {2}", - name, parameter['mode'], possible_mode_list) - + _issue = None + _issue, parameter['function'] = self.__check_force_setting(name, parameter['force'], parameter['function']) + if _issue: + _issue_list.append(_issue) + _issue, _action = self.__check_mode_setting(name, parameter['mode'], parameter['function'], parameter['action']) + if _issue: + _issue_list.append(_issue) + if _action: + self.__actions[name] = _action # create action based on function exists = False - _issue = None try: if parameter['function'] == "set": - if self.__ensure_action_exists("se_set", name): + _action_exists, _issue = self.__ensure_action_exists("se_set", name) + if _issue: + _issue_list.append(_issue) + if _action_exists: self.__raise_missing_parameter_error(parameter, 'to') self.__actions[name].update(parameter['to']) exists = True elif parameter['function'] == "force": - if self.__ensure_action_exists("se_force", name): + _action_exists, _issue = self.__ensure_action_exists("se_force", name) + if _issue: + _issue_list.append(_issue) + if _action_exists: self.__raise_missing_parameter_error(parameter, 'to') self.__actions[name].update(parameter['to']) exists = True elif parameter['function'] == "run": - if self.__ensure_action_exists("se_run", name): + _action_exists, _issue = self.__ensure_action_exists("se_run", name) + if _issue: + _issue_list.append(_issue) + if _action_exists: self.__raise_missing_parameter_error(parameter, 'eval') self.__actions[name].update(parameter['eval']) exists = True elif parameter['function'] == "byattr": - if self.__ensure_action_exists("se_byattr", name): + _action_exists, _issue = self.__ensure_action_exists("se_byattr", name) + if _issue: + _issue_list.append(_issue) + if _action_exists: self.__raise_missing_parameter_error(parameter, 'attribute') self.__actions[name].update(parameter['attribute']) exists = True elif parameter['function'] == "trigger": - if self.__ensure_action_exists("se_trigger", name): + _action_exists, _issue = self.__ensure_action_exists("se_trigger", name) + if _issue: + _issue_list.append(_issue) + if _action_exists: self.__raise_missing_parameter_error(parameter, 'logic') if 'value' in parameter and parameter['value'] is not None: self.__actions[name].update(parameter['logic'] + ':' + parameter['value']) @@ -320,62 +400,102 @@ def __handle_combined_action_attribute(self, name, value_list): self.__actions[name].update(parameter['logic']) exists = True elif parameter['function'] == "special": - if self.__ensure_action_exists("se_special", name): + _action_exists, _issue = self.__ensure_action_exists("se_special", name) + if _issue: + _issue_list.append(_issue) + if _action_exists: self.__raise_missing_parameter_error(parameter, 'value') self.__actions[name].update(parameter['value']) exists = True elif parameter['function'] == "add": - if self.__ensure_action_exists("se_add", name): + _action_exists, _issue = self.__ensure_action_exists("se_add", name) + if _issue: + _issue_list.append(_issue) + if _action_exists: self.__raise_missing_parameter_error(parameter, 'value') self.__actions[name].update(parameter['value']) exists = True elif parameter['function'] == "remove": - if self.__ensure_action_exists("se_remove", name): + _action_exists, _issue = self.__ensure_action_exists("se_remove", name) + if _issue: + _issue_list.append(_issue) + if _action_exists: self.__raise_missing_parameter_error(parameter, 'value') self.__actions[name].update(parameter['value']) exists = True elif parameter['function'] == "removeall": - if self.__ensure_action_exists("se_removeall", name): + _action_exists, _issue = self.__ensure_action_exists("se_removeall", name) + if _issue: + _issue_list.append(_issue) + if _action_exists: self.__raise_missing_parameter_error(parameter, 'value') self.__actions[name].update(parameter['value']) exists = True elif parameter['function'] == "removefirst": - if self.__ensure_action_exists("se_removefirst", name): + _action_exists, _issue = self.__ensure_action_exists("se_removefirst", name) + if _issue: + _issue_list.append(_issue) + if _action_exists: self.__raise_missing_parameter_error(parameter, 'value') self.__actions[name].update(parameter['value']) exists = True elif parameter['function'] == "removelast": - if self.__ensure_action_exists("se_removelast", name): + _action_exists, _issue = self.__ensure_action_exists("se_removelast", name) + if _issue: + _issue_list.append(_issue) + if _action_exists: self.__raise_missing_parameter_error(parameter, 'value') self.__actions[name].update(parameter['value']) exists = True + except ValueError as ex: exists = False if name in self.__actions: del self.__actions[name] - _issue = {name: {'issue': ex, 'issueorigin': [{'state': 'unknown', 'action': 'unknown'}]}} + _issue = {name: {'issue': ex, 'issueorigin': [{'state': 'unknown', 'action': parameter['function']}]}} + _issue_list.append(_issue) self._log_warning("Ignoring action {0} because: {1}", name, ex) # add additional parameters if exists: if parameter['instanteval'] is not None: - self.__actions[name].update_instanteval(parameter['instanteval']) + _issue = self.__actions[name].update_instanteval(parameter['instanteval']) + if _issue: + _issue_list.append(_issue) if parameter['repeat'] is not None: - self.__actions[name].update_repeat(parameter['repeat']) + _issue = self.__actions[name].update_repeat(parameter['repeat']) + if _issue: + _issue_list.append(_issue) if parameter['delay'] != 0: - self.__actions[name].update_delay(parameter['delay']) + _issue = self.__actions[name].update_delay(parameter['delay']) + if _issue: + _issue_list.append(_issue) if parameter['order'] is not None: - self.__actions[name].update_order(parameter['order']) + _issue = self.__actions[name].update_order(parameter['order']) + if _issue: + _issue_list.append(_issue) if parameter['conditionset'] is not None: - self.__actions[name].update_conditionsets(parameter['conditionset']) + _issue = self.__actions[name].update_conditionset(parameter['conditionset']) + if _issue: + _issue_list.append(_issue) if parameter['previousconditionset'] is not None: - self.__actions[name].update_previousconditionsets(parameter['previousconditionset']) + _issue = self.__actions[name].update_previousconditionset(parameter['previousconditionset']) + if _issue: + _issue_list.append(_issue) if parameter['previousstate_conditionset'] is not None: - self.__actions[name].update_previousstate_conditionsets(parameter['previousstate_conditionset']) + _issue = self.__actions[name].update_previousstate_conditionset(parameter['previousstate_conditionset']) + if _issue: + _issue_list.append(_issue) if parameter['mode'] is not None: - self.__actions[name].update_modes(parameter['mode']) - - return _issue + _val, _issue = self.__actions[name].update_mode(parameter['mode']) + if _issue: + _issue_list.append(_issue) + _issue, _action = self.__check_mode_setting(name, _val, parameter['function'], self.__actions[name]) + if _issue: + _issue_list.append(_issue) + if _action: + self.__actions[name] = _action + return _issue_list # noinspection PyMethodMayBeStatic def __raise_missing_parameter_error(self, parameter, param_name): @@ -410,9 +530,11 @@ def set(self, value): def execute(self, is_repeat: bool, allow_item_repeat: bool, state, additional_actions=None): actions = [] for name in self.__actions: + self._log_develop("Append action {}", self.__actions[name]) actions.append((self.__actions[name].get_order(), self.__actions[name])) if additional_actions is not None: for name in additional_actions.__actions: + self._log_develop("Append additional action {}", additional_actions.__actions[name]) actions.append((additional_actions.__actions[name].get_order(), additional_actions.__actions[name])) for order, action in sorted(actions, key=lambda x: x[0]): self.__queue.put([action, is_repeat, allow_item_repeat, state]) diff --git a/stateengine/StateEngineCondition.py b/stateengine/StateEngineCondition.py index e15d2c935..ce3dd0e75 100755 --- a/stateengine/StateEngineCondition.py +++ b/stateengine/StateEngineCondition.py @@ -44,6 +44,7 @@ def __init__(self, abitem, name: str): self.__item = None self.__status = None self.__eval = None + self.__status_eval = None self.__value = StateEngineValue.SeValue(self._abitem, "value", True) self.__min = StateEngineValue.SeValue(self._abitem, "min") self.__max = StateEngineValue.SeValue(self._abitem, "max") @@ -61,36 +62,94 @@ def __init__(self, abitem, name: str): self.__itemClass = Item def __repr__(self): - return "SeCondition 'item': {}, 'status': {}, 'eval': {}, 'value': {}".format(self.__item, self.__status, self.__eval, self.__value) + return "SeCondition 'item': {}, 'status': {}, 'eval': {}, " \ + "'status_eval': {}, 'value': {}".format(self.__item, self.__status, self.__eval, self.__status_eval, self.__value) + + def check_items(self, check, value=None, item_state=None): + item_issue, status_issue, eval_issue, status_eval_issue = None, None, None, None + item_value, status_value, eval_value, status_eval_value = None, None, None, None + if check == "se_item" or (check == "attribute" and self.__item is None and self.__eval is None): + if value is None: + value = StateEngineTools.find_attribute(self._sh, item_state, "se_item_" + self.__name) + if value is not None: + match = re.match(r'^(.*):', value) + if isinstance(value, str) and value.startswith("eval:"): + _, _, value = value.partition(":") + self.__eval = value + self.__item = None + elif match: + self._log_warning("Your item configuration '{0}' is wrong! Define a plain (relative) " + "item without {1} at the beginning!", value, match.group(1)) + item_issue = "Your eval configuration '{0}' is wrong, remove {1}".format(value, match.group(1)) + self.__item = None + else: + value, issue = self._abitem.return_item(value) + self.__item = value + item_value = value + if check == "se_status" or (check == "attribute" and self.__status is None and self.__status_eval is None): + if value is None: + value = StateEngineTools.find_attribute(self._sh, item_state, "se_status_" + self.__name) + if value is not None: + match = re.match(r'^(.*):', value) + if isinstance(value, str) and value.startswith("eval:"): + _, _, value = value.partition(":") + self.__status_eval = value + self.__status = None + elif match: + self._log_warning("Your item configuration '{0}' is wrong! Define a plain (relative) " + "item without {1} at the beginning!", value, match.group(1)) + status_issue = "Your eval configuration '{0}' is wrong, remove {1}".format(value, match.group(1)) + self.__status = None + value = None + else: + value, issue = self._abitem.return_item(value) + self.__status = value + status_value = value + if check == "se_eval" or (check == "attribute" and self.__eval is None): + if value is None: + value = StateEngineTools.find_attribute(self._sh, item_state, "se_eval_" + self.__name) + if value is not None: + match = re.match(r'^(.*):', value) + if value.startswith("eval:"): + _, _, value = value.partition("eval:") + elif match: + self._log_warning("Your eval configuration '{0}' is wrong! You have to define " + "a plain eval expression", value) + eval_issue = "Your eval configuration '{0}' is wrong!".format(value) + value = None + self.__eval = value + eval_value = value + if check == "se_status_eval" or (check == "attribute" and self.__status_eval is None): + if value is None: + value = StateEngineTools.find_attribute(self._sh, item_state, "se_status_eval_" + self.__name) + if value is not None: + match = re.match(r'^(.*):', value) + if value.startswith("eval:"): + _, _, value = value.partition("eval:") + elif match: + self._log_warning("Your status eval configuration '{0}' is wrong! You have to define " + "a plain eval expression", value) + status_eval_issue = "Your status eval configuration '{0}' is wrong!".format(value) + value = None + self.__status_eval = value + status_eval_value = value + return item_value, status_value, eval_value, status_eval_value, item_issue, status_issue, eval_issue, status_eval_issue # set a certain function to a given value - # func: Function to set ('item', 'eval', 'value', 'min', 'max', 'negate', 'changedby', 'updatedby', + # func: Function to set ('item', 'eval', 'status_eval', 'value', 'min', 'max', 'negate', 'changedby', 'updatedby', # 'triggeredby','changedbynegate', 'updatedbynegate', 'triggeredbynegate','agemin', 'agemax' or 'agenegate') # value: Value for function def set(self, func, value): issue = None if func == "se_item": - if ":" in value: - self._log_warning("Your item configuration '{0}' is wrong! Define a plain (relative) " - "item without item: at the beginning!", value) - _, _, value = value.partition(":") - self.__item, issue = self._abitem.return_item(value) + value, _, _, _, issue, _, _, _ = self.check_items("se_item", value) elif func == "se_status": - if ":" in value: - self._log_warning("Your status configuration '{0}' is wrong! Define a plain (relative) " - "item without item: at the beginning!", value) - _, _, value = value.partition(":") - self.__status, issue = self._abitem.return_item(value) + _, value, _, _, _, issue, _, _ = self.check_items("se_status", value) elif func == "se_eval": - if value.startswith("eval:"): - _, _, value = value.partition("eval:") - wrong_start = ["item:", "regex:", "value:", "var:"] - if any(value.startswith(wrong_start) for wrong_start in wrong_start): - self._log_warning("Your eval configuration '{0}' is wrong! You have to define " - "a plain eval expression", value) - issue = "Your eval configuration '{0}' is wrong!".format(value) - value = None - self.__eval = value + _, _, value, _, _, _, issue, _ = self.check_items("se_eval", value) + elif func == "se_status_eval": + _, _, _, value, _, _, _, issue = self.check_items("se_status_eval", value) + if func == "se_value": self.__value.set(value, self.__name) elif func == "se_min": @@ -117,18 +176,23 @@ def set(self, func, value): self.__negate = value elif func == "se_agenegate": self.__agenegate = value - elif func != "se_item" and func != "se_eval" and func != "se_status": + elif func != "se_item" and func != "se_eval" and func != "se_status_eval" and func != "se_status": self._log_warning("Function '{0}' is no valid function! Please check item attribute.", func) issue = "Function '{0}' is no valid function!".format(func) return issue def get(self): _eval_result = str(self.__eval) + _status_eval_result = str(self.__status_eval) if 'SeItem' in _eval_result: _eval_result = _eval_result.split('SeItem.')[1].split(' ')[0] if 'SeCurrent' in _eval_result: _eval_result = _eval_result.split('SeCurrent.')[1].split(' ')[0] - _value_result = str(self.__value.get_for_webif()) + if 'SeItem' in _status_eval_result: + _status_eval_result = _status_eval_result.split('SeItem.')[1].split(' ')[0] + if 'SeCurrent' in _status_eval_result: + _status_eval_result = _status_eval_result.split('SeCurrent.')[1].split(' ')[0] + _value_result = self.__value.get_for_webif() try: _item = self.__item.property.path except Exception: @@ -137,7 +201,8 @@ def get(self): _status = self.__status.property.path except Exception: _status = self.__status - result = {'item': _item, 'status': _status, 'eval': _eval_result, 'value': _value_result, + result = {'item': _item, 'status': _status, 'eval': _eval_result, 'status_eval': _status_eval_result, + 'value': _value_result, 'min': str(self.__min), 'max': str(self.__max), 'agemin': str(self.__agemin), 'agemax': str(self.__agemax), 'negate': str(self.__negate), 'agenegate': str(self.__agenegate), @@ -159,7 +224,7 @@ def complete(self, item_state): return False # set 'eval' for some known conditions if item and eval are not set, yet - if self.__item is None and self.__status is None and self.__eval is None: + if all(item is None for item in [self.__item, self.__status, self.__eval, self.__status_eval]): if self.__name == "weekday": self.__eval = StateEngineCurrent.values.get_weekday elif self.__name == "sun_azimut": @@ -211,35 +276,19 @@ def complete(self, item_state): elif self.__name == "original_source": self.__eval = self._abitem.get_update_original_source - # missing item in condition: Try to find it - if self.__item is None: - result = StateEngineTools.find_attribute(self._sh, item_state, "se_item_" + self.__name) - if result is not None: - self.__item, issue = self._abitem.return_item(result) - - # missing status in condition: Try to find it - if self.__status is None: - result = StateEngineTools.find_attribute(self._sh, item_state, "se_status_" + self.__name) - if result is not None: - self.__status, issue = self._abitem.return_item(result) - - # missing eval in condition: Try to find it - if self.__eval is None: - result = StateEngineTools.find_attribute(self._sh, item_state, "se_eval_" + self.__name) - if result is not None: - self.__eval = result - - # now we should have either 'item' or 'eval' set. If not, raise ValueError - if self.__item is None and self.__status is None and self.__eval is None: - raise ValueError("Neither 'item' nor 'status' nor 'eval' given!") - - if (self.__item is not None or self.__status is not None or self.__eval is not None)\ + self.check_items("attribute", None, item_state) + + # now we should have either 'item' or '(status)eval' set. If not, raise ValueError + if all(item is None for item in [self.__item, self.__status, self.__eval, self.__status_eval]): + raise ValueError("Neither 'item' nor 'status' nor '(status)eval' given!") + + if any(item is not None for item in [self.__item, self.__status, self.__eval, self.__status_eval])\ and not self.__changedby.is_empty() and self.__changedbynegate is None: self.__changedbynegate = False - if (self.__item is not None or self.__status is not None or self.__eval is not None)\ + if any(item is not None for item in [self.__item, self.__status, self.__eval, self.__status_eval])\ and not self.__updatedby.is_empty() and self.__updatedbynegate is None: self.__updatedbynegate = False - if (self.__item is not None or self.__status is not None or self.__eval is not None)\ + if any(item is not None for item in [self.__item, self.__status, self.__eval, self.__status_eval])\ and not self.__triggeredby.is_empty() and self.__triggeredbynegate is None: self.__triggeredbynegate = False @@ -261,7 +310,7 @@ def complete(self, item_state): elif self.__name == "time": self.__cast_all(StateEngineTools.cast_time) except Exception as ex: - raise ValueError("Condition {0}: Error when casting: {1}".format(self.__name, ex)) + raise ValueError("Error when casting: {0}".format(ex)) # 'agemin' and 'agemax' can only be used for items cond_min_max = self.__agemin.is_empty() and self.__agemax.is_empty() @@ -269,15 +318,22 @@ def complete(self, item_state): cond_evalitem = self.__eval and ("get_relative_item(" in self.__eval or "return_item(" in self.__eval) except Exception: cond_evalitem = False - if self.__item is None and self.__status is None and not cond_min_max and not cond_evalitem: + try: + cond_status_evalitem = self.__status_eval and \ + ("get_relative_item(" in self.__status_eval or "return_item(" in self.__status_eval) + except Exception: + cond_status_evalitem = False + if self.__item is None and self.__status is None and \ + not cond_min_max and not cond_evalitem and not cond_status_evalitem: raise ValueError("Condition {}: 'agemin'/'agemax' can not be used for eval!".format(self.__name)) return True # Check if condition is matching def check(self, state): # Ignore if no current value can be determined (should not happen as we check this earlier, but to be sure ...) - if self.__item is None and self.__status is None and self.__eval is None: - self._log_info("Condition '{0}': No item, status or eval found! Considering condition as matching!", self.__name) + if all(item is None for item in [self.__item, self.__status, self.__eval, self.__status_eval]): + self._log_info("Condition '{0}': No item, status or (status)eval found! " + "Considering condition as matching!", self.__name) return True self._log_debug("Condition '{0}': Checking all relevant stuff", self.__name) self._log_increase_indent() @@ -316,11 +372,17 @@ def write_to_logger(self): else: self._log_info("status item: {0} ({1})", self.__name, self.__status.property.path) if self.__eval is not None: - if isinstance(self.__item, list): - for e in self.__item: + if isinstance(self.__eval, list): + for e in self.__eval: self._log_info("eval: {0}", StateEngineTools.get_eval_name(e)) else: self._log_info("eval: {0}", StateEngineTools.get_eval_name(self.__eval)) + if self.__status_eval is not None: + if isinstance(self.__status_eval, list): + for e in self.__status_eval: + self._log_info("status eval: {0}", StateEngineTools.get_eval_name(e)) + else: + self._log_info("status eval: {0}", StateEngineTools.get_eval_name(self.__status_eval)) self.__value.write_to_logger() self.__min.write_to_logger() self.__max.write_to_logger() @@ -537,7 +599,7 @@ def __check_value(self, state): for i, _ in enumerate(min_value): min = None if min_value[i] == 'novalue' else min_value[i] max = None if max_value[i] == 'novalue' else max_value[i] - self._log_debug("Checking minvalue {} ({}) and maxvalue {}({}) against current {}({})", min, type(min), max, type(max), current, type(current)) + self._log_debug("Checking minvalue {} ({}) and maxvalue {} ({}) against current {} ({})", min, type(min), max, type(max), current, type(current)) if min is not None and max is not None and min > max: min, max = max, min self._log_warning("Condition {}: min must not be greater than max! " @@ -664,7 +726,7 @@ def __check_age(self, state): return True # Ignore if no current value can be determined - if self.__item is None and self.__status is None and self.__eval is None: + if all(item is None for item in [self.__item, self.__status, self.__eval, self.__status_eval]): self._log_warning("Age of '{0}': No item/eval found! Considering condition as matching!", self.__name) return True @@ -672,17 +734,25 @@ def __check_age(self, state): cond_evalitem = self.__eval and ("get_relative_item(" in self.__eval or "return_item(" in self.__eval) except Exception: cond_evalitem = False + try: + cond_status_evalitem = self.__status_eval and \ + ("get_relative_item(" in self.__status_eval or "return_item(" in self.__status_eval) + except Exception: + cond_status_evalitem = False if self.__item is None and cond_evalitem is False: - self._log_warning("Make sure your se_eval '{}' really contains an item and not an ID. If the age " - "condition does not work though, please check your eval!", self.__eval) - + self._log_warning("Make sure your se_eval/se_item: eval:<..> '{}' really returns an item and not an ID. " + "If the age condition does not work, please check your eval!", self.__eval) + if self.__status is None and cond_status_evalitem is False: + self._log_warning("Make sure your se_status: eval:<..> '{}' really returns an item and not an ID. " + "If the age condition does not work, please check your eval!", self.__status_eval) try: current = self.__get_current(eval_type='age') except Exception as ex: _key = ['{}'.format(state.id), 'conditionsets', '{}'.format(self._abitem.get_variable('current.conditionset_name')), '{}'.format(self.__name), 'match', 'age'] - self._abitem.update_webif(_key, 'Not possible to get age from eval {}'.format(self.__eval)) - self._log_warning("Age of '{0}': Not possible to get age from eval {1}! " - "Considering condition as matching: {2}", self.__name, self.__eval, ex) + self._abitem.update_webif(_key, 'Not possible to get age from eval {} ' + 'or status_eval {}'.format(self.__eval, self.__status_eval)) + self._log_warning("Age of '{0}': Not possible to get age from eval {1} or status_eval {2}! " + "Considering condition as matching: {3}", self.__name, self.__eval, self.__status_eval, ex) return True agemin = None if self.__agemin.is_empty() else self.__agemin.get() @@ -752,6 +822,32 @@ def __check_age(self, state): # Current value of condition (based on item or eval) def __get_current(self, eval_type='value'): + def check_eval(eval_or_status_eval): + if isinstance(eval_or_status_eval, str): + sh = self._sh + shtime = self._shtime + # noinspection PyUnusedLocal + if "stateengine_eval" in eval_or_status_eval or "se_eval" in eval_or_status_eval: + # noinspection PyUnusedLocal + stateengine_eval = se_eval = StateEngineEval.SeEval(self._abitem) + try: + eval_result = eval(eval_or_status_eval) + if isinstance(eval_result, self.__itemClass): + value = eval_result.property.last_change_age if eval_type == 'age' else \ + eval_result.property.last_change_by if eval_type == 'changedby' else \ + eval_result.property.last_update_by if eval_type == 'updatedby' else \ + eval_result.property.last_trigger_by if eval_type == 'triggeredby' else \ + eval_result.property.value + else: + value = eval_result + except Exception as ex: + text = "Condition {}: problem evaluating {}: {}" + raise ValueError(text.format(self.__name, eval_or_status_eval, ex)) + else: + return value + else: + return eval_or_status_eval() + if self.__status is not None: # noinspection PyUnusedLocal self._log_debug("Trying to get {} of status item {}", eval_type, self.__status) @@ -768,30 +864,13 @@ def __get_current(self, eval_type='value'): self.__item.property.last_update_by if eval_type == 'updatedby' else\ self.__item.property.last_trigger_by if eval_type == 'triggeredby' else\ self.__item.property.value - if self.__eval is not None: - # noinspection PyUnusedLocal - self._log_debug("Trying to get {} of eval {}", eval_type, self.__eval) - sh = self._sh - shtime = self._shtime - if isinstance(self.__eval, str): - # noinspection PyUnusedLocal - if "stateengine_eval" in self.__eval or "se_eval" in self.__eval: - # noinspection PyUnusedLocal - stateengine_eval = se_eval = StateEngineEval.SeEval(self._abitem) - try: - if isinstance(eval(self.__eval), self.__itemClass): - value = eval(self.__eval).property.last_change_age if eval_type == 'age' else \ - eval(self.__eval).property.last_change_by if eval_type == 'changedby' else \ - eval(self.__eval).property.last_update_by if eval_type == 'updatedby' else \ - eval(self.__eval).property.last_trigger_by if eval_type == 'triggeredby' else \ - eval(self.__eval).property.value - else: - value = eval(self.__eval) - except Exception as ex: - text = "Condition {}: problem evaluating {}: {}" - raise ValueError(text.format(self.__name, self.__eval, ex)) - else: - return value - else: - return self.__eval() - raise ValueError("Condition {}: Neither 'item' nor 'status' nor 'eval' given!".format(self.__name)) + if self.__status_eval is not None: + self._log_debug("Trying to get {} of statuseval {}", eval_type, self.__status_eval) + return_value = check_eval(self.__status_eval) + return return_value + elif self.__eval is not None: + self._log_debug("Trying to get {} of statuseval {}", eval_type, self.__eval) + return_value = check_eval(self.__eval) + return return_value + + raise ValueError("Condition {}: Neither 'item' nor 'status' nor '(status)eval' given!".format(self.__name)) diff --git a/stateengine/StateEngineConditionSet.py b/stateengine/StateEngineConditionSet.py index 256c2379b..8b9d42c7f 100755 --- a/stateengine/StateEngineConditionSet.py +++ b/stateengine/StateEngineConditionSet.py @@ -97,7 +97,7 @@ def update(self, item, grandparent_item): self.__conditions[name] = StateEngineCondition.SeCondition(self._abitem, name) issue = self.__conditions[name].set(func, item.conf[attribute]) self.__conditions.move_to_end(name, last=True) - if issue: + if issue not in [[], None, [None]]: self.__unused_attributes.update({name: {'attribute': attribute, 'issue': issue}}) elif name not in self.__used_attributes.keys(): self.__used_attributes.update({name: {'attribute': attribute}}) @@ -113,7 +113,7 @@ def update(self, item, grandparent_item): if name == "": continue cond1 = name not in self.__used_attributes.keys() - cond2 = func == "se_item" or func == "se_eval" or func == "se_status" + cond2 = func == "se_item" or func == "se_eval" or func == "se_status_eval" or func == "se_status" cond3 = name not in self.__unused_attributes.keys() if cond1: if cond2 and cond3: @@ -125,7 +125,7 @@ def update(self, item, grandparent_item): self.__conditions[name] = StateEngineCondition.SeCondition(self._abitem, name) try: issue = self.__conditions[name].set(func, grandparent_item.conf[attribute]) - if issue: + if issue not in [[], None, [None]]: self.__unused_attributes.update({name: {'attribute': attribute, 'issue': issue}}) except ValueError as ex: self.__unused_attributes.update({name: {'attribute': attribute, 'issue': ex}}) diff --git a/stateengine/StateEngineDefaults.py b/stateengine/StateEngineDefaults.py index 5486edeca..6b4a24500 100755 --- a/stateengine/StateEngineDefaults.py +++ b/stateengine/StateEngineDefaults.py @@ -36,8 +36,11 @@ logger = None +plugin_version = 0 + def write_to_log(logger): + logger.info("Plugin {0} Version {1}".format(plugin_identification, plugin_version)) logger.info("StateEngine default suntracking offset = {0}".format(suntracking_offset)) logger.info("StateEngine default suntracking lamella open value = {0}".format(lamella_open_value)) logger.info("StateEngine default startup delay = {0}".format(startup_delay)) diff --git a/stateengine/StateEngineEval.py b/stateengine/StateEngineEval.py index ffbc18f70..0bfa3de7c 100755 --- a/stateengine/StateEngineEval.py +++ b/stateengine/StateEngineEval.py @@ -155,6 +155,7 @@ def get_relative_item(self, subitem_id): # See description of StateEngineItem.SeItem.return_item for details def get_relative_itemvalue(self, subitem_id): self._eval_lock.acquire() + returnvalue = [] self._log_debug("Executing method 'get_relative_itemvalue({0})'", subitem_id) try: if self._abitem._initstate and subitem_id == '..state_name': @@ -163,12 +164,13 @@ def get_relative_itemvalue(self, subitem_id): else: item, issue = self._abitem.return_item(subitem_id) returnvalue = item.property.value - self._log_debug("Return item value '{0}' for item {1}", returnvalue, item.property.path) + returnvalue = StateEngineTools.convert_str_to_list(returnvalue) + self._log_debug("Return item value '{0}' for item {1}", returnvalue, subitem_id) except Exception as ex: - returnvalue = None self._log_warning("Problem evaluating value of '{0}': {1}", subitem_id, ex) finally: self._eval_lock.release() + returnvalue = returnvalue[0] if len(returnvalue) == 1 else None if len(returnvalue) == 0 else returnvalue return returnvalue # Return the property of an item related to the StateEngine Object Item @@ -192,6 +194,10 @@ def get_relative_itemproperty(self, subitem_id, prop): self._abitem.return_item(self._abitem._initstate.id)[0].property.path, returnvalue) else: returnvalue = getattr(item.property, prop) + if prop == "value": + returnvalue = StateEngineTools.convert_str_to_list(returnvalue) + returnvalue = returnvalue[0] if len(returnvalue) == 1 else None if len( + returnvalue) == 0 else returnvalue self._log_debug("Return item property {0} from {1}: {2}", prop, item.property.path, returnvalue) except Exception as ex: returnvalue = None diff --git a/stateengine/StateEngineFunctions.py b/stateengine/StateEngineFunctions.py index 6b90c0822..a8df0f13e 100755 --- a/stateengine/StateEngineFunctions.py +++ b/stateengine/StateEngineFunctions.py @@ -24,7 +24,9 @@ import re from . import StateEngineLogger from . import StateEngineTools +from . import StateEngineDefaults from lib.item import Items +from ast import literal_eval class SeFunctions: @@ -64,6 +66,39 @@ def __get_lock(self, lock_id): # If the original caller/source should be considered, the method returns the inverted value of the item. # Otherwise, the method returns the current value of the item, so that no change will be made def manual_item_update_eval(self, item_id, caller=None, source=None): + def check_include_exclude(entry_type): + conf_entry = item.conf["se_manual_{}".format(entry_type)] + if isinstance(conf_entry, str): + if ',' in conf_entry or conf_entry.startswith("["): + try: + new_conf_entry = literal_eval(conf_entry) + if isinstance(new_conf_entry, list): + conf_entry = new_conf_entry + except Exception: + conf_entry = [conf_entry, ] + else: + conf_entry = [conf_entry, ] + elif not isinstance(conf_entry, list): + elog.error("Item '{0}', Attribute 'se_manual_{1}': Value must be a string or a list!", item_id, entry_type) + return retval_no_trigger + elog.info("checking manual {0} values: {1}", entry_type, conf_entry) + elog.increase_indent() + + # If current value is in list -> Return "Trigger" + for e in conf_entry: + e = re.compile(e, re.IGNORECASE) + result = e.match(original) + elog.info("Checking regex result {}", result) + if result is not None: + elog.info("{0}: matching.", e) + elog.decrease_indent() + returnvalue = retval_trigger if entry_type == "include" else retval_no_trigger + elog.info("Writing value {0}", returnvalue) + return returnvalue + elog.info("{0}: not matching", e) + elog.decrease_indent() + return None + item = self.itemsApi.return_item(item_id) if item is None: self.logger.error("manual_item_update_eval: item {0} not found!".format(item_id)) @@ -83,11 +118,11 @@ def manual_item_update_eval(self, item_id, caller=None, source=None): self.logger.error("manual_item_update_item: se_manual_logitem {0} not found!".format(elog_item_id)) elog = StateEngineLogger.SeLoggerDummy() else: - elog = StateEngineLogger.SeLogger.create(elog_item) + elog = StateEngineLogger.SeLogger.create(elog_item, manual=True) else: elog = StateEngineLogger.SeLoggerDummy() elog.header("manual_item_update_eval") - elog.info("running for item '{0}' source '{1}' caller '{2}'", item_id, caller, source) + elog.info("running for item '{0}' source '{1}' caller '{2}'", item_id, source, caller) retval_no_trigger = item() retval_trigger = not item() @@ -97,7 +132,7 @@ def manual_item_update_eval(self, item_id, caller=None, source=None): elog.info("get_caller({0}, {1}): original trigger by {2}:{3}", caller, source, original_caller, original_source) original = "{}:{}".format(original_caller, original_source) - entry = re.compile("Stateengine Plugin", re.IGNORECASE) + entry = re.compile(StateEngineDefaults.plugin_identification, re.IGNORECASE) result = entry.match(original) if result is not None: elog.info("Manual item updated by Stateengine Plugin. Ignoring change and writing value {}", @@ -105,75 +140,23 @@ def manual_item_update_eval(self, item_id, caller=None, source=None): return retval_no_trigger if "se_manual_on" in item.conf: - # get list of include entries - include = item.conf["se_manual_on"] - if isinstance(include, str): - include = [include, ] - elif not isinstance(include, list): - elog.error("Item '{0}', Attribute 'se_manual_on': Value must be a string or a list!", item_id) - return retval_no_trigger - elog.info("checking manual on values: {0}", include) - elog.increase_indent() - - # If current value is in list -> Return "Trigger" - for entry in include: - entry = re.compile(entry, re.IGNORECASE) - result = entry.match(original) - elog.info("Checking regex result {}", result) - if result is not None: - elog.info("{0}: matching. Writing value {1}", entry, retval_no_trigger) - return retval_no_trigger - elog.info("{0}: not matching", entry) - elog.decrease_indent() + returnvalue = check_include_exclude("on") + if returnvalue is not None: + return returnvalue if "se_manual_exclude" in item.conf: - # get list of exclude entries - exclude = item.conf["se_manual_exclude"] - - if isinstance(exclude, str): - exclude = [exclude, ] - elif not isinstance(exclude, list): - elog.error("Item '{0}', Attribute 'se_manual_exclude': Value must be a string or a list!", item_id) - return retval_no_trigger - elog.info("checking exclude values: {0}", exclude) - elog.increase_indent() - - # If current value is in list -> Return "NoTrigger" - for entry in exclude: - entry = re.compile(entry, re.IGNORECASE) - result = entry.match(original) - elog.info("Checking regex result {}", result) - if result is not None: - elog.info("{0}: matching. Writing value {1}", entry, retval_no_trigger) - return retval_no_trigger - elog.info("{0}: not matching", entry) - elog.decrease_indent() + returnvalue = check_include_exclude("exclude") + if returnvalue is not None: + return returnvalue if "se_manual_include" in item.conf: - # get list of include entries - include = item.conf["se_manual_include"] - if isinstance(include, str): - include = [include, ] - elif not isinstance(include, list): - elog.error("Item '{0}', Attribute 'se_manual_include': Value must be a string or a list!", item_id) + returnvalue = check_include_exclude("include") + if returnvalue is not None: + return returnvalue + else: + # Current value not in list -> Return "No Trigger + elog.info("No include values matching. Writing value {0}", retval_no_trigger) return retval_no_trigger - elog.info("checking include values: {0}", include) - elog.increase_indent() - - # If current value is in list -> Return "Trigger" - for entry in include: - entry = re.compile(entry, re.IGNORECASE) - result = entry.match(original) - elog.info("Checking regex result {}", result) - if result is not None: - elog.info("{0}: matching. Writing value {1}", entry, retval_trigger) - return retval_trigger - elog.info("{0}: not matching", entry) - elog.decrease_indent() - - # Current value not in list -> Return "No Trigger - elog.info("No include values matching. Writing value {0}", retval_no_trigger) - return retval_no_trigger else: # No include-entries -> return "Trigger" elog.info("No include limitation. Writing value {0}", retval_trigger) diff --git a/stateengine/StateEngineItem.py b/stateengine/StateEngineItem.py index 3cd77f8f7..0ed756904 100755 --- a/stateengine/StateEngineItem.py +++ b/stateengine/StateEngineItem.py @@ -31,6 +31,7 @@ from . import StateEngineValue from . import StateEngineStruct from . import StateEngineStructs +from . import StateEngineEval from lib.item import Items from lib.shtime import Shtime @@ -39,6 +40,7 @@ import threading import queue import re +from ast import literal_eval # Class representing a blind item @@ -53,6 +55,10 @@ def id(self): def variables(self): return self.__variables + @property + def firstrun(self): + return self.__first_run + @property def templates(self): return self.__templates @@ -93,7 +99,7 @@ def logger(self): @property def instant_leaveaction(self): - return self.__instant_leaveaction + return self.__instant_leaveaction.get() @property def default_instant_leaveaction(self): @@ -101,13 +107,19 @@ def default_instant_leaveaction(self): @default_instant_leaveaction.setter def default_instant_leaveaction(self, value): - self.__default_instant_leaveaction = value + self.__default_instant_leaveaction.set(value) @property def laststate(self): _returnvalue = None if self.__laststate_item_id is None else self.__laststate_item_id.property.value return _returnvalue + @property + def laststate_releasedby(self): + _returnvalue = None if self.__laststate_item_id is None \ + else self.__release_info.get(self.__laststate_item_id.property.value) + return _returnvalue + @property def previousstate(self): _returnvalue = None if self.__previousstate_item_id is None else self.__previousstate_item_id.property.value @@ -176,9 +188,9 @@ def __init__(self, smarthome, item, se_plugin): self.__shtime = Shtime.get_instance() self.__se_plugin = se_plugin self.__active_schedulers = [] - self.__default_instant_leaveaction = StateEngineValue.SeValue(self, "Default Instant Leave Action", False, - "bool") - #self.__all_torelease = {} + self.__release_info = {} + self.__default_instant_leaveaction = StateEngineValue.SeValue(self, "Default Instant Leave Action", False, "bool") + self.__instant_leaveaction = StateEngineValue.SeValue(self, "Instant Leave Action", False, "num") try: self.__id = self.__item.property.path except Exception: @@ -186,39 +198,45 @@ def __init__(self, smarthome, item, se_plugin): self.__name = str(self.__item) self.__itemClass = Item # initialize logging - self.__logger.header("") - _log_level = StateEngineValue.SeValue(self, "Log Level", False, "num") + + self.__log_level = StateEngineValue.SeValue(self, "Log Level", False, "num") + _default_log_level = SeLogger.default_log_level.get() - _returnvalue, _returntype, _using_default, _issue = _log_level.set_from_attr(self.__item, "se_log_level", - _default_log_level) + _returnvalue, _returntype, _using_default, _issue = self.__log_level.set_from_attr(self.__item, "se_log_level", + _default_log_level) + self.__using_default_log_level = _using_default + _returnvalue = self.__log_level.get() + if isinstance(_returnvalue, list) and len(_returnvalue) == 1: + _returnvalue = _returnvalue[0] + self.__logger.log_level_as_num = 2 + self.__logger.header("") - if len(_returnvalue) > 1: + _startup_log_level = SeLogger.startup_log_level.get() + + if _startup_log_level > 0: + base = self.__sh.get_basedir() + SeLogger.manage_logdirectory(base, SeLogger.log_directory, True) + self.__logger.log_level_as_num = _startup_log_level + self.__logger.info("Set log level to startup log level {}", _startup_log_level) + + if isinstance(_returnvalue, list) and len(_returnvalue) > 1: self.__logger.warning("se_log_level for item {} can not be defined as a list" " ({}). Using default value {}.", self.id, _returnvalue, _default_log_level) - _log_level.set(_default_log_level) - elif len(_returnvalue) == 1 and _returnvalue[0] is None: - _log_level.set(_default_log_level) + self.__log_level.set(_default_log_level) + elif _returnvalue is None: + self.__log_level.set(_default_log_level) self.__logger.header("Initialize Item {0} (Log {1}, Level set" - " to {2} based on default log level {3}" - " because se_log_level has issues)".format(self.id, self.__logger.name, _log_level, + " to {2} based on default log level" + " because se_log_level has issues)".format(self.id, self.__logger.name, _default_log_level)) elif _using_default: self.__logger.header("Initialize Item {0} (Log {1}, Level set" " to {2} based on default log level {3})".format(self.id, self.__logger.name, - _log_level, - _default_log_level)) + _returnvalue, _default_log_level)) else: self.__logger.header("Initialize Item {0} (Log {1}, Level set" - " to {2}, default log level is {3})".format(self.id, self.__logger.name, _log_level, - _default_log_level)) - _startup_log_level = SeLogger.startup_log_level.get() - self.__logger.log_level.set(_startup_log_level) - self.__logger.info("Set log level to startup log level {}", _startup_log_level) - if _startup_log_level > 0: - base = self.__sh.get_basedir() - SeLogger.manage_logdirectory(base, SeLogger.log_directory, True) - self.__log_level = _log_level - self.__instant_leaveaction = StateEngineValue.SeValue(self, "Instant Leave Action", False, "num") + " to {2}, default log level is {3})".format(self.id, self.__logger.name, + _returnvalue, _default_log_level)) # get startup delay self.__startup_delay = StateEngineValue.SeValue(self, "Startup Delay", False, "num") @@ -226,8 +244,9 @@ def __init__(self, smarthome, item, se_plugin): self.__startup_delay_over = False # Init suspend settings + self.__default_suspend_time = StateEngineDefaults.suspend_time.get() self.__suspend_time = StateEngineValue.SeValue(self, "Suspension time on manual changes", False, "num") - self.__suspend_time.set_from_attr(self.__item, "se_suspend_time", StateEngineDefaults.suspend_time.get()) + self.__suspend_time.set_from_attr(self.__item, "se_suspend_time", self.__default_suspend_time) # Init laststate and previousstate items/values self.__config_issues = {} @@ -274,7 +293,7 @@ def __init__(self, smarthome, item, se_plugin): self.__previousstate_conditionset_internal_name = "" if self.__previousstate_conditionset_item_name is None else \ self.__previousstate_conditionset_item_name.property.value self.__config_issues.update(_issue) - filtered_dict = {key: value for key, value in self.__config_issues.items() if value.get('issue') is not None} + filtered_dict = {key: value for key, value in self.__config_issues.items() if value.get('issue') not in [[], [None], None]} self.__config_issues = filtered_dict self.__states = [] @@ -290,7 +309,7 @@ def __init__(self, smarthome, item, se_plugin): self.__repeat_actions = StateEngineValue.SeValue(self, "Repeat actions if state is not changed", False, "bool") self.__repeat_actions.set_from_attr(self.__item, "se_repeat_actions", True) - + self.__first_run = None self._initstate = None self._initactionname = None self.__update_trigger_item = None @@ -329,12 +348,12 @@ def __init__(self, smarthome, item, se_plugin): "previous.state_conditionset_name": "" } try: - _statecount = 0 + _statecount = 1 for item_state in self.__item.return_children(): - self.__initialize_state(item_state, _statecount) + _statecount = self.__initialize_state(item_state, _statecount) except Exception as ex: self.__logger.error("Ignoring stateevaluation for {} because {}", self.__id, ex) - + self.__reorder_states() try: self.__finish_states() except Exception as ex: @@ -352,17 +371,20 @@ def startup(self): startup_delay = 1 if self.__startup_delay.is_empty() or _startup_delay_param == 0 else _startup_delay_param if startup_delay > 0: first_run = self.__shtime.now() + datetime.timedelta(seconds=startup_delay) - self.__logger.info("Will start stateengine evaluation at {}", first_run) + self.__first_run = first_run.strftime('%H:%M:%S, %d.%m.') + self.__logger.info("Will start stateengine evaluation at {}", self.__first_run) scheduler_name = self.__id + "-Startup Delay" value = {"item": self.__item, "caller": "Init"} self.__se_plugin.scheduler_add(scheduler_name, self.__startup_delay_callback, value=value, next=first_run) elif startup_delay == -1: self.__startup_delay_over = True + self.__first_run = None self.__add_triggers() else: self.__startup_delay_callback(self.__item, "Init", None, None) - self.__logger.info("Reset log level to {}", self.__log_level) - self.__logger.log_level = self.__log_level + _log_level = self.__log_level.get() + self.__logger.info("Reset log level to {}", _log_level) + self.__logger.log_level_as_num = _log_level def show_issues_summary(self): # show issues summary @@ -371,20 +393,27 @@ def show_issues_summary(self): self.__unused_attributes = filtered_dict self.__logger.info("".ljust(80, "_")) - #filtered_dict = {key: value for key, value in self.__action_status.items() if value.get('issue') is not None} - #self.__action_status = filtered_dict + issues = 0 if self.__config_issues: + issues += 1 self.__log_issues('config entries') if self.__unused_attributes: + issues += 1 self.__log_issues('attributes') if self.__action_status: + issues += 1 self.__log_issues('actions') if self.__state_issues: + issues += 1 self.__log_issues('states') if self.__struct_issues: + issues += 1 self.__log_issues('structs') + if issues == 0: + self.__logger.info("No configuration issues found. Congratulations ;)") def update_leave_action(self, default_instant_leaveaction): + default_instant_leaveaction_value = default_instant_leaveaction.get() self.__default_instant_leaveaction = default_instant_leaveaction _returnvalue_leave, _returntype_leave, _using_default_leave, _issue = self.__instant_leaveaction.set_from_attr( @@ -392,23 +421,24 @@ def update_leave_action(self, default_instant_leaveaction): if len(_returnvalue_leave) > 1: self.__logger.warning("se_instant_leaveaction for item {} can not be defined as a list" - " ({}). Using default value {}.", self.id, _returnvalue_leave, default_instant_leaveaction) - self.__instant_leaveaction.set(default_instant_leaveaction) - self.__variables.update({"item.instant_leaveaction": default_instant_leaveaction}) + " ({}). Using default value {}.", self.id, _returnvalue_leave, + default_instant_leaveaction_value) + self.__instant_leaveaction = default_instant_leaveaction + self.__variables.update({"item.instant_leaveaction": default_instant_leaveaction_value}) elif len(_returnvalue_leave) == 1 and _returnvalue_leave[0] is None: - self.__instant_leaveaction.set(default_instant_leaveaction) - self.__variables.update({"item.instant_leaveaction": default_instant_leaveaction}) + self.__instant_leaveaction = default_instant_leaveaction + self.__variables.update({"item.instant_leaveaction": default_instant_leaveaction_value}) self.__logger.info("Using default instant_leaveaction {0} " - "as no se_instant_leaveaction is set.".format(default_instant_leaveaction)) + "as no se_instant_leaveaction is set.".format(default_instant_leaveaction_value)) elif _using_default_leave: - self.__variables.update({"item.instant_leaveaction": default_instant_leaveaction}) + self.__variables.update({"item.instant_leaveaction": default_instant_leaveaction_value}) self.__logger.info("Using default instant_leaveaction {0} " - "as no se_instant_leaveaction is set.".format(default_instant_leaveaction)) + "as no se_instant_leaveaction is set.".format(default_instant_leaveaction_value)) else: self.__variables.update({"item.instant_leaveaction": _returnvalue_leave}) self.__logger.info("Using instant_leaveaction {0} " "from attribute se_instant_leaveaction. " - "Default value is {1}".format(_returnvalue_leave, default_instant_leaveaction)) + "Default value is {1}".format(_returnvalue_leave, default_instant_leaveaction_value)) def updatetemplates(self, template, value): if value is None: @@ -434,30 +464,56 @@ def run_queue(self): self.__logger.debug("{} not running (anymore). Queue not activated.", StateEngineDefaults.plugin_identification) return - _current_log_level = self.__logger.get_loglevel() + _current_log_level = self.__log_level.get() _default_log_level = SeLogger.default_log_level.get() + + if _current_log_level <= -1: + self.__using_default_log_level = True + value = SeLogger.default_log_level.get() + else: + value = _current_log_level + self.__using_default_log_level = False + self.__logger.log_level_as_num = value + if _current_log_level > 0: base = self.__sh.get_basedir() SeLogger.manage_logdirectory(base, SeLogger.log_directory, True) - self.__logger.debug("Current log level {}, default {}, currently using default {}", - self.__logger.log_level, _default_log_level, self.__logger.using_default_log_level) - if self.__instant_leaveaction.get() <= -1: + additional_text = ", currently using default" if self.__using_default_log_level is True else "" + self.__logger.info("Current log level {} ({}), default {}{}", + _current_log_level, type(self.__logger.log_level), _default_log_level, additional_text) + _instant_leaveaction = self.__instant_leaveaction.get() + _default_instant_leaveaction_value = self.__default_instant_leaveaction.get() + if _instant_leaveaction <= -1: self.__using_default_instant_leaveaction = True + additional_text = ", currently using default" + elif _instant_leaveaction > 1: + self.__logger.info("Current se_instant_leaveaction {} is invalid. " + "It has to be set to -1, 0 or 1. Setting it to 1 instead.", _instant_leaveaction) + _instant_leaveaction = 1 + self.__using_default_instant_leaveaction = False + additional_text = "" else: self.__using_default_instant_leaveaction = False - self.__logger.debug("Current instant leave action {}, default {}, currently using default {}", - self.__instant_leaveaction, self.__default_instant_leaveaction, - self.__using_default_instant_leaveaction) - if self.__suspend_time.get() < 0: + additional_text = "" + self.__logger.debug("Current instant leave action {}, default {}{}", + _instant_leaveaction, _default_instant_leaveaction_value, additional_text) + _suspend_time = self.__suspend_time.get() + if _suspend_time < 0: self.__using_default_suspendtime = True + additional_text = ", currently using default" else: self.__using_default_suspendtime = False - self.__logger.debug("Current suspend time {}, default {}, currently using default {}", - self.__suspend_time, StateEngineDefaults.suspend_time, - self.__using_default_suspendtime) + additional_text = "" + self.__logger.debug("Current suspend time {}, default {}{}", + _suspend_time, self.__default_suspend_time, additional_text) self.update_lock.acquire(True, 10) + self.__reorder_states(init=False) all_released_by = {} new_state = None + if self.__using_default_instant_leaveaction: + _instant_leaveaction = _default_instant_leaveaction_value + else: + _instant_leaveaction = True if _instant_leaveaction == 1 else False while not self.__queue.empty() and self.__ab_alive: job = self.__queue.get() new_state = None @@ -484,6 +540,8 @@ def run_queue(self): # Find out what initially caused the update to trigger if the caller is "Eval" orig_caller, orig_source, orig_item = StateEngineTools.get_original_caller(self.__logger, caller, source, item) + if orig_item is None: + orig_item = item if orig_caller != caller: text = "{0} initially triggered by {1} (item={2} source={3} value={4})." self.__logger.debug(text, caller, orig_caller, orig_item.property.path, @@ -506,11 +564,11 @@ def run_queue(self): # Update current values StateEngineCurrent.update() - self.__variables["item.suspend_time"] = StateEngineDefaults.suspend_time.get() \ - if self.__using_default_suspendtime is True else self.__suspend_time.get() + self.__variables["item.suspend_time"] = self.__default_suspend_time \ + if self.__using_default_suspendtime is True else _suspend_time self.__variables["item.suspend_remaining"] = -1 - self.__variables["item.instant_leaveaction"] = self.__default_instant_leaveaction.get() \ - if self.__using_default_instant_leaveaction is True else self.__instant_leaveaction.get() + self.__variables["item.instant_leaveaction"] = _default_instant_leaveaction_value \ + if self.__using_default_instant_leaveaction is True else _instant_leaveaction # get last state last_state = self.__laststate_get() if last_state is not None: @@ -546,6 +604,10 @@ def run_queue(self): # find new state _leaveactions_run = False + if _instant_leaveaction >= 1 and caller != "Released_by Retrigger": + evaluated_instant_leaveaction = True + else: + evaluated_instant_leaveaction = False for state in self.__states: if not self.__ab_alive: self.__logger.debug("StateEngine Plugin not running (anymore). Stop state evaluation.") @@ -554,7 +616,7 @@ def run_queue(self): _key_name = ['{}'.format(state.id), 'name'] self.update_webif(_key_name, state.name) - result = self.__update_check_can_enter(state) + result = self.__update_check_can_enter(state, _instant_leaveaction) _previousstate_conditionset_id = _last_conditionset_id _previousstate_conditionset_name = _last_conditionset_name _last_conditionset_id = self.__lastconditionset_internal_id @@ -563,14 +625,8 @@ def run_queue(self): self.__conditionsets.update( {state.state_item.property.path: [_last_conditionset_id, _last_conditionset_name]}) # New state is different from last state - _instant_leaveaction = self.__instant_leaveaction.get() - if self.__using_default_instant_leaveaction: - _instant_leaveaction = self.__default_instant_leaveaction.get() - if _instant_leaveaction >= 1 and caller != "Released_by Retrigger": - _instant_leaveaction = True - else: - _instant_leaveaction = False - if result is False and last_state == state and _instant_leaveaction is True: + + if result is False and last_state == state and evaluated_instant_leaveaction is True: self.__logger.info("Leaving {0} ('{1}'). Running actions immediately.", last_state.id, last_state.name) last_state.run_leave(self.__repeat_actions.get()) @@ -602,7 +658,8 @@ def run_queue(self): self.update_lock.release() self.__logger.debug("State evaluation finished") self.__logger.info("State evaluation queue empty.") - self.__handle_releasedby(new_state, last_state) + self.__handle_releasedby(new_state, last_state, _instant_leaveaction) + return if new_state.is_copy_for: @@ -615,7 +672,7 @@ def run_queue(self): #self.lastconditionset_set(_original_conditionset_id, _original_conditionset_name) self.__logger.info("Leaving {0} ('{1}'). Condition set was: {2}.", last_state.id, last_state.name, _original_conditionset_id) - self.__update_check_can_enter(last_state, False) + self.__update_check_can_enter(last_state, _instant_leaveaction, False) last_state.run_leave(self.__repeat_actions.get()) _key_leave = ['{}'.format(last_state.id), 'leave'] _key_stay = ['{}'.format(last_state.id), 'stay'] @@ -624,7 +681,8 @@ def run_queue(self): self.update_webif(_key_leave, True) self.update_webif(_key_stay, False) self.update_webif(_key_enter, False) - self.__handle_releasedby(new_state, last_state) + self.__handle_releasedby(new_state, last_state, _instant_leaveaction) + if self.update_lock.locked(): self.update_lock.release() self.update_state(self.__item, "Released_by Retrigger", state.id) @@ -693,7 +751,7 @@ def run_queue(self): self.update_webif(_key_enter, False) self.__logger.debug("State evaluation finished") - all_released_by = self.__handle_releasedby(new_state, last_state) + all_released_by = self.__handle_releasedby(new_state, last_state, _instant_leaveaction) self.__logger.info("State evaluation queue empty.") if new_state: @@ -712,47 +770,56 @@ def __update_release_item_value(self, value, state): "state {} has to be defined with one '.' only.".format(value, state.id) self.__logger.warning("{} Changing it accordingly.", _returnvalue_issue) value = re.sub(r'\.+', '.', value) + if isinstance(value, list): + new_value = [] + for v in value: + if isinstance(v, str) and v.startswith("."): + v = "{}{}".format(state.id.rsplit(".", 1)[0], v) + new_value.append(v) + value = new_value if isinstance(value, str) and value.startswith("."): value = "{}{}".format(state.id.rsplit(".", 1)[0], value) - return value def __update_can_release(self, can_release, new_state=None): state_dict = {state.id: state for state in self.__states} for entry, release_list in can_release.items(): # Iterate through the dictionary items entry = self.__update_release_item_value(entry, new_state) - if entry in state_dict: - state = state_dict.get(entry) - if state.is_copy_for: - self.__logger.develop("State {} is a copy.", state.id) - #continue - can_release_list = [] - _stateindex = list(state_dict.keys()).index(state.id) - for e in release_list: - _valueindex = list(state_dict.keys()).index(e) if e in state_dict else -1 - self.__logger.develop("Testing entry in canrelease {}, state {} stateindex {}, "\ - "valueindex {}", e, state.id, _stateindex, _valueindex) - if e == state.id: - self.__logger.info("Value in se_released_by must not be identical to state. Ignoring {}", e) - elif _stateindex < _valueindex and not state.is_copy_for: - self.__logger.info("Value {} in se_released_by must have lower priority "\ - "than state. Ignoring {}", state.id, e) - else: - can_release_list.append(e) - self.__logger.develop("Value added to possible can release states {}", e) + entry = entry if isinstance(entry, list) else [entry] + for en in entry: + if en in state_dict: + state = state_dict.get(en) + if state.is_copy_for: + self.__logger.develop("State {} is a copy.", state.id) + can_release_list = [] + _stateindex = list(state_dict.keys()).index(state.id) + for e in release_list: + _valueindex = list(state_dict.keys()).index(e) if e in state_dict else -1 + self.__logger.develop("Testing entry in canrelease {}, state {} stateindex {}, "\ + "valueindex {}", e, state.id, _stateindex, _valueindex) + if e == state.id: + self.__logger.info("Value in se_released_by must not be identical to state. Ignoring {}", e) + elif _stateindex < _valueindex and not state.is_copy_for: + self.__logger.info("Value {} in se_released_by must have lower priority "\ + "than state. Ignoring {}", state.id, e) + else: + can_release_list.append(e) + self.__logger.develop("Value added to possible can release states {}", e) - state.update_can_release_internal(can_release_list) - self.__logger.develop("Updated 'can_release' property of state {} to {}", state.id, state.can_release) + state.update_can_release_internal(can_release_list) + self.__logger.develop("Updated 'can_release' property of state {} to {}", state.id, state.can_release) - else: - self.__logger.info("Entry {} in se_released_by of state(s) is not a valid state.", entry) + else: + self.__logger.info("Entry {} in se_released_by of state(s) is not a valid state.", entry) - def __handle_releasedby(self, new_state, last_state): + def __handle_releasedby(self, new_state, last_state, instant_leaveaction): def update_can_release_list(): for e in _returnvalue: e = self.__update_release_item_value(e, new_state) - if e and state.id not in can_release.setdefault(e, [state.id]): - can_release[e].append(state.id) + e = e if isinstance(e, list) else [e] + for entry in e: + if entry and state.id not in can_release.setdefault(entry, [state.id]): + can_release[entry].append(state.id) self.__logger.info("".ljust(80, "_")) self.__logger.info("Handling released_by attributes") @@ -765,10 +832,11 @@ def update_can_release_list(): skip_copy = False continue _returnvalue = state.releasedby + _returnvalue = _returnvalue if isinstance(_returnvalue, list) else [_returnvalue] + _returnvalue = StateEngineTools.flatten_list(_returnvalue) all_released_by.update({state: _returnvalue}) - if _returnvalue: - _returnvalue = _returnvalue if isinstance(_returnvalue, list) else [_returnvalue] + if _returnvalue not in [[], None, [None]]: update_can_release_list() self.__update_can_release(can_release, new_state) @@ -799,57 +867,63 @@ def update_can_release_list(): if new_state: new_state.was_releasedby = None _can_release_list = [] - releasedby = all_released_by.get(new_state) - self.__logger.develop("releasedby {}", releasedby) - if releasedby: + releasedby = all_released_by.get(new_state) + if releasedby not in [[], None, [None]]: + self.__logger.develop("releasedby {}", releasedby) state_dict = {item.id: item for item in self.__states} _stateindex = list(state_dict.keys()).index(new_state.id) releasedby = releasedby if isinstance(releasedby, list) else [releasedby] _checkedentries = [] for i, entry in enumerate(releasedby): entry = self.__update_release_item_value(entry, new_state) - if entry in _checkedentries: - self.__logger.develop("Entry {} defined by {} already checked, skipping", entry, releasedby[i]) - continue - cond_copy_for = entry in state_dict.keys() - if cond_copy_for and new_state == state_dict.get(entry).is_copy_for: - _can_release_list.append(entry) - self.__logger.develop("Entry {} defined by {} is a copy, skipping", entry, releasedby[i]) - continue - _entryindex = list(state_dict.keys()).index(entry) if entry in state_dict else -1 - self.__logger.develop("Testing if entry {} should become a state copy. "\ - "stateindex {}, entryindex {}", entry, _stateindex, _entryindex) - if entry == new_state.id: - self.__logger.warning("Value in se_released_by must no be identical to state. Ignoring {}", - entry) - elif _entryindex == -1: - self.__logger.warning("State in se_released_by does not exist. Ignoring {}", entry) - elif _stateindex > _entryindex: - self.__logger.warning("Value in se_released_by must have lower priority than state. Ignoring {}", - entry) - elif entry in state_dict.keys(): - relevant_state = state_dict.get(entry) - index = self.__states.index(new_state) - cond_index = relevant_state in self.__states and self.__states.index(relevant_state) != index - 1 - if cond_index or relevant_state not in self.__states: - current_log_level = self.__log_level.get() - if current_log_level < 3: - self.__logger.log_level.set(0) - can_enter = self.__update_check_can_enter(relevant_state) - #can_enter = relevant_state.can_enter() - self.__logger.log_level.set(current_log_level) - if relevant_state == last_state: - self.__logger.debug("Possible release state {} = last state {}, "\ - "not copying", relevant_state.id, last_state.id) - elif can_enter: - self.__logger.debug("Relevant state {} could enter, not copying", relevant_state.id) - elif not can_enter: - relevant_state.is_copy_for = new_state - self.__states.insert(index, relevant_state) - _can_release_list.append(relevant_state.id) - self.__logger.debug("Inserted copy of state {}", relevant_state.id) - _checkedentries.append(entry) + entry = entry if isinstance(entry, list) else [entry] + for e in entry: + if e in _checkedentries: + self.__logger.develop("Entry {} defined by {} already checked, skipping", e, releasedby[i]) + continue + cond_copy_for = e in state_dict.keys() + if cond_copy_for and new_state == state_dict.get(e).is_copy_for: + if e not in _can_release_list: + _can_release_list.append(e) + self.__logger.develop("Entry {} defined by {} is a copy, skipping", e, releasedby[i]) + continue + _entryindex = list(state_dict.keys()).index(e) if e in state_dict else -1 + self.__logger.develop("Testing if entry {} should become a state copy. "\ + "stateindex {}, entryindex {}", e, _stateindex, _entryindex) + if e == new_state.id: + self.__logger.warning("Value in se_released_by must no be identical to state. Ignoring {}", + e) + elif _entryindex == -1: + self.__logger.warning("State in se_released_by does not exist. Ignoring {}", e) + elif _stateindex > _entryindex: + self.__logger.warning("Value in se_released_by must have lower priority than state. Ignoring {}", + e) + elif e in state_dict.keys(): + relevant_state = state_dict.get(e) + index = self.__states.index(new_state) + cond_index = relevant_state in self.__states and self.__states.index(relevant_state) != index - 1 + if cond_index or relevant_state not in self.__states: + current_log_level = self.__log_level.get() + if current_log_level < 3: + self.__logger.log_level_as_num = 0 + can_enter = self.__update_check_can_enter(relevant_state, instant_leaveaction) + self.__logger.log_level_as_num = current_log_level + if relevant_state == last_state: + self.__logger.debug("Possible release state {} = last state {}, "\ + "not copying", relevant_state.id, last_state.id) + elif can_enter: + self.__logger.debug("Relevant state {} could enter, not copying", relevant_state.id) + elif not can_enter: + relevant_state.is_copy_for = new_state + self.__states.insert(index, relevant_state) + if relevant_state.id not in _can_release_list: + _can_release_list.append(relevant_state.id) + self.__logger.debug("Inserted copy of state {}", relevant_state.id) + _checkedentries.append(e) self.__logger.info("State {} can currently get released by: {}", new_state.id, _can_release_list) + self.__release_info = {new_state.id: _can_release_list} + _key_releasedby = ['{}'.format(new_state.id), 'releasedby'] + self.update_webif(_key_releasedby, _can_release_list) self.__logger.info("".ljust(80, "_")) return all_released_by @@ -882,7 +956,11 @@ def combine_dicts(dict1, dict2): for key, value in dict2.items(): if key in combined_dict: - combined_dict[key]['issueorigin'].extend(value['issueorigin']) + for k, v in combined_dict.items(): + v['issueorigin'].extend( + [item for item in v['issueorigin'] if item not in combined_dict[k]['issueorigin']]) + v['issue'].extend([item for item in v['issue'] if item not in combined_dict[k]['issue']]) + else: combined_dict[key] = value @@ -972,13 +1050,18 @@ def list_issues(v): warn = ', '.join(key for key in self.__config_issues.keys()) else: to_check = self.__unused_attributes.items() - warn = ', '.join(key for key in self.__unused_attributes.keys()) + warn_unused = ', '.join(key for key, value in self.__unused_attributes.items() if 'issue' not in value) + warn_issues = ', '.join(key for key, value in self.__unused_attributes.items() if 'issue' in value) self.__logger.info("") if issue_type == 'attributes': - self.__logger.info("These attributes are not used: {} Please check extended " - "log file for details.", warn) + if warn_unused: + self.__logger.info("These attributes are not used: {}. Please check extended " + "log file for details.", warn_unused) + if warn_issues: + self.__logger.warning("There are attribute issues: {}. Please check extended " + "log file for details.", warn_issues) else: - self.__logger.warning("There are {} issues: {} Please check extended " + self.__logger.warning("There are {} issues: {}. Please check extended " "log file for details.", issue_type, warn) self.__logger.info("") self.__logger.info("The following {} have issues:", issue_type) @@ -1013,8 +1096,15 @@ def list_issues(v): origin_text = 'state {}, action {}, on_{}'.format(origin.get('state'), origin.get('action'), origin.get('type')) elif issue_type == 'states': - origin_text = 'condition {} defined in conditionset {}'.format(origin.get('condition'), - origin.get('conditionset')) + if origin.get('condition') == 'GeneralError' and len(origin_list) == 1: + origin_text = 'there was a general error. The state' + elif origin.get('condition') == 'ValueError' and len(origin_list) == 1: + origin_text = 'there was a value error. The state' + else: + if origin.get('condition') in ['GeneralError', 'ValueError']: + continue + origin_text = 'condition {} defined in conditionset {}'.format(origin.get('condition'), + origin.get('conditionset')) else: origin_text = 'state {}, conditionset {}'.format(origin.get('state'), origin.get('conditionset')) @@ -1028,22 +1118,78 @@ def list_issues(v): self.__logger.info("{}", text) self.__logger.decrease_indent() + def __reorder_states(self, init=True): + _reordered_states = [] + self.__logger.info("".ljust(80, "_")) + self.__logger.info("Recalculating state order. Current order: {}", self.__states) + _copied_states = {} + _add_order = 0 + for i, state in enumerate(self.__states, 1): + try: + _original_order = state.order + _issue = None + if state.is_copy_for and state not in _copied_states: + _order = i + _add_order += 1 + _copied_states[state] = _order + else: + _issue = state.update_order() + _order = state.order + if _issue not in [[], None, [None]]: + self.__config_issues.update({state.id: {'issue': _issue, 'attribute': 'se_stateorder'}}) + self.__logger.warning("Issue while getting state order: {}, using original order {}", _issue, _original_order) + _order = _original_order + state.update_order(_original_order) + elif _copied_states.get(state) and _copied_states.get(state) > _order: + _reordered_states.remove((_copied_states.get(state), state)) + state.is_copy_for = None + _add_order -= 1 + elif state not in _copied_states: + _order += _add_order + _reordered_states.append((_order, state)) + except Exception as ex: + self.__logger.error("Problem setting order of state {0}: {1}", state.id, ex) + self.__config_issues.update({state.id: {'issue': ex, 'attribute': 'se_stateorder'}}) + self.__states = [] + for order, state in sorted(_reordered_states, key=lambda x: x[0]): + self.__states.append(state) + if init is False: + _reorder_webif = OrderedDict() + _copied_states = [] + for state in self.__states: + if state.is_copy_for and state not in _copied_states: + _copied_states.append(state) + else: + _reorder_webif[state.id] = self.__webif_infos[state.id] + self.__webif_infos = _reorder_webif + self.__logger.info("Recalculated state order. New order: {}", self.__states) + self.__logger.info("".ljust(80, "_")) + def __initialize_state(self, item_state, _statecount): - # initialize states try: _state = StateEngineState.SeState(self, item_state) - _statecount += 1 + _issue = _state.update_order(_statecount) + if _issue: + self.__config_issues.update({item_state.property.path: {'issue': _issue, 'attribute': 'se_stateorder'}}) + self.__logger.error("Issue with state {0} while setting order: {1}", item_state.property.path, _issue) self.__states.append(_state) self.__state_ids.update({item_state.property.path: _state}) - if _statecount == 1: - self.__unused_attributes = _state.unused_attributes.copy() + self.__logger.info("Appended state {}", item_state.property.path) + self.__unused_attributes = _state.unused_attributes.copy() filtered_dict = {key: value for key, value in self.__unused_attributes.items() if key not in _state.used_attributes} self.__unused_attributes = filtered_dict + return _statecount + 1 except ValueError as ex: + self.update_issues('state', {item_state.property.path: {'issue': ex, 'issueorigin': + [{'conditionset': 'None', 'condition': 'ValueError'}]}}) self.__logger.error("Ignoring state {0} because ValueError: {1}", item_state.property.path, ex) + return _statecount except Exception as ex: + self.update_issues('state', {item_state.property.path: {'issue': ex, 'issueorigin': + [{'conditionset': 'None', 'condition': 'GeneralError'}]}}) self.__logger.error("Ignoring state {0} because: {1}", item_state.property.path, ex) + return _statecount def __finish_states(self): # initialize states @@ -1064,10 +1210,10 @@ def update_state(self, item, caller=None, source=None, dest=None): # check if state can be entered after setting state-specific variables # state: state to check - def __update_check_can_enter(self, state, refill=True): + def __update_check_can_enter(self, state, instant_leaveaction, refill=True): try: wasreleasedby = state.was_releasedby.id - except Exception as ex: + except: wasreleasedby = state.was_releasedby try: iscopyfor = state.is_copy_for.id @@ -1089,7 +1235,7 @@ def __update_check_can_enter(self, state, refill=True): self.__variables["release.will_release"] = iscopyfor self.__variables["previous.state_id"] = self.__previousstate_internal_id self.__variables["previous.state_name"] = self.__previousstate_internal_name - self.__variables["item.instant_leaveaction"] = self.__instant_leaveaction.get() + self.__variables["item.instant_leaveaction"] = instant_leaveaction self.__variables["current.state_id"] = state.id self.__variables["current.state_name"] = state.name self.__variables["current.conditionset_id"] = self.__lastconditionset_internal_id @@ -1370,105 +1516,119 @@ def __verbose_crons_and_cycles(self): def __init_releasedby(self): def process_returnvalue(value): - self.__logger.info("Testing value {}", value) + self.__logger.debug("Testing value {}", value) _returnvalue_issue = None if value is None: return _returnvalue_issue try: original_value = value value = self.__update_release_item_value(_evaluated_returnvalue[i], state) - _stateindex = list(state_dict.keys()).index(state.id) - _valueindex = list(state_dict.keys()).index(value) if value in state_dict else -1 - if _returntype[i] == 'value' and _valueindex == - 1: #not any(value == test.id for test in self.__states): - _returnvalue_issue = "State {} defined by value in se_released_by attribute of state {} " \ - "does not exist.".format(value, state.id) - self.__logger.warning("{} Removing it.", _returnvalue_issue) - elif _returntype[i] == 'value' and _valueindex < _stateindex: - _returnvalue_issue = "State {} defined by value in se_released_by attribute of state {} " \ - "must be lower priority than actual state.".format(value, state.id) - self.__logger.warning("{} Removing it.", _returnvalue_issue) - elif _returntype[i] == 'value' and value == state.id: - _returnvalue_issue = "State {} defined by value in se_released_by attribute of state {} " \ - "must not be identical.".format(value, state.id) - self.__logger.warning("{} Removing it.", _returnvalue_issue) - elif _returntype[i] == 'item': - if value == state.id: - _returnvalue_issue = "State {} defined by {} in se_released_by attribute of state {} " \ - "must not be identical.".format(value, _returnvalue[i], state.id) - elif _valueindex == - 1: #not any(value == test.id for test in self.__states): - _returnvalue_issue = "State {} defined by {} in se_released_by attribute of state {} " \ - "does currently not exist.".format(value, _returnvalue[i], state.id) - elif _valueindex < _stateindex: + value = value if isinstance(value, list) else [value] + v_list = [] + for v in value: + _stateindex = list(state_dict.keys()).index(state.id) + _valueindex = list(state_dict.keys()).index(v) if v in state_dict else -1 + if _returntype[i] == 'value' and _valueindex == - 1: + _returnvalue_issue = "State {} defined by value in se_released_by attribute of state {} " \ + "does not exist.".format(v, state.id) + self.__logger.warning("{} Removing it.", _returnvalue_issue) + elif _returntype[i] == 'value' and _valueindex < _stateindex: _returnvalue_issue = "State {} defined by value in se_released_by attribute of state {} " \ - "must be lower priority than actual state.".format(value, state.id) - if _returnvalue_issue: - self.__logger.warning("{} Make sure to change item value.", _returnvalue_issue) - _convertedlist.append(original_value) - _converted_evaluatedlist.append(value) - _converted_typelist.append(_returntype[i]) - self.__logger.develop("Adding {} from item as releasedby for state {}", original_value, state.id) - elif _returntype[i] == 'regex': - matches = [test.id for test in self.__states if _evaluated_returnvalue[i].match(test.id)] - self.__logger.develop("matches {}", matches) - _returnvalue_issue_list = [] - for match in matches: - _valueindex = list(state_dict.keys()).index(match) if match in state_dict else -1 - if _valueindex == _stateindex: + "must be lower priority than actual state.".format(v, state.id) + self.__logger.warning("{} Removing it.", _returnvalue_issue) + elif _returntype[i] == 'value' and v == state.id: + _returnvalue_issue = "State {} defined by value in se_released_by attribute of state {} " \ + "must not be identical.".format(v, state.id) + self.__logger.warning("{} Removing it.", _returnvalue_issue) + elif _returntype[i] == 'item': + if v == state.id: _returnvalue_issue = "State {} defined by {} in se_released_by attribute of state {} " \ - "must not be identical.".format(match, _returnvalue[i], state.id) - self.__logger.warning("{} Removing it.", _returnvalue_issue) - if _returnvalue_issue not in _returnvalue_issue_list: - _returnvalue_issue_list.append(_returnvalue_issue) + "must not be identical.".format(v, _returnvalue[i], state.id) + elif _valueindex == - 1: #not any(value == test.id for test in self.__states): + _returnvalue_issue = "State {} defined by {} in se_released_by attribute of state {} " \ + "does currently not exist.".format(v, _returnvalue[i], state.id) elif _valueindex < _stateindex: - _returnvalue_issue = "State {} defined by {} in se_released_by " \ - "attribute of state {} must be lower priority "\ - "than actual state.".format(match, _returnvalue[i], state.id) + _returnvalue_issue = "State {} defined by value in se_released_by attribute of state {} " \ + "must be lower priority than actual state.".format(v, state.id) + if _returnvalue_issue: + self.__logger.warning("{} Make sure to change item value.", _returnvalue_issue) + if original_value not in _convertedlist: + _convertedlist.append(original_value) + self.__logger.develop("Adding {} from item as releasedby for state {}", original_value, + state.id) + v_list.append(v) + _converted_typelist.append(_returntype[i]) + + elif _returntype[i] == 'regex': + matches = [test.id for test in self.__states if _evaluated_returnvalue[i].match(test.id)] + self.__logger.develop("matches {}", matches) + _returnvalue_issue_list = [] + for match in matches: + _valueindex = list(state_dict.keys()).index(match) if match in state_dict else -1 + if _valueindex == _stateindex: + _returnvalue_issue = "State {} defined by {} in se_released_by attribute of state {} " \ + "must not be identical.".format(match, _returnvalue[i], state.id) + self.__logger.warning("{} Removing it.", _returnvalue_issue) + if _returnvalue_issue not in _returnvalue_issue_list: + _returnvalue_issue_list.append(_returnvalue_issue) + elif _valueindex < _stateindex: + _returnvalue_issue = "State {} defined by {} in se_released_by " \ + "attribute of state {} must be lower priority "\ + "than actual state.".format(match, _returnvalue[i], state.id) + self.__logger.warning("{} Removing it.", _returnvalue_issue) + if _returnvalue_issue not in _returnvalue_issue_list: + _returnvalue_issue_list.append(_returnvalue_issue) + else: + if match not in _convertedlist: + _convertedlist.append(match) + self.__logger.develop("Adding {} from regex as releasedby for state {}", match, + state.id) + v_list.append(value) + _converted_typelist.append(_returntype[i]) + + _returnvalue_issue = _returnvalue_issue_list + if not matches: + _returnvalue_issue = "No states match regex {} defined in "\ + "se_released_by attribute of state {}.".format(value, state.id) self.__logger.warning("{} Removing it.", _returnvalue_issue) - if _returnvalue_issue not in _returnvalue_issue_list: - _returnvalue_issue_list.append(_returnvalue_issue) - else: - _convertedlist.append(match) - _converted_evaluatedlist.append(value) - _converted_typelist.append(_returntype[i]) - self.__logger.develop("Adding {} from regex as releasedby for state {}", match, state.id) - _returnvalue_issue = _returnvalue_issue_list - if not matches: - _returnvalue_issue = "No states match regex {} defined in "\ - "se_released_by attribute of state {}.".format(value, state.id) - self.__logger.warning("{} Removing it.", _returnvalue_issue) - elif _returntype[i] == 'eval': - if value == state.id: + elif _returntype[i] == 'eval': + if v == state.id: + _returnvalue_issue = "State {} defined by {} in se_released_by attribute of state {} " \ + "must not be identical.".format(v, _returnvalue[i], state.id) + self.__logger.warning("{} Make sure eval will result in a useful value later on.", + _returnvalue_issue) + elif _valueindex < _stateindex: + _returnvalue_issue = "State {} defined by value in se_released_by attribute of state {} " \ + "must be lower priority than actual state.".format(v, state.id) + self.__logger.warning("{} Make sure eval will result in a useful value later on.", + _returnvalue_issue) + elif v is None: + _returnvalue_issue = "Eval defined by {} in se_released_by attribute of state {} " \ + "does currently return None.".format(_returnvalue[i], state.id) + self.__logger.warning("{} Make sure eval will result in a useful value later on.", + _returnvalue_issue) + if _returnvalue[i] not in _convertedlist: + _convertedlist.append(_returnvalue[i]) + self.__logger.develop("Adding {} from eval as releasedby for state {}", _returnvalue[i], + state.id) + v_list.append(v) + _converted_typelist.append(_returntype[i]) + + elif v and v == state.id: _returnvalue_issue = "State {} defined by {} in se_released_by attribute of state {} " \ - "must not be identical.".format(value, _returnvalue[i], state.id) - self.__logger.warning("{} Make sure eval will result in a useful value later on.", - _returnvalue_issue) - elif _valueindex < _stateindex: - _returnvalue_issue = "State {} defined by value in se_released_by attribute of state {} " \ - "must be lower priority than actual state.".format(value, state.id) - self.__logger.warning("{} Make sure eval will result in a useful value later on.", - _returnvalue_issue) - elif value is None: - _returnvalue_issue = "Eval defined by {} in se_released_by attribute of state {} " \ - "does currently return None.".format(_returnvalue[i], state.id) - self.__logger.warning("{} Make sure eval will result in a useful value later on.", - _returnvalue_issue) - _convertedlist.append(_returnvalue[i]) - _converted_evaluatedlist.append(value) - _converted_typelist.append(_returntype[i]) - self.__logger.develop("Adding {} from eval as releasedby for state {}", _returnvalue[i], state.id) - elif value and value == state.id: - _returnvalue_issue = "State {} defined by {} in se_released_by attribute of state {} " \ - "must not be identical.".format(value, _returnvalue[i], state.id) - self.__logger.warning("{} Removing it.", _returnvalue_issue) - elif value and value not in _convertedlist: - _convertedlist.append(value) - _converted_evaluatedlist.append(value) - _converted_typelist.append(_returntype[i]) - self.__logger.develop("Adding {} as releasedby for state {}", value, state.id) - else: - _returnvalue_issue = "Found invalid definition in se_released_by attribute "\ - "of state {}, original {}.".format(state.id, value, original_value) - self.__logger.warning("{} Removing it.", _returnvalue_issue) + "must not be identical.".format(v, _returnvalue[i], state.id) + self.__logger.warning("{} Removing it.", _returnvalue_issue) + elif v and v not in _convertedlist: + if value not in _convertedlist: + _convertedlist.append(value) + self.__logger.develop("Adding {} as releasedby for state {}", value, state.id) + v_list.append(v) + _converted_typelist.append(_returntype[i]) + else: + _returnvalue_issue = "Found invalid definition in se_released_by attribute "\ + "of state {}, original {}.".format(state.id, v, original_value) + self.__logger.warning("{} Removing it.", _returnvalue_issue) + _converted_evaluatedlist.append(v_list) except Exception as ex: _returnvalue_issue = "Issue with {} for released_by of state {} check: {}".format(value, state.id, ex) self.__logger.error(_returnvalue_issue) @@ -1480,20 +1640,21 @@ def update_can_release_list(): value = self.__update_release_item_value(_converted_evaluatedlist[i], state) elif _converted_typelist[i] == 'eval': value = _converted_evaluatedlist[i] - if value and can_release.get(value) and state.id not in can_release.get(value): - can_release[value].append(state.id) - elif value: - can_release.update({value: [state.id]}) + value = value if isinstance(value, list) else [value] + for v in value: + if v and can_release.get(v) and state.id not in can_release.get(v): + can_release[v].append(state.id) + elif v: + can_release.update({v: [state.id]}) self.__logger.info("".ljust(80, "_")) - self.__logger.info("Checking released_by attributes") + self.__logger.info("Initializing released_by attributes") can_release = {} state_dict = {state.id: state for state in self.__states} for state in self.__states: _issuelist = [] _returnvalue, _returntype, _issue = state.update_releasedby_internal() _returnvalue = copy.copy(_returnvalue) - _issuelist.append(_issue) if _returnvalue: _convertedlist = [] @@ -1501,11 +1662,11 @@ def update_can_release_list(): _converted_typelist = [] _returnvalue = _returnvalue if isinstance(_returnvalue, list) else [_returnvalue] _evaluated_returnvalue = state.releasedby - _evaluated_returnvalue = _evaluated_returnvalue if isinstance(_evaluated_returnvalue, list) \ - else [_evaluated_returnvalue] + _evaluated_returnvalue = _evaluated_returnvalue if isinstance(_evaluated_returnvalue, list) else [_evaluated_returnvalue] for i, entry in enumerate(_returnvalue): _issue = process_returnvalue(entry) - _issuelist.append(_issue) + if _issue is not None and _issue not in _issuelist: + _issuelist.append(_issue) update_can_release_list() _issuelist = StateEngineTools.flatten_list(_issuelist) _issuelist = [issue for issue in _issuelist if issue is not None and issue != []] @@ -1537,32 +1698,32 @@ def write_to_log(self): # log laststate settings if self.__laststate_item_id is not None: - self.__logger.info("Item 'Laststate Id': {0}", self.__laststate_item_id.property.path) + self.__logger.debug("Item 'Laststate Id': {0}", self.__laststate_item_id.property.path) if self.__laststate_item_name is not None: - self.__logger.info("Item 'Laststate Name': {0}", self.__laststate_item_name.property.path) + self.__logger.debug("Item 'Laststate Name': {0}", self.__laststate_item_name.property.path) # log previousstate settings if self.__previousstate_item_id is not None: - self.__logger.info("Item 'Previousstate Id': {0}", self.__previousstate_item_id.property.path) + self.__logger.debug("Item 'Previousstate Id': {0}", self.__previousstate_item_id.property.path) if self.__previousstate_item_name is not None: - self.__logger.info("Item 'Previousstate Name': {0}", self.__previousstate_item_name.property.path) + self.__logger.debug("Item 'Previousstate Name': {0}", self.__previousstate_item_name.property.path) # log lastcondition settings if self.__lastconditionset_item_id is not None: - self.__logger.info("Item 'Lastcondition Id': {0}", self.__lastconditionset_item_id.property.path) + self.__logger.debug("Item 'Lastcondition Id': {0}", self.__lastconditionset_item_id.property.path) if self.__lastconditionset_item_name is not None: - self.__logger.info("Item 'Lastcondition Name': {0}", self.__lastconditionset_item_name.property.path) + self.__logger.debug("Item 'Lastcondition Name': {0}", self.__lastconditionset_item_name.property.path) # log previouscondition settings if self.__previousconditionset_item_id is not None: - self.__logger.info("Item 'Previouscondition Id': {0}", self.__previousconditionset_item_id.property.path) + self.__logger.debug("Item 'Previouscondition Id': {0}", self.__previousconditionset_item_id.property.path) if self.__previousconditionset_item_name is not None: - self.__logger.info("Item 'Previouscondition Name': {0}", self.__previousconditionset_item_name.property.path) + self.__logger.debug("Item 'Previouscondition Name': {0}", self.__previousconditionset_item_name.property.path) if self.__previousstate_conditionset_item_id is not None: - self.__logger.info("Item 'Previousstate condition Id': {0}", self.__previousstate_conditionset_item_id.property.path) + self.__logger.debug("Item 'Previousstate condition Id': {0}", self.__previousstate_conditionset_item_id.property.path) if self.__previousstate_conditionset_item_name is not None: - self.__logger.info("Item 'Previousstate condition Name': {0}", + self.__logger.debug("Item 'Previousstate condition Name': {0}", self.__previousstate_conditionset_item_name.property.path) self.__init_releasedby() @@ -1572,7 +1733,7 @@ def write_to_log(self): state.write_to_log() self._initstate = None - filtered_dict = {key: value for key, value in self.__config_issues.items() if value.get('issue') not in [[], None]} + filtered_dict = {key: value for key, value in self.__config_issues.items() if value.get('issue') not in [[], [None], None]} self.__config_issues = filtered_dict # endregion @@ -1714,6 +1875,7 @@ def __startup_delay_callback(self, item, caller=None, source=None, dest=None): if self.__se_plugin.scheduler_get(scheduler_name): self.__se_plugin.scheduler_remove(scheduler_name) self.__logger.debug('Startup Delay over. Removed scheduler {}', scheduler_name) + self.__first_run = None self.update_state(item, "Startup Delay", source, dest) self.__add_triggers() @@ -1738,11 +1900,11 @@ def return_item(self, item_id): return self.itemsApi.return_item(item_id.id), None if item_id is None: _issue = "item_id is None" - return None, _issue + return None, [_issue] if not isinstance(item_id, str): _issue = "'{0}' is not defined as string.".format(item_id) self.__logger.info("{0} Check your item config!", _issue, item_id) - return None, _issue + return None, [_issue] item_id = item_id.strip() if item_id.startswith("struct:"): item = None @@ -1756,13 +1918,27 @@ def return_item(self, item_id): if item is None: _issue = "Item '{0}' in struct not found.".format(item_id) self.__logger.warning(_issue) - return item, _issue + return item, [_issue] if not item_id.startswith("."): - item = self.itemsApi.return_item(item_id) + match = re.match(r'^(.*):', item_id) + if item_id.startswith("eval:"): + if "stateengine_eval" in item_id or "se_eval" in item_id: + # noinspection PyUnusedLocal + stateengine_eval = se_eval = StateEngineEval.SeEval(self) + item = item_id.replace('sh', 'self._sh') + item = item.replace('shtime', 'self._shtime') + _, _, item = item.partition(":") + return item, None + elif match: + _issue = "Item '{0}' has to be defined as an item path or eval expression without {}.".format(match.group(1), item_id) + self.__logger.warning(_issue) + return None, [_issue] + else: + item = self.itemsApi.return_item(item_id) if item is None: _issue = "Item '{0}' not found.".format(item_id) self.__logger.warning(_issue) - return item, _issue + return item, [_issue] self.__logger.debug("Testing for relative item declaration {}", item_id) parent_level = 0 for c in item_id: @@ -1787,13 +1963,13 @@ def return_item(self, item_id): self.__logger.warning(_issue) else: self.__logger.develop("Determined item '{0}' for id {1}.", item.id, item_id) - return item, _issue + return item, [_issue] # Return an item related to the StateEngine object item # attribute: Name of the attribute of the StateEngine object item, which contains the item_id to read def return_item_by_attribute(self, attribute): if attribute not in self.__item.conf: - _issue = {attribute: {'issue': 'Attribute missing in stateeninge configuration.'}} + _issue = {attribute: {'issue': ['Attribute missing in stateeninge configuration.']}} self.__logger.warning("Attribute '{0}' missing in stateeninge configuration.", attribute) return None, _issue _returnvalue, _issue = self.return_item(self.__item.conf[attribute]) diff --git a/stateengine/StateEngineLogger.py b/stateengine/StateEngineLogger.py index 6e56af86f..07eabac00 100755 --- a/stateengine/StateEngineLogger.py +++ b/stateengine/StateEngineLogger.py @@ -57,32 +57,17 @@ def log_maxage(self, value): logger.error("The maximum age of the log files has to be an int number.") @property - def using_default_log_level(self): - return self.__using_default_log_level + def log_level_as_num(self): + return self.__log_level_as_num - @using_default_log_level.setter - def using_default_log_level(self, value): - self.__using_default_log_level = value + @log_level_as_num.setter + def log_level_as_num(self, value): + self.__log_level_as_num = value @property def name(self): return self.__name - # Set global log level - # loglevel: current loglevel - @property - def log_level(self): - return self.__log_level.get() - - @log_level.setter - def log_level(self, value): - try: - self.__log_level = int(value) - except ValueError: - self.__log_level = 0 - logger = StateEngineDefaults.logger - logger.error("Loglevel has to be an int number!") - @property def log_directory(self): return SeLogger.__log_directory @@ -137,29 +122,26 @@ def remove_old_logfiles(): # Return SeLogger instance for given item # item: item for which the detailed log is @staticmethod - def create(item): - return SeLogger(item) + def create(item, manual=False): + return SeLogger(item, manual) # Constructor # item: item for which the detailed log is (used as part of file name) - def __init__(self, item): + def __init__(self, item, manual=False): self.logger = logging.getLogger('stateengine.{}'.format(item.property.path)) self.__name = 'stateengine.{}'.format(item.property.path) self.__section = item.property.path.replace(".", "_").replace("/", "") self.__indentlevel = 0 - self.__default_log_level = None - self.__startup_log_level = None - self.__log_level = None - self.__using_default_log_level = False + if manual: + self.__log_level_as_num = 2 + else: + self.__log_level_as_num = 0 self.__logmaxage = None self.__date = None self.__logerror = False self.__filename = "" self.update_logfile() - # get current log level of abitem - def get_loglevel(self): - return self.log_level.get() # Update name logfile if required def update_logfile(self): @@ -186,13 +168,7 @@ def decrease_indent(self, by=1): # text: text to log def log(self, level, text, *args): # Section given: Check level - _log_level = self.get_loglevel() - if _log_level <= -1: - self.using_default_log_level = True - _log_level = SeLogger.default_log_level.get() - else: - self.using_default_log_level = False - if level <= _log_level: + if level <= self.__log_level_as_num: indent = "\t" * self.__indentlevel if args: text = text.format(*args) diff --git a/stateengine/StateEngineState.py b/stateengine/StateEngineState.py index e665a0788..8efc61aff 100755 --- a/stateengine/StateEngineState.py +++ b/stateengine/StateEngineState.py @@ -84,7 +84,15 @@ def releasedby(self): @releasedby.setter def releasedby(self, value): - self.__releasedby.set(value) + self.__releasedby.set(value, "", True, None, False) + + @property + def order(self): + return self.__order.get() + + @order.setter + def order(self, value): + self.__order.set(value, "", True, None, False) @property def can_release(self): @@ -92,7 +100,7 @@ def can_release(self): @can_release.setter def can_release(self, value): - self.__can_release.set(value) + self.__can_release.set(value, "", True, None, False) @property def has_released(self): @@ -100,7 +108,7 @@ def has_released(self): @has_released.setter def has_released(self, value): - self.__has_released.set(value) + self.__has_released.set(value, "", True, None, False) @property def was_releasedby(self): @@ -108,7 +116,7 @@ def was_releasedby(self): @was_releasedby.setter def was_releasedby(self, value): - self.__was_releasedby.set(value) + self.__was_releasedby.set(value, "", True, None, False) @property def is_copy_for(self): @@ -116,7 +124,13 @@ def is_copy_for(self): @is_copy_for.setter def is_copy_for(self, value): - self.__is_copy_for.set(value) + if value: + webif_id = value.id + else: + webif_id = None + _key_copy = ['{}'.format(self.id), 'is_copy_for'] + self._abitem.update_webif(_key_copy, webif_id) + self.__is_copy_for.set(value, "", True, None, False) # Constructor # abitem: parent SeItem instance @@ -149,6 +163,7 @@ def __init__(self, abitem, item_state): self.__actions_enter = StateEngineActions.SeActions(self._abitem) self.__actions_stay = StateEngineActions.SeActions(self._abitem) self.__actions_leave = StateEngineActions.SeActions(self._abitem) + self.__order = StateEngineValue.SeValue(self._abitem, "State Order", False, "num") self._log_increase_indent() try: self.__fill(self.__item, 0) @@ -184,12 +199,11 @@ def write_to_log(self): self._abitem.set_variable("current.state_name", self.name) self._abitem.set_variable("current.state_id", self.id) self.__text.write_to_logger() + self.__order.write_to_logger() self.__is_copy_for.write_to_logger() self.__releasedby.write_to_logger() self.__can_release.write_to_logger() - if self.__use_done: - _log_se_use = self.__use_done[0] if len(self.__use_done) == 1 else self.__use_done - self._log_info("State configuration extended by se_use: {}", _log_se_use) + self.__use.write_to_logger() self._log_info("Updating Web Interface...") self._log_increase_indent() @@ -199,7 +213,8 @@ def write_to_log(self): 'actions_enter_or_stay': {}, 'actions_stay': {}, 'actions_leave': {}, - 'leave': False, 'enter': False, 'stay': False}) + 'leave': False, 'enter': False, 'stay': False, + 'is_copy_for': None, 'releasedby': None}) self._log_decrease_indent() self._log_info("Finished Web Interface Update") @@ -240,6 +255,24 @@ def write_to_log(self): self._abitem.set_variable("current.state_id", "") self._log_decrease_indent() + def update_order(self, value=None): + if isinstance(value, list): + if len(value) > 1: + _default_value = self.__order.get() + self._log_warning("se_stateorder for item {} can not be defined as a list" + " ({}). Using default value {}.", self.id, value, _default_value) + value = _default_value + elif len(value) == 1: + value = value[0] + if value is None and "se_stateorder" in self.__item.conf: + _, _, _, _issue = self.__order.set_from_attr(self.__item, "se_stateorder") + elif value is not None: + _, _, _issue = self.__order.set(value, "", True, None, False) + else: + _issue = [None] + + return _issue + # run actions when entering the state # item_allow_repeat: Is repeating actions generally allowed for the item? def run_enter(self, allow_item_repeat: bool): @@ -313,10 +346,10 @@ def refill(self): def update_releasedby_internal(self, states=None): if states == []: - _returnvalue, _returntype, _issue = self.__releasedby.set([None]) + _returnvalue, _returntype, _issue = self.__releasedby.set([None], "", True, None, False) elif states: self._log_develop("Setting releasedby to {}", states) - _returnvalue, _returntype, _issue = self.__releasedby.set(states) + _returnvalue, _returntype, _issue = self.__releasedby.set(states, "", True, None, False) self._log_develop("returnvalue {}", _returnvalue) else: _returnvalue, _returntype, _, _issue = self.__releasedby.set_from_attr(self.__item, "se_released_by") @@ -324,9 +357,9 @@ def update_releasedby_internal(self, states=None): def update_can_release_internal(self, states): if states == []: - _returnvalue, _returntype, _issue = self.__can_release.set([None]) + _returnvalue, _returntype, _issue = self.__can_release.set([None], "", True, None, False) elif states: - _returnvalue, _returntype, _issue = self.__can_release.set(states) + _returnvalue, _returntype, _issue = self.__can_release.set(states, "", True, None, False) else: _returnvalue, _returntype, _issue = [None], [None], None return _returnvalue, _returntype, _issue @@ -344,6 +377,18 @@ def update_name(self, item_state, recursion_depth=0): self.__name = self.text return self.__name + def __fill_list(self, item_states, recursion_depth, se_use=None): + for i, element in enumerate(item_states): + if element == self.state_item: + self._log_info("Use element {} is same as current state - Ignoring.", element) + elif element is not None and element not in self.__use_done: + try: + _use = se_use[i] + except Exception: + _use = element + self.__fill(element, recursion_depth, _use) + self.__use_done.append(element) + # Read configuration from item and populate data in class # item_state: item to read from # recursion_depth: current recursion_depth (recursion is canceled after five levels) @@ -362,7 +407,11 @@ def update_unused(used_attributes, type, name): def update_action_status(action_status, actiontype): if action_status is None: return - + action_status = StateEngineTools.flatten_list(action_status) + if isinstance(action_status, list): + for e in action_status: + update_action_status(e, actiontype) + return for itm, dct in action_status.items(): if itm not in self.__action_status: self.__action_status.update({itm: dct}) @@ -370,6 +419,9 @@ def update_action_status(action_status, actiontype): for (itm, dct) in action_status.items(): issues = dct.get('issue') if issues: + if isinstance(issues, list): + self.__action_status[itm]['issue'].extend( + [issue for issue in issues if issue not in self.__action_status[itm]['issue']]) origin_list = self.__action_status[itm].get('issueorigin', []) new_list = origin_list.copy() for i, listitem in enumerate(origin_list): @@ -392,7 +444,8 @@ def update_action_status(action_status, actiontype): filtered_dict[key].update(nested_dict) #self._log_develop("Add {} to used {}", key, filtered_dict) self.__used_attributes = copy(filtered_dict) - filtered_dict = {key: value for key, value in self.__action_status.items() if value.get('issue') not in [[], None]} + filtered_dict = {key: value for key, value in self.__action_status.items() + if value.get('issue') not in [[], [None], None]} self.__action_status = filtered_dict #self._log_develop("Updated action status: {}, updated used {}", self.__action_status, self.__used_attributes) @@ -420,7 +473,7 @@ def update_action_status(action_status, actiontype): {item_state.property.path: {'issue': _issue, 'attribute': 'se_use'}}) self._log_warning("{} - ignoring.", _issue) else: - _use = [_use] if not isinstance(_use, list) else StateEngineTools.flatten_list(_use) + _use = [_use] if not isinstance(_use, list) else _use _returntype = [_returntype] if not isinstance(_returntype, list) else _returntype cleaned_use_list = [] for i, element in enumerate(_use): @@ -460,15 +513,20 @@ def update_action_status(action_status, actiontype): _path = _configvalue[i] self._log_info("se_use {} defined by item/eval. Even if current result is not valid, " "entry will be re-evaluated on next state evaluation.", _path) - if _path not in cleaned_use_list: + if _path is not None and _path not in cleaned_use_list: cleaned_use_list.append(_path) + self.__use_done.append(_path) if _path is None: pass - elif _fill and element not in self.__use_done: + elif element == self.state_item: + self._log_info("Use element {} is same as current state - Ignoring.", _name) + elif _fill and element is not None and element not in self.__use_done: self._log_develop("Adding element {} to state fill function.", _name) - self.__use_done.append(element) - self.__fill(element, recursion_depth + 1, _name) - + if isinstance(_name, list): + self.__fill_list(element, recursion_depth + 1, _name) + else: + self.__use_done.append(element) + self.__fill(element, recursion_depth + 1, _name) self.__use.set(cleaned_use_list) # Get action sets and condition sets @@ -489,9 +547,8 @@ def update_action_status(action_status, actiontype): if child_name == "enter" or child_name.startswith("enter_"): _conditioncount += 1 _unused_attributes, _used_attributes = self.__conditions.update(child_name, child_item, parent_item) - if _conditioncount == 1: - self.__unused_attributes = copy(_unused_attributes) - self.__used_attributes = copy(_used_attributes) + self.__unused_attributes = copy(_unused_attributes) + self.__used_attributes = copy(_used_attributes) for item in self.__unused_attributes.keys(): if 'issue' in self.__unused_attributes[item].keys(): if not self.__unused_attributes[item].get('issueorigin'): @@ -499,6 +556,7 @@ def update_action_status(action_status, actiontype): entry = {'state': self.id, 'conditionset': child_name} if entry not in self.__unused_attributes[item].get('issueorigin'): self.__unused_attributes[item]['issueorigin'].append(entry) + self._abitem.update_attributes(self.__unused_attributes, self.__used_attributes) except ValueError as ex: raise ValueError("Condition {0} error: {1}".format(child_name, ex)) @@ -506,7 +564,7 @@ def update_action_status(action_status, actiontype): for attribute in parent_item.conf: func, name = StateEngineTools.partition_strip(attribute, "_") cond1 = name and name not in self.__used_attributes - cond2 = func == "se_item" or func == "se_eval" or func == "se_status" + cond2 = func == "se_item" or func == "se_eval" or func == "se_status_eval" or func == "se_status" cond3 = name not in self.__unused_attributes.keys() if cond1 and cond2 and cond3: @@ -523,6 +581,7 @@ def update_action_status(action_status, actiontype): _, _action_status = self.__actions_enter.update(attribute, child_item.conf[attribute]) if _action_status: update_action_status(_action_status, 'enter') + self._abitem.update_action_status(self.__action_status) update_unused(_used_attributes, 'action', child_name) elif child_name == "on_stay": _actioncount += 1 @@ -531,6 +590,7 @@ def update_action_status(action_status, actiontype): _, _action_status = self.__actions_stay.update(attribute, child_item.conf[attribute]) if _action_status: update_action_status(_action_status, 'stay') + self._abitem.update_action_status(self.__action_status) update_unused(_used_attributes, 'action', child_name) elif child_name == "on_enter_or_stay": _actioncount += 1 @@ -539,6 +599,7 @@ def update_action_status(action_status, actiontype): _, _action_status = self.__actions_enter_or_stay.update(attribute, child_item.conf[attribute]) if _action_status: update_action_status(_action_status, 'enter_or_stay') + self._abitem.update_action_status(self.__action_status) update_unused(_used_attributes, 'action', child_name) elif child_name == "on_leave": _actioncount += 1 @@ -547,6 +608,7 @@ def update_action_status(action_status, actiontype): _, _action_status = self.__actions_leave.update(attribute, child_item.conf[attribute]) if _action_status: update_action_status(_action_status, 'leave') + self._abitem.update_action_status(self.__action_status) update_unused(_used_attributes, 'action', child_name) except ValueError as ex: raise ValueError("Condition {0} check for actions error: {1}".format(child_name, ex)) @@ -558,6 +620,7 @@ def update_action_status(action_status, actiontype): _action_status = _result[1] if _action_status: update_action_status(_action_status, 'enter_or_stay') + self._abitem.update_action_status(self.__action_status) _total_actioncount = _enter_actioncount + _stay_actioncount + _enter_stay_actioncount + _leave_actioncount @@ -568,15 +631,19 @@ def update_action_status(action_status, actiontype): _action_status = self.__actions_enter.complete(item_state, self.__conditions.evals_items) if _action_status: update_action_status(_action_status, 'enter') + self._abitem.update_action_status(self.__action_status) _action_status = self.__actions_stay.complete(item_state, self.__conditions.evals_items) if _action_status: update_action_status(_action_status, 'stay') + self._abitem.update_action_status(self.__action_status) _action_status = self.__actions_enter_or_stay.complete(item_state, self.__conditions.evals_items) if _action_status: update_action_status(_action_status, 'enter_or_stay') + self._abitem.update_action_status(self.__action_status) _action_status = self.__actions_leave.complete(item_state, self.__conditions.evals_items) if _action_status: update_action_status(_action_status, 'leave') + self._abitem.update_action_status(self.__action_status) self._abitem.update_action_status(self.__action_status) self._abitem.update_attributes(self.__unused_attributes, self.__used_attributes) _summary = "{} on_enter, {} on_stay , {} on_enter_or_stay, {} on_leave" diff --git a/stateengine/StateEngineTools.py b/stateengine/StateEngineTools.py index da3f9c4af..f47861f2a 100755 --- a/stateengine/StateEngineTools.py +++ b/stateengine/StateEngineTools.py @@ -124,7 +124,7 @@ def parse_relative(evalstr, begintag, endtags): rel = rest[:rest.find(endtag)] rest = rest[rest.find(endtag)+len(endtag):] if 'property' in endtag: - rest1 = re.split('( |\+|-|\*|/)', rest, 1) + rest1 = re.split('([ +\-*/])', rest, 1) rest = ''.join(rest1[1:]) pref += "se_eval.get_relative_itemproperty('{}', '{}')".format(rel, rest1[0]) elif '()' in endtag: @@ -299,6 +299,48 @@ def partition_strip(value, splitchar): return part1.strip(), part2.strip() +# return list representation of string +# value: list as string +# returns: list or original value +def convert_str_to_list(value): + if isinstance(value, str) and ("," in value and value.startswith("[")): + value = value.strip("[]") + if isinstance(value, str) and "," in value: + try: + elements = re.findall(r"'([^']+)'|([^,]+)", value) + flattened_elements = [element[0] if element[0] else element[1] for element in elements] + formatted_str = "[" + ", ".join( + ["'" + element.strip(" '\"") + "'" for element in flattened_elements]) + "]" + return literal_eval(formatted_str) + except Exception as ex: + raise ValueError("Problem converting string to list: {}".format(ex)) + elif isinstance(value, list): + return value + else: + return [value] + +# return dict representation of string +# value: OrderedDict as string +# returns: OrderedDict or original value +def convert_str_to_dict(value): + if isinstance(value, str) and value.startswith("["): + value = re.split('(, (?![^(]*\)))', value.strip('][')) + value = [s for s in value if s != ', '] + result = [] + for s in value: + m = re.match(r'^OrderedDict\((.+)\)$', s) + if m: + result.append(dict(literal_eval(m.group(1)))) + else: + result.append(literal_eval(s)) + value = result + else: + return value + try: + return literal_eval(value) + except Exception as ex: + raise ValueError("Problem converting string to OrderedDict: {}".format(ex)) + # return string representation of eval function # eval_func: eval function # returns: string representation @@ -333,10 +375,10 @@ def get_original_caller(elog, caller, source, item=None, eval_keyword=['Eval'], original_source = source else: original_source = "None" - while original_caller in eval_keyword: + while partition_strip(original_caller, ":")[0] in eval_keyword: original_item = itemsApi.return_item(original_source) if original_item is None: - elog.warning("get_caller({0}, {1}): original item not found", caller, source) + elog.info("get_caller({0}, {1}): original item not found", caller, source) break original_manipulated_by = original_item.property.last_update_by if eval_type == "update" else \ original_item.property.last_change_by diff --git a/stateengine/StateEngineValue.py b/stateengine/StateEngineValue.py index 4f26c950a..11774967c 100755 --- a/stateengine/StateEngineValue.py +++ b/stateengine/StateEngineValue.py @@ -112,20 +112,9 @@ def set_from_attr(self, item, attribute_name, default_value=None, reset=True, at # update value type correctly based on attr_type value = "{}:{}".format(attr_type, value) # Convert weird string representation of OrderedDict correctly - if isinstance(value, str) and value.startswith("["): - value = re.split('(, (?![^(]*\)))', value.strip('][')) - value = [s for s in value if s != ', '] - result = [] - for s in value: - m = re.match(r'^OrderedDict\((.+)\)$', s) - if m: - result.append(dict(ast.literal_eval(m.group(1)))) - else: - result.append(ast.literal_eval(s)) - value = result try: - value = ast.literal_eval(value) - except Exception as ex: + value = StateEngineTools.convert_str_to_dict(value) + except Exception: pass if value is not None: self._log_develop("Setting value {0}, attribute name {1}, reset {2}, type {3}", @@ -155,8 +144,9 @@ def __resetvalue(self): # Set value # value: string indicating value or source of value # name: name of object ("time" is being handled differently) - def set(self, value, name="", reset=True, item=None): - #value = copy.deepcopy(value) + def set(self, value, name="", reset=True, item=None, copyvalue=True): + if copyvalue is True: + value = copy.copy(value) if reset: self.__resetvalue() if isinstance(value, list): @@ -183,7 +173,8 @@ def set(self, value, name="", reset=True, item=None): field_value[i] = value[i] if source[i] not in self.__valid_valuetypes: _issue = "{0} is not a valid value type.".format(source[i]) - self.__issues.append(_issue) + if _issue not in self.__issues: + self.__issues.append(_issue) self._log_warning("{0} Use one of {1} instead. Value '{2}' " "will be handled the same as the item type, e.g. string, bool, etc.", _issue, self.__valid_valuetypes, field_value[i]) @@ -208,7 +199,8 @@ def set(self, value, name="", reset=True, item=None): val, field_value[i], source[i] = None, None, None else: _issue = "Template with name '{}' does not exist for this SE Item!".format(field_value[i]) - self.__issues.append(_issue) + if _issue not in self.__issues: + self.__issues.append(_issue) self._log_warning(_issue) self.__listorder = [i for i in self.__listorder if i != val] source[i], field_value[i], val = None, None, None @@ -236,13 +228,14 @@ def set(self, value, name="", reset=True, item=None): self._log_warning("Removing template {}: {}", self.__template, ex) else: _issue = "Template with name '{}' does not exist for this SE Item!".format(self.__template) - self.__issues.append(_issue) + if _issue not in self.__issues: + self.__issues.append(_issue) self._log_warning(_issue) self.__listorder = [i for i in self.__listorder if i != value] source, field_value, value = None, None, None try: - cond1 = source.lstrip('-').replace('.','',1).isdigit() - cond2 = field_value.lstrip('-').replace('.','',1).isdigit() + cond1 = source.lstrip('-').replace('.', '', 1).isdigit() + cond2 = field_value.lstrip('-').replace('.', '', 1).isdigit() except Exception: cond1 = False cond2 = False @@ -254,7 +247,8 @@ def set(self, value, name="", reset=True, item=None): source = "value" if source not in self.__valid_valuetypes: _issue = "{0} is not a valid value type.".format(source) - self.__issues.append(_issue) + if _issue not in self.__issues: + self.__issues.append(_issue) self._log_warning("{0} Use one of {1} instead. Value '{2}' " "will be handled the same as the item type, e.g. string, bool, etc.", _issue, self.__valid_valuetypes, field_value) @@ -284,7 +278,8 @@ def set(self, value, name="", reset=True, item=None): else: _issue = "Template with name '{}' does not exist for this SE Item!".format( self.__template) - self.__issues.append(_issue) + if _issue not in self.__issues: + self.__issues.append(_issue) self._log_warning(_issue) s = None try: @@ -299,19 +294,27 @@ def set(self, value, name="", reset=True, item=None): elif field_value[i] == "": field_value[i] = s s = "value" - cond3 = isinstance(field_value[i], str) and field_value[i].lstrip('-').replace('.','',1).isdigit() - if cond3: - field_value[i] = ast.literal_eval(field_value[i]) - elif isinstance(field_value[i], str) and field_value[i].lower() in ['true', 'yes']: - field_value[i] = True - elif isinstance(field_value[i], str) and field_value[i].lower() in ['false', 'no']: - field_value[i] = False - self.__value = [] if self.__value is None else [self.__value] if not isinstance(self.__value, list) else self.__value - self.__value.append(None if s != "value" else self.__do_cast(field_value[i])) + self.__value = [] if self.__value is None else [self.__value] if not isinstance(self.__value, + list) else self.__value + if s == "value": + cond3 = isinstance(field_value[i], str) and field_value[i].lstrip('-').replace('.','',1).isdigit() + if cond3: + field_value[i] = ast.literal_eval(field_value[i]) + elif isinstance(field_value[i], str) and field_value[i].lower() in ['true', 'yes']: + field_value[i] = True + elif isinstance(field_value[i], str) and field_value[i].lower() in ['false', 'no']: + field_value[i] = False + + _value, _issue = self.__do_cast(field_value[i]) + if _issue not in [[], None, [None], self.__issues]: + self.__issues.append(_issue) + self.__value.append(_value) + else: + self.__value.append(None) self.__item = [] if self.__item is None else [self.__item] if not isinstance(self.__item, list) else self.__item if s == "item": _item, _issue = self._abitem.return_item(field_value[i]) - if _issue: + if _issue not in [[], None, [None], self.__issues]: self.__issues.append(_issue) self.__item.append(None if s != "item" else self.__absolute_item(_item, field_value[i])) self.__eval = [] if self.__eval is None else [self.__eval] if not isinstance(self.__eval, list) else self.__eval @@ -322,23 +325,30 @@ def set(self, value, name="", reset=True, item=None): self.__struct.append(None if s != "struct" else StateEngineStructs.create(self._abitem, field_value[i])) self.__varname = [] if self.__varname is None else [self.__varname] if not isinstance(self.__varname, list) else self.__varname self.__varname.append(None if s != "var" else field_value[i]) - self.__item = [i for i in self.__item if i is not None] - self.__eval = [i for i in self.__eval if i is not None] - self.__regex = [i for i in self.__regex if i is not None] - self.__struct = [i for i in self.__struct if i is not None] - self.__varname = [i for i in self.__varname if i is not None] - self.__value = [i for i in self.__value if i is not None] - self.__value = self.__value[0] if len(self.__value) == 1 else None if len(self.__value) == 0 else self.__value - self.__item = self.__item[0] if len(self.__item) == 1 else None if len(self.__item) == 0 else self.__item - self.__eval = self.__eval[0] if len(self.__eval) == 1 else None if len(self.__eval) == 0 else self.__eval - self.__regex = self.__regex[0] if len(self.__regex) == 1 else None if len(self.__regex) == 0 else self.__regex - self.__struct = None if len(self.__struct) == 0 else self.__struct - self.__varname = self.__varname[0] if len(self.__varname) == 1 else None if len(self.__varname) == 0 else self.__varname + + if self.__item: + self.__item = [i for i in self.__item if i is not None] + self.__item = self.__item[0] if len(self.__item) == 1 else None if len(self.__item) == 0 else self.__item + if self.__eval: + self.__eval = [i for i in self.__eval if i is not None] + self.__eval = self.__eval[0] if len(self.__eval) == 1 else None if len(self.__eval) == 0 else self.__eval + if self.__regex: + self.__regex = [i for i in self.__regex if i is not None] + self.__regex = self.__regex[0] if len(self.__regex) == 1 else None if len(self.__regex) == 0 else self.__regex + if self.__struct: + self.__struct = [i for i in self.__struct if i is not None] + self.__struct = None if len(self.__struct) == 0 else self.__struct + if self.__varname: + self.__varname = [i for i in self.__varname if i is not None] + self.__varname = self.__varname[0] if len(self.__varname) == 1 else None if len(self.__varname) == 0 else self.__varname + if self.__value: + self.__value = [i for i in self.__value if i is not None] + self.__value = self.__value[0] if len(self.__value) == 1 else None if len(self.__value) == 0 else self.__value else: if source == "item": _item, _issue = self._abitem.return_item(field_value) - if _issue: + if _issue not in [[], None, [None], self.__issues]: self.__issues.append(_issue) self.__item = None if source != "item" else self.__absolute_item(_item, field_value) self.__eval = None if source != "eval" else field_value @@ -356,19 +366,23 @@ def set(self, value, name="", reset=True, item=None): field_value = True elif isinstance(field_value, str) and field_value.lower() in ['false', 'no']: field_value = False - self.__value = self.__do_cast(field_value) + self.__value, _issue = self.__do_cast(field_value) + if _issue not in [[], None, [None], self.__issues]: + self.__issues.append(_issue) else: self.__value = None self.__issues = StateEngineTools.flatten_list(self.__issues) self.__listorder = StateEngineTools.flatten_list(self.__listorder) self.__type_listorder = StateEngineTools.flatten_list(self.__type_listorder) + del value return self.__listorder, self.__type_listorder, self.__issues # Set cast function # cast_func: cast function def set_cast(self, cast_func): self.__cast_func = cast_func - self.__value = self.__do_cast(self.__value) + self.__value, _issue = self.__do_cast(self.__value) + return [_issue] # determine and return value def get(self, default=None, originalorder=True): @@ -406,7 +420,7 @@ def get(self, default=None, originalorder=True): def get_for_webif(self): returnvalues = self.get() returnvalues = self.__varname if returnvalues == '' else returnvalues - return returnvalues + return str(returnvalues) def get_type(self): if len(self.__listorder) <= 1: @@ -504,8 +518,6 @@ def get_text(self, prefix=None, suffix=None): def cast_item(self, value): try: _returnvalue, _issue = self._abitem.return_item(value) - if _issue: - self.__issues.append(_issue) return _returnvalue except Exception as ex: self._log_error("Can't cast {0} to item/struct! {1}".format(value, ex)) @@ -546,6 +558,7 @@ def __absolute_item(self, value, id=None): # Cast given value, if cast-function is set # value: value to cast def __do_cast(self, value, id=None): + _issue = None if value is not None and self.__cast_func is not None: try: if isinstance(value, list): @@ -556,7 +569,6 @@ def __do_cast(self, value, id=None): except Exception as ex: _newvalue = None _issue = "Problem casting element '{0}' to {1}: {2}.".format(element, self.__cast_func, ex) - self.__issues.append(_issue) self._log_warning(_issue) valuelist.append(_newvalue) if element in self.__listorder: @@ -584,18 +596,15 @@ def __do_cast(self, value, id=None): except Exception as ex: if any(x in value for x in ['sh.', '_eval', '(']): _issue = "You most likely forgot to prefix your expression with 'eval:'" - self.__issues.append(_issue) raise ValueError(_issue) else: - _issue = "Not possible to cast because {}".format(ex) - self.__issues.append(_issue) + _issue = "Not possible to cast '{}' because {}".format(value, ex) raise ValueError(_issue) if value in self.__listorder: self.__listorder[self.__listorder.index(value)] = _newvalue value = _newvalue except Exception as ex: - _issue = "Problem casting '{0}' to {1}: {2}.".format(value, self.__cast_func, ex) - self.__issues.append(_issue) + _issue = "Problem casting '{0}': {1}.".format(value, ex) self._log_warning(_issue) if '_cast_list' in self.__cast_func.__globals__ and self.__cast_func == self.__cast_func.__globals__['_cast_list']: try: @@ -608,9 +617,9 @@ def __do_cast(self, value, id=None): value = [value] self._log_debug("Original casting of {} to {} failed. New cast is now: {}.", value, self.__cast_func, type(value)) - return value - return None - return value + return value, _issue + return None, _issue + return value, _issue # Determine value by using a struct def __get_from_struct(self): @@ -618,13 +627,13 @@ def __get_from_struct(self): if isinstance(self.__struct, list): for val in self.__struct: if val is not None: - _newvalue = self.__do_cast(val) + _newvalue, _issue = self.__do_cast(val) values.append(_newvalue) if 'struct:{}'.format(val.property.path) in self.__listorder: self.__listorder[self.__listorder.index('struct:{}'.format(val.property.path))] = _newvalue else: if self.__struct is not None: - _newvalue = self.__do_cast(self.__struct) + _newvalue, _issue = self.__do_cast(self.__struct) if 'struct:{}'.format(self.__regex) in self.__listorder: self.__listorder[self.__listorder.index('struct:{}'.format(self.__struct))] = _newvalue values = _newvalue @@ -632,14 +641,14 @@ def __get_from_struct(self): return values try: - _newvalue = self.__do_cast(self.__struct) + _newvalue, _issue = self.__do_cast(self.__struct) if 'struct:{}'.format(self.__struct) in self.__listorder: self.__listorder[self.__listorder.index('struct:{}'.format(self.__struct))] = _newvalue values = _newvalue except Exception as ex: values = self.__struct _issue = "Problem while getting from struct '{0}': {1}.".format(values, ex) - self.__issues.append(_issue) + #self.__issues.append(_issue) self._log_info(_issue) return values @@ -669,7 +678,7 @@ def __get_from_regex(self): except Exception as ex: values = self.__regex _issue = "Problem while creating regex '{0}': {1}.".format(values, ex) - self.__issues.append(_issue) + #self.__issues.append(_issue) self._log_info(_issue) return values @@ -686,9 +695,9 @@ def __get_eval(self): self._log_debug("Checking eval: {0}", self.__eval) self._log_increase_indent() try: - _newvalue = self.__do_cast(eval(self.__eval)) + _newvalue, _issue = self.__do_cast(eval(self.__eval)) if 'eval:{}'.format(self.__eval) in self.__listorder: - self.__listorder[self.__listorder.index('eval:{}'.format(self.__eval))] = _newvalue + self.__listorder[self.__listorder.index('eval:{}'.format(self.__eval))] = [_newvalue] values = _newvalue self._log_decrease_indent() self._log_debug("Eval result: {0} ({1}).", values, type(values)) @@ -696,7 +705,7 @@ def __get_eval(self): except Exception as ex: self._log_decrease_indent() _issue = "Problem evaluating '{0}': {1}.".format(StateEngineTools.get_eval_name(self.__eval), ex) - self.__issues.append(_issue) + #self.__issues.append(_issue) self._log_warning(_issue) self._log_increase_indent() values = None @@ -717,9 +726,9 @@ def __get_eval(self): # noinspection PyUnusedLocal stateengine_eval = se_eval = StateEngineEval.SeEval(self._abitem) try: - _newvalue = self.__do_cast(eval(val)) + _newvalue, _issue = self.__do_cast(eval(val)) if 'eval:{}'.format(val) in self.__listorder: - self.__listorder[self.__listorder.index('eval:{}'.format(val))] = _newvalue + self.__listorder[self.__listorder.index('eval:{}'.format(val))] = [_newvalue] value = _newvalue self._log_decrease_indent() self._log_debug("Eval result from list: {0}.", value) @@ -728,33 +737,33 @@ def __get_eval(self): self._log_decrease_indent() _issue = "Problem evaluating from list '{0}': {1}.".format( StateEngineTools.get_eval_name(val), ex) - self.__issues.append(_issue) + #self.__issues.append(_issue) self._log_warning(_issue) self._log_increase_indent() value = None else: try: - _newvalue = self.__do_cast(val()) + _newvalue, _issue = self.__do_cast(val()) if 'eval:{}'.format(val) in self.__listorder: - self.__listorder[self.__listorder.index('eval:{}'.format(val))] = _newvalue + self.__listorder[self.__listorder.index('eval:{}'.format(val))] = [_newvalue] value = _newvalue except Exception as ex: self._log_decrease_indent() _issue = "Problem evaluating '{0}': {1}.".format( StateEngineTools.get_eval_name(val), ex) - self.__issues.append(_issue) + #self.__issues.append(_issue) self._log_info(_issue) value = None if value is not None: - values.append(self.__do_cast(value)) + values.append(value) self._log_decrease_indent() else: self._log_debug("Checking eval (no str, no list): {0}.", self.__eval) try: self._log_increase_indent() - _newvalue = self.__do_cast(self.__eval()) + _newvalue, _issue = self.__do_cast(self.__eval()) if 'eval:{}'.format(self.__eval) in self.__listorder: - self.__listorder[self.__listorder.index('eval:{}'.format(self.__eval))] = _newvalue + self.__listorder[self.__listorder.index('eval:{}'.format(self.__eval))] = [_newvalue] values = _newvalue self._log_decrease_indent() self._log_debug("Eval result (no str, no list): {0}.", values) @@ -762,7 +771,7 @@ def __get_eval(self): except Exception as ex: self._log_decrease_indent() _issue = "Problem evaluating '{0}': {1}.".format(StateEngineTools.get_eval_name(self.__eval), ex) - self.__issues.append(_issue) + #self.__issues.append(_issue) self._log_warning(_issue) self._log_increase_indent() return None @@ -771,47 +780,95 @@ def __get_eval(self): # Determine value from item def __get_from_item(self): + _issue_list = [] if isinstance(self.__item, list): values = [] for val in self.__item: + _new_values = [] if val is None: _newvalue = None else: - _newvalue = self.__do_cast(val.property.value) - values.append(_newvalue) - if 'item:{}'.format(val) in self.__listorder: - self.__listorder[self.__listorder.index('item:{}'.format(val))] = _newvalue + try: + checked_entry = StateEngineTools.convert_str_to_list(val.property.value) + except Exception as ex: + self._log_warning("While getting from list item: {}", ex) + checked_entry = [] + checked_entry = checked_entry if isinstance(checked_entry, list) else [checked_entry] + + for entry in checked_entry: + _newvalue, _issue = self.__do_cast(entry) + if _issue not in [[], None, [None], _issue_list]: + _issue_list.append(_issue) + if _newvalue is not None: + _new_values.append(_newvalue) + + _new_values = _new_values[0] if len(_new_values) == 1 else None if len(_new_values) == 0 else _new_values + search_item = 'item:{}'.format(val) + if search_item in self.__listorder: + index = self.__listorder.index(search_item) + self.__listorder[index] = _new_values + search_item = self.itemsApi.return_item(val) or val + if search_item is not None and search_item in self.__listorder: + index = self.__listorder.index(search_item) + self.__listorder[index] = _new_values + values.append(_new_values) + if values is not None: + return values else: if self.__item is None: return None - _newvalue = self.__do_cast(self.__item.property.value) - if 'item:{}'.format(self.__item) in self.__listorder: - self.__listorder[self.__listorder.index('item:{}'.format(self.__item))] = _newvalue - values = _newvalue - if values is not None: - return values + try: + checked_entry = StateEngineTools.convert_str_to_list(self.__item.property.value) + except Exception as ex: + self._log_warning("While getting from item: {}", ex) + checked_entry = [] + checked_entry = checked_entry if isinstance(checked_entry, list) else [checked_entry] + _new_values = [] + for entry in checked_entry: + _newvalue, _issue = self.__do_cast(entry) + if _issue not in [[], None, [None], _issue_list]: + _issue_list.append(_issue) + if _newvalue is not None: + _new_values.append(_newvalue) + _new_values = _new_values[0] if len(_new_values) == 1 else None if len(_new_values) == 0 else [_new_values] + search_item = 'item:{}'.format(self.__item) + if search_item in self.__listorder: + index = self.__listorder.index(search_item) + self.__listorder[index] = _new_values + if self.__item in self.__listorder: + index = self.__listorder.index(self.__item) + self.__listorder[index] = _new_values + values = _new_values + if values is not None: + return values try: _newvalue = self.__item.property.path - if 'item:{}'.format(self.__item) in self.__listorder: - self.__listorder[self.__listorder.index('item:{}'.format(self.__item))] = _newvalue + search_item = 'item:{}'.format(self.__item) + if search_item in self.__listorder: + index = self.__listorder.index(search_item) + self.__listorder[index] = _newvalue values = _newvalue except Exception as ex: values = self.__item _issue = "Problem while reading item path '{0}': {1}.".format(values, ex) - self.__issues.append(_issue) + if _issue not in _issue_list: + _issue_list.append(_issue) self._log_info(_issue) - return self.__do_cast(values) + _newvalue, _issue = self.__do_cast(values) + if _issue not in [[], None, [None], _issue_list]: + _issue_list.append(_issue) + return _newvalue # Determine value from variable def __get_from_variable(self): def update_value(varname): value = self._abitem.get_variable(varname) - new_value = self.__do_cast(value) + new_value, _issue = self.__do_cast(value) new_value = 'var:{}'.format(varname) if new_value == '' else new_value if isinstance(new_value, str) and 'Unknown variable' in new_value: issue = "There is a problem with your variable {}".format(new_value) - self.__issues.append(issue) + #self.__issues.append(issue) self._log_warning(issue) new_value = '' self._log_debug("Checking variable '{0}', value {1} from list {2}", diff --git a/stateengine/StateEngineWebif.py b/stateengine/StateEngineWebif.py index 84399112a..a40a5a300 100755 --- a/stateengine/StateEngineWebif.py +++ b/stateengine/StateEngineWebif.py @@ -135,21 +135,22 @@ def _check_webif_conditions(action_dict, condition_to_meet: str, conditionset: s else " ({} not met)".format(condition_info) if not condition_met\ else " (no repeat)" if _repeat is False and originaltype == 'actions_stay'\ else " (delay: {})".format(_delay) if _delay > 0\ - else " (wrong delay!)" if _delay < 0\ + else " (cancel delay!)" if _delay == -1 \ + else " (wrong delay!)" if _delay < -1 \ else " (delta {} < {})".format(_delta, _mindelta) if cond_delta and cond1 and cond2\ else "" action1 = action_dict.get('function') if action1 == 'set': - action2 = action_dict.get('item') - value_check = action_dict.get('value') + action2 = str(action_dict.get('item')) + value_check = str(action_dict.get('value')) value_check = '""' if value_check == "" else value_check - is_number = value_check.lstrip('-').replace('.','',1).isdigit() + is_number = value_check.lstrip('-').replace('.', '', 1).isdigit() if is_number and "." in value_check: value_check = round(float(value_check), 2) action3 = 'to {}'.format(value_check) elif action1 == 'special': - action2 = action_dict.get('special') - action3 = action_dict.get('value') + action2 = str(action_dict.get('special')) + action3 = str(action_dict.get('value')) else: action2 = 'None' action3 = "" @@ -157,13 +158,17 @@ def _check_webif_conditions(action_dict, condition_to_meet: str, conditionset: s cond_enter = originaltype == 'actions_enter' and self.__states[state].get('enter') is True cond_stay = originaltype == 'actions_stay' and self.__states[state].get('stay') is True active = True if (cond_enter or cond_stay) and cond1 else False - success_info = '' \ + success_info = '' \ if _issue is not None and active \ - else '' \ + else '' \ if (_success == 'False' or not condition_met) and active \ - else '' \ + else '' \ + if _success == 'Scheduled' and active \ + else '' \ + if _success == 'True' and active and _delay > 0 \ + else '' \ if _success == 'True' and active \ - else '' + else '' if not action2 == 'None': actionlabel += '{} {} {} {}'.format(fontcolor, action1, action2, action3, additionaltext) actionlabel += '{}'.format(success_info) @@ -188,10 +193,11 @@ def _conditionlabel(self, state, conditionset, i): current = condition_dict.get('current') match = condition_dict.get('match') - item_none = str(condition_dict.get('item')) == 'None' status_none = str(condition_dict.get('status')) == 'None' - eval_none = condition_dict.get('eval') == 'None' - value_none = condition_dict.get('value') == 'None' + item_none = str(condition_dict.get('item')) == 'None' or not status_none + status_eval_none = condition_dict.get('status_eval') == 'None' + eval_none = condition_dict.get('eval') == 'None' or not status_eval_none + value_none = str(condition_dict.get('value')) == 'None' min_none = condition_dict.get('min') == 'None' max_none = condition_dict.get('max') == 'None' agemin_none = condition_dict.get('agemin') == 'None' @@ -202,17 +208,10 @@ def _conditionlabel(self, state, conditionset, i): for compare in condition_dict: cond1 = not condition_dict.get(compare) == 'None' - cond2 = not compare == 'item' - cond3 = not compare == 'eval' - cond4 = not compare == 'negate' - cond5 = not compare == 'agenegate' - cond6 = not compare == 'changedbynegate' - cond7 = not compare == 'updatedbynegate' - cond8 = not compare == 'triggeredbynegate' - cond9 = not compare == 'status' - cond10 = not compare == 'current' - cond11 = not compare == 'match' - if cond1 and cond2 and cond3 and cond4 and cond5 and cond6 and cond7 and cond8 and cond9 and cond10 and cond11: + excluded_values = ['item', 'eval', 'negate', 'agenegate', 'changedbynegate', + 'updatedbynegate', 'triggeredbynegate', 'status', 'current', 'match', 'status_eval'] + + if cond1 and compare not in excluded_values: try: list_index = list(self.__states.keys()).index(self.__active_state) except Exception: @@ -226,6 +225,7 @@ def _conditionlabel(self, state, conditionset, i): info_status = str(condition_dict.get('status') or '') info_item = str(condition_dict.get('item') or '') info_eval = str(condition_dict.get('eval') or '') + info_status_eval = str(condition_dict.get('status_eval') or '') info_compare = str(condition_dict.get(compare) or '') if not status_none: textlength = len(info_status) @@ -234,13 +234,13 @@ def _conditionlabel(self, state, conditionset, i): condition_tooltip += ' ' tooltip_count += 1 condition_tooltip += '{}'.format(condition_dict.get('status')) - elif not item_none: - textlength = len(info_item) + elif not status_eval_none: + textlength = len(info_status_eval) if textlength > self.__textlimit: if tooltip_count > 0: condition_tooltip += ' ' tooltip_count += 1 - condition_tooltip += '{}'.format(condition_dict.get('item')) + condition_tooltip += '{}'.format(condition_dict.get('status_eval')) elif not eval_none: textlength = len(info_eval) if textlength > self.__textlimit: @@ -248,14 +248,21 @@ def _conditionlabel(self, state, conditionset, i): condition_tooltip += ' ' tooltip_count += 1 condition_tooltip += '{}'.format(condition_dict.get('eval')) + elif not item_none: + textlength = len(info_item) + if textlength > self.__textlimit: + if tooltip_count > 0: + condition_tooltip += ' ' + tooltip_count += 1 + condition_tooltip += '{}'.format(condition_dict.get('item')) else: textlength = 0 info_item = info_item[:self.__textlimit] + '..  ' * int(textlength > self.__textlimit) info_status = info_status[:self.__textlimit] + '..  ' * int(textlength > self.__textlimit) info_eval = info_eval[:self.__textlimit] + '..  ' * int(textlength > self.__textlimit) - info_value = info_compare[:self.__textlimit] + '..  ' * \ - int(len(info_compare) > self.__textlimit) + info_status_eval = info_status_eval[:self.__textlimit] + '..  ' * int(textlength > self.__textlimit) + info_value = info_compare[:self.__textlimit] + '..  ' * int(len(info_compare) > self.__textlimit) textlength = len(info_compare) if textlength > self.__textlimit: if tooltip_count > 0: @@ -265,10 +272,12 @@ def _conditionlabel(self, state, conditionset, i): if not status_none: info = info_status - elif not item_none: - info = info_item + elif not status_eval_none: + info = info_status_eval elif not eval_none: info = info_eval + elif not item_none: + info = info_item else: info = "" conditionlist += '{}'.format(info) @@ -295,12 +304,16 @@ def _conditionlabel(self, state, conditionset, i): else match.get('age') if compare in ["agemin", "agemax", "age"]\ else match.get(compare) conditionlist += '{}'.format(comparison) - conditionlist += '"{}"'.format(info) if not item_none and not status_none and not eval_none else '' + conditionlist += '"{}"'.format(info) if not item_none and not status_none \ + and not eval_none and not status_eval_none else '' info = info_value + cond1 = eval_none and not item_none + cond2 = eval_none and (not status_none or not status_eval_none) + cond3 = not eval_none and item_none + cond4 = not eval_none and status_eval_none and status_none conditionlist += '{}'.format(info) if not condition_dict.get(compare) == 'None' and ( - (eval_none and not item_none) or (eval_none and not status_none) or \ - (not eval_none and item_none) or (not eval_none and status_none)) else '' + cond1 or cond2 or cond3 or cond4) else '' conditionlist += ' (negate)' if condition_dict.get('negate') == 'True' and "age" \ not in compare and not compare == "value" else '' conditionlist += ' (negate)' if condition_dict.get('agenegate') == 'True' and "age" in compare else '' @@ -334,7 +347,7 @@ def _add_actioncondition(self, state, conditionset, action_type, new_y, cond1, c if self.__nodes.get('{}_{}_state_actions_enter_edge'.format(state, conditionset)) is None: self.__nodes['{}_{}_state_{}_edge'.format(state, conditionset, action_type)] = \ pydotplus.Edge(self.__nodes['{}_{}'.format(state, conditionset)], self.__nodes['{}_{}_state_{}'.format( - state, conditionset, action_type)], style='bold', taillabel=" True", tooltip='first enter') + state, conditionset, action_type)], style='bold', taillabel=" True", tooltip='first enter') self.__graph.add_edge(self.__nodes['{}_{}_state_{}_edge'.format(state, conditionset, action_type)]) else: self.__graph.add_edge(pydotplus.Edge(self.__nodes['{}_{}_state_actions_enter'.format(state, conditionset)], @@ -361,7 +374,6 @@ def drawgraph(self, filename): previousconditionset = '' previousstate = '' previousstate_conditionset = '' - #self._log_debug('STATES {}', self.__states) for i, state in enumerate(self.__states): #self._log_debug('Adding state for webif {}', self.__states[state]) if isinstance(self.__states[state], (OrderedDict, dict)): @@ -398,10 +410,12 @@ def drawgraph(self, filename): new_y -= 1 * self.__scalefactor position = '{},{}!'.format(0, new_y) #self._log_debug('state: {} {}',state, position) + self.__nodes[state] = pydotplus.Node(state, pos=position, pin=True, notranslate=True, style="filled", fillcolor=color, shape="ellipse", label='<
' - '
{}
{}
>'.format(state, self.__states[state]['name'])) + '{}>'.format( + state, self.__states[state]['name'])) position = '{},{}!'.format(0.5, new_y) self.__nodes['{}_right'.format(state)] = pydotplus.Node('{}_right'.format(state), pos=position, shape="square", width="0", label="") @@ -422,13 +436,16 @@ def drawgraph(self, filename): for j, conditionset in enumerate(self.__states[state]['conditionsets']): if len(actions_enter) > 0 or len(actions_enter_or_stay) > 0: - actionlist_enter, action_tooltip_enter, action_tooltip_count_enter = self._actionlabel(state, 'actions_enter', conditionset, previousconditionset, previousstate_conditionset) + actionlist_enter, action_tooltip_enter, action_tooltip_count_enter = \ + self._actionlabel(state, 'actions_enter', conditionset, previousconditionset, previousstate_conditionset) if len(actions_stay) > 0 or len(actions_enter_or_stay) > 0: - actionlist_stay, action_tooltip_stay, action_tooltip_count_stay = self._actionlabel(state, 'actions_stay', conditionset, previousconditionset, previousstate_conditionset) + actionlist_stay, action_tooltip_stay, action_tooltip_count_stay = \ + self._actionlabel(state, 'actions_stay', conditionset, previousconditionset, previousstate_conditionset) if len(actions_leave) > 0: - actionlist_leave, action_tooltip_leave, action_tooltip_count_leave = self._actionlabel(state, 'actions_leave', conditionset, previousconditionset, previousstate_conditionset) + actionlist_leave, action_tooltip_leave, action_tooltip_count_leave = \ + self._actionlabel(state, 'actions_leave', conditionset, previousconditionset, previousstate_conditionset) new_y -= 1 * self.__scalefactor if j == 0 else 2 * self.__scalefactor position = '{},{}!'.format(0.5, new_y) @@ -512,11 +529,16 @@ def drawgraph(self, filename): self.__graph.add_edge(pydotplus.Edge(self.__nodes['{}_{}'.format(state, conditionset)], self.__nodes['{}_{}_right'.format(state, conditionset)], style='bold', taillabel=" True", tooltip='action on enter')) - + if self.__states[state].get('is_copy_for'): + xlabel = "can currently release {}\n\r".format(self.__states[state].get('is_copy_for')) + elif self.__states[state].get('releasedby'): + xlabel = "can currently get released by {}\n\r".format(self.__states[state].get('releasedby')) + else: + xlabel = "" if j == 0: self.__graph.add_edge(pydotplus.Edge(self.__nodes[state], self.__nodes['{}_right'.format(state)], - style='bold', color='black', dir='none', - edgetooltip='check first conditionset')) + style='bold', color='black', dir='none', + xlabel=xlabel, edgetooltip='check first conditionset')) self.__graph.add_edge(pydotplus.Edge(self.__nodes['{}_right'.format(state)], self.__nodes['{}_{}'.format(state, conditionset)], style='bold', color='black', tooltip='check first conditionset')) @@ -562,7 +584,7 @@ def drawgraph(self, filename): self.__graph.add_node(self.__nodes['{}_actions_leave'.format(state)]) self.__graph.add_edge(pydotplus.Edge(self.__nodes['{}_leave'.format(state)], self.__nodes['{}_actions_leave'.format(state)], style='bold', - taillabel=" True", tooltip='run leave actions')) + taillabel=" True", tooltip='run leave actions')) previous_state = state diff --git a/stateengine/__init__.py b/stateengine/__init__.py index 7b0598afb..dd10a4dad 100755 --- a/stateengine/__init__.py +++ b/stateengine/__init__.py @@ -35,6 +35,7 @@ from lib.model.smartplugin import * from lib.item import Items from .webif import WebInterface +from datetime import datetime try: import pydotplus @@ -95,6 +96,8 @@ def __init__(self, sh): StateEngineDefaults.suntracking_offset = self.get_parameter_value("lamella_offset") StateEngineDefaults.lamella_open_value = self.get_parameter_value("lamella_open_value") + StateEngineDefaults.plugin_identification = self.get_fullname() + StateEngineDefaults.plugin_version = self.PLUGIN_VERSION StateEngineDefaults.write_to_log(self.logger) StateEngineCurrent.init(self.get_sh()) @@ -228,14 +231,23 @@ def get_graph(self, abitem, graphtype='link'): try: if graphtype == 'link': return ''.format(abitem) - else: + elif abitem.firstrun is None: webif.drawgraph(vis_file) - return '\ - '.format(abitem) + try: + change_timestamp = os.path.getmtime(vis_file) + change_datetime = datetime.fromtimestamp(change_timestamp) + formatted_date = change_datetime.strftime('%H:%M:%S, %d. %B') + except Exception: + formatted_date = "Unbekannt" + return f'
{self.translate("Letzte Aktualisierung:")} {formatted_date}
\ + \ + ' + else: + return '' except pydotplus.graphviz.InvocationException as ex: self.logger.error("Problem getting graph for {}. Error: {}".format(abitem, ex)) return '

Can not show visualization. Most likely GraphViz is not installed.

' \ diff --git a/stateengine/locale.yaml b/stateengine/locale.yaml index 229980725..3e79465ce 100755 --- a/stateengine/locale.yaml +++ b/stateengine/locale.yaml @@ -18,3 +18,12 @@ plugin_translations: 'SE Item': {'de': '=', 'en': '='} 'Detailvisualisierung': {'de': '=', 'en': 'Detailed Visualization'} 'KeineVisualisierung': {'de': 'Visualisierung nicht verfügbar', 'en': 'Visualization not available'} + 'Zoom +': {'de': '=', 'en': '='} + 'Zoom -': {'de': '=', 'en': '='} + 'Zoom Reset': {'de': '=', 'en': '='} + 'Zoom/Pan aktiv': {'de': '=', 'en': 'Zoom/Pan active'} + 'Klicken zum Öffnen des SVG Files': {'de': '=', 'en': 'Click to open the SVG file'} + 'ist noch nicht initialisiert.': {'de': '=', 'en': 'is not initialized yet.'} + 'Die erste Evaluierung ist geplant für:': {'de': '=', 'en': 'The first evaluation is planned for:'} + 'Letzte Aktualisierung:': {'de': '=', 'en': 'Last Update:'} + 'Potenziell Released': {'de': '=', 'en': 'Potential Released'} diff --git a/stateengine/plugin.yaml b/stateengine/plugin.yaml index 377a8b2d2..ee92b2ab8 100755 --- a/stateengine/plugin.yaml +++ b/stateengine/plugin.yaml @@ -539,6 +539,24 @@ item_attributes: is used. ' + se_stateorder: + type: foo + valid_min: 1 + description: + de: 'Position des Zustands bezüglich der Evaluierungsabfolge' + en: 'Position of the state regarding the evaluation order' + description_long: + de: 'Durch diesen Wert wird bestimmt, an welcher Position der Status eingeordnet wird. + Normalerweise ist die Sortierung abhängig von der Angabe der Stati innerhalb des + rules Items. Durch diesen Wert kann die Sortierung zur Laufzeit abgeändert werden. + Der Wert kann als Zahl, item oder eval deklariert werden. + ' + en: 'This value determines the position at which the status is sorted. + Normally, the sorting depends on the specification of the statuses within the + rules item. This value can be used to change the sorting at runtime. + The value can be declared as an int number, item or eval expression. + ' + se_item_suspend_end: type: str description: @@ -735,7 +753,6 @@ item_structs: rules: se_item_lock: ..lock eval_trigger: - - merge_unique* - ..lock lock: @@ -805,8 +822,8 @@ item_structs: remark: Adapt the se_manual_exclude the way you need it #se_manual_include: KNX:* Force manual mode based on source se_manual_exclude: - - database:* - - init:* + - database:.* + - init:.* retrigger: remark: Item to retrigger the rule set evaluation @@ -871,7 +888,6 @@ item_structs: se_suspend_time: item:..settings.suspendduration.seconds eval_trigger: - - merge_unique* - ..manuell suspend: @@ -1002,8 +1018,8 @@ item_structs: remark: Adapt the se_manual_exclude the way you need it #se_manual_include: KNX:* Force manual mode based on source se_manual_exclude: - - database:* - - init:* + - database:.* + - init:.* retrigger: remark: Item to retrigger the rule set evaluation @@ -1116,14 +1132,14 @@ item_structs: type: str visu_acl: rw cache: True - initial_value: '' + initial_value: 'struct:stateengine.state_standard.rules.standard' additionaluse2: remark: set this value to a struct or (relative) state that should be added to the condition sets if suspendvariant is 2 type: str visu_acl: rw cache: True - initial_value: '' + initial_value: 'struct:stateengine.state_standard.rules.standard' suspendduration: remark: duration of suspend mode in minutes (gets converted automatically) @@ -1175,7 +1191,6 @@ item_structs: se_suspend_time: eval:se_eval.get_relative_itemvalue('..settings.suspendvariant.suspendduration{}.seconds'.format(max(0, min(se_eval.get_relative_itemvalue('..settings.suspendvariant'), 2)))) eval_trigger: - - merge_unique* - ..manuell suspend: @@ -1519,10 +1534,9 @@ item_attribute_prefixes: en: 'Definition wether an action should be repeated or not when reentering the same state (deprecated - use se_action instead)' se_order_: - type: int description: - de: 'Definiert die Reihenfolge einer Aktion (veraltet - Nutze stattdessen se_action)' - en: 'Definition of the running order of an action (deprecated - use se_action instead)' + de: 'Definiert die Reihenfolge einer Aktion als Integerzahl (veraltet - Nutze stattdessen se_action)' + en: 'Definition of the running order of an action as integer (deprecated - use se_action instead)' se_manual_: description: diff --git a/stateengine/user_doc.rst b/stateengine/user_doc.rst index 58bf3d5ac..b9cdbf3b1 100755 --- a/stateengine/user_doc.rst +++ b/stateengine/user_doc.rst @@ -55,11 +55,5 @@ Das Webinterface bietet folgende Übersichtsinformationen: :alt: Web Interface Overview :align: center -Ein Klick auf das Lupensymbol in der Visu-Spalte öffnet die Detailansicht. Hier ist zu sehen, welcher Zustand eingenommen werden könnte, welcher aktiv ist und welche Aktionen bei welcher Bedingung ausgeführt werden. - - .. image:: user_doc/assets/webif_stateengine_detail.png - :height: 1656px - :width: 3312px - :scale: 25% - :alt: Web Interface Detail - :align: center +Ein Klick auf das Lupensymbol in der Visu-Spalte öffnet die Detailansicht. Hier ist zu sehen, welcher Zustand eingenommen werden könnte, welcher aktiv ist und welche Aktionen bei welcher Bedingung ausgeführt werden. Ein Beispiel ist in der +Sektion zu finden. diff --git a/stateengine/user_doc/01_allgemein.rst b/stateengine/user_doc/01_allgemein.rst index 896d0992a..bcae88653 100755 --- a/stateengine/user_doc/01_allgemein.rst +++ b/stateengine/user_doc/01_allgemein.rst @@ -69,15 +69,23 @@ Webinterface Über das Webinterface lässt sich auf einen Blick erkennen, welche State Engine sich in welchem Zustand befindet. Zusätzlich ist es möglich, durch Klick auf einen Eintrag die komplette State Engine visuell zu betrachten. Dabei ist folgende Farbkodierung zu beachten: + - grau: wurde nicht evaluiert (weil bereits ein höherrangiger Zustand eingenommen wurde) - grün: aktueller Zustand / ausgeführte Aktion - rot: Bedingungen nicht erfüllt +Innerhalb einer Bedingungsgruppe wird bei evaluierten Zuständen ein rotes X angezeigt, +wenn die Bedingung nicht wahr ist oder ein grünes Häkchen, falls die Bedingung erfüllt ist. + Bei den Aktionen sind die einzelnen Zeilen unter Umständen ebenfalls farbkodiert: + - schwarz: Aktion normal ausgeführt - weiß: Aktion nicht ausgeführt, da Bedingungen nicht erfüllt - grau: Aktion wird erst mit Verzögerung ausgeführt - rot: Fehler in der Konfiguration -.. image:: assets/webinterface.png +Zudem wird hinter ausgeführten Aktionen ein grünes Häkchen angezeigt, hinter nicht ausgeführten +(weil beispielsweise Bedingungen nicht erfüllt sind) ein rotes X und hinter Problemen ein Warnsignal. + +.. image:: assets/webif_stateengine_detail.png :class: screenshot diff --git a/stateengine/user_doc/02_konfiguration.rst b/stateengine/user_doc/02_konfiguration.rst index 90ea0f69f..5ae67824e 100755 --- a/stateengine/user_doc/02_konfiguration.rst +++ b/stateengine/user_doc/02_konfiguration.rst @@ -46,10 +46,11 @@ Logging Es gibt zwei Möglichkeiten, den Output des Plugins zu loggen: **intern** -Hierbei werden, sofern das Loglevel 1 oder 2 beträgt, sämtliche Logeinträge in +Hierbei werden, sofern das Loglevel 1 oder mehr beträgt, sämtliche Logeinträge in eigene Dateien in einem selbst definierten Verzeichnis geschrieben. Das Loglevel kann sowohl global in der etc/plugin.yaml Datei deklariert, als auch individuell pro Item mittels ``se_log_level`` (dort wo auch se_plugin: active steht) überschrieben werden. +Wird im Item nichts angegeben oder das Attribut mit dem Wert -1 angegeben, wird der Standardwert herangezogen. **logging.yaml** Sowohl der Output des Plugins generell, als auch der Einträge für bestimmte Items diff --git a/stateengine/user_doc/03_regelwerk.rst b/stateengine/user_doc/03_regelwerk.rst index ab841d13c..445dc07bb 100755 --- a/stateengine/user_doc/03_regelwerk.rst +++ b/stateengine/user_doc/03_regelwerk.rst @@ -22,7 +22,9 @@ eingebunden werden. Das Item sieht dann folgendermaßen aus: Bei jedem Aufruf des Regelwerk-Items - im Beispiel auf Grund der cycle Angabe also alle 60 Sekunden - werden die Zustände hierarchisch evaluiert. -Zuerst wird also der erste Status getestet. Kann dieser nicht aktiviert werden, +Zuerst wird also der als erstes angegebene Status getestet. Die Reihenfolge ergibt sich +aus der YAML Datei oder alternativ aus dem optionalen Attribut ``se_stateorder``. +Kann der Zustand nicht aktiviert werden, folgt der darunter angegebene, etc. Details hierzu finden sich im nächsten Teil der Dokumentation. @@ -49,14 +51,18 @@ Diese Items müssen auf Ebene des Regelwerk-Items über das Attribut ``se_item_`` bekannt gemacht werden. Um einfacher zwischen Items, die für Bedingungen und solchen, die für Aktionen genutzt werden, unterscheiden zu können, können Items, die nur für Bedingungen gebraucht werden, mittels ``se_status_`` -deklariert werden. Diese Variante ist auch besonders dann relevant, wenn es zwei separate Items -für "Senden" und "Empfangen" gibt, also z.B. Senden der Jalousiehöhe und Empfangen des aktuellen -Werts vom KNX-Aktor. +deklariert werden. + +Anstatt direkt das Item in Form des absoluten oder relativen Pfades zu setzen, kann auch ein +eval-Ausdruck mittels ``eval:`` angegeben werden. -Anstatt direkt das Item in Form des absoluten oder relativen Pfades mittels ``se_item_`` zu -setzen, kann auch die Angabe ``se_eval_`` genutzt werden. In diesem Fall wird eine beliebige -Funktion anstelle des Itemnamen angegeben. Dies ist primär für das Setzen von "dynamischen" Items -gedacht, allerdings ist es auch möglich, hier einen beliebigen Eval-Ausdruck als Bedingung festzulegen. +.. hint:: + + Aus Kompatibilitätsgründen kann für das Setzen "dynamischer" Items auch die Angabe ``se_eval_`` + oder ``se_status_eval_`` genutzt werden. In diesem Fall wird eine beliebige + Funktion anstelle des Itemnamen angegeben, also beispielsweise + se_eval_height: se_eval.get_relative_item('..test'). Hierzu in den Kapiteln :ref:`Bedingungen` + und :ref:`Aktionen` mehr. Beispiel se_item @@ -64,12 +70,12 @@ Beispiel se_item Im Beispiel wird durch ``se_item_height`` das Item ``beispiel.raffstore1.hoehe`` dem Plugin unter dem Namen "height" bekannt gemacht. Das Item ``beispiel.wetterstation.helligkeit`` -wird durch ``se_item_brightness`` (alternativ via ``se_status_brightness``) als "brightness" referenziert. +wird durch ``se_item_brightness`` als "brightness" referenziert. Auf diese Namen beziehen sich nun in weiterer Folge Bedingungen und Aktionen. Im Beispiel wird im Zustand Nacht das Item ``beispiel.raffstore1.hoehe`` auf den Wert 100 gesetzt, sobald -``beispiel.wetterstation.helligkeit`` den Wert 25 übersteigt. Erklärungen zu Bedingungen -und Aktionen folgen auf den nächsten Seiten. +``beispiel.wetterstation.helligkeit`` den Wert 25 übersteigt. Erklärungen zu Bedingungen und +Aktionen folgen auf den nächsten Seiten. .. code-block:: yaml @@ -91,18 +97,41 @@ und Aktionen folgen auf den nächsten Seiten. enter_toodark: se_max_brightness: 25 +Beispiel se_status +================== + +Wie erwähnt, können Items, die nur für Bedingungen genutzt werden, auch mittels se_status deklariert +werden. Diese Variante ist aber auch besonders dann relevant, wenn es zwei separate Items +für "Senden" und "Empfangen" gibt, also z.B. Senden der Jalousiehöhe und Empfangen des aktuellen +Werts vom KNX-Aktor. + +Im Beispiel wird durch ``se_item_height`` das Item ``beispiel.raffstore1.hoehe`` (das den Befehl an den +KNX Aktor übermittelt) dem Plugin unter dem Namen "height" bekannt gemacht. ``se_status_height`` referenziert auf das +separate Status-Item (das vom KNX Aktor den Rückmeldestatus erhält) ``beispiel.raffstore1.hoehe.status``. +Dies ist aktuell insbesondere dann wichtig, wenn `se_mindelta_height`` genutzt wird (siehe :ref:`Aktionen`). + +.. code-block:: yaml + + raffstore1: + automatik: + struct: stateengine.general + rules: + se_item_height: beispiel.raffstore1.hoehe + se_status_height: beispiel.raffstore1.hoehe.status + se_mindelta_height: 10 + + Standard: + on_enter_or_stay: + se_action_height: + - 'function: set' + - 'to: 100' + + Beispiel se_eval ================ -se_eval ist für Sonderfälle und etwas komplexere Konfigurationen sinnvoll, kann aber -im ersten Durchlauf ignoriert werden. Es wird daher empfohlen, als Beginner -dieses Beispiel einfach zu überspringen ;) - -Im Beispiel wird durch ``se_eval_brightness`` das Item für den Check von -Bedingungen bekannt gemacht. Aufgrund der angegebenen Funktion wird das Item -abhängig vom aktuellen Zustandsnamen eruiert. Da Zustand_Eins den Namen "sueden" -hat, wird somit auch der Wert von wetterstation.helligkeit_sueden abgefragt. -Würde der Zustand "osten" heißen, würde der Helligkeitswert vom Osten getestet werden. +Im Beispiel werden zwei Helligkeitswerte addiert und das Resultat durch 2 geteilt +(also der Mittelwert gebildet). Das Resultat wird dann mit dem Wert 5000 verglichen. .. code-block:: yaml @@ -122,7 +151,7 @@ Würde der Zustand "osten" heißen, würde der Helligkeitswert vom Osten geteste automatik: struct: stateengine.general rules: - se_eval_brightness: se_eval.get_relative_itemvalue('wetterstation.helligkeit_{}'.format(se_eval.get_variable('current.state_name'))) + se_eval_brightness: (se_eval.get_relative_itemvalue('wetterstation.helligkeit_sueden') + se_eval.get_relative_itemvalue('wetterstation.helligkeit_osten'))/2 Zustand_Eins: name: sueden diff --git a/stateengine/user_doc/04_zustand.rst b/stateengine/user_doc/04_zustand.rst index 6404227a9..10a3befe0 100755 --- a/stateengine/user_doc/04_zustand.rst +++ b/stateengine/user_doc/04_zustand.rst @@ -11,6 +11,10 @@ Zustände Alle Items unterhalb des Regelwerk-Items (``rules``) beschreiben Zustände des Objekts ("Zustands-Item"). Die Ids der Zustands-Items sind beliebig, im Beispiel ``day``. +Prinzipiell werden die Zustände der Reihe nach, wie sie im YAML +File angegeben wurden evaluiert. Es ist allerdings auch möglich, +die Reihenfolge (selbst zur Laufzeit) mittels ``se_stateorder`` +zu verändern. Dies ist insbesondere für die ersten Tests sinnvoll. .. code-block:: yaml @@ -81,7 +85,7 @@ Zusätzlich können eigene Zustände (beispielsweise day) definiert werden. - stateengine.general - stateengine.state_release - stateengine.state_lock - - stateengine.state_suspend + - stateengine.state_suspend rules: day: diff --git a/stateengine/user_doc/05_bedingungen.rst b/stateengine/user_doc/05_bedingungen.rst index c72b2e312..94335b705 100755 --- a/stateengine/user_doc/05_bedingungen.rst +++ b/stateengine/user_doc/05_bedingungen.rst @@ -26,6 +26,52 @@ die Helligkeit (über se_item_brightness oder se_status_brightness definiert) ü enter: se_min_brightness: 500 +Name der Bedingung +------------------ + +Der Name einer Bedingung setzt sich aus folgenden drei Teilen zusammen, +die jeweils mit einem Unterstrich "_" getrennt werden: + +- ``se_``: eindeutiger Prefix, um dem Plugin zugeordnet zu werden +- ````: siehe unten. Beispiel: min = der Wert des muss mindestens dem beim Attribut angegebenen Wert entsprechen. +- ````: Hier wird entweder das im Regelwerk-Item mittels ``se_item_`` oder ``se_status_`` deklarierte Item oder eine besondere Bedingung (siehe unten) referenziert. + + +Referenzieren von Items +----------------------- + +Für jede "standardmäßige" Bedingung muss ein Item hinterlegt werden, das geprüft werden soll. +Dies geschieht in der Regel durch ``se_status_``, kann aber auch durch ``se_item_`` +erfolgen, falls z.B. das gleiche Item für Bedingungen und Aktionen gebraucht wird. + +Im Beispiel wird durch ``se_status_brightness`` das Item für den Check von +Bedingungen bekannt gemacht. Aufgrund der angegebenen eval-Funktion wird das Item +abhängig vom aktuellen Zustandsnamen eruiert. Da Zustand_Eins den Namen "sueden" +hat, wird somit der Wert von wetterstation.helligkeit_sueden abgefragt. Ist dieser +mehr als 499, ist die Bedingung erfüllt. Würde der Zustand "osten" heißen (Name von Zustand_Zwei), +würde der Helligkeitswert vom Osten getestet werden. Bedingung wäre dann erfüllt, +wenn die Helligkeit 1500 oder mehr beträge. + +.. code-block:: yaml + + #items/item.yaml + raffstore1: + automatik: + struct: stateengine.general + rules: + se_status_brightness: eval:se_eval.get_relative_itemvalue('wetterstation.helligkeit_{}'.format(se_eval.get_variable('current.state_name'))) + + Zustand_Eins: + name: sueden + enter: + se_min_brightness: 500 + + Zustand_Zwei: + name: osten + enter: + se_min_brightness: 1500 + + Bedingungsgruppen ----------------- @@ -60,25 +106,13 @@ Wertevergleich Der zu vergleichende Wert einer Bedingung kann auf folgende Arten definiert werden: - statischer Wert (also z.B. 500 Lux). Wird angegegeben mit ``value:500``, wobei das value: auch weggelassen werden kann. -- Item (beispielsweise ein Item namens settings.helligkeitsschwellwert). Wird angegeben mit ``item:settings.helligkeitsschwellwert`` +- Item (beispielsweise ein Item namens settings.helligkeitsschwellwert). Wird angegeben mit ``item:settings.helligkeitsschwellwert``. Das Item kann auch eine Liste von Werten beinhalten. - Eval-Funktion (siehe auch `eval Ausdrücke `_). Wird angegeben mit ``eval:1*2*se_eval.get_relative_itemvalue('..bla')`` - Regular Expression (siehe auch ` RegEx Howto `_) - Vergleich mittels re.fullmatch, wobei Groß/Kleinschreibung ignoriert wird. Wird angegeben mit ``regex:StateEngine Plugin:(.*)`` - Template: eine Vorlage, z.B. eine eval Funktion, die immer wieder innerhalb des StateEngine Items eingesetzt werden kann. Angegeben durch ``template:`` -Name der Bedingung ------------------- - -Der Name einer Bedingung setzt sich aus folgenden drei Teilen zusammen, -die jeweils mit einem Unterstrich "_" getrennt werden: - -- ``se_``: eindeutiger Prefix, um dem Plugin zugeordnet zu werden -- ````: siehe unten. Beispiel: min = der Wert des muss mindestens dem beim Attribut angegebenen Wert entsprechen. -- ````: Hier wird entweder das im Regelwerk-Item mittels ``se_item_`` -oder ``se_status_`` deklarierte Item oder eine besondere Bedingung (siehe unten) referenziert. - - Templates für Bedingungsabfragen -------------------------------- @@ -352,7 +386,7 @@ Freitag, 5 = Samstag, 6 = Sonntag Der Azimut (Horizontalwinkel) ist die Kompassrichtung, in der die Sonne steht. Der Azimut wird von smarthomeNg auf Basis der aktuellen Zeit sowie der konfigurierten geographischen Position -berechnet. Siehe auch `Dokumentation `_ +berechnet. Siehe auch `Dokumentation `_ für Voraussetzungen zur Berechnung der Sonnenposition. Beispielwerte: 0 → Sonne exakt im Norden, 90 → Sonne exakt im Osten, 180 → Sonne exakt im Süden, 270 → Sonne exakt im Westen @@ -363,8 +397,8 @@ Osten, 180 → Sonne exakt im Süden, 270 → Sonne exakt im Westen Die Altitude (Vertikalwikel) ist der Winkel, in dem die Sonne über dem Horizont steht. Die Altitude wird von smarthomeNG auf Basis der aktuellen Zeit sowie der konfigurierten geographischen -Position berechnet. Siehe auch `SmarthomeNG -Dokumentation `_ +Position berechnet. Siehe ebenfalls `SmarthomeNG +Dokumentation `_ für Voraussetzungen zur Berechnung der Sonnenposition. Werte: negativ → Sonne unterhalb des Horizonts, 0 → Sonnenaufgang/Sonnenuntergang, 90 → Sonne exakt im Zenith @@ -394,7 +428,7 @@ verwendet werden. Die Abfrage se_value_laststate ist besonders wichtig für Bedingungsabfragen, die über das Verbleiben im aktuellen Zustand -bestimmen (z.b. enter_stay). So können aber auch Stati übersprungen +bestimmen (z.b. enter_stay). So können aber auch Zustände übersprungen werden, wenn sie nicht nach einem bestimmten anderen Zustand aktiviert werden sollen. Wichtig: Hier muss die vollständige Item-Id angegeben werden diff --git a/stateengine/user_doc/06_aktionen.rst b/stateengine/user_doc/06_aktionen.rst index 0182d969b..55d6b187c 100755 --- a/stateengine/user_doc/06_aktionen.rst +++ b/stateengine/user_doc/06_aktionen.rst @@ -9,8 +9,7 @@ Aktionen Es gibt zwei Möglichkeiten, Aktionen zu definieren. Die :ref:`Aktionen - einzeln` Variante wird am Ende der Dokumentation der Vollständigkeit halber beschrieben. Für einfache Aktionen ohne Angabe zusätzlicher Attribute wie delay, order, repeat, etc. -kann diese andere Möglichkeit der Aktionsangabe durchaus Sinn machen. Sie wurde -allerdings in der weiteren Pluginentwicklung nicht mehr getestet. +kann diese andere Möglichkeit der Aktionsangabe durchaus Sinn machen. Bei der hier beschriebenen kombinierten Variante zur Definition von Aktionen werden alle Parameter einer Aktion in einem Attribut definiert. Der Aktionsname ``se_action_`` @@ -29,7 +28,7 @@ das den Wert vom KNX-Aktor empfängt. Außerdem ist es möglich, über ``se_repeat_actions`` generell zu definieren, ob Aktionen für die Stateengine wiederholt ausgeführt werden sollen oder nicht. Diese Konfiguration -kann für einzelne Aktionen individuell über die Angabe ``repeat`` überschrieben werden. Siehe auch :ref:`Aktionen`. +kann für einzelne Aktionen individuell über die Angabe ``repeat`` überschrieben werden. Beispiel zu Aktionen -------------------- @@ -346,20 +345,20 @@ Die einzelnen Angaben einer Liste werden als ``OR`` evaluiert. .. code-block:: yaml -screens: - conditionset_to_check: - type: str - value: "screens.osten_s1.automatik.rules.abend.enter_abend" + screens: + conditionset_to_check: + type: str + value: "screens.osten_s1.automatik.rules.abend.enter_abend" - conditionset: - - regex:enter_(.*)_test - - eval:sh.screens.conditionset_to_check.property.name + conditionset: + - regex:enter_(.*)_test + - eval:sh.screens.conditionset_to_check.property.name Der gesamte Pfad könnte wie folgt evaluiert werden: .. code-block:: yaml - "eval:se_eval.get_relative_itemid('{}.'.format(se_eval.get_relative_itemvalue('..state_id')))" + "eval:se_eval.get_relative_itemid('{}.'.format(se_eval.get_relative_itemvalue('..state_id')))" Eine sinnvolle Anwendung hierfür wäre, anstelle von verschiedenen Zuständen mit leicht anderen Bedingungen, alles in einen Zustand zu packen und anhand des Conditionsets diff --git a/stateengine/user_doc/07_zeitpunkt.rst b/stateengine/user_doc/07_zeitpunkt.rst index dadb59360..98774cb2e 100755 --- a/stateengine/user_doc/07_zeitpunkt.rst +++ b/stateengine/user_doc/07_zeitpunkt.rst @@ -52,7 +52,7 @@ Die Konfiguration von instant_leaveaction bestimmt, ob on_leave Aktionen sofort eines Zustands ausgeführt werden oder erst am Ende der Statusevaluierung. Die Option kann sowohl in der globalen Pluginkonfiguration mittels ``instant_leaveaction`` (boolscher Wert True oder False), als auch pro Item -mittels ``se_instant_leaveaction``festgelegt werden. Letzteres Attribut kann auch +mittels ``se_instant_leaveaction`` festgelegt werden. Letzteres Attribut kann auch auf ein Item verweisen, dem der Wert -1 = Nutzen des Default Wertes, 0 = False, 1 = True zugewiesen werden kann. Im ``general struct`` sind bereits entsprechende Einträge und Items angelegt (mit einem Wert von -1). diff --git a/stateengine/user_doc/08_beispiel.rst b/stateengine/user_doc/08_beispiel.rst index c4767ab0b..118983b21 100755 --- a/stateengine/user_doc/08_beispiel.rst +++ b/stateengine/user_doc/08_beispiel.rst @@ -390,14 +390,14 @@ Beim zweiten Durchlauf wird somit der Zustand Sonnenschutz aktiviert. Der Raffst Let's play god. Ändern wir das Wetter ;) Entweder über das CLI, Visu oder Backend-Plugin oder Admin-Interface: -c) up beispiel.wetterstation.helligkeit=35000 +c) beispiel.wetterstation.helligkeit=35000 - Die erste Bedingungsgruppe des Sonnenstandzustands ist nicht mehr "wahr", da die Helligkeit zu niedrig ist. - Es wird ``enter_hysterese`` evaluiert. Da die Helligkeit noch über 25000 und die Sonnenposition gleich wie zuvor ist, ist diese Gruppe wahr. Der Sonnenschutz bleibt somit aktiv, weil trotz der Helligkeitsverringerung der untere Schwellwert noch überschritten wurde. Der Raffstore bleibt unten. -d) up beispiel.wetterstation.helligkeit=15000 +d) beispiel.wetterstation.helligkeit=15000 - Die ersten beiden Bedingungsgruppen sind unwahr, da die Helligkeit zu gering ist. - Durch den Eintrag ``se_agemax_brightnessGt25k: 60`` in der Gruppe ``enter_delay`` wird 60 Sekunden gewartet. @@ -411,13 +411,13 @@ e) Es erfolgt eine weitere Evaluierung des Automaten durch das cycle Attribut: Der Zustand wird verlassen. Gibt es einen nachfolgenden Zustand, der eingenommen werden kann, ist dies der neue aktive Zustand. Gibt es keine Zustände, die aktiviert werden könnten, verbleibt die State Engine beim letzten aktiven Zustand, also beim Sonnenschutz. Im Beispiel gibt es noch einen Standard "Tag" Eintrag, wodurch der Raffstore hoch fährt. -f) up beispiel.raffstore1.aufab = 1 +f) beispiel.raffstore1.aufab = 1 - Durch Triggern des "Manuell" Items wird die Zustandsevaluierung pausiert. Sämtliche Änderungen der Helligkeit, Temperatur, etc. werden für die suspend_time ignoriert. Die Dauer ist im Template auf 60 Minuten festgelegt, kann aber manuell durch Ändern des entsprechenden Items geändert werden. -g) up beispiel.raffstore1.automatik.settings.suspendduration = 1 +g) beispiel.raffstore1.automatik.settings.suspendduration = 1 - Die Suspendzeit wird auf eine Minute verkürzt. - Beim erneuten Durchlauf ist die Suspendzeit abgelaufen, daher dieser Zustand nicht mehr aktiv. @@ -639,9 +639,9 @@ Settings für Itemwerte ---------------------- Das Setup ist besonders flexibel, wenn zu setzende Werte nicht fix in den Zustandsvorgaben -definiert werden, sondern in eigenen Items, die dann jederzeit zur Laufzeit abänderbar +definiert werden, sondern in eigenen Items, die dann jederzeit zur Laufzeit änderbar sind. Das folgende Beispiel zeigt eine Leuchte, die abhängig vom aktuell definierten -Lichtmodus (z.B. über die Visu) verschiedene Stati einnimmt und immer wieder dieselben +Lichtmodus (z.B. über die Visu) verschiedene Zustände einnimmt und immer wieder dieselben Änderungen vornimmt. Sollte eine Änderung nicht möglich sein, weil das entsprechende Item nicht existiert, wird das Plugin die Aktion einfach ignorieren. @@ -649,8 +649,6 @@ Die Struct-Vorlagen sehen dabei folgendermaßen aus. Besonders ist der Eval Ausd Dieser führt dazu, dass der zu setzende Wert aus dem Item ``automatik.settings..sollwert`` im aktuellen Item gelesen wird. Somit kann diese Vorlage für sämtliche Zustände 1:1 eingesetzt werden, wobei natürlich zu beachten ist, dass sowohl "Settings" als auch Zustand richtig benannt sind. -Das Item state_name wird bis zur Pluginversion 1.5.0 erst nach Ausführen der Aktionen aktualisiert, -weshalb diese Vorgehensweise erst ab 1.5.1 empfohlen wird. .. code-block:: yaml @@ -758,7 +756,7 @@ Letzten Endes wird alles in einem item.yaml auf folgende Art und Weise implement - licht_rules_heimkino - licht_rules_lichtkurve - remark: Das eval_trigger muss vor SmarthomeNG 1.7 noch manuell mit der kompletten Liste überschrieben werden, auch wenn die Structs bereits Einträge enthalten. Ab 1.7 würde licht.modus* ausreichen! + remark: Das eval_trigger muss vor SmarthomeNG 1.7 noch manuell mit der kompletten Liste überschrieben werden, auch wenn die Structs bereits Einträge enthalten. Ab 1.7 würde merge_unique* und licht.modus* ausreichen! eval_trigger: - ..settings_edited - ..lock diff --git a/stateengine/user_doc/09_vorlagen.rst b/stateengine/user_doc/09_vorlagen.rst index bd72ebd68..cf90e8e35 100755 --- a/stateengine/user_doc/09_vorlagen.rst +++ b/stateengine/user_doc/09_vorlagen.rst @@ -43,9 +43,7 @@ können wie folgt eingebunden werden: rules: eval_trigger: - - ..lock - - ..supsend - - .. release + - merge_unique* - beispiel.trigger additional_state1: @@ -63,81 +61,17 @@ general Die ``general`` Vorlage enthält die Items, die generell für einen Zustandsautomaten angelegt werden sollten. Das "rules" Item ist das Regelwerk-Item mit aktiviertem -se_plugin. Dieser Codeblock wird zwingend von jedem Zustandsautomaten benötigt. - -.. code-block:: yaml - - #stateengine.general - state_id: - # The id/path of the actual state is assigned to this item by the stateengine - type: str - visu_acl: r - cache: True - - state_name: - # The name of the actual state is assigned to this item by the stateengine - type: str - visu_acl: r - cache: True - - conditionset_id: - remark: The id/path of the actual condition set is assigned to this item by the stateengine - type: str - visu_acl: r - cache: True - - conditionset_name: - remark: The name of the actual condition set is assigned to this item by the stateengine - type: str - visu_acl: r - cache: True - - rules: - name: Regeln und Item Verweise für den Zustandsautomaten - type: bool - se_plugin: active - eval: True - - # se_startup_delay: 30 - # se_repeat_actions: true - # se_suspend_time: 7200 - - se_laststate_item_id: ..state_id - se_laststate_item_name: ..state_name - se_lastconditionset_item_id: ..conditionset_id - se_lastconditionset_item_name: ..conditionset_name +se_plugin. Außerdem werden zwei Settings Items angelegt, um das Log Level und +"Instant Leaveaction" per Item konfigurieren und zur Laufzeit ändern zu können. +Dieser Codeblock wird zwingend von jedem Zustandsautomaten benötigt. lock ==== Die ``state_lock`` Vorlage beinhaltet zum einen den Lock Zustand mit dem Namen "gesperrt", zum anderen ein Item mit dem Namen ``lock``. Wird dieses auf "1/True" gesetzt, wird der -Zustand eingenommen. Der Zustand sollte immer als erster Zustand eingebunden werden. - -.. code-block:: yaml - - #stateengine.state_lock - lock: - type: bool - knx_dpt: 1 - visu_acl: rw - cache: 'on' - - rules: - se_item_lock: ..lock - eval_trigger: - - ..lock - - lock: - name: gesperrt - - on_leave: - se_action_lock: - - 'function: set' - - 'to: False' - - enter: - se_value_lock: True +Zustand so lange eingenommen, bis das Item wieder auf False gestellt wird. So lässt sich zeitweise +die Evaluierung anderer Zustände pausieren. Der Zustand sollte immer als erster Zustand eingebunden werden. suspend ======= @@ -162,198 +96,19 @@ Setzt man das Item ``settings.suspend_active`` auf False, wird der Pause-Zustand deaktiviert und manuelle Betätigungen werden beim nächsten Durchlauf eventuell durch andere Zustände überschrieben. -.. code-block:: yaml +suspend_dynamic +=============== - #stateengine.state_suspend - state_suspend: - name: Zustandsvorlage für manuelles Aussetzen - - suspend: - type: bool - knx_dpt: 1 - visu_acl: rw - cache: True - - visu: - type: bool - knx_dpt: 1 - visu_acl: rw - cache: True - - suspend_end: - type: str - visu_acl: ro - eval: "'' if not any(char.isdigit() for char in sh..self.date_time()) else sh..self.date_time().split(' ')[1].split('.')[0]" - eval_trigger: .date_time - crontab: init - - date_time: - type: str - visu_acl: ro - cache: True - - unix_timestamp: - type: num - visu_acl: ro - eval: "0 if not any(char.isdigit() for char in sh...date_time()) else sh.tools.dt2ts(shtime.datetime_transform(sh...date_time())) * 1000" - eval_trigger: ..date_time - crontab: init - - suspend_start: - type: str - visu_acl: ro - eval: "'' if not any(char.isdigit() for char in sh..self.date_time()) else sh..self.date_time().split(' ')[1].split('.')[0]" - eval_trigger: .date_time - crontab: init - - date_time: - type: str - visu_acl: ro - cache: True - - unix_timestamp: - remark: Can be used for the clock.countdown widget - type: num - visu_acl: ro - eval: "0 if not any(char.isdigit() for char in sh...date_time()) else sh.tools.dt2ts(shtime.datetime_transform(sh...date_time())) * 1000" - eval_trigger: ..date_time - crontab: init - - manuell: - type: bool - name: manuell - se_manual_invert: True - remark: Adapt the se_manual_exclude the way you need it - #se_manual_include: KNX:* Force manual mode based on source - se_manual_exclude: - - database:* - - init:* - - retrigger: - remark: Item to retrigger the rule set evaluation - type: bool - visu_acl: rw - enforce_updates: True - on_update: ..rules = True - - settings: - remark: Use these settings for your condition values - type: foo - eval: (sh..suspendduration(sh..suspendduration(), "Init", "Start"), sh..suspendvariant.suspendduration0(sh..suspendduration(), "Init", "Start"), sh..suspendvariant.suspendduration1(sh..suspendvariant.suspendduration1(), "Init", "Start"), sh..suspendvariant.suspendduration2(sh..suspendvariant.suspendduration2(), "Init", "Start")) - crontab: init = True - - suspendduration: - remark: duration of suspend mode in minutes (gets converted automatically) - type: num - visu_acl: rw - cache: True - initial_value: 60 - on_change: .seconds = value * 60 if not sh..self.property.last_change_by == "On_Change:{}".format(sh..seconds.property.path) else None - on_update: .seconds = value * 60 if "Init" in sh..self.property.last_update_by else None - - duration_format: - remark: Can be used for the clock.countdown widget - type: str - cache: True - visu_acl: ro - eval: "'{}d {}h {}i {}s'.format(int(sh...seconds()//86400), int((sh...seconds()%86400)//3600), int((sh...seconds()%3600)//60), round((sh...seconds()%3600)%60))" - eval_trigger: - - ..seconds - - .. - - seconds: - remark: duration of suspend mode in seconds (gets converted automatically) - type: num - visu_acl: rw - cache: True - on_change: .. = value / 60 if not sh..self.property.last_change_by in [ "On_Change:{}".format(sh....property.path), "On_Update:{}".format(sh....property.path)] else None - - suspend_active: - remark: Use this to (de)activate suspend mode in general - type: bool - visu_acl: rw - cache: True - initial_value: True - - settings_edited: - type: bool - name: settings editiert - eval_trigger: ...settings.* - eval: not sh..self() - on_update: ...retrigger = True if sh..self.property.prev_update_age > 0.1 else None - - rules: - se_item_suspend: ..suspend - se_item_suspend_visu: ..suspend.visu - se_item_suspend_end: ..suspend_end.date_time - se_item_suspend_start: ..suspend_start.date_time - se_item_suspend_active: ..settings.suspend_active - se_suspend_time: ..settings.suspendduration - - eval_trigger: - - ..manuell - - suspend: - name: ausgesetzt - - on_enter: - se_action_suspend_visu: - - 'function: set' - - 'to: True' - - 'order: 2' - - on_enter_or_stay: - se_action_suspend: - - 'function: special' - - 'value: suspend:..suspend, ..manuell' - - 'repeat: True' - - 'order: 1' - se_action_suspend_end: - - 'function: set' - - "to: eval:se_eval.insert_suspend_time('..suspend', suspend_text='%Y-%m-%d %H:%M:%S.%f%z')" - - 'repeat: True' - - 'order: 3' - se_action_suspend_start: - - 'function: set' - - "to: eval:str(shtime.now())" - - 'repeat: True' - - 'conditionset: enter_manuell' - - 'order: 4' - se_action_retrigger: - - 'function: special' - - 'value: retrigger:..retrigger' - - 'delay: var:item.suspend_remaining' - - 'repeat: True' - - 'order: 5' - - on_leave: - se_action_suspend: - - 'function: set' - - 'to: False' - - 'order: 2' - se_action_suspend_visu: - - 'function: set' - - 'to: False' - - 'order: 3' - se_action_suspend_end: - - 'function: set' - - 'to: ' - - 'order: 4' - se_action_suspend_start: - - 'function: set' - - 'to: ' - - 'order: 5' - - 'delay: 1' - - enter_manuell: - se_value_trigger_source: eval:se_eval.get_relative_itemproperty('..manuell', 'path') - se_value_suspend_active: True - - enter_stay: - se_value_laststate: var:current.state_id - se_agemax_suspend: var:item.suspend_time - se_value_suspend: True - se_value_suspend_active: True +Eine Variante des Suspendmodus, bei dem es möglich ist, bis zu drei verschiedene +Suspendzeiten zu deklarieren. Außerdem kann man definieren, ob noch zusätzliche Zustände +integriert werden sollen. Dabei ist zu beachten, dass standardmäßig der "Standard"-Status +mit eingebunden wird. Da dieser leer ist, wird nichts passieren. Bei Bedarf kann der Wert +in den Items ``automatik.settings.suspendvariant.additionaluse[0-2]`` geändert werden. + +Welche Zeiten und Zustände letztlich genutzt werden, wird durch Setzen des Items +``suspendvariant`` bestimmt. Der Wert muss zwischen 0 und 2 liegen. + +Weitere Informationen sind unter :ref:`Besondere Zustände` zu finden. release ======= @@ -362,54 +117,11 @@ Die ``state_release`` Vorlage ist nicht unbedingt nötig, kann aber dazu genutzt schnell den Sperr- oder Pause-Zustand zu verlassen und die erneute Evaluierung der Zustände anzuleiern. -.. code-block:: yaml - - #stateengine.state_release - release: #triggers the release - type: bool - knx_dpt: 1 - visu_acl: rw - enforce_updates: True - - rules: - se_item_lock: ..lock - se_item_suspend: ..suspend - se_item_retrigger: ..rules - se_item_release: ..release - se_item_suspend_end: ..suspend_end - eval_trigger: - - ..release - - release: - name: release - - on_enter_or_stay: - se_action_suspend: - - 'function: set' - - 'to: False' - - 'order: 1' - se_action_lock: - - 'function: set' - - 'to: False' - - 'order: 2' - se_action_release: - - 'function: set' - - 'to: False' - - 'order: 3' - se_action_suspend_end: - - 'function: set' - - 'to: ' - - 'order: 4' - se_action_retrigger: - - 'function: set' - - 'to: True' - - 'order: 5' - - 'repeat: True' - - 'delay: 1' - - enter: - se_value_release: True +standard +======== +Ein praktisch leerer Status, der immer am Ende angehängt werden sollte. Dieser Status wird +eingenommen, wenn keine Bedingungen der anderen Zustände erfüllt sind. Pluginspezifische Templates --------------------------- diff --git a/stateengine/user_doc/11_sonderzustaende.rst b/stateengine/user_doc/11_sonderzustaende.rst index c59f12456..bda77918c 100755 --- a/stateengine/user_doc/11_sonderzustaende.rst +++ b/stateengine/user_doc/11_sonderzustaende.rst @@ -284,8 +284,37 @@ abweichend sein soll, kann dort das Attribut angegeben werden. Der Parameter kann auch durch ein Item oder eval festgelegt werden. Letzteres ermöglicht es, je nach Situation die Suspenddauer von verschiedenen Items -abhängig zu machen. Im struct ``state_suspend_dynamic`` wird hier das Item automatik.settings.suspendduration.seconds verknüpft bzw. -für die verschiedenen "suspendvariants" automatik.settings.suspendvariant.suspendduration[0-2].seconds. +abhängig zu machen. Im struct ``state_suspend_dynamic`` wird hier das +Item automatik.settings.suspendduration.seconds verknüpft bzw. +für die verschiedenen "suspendvariants" die Items automatik.settings.suspendvariant.suspendduration[0-2].seconds. Hierzu ist im struct ein Item settings.suspendvariant integriert, das einen numerischen Wert zwischen 0 und 2 erwartet. 0 ist dabei die "normale" Funktionsweise, eine 1 würde auf die duration1 und eine 2 auf die duration2 verweisen. + +Um diese unterschiedlichen Dauerangaben zu nutzen, ist der Wert von suspendvariant in den entsprechenden +Zuständen zu setzen. Außerdem sollte beim Beenden des Suspendstatus der Wert wieder auf 0 oder den +vorherigen Wert gesetzt werden (was im entsprechenden Struct auch passiert). + +.. code-block:: yaml + + #items/item.yaml + beispiel: + raffstore1: + automatik: + struct: + - stateengine.general + - stateengine.state_release + - stateengine.state_lock + - stateengine.state_suspend_dynamic + - beschattung_se_state_abend + - beschattung_se_state_nacht + - beschattung_se_state_schnee + - beschattung_se_state_standard + + rules: + nacht: + on_leave: + se_set_suspendvariant: 1 + schnee: + on_leave: + se_set_suspendvariant: 2 diff --git a/stateengine/user_doc/12_aktioneneinzeln.rst b/stateengine/user_doc/12_aktioneneinzeln.rst index 62a88a809..9363459ae 100755 --- a/stateengine/user_doc/12_aktioneneinzeln.rst +++ b/stateengine/user_doc/12_aktioneneinzeln.rst @@ -46,12 +46,12 @@ Einziger Unterschied ist, dass die Wertänderung erzwungen wird: Wenn das Item bereits den zu setzenden Wert hat, dann ändert smarthomeNG das Item nicht. Selbst wenn beim Item das Attribut ``enforce_updates: yes`` gesetzt ist, wird zwar der Wert neu -gesetzt, der von smarthomeNG die Änderungszeit nicht neu gesetzt. Mit +gesetzt, aber nicht die Änderungszeit. Mit dem Attribut ``se_force_`` wird das Plugin den Wert des Items bei Bedarf zuerst auf einen anderen Wert ändern und dann auf dem Zielwert setzen. Damit erfolgt auf jeden Fall eine Wertänderung (ggf. sogar zwei) mit allen damit in Zusammenhang -stehenden Änderungen (eval's, Aktualisierung der Änderungszeiten, +stehenden Änderungen (evals, Aktualisierung der Änderungszeiten, etc). **Aktion run: Ausführen einer Funktion** @@ -164,7 +164,7 @@ Aktion ausgeführt werden soll. Die Angabe erfolgt in Sekunden oder mit dem Suffix "m" in Minuten. Der Timer zur Ausführung der Aktion nach der angegebenen -Verzögerung wird entfernt, wenn eine gleichartike Aktion +Verzögerung wird entfernt, wenn eine gleichartige Aktion ausgeführt werden soll (egal ob verzögert oder nicht). Wenn also die Verzögerung größer als der ``cycle`` ist, wird die Aktion nie durchgeführt werden, es sei denn die Aktion soll nur diff --git a/stateengine/user_doc/13_sonstiges.rst b/stateengine/user_doc/13_sonstiges.rst index 69e5768e5..44d6b2731 100755 --- a/stateengine/user_doc/13_sonstiges.rst +++ b/stateengine/user_doc/13_sonstiges.rst @@ -20,7 +20,9 @@ Seit Version 1.8 wird se_use gleich behandelt wie andere Plugin spezifische Attr Dadurch ist es nicht nur möglich, eine Liste von einzubindenden Zuständen zu deklarieren, sondern auch auf die verschiedenen Schlüsselwörter zurückzugreifen: -- item: liest den Wert aus gegebenem Item aus und nutzt diesen als Zustandserweiterung +- item: liest den Wert aus gegebenem Item aus und nutzt diesen als Zustandserweiterung. Es ist möglich, + mehrere Zustände als Liste im Item zu deklarieren, indem entweder ein Item mit Typ "list" referenziert wird + oder mehrere Einträge in einem "str" Item durch ein Komma getrennt angegeben werden. - eval: ermöglicht das dynamische Erweitern des Zustands, z.B. abhängig von einem vorigen Zustand, etc. - value: sucht das angegebene Item und bindet dieses ein. Der Wert kann auch als relativer Pfad angegeben werden. Hierbei ist zu beachten, dass die relative Adressierung @@ -38,6 +40,21 @@ Heißt, etwaige Zustandseinstellungen im eigentlichen Item erweitern und Weitere Details sind unter :ref:`Zustand-Templates` zu finden. + +Neukonfiguration der Hierarchie +------------------------------- + +**se_stateorder (optional):** +*Festlegen der Reihenfolge eines Status in der Hierarchiefolge* + +Dieses Attribut ermöglicht es, die hierarchische Reihenfolge, die sich ursprünglich +aus der YAML Datei ergibt, anzupassen. Dies kann durch folgende Schlüsselwörter geschehen: + +- item: liest den Wert aus gegebenem Item aus. Das Item sollte einen integer Wert beinhalten. +- eval: ermöglicht die hierarchische Neuordnung mittels Eval-Ausdruck +- value: Angabe der hierarchischen Position durch Angabe eines Integer-Werts (min. 0) + + Auflösen von Zuständen ---------------------- @@ -50,9 +67,9 @@ eingenommen werden könnten. Gewünscht wird dies normalerweise beim Suspendzustand, allerdings kann das Attribut bei jedem beliebigem Zustand genutzt werden. Seit Version 2.0 wird se_released_by gleich behandelt wie andere Plugin spezifische Attribute mit Wertzuweisung, es können -also alle gültigen Schlüsselwörter genutzt werden. Außerdem wurde in -der Pluginversion 2.0 das released_by Feature komplett überarbeitet, sodass -es nun zuverlässig funktionieren sollte ;) +also alle gültigen Schlüsselwörter und auch Listenangaben in Items genutzt werden. +Außerdem wurde in der Pluginversion 2.0 das released_by Feature komplett überarbeitet, +sodass es nun zuverlässig funktionieren sollte ;) Ein Zustand mit diesem Attribut wird aufgelöst, also (vorerst) nicht mehr eingenommen, sobald ein mit dem diff --git a/stateengine/user_doc/assets/webif_stateengine_detail.png b/stateengine/user_doc/assets/webif_stateengine_detail.png old mode 100755 new mode 100644 index a3dccc55d..817b27003 Binary files a/stateengine/user_doc/assets/webif_stateengine_detail.png and b/stateengine/user_doc/assets/webif_stateengine_detail.png differ diff --git a/stateengine/user_doc/assets/webinterface.png b/stateengine/user_doc/assets/webinterface.png deleted file mode 100755 index 5427b61c0..000000000 Binary files a/stateengine/user_doc/assets/webinterface.png and /dev/null differ diff --git a/stateengine/webif/__init__.py b/stateengine/webif/__init__.py index de8ba7332..b53855a6e 100755 --- a/stateengine/webif/__init__.py +++ b/stateengine/webif/__init__.py @@ -84,7 +84,7 @@ def index(self, action=None, item_id=None, item_path=None, reload=None, abitem=N if self.vis_enabled: self.plugin.get_graph(abitem, 'graph') tmpl = self.tplenv.get_template('visu.html') - return tmpl.render(p=self.plugin, item=abitem, + return tmpl.render(p=self.plugin, item=abitem, firstrun=str(abitem.firstrun), language=self.plugin.get_sh().get_defaultlanguage(), now=self.plugin.shtime.now()) # add values to be passed to the Jinja2 template eg: tmpl.render(p=self.plugin, interface=interface, ...) return tmpl.render(p=self.plugin, @@ -107,13 +107,32 @@ def get_data_html(self, dataSet=None): # get the new data data = {} for item in self.plugin.get_items(): + laststate = item.laststate_name + laststate = "-" if laststate in ["", None] else laststate conditionset = item.lastconditionset_name - conditionset = "-" if conditionset == "" else conditionset - log_level = str(item.logger.log_level) - data.update({item.id: {'laststate': item.laststate_name, - 'lastconditionset': conditionset, 'log_level': log_level}}) + conditionset = "-" if conditionset in ["", None] else conditionset + ll = item.logger.log_level_as_num + if item.laststate_releasedby in [None, []]: + lsr = "-" + else: + lsr = [entry.split('.')[-1] for entry in item.laststate_releasedby] + data.update({item.id: {'laststate': laststate, + 'lastconditionset': conditionset, 'log_level': ll, + 'laststate_releasedby': lsr}}) try: return json.dumps(data) except Exception as e: self.logger.error(f"get_data_html exception: {e}") - return {} + elif dataSet and isinstance(dataSet, str): + try: + dataSet = self.plugin.abitems[dataSet] + except Exception as e: + self.logger.warning("Item {} not initialized yet. " + "Try again later. Error: {}".format(dataSet, e)) + return json.dumps({"success": "error"}) + if self.vis_enabled and dataSet.firstrun is None: + self.plugin.get_graph(dataSet, 'graph') + return json.dumps({"success": "true"}) + return json.dumps({"success": "false"}) + else: + return {} diff --git a/stateengine/webif/static/img/visualisations/sign_delay.png b/stateengine/webif/static/img/visualisations/sign_delay.png new file mode 100644 index 000000000..eaa0cda82 Binary files /dev/null and b/stateengine/webif/static/img/visualisations/sign_delay.png differ diff --git a/stateengine/webif/static/img/visualisations/sign_false.png b/stateengine/webif/static/img/visualisations/sign_false.png index 6d6433cc1..8f86cde95 100644 Binary files a/stateengine/webif/static/img/visualisations/sign_false.png and b/stateengine/webif/static/img/visualisations/sign_false.png differ diff --git a/stateengine/webif/static/img/visualisations/sign_scheduled.png b/stateengine/webif/static/img/visualisations/sign_scheduled.png new file mode 100644 index 000000000..a59cc00b7 Binary files /dev/null and b/stateengine/webif/static/img/visualisations/sign_scheduled.png differ diff --git a/stateengine/webif/static/img/visualisations/sign_true.png b/stateengine/webif/static/img/visualisations/sign_true.png index c77f8557e..26ff619ee 100644 Binary files a/stateengine/webif/static/img/visualisations/sign_true.png and b/stateengine/webif/static/img/visualisations/sign_true.png differ diff --git a/stateengine/webif/static/img/visualisations/sign_warn.png b/stateengine/webif/static/img/visualisations/sign_warn.png index e58ab170e..3084bec58 100644 Binary files a/stateengine/webif/static/img/visualisations/sign_warn.png and b/stateengine/webif/static/img/visualisations/sign_warn.png differ diff --git a/stateengine/webif/static/panzoom.min.js b/stateengine/webif/static/panzoom.min.js new file mode 100644 index 000000000..8d3ad220b --- /dev/null +++ b/stateengine/webif/static/panzoom.min.js @@ -0,0 +1,6 @@ +/** +* Panzoom for panning and zooming elements using CSS transforms +* Copyright Timmy Willison and other contributors +* https://github.com/timmywil/panzoom/blob/main/MIT-License.txt +*/ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Panzoom=e()}(this,function(){"use strict";var Y=function(){return(Y=Object.assign||function(t){for(var e,n=1,o=arguments.length;n - + + + + + +{% endblock pluginscripts %} +{% set update_interval = 0 %} {% set logo_frame = false %} +{% set dataSet = item %} {% set tab1title = "" ~ _('Visualisierung') ~ "" %} {% set tabcount = 1 %} {% block bodytab1 %} - -
-
+{% if firstrun == 'None' %} +
{{ _('Detailvisualisierung') }} - {{ item }} - {{ _('Klicken für volle Größe') }} - {{ p.get_graph(item, 'graph') }} + {{ _('Klicken zum Öffnen des SVG Files') }} +
+ Mittels Buttons und Slider kann jederzeit gezoomt werden. Ist die Zoom-Funktion aktiviert, + kann zusätzlich bei Halten der "Shift"-Taste mittels Mausrad in der Grafik gezoomt werden. + Die linke Maustaste ermöglicht dann auch ein Verschieben des Ausschnitts. Dabei werden + allerdings die Tooltips nicht angezeigt - hierfür ist die Zoom-Funktion zu deaktivieren. + +
+
+ + + + + {{_('Zoom/Pan aktiv')}} +
+ +
+ + {{ p.get_graph(item, 'graph') }} +
+{% else %} +
+ {{ item }} {{ _('ist noch nicht initialisiert.') }} {{ _('Die erste Evaluierung ist geplant für:') }} {{ firstrun }}
-
+
+ +
+{% endif %} + {% endblock bodytab1 %} diff --git a/tasmota/user_doc.rst b/tasmota/user_doc.rst index 6cda64164..10523867b 100755 --- a/tasmota/user_doc.rst +++ b/tasmota/user_doc.rst @@ -90,30 +90,34 @@ Dabei definiert - ``tasmota_sml_attr`` den Namen des Keys aus dem Werte-Dictionary, dass dem Item zugewiesen werden soll. Die/Eine MQTT Message zum Beispiel oben. + .. code-block:: text + ``tele/tasmota_sml2mqtt/SENSOR = {"Time":"2023-01-27T17:20:45","MT631":{"Total_in":0001.000}}`` Den Namen des SML-Devices (hier MT631), die Keys für das gelieferte Dictionary (Zuweisung des Werte) etc. wird direkt im Tasmota-Script zum Konfiguration des SML-Devices definiert. - .. code-block:: text - >D - >B +.. code-block:: text - =>sensor53 r - >M 1 - +1,3,s,0,9600,MT631 - 1,77070100010800ff@1000,Gesamtverbrauch,KWh,Total_in,2 - 1,77070100100700ff@1,aktueller Verbrauch,W,Power_curr,2 - # + >D + >B + + =>sensor53 r + >M 1 + +1,3,s,0,9600,MT631 + 1,77070100010800ff@1000,Gesamtverbrauch,KWh,Total_in,2 + 1,77070100100700ff@1,aktueller Verbrauch,W,Power_curr,2 + # Der Sendezykus der Werte über ebenfalls in der Konfiguration des Scripts mit definiert. "number of decimal places. Add 16 to transmit the data immediately. Otherwise it is transmitted on TelePeriod only." Siehe hierzu: https://tasmota.github.io/docs/Smart-Meter-Interface/#meter-metrics - .. code-block:: text - 1,1-0:1.8.0*255(@1,consumption,KWh,Total_in,4 precision of 4, transmitted only on TelePeriod - 1,1-0:1.8.0*255(@1,consumption,KWh,Total_in,20 precision of 4, transmitted immediately (4 + 16 = 20) +.. code-block:: text + + 1,1-0:1.8.0*255(@1,consumption,KWh,Total_in,4 precision of 4, transmitted only on TelePeriod + 1,1-0:1.8.0*255(@1,consumption,KWh,Total_in,20 precision of 4, transmitted immediately (4 + 16 = 20) Vollständige Informationen zur Konfiguration und die Beschreibung der Item-Attribute sind unter **plugin.yaml** zu finden. diff --git a/telegram/README.rst.off b/telegram/README.rst.off deleted file mode 100755 index c547faa89..000000000 --- a/telegram/README.rst.off +++ /dev/null @@ -1,573 +0,0 @@ -======== -telegram -======== - -Das Plugin dient zum Senden und Empfangen von Nachrichten über den -`Telegram Nachrichten Dienst `_ - -Abhängigkeiten -============== - -Es wird die Bibliothek ``python-telegram-bot`` benötigt. -Diese ist in der ``requirements.txt`` enthalten. -Bevor das Plugin genutzt werden kann, muß die Bibliothek installiert werden: - -* Entweder mit ``sudo pip install -r requirements.txt`` - -oder - -* unter Benutzung von ``pip install -r requirements.txt`` innerhalb - des Verzeichnisses ``/usr/local/smarthome/plugins/telegram``. - -Konfiguration von Telegram -========================== - -Zuerst muß ein eigener Bot bei Telegram erstellt werden: - -* An ``Botfather`` das Kommando ``/newbot`` senden. -* Dann muß ein **Bot Name** vergeben werden der noch nicht existiert. -* Weitere Bot Details können eingestellt werden, wenn das Kommando - ``/mybots`` an den BotFather gesendet wird. - -Der BotFather erstellt für den neuen Bot ein sogenanntes **token** also einen einzigartigen Schlüssel. - -Konfiguration des Plugins -========================= - -Die Konfiguration des Plugins ist auch unter :doc:`/plugins_doc/config/telegram` beschrieben bzw. in der **plugin.yaml** nachzulesen. - - -Der erstelle **token** muß in der ``plugin.yaml`` von SmartHomeNG eingetragen werden. Das kann im Admin-IF geschehen oder durch direkten Eintrag in die ``plugin.yaml``. - -.. code::yaml - - telegram: - plugin_name: telegram - name: Mein Haus - token: 123456789:BBCCfd78dsf98sd9ds-_HJKShh4z5z4zh22 - -* name: Eine Beschreibung des Bots -* token: Der oben beschriebene einzigartige Schlüssel mit dem der Bot bei Telegram identifiziert wird. - -Item Konfiguration -================== - -Jeder Chat, der auf den Bot zugreifen soll, muß SmartHomeNG bekannt gemacht werden. -Das geschieht über ein Item das das Attribut ``telegram_chat_ids`` mit dem Parameter True hat und als Wert ein Dictionary hat. -Im Dictionary sind Paare von Chat-ID und Berechtigung gespeichert. - -.. code::yaml - - Chat_Ids: - type: dict - telegram_chat_ids: True - # cache bietet sich an um Änderungen an den trusted_chat_ids während der - # Laufzeit von SmartHomeNG zu speichern und nach Neustart wieder zu laden - # es wird dann der letzte Wert geladen - cache: 'True' - # Beispiel value: '{ 3234123342: 1, 9234123341: 0 }' - # Ein Dictionary mit chat id und 1 für Lese und Schreibzugriff oder 0 für einen nur Lese-Zugriff - # Nachfolgend ein Chat dem Lese- und Schreibrechte gewährt werden - value: '{ 3234123342: 1 }' - -Um die Chat Id zu bekommen, muß der Bot (und das Plugin) zunächst laufen. Dazu wird SmartHomeNG (neu) gestartet. - -Im Telegram Client wird der Bot als Chatpartner aufgerufen und das Kommando ``/start`` an den Bot gesendet. - -Der Bot reagiert mit einer Meldung, das die Chat-ID noch nicht bekannt ist und diese zunächst eingetragen werden muß. Mit der nun bekannten Chat-ID wird -über das AdminIF das Items Dictionary des entsprechenden Items aus dem obigen Beispiel mit den eigenen Chat-IDs erweitert. - -Ein erneutes Kommando im Telegram Client an den Bot mit ``/start`` sollte nun die Meldung ergeben, das der Chat bekannt ist und weiterhin, welche -Zugriffsrechte der Chat auf den Bot hat. - - -telegram_chat_ids ------------------ - -Es muß ein Item mit dem Typ Dictionary mit dem Attribut ``telegram_chat_ids`` und dem Parameterwert ``True`` angelegt werden. -In ihm werden Chat-IDs und Zugriff auf den Bot gespeichert. Siehe obiges Beispiel. - - -telegram_message ------------------ -Items mit dem Attribut ``telegram_message`` lösen eine Nachricht aus, wenn sich der Itemwert ändert. Es ist möglich Platzhalter -in der Nachricht zu verwenden. - -Verfügbare Platzhalter: - -[ID] [NAME] [VALUE] [CALLER] [SOURCE] [DEST] - -Einfaches Beispiel -'''''''''''''''''' - -.. code:: yaml - - Tuerklingel: - name: Türklingel (entprellt) - type: bool - knx_dpt: 1 - telegram_message: 'Es klingelt an der Tür' - -Beispiel mit Platzhaltern -''''''''''''''''''''''''' - -.. code:: yaml - - state_name: - name: Name des aktuellen Zustands - type: str - visu_acl: r - cache: 'on' - telegram_message: 'New AutoBlind state: [VALUE]' - - -telegram_condition ------------------- - -Da es Situationen gibt die für Items ein ``enforce_updates: True`` benötigen, würde bei ``telegram_message`` bei jeder Aktualisierung des Items eine Nachricht verschickt werden. -Um das zu verhindern, kann einem Item das Attribut ``telegram_condition: on_change`` zugewiesen werden. - -Einfaches Beispiel -'''''''''''''''''' - -.. code:: yaml - - Tuerklingel: - type: bool - knx_dpt: 1 - enforce_updates: True - telegram_message: 'Es klingelt an der Tür' - telegram_condition: on_change - telegram_value_match_regex: (true|True|1) - -Dadurch wird auf eine mehrfache Zuweisung des Items mit dem Wert ``True`` nur einmal mit einer Nachricht reagiert. Um eine weitere Nachricht zu generieren -muss das Item zunächst wieder den Wert ``False`` annehmen. Das Attribut ``telegram_value_match_regex`` filtert den Wert so das es bei der Änderung des Itemwertes -auf ``False`` zu keiner Meldung *Es klingelt an der Tür* kommt. - - -telegram_value_match_regex --------------------------- - -Ist zusätzlich zum Attribut ``telegram_message`` auch das Attribut ``telegram_value_match_regex`` gesetzt, wird der Itemwert geprüft, bevor eine -Nachricht gesendet wird. Geprüft wird gegen/mit den Regex, der als Parameterwert angegeben ist. - -Beispiel -'''''''' - -.. code:: yaml - - TestNum: - type: num - cache: True - telegram_message: 'TestNum: [VALUE]' - telegram_value_match_regex: '[0-1][0-9]' # nur Nachrichten senden wenn Zahlen von 0 - 19 - TestBool: - type: bool - cache: True - telegram_message: "TestBool: [VALUE]" - telegram_value_match_regex: 1 # nur Nachricht senden wenn 1 (True) - - -telegram_message_chat_id ------------------------- -Ist zusätzlich zum Attribut ``telegram_message`` auch das Attribut ``telegram_message_chat_id`` gesetzt, wird die Nachricht nur an die dort angegebene Chat-ID (hier 3234123342) gesendet. -Ist das Attribut nicht gesetzt, erfolgt der Versand der Nachricht an alle Chat-IDs, die dem Plugin bekannt sind. - -Einfaches Beispiel -'''''''''''''''''' - -.. code:: yaml - - Tuerklingel: - type: bool - knx_dpt: 1 - enforce_updates: True - telegram_message: 'Es klingelt an der Tür' - telegram_message_chat_id: 3234123342 - telegram_value_match_regex: (true|True|1) - - -telegram_info -------------- - -Für alle Items mit diesem Attribut wird eine Liste mit Kommandos für den Bot erstellt. Der Listeneintrag entspricht dabei dem Attributwert. -Wird das Kommando ``/info`` an den Bot gesendet, so erstellt der Bot ein Tastaturmenü, dass jedes Attribut mindestens einmal als Kommando enthält. -Bei Auswahl eines dieser Kommandos im Telegram Client wird dann für jedes Item, dass das Attribut ``telegram_info`` und als Attributwert den Kommandonamen enthält -der Wert des Items ausgegeben. - -Beispiel -'''''''' - -.. code:: yaml - - Aussentemperatur: - name: Aussentemperatur in °C - type: num - knx_dpt: 9 - telegram_info: wetter - - Wind_kmh: - name: Windgeschwindigkeit in kmh - type: num - knx_dpt: 9 - telegram_info: wetter - - Raumtemperatur: - name: Raumtemperatur Wohnzimmer in °C - type: num - knx_dpt: 9 - telegram_info: rtr_ist - -Das Kommando ``/info`` veranlasst den Bot zu antworten mit - -.. code:: - - [/wetter] [/rtr_ist] - -Wählt man am Telegram Client daraufhin ``[/wetter]`` aus, so werden - -.. code:: - - Aussentemperatur = -10,6 - Wind_kmh = 12.6 - -ausgegeben. Bei der Auswahl des Kommandos ``[/rtr_ist]`` antwortet der Bot mit - -.. code:: - - Raumtemperatur = 22.6 - - -telegram_text -------------- - -Items mit dem Attribut ``telegram_text`` und dem Attributwert ``True`` bekommen eine Mitteilung, die von einem Telegram Client an den Bot gesendet wird, als Wert zugewiesen. - -Beispiel -'''''''' - -.. code:: yaml - - telegram_message: - name: Textnachricht von Telegram - type: str - telegram_text: true - -Nach der Eingabe von ``Hello world!`` am Telegram wird das Item ``telegram_message`` -auf ``: Chat-ID: Hello world!`` gesetzt. -Ein John Doe ergäbe also ``John Doe: xxxxxx: Hello world!`` - -Mit einer Logik kann basierend darauf ein Menu und entsprechende Abfragen an shNG gestellt werden. -Siehe dazu ein Beispiel weiter unten. - -telegram_control -------------- - -Für alle Items mit diesem Attribut wird eine Liste mit Kommandos für den Bot erstellt. Der Listeneintrag muss mit ``name`` spezifiziert werden. -Wird das Kommando ``/control`` an den Bot gesendet, so erstellt der Bot ein Tastaturmenü, dass jedes Attribut als Kommando enthält. -Dabei werden auch alle aktuellen Werte der Items ausgegeben. -Bei Auswahl eines dieser Kommandos im Telegram Client kann dann ein Item vom Type bool geschalten werden (on/off) oder beim Type 'num' kein eine Zahl zum SH-Item gesendet werden. - -``name`` Item wird mit diesem Namen im Bot als Kommando dargestellt -``type`` Möglichkeiten: on, off, onoff, toggle, num - on * nur Einschalten ist möglich - off * nur Ausschalten ist möglich - onoff * das Ein- und Ausschalten muss mit einen weiteren Kommando vom Tastaturmenu ausgewählt werden - [On] [Off] (nach einem Timeout ohne Antwort wird der Befehl abgebrochen) - toggle * der Wert des Items wird umgeschltet (0 zu 1; 1 zu 0) - num * es kann eine Zahl an SH gesendet werden und das entsprechende Item wird damit geschrieben. (nach einem Timeout ohne Antwort wird der Befehl abgebrochen) -``question``Sicherheitsabfrage vor dem Schalten des Items (verwendbar bei type:on/off/toggle - nach einem Timeout ohne Antwort wird der Befehl abgebrochen) - [Yes] [No] -``min`` Minimalwert (verwendbar bei type:num) -``max`` Maximalwert (verwendbar bei type:num) -``timeout`` Zeit nach welcher der Befehl mit Antwort(onoff/question/num) abgebrochen wird (default 20Sekunden) - - -Beispiel -'''''''' - -.. code:: yaml - - BeregnungZone1: - type: bool - cache: True - telegram_control: "name:BeregnungZ1, type:onoff" - BeregnungZone2: - type: bool - cache: True - telegram_control: "name:BeregnungZ2, type:toggle, question:Ventil wirklich umschalten" - Gartentor: - type: bool - cache: True - telegram_control: "name:Gartentor, type:on, question:Gartentor wirklich öffnen?" - Dachfenster: - type: num - cache: True - telegram_control: "name:Dachfenster, type:num, min:0, max:100, timeout:30" - Kamera: - type: bool - cache: True - telegram_control: "name:Kamera, type:toggle - eval: sh.plugins.return_plugin("telegram").photo_broadcast("http://192.168.0.78/snapshot/view0.jpg", datetime.datetime.now().strftime("%H:%M %d.%m.%Y")) - - -Das Kommando ``/control`` veranlasst den Bot zu antworten mit - -.. code:: - - [/BeregnungZ1] [/BeregnungZ2] [/Gartentor] - [/Dachfenster] [/Kamera] - - -Funktionen -========== - -Das Plugin stellt derzeit zwei Funktionen zur Nutzung in Logiken bereit: - - -msg_broadcast -------------- - -Argumente beim Funktionsaufruf: - -**msg**: Die Nachricht, die verschickt werden soll - -**chat_id**: - - Eine Chat-ID oder eine Liste von Chat-IDs. - - Wird keine ID oder None angegeben, so wird an alle autorisierten Chats gesendet - -photo_broadcast ---------------- - -Argumente beim Funktionsaufruf: - -**path_or_URL**: - - entweder ein lokaler Pfad, der auf eine Bilddatei zeigt log_directory oder - - eine URL mit einem Link. Wenn der Link lokal ist, - -**caption**: - - Titel der Bilddatei, kann auch Dateiname sein oder Datum - - Vorgabewert: None - -**chat_id**: - - eine Chat Id oder eine Liste von Chat ids. Wird keine ID oder None angegeben, - so wird an alle autorisierten Chats gesendet - - Vorgabewert: None - -**local_prepare** - - Ist für das zu sendende Bild eine URL angegeben, ruft das Plugin die - Daten von der URL lokal ab und sendet die Daten dann an den Telegram Server. - Beispiel dafür ist eine URL einer lokalen Webcam. - Soll stattdessen eine im Internet frei zugängliche URL abgerufen werden, - so wird dieses Argument auf False gesetzt und es wird nur die URL - an Telegram geschickt und der lokale Rechner von den Daten entlastet. - Aktuell kann das Plugin nicht mit Benutzername und Passwort geschützten - URL umgehen. - - Vorgabewert: True - -Beispiele ---------- - -Die folgende Beispiellogik zeigt einige Nutzungsmöglichkeiten für die Funktionen: - -.. code:: python - - # Eine Nachricht `Hello world!` wird an alle vertrauten Chat Ids gesendet - msg = "Hello world!" - sh.telegram.msg_broadcast(msg) - - # Ein Bild von einem externen Server soll gesendet werden. - # Nur die URL wird an Telegram gesendet und keine Daten lokal aufbereitet - sh.telegram.photo_broadcast("https://cdn.pixabay.com/photo/2018/10/09/16/20/dog-3735336_960_720.jpg", "A dog", None, False) - - # Bild auf lokalem Server mit aktueller Zeit an Telegram senden - my_webcam_url = "http:// .... bitte lokale URL hier einfügen zum Test ..." - sh.telegram.photo_broadcast(my_webcam_url, "My webcam at {:%Y-%m-%d %H:%M:%S}".format(sh.shtime.now())) - - # Bild senden aber den Inhalt lokal vorbereiten - sh.telegram.photo_broadcast("https://cdn.pixabay.com/photo/2018/10/09/16/20/dog-3735336_960_720.jpg", "The dog again (data locally prepared)") - - local_file = "/usr/local/smarthome/var/ ... bitte eine lokal gespeicherte Datei angeben ..." - sh.telegram.photo_broadcast(local_file, local_file) - - -Anwendungen -=========== - -Menugestützte Interaktion zwischen Telegram und shNG ----------------------------------------------------- - -Diese Anwendung nutzt den Wert, den Telegram in das Item mit dem Attribut ``telegram_text`` schreibt. -Dieser Wert beinhaltet den den User, die Chat-ID und die Message. Basierend auf diesem wird mit einer Logik ein Menu im Chat -dargestellt und die entsprechenden Aktionen ausgeführt. - -.. code:: python - - #!/usr/bin/env python3 - # telegram_message.py - - logger.info(f"Logik '{logic.id()}' ausgelöst durch: {trigger['by']} und {trigger['source']} mit Wert {trigger['value']}") - - telegram_plugin = sh.plugins.return_plugin('telegram') - - def bool2str(wert, typus, logic=logic): - logger.info(f"bool2str wert = {wert}, typus = {typus}") - if type(wert) is bool: - if typus == 1: - result = 'verschlossen' if wert is True else 'offen' - elif typus ==2: - result = 'an' if wert is True else 'aus' - elif typus ==3: - result = 'ja' if wert is True else 'nein' - else: - result = 'typus noch nicht definiert' - else: - result = 'Wert ist nicht vom Type bool' - return result - logic.bool2str = bool2str - - # Telegram Message einlesen und verarbeiten - message = sh.telegram.info.message() - message_user = message[:message.find(":")].lower() - message_chat_id = message[message.find(":")+2:len(message)] - message_text = message_chat_id[message_chat_id.find(":")+2:].lower() - message_chat_id = message_chat_id[:message_chat_id.find(":")] - - ## Menu definieren - if message_chat_id == 'xxxxxxx': - # Menu Ebene1 - custom_keyboard_ubersicht = {'keyboard':[['Rolladen','Tür&Tor'], ['Heizung','Schalten'], ['Wetter','Verkehr','Tanken']] , 'resize_keyboard': True, 'one_time_keyboard': False} - elif message_chat_id == 'yyyyyyy': - # Menu Ebene1 - custom_keyboard_ubersicht = {'keyboard':[['Wetter','Tür&Tor'], ['Heizung','Tanken']] , 'resize_keyboard': True, 'one_time_keyboard': False} - - # Menu Ebene2 - custom_keyboard_wetter = {'keyboard':[['zurück'], ['aktuell', 'historisch']] , 'resize_keyboard': True, 'one_time_keyboard': False} - custom_keyboard_schalten = {'keyboard':[['zurück'], ['LED Nische WZ', 'LED Nische EZ']] , 'resize_keyboard': True, 'one_time_keyboard': False} - custom_keyboard_heizung = {'keyboard':[['zurück'], ['Heizung Status'],['HK_2 Standby', 'HK_2 Normal'], ['EG/OG bewohnt', 'EG/OG unbewohnt'], ['Warmwasser Status'],['Warmwasser AN', 'Warmwasser AUS']] , 'resize_keyboard': True, 'one_time_keyboard': False} - custom_keyboard_verkehr = {'keyboard':[['zurück'], ['Arbeitsweg', 'Heimweg']] , 'resize_keyboard': True, 'one_time_keyboard': False} - custom_keyboard_rolladen = {'keyboard':[['zurück'], ['Rollladen Status'], ['EG Automatik An','OG Automatik An'], ['EG Automatik Aus','OG Automatik Aus']] , 'resize_keyboard': True, 'one_time_keyboard': False} - - ## Menu auswählen und senden - msg = '' - parse_mode = 'HTML' - reply_markup = {} - - if message_text == 'menu' or message_text == "zurück": - msg = 'Bitte auswählen:' - reply_markup = custom_keyboard_ubersicht - elif message_text == 'wetter': - msg = 'Bitte auswählen:' - reply_markup = custom_keyboard_wetter - elif message_text == 'heizung': - msg = 'Bitte auswählen:' - reply_markup = custom_keyboard_heizung - elif message_text == 'schalten': - msg = 'Bitte auswählen:' - reply_markup = custom_keyboard_schalten - elif message_text == 'verkehr': - msg = 'Bitte auswählen:' - reply_markup = custom_keyboard_verkehr - elif message_text == 'rolladen': - msg = 'Bitte auswählen:' - reply_markup = custom_keyboard_rolladen - - ## Messages definieren und senden - # Wetter - if message_text == 'aktuell': - msg = 'Wetter:\naktuelle. Temp.: ' + str(sh.raumtemp.aussen.nord()) + ' °C \ - \ngefühlte Temp.: ' + str(sh.wetter.froggit.wetterstation.feelslikec()) + ' °C \ - \nrel. Luftfeuchte: ' + str(sh.raumtemp.aussen.nord.luftfeuchtigkeit.hum_ist()) + ' % \ - \nRegen letzte h: ' + str(sh.wetter.froggit.wetterstation.hourlyrainmm()) + ' l/m² \ - \nRegen heute: ' + str(sh.wetter.froggit.wetterstation.dailyrainmm()) + ' l/m² \ - \nLuftdruck: ' + str(sh.raumtemp.eg.diele.luftdruck()) + ' hPa \ - \nWind Mittel: {:3.2f}'.format(sh.wetter.froggit.wetterstation.windgustkmh_max10m()) + ' km/h \ - \nWind Spitze: {:3.2f}'.format(sh.wetter.froggit.wetterstation.maxdailygust()) + ' km/h ' - elif message_text == 'historisch': - msg = 'bislang nicht definiert' - - # Warmwasser - elif message_text == 'warmwasser status': - msg = 'Warmwasser:\nSoll_Temp: ' + str(sh.heizung.warmwasser.temperatur_soll()) + ' °C \ - \nIst_Temp: ' + str(sh.heizung.warmwasser.temperatur_ist()) + ' °C \ - \nPumpe: ' + logic.bool2str(sh.heizung.warmwasser.speicherladepumpe_knx(), 2) - elif message_text == 'warmwasser aus': - sh.heizung.warmwasser.temperatur_soll(10) - msg = 'Warmwasser:\nSoll_Temp: ' + str(sh.heizung.warmwasser.temperatur_soll()) + ' °C \ - \nIst_Temp: ' + str(sh.heizung.warmwasser.temperatur_ist()) + ' °C \ - \nPumpe: ' + logic.bool2str(sh.heizung.warmwasser.speicherladepumpe_knx(), 2) - elif message_text == 'warmwasser an': - sh.heizung.warmwasser.temperatur_soll(40) - msg = 'Warmwasser:\nSoll_Temp: ' + str(sh.heizung.warmwasser.temperatur_soll()) + ' °C \ - \nIst_Temp: ' + str(sh.heizung.warmwasser.temperatur_ist()) + ' °C \ - \nPumpe: ' + logic.bool2str(sh.heizung.warmwasser.speicherladepumpe_knx(), 2) - - # Heizung - elif message_text == 'heizung status': - msg = 'HK_2:\nBetriebsart A1: ' + str(sh.heizung.heizkreis_a1m1.betriebsart.betriebsart.betriebsart_str()) +'\ - \nBetriebsart M2: ' + str(sh.heizung.heizkreis_m2.betriebsart.betriebsart.betriebsart_str()) +'\ - \nPumpe A1: ' + logic.bool2str(sh.heizung.heizkreis_a1m1.status.hk_pumpe_knx(), 2) +'\ - \nPumpe M2: ' + logic.bool2str(sh.heizung.heizkreis_m2.status.hk_pumpe_knx(), 2) +'\ - \nEG/OG bewohnt: ' + logic.bool2str(sh.raumtemp.anwesend_eg_og(), 3) +'\ - \nUG bewohnt: ' + logic.bool2str(sh.raumtemp.anwesend_eg_og(), 3) - elif message_text == 'hk_2 standby': - sh.heizung.heizkreis_m2.betriebsart.betriebsart(0) - msg = 'HK_2:\nneue Betriebsart M2: ' + str(sh.heizung.heizkreis_m2.betriebsart.betriebsart.betriebsart_str()) - elif message_text == 'hk_2 normal': - sh.heizung.heizkreis_m2.betriebsart.betriebsart(2) - msg = 'HK_2:\nneue Betriebsart M2: ' + str(sh.heizung.heizkreis_m2.betriebsart.betriebsart.betriebsart_str()) - elif message_text == 'eg/og bewohnt': - sh.raumtemp.anwesend_eg_og(1) - msg = 'HK_2:\nEG/OG bewohnt: ' + logic.bool2str(sh.raumtemp.anwesend_eg_og(), 3) - elif message_text == 'eg/og unbewohnt': - sh.raumtemp.anwesend_eg_og(0) - msg = 'HK_2:\nEG/OG bewohnt: ' + logic.bool2str(sh.raumtemp.anwesend_eg_og(), 3) - - # Schalten - elif message_text == 'led nische wz': - sh.licht.wohnzimmer.vorsatz_nische.onoff(not sh.licht.wohnzimmer.vorsatz_nische.onoff()) - msg = 'Nischenbeleuchtung:\nWohnzimmer: ' + logic.bool2str(sh.licht.wohnzimmer.vorsatz_nische.onoff(), 2) - - elif message_text == 'led nische ez': - sh.licht.wohnzimmer.tv_wand_nische.onoff(not sh.licht.wohnzimmer.tv_wand_nische.onoff()) - msg = 'Nischenbeleuchtung:\nEsszimmer: ' + logic.bool2str(sh.licht.wohnzimmer.tv_wand_nische.onoff(), 2) - - # Verkehr - elif message_text == 'arbeitsweg': - sh.verkehrsinfo.calculate_way_work(1) - time.sleep(0.5) - msg = 'Arbeitsweg:\n ' + str(sh.verkehrsinfo.travel_summary()) - elif message_text == 'heimweg': - sh.verkehrsinfo.calculate_way_home(1) - time.sleep(0.5) - msg = 'Heimweg:\n ' + str(sh.verkehrsinfo.travel_summary()) - - # Tür&Tor - elif message_text == 'tür&tor': - msg = 'Tür&Tor:\nKellertür: ' + logic.bool2str(sh.fenster_tuer_kontakte.kellertuer.verschlossen(), 1) +'\ - \nGaragentür: ' + logic.bool2str(sh.fenster_tuer_kontakte.seitentuer_garage.verschlossen(), 1) +'\ - \nGaragentor links: ' + str(sh.fenster_tuer_kontakte.garagentor_links.text()) +'\ - \nGaragentor rechts: ' + str(sh.fenster_tuer_kontakte.garagentor_rechts.text()) - - # Rolladen - elif message_text == 'rollladen status': - msg = 'Rolladen:\nEG Beschattungsautomatik: ' + logic.bool2str(sh.rollladen.eg.beschattungsautomatik(), 2) +'\ - \nEG Fahrautomatik: ' + logic.bool2str(sh.rollladen.eg.alle.automatik(), 2) +'\ - \nOG Beschattungsautomatik: ' + logic.bool2str(sh.rollladen.og.beschattungsautomatik(), 2) +'\ - \nEG Fahrautomatik: ' + logic.bool2str(sh.rollladen.og.alle.automatik(), 2) - elif message_text == 'eg automatik an': - sh.rollladen.eg.alle.automatik(1) - msg = 'Rolladen:\nEG Fahrautomatik: ' + logic.bool2str(sh.rollladen.eg.alle.automatik(), 2) - elif message_text == 'eg automatik aus': - sh.rollladen.eg.alle.automatik(0) - msg = 'Rolladen:\nEG Fahrautomatik: ' + logic.bool2str(sh.rollladen.eg.alle.automatik(), 2) - elif message_text == 'og automatik an': - sh.rollladen.og.alle.automatik(1) - msg = 'Rolladen:\nOG Fahrautomatik: ' + logic.bool2str(sh.rollladen.og.alle.automatik(), 2) - elif message_text == 'og automatik aus': - sh.rollladen.og.alle.automatik(0) - msg = 'Rolladen:\nOG Fahrautomatik: ' + logic.bool2str(sh.rollladen.og.alle.automatik(), 2) - - # Message senden - if msg != '': - telegram_plugin.msg_broadcast(msg, message_chat_id, reply_markup, parse_mode) \ No newline at end of file diff --git a/telegram/__init__.py b/telegram/__init__.py index f6df66617..cba8102da 100755 --- a/telegram/__init__.py +++ b/telegram/__init__.py @@ -8,6 +8,8 @@ # # This file is part of SmartHomeNG. # +# Telegram Plugin for querying and updating items or sending messages via Telegram +# # 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 @@ -66,7 +68,7 @@ class Telegram(SmartPlugin): - PLUGIN_VERSION = "2.0.0" + PLUGIN_VERSION = "2.0.1" _items = [] # all items using attribute ``telegram_message`` _items_info = {} # dict used whith the info-command: key = attribute_value, val= item_list telegram_info @@ -83,12 +85,12 @@ def __init__(self, sh): """ self.logger.info('Init telegram plugin') - + # Call init code of parent class (SmartPlugin or MqttPlugin) super().__init__() if not self._init_complete: return - + if self.logger.isEnabledFor(logging.DEBUG): self.logger.debug(f"init {__name__}") self._init_complete = False @@ -113,12 +115,12 @@ def __init__(self, sh): self._pretty_thread_names = self.get_parameter_value('pretty_thread_names') self._resend_delay = self.get_parameter_value('resend_delay') self._resend_attemps = self.get_parameter_value('resend_attemps') - + self._bot = None self._queue = Queue() - + self._application = Application.builder().token(self._token).build() - + if self.logger.isEnabledFor(logging.DEBUG): self.logger.debug("adding command handlers to application") @@ -134,7 +136,7 @@ def __init__(self, sh): self._application.add_handler(CommandHandler('control', self.cHandler_control)) # Filters.text includes also commands, starting with ``/`` so it is needed to exclude them. self._application.add_handler(MessageHandler(filters.TEXT & (~filters.COMMAND), self.mHandler)) - + self.init_webinterface() if not self.init_webinterface(WebInterface): self.logger.error("Unable to start Webinterface") @@ -142,7 +144,7 @@ def __init__(self, sh): else: if self.logger.isEnabledFor(logging.DEBUG): self.logger.debug("Init complete") - + self._init_complete = True def __call__(self, msg, chat_id=None): @@ -161,22 +163,22 @@ def run(self): """ if self.logger.isEnabledFor(logging.DEBUG): self.logger.debug("Run method called") - + self.logics = Logics.get_instance() # Returns the instance of the Logics class, to be used to access the logics-api - + self.alive = True - + self._loop.run_until_complete(self.run_coros()) if self.logger.isEnabledFor(logging.DEBUG): self.logger.debug(f"Run method ended") - + def stop(self): """ This is called when the plugins thread is about to stop """ if self.logger.isEnabledFor(logging.DEBUG): self.logger.debug("stop telegram plugin") - + try: if self._bye_msg: cids = [key for key, value in self._chat_ids_item().items() if value == 1] @@ -185,7 +187,7 @@ def stop(self): self.logger.debug("sent bye message") except Exception as e: self.logger.error(f"could not send bye message [{e}]") - + time.sleep(1) self.alive = False # Clears the infiniti loop in sendQueue try: @@ -211,10 +213,10 @@ async def run_coros(self): self._taskConn = asyncio.create_task(self.connect()) self._taskQueue = asyncio.create_task(self.sendQueue()) await asyncio.gather(self._taskConn, self._taskQueue) - + async def connect(self): """ - Connects + Connects """ if self.logger.isEnabledFor(logging.DEBUG): self.logger.debug("connect method called") @@ -222,7 +224,7 @@ async def connect(self): await self._application.initialize() await self._application.start() self._updater = self._application.updater - + q = await self._updater.start_polling(timeout=self._long_polling_timeout, error_callback=self.error_handler) if self.logger.isEnabledFor(logging.DEBUG): @@ -235,14 +237,14 @@ async def connect(self): self.logger.debug(f"sent welcome message {self._welcome_msg}") cids = [key for key, value in self._chat_ids_item().items() if value == 1] self.msg_broadcast(self._welcome_msg, chat_id=cids) - + except TelegramError as e: # catch Unauthorized errors due to an invalid token self.logger.error(f"Unable to start up Telegram conversation. Maybe an invalid token? {e}") return False if self.logger.isEnabledFor(logging.DEBUG): self.logger.debug("connect method end") - + def error_handler(self, update, context): """ Just logs an error in case of a problem @@ -257,7 +259,7 @@ async def sendQueue(self): Waiting for messages to be sent in the queue and sending them to Telegram. The queue expects a dictionary with various parameters dict txt: {"msgType":"Text", "msg":msg, "chat_id":chat_id, "reply_markup":reply_markup, "parse_mode":parse_mode } - dict photo: {"msgType":"Photo", "photofile_or_url":photofile_or_url, "chat_id":chat_id, "caption":caption, "local_prepare":local_prepare} + dict photo: {"msgType":"Photo", "photofile_or_url":photofile_or_url, "chat_id":chat_id, "caption":caption, "local_prepare":local_prepare} """ if self.logger.isEnabledFor(logging.DEBUG): self.logger.debug(f"sendQueue called - queue: [{self._queue}]") @@ -275,7 +277,7 @@ async def sendQueue(self): resendDelay = message["resendDelay"] if "resendAttemps" in message: resendAttemps = message["resendAttemps"] - + if resendDelay <= 0: if self.logger.isEnabledFor(logging.DEBUG): self.logger.debug(f"message queue {message}") @@ -283,18 +285,18 @@ async def sendQueue(self): result = await self.async_msg_broadcast(message["msg"], message["chat_id"], message["reply_markup"], message["parse_mode"]) elif message["msgType"] == "Photo": result = await self.async_photo_broadcast(message["photofile_or_url"], message["caption"], message["chat_id"], message["local_prepare"]) - + # An error occurred while sending - result: list containing the dic of the failed send attempt - if result: + if result: for res in result: resendAttemps+=1 if resendAttemps > self._resend_attemps: if self.logger.isEnabledFor(logging.DEBUG): self.logger.debug(f"don't initiate any further send attempts for: {res}") - break + break else: resendDelay = self._resend_delay - + # Including the sendDelay and sendAttempts in the queue message for the next send attempt. res["resendDelay"] = resendDelay res["resendAttemps"] = resendAttemps @@ -312,7 +314,7 @@ async def sendQueue(self): async def disconnect(self): """ - Stop listening to push updates and logout of this istances Apple TV + Stop listening to push updates and shutdown """ self.logger.info(f"disconnecting") @@ -522,7 +524,7 @@ async def async_msg_broadcast(self, msg, chat_id=None, reply_markup=None, parse_ sendResult = [] if self.logger.isEnabledFor(logging.DEBUG): self.logger.debug(f"async msg_broadcast called") - + for cid in self.get_chat_id_list(chat_id): try: response = await self._bot.send_message(chat_id=cid, text=msg, reply_markup=reply_markup, parse_mode=parse_mode) @@ -543,7 +545,7 @@ async def async_msg_broadcast(self, msg, chat_id=None, reply_markup=None, parse_ return None else: return sendResult - + def msg_broadcast(self, msg, chat_id=None, reply_markup=None, parse_mode=None): if self.alive: @@ -554,7 +556,7 @@ def msg_broadcast(self, msg, chat_id=None, reply_markup=None, parse_mode=None): self._queue.put(q_msg) except Exception as e: self.logger.debug(f"Exception '{e}' occurred, please inform plugin maintainer!") - + async def async_photo_broadcast(self, photofile_or_url, caption=None, chat_id=None, local_prepare=True): """ Send an image to the given chat @@ -1042,7 +1044,7 @@ def list_items_control(self): if not text: text = self.translate("no items found with the attribute %s") % ITEM_ATTR_CONTROL #self._bot.sendMessage(chat_id=chat_id, text=text) - return text + return text async def change_item(self, update, context, name, dicCtl): """ @@ -1146,4 +1148,3 @@ async def telegram_change_item_timeout(self, **kwargs): self._waitAnswer = None # self._bot.send_message(chat_id=update.message.chat.id, text=self.translate("Control/Change item-values:"), reply_markup={"keyboard": self.create_control_reply_markup()}) await context.bot.sendMessage(chat_id=update.message.chat.id, text=self.translate("Control/Change item-values:"), reply_markup={"keyboard": self.create_control_reply_markup()}) - diff --git a/telegram/assets/telegram_webif.png b/telegram/assets/telegram_webif.png new file mode 100644 index 000000000..0044a2696 Binary files /dev/null and b/telegram/assets/telegram_webif.png differ diff --git a/telegram/assets/webif1.png b/telegram/assets/webif1.png deleted file mode 100755 index eb833903e..000000000 Binary files a/telegram/assets/webif1.png and /dev/null differ diff --git a/telegram/assets/webif2.png b/telegram/assets/webif2.png deleted file mode 100755 index 25b27b146..000000000 Binary files a/telegram/assets/webif2.png and /dev/null differ diff --git a/telegram/plugin.yaml b/telegram/plugin.yaml index 18bb738dc..2173691c7 100755 --- a/telegram/plugin.yaml +++ b/telegram/plugin.yaml @@ -7,12 +7,11 @@ plugin: en: 'Connects to the telegram messenger service' maintainer: gamade, ivan73, bmxp state: ready - tester: NONE + tester: onkelandy keywords: telegram chat messenger photo - documentation: http://smarthomeng.de/user/plugins/telegram/user_doc.html support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1548691-support-thread-für-das-telegram-plugin - version: 2.0.0 # Plugin version + version: 2.0.1 # Plugin version sh_minversion: 1.9.5 # minimum shNG version to use this plugin # sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) # py_minversion: 3.6 # minimum Python version to use for this plugin @@ -147,7 +146,7 @@ item_attributes: telegram_control: type: str description: - de: 'Item schreiben per Telegram Keyboard. Der Wert des Attributes (mit mehreren Paramtern) bestimmt das Telegram Keyboard Kommando' + de: 'Item schreiben per Telegram Keyboard. Den Wert des Attributes (mit mehreren Paramtern) bestimmt das Telegram Keyboard Kommando' en: 'Write items with telegram keyboard. The value of the attribute (with parameters) defines the telegram keyboard command' logic_parameters: NONE diff --git a/telegram/user_doc.rst b/telegram/user_doc.rst index 3afa7dfda..e1b8890a8 100755 --- a/telegram/user_doc.rst +++ b/telegram/user_doc.rst @@ -5,6 +5,13 @@ telegram ======== +.. image:: webif/static/img/plugin_logo.svg + :alt: plugin logo + :width: 300px + :height: 300px + :scale: 50 % + :align: left + Das Plugin dient zum Senden und Empfangen von Nachrichten über den `Telegram Nachrichten Dienst `_ @@ -71,7 +78,7 @@ Im Dictionary sind Paare von Chat-ID und Berechtigung gespeichert. # Beispiel value: '{ 3234123342: 1, 9234123341: 0 }' # Ein Dictionary mit chat id und: # 2 für Lese und Schreibzugriff ohne Willkommens- und Ende Nachricht - # 1 für Lese und Schreibzugriff + # 1 für Lese und Schreibzugriff # 0 für einen nur Lese-Zugriff # Nachfolgend ein Chat dem Lese- und Schreibrechte gewährt werden value: '{ 3234123342: 1 }' @@ -95,7 +102,8 @@ In ihm werden Chat-IDs und Zugriff auf den Bot gespeichert. Siehe obiges Beispie telegram_message ------------------ +---------------- + Items mit dem Attribut ``telegram_message`` lösen eine Nachricht aus, wenn sich der Itemwert ändert. Es ist möglich Platzhalter in der Nachricht zu verwenden. @@ -147,7 +155,7 @@ Einfaches Beispiel telegram_value_match_regex: (true|True|1) Dadurch wird auf eine mehrfache Zuweisung des Items mit dem Wert ``True`` nur einmal mit einer Nachricht reagiert. Um eine weitere Nachricht zu generieren -muss das Item zunächst wieder den Wert ``False`` annehmen. Das Attribut ``telegram_value_match_regex`` filtert den Wert so das es bei der Änderung des Itemwertes +muss das Item zunächst wieder den Wert ``False`` annehmen. Das Attribut ``telegram_value_match_regex`` filtert den Wert so das es bei der Änderung des Itemwertes auf ``False`` zu keiner Meldung *Es klingelt an der Tür* kommt. @@ -172,9 +180,9 @@ Beispiel cache: True telegram_message: "TestBool: [VALUE]" telegram_value_match_regex: 1 # nur Nachricht senden wenn 1 (True) - - -telegram_message_chat_id + + +telegram_message_chat_id ------------------------ Ist zusätzlich zum Attribut ``telegram_message`` auch das Attribut ``telegram_message_chat_id`` gesetzt, wird die Nachricht nur an die dort angegebene Chat-ID (hier 3234123342) gesendet. Ist das Attribut nicht gesetzt, erfolgt der Versand der Nachricht an alle Chat-IDs, die dem Plugin bekannt sind. @@ -267,26 +275,36 @@ Mit einer Logik kann basierend darauf ein Menu und entsprechende Abfragen an shN Siehe dazu ein Beispiel weiter unten. telegram_control -------------- +---------------- Für alle Items mit diesem Attribut wird eine Liste mit Kommandos für den Bot erstellt. Der Listeneintrag muss mit ``name`` spezifiziert werden. Wird das Kommando ``/control`` an den Bot gesendet, so erstellt der Bot ein Tastaturmenü, dass jedes Attribut als Kommando enthält. Dabei werden auch alle aktuellen Werte der Items ausgegeben. Bei Auswahl eines dieser Kommandos im Telegram Client kann dann ein Item vom Type bool geschalten werden (on/off) oder beim Type 'num' kein eine Zahl zum SH-Item gesendet werden. -``name`` Item wird mit diesem Namen im Bot als Kommando dargestellt -``type`` Möglichkeiten: on, off, onoff, toggle, num - on * nur Einschalten ist möglich - off * nur Ausschalten ist möglich - onoff * das Ein- und Ausschalten muss mit einen weiteren Kommando vom Tastaturmenu ausgewählt werden - [On] [Off] (nach einem Timeout ohne Antwort wird der Befehl abgebrochen) - toggle * der Wert des Items wird umgeschltet (0 zu 1; 1 zu 0) - num * es kann eine Zahl an SH gesendet werden und das entsprechende Item wird damit geschrieben. (nach einem Timeout ohne Antwort wird der Befehl abgebrochen) -``question``Sicherheitsabfrage vor dem Schalten des Items (verwendbar bei type:on/off/toggle - nach einem Timeout ohne Antwort wird der Befehl abgebrochen) - [Yes] [No] -``min`` Minimalwert (verwendbar bei type:num) -``max`` Maximalwert (verwendbar bei type:num) -``timeout`` Zeit nach welcher der Befehl mit Antwort(onoff/question/num) abgebrochen wird (default 20Sekunden) +``name`` + Item wird mit diesem Namen im Bot als Kommando dargestellt +``type`` + Möglichkeiten: on, off, onoff, toggle, num + on + * nur Einschalten ist möglich + off + * nur Ausschalten ist möglich + onoff + * das Ein- und Ausschalten muss mit einen weiteren Kommando vom Tastaturmenu ausgewählt werden + [On] [Off] (nach einem Timeout ohne Antwort wird der Befehl abgebrochen) + toggle + * der Wert des Items wird umgeschltet (0 zu 1; 1 zu 0) + num + * es kann eine Zahl an SH gesendet werden und das entsprechende Item wird damit geschrieben. (nach einem Timeout ohne Antwort wird der Befehl abgebrochen) +``question`` + Sicherheitsabfrage vor dem Schalten des Items (verwendbar bei type:on/off/toggle - nach einem Timeout ohne Antwort wird der Befehl abgebrochen) [Yes] [No] +``min`` + Minimalwert (verwendbar bei type:num) +``max`` + Maximalwert (verwendbar bei type:num) +``timeout`` + Zeit nach welcher der Befehl mit Antwort(onoff/question/num) abgebrochen wird (default 20Sekunden) Beispiel @@ -379,8 +397,8 @@ Die folgende Beispiellogik zeigt einige Nutzungsmöglichkeiten für die Funktion .. code:: python - telegram_plugin = sh.plugins.return_plugin('telegram') - + telegram_plugin = sh.plugins.return_plugin('telegram') + # Eine Nachricht `Hello world!` wird an alle vertrauten Chat Ids gesendet msg = "Hello world!" telegram_plugin.msg_broadcast(msg) @@ -403,8 +421,8 @@ Die folgende Beispiellogik zeigt einige Nutzungsmöglichkeiten für die Funktion Anwendungen =========== -Menugestützte Interaktion zwischen Telegram und shNG ----------------------------------------------------- +Menugestützte Interaktion zwischen Telegram und SmartHomeNG +----------------------------------------------------------- Diese Anwendung nutzt den Wert, den Telegram in das Item mit dem Attribut ``telegram_text`` schreibt. Dieser Wert beinhaltet den den User, die Chat-ID und die Message. Basierend auf diesem wird mit einer Logik ein Menu im Chat @@ -579,4 +597,28 @@ dargestellt und die entsprechenden Aktionen ausgeführt. # Message senden if msg != '': - telegram_plugin.msg_broadcast(msg, message_chat_id, reply_markup, parse_mode) \ No newline at end of file + telegram_plugin.msg_broadcast(msg, message_chat_id, reply_markup, parse_mode) + +Web Interface +============= + +Das Webinterface bietet folgende Informationen: + +- **Allgemeines**: Oben rechts wird das Timeout, Begrüßungs- und Verabschiedungsnachricht angezeigt + +- **Output Items**: Sämtliche Items, die zum Senden einer Nachricht beitragen + +- **Input Items**: Items, über die Nachrichten empfangen werden können + +- **Telegram Control**: Items, die über Telegram geändert werden können + +- **Telegram Infos**: Befehle mit den zugehörigen Items, deren Werte auf eine Abfrage hin kommuniziert werden + +- **Chat IDs**: Registrierte Chat IDs inkl. Angabe der Zugriffe + +.. image:: assets/telegram_webif.png + :height: 1584px + :width: 3340px + :scale: 25% + :alt: Web Interface + :align: center diff --git a/telegram/webif/templates/index.html b/telegram/webif/templates/index.html index 04b66d672..fd4c95ec9 100755 --- a/telegram/webif/templates/index.html +++ b/telegram/webif/templates/index.html @@ -3,29 +3,14 @@ + +{% endblock pluginscripts %} +{% block headtable %} - + @@ -33,21 +128,6 @@
{{ _('NumFromStart') }}{{ p._model.get_total_number_of_requests_to_controller() }}{{ p._model.get_total_number_of_requests_to_controller() }} Controller URL {{ p._model.get_controller_url() }}
{% endblock headtable %} - - -{% block buttons %} -{% if 1==2 %} -
- -
-{% endif %} -{% endblock %} - - {% set tabcount = 2 %} @@ -64,104 +144,53 @@ --> {% set tab1title = "Items (" ~ p._model.get_item_count() ~ ")" %} {% block bodytab1 %} - -
-
- - - - - - - - - - - - - - {% for item in p._model.get_items() %} - - - - - - - - - - - - - - - - - {% endfor %} - -
 {{ _('Pfad') }}{{ _('Typ') }}unifi_type{{ _('Item Wert') }}{{ _('Letztes Update') }}{{ _('Letzter Change') }}
- {% set warn_level = p._model.get_item_issues(item.id()).get_worst_level() %} - {% if warn_level == 1 %} - - - {% elif warn_level == 2 %} - - - {% elif warn_level == 3 %} - - - {% elif warn_level == 4 %} - - - {% endif %} -
{{ item.id() }}
{{ item.type() }}{{ item.conf['unifi_type'] }}{{ item() }}{{ item.last_update().strftime('%d.%m.%Y %H:%M:%S') }}{{ item.last_change().strftime('%d.%m.%Y %H:%M:%S') }}
 
  - {% for issue in p._model.get_item_issues(item.id()).get_issues() %} - {% if issue[0:2] == "1:" %} -   {{ issue[2:400] }} - {% elif issue[0:2] == "2:" %} - {{ issue[2:400] }} - {% elif issue[0:2] == "3:" %} - {{ issue[2:400] }} - {% elif issue[0:2] == "4:" %} - {{ issue[2:400] }} - {% else %} - {{ issue }} - {% endif %} -
- {% endfor %} -
-
-
- - + + + + + + + + + + + + + + {% for item in p._model.get_items() %} + + + + + + + + + {% endfor %} + +
{{ _('Pfad') }}{{ _('Typ') }}unifi_type{{ _('Item Wert') }}{{ _('Letztes Update') }}{{ _('Letzter Change') }}{{ _('Probleme') }}
{{ item.id() }}
{{ item.type() }}{{ item.conf['unifi_type'] }}{{ item() }}{{ item.last_update().strftime('%d.%m.%Y %H:%M:%S') }}{{ item.last_change().strftime('%d.%m.%Y %H:%M:%S') }} + {% if p._model.get_item_issues(item.id()).get_issues() | length == 0 %} + {{ _('Keine')}} + {% else %} + {% for issue in p._model.get_item_issues(item.id()).get_issues() %} + {% if p._model.get_item_issues(item.id()).get_issues() | length > 1 and loop.index == 1 %} +
+ {% endif %} + {% if issue[0:2] == "1:" %} + {{ issue[2:400] }} + {% elif issue[0:2] == "2:" %} + {{ issue[2:400] }} + {% elif issue[0:2] == "3:" %} + {{ issue[2:400] }} + {% elif issue[0:2] == "4:" %} + {{ issue[2:400] }} + {% else %} + {{ issue }} + {% endif %} +
+ {% endfor %} + {% endif %} +
{% endblock bodytab1 %} @@ -173,4 +202,4 @@ {{ p._model.get_item_hierarchy() }}
-{% endblock bodytab2 %} \ No newline at end of file +{% endblock bodytab2 %} diff --git a/uzsu/__init__.py b/uzsu/__init__.py index 7c4ae7a00..f752f29d8 100755 --- a/uzsu/__init__.py +++ b/uzsu/__init__.py @@ -104,7 +104,7 @@ class UZSU(SmartPlugin): ALLOW_MULTIINSTANCE = False - PLUGIN_VERSION = "1.6.5" # item buffer for all uzsu enabled items + PLUGIN_VERSION = "1.6.6" # item buffer for all uzsu enabled items def __init__(self, smarthome): """ @@ -137,7 +137,7 @@ def run(self): self.logger.debug("run method called") self.alive = True self.scheduler_add('uzsu_sunupdate', self._update_all_suns, - value={'caller': 'scheduler'}, cron=self._suncalculation_cron) + value={'caller': 'Scheduler:UZSU'}, cron=self._suncalculation_cron) self.logger.info("Adding sun update schedule for midnight") for item in self._items: @@ -180,6 +180,7 @@ def stop(self): Stop method for the plugin """ self.logger.debug("stop method called") + self.scheduler_remove('uzsu_sunupdate') for item in self._items: try: self.scheduler_remove('{}'.format(item.property.path)) @@ -195,7 +196,7 @@ def _update_all_suns(self, caller=None): :type caller: str """ for item in self._items: - success = self._update_sun(item) + success = self._update_sun(item, caller="update_all_suns") if success: self.logger.debug('Updating sun info for item {}. Caller: {}'.format(item, caller)) self._update_item(item, 'UZSU Plugin', 'update_all_suns') @@ -223,8 +224,8 @@ def _update_sun(self, item, caller=None): item, caller, self._items[item]['sunrise'], self._items[item]['sunset'])) success = True except Exception as e: - success = False - self.logger.debug("Not updated sun entries for item {}. Error {}".format(item, e)) + success = "Not updated sun entries for item {}. Error {}".format(item, e) + self.logger.debug(success) return success def _update_suncalc(self, item, entry, entryindex, entryvalue): @@ -355,7 +356,8 @@ def _logics_planned(self, item=None): if self._planned.get(item) not in [None, {}, 'notinit'] and self._items[item].get('active') is True: self.logger.info("Item '{}' is going to be set to {} at {}".format( item, self._planned[item]['value'], self._planned[item]['next'])) - self._webdata['items'][item.id()].update({'planned': {'value': self._planned[item]['value'], 'time': self._planned[item]['next']}}) + self._webdata['items'][item.id()].update({'planned': {'value': self._planned[item]['value'], + 'time': self._planned[item]['next']}}) return self._planned[item] elif self._planned.get(item) == 'notinit' and self._items[item].get('active') is True: self.logger.info("Item '{}' is active but not fully initialized yet.".format(item)) @@ -524,26 +526,24 @@ def _update_item(self, item, caller="", comment=""): success = self._get_sun4week(item, caller="_update_item") if success: self.logger.debug('Updated weekly sun info for item {}' - ' caller : {} comment : {}'.format(item, caller, comment)) + ' caller: {} comment: {}'.format(item, caller, comment)) else: self.logger.debug('Issues with updating weekly sun info' - ' for item {} caller : {} comment : {}'.format(item, caller, comment)) - success = False + ' for item {} caller: {} comment: {}'.format(item, caller, comment)) success = self._series_calculate(item, caller, comment) - if success: + if success is True: self.logger.debug('Updated seriesCalculated for item {}' - ' caller : {} comment : {}'.format(item, caller, comment)) + ' caller: {} comment: {}'.format(item, caller, comment)) else: self.logger.debug('Issues with updating seriesCalculated' - ' for item {} caller : {} comment : {}'.format(item, caller, comment)) - success = False + ' for item {} caller: {} comment: {}, issue: {}'.format(item, caller, comment, success)) success = self._update_sun(item, caller="_update_item") - if success: + if success is True: self.logger.debug('Updated sunset/rise calculations for item {}' - ' caller : {} comment : {}'.format(item, caller, comment)) + ' caller: {} comment: {}'.format(item, caller, comment)) else: self.logger.debug('Issues with updating sunset/rise calculations' - ' for item {} caller : {} comment : {}'.format(item, caller, comment)) + ' for item {} caller: {} comment: {}, issue: {}'.format(item, caller, comment, success)) item(self._items[item], caller, comment) self._webdata['items'][item.id()].update({'interpolation': self._items[item].get('interpolation')}) self._webdata['items'][item.id()].update({'active': str(self._items[item].get('active'))}) @@ -562,13 +562,13 @@ def _schedule(self, item, caller=None): This function schedules an item: First the item is removed from the scheduler. If the item is active then the list is searched for the nearest next execution time. No matter if active or not the calculation for the execution time is triggered. - :param item: item to be updated towards the plugin + :param item: item to be updated towards the plugin. :param caller: if given it represents the callers name. If the caller is set - to "dry_run" the evaluation of sun entries takes place but no scheduler will be set + to "dry_run" the evaluation of sun entries takes place but no scheduler will be set. """ if caller != "dry_run": self.scheduler_remove('{}'.format(item.property.path)) - _caller = "scheduler" + _caller = "Scheduler:UZSU" self.logger.debug('Schedule Item {}, Trigger: {}, Changed by: {}'.format( item, caller, item.changed_by())) else: @@ -717,7 +717,7 @@ def _schedule(self, item, caller=None): self._webdata['items'][item.id()].update({'planned': {'value': _value, 'time': _next.strftime('%d.%m.%Y %H:%M')}}) self._update_count['done'] = self._update_count.get('done') + 1 self.scheduler_add('{}'.format(item.property.path), self._set, - value={'item': item, 'value': _value}, next=_next) + value={'item': item, 'value': _value, 'caller': 'Scheduler'}, next=_next) if self._update_count.get('done') == self._update_count.get('todo'): self.scheduler_trigger('uzsu_sunupdate', by='UZSU Plugin') self._update_count = {'done': 0, 'todo': 0} @@ -736,7 +736,7 @@ def _set(self, item=None, value=None, caller=None): _uzsuitem, _itemvalue = self._get_dependant(item) _uzsuitem(value, 'UZSU Plugin', 'set') self._webdata['items'][item.id()].update({'depend': {'item': _uzsuitem.id(), 'value': str(_itemvalue)}}) - if not caller: + if not caller or caller == "Scheduler": self._schedule(item, caller='set') def _get_time(self, entry, timescan, item=None, entryindex=None, caller=None): @@ -767,6 +767,7 @@ def _get_time(self, entry, timescan, item=None, entryindex=None, caller=None): if 'time' not in entry: return None, None value = entry['value'] + next = None active = True if caller == "dry_run" else entry['active'] today = datetime.today() tomorrow = today + timedelta(days=1) @@ -793,7 +794,7 @@ def _get_time(self, entry, timescan, item=None, entryindex=None, caller=None): rrule = rrulestr(entry['rrule'], dtstart=datetime.combine( weekbefore, self._sun(datetime.combine(weekbefore.date(), datetime.min.time()).replace(tzinfo=self._timezone), - time, timescan).time())) + time, timescan).time())) self.logger.debug("Looking for {} sun-related time. Found rrule: {}".format( timescan, str(rrule).replace('\n', ';'))) else: @@ -809,7 +810,7 @@ def _get_time(self, entry, timescan, item=None, entryindex=None, caller=None): sleep(0.01) next = self._sun(datetime.combine(dt.date(), datetime.min.time()).replace(tzinfo=self._timezone), - time, timescan) + time, timescan) self.logger.debug("Result parsing time (rrule) {}: {}".format(time, next)) if entryindex is not None and timescan == 'next': self._update_suncalc(item, entry, entryindex, next.strftime("%H:%M")) @@ -855,12 +856,12 @@ def _get_time(self, entry, timescan, item=None, entryindex=None, caller=None): self.logger.debug("Looking for {} series-related time. Found rrule: {} with start-time . {}".format( timescan, entry['rrule'].replace('\n', ';'), entry['series']['timeSeriesMin'])) - cond_today = next.date() == today.date() - cond_yesterday = next.date() - timedelta(days=1) == yesterday.date() - cond_tomorrow = next.date() == tomorrow.date() - cond_next = next > datetime.now(self._timezone) - cond_previous_today = next - timedelta(seconds=1) < datetime.now(self._timezone) - cond_previous_yesterday = next - timedelta(days=1) < datetime.now(self._timezone) + cond_today = False if next is None else next.date() == today.date() + cond_yesterday = False if next is None else next.date() - timedelta(days=1) == yesterday.date() + cond_tomorrow = False if next is None else next.date() == tomorrow.date() + cond_next = False if next is None else next > datetime.now(self._timezone) + cond_previous_today = False if next is None else next - timedelta(seconds=1) < datetime.now(self._timezone) + cond_previous_yesterday = False if next is None else next - timedelta(days=1) < datetime.now(self._timezone) if next and cond_today and cond_next: self._itpl[item][next.timestamp() * 1000.0] = value self.logger.debug("Return next today: {}, value {}".format(next, value)) @@ -893,7 +894,8 @@ def _series_calculate(self, item, caller=None, source=None): """ self.logger.debug("Series Calculate method for item {} called by {}. Source: {}".format(item, caller, source)) if not self._items[item].get('list'): - return + issue = "No list entry in UZSU dict for item {}".format(item) + return issue try: mydays = ['MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU'] for i, mydict in enumerate(self._items[item]['list']): @@ -906,32 +908,36 @@ def _series_calculate(self, item, caller=None, source=None): try: ##################### seriesbegin, seriesend, daycount, mydict = self._fix_empty_values(mydict) - intervall = mydict['series'].get('timeSeriesIntervall', None) + interval = mydict['series'].get('timeSeriesIntervall', None) seriesstart = seriesbegin + endtime = None - if intervall is None or intervall == "": - self.logger.warning("Could not calculate serie for item {}" - " - because intervall is None - {}".format(item, mydict)) - return + if interval is None or interval == "": + issue = "Could not calculate serie for item {}"\ + " - because interval is None - {}".format(item, mydict) + self.logger.warning(issue) + return issue if (daycount == '' or daycount is None) and seriesend is None: - self.logger.warning("Could not calculate series" - " because timeSeriesCount is NONE and TimeSeriesMax is NONE") - return + issue = "Could not calculate series because "\ + "timeSeriesCount is NONE and TimeSeriesMax is NONE" + self.logger.warning(issue) + return issue - intervall = int(intervall.split(":")[0]) * 60 + int(mydict['series']['timeSeriesIntervall'].split(":")[1]) + interval = int(interval.split(":")[0]) * 60 + int(mydict['series']['timeSeriesIntervall'].split(":")[1]) - if intervall == 0: - self.logger.warning("Could not calculate serie because intervall is ZERO - {}".format(mydict)) - return + if interval == 0: + issue = "Could not calculate serie because interval is ZERO - {}".format(mydict) + self.logger.warning(issue) + return issue if daycount is not None and daycount != '': - if int(daycount) * intervall >= 1440: + if int(daycount) * interval >= 1440: org_daycount = daycount - daycount = int(1439 / intervall) + daycount = int(1439 / interval) self.logger.warning("Cut your SerieCount to {} -" - " because intervall {} x SerieCount {}" - " is more than 24h".format(daycount, intervall, org_daycount)) + " because interval {} x SerieCount {}" + " is more than 24h".format(daycount, interval, org_daycount)) if 'sun' not in mydict['series']['timeSeriesMin']: starttime = datetime.strptime(mydict['series']['timeSeriesMin'], "%H:%M") @@ -944,7 +950,7 @@ def _series_calculate(self, item, caller=None, source=None): # calculate End of Serie by Count if seriesend is None: endtime = starttime - endtime += timedelta(minutes=intervall * int(daycount)) + endtime += timedelta(minutes=interval * int(daycount)) if seriesend is not None and 'sun' in seriesend: mytime = self._sun(datetime.now().replace(hour=0, minute=0, second=0).astimezone(self._timezone), @@ -954,7 +960,7 @@ def _series_calculate(self, item, caller=None, source=None): elif seriesend is not None and 'sun' not in seriesend: endtime = datetime.strptime(seriesend, "%H:%M") - if seriesend is None: + if seriesend is None and endtime: seriesend = str(endtime.time())[:5] if endtime <= starttime: @@ -964,13 +970,13 @@ def _series_calculate(self, item, caller=None, source=None): original_daycount = daycount if daycount is None: - daycount = int(timediff.total_seconds() / 60 / intervall) + daycount = int(timediff.total_seconds() / 60 / interval) else: - new_daycount = int(timediff.total_seconds() / 60 / intervall) + new_daycount = int(timediff.total_seconds() / 60 / interval) if int(daycount) > new_daycount: - self.logger.warning("Cut your SerieCount to {} - because intervall {}" + self.logger.warning("Cut your SerieCount to {} - because interval {}" " x SerieCount {} is not possible between {} and {}".format( - new_daycount, intervall, daycount, starttime, endtime)) + new_daycount, interval, daycount, starttime, endtime)) daycount = new_daycount ##################### @@ -981,13 +987,13 @@ def _series_calculate(self, item, caller=None, source=None): str(starttime.minute)).time())) mynewlist = [] - intervall = int(mydict['series']['timeSeriesIntervall'].split(":")[0])*60 + \ + interval = int(mydict['series']['timeSeriesIntervall'].split(":")[0])*60 + \ int(mydict['series']['timeSeriesIntervall'].split(":")[1]) exceptions = 0 for day in list(rrule): if not mydays[day.weekday()] in mydict['rrule']: continue - myrulenext = "FREQ=MINUTELY;COUNT={};INTERVAL={}".format(daycount, intervall) + myrulenext = "FREQ=MINUTELY;COUNT={};INTERVAL={}".format(daycount, interval) if 'sun' not in mydict['series']['timeSeriesMin']: starttime = datetime.strptime(mydict['series']['timeSeriesMin'], "%H:%M") @@ -1029,13 +1035,14 @@ def _series_calculate(self, item, caller=None, source=None): mytpl = {'seriesMin': str(seriestarttime.time())[:5]} if original_daycount is not None: mytpl['seriesMax'] = str((seriestarttime + - timedelta(minutes=intervall * count)).time())[:5] + timedelta(minutes=interval * count)).time())[:5] else: mytpl['seriesMax'] = "{:02d}".format(endtime.hour) + ":" + \ "{:02d}".format(endtime.minute) mytpl['seriesDay'] = actday mytpl['maxCountCalculated'] = count if exceptions == 0 else 0 - self.logger.debug("Mytpl: {}, count {}, daycount {}, interval {}".format(mytpl, count, daycount, intervall)) + self.logger.debug("Mytpl: {}, count {}, " + "daycount {}, interval {}".format(mytpl, count, daycount, interval)) mynewlist.append(mytpl) count = 0 seriestarttime = None @@ -1051,14 +1058,14 @@ def _series_calculate(self, item, caller=None, source=None): if seriestarttime is not None: mytpl = {'seriesMin': str(seriestarttime.time())[:5]} if original_daycount is not None: - mytpl['seriesMax'] = str((seriestarttime + timedelta(minutes=intervall * count)).time())[:5] + mytpl['seriesMax'] = str((seriestarttime + timedelta(minutes=interval * count)).time())[:5] else: mytpl['seriesMax'] = "{:02d}".format(endtime.hour) + ":" + "{:02d}".format(endtime.minute) mytpl['maxCountCalculated'] = count if exceptions == 0 else 0 mytpl['seriesDay'] = actday self.logger.debug("Mytpl for last time of day: {}," " count {} daycount {}," - " interval {}".format(mytpl, count, original_daycount, intervall)) + " interval {}".format(mytpl, count, original_daycount, interval)) mynewlist.append(mytpl) if mynewlist: @@ -1125,15 +1132,15 @@ def _series_get_time(self, mydict, timescan=''): returnvalue = None seriesbegin, seriesend, daycount, mydict = self._fix_empty_values(mydict) - intervall = mydict['series'].get('timeSeriesIntervall', None) + interval = mydict['series'].get('timeSeriesIntervall', None) seriesstart = seriesbegin - if intervall is not None and intervall != "": - intervall = int(intervall.split(":")[0])*60 + int(mydict['series']['timeSeriesIntervall'].split(":")[1]) + if interval is not None and interval != "": + interval = int(interval.split(":")[0])*60 + int(mydict['series']['timeSeriesIntervall'].split(":")[1]) else: return returnvalue - if intervall == 0: - self.logger.warning("Could not calculate serie because intervall is ZERO - {}".format(mydict)) + if interval == 0: + self.logger.warning("Could not calculate serie because interval is ZERO - {}".format(mydict)) return returnvalue if 'sun' not in mydict['series']['timeSeriesMin']: @@ -1155,30 +1162,29 @@ def _series_get_time(self, mydict, timescan=''): if endtime < starttime: endtime += timedelta(days=1) timediff = endtime - starttime - daycount = int(timediff.total_seconds() / 60 / intervall) + daycount = int(timediff.total_seconds() / 60 / interval) else: if seriesend is None: endtime = starttime - endtime += timedelta(minutes=intervall * int(daycount)) + endtime += timedelta(minutes=interval * int(daycount)) timediff = endtime - starttime - daycount = int(timediff.total_seconds() / 60 / intervall) + daycount = int(timediff.total_seconds() / 60 / interval) else: endtime = datetime.strptime(seriesend, "%H:%M") timediff = endtime - starttime if daycount is not None and daycount != '': - if seriesend is None and (int(daycount) * intervall >= 1440): + if seriesend is None and (int(daycount) * interval >= 1440): org_count = daycount - count = int(1439 / intervall) - self.logger.warning("Cut your SerieCount to {} - because intervall {}" - " x SerieCount {} is more than 24h".format(count, intervall, org_count)) + count = int(1439 / interval) + self.logger.warning("Cut your SerieCount to {} - because interval {}" + " x SerieCount {} is more than 24h".format(count, interval, org_count)) else: - new_daycount = int(timediff.total_seconds() / 60 / intervall) - #self.logger.debug("new daycount: {}, seriesend {}".format(new_daycount, seriesend)) + new_daycount = int(timediff.total_seconds() / 60 / interval) if int(daycount) > new_daycount: - self.logger.warning("Cut your SerieCount to {} - because intervall {}" + self.logger.warning("Cut your SerieCount to {} - because interval {}" " x SerieCount {} is not possible between {} and {}".format( - new_daycount, intervall, daycount, datetime.strftime(starttime, "%H:%M"), + new_daycount, interval, daycount, datetime.strftime(starttime, "%H:%M"), datetime.strftime(endtime, "%H:%M"))) daycount = new_daycount mylist = OrderedDict() @@ -1191,7 +1197,7 @@ def _series_get_time(self, mydict, timescan=''): timestamp = day mylist[timestamp] = 'x' while mycount < daycount: - timestamp = timestamp + timedelta(minutes=intervall) + timestamp = timestamp + timedelta(minutes=interval) mylist[timestamp] = 'x' mycount += 1 diff --git a/uzsu/plugin.yaml b/uzsu/plugin.yaml index 29bc5e70e..5a0096583 100755 --- a/uzsu/plugin.yaml +++ b/uzsu/plugin.yaml @@ -46,7 +46,7 @@ plugin: keywords: scheduler uzsu trigger series support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1364692-supportthread-für-uzsu-plugin - version: 1.6.5 # Plugin version + version: 1.6.6 # Plugin version sh_minversion: 1.6 # minimum shNG version to use this plugin # sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) multi_instance: False # plugin supports multi instance diff --git a/uzsu/webif/templates/index.html b/uzsu/webif/templates/index.html index 9f9eec53b..b3edbd801 100755 --- a/uzsu/webif/templates/index.html +++ b/uzsu/webif/templates/index.html @@ -20,22 +20,9 @@