diff --git a/apcups/user_doc.rst b/apcups/user_doc.rst index b37310870..e8c22464f 100644 --- a/apcups/user_doc.rst +++ b/apcups/user_doc.rst @@ -2,9 +2,9 @@ .. index:: apcups -=============================== +====== apcups -=============================== +====== .. image:: webif/static/img/plugin_logo.png @@ -14,8 +14,6 @@ apcups :scale: 50 % :align: left - - Anforderungen ============= @@ -33,7 +31,7 @@ Wenn der Daemon lokal installiert ist, sollte die Datei ``/etc/apcupsd/apcupsd.c Unterstützte Geräte ------------------- -Aollte mit allen APC UPS Geräten funktionieren die den apcupsd unterstützen. Getestet wurde nur mit einer **smartUPS**. +Sollte mit allen APC UPS Geräten funktionieren, die den apcupsd unterstützen. Getestet wurde nur mit einer **smartUPS**. Konfiguration @@ -134,18 +132,15 @@ Zu den Informationen, welche Funktionen das Plugin bereitstellt (z.B. zur Nutzun bitte die Dokumentation :doc:`Dokumentation ` lesen, die aus den Metadaten der plugin.yaml erzeugt wurde (siehe oben). - - -| Beispiele ========= -Beispiel -~~~~~~~ +Schlüssel auslesen +~~~~~~~~~~~~~~~~~~ -The example below will read the keys **LINEV**, **STATUS** and -**TIMELEFT** and returns their values. +Das folgende Beispiel liest die Schlüssel **LINEV**, **STATUS** und +**TIMELEFT** und gibt deren Werte zurück. .. code:: yaml @@ -170,165 +165,142 @@ The example below will read the keys **LINEV**, **STATUS** and type: num apcups: timeleft -**type** depends on the values. +**type** hängt von den Werten ab. 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 +Laut `APC `_) +ist die Bedeutung der Variablen wie folgt: :: - 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. - + 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 - -Tab 1: ----------------------- - - - -.. image:: assets/webif_tab1.jpg - :class: screenshot - - - - +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/artnet/README.md b/artnet/README.md deleted file mode 100755 index bdf79d60d..000000000 --- a/artnet/README.md +++ /dev/null @@ -1,99 +0,0 @@ -# Artnet - -## Requirements - -You need a device understanding Artnet. -I suggest to use the software OLA http://www.opendmx.net/index.php/Open_Lighting_Architecture to translate the ArtNet packets into DMX Signals. -Alternatively you can use any Art-Net to DMX Adapter. (Tested with https://www.ulrichradig.de/home/index.php/dmx/art-net-box) -OLA supports most USB -> DMX Adapters available at the moment. -For specifications of the Art-Net look at https://art-net.org.uk/resources/art-net-specification/ - -## Supported Hardware - -* Hardware supported by OLA. See Link above. - -## Configuration - -### plugin.yaml - -```yaml -artnet1: - plugin_name: artnet - artnet_universe: 0 - artnet_net: 0 - artnet_subnet: 0 - ip: 192.168.0.99 - port: 6454 - update_cycle: 120 - instance: keller -``` - -#### Attributes - * `artnet_universe`: Art-Net Universe, default: 0 - * `artnet_net`: Art-Net Net, default: 0 - * `artnet_subnet`: Art-Net Subnet, default: 0 - * `ip`: IP-address of your Art-Net node, mandatory, no default - * `port`: Port to reach your Art-Net node, defaul 6454 - * `update_cycle`: timeperiod between two update cycles, default 0 for no update. If a cycle is provided the current channel-settings is updated to Art-Net every n-th second. - * `instance`: Name of this plugin instance (e.g. above: keller) - -### items.yaml - -#### artnet_address -This attribute assigns an item to the respective artnet-address (DMX channel) - -### Example: -```yaml - lightbar: - red: - artnet_address@keller: 1 - green: - artnet_address@keller: 2 - blue: - artnet_address@keller: 3 -``` - -### logic.yaml -Notice: First DMX channel is 1! Not 0! - -To send DMX Data to the universe set in plugin.yaml you have 4 possibilities: - -#### 0) Use items - -as explained above you can use items for that - -#### 1) Send single value -``sh.artnet1(, )`` - -Sets DMX_CHAN to value DMX_VALUE. - -Example: ``sh.artnet1(12,255)`` -If channels 1-11 are already set, they will not change. -If channels 1-11 are not set till now, the will be set to value 0. -This is needed because on a DMX bus you can not set just one specific channel. -You have to begin at first channel setting your values. - -#### 2) Send a list of values starting at channel -``sh.artnet1(, )`` -Sends to DMX Bus starting at - -Example: -``sh.artnet1(10,[0,33,44,55,99])`` -If channels 1-9 are already set, they will not change. -If channels 1-9 are not set till now, the will be set to value 0. -This is needed because on a DMX bus you can not set just one specific channel. -You have to begin at first channel setting your values. -Values in square brackets will be written to channel (10-14) - -#### 3) Send a list of values - -``sh.artnet1()`` - -Sends to DMX Bus starting at channel 1 - -This is nearly the same as 2) but without starting channel. - -Example: - -``sh.artnet1([0,33,44,55,99])`` -Values in Square brackets will be written to channel (1-5) diff --git a/artnet/__init__.py b/artnet/__init__.py index 5c9792a5e..bfdfc3de3 100755 --- a/artnet/__init__.py +++ b/artnet/__init__.py @@ -29,10 +29,11 @@ from lib.model.smartplugin import * from lib.module import Modules +from .webif import WebInterface class ArtNet_Model: - def __init__(self, ip, port: int, net: int, subnet: int, universe: int, instance_name, update_cycle: int, min_channels: int): + def __init__(self, ip, port: int, net: int, subnet: int, universe: int, instance_name, update_cycle: int, min_channels: int, plugin): self._ip = ip self._port = port @@ -42,6 +43,7 @@ def __init__(self, ip, port: int, net: int, subnet: int, universe: int, instance self._instance_name = instance_name self._update_cycle = update_cycle self._min_channels = min_channels + self._plugin = plugin self._items = [] @@ -91,8 +93,8 @@ def get_items(self): :return: array of items held by the device, sorted by their DMX-address """ - return sorted(self._items, key=lambda i: self.get_iattr_value(i.conf, "artnet_address")) - + return sorted(self._items, key=lambda i: self._plugin.get_iattr_value(i.conf, "artnet_address")) + def get_min_channels(self): """ @@ -106,7 +108,7 @@ def get_min_channels(self): class ArtNet(SmartPlugin): ALLOW_MULTIINSTANCE = True - PLUGIN_VERSION = "1.6.0" + PLUGIN_VERSION = "1.6.1" ADDR_ATTR = 'artnet_address' packet_counter = 1 @@ -116,6 +118,7 @@ def __init__(self, sh, *args, **kwargs): """ Initalizes the plugin. The parameters describe for this method are pulled from the entry in plugin.conf. """ + super().__init__() self.logger.info('Init ArtNet Plugin') self._model = ArtNet_Model(ip=self.get_parameter_value('ip'), @@ -125,14 +128,15 @@ def __init__(self, sh, *args, **kwargs): universe=self.get_parameter_value('artnet_universe'), instance_name=self.get_instance_name(), update_cycle=self.get_parameter_value('update_cycle'), - min_channels=self.get_parameter_value('min_channels') + min_channels=self.get_parameter_value('min_channels'), + plugin=self ) while len(self.dmxdata) < self._model._min_channels: self.dmxdata.append(0) self.s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - self.init_webinterface() + self.init_webinterface(WebInterface) self.logger.debug("Init ArtNet Plugin for %s done" % self._model._instance_name) @@ -180,7 +184,10 @@ def run(self): for it in self._model._items: adr = int(self.get_iattr_value(it.conf, self.ADDR_ATTR)) val = it() - if val < 0 or val > 255: + if val is None: + self.logger.warning(f"Value for address {adr} is None.") + continue + elif val < 0 or val > 255: self.logger.warning( "Impossible to update address: %s to value %s from item %s, value has to be >=0 and <=255" % (adr, val, it)) else: @@ -241,7 +248,7 @@ def send_frame(self, dmxframe): def __ArtDMX_broadcast(self): """ - Assemble data according to ArtDmx packet definition, see at + Assemble data according to ArtDmx packet definition, see at https://artisticlicence.com/WebSiteMaster/User Guides/art-net.pdf """ data = [] @@ -267,10 +274,11 @@ def __ArtDMX_broadcast(self): # Length of DMX Data, High Byte First data.append(struct.pack('>H', len(self.dmxdata))) - + # DMX Data for d in self.dmxdata: - data.append(struct.pack('B', int(d))) + if d is not None: + data.append(struct.pack('B', int(d))) # convert from list to string result = bytes() @@ -288,79 +296,3 @@ def __ArtDMX_broadcast(self): self._model._subnet, self._model._universe)) self.s.sendto(result, (self._model._ip, self._model._port)) - - def init_webinterface(self): - """" - Initialize the web interface for this plugin - - This method is only needed if the plugin is implementing a web interface - """ - try: - # try/except to handle running in a core version that does not support modules - self.mod_http = Modules.get_instance().get_module('http') - except: - self.mod_http = None - if self.mod_http == None: - self.logger.error("Plugin '{}': Not initializing the web interface".format(self.get_shortname())) - return False - - # set application configuration for cherrypy - webif_dir = self.path_join(self.get_plugin_dir(), 'webif') - config = { - '/': { - 'tools.staticdir.root': webif_dir, - }, - '/static': { - 'tools.staticdir.on': True, - 'tools.staticdir.dir': 'static' - } - } - - # Register the web interface as a cherrypy app - self.mod_http.register_webif(WebInterface(webif_dir, self), - self.get_shortname(), - config, - self.get_classname(), self.get_instance_name(), - description='') - - return True - -# ------------------------------------------ -# Webinterface of the plugin -# ------------------------------------------ - - -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 = logging.getLogger(__name__) - self.webif_dir = webif_dir - self.plugin = plugin - - 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 beeing rendered - """ - tabcount = 1 - - tmpl = self.tplenv.get_template('index.html') - return tmpl.render(plugin_shortname=self.plugin.get_shortname(), - plugin_version=self.plugin.get_version(), - plugin_info=self.plugin.get_info(), - tabcount=tabcount, - p=self.plugin) diff --git a/artnet/assets/artnet_webif.png b/artnet/assets/artnet_webif.png new file mode 100644 index 000000000..bf248c8ca Binary files /dev/null and b/artnet/assets/artnet_webif.png differ diff --git a/artnet/plugin.yaml b/artnet/plugin.yaml index f03f81074..b4483b8e5 100755 --- a/artnet/plugin.yaml +++ b/artnet/plugin.yaml @@ -11,7 +11,7 @@ plugin: keywords: dmx # documentation: https://github.com/smarthomeNG/plugins/blob/develop/mqtt/README.md # url of documentation (wiki) page - version: 1.6.0 # Plugin version + version: 1.6.1 # Plugin version sh_minversion: 1.5.1 # minimum shNG version to use this plugin # sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) multi_instance: True @@ -54,7 +54,7 @@ parameters: de: 'Gibt das Art-Net Sub-Net an' en: 'Specifies the Art-Net Sub-Net to use' valid_min: 0 - valid_max: 15 + valid_max: 15 artnet_universe: type: int default: 0 @@ -85,7 +85,7 @@ item_attributes: logic_parameters: NONE -plugin_functions: +plugin_functions: send_single_value: type: void description: diff --git a/artnet/user_doc.rst b/artnet/user_doc.rst new file mode 100644 index 000000000..3fbdce443 --- /dev/null +++ b/artnet/user_doc.rst @@ -0,0 +1,136 @@ +.. index:: Plugins; artnet +.. index:: artnet + +====== +artnet +====== + +.. image:: webif/static/img/plugin_logo.png + :alt: plugin logo + :width: 300px + :height: 300px + :scale: 50 % + :align: left + +Anforderungen +============= + +Sie benötigen ein Gerät, das Artnet versteht. Es wird die Verwendung der Software +`OLA `_ empfohlen, +um die ArtNet-Pakete in DMX-Signale zu übersetzen. Alternativ kann auch +ein beliebiger Art-Net zu DMX Adapter verwendet werde. (Getestet mit `Radig Art-Net Box `_) OLA unterstützt +die meisten zur Zeit erhältlichen USB -> DMX-Adapter. Für Spezifikationen +des Art-Net ist die `Art-Net Doku `_ heranzuziehen. + +Unterstützte Hardware +===================== + +Von `OLA `_ unterstützte Hardware + +Konfiguration +============= + +.. important:: + + Detaillierte Informationen zur Konfiguration des Plugins sind unter :doc:`/plugins_doc/config/artnet` zu finden. + +plugins.yaml +~~~~~~~~~~~~ + +.. code-block:: yaml + + # etc/plugin.yaml + artnet1: + plugin_name: artnet + artnet_universe: 0 + artnet_net: 0 + artnet_subnet: 0 + ip: 192.168.0.99 + port: 6454 + update_cycle: 120 + instance: keller + +items.yaml +~~~~~~~~~~ + +.. code-block:: yaml + + lightbar: + red: + artnet_address@keller: 1 # DMX Adresse + green: + artnet_address@keller: 2 # DMX Adresse + blue: + artnet_address@keller: 3 # DMX Adresse + +logic.yaml +~~~~~~~~~~ + +Hinweis: Der erste DMX-Kanal ist 1! Nicht 0! + +Um DMX-Daten an das in plugin.yaml eingestellte "Universum" zu senden, gibt es vier +Möglichkeiten: + +a) Nutzen der Item-Einträge + +siehe oben + +b) einzelnen Wert senden + +``sh.artnet1(, )`` + +Setzt DMX_CHAN auf den Wert DMX_VALUE. + +Beispiel: ``sh.artnet1(12,255)`` +Wenn die Kanäle 1-11 bereits gesetzt sind, +werden sie nicht geändert. Wenn die Kanäle 1-11 noch nicht gesetzt sind, werden sie +auf den Wert 0 gesetzt. Dies ist notwendig, weil man bei einem DMX-Bus nicht nur einen +bestimmten Kanal einstellen kann. Sie müssen mit dem ersten Kanal beginnen und die +Werte einstellen. + +c) Liste von Werten ab einem bestimmten Kanal senden + +``sh.artnet1(, )`` + +Beispiel: ``sh.artnet1(10,[0,33,44,55,99])`` +Wenn die Kanäle 1-9 bereits eingestellt sind, werden sie nicht geändert. +Wenn die Kanäle 1-9 noch nicht gesetzt sind, wird der +auf den Wert 0 gesetzt werden. Dies ist notwendig, weil man auf einem DMX-Bus nicht +nur einen bestimmten Kanal einstellen kann. Sie müssen mit dem ersten Kanal beginnen +mit der Einstellung der Werte. Die Werte in eckigen Klammern werden an die Kanäle 10-14 geschickt. + +d) Liste von Werten setzen + +``sh.artnet1()`` + +Sendet an den DMX Bus beginnend mit Kanal 1, prinzipiell äquivalent mit Variante c. + +Beispiel: ``sh.artnet1([0,33,44,55,99])`` +Die Werte in eckigen Klammern werden auf den Kanal (1-5) geschrieben + +Web Interface +============= + +Das Web Interface enthält folgende Informationen: + +- **Pfad**: Itempfad + +- **Typ**: Itemtyp + +- **Artnet-Kanal**: Nummer des Kanals + +- **Artnet-Wert**: Artnet Wert + +- **Itemwert**: Wert des Items + +- **Letztes Update**: Zeit und Datum der letzten Itemaktualisierung + +- **Letzter Change**: Zeit und Datum der letzten Itemänderung + + +.. image:: assets/artnet_webif.png + :height: 1150px + :width: 2782px + :scale: 30% + :alt: Web Interface + :align: center diff --git a/artnet/webif/__init__.py b/artnet/webif/__init__.py new file mode 100755 index 000000000..25b8162a0 --- /dev/null +++ b/artnet/webif/__init__.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +# vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab +######################################################################### +# Copyright 2020- +######################################################################### +# This file is part of SmartHomeNG. +# https://www.smarthomeNG.de +# https://knx-user-forum.de/forum/supportforen/smarthome-py +# +# Sample plugin for new plugins to run with SmartHomeNG version 1.5 and +# upwards. +# +# 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 logging +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.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 beeing rendered + """ + tabcount = 1 + + tmpl = self.tplenv.get_template('index.html') + pagelength = self.plugin.get_parameter_value('webif_pagelength') + return tmpl.render(plugin_shortname=self.plugin.get_shortname(), + plugin_version=self.plugin.get_version(), + plugin_info=self.plugin.get_info(), + webif_pagelength=pagelength, + tabcount=tabcount, + p=self.plugin) diff --git a/artnet/webif/templates/index.html b/artnet/webif/templates/index.html index 5a434a427..582a3e09b 100755 --- a/artnet/webif/templates/index.html +++ b/artnet/webif/templates/index.html @@ -4,7 +4,20 @@ {% if language not in ['en','de'] %} {% set language = 'en' %} {% endif %} +{% block pluginscripts %} + +{% endblock pluginscripts %} {% block headtable %} @@ -29,12 +42,11 @@
{% endblock %} + {% block bodytab1 %} -
-
- +
- + @@ -51,7 +63,7 @@ {% else %} {% set instance_key = "artnet_address" %} {% endif %} - + @@ -63,6 +75,4 @@ {% endfor %}
{{ _('Pfad') }} {{ _('Typ') }} {{ _('Artnet-Kanal') }}
{{ item.id() }} {{ item.type() }} {{ item.conf[instance_key] }}
-
-
-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/asterisk/__init__.py b/asterisk/__init__.py index d67cc5e18..b415b3427 100755 --- a/asterisk/__init__.py +++ b/asterisk/__init__.py @@ -31,7 +31,7 @@ class Asterisk(SmartPlugin): - PLUGIN_VERSION = "1.4.1" + PLUGIN_VERSION = "1.4.2" DB = 'ast_db' DEV = 'ast_dev' @@ -237,8 +237,27 @@ def hangup(self, hang): self._command({'Action': 'Hangup', 'Channel': channel}, reply=False) def found_terminator(self, client, data): - """called upon data reception""" - data = data.decode() + """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(': ') diff --git a/asterisk/plugin.yaml b/asterisk/plugin.yaml index 02c37d02b..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.1 # 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/avm/__init__.py b/avm/__init__.py index b088acda5..84bc7a596 100644 --- a/avm/__init__.py +++ b/avm/__init__.py @@ -113,7 +113,7 @@ class AVM(SmartPlugin): """ Main class of the Plugin. Does all plugin specific stuff """ - PLUGIN_VERSION = '2.2.0' + PLUGIN_VERSION = '2.2.1' # ToDo: FritzHome.handle_updated_item: implement 'saturation' # ToDo: FritzHome.handle_updated_item: implement 'unmapped_hue' @@ -928,6 +928,9 @@ def cyclic_item_update(self, read_all: bool = False): item_count = 0 for item in self.item_list(): + if not self._plugin_instance.alive: + return + if not self.connected: self.logger.warning("FritzDevice not connected. No update of item values possible.") return @@ -2056,6 +2059,8 @@ def handle_updated_item(self, item, avm_data_type: str, readafterwrite: int): Updated Item will be processed and value communicated to AVM Device """ + self.logger.debug(f"handle_updated_item: item={item.path()}, {avm_data_type=}, value={item()}, {readafterwrite=}") + # define set method per avm_data_type // all avm_data_types of AHA_WO_ATTRIBUTES + AHA_RW_ATTRIBUTES must be defined here _dispatcher = {'window_open': (self.set_window_open, {'seconds': item()}, self.get_window_open), 'target_temperature': (self.set_target_temperature, {'temperature': item()}, self.get_target_temperature), @@ -2065,9 +2070,9 @@ def handle_updated_item(self, item, avm_data_type: str, readafterwrite: int): 'levelpercentage': (self.set_level_percentage, {'level': item()}, self.get_level_percentage), 'switch_state': (self.set_switch_state, {'state': item()}, self.get_switch_state), 'switch_toggle': (self.set_switch_state_toggle, {}, self.get_switch_state), - 'colortemperature': (self.set_color_temp, {'temperature': item(), 'duration': 1}, self.get_color_temp), - 'hue': (self.set_hue, {'hue': int(), 'duration': 1}, self.get_hue), - 'saturation': (self.set_saturation, {'hue': int(), 'duration': 1}, self.get_saturation), + 'colortemperature': (self.set_color_temp, {'temperature': item()}, self.get_color_temp), + 'hue': (self.set_hue, {'hue': item()}, self.get_hue), + 'saturation': (self.set_saturation, {'saturation': item()}, self.get_saturation), 'unmapped_hue': (self.set_unmapped_hue, {'hue': item()}, self.get_unmapped_hue), 'unmapped_saturation': (self.set_unmapped_saturation, {'saturation': item()}, self.get_unmapped_saturation), 'color': (self.set_color, {'hs': item(), 'duration': 1, 'mapped': False}, self.get_color), @@ -2663,36 +2668,34 @@ def get_state(self, ain: str): def set_level(self, ain: str, level: int): """ - Set level/brightness/height in interval [0,255]. + Set level/brightness/height in range 0-255 """ if not self.LEVEL_RANGE['min'] <= level <= self.LEVEL_RANGE['max']: level = clamp(level, self.LEVEL_RANGE['min'], self.LEVEL_RANGE['max']) - self.logger.warning(f"set_level: level value must be between {self.LEVEL_RANGE['min']} and {self.LEVEL_RANGE['max']}; hue will be set to {level}") - else: - level = int(level) + self.logger.warning(f"set_level: level value must be between {self.LEVEL_RANGE['min']} and {self.LEVEL_RANGE['max']}; level will be set to {level}") if not level and self.get_state(ain): self.set_state_off(ain) elif level and not self.get_state(ain): self.set_state_on(ain) - return self.aha_request("setlevel", ain=ain, param={'level': level}, result_type='int') + return self.aha_request("setlevel", ain=ain, param={'level': int(level)}, result_type='int') @NoKeyOrAttributeError def get_level(self, ain: str): """ - get level/brightness/height in interval [0,255]. + get level/brightness/height in range 0-255 """ return self.get_devices_as_dict()[ain].level def set_level_percentage(self, ain: str, level: int): """ - Set level/brightness/height in interval [0,100]. + Set level/brightness/height in range 0-100 """ if not self.LEVEL_PERCENTAGE_RANGE['min'] <= level <= self.LEVEL_PERCENTAGE_RANGE['max']: level = clamp(level, self.LEVEL_PERCENTAGE_RANGE['min'], self.LEVEL_PERCENTAGE_RANGE['max']) - self.logger.warning(f"set_level_percentage: level value must be between {self.LEVEL_PERCENTAGE_RANGE['min']} and {self.LEVEL_PERCENTAGE_RANGE['max']}; hue will be set to {level}") + self.logger.warning(f"set_level_percentage: level value must be between {self.LEVEL_PERCENTAGE_RANGE['min']} and {self.LEVEL_PERCENTAGE_RANGE['max']}; levelpercentage will be set to {level}") else: level = int(level) @@ -2706,7 +2709,7 @@ def set_level_percentage(self, ain: str, level: int): @NoKeyOrAttributeError def get_level_percentage(self, ain: str): """ - get level/brightness/height in interval [0,100]. + get level/brightness/height in range 0-100 """ return self.get_devices_as_dict()[ain].levelpercentage @@ -2720,59 +2723,69 @@ def _get_colordefaults(self, ain: str): @NoKeyOrAttributeError def get_hue(self, ain: str) -> int: - """get hue value represented in hsv domain as integer value between [0,359].""" + """ + get hue value represented in hsv domain as integer value between 0-359 + """ return self.get_devices_as_dict()[ain].hue - def set_hue(self, ain: str, hue: int) -> bool: - """set hue value (0-359)""" - self.logger.debug(f"set_unmapped_hue called with value={hue}") + def set_hue(self, ain: str, hue: int, duration: int = 1) -> bool: + """set hue value as integer value in range 0-359""" + self.logger.debug(f"set_hue called with value={hue}") - if (hue < 0) or hue > 359: - self.logger.error(f"set_unmapped_hue, hue value must be between 0 and 359") - return False + if not self.HUE_RANGE['min'] <= hue <= self.HUE_RANGE['max']: + hue = clamp(hue, self.HUE_RANGE['min'], self.HUE_RANGE['max']) + self.logger.error(f"set_hue: hue value must be between 0 and 359. hue set to {hue}") saturation = getattr(self.get_devices_as_dict()[ain], 'saturation', None) if saturation: - self.logger.debug(f"set_hue: set_unmapped_hue, hue {hue}, saturation is {saturation}") - # saturation variable is scaled to 0-100. Scale to 0-255 for AVM AHA interface - self.set_color(ain, [hue, saturation], mapped=False) + self.logger.debug(f"set_hue: {hue=}, {saturation=}") + self.set_color(ain, hs=[int(hue), saturation], duration=duration, mapped=False) return True + return False + @NoKeyOrAttributeError def get_saturation(self, ain: str) -> int: - """get saturation as integer value between 0-100.""" + """ + get saturation as integer value in range 0-255 + """ return self.get_devices_as_dict()[ain].saturation - def set_saturation(self, ain: str, saturation: int) -> bool: + def set_saturation(self, ain: str, saturation: int, duration: int = 1) -> bool: """ - set saturation value - saturation defined in range (0-100) + set saturation value as integer value in range 0-255 """ - self.logger.debug(f" set_unmapped_saturation is called with value={saturation} defined in range 0-100") + self.logger.debug(f"set_saturation called with value={saturation}") - if (saturation < 0) or saturation > 100: - self.logger.error(f"set_unmapped_saturation: value must be between 0 and 100") - return False + if not self.SATURATION_RANGE['min'] <= saturation <= self.SATURATION_RANGE['max']: + saturation = clamp(saturation, self.SATURATION_RANGE['min'], self.SATURATION_RANGE['max']) + self.logger.error(f"set_saturation: saturation value must be between 0 and 255. saturation set to {saturation}") hue = getattr(self.get_devices_as_dict()[ain], 'hue', None) if hue: - self.logger.debug(f"success: set_saturation: value is {saturation} (0-100), hue {hue}") - # Plugin handels saturation value in the range of 0-100. AVM function expect saturation to be within 0-255. Therefore, scale value: - self.set_color(ain, [hue, int(saturation*2.55)], mapped=False) + self.logger.debug(f"success: set_saturation: {saturation=}, hue {hue=}") + self.set_color(ain, hs=[hue, int(saturation)], duration=duration, mapped=False) + return True + + return False @NoKeyOrAttributeError def get_unmapped_hue(self, ain: str) -> int: - """get unmapped hue value represented in hsv domain as integer value between [0,359].""" + """ + get unmapped hue value represented in hsv domain as integer value in range 0-359 + """ self.logger.debug("get_unmapped_hue called.") return self.get_devices_as_dict()[ain].unmapped_hue - def set_unmapped_hue(self, ain: str, hue: int) -> bool: - """set hue value (0-359)""" + def set_unmapped_hue(self, ain: str, hue: int, duration: int = 1) -> bool: + """ + set hue value as integer value in range 0-359 + """ self.logger.debug(f"set_unmapped_hue called with value={hue}") - if (hue < 0) or hue > 359: - self.logger.error(f"set_unmapped_hue, hue value must be between 0 and 359") - return False + if not self.HUE_RANGE['min'] <= hue <= self.HUE_RANGE['max']: + hue = clamp(hue, self.HUE_RANGE['min'], self.HUE_RANGE['max']) + self.logger.error(f"set_unmapped_hue: hue value must be between 0 and 359. hue set to {hue}") saturation = getattr(self.get_devices_as_dict()[ain], 'unmapped_saturation', None) if not saturation: @@ -2780,36 +2793,42 @@ def set_unmapped_hue(self, ain: str, hue: int) -> bool: saturation = getattr(self.get_devices_as_dict()[ain], 'saturation', None) if saturation: - self.logger.debug(f"set_unmapped_hue: set_unmapped_hue, hue {hue}, saturation is {saturation}") - # saturation variable is scaled to 0-100. Scale to 0-255 for AVM AHA interface - self.set_color(ain, [hue, saturation], mapped=False) + self.logger.debug(f"set_unmapped_hue: hue {hue}, saturation {saturation}") + self.set_color(ain, hs=[int(hue), saturation], duration=duration, mapped=False) return True + return False + @NoKeyOrAttributeError def get_unmapped_saturation(self, ain: str) -> int: - """get saturation as integer value between 0-100.""" + """ + get saturation as integer value between 0-255. + """ self.logger.warning("Debug: get_unmapped_saturation called.") return self.get_devices_as_dict()[ain].unmapped_saturation - def set_unmapped_saturation(self, ain: str, saturation: int) -> bool: + def set_unmapped_saturation(self, ain: str, saturation: int, duration: int = 1) -> bool: """ - set saturation value - saturation defined in range (0-100) + set saturation value as integer value in range 0-255 """ - self.logger.debug(f" set_unmapped_saturation is called with value={saturation} defined in range 0-100") - if (saturation < 0) or saturation > 100: - self.logger.error(f"set_unmapped_saturation: value must be between 0 and 100") - return False + self.logger.debug(f" set_unmapped_saturation called with value={saturation}") + + if not self.SATURATION_RANGE['min'] <= saturation <= self.SATURATION_RANGE['max']: + saturation = clamp(saturation, self.SATURATION_RANGE['min'], self.SATURATION_RANGE['max']) + self.logger.error(f"set_saturation: saturation value must be between 0 and 255. saturation set to {saturation}") hue = getattr(self.get_devices_as_dict()[ain], 'unmapped_hue', None) if not hue: self.logger.info(f"set_unmapped_saturation: unable to get value for 'unmapped_hue', try to use value for 'hue'") hue = getattr(self.get_devices_as_dict()[ain], 'hue', None) + if hue: - self.logger.debug(f"success: set_unmapped_saturation: value is {saturation} (0-100), hue {hue}") - # Plugin handels saturation value in the range of 0-100. AVM function expect saturation to be within 0-255. Therefore, scale value: - self.set_color(ain, [hue, int(saturation*2.55)], mapped=False) + self.logger.debug(f"success: set_unmapped_saturation: {saturation=}, {hue=}") + self.set_color(ain, hs=[hue, int(saturation)], duration=duration, mapped=False) + return True + + return False def get_colors(self, ain: str) -> dict: """ @@ -2837,7 +2856,7 @@ def set_color(self, ain: str, hs: list, duration: int = 1, mapped: bool = True) hs: colorspace element obtained from get_colors() hs is an array including hue, saturation and level hue must be within range 0-359 - saturation must be within range 0-100 + saturation must be within range 0-255 duration: Speed of change in seconds, 0 = instant mapped = True uses the AVM setcolor function. It only supports pre-defined colors that can be obtained by the get_colors function. mapped = False uses the AVM setunmappedcolor function, featured by AVM firmwareversion since approximately Q2 2022. It supports every combination if hue/saturation/level @@ -2848,7 +2867,7 @@ def set_color(self, ain: str, hs: list, duration: int = 1, mapped: bool = True) return False hue = to_int(hs[0]) - saturation = int(to_int(hs[1])*2.55) + saturation = to_int(hs[1]) duration = to_int(duration) * 10 # Range checks: @@ -2883,21 +2902,24 @@ def set_color(self, ain: str, hs: list, duration: int = 1, mapped: bool = True) @NoKeyOrAttributeError def get_color(self, ain: str) -> list: - """get hue, saturation value as list""" + """ + get hue, saturation value as list + """ return self.get_devices_as_dict()[ain].color def get_hsv(self, ain: str) -> list: - """get hue, saturation, level value as list""" + """ + get hue, saturation, level value as list + """ return self.get_devices_as_dict()[ain].hsv def set_hsv(self, ain: str, hsv: list, duration: int = 1, mapped: bool = True) -> bool: """ Set hue, saturation, level. - hsv: colorspace element obtained from get_colors() - hsv is an array including hue, saturation and level + hsv: array including hue, saturation and level hue must be within range 0-359 - saturation must be within range 0-100 - value must be within range 0-100 + saturation must be within range 0-255 + value must be within range 0-255 duration: Speed of change in seconds, 0 = instant mapped = True uses the AVM setcolor function. It only supports pre-defined colors that can be obtained by the get_colors function. mapped = False uses the AVM setunmappedcolor function, featured by AVM firmwareversion since approximately Q2 2022. It supports every combination if hue/saturation/level @@ -2912,12 +2934,12 @@ def set_hsv(self, ain: str, hsv: list, duration: int = 1, mapped: bool = True) - hue, saturation, level = hsv result_hs = self.set_color(ain, [hue, saturation], duration, mapped) - result_l = self.set_level_percentage(ain, level) + result_l = self.set_level(ain, level) self.logger.debug(f"set_hsv: in mapped '{mapped}': result_hs={result_hs}, result_l={result_l}") return result_hs & result_l - def set_color_discrete(self, ain, hue, duration=0): + def set_color_discrete(self, ain: str, hue: int, duration: int = 0): """ Set Led color to the closest discrete hue value. Currently, only those are supported for FritzDect500 RGB LED bulbs """ @@ -2963,7 +2985,7 @@ def set_color_discrete(self, ain, hue, duration=0): return self.aha_request("setcolor", ain=ain, param=param, result_type='int') - def get_color_temps(self, ain): + def get_color_temps(self, ain: str): """ Get temperatures supported by this lightbulb. """ @@ -3793,11 +3815,9 @@ def _update_color_from_node(self, node): self.fullcolorsupport = bool(colorcontrol_element.attrib.get("fullcolorsupport")) self.mapped = bool(colorcontrol_element.attrib.get("mapped")) self.hue = get_node_value_as_int(colorcontrol_element, "hue") - saturation = get_node_value_as_int(colorcontrol_element, "saturation") - self.saturation = int(saturation/2.55) + self.saturation = get_node_value_as_int(colorcontrol_element, "saturation") self.unmapped_hue = get_node_value_as_int(colorcontrol_element, "unmapped_hue") - unmapped_saturation = get_node_value_as_int(colorcontrol_element, "unmapped_saturation") - self.unmapped_saturation = int(unmapped_saturation/2.55) + self.unmapped_saturation = get_node_value_as_int(colorcontrol_element, "unmapped_saturation") self.colortemperature = get_node_value_as_int(colorcontrol_element, "temperature") if self.mapped: @@ -3807,22 +3827,22 @@ def _update_color_from_node(self, node): self.logger.debug(f"FritzColor: created color={self.color} with mapped={self.mapped}") - # get levelpercentage + # get level levelcontrol_element = node.find("levelcontrol") if levelcontrol_element is not None: - levelpercentage = get_node_value_as_int(levelcontrol_element, "levelpercentage") + level = get_node_value_as_int(levelcontrol_element, "level") else: - levelpercentage = 0 + level = 0 # Set Level to zero for consistency, if light is off: state_element = node.find("simpleonoff") if state_element is not None: simpleonoff = get_node_value_as_int_as_bool(state_element, "state") if simpleonoff is False: - levelpercentage = 0 + level = 0 self.hsv = self.color.copy() - self.hsv.append(levelpercentage) + self.hsv.append(level) def get_colors(self): """Get the supported colors.""" diff --git a/avm/item_attributes_master.py b/avm/item_attributes_master.py index 79fd7691d..ad21d92fa 100644 --- a/avm/item_attributes_master.py +++ b/avm/item_attributes_master.py @@ -200,7 +200,7 @@ 'unmapped_hue': {'interface': 'aha', 'group': 'color', 'sub_group': None, 'access': 'rw', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Hue mit Wertebereich von 0° bis 359° (Status und Setzen)'}, 'unmapped_saturation': {'interface': 'aha', 'group': 'color', 'sub_group': None, 'access': 'rw', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Saturation mit Wertebereich von 0 bis 255 (Status und Setzen)'}, 'color': {'interface': 'aha', 'group': 'color', 'sub_group': None, 'access': 'rw', 'type': 'list ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Farbwerte als Liste [Hue, Saturation] (Status und Setzen)'}, - 'hsv': {'interface': 'aha', 'group': 'color', 'sub_group': None, 'access': 'rw', 'type': 'list ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Farbwerte und Helligkeit als Liste [Hue, Saturation, Level in Prozent] (Status und Setzen)'}, + 'hsv': {'interface': 'aha', 'group': 'color', 'sub_group': None, 'access': 'rw', 'type': 'list ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Farbwerte und Helligkeit als Liste [Hue (0-359), Saturation (0-255), Level (0-255)] (Status und Setzen)'}, 'color_mode': {'interface': 'aha', 'group': 'color', 'sub_group': None, 'access': 'ro', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Aktueller Farbmodus (1-HueSaturation-Mode; 4-Farbtemperatur-Mode)'}, 'supported_color_mode': {'interface': 'aha', 'group': 'color', 'sub_group': None, 'access': 'ro', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Unterstützer Farbmodus (1-HueSaturation-Mode; 4-Farbtemperatur-Mode)'}, 'fullcolorsupport': {'interface': 'aha', 'group': 'color', 'sub_group': None, 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Lampe unterstützt setunmappedcolor'}, diff --git a/avm/plugin.yaml b/avm/plugin.yaml index bdc88c6f0..d9762b68a 100644 --- a/avm/plugin.yaml +++ b/avm/plugin.yaml @@ -12,7 +12,7 @@ plugin: documentation: http://smarthomeng.de/user/plugins/avm/user_doc.html support: https://knx-user-forum.de/forum/supportforen/smarthome-py/934835-avm-plugin - version: 2.2.0 # Plugin version (must match the version specified in __init__.py) + version: 2.2.1 # Plugin version (must match the version specified in __init__.py) sh_minversion: 1.8 # 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 @@ -425,19 +425,19 @@ item_attributes: # aha-level Attributes - Level/Niveau von 0 bis 255 (Setzen) - Level/Niveau von 0 bis 255 (Setzen & Status) - - Level/Niveau von 0% bis 100% (Setzen) - - Level/Niveau von 0% bis 100% (Setzen & Status) + - Level/Niveau in Prozent von 0% bis 100% (Setzen) + - Level/Niveau in Prozent von 0% bis 100% (Setzen & Status) # aha-color Attributes - - Hue (Setzen) - - Hue (Status und Setzen) - - Saturation (Setzen) - - Saturation (Status und Setzen) - - Farbtemperatur (Setzen) - - Farbtemperatur (Status und Setzen) - - Hue (Status und Setzen) - - Saturation (Status und Setzen) - - Farbwerte als Liste h,s (Status und Setzen) - - Farbwerte und Helligkeit als Liste h,s,v (Status und Setzen) + - Hue mit Wertebereich von 0° bis 359° (Setzen) + - Hue mit Wertebereich von 0° bis 359° (Status und Setzen) + - Saturation mit Wertebereich von 0 bis 255 (Setzen) + - Saturation mit Wertebereich von 0 bis 255 (Status und Setzen) + - Farbtemperatur mit Wertebereich von 2700K bis 6500K (Setzen) + - Farbtemperatur mit Wertebereich von 2700K bis 6500K (Status und Setzen) + - Hue mit Wertebereich von 0° bis 359° (Status und Setzen) + - Saturation mit Wertebereich von 0 bis 255 (Status und Setzen) + - Farbwerte als Liste [Hue, Saturation] (Status und Setzen) + - Farbwerte und Helligkeit als Liste [Hue (0-359), Saturation (0-255), Level (0-255)] (Status und Setzen) - Aktueller Farbmodus (1-HueSaturation-Mode; 4-Farbtemperatur-Mode) - Unterstützer Farbmodus (1-HueSaturation-Mode; 4-Farbtemperatur-Mode) - Lampe unterstützt setunmappedcolor diff --git a/avm/user_doc.rst b/avm/user_doc.rst index a6d8be7f0..7ce046853 100644 --- a/avm/user_doc.rst +++ b/avm/user_doc.rst @@ -336,7 +336,7 @@ AHA-Interface - color: Farbwerte als Liste [Hue, Saturation] (Status und Setzen) | Zugriff: rw | Item-Type: list -- hsv: Farbwerte und Helligkeit als Liste [Hue, Saturation, Level in Prozent] (Status und Setzen) | Zugriff: rw | Item-Type: list +- hsv: Farbwerte und Helligkeit als Liste [Hue (0-359), Saturation (0-255), Level (0-255)] (Status und Setzen) | Zugriff: rw | Item-Type: list - color_mode: Aktueller Farbmodus (1-HueSaturation-Mode; 4-Farbtemperatur-Mode) | Zugriff: ro | Item-Type: num diff --git a/byd_bat/__init__.py b/byd_bat/__init__.py index 91594310c..5419a7224 100644 --- a/byd_bat/__init__.py +++ b/byd_bat/__init__.py @@ -37,6 +37,10 @@ # # V0.0.4 230904 - Bilder JPG in PNG konvertiert fuer user_doc.rst # +# V0.0.5 231030 - Diagnose ergaenzt: Bat-Voltag, V-Out, Current +# - Liste der Wechselrichter aktualisiert +# - Alle Plot-Dateien beim Plugin-Start loeschen +# # ----------------------------------------------------------------------- # # Als Basis fuer die Implementierung wurde die folgende Quelle verwendet: @@ -45,7 +49,10 @@ # # Diverse Notizen # -# - Datenpaket wird mit CRC16/MODBUS am Ende abgeschlossen (2 Byte, LSB,MSB) +# - Beginn Frame (Senden/Empfangen): 0x01 0x03 +# - Antwort 3.Byte (direkt nach Header): Anzahl Bytes Nutzdaten (2 Byte CRC werden mitgezaehlt) +# - Datenpaket wird mit CRC16/MODBUS am Ende abgeschlossen (2 Byte, LSB,MSB) (Nutzdaten+Längenbyte) +# - Der Server im BYD akzeptiert nur 1 Verbindung auf Port 8080/TCP ! # # ----------------------------------------------------------------------- @@ -59,6 +66,7 @@ import matplotlib import matplotlib.pyplot as plt import numpy as np +import os byd_ip_default = "192.168.16.254" @@ -72,7 +80,7 @@ byd_timeout_2s = 2.0 byd_timeout_8s = 8.0 -byd_tours_max = 3 +byd_towers_max = 3 byd_cells_max = 160 byd_temps_max = 64 @@ -122,48 +130,52 @@ "Low Temperature Discharging (Cells)" ] +byd_invs_max = 19 byd_invs = [ - "Fronius HV", - "Goodwe HV", - "Fronius HV", - "Kostal HV", - "Goodwe HV", - "SMA SBS3.7/5.0", - "Kostal HV", - "SMA SBS3.7/5.0", - "Sungrow HV", - "Sungrow HV", - "Kaco HV", - "Kaco HV", - "Ingeteam HV", - "Ingeteam HV", - "SMA SBS 2.5 HV", - "", - "SMA SBS 2.5 HV", - "Fronius HV" + "Fronius HV", # 0 + "Goodwe HV", # 1 + "Fronius HV", # 2 + "Kostal HV", # 3 + "Goodwe HV", # 4 + "SMA SBS3.7/5.0", # 5 + "Kostal HV", # 6 + "SMA SBS3.7/5.0", # 7 + "Sungrow HV", # 8 + "Sungrow HV", # 9 + "Kaco HV", # 10 + "Kaco HV", # 11 + "Ingeteam HV", # 12 + "Ingeteam HV", # 13 + "SMA SBS 2.5 HV", # 14 + "", # 15 + "SMA SBS 2.5 HV", # 16 + "Fronius HV", # 17 + "", # 18 + "SMA STPx.0-3SE-40" # 19 ] +byd_invs_lvs_max = 19 byd_invs_lvs = [ - "Fronius HV", - "Goodwe HV", - "Goodwe HV", - "Kostal HV", - "Selectronic LV", - "SMA SBS3.7/5.0", - "SMA LV", - "Victron LV", - "Suntech LV", - "Sungrow HV", - "Kaco HV", - "Studer LV", - "Solar Edge LV", - "Ingeteam HV", - "Sungrow LV", - "Schneider LV", - "SMA SBS2.5 HV", - "Solar Edge LV", - "Solar Edge LV", - "Solar Edge LV" + "Fronius HV", # 0 + "Goodwe HV", # 1 + "Goodwe HV", # 2 + "Kostal HV", # 3 + "Selectronic LV", # 4 + "SMA SBS3.7/5.0", # 5 + "SMA LV", # 6 + "Victron LV", # 7 + "Suntech LV", # 8 + "Sungrow HV", # 9 + "Kaco HV", # 10 + "Studer LV", # 11 + "Solar Edge LV", # 12 + "Ingeteam HV", # 13 + "Sungrow LV", # 14 + "Schneider LV", # 15 + "SMA SBS2.5 HV", # 16 + "Solar Edge LV", # 17 + "Solar Edge LV", # 18 + "Solar Edge LV" # 19 ] @@ -178,7 +190,7 @@ class properties and methods (class variables and class functions) are already available! """ - PLUGIN_VERSION = '0.0.4' + PLUGIN_VERSION = '0.0.5' def __init__(self,sh): """ @@ -226,6 +238,9 @@ def __init__(self,sh): self.byd_root_found = False self.byd_diag_soc = [] + self.byd_diag_bat_voltag = [] + self.byd_diag_v_out = [] + self.byd_diag_current = [] self.byd_diag_volt_max = [] self.byd_diag_volt_max_c = [] self.byd_diag_volt_min = [] @@ -234,8 +249,11 @@ def __init__(self,sh): self.byd_diag_temp_min_c = [] self.byd_volt_cell = [] self.byd_temp_cell = [] - for x in range(0,byd_tours_max + 1): + for x in range(0,byd_towers_max + 1): self.byd_diag_soc.append(0) + self.byd_diag_bat_voltag.append(0) + self.byd_diag_v_out.append(0) + self.byd_diag_current.append(0) self.byd_diag_volt_max.append(0) self.byd_diag_volt_max_c.append(0) self.byd_diag_volt_min.append(0) @@ -253,6 +271,10 @@ def __init__(self,sh): self.last_homedata = self.now_str() self.last_diagdata = self.now_str() + + self.plt_file_del() + + # self.simulate_data() # for internal tests only # Initialization code goes here @@ -507,7 +529,7 @@ def decode_0(self,data): # Anzahl Tuerme und Anzahl Module pro Turm self.byd_bms_qty = data[36] // 0x10 - if (self.byd_bms_qty == 0) or (self.byd_bms_qty > byd_tours_max): + if (self.byd_bms_qty == 0) or (self.byd_bms_qty > byd_towers_max): self.byd_bms_qty = 1 self.byd_modules = data[36] % 0x10 self.byd_batt_type_snr = data[5] @@ -541,7 +563,7 @@ def decode_1(self,data): self.byd_volt_max = self.buf2int16SI(data,5) * 1.0 / 100.0 self.byd_volt_min = self.buf2int16SI(data,7) * 1.0 / 100.0 self.byd_volt_diff = self.byd_volt_max - self.byd_volt_min - self.byd_current = self.buf2int16SI(data,11) * 1.0 / 10.0 + self.byd_current = self.buf2int16SI(data,11) * 1.0 / 10.0 # Byte 11+12 self.byd_power = self.byd_volt_out * self.byd_current if self.byd_power >= 0: self.byd_power_discharge = self.byd_power @@ -632,9 +654,15 @@ def decode_2(self,data): self.byd_inv_type = data[3] if self.byd_batt_str == "LVS": - self.byd_inv_str = byd_invs_lvs[self.byd_inv_type] + if self.byd_inv_type <= byd_invs_lvs_max: + self.byd_inv_str = byd_invs_lvs[self.byd_inv_type] + else: + self.byd_inv_str = "unknown" else: - self.byd_inv_str = byd_invs[self.byd_inv_type] + if self.byd_inv_type <= byd_invs_max: + self.byd_inv_str = byd_invs[self.byd_inv_type] + else: + self.byd_inv_str = "unknown" self.log_debug("Inv Type : " + self.byd_inv_str + " (" + str(self.byd_inv_type) + ")") self.log_debug("Batt Type : " + self.byd_batt_str + " (" + str(self.byd_batt_type) + ")") @@ -652,7 +680,10 @@ def decode_5(self,data,x): self.log_debug("decode_5 (" + str(x) + ") : " + data.hex()) - self.byd_diag_soc[x] = self.buf2int16SI(data,53) * 1.0 / 10.0 + self.byd_diag_soc[x] = self.buf2int16SI(data,53) * 1.0 / 10.0 # Byte 53+54 + self.byd_diag_bat_voltag[x] = self.buf2int16SI(data,45) * 1.0 / 10.0 # Byte 45+46 + self.byd_diag_v_out[x] = self.buf2int16SI(data,51) * 1.0 / 10.0 # Byte 51+52 + self.byd_diag_current[x] = self.buf2int16SI(data,57) * 1.0 / 10.0 # Byte 57+58 self.byd_diag_volt_max[x] = self.buf2int16SI(data,5) / 1000.0 self.byd_diag_volt_max_c[x] = data[9] self.byd_diag_volt_min[x] = self.buf2int16SI(data,7) / 1000.0 @@ -664,11 +695,14 @@ def decode_5(self,data,x): for xx in range(0,16): self.byd_volt_cell[x][xx] = self.buf2int16SI(data,101 + (xx * 2)) / 1000.0 - self.log_debug("SOC : " + str(self.byd_diag_soc[x])) - self.log_debug("Volt max : " + str(self.byd_diag_volt_max[x]) + " c=" + str(self.byd_diag_volt_max_c[x])) - self.log_debug("Volt min : " + str(self.byd_diag_volt_min[x]) + " c=" + str(self.byd_diag_volt_min_c[x])) - self.log_debug("Temp max : " + " c=" + str(self.byd_diag_temp_max_c[x])) - self.log_debug("Temp min : " + " c=" + str(self.byd_diag_temp_min_c[x])) + self.log_debug("SOC : " + str(self.byd_diag_soc[x])) + self.log_debug("Bat Voltag : " + str(self.byd_diag_bat_voltag[x])) + self.log_debug("V-Out : " + str(self.byd_diag_v_out[x])) + self.log_debug("Current : " + str(self.byd_diag_current[x])) + self.log_debug("Volt max : " + str(self.byd_diag_volt_max[x]) + " c=" + str(self.byd_diag_volt_max_c[x])) + self.log_debug("Volt min : " + str(self.byd_diag_volt_min[x]) + " c=" + str(self.byd_diag_volt_min_c[x])) + self.log_debug("Temp max : " + " c=" + str(self.byd_diag_temp_max_c[x])) + self.log_debug("Temp min : " + " c=" + str(self.byd_diag_temp_min_c[x])) # for xx in range(0,16): # self.log_debug("Turm " + str(x) + " Volt " + str(xx) + " : " + str(self.byd_volt_cell[x][xx])) @@ -781,6 +815,9 @@ def diagdata_save(self,device): def diagdata_save_one(self,device,x): device.soc(self.byd_diag_soc[x]) + device.bat_voltag(self.byd_diag_bat_voltag[x]) + device.v_out(self.byd_diag_v_out[x]) + device.current(self.byd_diag_current[x]) device.volt_max.volt(self.byd_diag_volt_max[x]) device.volt_max.cell(self.byd_diag_volt_max_c[x]) device.volt_min.volt(self.byd_diag_volt_min[x]) @@ -831,9 +868,9 @@ def diag_plot(self,x): ax.spines[:].set_visible(False) ax.set_xticks(np.arange(dd.shape[1] + 1) - .5,minor=True) - ax.set_yticks(np.arange(dd.shape[0] + 1) - .5,minor=True,size=10) + ax.set_yticks(np.arange(dd.shape[0] + 1) - .5,minor=True) ax.tick_params(which='minor',bottom=False,left=False) - ax.tick_params(axis='y',colors='white') + ax.tick_params(axis='y',colors='white',labelsize=10) textcolors = ("white","black") threshold = im.norm(dd.max()) / 2. @@ -891,9 +928,9 @@ def diag_plot(self,x): ax.spines[:].set_visible(False) ax.set_xticks(np.arange(dd.shape[1] + 1) - .5,minor=True) - ax.set_yticks(np.arange(dd.shape[0] + 1) - .5,minor=True,size=10) + ax.set_yticks(np.arange(dd.shape[0] + 1) - .5,minor=True) ax.tick_params(which='minor',bottom=False,left=False) - ax.tick_params(axis='y',colors='white') + ax.tick_params(axis='y',colors='white',labelsize=10) textcolors = ("black","white") threshold = im.norm(dd.max()) / 2. @@ -918,6 +955,55 @@ def diag_plot(self,x): plt.close('all') return + + def plt_file_del(self): + # Loescht alle Plot-Dateien + + # Spannungs-Plots + fn = self.get_plugin_dir() + byd_webif_img + byd_fname_volt + str(1) + byd_fname_ext + if os.path.exists(fn) == True: + os.remove(fn) + fn = self.get_plugin_dir() + byd_webif_img + byd_fname_volt + str(2) + byd_fname_ext + if os.path.exists(fn) == True: + os.remove(fn) + fn = self.get_plugin_dir() + byd_webif_img + byd_fname_volt + str(3) + byd_fname_ext + if os.path.exists(fn) == True: + os.remove(fn) + + if len(self.bpath) != byd_path_empty: + fn = self.bpath + byd_fname_volt + str(1) + byd_fname_ext + if os.path.exists(fn) == True: + os.remove(fn) + fn = self.bpath + byd_fname_volt + str(2) + byd_fname_ext + if os.path.exists(fn) == True: + os.remove(fn) + fn = self.bpath + byd_fname_volt + str(3) + byd_fname_ext + if os.path.exists(fn) == True: + os.remove(fn) + + # Temperatur-Plots + fn = self.get_plugin_dir() + byd_webif_img + byd_fname_temp + str(1) + byd_fname_ext + if os.path.exists(fn) == True: + os.remove(fn) + fn = self.get_plugin_dir() + byd_webif_img + byd_fname_temp + str(2) + byd_fname_ext + if os.path.exists(fn) == True: + os.remove(fn) + fn = self.get_plugin_dir() + byd_webif_img + byd_fname_temp + str(3) + byd_fname_ext + if os.path.exists(fn) == True: + os.remove(fn) + + if len(self.bpath) != byd_path_empty: + fn = self.bpath + byd_fname_temp + str(1) + byd_fname_ext + if os.path.exists(fn) == True: + os.remove(fn) + fn = self.bpath + byd_fname_temp + str(2) + byd_fname_ext + if os.path.exists(fn) == True: + os.remove(fn) + fn = self.bpath + byd_fname_temp + str(3) + byd_fname_ext + if os.path.exists(fn) == True: + os.remove(fn) + + return def buf2int16SI(self,byteArray,pos): # signed result = byteArray[pos] * 256 + byteArray[pos + 1] @@ -981,3 +1067,27 @@ def init_webinterface(self): description='') return True + + def simulate_data(self): + # For internal tests only + self.byd_modules = 7 + self.byd_batt_str = "HVM" + self.byd_capacity_module = 2.76 + self.byd_volt_n = 16 + self.byd_temp_n = 8 + self.byd_cells_n = self.byd_modules * self.byd_volt_n + self.byd_temps_n = self.byd_modules * self.byd_temp_n + + for xx in range(0,self.byd_cells_n): + if (xx % 2) == 0: + self.byd_volt_cell[1][xx] = 2.2 + else: + self.byd_volt_cell[1][xx] = 2.4 + for xx in range(0,self.byd_temps_n): + if (xx % 2) == 0: + self.byd_temp_cell[1][xx] = 23 + else: + self.byd_temp_cell[1][xx] = 26 + + self.diag_plot(1) + \ No newline at end of file diff --git a/byd_bat/locale.yaml b/byd_bat/locale.yaml index f29ff2617..0d6f17c2b 100644 --- a/byd_bat/locale.yaml +++ b/byd_bat/locale.yaml @@ -33,6 +33,14 @@ plugin_translations: 'Module pro Turm': {'de': '=', 'en': 'Modules per tower'} 'Parameter': {'de': '=', 'en': 'Parameter'} 'Fehler': {'de': '=', 'en': 'Error'} + + 'Batteriespannung': {'de': '=', 'en': 'Batteryvoltage'} + 'Spannung Out': {'de': '=', 'en': 'Voltage Out'} + 'Strom': {'de': '=', 'en': 'Current'} + 'Spannung max (Zelle)': {'de': '=', 'en': 'Voltage max (cell)'} + 'Spannung min (Zelle)': {'de': '=', 'en': 'Voltage min (cell)'} + 'Temperatur Zelle max': {'de': '=', 'en': 'Temperature cell max'} + 'Temperatur Zelle min': {'de': '=', 'en': 'Temperature cell min'} # Alternative format for translations of longer texts: 'Hier kommt der Inhalt des Webinterfaces hin.': diff --git a/byd_bat/plugin.yaml b/byd_bat/plugin.yaml index 305458573..8186f4e6d 100644 --- a/byd_bat/plugin.yaml +++ b/byd_bat/plugin.yaml @@ -12,10 +12,10 @@ plugin: # documentation: https://github.com/smarthomeNG/smarthome/wiki/CLI-Plugin # url of documentation (wiki) page support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1886748-support-thread-f%C3%BCr-das-byd-batterie-plugin - version: 0.0.4 # Plugin version (must match the version specified in __init__.py) + version: 0.0.5 # Plugin version (must match the version specified in __init__.py) sh_minversion: 1.9 # minimum shNG version to use this plugin # sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) -# py_minversion: 3.6 # minimum Python version to use for this plugin + py_minversion: 3.9 # minimum Python version to use for this plugin # py_maxversion: # maximum Python version to use for this plugin (leave empty if latest) multi_instance: false # plugin supports multi instance restartable: true @@ -213,6 +213,21 @@ item_structs: visu_acl: ro database: init + bat_voltag: # BAT Voltage [V] + type: num + visu_acl: ro + database: init + + v_out: # V-Out [V] + type: num + visu_acl: ro + database: init + + current: # Current [A] (+ discharge / - charge battery) + type: num + visu_acl: ro + database: init + volt_max: volt: # max voltage [V] @@ -254,6 +269,21 @@ item_structs: visu_acl: ro database: init + bat_voltag: # BAT Voltage [V] + type: num + visu_acl: ro + database: init + + v_out: # V-Out [V] + type: num + visu_acl: ro + database: init + + current: # Current [A] (+ discharge / - charge battery) + type: num + visu_acl: ro + database: init + volt_max: volt: # max voltage [V] @@ -295,6 +325,21 @@ item_structs: visu_acl: ro database: init + bat_voltag: # BAT Voltage [V] + type: num + visu_acl: ro + database: init + + v_out: # V-Out [V] + type: num + visu_acl: ro + database: init + + current: # Current [A] (+ discharge / - charge battery) + type: num + visu_acl: ro + database: init + volt_max: volt: # max voltage [V] diff --git a/byd_bat/requirements.txt b/byd_bat/requirements.txt index 6ccafc3f9..11c0a0a75 100644 --- a/byd_bat/requirements.txt +++ b/byd_bat/requirements.txt @@ -1 +1 @@ -matplotlib +matplotlib>=3.8.0 diff --git a/byd_bat/webif/__init__.py b/byd_bat/webif/__init__.py index 0bdd656ac..60b1fd383 100644 --- a/byd_bat/webif/__init__.py +++ b/byd_bat/webif/__init__.py @@ -124,34 +124,45 @@ def get_data_html(self, dataSet=None): data['serial'] = self.plugin.byd_serial data['t1_soc'] = f'{self.plugin.byd_diag_soc[1]:.1f}' + " %" + data['t1_bat_voltag'] = f'{self.plugin.byd_diag_bat_voltag[1]:.1f}' + " V" + data['t1_v_out'] = f'{self.plugin.byd_diag_v_out[1]:.1f}' + " V" + data['t1_current'] = f'{self.plugin.byd_diag_current[1]:.1f}' + " A" data['t1_volt_max'] = f'{self.plugin.byd_diag_volt_max[1]:.3f}' + " V (" + str(self.plugin.byd_diag_volt_max_c[1]) + ")" data['t1_volt_min'] = f'{self.plugin.byd_diag_volt_min[1]:.3f}' + " V (" + str(self.plugin.byd_diag_volt_min_c[1]) + ")" data['t1_temp_max_cell'] = str(self.plugin.byd_diag_temp_max_c[1]) data['t1_temp_min_cell'] = str(self.plugin.byd_diag_temp_min_c[1]) if self.plugin.byd_bms_qty > 1: data['t2_soc'] = f'{self.plugin.byd_diag_soc[2]:.1f}' + " %" + data['t2_bat_voltag'] = f'{self.plugin.byd_diag_bat_voltag[2]:.1f}' + " V" + data['t2_v_out'] = f'{self.plugin.byd_diag_v_out[2]:.1f}' + " V" + data['t2_current'] = f'{self.plugin.byd_diag_current[2]:.1f}' + " A" data['t2_volt_max'] = f'{self.plugin.byd_diag_volt_max[2]:.3f}' + " V (" + str(self.plugin.byd_diag_volt_max_c[2]) + ")" data['t2_volt_min'] = f'{self.plugin.byd_diag_volt_min[2]:.3f}' + " V (" + str(self.plugin.byd_diag_volt_min_c[2]) + ")" data['t2_temp_max_cell'] = str(self.plugin.byd_diag_temp_max_c[2]) data['t2_temp_min_cell'] = str(self.plugin.byd_diag_temp_min_c[2]) else: data['t2_soc'] = "-" - data['t2_soc'] = "-" + data['t2_bat_voltag'] = "-" + data['t2_v_out'] = "-" + data['t2_current'] = "-" data['t2_volt_max'] = "-" data['t2_volt_min'] = "-" data['t2_temp_max_cell'] = "-" data['t2_temp_min_cell'] = "-" if self.plugin.byd_bms_qty > 2: data['t3_soc'] = f'{self.plugin.byd_diag_soc[3]:.1f}' + " %" - data['t3_soc'] = f'{self.plugin.byd_diag_soc[3]:.1f}' + " %" + data['t3_bat_voltag'] = f'{self.plugin.byd_diag_bat_voltag[3]:.1f}' + " V" + data['t3_v_out'] = f'{self.plugin.byd_diag_v_out[3]:.1f}' + " V" + data['t3_current'] = f'{self.plugin.byd_diag_current[3]:.1f}' + " A" data['t3_volt_max'] = f'{self.plugin.byd_diag_volt_max[3]:.3f}' + " V (" + str(self.plugin.byd_diag_volt_max_c[3]) + ")" data['t3_volt_min'] = f'{self.plugin.byd_diag_volt_min[3]:.3f}' + " V (" + str(self.plugin.byd_diag_volt_min_c[3]) + ")" data['t3_temp_max_cell'] = str(self.plugin.byd_diag_temp_max_c[3]) data['t3_temp_min_cell'] = str(self.plugin.byd_diag_temp_min_c[3]) else: data['t3_soc'] = "-" - data['t3_soc'] = "-" - data['t3_soc'] = "-" + data['t3_bat_voltag'] = "-" + data['t3_v_out'] = "-" + data['t3_current'] = "-" data['t3_volt_max'] = "-" data['t3_volt_min'] = "-" data['t3_temp_max_cell'] = "-" diff --git a/byd_bat/webif/templates/index.html b/byd_bat/webif/templates/index.html index b8924d9f4..d441d5edf 100644 --- a/byd_bat/webif/templates/index.html +++ b/byd_bat/webif/templates/index.html @@ -100,16 +100,25 @@ shngInsertText('serial',objResponse['serial']); shngInsertText('t1_soc',objResponse['t1_soc']); + shngInsertText('t1_bat_voltag',objResponse['t1_bat_voltag']); + shngInsertText('t1_v_out',objResponse['t1_v_out']); + shngInsertText('t1_current',objResponse['t1_current']); shngInsertText('t1_volt_max',objResponse['t1_volt_max']); shngInsertText('t1_volt_min',objResponse['t1_volt_min']); shngInsertText('t1_temp_max_cell',objResponse['t1_temp_max_cell']); shngInsertText('t1_temp_min_cell',objResponse['t1_temp_min_cell']); shngInsertText('t2_soc',objResponse['t2_soc']); + shngInsertText('t2_bat_voltag',objResponse['t2_bat_voltag']); + shngInsertText('t2_v_out',objResponse['t2_v_out']); + shngInsertText('t2_current',objResponse['t2_current']); shngInsertText('t2_volt_max',objResponse['t2_volt_max']); shngInsertText('t2_volt_min',objResponse['t2_volt_min']); shngInsertText('t2_temp_max_cell',objResponse['t2_temp_max_cell']); shngInsertText('t2_temp_min_cell',objResponse['t2_temp_min_cell']); shngInsertText('t3_soc',objResponse['t3_soc']); + shngInsertText('t3_bat_voltag',objResponse['t3_bat_voltag']); + shngInsertText('t3_v_out',objResponse['t3_v_out']); + shngInsertText('t3_current',objResponse['t3_current']); shngInsertText('t3_volt_max',objResponse['t3_volt_max']); shngInsertText('t3_volt_min',objResponse['t3_volt_min']); shngInsertText('t3_temp_max_cell',objResponse['t3_temp_max_cell']); @@ -407,25 +416,43 @@ - Spannung max (Zelle): + {{ _('Batteriespannung') }}: + + + + + + {{ _('Spannung Out') }}: + + + + + + {{ _('Strom') }}: + + + + + + {{ _('Spannung max (Zelle)') }}: - Spannung min (Zelle): + {{ _('Spannung min (Zelle)') }}: - Temperatur Zelle max: + {{ _('Temperatur Zelle max') }}: - Temperatur Zelle min: + {{ _('Temperatur Zelle min') }}: @@ -442,10 +469,10 @@ {% block bodytab3 %} - + - + @@ -461,10 +488,10 @@ {% block bodytab4 %}
Turm 1 nicht vorhanden
Turm 2 nicht vorhanden
Turm 3 nicht vorhanden
- + - + diff --git a/denon/__init__.py b/denon/__init__.py index c21477510..56b0f7818 100755 --- a/denon/__init__.py +++ b/denon/__init__.py @@ -103,7 +103,7 @@ def on_data_received(self, by, data, command=None): # command can be a string (classic single command) or # - new - a list of strings if multiple commands are identified # in that case, work on all strings - commands = self._commands.get_command_from_reply(data) + commands = self._commands.get_commands_from_reply(data) if not commands: if self._discard_unknown_command: self.logger.debug(f'data "{data}" did not identify a known command, ignoring it') diff --git a/enocean/__init__.py b/enocean/__init__.py index 0871991ad..09b4a505b 100755 --- a/enocean/__init__.py +++ b/enocean/__init__.py @@ -165,7 +165,7 @@ class EnOcean(SmartPlugin): ALLOW_MULTIINSTANCE = False - PLUGIN_VERSION = "1.3.11" + PLUGIN_VERSION = "1.4.0" def __init__(self, sh): @@ -187,13 +187,7 @@ def __init__(self, sh): self.tx_id = int(tx_id, 16) self.logger.info(f"Stick TX ID configured via plugin.conf to: {tx_id}") self._log_unknown_msg = self.get_parameter_value("log_unknown_messages") - try: - self._tcm = serial.serial_for_url(self.port, 57600, timeout=1.5) - except Exception as e: - self._tcm = None - self._init_complete = False - self.logger.error(f"Exception occurred during serial open: {e}") - return + self._tcm = None self._cmd_lock = threading.Lock() self._response_lock = threading.Condition() self._rx_items = {} @@ -447,7 +441,18 @@ def run(self): self.logger.debug("Call function << run >>") self.alive = True self.UTE_listen = False - #self.learn_id = 0 + + # open serial or serial2TCP device: + try: + self._tcm = serial.serial_for_url(self.port, 57600, timeout=1.5) + except Exception as e: + self._tcm = None + self._init_complete = False + self.logger.error(f"Exception occurred during serial open: {e}") + return + else: + self.logger.info(f"Serial port successfully opened at port {self.port}") + t = threading.Thread(target=self._startup, name="enocean-startup") # if you need to create child threads, do not make them daemon = True! # They will not shutdown properly. (It's a python bug) @@ -508,6 +513,8 @@ def run(self): self._tcm.close() except Exception as e: self.logger.error(f"Exception during tcm close occured: {e}") + else: + self.logger.info(f"Enocean serial device closed") self.logger.info("Run method stopped") def stop(self): diff --git a/enocean/plugin.yaml b/enocean/plugin.yaml index 4d40fe6a1..ebaf6538b 100755 --- a/enocean/plugin.yaml +++ b/enocean/plugin.yaml @@ -16,7 +16,7 @@ plugin: # url of the support thread support: https://knx-user-forum.de/forum/supportforen/smarthome-py/26542-featurewunsch-enocean-plugin/page13 - version: 1.3.11 # Plugin version + version: 1.4.0 # Plugin version sh_minversion: 1.3 # 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/enocean/user_doc.rst b/enocean/user_doc.rst index d8b4d9c28..94b102c41 100755 --- a/enocean/user_doc.rst +++ b/enocean/user_doc.rst @@ -55,6 +55,7 @@ Alternativ kann eines der oben erwähnten Serial-Geräte auch über das Netzwerk 57600n81,local Für und sind die entsprechenden Werte einzufügen. + Konfiguration ============= @@ -112,7 +113,7 @@ Zu b) 4. Nach dem Neustart das Logfile öffnen und nach dem Eintrag ``enocean: Base ID = 0xYYYYZZZZ`` suchen. -6. Übernahme dieser im Log angezeigten Base-ID in die plugin.yaml als Parameter `tx_id`. +5. Übernahme dieser im Log angezeigten Base-ID in die plugin.yaml als Parameter `tx_id`. item.yaml diff --git a/epson/__init__.py b/epson/__init__.py index 89fa6676a..f68022d22 100755 --- a/epson/__init__.py +++ b/epson/__init__.py @@ -81,7 +81,7 @@ def on_data_received(self, by, data, command=None): # command can be a string (classic single command) or # - new - a list of strings if multiple commands are identified # in that case, work on all strings - commands = self._commands.get_command_from_reply(data) + commands = self._commands.get_commands_from_reply(data) if not commands: if self._discard_unknown_command: self.logger.debug(f'data "{data}" did not identify a known command, ignoring it') diff --git a/influxdb/README.md b/influxdb/README.md deleted file mode 100755 index 91f05d339..000000000 --- a/influxdb/README.md +++ /dev/null @@ -1,86 +0,0 @@ -# influxdb - -## Logging to InfluxDB over UDP or HTTP - -Logging items to the time-series database [InfluxDB](https://www.influxdata.com/time-series-platform/) - -This started as a fork of the plugin `influxdata` with the following enhancements: -- proper naming -- specify a name for the measurement instead of falling back to the item's ID -- specify additional tags or fields globally (plugin.yaml) and/or on per-item basis - -The special smarthomeNG attributes `caller`, `source` and `dest` are always logged as tags. - -Only if a measurement name is specified, the item's ID is automatically logged as well (tag `item`) - if you don't specify a measurement-name, the name will fallback to the item's ID which makes the item-tag redundant - -## Proper Logging -Please read the [Key Concepts](https://docs.influxdata.com/influxdb/v1.8/concepts/key_concepts/) and [Schema Design](https://docs.influxdata.com/influxdb/v1.8/concepts/schema_and_data_layout/) - -Especially these: -- [Encode meta data in tags](https://docs.influxdata.com/influxdb/v1.8/concepts/schema_and_data_layout/#encode-meta-data-in-tags) -- [Avoid encoding data in measurement names](https://docs.influxdata.com/influxdb/v1.8/concepts/schema_and_data_layout/#avoid-encoding-data-in-measurement-names) -- [Avoid putting more than one piece of information in one tag](https://docs.influxdata.com/influxdb/v1.8/concepts/schema_and_data_layout/#avoid-putting-more-than-one-piece-of-information-in-one-tag) - -## Setup - -### /etc/influxdb/influxdb.conf - -If you use UDP, you have to explicitly enable the UDP endpoint in influxdb. The UDP endpoint cannot be auth-protected and is bound to a specific database - -``` -[[udp]] - enabled = true - bind-address = ":8089" - database = "smarthome" - # retention-policy = "" -``` - -If you want to use HTTP access no additional configuration is needed as HTTP access to the influxdb is enabled by default. - -### plugin.yaml - -you can setup global tags and fields (JSON encoded) - -```yaml -influxdb: - plugin_name: influxdb - # host: localhost - # udp_port: 8089 - # keyword: influxdb - # value_field: value - # write_http: True - # http_port: 8086 - tags: '{"key": "value", "foo": "bar"}' - fields: '{"key": "value", "foo": "bar"}' -``` - -### items.yaml - -logging into a measurement named `root.some_item`, default tags and tags/fields as specified in plugin.yaml - -```yaml -root: - - some_item: - influxdb: 'true' -``` - -if `keyword` in plugin.yaml is set to `sqlite` this can also be used as a drop-in replacement for sqlite. - -```yaml -root: - - some_item: - sqlite: 'true' -``` - -*recommended*: logging into the measurement `temp` with an additional tag `room` -and default tags (including `item: root.dining_temp`) and tags/fields as specified in plugin.yaml - -```yaml -root: - - dining_temp: - influxdb_name: temp - influxdb_tags: '{"room": "dining"}' -``` diff --git a/influxdb/__init__.py b/influxdb/__init__.py index 4b3169572..71849f43c 100755 --- a/influxdb/__init__.py +++ b/influxdb/__init__.py @@ -26,10 +26,11 @@ import requests class InfluxDB(SmartPlugin): - PLUGIN_VERSION = "1.0.2" + PLUGIN_VERSION = "1.0.3" ALLOW_MULTIINSTANCE = False def __init__(self, smarthome): + super().__init__() self.logger = logging.getLogger(__name__) self.logger.info('Init InfluxDB') diff --git a/influxdb/plugin.yaml b/influxdb/plugin.yaml index 23c81a2aa..d72cc624f 100755 --- a/influxdb/plugin.yaml +++ b/influxdb/plugin.yaml @@ -15,7 +15,7 @@ plugin: #documentation: https://www.smarthomeng.de/user/plugins/influxdb/user_doc.html support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1498207-support-thread-f%C3%BCr-influxdb-plugin - version: 1.0.2 # Plugin version + version: 1.0.3 # Plugin version sh_minversion: 1.1 # 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/influxdb/user_doc.rst b/influxdb/user_doc.rst new file mode 100644 index 000000000..79a218c0a --- /dev/null +++ b/influxdb/user_doc.rst @@ -0,0 +1,124 @@ +.. index:: Plugins; influxdb +.. index:: influxdb + +======== +influxdb +======== + +.. image:: webif/static/img/plugin_logo.png + :alt: plugin logo + :width: 300px + :height: 300px + :scale: 50 % + :align: left + +Konfiguration +============= + +Die Informationen zur Konfiguration des Plugins sind unter :doc:`/plugins_doc/config/influxdb` beschrieben. + +/etc/influxdb/influxdb.conf +--------------------------- + +Wenn UDP verwendet wird, muss der UDP-Endpunkt explizit in influxdb aktiviert werden. +Der UDP-Endpunkt ist nicht auth-geschützt und ist an eine bestimmte Datenbank gebunden. + +.. code-block:: yaml + + [[udp]] + enabled = true + bind-address = ":8089" + database = "smarthome" + # retention-policy = "" + +Wenn Sie den HTTP-Zugang verwenden möchten, ist keine zusätzliche Konfiguration erforderlich, da der +HTTP-Zugriff auf die influxdb standardmäßig aktiviert ist. + +plugin.yaml +----------- + +Es können globale Tags und Felder angegeben werden: + +.. code-block:: yaml + + influxdb: + plugin_name: influxdb + # host: localhost + # udp_port: 8089 + # keyword: influxdb + # value_field: value + # write_http: True + # http_port: 8086 + tags: '{"key": "value", "foo": "bar"}' + fields: '{"key": "value", "foo": "bar"}' + +items.yaml +---------- + +Logging in eine Messung namens ``root.some_item``, Standard-Tags und +Tags/Felder wie in plugin.yaml angegeben + +.. code:: yaml + + root: + some_item: + influxdb: 'true' + +Wenn ``keyword`` in der plugin.yaml auf ``sqlite`` gesetzt wird, kann dies auch +als Ersatz für sqlite verwendet werden. + +.. code:: yaml + + root: + some_item: + sqlite: 'true' + +*empfohlen*: Loggen der Messung ``temp`` mit einem zusätzlichen +Tag ``room`` und Standard-Tags (einschließlich ``item: root.dining_temp``) und +Tags/Felder wie in plugin.yaml angegeben + +.. code:: yaml + + root: + dining_temp: + influxdb_name: temp + influxdb_tags: '{"room": "dining"}' + + +In InfluxDB über UDP oder HTTP loggen +===================================== + +Protokollierung von Elementen in der Zeitseriendatenbank +`InfluxDB `_ + +Dieses Plugin ist ein Fork von ``influxdata`` mit den folgenden +Erweiterungen: + +- korrekte Namensgebung +- Angabe eines Namens für die Messung statt auf die ID des Elements zurückzugreifen +- zusätzliche Tags oder Felder global (plugin.yaml) und/oder pro Element + +Die speziellen smarthomeNG Attribute ``caller``, ``source`` und ``dest`` +werden immer als Tags protokolliert. + +Nur wenn ein Messungsname angegeben wird, wird automatisch auch die ID des Elements +mitprotokolliert (Tag ``item``) - wenn Sie keinen Messungsnamen angeben, +wird der Name auf die ID des Items zurückgreifen, was den Item-Tag +überflüssig macht + +Korrektes Logging +================= + +Bitte lesen Sie die `Key Konzepte `_ +und `Schema Design `_ + +Insbesondere diese: + +- `Metadaten kodieren in Tags `_ +- `Vermeiden Sie die Kodierung von Daten in Messnamen `_ +- Vermeiden Sie mehr als eine Information in einem Tag `_ + +Web Interface +============= + +Das Plugin stellt kein Web Interface zur Verfügung. diff --git a/influxdb/webif/static/img/plugin_logo.png b/influxdb/webif/static/img/plugin_logo.png new file mode 100644 index 000000000..a605a023c Binary files /dev/null and b/influxdb/webif/static/img/plugin_logo.png differ diff --git a/influxdb2/assets/influxdb2_webif.png b/influxdb2/assets/influxdb2_webif.png new file mode 100644 index 000000000..549cbbdfb Binary files /dev/null and b/influxdb2/assets/influxdb2_webif.png differ diff --git a/influxdb2/user_doc.rst b/influxdb2/user_doc.rst index e07548b0b..242df4926 100755 --- a/influxdb2/user_doc.rst +++ b/influxdb2/user_doc.rst @@ -89,8 +89,6 @@ Mit jedem Item Wert, der in einem InfluxDB Bucket abgelegt werden, werden folgen - **str_value** - enthält nicht numerische Werte, die in der Datenbank abgelegt werden sollen. - - Konfiguration ============= @@ -100,14 +98,72 @@ Die Plugin Parameter und die Informationen zur Item-spezifischen Konfiguration d unter :doc:`/plugins_doc/config/influxdb2` nachzulesen. -Beispiele ---------- +Daten aus dem Database Plugin transferieren +=========================================== + +Diese Anleitung wurde unter influxdb2 getestet und muss eventuell für influxdb1 adaptiert werden. + +1. Pandas und influxdb_client Module für Python installieren +2. CSV-Dump aus dem Webinterface des Datenbank-Plugins herunterladen +3. Anpassen der Zugriffsparameter im unten stehenden Skript +4. Anpassen des Pfads zur CVS-Datei +5. Ausführen des Skripts +6. Abhängig von der Größe der Datenbank ist Geduld gefragt. + + +.. code-block:: python + + from influxdb_client import InfluxDBClient + from influxdb_client.client.write_api import SYNCHRONOUS + import pandas as pd + + + # ---------------------------------------------- + ip = "localhost" + port = 8086 + token = "******************" + org = "smarthomeng" + bucket = "shng" + value_field = "value" + str_value_field = "str_value" + + csvfile = "smarthomeng_dump.csv" + # ---------------------------------------------- + + + client = InfluxDBClient(url=f"http://{ip}:{port}", token=token, org=org) + write_api = client.write_api(write_options=SYNCHRONOUS) + + df = pd.read_csv(csvfile, sep=';', header=0) + df = df.reset_index() + + num_rows = len(df.index) + last_progress_percent = -1 + + for index, row in df.iterrows(): + progress_percent = int((index/num_rows)*100) + if last_progress_percent != progress_percent: + print(f"{progress_percent}%") + last_progress_percent = progress_percent + + p = {'measurement': row['item_name'], 'time': int(row['time']) * 1000000, + 'tags': {'item': row['item_name']}, + 'fields': {value_field: row['val_num'], str_value_field: row['val_str']} + } + write_api.write(bucket=bucket, record=p) + + client.close() -Hier können ausführlichere Beispiele und Anwendungsfälle beschrieben werden. -... Web Interface ============= -... +Das Web Interface ermöglicht das Betrachten der Items, die mit der Datenbank verbunden sind. + +.. image:: assets/influxdb2_webif.png + :height: 1610px + :width: 3304px + :scale: 25% + :alt: Web Interface + :align: center diff --git a/influxdb2/webif/templates/index.html b/influxdb2/webif/templates/index.html index 709ca1d72..99e2b47c0 100755 --- a/influxdb2/webif/templates/index.html +++ b/influxdb2/webif/templates/index.html @@ -33,23 +33,13 @@ } } - + + + + +{% endblock pluginscripts %} + + +{% block headtable %} + +
Turm 1 nicht vorhanden
Turm 2 nicht vorhanden
Turm 3 nicht vorhanden
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ _('Host') }}{{ p._device.host }}:{{ p._device.port }} {% if p._device.use_ssl == True %}(TLS){% endif %}{{ _('Model') }}{{ p._model }}
{{ _('Identity') }}{{ p._identity }}{{ _('Serial number') }}{{ p._serial }}
{{ _('API username') }}{{ p._device.username }}{{ _('RouterOS version') }}{{ p._osversion }}
+{% endblock headtable %} + + + +{% block buttons %} +{% if 1==2 %} +
+ +
+{% endif %} +{% endblock %} + + +{% set tabcount = 2 %} + + + +{% if item_count==0 %} + {% set start_tab = 1 %} +{% endif %} + + + +{% set tab1title = "" ~ _('Items') ~ " (" ~ item_count ~ ")" %} +{% block bodytab1 %} + +
+ {{ _('Hier kommt der Inhalt des Webinterfaces hin.') }} (optional) +
+ + + + + + + + + + + + + + + {% for item in p._items %} + + + + filterMikrotikValues + + + + + + + {% endfor %} + +
{{ _('Item') }}{{ _('Port') }}{{ _('Parameter') }}{{ _('Wert') }}
{{ item._path }}{{ p.get_iattr_value(item.conf, 'mikrotik_port') }}{{ p.get_iattr_value(item.conf, 'mikrotik_parameter') }} + {% if p.get_iattr_value(item.conf, 'mikrotik_parameter') == 'active' %} + {% if item() == True %} + + {% else %} + + {% endif %} + {% elif p.get_iattr_value(item.conf, 'mikrotik_parameter') == 'enabled' %} + {% if item() == True %} + + {% else %} + + {% endif %} + {% elif p.get_iattr_value(item.conf, 'mikrotik_parameter') == 'poe' %} + {% if item() == True %} + + {% else %} + + {% endif %} + {% else %} + {{ item() }} + {% endif %} +
+ +
+ Etwaige Informationen unterhalb der Tabelle (optional) +
+ +{% endblock bodytab1 %} + + + +{% set tab2title = "" ~ _('Port list') ~ " (" ~ len(p._interfaces) ~ ")" %} +{% block bodytab2 %} + +
+ {{ _('Hier kommt der Inhalt des Webinterfaces hin.') }} (optional) +
+ + + + + + + + + + + + + + + + + + + + + {% for interface in p._interfaces %} + + + + + + + + + + + + + + + + {% endfor %} + +
{{ _('Id') }}{{ _('Name') }}{{ _('Default name') }}{{ _('Comment') }}{{ _('Speed') }}{{ _('PVID') }}{{ _('Up') }}{{ _('Enabled') }}{{ _('POE Enabled') }}
{{ interface.id }}{{ interface.name }}{{ interface.defaultname }} + {{ interface.comment }} + + {{ interface.speed }}{{ interface.pvid }} + {% if interface.running == 'true' %} + + {% else %} + + {% endif %} + + {% if interface.disabled == 'false' %} + + {% else %} + + {% endif %} + + {% if interface.poe == 'auto-on' %} + + {% elif interface.poe == 'off' %} + + {% else %} + - + {% endif %} +
+ +
+ Etwaige Informationen unterhalb der Tabelle (optional) +
+{% endblock bodytab2 %} + + diff --git a/neato/__init__.py b/neato/__init__.py index d9eb69282..8bcab3b96 100755 --- a/neato/__init__.py +++ b/neato/__init__.py @@ -30,7 +30,7 @@ class Neato(SmartPlugin): - PLUGIN_VERSION = '1.6.8' + PLUGIN_VERSION = '1.6.9' robot = 'None' def __init__(self, sh): diff --git a/neato/plugin.yaml b/neato/plugin.yaml index c499cbe43..75c9ba37c 100755 --- a/neato/plugin.yaml +++ b/neato/plugin.yaml @@ -12,7 +12,7 @@ plugin: documentation: https://github.com/smarthomeng/plugins/blob/develop/neato/README.md support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1417295-support-thread-plugin-neato - version: 1.6.8 # Plugin version + version: 1.6.9 # 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 diff --git a/neato/robot.py b/neato/robot.py index d40f3d872..050875895 100755 --- a/neato/robot.py +++ b/neato/robot.py @@ -150,7 +150,7 @@ def robot_command(self, command, arg1 = None, arg2 = None): responseJson = start_cleaning_response.json() self.logger.debug("Debug: send command response: {0}".format(start_cleaning_response.text)) if log_message: - self.logger.info("Requested Info: {0}".format(start_cleaning_response.text)) + self.logger.warning("INFO: Requested Info: {0}".format(start_cleaning_response.text)) if 'result' in responseJson: if str(responseJson['result']) == 'ok': @@ -202,7 +202,12 @@ def update_robot(self): 'Date': self.__get_current_date(), 'Accept': 'application/vnd.neato.nucleo.v1', 'Authorization': 'NEATOAPP ' + h.hexdigest()}, timeout=self._timeout, verify=self._verifySSL ) - + except requests.exceptions.ConnectionError as e: + self.logger.warning("Robot: This test works!: %s" % str(e)) + return 'error' + except requests.exceptions.Timeout as e: + self.logger.warning("Robot: Timeout exception during cloud state request: %s" % str(e)) + return 'error' except Exception as e: self.logger.error("Robot: Exception during cloud state request: %s" % str(e)) return 'error' @@ -287,7 +292,8 @@ def update_robot(self): self.navigationMode = response['cleaning']['navigationMode'] self.spotWidth = response['cleaning']['spotWidth'] self.spotHeight = response['cleaning']['spotHeight'] - self.mapId = response['cleaning']['mapId'] + if 'mapId' in response['cleaning']: + self.mapId = response['cleaning']['mapId'] return response @@ -364,7 +370,7 @@ def __get_current_date(self): try: locale.setlocale(locale.LC_TIME, 'en_US.utf8') except locale.Error as e: - self.logger.error("Robot: Locale setting Error. Please install locale en_US.utf8: "+e) + self.logger.error("Robot: Locale setting error. Please install locale en_US.utf8: "+e) return None date = time.strftime('%a, %d %b %Y %H:%M:%S', time.gmtime()) + ' GMT' locale.setlocale(locale.LC_TIME, saved_locale) diff --git a/neato/webif/__init__.py b/neato/webif/__init__.py index fd604010b..f5c81c9f1 100755 --- a/neato/webif/__init__.py +++ b/neato/webif/__init__.py @@ -113,8 +113,8 @@ def index(self, reload=None, action=None, email=None, hashInput=None, code=None, self.plugin.dismiss_current_alert() resetAlarmsSuccessfull = True elif action =="listAvailableMaps": - self.logger.warning("List all available maps via webinterface") boundaryListSuccessfull = self.plugin.get_map_boundaries(map_id=mapIDInput) + self.logger.warning(f"Request all available maps via webinterface successfull: {boundaryListSuccessfull }") else: self.logger.error("Unknown command received via webinterface") diff --git a/nut/__init__.py b/nut/__init__.py index 512c7c3f3..9de84e70b 100755 --- a/nut/__init__.py +++ b/nut/__init__.py @@ -22,7 +22,7 @@ from lib.model.smartplugin import SmartPlugin class NUT(SmartPlugin): - PLUGIN_VERSION = '1.3.2' + PLUGIN_VERSION = '1.3.3' ALLOW_MULTIINSTANCE = True def __init__(self, sh): @@ -36,20 +36,21 @@ def __init__(self, sh): self._sh = sh self._cycle = self.get_parameter_value("cycle") self._host = self.get_parameter_value("host") - self._port = self.get_parameter_value("port ") + self._port = self.get_parameter_value("port") self._ups = self.get_parameter_value("ups") self._timeout = self.get_parameter_value("timeout") self._conn = None self._items = {} - self._sh.scheduler.add(__name__, self._read_ups, prio = 5, cycle = self._cycle) - self.logger.info('Init NUT Plugin') + self.logger.info('NUT Plugin initialized') def run(self): + self._sh.scheduler.add('poll_nut_device', self._read_ups, prio = 5, cycle = self._cycle) self.alive = True def stop(self): self.alive = False + self.scheduler_remove('poll_nut_device') if self._conn: self._conn.close() @@ -64,6 +65,7 @@ def update_item(self, item, caller=None, source=None, dest=None): return def _read_ups(self): + self.logger.debug(f"Trying to connect to {self._host} on port {self._port}") try: self._conn = telnetlib.Telnet(self._host, self._port) self._conn.write('LIST VAR {}\n'.format(self._ups).encode('ascii')) diff --git a/nut/plugin.yaml b/nut/plugin.yaml index f80f622ab..cfbffc1f0 100755 --- a/nut/plugin.yaml +++ b/nut/plugin.yaml @@ -21,7 +21,7 @@ plugin: # 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.2 # Plugin version + version: 1.3.3 # Plugin version sh_minversion: 1.3 # 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 diff --git a/russound/__init__.py b/russound/__init__.py index 23496a272..8131d29cf 100755 --- a/russound/__init__.py +++ b/russound/__init__.py @@ -42,7 +42,7 @@ class Russound(SmartPlugin): the update functions for the items """ - PLUGIN_VERSION = '1.7.1' + PLUGIN_VERSION = '1.7.2' def __init__(self, sh, *args, **kwargs): """ @@ -52,11 +52,11 @@ def __init__(self, sh, *args, **kwargs): if '.'.join(VERSION.split('.', 2)[:2]) <= '1.5': self.logger = logging.getLogger(__name__) + super().__init__(sh, args, kwargs) try: # sh = self.get_sh() to get it. self.host = self.get_parameter_value('host') self.port = self.get_parameter_value('port') - pass except KeyError as e: self.logger.critical( "Plugin '{}': Inconsistent plugin (invalid metadata definition: {} not defined)".format(self.get_shortname(), e)) @@ -86,14 +86,8 @@ def run(self): def activate(self): self.logger.debug("Activate method called, queries to russound will be resumes and data will be written again") - self.suspended = False - self._client.connect() + self.resume() - def suspend(self): - self.logger.debug("Suspend method called, queries to russound will not be made and data will not be written") - self.suspended = True - self._client.close() - def stop(self): """ Stop method for the plugin @@ -102,6 +96,12 @@ def stop(self): self.alive = False self._client.close() + def connect(self): + self._client.open() + + def disconnect(self): + self._client.close() + def parse_item(self, item): """ Default plugin parse_item method. Is called when the plugin is initialized. @@ -121,6 +121,11 @@ def parse_item(self, item): # self.logger.debug("Source {0} added".format(s)) # return None + if item.path() == self._suspend_item_path: + self._suspend_item = item + self.logger.info(f'set suspend_item to {item.path()}') + return + if self.has_iattr(item.conf, 'rus_path'): self.logger.debug("parse item: {}".format(item)) @@ -200,6 +205,14 @@ def update_item(self, item, caller=None, source=None, dest=None): # and only, if the item has not been changed by this this plugin: self.logger.info("Update item: {}, item has been changed outside this plugin (caller={}, source={}, dest={})".format(item.id(), caller, source, dest)) + if item.path() == self._suspend_item_path: + if self._suspend_item is not None: + if item(): + self.suspend(f'suspend item {item.path()}') + else: + self.resume(f'suspend item {item.path()}') + return + if self.has_iattr(item.conf, 'rus_path'): path = self.get_iattr_value(item.conf, 'rus_path') p = self.params[path] diff --git a/russound/plugin.yaml b/russound/plugin.yaml index b997393d0..96dd49907 100755 --- a/russound/plugin.yaml +++ b/russound/plugin.yaml @@ -12,7 +12,7 @@ plugin: documentation: https://www.smarthomeng.de/developer/plugins/russound/user_doc.html # url of documentation (wiki) page support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1800440-support-thread-für-das-russound-plugin - version: 1.7.1 # Plugin version + version: 1.7.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) multi_instance: false # plugin supports multi instance @@ -36,6 +36,13 @@ parameters: en: 'Russound port' fr: "Port de Russound" + standby_item: + type: str + default: '' + description: + de: 'Item zum Aktivieren des Suspend-Modus' + en: 'item for activating suspend mode' + item_attributes: rus_path: type: str diff --git a/sml2/__init__.py b/sml2/__init__.py index 93ff85896..e1bf6d2fa 100755 --- a/sml2/__init__.py +++ b/sml2/__init__.py @@ -2,7 +2,7 @@ # vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab ######################################################################### # Copyright 2012-2014 Oliver Hinckel github@ollisnet.de -# Copyright 2018-2021 Bernd.Meiners@mail.de +# Copyright 2018-2023 Bernd.Meiners@mail.de # Copyright 2022-2022 Julian Scholle julian.scholle@googlemail.com ######################################################################### # @@ -35,6 +35,7 @@ import traceback from smllib import SmlStreamReader from smllib import const as smlConst +from .const import FURTHER_OBIS_NAMES from lib.module import Modules from lib.item import Items @@ -95,7 +96,7 @@ def connection_lost(self, exc): self.smlx.logger.error("Connection so serial device was closed") self.smlx.connected = False - PLUGIN_VERSION = '2.0.0' + PLUGIN_VERSION = '2.0.1' def __init__(self, sh): """ @@ -135,6 +136,8 @@ def __init__(self, sh): self.init_webinterface(WebInterface) self.task = None self.values = {} + self.obis_names = { **smlConst.OBIS_NAMES, **FURTHER_OBIS_NAMES } + self.obis_units = smlConst.UNITS def run(self): """ @@ -339,8 +342,8 @@ def parse_data(self): obis_code = sml_entry.obis.obis_code if obis_code not in self.values: self.values[obis_code] = dict() - self.values[obis_code]['name'] = smlConst.OBIS_NAMES.get(sml_entry.obis) - self.values[obis_code]['unit'] = smlConst.UNITS.get(sml_entry.unit) + self.values[obis_code]['name'] = self.obis_names.get(sml_entry.obis) + self.values[obis_code]['unit'] = self.obis_units.get(sml_entry.unit) if obis_code in self._items: if 'valueReal' in self._items[obis_code]: for item in self._items[obis_code]['valueReal']: diff --git a/sml2/const.py b/sml2/const.py new file mode 100644 index 000000000..0cda73469 --- /dev/null +++ b/sml2/const.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 + +FURTHER_OBIS_NAMES = { + '010000020000': 'Firmware Version, Firmware Prüfsumme CRC, Datum', + '0100010800ff': 'Bezug Zählerstand Total', + '0100010801ff': 'Bezug Zählerstand Tarif 1', + '0100010802ff': 'Bezug Zählerstand Tarif 2', + '0100011100ff': 'Total-Zählerstand', + '0100020800ff': 'Einspeisung Zählerstand Total', + '0100020801ff': 'Einspeisung Zählerstand Tarif 1', + '0100020802ff': 'Einspeisung Zählerstand Tarif 2', + '0100600100ff': 'Server-ID', + '010060320101': 'Hersteller-Identifikation', + '0100605a0201': 'Prüfsumme', +} diff --git a/sml2/plugin.yaml b/sml2/plugin.yaml index 5ec3a4801..40e59a7de 100755 --- a/sml2/plugin.yaml +++ b/sml2/plugin.yaml @@ -12,7 +12,7 @@ plugin: documentation: https://www.smarthomeng.de/developer/plugins/smlx/user_doc.html support: https://knx-user-forum.de/forum/supportforen/smarthome-py/39119-sml-plugin-datenblock-größenfehler restartable: True - version: 2.0.0 # Plugin version + version: 2.0.1 # Plugin version sh_minversion: 1.4.2 # 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 diff --git a/sml2/requirements.txt b/sml2/requirements.txt index 086f48da0..dd9fa89f8 100755 --- a/sml2/requirements.txt +++ b/sml2/requirements.txt @@ -1,3 +1,3 @@ pyserial>=3.2.1 -SmlLib>=1.2 +SmlLib>=1.3 pyserial-asyncio>=0.6 diff --git a/sml2/webif/__init__.py b/sml2/webif/__init__.py index 631616238..e6d04003c 100755 --- a/sml2/webif/__init__.py +++ b/sml2/webif/__init__.py @@ -71,10 +71,7 @@ def index(self, reload=None): """ tmpl = self.tplenv.get_template('index.html') # Setting pagelength (max. number of table entries per page) for web interface - try: - pagelength = self.plugin.webif_pagelength - except Exception: - pagelength = 100 + pagelength = self.plugin.get_parameter_value('webif_pagelength') # 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, diff --git a/sml2/webif/templates/index.html b/sml2/webif/templates/index.html index bc5db2265..f7746f2b4 100755 --- a/sml2/webif/templates/index.html +++ b/sml2/webif/templates/index.html @@ -8,16 +8,16 @@ {% block pluginstyles %} {% endblock pluginstyles %} @@ -27,54 +27,34 @@ --> {% 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 d86f35332..1a04ce5d3 100755 --- a/sonos/__init__.py +++ b/sonos/__init__.py @@ -2314,7 +2314,7 @@ def _play_tunein(self, station_name: str, music_service: str = 'TuneIn', start: # if a patch is applied. # ------------------------------------------------------------------------------------------------------------ # - self.logger.warning(f"DEBUG: _play_radio start") + self.logger.warning(f"DEBUG: _play_tunein start") if not self._check_property(): return False, "Property check failed" @@ -2400,9 +2400,8 @@ def _play_tunein(self, station_name: str, music_service: str = 'TuneIn', start: 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. with space in the name. Other names can cause infinite loops. 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 @@ -2424,7 +2423,7 @@ def _play_radio(self, station_name: str, music_service: str = 'TuneIn', start: b ' """ - + 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() @@ -2435,16 +2434,14 @@ def _play_radio(self, station_name: str, music_service: str = 'TuneIn', start: b # 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 @@ -2455,11 +2452,11 @@ def _play_radio(self, station_name: str, music_service: str = 'TuneIn', start: b 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 @@ -2468,9 +2465,27 @@ def _play_radio(self, station_name: str, music_service: str = 'TuneIn', start: b 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 @@ -2979,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.""" diff --git a/sonos/plugin.yaml b/sonos/plugin.yaml index 4c4917a7e..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 diff --git a/sonos/user_doc.rst b/sonos/user_doc.rst index c2c922622..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 @@ -603,7 +604,6 @@ Hierzu wird ein untergeordnetes Item mit ``volume_dpt3`` angelegt, siehe Beispie zone_group_members ------------------ - ``read`` Gibt eine Liste aller UIDs aus, die sich in der Gruppe des Speakers befinden. Die Liste enthält auch den aktuellen Speaker. @@ -611,15 +611,13 @@ Das Item wird über Sonos Events aktualisiert und zeigt daher immer den aktuelle sonos_favorites --------------- - ``read`` Liest die Liste der gespeicherten Sonos Favoriten. Das Item ist vom Typ List. Das Item wird über Sonos Events aktualisiert und zeigt daher immer den aktuellen Status an. favorite_radio_stations ------------------------ - +--------------- ``read`` Liest die Liste der gespeicherten Tunein Favoriten. Das Item ist vom Typ List. @@ -627,7 +625,6 @@ Das Item wird über Sonos Events aktualisiert und zeigt daher immer den aktuelle play_favorite_title ------------------- - ``write`` Spielt einen gespeicherten Sonos Favoriten anhand eines Namens. Das Item ist vom Typ String. @@ -636,7 +633,6 @@ Die Liste der gespeicherten Favoriten kann mit dem Attribut ``sonos_favorites`` play_favorite_number -------------------- - ``write`` Spielt einen gespeicherten Sonos Favoriten anhand der Nummer des Listeneintrages. Das Item ist vom Typ Number @@ -645,7 +641,6 @@ Der Befehl ist ein Gruppenbefehl und wird für alle Speaker einer Gruppe angewen play_favorite_radio_title ------------------------- - ``write`` Spielt einen gespeicherten Tunein Radio Favoriten anhand eines Namens. Das Item ist vom Typ String. @@ -654,7 +649,6 @@ Die Liste der gespeicherten Favoriten kann mit dem Attribut ``favorite_radio_sta play_favorite_radio_number -------------------------- - ``write`` Spielt einen gespeicherten Tunein Radio Favoriten anhand der Nummer des Listeneintrages. Das Item ist vom Typ Number @@ -668,10 +662,9 @@ Nicht echtzeitfähige Eigenschaften Einige Eigenschaften sind nicht Event basiert. Das bedeutet, dass sie nicht direkt nach Änderung über ein Event aktualisiert werden, sondern die Änderung erst bei der nächsten zyklischen Abfrage bei smarthomeNG ankommt. - Folgende Eigenschaften sind **nicht** Event basiert: -* snooze -* status_light + * snooze + * status_light Gruppenbefehle @@ -679,18 +672,18 @@ Gruppenbefehle Einige Items werden immer als Gruppenbefehl, d.h. auf alle Speaker innerhalb einer Gruppe ausgeführt. Folgende Methoden sind Gruppenbefehle: -* play -* pause -* stop -* mute -* cross_fade -* snooze -* play_mode -* next -* previous -* play_tunein -* play_url -* load_sonos_playlist + * play + * pause + * stop + * mute + * cross_fade + * snooze + * play_mode + * next + * previous + * play_tunein + * play_url + * load_sonos_playlist Für diese Items ist es egal, für welchen Speaker einer Gruppe diese Kommandos gesendet werden. Sie werden automatisch für alle Speaker einer Gruppe angewendet. @@ -735,7 +728,7 @@ Beispiel: do_something() 4a) Lautstärke inkrementell verstellen (via KNX dpt3) ------------------------------------------------------ +---------------------------------------------------- Dieses Beispiel zeigt die Verstellung der Laustärke inkrementell via dpt3: 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/StateEngineItem.py b/stateengine/StateEngineItem.py index bedbe383a..49476cb11 100755 --- a/stateengine/StateEngineItem.py +++ b/stateengine/StateEngineItem.py @@ -40,6 +40,7 @@ import threading import queue import re +from ast import literal_eval # Class representing a blind item @@ -347,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: @@ -506,6 +507,7 @@ def run_queue(self): 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: @@ -657,6 +659,7 @@ def run_queue(self): self.__logger.debug("State evaluation finished") self.__logger.info("State evaluation queue empty.") self.__handle_releasedby(new_state, last_state, _instant_leaveaction) + return if new_state.is_copy_for: @@ -679,6 +682,7 @@ def run_queue(self): self.update_webif(_key_stay, False) self.update_webif(_key_enter, False) 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) @@ -766,50 +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, instant_leaveaction): def update_can_release_list(): for e in _returnvalue: - if isinstance(e, list): - self.__logger.warning("Entry {} should not be list. Please check!", e) - e = e[0] 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") @@ -822,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) @@ -857,54 +868,58 @@ def update_can_release_list(): new_state.was_releasedby = None _can_release_list = [] releasedby = all_released_by.get(new_state) - self.__logger.develop("releasedby {}", releasedby) - if releasedby: + 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_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) - _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'] @@ -1103,26 +1118,107 @@ 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 + _changed_orders = [] + 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 - 0.01 + _copied_states[state] = _order + self.__logger.develop("State {} is copy, set to {}", state, _order) + else: + _issue = state.update_order() + _order = state.order + if _order != _original_order: + _changed_orders.append(_order) + _add_order -= 1 + self.__logger.develop("State {} changed order: {}," + " i: {} add order: {}.", + state, _order, i, _add_order) + elif any(_order < value for value in _changed_orders): + _order = i + _add_order + _issue = state.update_order(_order) + self.__logger.develop("State {} smaller, order: {}," + " i: {} add order: {}.", + state, _order, i, _add_order) + elif any(_order == value for value in _changed_orders): + _order = i + _add_order + _issue = state.update_order(_order) + self.__logger.develop("State {} equal, order: {}," + " i: {} add order: {}.", + state, _order, i, _add_order) + _add_order += 1 + else: + self.__logger.develop("State {} order: {}," + " i: {} add order: {}.", + state, _order, i, _add_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 and init is False: + _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) + 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) + self.__logger.error("Ignoring state {0} because: {1}", + item_state.property.path, ex) + return _statecount def __finish_states(self): # initialize states @@ -1146,7 +1242,7 @@ def update_state(self, item, caller=None, source=None, dest=None): 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 @@ -1449,105 +1545,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) @@ -1559,20 +1669,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 = [] @@ -1580,11 +1691,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 != []] @@ -1880,7 +1991,7 @@ def return_item(self, item_id): _issue = "Determined item '{0}' does not exist.".format(item_id) self.__logger.warning(_issue) else: - self.__logger.develop("Determined item '{0}' for id {1}.", item.id, item_id) + self.__logger.develop("Determined item '{0}' for id {1}.", item.property.path, item_id) return item, [_issue] # Return an item related to the StateEngine object item diff --git a/stateengine/StateEngineState.py b/stateengine/StateEngineState.py index bfdddb35a..8efc61aff 100755 --- a/stateengine/StateEngineState.py +++ b/stateengine/StateEngineState.py @@ -86,6 +86,14 @@ def releasedby(self): def releasedby(self, 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): return self.__can_release.get() @@ -155,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) @@ -190,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() @@ -247,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): @@ -351,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) @@ -370,7 +408,6 @@ def update_action_status(action_status, actiontype): if action_status is None: return action_status = StateEngineTools.flatten_list(action_status) - #self._log_debug("Action status: {}", action_status) if isinstance(action_status, list): for e in action_status: update_action_status(e, actiontype) @@ -436,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): @@ -476,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 diff --git a/stateengine/StateEngineTools.py b/stateengine/StateEngineTools.py index 23703918b..f47861f2a 100755 --- a/stateengine/StateEngineTools.py +++ b/stateengine/StateEngineTools.py @@ -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 diff --git a/stateengine/StateEngineValue.py b/stateengine/StateEngineValue.py index 1ca824350..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}", @@ -184,7 +173,8 @@ def set(self, value, name="", reset=True, item=None, copyvalue=True): 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]) @@ -209,7 +199,8 @@ def set(self, value, name="", reset=True, item=None, copyvalue=True): 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 @@ -237,13 +228,14 @@ def set(self, value, name="", reset=True, item=None, copyvalue=True): 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 @@ -255,7 +247,8 @@ def set(self, value, name="", reset=True, item=None, copyvalue=True): 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) @@ -285,7 +278,8 @@ def set(self, value, name="", reset=True, item=None, copyvalue=True): 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: @@ -312,7 +306,7 @@ def set(self, value, name="", reset=True, item=None, copyvalue=True): field_value[i] = False _value, _issue = self.__do_cast(field_value[i]) - if _issue: + if _issue not in [[], None, [None], self.__issues]: self.__issues.append(_issue) self.__value.append(_value) else: @@ -320,7 +314,7 @@ def set(self, value, name="", reset=True, item=None, copyvalue=True): 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 @@ -331,23 +325,30 @@ def set(self, value, name="", reset=True, item=None, copyvalue=True): 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 @@ -366,7 +367,7 @@ def set(self, value, name="", reset=True, item=None, copyvalue=True): elif isinstance(field_value, str) and field_value.lower() in ['false', 'no']: field_value = False self.__value, _issue = self.__do_cast(field_value) - if _issue: + if _issue not in [[], None, [None], self.__issues]: self.__issues.append(_issue) else: self.__value = None @@ -696,7 +697,7 @@ def __get_eval(self): try: _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)) @@ -727,7 +728,7 @@ def __get_eval(self): try: _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) @@ -744,7 +745,7 @@ def __get_eval(self): try: _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() @@ -754,8 +755,7 @@ def __get_eval(self): self._log_info(_issue) value = None if value is not None: - _newvalue, _issue = self.__do_cast(value) - values.append(_newvalue) + values.append(value) self._log_decrease_indent() else: self._log_debug("Checking eval (no str, no list): {0}.", self.__eval) @@ -763,7 +763,7 @@ def __get_eval(self): self._log_increase_indent() _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) @@ -784,30 +784,63 @@ def __get_from_item(self): if isinstance(self.__item, list): values = [] for val in self.__item: + _new_values = [] if val is None: _newvalue = None else: - _newvalue, _issue = self.__do_cast(val.property.value) - if _issue: - _issue_list.append(_issue) - values.append(_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] = _newvalue + 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, _issue = self.__do_cast(self.__item.property.value) - if _issue: - _issue_list.append(_issue) + 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] = _newvalue - values = _newvalue - if values is not None: - return values + 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 @@ -819,10 +852,11 @@ def __get_from_item(self): except Exception as ex: values = self.__item _issue = "Problem while reading item path '{0}': {1}.".format(values, ex) - _issue_list.append(_issue) + if _issue not in _issue_list: + _issue_list.append(_issue) self._log_info(_issue) _newvalue, _issue = self.__do_cast(values) - if _issue: + if _issue not in [[], None, [None], _issue_list]: _issue_list.append(_issue) return _newvalue diff --git a/stateengine/plugin.yaml b/stateengine/plugin.yaml index e91ad8795..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: diff --git a/stateengine/user_doc/03_regelwerk.rst b/stateengine/user_doc/03_regelwerk.rst index 90cc18339..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. 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 62c884579..94335b705 100755 --- a/stateengine/user_doc/05_bedingungen.rst +++ b/stateengine/user_doc/05_bedingungen.rst @@ -106,7 +106,7 @@ 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 @@ -428,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/08_beispiel.rst b/stateengine/user_doc/08_beispiel.rst index 8a300d35b..118983b21 100755 --- a/stateengine/user_doc/08_beispiel.rst +++ b/stateengine/user_doc/08_beispiel.rst @@ -641,7 +641,7 @@ 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 ä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. diff --git a/stateengine/user_doc/09_vorlagen.rst b/stateengine/user_doc/09_vorlagen.rst index 68ce0409e..cf90e8e35 100755 --- a/stateengine/user_doc/09_vorlagen.rst +++ b/stateengine/user_doc/09_vorlagen.rst @@ -121,7 +121,7 @@ standard ======== Ein praktisch leerer Status, der immer am Ende angehängt werden sollte. Dieser Status wird -eingenommen, wenn keine Bedingungen der anderen Stati erfüllt sind. +eingenommen, wenn keine Bedingungen der anderen Zustände erfüllt sind. Pluginspezifische Templates --------------------------- 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/webif/templates/visu.html b/stateengine/webif/templates/visu.html index 11ea42a7b..8b2d31970 100755 --- a/stateengine/webif/templates/visu.html +++ b/stateengine/webif/templates/visu.html @@ -95,7 +95,7 @@ const toolTip = document.getElementById('toolTip'); toolTip.addEventListener('click', function() { - if ($('#visu_object').hasClass("enable_tooltip")) { + if ($('#visu_object').hasClass("enable_tooltip") || !toolTip.checked) { $('#visu_object').removeClass("enable_tooltip"); } else { 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 245f4740e..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 }' @@ -148,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. @@ -173,8 +180,8 @@ Beispiel 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. @@ -277,24 +284,24 @@ Bei Auswahl eines dieser Kommandos im Telegram Client kann dann ein Item vom Typ ``name`` Item wird mit diesem Namen im Bot als Kommando dargestellt -``type`` +``type`` Möglichkeiten: on, off, onoff, toggle, num - on + on * nur Einschalten ist möglich - off + off * nur Ausschalten ist möglich - onoff - * das Ein- und Ausschalten muss mit einen weiteren Kommando vom Tastaturmenu ausgewählt werden + 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 + toggle * der Wert des Items wird umgeschltet (0 zu 1; 1 zu 0) - num + 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`` +``min`` Minimalwert (verwendbar bei type:num) -``max`` +``max`` Maximalwert (verwendbar bei type:num) ``timeout`` Zeit nach welcher der Befehl mit Antwort(onoff/question/num) abgebrochen wird (default 20Sekunden) @@ -390,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) @@ -590,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 %} +{% set logo_frame = false %} + +{% set tab1title = "" ~ p.get_shortname() ~ " Items (" ~ item_count ~ ")" %} +{% set tabcount = 1 %} + +{% block headtable %} +{% set sun = namespace() %} +{% set sun.sunrise_text = '' %} +{% set sun.sunset_text = '' %} +{% set entries_rise = p._webdata['sunCalculated']['sunrise'] %} +{% set entries_set = p._webdata['sunCalculated']['sunset'] %} +{% for i in entries_rise %} + {% set sun.sunrise_text = sun.sunrise_text ~ i ~ ': ' %} + {% set sun.sunrise_text = sun.sunrise_text ~ p._webdata['sunCalculated']['sunrise'][i] %} + {% set sun.sunset_text = sun.sunset_text ~ i ~ ': ' %} + {% set sun.sunset_text = sun.sunset_text ~ p._webdata['sunCalculated']['sunset'][i] %} + {% if not loop.last %} + {% set sun.sunrise_text = sun.sunrise_text ~ ', ' %} + {% set sun.sunset_text = sun.sunset_text ~ ', ' %} + {% endif %} +{% endfor %} + + + + + + + + + + + + + +
{{ _('Sonnenaufgang') }}
+ {{ sun.sunrise_text }}
{{ _('Sonnenuntergang') }}
+ {{ sun.sunset_text }}
Items: {{ item_count }}
+{% endblock headtable %} + +{% block bodytab1 %} +
+
+ + {{ _('Die folgenden Items sind dem UZSU Plugin zugewiesen') }}. +
+ + + + + + + + + + + + + + + + + {% for item in p._webdata['items'] %} + {% set planned = p._webdata['items'][item]['planned']['value'] %} + {% if (p._webdata['items'][item]['active'] == 'True' and planned == '-') or + p._webdata['items'][item]['interpolation'] is not defined or p._webdata['items'][item]['interpolation']['itemtype'] == none %} + {% set color = 'red' %} + {% elif p._webdata['items'][item]['planned'] == '-' or p._webdata['items'][item]['active'] == 'False' %} + {% set color = 'gray' %} + {% else %} + {% set color = 'green' %} + {% endif %} + + + + + + + + + + + + + {% endfor %} + +
{{ _('UZSU Item') }}{{ _('Abhängige Items (mit Typ)') }}{{ _('Wert') }}{{ _('Nächster Wert') }}{{ _('Nächstes Update') }}{{ _('Letzter Wert') }}{{ _('Interpolation (Intervall)') }}{{ _('Init') }}{{ _('Dictionary') }}
{{ item }} + {% if p._webdata['items'][item]['interpolation'] is not defined %} + + {% elif p._webdata['items'][item]['interpolation']['itemtype'] == none %} + {{ _('Item existiert nicht!') }} + {% else %}{{ p._webdata['items'][item]['depend']['item'] }} ({{ p._webdata['items'][item]['interpolation']['itemtype'] }}) + {% endif %} + + {% if p._webdata['items'][item]['depend']['value'] is defined %} + {{ p._webdata['items'][item]['depend']['value'] }} + {% endif %} + + {{ p._webdata['items'][item]['planned']['value'] }} + + {% if p._webdata['items'][item]['planned']['time'] == '-' %} + - + {% else %} + ‪{{ p._webdata['items'][item]['planned']['time'] }}‬ + {% endif %} + + {{ p._webdata['items'][item]['lastvalue'] }} + + {% if p._webdata['items'][item]['interpolation'] is not defined %} + {{ _('fehlt!') }} + {% elif p._webdata['items'][item]['interpolation']['type'] %} + {{ p._webdata['items'][item]['interpolation']['type'] }} + {% if p._webdata['items'][item]['interpolation']['type'] != 'none' and p._webdata['items'][item]['interpolation']['interval'] %} + ({{ p._webdata['items'][item]['interpolation']['interval'] }}) + {% endif %} + {% else %}- + {% endif %} + + {% if p._webdata['items'][item]['interpolation'] is not defined %} + {{ _('fehlt!') }} + {% elif p._webdata['items'][item]['interpolation']['initage'] is defined %} + {{ p._webdata['items'][item]['interpolation']['initage'] }} + {% else %}- + {% endif %} + + {{ p._webdata['items'][item]['dict'] }} +
+
+{% endblock bodytab1 %} diff --git a/uzsu/plugin.yaml b/uzsu/plugin.yaml index 5a0096583..79056da47 100755 --- a/uzsu/plugin.yaml +++ b/uzsu/plugin.yaml @@ -18,35 +18,13 @@ plugin: Furthermore the interpolation function allows the calculation of values between two manual settings. You can use this feature for smooth light curves based on the time of the day. ' - requirements: - de: 'SciPy python Modul' - en: 'SciPy python module' - requirements_long: - de: 'Das Plugin benötigt die folgende Software:\n - \n - - libatlas-base-dev: Zumindest auf einem Raspberry Pi mit Debian Stretch ist der Befehl nötig: ``sudo apt install libatlas-base-dev``\n - - Bei neueren SciPy Versionen kann auf einem Raspi der Build trotzdem scheitern. Es ist dann empfohlen, die aktuellste passende Datei (armv6 = Raspi1, armv7 = Rest) - von hier herunterzuladen und ``pip3 install scipy*.whl`` zu starten: https://www.piwheels.org/simple/scipy/\n - - Python Modul scipy: ``pip3 install scipy``. Es wird empfohlen, zuerst die Pythonmodule zu aktualisieren, - aber unbedingt darauf zu achten, dass die Requirements von SmarthomeNG erfüllt bleiben! - Sollte die Installation via pip nicht funktionieren: ``sudo apt update && sudo apt install -y python3-scipy``\n - ' - en: 'This plugin needs the following software to be installed and running:\n - \n - - libatlas-base-dev: On Raspberry Pi debian stretch you also have to run ``sudo apt install libatlas-base-dev``\n - - With newer SciPy versions build can still fail on Raspis. It is recommended to download the most recent file (armv6 = Raspi1, armv7 = others) from here and run - ``pip3 install scipy*.whl``: https://www.piwheels.org/simple/scipy/\n - - Python module scipy: ``pip3 install scipy``. Update your Python packages first - (but make sure they still meet the requirements for smarthomeng)! - If that does not work you can use: ``sudo apt update && sudo apt install -y python3-scipy``\n - ' - maintainer: cmalo, bmxp, onkelandy, andrek - tester: Sandman60, cmalo, schuma + maintainer: cmalo, bmxp, onkelandy, andrek, morg42 + tester: Sandman60, cmalo, schuma, morg42 state: ready keywords: scheduler uzsu trigger series support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1364692-supportthread-für-uzsu-plugin - version: 1.6.6 # Plugin version + version: 2.0.0 # 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/withings_health/README.md b/withings_health/README.md deleted file mode 100755 index e5442f99b..000000000 --- a/withings_health/README.md +++ /dev/null @@ -1,114 +0,0 @@ -# Withings Health - -## Description - -This plugin allows to retrieve data from the Withings (former Nokia) Health API (https://developer.withings.com/api). Currently it -only has support for "Withings WS-50 Smart Body Analyzer", a wifi capabale scale. - -Support Thread: https://knx-user-forum.de/forum/supportforen/smarthome-py/1141179-nokia-health-plugin - -## Requirements - -This plugin requires lib withings-api. You can install this lib with: - -```bash -sudo pip3 install withings-api --upgrade -``` - -You have to register at https://account.withings.com/partner/add_oauth2. -The callback URL to enter when registering is shown via the plugin's web interface and can be added as soon as client_id and consumer_secret have been set in etc/plugin.yaml - -The OAuth2 process can then be triggered via the Web Interface of the plugin. Therefore, at least the first four items of the example below need to exist (access_token, token_expiry, token_type, refresh_token). - -In case your SmartHomeNG instance is offline for too long, the tokens expire. You then have to start the OAuth2 process via the Web Interface again. Errors will be logged in this case! - -## Configuration - -### plugin.yaml -```yaml -withings_health: - plugin_name: withings_health - user_id: - client_id: - consumer_secret: - cycle: 300 - instance: withings_health - -``` - -### items.yaml - -Please be aware that there are dependencies for the values. E.g. the body measurement index will only be calculated if a -height exists. From what i saw so far is, that the height is transmitted only one time, the first time the scale -communicates with the Withings (former Nokia) servers. In case you miss it, set the item value manually! - -The first four items are mandatory, as they are needed for OAuth2 data! - -```yaml -body: - - access_token: - type: str - visu_acl: ro - cache: yes - withings_type@withings_health: access_token - - token_expiry: - type: num - visu_acl: ro - cache: yes - withings_type@withings_health: token_expiry - - token_type: - type: str - visu_acl: ro - cache: yes - withings_type@withings_health: token_type - - refresh_token: - type: str - visu_acl: ro - cache: yes - withings_type@withings_health: refresh_token - - weight: - type: num - visu_acl: ro - withings_type@withings_health: weight - - height: - type: num - visu_acl: ro - withings_type@withings_health: height - - bmi: - type: num - visu_acl: ro - withings_type@withings_health: bmi - - bmi_text: - type: str - visu_acl: ro - withings_type@withings_health: bmi_text - - fat_ratio: - type: num - visu_acl: ro - withings_type@withings_health: fat_ratio - - fat_free_mass: - type: num - visu_acl: ro - withings_type@withings_health: fat_free_mass - - fat_mass_weight: - type: num - visu_acl: ro - withings_type@withings_health: fat_mass_weight - - heart_rate: - type: num - visu_acl: ro - withings_type@withings_health: heart_rate -``` - diff --git a/withings_health/__init__.py b/withings_health/__init__.py index d7cb50ec7..b473f0fb2 100755 --- a/withings_health/__init__.py +++ b/withings_health/__init__.py @@ -7,8 +7,7 @@ # https://www.smarthomeNG.de # https://knx-user-forum.de/forum/supportforen/smarthome-py # -# Sample plugin for new plugins to run with SmartHomeNG version 1.5 and -# upwards. +# Plugin for withings health devices # # SmartHomeNG is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -37,7 +36,7 @@ class WithingsHealth(SmartPlugin): - PLUGIN_VERSION = "1.8.2" + PLUGIN_VERSION = "1.8.3" def __init__(self, sh): super().__init__() @@ -129,8 +128,10 @@ def _update(self): userid=self._user_id, client_id=self._client_id, consumer_secret=self._consumer_secret) - - self._client = WithingsApi(self._creds, refresh_cb=self._store_tokens) + try: + self._client = WithingsApi(self._creds, refresh_cb=self._store_tokens) + except Exception as e: + self.logger.error("Client can not be initialized.") else: self.logger.error( "Token is expired, run OAuth2 again from Web Interface (Expiry Date: {}).".format( diff --git a/withings_health/assets/withings_webif.png b/withings_health/assets/withings_webif.png new file mode 100644 index 000000000..44332de59 Binary files /dev/null and b/withings_health/assets/withings_webif.png differ diff --git a/withings_health/plugin.yaml b/withings_health/plugin.yaml index 233d720b4..8ada1217f 100755 --- a/withings_health/plugin.yaml +++ b/withings_health/plugin.yaml @@ -9,10 +9,9 @@ plugin: tester: 'psilo909' state: ready keywords: health - documentation: 'http://smarthomeng.de/user/plugins_doc/config/withings_health.html' support: 'https://knx-user-forum.de/forum/supportforen/smarthome-py/1141179-nokia-health-plugin' - version: 1.8.2 # Plugin version + version: 1.8.3 # Plugin version sh_minversion: 1.7 # 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 @@ -38,7 +37,6 @@ parameters: consumer_secret: type: str - default: 300 mandatory: True description: de: 'Consumer-Geheimnis von https://account.health.nokia.com/partner/dashboard_oauth2' diff --git a/withings_health/user_doc.rst b/withings_health/user_doc.rst new file mode 100644 index 000000000..d4c3febdb --- /dev/null +++ b/withings_health/user_doc.rst @@ -0,0 +1,152 @@ +.. index:: Plugins; withings_health +.. index:: withings_health + +=============== +withings_health +=============== + +.. image:: webif/static/img/plugin_logo.png + :alt: plugin logo + :width: 300px + :height: 300px + :scale: 50 % + :align: left + +Dieses Plugin ermöglicht den Abruf von Daten aus der Withings (ehemals Nokia) +`Health API (https://developer.withings.com/api)`_. Derzeit bietet es nur +Unterstützung für den "Withings WS-50 Smart Body Analyzer", eine WLAN-fähige Waage. + + +Vorbereitung +============ + +Dieses Plugin benötigt die withings-api. + +Sie müssen sich unter `Withings Account (https://account.withings.com/)`_ registrieren und im Dashboard +eine Applikation anlegen. Der Name ist frei wählbar, die (lokale) Callback-URL wird über die Weboberfläche des Plugins angezeigt: :/plugin/withings_health. +Wenn Sie sich bei der `Withings App (https://app.withings.com/)`_ einloggen, kann die achtstellige Zahl +in der URL ausgelesen und in der Pluginkonfiguration als user_id angegeben werden. + +Weiters muss das Plugin struct mittles ``struct: withings_health.body`` eingebunden werden. + +Der OAuth2-Prozess muss dann über die Weboberfläche des +Plugins ausgelöst werden. Daher müssen zumindest die ersten vier Elemente des folgenden Beispiels +vorhanden sein müssen (access_token, token_expiry, token_type, refresh_token). + +Falls Ihre SmartHomeNG-Instanz zu lange offline ist, verfallen die Token. +Sie müssen dann den OAuth2-Prozess über das Webinterface neu starten. In diesem Fall werden Fehler protokolliert! + +Konfiguration +============= + +.. important:: + + Detaillierte Informationen zur Konfiguration des Plugins sind unter :doc:`/plugins_doc/config/withings_health` zu finden. + + +plugin.yaml +----------- + +.. code-block:: yaml + + withings_health: + plugin_name: withings_health + user_id: + client_id: + consumer_secret: + cycle: 300 + instance: withings_health + + +items.yaml +---------- + +Bitte beachten Sie, dass es Abhängigkeiten bei den Werten gibt. So wird z.B. der +Körpermaßindex nur berechnet, wenn eine Körpergröße vorhanden ist. Diese wird nur einmal übertragen, nämlich +wenn die Waage das erste Mal mit den Withings (ehemals Nokia) Servern kommuniziert. +Notfalls muss der Wert manuell hinterlegt werden. + +Die ersten vier Elemente sind obligatorisch, da sie für OAuth2-Daten benötigt werden! +Die sinnvollste Herangehensweise ist hier, das Plugin struct ``body`` komplett zu integrieren. + +.. code-block:: yaml + + body: + + access_token: + type: str + visu_acl: ro + cache: yes + withings_type@withings_health: access_token + + token_expiry: + type: num + visu_acl: ro + cache: yes + withings_type@withings_health: token_expiry + + token_type: + type: str + visu_acl: ro + cache: yes + withings_type@withings_health: token_type + + refresh_token: + type: str + visu_acl: ro + cache: yes + withings_type@withings_health: refresh_token + + weight: + type: num + visu_acl: ro + withings_type@withings_health: weight + + height: + type: num + visu_acl: ro + withings_type@withings_health: height + + bmi: + type: num + visu_acl: ro + withings_type@withings_health: bmi + + bmi_text: + type: str + visu_acl: ro + withings_type@withings_health: bmi_text + + fat_ratio: + type: num + visu_acl: ro + withings_type@withings_health: fat_ratio + + fat_free_mass: + type: num + visu_acl: ro + withings_type@withings_health: fat_free_mass + + fat_mass_weight: + type: num + visu_acl: ro + withings_type@withings_health: fat_mass_weight + + heart_rate: + type: num + visu_acl: ro + withings_type@withings_health: heart_rate + +Web Interface +============= + +Das Webinterface sollte zur erstmaligen Herstellung der Verbindung (Authentifizierung) genutzt werden. + +Außerdem werden die Informationen zu den passenden Items angezeigt. + +.. image:: assets/withings_webif.png + :height: 1656px + :width: 3328px + :scale: 25% + :alt: Web Interface + :align: center diff --git a/withings_health/webif/__init__.py b/withings_health/webif/__init__.py index e8422f71c..48c8ec737 100755 --- a/withings_health/webif/__init__.py +++ b/withings_health/webif/__init__.py @@ -105,11 +105,17 @@ def index(self, reload=None, state=None, code=None, error=None): self.plugin._client = None tmpl = self.tplenv.get_template('index.html') + try: + token_expiry_val = datetime.datetime.fromtimestamp( + self.plugin.get_item('token_expiry').property.value, tz=self.plugin.shtime.tzinfo()) + except Exception as e: + self.logger.error("Please integrate the plugin struct to make the plugin work correctly.") + token_expiry_val = 0 return tmpl.render(plugin_shortname=self.plugin.get_shortname(), plugin_version=self.plugin.get_version(), interface=None, item_count=len(self.plugin.get_items()), plugin_info=self.plugin.get_info(), tabcount=2, callback_url=self.plugin.get_callback_url(), tab1title="Withings Health Items (%s)" % len(self.plugin.get_items()), tab2title="OAuth2 Data", authorize_url=self._auth.get_authorize_url(), - p=self.plugin, token_expiry=datetime.datetime.fromtimestamp(self.plugin.get_item( - 'token_expiry')(), tz=self.plugin.shtime.tzinfo()), now=self.plugin.shtime.now(), code=code, + p=self.plugin, token_expiry=token_expiry_val, + now=self.plugin.shtime.now(), code=code, state=state, reload=reload, language=self.plugin.get_sh().get_defaultlanguage()) diff --git a/withings_health/webif/templates/index.html b/withings_health/webif/templates/index.html index 9c04c73e3..55d181a8d 100755 --- a/withings_health/webif/templates/index.html +++ b/withings_health/webif/templates/index.html @@ -93,7 +93,7 @@
{{ _('Withings Health Items') }} ({{ p.get_items()|length }})
{{ _('OAuth2 Authorization URL') }} - {{ _('Hier zuerst registrieren:') }} https://account.withings.com/partner/add_oauth2
+ {{ _('Hier zuerst registrieren') }}
{{ _('Hier klicken, um OAuth2 Prozess zu starten!') }} @@ -118,4 +118,4 @@
{{ _('Withings Health Items') }} ({{ p.get_items()|length }})
{{ p.get_item('refresh_token')() }} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/yamahayxc/__init__.py b/yamahayxc/__init__.py index db637283a..a2ec97899 100755 --- a/yamahayxc/__init__.py +++ b/yamahayxc/__init__.py @@ -320,7 +320,8 @@ def _update_state(self, yamaha_host, update_items=True): if state is None: return state2 = self._submit_payload(yamaha_host, self._build_cmd_get_play_state()) - state.update(state2) + state2.update(state) + state = state2 # retrieving only single items from device is not possible # so just get everything and update sh.py items