diff --git a/README.md b/README.md
index a218b6397b..937d41f0bf 100644
--- a/README.md
+++ b/README.md
@@ -4,16 +4,44 @@
![Github Tag](https://img.shields.io/github/v/release/smarthomeng/smarthome?sort=semver)
![Made with Python](https://img.shields.io/badge/made%20with-python-blue.svg)
-[![Build Status on TravisCI](https://travis-ci.org/smarthomeNG/smarthome.svg?branch=develop)](https://travis-ci.org/smarthomeNG/smarthome)
+[![Build Status on TravisCI](https://travis-ci.com/smarthomeNG/smarthome.svg?branch=master)](https://travis-ci.com/smarthomeNG/smarthome)
[![Join the chat at https://gitter.im/smarthomeNG/smarthome](https://badges.gitter.im/smarthomeNG/smarthome.svg)](https://gitter.im/smarthomeNG/smarthome?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
-SmartHomeNG [1] is a software that serves a basis for home automation. It interconnects multiple devices using plugins to access their specific interfaces.
-This file contains basic information about the basic directories of SmartHomeNG.
+SmartHomeNG [1] ist eine Software die eine Basis für eine Heimautomation bereitstellt. Über Plugins können spezielle Schnittstellen angesprochen und damit die Funktionalität des Gesamtsystems erweitert werden.
-Developer documentation ([part of the user documentation](https://www.smarthomeng.de/user/entwicklung/entwicklung.html)) and user documentation ([german](https://www.smarthomeNG.de/user)) can be found on [www.smarthomeNG.de](https://www.smarthomeNG.de)
+Auf der ([Webseite des Projektes](https://www.smarthomeNG.de)) kann eine [Benutzerdokumentation](https://www.smarthomeNG.de) eingesehen werden.
+
+Ein [Wiki](https://github.com/smarthomeNG/smarthome/wiki) existiert zumeist in deutscher Sprache.
+
+Die Kernfunktionalität wird alle 6-9 Monate in einem Release erweitert und freigegeben.
+
+## Benutzte Werkzeuge
+
+| Werkzeug | beschreibung |
+| --- | :--- |
+| | SmartHomeNG wird mit der Pycharm IDE entwickelt. |
+| | Das Admin Interface von SmartHomeNG wird mit WebStorm IDE entwickelt. |
+
+## Aktueller Status der Entwicklung
+
+[![Aktuelle Entwicklung](https://travis-ci.com/smarthomeNG/smarthome.svg?branch=develop)](https://travis-ci.com/smarthomeNG/smarthome)
+
+
+---
+
+# SmartHomeNG and other languages
+
+SmartHomeNG [1] is a software that serves as a basis for home automation. It interconnects multiple devices using plugins to access their specific interfaces.
+
+User documentation ([german](https://www.smarthomeNG.de/user)) and developer documentation ([part of the user documentation](https://www.smarthomeng.de/user/entwicklung/entwicklung.html)) can be found on [www.smarthomeNG.de](https://www.smarthomeNG.de)
Additional information can be found in the [SmartHomeNG Wiki](https://github.com/smarthomeNG/smarthome/wiki).
+It is possible to read the documentation with [Google's translation service](https://translate.google.com/translate?hl=&sl=de&tl=en&u=https://www.smarthomeng.de/dev/user/) in other languages as well.
+
+This readme file contains basic information about the root directories of SmartHomeNG for an overview.
+
+
## Used Tools
| Tool | Description |
@@ -25,24 +53,24 @@ Additional information can be found in the [SmartHomeNG Wiki](https://github.com
## Directory Structure
| directory | description|
-| --- | :--- |
+| --- | :--- |
|bin | the main python file is based here |
-|dev | if you plan to create a plugin then this is the folder you want to have a closer look at |
+|dev | sample files for creating own plugins and modules |
|doc | Source files for the user- and developer documentation |
-|etc | the three basic configuration files smarthome.yaml, module.yaml, plugin.yaml, logic.yaml and logging.yaml are located here, you will edit these files to reflect your basic settings|
-|items | put here your own files for your items |
-|lib | some more core python modules are in this directory. You won't need to change anything here
-|logics | here your logic files are put
-|modules | here are all loadable core-modules located (one subdirectory for every module)
-|plugins | here are all plugins located (one subdirectory for every plugin). The plugins have to be installed from a separate repository (smarthomeNG/plugins)
-|scenes | the scenes are stored here
-| tests | The code for the automated travis tests is stored here
-|tools | there are some tools which help you for creating an initial configuration
-|var | everything that is changed by smarthome is put here, e.g. logfiles, cache, sqlite database etc.
+|etc | the five basic configuration files smarthome.yaml, module.yaml, plugin.yaml, logic.yaml and logging.yaml are located here, you need to edit these files to reflect your basic settings |
+|items | put your own files for your items here |
+|lib | some more core python modules are in this directory. You won't need to change anything here |
+|logics | put your own files for your logics here |
+|modules | here are all loadable core-modules located (one subdirectory for every module) |
+|plugins | here are all plugins located (one subdirectory for every plugin). The plugins have to be installed from a separate repository (smarthomeNG/plugins) |
+|scenes | the scenes are stored here |
+|tests | the code for the automated travis tests is stored here |
+|tools | there are some tools which help you with creating an initial configuration |
+|var | everything that is changed by smarthome is put here, e.g. logfiles, cache, sqlite database etc. |
## Some more detailed info on the configuration files
-As of Version 1.5 the old conf format will still be valid but will be moved out of the docs since it's deprecated now for some time.
+As of Version 1.5 the old conf format will still be valid but is removed from the docs since it's been deprecated now for some time.
### etc/smarthome.yaml
Upon installation you will need to create this file and specify your location.
@@ -57,7 +85,7 @@ tz: Europe/Berlin
```
### etc/module.yaml
-Upon installation you will need to create this file and configure the modules and their parameters. On first start of SmartHomeNG this file is created from ```etc/module.yaml.default```.
+Upon installation you will need to create this file and configure the modules and their parameters. On first start of SmartHomeNG this file is created from ```etc/module.yaml.default```, if not already present.
An example is shown below:
@@ -76,7 +104,7 @@ admin:
```
### etc/plugin.yaml
-Upon installation you will need to create this file and configure the plugins and their parameters. On first start of SmartHomeNG this file is created from ```etc/plugin.yaml.default```.
+Upon installation you will need to create this file and configure the plugins and their parameters. On first start of SmartHomeNG this file is created from ```etc/plugin.yaml.default```, if not already present.
An example is shown below:
@@ -93,7 +121,7 @@ database:
cli:
plugin_name: cli
ip: 0.0.0.0
- update: True
+ update: true
websocket:
plugin_name: visu_websocket
@@ -147,5 +175,5 @@ If you want to read an item call `sh.item.path()` or to set an item `sh.item.pat
```python
# logics/sunset.py
if sh.global.sun(): # if sh.global.sun() == True:
- sh.gloabl.sun(False) # set it to False
+ sh.global.sun(False) # set it to False
```
diff --git a/bin/locale.yaml b/bin/locale.yaml
index 3239b67c5c..1ad8f159a6 100644
--- a/bin/locale.yaml
+++ b/bin/locale.yaml
@@ -75,6 +75,67 @@ global_translations:
'Letztes Update': {'de': '=', 'en': 'Last Update', 'fr': ''}
'Letzter Change': {'de': '=', 'en': 'Last Change', 'fr': ''}
+ #Translations for lib.scene:
+ "A second 'scenes' object has been created. There should only be ONE instance of class 'Scenes'!!! Called from: {frame1} ({frame2})":
+ 'de': "Ein zweites Szenenobjekt wurde erzeugt. Es darf jedoch nur EINE Instanz der Klasse 'Szenes' geben!!! Aufgerufen durch: {frame1} ({frame2})"
+ 'en': '='
+ "Directory '{scenes_dir}' not found. Ignoring scenes.":
+ 'de': "Directory '{scenes_dir}' nicht gefunden. Szenen werden ignoriert."
+ 'en': '='
+ "Scene {scene}, state {state}: action '{action}' is not a dict":
+ 'de': "Szene {scene}, Status {state}: Action '{action}' ist kein Dictionary"
+ 'en': '='
+ "Scene {scene}, state {state}: actions are not a list":
+ 'de': "Szene {scene}, Status {state}: Actionen sind keine Liste"
+ 'en': '='
+ "Reloaded all scenes":
+ 'de': "Alle Szenen neu geladen"
+ 'en': '='
+ "Problem reading scene file {file}: No .yaml or .conf file found with this name":
+ 'de': "Problem beim lesen der Szenen Datei {file}: Keine .yaml oder .conf Datei mit diesem Namen gefunden"
+ 'en': '='
+ "Problem evaluating: {value} - {exception}":
+ 'de': "Problem beim berechnen: {value} - {exception}"
+ 'en': '='
+ "Invalid state '{state}' for scene {scene}":
+ 'de': "Ungültiger Status '{state}' für Szene {scene}"
+ 'en': '='
+ "Learn set to 'False', because '{rvalue}' != '{value}'":
+ 'de': "Learn auf False gesetzt, weil '{rvalue}' != '{value}'"
+ 'en': '='
+ "Could not find item or logic '{ditemname}' specified in {file}":
+ 'de': "Item oder Logik '{ditemname}' nicht gefunden. Wurde in {file} spezifiziert"
+ 'en': '='
+ "unable to get self._scenes['{scenename}']['{action}'][0][2] <- {res}":
+ 'de': "self._scenes['{scenename}']['{action}'][0][2] kann nicht gelesen werden <- {res}"
+ 'en': '='
+
+ #Translations for lib.userfunctions:
+ "Imported userfunctions from '{mmodule}' v{version} - {description}":
+ 'de': "Userfunctions importiert aus '{module}' v{version} - {description}"
+ 'en': '='
+ "Error importing userfunctions from '{module}': {error}":
+ 'de': "Fehler beim importieren von Userfunctions aus '{module}': {error}"
+ 'en': '='
+ "Error reloading userfunctions '{module}': Module is not loaded, trying to newly import userfunctions '{module}' instead":
+ 'de': "Fehler beim Reload von Userfunctions '{module}': Modul ist nicht geladen, versuche stattdessen Userfunctions '{module}' neu zu importieren"
+ 'en': '='
+ "Error reloading userfunctions '{module}': {error} - old version of '{module}' is still active":
+ 'de': "Fehler beim Reload von userfunctions '{module}': {error} - alte Version von '{module}' ist noch aktiv"
+ 'en': '='
+ "Reloaded userfunctions '{module}'":
+ 'de': "Reload: Userfunctions '{module}' neu geladen"
+ 'en': '='
+ "Reload: Userfunctions '{module}' do not exist":
+ 'de': "Reload: Userfunctions '{module}' existieren nicht"
+ 'en': '='
+ "Reload: Loaded new userfunctions '{module}'":
+ 'de': "Reload: Neue Userfunctions '{module}' geladen"
+ 'en': '='
+ "No userfunctions are loaded, nothing to reload":
+ 'de': "Es existieren keine Userfunctions, es gibt nichts für einen Reload"
+ 'en': '='
+
lib.shtime_translations:
'defined': {'de': 'definiert', 'en': '='}
@@ -114,5 +175,12 @@ lib.shtime_translations:
'de': "Aufgerufen mit einem Parameter der nicht vom Typ 'datetime' ist: {dt1}, {dt2}"
'en': '='
"Called with point in time that is earlier than now: {dt}":
- 'de': 'Aufgerufen mit Zeitpunkt in der Vergangenheit'
+ 'de': 'Aufgerufen mit Zeitpunkt in der Vergangenheit: {dt}'
'en': '='
+ "Calculating beginning of week based on year {year}, week {week} and offset {offset}":
+ 'de': 'Berechne Wochenstart basierend auf Jahr {year}, Woche {week} und Offset {offset}'
+ 'en': '='
+ "Calculating length of month based on year {year}, month {month}{debug_month}":
+ 'de': 'Berechne Länge des Monats basierend auf Jahr {year}, Monat {month}{debug_month}'
+ 'en': '='
+
diff --git a/bin/shngversion.py b/bin/shngversion.py
index 2266edcf94..21dd836b82 100644
--- a/bin/shngversion.py
+++ b/bin/shngversion.py
@@ -69,9 +69,13 @@
# Update auf 1.8.1a wg. Kennzeichnung des Stands als "nach dem v1.8.1 Release"
# Update auf 1.8.2 wg. Release
+# Update auf 1.8.2a wg. Kennzeichnung des Stands als "nach dem v1.8.2 Release"
+# Update auf 1.8.2b wg. Erweiterung des Item Loggings"
+# Update auf 1.8.2c wg. Wegen Anpassungen an mem-logging / lib.log
+# Update auf 1.8.2d Unterstützung für User-Functions
-shNG_version = '1.8.2'
-shNG_branch = 'master'
+shNG_version = '1.8.2d'
+shNG_branch = 'develop'
# ---------------------------------------------------------------------------------
FileBASE = None
diff --git a/bin/smarthome.py b/bin/smarthome.py
index 83964e0d66..cc9c1a186b 100644
--- a/bin/smarthome.py
+++ b/bin/smarthome.py
@@ -54,6 +54,7 @@
# https://stackoverflow.com/questions/31469707/changing-the-locale-preferred-encoding-in-python-3-in-windows
+
#####################################################################
# Read command line arguments
#####################################################################
@@ -137,12 +138,12 @@
# Import SmartHomeNG Modules
#####################################################################
#import lib.config
-#import lib.connection
import lib.daemon
#import lib.item
#import lib.log
#import lib.logic
#import lib.module
+#import lib.network
#import lib.plugin
#import lib.scene
#import lib.scheduler
diff --git a/dev/sample_module/__init__.py b/dev/sample_module/__init__.py
index cef09d4ba8..133a904829 100644
--- a/dev/sample_module/__init__.py
+++ b/dev/sample_module/__init__.py
@@ -22,17 +22,19 @@
import os
import logging
-import json
-import cherrypy
+# import json
+# import cherrypy
from lib.model.module import Module
from lib.module import Modules
from lib.shtime import Shtime
+from lib.Utils import Utils
+
class SampleModule(Module):
- version = '1.7.0'
+ version = '1.0.0'
longname = '... module for SmartHomeNG'
port = 0
@@ -47,8 +49,7 @@ def __init__(self, sh, testparam=''):
self.logger = logging.getLogger(__name__)
self._sh = sh
self.shtime = Shtime.get_instance()
- self.logger.debug("Module '{}': Initializing".format(self._shortname))
-
+ self.logger.debug(f"Module '{self._shortname}': Initializing")
# Test if http module is loaded (if the module uses http)
# try:
@@ -63,20 +64,18 @@ def __init__(self, sh, testparam=''):
#
# self._showtraceback = self.mod_http._showtraceback
-
# get the parameters for the module (as defined in metadata module.yaml):
- self.logger.debug("Module '{}': Parameters = '{}'".format(self._shortname, dict(self._parameters)))
+ self.logger.debug(f"Module '{self._shortname}': Parameters = '{dict(self._parameters)}'")
try:
# self.broker_ip = self._parameters['broker_host']
pass
except KeyError as e:
self.logger.critical(
- "Module '{}': Inconsistent module (invalid metadata definition: {} not defined)".format(self._shortname, e))
+ f"Module '{self._shortname}': Inconsistent module (invalid metadata definition: {e} not defined)")
self._init_complete = False
return
- ip = get_local_ipv4_address()
-
+ ip = Utils.get_local_ipv4_address() # remove line if `ip` unused
def start(self):
"""
@@ -87,7 +86,6 @@ def start(self):
"""
pass
-
def stop(self):
"""
If the module has started threads or uses python modules that created threads,
@@ -99,33 +97,6 @@ def stop(self):
pass
-
-def get_local_ipv4_address():
- """
- Get's local ipv4 address of the interface with the default gateway.
- Return '127.0.0.1' if no suitable interface is found
-
- :return: IPv4 address as a string
- :rtype: string
- """
- s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- try:
- s.connect(('8.8.8.8', 1))
- IP = s.getsockname()[0]
- except:
- IP = '127.0.0.1'
- finally:
- s.close()
- return IP
-
-
def translate(s):
+ # needed for AdminUI
return s
-
-
-#import socket
-
-#from lib.plugin import Plugins
-#from lib.utils import Utils
-
-
diff --git a/dev/sample_module/module.yaml b/dev/sample_module/module.yaml
index 3cd752a847..7c89233ccd 100644
--- a/dev/sample_module/module.yaml
+++ b/dev/sample_module/module.yaml
@@ -2,8 +2,8 @@
module:
# Global plugin attributes
classname: SampleModule
- version: 1.7.0
- sh_minversion: 1.7
+ version: 1.0.0
+ sh_minversion: 1.8
# sh_maxversion: # maximum shNG version to use this module (leave empty if latest)
# py_minversion: 3.6 # minimum Python version to use for this module
# py_maxversion: # maximum Python version to use for this module (leave empty if latest)
diff --git a/dev/sample_mqttplugin/__init__.py b/dev/sample_mqttplugin/__init__.py
index b4baa8d9a0..aac813ddd2 100644
--- a/dev/sample_mqttplugin/__init__.py
+++ b/dev/sample_mqttplugin/__init__.py
@@ -25,7 +25,7 @@
#
#########################################################################
-from lib.model.mqttplugin import *
+from lib.model.mqttplugin import MqttPlugin
from lib.item import Items
from .webif import WebInterface
@@ -41,7 +41,7 @@ class SampleMqttPlugin(MqttPlugin):
the update functions for the items
"""
- PLUGIN_VERSION = '1.7.0' # (must match the version specified in plugin.yaml), use '1.0.0' for your initial plugin Release
+ PLUGIN_VERSION = '1.0.0' # (must match the version specified in plugin.yaml), use '1.0.0' for your initial plugin Release
def __init__(self, sh):
"""
@@ -69,7 +69,7 @@ def __init__(self, sh):
# return
# if plugin should start even without web interface
- self.init_webinterface()
+ self.init_webinterface(WebInterface)
return
@@ -108,7 +108,7 @@ def parse_item(self, item):
can be sent to the knx with a knx write function within the knx plugin.
"""
if self.has_iattr(item.conf, 'foo_itemid'):
- self.logger.debug("parse item: {}".format(item))
+ self.logger.debug(f"parse item: {item.property.path}")
# subscribe to topic for relay state
# mqtt_id = self.get_iattr_value(item.conf, 'foo_itemid').upper()
@@ -154,13 +154,10 @@ def update_item(self, item, caller=None, source=None, dest=None):
if self.alive and caller != self.get_shortname():
# code to execute if the plugin is not stopped
# 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".format(item.id()))
+ self.logger.info(f"Update item: {item.property.path}, item has been changed outside this plugin")
if self.has_iattr(item.conf, 'foo_itemtag'):
self.logger.debug(
- "update_item was called with item '{}' from caller '{}', source '{}' and dest '{}'".format(item,
- caller,
- source,
- dest))
+ f"update_item was called with item {item.property.path} from caller {caller}, source {source} and dest {dest}")
pass
diff --git a/dev/sample_mqttplugin/mqtt_shelly_example_simple.py b/dev/sample_mqttplugin/mqtt_shelly_example_simple.py
index d1f2a9a30e..11b30fedbc 100644
--- a/dev/sample_mqttplugin/mqtt_shelly_example_simple.py
+++ b/dev/sample_mqttplugin/mqtt_shelly_example_simple.py
@@ -29,8 +29,9 @@
import json
from lib.module import Modules
-from lib.model.mqttplugin import *
+from lib.model.mqttplugin import MqttPlugin
from lib.item import Items
+from .webif import WebInterface
class Shelly(MqttPlugin):
@@ -66,8 +67,8 @@ def __init__(self, sh):
# Initialization code goes here
self.shelly_items = [] # to hold item information for web interface
- # if plugin should start even without web interface
- self.init_webinterface()
+ # if plugin should start even without web interface
+ self.init_webinterface(WebInterface)
return
@@ -112,7 +113,7 @@ def parse_item(self, item):
can be sent to the knx with a knx write function within the knx plugin.
"""
if self.has_iattr(item.conf, 'shelly_id'):
- self.logger.debug("parsing item: {0}".format(item.id()))
+ self.logger.debug(f"parsing item: {item}")
shelly_id = self.get_iattr_value(item.conf, 'shelly_id').upper()
shelly_type = self.get_iattr_value(item.conf, 'shelly_type').lower()
@@ -125,7 +126,7 @@ def parse_item(self, item):
# subscribe to topic for relay state
topic = 'shellies/' + shelly_type + '-' + shelly_id + '/relay/' + shelly_relay
payload_type = item.property.type
- bool_values = ['off','on']
+ bool_values = ['off', 'on']
self.add_subscription(topic, payload_type, bool_values, item=item)
return self.update_item
@@ -153,12 +154,12 @@ def update_item(self, item, caller=None, source=None, dest=None):
:param source: if given it represents the source
:param dest: if given it represents the dest
"""
- self.logger.info("update_item: {}".format(item.id()))
+ self.logger.info(f"update_item: {item}")
if self.alive and caller != self.get_shortname():
# code to execute if the plugin is not stopped
# 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".format(item.id()))
+ self.logger.info(f"update_item: {item}, item has been changed outside this plugin")
# publish topic with new relay state
shelly_id = self.get_iattr_value(item.conf, 'shelly_id').upper()
@@ -167,118 +168,4 @@ def update_item(self, item, caller=None, source=None, dest=None):
if not shelly_relay:
shelly_relay = '0'
topic = 'shellies/' + shelly_type + '-' + shelly_id + '/relay/' + shelly_relay + '/command'
- self.publish_topic(topic, item(), item, bool_values=['off','on'])
-
- # -----------------------------------------------------------------------
-
- 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:
- self.mod_http = Modules.get_instance().get_module('http') # try/except to handle running in a core version that does not support modules
- except:
- self.mod_http = None
- if self.mod_http == None:
- self.logger.error("Not initializing the web interface")
- return False
-
- import sys
- if not "SmartPluginWebIf" in list(sys.modules['lib.model.smartplugin'].__dict__):
- self.logger.warning("Web interface needs SmartHomeNG v1.5 and up. Not initializing the web interface")
- 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
-# -----------------------------------------------------------------------
-
-import cherrypy
-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 = logging.getLogger(__name__)
- self.webif_dir = webif_dir
- self.plugin = plugin
- self.tplenv = self.init_template_environment()
-
- self.items = Items.get_instance()
-
- @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
- """
- self.plugin.get_broker_info()
-
- tmpl = self.tplenv.get_template('index.html')
- # add values to be passed to the Jinja2 template eg: tmpl.render(p=self.plugin, interface=interface, ...)
- return tmpl.render(p=self.plugin, items=sorted(self.items.return_items(), key=lambda k: str.lower(k['_path'])))
-
-
- @cherrypy.expose
- def get_data_html(self, dataSet=None):
- """
- Return data to update the webpage
-
- For the standard update mechanism of the web interface, the dataSet to return the data for is None
-
- :param dataSet: Dataset for which the data should be returned (standard: None)
- :return: dict with the data needed to update the web page.
- """
- if dataSet is None:
- # get the new data
- self.plugin.get_broker_info()
- data = {}
- data['broker_info'] = self.plugin._broker
- data['broker_uptime'] = self.plugin.broker_uptime()
- data['item_values'] = self.plugin._item_values
-
- # return it as json the the web page
- try:
- return json.dumps(data)
- except Exception as e:
- self.logger.error("get_data_html exception: {}".format(e))
- return {}
-
- return
-
+ self.publish_topic(topic, item(), item, bool_values=['off', 'on'])
diff --git a/dev/sample_mqttplugin/plugin.yaml b/dev/sample_mqttplugin/plugin.yaml
index 7ed9e10da3..cd93fae97f 100644
--- a/dev/sample_mqttplugin/plugin.yaml
+++ b/dev/sample_mqttplugin/plugin.yaml
@@ -12,12 +12,12 @@ 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.7.0 # Plugin version
- sh_minversion: 1.7 # minimum shNG version to use this plugin
+ version: 1.0.0 # Plugin version
+ 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
# py_maxversion: # maximum Python version to use for this plugin (leave empty if latest)
- multi_instance: False # plugin supports multi instance
+ multi_instance: false # plugin supports multi instance
restartable: unknown
classname: SamplePlugin # class containing the plugin
diff --git a/dev/sample_mqttplugin/webif/__init__.py b/dev/sample_mqttplugin/webif/__init__.py
index 2e0335785e..6bd998cd0a 100644
--- a/dev/sample_mqttplugin/webif/__init__.py
+++ b/dev/sample_mqttplugin/webif/__init__.py
@@ -70,6 +70,8 @@ def index(self, reload=None):
:return: contents of the template after beeing rendered
"""
+ self.plugin.get_broker_info()
+
tmpl = self.tplenv.get_template('index.html')
# add values to be passed to the Jinja2 template eg: tmpl.render(p=self.plugin, interface=interface, ...)
return tmpl.render(p=self.plugin,
@@ -89,16 +91,18 @@ def get_data_html(self, dataSet=None):
"""
if dataSet is None:
# get the new data
+ self.plugin.get_broker_info()
data = {}
+ data['broker_info'] = self.plugin._broker
+ data['broker_uptime'] = self.plugin.broker_uptime()
+ data['item_values'] = self.plugin._item_values
- # data['item'] = {}
- # for i in self.plugin.items:
- # data['item'][i]['value'] = self.plugin.getitemvalue(i)
- #
# return it as json the the web page
- # try:
- # return json.dumps(data)
- # except Exception as e:
- # self.logger.error("get_data_html exception: {}".format(e))
- return {}
+ try:
+ return json.dumps(data)
+ except Exception as e:
+ self.logger.error("get_data_html exception: {}".format(e))
+ return {}
+
+ return
diff --git a/dev/sample_plugin/__init__.py b/dev/sample_plugin/__init__.py
index ce45afa689..8b47bbe00f 100644
--- a/dev/sample_plugin/__init__.py
+++ b/dev/sample_plugin/__init__.py
@@ -7,7 +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
+# Sample plugin for new plugins to run with SmartHomeNG version 1.8 and
# upwards.
#
# SmartHomeNG is free software: you can redistribute it and/or modify
@@ -25,7 +25,7 @@
#
#########################################################################
-from lib.model.smartplugin import *
+from lib.model.smartplugin import SmartPlugin
from lib.item import Items
from .webif import WebInterface
@@ -39,9 +39,13 @@ class SamplePlugin(SmartPlugin):
"""
Main class of the Plugin. Does all plugin specific stuff and provides
the update functions for the items
+
+ HINT: Please have a look at the SmartPlugin class to see which
+ class properties and methods (class variables and class functions)
+ are already available!
"""
- PLUGIN_VERSION = '1.7.1' # (must match the version specified in plugin.yaml), use '1.0.0' for your initial plugin Release
+ PLUGIN_VERSION = '1.0.0' # (must match the version specified in plugin.yaml), use '1.0.0' for your initial plugin Release
def __init__(self, sh):
"""
@@ -115,10 +119,11 @@ def parse_item(self, item):
can be sent to the knx with a knx write function within the knx plugin.
"""
if self.has_iattr(item.conf, 'foo_itemtag'):
- self.logger.debug("parse item: {}".format(item))
+ self.logger.debug(f"parse item: {item}")
# todo
# if interesting item for sending values:
+ # self._itemlist.append(item)
# return self.update_item
def parse_logic(self, logic):
@@ -145,11 +150,10 @@ def update_item(self, item, caller=None, source=None, dest=None):
if self.alive and caller != self.get_shortname():
# code to execute if the plugin is not stopped
# 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".format(item.id()))
+ self.logger.info(f"Update item: {item.property.path}, item has been changed outside this plugin")
if self.has_iattr(item.conf, 'foo_itemtag'):
- self.logger.debug("update_item was called with item '{}' from caller '{}', source '{}' and dest '{}'".format(item,
- caller, source, dest))
+ self.logger.debug(f"update_item was called with item {item.property.path} from caller {caller}, source {source} and dest {dest}")
pass
def poll_device(self):
@@ -175,5 +179,3 @@ def poll_device(self):
# # the source should be included when updating the the value:
# item(device_value, self.get_shortname(), source=device_source_id)
pass
-
-
diff --git a/dev/sample_plugin/plugin.yaml b/dev/sample_plugin/plugin.yaml
index d9248b2318..17cda20d27 100644
--- a/dev/sample_plugin/plugin.yaml
+++ b/dev/sample_plugin/plugin.yaml
@@ -12,12 +12,12 @@ 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.7.1 # Plugin version (must match the version specified in __init__.py)
- sh_minversion: 1.5 # minimum shNG version to use this plugin
+ version: 1.0.0 # 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
# py_maxversion: # maximum Python version to use for this plugin (leave empty if latest)
- multi_instance: False # plugin supports multi instance
+ multi_instance: false # plugin supports multi instance
restartable: unknown
classname: SamplePlugin # class containing the plugin
@@ -45,7 +45,7 @@ parameters:
param3:
type: str
# If 'mandatory' is specified, a 'default' attribute must not be specified
- mandatory: True
+ mandatory: true
description:
de: 'Demo Parameter der angegeben werden muss'
en: 'Demonstration parameter which has to be specified'
diff --git a/dev/sample_plugin/user_doc.rst b/dev/sample_plugin/user_doc.rst
index 18e218c0a2..fdfdbd311e 100644
--- a/dev/sample_plugin/user_doc.rst
+++ b/dev/sample_plugin/user_doc.rst
@@ -60,7 +60,8 @@ Hier können ausführlichere Beispiele und Anwendungsfälle beschrieben werden.
Web Interface
-------------
+Die Datei ``dev/sample_plugin/webif/templates/index.html`` sollte als Grundlage für Webinterfaces genutzt werden. Um Tabelleninhalte nach Spalten filtern und sortieren zu können, muss der entsprechende Code Block mit Referenz auf die relevante Table ID eingefügt werden (siehe Doku).
+
SmartHomeNG liefert eine Reihe Komponenten von Drittherstellern mit, die für die Gestaltung des Webinterfaces genutzt werden können. Erweiterungen dieser Komponenten usw. finden sich im Ordner ``/modules/http/webif/gstatic``.
Wenn das Plugin darüber hinaus noch Komponenten benötigt, werden diese im Ordner ``webif/static`` des Plugins abgelegt.
-
\ No newline at end of file
diff --git a/dev/sample_plugin/webif/templates/index.html b/dev/sample_plugin/webif/templates/index.html
index 0d49ce7ded..421d07c4f6 100644
--- a/dev/sample_plugin/webif/templates/index.html
+++ b/dev/sample_plugin/webif/templates/index.html
@@ -15,14 +15,37 @@
var objResponse = JSON.parse(response);
myProto = document.getElementById(dataSet);
for (var device in objResponse) {
-
+ */
}
}
}
+
+
{% endblock pluginscripts %}
@@ -70,7 +93,7 @@
{% endblock %}
{% set tabcount = 4 %}
@@ -90,6 +113,36 @@
{% block bodytab1 %}
{{ _('Hier kommt der Inhalt des Webinterfaces hin.') }}
+
+
+
+
+ {{ _('Item') }}
+ {{ _('Wert') }}
+
+
+
+ {% for item in p.get_items() %}
+ {% if p.has_iattr(item.conf, '') %}
+
+ {{ item._path }}
+ {{ item() }}
+
+ {% endif %}
+ {% endfor %}
+
+
+
{% endblock bodytab1 %}
diff --git a/doc/TO DO b/doc/TO DO
deleted file mode 100644
index 866d9ff4e0..0000000000
--- a/doc/TO DO
+++ /dev/null
@@ -1,21 +0,0 @@
-
-Beim Bau der v1.7.1-master Dokumentation:
-
-Developer Doku:
-conf_to_yaml_converter.py - tool to convert shng .conf files to yaml
-
-
-WARNING: invalid signature for automethod ('modules.admin::WebApi.')
-WARNING: don't know which module to import for autodocumenting 'modules.admin::WebApi.' (try placing a "module" or "currentmodule" directive in the document, or giving an explicit module name)
-looking for now-outdated files... none found
-
-
-
-User Doku:
-reading sources... [100%] visualisierung/visualisierung_widgets
-/usr/local/shng_doc/work/doc/user/source/konfiguration/konfigurationsdateien/scenes.rst:209: WARNING: Explicit markup ends without a blank line; unexpected unindent.
-/usr/local/shng_doc/work/doc/user/source/konfiguration/konfigurationsdateien/scenes.rst:220: WARNING: Explicit markup ends without a blank line; unexpected unindent.
-/usr/local/shng_doc/work/doc/user/source/plugins_doc/config/ical.rst:86: WARNING: Inline strong start-string without end-string.
-/usr/local/shng_doc/work/doc/user/source/plugins_doc/config/ical.rst:86: WARNING: Inline emphasis start-string without end-string.
-/usr/local/shng_doc/work/doc/user/source/plugins_doc/config/yamahayxc.rst:36: WARNING: Block quote ends without a blank line; unexpected unindent.
-looking for now-outdated files... none found
diff --git a/doc/build_plugin_config_files.py b/doc/build_plugin_config_files.py
index 3d317f25bb..a336786df8 100644
--- a/doc/build_plugin_config_files.py
+++ b/doc/build_plugin_config_files.py
@@ -627,11 +627,17 @@ def write_configfile(plg, configfile_dir, language='de'):
if fp != '':
fp += ', '
fp += par
- if func_param_yaml[par].get('default', None) != None:
- default = str(func_param_yaml[par].get('default', None))
- if func_param_yaml[par].get('type', 'foo') == 'str':
- default = " '" + default + "'"
- fp += '=' + default
+ if func_param_yaml[par] != None and isinstance(func_param_yaml[par], dict):
+ if func_param_yaml[par].get('default', None) != None:
+ default = str(func_param_yaml[par].get('default', None))
+ if func_param_yaml[par].get('type', 'foo') == 'str':
+ default = " '" + default + "'"
+ fp += '=' + default
+ else:
+ print(f"\n\nFEHLER: Ungültige Plugin-Funktion:")
+ print(f"Plugin: {plgname}")
+ print(f"par : {par}\n")
+ print(f"func_param_yaml: {func_param_yaml}\n")
write_heading(fh, f + '('+fp+')', 2)
fh.write('\n')
diff --git a/doc/developer/source/core_libraries.rst b/doc/developer/source/core_libraries.rst
index 84175de16c..d04c15cf4c 100644
--- a/doc/developer/source/core_libraries.rst
+++ b/doc/developer/source/core_libraries.rst
@@ -30,7 +30,6 @@ Logiken verwendet werden:
:maxdepth: 5
:titlesonly:
- /lib/connection
/lib/db
/lib/logutils
/lib/network
diff --git a/doc/developer/source/development_plugin/libraries_plugins.rst b/doc/developer/source/development_plugin/libraries_plugins.rst
index 95e71e9fed..10188b6642 100644
--- a/doc/developer/source/development_plugin/libraries_plugins.rst
+++ b/doc/developer/source/development_plugin/libraries_plugins.rst
@@ -9,7 +9,6 @@ The description of their functions is shown here:
:maxdepth: 4
:titlesonly:
- /lib/connection
/lib/db
/lib/logutils
/lib/network
diff --git a/doc/developer/source/development_plugin/webinterface_filling_webinterface.rst b/doc/developer/source/development_plugin/webinterface_filling_webinterface.rst
index aece853571..7ff36ada7c 100644
--- a/doc/developer/source/development_plugin/webinterface_filling_webinterface.rst
+++ b/doc/developer/source/development_plugin/webinterface_filling_webinterface.rst
@@ -61,13 +61,15 @@ To bring the webinterface up to life, the following steps should be followed:
2. Modify the template **webif/templates/index.html** to display the data you want.
To display a list of the items selected by the Python code above on the first tab of the
body of the webinterface, insert the following code between ``{% block bodytab1 %}`` and
- ``{% endblock bodytab1 %}``:
+ ``{% endblock bodytab1 %}``. Make sure to use correct HTML code for the tables
+ including ```` and `` `` tags with the corresponding end tags
+ and to set a unique table id for every table.
.. code-block:: HTML
-
+
{{ _('Item') }}
@@ -88,8 +90,30 @@ To bring the webinterface up to life, the following steps should be followed:
- 3. The logo on the topleft is automatically replaced with the logo of the **plugin type**.
- If the webinterface should have an individaul logo, the file with the logo must be placed in
- the directory **webif/static/img** and has to be named **plugin_logo**. It may be of type **.png**, **.jpg** or **.svg**.
+ 3. Add the following script code between ``{% block pluginscripts %}`` and
+ ``{% endblock pluginscripts %}`` to enable filtering and sorting of the tables.
+ The code ``$('#maintable').DataTable( { "paging": false, fixedHeader: true } );``
+ has to be copied for every table using the sort/filter feature!
+ Make sure to adapt the table id (#maintable) accordingly:
+ .. code-block:: HTML
+
+
+ 4. The logo on the topleft is automatically replaced with the logo of the **plugin type**.
+ If the webinterface should have an individaul logo, the file with the logo must be placed in
+ the directory **webif/static/img** and has to be named **plugin_logo**. It may be of type **.png**, **.jpg** or **.svg**.
diff --git a/doc/developer/source/requirements.rst b/doc/developer/source/requirements.rst
index ca2416885f..82e6891045 100644
--- a/doc/developer/source/requirements.rst
+++ b/doc/developer/source/requirements.rst
@@ -30,4 +30,3 @@ If using a hardware platform without buffered real time clock it is mandatory to
Otherwise SmartHomeNG will not start due to missing time information.
Some libraries within SmartHomeNG still use functions depending on Unix flavour.
-Thus SmartHomeNG does not run on Windows and MacOS right now.
diff --git a/doc/requirements.txt b/doc/requirements.txt
index f81c958c61..07c78bf22d 100644
--- a/doc/requirements.txt
+++ b/doc/requirements.txt
@@ -1,11 +1,29 @@
-sphinx>=3.0
+sphinx>=3.1,<4
sphinx-rtd-theme
-recommonmark>=0.6.0
+
+# create tabs for content for alternative ways to use like | Linux | MacOS | Windows |
+# https://sphinx-tabs.readthedocs.io/en/latest/
+sphinx-tabs
+
+# the following extensions create collapsible parts of documentation
+# sphinx-panels seem to be most extensive
+#sphinx-togglebutton
+#sphinx-toolbox
+#sphinx-panels https://sphinx-panels.readthedocs.io/en/latest/
+
+# recommonmark is marked deprecated as of April 30th, 2021
+#recommonmark>=0.6.0
+# the replacement to support Markdown like Readme.md is MyST
+# see at https://www.sphinx-doc.org/en/master/usage/markdown.html and
+# at https://myst-parser.readthedocs.io/en/latest/using/intro.html for a primer on MyST
+myst-parser
ruamel.yaml>=0.13.7,<=0.15.74;python_version<'3.7'
ruamel.yaml>=0.15.0,<=0.15.74;python_version=='3.7'
ruamel.yaml>=0.15.78,<=0.16.8;python_version>='3.8'
-# to create pdf files install also
+# to create pdf files it would be needed to install also
# rst2pdf
-# svglib
\ No newline at end of file
+# svglib
+# but there is no fully working lib to convert svg to pdf currently so we can
+# not create pdf files at the moment
diff --git a/doc/user/source/_static/img/SmarthomeNG_V1.8.0.svg b/doc/user/source/_static/img/SmarthomeNG_V1.8.0.svg
new file mode 100644
index 0000000000..c47d2b495f
--- /dev/null
+++ b/doc/user/source/_static/img/SmarthomeNG_V1.8.0.svg
@@ -0,0 +1,2885 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hardware
+
+
+ z.B. Intel NUC, Raspberry Pi 1-3, BeagleBone, etc. oder Virtuelle Maschine
+
+
+
+
+
+
+
+
+ Betriebssystem
+
+
+ Linux
+ z.B. Debian, Raspbian, Ubuntu etc.
+
+
+
+
+
+
+
+
+ SmartHomeNG
+
+
+ Programmiersprache: Python3, Version >= 3.5
+
+
+ Basispfad: /var/usr/local/smarthome
+
+
+ Konfigurationsdateien in ./etc im YAML Format:
+
+
+
+ smarthome.yaml
+ → Geografieinformationen
+
+
+ logic.yaml
+ → verwendete Logiken
+
+
+ plugin.yaml
+ → verwendete Plugins
+
+
+ Konfigurationsdateien in ./items
+
+
+
+ eine odere mehrere Dateien mit Item-Definitionen
+
+
+ Konfigurationsdateien in ./logics
+
+
+
+ Je Logik eine Datei mit Python-Code z.B.
+
+
+ InitSmarthome.py
+
+
+
+
+
+
+
+
+
+
+
+
+
+ mqtt Modul
+
+
+
+
+
+
+
+
+ KNX
+
+
+
+
+
+
+ Plugins
+
+
+
+
+
+
+
+
+
+
+
+
+
+ knxd
+
+
+ Greift auf direkt angeschlossene
+
+
+ Hardware wie USB, ROT zu. Der
+
+
+ knxd kann aber auch auf
+
+
+ anderen Systemen installiert
+
+
+ sein wie z.B. Wiregate
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ owserver
+
+
+ Greift auf direkt angeschlossene
+
+
+ Hardware zu,
+
+
+ Der owserver kann aber auch
+
+
+ auf einem anderen System
+
+
+ installiert sein wie z.B. Wiregate
+
+
+
+
+
+
+
+
+
+
+
+
+
+ SmartVISU (HTML+PHP)
+
+
+ PHP und HTML-Dateien unter /var/html/www/smartvisu
+
+
+ · benötigt einen Webserver (Apache2, lighttpd, nginx) und PHP7
+
+
+ · liefert über PHP Seiten die statischen HTML aus
+
+
+ · Templateengine TWIG ist Basis für die von PHP erstellten Seiten
+
+
+ ·
+ keine
+ Verbindung vom Webserver aus zu SmartHomeNG
+
+
+ · Über Visu-Plugin von smarthomeNG werden Webseiten
+
+
+ auf Basis der Items erstellt: sv_widget = {{ … }}}
+
+
+
+
+
+
+
+ KNX Bus
+
+
+
+
+
+
+
+ IP-
+
+
+ Schnittstelle
+
+
+
+
+
+
+
+
+ IP-
+
+
+ Router
+
+
+
+
+
+
+
+ USB
+
+
+
+
+
+
+ ROT
+
+
+
+
+
+
+ RS232
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Onewire Bus
+
+
+
+
+
+
+
+
+ MQTT Broker (Mosquitto)
+
+
+
+
+
+
+ USB
+
+
+ Busmaster
+
+
+
+
+
+
+
+ ROT
+
+
+
+
+
+
+
+
+ IoT
+ Device
+
+
+
+
+
+
+ IoT
+ Device
+
+
+
+
+
+
+ IoT
+ Device
+
+
+
+
+
+
+ IoT
+ Device
+
+
+
+
+
+
+ IoT
+ Device
+
+
+
+
+
+
+ IoT
+ Device
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ smartvisu
+
+
+
+
+
+
+ 1wire
+
+
+
+
+
+
+
+ Mailsend
+
+
+ Senden und
+
+
+ Empfangen
+
+
+
+
+
+
+
+
+ UZSU
+
+
+ Universelle
+
+
+ Zeitschaltuhr
+
+
+
+
+
+
+
+
+ database
+
+
+ Messdaten
+
+
+ speichern
+
+
+
+
+
+
+
+
+ Webbrowser (Firefox, Chrome & Co.)
+
+
+
+
+
+
+
+
+
+
+
+
+ SmartVISU
+
+
+ bekommt das Grundgerüst der Seite über http vom
+
+
+ Webserver (Apache etc.)
+
+
+ benötigt zwingend Javascript
+
+
+ Browser ohne Websockets funktionieren nicht
+
+
+ Itemwerte; Datenlisten etc. werden JSON codiert über
+
+
+ Websocket-Verbindung übertragen
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Admin Interface, liefert Informationen
+
+
+ über SmartHomeNG:
+
+
+ Als Basis in Angular implementiert
+
+
+ benötigt im Browser zwingend Javascript
+
+
+ Anfragen an SmartHomeNG laufen über Ajax
+
+
+ Informationen u.a Logger, Logiken, Scheduler, Items,
+
+
+ Umgebung, etc.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ InitSmarthome:
+
+
+ filename: InitSmarthome.py
+
+
+ crontab: init
+
+
+
+
+
+
+
+ EG:
+
+
+ Kueche:
+
+
+ Licht:
+
+
+ name: Küchenlicht
+
+
+ type: bool
+
+
+ OG:
+
+
+ Schlafzimmer:
+
+
+ Dimmer:
+
+
+ type: num
+
+
+
+
+
+
+
+
+
+
+
+
+ # Airport Berlin Tegel
+
+
+ lat: 52.5588327
+
+
+ lon: 13.2884374
+
+
+ elev: 35
+
+
+ tz: 'Europe/Berlin'
+
+
+
+
+
+
+
+
+ smartvisu:
+
+
+ plugin_name: smartvisu
+
+
+
+ generate_pages: false
+
+
+
+
+
+
+
+ #!/usr/bin/env python
+
+
+ import logging
+
+
+ logger = logging.getLogger(__name__)
+
+
+ logger.info('# Neustart der Logik #')
+
+
+
+
+
+
+
+
+
+
+
+ +-- smartvisu
+
+ ¦ +-- apps # ein paar Anwendungen für z.B. Webcam, Diashow etc.
+
+ ¦ +-- designs # verschiedene Designs
+
+ ¦ +-- driver # Websocket Treiber für Kommunikation mit SmartHomeNG
+
+ ¦ +-- icons # Icons im SVG Format
+
+ ¦ +-- lang # Formatanweisungen für verschiedene Sprachen
+
+ ¦ +-- lib # Benötigte Bibliotheken
+
+ ¦ +-- pages # Unterverzeichnis = Visu
+ ¦ ¦ +-- _template # dieses Verzeichnis kopieren
+ ¦ ¦ ¦ +-- category.html #
+ und als Basis für eigene manuell erstellte
+
+ ¦ ¦ ¦ +-- index.html #
+ Seiten verwenden
+
+ ¦ ¦ ¦ +-- rooms.html #
+
+ ¦ ¦ ¦ +-- room_sleeping.html # Schlafraum diese HMTL anpassen mit Widget
+
+ ¦ ¦ ¦ +-- rooms_menu.html # Hier kommen die Links auf verschiedene Räume rein
+
+ ¦ ¦ ¦ +-- visu.css
+
+ ¦ ¦ ¦ +-- visu.js
+
+ ¦ ¦ +-- base # Basisdaten für alle Visu
+
+ ¦ ¦ +-- docu # Dokumentation
+
+ ¦ ¦ +-- example1.smarthome
+
+ ¦ ¦ +-- example2.knxd
+ ¦ ¦ +-- example3.graphic ¦ ¦ +-- example4.quad
+ ¦ ¦ +-- smarthome # Wenn das smartvisu Plugin installiert ist,
+
+ # werden hier die automatisch aus den Items
+
+ # generierten Seiten abgelegt
+
+
+ # 1. Demoseite # 2. Demoseite mit knxd Treiber # 3. Demoseite mit speziellen Anforderungen # 4. Demoseite mit Aufteilung in 4 Quadranten
+
+
+
+
+
+
+ Webserver
+
+
+ Apache2 NGinx
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ TCP
+
+
+ Port 6720
+
+
+
+
+
+
+
+
+ TCP
+
+
+ Port 1883
+
+
+
+
+
+
+
+
+
+
+ TCP
+
+
+ Port 3404
+
+
+
+
+
+
+
+ TCP Port 80
+
+
+ HTML5
+
+
+
+
+
+
+
+
+
+
+ Datenreihen
+
+
+ speichern und
+
+
+ für Charts
+
+
+ in SmartVISU
+
+
+ bereitstellen
+
+
+
+
+
+
+ Websocket
+
+
+
+
+
+
+
+
+
+ Aus Einträgen in den Item
+
+
+ Konfigurationsdateien der Art
+
+
+ sv_widget: {{ … }}
+
+
+ erstellt das Plugin Seiten und
+
+
+ Widgeteinträge für
+
+
+ eine fertige Visu
+
+
+
+
+
+
+
+
+ DLMS+SML
+
+
+ Smartmeter
+
+
+ auslesen
+
+
+
+
+
+
+
+
+ Telegram
+
+
+ Kurzmitteilungen
+
+
+ versenden
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ http Modul
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ admin Modul
+
+
+
+
+
+
+
+
+
+ webservices Modul
+
diff --git a/doc/user/source/admin/admin.rst b/doc/user/source/admin/admin.rst
index a5e2e09956..5c7059a470 100644
--- a/doc/user/source/admin/admin.rst
+++ b/doc/user/source/admin/admin.rst
@@ -13,21 +13,8 @@
Administrations-Interface :greensup:`Update`
============================================
-Seit SmartHomeNG v1.2 steht eine graphische Oberfläche zur Verfügung, die bei der Administration
-von SmartHomeNG hilft. Dazu implementiert SmartHomeNG einen eigenen Webserver, der in der Standardkonfiguration
-auf **Port 8383** hört.
-
-Diese Oberfläche wurde bisher durch das **Backend Plugin** zur Verfügung gestellt. Das Backend Plugin ist unter
-:doc:`/plugins/backend/user_doc` ausführlich beschrieben. Es wird jedoch in einer der kommenden Versionen entfernt,
-da seit v1.6 ein neues graphisches Administrations-Interface zur Verfügung steht, welches eine Weiterentwicklung des
-Backend Plugins ist und in der Funktionalität über die Möglichkeiten des Backend Plugins hinausgeht.
-
-Während das Backend Plugin vorwiegend zur Anzeige von Informationen über die SmartHomeNG Installation diente, ermöglicht
-das Admin-Interface nach die vollständige Konfiguration von SmartHomeNG.
-
-Um einen problemlosen Übergang zu gewährleisten, steht das Backend Plugin für einen Übergangszeit weiter zur Verfügung.
-Es ist als deprecated (veraltet) eingestuft und wird voraussichtlich mit v1.8 von SmartHomeNG entfernt.
-
+Seit SmartHomeNG v1.6 steht ein graphisches Administrations-Interface zur Verfügung, welches die vollständige
+Administration von SmartHomeNG ermöglicht.
Das Administrations-Interface wird durch folgenden Aufruf gestartet:
@@ -37,7 +24,7 @@ Das Administrations-Interface wird durch folgenden Aufruf gestartet:
Über die Systemkonfiguration kann eingestellt werden, dass ```http://:8383```
-statt wie bisher auf das Backend Plugin, auf das Administrations-Interface verweist
+automatisch auf das Administrations-Interface verweist.
Falls in der Konfiguration für das http Modul eine User/Passwort Kombination konfiguriert wurde, wird diese benötigt um
diff --git a/doc/user/source/admin/assets/items-structtemplates.jpg b/doc/user/source/admin/assets/items-structtemplates.jpg
index 4e50190137..8d418eabba 100644
Binary files a/doc/user/source/admin/assets/items-structtemplates.jpg and b/doc/user/source/admin/assets/items-structtemplates.jpg differ
diff --git a/doc/user/source/admin/assets/scene-config.jpg b/doc/user/source/admin/assets/scene-config.jpg
index 2fe6e83727..df99c97ade 100644
Binary files a/doc/user/source/admin/assets/scene-config.jpg and b/doc/user/source/admin/assets/scene-config.jpg differ
diff --git a/doc/user/source/admin/assets/services-uf_editor.jpg b/doc/user/source/admin/assets/services-uf_editor.jpg
new file mode 100644
index 0000000000..8e3e34145f
Binary files /dev/null and b/doc/user/source/admin/assets/services-uf_editor.jpg differ
diff --git a/doc/user/source/admin/assets/system-info.jpg b/doc/user/source/admin/assets/system-info.jpg
index 19e298891f..74fe0647c9 100644
Binary files a/doc/user/source/admin/assets/system-info.jpg and b/doc/user/source/admin/assets/system-info.jpg differ
diff --git a/doc/user/source/admin/dienste.rst b/doc/user/source/admin/dienste.rst
index f1e69b7a02..6345e35cc9 100644
--- a/doc/user/source/admin/dienste.rst
+++ b/doc/user/source/admin/dienste.rst
@@ -126,3 +126,15 @@ Gelöscht werden können entweder einzelne Cache Dateien durch den **Löschen**
zu löschenden Cache Dateien können mit Hilfe der Checkbox in der jeweiligen Zeile markiert werden und anschließend mit
dem Button **Ausgewählte Löschen** gelöscht werden.
+
+Userfunction Editor
+===================
+
+Ab Version 1.9 von SmartHomeNG ist die Möglichkeit implementiert, benutzerdefinierte Funktionen (Userfunctions) zu
+schreiben und in eval Statements sowie in Logiken zu verwenden.
+
+Es steht ein Editor zum erstellen und bearbeiten von Userfunctions zur Verfügung. Dieser findet sich
+unter **Dienste/User-Funktionen**.
+
+.. image:: assets/services-uf_editor.jpg
+ :class: screenshot
diff --git a/doc/user/source/conf.py b/doc/user/source/conf.py
index 85d956ac3d..275c84477d 100644
--- a/doc/user/source/conf.py
+++ b/doc/user/source/conf.py
@@ -39,14 +39,20 @@
'sphinx.ext.ifconfig',
'sphinx.ext.viewcode',
'sphinx.ext.githubpages',
- 'recommonmark']
-# 'recommonmark',
+ 'sphinx_tabs.tabs',
+ 'myst_parser']
# 'rst2pdf.pdfbuilder']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
-from recommonmark.parser import CommonMarkParser
+# Markdown Support via MyST
+# without the following, we will get warnings from Parsing old Readme.md as described in
+# https://myst-parser.readthedocs.io/en/latest/using/howto.html#suppress-warnings
+suppress_warnings = ["myst.header"]
+
+# Not used any more
+#from recommonmark.parser import CommonMarkParser
# for autostructify
#import recommonmark
@@ -77,7 +83,7 @@
# General information about the project.
#project = u'SmartHomeNG'
project = u'Anwenderdokumentation v'
-copyright = u'2016-2021 SmartHomeNG Team, SmartHomeNG is based on smarthome.py © Marcus Popp'
+copyright = u'2016-2021 SmartHomeNG Team - SmartHomeNG is based on smarthome.py © Marcus Popp'
# The full version, including alpha/beta/rc tags.
#release = '1.3a dev (as of 13. October 2017)' 13. October 2017 is replaced by makefile with a date in the form of '2. September 2017'
@@ -358,4 +364,4 @@ def setup(app):
pdf_fit_background_mode = 'scale'
# Repeat table header on tables that cross a page boundary?
-pdf_repeat_table_rows = True
\ No newline at end of file
+pdf_repeat_table_rows = True
diff --git a/doc/user/source/dummy_for_readmes.rst b/doc/user/source/dummy_for_readmes.rst
index cea3caf94e..d059ee47bf 100644
--- a/doc/user/source/dummy_for_readmes.rst
+++ b/doc/user/source/dummy_for_readmes.rst
@@ -14,6 +14,7 @@
/dev/sample_module/README.md
/dev/sample_mqttplugin/user_doc.rst
/dev/sample_plugin/user_doc.rst
+ /lib/connection.rst
/modules/admin/README.md
/modules/http/README.md
/modules/mqtt/README.md
diff --git a/doc/user/source/einleitung.rst b/doc/user/source/einleitung.rst
index 2ac6ed7ae6..bcb8bde8d6 100644
--- a/doc/user/source/einleitung.rst
+++ b/doc/user/source/einleitung.rst
@@ -8,7 +8,7 @@ Einleitung
==========
Übersicht über SmartHomeNG
---------------------------
+----------------------------
SmartHomeNG ist ein System das als Metagateway zwischen verschiedenen
"Dingen" fungiert und dient der Verbindung unterschiedlicher
@@ -18,17 +18,18 @@ So ist es möglich dass die Klingel mit der Musikanlage und TV spricht,
und dessen Wiedergabe unterbricht oder bei Abwesenheit eine Nachricht
per Email verschickt.
-Eine umfassende Entwickler-Dokumentation in englischer Sprache gibt es
-unter
-`www.SmartHomeNG.de/developer `__.
+Eine umfassende (veraltete) Entwickler-Dokumentation in englischer Sprache gibt es
+unter `www.SmartHomeNG.de/developer `__.
+Nach und nach werden diese Informationen übersetztz und in die User Dokumentation
+integriert.
-Natürlich lebt dieses Projekt wie alles an Open Source vom mitmachen und
-alle sind eingeladen, das im Rahmen ihrer Möglichkeiten zu tun. Das z.B.
-kann gerne die Erweiterung eines How-To, die Kommentierung bestehenden
+Natürlich lebt dieses Projekt wie alles bei Open Source vom mitmachen.
+Jeder ist eingeladen im Rahmen seiner Möglichkeiten beizutragen.
+Das kann gerne die Erweiterung eines How-To, die Kommentierung bestehenden
Codes oder auch Beispiele sein.
Wie alles zusammenhängt
------------------------
+-------------------------
-.. figure:: /_static/img/SmarthomeNG_V1.7.0.svg
+.. figure:: /_static/img/SmarthomeNG_V1.8.0.svg
:alt: Image
diff --git a/doc/user/source/entwicklung/core/core_libraries.rst b/doc/user/source/entwicklung/core/core_libraries.rst
index fe037c5d84..c4237d6539 100644
--- a/doc/user/source/entwicklung/core/core_libraries.rst
+++ b/doc/user/source/entwicklung/core/core_libraries.rst
@@ -31,7 +31,6 @@ Die folgenden Programm Module können ebenfalls für die Plugin Entwicklung verw
:maxdepth: 5
:titlesonly:
- /lib/connection
/lib/db
/lib/logutils
/lib/network
diff --git a/doc/user/source/entwicklung/entwicklung.rst b/doc/user/source/entwicklung/entwicklung.rst
index 63e2e1b488..cec244da02 100644
--- a/doc/user/source/entwicklung/entwicklung.rst
+++ b/doc/user/source/entwicklung/entwicklung.rst
@@ -5,8 +5,8 @@
.. role:: redsup
-Entwicklung :redsup:`Neu`
-=========================
+Entwicklung
+===========
Hier entsteht nach und nach der Teil der Dokumentation, welcher sich mit der Entwicklung von SmartHomeNG befasst.
diff --git a/doc/user/source/entwicklung/libraries_plugins_logics.rst b/doc/user/source/entwicklung/libraries_plugins_logics.rst
index ddc26883f8..f7ab83e06c 100644
--- a/doc/user/source/entwicklung/libraries_plugins_logics.rst
+++ b/doc/user/source/entwicklung/libraries_plugins_logics.rst
@@ -9,7 +9,6 @@ Der Aufruf der darin bereitgestellten Funktionen ist im Folgenden beschrieben:
:maxdepth: 4
:titlesonly:
- /lib/connection
/lib/db
/lib/logutils
/lib/network
diff --git a/doc/user/source/entwicklung/logiken/logiken.rst b/doc/user/source/entwicklung/logiken/logiken.rst
index 7b9a0c425e..d15d1f5120 100644
--- a/doc/user/source/entwicklung/logiken/logiken.rst
+++ b/doc/user/source/entwicklung/logiken/logiken.rst
@@ -106,55 +106,125 @@ Die Zeitspanne für die zyklische Ausführung kann auf zwei Arten angegeben werd
1. Eine Zahl die die Zeitspanne in Sekunden angibt, kann optional mit einem ``s`` gekennzeichnet werden oder
2. eine Zahl gefolgt von ``m`` die eine Zeitspanne in Minuten angibt
-crontab
-~~~~~~~
+.. role:: bluesup
+
+crontab :bluesup:`Update`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. Der Inhalt der Beschreibung von crontab wurde aus referenz/items/standard_attribute/crontab.rst 1:1 kopiert
+
+Es gibt drei verschiedene Parametersätze für ein Crontab Attribut:
+
+.. tabs::
+
+ .. tab:: init
+ Das Item wird zum Start von SmarthomeNG aktualisiert und triggert
+ dadurch unter Umständen eine zugewiesene Logik:
+
+ .. code-block:: yaml
+
+ crontab: init
+
+ Hier kann auch zusätzlich ein Offset angegeben werden um den
+ tatsächlichen Zeitpunkt zu verschieben:
+
+ .. code-block:: yaml
+
+ crontab: init+10 # 10 Sekunden nach Start
+
+ .. tab:: Zeitpunkte
+
+ Das Item soll zu bestimmten Zeitpunkten aktualisiert werden.
+ Die Schreibweise ist an Linux Crontab angelehnt, entspricht diesem aber nicht genau.
+ Es gibt je nach Parameteranzahl 3 Varianten:
+
+ * ``crontab: ``
+ * ``crontab: ``
+ * ``crontab: ``
+
+ Dabei sind je nach Variante folgende Werte zulässig:
+
+ * Sekunde: ``0`` bis ``59``
+ * Minute: ``0`` bis ``59``
+ * Stunde: ``0`` bis ``23``
+ * Tag: ``1`` bis ``31``
+ * Monat: 1 bis 12 oder ``jan`` bis ``dec``
+ * Wochentag ``0`` bis ``6`` oder ``mon``, ``tue``, ``wed``, ``thu``, ``fri``, ``sat``, ``sun``
-Ähnlich wie Unix crontab mit den folgenden Optionen:
+ Alle Parameter müssen durch ein Leerzeichen getrennt sein und innerhalb eines Parameters
+ darf kein zusätzliches Leerzeichen vorhanden sein, sonst kann der Parametersatz nicht ausgewertet werden.
-* ``crontab: init``
- Ausführung der Logik beim Start von SmartHomeNG
+ Im folgenden Beispiel wird jeden Tag um 23:59 ein Trigger erzeugt und der Wert 70 gesetzt.
-* ``crontab: Minute Stunde Tag Wochentag``
- Siehe Beschreibung von Unix crontab und Online Generatoren für Details
+ .. code-block:: yaml
- - Minute: Wert im Bereich [0...59], oder Komma getrennte Liste, oder * (jede Minute)
- - Stunde: Wert im Bereich [0...23], oder Komma getrennte Liste, oder * (jede Stunde)
- - Tag: Wert im Bereich [0...28], oder Komma getrennte Liste, oder * (jeden Tag)
- **Achtung**: Derzeit keine Werte für Tage größer 28 nutzen
- - Wochentag: Wert im Bereich [0...6] (0 = Montag), oder Komma getrennte Liste, oder * (jeden Wochentag)
+ crontab: 59 23 * * = 70
-``crontab: sunrise``
- Startet die Logik bei Sonnenaufgang
+ Für jede dieser Zeiteinheiten (Minuten, Stunde, Tag, Wochentag) werden
+ folgende Muster unterstützt (Beispiel jeweils ohne Anführungszeichen verwenden):
-``crontab: sunset``
- Startet die Logik bei Sonnenuntergang
+ * eine einzelne Zahl, z.B. ``8`` → immer zur/zum 8. Sekunde/Minute/Stunde/Tag/Wochentag
+ * eine Liste von Zahlen, z.B. ``2,8,16`` → immer zur/zum 2., 8. und 16. Sekunde/Minute/Stunde/Tag/Monat/Wochentag
+ * ein Wertebereich, z.B. ``1-5`` → immer zwischen dem/der 1. und 5. Sekunde/Minute/Stunde/Tag/Monat/Wochentag
+ * einen Interval, z.B. ``\*\/4`` → immer alle 4 Sekunden/Minuten/Stunden/Tage/Wochentage
+ * einen Stern, z.B. ``*`` → jede Sekunde/Minute/Stunde/Tag/Monat/Wochentag
- Für Sonnenaufgang oder Sonnenuntergang können folgende Erweiterungen genutzt werden:
+ .. tab:: Zeitpunkte bezogen auf Aufgang von Sonne oder Mond
- - Ein Offsetwert zum Horizont in Grad.
- Beispiel ``crontab: sunset-6``
- Dazu muss in der smarthome.yaml Längen und Breitengrad eingestellt sein.
- - Ein Offsetwert in Minuten der durch ein angehängtes m gekennzeichnet wird
- Beispiel: ``crontab: sunset-10m``
- - Eine Beschränkung der Zeit für die Ausführung
+ Nach dem Muster ``[H:M<](sunrise|sunset|moonrise|moonset)[+|-][offset][ )`` kann ein Triggerpunkt bezogen
+ auf Sonne oder Mond berechnet werden:
- .. code-block:: yaml
- :caption: Konfiguration mit YAML Syntax
+ * ``sunrise`` → immer zum Sonnenaufgang
+ * ``sunset`` → immer zum Sonnenuntergang
+ * ``sunrise`` und untere Begrenzung → ``06:00`` und ````
+ sowie der jeweiligen End-Tags. Außerdem muss jeder Tabelle eine einzigartige ID vergeben werden.
.. code-block:: HTML
-
+
{{ _('Item') }}
@@ -85,4 +87,29 @@ Die folgenden Schritte dienen dazu, das Webinterface mit Leben zu füllen:
- 3. Das Logo oben links auf der Seite wird automatisch durch das Logo des konfigurierten Plugin-Typs ersetzt. Wenn das Webinterface ein eigenes Logo mitbringen soll, muss das entsprechende Bild im Verzeichnis ``webif/static/img`` mit dem Namen ``plugin_logo`` abgelegt sein. Die zulässigen Dateiformate sind **.png**, **.jpg** oder **.svg**. Dabei sollte die Größe der Bilddatei die Größe des angezeigten Logos (derzeit ca. 180x150 Pixel) nicht überschreiten, um unnötige Datenübertragungen zu vermeiden.
+ 3. Folgender Script Code muss zwischen ``{% block pluginscripts %}`` und
+ ``{% endblock pluginscripts %}`` eingefügt werden, um ein Filtern und Sortieren
+ der Tabellen zu ermöglichen.
+ Der Code ``$('#maintable').DataTable( { "paging": false, fixedHeader: true } );``
+ muss für jede Tabelle, für die Filtern/Sortieren ermöglicht werden soll, kopiert werden.
+ Dabei ist sicher zu stellen, dass die ID (#maintable) jeweils richtig angepasst wird:
+
+ .. code-block:: HTML
+
+
+
+ 4. Das Logo oben links auf der Seite wird automatisch durch das Logo des konfigurierten Plugin-Typs ersetzt. Wenn das Webinterface ein eigenes Logo mitbringen soll, muss das entsprechende Bild im Verzeichnis ``webif/static/img`` mit dem Namen ``plugin_logo`` abgelegt sein. Die zulässigen Dateiformate sind **.png**, **.jpg** oder **.svg**. Dabei sollte die Größe der Bilddatei die Größe des angezeigten Logos (derzeit ca. 180x150 Pixel) nicht überschreiten, um unnötige Datenübertragungen zu vermeiden.
diff --git a/doc/user/source/index.rst b/doc/user/source/index.rst
index da0254d52a..19069cb7fb 100644
--- a/doc/user/source/index.rst
+++ b/doc/user/source/index.rst
@@ -7,7 +7,7 @@ SmartHomeNG
Anwenderdokumentation
=====================
-SmartHomeNG [#f1]_ ist ein System das als Metagateway zwischen verschiedenen "Dingen" fungiert und
+SmartHomeNG ist ein System das als Metagateway zwischen verschiedenen "Dingen" fungiert und
dient der Verbindung unterschiedlicher Geräte-Schnittstellen. Die Standard-Schnittstelle eines
Gerätes wird durch das Metagateway so um viele zusätzliche Schnittstellen erweitert. So ist es
möglich dass die Klingel mit der Musikanlage und TV spricht, und dessen Wiedergabe unterbricht
@@ -35,8 +35,6 @@ oder im `Chat auf gitter.im `_ .
**Anmerkungen** und **Änderungswünsche** zu dieser Anwenderdokumentation bitte auf
`dieser Feedback Seite `_ hinterlassen.
-.. [#f1] SmartHomeNG © Copyright 2016-2020 SmartHomeNG Team, basiert auf smarthome.py © 2011-2014 Marcus Popp.
-
.. :titlesonly:
.. toctree::
@@ -44,6 +42,7 @@ oder im `Chat auf gitter.im `_ .
:hidden:
einleitung.md
+ was_ist_neu.rst
installation/installation.rst
konfiguration/konfiguration.rst
plugins_all.rst
diff --git a/doc/user/source/installation/anforderungen.rst b/doc/user/source/installation/anforderungen.rst
index 6a38737c77..602302a8a1 100644
--- a/doc/user/source/installation/anforderungen.rst
+++ b/doc/user/source/installation/anforderungen.rst
@@ -1,5 +1,7 @@
:tocdepth: 2
+.. index:: Linux, MacOS, Unix, Windows
+
Hard- u. Software Anforderungen
===============================
@@ -24,6 +26,9 @@ Häufig verwendete Hardware ist:
besonders wenn die Webinterfaces der Plugins genutzt werden und falls die Visualisierung (smartVISU) auf dem
selben System betrieben werden sollen. Der Großteil der Nutzer verwendet diese Hardware, siehe
`Umfrage `__
+ Die verschiedenen Raspberry Pi Varianten wurden in diesem
+ `Geschwindigkeitstest `_
+ miteinander verglichen.
- Intel NUC (Empfohlen für Stabilität und Geschwindigkeit, auch wenn
diese Rechner mehr Leistung haben, als benötigt wird. Unterstützt
normale SATA Festplatten/SSD, was ein Vorteil gegenüber den Raspberry Pi
@@ -42,11 +47,11 @@ virtuelle Maschine mit 512MB RAM und zwischen 40GB und 80GB
Plattenplatz.
-Raspberry Pi 2, 3 oder 4
-~~~~~~~~~~~~~~~~~~~~~~~~
+Raspberry Pi 3 oder 4
+~~~~~~~~~~~~~~~~~~~~~
-SmartHomeNG ist auf einem Raspberry Pi 1 zwar lauffähig, sollte dann aber nur in einer Minimalkonfiguration eingesetzt
-werden.
+SmartHomeNG ist auf einem Raspberry Pi 1 oder Pi 2 zwar lauffähig, sollte dann aber nur in einer Minimalkonfiguration
+eingesetzt werden.
Vorteile:
^^^^^^^^^
@@ -126,11 +131,11 @@ Nachteile:
- es hängt sehr von der Plattform ab ob sich Nachteile ergeben
-Betriebssystem
---------------
+Betriebssysteme
+---------------
-Ein beliebiges Linux oder Unix System (mit Shell Zugang um die Requirements und SmartHomeNG zu installieren) sollte
-funktionieren.
+Ein beliebiges **Linux** oder **Unix System** (mit Shell Zugang um die Requirements und SmartHomeNG zu installieren)
+sollte funktionieren.
SmartHomeNG ist mindestens getestet auf Raspbian und Debian Buster (amd64)
@@ -139,13 +144,15 @@ Einsatz eines NTP Daemons notwendig, um die Zeit über das Internet zu
beziehen. Sonst wird SmartHomeNG aufgrund der fehlenden Zeitinformation
nicht starten.
-Einige Libraries in SmartHomeNG benutzen noch Bibliotheken, die ein Unix-artiges Betriebssystem voraussetzen.
-Daher läuft SmartHomeNG nicht auf Windows.
+Einige Libraries in SmartHomeNG benutzen Bibliotheken, die ein Unix-artiges Betriebssystem voraussetzen
+oder spezielle Hardware erwarten.
+
+Ab SmartHomeNG v1.6 sollte eine Installation unter **MacOS** (BSD Unix) möglich sein.
-Ab SmartHomeNG v1.6 sollte eine Installation unter MacOS möglich sein.
+Ab SmartHomeNG v1.8.2 sollte eine Installation unter **Windows** möglich sein.
-weitere Software
+Python Versionen
----------------
Die aktuelle Version von SmartHomeNG setzt Python der Version 3.6 oder neuer voraus.
@@ -167,15 +174,27 @@ Version aktuelle Python Version und die zwei Vorgängerversionen.**
"v1.7", "Python 3.7", "Python 3.5, 3.6, 3.7"
"v1.8", "Python 3.8", "Python 3.6, 3.7, 3.8"
"v1.9", "Python 3.9", "Python 3.7, 3.8, 3.9"
+ "v1.10", "Python 3.10", "Python 3.8, 3.9, 3.10"
Das bedeutet nicht automatisch, dass SmartHomeNG mit älteren Python Versionen nicht mehr funktioniert,
-die Entwicklung wird nur nicht mehr mit älteren Versionen getestet.
+die Entwicklung wird nur nicht mehr mit älteren Versionen getestet. Zudem bekommen ältere Python Versionen keine
+Bugfixes mehr sondern nur noch Sicherheits-Updates.
Python 3.6 jedoch hat eine Reihe sehr interessanter Features und Verbesserungen gebracht, die nur dann in SmartHomeNG
genutzt werden können, wenn sichergestellt ist dass SmartHomeNG mindestens unter Python 3.6 gestartet wurde. Daher
wurde für SmartHomeNG v1.8 die **Absolute Minimum Python Version** auf 3.6 angehoben.
-Debian Buster bringt aktuell Python 3.7.x und PHP 7.3 mit und Ubuntu 20.04 LTS Python 3.8.x sowie PHP 7.4 und PHP 7.3
+Beispiele für Linux-System und mitgeliefere Software Versionen:
+
+ * Debian 9 (Stretch) beinhaltet Python 3.5 und PHP 7.0
+ * Debian 10 (Buster) beinhaltet Python 3.7 und PHP 7.3
+ * Debian 11 (Bullseye) beinhaltet Python 3.9 und PHP 7.4
+ * Ubuntu 18.04 LTS (Bionic Beaver) beinhaltet Python 3.6 und PHP 7.2
+ * Ubuntu 20.04 LTS (Focal Fossa) beinhaltet Python 3.8 und PHP 7.4
+
+Aus den Beispielen ist ersichtlich, das Debian Stretch nicht mehr für Neuinstallationen verwendet werden sollte.
+Bei Ubuntu sollte man die LTS (Long Term Support) Varianten bevorzugen um nicht andauern mit Systemänderungen konfrontiert zu werden
-PHP wird für SmartHomeNG selbst nicht benötigt, ist jedoch eine Voraussetzung für den Einsatz von smartVISU.
+PHP wird für SmartHomeNG selbst nicht benötigt, ist jedoch eine Voraussetzung für den Einsatz der
+`SmartVISU `_.
diff --git a/doc/user/source/installation/komplettanleitung/02_smarthomeng.rst b/doc/user/source/installation/komplettanleitung/02_smarthomeng.rst
index 94020627f9..a1f346c6dd 100644
--- a/doc/user/source/installation/komplettanleitung/02_smarthomeng.rst
+++ b/doc/user/source/installation/komplettanleitung/02_smarthomeng.rst
@@ -61,92 +61,92 @@ Bitte auf den **Punkt** am Ende des ersten **git clone** Kommandos achten!
Weitere Python Bibliotheken installieren
========================================
-Ab Version 1.7 kann SmartHomeNG benötigte Pakete selbst nachinstallieren. Eine manuelle Installation
-ist daher nur bei älteren Versionen von SmartHomeNG notwendig. (Siehe nächster Abschnitt)
+.. tabs::
-Wenn SmartHomeNG in einer Python Umgebung gestartet wird in der nicht der minimale Set an Packages installiert ist,
-wird dieser installiert und die Informationen werden auf die Konsole ausgegeben (da das Logging dann noch nicht
-konfiguriert werden kann). Anschließend startet SmartHomeNG neu. Das sieht folgendermaßen aus.
+ .. tab:: SmartHomeNG ab v1.7
-.. code-block:: bash
+ SmartHomeNG kann benötigte Pakete selbst nachinstallieren.
- $ python3 bin/smarthome.py
+ Wenn SmartHomeNG in einer Python Umgebung gestartet wird in der nicht der minimale Set an Packages installiert ist,
+ wird dieser installiert und die Informationen werden auf die Konsole ausgegeben (da das Logging dann noch nicht
+ konfiguriert werden kann). Anschließend startet SmartHomeNG neu. Das sieht folgendermaßen aus.
- test_requirements: 'ephem' not installed. Minimum v3.7 needed
- test_requirements: 'holidays' not installed. Minimum v0.9.11 needed
- test_requirements: 'psutil' not installed, any version needed
- test_requirements: 'python-dateutil' not installed. Minimum v2.5.3 needed
- test_requirements: 'requests' not installed. Minimum v2.20.0 needed
- test_requirements: 'ruamel.yaml' not installed. Minimum v0.13.7 needed
+ .. code-block:: bash
- Installing core requirements for the current user, please wait...
- Running in a virtualenv environment,
- installing core requirements only to actual virtualenv, please wait...
+ $ python3 bin/smarthome.py
- core requirements installed
+ test_requirements: 'ephem' not installed. Minimum v3.7 needed
+ test_requirements: 'holidays' not installed. Minimum v0.9.11 needed
+ test_requirements: 'psutil' not installed, any version needed
+ test_requirements: 'python-dateutil' not installed. Minimum v2.5.3 needed
+ test_requirements: 'requests' not installed. Minimum v2.20.0 needed
+ test_requirements: 'ruamel.yaml' not installed. Minimum v0.13.7 needed
- Starting SmartHomeNG again...
- Daemon PID 4024
+ Installing core requirements for the current user, please wait...
+ Running in a virtualenv environment,
+ installing core requirements only to actual virtualenv, please wait...
- $
+ core requirements installed
-Danach kann der Core von SmartHomeNG vollständig initialisiert werden und Ausgaben erfolgen in smarthome-warnings.log
+ Starting SmartHomeNG again...
+ Daemon PID 4024
-Anschließend prüft SmartHomeNG ob die benötigten Pakete für die ladbaren Module und für die konfigurierten Plugins
-installiert sind. Falls nicht, werden diese jeweils installiert und SmartHomeNG startet sich erneut.
+ $
-.. note::
+ Danach kann der Core von SmartHomeNG vollständig initialisiert werden und Ausgaben erfolgen in smarthome-warnings.log
- Dieser Mechanismus sorgt auch dafür, dass Pakete die von später konfigurierten Plugins benötigt werden, automatisch
- nachinstalliert werden.
+ Anschließend prüft SmartHomeNG ob die benötigten Pakete für die ladbaren Module und für die konfigurierten Plugins
+ installiert sind. Falls nicht, werden diese jeweils installiert und SmartHomeNG startet sich erneut.
+ .. note::
+ Dieser Mechanismus sorgt auch dafür, dass Pakete die von später konfigurierten Plugins benötigt werden, automatisch
+ nachinstalliert werden.
-Python Bibliotheken installieren (für SmartHomeNG vor v1.7)
---------------------------------------------------------------
+ .. tab:: SmartHomeNG vor v1.7
-Für den ersten Start müssen noch einige Python Packages nachgeladen werden.
-Im Unterordner ``requirements`` befindet sich dafür eine Datei ``base.txt``.
-In dieser Datei stehen die von SmartHomeNG grundlegend benötigten Bibliotheken.
-Diese können wie folgt installiert werden:
+ Für den ersten Start müssen noch einige Python Packages nachgeladen werden.
+ Im Unterordner ``requirements`` befindet sich dafür eine Datei ``base.txt``.
+ In dieser Datei stehen die von SmartHomeNG grundlegend benötigten Bibliotheken.
+ Diese können wie folgt installiert werden:
-.. code-block:: bash
+ .. code-block:: bash
- cd /usr/local/smarthome
- pip3 install -r requirements/base.txt --user
+ cd /usr/local/smarthome
+ pip3 install -r requirements/base.txt --user
-.. attention::
+ .. attention::
- In früheren Beschreibungen wurde die globale Installation von Python Packages mit dem sudo Kommando
- beschrieben:
+ In früheren Beschreibungen wurde die globale Installation von Python Packages mit dem sudo Kommando
+ beschrieben:
- sudo pip3 install -r requirements/base.txt
+ sudo pip3 install -r requirements/base.txt
- Dieses funktioniert unter Debian Buster **NICHT** mehr. Zumindest unter Buster **muss** die Installation
- für den entsprechenden User mit **--user** erfolgen (wie oben beschrieben).
+ Dieses funktioniert unter Debian Buster **NICHT** mehr. Zumindest unter Buster **muss** die Installation
+ für den entsprechenden User mit **--user** erfolgen (wie oben beschrieben).
-.. note::
+ .. note::
- Falls mehrere Python3 Versionen installiert sind, kann es zu Problemen kommen, da pip die Bibliotheken immer nur
- in eine der installierten Python 3 Versionen installiert.
+ Falls mehrere Python3 Versionen installiert sind, kann es zu Problemen kommen, da pip die Bibliotheken immer nur
+ in eine der installierten Python 3 Versionen installiert.
- Um sicherzustellen, dass die Bibliotheken in die Python3 Version installiert werden, muss pip3 aus genau dieser
- Python3 Umgebung aufgerufen werden.
+ Um sicherzustellen, dass die Bibliotheken in die Python3 Version installiert werden, muss pip3 aus genau dieser
+ Python3 Umgebung aufgerufen werden.
- Um das sicherzustellen, ist statt
+ Um das sicherzustellen, ist statt
- .. code-block:: bash
+ .. code-block:: bash
- pip3 install -r requirements/base.txt --user
+ pip3 install -r requirements/base.txt --user
- der folgende Befehl auszuführen:
+ der folgende Befehl auszuführen:
- .. code-block:: bash
+ .. code-block:: bash
- -m pip3 install -r requirements/base.txt --user
+ -m pip3 install -r requirements/base.txt --user
-Jetzt ist SmartHomeNG installiert und kann konfiguriert werden.
+ Jetzt ist SmartHomeNG installiert und kann konfiguriert werden.
Erstmaliger Start von SmartHomeNG
@@ -218,8 +218,6 @@ und es wurde getestet ob SmartHomeNG läuft.
-
-
SmartHomeNG konfigurieren
-------------------------
@@ -490,4 +488,3 @@ Admin Interface
Die weitere Konfiguration kann auch über die GUI erfolgen, wie im Abschnitt `SmartHomeNG konfigurieren <#smarthomeng-konfigurieren>`__
beschrieben.
-
diff --git a/doc/user/source/installation/komplettanleitung/04_smartvisu.rst b/doc/user/source/installation/komplettanleitung/04_smartvisu.rst
index 4c60779a7c..d9c4e9923a 100644
--- a/doc/user/source/installation/komplettanleitung/04_smartvisu.rst
+++ b/doc/user/source/installation/komplettanleitung/04_smartvisu.rst
@@ -44,13 +44,16 @@ das für den **Apache2** Webserver zugänglich ist:
chmod g+rws smartvisu/
cd smartvisu
git clone git://github.com/Martin-Gleiss/smartvisu.git .
+ # Schreibrechte für Cache und Konfigurationsdateien setzen
bash setpermissions
Bitte auf den **Punkt** am Ende des **git clone** Kommandos achten!
-Für den ordnungsgemäßen Betrieb braucht die SmartVISU noch das SmartHomeNG Plugin
-**visu_websocket**. Dieses ist in der **plugin.yaml.default** bereits vorkonfiguriert
-und wird beim ersten Start nach einer frischen Installation in die **plugin.yaml**
+Eine Besonderheit des Apache Webservers ist sein spezieller Umgang mit einem Ordner namens "icons" im Root-Verzeichnis. Da smartVISU einen solchen Ordner verwendet, sollte sie immer wie oben angegeben in einem Unterverzeichnis angelegt werden, damit keine Konflikte entstehen. Dies gilt auch für Docker-Umgebungen.
+
+Für den ordnungsgemäßen Betrieb braucht die SmartVISU noch das SmartHomeNG Plugin **smartvisu** und das **Websocket-Modul** (oder
+"visu_websocket", das aber seit v1.8 deprecated ist). Beide sind in der **plugin.yaml.default** und **module.yaml.default** bereits vorkonfiguriert
+und werden beim ersten Start nach einer frischen Installation in die Einstellungen
übernommen.
@@ -84,17 +87,17 @@ Eigene Visu Seiten anlegen
==========================
Um mit der SmartVISU eine eigene Visu anzulegen, muss innerhalb des Ordners ``pages`` der SmartVISU ein neues
-Verzeichnis angelegt werden, in dem dann die eigenen Seiten z.B. für Räume oder Funktionsbereich abgelegt werden.
+Verzeichnis angelegt werden, in dem dann die eigenen Seiten z.B. für Räume oder Funktionsbereiche abgelegt werden.
Es existiert im Ordner ``pages`` bereits ein Unterordner ``_template``. Dieser wird als Basis der neuen Visu einfach
kopiert ``cp _template ``. Für ```` sollte **nicht smarthome** gewählt werden
-wenn später die Visu vom SmartHomeNG Plugin **visu\_smartvisu** erstellt werden soll. Die manuell erstellten Seiten
+wenn später die Visu vom SmartHomeNG Plugin **smartvisu** erstellt werden soll. Die manuell erstellten Seiten
könnten sonst einfach von SmartHomeNG überschrieben werden.
Die Dateien für die SmartVISU sind einfache HTML Dateien. Die einzelnen Bedienelemente wie Buttons, Flips,
Werteanzeigen (sogenannte Widgets) sind Makros die mit der Makrosprache **TWIG** definiert sind.
Die HTML können auf eigene Bedürfnisse beliebig angepasst werden.
-Im einzelnen ist das zwar auf der veralteten `Projektseite smartVISU `__ nachzulesen,
-es wird aber empfohlen die entsprechende Dokumentation nachzuinstallieren (siehe unten).
+Im einzelnen ist das zwar auf der `Projektseite smartVISU `__ nachzulesen,
+es wird aber empfohlen die entsprechende Dokumentation aus GitGub nachzuinstallieren, wo immer die aktuellste Version gepflegt ist (siehe unten).
Die durch die SmartVISU generierten HTML Seiten sind zwar responsiv aber durchweg statisch.
Die Kommunikation zwischen SmartHomeNG und der SmartVISU erfolgt über ein Websocket Plugin
für SmartHomeNG und JavaScript Code der in der HTML Seite eingebunden wird. Der Javascript Code manipuliert dann
@@ -129,7 +132,10 @@ SmartHomeNG Plugin **visu\_smartvisu**
sollte man sich zuerst mit der Dokumentation der smartVISU vertraut machen. Wenn man mit einem Browser
die Seite einer noch nicht konfigurierten smartVISU aufruft, kommt man zu einer Inline Dokumentation der
smartVISU. Eine umfassende aktuelle Kurzanleitung kann nachinstalliert werden. Wie das geht, ist weiter
- unten beschrieben.
+ oben beschrieben.
+ Zudem gibt es seit smartVISU v3.0 den Widget Assistenten, mit dem die benötigten Widgets parametriert,
+ getestet und in die Zwischenablage kopiert werden können. Der Widget Assistent ist über das
+ Systemmenü zu erreichen.
Mit dem Plugin **smartvisu** können aus der Definition der Items in SmartHomeNG automatisch Visuseiten
erstellt werden. Diese Visu Seiten werden im Verzeichnis ``smarthome`` des ``pages`` Verzeichnisses der
@@ -143,4 +149,3 @@ Es ist möglich automatisch generierte und manuell erstellte Seiten zu mischen.
in unter :doc:`Visualisierung ` und in der
:doc:`Dokumentation des Plugins ` beschrieben.
-
diff --git a/doc/user/source/installation/komplettanleitung/komplettanleitung.rst b/doc/user/source/installation/komplettanleitung/komplettanleitung.rst
index ad071f9372..6b5ea56e6f 100644
--- a/doc/user/source/installation/komplettanleitung/komplettanleitung.rst
+++ b/doc/user/source/installation/komplettanleitung/komplettanleitung.rst
@@ -8,7 +8,7 @@
Komplettanleitung
=================
-Diese Anleitung beschreibt eine komplette Installation von **SmartHomeNG v1.7** auf
+Diese Anleitung beschreibt eine komplette Installation von **SmartHomeNG v1.9** auf
einem Linuxsystem mit Debian 10 (Buster).
Zusätzlich wird die Installation folgender weiterer Pakete beschrieben:
diff --git a/doc/user/source/konfiguration/assets/uf_eval_checker1.jpg b/doc/user/source/konfiguration/assets/uf_eval_checker1.jpg
new file mode 100644
index 0000000000..81a2a8ebee
Binary files /dev/null and b/doc/user/source/konfiguration/assets/uf_eval_checker1.jpg differ
diff --git a/doc/user/source/konfiguration/item_structs.rst b/doc/user/source/konfiguration/item_structs.rst
index 7e4094cf29..223abb2233 100644
--- a/doc/user/source/konfiguration/item_structs.rst
+++ b/doc/user/source/konfiguration/item_structs.rst
@@ -27,6 +27,14 @@ Demzufolge können die Item-Struktur-Templates an zwei verschiedenen Stellen def
- der Nutzer kann die Strukturen in der Konfigurationsdatei ../etc/struct.yaml definieren
- Autoren von Plugins können die Strukturen in den Metadaten des Plugins definieren. Beim Start von SmartHomeNG stehen die dann die Strukturen aller konfigurierten Plugins zur Verfügung.
+.. note::
+
+ Ab SmartHomeNG v1.9 können Item-Struktur-Template Definitionen auf mehrere Dateien verteilt werden.
+
+ Außer der Datei **struct.yaml** im Verzeichnis ../etc können weitere Dateien angelegt werden.
+ Deren Name muss mit **struct_** beginnen. Der danach folgende Teil des Dateinamens wird dabei dem
+ struct Namen als Prefix vorangestellt, um Namensdopplungen vorzubeugen.
+
Um eine doppelte Namensvergabe zu vermeiden, wird bei der Nutzung den structs, die in Plugins definiert wurden, der
Name des Plugins vorangestellt. Wenn z.B. die struct **weather** genutzt werden soll, die im Plugin **darksky**
definiert wurde, so muss als Referenz **darksky.weather** angegeben werden.
@@ -45,8 +53,8 @@ die Template Strukturen in der Reihenfolge angewendet, in der sie in der Liste a
- mein_wetter2
- ....
-struct bei Plugins
-==================
+struct-Templates in Plugins
+===========================
Anwendung
---------
@@ -84,7 +92,7 @@ Um nun die ganzen Items für die Wettervorhersage anzulegen, muss nur noch für
:caption: items/item.yaml
outside:
- my_weather:
+ local_weather:
struct: darksky.weather
@@ -156,8 +164,8 @@ Das kann man auch in der Administrationsoberfläche sehen.
-struct bei selbst definierte Item-Struktur-Templates
-====================================================
+selbst definierte struct-Templates
+==================================
Anwendung
---------
@@ -166,12 +174,12 @@ Eigens definierte Item-Struktur-Templates werden in der Konfigurationsdatei **..
Hierbei gibt die oberste Ebene den Namen der Templates an. Darunter können Item-Strukturen definiert werden, wie man es
auch in der Item Definition in den items.yaml Dateien machen würde. Das folgende Beispiel zeigt die Definition von zwei
-Strukturen (**my_struct_01** und **my_struct_02**):
+Strukturen (**individual_struct_01** und **individual_struct_02**):
.. code-block:: yaml
:caption: etc/struct.yaml
- my_struct_01:
+ individual_struct_01:
name: Name der erste eigenen Item Struktur
item_01:
@@ -188,7 +196,7 @@ Strukturen (**my_struct_01** und **my_struct_02**):
...
- my_struct_02:
+ individual_struct_02:
name: Name der zweiten eigenen Item Struktur
type: bool
@@ -209,7 +217,7 @@ Wenn jetzt in der Item Definition diese Strukturen referenziert werden:
my_tree:
my_complex_data:
name: Geänderter Name für meine komplexen Daten
- struct: my_struct_01
+ struct: individual_struct_01
individual_item:
name: Individuelles Item
@@ -225,7 +233,7 @@ entsteht im Item-Tree die selbe Struktur, als wenn man folgendes direkt in die i
my_tree:
my_complex_data:
name: Geänderter Name für meine komplexen Daten
- #struct: my_struct_01
+ #struct: individual_struct_01
item_01:
name: Erstes Item
@@ -245,9 +253,9 @@ entsteht im Item-Tree die selbe Struktur, als wenn man folgendes direkt in die i
...
-Beim Einfügen der Struktur bleibt das Attribut **struct** erhalten, so dass man zur Laufzeit sehen kann,
+Beim Einfügen der Struktur bleibt das Attribut **struct** erhalten, so dass man zur Laufzeit sehen kann,
dass die Struktur zumindest in Teilen aus einem Template stammt.
-Die Definition des Attributes **name** aus dem Template wird durch die Angabe aus der Datei items/item.yaml ersetzt.
+Die Definition des Attributes **name** aus dem Template wird durch die Angabe aus der Datei items/item.yaml ersetzt.
Das **individual_item** wird an die Struktur des Templates angefügt.
(Siehe :doc:`Konfigurationsdateien/struct.yaml `)
@@ -278,11 +286,13 @@ Grundsätzlich werden alle Attribute zu einem Item, dass in mehreren Item YAML-D
.. note::
Gibt es eine Attributdefinition an mehreren Stellen, gelten folgende Regeln:
+
- Beim Lesen der Item Definition gewinnt die Attributdefinition, welche zuletzt eingelesen wird. Regel: **"last wins"**
- In Struktur- /Unterstrukturdefinitionen gewinnt die zuerst eingelesene Attributdefinition. Regel: **"first wins"**
- Wenn ein Attribut in einem struct-Template und in den Item Definitionen definiert wird, "gewinnt" die Angabe aus der
Item Definition. Regel: **"Item wins"**
+
Beim Auflösen von Unterstrukturen gewinnt die Definition der Struktur der oberen Ebene, wenn das Attribut
in der Struktur der oberen Ebene vor dem **struct**-Attribut definiert ist. Dies ermöglicht ein "Überschreiben"
von Attributwerten, die in einer Unterstruktur definiert wurden. Wenn das Attribut nach dem
@@ -293,13 +303,15 @@ Re-Definieren von list-Attributen
----------------------------------
Das Verhalten bei Re-Definieren von list-Attributen ist abhängig von der Anwendung. Zu unterscheiden gilt, ob es
+
- ein struct in einem Item ist, oder
- ein sub-struct in einem struct.
.. note::
Gibt es eine Attributdefinition mit Listen an mehreren Stellen, gelten folgende Regeln:
- - Bei structs/substructs werden Listen immer gemergt.
- - Bei Items/structs nur, wenn dort Am Anfang einer der Spezialeinträge steht.
+
+ - Bei structs/substructs werden Listen immer gemergt.
+ - Bei Items/structs nur, wenn dort Am Anfang einer der Spezialeinträge steht.
Verhalten bei struct in einem Item
@@ -311,6 +323,7 @@ miteinander verbunden werden. Dabei wird die Liste aus dem **struct** Template a
angehängt.
Dazu müssen folgende Voraussetzungen erfüllt sein:
+
- Das zu mergende Attribut MUSS vor dem **struct** Attribut definiert werden
- Das zu mergende Attribut MUSS im Item als Liste definiert sein
- Das zu mergende Attribut MUSS im Item als ersten Eintrag **merge\*** oder **merge_unique\*** enthalten
@@ -327,6 +340,28 @@ werden die Listen zusammengefügt. Die Reihenfolge der Listeneinträge wird durc
Attributdefinitionen eingelesen werden.
+Verwendung mehrerer Definitionsdateien
+======================================
+
+Wenn structs in der Datei **../etc/struct.yaml** definiert werden, ist der Name der geladenen struct zur Laufzeit identisch
+mit dem Namen, der in der Datei definiert wurde.
+
+Wenn eine Datei in einer Datei nach dem Namensschema **../etc/struct_*.yaml** definiert wird, wird dem Namen
+der struct ein Präfix vorangestellt, um Namensdoppelungen zu vermeiden. Der Präfix ist der auf **struct_**
+folgende Teil des Dateinamens. Wenn also eine struct mit dem Namem **individual_struct** in der Datei mit dem Namen
+../etc/**struct_test**.yaml definiert wird, wird als Präfix für die Herkunft **test** vorangestellt. Der struct Name wäre
+also **test.individual_struct**.
+
+Das könnte jedoch zu Namenskonflikten führen, falls hierbei der Name eines Plugins verwendet wird.
+Falls z.B. eine struct in einer Datei ../etc/**struct_stateengine**.yaml definiert wird, könnte es zu
+Namenskonflikten mit den structs kommen, die durch das **stateengine Plugin** definiert sind. Deshalb wird ein weiterer
+Präfix **my** dem struct Namen vorangestellt, um Namenskonflikte mit structs aus Plugins auszuschließen.
+
+Die struct **individual_struct** in der Datei mit dem Namen ../etc/**struct_test**.yaml definiert wurde,
+trägt zur Laufzeit also den Namen **my.test.individual_struct**. Unter diesem Namen wird sie in der Admin GUI
+angezeigt und muss auch so in Item Definitionen referenziert werden.
+
+
Beispiele
=========
diff --git a/doc/user/source/konfiguration/konfiguration.rst b/doc/user/source/konfiguration/konfiguration.rst
index 335b0f0550..5bf2b4115b 100644
--- a/doc/user/source/konfiguration/konfiguration.rst
+++ b/doc/user/source/konfiguration/konfiguration.rst
@@ -2,11 +2,12 @@
.. index:: Konfiguration
.. role:: bluesup
+.. role:: greensup
.. role:: redsup
-=============
-Konfiguration
-=============
+================================
+Konfiguration :greensup:`Update`
+================================
Für Einsteiger ist auf jeden Fall die Konfiguration über die GUI zu empfehlen.
@@ -33,6 +34,7 @@ vornehmen möchten
logiken.rst
logging.rst
szenen.rst
+ userfunctions.rst
konfiguration_backup_restore
diff --git a/doc/user/source/konfiguration/konfiguration_backup_restore.rst b/doc/user/source/konfiguration/konfiguration_backup_restore.rst
index 37ccf959ca..9ceb42e7a2 100644
--- a/doc/user/source/konfiguration/konfiguration_backup_restore.rst
+++ b/doc/user/source/konfiguration/konfiguration_backup_restore.rst
@@ -30,7 +30,7 @@ Der Dateiname hat dann die Form **shng_config_backup_YYYY-MM-TT_hh-mm-ss.zip**.
Die Sicherung von der Kommandozeile aus kann durchgeführt werden, während eine Instanz von SmartHomeNG läuft. Es ist
nicht notwendig ein laufendes SmartHomeNG vorher zu beenden.
-.. note::
+.. attention::
Es werden keine Konfigurationsdateien des alten .CONF Formats gesichert, sondern ausschließlich YAML Dateien.
@@ -65,16 +65,19 @@ Beim sichern werden folgende Daten in das zip-Archiv übernommen:
- /etc/plugin.yaml
- /etc/smarthome.yaml
- /etc/struct.yaml
+ - /etc/struct\_\*.yaml
- /etc/\*.cer
+ - /etc/\*.pem
- /etc/\*.key
- - /items - alle .yaml Dateien
- - /logic - alle .yaml Dateien
- - /scenes - alle .yaml Dateien
+ - /functions\*.*
+ - /items\*.yaml
+ - /logic\*.yaml
+ - /scenes\*.yaml
+ - /scenes\*.conf
-.. warning::
+.. attention::
- Zertifikats- und Key Dateien (\*.cer, \*.key) für tls/https werden in SmartHomeNG v1.6 und v1.6.1 NICHT gesichert.
- Dieses erfolgt erst in höheren Releases.
+ Zertifikats- und Key Dateien (\*.cer, \*.pem und \*.key) für tls/https werden erst ab SmartHomeNG v1.7 gesichert.
Falls SmartHomeNG mit der Option **-c** bzw. **--config_dir** gestartet wurde, so wird dieses beim Sichern und
@@ -83,8 +86,12 @@ Wiederherstellen berücksichtigt.
.. warning::
- Ganz ausdrücklich werden keine Daten aus dem Unterverzeichnis ``var`` gesichert.
- Also keine Datenbank aus ``var/db`` oder ``var/rrd``, keine Logfiles aus ``log`` und auch keine Cache Daten aus ``var/cache``
- die via Attribut ``cache: True`` befüllt werden.
+ Es werden nur **Konfigurationsdaten** gesichert.
+
+ Ganz ausdrücklich werden **keine** Daten aus dem Unterverzeichnis ``var`` gesichert.
+ Also keine Datenbank aus ``var/db`` oder ``var/rrd``, keine Logfiles aus ``log`` und auch keine Cache Daten
+ aus ``var/cache`` die via Attribut ``cache: True`` befüllt werden.
+
+ Sollen diese Daten gesichert werden, so muß SmartHomeNG zuerst beendet und danach die gewünschten Dateie manuell
+ gesichert werden.
- Sollen diese Daten gesichert werden, so muß SmartHomeNG zuerst beendet und danach die gewünschten Dateie manuell gesichert werden.
\ No newline at end of file
diff --git a/doc/user/source/konfiguration/konfiguration_ueberblick.rst b/doc/user/source/konfiguration/konfiguration_ueberblick.rst
index e19e899d0b..cf7bc37631 100644
--- a/doc/user/source/konfiguration/konfiguration_ueberblick.rst
+++ b/doc/user/source/konfiguration/konfiguration_ueberblick.rst
@@ -44,6 +44,8 @@ Passende Editoren für Python und YAML Dateien sind z.B.
| Windows | `Notepad++ `_ |
| | |
| | `Atom `_ |
+| | |
+| | `Visual Studio Code `_ |
+-------------+-----------------------------------------------------------------------+
| Mac | `BBEdit `_ |
| | |
@@ -69,36 +71,38 @@ Verzeichnisse in SmartHomeNG
Die Verzeichnisse sind im Hauptverzeichnis von SmartHomeNG zu finden, für gewöhnlich im Verzeichnis `/usr/local/smarthome``.
-+--------------+-----------------------------------------------------------------------------------------------------------------------------+
-| Verzeichnis | Beschreibung / Inhalt |
-+==============+=============================================================================================================================+
-| ``bin`` | Hauptmodul von SmarthomeNG |
-+--------------+-----------------------------------------------------------------------------------------------------------------------------+
-| ``dev`` | Grundgerüst und Infos zur Pluginentwicklung |
-+--------------+-----------------------------------------------------------------------------------------------------------------------------+
-| ``doc`` | Wird einmal die Dokumentation enthalten |
-+--------------+-----------------------------------------------------------------------------------------------------------------------------+
-| ``etc`` | enthält mindestens **smarthome.yaml**, **plugin.yaml** und **logic.yaml**. |
-| | In diesen Dateien befindet sich die Konfiguration des Grundsystems |
-+--------------+-----------------------------------------------------------------------------------------------------------------------------+
-| ``examples`` | Beispiele für Items |
-+--------------+-----------------------------------------------------------------------------------------------------------------------------+
-| ``items`` | Items |
-+--------------+-----------------------------------------------------------------------------------------------------------------------------+
-| ``lib`` | Modulbibliothek für das Hauptprogramm |
-+--------------+-----------------------------------------------------------------------------------------------------------------------------+
-| ``logics`` | Jede Logik bekommt hier eine kleine Datei mit Python Code |
-+--------------+-----------------------------------------------------------------------------------------------------------------------------+
-| ``plugins`` | Modulbibliothek für die Plugins. Jedes Plugin hat sein eigenes Unterverzeichnis |
-+--------------+-----------------------------------------------------------------------------------------------------------------------------+
-| ``scenes`` | Gespeicherte Szenen |
-+--------------+-----------------------------------------------------------------------------------------------------------------------------+
-| ``tests`` | Hilfsprogramme zum Testen von Modulen des Systems |
-+--------------+-----------------------------------------------------------------------------------------------------------------------------+
-| ``tools`` | Hilfsprogramme |
-+--------------+-----------------------------------------------------------------------------------------------------------------------------+
-| ``var`` | Daten die vom SmartHomeNG zur Laufzeit gespeichert und gelesen werden also z.B. Logdateien, cache, sqlite Datenbank, etc. |
-+--------------+-----------------------------------------------------------------------------------------------------------------------------+
++---------------+-----------------------------------------------------------------------------------------------------------------------------+
+| Verzeichnis | Beschreibung / Inhalt |
++===============+=============================================================================================================================+
+| ``bin`` | Hauptmodul von SmarthomeNG |
++---------------+-----------------------------------------------------------------------------------------------------------------------------+
+| ``dev`` | Grundgerüst und Infos zur Pluginentwicklung |
++---------------+-----------------------------------------------------------------------------------------------------------------------------+
+| ``doc`` | Wird einmal die Dokumentation enthalten |
++---------------+-----------------------------------------------------------------------------------------------------------------------------+
+| ``etc`` | enthält mindestens **smarthome.yaml**, **plugin.yaml** und **logic.yaml**. |
+| | In diesen Dateien befindet sich die Konfiguration des Grundsystems |
++---------------+-----------------------------------------------------------------------------------------------------------------------------+
+| ``examples`` | Beispiele für Items |
++---------------+-----------------------------------------------------------------------------------------------------------------------------+
+| ``functions`` | In diesem Verzeichnis werden benutzerdefinierte Funktionen (Userfunctions) gespeichert |
++---------------+-----------------------------------------------------------------------------------------------------------------------------+
+| ``items`` | Items |
++---------------+-----------------------------------------------------------------------------------------------------------------------------+
+| ``lib`` | Modulbibliothek für das Hauptprogramm |
++---------------+-----------------------------------------------------------------------------------------------------------------------------+
+| ``logics`` | Jede Logik bekommt hier eine kleine Datei mit Python Code |
++---------------+-----------------------------------------------------------------------------------------------------------------------------+
+| ``plugins`` | Modulbibliothek für die Plugins. Jedes Plugin hat sein eigenes Unterverzeichnis |
++---------------+-----------------------------------------------------------------------------------------------------------------------------+
+| ``scenes`` | Gespeicherte Szenen |
++---------------+-----------------------------------------------------------------------------------------------------------------------------+
+| ``tests`` | Hilfsprogramme zum Testen von Modulen des Systems |
++---------------+-----------------------------------------------------------------------------------------------------------------------------+
+| ``tools`` | enthält Hilfsprogramme |
++---------------+-----------------------------------------------------------------------------------------------------------------------------+
+| ``var`` | Daten die vom SmartHomeNG zur Laufzeit gespeichert und gelesen werden also z.B. Logdateien, cache, sqlite Datenbank, etc. |
++---------------+-----------------------------------------------------------------------------------------------------------------------------+
Dateien im Verzeichnis *../etc*
@@ -266,6 +270,12 @@ konzentrieren kann.
Weitere Informationen gibt es unter `Konfiguration - Logging `_
+Dateien im Verzeichnis *../functions*
+-------------------------------------
+
+Hier werden benutzerdefinierte Funktionen (Userfunctions) gespeichert.
+
+
Dateien im Verzeichnis *../items*
---------------------------------
diff --git a/doc/user/source/konfiguration/konfigurationsdateien/scenes.rst b/doc/user/source/konfiguration/konfigurationsdateien/scenes.rst
index 5328f8598b..42928b7d67 100644
--- a/doc/user/source/konfiguration/konfigurationsdateien/scenes.rst
+++ b/doc/user/source/konfiguration/konfigurationsdateien/scenes.rst
@@ -34,7 +34,8 @@ Szenen
Für die Verwendung von Szenen ist eine Konfigurationsdatei für jedes 'Szenenobjekt' im Szenenverzeichnis
erforderlich. Diese Dateien können im alten Szenen-Conf Format (Endung '.conf') oder im
yaml Format (Endung '.yaml') erstellt werden und müssen als Dateinamen den Item-Path des Items
-tragen in dem die Szene definiert ist und über das der Status der Szene gesteuert wird.
+(mit der entsprechenden Dateiendung) tragen, in dem die Szene definiert ist und über das der Status der
+Szene gesteuert wird.
altes Konfigurationsformat
@@ -108,8 +109,11 @@ Jede einzelne Aktion ist durch die Keys ``item:`` , ``value:`` und ``learn:`` de
Die Verwendung von Wildcards (*) in den ``item:`` Definitionen ist nicht möglich.
Der Key **item** enthält den Pfad des Items, das verändert werden soll. Der Key **value** enthält
-den Wert auf den das Item gesetzt werden soll. Anstelle eines festen Wertes, kann hier auch ein
-**eval** Ausdruck angegeben werden. Der Key **learn** ist optional. Wird er nicht angegeben,
+den Wert auf den das Item gesetzt werden soll. Wenn dem Item Stringwerte zugewiesen werden sollen,
+müssen diese zweifach in Anführungszeichen angegeben werden, z.B. ``"'Wert'"``, Zahlen können direkt
+angegeben werden, und andere Werte wie z.B. Listen oder dicts müssen einfach in Anführungszeichen
+stehen, z.B. ``"{'key': 'value'}"`` oder ``'["listitem1", "listitem2"]'``. Anstelle eines festen Wertes
+kann hier auch ein **eval** Ausdruck angegeben werden. Der Key **learn** ist optional. Wird er nicht angegeben,
wird der Wert False für **learn** angenommen. Außerdem wird der Wert für **learn** immer auf False
gesetzt, wenn **value** einen Ausdruck und keinen absoluten Wert enthält.
diff --git a/doc/user/source/konfiguration/konfigurationsdateien/struct.rst b/doc/user/source/konfiguration/konfigurationsdateien/struct.rst
index 3dcd016533..256f2fbdb5 100644
--- a/doc/user/source/konfiguration/konfigurationsdateien/struct.rst
+++ b/doc/user/source/konfiguration/konfigurationsdateien/struct.rst
@@ -4,12 +4,18 @@
.. index:: structs; struct.yaml
-struct.yaml
-===========
+struct.yaml und struct_*.yaml
+=============================
In dieser Konfigurationsdatei können eigene Item-Strukturen angelegt werden, die über das **struct** Attribut als
Template verwendet werden können.
+Ab SmartHomeNG v1.9 können eigene Item-Strukturen auf mehrere Dateien verteilt werden.
+
+Außer der Datei **struct.yaml** im Verzeichnis ../etc können weitere Dateien angelegt werden. Deren
+Name muss mit **struct_** beginnen. Der danach folgende Teil des Dateinamens wird dabei dem struct Namen
+als Prefix vorangestellt, um Namensdopplungen vorzubeugen.
+
.. note::
Weitergehende Informationen zu structs sind unter :doc:`Konfiguration/struct ` und in
diff --git a/doc/user/source/konfiguration/logging.rst b/doc/user/source/konfiguration/logging.rst
index fe3ae94855..a8a34bd973 100644
--- a/doc/user/source/konfiguration/logging.rst
+++ b/doc/user/source/konfiguration/logging.rst
@@ -6,32 +6,85 @@
Logging
#######
-Zur Konfiguration des Loggings mit SmartHomeNG wird seit der Version 1.2 eine Konfigurationsdatei
-im YAML Format verwendet.
-
+Ein `Log `_ zeichnet Ergebnisse von Vorgängen oder Berechnungen auf
+und dient der Dokumentation. Anhand eines Logs kann man Programmfehlern auf die Spur kommen oder bestimmte
+Situationen können im Nachhinein untersucht werden. Je detaillierter ein Log geführt wird, desto einfacher
+ist die Untersuchung bestimmter Sachverhalte.
+Je nachdem, was man untersuchen möchte, kann man mit einem **Logging Level** im Programm vorgeben wie ernst
+oder wie wichtig ein bestimmter Logeintrag ist.
+Innerhalb des Kerns von SmartHomeNG finden sich zum Beispiel Einträge im Programm mit dem Log Level **NOTICE**
+die in einer Logdatei dann im Ergebnis so aufgezeichnet werden:
+
+``2021-04-16 21:56:31 NOTICE lib.smarthome -------------------- Init SmartHomeNG 1.8.2c.4e0938c2.develop --------------------``
+
+Das ist als Information zu sehen um bei Problemen Hilfe zu erhalten. Es deutet hier nichts auf Fehler oder Probleme hin.
+Ein anderer Logging Befehl im Core mit dem Log Level **WARNING** erzeugt hingehen folgendes:
+
+``2021-04-16 21:56:32 WARNING lib.module Not loading module Mqtt from section 'mqtt': Module is disabled``
+
+Das ist als Warnung gedacht um darauf hinzuweisen, das ein Module nicht geladen wird und in dieser Folge eventuell
+weitere Fehler oder Probleme auftauchen könnten. Steigerungen von Warnungen sind Log Level **ERROR** oder **CRITICAL**.
+Während ein **ERROR** also ein Fehler durchaus bedeuten kann das SmartHomeNG weiterarbeiten kann, bedeutet ein **CRITICAL**
+also ein kritischer Fehler das das Programm beendet werden muss.
+Fehlt ein für den Kern von SmartHomeNG benötigtes Modul, so stell das einen kritischen Fehler dar.
+
+Die Log Level in der Übersicht, absteigend in der Bedeutung für den Programmablauf:
+
+.. list-table:: Log Level
+ :header-rows: 1
+
+ * - Level
+ - Numerischer Wert
+ - Anmerkung
+ * - 50
+ - CRITICAL
+ - kritisch, führt zumeist zum Programmabbruch
+ * - 40
+ - ERROR
+ - Fehler im Programmablauf, Programm kann zumeist weiterlaufen, Funktionalität möglicherweise eingeschränkt
+ * - 31
+ - NOTICE
+ - Ein Hinweis der zur grundlegenden Information dient und nicht als Warnung verstanden werden soll.
+ Dieser Log Level ist spezifisch für SmartHomeNG und ist im Standard Logging von Python nicht vordefiniert.
+ * - 30
+ - WARNING
+ - Warnung das etwas unerwartetes passiert ist aber trotzdem weitergearbeitet werden kann
+ * - 20
+ - INFO
+ - Eine Ablaufinformation die nicht unbedingt wichtig ist
+ * - DEBUG
+ - 10
+ - Informationen für die Fehlersuche die normalerweise nicht benötigt werden
+ * - NOTSET
+ - 0
+ - Es wird kein Logeintrag erzeugt
+
+Es können prinzipiell auch weitere eigene Log Level definiert werden die dann für besondere Situationen benutzt werden können.
+Ein Beispiel wäre ein Log Level **VERBOSE** mit dem Wert **8** der für die Fehlersuche in einem bestimmten Bereich eines Plugins
+Verwendung finden könnte.
+Für SmartHomeNG ist derzeit nur **NOTICE** vordefiniert um informelle Logging Einträge zu erzeugen, die nicht als Warnung
+verstanden werden sollen.
Konfiguration des Loggings
==========================
-Die Datei **../etc/logging.yaml** befindet sich bereits vorkonfiguriert in dem Verzeichnis.
+Auf der Seite `Python Logging `_
+sind die Konfigurationsmöglichkeiten detailliert beschrieben.
+
+SmartHomeNG lädt beim Start die Konfiguration des Logging aus der Datei **etc/logging.yaml**. Ist diese Datei nicht vorhanden,
+so versucht SmartHomeNG die Datei **etc/logging.yaml.default** zu kopieren nach **etc/logging.yaml** und dann daraus
+die Konfiguration des Loggings zu laden.
+
+Wenn bei der Konfiguration des Loggings etwas schief geht, kann also jederzeit die Datei **etc/logging.yaml** gelöscht oder
+besser umbenannt werden und wird dann beim nächsten Neustart durch den Inhalt der **etc/logging.yaml.default** frisch bereitgestellt.
-Die Datei sieht so aus:
+Ein Beispiel für **etc/logging.yaml.default** im Folgenden:
.. literalinclude:: ../../../../etc/logging.yaml.default
:caption: ../etc/logging.yaml
:language: yaml
-
-In die Konfigurationsmöglichkeiten des Python Loggings kann sich hier eingelesen werden:
-https://docs.python.org/3.4/library/logging.html#module-logging
-
-Die Datei **../etc/logging.yaml** hat kein SmartHomeNG spezifisches Format. Sie wird mit der
-Funktion `logging.config.dictConfig()` (Bestandteil der Python Standardbibliothek) eingelesen.
-
-Informationen zu dieser Python Funktion und den damit verbundenen Möglichkeiten gibt es hier:
-https://docs.python.org/3.4/library/logging.config.html#module-logging.config
-
Kurzdoku der Einträge in der Konfigurationsdatei
------------------------------------------------
@@ -89,6 +142,26 @@ Eintrag im Handler **file:** erfolgen. Der Eintrag `level: WARNING` führt dazu,
Handler **file:** nur Ausgaben für Fehler und Warnungen erfolgen. INFO und DEBUG Ausgaben erfolgen
dann nur noch über den zusätzlichen Handler.
+|
+
+Logging Handler und Filter
+==========================
+
+Zusätzlich zu den Logging Handlern, die im Standard Logging Modul von Python definiert, bringt
+SmartHomeNG weitere Handler und Filter mit, die bei der Konfiguration in ../etc/logging.yaml verwendet werden
+können.
+
+Die Beschreibung dieser Handler und Filter ist im Referenz Abschnitt unter Logging zu finden:
+
+.. toctree::
+
+.. toctree::
+ :maxdepth: 4
+ :titlesonly:
+
+ /referenz/logging/logging_handler
+ /referenz/logging/logging_filter
+
Plugin und Logik Entwicklung
============================
@@ -131,11 +204,11 @@ Logging der Veränderung von Items
---------------------------------
Die Veränderung von Item Werten kann am einfachsten geloggt werden, indem bei dem Item das Attribut **log_change** gesetzt
-wird und auf einen entsprechenden Item Logger verweist. Der Item Logger muss in der ../etc/logging.yaml mit Level INFO oder
+wird und auf einen entsprechenden Item Logger verweist. Der Item Logger muss in der etc/logging.yaml mit Level INFO oder
DEBUG definiert sein.
.. code-block:: yaml
- :caption: ../items/items.yaml
+ :caption: items/items.yaml
test:
item:
@@ -145,9 +218,9 @@ DEBUG definiert sein.
und
.. code-block:: yaml
- :caption: ../etc/logging.yaml
+ :caption: etc/logging.yaml
- ...
+ ...-
logger:
items_:
@@ -166,7 +239,6 @@ von RegEx Ausdrücken sucht, der wird hier :doc:`Logging - Best Practices `
-beschrieben. Zu beachten ist, dass die Konfigurationsdateien für Szenen nur eingelesen werden, wenn ein Item
-gleichen Namens definiert ist und der Type dieses Items **scene** ist.
+beschrieben. Zu beachten ist, dass die Konfigurationsdateien für Szenen nur eingelesen werden, wenn ein Item **pfad.item**
+des Typs **scene** definiert ist und die Szenen-Datei **pfad.item.yaml** bzw. **pfad.item.conf** benannt ist.
Funktionsweise von Szenen
diff --git a/doc/user/source/konfiguration/userfunctions.rst b/doc/user/source/konfiguration/userfunctions.rst
new file mode 100644
index 0000000000..a8389aa69c
--- /dev/null
+++ b/doc/user/source/konfiguration/userfunctions.rst
@@ -0,0 +1,61 @@
+
+.. role:: bluesup
+.. role:: greensup
+.. role:: redsup
+
+===========================
+Userfunctions :redsup:`Neu`
+===========================
+
+Ab Version 1.9 von SmartHomeNG ist die Möglichkeit implementiert, benutzerdefinierte Funktionen (Userfunctions) zu
+schreiben und in eval Statements sowie in Logiken zu verwenden. Diese Funktionen können zur Laufzeit verändert und
+neu geladen werden.
+
+Die Python Dateien mit den Funktionen müssen dazu im Verzeichnis **../functions** abgelegt werden. Es sind normale
+Python Dateien, die mehrere Funktionen enthalten können. Als formale Anforderung sind nur Informationen zur Version
+und eine kurze Beschreibung des Zwecks der Funktionen dieser Datei anzugeben.
+
+Im folgenden Beispiel wird eine Datei (Funktionssammlung) mit dem Namen **anhalter.py** im Verzeichnis **../functions**
+erzeugt:
+
+Die Python Datei sieht folgendermaßen aus:
+
+.. code-block:: python
+ :caption: /usr/local/smarthome/functions/anhalter.py
+
+ #!/usr/bin/env python3
+ # anhalter.py
+
+ _VERSION = '0.1.0'
+ _DESCRIPTION = 'Per Anhalter durch die Galaxis'
+
+ def zweiundvierzig():
+
+ return 'Die Antwort auf die Frage aller Fragen'
+
+
+Wenn nach dem Erstellen dieser Datei SmartHomeNG neu gestartet wird, sieht man im Warnings-Log, dass die Datei
+importiert wird:
+
+.. code::
+
+ 2021-10-14 20:07:08 NOTICE lib.smarthome -------------------- Init SmartHomeNG 1.8.2d.b81166c3.develop --------------------
+ 2021-10-14 20:07:08 NOTICE lib.smarthome Running in Python interpreter 'v3.8.3 final' in virtual environment, from directory /usr/local/shng_dev
+ 2021-10-14 20:07:08 NOTICE lib.smarthome - on Linux-4.9.0-6-amd64-x86_64-with-glibc2.17 (pid=4584)
+ 2021-10-14 20:07:08 NOTICE lib.smarthome - Nutze Feiertage für Land 'DE', Provinz 'HH', 1 benutzerdefinierte(r) Feiertag(e) definiert
+ 2021-10-14 20:07:11 NOTICE lib.userfunctions Userfunctions importiert aus 'anhalter' v0.1.0 - Per Anhalter durch die Galaxis
+ 2021-10-14 20:08:25 NOTICE lib.smarthome -------------------- SmartHomeNG initialization finished --------------------
+
+
+Nun kann man die in der Datei definierten Funktionen nutzen. Allen Userfunctions ist beim Aufruf **uf.** (für
+userfunctions) gefolgt von dem Namen der Datei voranzustellen. Die Funktion **zweiundvierzig** ist also als
+**uf.anhalter.zweiundvierzig()** aufzurufen. In der Admin GUI im eval Syntax Checker sieht das denn folgendermaßen
+aus:
+
+.. image:: assets/uf_eval_checker1.jpg
+ :class: screenshot
+
+Analog können die Funtionen in **eval** Attributen in Item Definitionen und in Logiken aufgerufen werden.
+
+Weitere Details zu Userfunctions sind im Abschnitt :doc:`Referenz ` zu finden.
+
diff --git a/doc/user/source/logiken/logics.rst b/doc/user/source/logiken/logics.rst
index 32c788b575..b9ee90bd8b 100644
--- a/doc/user/source/logiken/logics.rst
+++ b/doc/user/source/logiken/logics.rst
@@ -21,15 +21,34 @@ Die Logik-Skripte müssen im Verzeichnis **../logics** der SmartHomeNG Installat
Grundlegende Struktur
=====================
-Das wichtigste Objekt, dass in Logiken verwendet wird, ist **sh**. Dies ist das Smarthome-Objekt.
+Das wichtigste Objekt, das in Logiken verwendet wird, ist **sh**. Dies ist das Smarthome-Objekt.
Es enthält jedes Detail über die laufende SmartHomeNG Instanz. Mit diesem Objekt ist es möglich auf
-alle Items, Plugins und Grundfunktionen von SmartHomeNG zuzugreifen. Um den Wert eines Items zu
-erhalten, rufen Sie zum Beispiel den Namen auf: sh.path.item(). Um einen neuen Wert zu setzen,
-geben Sie ihn einfach als Argument an: sh.path.item(neuer_wert).
+alle Items, Plugins und Grundfunktionen von SmartHomeNG zuzugreifen.
-Es ist sehr wichtig, immer mit Klammern **()** auf die Items zuzugreifen! Andernfalls würde ein
-Fehler auftreten.
+Zugriff auf Items und Werte
+===========================
+
+Um den Wert eines Items zu erhalten, rufen Sie zum Beispiel den Namen auf: sh.path.item().
+Um einen neuen Wert zu setzen, geben Sie ihn einfach als Argument an: sh.path.item(neuer_wert).
+
+.. attention::
+
+ Zuweisung von Item-Werten:
+
+ Es ist sehr wichtig, immer mit Klammern **()** auf die Items zuzugreifen! Wenn das Item direkt
+ zugewiesen wird, z.B. mit sh.path.item = Wert, dann wird das item-Objekt in SmartHomeNG überschrieben.
+
+ In diesem Fall kann die mitgelieferte Logik **check_items.py** verwendet werden, um auf Vorhandensein
+ entsprechend beschädigter Items zu prüfen und diese wiederherzustellen. Alternativ werden die Items nach
+ einem Neustart von SmartHomeNG neu erstellt.
+
+
+Alternativ kann auch über die Item-Properties auf den Wert zugegriffen werden: sh.path.item.propery.value
+gibt den Wert zurück, mit sh.path.item.property.value = Wert kann der Wert zugewiesen werden. Diese Variante
+lässt sich wie eine normale Variablenzuweisung nutzen.
+Beispiel
+========
Eine Logik sieht prinzipiell folgendermaßen aus:
diff --git a/doc/user/source/logiken/objekteundmethoden_feiertage_datum_zeit.rst b/doc/user/source/logiken/objekteundmethoden_feiertage_datum_zeit.rst
index 792758027d..15811c0b85 100644
--- a/doc/user/source/logiken/objekteundmethoden_feiertage_datum_zeit.rst
+++ b/doc/user/source/logiken/objekteundmethoden_feiertage_datum_zeit.rst
@@ -15,7 +15,7 @@ Feiertage, Daten und Zeiten
Das Modul **shtime** stellt eine Reihe von Funktionen zur Verfügung, die es erlauben festzustellen ob ein Datum ein
Feiertag ist (und welcher). Dazu muss die Verwendung von Feiertagen in **/etc/holidays.yaml** konfiguriert sein.
-Weiterhin gibt es Funktionen, die den Umgang mit Datums und Zeitangaben vereinfachen.
+Weiterhin gibt es Funktionen, die den Umgang mit Datums- und Zeitangaben vereinfachen.
Wenn eine Funktion als Parameter ein Datum oder einen Datum/Zeit Wert erwartet, kann der Parameter in einer der
folgenden Formate angegeben werden:
@@ -39,126 +39,131 @@ folgenden Formate angegeben werden:
Die Funktionen für Feiertags- und Wochenend-Handling sind folgende:
-+-------------------------------------------+---------------------------------------------------------------------------+
-| Funktion | Erläuterung |
-+===========================================+===========================================================================+
-| shtime.is_holiday(date) | Liefert **True**, falls das Datum ein Feiertag (gesetzlich oder |
-| | benutzerdefiniert) ist |
-+-------------------------------------------+---------------------------------------------------------------------------+
-| shtime.is_public_holiday(date) | Liefert **True**, falls das Datum ein gesetzlicher Feiertag ist |
-+-------------------------------------------+---------------------------------------------------------------------------+
-| shtime.holiday_name(date, as_list=False) | Liefert den Namen des Feiertags, falls das Datum ein Feiertag ist. |
-| | Wenn mehrere Feiertage auf das selbe Datum fallen, werden sie Komma- |
-| | getrennt zurück geleifert. Falls **as_list** auf **True** gesetzt wird, |
-| | ist das Ergebnis kein String, sondern eine Liste mit den Feiertagsnamen. |
-+-------------------------------------------+---------------------------------------------------------------------------+
-| shtime.holiday_list(year) | Liefert eine Liste aller Feiertage für ein Jahr |
-+-------------------------------------------+---------------------------------------------------------------------------+
-| shtime.public_holiday_list(year) | Liefert eine Liste aller gesetzlichen Feiertage für ein Jahr |
-+-------------------------------------------+---------------------------------------------------------------------------+
-| shtime.is_weekend(date) | Liefert **True**, falls das Datum auf ein Wochenende (Sa, So) fällt |
-+-------------------------------------------+---------------------------------------------------------------------------+
-| shtime.add_custom_holiday(cust_date) | Trägt benutzerdefinierte Feiertage ein, die den Bedingungen des |
-| | übergebenen **dict** cust_date entsprechen. Das **dict** hat die selbe |
-| | Struktur, wie in der Definition in /etc/holidays.yaml |
-+-------------------------------------------+---------------------------------------------------------------------------+
-| shtime.add_custom_holiday_range(from_date,| Markiert jeden Tag, beginnend mit **fromdate** bis inklusive **to_date** |
-| to_date=None, holiday_name=' ') | als Ferientag mit dem angegebenen Namen |
-+-------------------------------------------+---------------------------------------------------------------------------+
-| shtime.time_since(dt, resulttype) | Liefert die Zeitdifferenz zwischen dem angegeben Zeitpunkt **dt** und |
-| | jetzt. **resulttype** gibt an, ob das Ergebnis als str (**s**) , als |
-| | float Minuten (**m**), Stunden (**h**), Tagen (*+d**), als int Minuten |
-| | (**im**), Stunden (**ih**), Tagen (*+id**) oder als tuple (**dhms**) bzw. |
-| | (**ds**) zurück gegeben werden soll. Falls nicht angegeben, wird **s** |
-| | verwendet. |
-+-------------------------------------------+---------------------------------------------------------------------------+
-| shtime.time_until(dt, resulttype) | Liefert die Zeitdifferenz zwischen jetzt und dem angegeben Zeitpunkt |
-| | **dt**. **resulttype** ist analog zu **time_since()** zu verwenden. |
-+-------------------------------------------+---------------------------------------------------------------------------+
-| shtime.time_diff(dt1, dt2, resulttype) | Liefert die Zeitdifferenz zwischen den beiden angegebenen Zeitpunkten |
-| | **dt1** und **dt2**. **resulttype** ist analog zu **time_since()** zu |
-| | verwenden. |
-+-------------------------------------------+---------------------------------------------------------------------------+
-| shtime.beginning_of_week(week, year) | Liefert das Datum des ersten Tages der angegebenen Woche. Falls **week** |
-| | oder **year** nicht angegeben werden, wird der jeweils aktuelle Wert |
-| | verwendet. |
-+-------------------------------------------+---------------------------------------------------------------------------+
-| shtime.beginning_of_month(month, year) | Liefert das Datum des ersten Tages des angegebenen Monats. Falls **month**|
-| | oder **year** nicht angegeben werden, wird der jeweils aktuelle Wert |
-| | verwendet. |
-+-------------------------------------------+---------------------------------------------------------------------------+
-| shtime.beginning_of_year(year) | Liefert das Datum des ersten Tages des angegebenen Jahres. Falls **year** |
-| | nicht angegeben wird, wird das aktuelle Jahr verwendet. |
-+-------------------------------------------+---------------------------------------------------------------------------+
++------------------------------------------------+----------------------------------------------------------------------------+
+| Funktion | Erläuterung |
++================================================+============================================================================+
+| shtime.is_holiday(date) | Liefert **True**, falls das Datum ein Feiertag (gesetzlich oder |
+| | benutzerdefiniert) ist |
++------------------------------------------------+----------------------------------------------------------------------------+
+| shtime.is_public_holiday(date) | Liefert **True**, falls das Datum ein gesetzlicher Feiertag ist |
++------------------------------------------------+----------------------------------------------------------------------------+
+| shtime.holiday_name(date, as_list=False) | Liefert den Namen des Feiertags, falls das Datum ein Feriertag ist. |
+| | Wenn mehrere Feiertage auf das selbe Datum fallen, werden sie Komma- |
+| | getrennt zurück geleifert. Falls **as_list** auf **True** gesetzt wird, |
+| | ist das Ergebnis kein String, sondern eine Liste mit den Feiertagsnamen. |
++------------------------------------------------+----------------------------------------------------------------------------+
+| shtime.holiday_list(year) | Liefert eine Liste aller Feiertage für ein Jahr |
++------------------------------------------------+----------------------------------------------------------------------------+
+| shtime.public_holiday_list(year) | Liefert eine Liste aller gesetzlichen Feiertage für ein Jahr |
++------------------------------------------------+----------------------------------------------------------------------------+
+| shtime.is_weekend(date) | Liefert **True**, falls das Datum auf ein Wochenende (Sa, So) fällt |
++------------------------------------------------+----------------------------------------------------------------------------+
+| shtime.add_custom_holiday(cust_date) | Trägt benutzerdefinierte Feiertage ein, die den Bedingungen des |
+| | übergebenen **dict** cust_date entsprechen. Das **dict** hat die selbe |
+| | Struktur, wie in der Definition in /etc/holidays.yaml |
++------------------------------------------------+----------------------------------------------------------------------------+
+| shtime.add_custom_holiday_range(from_date, | Markiert jeden Tag, beginnend mit **fromdate** bis inklusive **to_date** |
+| to_date=None, holiday_name=' ') | als Ferientag mit dem angegebenen Namen |
++------------------------------------------------+----------------------------------------------------------------------------+
+| shtime.time_since(dt, resulttype) | Liefert die Zeitdifferenz zwischen dem angegeben Zeitpunkt **dt** und |
+| | jetzt. **resulttype** gibt an, ob das Ergebnis als str (**s**) , als |
+| | float Minuten (**m**), Stunden (**h**), Tagen (*+d**), als int Minuten |
+| | (**im**), Stunden (**ih**), Tagen (*+id**) oder als tuple (**dhms**) bzw. |
+| | (**ds**) zurück gegeben werden soll. Falls nicht angegeben, wird **s** |
+| | verwendet. |
++------------------------------------------------+----------------------------------------------------------------------------+
+| shtime.time_until(dt, resulttype) | Liefert die Zeitdifferenz zwischen jetzt und dem angegeben Zeitpunkt |
+| | **dt**. **resulttype** ist analog zu **time_since()** zu verwenden. |
++------------------------------------------------+----------------------------------------------------------------------------+
+| shtime.time_diff(dt1, dt2, resulttype) | Liefert die Zeitdifferenz zwischen den beiden angegebenen Zeitpunkten |
+| | **dt1** und **dt2**. **resulttype** ist analog zu **time_since()** zu |
+| | verwenden. |
++------------------------------------------------+----------------------------------------------------------------------------+
+| shtime.beginning_of_week(week, year, offset) | Liefert das Datum des ersten Tages der angegebenen Woche. Falls **week** |
+| | oder **year** nicht angegeben werden, wird der jeweils aktuelle Wert |
+| | verwendet. **offset** ermöglicht es, den Wochenstart einer früheren oder |
+| | späteren Woche zu eruieren (default 0). |
++------------------------------------------------+----------------------------------------------------------------------------+
+| shtime.beginning_of_month(month, year, offset) | Liefert das Datum des ersten Tages des angegebenen Monats. Falls **month** |
+| | oder **year** nicht angegeben werden, wird der jeweils aktuelle Wert |
+| | verwendet. **offset** ermöglicht es, den Wochenstart eines früheren oder |
+| | späteren Monats zu eruieren (default 0). |
++------------------------------------------------+----------------------------------------------------------------------------+
+| shtime.beginning_of_year(year, offset) | Liefert das Datum des ersten Tages des angegebenen Jahres. Falls **year** |
+| | nicht angegeben wird, wird das aktuelle Jahr verwendet. |
+| | **offset** ermöglicht es, den Jahresstart eines früheren oder |
+| | späteren Jahrs zu eruieren (default 0). |
++------------------------------------------------+----------------------------------------------------------------------------+
.. index:: Funktionen; Datum und Zeit
Die Funktionen für das Datums-Handling sind folgende:
-+---------------------------------------+---------------------------------------------------------------------------------+
-| Funktion | Erläuterung |
-+=======================================+=================================================================================+
-| shtime.today() | Liefert das aktuelle Datum als **date** |
-+---------------------------------------+---------------------------------------------------------------------------------+
-| shtime.tomorrow() | Liefert das Datum des folgenden Tages als **date** |
-+---------------------------------------+---------------------------------------------------------------------------------+
-| shtime.yesterday() | Liefert das Datum des zurück liegenden Tages als **date** |
-+---------------------------------------+---------------------------------------------------------------------------------+
-| shtime.beginning_of_week(week=None, | Liefert das Datum des Montags der Woche als **date** |
-| year=None) | |
-+---------------------------------------+---------------------------------------------------------------------------------+
-| shtime.beginning_of_month(month=None, | Liefert das Datum des 1. des angegebenen Monats als **date** |
-| year=None) | |
-+---------------------------------------+---------------------------------------------------------------------------------+
-| shtime.beginning_of_year(year=None) | Liefert das Datum des 1. Januar des angegebenen Jahres als **date** |
-+---------------------------------------+---------------------------------------------------------------------------------+
-| shtime.current_year() | Liefert das aktuelle Jahr |
-+---------------------------------------+---------------------------------------------------------------------------------+
-| shtime.current_month() | Liefert den aktuellen Monat |
-+---------------------------------------+---------------------------------------------------------------------------------+
-| shtime.current_day() | Liefert den aktuellen Tag |
-+---------------------------------------+---------------------------------------------------------------------------------+
-| shtime.day_of_year(date) | Liefert als Ergebnis, der wievielte Tag im Jahr das angegebene Datum ist |
-+---------------------------------------+---------------------------------------------------------------------------------+
-| shtime.length_of_year(year) | Liefert die Anzahl Tage, die das angegebene Jahr hat |
-+---------------------------------------+---------------------------------------------------------------------------------+
-| shtime.length_of_month(month, year) | Liefert die Anzahl Tage, die der angegebene Monat im angegebenen Jahr hat |
-+---------------------------------------+---------------------------------------------------------------------------------+
-| shtime.calendar_week(date) | Liefert die Kalenderwoche (nach ISO), in der das angegebene Datum liegt |
-+---------------------------------------+---------------------------------------------------------------------------------+
-| shtime.weekday(date) | Liefert den Wochentag nach ISO (1=Montag - 7=Sonntag) für das angegebene Datum |
-+---------------------------------------+---------------------------------------------------------------------------------+
-| shtime.weekday_name(date) | Liefert den Namen des Wochentags für das angegebene Datum |
-+---------------------------------------+---------------------------------------------------------------------------------+
-| shtime.date_transform(date) | Wandelt ein Datum welches als **date**, **datetime** oder **sting** angegeben |
-| | wurde, in ein Datum vom Typ **date** |
-+---------------------------------------+---------------------------------------------------------------------------------+
-| shtime.datetime_transform(date) | Wandelt eine Datums/Zeitangabe welche als **date**, **datetime** oder **sting** |
-| | angegeben wurde, in ein eine Datums/Zeitangabe vom Typ **datetime** |
-+---------------------------------------+---------------------------------------------------------------------------------+
-| shtime.time_since(dt, resulttype='s') | Liefert die vergangene Zeit von der angegeben Datums/Zeitangabe bis jetzt. |
-| | Über den Parameter **resulttype** kann festgelegt warden, in welcher Form |
-| | das Ergebnis zurück geliefert werden soll: |
-| | |
-| | - s -> Anzahl Sekunden |
-| | - m -> Minuten (mit Nachkommastellen) |
-| | - h -> Stunden (mit Nachkommastellen) |
-| | - d -> Tage (mit Nachkommastellen) |
-| | - im -> Anzahl Minuten (Ganzzahl) |
-| | - ih -> Anzahl Stunden (Ganzzahl) |
-| | - id -> Anzahl Tage (Ganzzahl) |
-| | - dhms -> Tuple (, , , ) |
-| | - ds -> Tuple (, ) |
-+---------------------------------------+---------------------------------------------------------------------------------+
-| shtime.time_until(dt, resulttype='s') | Liefert die vergehende Zeit von jetzt bis zur angegeben Datums/Zeitangabe. |
-| | Der Parameter **resulttype** ist bei der Funktion **shtime.time_since()** |
-| | beschrieben. |
-+---------------------------------------+---------------------------------------------------------------------------------+
-| shtime.time_diff(dt1, dt2, | Liefert die vergehende Zeit von jetzt bis zur angegeben Datums/Zeitangabe. |
-| resulttype='s') | Der Parameter **resulttype** ist bei der Funktion **shtime.time_since()** |
-| | beschrieben. |
-+---------------------------------------+---------------------------------------------------------------------------------+
++-----------------------------------------------+----------------------------------------------------------------------------------+
+| Funktion | Erläuterung |
++===============================================+==================================================================================+
+| shtime.today(offset=0) | Liefert das aktuelle Datum als **date** |
++-----------------------------------------------+----------------------------------------------------------------------------------+
+| shtime.tomorrow() | Liefert das Datum des folgenden Tages als **date** |
++-----------------------------------------------+----------------------------------------------------------------------------------+
+| shtime.yesterday() | Liefert das Datum des zurück liegenden Tages als **date** |
++-----------------------------------------------+----------------------------------------------------------------------------------+
+| shtime.beginning_of_week(week=None, | Liefert das Datum des Montags der Woche als **date** |
+| year=None, offset=0) | |
++-----------------------------------------------+----------------------------------------------------------------------------------+
+| shtime.beginning_of_month(month=None, | Liefert das Datum des 1. des angegebenen Monats als **date** |
+| year=None, offset=0) | |
++-----------------------------------------------+----------------------------------------------------------------------------------+
+| shtime.beginning_of_year(year=None, offset=0) | Liefert das Datum des 1. Januar des angegebenen Jahres als **date** |
++-----------------------------------------------+----------------------------------------------------------------------------------+
+| shtime.current_year(offset=0) | Liefert das aktuelle Jahr |
++-----------------------------------------------+----------------------------------------------------------------------------------+
+| shtime.current_month(offset=0) | Liefert den aktuellen Monat |
++-----------------------------------------------+----------------------------------------------------------------------------------+
+| shtime.current_day(offset=0) | Liefert den aktuellen Tag |
++-----------------------------------------------+----------------------------------------------------------------------------------+
+| shtime.day_of_year(date=None, offset=0) | Liefert als Ergebnis, der wievielte Tag im Jahr das angegebene Datum ist |
++-----------------------------------------------+----------------------------------------------------------------------------------+
+| shtime.length_of_year(year=None, offset=0) | Liefert die Anzahl Tage, die das angegebene Jahr hat |
++-----------------------------------------------+----------------------------------------------------------------------------------+
+| shtime.length_of_month(month=None, year=None, | Liefert die Anzahl Tage, die der angegebene Monat im angegebenen Jahr hat |
+| offset=0) | |
++-----------------------------------------------+----------------------------------------------------------------------------------+
+| shtime.calendar_week(date=None, offset=0) | Liefert die Kalenderwoche (nach ISO), in der das angegebene Datum liegt |
++-----------------------------------------------+----------------------------------------------------------------------------------+
+| shtime.weekday(date=None, offset=0) | Liefert den Wochentag nach ISO (1=Montag - 7=Sonntag) für das angegebene Datum |
++-----------------------------------------------+----------------------------------------------------------------------------------+
+| shtime.weekday_name(date=None, offset=0) | Liefert den Namen des Wochentags für das angegebene Datum |
++-----------------------------------------------+----------------------------------------------------------------------------------+
+| shtime.date_transform(date) | Wandelt ein Datum welches als **date**, **datetime** oder **string** angegeben |
+| | wurde, in ein Datum vom Typ **date** |
++-----------------------------------------------+----------------------------------------------------------------------------------+
+| shtime.datetime_transform(date) | Wandelt eine Datums/Zeitangabe welche als **date**, **datetime** oder **string** |
+| | angegeben wurde, in ein eine Datums/Zeitangabe vom Typ **datetime** |
++-----------------------------------------------+----------------------------------------------------------------------------------+
+| shtime.time_since(dt, resulttype='s') | Liefert die vergangene Zeit von der angegeben Datums/Zeitangabe bis jetzt. |
+| | Über den Parameter **resulttype** kann festgelegt warden, in welcher Form |
+| | das Ergebnis zurück geliefert werden soll: |
+| | |
+| | - s -> Anzahl Sekunden |
+| | - m -> Minuten (mit Nachkommastellen) |
+| | - h -> Stunden (mit Nachkommastellen) |
+| | - d -> Tage (mit Nachkommastellen) |
+| | - im -> Anzahl Minuten (Ganzzahl) |
+| | - ih -> Anzahl Stunden (Ganzzahl) |
+| | - id -> Anzahl Tage (Ganzzahl) |
+| | - dhms -> Tuple (, , , ) |
+| | - ds -> Tuple (, ) |
++-----------------------------------------------+----------------------------------------------------------------------------------+
+| shtime.time_until(dt, resulttype='s') | Liefert die vergehende Zeit von jetzt bis zur angegeben Datums/Zeitangabe. |
+| | Der Parameter **resulttype** ist bei der Funktion **shtime.time_since()** |
+| | beschrieben. |
++-----------------------------------------------+----------------------------------------------------------------------------------+
+| shtime.time_diff(dt1, dt2, | Liefert die vergehende Zeit von jetzt bis zur angegeben Datums/Zeitangabe. |
+| resulttype='s') | Der Parameter **resulttype** ist bei der Funktion **shtime.time_since()** |
+| | beschrieben. |
++-----------------------------------------------+----------------------------------------------------------------------------------+
.. note::
@@ -169,6 +174,11 @@ Die Funktionen für das Datums-Handling sind folgende:
Funktionen, die als Parameter ein **year** und/oder **month** erwarten, können ohne diesen Parameter aufgerufen
werden. Dann wird eine Liste über alle vorberechneten Feiertage zurück geliefert.
+ Funktionen, die als Parameter ein **offset** erwarten, können ohne diesen Parameter aufgerufen werden. Der Standardwert ist 0.
+ Offset ist ein positiver (für zukünftige Werte) oder negativer (für vergangene Werte) Integerwert.
+ Beispiel: shtime.beginning_of_week(None, -2) würde das Startdatum der vorletzten Woche liefern.
+ shtime.day_of_year(shtime.today(), 2) oder shtime.day_of_year(shtime.today(2))
+ den Tag innerhalb des aktuellen Jahres von übermorgen.
.. tip::
@@ -192,4 +202,3 @@ Die Funktionen für das Zeit-Handling sind folgende:
| shtime.runtime() | Liefert die Laufzeit von SmartHomeNG, seit SmartHomeNG das letzte mal gestartet wurde. |
+---------------------------------+----------------------------------------------------------------------------------------+
-
diff --git a/doc/user/source/plugins_all.rst b/doc/user/source/plugins_all.rst
index 67bb125bbb..894ddab446 100644
--- a/doc/user/source/plugins_all.rst
+++ b/doc/user/source/plugins_all.rst
@@ -22,7 +22,16 @@ in der Navigationsleiste.
Hinweise auf weitere Plugins, die sich nicht im SmartHomeNG Repository befinden, sind auf der
entsprechenden `Wiki Seite `_ zu finden.
-Die Plugins sind in die folgenden Kategorien unterteilt:
+Die Plugins sind Kategorien unterteilt, die von der Interaktion mit bzw. Ansteuerung von externen Geräten oder Diensten ableiten.
+
+ - keine Anbindung von Geräten/Diensten: **System-Plugin**
+ - Ansteuerung eines Gerätes je Plugin-Instanz: **Interface-Plugin**
+ - Ansteuerung mehrerer Geräte je Plugin-Instanz: **Gateway-Plugin**
+ - reine Unterstützung von Übertragungsprotokollen: **Protokoll-Plugin**
+ - Anbindung von Internet-Diensten: **Web-Plugin**
+
+
+Die Plugins und die jeweiligen Beschreibungen sind auf den folgenden Seiten aufgelistet:
.. toctree::
:maxdepth: 1
diff --git a/doc/user/source/plugins_doc/plugins_web_header.rst b/doc/user/source/plugins_doc/plugins_web_header.rst
index e4acd0d95c..b7525bc07d 100644
--- a/doc/user/source/plugins_doc/plugins_web_header.rst
+++ b/doc/user/source/plugins_doc/plugins_web_header.rst
@@ -14,7 +14,7 @@ Web/Cloud Plugins
-Ein Web- oder Cloud-Plugin implementiert den Zugriff zu lokalen Netzwerk Services oder zu
+Ein Web- (oder Cloud-) Plugin implementiert den Zugriff zu lokalen Netzwerk-Services oder zu
Internet Services.
|br|
diff --git a/doc/user/source/referenz/assets/uf_editor1.jpg b/doc/user/source/referenz/assets/uf_editor1.jpg
new file mode 100644
index 0000000000..8e3e34145f
Binary files /dev/null and b/doc/user/source/referenz/assets/uf_editor1.jpg differ
diff --git a/doc/user/source/referenz/assets/uf_eval_checker1.jpg b/doc/user/source/referenz/assets/uf_eval_checker1.jpg
new file mode 100644
index 0000000000..81a2a8ebee
Binary files /dev/null and b/doc/user/source/referenz/assets/uf_eval_checker1.jpg differ
diff --git a/doc/user/source/referenz/items/standard_attribute/crontab.rst b/doc/user/source/referenz/items/standard_attribute/crontab.rst
index 6cee4e21ca..90366e2faf 100644
--- a/doc/user/source/referenz/items/standard_attribute/crontab.rst
+++ b/doc/user/source/referenz/items/standard_attribute/crontab.rst
@@ -1,94 +1,103 @@
.. index:: Standard-Attribute; crontab
.. index:: crontab
-crontab
-=======
+.. role:: bluesup
-Das Item wird zum Start von SmarthomeNG aktualisiert und triggert
-dadurch unter Umständen eine zugewiesene Logik:
+crontab :bluesup:`Update`
+===========================
-.. code-block:: yaml
+Es gibt drei verschiedene Parametersätze für ein Crontab Attribut:
- crontab: init
+.. tabs::
-Hier kann auch zusätzlich ein Offset angegeben werden um den
-tatsächlichen Zeitpunkt zu verschieben:
+ .. tab:: init
+ Das Item wird zum Start von SmarthomeNG aktualisiert und triggert
+ dadurch unter Umständen eine zugewiesene Logik:
-.. code-block:: yaml
+ .. code-block:: yaml
- crontab: init+10 # 10 Sekunden nach Start
+ crontab: init
-Das Item soll zu bestimmten Zeitpunkten aktualisiert werden:
+ Hier kann auch zusätzlich ein Offset angegeben werden um den
+ tatsächlichen Zeitpunkt zu verschieben:
-.. code-block:: yaml
+ .. code-block:: yaml
- crontab:
+ crontab: init+10 # 10 Sekunden nach Start
-Dabei sind folgende Werte zulässig:
+ .. tab:: Zeitpunkte
- - Minute: 0 bis 59
- - Stunde: 0 bis 23
- - Tag: 1 bis 31
- - Wochentag 0 (Montag) bis 6 (Sonntag)
+ Das Item soll zu bestimmten Zeitpunkten aktualisiert werden.
+ Die Schreibweise ist an Linux Crontab angelehnt, entspricht diesem aber nicht genau.
+ Es gibt je nach Parameteranzahl 3 Varianten:
+ * ``crontab: ``
+ * ``crontab: ``
+ * ``crontab: ``
-Das Item soll zu bestimmten Zeitpunkten aktualisiert und auf einen bestimmten Wert gesetzt werden:
+ Dabei sind je nach Variante folgende Werte zulässig:
-.. code-block:: yaml
+ * Sekunde: ``0`` bis ``59``
+ * Minute: ``0`` bis ``59``
+ * Stunde: ``0`` bis ``23``
+ * Tag: ``1`` bis ``31``
+ * Monat: 1 bis 12 oder ``jan`` bis ``dec``
+ * Wochentag ``0`` bis ``6`` oder ``mon``, ``tue``, ``wed``, ``thu``, ``fri``, ``sat``, ``sun``
- crontab: =
+ Alle Parameter müssen durch ein Leerzeichen getrennt sein und innerhalb eines Parameters
+ darf kein zusätzliches Leerzeichen vorhanden sein, sonst kann der Parametersatz nicht ausgewertet werden.
- # Beispiel: Hier wird um 23:59, Jeden Tag, Jeden Wochentag ausgelöst und der Wert 70 gesetzt
- # crontab: 59 23 * * = 70
+ Im folgenden Beispiel wird jeden Tag um 23:59 ein Trigger erzeugt und der Wert 70 gesetzt.
+ .. code-block:: yaml
-Für jede dieser Zeiteinheiten (Minuten, Stunde, Tag, Wochentag) werden
-folgende Muster unterstützt (Beispiel jeweils ohne Anführungszeichen verwenden):
+ crontab: 59 23 * * = 70
-* eine einzelne Zahl, z.B. "8" (immer zur/zum 8. Minuten/Stunde/Tag/Wochentag)
-* eine Liste von Zahlen, z.B. "0,8,16" (immer zur/zum 0., 8. und 16. Minute/Stunde/Tag/Wochentag)
-* ein Wertebereich, z.B. "0-8" (immer zwischen dem/der 0. und 8. Minute/Stunde/Tag/Wochentag)
-* einen Interval, z.B. "\*\/4" (immer alle 4 Minuten/Stunden/Tage/Wochetag)
-* einen Stern, z.B. "*" (jede Minuten/Stunde/Tag/Wochentag)
+ Für jede dieser Zeiteinheiten (Minuten, Stunde, Tag, Wochentag) werden
+ folgende Muster unterstützt (Beispiel jeweils ohne Anführungszeichen verwenden):
+ * eine einzelne Zahl, z.B. ``8`` → immer zur/zum 8. Sekunde/Minute/Stunde/Tag/Wochentag
+ * eine Liste von Zahlen, z.B. ``2,8,16`` → immer zur/zum 2., 8. und 16. Sekunde/Minute/Stunde/Tag/Monat/Wochentag
+ * ein Wertebereich, z.B. ``1-5`` → immer zwischen dem/der 1. und 5. Sekunde/Minute/Stunde/Tag/Monat/Wochentag
+ * einen Interval, z.B. ``\*\/4`` → immer alle 4 Sekunden/Minuten/Stunden/Tage/Wochentage
+ * einen Stern, z.B. ``*`` → jede Sekunde/Minute/Stunde/Tag/Monat/Wochentag
-Ausser diesem Muster wird noch ein weiteres Muster in Bezug auf den
-Sonnenauf- sowie Sonnenuntergang unterstützt, z.B:
+ .. tab:: Zeitpunkte bezogen auf Aufgang von Sonne oder Mond
-* den Wert “sunrise”, z.B. ``crontab: sunrise`` (immer zum Sonnenaufgang)
-* den Wert “sunset”, z.B. ``crontab: sunset`` (immer zum Sonnenuntergang)
-* den Wert (z.B. “sunrise”) und Limitierung vorher, z.B.
- ``crontab: 06:00 )`` kann ein Triggerpunkt bezogen
+ auf Sonne oder Mond berechnet werden:
+ * ``sunrise`` → immer zum Sonnenaufgang
+ * ``sunset`` → immer zum Sonnenuntergang
+ * ``sunrise`` und untere Begrenzung → ``06:00** konfiguriert sein. Wertänderungen des Items werden dann mit dem Level **INFO** geloggt.
+
+Ab **SmartHomeNG v1.9** kann das Item Logging über die Attribute **log_level** und **log_text** an die eigenen
+Bedürfnisse angepasst werden.
+
+
+Attribut *log_level*
+====================
+
+Das Attribut **log_level** ermöglicht es einen anderen Loglevel für den durch **log_change** ausgelösten Log-Eintrag
+festzulegen. Der angegebene Loglevel kann jeder in SmartHomeNG unterstützte Python Loglevel sein (DEBUG, INFO, WARNING,
+ERROR, CRITICAL). Die Angabe kann durch den Namen oder den Integer Wert des Loglevels erfolgen.
+
+**Beispiel:** ``log_level: WARNING``
+
+
+Attribut *log_text*
+===================
+
+Das Attribut **log_text** ermöglicht es einen eigenen Text für den Logeintrag festzulegen. **log_text** kann dabei
+eine Reihe von Variablen und eval-Ausdrücken enthalten.
+
+
+.. attention::
+
+ **Achtung:** log_text darf keine Single-Quotes (``'``) enthalten!
+
+ Falls es aufgrund des YAML Syntaxes notwendig kann der gesamte String für log_text in Single-Quotes (')
+ eingeschlossen werden.
+
+ **Beispiel:** ``log_text: 'Alter={age}'``
+
+
+
+Variablen in log_text
+---------------------
+
+Um den Log Text dynamisch gestalten zu können, können variable Werte in den Text String eingeschlossen werden.
+
+Die Variablen müssen in geschweifte Klammern eingeschlossen werden.
+
+**Beispiel:** ``log_text: Das Alter des Item-Wertes ist {age} Sekunden``
+
+Unterstützt werden folgende Variablen/Platzhalter:
+
++-----------------+------------------------------------------------------------------------------+
+| **Variable** | **Beschreibung** |
++=================+==============================================================================+
+| {value} | Aktueller Wert des Items |
++-----------------+------------------------------------------------------------------------------+
+| {caller} | Aufrufendes Objekt, welches das Item verändert |
++-----------------+------------------------------------------------------------------------------+
+| {source} | Quelle des Item Werts (oder None) |
++-----------------+------------------------------------------------------------------------------+
+| {dest} | Ziel des Wertes (oder None) |
++-----------------+------------------------------------------------------------------------------+
+| {name} | Name des Items |
++-----------------+------------------------------------------------------------------------------+
+| {age} | Alter des aktuellen Item-Wertes |
++-----------------+------------------------------------------------------------------------------+
+| {mvalue} | Über ``log_mapping`` zugewiesener Wert für den eigentlichen Item Wert |
++-----------------+------------------------------------------------------------------------------+
+| {lvalue} | Letzter Wert des Items |
++-----------------+------------------------------------------------------------------------------+
+| {mlvalue} | Über ``log_mapping`` zugewiesener Wert für den letzten Item Wert |
++-----------------+------------------------------------------------------------------------------+
+| {pname} | Name des Parent-Items |
++-----------------+------------------------------------------------------------------------------+
+| {id} | ID (Pfad) des Items |
++-----------------+------------------------------------------------------------------------------+
+| {pid} | ID (Pfad) des Parent-Items |
++-----------------+------------------------------------------------------------------------------+
+| {lowlimit} | unterer Grenzwert für Logeinträge |
++-----------------+------------------------------------------------------------------------------+
+| {highlimit} | oberer Grenzwert für Logeinträge |
++-----------------+------------------------------------------------------------------------------+
+
+
+eval-Ausdrücke in log_text
+--------------------------
+
+Zusätzlich zu den Variaben, können in den Log Text auch eval Ausdrücke eingeschlossen werden. Der Syntax dazu ist
+folgender: ``{eval()}``. Dabei muss sicher gestellt sein, dass der Ausdruck ein String ist. Wenn man
+zum Beispiel nur Zahlen addiert ``3+5``, muss dieser Ausdruck in **doppelte** Anführungszeichen (``"``) gesetzt werden:
+``{eval("3+5")}``.
+
+Es können auch mehrere eval-Ausdrücke in einen Log Text eingebunden und mit Variablen konfiguriert werden.
+
+**Beispiel:** ``log_text: Ergebnis={eval("1+4")} für item {id}``
+
+
+Attribut *log_mapping*
+======================
+
+Über das **log_mapping** Attribut kann festgelegt werden, auf welche Werte/Strings der Wert eines Items für das
+Logging gemappt werden soll. Das Attribut **log_mapping** enthält dazu in einem String die Beschreibung eines
+dicts. Wobei der Key den zu übersetzenden/mappenden Wert angibt und der dazu gehörige Value des dicts den String
+angeibt, der über die Variable ``{mvalue}`` ausgegeben wird.
+
+**Beispiel:**
+
+.. code-block:: yaml
+
+ log_mapping: "{
+ '1': 'Eins',
+ '2': 'Zwei',
+ '3': 'Drei'
+ }"
+
+
+Attribut *log_rules*
+====================
+
+Über das **log_rules** Attribut kann festgelegt werden, welche zusätzliche Regeln für das Erzeugen des Log-Eintrages
+anzuwenden sind. Das Attribut **log_rules** enthält dazu in einem String die Beschreibung eines dicts.
+
+**Beispiel:**
+
+.. code-block:: yaml
+
+ log_rules: "{
+ 'lowlimit' : -1.0,
+ 'highlimit': 10.0,
+ 'filter': [1, 2, 5]
+ }"
+
+Die Filter Liste hat dabei vorrang. Es wird also nur bei den Werten 1, 2 und 5 geloggt, obwohl lowlimit und
+highlimit weitere Werte zulassen würden.
+
+
+lowlimit
+--------
+
+``lowlimit`` ein Wert der angibt, unterhalb welchen Wertes des Items **kein** Logeintrag geschrieben werden soll.
+Werte werden geschrieben, Wenn **lowlimit** <= **value** ist.
+
+**low_limit** kann nur auf Items vom Typ **num** angewendet werden.
+
+
+highlimit
+---------
+
+``highlimit`` ein Wert der angibt, oberhalb welchen Wertes des Items **kein** Logeintrag geschrieben werden soll.
+Werte werden geschrieben, Wenn **value** < **highlimit** ist.
+
+**highlimit** kann nur auf Items vom Typ **num** angewendet werden.
+
+
+filter
+------
+
+``filter`` eine Werteliste die angibt, bei welchen Werten des Items ein Logeintrag geschrieben werden soll.
+
+Wenn das Item vom Typ **num** ist, muss die Liste auch numerische Werte (int oder float) enthalten
+(``'filter': [1, 2, 5, 2.1]``). Falls das Item von einem anderen Datentyp ist, muss die Liste Strings
+enthalten (``'filter': ['1', '2', '5']``).
diff --git a/doc/user/source/referenz/items/standard_attribute/on_update.rst b/doc/user/source/referenz/items/standard_attribute/on_update.rst
index 013f4cf973..45f6402006 100644
--- a/doc/user/source/referenz/items/standard_attribute/on_update.rst
+++ b/doc/user/source/referenz/items/standard_attribute/on_update.rst
@@ -43,7 +43,7 @@ Die Syntax ist wie folgt:
Attributs den vorangegangenen Wert des Items. Wenn im **on_update** Ausdruck auf den vorangegangenen
Wert des Items zugegriffen werden soll, geht das mit der Item-Methode **prev_value()** oder dem
Item Property **property.last_value**. Um das Item selbst zu adressieren kann am einfachsten
- die relative Adressierung mittels **sh.self.prev_value()** eingesetzt werden.
+ die relative Adressierung mittels **sh..self.prev_value()** eingesetzt werden.
.. attention::
@@ -109,7 +109,7 @@ Wenn in **eval** Ausdrücken in **on_change** oder **on_update** Attributen auf
des Items zugegriffen werden soll, muss dazu die Item Funktion **prev_value()** oder
das Item Property **property.last_value** genutzt werden.
Auf den alten Wert des aktuellen Items kann ohne die Angabe des vollständigen Item Pfades durch
-den Ausdruck **sh.self.prev_value()** zugegriffen werden.
+den Ausdruck **sh..self.prev_value()** zugegriffen werden.
.. attention::
diff --git a/doc/user/source/referenz/items/standard_attribute/standard_attribute.rst b/doc/user/source/referenz/items/standard_attribute/standard_attribute.rst
index b377b603b0..61164e1a1a 100644
--- a/doc/user/source/referenz/items/standard_attribute/standard_attribute.rst
+++ b/doc/user/source/referenz/items/standard_attribute/standard_attribute.rst
@@ -10,6 +10,10 @@
.. index:: value
.. index:: Standard-Attribute; log_change
.. index:: log_change
+.. index:: Standard-Attribute; log_level
+.. index:: log_level
+.. index:: Standard-Attribute; log_text
+.. index:: log_text
.. index:: Standard-Attribute; name
.. index:: name
.. index:: Standard-Attribute; remark
@@ -20,8 +24,8 @@
.. role:: bluesup
-Standard Attribute
-==================
+Standard Attribute :bluesup:`Update`
+====================================
In SmartHomeNG werden eine Reihe von Standard Attributen unterstützt. Diese sind in der folgenden
@@ -34,19 +38,19 @@ plugin-spezifischen Attribute ist in der Dokumentation des jeweiligen Plugins na
| **Attribut** | **Beschreibung** |
+=================+========================================================================================+
| autotimer | setzt den Wert des Items nach einer Zeitspanne auf einen bestimmten Wert. |
-| | **Ab SmartHomeNG v1.3** werden die Konfigurationsmöglichkeiten erweitert |
+| | Ab SmartHomeNG v1.3 wurden die Konfigurationsmöglichkeiten erweitert |
| | (siehe :doc:`autotimer <./autotimer>`). |
+-----------------+----------------------------------------------------------------------------------------+
-| cache | Wenn 'Yes', dann wird der Wert des Items zwischengespeichert und beim |
-| | erneuten Start von SmartHomeNG wird der alte Wert aus dem Zwischenspeicher |
-| | geladen (vergleichbar mit dem Permanentspeicher vom HS) |
+| cache | Wenn das Attribut auf **True** (oder 'Yes') gesetzt wird, dann wird der Wert des Items |
+| | zwischengespeichert und beim erneuten Start von SmartHomeNG wird der alte Wert aus dem |
+| | Zwischenspeicher geladen (vergleichbar mit dem Permanentspeicher vom HS) |
+-----------------+----------------------------------------------------------------------------------------+
| crontab | Die Evaluierung des Items findet zu angegebenen Zeitpunkten statt (siehe |
| | Beschreibung unten) |
+-----------------+----------------------------------------------------------------------------------------+
| cycle | Definiert ein regelmäßiges Aufrufen des Items (und damit der verknüpften |
-| | Logik oder Eval-Funktion). **Ab SmartHomeNG v1.3** werden die |
-| | Konfigurationsmöglichkeiten erweitert (siehe Beschreibung unten). |
+| | Logik oder Eval-Funktion). Ab SmartHomeNG v1.3 wurden die |
+| | Konfigurationsmöglichkeiten erweitert (siehe :doc:`cycle <./cycle>`). |
+-----------------+----------------------------------------------------------------------------------------+
| enforce_updates | Wenn das Attribut auf **True** gesetzt wird, führt jede Wertzuweisung ans Item |
| | dazu, das abhängige Logiken und item Evaluationen getriggert werden, auch |
@@ -70,21 +74,41 @@ plugin-spezifischen Attribute ist in der Dokumentation des jeweiligen Plugins na
| | vom Typ **dict** erfolgen soll, muss unbedingt darauf geachtet werden, dass |
| | der angegebene Wert in Anführungszeichen gesetzt wird, damit yaml nicht den |
| | Wert nicht als Datenstruktur interpretiert. |
-| | (Also folgendermaßen: **initial_value**: "{'k1': 'v1', 'k2': 'v2'}" ) |
+| | (Also folgendermaßen: ``initial_value: "{'k1': 'v1', 'k2': 'v2'}"`` ) |
+-----------------+----------------------------------------------------------------------------------------+
-| log_change | Ermöglicht das Loggen jeder Veränderung des Item-Wertes. **log_change** muss |
-| | dazu den Namen des zu verwendeten Loggers enthalten. In **logging.yaml** |
-| | muss der Logger als **items.** konfiguriert sein. Wertänderungen des |
-| | Items werden dann mit dem Level INFO geloggt. **Ab SmartHomeNG v1.5** |
+| log_change | Ermöglicht das Loggen jeder Veränderung des Item-Wertes. **log_change** muss dazu den |
+| | Namen des zu verwendeten Loggers enthalten. In **logging.yaml** muss der Logger als |
+| | **items.** konfiguriert sein. Wertänderungen des Items werden dann mit dem Level |
+| | **INFO** geloggt. (siehe :doc:`log_change <./log_change>`) **Ab SmartHomeNG v1.5** |
++-----------------+----------------------------------------------------------------------------------------+
+| log_level | Ermöglicht es einen anderen Loglevel für log_change festzulegen. Der angegebene |
+| | Log_level kann jeder in SmartHomeNG unterstützte Python Loglevel sein. Die Angabe kann |
+| | durch den Namen oder den Integer Wert des Loglevels erfolgen. **Ab SmartHomeNG v1.9** |
++-----------------+----------------------------------------------------------------------------------------+
+| log_text | Ermöglicht es einen eigenen Text für den Logeintrag festzulegen. **log_text** kann |
+| | dabei eine Reihe von Variablen und eval-Ausdrücken enthalten. **Ab SmartHomeNG v1.9** |
+| | Unterstützt werden folgende Variablen: value, caller, source, dest, id, name, age, |
+| | mvalue, lvalue, mlvalue, pid, pname, lowlimit, highlimit |
+| | |
+| | **Achtung:** log_text darf keine Single-Quotes (``'``) enthalten! |
+| | Falls es aufgrund des YAML Syntaxes notwendig kann der gesamte String für log_text |
+| | in Single-Quotes (``'``) eingeschlossen werden. **Beispiel:** |
+| | ``log_text: 'Alter={age}'`` |
++-----------------+----------------------------------------------------------------------------------------+
+| log_mapping | Ermöglicht es im Text ein Mapping von **value** auf einen anderen Wert auszugeben |
+| | (siehe :doc:`log_change <./log_change>`). **Ab SmartHomeNG v1.9** |
++-----------------+----------------------------------------------------------------------------------------+
+| log_rules | Ermöglicht es Regeln zum log_change zu definieren |
+| | (siehe :doc:`log_change <./log_change>`). **Ab SmartHomeNG v1.9** |
+-----------------+----------------------------------------------------------------------------------------+
| name | ein optionaler Name für das Item |
+-----------------+----------------------------------------------------------------------------------------+
| on_update | Ermöglicht das setzen des Wertes anderer Items, wenn das aktuelle Item ein |
| | Update erhält (auch wenn sich der Wert des aktuellen Items dabei nicht |
-| | ändert). **Ab SmartHomeNG v1.4** |
+| | ändert). |
+-----------------+----------------------------------------------------------------------------------------+
| on_change | Ermöglicht das Setzen des Wertes anderer Items, wenn der Wert des aktuellen |
-| | Items verändert wird. **Ab SmartHomeNG v1.4** |
+| | Items verändert wird. |
+-----------------+----------------------------------------------------------------------------------------+
| remark | ein optionaler Kommentar für das Item. Es ist sinnvoll Kommentare zu einem |
| | Item als **remark** Attribut zu erfassen und nicht als Kommentar ( **#** ) |
@@ -121,6 +145,7 @@ plugin-spezifischen Attribute ist in der Dokumentation des jeweiligen Plugins na
enforce_updates
enforce_change
eval
+ log_change
on_update
struct
type
diff --git a/doc/user/source/referenz/logging/logging.rst b/doc/user/source/referenz/logging/logging.rst
new file mode 100644
index 0000000000..f7d9266842
--- /dev/null
+++ b/doc/user/source/referenz/logging/logging.rst
@@ -0,0 +1,30 @@
+
+.. index:: Referenz; Logging
+.. Index:: Logging; Referenz
+
+.. role:: bluesup
+.. role:: redsup
+
+
+=====================
+Logging :redsup:`Neu`
+=====================
+
+SmartHomeNG nutzt das Logging-Modul von Python. In den folgenden Abschnitten sind Informationen zum Logging
+in SmartHomeNG zu finden.
+
+Die vollständige zum Python Logging Modul ist unter
+`logging - Logging facility for Python `_ zu finden.
+
+|
+
+.. toctree::
+ :maxdepth: 4
+ :hidden:
+ :titlesonly:
+
+ logging_handler
+ logging_formatter
+ logging_filter
+
+
diff --git a/doc/user/source/referenz/logging/logging_filter.rst b/doc/user/source/referenz/logging/logging_filter.rst
new file mode 100644
index 0000000000..358f2daf37
--- /dev/null
+++ b/doc/user/source/referenz/logging/logging_filter.rst
@@ -0,0 +1,116 @@
+
+.. index:: Referenz; Logging Filter
+.. Index:: Logging Filter; Referenz
+
+.. role:: bluesup
+.. role:: redsup
+
+
+==============
+Logging Filter
+==============
+
+Mit der Hilfe von Logging Filtern kann innerhalb eines Handlers gesteuert werden, ob ein Log Record erzeugt
+werden soll, oder ob die Information verworfen werden soll.
+
+SmartHomeNG bringt einige Logging Filter mit.
+
+|
+
+CherryPyFilter
+==============
+
+Das im http Modul von SmartHomeNG genutzte CherryPy Modul erzeugt leider unerwartete/unerwünschte Log Einträge,
+die in einigen Logs auftreten können. Um diese Log Einträge zu unterdrücken, kann der **CherryPyFilter**
+genutzt werden, den das http Modul mitbringt.
+
+Benutzung des Filters
+---------------------
+
+In der Datei ``../etc/logging.yaml`` wird der **CherryPyFilter** im Abschnitt ``filters:`` zur Nutzung
+konfiguriert.
+
+.. code-block:: yaml
+
+ filters:
+ cherrypy_filter:
+ (): modules.http.CherryPyFilter
+
+Um den Filter in einem Log-Handler anzuwenden, muss der Filter noch in der Konfiguration des entsprechenden
+Handlers im Abschnitt ``handlers:`` konfiguriert werden:
+
+.. code-block:: yaml
+
+ handlers:
+ shng_details_file:
+ (): lib.log.ShngTimedRotatingFileHandler
+ formatter: shng_simple
+ level: DEBUG
+ utc: false
+ when: midnight
+ backupCount: 7
+ filename: ./var/log/smarthome-details.log
+ encoding: utf8
+ filters: [cherrypy_filter]
+
+|
+
+DuplicateFilter
+===============
+
+Manchmal ist es wünschenswert mehrere gleich lautende Logeinträge, die direkt aufeinander folgen zu unterdrücken
+und nur den ersten Log Eintrag wirklich in das Log zu schreiben. Diese Aufgabe erfüllt der **DuplicateFilter**.
+
+Benutzung des Filters
+---------------------
+
+In der Datei ``../etc/logging.yaml`` wird der **DuplicateFilter** im Abschnitt ``filters:`` zur Nutzung
+konfiguriert.
+
+.. code-block:: yaml
+
+ filters:
+ duplicatefilter:
+ (): lib.logutils.DuplicateFilter
+
+Um den Filter in einem Log-Handler anzuwenden, muss der Filter noch in der Konfiguration des entsprechenden
+Handlers im Abschnitt ``handlers:`` konfiguriert werden.
+
+|
+
+Filter
+======
+
+**Filter** ist ein recht universeller Filter, der über eine Reihe von Parametern konfiguriert werden kann:
+
+ - **name**: Name des Loggers (regex)
+ - **module**: Loggendes Module (regex)
+ - **msg**: Log Message (regex)
+ - **timestamp**: Zeitpunkt des Logeintrages (regex)
+ - **invert**: Über **invert** kann das durch die obigen Parameter erzeugte Filter Ergebnis invertiert werden
+
+Bis auf **invert** können alle Parameter Listen von Strings sein. Diese Strings können Regular Expressions
+enthalten.
+
+Benutzung des Filters
+---------------------
+
+In der Datei ``../etc/logging.yaml`` wird der **Filter** im Abschnitt ``filters:`` zur Nutzung
+konfiguriert.
+
+.. code-block:: yaml
+
+ filters:
+ duplicatefilter:
+ (): lib.logutils.Filter
+ name: []
+ module: []
+ msg: []
+ timestamp: []
+ invert: False
+
+Um den Filter in einem Log-Handler anzuwenden, muss der Filter noch in der Konfiguration des entsprechenden
+Handlers im Abschnitt ``handlers:`` konfiguriert werden.
+
+|
+
diff --git a/doc/user/source/referenz/logging/logging_formatter.rst b/doc/user/source/referenz/logging/logging_formatter.rst
new file mode 100644
index 0000000000..14e891807c
--- /dev/null
+++ b/doc/user/source/referenz/logging/logging_formatter.rst
@@ -0,0 +1,48 @@
+
+.. index:: Referenz; Logging Formatter
+.. Index:: Logging Formatter; Referenz
+
+.. role:: bluesup
+.. role:: redsup
+
+
+=================
+Logging Formatter
+=================
+
+Mit **Logging Formattern** wird festgelegt, wie die Informationen aufbereitet werden sollen, wenn sie in ein
+Log geschrieben werden.
+
+Der standardmäßig in SmartHomeNG verwendete Logger ist **shng_simple** und in der Konfigurationsdatei
+``../etc/logging.yaml`` definiert. Dieser Logger sollte für das Warnings-Log (und möglichst auch für die weitern
+Logs) verwendet werden. Er bietet einen guten Kompromiss zwischen Übersichtlichkeit/Lesbarkeit und Detailreichtum.
+Die Ausgaben mit diesem Formatter helfen besonders bei Supportanfragen.
+
+Der **shng_simple** Formatter gibt außer der Log Message die folgenden Attribute aus: Den Zeitpunkt, den Loglevel
+und den Namen des Loggers. Er ist folgendermaßen definiert:
+
+.. code-block:: yaml
+
+ formatters:
+
+ shng_simple:
+ format: '%(asctime)s %(levelname)-8s %(name)-19s %(message)s'
+ datefmt: '%Y-%m-%d %H:%M:%S'
+
+Wenn zusätzlich die Ausgabe der Zeitzone benötigt/gewünscht wird, kann das Datumsformat entsprechend angepasst
+werden:
+
+.. code-block:: yaml
+
+ formatters:
+
+ shng_simple:
+ format: '%(asctime)s %(levelname)-8s %(name)-19s %(message)s'
+ datefmt: '%Y-%m-%d %H:%M:%S %Z'
+
+
+Eine vollständige Liste der Attribute eines Log-Records kann in der Python Dokumentation unter
+`logging - Logging facility for Python `_ nachgelesen werden.
+
+|
+
diff --git a/doc/user/source/referenz/logging/logging_handler.rst b/doc/user/source/referenz/logging/logging_handler.rst
new file mode 100644
index 0000000000..7ed34facc0
--- /dev/null
+++ b/doc/user/source/referenz/logging/logging_handler.rst
@@ -0,0 +1,122 @@
+
+.. index:: Referenz; Logging
+.. Index:: Logging; Referenz
+
+.. role:: bluesup
+.. role:: redsup
+
+
+===============
+Logging Handler
+===============
+
+Zusätzlich zu den Logging Handlern, die im Standard Logging Modul von Python definiert, bringt SmartHomeNG
+weitere Handler mit, die bei der Konfiguration in ../etc/logging.yaml verwendet werden können.
+
+|
+
+ShngTimedRotatingFileHandler
+============================
+
+Der **ShngTimedRotatingFileHandler** ist eine Variante des **TimedRotatingFileHandler**, der im Python
+Logging Modul vorhanden ist (logging.handlers.TimedRotatingFileHandler).
+
+Der **TimedRotatingFileHandler** benennt die Backaup Versionen einer Log Datei in einer Art und Weise um, die
+im Handling umständlich sein kann, da die Datei dabei die normale Extension **.log** verliert:
+
+ smarthome-warnings.log --> smarthome-warnings.log.2021-04-06
+
+Der **ShngTimedRotatingFileHandler** hat die gleiche Funktionalität und Konfigurierbarkeit wie der
+**TimedRotatingFileHandler**, bildet den Namen für die Backup Dateien jedoch anders. Der Timestamp (2021-04-06)
+wird nicht an das Ende des Dateinamens angefügt, sondern vor der Extenstion **.log** eingefügt:
+
+ smarthome-warnings.log --> smarthome-warnings.2021-04-06.log
+
+Dadurch bleibt die normale Extension des Dateinamens erhalten und die Dateien können z.B. durch Doppel-Klick
+in der GUI eines Betriebssystems geöffnet werden.
+
+Benutzung des Handlers
+----------------------
+
+In der Datei ``../etc/logging.yaml`` wird der Handler als Ersatz für **TimedRotatingFileHandler** eingesetzt.
+Dort wo im Abschnitt ``handler:`` bisher der Handler **TimedRotatingFileHandler** konfiguriert ist:
+
+.. code-block:: yaml
+
+ handler:
+ shng_warnings_file:
+ class: logging.handlers.TimedRotatingFileHandler
+ formatter: shng_simple
+ level: WARNING
+ utc: false
+ when: midnight
+ backupCount: 7
+ filename: ./var/log/smarthome-warnings.log
+ encoding: utf8
+
+wird anstelle des ``class:`` Attributes der Handler **ShngTimedRotatingFileHandler** folgendermaßen konfiguriert:
+
+.. code-block:: yaml
+
+ handler:
+ shng_warnings_file:
+ (): lib.log.ShngTimedRotatingFileHandler
+ formatter: shng_simple
+ level: WARNING
+ utc: false
+ when: midnight
+ backupCount: 7
+ filename: ./var/log/smarthome-warnings.log
+ encoding: utf8
+
+|
+
+ShngMemLogHandler
+=================
+
+In SmartHomeNG ist seit je her standardmäßig ein Memory Log konfiguriert. Dieses Memory Log ist bisher
+völlig losgelöst vom Python Logging System. Das memory Log trägt den Namen 'env.core.log' und wird dazu verwendet
+in der smartVISU auf der Konfigurationsseite unter 'SmartHomeNG' Log Einträge anzuzeigen. Das Memory Log
+'env.core.log' kann auch mit Hilfe des **cli** Plugins angezeigt werden.
+
+Weitere Memory Logs (die bisher mit dem **memlog** Plugin angelegt werden konnten/mussten) können in der smartVISU
+mit Hilfe der Widgets **status.log** und des **cli** Plugins angezeigt werden.
+
+Mit Hilfe des **ShngMemLogHandler** können nun normale Log Einträge, die über das Standard Python Logging erzeugt
+werden, in ein Memory Log geloggt werden. Der **ShngMemLogHandler** legt dazu ein entsprechendes Memory Log an
+und der Handler wird in den konfigurierten Loggern als (zusätzlicher) Logger konfiguriert.
+
+Benutzung des Handlers
+----------------------
+
+In der Datei ``../etc/logging.yaml`` wird der **ShngMemLogHandler** im Abschnitt ``handler:`` konfiguriert.
+
+.. code-block:: yaml
+
+ handler:
+ memory_heizung:
+ (): lib.log.ShngMemLogHandler
+ logname: mem_heiz
+ maxlen: 60
+ level: INFO
+
+
+**ShngMemLogHandler** hat zwei Parameter:
+
+ - ``logname:`` - Legt den Namen fest, unter dem das Memory Log aus der smartVISU oder dem **cli** Plugin
+ angesprochen werden kann.
+ - ``maxlen:`` - Legt fest, wie viele Einträge ein Memory Log aufnehmen kann, bevor der älteste Eintrag
+ gelöscht wird.
+ - ``level:`` - Legt den minmalen Log Level fest, der in das Memory Log geschrieben wird
+
+|
+
+Um den Handler im Python Logging zu nutzen, wird es anschließend der Handler zum gewünschten Logger hinzu
+gefügt.
+
+.. code-block:: yaml
+
+ loggers:
+ heizung:
+ handlers: [shng_heizung_file, memory_heizung]
+
diff --git a/doc/user/source/referenz/metadata/item_attribute_prefixes.rst b/doc/user/source/referenz/metadata/item_attribute_prefixes.rst
index dc41ca20dc..ecf1a2ebd9 100644
--- a/doc/user/source/referenz/metadata/item_attribute_prefixes.rst
+++ b/doc/user/source/referenz/metadata/item_attribute_prefixes.rst
@@ -5,8 +5,8 @@
.. index:: item_attribute_prefixes; Plugin Metadaten
.. index:: Plugin Metadaten; item_attribute_prefixes
-``item_attribute_prefixes``
----------------------------
+item_attribute_prefixes
+-------------------------
Falls ein Plugin eine Reihe von Items definiert, deren vollständiger Name erst zur Konfigurationszeit bekannt ist,
werden diese Items durch ihren Namens-Anfang (Präfix) definiert. Diese Art von Item Definition (und damit auch dieser
diff --git a/doc/user/source/referenz/metadata/module_global.rst b/doc/user/source/referenz/metadata/module_global.rst
index 6a21ffee8a..0051760d72 100644
--- a/doc/user/source/referenz/metadata/module_global.rst
+++ b/doc/user/source/referenz/metadata/module_global.rst
@@ -7,8 +7,8 @@
.. index:: Modul Metadaten; Globale Metadaten
-``module``
-----------
+module
+------
Der globale Metadaten Abschnitt ``module:`` kennt die folgenden Schlüsselbegriffe:
diff --git a/doc/user/source/referenz/metadata/plugin_global.rst b/doc/user/source/referenz/metadata/plugin_global.rst
index 1deab18732..4d92e51f2d 100644
--- a/doc/user/source/referenz/metadata/plugin_global.rst
+++ b/doc/user/source/referenz/metadata/plugin_global.rst
@@ -40,7 +40,16 @@ Der globale Metadaten Abschnitt ``plugin:`` kennt die folgenden Schlüsselbegrif
Beschreibung der Schlüsselbegriffe im Abschnitt ``plugin:``
- - ``type:`` Beschreibt den Typ des Plugins (gültige Werte: ``gateway``, ``interface``, ``protocol``, ``system``, ``cloud`` oder *leer* für ein nicht klassifiziertes Plugin
+ - ``type:`` Beschreibt den Typ des Plugins (gültige Werte: ``gateway``, ``interface``, ``protocol``, ``system``, ``web`` oder *leer* für ein nicht klassifiziertes Plugin.
+
+ Die Typen sind wie folgt definiert:
+
+ - keine Anbindung von Geräten/Diensten: **System-Plugin**
+ - Ansteuerung eines Gerätes je Plugin-Instanz: **Interface-Plugin**
+ - Ansteuerung mehrerer Geräte je Plugin-Instanz: **Gateway-Plugin**
+ - reine Unterstützung von Übertragungsprotokollen: **Protokoll-Plugin**
+ - Anbindung von Internet-Diensten: **Web-Plugin**
+
- ``description:`` Mehrsprachiger Text, der die Funktion das Plugins beschreibt. Die Beschreibung wird bei der
Generierung des Dokumentations-Seiten des Plugins verwendet - Die Texte in den verschiedenen Sprachen werden
als Unter-Einträge in der Form : erfasst. Zur Identifikation der Sprache werden die 2-stelligen
diff --git a/doc/user/source/referenz/module/module.rst b/doc/user/source/referenz/module/module.rst
index 453324c301..d170bc1b96 100644
--- a/doc/user/source/referenz/module/module.rst
+++ b/doc/user/source/referenz/module/module.rst
@@ -23,6 +23,7 @@ Bisher existieren die folgenden Module:
module_admin
module_mqtt
module_websocket
+ module_metadata
Die Konfiguration der Module ist im Abschnitt :doc:`Konfiguration ` dieser
diff --git a/doc/user/source/referenz/module/module_metadata.rst b/doc/user/source/referenz/module/module_metadata.rst
new file mode 100644
index 0000000000..b1820aa4b1
--- /dev/null
+++ b/doc/user/source/referenz/module/module_metadata.rst
@@ -0,0 +1,39 @@
+
+Metadaten für Module
+====================
+
+Module werden in der Datei ``../etc/module.yaml`` bzw. über die Admit GUI konfiguriert. Die Parameter sind in
+der Dokumentation des Moduls beschrieben.
+
+
+Ein Modul besteht im minimum aus zwei Dateien:
+
+- Der Modul Code: ``__init__.py``
+- Die Metadaten: ``module.yaml``
+
+Eine genaue Beschreibung welche weiteren Dateien und Unterverzeichnisse ein Modul haben kann, ist im Abschnitt
+:doc:`Entwicklung ` beschrieben.
+
+Alle Dateien sind in einem Verzeichnis unterhalb von ``../modules`` gespeichert, welches den Namen des
+Moduls trägt (nur in Kleinbuchstaben).
+
+
+Die **Metadaten** Datei eines Moduls heißt ``/modules//module.yaml``. Die bis zu sieben
+Abschnitte, die im folgenden beschrieben sind.
+
+- ``module:`` - Globale Metadaten des Moduls
+- ``parameters:`` - Definition der Parameter, welche zur Konfiguration des Moduls in der Datei ``../etc/module.yaml``
+ benutzt werden können
+
+|
+
+Für Module werden die folgenden Abschnitte in der Metadaten Datei ``module.yaml`` des jeweiligen Moduls genutzt:
+
+.. toctree::
+ :maxdepth: 4
+ :titlesonly:
+
+ /referenz/metadata/module_global
+ /referenz/metadata/parameters
+
+|
diff --git a/doc/user/source/referenz/plugins/plugin_metadata.rst b/doc/user/source/referenz/plugins/plugin_metadata.rst
index 83b30dd3ea..11a2c89e36 100644
--- a/doc/user/source/referenz/plugins/plugin_metadata.rst
+++ b/doc/user/source/referenz/plugins/plugin_metadata.rst
@@ -21,10 +21,9 @@ Plugins trägt (nur in Kleinbuchstaben).
Die **Metadaten** Datei eines Plugins heißt ``/plugins//plugin.yaml``. Die bis zu sieben
Abschnitte, die im folgenden beschrieben sind.
-additional sections:
- ``plugin:`` - Globale Metadaten des Plugins
-- ``parameters:`` - Definition der Parameter, welche zur Konfiguration des Pluginsin der Datei ``../etc/plugin.yaml``
+- ``parameters:`` - Definition der Parameter, welche zur Konfiguration des Plugins in der Datei ``../etc/plugin.yaml``
benutzt werden können
- ``item_attributes:`` - Definition der Item Attribute, die durch das Plugin genutzt/unterstützt werden
- ``item_structs:`` - Definition von Item Strukturen, welche im Zusammenhang mit dem Plugin genutzt werden können
@@ -34,18 +33,22 @@ additional sections:
- ``plugin_functions:`` - Beschreibung öffentlicher Funktionen des Plugins, die durch Logiken oder andere Plugins
genutzt werden können
+|
-.. include:: /referenz/metadata/plugin_global.rst
+Für Plugins werden die folgenden Abschnitte in der Metadaten Datei ``plugin.yaml`` des jeweiligen Plugins genutzt:
-.. include:: /referenz/metadata/parameters.rst
+.. toctree::
+ :maxdepth: 4
+ :titlesonly:
-.. include:: /referenz/metadata/item_attributes.rst
+ /referenz/metadata/plugin_global
+ /referenz/metadata/parameters
+ /referenz/metadata/item_attributes
+ /referenz/metadata/item_structs
+ /referenz/metadata/item_attribute_prefixes
+ /referenz/metadata/logic_parameters
+ /referenz/metadata/plugin_functions
-.. include:: /referenz/metadata/item_structs.rst
+|
-.. include:: /referenz/metadata/item_attribute_prefixes.rst
-
-.. include:: /referenz/metadata/logic_parameters.rst
-
-.. include:: /referenz/metadata/plugin_functions.rst
diff --git a/doc/user/source/referenz/referenz.rst b/doc/user/source/referenz/referenz.rst
index 88dbb5e81c..04595ce216 100644
--- a/doc/user/source/referenz/referenz.rst
+++ b/doc/user/source/referenz/referenz.rst
@@ -2,11 +2,12 @@
.. index:: Referenz
.. role:: bluesup
+.. role:: greensup
.. role:: redsup
-Referenz :redsup:`Neu`
-======================
+Referenz :greensup:`Update`
+===========================
Hier entsteht nach und nach eine Referenz in der Details zu einzelnenen Themen von SmartHomeNG nachgelesen werden
können.
@@ -22,4 +23,5 @@ können.
module/module
plugins/plugins
metadata/metadata
-
+ logging/logging
+ userfunctions/userfunctions
diff --git a/doc/user/source/referenz/userfunctions/userfunctions.rst b/doc/user/source/referenz/userfunctions/userfunctions.rst
new file mode 100644
index 0000000000..26dddcfbcd
--- /dev/null
+++ b/doc/user/source/referenz/userfunctions/userfunctions.rst
@@ -0,0 +1,251 @@
+
+.. role:: bluesup
+.. role:: greensup
+.. role:: redsup
+
+===========================
+Userfunctions :redsup:`Neu`
+===========================
+
+Ab Version 1.9 von SmartHomeNG ist die Möglichkeit implementiert, benutzerdefinierte Funktionen (Userfunctions) zu
+schreiben und in eval Statements sowie in Logiken zu verwenden.
+
+
+Erstellung und Speicherung
+==========================
+
+Die Python Dateien mit den Funktionen müssen dazu im Verzeichnis **../functions** abgelegt werden. Es sind normale
+Python Dateien, die mehrere Funktionen enthalten können. Als formale Anforderung sind nur Informationen zur Version
+und eine kurze Beschreibung des Zwecks der Funktionen dieser Datei anzugeben. Diese müssen in der Python Datei
+als globale Variablen **_VERSION** und **_DESCRIPTION** definiert werden.
+
+Im folgenden Beispiel wird eine Datei (Funktionssammlung) mit dem Namen **anhalter.py** im Verzeichnis **../functions**
+erzeugt:
+
+Die Python Datei sieht folgendermaßen aus:
+
+.. code-block:: python
+ :caption: /usr/local/smarthome/functions/anhalter.py
+
+ #!/usr/bin/env python3
+ # anhalter.py
+
+ _VERSION = '0.1.0'
+ _DESCRIPTION = 'Per Anhalter durch die Galaxis'
+
+ def zweiundvierzig():
+
+ return 'Die Antwort auf die Frage aller Fragen'
+
+
+Verwendung der Admin GUI
+------------------------
+
+In der Admin GUI steht ein Editor zum erstellen und bearbeiten von Userfunctions zur Verfügung. Dieser findet sich
+unter **Dienste/User-Funktionen**.
+
+.. image:: /referenz/assets/uf_editor1.jpg
+ :class: screenshot
+
+
+
+Initialisierung der Userfunctions
+=================================
+
+Wenn nach dem Erstellen dieser Datei SmartHomeNG neu gestartet wird, sieht man im Warnings-Log, dass die Datei
+importiert wird:
+
+.. code::
+
+ 2021-10-14 20:07:08 NOTICE lib.smarthome -------------------- Init SmartHomeNG 1.8.2d.b81166c3.develop --------------------
+ 2021-10-14 20:07:08 NOTICE lib.smarthome Running in Python interpreter 'v3.8.3 final' in virtual environment, from directory /usr/local/shng_dev
+ 2021-10-14 20:07:08 NOTICE lib.smarthome - on Linux-4.9.0-6-amd64-x86_64-with-glibc2.17 (pid=4584)
+ 2021-10-14 20:07:08 NOTICE lib.smarthome - Nutze Feiertage für Land 'DE', Provinz 'HH', 1 benutzerdefinierte(r) Feiertag(e) definiert
+ 2021-10-14 20:07:11 NOTICE lib.userfunctions Importing userfunctions uf.anhalter v0.1.0 - Per Anhalter durch die Galaxis
+ 2021-10-14 20:08:25 NOTICE lib.smarthome -------------------- SmartHomeNG initialization finished --------------------
+
+
+Nun kann man die in der Datei definierten Funktionen nutzen.
+
+Aufruf einer Userfunction
+=========================
+
+Allen Userfunctions ist beim Aufruf **uf.** (für userfunctions) gefolgt von dem Namen der Datei voranzustellen. Die
+Funktion **zweiundvierzig** ist also als ``uf.anhalter.zweiundvierzig()`` aufzurufen. In der Admin GUI im
+**eval Syntax Checker** sieht das denn folgendermaßen aus:
+
+.. image:: /referenz/assets/uf_eval_checker1.jpg
+ :class: screenshot
+
+Analog können die Funtionen in **eval** Attributen in Item Definitionen und in Logiken aufgerufen werden.
+
+
+Logging aus Userfunctions
+=========================
+
+Als Hilfestellung bei der Erstellung und dem Testen von Funktionen kann aus den Funktionen heraus geloggt werden.
+Dazu muss das Logging Modul importiert und ein Logger definiert werden:
+
+.. code-block:: python
+ :caption: /usr/local/smarthome/functions/anhalter.py
+
+ #!/usr/bin/env python3
+ # anhalter.py
+
+ import logging
+ _logger = logging.getLogger(__name__)
+
+ _VERSION = '0.1.0'
+ _DESCRIPTION = 'Per Anhalter durch die Galaxis'
+
+ def zweiundvierzig():
+
+ _logger.warning("Die Userfunction 'zweiundvierzig' wurde aufgerufen")
+ return 'Die Antwort auf die Frage aller Fragen'
+
+
+Der Name des Loggers im obigen Beispiel ist **functions.anhalter**:
+
+.. code::
+
+ 2021-10-15 10:08:14 NOTICE lib.smarthome -------------------- SmartHomeNG initialization finished --------------------
+ 2021-10-15 10:12:29 WARNING functions.anhalter Die Userfunction 'zweiundvierzig' wurde aufgerufen
+
+
+Damit aus Funktionen ein Logging mit Leveln kleiner als WARNING erfolgt, muss in der Logging Konfiguration
+../etc/logging.yaml ein entsprechender Logger für **functions** ergänzt werden:
+
+.. code:: yaml
+
+ loggers:
+ functions:
+ handlers: [shng_details_file]
+ level: INFO
+
+
+Nutzung von Item Werten
+=======================
+
+Für die Berechungen in den Userfunctions werden häufig die aktuellen Werte von Items benötigt. Um diese Werte in
+den Userfunctions zu erhalten, gibt es zwei Möglichkeiten.
+
+
+Übergabe als Parameter
+----------------------
+
+Damit die Funktionen unabhängig von der Item Struktur der aktuellen SmartHomeNG sind, sollten Item Werte als
+Parameter übergeben werden. Dadurch können Dateien mit Userfunctions einfach an andere Anwender weiter gegeben
+werden:
+
+.. code:: yaml
+
+ test_item:
+ type: num
+ eval_trigger: env.location.sun_position.elevation.degrees
+ eval: uf.lamellen_oeffnung_ost( sh.env.location.sun_position.elevation.degrees() )
+
+
+Die Userfunction dazu kann z.B. folgendermaßen aussehen:
+
+.. code-block:: python
+ :caption: /usr/local/smarthome/functions/beschattung.py
+
+ _VERSION = '0.1.0'
+ _DESCRIPTION = 'Hilfsfunktionen zur Beschattungssteuerung per Stateengine'
+
+ def lamellen_oeffnung_ost(elevation):
+ """
+ Bestimmung der Stellung der Ost Lamellen im Wohnbereich
+
+ :param elevation: Sonnen Position (Höhe in Grad)
+ :return: Schließung der Lamellen in Prozent
+ """
+
+ return 87 if elevation <= 6.6 else 84 if elevation <= 11.5 else 81 if elevation <= 14.8 else 78 if elevation <= 19.4 else 74 if elevation <= 16.1 else 70 if elevation <= 28 else 65 if elevation <= 30.9 else 60 if elevation <= 33.9 else 54
+
+
+durch das Smarthome-Objekt
+--------------------------
+
+Falls eine größere Zahl an Item Werten übergeben werden soll und eine Weitergabe an andere Anwender nicht geplant ist,
+kann die Userfunction so geschrieben werden, dass sie die Item Struktur kennt und voraussetzt.
+
+Statt mehrere Items als einzelne Parameter zu übergeben, braucht dann nur das Smarthome-Objekt übergeben zu werden.
+Das folgende Beispiel zeigt beide Varianten (übergabe der Item Werte und Referenzierung über das Smarthome-Objekt).
+
+.. code:: yaml
+
+ test_item:
+ # Übergabe der Item Werte
+ type: num
+ eval_trigger:
+ - env.location.sun_position.azimut.degrees
+ - env.location.sun_position.elevation.degrees
+ eval: uf.lamellen_oeffnung_sued( sh.env.location.sun_position.azimut.degrees(), sh.env.location.sun_position.elevation.degrees() )
+
+
+.. code-block:: python
+ :caption: /usr/local/smarthome/functions/beschattung.py
+
+ _VERSION = '0.2.0'
+ _DESCRIPTION = 'Hilfsfunktionen zur Beschattungssteuerung per Stateengine'
+
+ def lamellen_oeffnung_sued(azimut, elevation):
+ """
+ Bestimmung der Stellung der Ost Lamellen im Wohnbereich
+
+ :param azimut: Sonnen Position (Himmelsrichtung in Grad)
+ :param elevation: Sonnen Position (Höhe in Grad)
+ :return: Schließung der Lamellen in Prozent
+ """
+
+ return 60 if (azimut>=230 and elevation>0.0 ) else 63 if (azimut>=214 and elevation>0.0 )else 72 if elevation<=7.0 else 69 if elevation<=24.0 else 66
+
+
+kann das Smarthome-Objekt übergeben werden. Das würde dann folgendermaßen aussehen:
+
+.. code:: yaml
+
+ test_item:
+ # Übergabe des Smarthome-Objektes sh
+ type: num
+ eval_trigger:
+ - env.location.sun_position.azimut.degrees
+ - env.location.sun_position.elevation.degrees
+ eval: uf.lamellen_oeffnung_sued(sh)
+
+
+Die Userfunction dazu kann z.B. folgendermaßen aussehen:
+
+.. code-block:: python
+ :caption: /usr/local/smarthome/functions/beschattung.py
+
+ _VERSION = '0.2.1'
+ _DESCRIPTION = 'Hilfsfunktionen zur Beschattungssteuerung per Stateengine'
+
+ def lamellen_oeffnung_sued(sh):
+ """
+ Bestimmung der Stellung der Ost Lamellen im Wohnbereich
+
+ :param sh: smarthome objekt
+ :return: Schließung der Lamellen in Prozent
+ """
+
+ azimut = sh.env.location.sun_position.azimut.degrees()
+ elevation = sh.env.location.sun_position.elevation.degrees()
+
+ return 60 if (azimut>=230 and elevation>0.0 ) else 63 if (azimut>=214 and elevation>0.0 )else 72 if elevation<=7.0 else 69 if elevation<=24.0 else 66
+
+
+Reload von Userfunctions
+========================
+
+Benutzerdefinierte Funktionen können während der Laufzeit von SmartHomeNG verändert und neu geladen werden. Dabei
+können einzelne Module mit Userfunctions neu geladen werden oder alle Module mit Usewrfunctions auf einmal.
+
+In der Admin GUI erfolgt das auf der Seite mit dem Editor für Userfunctions. Alternativ können die Module über
+den **eval Syntax Checker** neu geladen werden. Um die Datei des obigen Beispiels neu zu laden, muss
+man **uf.reload('anhalter')** eingeben und **Prüfen** klicken.
+
+Man kann auch alle benutzerdefinierte Dateien neu laden, indem man **uf.reload_all()** eingibt und **Prüfen** klickt.
+
diff --git a/doc/user/source/release/1_8_2.rst b/doc/user/source/release/1_8_2.rst
index ba67ad5f3b..589544d67f 100644
--- a/doc/user/source/release/1_8_2.rst
+++ b/doc/user/source/release/1_8_2.rst
@@ -1,18 +1,9 @@
================================
-Release 1.8.2 - xx. Februar 2021
+Release 1.8.2 - 21. Februar 2021
================================
Dieses ist ein Bugfix Release für SmartHomeNG v1.8
-.. note::
-
- Diese Release Notes sind ein Arbeitsstand.
-
- - Berücksichtigt sind Commits im smarthome Repository bis incl. 20. Feb 2021
- (Merge pull request #394 from onkelandy/network...)
- - Berücksichtigt sind Commits im plugins Repository bis incl. 20. Feb 2021
- (shelly: Missing webif file added)
-
Überblick
=========
diff --git a/doc/user/source/release/1_9.rst b/doc/user/source/release/1_9.rst
new file mode 100644
index 0000000000..bf1a7fbe6d
--- /dev/null
+++ b/doc/user/source/release/1_9.rst
@@ -0,0 +1,660 @@
+===============================
+Release 1.9 - 28. Dezember 2021
+===============================
+
+Es gibt eine Menge neuer Features im Core von SmartHomeNG und den Plugins.
+
+.. comment note::
+
+ Diese Release Notes sind ein Arbeitsstand.
+
+ - Berücksichtigt sind Commits im smarthome Repository bis incl. 27. Dezember 2021
+ (...)
+ - Berücksichtigt sind Commits im plugins Repository bis incl. 28. Dezember 2021
+ (openweathermap: Added name of location to locals-struct)
+
+
+Überblick
+=========
+
+Dieses ist neues Release für SmartHomeNG. Die Änderungen gegenüber dem Release v1.8.x sind im
+folgenden in diesen Release Notes beschrieben.
+
+
+Unterstützte Python Versionen
+-----------------------------
+
+Die älteste offiziell unterstützte Python Version für SmartHomeNG Release 1.9 ist Python 3.7.
+(Siehe auch *Hard- u. Software Anforderungen* im Abschnitt *Installation* zu unterstützten Python Versionen)
+
+Das bedeutet nicht unbedingt, dass SmartHomeNG ab Release 1.9 nicht mehr unter älteren Python Versionen läuft,
+sondern das SmartHomeNG nicht mehr mit älteren Python Versionen getestet wird und das gemeldete Fehler mit älteren
+Python Versionen nicht mehr zu Buxfixen führen.
+
+
+Minimum Python Version
+^^^^^^^^^^^^^^^^^^^^^^
+
+Die absolute Minimum Python Version in der SmartHomeNG startet wurde auf v3.6 angehoben, da Python 3.5 im
+September 2020 End-of-Life (End of security fixes) gegangen ist. Bei einer Neuinstallation wird jedoch empfohlen
+auf einer der neueren Python Versionen (3.7 oder 3.8) aufzusetzen.
+
+
+Änderungen am Core
+==================
+
+Bugfixes in the CORE
+--------------------
+
+* Fixes in lib.network
+* Fixes in lib.utils
+
+* Fixes in modules.mqtt
+* Fixes in modules.websocket
+
+* modules.websocket: Bugfix for smartVISU payload protocol (command 'log')
+* create var/log directory prior recording output from pip
+
+
+Updates in the CORE
+-------------------
+
+* Removed references to lib.connection
+* etc.logging.yaml.default: Changes to new logging handlers
+* move crontab in lib.triggertimes, extend syntax for crontabs
+
+* Logics:
+
+ * Logic **check_items.py**: Check items for damaged items (created in logics)
+ It is not possible with Python to intercept an assignment to a variable or an objects' attribute. The only
+ thing one can do is search all items for a mismatching item type.
+
+It is not possible with Python to intercept an assignment to a variable or an
+objects' attribute. The only thing one can do is search all items for a
+mismatching item type.
+
+* lib.backup:
+
+ * Added new struct files of ../etc directory to configuration backup
+ * Added \*.pem to backup of certificate files
+ * Certificate backup now backs up \*.pem files for certificates that are not named \*.cer
+
+* lib.env.location:
+
+ * Added lat, lon and elev settings from smarthome.yaml to items
+
+* lib.item:
+
+ * Added loading of structs from multiple files (etc/struct_xyz.yaml) in addition to loading from etc/struct.yaml
+ * Extended functionallity for item logging (incl. shngadmin and documentation)
+ * Added attribute source to timer function
+ * Improved logging for items with cache attribute
+ * items: optionally return items sorted
+ * Bugfix for autotimer method of an item
+
+* lib.log:
+
+ * Improved handling of loglevel NOTICE
+
+* lib.metadata
+
+ * bugfix in version checking
+
+* lib.network:
+
+ * first udp server implementation
+ * Removed setting of loglevel for logger lib.network (should be defined in etc/logging.yaml)
+ * Handle 'broken pipe' error on remote disconnect
+ * added log entry for truncated send
+ * Fix received data processing
+ * Fix missing bytes/str conversion
+ * Better and faster shutdown handling
+ * Fix callback syntax
+ * Exception handling for callbacks
+
+* lib.scene:
+
+ * Extended eval to use shtime, userfunctions and math (analog to eval attribute of items)
+ * Implemented reload of all scenes
+ * implemented multi language support for log entries
+
+* lib.smarthome:
+
+ * Added loglevel NOTICE
+ * Improved handling of memory logs
+
+* lib.tools:
+
+ * Fix for daylight saving time in tools.dt2ts() and tools.dt2js()
+
+* lib.userfunctions
+
+ * New library, that implements userfunctions for eval-statements and logics
+ * Implemented userfunctions for evalchecker in admin gui
+
+* Modules:
+
+ * admin:
+
+ * Display of structs in shngadmin is now sorted and grouped by plugin
+ * Randomized calls to find blog articles on smarthomeng.de
+ * Added level NOTICE to api
+ * GUI: Added loglevel NOTICE
+ * GUI Added reload button for scenes
+ * Implemented html escape for dicts and lists in item detail view
+ * Bugfix for list loggers (Issue #411) "dictionary changed size during iteration"
+ * GUI: Fix for handling/editing custom holidays
+ * Fix for compatibility to newer PyJWT versions
+ * Added support for user functions
+ * Added button to reload scenes
+ * Added shngadmin version to system property page
+
+ * http:
+
+ * update chartjs to 2.9.4
+ * added Datatables Javascript v1.11.0 to allow table sorting in WebIFs, updated documentation
+ * updated bootstrap to 4.6.0
+ * updated bootstrap datepicker to 1.9.0
+ * updated Font Awesome to 5.15.4
+ * updated jquery to 3.6.0
+ * updated popper.js to 2.10.1
+
+ * websocket:
+
+ * Changes to memory logging in core
+ * Added missing requirements.txt
+ * Exitcode 1001 is now logged as info, not as exception
+
+* Plugins:
+
+ * ...
+
+* tests:
+
+ * mock.core: Read core version from bin.shngversion.py
+ * migrated tests to Travis-CI.com, updated Readme
+
+
+Änderungen bei Plugins
+======================
+
+New Plugins
+-----------
+
+For details of the changes of the individual plugins, please refer to the documentation of the respective plugin.
+
+* avm_smarthome: AVM smarthome plugin for DECT sockes, smart radiator control DECT301 and Comet DECT and DECT
+ smarthome sensors based on HTTP GET Request
+* homeconnect: usage of the BSH/Siemens HomeConnect interface with oauth2
+* husky: plugin to control Husqvarna automower
+* modbus_tcp: New plugin to read registers from modbusTcp-device
+* philips_tv: Added initial support for Philips TV with OAuth2 authentication
+* sma_mb: this plug-in reads the current values of an SMA inverter via SMA Speedwire fieldbus/Modbus
+* text_display: New text display Plugin
+* timmy: Plugin für Ein-/Ausschaltverzögerung und Blinken
+
+
+Plugin Updates
+--------------
+
+* asterix:
+
+ * adjusted plugin to lib.network
+
+* avm:
+
+ * handle callmonitor reconnect
+ * avoid error message on requested shutdown
+ * moved webif to seperate file
+ * fixed rare error in function _update_home_automation
+ * catching exceptions when Ethernet is temporary unavailable
+
+* bose_soundtouch:
+
+ * Improved error handling
+
+* bsblan:
+
+ * revised README
+ * compatibility check for BSB-LAN Version 2.x
+ * adjusted link to icon in readme.md
+
+* casambi:
+
+ * Catch socket errors leading to unintentional termination of EventHandlerThread
+ * deleted readme and improved user_doc
+ * added automatic sessionID request, e.g. after Casambi API key validity has been extended
+ * improved webinterface
+ * added english translation for webinterface
+ * added python websocket to plugin requirements
+ * fixed requirement websocket-client
+ * added tunable white (CCT) support
+ * added extended debugging for CCT commands
+ * debugging setups with more than one Casambi network
+ * removed unjustified error/warning messages
+ * fixed status decode error
+ * added backend online status parsing to item
+ * fixed unknown variable error in debug message
+ * Trigger socket reinitialization after pipe error
+ * Switched logger outputs to f-strings
+
+* cli:
+
+ * adjustments to new network classes
+ * fixed error - self.alive
+ * added 'logl' (log-list) command
+ * updated output of command 'logd'
+ * updated to conform with changes to memory logging in core
+
+* comfoair:
+
+ * removed lib.connection references for cleanup
+
+* darksky:
+
+ * added URL for data retrieval to webif
+ * switched default to "ca" to have wind in kmh
+ * added some more attributes to webif
+ * set to deprecated for next plugin release, API ends 2021
+
+* database:
+
+ * updated to use newest version of datepicker
+ * Improved robustness, limit reconnects improved plugin robustness, if db is not available (e.g. temporarily missing ethernet)
+ * Limit number of reconnects
+ * Fixed bug in item_detail page
+ * added Datatable to overview and details
+
+* dlms:
+
+ * added parameter to allow listen only mode
+ * extend webinterface with list of common obis codes
+ * allow crontab timings, enhance listen only smartmeter handling, improve getting manufacturer list
+
+* ebus:
+
+ * removed lib.connection references for cleanup
+
+* ecmd:
+
+ * removed lib.connection references for cleanup
+
+* enocean:
+
+ * added debug infos for powermeter devices
+ * changed to new is_alive() syntax for python 3.9
+ * updated to use newest version of datepicker
+ * removed datepicker includes, which are no longer necessary for this plugin
+ * Added debug info to BaseID error message
+ * Adapted logging to fstrings
+ * Added optional item attribute "enocean_device" to select appropriate learn message
+
+* garminconnect:
+
+ * Updated to use newest version of datepicker
+
+* gpio:
+
+ * fix local variable 'err' referenced before assignment in line 126
+ * implement datatables JS in webif
+ * rename webif tables correctly
+ * improve error handling on startup and bump version to 1.5.1
+
+* hue:
+
+ * Small BugFix in UpdateGoupItems
+
+* hue2:
+
+ * Changed create_new_username() to support qhue v2.0.0 and up
+ * Implemented bridge discovery via mdns (for bridges v2)
+ * Reimplemented bridge discovery via upnp (for bridges v1)
+ * Removed bridge discovery through hue portal (old Philips site)
+ * Implemented new Signify broker discovery methods
+ * automatic discovery at startup takes place only if stored ip address does not point to a hue bridge
+
+* husky:
+
+ * added error/debug message if model, id or name cannot be extracted from json response
+ * added logger to Mower class
+ * degraded error message on missing model type to debug level
+
+* ical:
+
+ * adapted to new lib.network
+ * made cycle to a class attribute (self._cycle)
+
+* jsonread:
+
+ * now has a webinterface
+ * some minor text changes to metadata (plugin.yaml)
+ * remove old readme.md
+ * corrected plugin.yaml (it was not a valid yaml file any more)
+
+* knx:
+
+ * adjusted plugin to lib.network
+ * added DPT 251.600 RGBW
+ * fix webinterface fix mixup
+ * add password for knxproj to webif, introduce knxd namespace for constants, update doku
+ * removed local redundant datatables
+
+* kodi:
+
+ * make favourites type dict instead of str
+
+* mailrcv: catch exception when trying to close imap even if it's not possible
+
+* memlog:
+
+ * updated to conform with changes to memory logging in core
+
+* mpd:
+
+ * adjusted plugin to lib.network
+
+* mqtt:
+
+ * updated to use newest version of datepicker
+
+* neato:
+
+ * added new function start_robot to enable single room cleaning; added new function get_map_boundaries to request
+ available map boundaries (rooms) for a given map; added new function dismiss_current_alert to reset current alerts
+ * fix for clean_room command
+ * bugfix in metadata (plugin-function definition hat indentation error)
+ * added option to clear errors/alarms in neato/vorwerk backend via plugin's webif
+ * added english translation for webinterface
+ * deactivate SSL verify
+ * added return values for plugin commands
+ * added function list available rooms to plugin webif
+ * improved map cleaning control
+ * Added return values for plugin commands; added function list available rooms to plugin webif. Improved map cleaning control
+ * Added 'robot not online' warning
+ * Added command to dismiss backend alerts (dustbin full etc.) via item
+ * Modifications by ivan73 (without desciption)
+
+* network:
+
+ * adapted plugin to lib.network
+ * improved plugin parameter handling
+ * fixed starting server only on run()
+ * adjusted logging
+
+* nuki:
+
+ * fixed get_local_ipv4_address handling
+
+* nut:
+
+ * catching exception if network is not available
+ * added UPS via Synology disk station example to readme
+ * fixed error occurring after exception of type "network not available"
+
+* odlinfo:
+
+ * Updated to new data interface https://odlinfo.bfs.de/ODL/DE/service/datenschnittstelle/datenschnittstelle_node.html
+ * No more use and password needed
+ * Added web interface
+ * Added cycle and cached json data
+ * Added manual update option, reduced default cycle to 1800 sec
+ * Bumped version to 1.5.1
+ * Added auto update for items
+ * Modifications by ivan73 (without desciption) -> 1.5.2
+
+
+* onewire:
+
+ * improve error handling
+ * enhanced tree function in owbase
+
+* openweathermap:
+
+ * corrected user_doc (replaced all references to darksky plugin)
+ * multiple changes, bumped version to 1.8.2
+ * Fixed bug in metadaa (plugin.yaml)
+ * Removed extra line with API-key which displayed only asterixes from web interface
+ * Added name of location to locals-struct
+ * Bumped version to 1.8.3
+
+* raumfeld
+
+ * removed lib.connection references for cleanup
+ * some cleanup
+
+* raumfeld_ng:
+
+ * Bugfix in poll_device (get_sh())
+ * Added get_mediainfo to valid_list of rf_attr item attribute
+
+* resol:
+
+ * Catch wrong message sizes
+ * Fixed scheduler stop on plugin exit
+ * Robustness measures when Ethernet is temporary not available
+ * Added socket shutdown on plugin stop
+ * Plugin performance: Do not register receive only attributes for update_item function
+ * Modifications by ivan73 (without desciption)
+
+* robonect:
+
+ * corrected datatype for unix timestamp error_unix
+ * extended by some MQTT commands
+ * changed indent of mode item
+ * changed "and not" to "or"
+ * added keychecks to avoid exceptions
+ * added buttons in webif to switch modes
+ * don't try to iterate error list in case robonect has no wifi connection (error list is None then)
+ * caching full error list
+ * added mode to webservices set for automower (helps only, if webservices plugin is used)
+ * added check for mqtt mode
+
+* rpi1wire:
+
+ * Updated user docu, webif and Code cleanup
+ * Corrected errors in structure of user documentation
+
+* russound:
+
+ * adjusted plugin to lib.network
+
+* shelly:
+
+ * add support for Shelly H&T
+ * Some updates
+ * bumped version to 1.2.0
+
+* simulation:
+
+ * fix parameters
+
+* smartvisu:
+
+ * added parameter create_masteritem_file
+ * adjusted web interface
+ * improve descriptions for widget names and blocks
+
+* sml:
+
+ * removed lib.connection references for cleanup
+
+* smlx:
+
+ * changed from readme to user_doc docu, provide a requirements.txt
+ * removed lib.connection references for cleanup
+
+* snmp:
+
+ * functional update of plugin incl enhancement of WebIF
+
+* sonos:
+
+ * added plugin webinterface
+ * added name for SoCo EventServerThread
+ * catching rare exception that could occur during automatic IP detection and invalid network connectivity
+ * adapted behavior of play_snippet if stop() functionality is currently not supported by the respective speaker
+ * upgrade to SoCo 0.22 framework
+ * display number of online speakers on Webinterface
+ * pgrade SoCo base framework to Version 0.24.0; additional robustness improvements
+
+* speech:
+
+ * adjusted plugin to lib.network
+
+* squeezebox:
+
+ * change struct wipecache to str as the value might also be a string like "queue"
+
+* stateengine:
+
+ * moved web interface to a separate file
+ * change logging: general log is plugins.stateengine and se_item logs are logged to "stateengine" (without plugins. prefix)
+ * improve log handling
+ * handle problem when SE item has name, bump version to 1.9.2
+ * improve logging and source for item update
+ * fix docu example for south and se_use
+ * lower case log directory
+ * optional offset for sun_tracking function
+ * moved webif to external file
+ * new logger names, fix items having a name
+ * corrected intentation in user_doc/13_sonstiges.rst
+ * add offset and value for open lamella value parameters to improve sun_tracking function
+ * replace sh.tools.dt2ts() by timestamp() for evaluating the start_time of the suspend state
+ * change web visu - condition rectangle now has dynamic width
+ * better sun_tracking offset handling
+ * correct webif colors and conditionlist if no conditionsets given
+
+* tasmota:
+
+ * Functional Update of Tasmota Plugin incl WebIF Rework
+
+* telegram:
+
+ * add new attribut telegram_condition to suppress multiple messages upon update
+ * Add possibility to send telegram message zu just 1 chat-id
+ * Add chat-if to "telegram-info" to allow response depending on chat-id
+ * Updated user docu, webif and code cleanup
+
+* unifi:
+
+ * moved dependency from lib.network to lib.utils
+
+* uzsu:
+
+ * outsource webif and fix webinterface problem with showing the whole dictionary when a rule contains a "<"
+ * update webif to use datatables JS
+ * Update req. for python 3.7 and 3.9
+ * Minimize dict item renewal: lastvalue not written to dict anymore, fix bug in sun calculated values
+ * Remove lastvalue from dict on start as it is not used anymore
+ * Fix webIF overlay when clicking on entry
+ * Improve last value struct and handling
+ * xtensions for series - second try
+ * Sun calculation cron is now adjustable in plugin settings
+ * Modifications by ivan73 (without desciption)
+
+* viessmann:
+
+ * fixes webif includes
+ * fix cyclic due calculation
+
+* visu_websocket:
+
+ * updated to conform with changes to memory logging in core
+ * fix parameters in widget call
+
+* webservices:
+
+ * moveed and translated readme.md documentation to user_doc.rst
+ * remove readme.md, create user_doc.rst, use sphinx-tabs
+
+* withings_health:
+
+ * updated to newest version of withings-api
+ * moved webif to seperate file
+
+* wol:
+
+ * now has a web interface with items and interactive wol
+ * Corrected metadata - changed type of wol_ip from ip4 to ipv4
+
+* xiaomi_vac:
+
+ * use datatables js in webif
+ * fix problem with newer miio module (>=0.5.8) that doesn't accept return_list argument for clean_details method
+ * Bump version to 1.1.2
+ * ompatibility with newer python-miio modules (0.5.9+)
+ * Bump version to 1.2.0
+
+* xmpp:
+
+ * Try to reconnect when loosing connection
+
+
+Outdated Plugins
+----------------
+
+The following plugins were already marked in version v1.6 as *deprecated*. This means that the plugins
+are still working, but are not developed further anymore and are removed from the release of SmartHomeNG
+in the next release. User of these plugins should switch to corresponding succeeding plugins.
+
+* System Plugins
+
+ * backend - use the administration interface instead
+ * sqlite_visu2_8 - switch to the **database** plugin
+
+* Web Plugins
+
+ * wunderground - the free API is not provided anymore by Wunderground
+
+
+The following plugins are marked as *deprecated* with SmartHomeNG v1.7, because neither user nor tester have been found:
+
+* Gateway Plugins
+
+ * ecmd
+ * elro
+ * iaqstick
+ * snom
+ * tellstick
+
+* Interface Plugins
+
+ * easymeter
+ * smawb
+ * vr100
+
+* Web Plugins
+
+ * nma
+
+Moreover, the previous mqtt plugin was renamed to mqtt1 and marked as *deprecated*, because the new mqtt
+plugin takes over the functionality. This plugin is based on the mqtt module and the recent core.
+
+
+Retired Plugins
+---------------
+
+The following plugins have been retired. They had been deprecated in one of the preceding releases of SmartHomeNG.
+They have been removed from the plugins repository, but they can still be found on github. Now they reside in
+the **plugin_archive** repository from where they can be downloaded if they are still needed.
+
+* alexa - switch to the **alexa4p3** plugin
+* boxcar - classic Plugin, not used according to survey in knx-user-forum
+* mail - switch to the **mailsend** and **mailrcv** plugin
+* netio230b - classic plugin, not used according to survey in knx-user-forum
+* openenergymonitor - classic plugin, not used according to survey in knx-user-forum
+* smawb - classic plugin, not used according to survey in knx-user-forum
+* sqlite - switch to the **database** plugin
+* tellstick - classic Plugin, not used according to survey in knx-user-forum
+
+
+Weitere Änderungen
+==================
+
+
+Documentation
+-------------
+
+* Changed Requirements for documentation build, added tab extension to sphinx, introduced MyST
+* Documentation build should now run under Windows
+
diff --git a/doc/user/source/was_ist_neu.rst b/doc/user/source/was_ist_neu.rst
new file mode 100644
index 0000000000..660eee0dc6
--- /dev/null
+++ b/doc/user/source/was_ist_neu.rst
@@ -0,0 +1,28 @@
+:tocdepth: 1
+
+Neuerungen im Release v1.9
+==========================
+
+Hier ist eine Kurzübersicht über größere Neuerungen im aktuellen Release.
+Eine vollständige Übersicht der Änderungen ist den den :doc:`Release Notes ` zu finden.
+
+ - **Structs**: Es sind mehrere struct Definitionsdateien möglich
+ - **Item Logging**: Funktionalität stark erweitert (kann das operationslog Plugin ersetzen)
+ - **Logging**: Es gibt einen Handler, der beim rotieren der Log Dateien die File-Extension erhält
+ - **Logging**: Es gibt einen Handler, der beim Logging den Zugriff auf die memory Logs von SmartHomeNG erlaubt
+ - **Userfunctions**: Es können Python Funktionen definiert werden, die in eval Statements und Logiken verwendet
+ werden können
+ - **Szenen**: Die Szenen Definitionsdateien können neu geladen werden, ohne SmartHomeNG neu starten zu müssen
+
+Details zu den genannten Punkten sind in den Abschnitten :doc:`Konfiguration `
+bzw. :doc:`Referenz ` zu finden.
+
+|
+
+Auch bei den Plugins hat es größere Änderungen gegeben:
+
+ - **diverse Plugins**: Umstellung der Plugins die bisher lib.connection nutzten auf lib.network
+
+|
+
+(Diese Seite muss vom Layout noch überarbeitet werden)
diff --git a/etc/logging.yaml.default b/etc/logging.yaml.default
index c1eb0f2cd0..8c62130600 100644
--- a/etc/logging.yaml.default
+++ b/etc/logging.yaml.default
@@ -59,7 +59,8 @@ handlers:
# The TimedRotatingFileHandler seperates the logentries by day and
# keeps the entries of the last seven days in seperate files.
#
- class: logging.handlers.TimedRotatingFileHandler
+ #class: logging.handlers.TimedRotatingFileHandler
+ (): lib.log.ShngTimedRotatingFileHandler
formatter: shng_simple
level: WARNING
utc: false
@@ -75,7 +76,7 @@ handlers:
# The TimedRotatingFileHandler seperates the logentries by day and
# keeps the entries of the last seven days in seperate files.
#
- class: logging.handlers.TimedRotatingFileHandler
+ (): lib.log.ShngTimedRotatingFileHandler
formatter: shng_simple
level: DEBUG
utc: false
@@ -93,7 +94,7 @@ handlers:
# # The TimedRotatingFileHandler seperates the logentries by day and
# # keeps the entries of the last seven days in seperate files.
# #
- # class: logging.handlers.TimedRotatingFileHandler
+ # (): lib.log.ShngTimedRotatingFileHandler
# formatter: shng_detail
# level: DEBUG
# utc: false
@@ -106,7 +107,7 @@ handlers:
#shng_busmonitor_file:
# # This handler must be enabled when busmonitor logging from the knx plugin should be used.
# #
- # class: logging.handlers.TimedRotatingFileHandler
+ # (): lib.log.ShngTimedRotatingFileHandler
# formatter: shng_busmonitor
# level: DEBUG
# when: midnight
@@ -117,7 +118,7 @@ handlers:
#shng_items_file:
# # This handler is an example for logging item-value changes to a seperate log file
# #
- # class: logging.handlers.TimedRotatingFileHandler
+ # (): lib.log.ShngTimedRotatingFileHandler
# formatter: shng_items
# when: midnight
# backupCount: 7
@@ -133,21 +134,25 @@ loggers:
# The following default loggers should not be changed. If additional logging
# is required, a logger for the specific lib, module or plugin shoud be added.
#
+ functions:
+ handlers: [shng_details_file]
+ level: INFO
+
lib:
# Default logger for SmartHomeNG libraries
handlers: [shng_details_file]
level: WARNING
- lib.smarthome.main:
+ lib.smarthome:
# Add all logging handlers that should receive the initial log lines after a startup
# (example below) but leave out the logging handlers that are defined in the root-logger
# (otherwise log entries will be doubled).
#
- # 2020-12-29 11:35:34 WARNING lib.smarthome.main -------------------- Init SmartHomeNG 1.8.0 --------------------
- # 2020-12-29 11:35:34 WARNING lib.smarthome.main Running in Python interpreter 'v3.8.3 final' in virtual environment
- # 2020-12-29 11:35:34 WARNING lib.smarthome.main - on Linux-4.9.0-6-amd64-x86_64-with-glibc2.17 (pid=24407)
- # 2020-12-29 11:35:35 WARNING lib.smarthome.main - Nutze Feiertage für Land 'DE', Provinz 'HH', 1 benutzerdefinierte(r) Feiertag(e) definiert
- # 2020-12-29 11:36:54 WARNING lib.smarthome.main -------------------- SmartHomeNG initialization finished --------------------
+ # 2020-12-29 11:35:34 WARNING lib.smarthome -------------------- Init SmartHomeNG 1.8.0 --------------------
+ # 2020-12-29 11:35:34 WARNING lib.smarthome Running in Python interpreter 'v3.8.3 final' in virtual environment
+ # 2020-12-29 11:35:34 WARNING lib.smarthome - on Linux-4.9.0-6-amd64-x86_64-with-glibc2.17 (pid=24407)
+ # 2020-12-29 11:35:35 WARNING lib.smarthome - Nutze Feiertage für Land 'DE', Provinz 'HH', 1 benutzerdefinierte(r) Feiertag(e) definiert
+ # 2020-12-29 11:36:54 WARNING lib.smarthome -------------------- SmartHomeNG initialization finished --------------------
#
# logging to shng_details_file is already enabled in logger lib:
#handlers: [shng_develop_file]
diff --git a/functions/.gitignore b/functions/.gitignore
new file mode 100644
index 0000000000..5b5726d9f4
--- /dev/null
+++ b/functions/.gitignore
@@ -0,0 +1,5 @@
+# ignore everything
+*
+# except .gitignore and template file
+!.gitignore
+!uf.tpl
diff --git a/functions/uf.tpl b/functions/uf.tpl
new file mode 100644
index 0000000000..dd523d0aad
--- /dev/null
+++ b/functions/uf.tpl
@@ -0,0 +1,25 @@
+#
+# This file contains user defined functions for use with SmartHomeNG
+#
+import logging
+_logger = logging.getLogger(__name__)
+
+_VERSION = '0.1.0'
+_DESCRIPTION = 'Per Anhalter durch die Galaxis'
+
+#
+# Example functions
+#
+def zweiundvierzig():
+
+ return 'Die Antwort auf die Frage aller Fragen'
+
+def itemtest(sh):
+
+ return sh.env.location.sun_position.elevation.degrees()
+
+def log_test():
+
+ _logger.warning('Log-Test aus einer Userfunction')
+
+ return
diff --git a/lib/aioudp.py b/lib/aioudp.py
new file mode 100644
index 0000000000..3eaa3829df
--- /dev/null
+++ b/lib/aioudp.py
@@ -0,0 +1,98 @@
+#!/usr/bin/env python3
+# vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab
+#########################################################################
+# Based on aioudp by bashkirtsevich: https://github.com/bashkirtsevich-llc/aioudp
+# Copyright 2020- Sebastian Helms
+#########################################################################
+# This file is part of SmartHomeNG
+#
+# SmartHomeNG is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# SmartHomeNG is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with SmartHomeNG If not, see .
+#########################################################################
+
+
+import asyncio
+import socket
+from collections import deque
+
+
+class aioUDPServer():
+ def __init__(self):
+ self._recv_max_size = 4096
+
+ self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 0)
+ self._sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ self._sock.setblocking(False)
+
+ self._send_event = asyncio.Event()
+ self._send_queue = deque()
+
+ self._subscribers = {}
+ self._task = None
+
+ # region Interface
+ def run(self, host, port, loop):
+ self.loop = loop
+ self._sock.bind((host, port))
+ self._run_future(self._recv_periodically())
+
+ def stop(self):
+ self._sock.close()
+ self._subscribers = {}
+
+ def subscribe(self, fut):
+ self._subscribers[id(fut)] = fut
+
+ def unsubscribe(self, fut):
+ self._subscribers.pop(id(fut), None)
+
+ # endregion
+
+ def _run_future(self, *args):
+ for fut in args:
+ asyncio.ensure_future(fut, loop=self.loop)
+
+ def _sock_recv(self, fut=None, registered=False):
+ fd = self._sock.fileno()
+
+ if fut is None:
+ fut = self.loop.create_future()
+
+ if registered:
+ self.loop.remove_reader(fd)
+
+ try:
+ data, addr = self._sock.recvfrom(self._recv_max_size)
+ except (BlockingIOError, InterruptedError):
+ self.loop.add_reader(fd, self._sock_recv, fut, True)
+ except Exception as e:
+ fut.set_result(0)
+ self._socket_error(e)
+ else:
+ fut.set_result((data, addr))
+
+ return fut
+
+ async def _recv_periodically(self):
+ while True:
+ data, addr = await self._sock_recv()
+ self._notify_subscribers(*self._datagram_received(data, addr))
+
+ def _socket_error(self, e):
+ pass
+
+ def _datagram_received(self, data, addr):
+ return data, addr
+
+ def _notify_subscribers(self, data, addr):
+ self._run_future(*(fut(data, addr) for fut in self._subscribers.values()))
\ No newline at end of file
diff --git a/lib/backup.py b/lib/backup.py
index b26c1265ab..25f3a027ab 100644
--- a/lib/backup.py
+++ b/lib/backup.py
@@ -24,6 +24,7 @@
"""
import copy
+import glob
import logging
import zipfile
import shutil
@@ -106,6 +107,7 @@ def create_backup(conf_base_dir, base_dir, filename_with_timestamp=False, before
items_dir = os.path.join(conf_base_dir, 'items')
logic_dir = os.path.join(conf_base_dir, 'logics')
scenes_dir = os.path.join(conf_base_dir, 'scenes')
+ uf_dir = os.path.join(conf_base_dir, 'functions')
# create new zip file
@@ -123,10 +125,17 @@ def create_backup(conf_base_dir, base_dir, filename_with_timestamp=False, before
backup_file(backupzip, source_dir, arc_dir, 'module.yaml')
backup_file(backupzip, source_dir, arc_dir, 'plugin.yaml')
backup_file(backupzip, source_dir, arc_dir, 'smarthome.yaml')
+
backup_file(backupzip, source_dir, arc_dir, 'struct.yaml')
+ struct_files = glob.glob(os.path.join( etc_dir, 'struct_*.yaml'))
+ for pn in struct_files:
+ fn = os.path.split(pn)[1]
+ backup_file(backupzip, source_dir, arc_dir, fn)
# backup certificate files from /etc
backup_directory(backupzip, etc_dir, '.cer')
+ backup_directory(backupzip, etc_dir, '.pem')
+
backup_directory(backupzip, etc_dir, '.key')
# backup files from /items
@@ -139,13 +148,17 @@ def create_backup(conf_base_dir, base_dir, filename_with_timestamp=False, before
# backup files from /scenes
#logger.warning("- scenes_dir = {}".format(scenes_dir))
- backup_directory(backupzip, scenes_dir)
+ backup_directory(backupzip, scenes_dir, '.yaml')
+ backup_directory(backupzip, scenes_dir, '.conf')
+
+ # backup files from /functions
+ #logger.warning("- uf_dir = {}".format(uf_dir))
+ backup_directory(backupzip, uf_dir, '.*')
zipped_files = backupzip.namelist()
logger.info("Zipped files: {}".format(zipped_files))
backupzip.close()
-
#logger.warning("- backup_dir = {}".format(backup_dir))
shtime = Shtime.get_instance()
@@ -211,7 +224,7 @@ def backup_directory(backupzip, source_dir, extenstion='.yaml'):
arc_dir = dir + os.path.sep
files = []
for filename in os.listdir(source_dir):
- if filename.endswith(extenstion):
+ if filename.endswith(extenstion) or extenstion == '.*':
backup_file(backupzip, source_dir, arc_dir, filename)
return
@@ -240,6 +253,7 @@ def restore_backup(conf_base_dir, base_dir):
items_dir = os.path.join(conf_base_dir, 'items')
logic_dir = os.path.join(conf_base_dir, 'logics')
scenes_dir = os.path.join(conf_base_dir, 'scenes')
+ uf_dir = os.path.join(conf_base_dir, 'functions')
archive_file = ''
for filename in os.listdir(restore_dir):
@@ -278,6 +292,9 @@ def restore_backup(conf_base_dir, base_dir):
# backup files from /scenes
restore_directory(restorezip, 'scenes', scenes_dir, overwrite)
+ # backup files from /scenes
+ restore_directory(restorezip, 'functions', uf_dir, overwrite)
+
# mark zip-file as restored
os.rename(restorezip_filename, restorezip_filename + '.done')
diff --git a/lib/connection.py b/lib/connection.py
index d291decf0c..d737679b02 100644
--- a/lib/connection.py
+++ b/lib/connection.py
@@ -21,13 +21,14 @@
"""
-This library is softly on it's way out. In the future network classes for SmartHomeNG
-will be implemented trough the network library lib.network, which is still in development.
+This library is on its way out. Network classes for SmartHomeNG are provided by
+lib.network. Creating lib.connection Server and Client class object will
+create an appropriate WARNING log entry.
-The following modules use an import lib.connection as of April 2018:
+The following modules use an import lib.connection as of December 2021:
smarthome.py for an object of Connections()
Plugins:
-russound, network, visu_websocket, asterisk, knx, squeezebox, nuki, mpd, raumfeld, cli, speech, xbmc, lirc
+visu_websocket
"""
import logging
@@ -54,6 +55,7 @@ class Stream() and thus also to class Client() which inherits from Stream()
_family = {'UDP': socket.AF_INET, 'UDP6': socket.AF_INET6, 'TCP': socket.AF_INET, 'TCP6': socket.AF_INET6}
_type = {'UDP': socket.SOCK_DGRAM, 'UDP6': socket.SOCK_DGRAM, 'TCP': socket.SOCK_STREAM, 'TCP6': socket.SOCK_STREAM}
_monitor = []
+ _deprecated_wanings = True
def __init__(self, monitor=False):
self._name = self.__class__.__name__
@@ -65,6 +67,53 @@ def _create_socket(self, flags=None):
self.socket = socket.socket(family, type, proto)
return sockaddr
+ def _deprecated_warning(self, n_func=''):
+ """
+ Display function deprecated warning
+ """
+ if hasattr(self, '_deprecated_warnings'):
+ if lib.utils.Utils.to_bool(self._deprecated_warnings) == False:
+ return
+ else:
+ return # if parameter is not defined
+
+ d_func = 'sh.'+str(sys._getframe(1).f_code.co_name)+'()'
+ if n_func != '':
+ n_func = '- use the '+n_func+' instead'
+ try:
+ d_test = ' (' + str(sys._getframe(2).f_locals['self'].__module__) + ')'
+ except:
+ d_test = ''
+
+ called_by = str(sys._getframe(2).f_code.co_name)
+ in_class = ''
+ try:
+ in_class = 'class ' + str(sys._getframe(2).f_locals['self'].__class__.__name__) + d_test
+ except:
+ in_class = 'a logic?' + d_test
+ if called_by == '':
+ called_by = str(sys._getframe(3).f_code.co_name)
+ level = 3
+ while True:
+ level += 1
+ try:
+ c_b = str(sys._getframe(level).f_code.co_name)
+ except ValueError:
+ c_b = ''
+ if c_b == '':
+ break
+ called_by += ' -> ' + c_b
+
+# called_by = str(sys._getframe(3).f_code.co_name)
+
+ if not hasattr(self, 'dep_id_list'):
+ self.dep_id_list = []
+ id_str = d_func + '|' + in_class + '|' + called_by
+ if not id_str in self.dep_id_list:
+ self.logger.warning("DEPRECATED: Used function '{}', called in '{}' by '{}' {}".format(d_func, in_class, called_by, n_func))
+ self.dep_id_list.append(id_str)
+ return
+
class Connections(Base):
"""
@@ -354,6 +403,7 @@ def __init__(self, host, port, proto='TCP'):
self._proto = proto
self.address = "{}:{}".format(host, port)
self.connected = False
+ self._deprecated_warning('lib.network.Tcp_server() class')
def connect(self):
try:
@@ -581,6 +631,7 @@ def __init__(self, host, port, proto='TCP', monitor=False):
self._connection_attempts = 0
self._connection_errorlog = 60
self._connection_lock = threading.Lock()
+ self._deprecated_warning('lib.network.Tcp_client() class')
def connect(self):
self._connection_lock.acquire()
diff --git a/lib/constants.py b/lib/constants.py
index e43ba0f956..9c252f7f6a 100644
--- a/lib/constants.py
+++ b/lib/constants.py
@@ -53,7 +53,13 @@
KEY_AUTOTIMER = 'autotimer'
KEY_ON_UPDATE = 'on_update'
KEY_ON_CHANGE = 'on_change'
-KEY_LOG_CHANGE = 'log_change'
+
+KEY_LOG_CHANGE = 'log_change'
+KEY_LOG_LEVEL = 'log_level'
+KEY_LOG_TEXT = 'log_text'
+KEY_LOG_MAPPING = 'log_mapping'
+KEY_LOG_RULES = 'log_rules'
+
KEY_STRUCT = 'struct'
KEY_REMARK = 'remark'
diff --git a/lib/env/location.py b/lib/env/location.py
index 453ff52ea7..442d2219e1 100644
--- a/lib/env/location.py
+++ b/lib/env/location.py
@@ -1,6 +1,13 @@
# lib/env/location.py
+if sh.env.location.lon() == 0 and sh.env.location.lat() == 0:
+ try:
+ sh.env.location.lon(sh._lon, logic.lname)
+ sh.env.location.lat(sh._lat, logic.lname)
+ sh.env.location.elev(sh._elev, logic.lname)
+ except: pass
+
if sh.sun:
try:
# sunrise = sh.sun.rise().astimezone(sh.tzinfo())
diff --git a/lib/env/location.yaml b/lib/env/location.yaml
index 62712837b1..4d49936b51 100644
--- a/lib/env/location.yaml
+++ b/lib/env/location.yaml
@@ -2,6 +2,15 @@ env:
location:
+ lat:
+ type: num
+
+ lon:
+ type: num
+
+ elev:
+ type: num
+
day:
type: bool
diff --git a/lib/item/item.py b/lib/item/item.py
index 2c72f9e058..cf4ebcff36 100644
--- a/lib/item/item.py
+++ b/lib/item/item.py
@@ -29,6 +29,7 @@
import copy
import json
import threading
+import ast
import time # for calls to time in eval
import math # for calls to math in eval
@@ -41,8 +42,9 @@
from lib.constants import (ITEM_DEFAULTS, FOO, KEY_ENFORCE_UPDATES, KEY_ENFORCE_CHANGE, KEY_CACHE, KEY_CYCLE, KEY_CRONTAB, KEY_EVAL,
KEY_EVAL_TRIGGER, KEY_TRIGGER, KEY_CONDITION, KEY_NAME, KEY_TYPE, KEY_STRUCT, KEY_REMARK, KEY_INSTANCE,
KEY_VALUE, KEY_INITVALUE, PLUGIN_PARSE_ITEM, KEY_AUTOTIMER, KEY_ON_UPDATE, KEY_ON_CHANGE,
- KEY_LOG_CHANGE, KEY_THRESHOLD,
- KEY_ATTRIB_COMPAT, ATTRIB_COMPAT_V12, ATTRIB_COMPAT_LATEST)
+ KEY_LOG_CHANGE, KEY_LOG_LEVEL, KEY_LOG_TEXT, KEY_LOG_MAPPING, KEY_LOG_RULES,
+ KEY_THRESHOLD, KEY_ATTRIB_COMPAT, ATTRIB_COMPAT_V12, ATTRIB_COMPAT_LATEST)
+from lib.utils import Utils
from .property import Property
from .helpers import *
@@ -132,6 +134,11 @@ def __init__(self, smarthome, parent, path, config, items_instance=None):
self._on_change_dest_var_unexp = [] # -> KEY_ON_CHANGE destination var (with unexpanded item reference)
self._log_change = None
self._log_change_logger = None
+ self._log_level = None
+ self._log_level_name = None
+ self._log_mapping = {}
+ self._log_rules = {}
+ self._log_text = None
self._fading = False
self._items_to_trigger = []
self.__last_change = self.shtime.now()
@@ -226,13 +233,52 @@ def __init__(self, smarthome, parent, path, config, items_instance=None):
logger.warning("Item __init__: {}: Invalid trigger_condition specified! Must be a list".format(self._path))
elif attr in [KEY_ON_CHANGE, KEY_ON_UPDATE]:
self._process_on_xx_list(attr, value)
+
+ elif attr in [KEY_LOG_LEVEL]:
+ if value != '':
+ level = value.upper()
+ level_name = level
+ if Utils.is_int(level):
+ level = int(level)
+ level_name = logging.getLevelName(level)
+ if logging.getLevelName(level) == 'Level ' + str(level):
+ logger.warning(f"Item {self._path}: Invalid loglevel '{value}' defined in attribute '{KEY_LOG_LEVEL}' - Level 'INFO' will be used instead")
+ setattr(self, '_log_level_name', 'INFO')
+ setattr(self, '_log_level', logging.getLevelName('INFO'))
+ else:
+ setattr(self, '_log_level_name', level_name)
+ setattr(self, '_log_level', logging.getLevelName(level_name))
elif attr in [KEY_LOG_CHANGE]:
if value != '':
setattr(self, '_log_change', value)
self._log_change_logger = logging.getLogger('items.'+value)
# set level to make logger appear in internal list of loggers (if not configured by logging.yaml)
if self._log_change_logger.level == 0:
- self._log_change_logger.setLevel('INFO')
+ if self._log_level == 'DEBUG':
+ self._log_change_logger.setLevel('DEBUG')
+ else:
+ self._log_change_logger.setLevel('INFO')
+ if self._log_level is None:
+ setattr(self, '_log_level_name', 'INFO')
+ setattr(self, '_log_level', logging.getLevelName('INFO'))
+ elif attr in [KEY_LOG_MAPPING]:
+ if value != '':
+ try:
+ value_dict = ast.literal_eval(value)
+ setattr(self, '_log_mapping', value_dict)
+ except Exception as e:
+ logger.warning(f"Item {self._path}: Invalid data for attribute '{KEY_LOG_MAPPING}': {value} - Exception: {e}")
+ elif attr in [KEY_LOG_RULES]:
+ if value != '':
+ try:
+ value_dict = ast.literal_eval(value)
+ setattr(self, '_log_rules', value_dict)
+ except Exception as e:
+ logger.warning(f"Item {self._path}: Invalid data for attribute '{KEY_LOG_RULES}': {value} - Exception: {e}")
+ elif attr in [KEY_LOG_TEXT]:
+ if value != '':
+ setattr(self, '_log_text', value)
+
elif attr == KEY_AUTOTIMER:
time, value, compat = split_duration_value_string(value, ATTRIB_COMPAT_DEFAULT)
timeitem = None
@@ -360,7 +406,10 @@ def __init__(self, smarthome, parent, path, config, items_instance=None):
# Write item value to log, if Item has attribute log_change set
self._log_on_change(self._value, 'Init', 'Cache', None)
except Exception as e:
- logger.warning("Item {}: problem reading cache: {}".format(self._path, e))
+ if str(e).startswith('[Errno 2]'):
+ logger.info("Item {}: No cached value: {}".format(self._path, e))
+ else:
+ logger.warning("Item {}: Problem reading cache: {}".format(self._path, e))
#############################################################
# Cache write/init
@@ -368,7 +417,7 @@ def __init__(self, smarthome, parent, path, config, items_instance=None):
if self._cache:
if not os.path.isfile(self._cache):
cache_write(self._cache, self._value)
- logger.warning("Item {}: Created cache for item: {}".format(self._cache, self._cache))
+ logger.notice("Created cache for item: {} in file {}".format(self._cache, self._cache))
#############################################################
# Plugins
@@ -1133,6 +1182,9 @@ def __run_eval(self, value=None, caller='Eval', source=None, dest=None):
shtime = self.shtime
items = _items_instance
import math
+ import lib.userfunctions as uf
+ # uf.import_user_modules() - Modules were loaded during initialization phase of shng
+
cond = eval(self._trigger_condition)
logger.warning("Item {}: Condition result '{}' evaluating trigger condition {}".format(self._path, cond, self._trigger_condition))
except Exception as e:
@@ -1153,6 +1205,9 @@ def __run_eval(self, value=None, caller='Eval', source=None, dest=None):
shtime = self.shtime
items = _items_instance
import math
+ import lib.userfunctions as uf
+ # uf.import_user_modules() - Modules were loaded during initialization phase of shng
+
try:
#logger.warning("Item {}: Evaluating item value {}".format(self._path, self._eval))
value = eval(self._eval)
@@ -1189,6 +1244,8 @@ def _run_on_xxx(self, path, value, on_dest, on_eval, attr='?'):
shtime = self.shtime
items = _items_instance
import math
+ import lib.userfunctions as uf
+ #uf.import_user_modules() - Modules were loaded during initialization phase of shng
logger.info("Item {}: '{}' evaluating {} = {}".format(self._path, attr, on_dest, on_eval))
@@ -1253,19 +1310,82 @@ def __run_on_change(self, value=None):
self._run_on_xxx(self._path, value, on_change_dest, on_change_eval, 'On_Change')
+ def _log_build_standardtext(self, value, caller, source=None, dest=None):
+
+ log_src = ''
+ if source is not None:
+ log_src += ' (' + source + ')'
+ log_dst = ''
+ if dest is not None:
+ log_dst += ', dest: ' + dest
+ txt = f"Item Change: {self._path} = {value} - caller: {caller}{log_src}{log_dst}"
+ return txt
+
+
+ def _log_build_text(self, value, caller, source=None, dest=None):
+
+ # value
+ # caller
+ # source
+ # dest
+ lvalue = self.property.last_value
+ mlvalue = self._log_mapping.get(lvalue, lvalue)
+ name = self._name
+ age = round(self._get_last_change_age(), 2)
+ pname = self.__parent._name
+ id = self._path
+ pid = self.__parent._path
+ mvalue = self._log_mapping.get(value, value)
+ lowlimit = self._log_rules.get('lowlimit', None)
+ highlimit = self._log_rules.get('highlimit', None)
+
+ try:
+ #logger.warning(f"self._log_text: {self._log_text}, type={type(self._log_text)}")
+ txt = eval(f"f'{self._log_text}'")
+ except Exception as e:
+ logger.error(f"{id}: Invalid log_text template ' {self._log_text}' - (Exception: {e})")
+ txt = self._log_text
+ return txt
+
+
def _log_on_change(self, value, caller, source=None, dest=None):
"""
Write log, if Item has attribute log_change set
:return:
"""
if self._log_change_logger is not None:
- log_src = ''
- if source is not None:
- log_src += ' (' + source + ')'
- log_dst = ''
- if dest is not None:
- log_dst += ', dest: ' + dest
- self._log_change_logger.info("Item Change: {} = {} - caller: {}{}{}".format(self._path, value, caller, log_src, log_dst))
+ filter_list = self._log_rules.get('filter', [])
+
+ if self._type == 'num':
+ low_limit = self._log_rules.get('lowlimit', None)
+ if low_limit:
+ if low_limit > float(value):
+ return
+ high_limit = self._log_rules.get('highlimit', None)
+ if high_limit:
+ if high_limit <= float(value):
+ return
+ if filter_list != []:
+ if not float(value) in filter_list:
+ return
+ else:
+ if filter_list != []:
+ if not value in filter_list:
+ return
+
+ if self._log_text is None:
+ txt = self._log_build_standardtext(value, caller, source, dest)
+ else:
+ txt = self._log_build_text(value, caller, source, dest)
+
+ # log_src = ''
+ # if source is not None:
+ # log_src += ' (' + source + ')'
+ # log_dst = ''
+ # if dest is not None:
+ # log_dst += ', dest: ' + dest
+ #self._log_change_logger.log(self._log_level, "Item Change: {} = {} - caller: {}{}{}".format(self._path, value, caller, log_src, log_dst))
+ self._log_change_logger.log(self._log_level, txt)
def __trigger_logics(self, source_details=None):
@@ -1437,7 +1557,7 @@ def get_method_triggers(self):
return self.__methods_to_trigger
- def timer(self, time, value, auto=False, compat=ATTRIB_COMPAT_DEFAULT):
+ def timer(self, time, value, auto=False, compat=ATTRIB_COMPAT_DEFAULT, source=None):
time = self._cast_duration(time)
value = self._castvalue_to_itemtype(value, compat)
if auto:
@@ -1446,7 +1566,10 @@ def timer(self, time, value, auto=False, compat=ATTRIB_COMPAT_DEFAULT):
else:
caller = 'Timer'
next = self.shtime.now() + datetime.timedelta(seconds=time)
- self._sh.scheduler.add(self._itemname_prefix+self.id() + '-Timer', self.__call__, value={'value': value, 'caller': caller}, next=next)
+ if source is None:
+ self._sh.scheduler.add(self._itemname_prefix+self.id() + '-Timer', self.__call__, value={'value': value, 'caller': caller}, next=next)
+ else:
+ self._sh.scheduler.add(self._itemname_prefix+self.id() + '-Timer', self.__call__, value={'value': value, 'caller': caller, 'source': source}, next=next)
def remove_timer(self):
@@ -1455,6 +1578,7 @@ def remove_timer(self):
def autotimer(self, time=None, value=None, compat=ATTRIB_COMPAT_V12):
if time is not None and value is not None:
+ time = self._cast_duration(time)
self._autotimer = [(time, value), compat, None, None]
else:
self._autotimer = False
diff --git a/lib/item/items.py b/lib/item/items.py
index 4bc10fc2a7..e5079603f0 100644
--- a/lib/item/items.py
+++ b/lib/item/items.py
@@ -292,16 +292,23 @@ def return_item(self, string):
return self.__item_dict[string]
- def return_items(self):
+ def return_items(self, sorted=False):
"""
Function to return a list with all defined items
+ :param sorted: return list sorted alphabetically, defaults to False
+ :type sorted: bool
+
:return: List of all items
:rtype: list
"""
- for item in self.__items:
- yield self.__item_dict[item]
+ if sorted:
+ for item in sorted(self.__items):
+ yield self.__item_dict[item]
+ else:
+ for item in self.__items:
+ yield self.__item_dict[item]
def match_items(self, regex):
diff --git a/lib/item/structs.py b/lib/item/structs.py
index 986bfbcdff..258d086254 100644
--- a/lib/item/structs.py
+++ b/lib/item/structs.py
@@ -238,6 +238,37 @@ def return_struct_definitions(self, all=True):
return result
+ def load_struct_definitions_from_file(self, etc_dir, fn, key_prefix):
+ """
+ Loads struct definitions from a file
+
+ :param etc_dir: path to etc directory of SmartHomeNG
+ :param fn: filename to load struct definition(s) from
+ :param key_prefix: prefix to be used when adding struct(s) to loaded definitions
+ """
+ if key_prefix == '':
+ self.logger.info(f"Loading struct file '{fn}' without key-prefix")
+ else:
+ self.logger.info(f"Loading struct file '{fn}' with key-prefix '{key_prefix}'")
+
+ # Read in item structs from ../etc/struct.yaml
+ struct_definitions = shyaml.yaml_load(os.path.join(etc_dir, fn), ordered=True, ignore_notfound=True)
+
+ # if valid struct definition file etc/struct.yaml ist found
+ if struct_definitions is not None:
+ if isinstance(struct_definitions, collections.OrderedDict):
+ for key in struct_definitions:
+ if fn == 'struct.yaml':
+ struct_name = key
+ else:
+ struct_name = key_prefix + '.' + key
+ self.add_struct_definition('', struct_name, struct_definitions[key])
+ else:
+ self.logger.error(f"load_itemdefinitions(): Invalid content in {fn}: struct_definitions = '{struct_definitions}'")
+
+ return
+
+
def load_struct_definitions(self, etc_dir):
# --------------------------------------------------------------------
@@ -245,18 +276,20 @@ def load_struct_definitions(self, etc_dir):
#
# structs are merged into the item tree in lib.config
#
- # structs are read in from metadata file of plugins while loading plugins
- # and from ../etc/struct.yaml
+ # - plugin-structs are read in from metadata file of plugins while loading plugins
+ # - other structs are read in from ../etc/struct.yaml by this procedure
+ # - further structs are read in from ../etc/struct_.yaml by this procedure
#
- # Read in item structs from ../etc/struct.yaml
- struct_definitions = shyaml.yaml_load(os.path.join(etc_dir, 'struct.yaml'), ordered=True, ignore_notfound=True)
- if struct_definitions is not None:
- if isinstance(struct_definitions, collections.OrderedDict):
- for key in struct_definitions:
- self.add_struct_definition('', key, struct_definitions[key])
- else:
- self.logger.error("load_itemdefinitions(): Invalid content in struct.yaml: struct_definitions = '{}'".format(struct_definitions))
+ self.load_struct_definitions_from_file(etc_dir, 'struct.yaml', '')
+
+ # look for further struct files
+ fl = os.listdir(etc_dir)
+ for fn in fl:
+ if fn.startswith('struct_') and fn.endswith('.yaml'):
+ key_prefix = 'my.' + fn[7:-5]
+ self.load_struct_definitions_from_file(etc_dir, fn, key_prefix)
+ # Resolve struct references in structs and fill in the content of the struct
self.fill_nested_structs()
# for Testing: Save structure of joined item structs
diff --git a/lib/item_old.py b/lib/item_old.py
deleted file mode 100644
index 8c1516895c..0000000000
--- a/lib/item_old.py
+++ /dev/null
@@ -1,2780 +0,0 @@
-#!/usr/bin/env python3
-# vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab
-#########################################################################
-# Copyright 2016-2018 Martin Sinn m.sinn@gmx.de
-# Copyright 2016 Christian Straßburg c.strassburg@gmx.de
-# Copyright 2012-2013 Marcus Popp marcus@popp.mx
-#########################################################################
-# This file is part of SmartHomeNG.
-#
-# SmartHomeNG is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# SmartHomeNG is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with SmartHomeNG. If not, see .
-#########################################################################
-
-
-"""
-This library implements items in SmartHomeNG.
-
-The main class ``Items`` implements the handling for all items. This class has a static method to get a handle to the
-instance of the Items class, that is created during initialization of SmartHomeNG. This method implements a way to
-access the API for handling items without having to juggle through the object hierarchy of the running SmartHomeNG.
-
-This API enables plugins and logics to access the details of the items initialized in SmartHomeNG.
-
-Each item is represented by an instance of the class ``Item``.
-
-The methods of the class Items implement the API for items.
-They can be used the following way: To call eg. **get_toplevel_items()**, use the following syntax:
-
-.. code-block:: python
-
- from lib.item import Items
- sh_items = Items.get_instance()
-
- # to access a method (eg. get_toplevel_items()):
- tl_items = sh_items.get_toplevel_items()
-
-
-:Note: Do not use the functions or variables of the main smarthome object any more. They are deprecated. Use the methods of the class **Items** instead.
-
-:Note: This library is part of the core of SmartHomeNG. Regular plugins should not need to use this API. It is manily implemented for plugins near to the core like **backend** and the core itself!
-
-"""
-import copy
-import datetime
-import dateutil.parser
-import time # for calls to time in eval
-import logging
-import collections
-import os
-import re
-import pickle
-import threading
-import math # for calls to math in eval
-from math import *
-import json
-from ast import literal_eval
-import inspect
-
-from lib.plugin import Plugins
-import lib.shyaml as shyaml
-from lib.shtime import Shtime
-
-import lib.utils
-from lib.constants import (ITEM_DEFAULTS, FOO, KEY_ENFORCE_UPDATES, KEY_ENFORCE_CHANGE, KEY_CACHE, KEY_CYCLE, KEY_CRONTAB, KEY_EVAL,
- KEY_EVAL_TRIGGER, KEY_TRIGGER, KEY_CONDITION, KEY_NAME, KEY_TYPE, KEY_STRUCT,
- KEY_VALUE, KEY_INITVALUE, PLUGIN_PARSE_ITEM, KEY_AUTOTIMER, KEY_ON_UPDATE, KEY_ON_CHANGE,
- KEY_LOG_CHANGE, KEY_THRESHOLD, CACHE_FORMAT, CACHE_JSON, CACHE_PICKLE,
- KEY_ATTRIB_COMPAT, ATTRIB_COMPAT_V12, ATTRIB_COMPAT_LATEST)
-
-
-ATTRIB_COMPAT_DEFAULT_FALLBACK = ATTRIB_COMPAT_V12
-ATTRIB_COMPAT_DEFAULT = ''
-
-
-logger = logging.getLogger(__name__)
-
-
-_items_instance = None # Pointer to the initialized instance of the Items class (for use by static methods)
-
-
-
-class Items():
- """
- Items loader class. (Item-methods from bin/smarthome.py are moved here.)
-
- - An instance is created during initialization by bin/smarthome.py
- - There should be only one instance of this class. So: Don't create another instance
-
- :param smarthome: Instance of the smarthome master-object
- :type smarthome: object
- """
-
- __items = [] # list with the paths of all items that are defined
- __item_dict = {} # dict with all the items that are defined in the form: {"": "", ...}
-
- _children = [] # List of top level items
-
- _struct_definitions = collections.OrderedDict() # definitions of item structures
-
-
- def __init__(self, smarthome):
- self._sh = smarthome
-
- global _items_instance
- if _items_instance is not None:
- import inspect
- curframe = inspect.currentframe()
- calframe = inspect.getouterframes(curframe, 4)
- logger.critical("A second 'items' object has been created. There should only be ONE instance of class 'Items'!!! Called from: {} ({})".format(calframe[1][1], calframe[1][3]))
-
- _items_instance = self
-
-
- # -----------------------------------------------------------------------------------------
- # Following (static) method of the class Items implement the API for Items in SmartHomeNG
- # -----------------------------------------------------------------------------------------
-
- @staticmethod
- def get_instance():
- """
- Returns the instance of the Items class, to be used to access the items-api
-
- Use it the following way to access the api:
-
- .. code-block:: python
-
- from lib.item import Items
- items = Items.get_instance()
-
- # to access a method (eg. return_items()):
- items.return_items()
-
-
- :return: items instance
- :rtype: object
- """
- return _items_instance
-
-
- # -----------------------------------------------------------------------------------------
- # Following methods handle structs
- # -----------------------------------------------------------------------------------------
-
- struct_merge_lists = True
-
-
- def merge_structlists(self, l1, l2, key=''):
- if not self.struct_merge_lists:
- logger.warning("merge_structlists: Not merging lists, key '{}' value '{}' is ignored'".format(key, l2))
- return l1 # First wins
- else:
- if not isinstance(l1, list):
- l1 = [l1]
- if not isinstance(l2, list):
- l2 = [l2]
- return l1 + l2
-
-
- def add_struct_definition(self, plugin_name, struct_name, struct):
- """
- Add a struct definition
-
- called when reading in item structs from ../etc/struct.yaml
- or from lib.plugin when reading in plugin-metadata
-
- :param plugin_name:
- :param struct_name:
- :param struct:
- :return:
- """
- if plugin_name == '':
- name = struct_name
- else:
- name = plugin_name + '.' + struct_name
-
- logger.info("add_struct_definition: struct '{}' = {}".format(name, struct))
- self._struct_definitions[name] = struct
- return
-
-
- def merge(self, source, destination, source_name='', dest_name=''):
- '''
- Merges an OrderedDict Tree into another one
-
- :param source: source tree to merge into another one
- :param destination: destination tree to merge into
- :type source: OrderedDict
- :type destination: OrderedDict
-
- :return: Merged configuration tree
- :rtype: OrderedDict
-
- :Example: Run me with nosetests --with-doctest file.py
-
- .. code-block:: python
-
- >>> a = { 'first' : { 'all_rows' : { 'pass' : 'dog', 'number' : '1' } } }
- >>> b = { 'first' : { 'all_rows' : { 'fail' : 'cat', 'number' : '5' } } }
- >>> merge(b, a) == { 'first' : { 'all_rows' : { 'pass' : 'dog', 'fail' : 'cat', 'number' : '5' } } }
- True
-
- '''
- for key, value in source.items():
- if isinstance(value, collections.OrderedDict):
- # get node or create one
- node = destination.setdefault(key, collections.OrderedDict())
- if node == 'None':
- destination[key] = value
- else:
- self.merge(value, node, source_name, dest_name)
- else:
- if isinstance(value, list) or isinstance(destination.get(key, None), list):
- if destination.get(key, None) is None:
- destination[key] = value
- else:
- destination[key] = self.merge_structlists(destination[key], value, key)
- else:
- # convert to string and remove newlines from multiline attributes
- if destination.get(key, None) is None:
- destination[key] = str(value).replace('\n', '')
- return destination
-
-
- def resolve_structs(self, struct, struct_name, substruct_names):
- """
- Resolve a struct reference
-
- if the struct definition that is to be inserted contains a struct reference, it is resolved first
-
- :param struct: struct that contains a struct reference
- :param substruct: sub-struct definition that shall be inserted
- :param struct_name: name of the struct that contains a struct reference
- :param substruct_name: name of the sub-struct definition that shall be inserted
- """
-
- logger.info("resolve_structs: struct_name='{}', substruct_names='{}'".format(struct_name, substruct_names))
-
- new_struct = collections.OrderedDict()
- structentry_list = list(struct.keys())
- for structentry in structentry_list:
- # copy all existing attributes and sub-entrys of the struct
- if new_struct.get(structentry, None) is None:
- logger.info("resolve_struct: - copy attribute structentry='{}', value='{}'".format(structentry, struct[structentry]))
- new_struct[structentry] = copy.deepcopy(struct[structentry])
- else:
- logger.debug("resolve_struct: - key='{}', value is ignored'".format(structentry))
- if structentry == 'struct':
- for substruct_name in substruct_names:
- # for every substruct
- logger.info("resolve_struct: ->substruct_name='{}'".format(substruct_name))
- substruct = self._struct_definitions.get(substruct_name, None)
- # merge in the sub-struct
- for key in substruct:
- if new_struct.get(key, None) is None:
- logger.info("resolve_struct: - key='{}', value='{}' -> new_struct='{}'".format(key, substruct[key], new_struct))
- new_struct[key] = copy.deepcopy(substruct[key])
- elif isinstance(new_struct.get(key, None), dict):
- logger.info("resolve_struct: - merge key='{}', value='{}' -> new_struct='{}'".format(key, substruct[key], new_struct))
- self.merge(substruct[key], new_struct[key], key, struct_name+'.'+key)
- elif isinstance(new_struct.get(key, None), list) or isinstance(substruct.get(key, None), list):
- new_struct[key] = self.merge_structlists(new_struct[key], substruct[key], key)
- else:
- logger.debug("resolve_struct: - key='{}', value '{}' is ignored'".format(key, substruct[key]))
-
- return new_struct
-
-
- def fill_nested_structs(self):
- """
- Resolve struct references in structs and fill in the content of the struct
-
- :return:
- """
- for struct_name in self._struct_definitions:
- # for every defined struct
- struct = self._struct_definitions[struct_name]
- substruct_names = struct.get('struct', None)
- if substruct_names is not None:
- # stuct has a sub-struct
- if isinstance(substruct_names, str):
- substruct_names = [substruct_names]
- struct = self.resolve_structs(struct, struct_name, substruct_names)
- self._struct_definitions[struct_name] = struct
-
-
- def return_struct_definitions(self):
- """
- Return all loaded structure template definitions
-
- :return:
- :rtype: dict
- """
- return self._struct_definitions
-
-
- def load_itemdefinitions(self, env_dir, items_dir, etc_dir, plugins_dir):
- """
- Load item definitions
-
- This method is called during initialization of SmartHomeNG to initialize the item tree.
- For that, it loads the item definitions from **../items** directory through calling the function **parse_itemsdir()**
- from **lib.config**
-
- :param env_dir: path to the directory containing the core's environment item definition files
- :param items_dir: path to the directory containing the user's item definition files
- :param etc_dir: path to the directory containing the user's configuration files (only used for 'struct' support)
- :param plugins_dir: path to the directory containing the plugins (only used for 'struct' support)
- :type env_dir: str
- :type items_dir: str
- :type etc_dir: str
- :type plugins_dir: str
- """
-
- # --------------------------------------------------------------------
- # Read in all struct definitions before reading item definitions
- #
- # structs are merged into the item tree in lib.config
- #
- # structs are read in from metadata file of plugins while loading plugins
- # and from ../etc/struct.yaml
- #
- # Read in item structs from ../etc/struct.yaml
- struct_definitions = shyaml.yaml_load(os.path.join(etc_dir, 'struct.yaml'), ordered=True, ignore_notfound=True)
- if struct_definitions is not None:
- if isinstance(struct_definitions, collections.OrderedDict):
- for key in struct_definitions:
- self.add_struct_definition('', key, struct_definitions[key])
- else:
- logger.error("load_itemdefinitions(): Invalid content in struct.yaml: struct_definitions = '{}'".format(struct_definitions))
-
- self.fill_nested_structs()
-
- # for Testing: Save structure of joined item structs
- logger.warning("load_itemdefinitions(): For testing the joined item structs are saved to {}".format(os.path.join(etc_dir, 'structs_joined.yaml')))
- shyaml.yaml_save(os.path.join(etc_dir, 'structs_joined.yaml'), self._struct_definitions)
-
- # --------------------------------------------------------------------
- # Read in item definitions
- #
- item_conf = None
- item_conf = lib.config.parse_itemsdir(env_dir, item_conf)
- item_conf = lib.config.parse_itemsdir(items_dir, item_conf, addfilenames=True, struct_dict=self._struct_definitions)
-
- for attr, value in item_conf.items():
- if isinstance(value, dict):
- child_path = attr
- try:
- # (smarthome, parent, path, config):
- child = Item(self._sh, self, child_path, value)
- except Exception as e:
- logger.error("load_itemdefinitions: Item {}: problem creating: ()".format(child_path, e))
- else:
- vars(self)[attr] = child
- vars(self._sh)[attr] = child
- self.add_item(child_path, child)
- self._children.append(child)
- del(item_conf) # clean up
-
- # --------------------------------------------------------------------
- # prepare loaded items for run phase of SmartHomeNG
- #
- for item in self.return_items():
- item._init_prerun()
- # starting schedulers (for crontab and cycle attributes) moved to the end of the initialization in SmartHomeNG v1.6
- for item in self.return_items():
- item._init_start_scheduler()
- for item in self.return_items():
- item._init_run()
-
-# self.item_count = len(self.__items)
-# self._sh.item_count = self.item_count()
-
-
- def add_item(self, path, item):
- """
- Function to to add an item to the dictionary of items.
- If the path does not exist, it is created
-
- :param path: Path of the item
- :param item: The item itself
- :type path: str
- :type item: object
- """
-
- if path not in self.__items:
- self.__items.append(path)
- print("\nitems.add_item: path={}, item={} -> {}".format(path, item, self.__items))
- self.__item_dict[path] = item
-
- # aus bin/smarthome.py
- # def __iter__(self):
- # for child in self.__children:
- # yield child
-
- def get_toplevel_items(self):
- """
- Returns a list with all items defined at the top level
-
- :return: items defined at the top level
- :rtype: list
- """
- for child in self._children:
- yield child
-
- # aus lib.logic.py
- # def __iter__(self):
- # for logic in self._logics:
- # yield logic
-
-
-
- def return_item(self, string):
- """
- Function to return the item for a given path
-
- :param string: Path of the item to return
- :type string: str
-
- :return: Item
- :rtype: object
- """
-
- if string in self.__items:
- return self.__item_dict[string]
-
-
- def return_items(self):
- """
- Function to return a list with all defined items
-
- :return: List of all items
- :rtype: list
- """
-
- for item in self.__items:
- yield self.__item_dict[item]
-
-
- def match_items(self, regex):
- """
- Function to match items against a regular expression
-
- :param regex: Regular expression to match items against
- :type regex: str
-
- :return: List of matching items
- :rtype: list
- """
-
- regex, __, attr = regex.partition(':')
- regex = regex.replace('.', '\.').replace('*', '.*') + '$'
- regex = re.compile(regex)
- attr, __, val = attr.partition('[')
- val = val.rstrip(']')
- if attr != '' and val != '':
- return [self.__item_dict[item] for item in self.__items if regex.match(item) and attr in self.__item_dict[item].conf and ((type(self.__item_dict[item].conf[attr]) in [list,dict] and val in self.__item_dict[item].conf[attr]) or (val == self.__item_dict[item].conf[attr]))]
- elif attr != '':
- return [self.__item_dict[item] for item in self.__items if regex.match(item) and attr in self.__item_dict[item].conf]
- else:
- return [self.__item_dict[item] for item in self.__items if regex.match(item)]
-
-
- def _attribute_find(self, attr, attr_list):
- """
- Find an attribute in an attribute list
-
- :param attr:
- :param attr_list:
- :return:
-
- examples:
- attr_list = ['avm_identifier' , 'avm_data_type@willy_tel', 'avm_wlan_index', 'visu_acl']
-
- attr result
- ---_ ------
- 'willy_tel' -> False
- '@willy_tel' -> True
- '@fritz_wz' -> False
-
- 'avm_data_type@willy_tel' -> True
- 'avm_data_type@fritz_wz' -> False
- 'avm_data_type' -> False
- 'avm_data_type@' -> True
-
- 'avm_wlan_index' -> True
- 'avm_wlan_index@' -> True
-
- 'visu_acl' -> True
- '@visu_acl' -> False
-
- """
- result = False
- if attr.endswith('@'):
- result = any(s for s in attr_list if s.startswith(attr))
- if not result:
- result = attr[:-1] in attr_list
- elif attr.startswith('@'):
- result = any(s for s in attr_list if s.endswith(attr))
- else:
- result = attr in attr_list
- return result
-
-
- def find_items(self, conf):
- """
- Function to find items that match the specified configuration
-
- :param conf: Configuration to look for
- :type conf: str
-
- :return: list of matching items
- :rtype: list
- """
-
- for item in self.__items:
- # if conf in self.__item_dict[item].conf:
- # yield self.__item_dict[item]
- if self._attribute_find(conf, self.return_item(item).property.attributes):
- yield self.__item_dict[item]
-
-
- def find_children(self, parent, conf):
- """
- Function to find children with the specified configuration
-
- :param parent: parent item on which to start the search
- :param conf: Configuration to look for
- :type parent: str
- :type conf: str
-
- :return: list or matching child-items
- :rtype: list
- """
-
- children = []
- for item in parent:
- # if conf in item.conf:
- # children.append(item)
- if self._attribute_find(conf, item.property.attributes):
- children.append(item)
- children += self.find_children(item, conf)
- return children
-
-
- def item_count(self):
- """
- Return the number of defined items
-
- :return: number of items
- :rtype: int
- """
- return len(self.__items)
-
-
- def stop(self, signum=None, frame=None):
- """
- Stop what all items are doing
-
- At the moment, it stops fading of all items
- """
- for item in self.__items:
- self.__item_dict[item]._fading = False
-
-
-
-
-#####################################################################
-# Item Class
-#####################################################################
-
-"""
-The class ``Item`` implements the methods and attributes of an item. Each item is represented by an instance of the class ``Item``.
-"""
-
-class Item():
- """
- Class from which item objects are created
-
- The class ``Item`` implements the methods and attributes of an item. Each item is represented by an instance
- of the class ``Item``. For an item to be valid and usable, it has to be part of the item tree, which is
- maintained by an object of class ``Items``.
-
- This class is used by the method ```load_itemdefinitions()`` of the **Items** object.
- """
-
- _itemname_prefix = 'items.' # prefix for scheduler names
-
- def __init__(self, smarthome, parent, path, config):
- self._sh = smarthome
- self._use_conditional_triggers = False
- try:
- if self._sh._use_conditional_triggers.lower() == 'true':
- self._use_conditional_triggers = True
- except: pass
-
- self.plugins = Plugins.get_instance()
- self.shtime = Shtime.get_instance()
-
- self._filename = None
- self._autotimer = False
- self._cache = False
- self.cast = _cast_bool
- self.__changed_by = 'Init:None'
- self.__updated_by = 'Init:None'
- self.__children = []
- self.conf = {}
- self._crontab = None
- self._cycle = None
- self._enforce_updates = False
- self._enforce_change = False
- self._eval = None # -> KEY_EVAL
- self._eval_unexpanded = ''
- self._eval_trigger = False
- self._trigger = False
- self._trigger_unexpanded = []
- self._trigger_condition_raw = []
- self._trigger_condition = None
- self._on_update = None # -> KEY_ON_UPDATE eval expression
- self._on_change = None # -> KEY_ON_CHANGE eval expression
- self._on_update_dest_var = None # -> KEY_ON_UPDATE destination var
- self._on_change_dest_var = None # -> KEY_ON_CHANGE destination var
- self._on_update_unexpanded = [] # -> KEY_ON_UPDATE eval expression (with unexpanded item references)
- self._on_change_unexpanded = [] # -> KEY_ON_CHANGE eval expression (with unexpanded item references)
- self._on_update_dest_var_unexp = [] # -> KEY_ON_UPDATE destination var (with unexpanded item reference)
- self._on_change_dest_var_unexp = [] # -> KEY_ON_CHANGE destination var (with unexpanded item reference)
- self._log_change = None
- self._log_change_logger = None
- self._fading = False
- self._items_to_trigger = []
- self.__last_change = self.shtime.now()
- self.__last_update = self.shtime.now()
- self._lock = threading.Condition()
- self.__logics_to_trigger = []
- self._name = path
- self.__prev_change = self.shtime.now()
- self.__prev_update = self.shtime.now()
- self.__methods_to_trigger = []
- self.__parent = parent
- self._path = path
- self._sh = smarthome
- self._threshold = False
- self._threshold_data = [0,0,False]
- self._type = None
- self._struct = None
- self._value = None
- self.__last_value = None
- self.__prev_value = None
-
- self.property = self.Property(self)
- # history
- # TODO: create history Arrays for some values (value, last_change, last_update (usage: multiklick,...)
- # self.__history = [None, None, None, None, None]
- #
- # def getValue(num):
- # return (str(self.__history[(num - 1)]))
- #
- # def addValue(avalue):
- # self.__history.append(avalue)
- # if len(self.__history) > 5:
- # self.__history.pop(0)
- #
- if hasattr(smarthome, '_item_change_log'):
- self._change_logger = logger.info
- else:
- self._change_logger = logger.debug
- #############################################################
- # Initialize attribute assignment compatibility
- #############################################################
- global ATTRIB_COMPAT_DEFAULT
- if ATTRIB_COMPAT_DEFAULT == '':
- if hasattr(smarthome, '_'+KEY_ATTRIB_COMPAT):
- config_attrib = getattr(smarthome,'_'+KEY_ATTRIB_COMPAT)
- if str(config_attrib) in [ATTRIB_COMPAT_V12, ATTRIB_COMPAT_LATEST]:
- logger.info("Global configuration: '{}' = '{}'.".format(KEY_ATTRIB_COMPAT, str(config_attrib)))
- ATTRIB_COMPAT_DEFAULT = config_attrib
- else:
- logger.warning("Global configuration: '{}' has invalid value '{}'.".format(KEY_ATTRIB_COMPAT, str(config_attrib)))
- if ATTRIB_COMPAT_DEFAULT == '':
- ATTRIB_COMPAT_DEFAULT = ATTRIB_COMPAT_DEFAULT_FALLBACK
- #############################################################
- # Item Attributes
- #############################################################
- for attr, value in config.items():
- if not isinstance(value, dict):
- if attr in [KEY_CYCLE, KEY_NAME, KEY_TYPE, KEY_STRUCT, KEY_VALUE, KEY_INITVALUE]:
- if attr == KEY_INITVALUE:
- attr = KEY_VALUE
- setattr(self, '_' + attr, value)
- elif attr in [KEY_EVAL]:
- self._process_eval(value)
- elif attr in [KEY_CACHE, KEY_ENFORCE_UPDATES, KEY_ENFORCE_CHANGE]: # cast to bool
- try:
- setattr(self, '_' + attr, _cast_bool(value))
- except:
- logger.warning("Item '{0}': problem parsing '{1}'.".format(self._path, attr))
- continue
- elif attr in [KEY_CRONTAB]: # cast to list
- if isinstance(value, str):
- value = [value, ]
- setattr(self, '_' + attr, value)
- elif attr in [KEY_EVAL_TRIGGER] or (self._use_conditional_triggers and attr in [KEY_TRIGGER]): # cast to list
- self._process_trigger_list(attr, value)
- elif (attr in [KEY_CONDITION]) and self._use_conditional_triggers: # cast to list
- if isinstance(value, list):
- cond_list = []
- for cond in value:
- cond_list.append(dict(cond))
- self._trigger_condition = self._build_trigger_condition_eval(cond_list)
- self._trigger_condition_raw = cond_list
- else:
- logger.warning("Item __init__: {}: Invalid trigger_condition specified! Must be a list".format(self._path))
- elif attr in [KEY_ON_CHANGE, KEY_ON_UPDATE]:
- self._process_on_xx_list(attr, value)
- elif attr in [KEY_LOG_CHANGE]:
- if value != '':
- setattr(self, '_log_change', value)
- self._log_change_logger = logging.getLogger('items.'+value)
- # set level to make logger appear in internal list of loggers (if not configured by logging.yaml)
- if self._log_change_logger.level == 0:
- self._log_change_logger.setLevel('INFO')
- elif attr == KEY_AUTOTIMER:
- time, value, compat = _split_duration_value_string(value)
- timeitem = None
- valueitem = None
- if time.lower().startswith('sh.') and time.endswith('()'):
- timeitem = self.get_absolutepath(time[3:-2], KEY_AUTOTIMER)
- time = 0
- if value.lower().startswith('sh.') and value.endswith('()'):
- valueitem = self.get_absolutepath(value[3:-2], KEY_AUTOTIMER)
- value = ''
- value = self._castvalue_to_itemtype(value, compat)
- self._autotimer = [ (self._cast_duration(time), value), compat, timeitem, valueitem]
- elif attr == KEY_THRESHOLD:
- low, __, high = value.rpartition(':')
- if not low:
- low = high
- self._threshold = True
- self.__th_crossed = False
- self.__th_low = float(low.strip())
- self.__th_high = float(high.strip())
- self._threshold_data[0] = self.__th_low
- self._threshold_data[1] = self.__th_high
- self._threshold_data[2] = self.__th_crossed
- logger.debug("Item {}: set threshold => low: {} high: {}".format(self._path, self.__th_low, self.__th_high))
- elif attr == '_filename':
- # name of file, which defines this item
- setattr(self, attr, value)
- else:
- # the following code is executed for plugin specific attributes:
- #
- # get value from attribute of other (relative addressed) item
- # at the moment only parent and grandparent item are supported
- if (type(value) is str) and (value.startswith('..:') or value.startswith('...:')):
- fromitem = value.split(':')[0]
- fromattr = value.split(':')[1]
- if fromattr in ['', '.']:
- fromattr = attr
- if fromitem == '..':
- self.conf[attr] = self._get_attr_from_parent(fromattr)
- elif fromitem == '...':
- self.conf[attr] = self._get_attr_from_grandparent(fromattr)
- else:
- self.conf[attr] = value
- # logger.warning("Item rel. from (grand)parent: fromitem = {}, fromattr = {}, self.conf[attr] = {}".format(fromitem, fromattr, self.conf[attr]))
- else:
- self.conf[attr] = value
-
- self.property.init_dynamic_properties()
-
- #############################################################
- # Child Items
- #############################################################
- for attr, value in config.items():
- if isinstance(value, dict):
- child_path = self._path + '.' + attr
- try:
- child = Item(smarthome, self, child_path, value)
- except Exception as e:
- logger.exception("Item {}: problem creating: {}".format(child_path, e))
- else:
- vars(self)[attr] = child
- _items_instance.add_item(child_path, child)
- self.__children.append(child)
- #############################################################
- # Cache
- #############################################################
- if self._cache:
- self._cache = self._sh._cache_dir + self._path
- try:
- self.__last_change, self._value = _cache_read(self._cache, self.shtime.tzinfo())
- self.__last_update = self.__last_change
- self.__prev_change = self.__last_change
- self.__prev_update = self.__last_change
- self.__changed_by = 'Cache:None'
- self.__updated_by = 'Cache:None'
- except Exception as e:
- logger.warning("Item {}: problem reading cache: {}".format(self._path, e))
- #############################################################
- # Type
- #############################################################
- #__defaults = {'num': 0, 'str': '', 'bool': False, 'list': [], 'dict': {}, 'foo': None, 'scene': 0}
- if self._type is None:
- self._type = FOO # MSinn
- if self._type not in ITEM_DEFAULTS:
- logger.error("Item {}: type '{}' unknown. Please use one of: {}.".format(self._path, self._type, ', '.join(list(ITEM_DEFAULTS.keys()))))
- raise AttributeError
- self.cast = globals()['_cast_' + self._type]
- #############################################################
- # Value
- #############################################################
- if self._value is None:
- self._value = ITEM_DEFAULTS[self._type]
- try:
- self._value = self.cast(self._value)
- except:
- logger.error("Item {}: value {} does not match type {}.".format(self._path, self._value, self._type))
- raise
- self.__prev_value = self.__last_value
- self.__last_value = self._value
- #############################################################
- # Cache write/init
- #############################################################
- if self._cache:
- if not os.path.isfile(self._cache):
- _cache_write(self._cache, self._value)
- logger.warning("Item {}: Created cache for item: {}".format(self._cache, self._cache))
- #############################################################
- # Plugins
- #############################################################
- for plugin in self.plugins.return_plugins():
- #plugin.xxx = [] # Empty reference list list of items
- if hasattr(plugin, PLUGIN_PARSE_ITEM):
- update = plugin.parse_item(self)
- if update:
- try:
- plugin._append_to_itemlist(self)
- except:
- pass
- self.add_method_trigger(update)
-
-
-
- def _split_destitem_from_value(self, value):
- """
- For on_change and on_update: spit destination item from attribute value
-
- :param value: attribute value
-
- :return: dest_item, value
- :rtype: str, str
- """
- dest_item = ''
- # Check if assignment operator ('=') exists
- if value.find('=') != -1:
- # If delimiter exists, check if equal operator exists
- if value.find('==') != -1:
- # equal operator exists
- if value.find('=') < value.find('=='):
- # assignment operator exists in front of equal operator
- dest_item = value[:value.find('=')].strip()
- value = value[value.find('=')+1:].strip()
- else:
- # if equal operator does not exist
- dest_item = value[:value.find('=')]
- value = value[value.find('=')+1:].strip()
- return dest_item, value
-
-
- def _castvalue_to_itemtype(self, value, compat):
- """
- casts the value to the type of the item, if backward compatibility
- to version 1.2 (ATTRIB_COMPAT_V12) is not enabled
-
- If backward compatibility is enabled, the value is returned unchanged
-
- :param value: value to be casted
- :param compat: compatibility attribute
- :return: return casted value
- """
- # casting of value, if compat = latest
- if compat == ATTRIB_COMPAT_LATEST:
- if self._type != None:
- mycast = globals()['_cast_' + self._type]
- try:
- value = mycast(value)
- except:
- logger.warning("Item {}: Unable to cast '{}' to {}".format(self._path, str(value), self._type))
- if isinstance(value, list):
- value = []
- elif isinstance(value, dict):
- value = {}
- else:
- value = mycast('')
- else:
- logger.warning("Item {}: Unable to cast '{}' to {}".format(self._path, str(value), self._type))
- return value
-
-
- def _cast_duration(self, time):
- """
- casts a time value string (e.g. '5m') to an duration integer
- used for autotimer, timer, cycle
-
- supported formats for time parameter:
- - seconds as integer (45)
- - seconds as a string ('45')
- - seconds as a string, trailed by 's' ('45s')
- - minutes as a string, trailed by 'm' ('5m'), is converted to seconds (300)
-
- :param time: string containing the duration
- :param itempath: item path as additional information for logging
- :return: number of seconds as an integer
- """
- if isinstance(time, str):
- try:
- time = time.strip()
- if time.endswith('m'):
- time = int(time.strip('m')) * 60
- elif time.endswith('s'):
- time = int(time.strip('s'))
- else:
- time = int(time)
- except Exception as e:
- logger.warning("Item {}: _cast_duration ({}) problem: {}".format(self._path, time, e))
- time = False
- elif isinstance(time, int):
- time = int(time)
- else:
- logger.warning("Item {}: _cast_duration ({}) problem: unable to convert to int".format(self._path, time))
- time = False
- return(time)
-
-
- def _build_cycledict(self, value):
- """
- builds a dict for a cycle parameter from a duration_value_string
-
- This dict is to be passed to the scheduler to circumvent the parameter
- parsing within the scheduler, which can't to casting
-
- :param value: raw attribute string containing duration, value (and compatibility)
- :return: cycle-dict for a call to scheduler.add
- """
- time, value, compat = _split_duration_value_string(value)
- time = self._cast_duration(time)
- value = self._castvalue_to_itemtype(value, compat)
- cycle = {time: value}
- return cycle
-
-
- """
- --------------------------------------------------------------------------------------------
- """
-
-
- def _build_on_xx_list(self, on_dest_list, on_eval_list):
- """
- build on_xx data
- """
- on_list = []
- if on_dest_list is not None:
- if isinstance(on_dest_list, list):
- for on_dest, on_eval in zip(on_dest_list, on_eval_list):
- if on_dest != '':
- on_list.append(on_dest.strip() + ' = ' + on_eval)
- else:
- on_list.append(on_eval)
- else:
- if on_dest_list != '':
- on_list.append(on_dest_list + ' = ' + on_eval_list)
- else:
- on_list.append(on_eval_list)
- return on_list
-
-
- def _process_eval(self, value):
-
- if value == '':
- self._eval_unexpanded = ''
- self._eval = None
- else:
- self._eval_unexpanded = value
- value = self.get_stringwithabsolutepathes(value, 'sh.', '(', KEY_EVAL)
- self._eval = value
-
-
- def _process_trigger_list(self, attr, value):
-
- if isinstance(value, str):
- value = [value, ]
- self._trigger_unexpanded = value
- expandedvalue = []
- for path in value:
- expandedvalue.append(self.get_absolutepath(path, attr))
- self._trigger = expandedvalue
-
-
- def _process_on_xx_list(self, attr, value):
-
- if isinstance(value, str):
- value = [value]
- val_list = []
- val_list_unexpanded = []
- dest_var_list = []
- dest_var_list_unexp = []
- for val in value:
- # separate destination item (if it exists)
- dest_item, val = self._split_destitem_from_value(val)
- dest_var_list_unexp.append(dest_item)
- # expand relative item paths
- dest_item = self.get_absolutepath(dest_item, KEY_ON_CHANGE).strip()
- # val = 'sh.'+dest_item+'( '+ self.get_stringwithabsolutepathes(val, 'sh.', '(', KEY_ON_CHANGE) +' )'
- val_list_unexpanded.append(val)
- val = self.get_stringwithabsolutepathes(val, 'sh.', '(', KEY_ON_CHANGE)
- # logger.warning("Item __init__: {}: for attr '{}', dest_item '{}', val '{}'".format(self._path, attr, dest_item, val))
- val_list.append(val)
- dest_var_list.append(dest_item)
- setattr(self, '_' + attr + '_unexpanded', val_list_unexpanded)
- setattr(self, '_' + attr, val_list)
- setattr(self, '_' + attr + '_dest_var', dest_var_list)
- setattr(self, '_' + attr + '_dest_var_unexp', dest_var_list_unexp)
- return
-
-
- """
- --------------------------------------------------------------------------------------------
- ---
- --- Properties of the item, that can be accessed from outside of the item class
- ---
- """
-
- class Property:
- """
- Inner class Property of item class.
-
- This class encapsulates all properties that are publicly available
-
- An instance of this class is created in the __init__ method of the item class
- """
-
- def __init__(self, parent):
- self._item = parent
-
- def _ro_error(self):
- prop = inspect.stack()[1][3]
- logger.error("Cannot set readonly property '{}' of item '{}'".format(prop, self._item._path))
- return
-
- def _type_error(self, err):
- prop = inspect.stack()[1][3]
- logger.error("Cannot set property '{}' of item '{}' to a {} value".format(prop, self._item._path, err))
- return
-
- def _cast_warning(self, value):
- prop = inspect.stack()[1][3]
- logger.warning("Casting value '{}' to required type before assigning it to property '{}' of item '{}'".format(value, prop, self._item._path))
- return
-
- @property
- def attributes(self):
- """
- Read-Only Property: attributes. List of plugin-specific attribute names
-
- Available in SmartHomeNG v1.6 and above
-
- :return: path of the item
- :rtype: str
- """
- return list(self._item.conf.keys())
-
-
- def init_dynamic_properties(self):
- """
- Initialize dynamic properties to get the values of plugin-specific attributes
- """
- for confattr in self._item.conf.keys():
- setattr(self, confattr, self.get_config_attribute(confattr))
- return
-
- def get_config_attribute(self, attr):
- return self._item.conf.get(attr, '')
-
-
- @property
- def defined_in(self):
- """
- Read-Only Property: defined_in . The filename in which the item was defined
-
- Available in SmartHomeNG v1.6 and above
-
- :return: path of the item
- :rtype: str
- """
- return self._item._filename
-
- @defined_in.setter
- def defined_in(self, value):
- self._ro_error()
- return
-
-
- @property
- def enforce_updates(self):
- """
- Property: enforce_updates
-
- Available in SmartHomeNG v1.6 and above
-
- :param value: enforce_update state of the item
- :type value: bool
-
- :return: enforce_update state of the item
- :rtype: bool
- """
- return self._item._enforce_updates
-
- @enforce_updates.setter
- def enforce_updates(self, value):
-
- if isinstance(value, bool):
- self._item._enforce_updates = value
- return
- else:
- self._type_error('non-boolean')
- return
-
-
- @property
- def enforce_change(self):
- """
- Property: enforce_change
-
- Available in SmartHomeNG v1.6 and above
-
- :param value: enforce_change state of the item
- :type value: bool
-
- :return: enforce_change state of the item
- :rtype: bool
- """
- return self._item._enforce_change
-
- @enforce_change.setter
- def enforce_change(self, value):
-
- if isinstance(value, bool):
- self._item._enforce_change = value
- return
- else:
- self._type_error('non-boolean')
- return
-
-
- @property
- def eval(self):
- """
- Property: eval expression
-
- Available in SmartHomeNG v1.6 and above
-
- :param value: eval expression of the item
- :type value: str
-
- :return: eval expression of the item
- :rtype: str
- """
- if self._item._eval:
- return self._item._eval
- return ''
-
- @eval.setter
- def eval(self, value):
-
- if isinstance(value, str):
- if value == '':
- self._item._eval = None
- else:
- self._item._eval = value
- return
- else:
- self._type_error('non-non-string')
- return
-
-
- @property
- def eval_unexpanded(self):
- """
- Property: eval expression
-
- Available in SmartHomeNG v1.6 and above
-
- :param value: eval expression of the item
- :type value: str
-
- :return: eval expression of the item
- :rtype: str
- """
- if self._item._eval:
- return self._item._eval
- return ''
-
- @eval_unexpanded.setter
- def eval_unexpanded(self, value):
-
- if isinstance(value, str):
- self._item._lock.acquire()
- self._item._process_eval(value)
- self._item._lock.release()
- return
- else:
- self._type_error('non-non-string')
- return
-
-
- @property
- def last_change(self):
- """
- Read-Only Property: last_change
-
- Available in SmartHomeNG v1.6 and above
-
- :return: path of the item
- :rtype: str
- """
- return self._item._get_last_change()
-
- @last_change.setter
- def last_change(self, value):
- self._ro_error()
- return
-
-
- @property
- def last_change_age(self):
- """
- Read-Only Property: last_change_age
-
- Available in SmartHomeNG v1.6 and above
-
- :return: path of the item
- :rtype: str
- """
- return self._item._get_last_change_age()
-
- @last_change_age.setter
- def last_change_age(self, value):
- self._ro_error()
- return
-
-
- @property
- def last_change_by(self):
- """
- Read-Only Property: last_change_by
-
- Available in SmartHomeNG v1.6 and above
-
- :return: path of the item
- :rtype: str
- """
- return self._item._get_last_change_by()
-
- @last_change_by.setter
- def last_change_by(self, value):
- self._ro_error()
- return
-
-
- @property
- def last_update(self):
- """
- Read-Only Property: last_update
-
- Available in SmartHomeNG v1.6 and above
-
- :return: path of the item
- :rtype: str
- """
- return self._item._get_last_update()
-
- @last_update.setter
- def last_update(self, value):
- self._ro_error()
- return
-
-
- @property
- def last_update_age(self):
- """
- Read-Only Property: last_update_age
-
- Available in SmartHomeNG v1.6 and above
-
- :return: path of the item
- :rtype: str
- """
- return self._item._get_last_update_age()
-
- @last_update_age.setter
- def last_update_age(self, value):
- self._ro_error()
- return
-
-
- @property
- def last_update_by(self):
- """
- Read-Only Property: last_update_by
-
- Available in SmartHomeNG v1.6 and above
-
- :return: path of the item
- :rtype: str
- """
- return self._item._get_last_update_by()
-
- @last_update_by.setter
- def last_update_by(self, value):
- self._ro_error()
- return
-
-
- @property
- def last_value(self):
- """
- Read-Only Property: last_value
-
- Available in SmartHomeNG v1.6 and above
-
- :return: path of the item
- :rtype: str
- """
- return self._item._get_last_value()
-
- @last_value.setter
- def last_value(self, value):
- self._ro_error()
- return
-
-
- @property
- def name(self):
- """
- Property: name
-
- Available in SmartHomeNG v1.6 and above
-
- :param value: name of the item
- :type value: str
-
- :return: name of the item
- :rtype: str
- """
- return self._item._name
-
- @name.setter
- def name(self, value):
-
- if not isinstance(value, str):
- self._cast_warning(value)
- value = '{}'.format(value)
- if value == '':
- self._item._name = self._item._path
- else:
- self._item._name = value
- return
-
-
- @property
- def on_change(self):
- """
- Read-Only Property: on_update
-
- Available in SmartHomeNG v1.6 and above
-
- :return: path of on_update definitions
- :rtype: str
- """
- return self._item._build_on_xx_list(self._item._on_change_dest_var, self._item._on_change)
-
- @on_change.setter
- def on_change(self, value):
- self._ro_error()
- return
-
-
- @property
- def on_change_unexpanded(self):
- """
- Read-Only Property: on_update
-
- Available in SmartHomeNG v1.6 and above
-
- :return: path of on_update definitions
- :rtype: str
- """
- return self._item._build_on_xx_list(self._item._on_change_dest_var_unexp, self._item._on_change_unexpanded)
-
- @on_change_unexpanded.setter
- def on_change_unexpanded(self, value):
- if isinstance(value, str):
- value = [value]
- if isinstance(value, list):
- if value == [] or self._checkstrtype(value):
- self._item._lock.acquire()
- self._item._process_on_xx_list('on_change', value)
- self._item._lock.release()
- else:
- self._type_error('list containing non-string')
- return
- return
- else:
- self._type_error('non-list')
- return
-
- @property
- def on_update(self):
- """
- Read-Only Property: on_update
-
- Available in SmartHomeNG v1.6 and above
-
- :return: path of on_update definitions
- :rtype: str
- """
- return self._item._build_on_xx_list(self._item._on_update_dest_var, self._item._on_update)
-
-
- @on_update.setter
- def on_update(self, value):
- self._ro_error()
- return
-
-
- @property
- def on_update_unexpanded(self):
- """
- Read-Only Property: on_update
-
- Available in SmartHomeNG v1.6 and above
-
- :return: path of on_update definitions
- :rtype: str
- """
- return self._item._build_on_xx_list(self._item._on_update_dest_var_unexp, self._item._on_update_unexpanded)
-
-
- @on_update_unexpanded.setter
- def on_update_unexpanded(self, value):
- if isinstance(value, str):
- value = [value]
- if isinstance(value, list):
- if value == [] or self._checkstrtype(value):
- self._item._lock.acquire()
- self._item._process_on_xx_list('on_update', value)
- self._item._lock.release()
- else:
- self._type_error('list containing non-string')
- return
- return
- else:
- self._type_error('non-list')
- return
-
-
- @property
- def path(self):
- """
- Read-Only Property: path
-
- Available in SmartHomeNG v1.6 and above
-
- :return: path of the item
- :rtype: str
- """
- return self._item._path
-
- @path.setter
- def path(self, value):
- self._ro_error()
- return
-
-
- @property
- def prev_change(self):
- """
- Read-Only Property: prev_change
-
- Available in SmartHomeNG v1.6 and above
-
- :return: path of the item
- :rtype: str
- """
- return self._item._get_prev_change()
-
- @prev_change.setter
- def prev_change(self, value):
- self._ro_error()
- return
-
-
- @property
- def prev_change_age(self):
- """
- Read-Only Property: prev_change_age
-
- Available in SmartHomeNG v1.6 and above
-
- :return: path of the item
- :rtype: str
- """
- return self._item._get_prev_change_age()
-
- @prev_change_age.setter
- def prev_change_age(self, value):
- self._ro_error()
- return
-
-
- @property
- def prev_change_by(self):
- """
- Read-Only Property: prev_change_by
-
- Available in SmartHomeNG v1.6 and above
-
- :return: path of the item
- :rtype: str
- """
- return self._item._get_prev_change_by()
-
- @prev_change_by.setter
- def prev_change_by(self, value):
- self._ro_error()
- return
-
-
- @property
- def prev_update(self):
- """
- Read-Only Property: prev_update
-
- Available in SmartHomeNG v1.6 and above
-
- :return: path of the item
- :rtype: str
- """
- return self._item._get_prev_update()
-
- @prev_update.setter
- def prev_update(self, value):
- self._ro_error()
- return
-
-
- @property
- def prev_update_age(self):
- """
- Read-Only Property: prev_update_age
-
- Available in SmartHomeNG v1.6 and above
-
- :return: path of the item
- :rtype: str
- """
- return self._item._get_prev_update_age()
-
- @prev_update_age.setter
- def prev_update_age(self, value):
- self._ro_error()
- return
-
-
- @property
- def prev_update_by(self):
- """
- Read-Only Property: prev_update_by
-
- Available in SmartHomeNG v1.6 and above
-
- :return: path of the item
- :rtype: str
- """
- return self._item._get_prev_update_by()
-
- @prev_update_by.setter
- def prev_update_by(self, value):
- self._ro_error()
- return
-
-
- @property
- def prev_value(self):
- """
- Read-Only Property: prev_value
-
- Available in SmartHomeNG v1.6 and above
-
- :return: path of the item
- :rtype: str
- """
- return self._item._get_prev_value()
-
- @last_value.setter
- def last_value(self, value):
- self._ro_error()
- return
-
-
- @property
- def trigger(self):
- """
- Property: Triggers of the item
-
- Available in SmartHomeNG v1.6 and above
-
- :param value: list of triggers
- :type value: list
-
- :return: [] if not defined or a list of triggers
- :rtype: list of str
- """
- if self._item._trigger:
- return self._item._trigger
- return []
-
- def _checkstrtype(self, obj):
- return bool(obj) and all(isinstance(elem, str) for elem in obj)
-
- @trigger.setter
- def trigger(self, value):
-
- if isinstance(value, list):
- if value == []:
- self._item._trigger = False
- self._item._trigger_unexpanded = []
- else:
- if self._checkstrtype(value):
- self._item._trigger = value
- self._item._trigger_unexpanded = value
- else:
- self._type_error('list containing non-string')
- return
- return
- else:
- self._type_error('non-list')
- return
-
-
- @property
- def trigger_unexpanded(self):
- """
- Property: Triggers of the item
-
- Available in SmartHomeNG v1.6 and above
-
- :param value: list of triggers
- :type value: list
-
- :return: [] if not defined or a list of triggers
- :rtype: list of str
- """
- if self._item._trigger:
- return self._item._trigger_unexpanded
- return []
-
- @trigger_unexpanded.setter
- def trigger_unexpanded(self, value):
- if isinstance(value, str):
- value = [value]
- if isinstance(value, list):
- if value == [] or self._checkstrtype(value):
- self._item._lock.acquire()
- self._item._process_trigger_list('trigger', value)
- self._item._lock.release()
- else:
- self._type_error('list containing non-string')
- return
- return
- else:
- self._type_error('non-list')
- return
-
-
- @property
- def type(self):
- """
- Read-Only Property: type
-
- Available in SmartHomeNG v1.6 and above
-
- :return: type of the item
- :rtype: str
- """
- return self._item._type
-
- @type.setter
- def type(self, value):
- self._ro_error()
- return
-
-
- @property
- def value(self):
- """
- Property: value
-
- Available in SmartHomeNG v1.6 and above
-
- :param value: value of the item
- :type value:
-
- :return: value of the item
- :rtype:
- """
- return copy.deepcopy(self._item._value)
-
- @value.setter
- def value(self, value):
-
- #self._item.set(value, 'assign property')
- #self._item.__update(value, caller='assign property')
- self._item(value, caller='assign property')
- return
-
-
- """
- ---
- --- End of Properties class
- ---
- --------------------------------------------------------------------------------------------
- """
-
- def _get_last_change(self):
- return self.__last_change
-
- def _get_last_change_age(self):
- delta = self.shtime.now() - self.__last_change
- return delta.total_seconds()
-
- def _get_last_change_by(self):
- return self.__changed_by
-
- def _get_last_update(self):
- return self.__last_update
-
- def _get_last_update_by(self):
- return self.__updated_by
-
- def _get_last_update_age(self):
- delta = self.shtime.now() - self.__last_update
- return delta.total_seconds()
-
- def _get_last_value(self):
- return self.__last_value
-
- def _get_prev_change(self):
- return self.__prev_change
-
- def _get_prev_change_age(self):
- delta = self.__last_change - self.__prev_change
- if delta.total_seconds() < 0.0001:
- return 0.0
- return delta.total_seconds()
-
- def _get_prev_change_by(self):
- return 'N/A'
-
- def _get_prev_update(self):
- return self.__prev_change
-
- def _get_prev_update_age(self):
-
- delta = self.__last_update - self.__prev_update
- if delta.total_seconds() < 0.0001:
- return 0.0
- return delta.total_seconds()
-
- def _get_prev_update_by(self):
- return 'N/A'
-
- def _get_prev_value(self):
- return self.__prev_value
-
-
-
- """
- Following are methods to get attributes of the item
- """
-
- def path(self):
- """
- Path of the item
-
- Available only in SmartHomeNG v1.6, not in versions above
-
- :return: String with the path of the item
- :rtype: str
- """
- return self.property.path
-
- def id(self):
- """
- Old method name - Use item.path() instead of item.id()
- """
- return self.property.path
-
-
- def type(self):
- """
- Datatype of the item
-
- :return: Datatype of the item
- :rtype: str
- """
- return self.property.type
-
-
- def last_change(self):
- """
- Timestamp of last change of item's value
-
- :return: Timestamp of last change
- """
- return self.property.last_change
-
- def age(self):
- """
- Age of the item's actual value. Returns the time in seconds since the last change of the value
-
- :return: Age of the value
- :rtype: int
- """
- return self.property.last_change_age
-
-
- def last_update(self):
- """
- Timestamp of last update of item's value (not necessarily change)
-
- :return: Timestamp of last update
- """
- return self.property.last_update
-
- def update_age(self):
- """
- Update-age of the item's actual value. Returns the time in seconds since the value has been updated (not necessarily changed)
-
- :return: Update-age of the value
- :rtype: int
- """
- return self.property.last_update_age
-
- def prev_change(self):
- """
- Timestamp of the previous (next-to-last) change of item's value
-
- :return: Timestamp of previous change
- """
- return self.property.prev_change
-
- def prev_age(self):
- """
- Age of the item's previous value. Returns the time in seconds the item had the the previous value
-
- :return: Age of the previous value
- :rtype: int
- """
- return self.property.prev_change_age
-
- def prev_update(self):
- """
- Timestamp of previous (next-to-last) update of item's value (not necessarily change)
-
- :return: Timestamp of previous update
- """
- return self.property.prev_update
-
- def prev_update_age(self):
- """
- Update-age of the item's previous value. Returns the time in seconds the previous value existed
- since it had been updated (not necessarily changed)
-
- :return: Update-age of the previous value
- :rtype: int
- """
- return self.property.prev_update_age
-
- def prev_value(self):
- """
- Next-to-last value of the item
-
- :return: Next-to-last value of the item
- """
- return self.property.last_value
-
- def changed_by(self):
- """
- Returns an indication, which plugin, logic or event changed the item's value
-
- :return: Changer of item's value
- :rtype: str
- """
- return self.property.last_change_by
-
- def updated_by(self):
- """
- Returns an indication, which plugin, logic or event updated (not necessarily changed) the item's value
-
- :return: Updater of item's value
- :rtype: str
- """
- return self.property.last_update_by
-
-
- """
- Following are methods to handle relative item paths
- """
-
- def get_absolutepath(self, relativepath, attribute=''):
- """
- Builds an absolute item path relative to the current item
-
- :param relativepath: string with the relative item path
- :param attribute: string with the name of the item's attribute, which contains the relative path (for log entries)
-
- :return: string with the absolute item path
- """
- if not isinstance(relativepath, str):
- return relativepath
- if (len(relativepath) == 0) or ((len(relativepath) > 0) and (relativepath[0] != '.')):
- return relativepath
- relpath = relativepath.rstrip()
- rootpath = self._path
-
- while (len(relpath) > 0) and (relpath[0] == '.'):
- relpath = relpath[1:]
- if (len(relpath) > 0) and (relpath[0] == '.'):
- if rootpath.rfind('.') == -1:
- if rootpath == '':
- relpath = ''
- logger.error(
- "{}.get_absolutepath(): Relative path trying to access above root level on attribute '{}'".format(
- self._path, attribute))
- else:
- rootpath = ''
- else:
- rootpath = rootpath[:rootpath.rfind('.')]
-
- if relpath != '':
- if rootpath != '':
- rootpath += '.' + relpath
- else:
- rootpath = relpath
- logger.info(
- "{}.get_absolutepath('{}'): Result = '{}' (for attribute '{}')".format(self._path, relativepath, rootpath,
- attribute))
- if rootpath[-5:] == '.self':
- rootpath = rootpath.replace('.self', '')
- rootpath = rootpath.replace('.self.', '.')
- return rootpath
-
- def expand_relativepathes(self, attr, begintag, endtag):
- """
- converts a configuration attribute containing relative item paths
- to absolute paths
-
- The item's attribute can be of type str or list (of strings)
-
- The begintag and the endtag remain in the result string!
-
- :param attr: Name of the attribute. Use * as a wildcard at the end
- :param begintag: string or list of strings that signals the beginning of a relative path is following
- :param endtag: string or list of strings that signals the end of a relative path
-
- """
- def __checkforentry(attr):
- if isinstance(self.conf[attr], str):
- if (begintag != '') and (endtag != ''):
- self.conf[attr] = self.get_stringwithabsolutepathes(self.conf[attr], begintag, endtag, attr)
- elif (begintag == '') and (endtag == ''):
- self.conf[attr] = self.get_absolutepath(self.conf[attr], attr)
- elif isinstance(self.conf[attr], list):
- logger.debug("expand_relativepathes(1): to expand={}".format(self.conf[attr]))
- new_attr = []
- for a in self.conf[attr]:
- # Convert accidentally wrong dict entries to string
- if isinstance(a, dict):
- a = list("{!s}:{!s}".format(k,v) for (k,v) in a.items())[0]
- logger.debug("expand_relativepathes: before : to expand={}".format(a))
- if (begintag != '') and (endtag != ''):
- a = self.get_stringwithabsolutepathes(a, begintag, endtag, attr)
- elif (begintag == '') and (endtag == ''):
- a = self.get_absolutepath(a, attr)
- logger.debug("expand_relativepathes: after: to expand={}".format(a))
- new_attr.append(a)
- self.conf[attr] = new_attr
- logger.debug("expand_relativepathes(2): expanded={}".format(self.conf[attr]))
- else:
- logger.warning("expand_relativepathes: attr={} can not expand for type(self.conf[attr])={}".format(attr, type(self.conf[attr])))
-
- # Check if wildcard is used
- if isinstance(attr, str) and attr[-1:] == "*":
- for entry in self.conf:
- if attr[:-1] in entry:
- __checkforentry(entry)
- elif attr in self.conf:
- __checkforentry(attr)
- return
-
-
- def get_stringwithabsolutepathes(self, evalstr, begintag, endtag, attribute=''):
- """
- converts a string containing relative item paths
- to a string with absolute item paths
-
- The begintag and the endtag remain in the result string!
-
- :param evalstr: string with the statement that may contain relative item paths
- :param begintag: string that signals the beginning of a relative path is following
- :param endtag: string that signals the end of a relative path
- :param attribute: string with the name of the item's attribute, which contains the relative path
-
- :return: string with the statement containing absolute item paths
- """
- def __checkfortags(evalstr, begintag, endtag):
- pref = ''
- rest = evalstr
- while (rest.find(begintag+'.') != -1):
- pref += rest[:rest.find(begintag+'.')+len(begintag)]
- rest = rest[rest.find(begintag+'.')+len(begintag):]
- if endtag == '':
- rel = rest
- rest = ''
- else:
- rel = rest[:rest.find(endtag)]
- rest = rest[rest.find(endtag):]
- pref += self.get_absolutepath(rel, attribute)
-
- pref += rest
- logger.debug("{}.get_stringwithabsolutepathes('{}') with begintag = '{}', endtag = '{}': result = '{}'".format(
- self._path, evalstr, begintag, endtag, pref))
- return pref
-
- if not isinstance(evalstr, str):
- return evalstr
-
- if isinstance(begintag, list):
- # Fill end or begintag with empty tags if list length is not equal
- diff_len = len(begintag) - len(endtag)
- begintag = begintag + [''] * abs(diff_len) if diff_len < 0 else begintag
- endtag = endtag + [''] * diff_len if diff_len > 0 else endtag
- for i, _ in enumerate(begintag):
- if not evalstr.find(begintag[i]+'.') == -1:
- evalstr = __checkfortags(evalstr, begintag[i], endtag[i])
- pref = evalstr
- else:
- if evalstr.find(begintag+'.') == -1:
- return evalstr
- pref = __checkfortags(evalstr, begintag, endtag)
- return pref
-
-
- def _get_attr_from_parent(self, attr):
- """
- Get value from parent
-
- :param attr: Get the value from this attribute of the parent item
- :return: value from attribute of parent item
- """
- pitem = self.return_parent()
- pattr_value = pitem.conf.get(attr, '')
- # logger.warning("_get_attr_from_parent Item {}: for attr '{}'".format(self._path, attr))
- # logger.warning("_get_attr_from_parent Item {}: for parent '{}', pattr_value '{}'".format(self._path, pitem._path, pattr_value))
- return pattr_value
-
-
- def _get_attr_from_grandparent(self, attr):
- """
- Get value from grandparent
-
- :param attr: Get the value from this attribute of the grandparent item
- :return: value from attribute of grandparent item
- """
- pitem = self.return_parent()
- gpitem = pitem.return_parent()
- gpattr_value = pitem.get(attr, '')
-# logger.warning("_get_attr_from_grandparent Item {}: for attr '{}'".format(self._path, attr))
-# logger.warning("_get_attr_from_grandparent Item {}: for grandparent '{}', gpattr_value '{}'".format(self._path, gpitem._path, gpattr_value))
- return gpattr_value
-
-
- def _build_trigger_condition_eval(self, trigger_condition):
- """
- Build conditional eval expression from trigger_condition attribute
-
- :param trigger_condition: list of condition dicts
- :return:
- """
- wrk_eval = []
- for or_cond in trigger_condition:
- for ckey in or_cond:
- if ckey.lower() == 'value':
- pass
- else:
- and_cond = []
- for cond in or_cond[ckey]:
- wrk = cond
- if (wrk.find('=') != -1) and (wrk.find('==') == -1) and \
- (wrk.find('<=') == -1) and (wrk.find('>=') == -1) and \
- (wrk.find('=<') == -1) and (wrk.find('=>') == -1):
- wrk = wrk.replace('=', '==')
-
- p = wrk.lower().find('true')
- if p != -1:
- wrk = wrk[:p]+'True'+wrk[p+4:]
- p = wrk.lower().find('false')
- if p != -1:
- wrk = wrk[:p]+'False'+wrk[p+5:]
-
- # expand relative item paths
- wrk = self.get_stringwithabsolutepathes(wrk, 'sh.', '(', KEY_CONDITION)
-
- and_cond.append(wrk)
-
- wrk = ') and ('.join(and_cond)
- if len(or_cond[ckey]) > 1:
- wrk = '(' + wrk + ')'
- wrk_eval.append(wrk)
-
- # wrk_eval.append(str(or_cond[ckey]))
- result = ') or ('.join(wrk_eval)
-
- if len(trigger_condition) > 1:
- result = '(' + result + ')'
-
- return result
-
-
- def __call__(self, value=None, caller='Logic', source=None, dest=None):
- if value is None or self._type is None:
- return copy.deepcopy(self._value)
- if self._eval:
- args = {'value': value, 'caller': caller, 'source': source, 'dest': dest}
- self._sh.trigger(name=self._path + '-eval', obj=self.__run_eval, value=args, by=caller, source=source, dest=dest)
- else:
- self.__update(value, caller, source, dest)
-
- def __iter__(self):
- for child in self.__children:
- yield child
-
- def __setitem__(self, item, value):
- vars(self)[item] = value
-
- def __getitem__(self, item):
- return vars(self)[item]
-
- def __bool__(self):
- return bool(self._value)
-
- def __str__(self):
- return self._name
-
- def __repr__(self):
- return "Item: {}".format(self._path)
-
-
- def _init_prerun(self):
- """
- Build eval expressions from special functions and triggers before first run
-
- Called from Items.load_itemdefinitions
- """
- if self._trigger:
- # Only if item has an eval_trigger
- _items = []
- for trigger in self._trigger:
- if _items_instance.match_items(trigger) == [] and self._eval:
- logger.warning("item '{}': trigger item '{}' not found for function '{}'".format(self._path, trigger, self._eval))
- _items.extend(_items_instance.match_items(trigger))
- for item in _items:
- if item != self: # prevent loop
- item._items_to_trigger.append(self)
- if self._eval:
- # Build eval statement from trigger items (joined by given function)
- items = ['sh.' + str(x.id()) + '()' for x in _items]
- if self._eval == 'and':
- self._eval = ' and '.join(items)
- elif self._eval == 'or':
- self._eval = ' or '.join(items)
- elif self._eval == 'sum':
- self._eval = ' + '.join(items)
- elif self._eval == 'avg':
- self._eval = '({0})/{1}'.format(' + '.join(items), len(items))
- elif self._eval == 'max':
- self._eval = 'max({0})'.format(','.join(items))
- elif self._eval == 'min':
- self._eval = 'min({0})'.format(','.join(items))
-
-
- def _init_start_scheduler(self):
- """
- Start schedulers of the items which have a crontab or a cycle attribute
-
- up to version 1.5 of SmartHomeNG the schedulers were started when initializing the item. That
- could lead to a scheduler to fire a routine, which references an item which is not yet initialized
- :return:
- """
-
- #############################################################
- # Crontab/Cycle
- #############################################################
- if self._crontab is not None or self._cycle is not None:
- cycle = self._cycle
- if cycle is not None:
- cycle = self._build_cycledict(cycle)
- self._sh.scheduler.add(self._itemname_prefix+self._path, self, cron=self._crontab, cycle=cycle)
-
- return
-
-
- def _init_run(self):
- """
- Run initial eval to set an initial value for the item
-
- Called from Items.load_itemdefinitions
- """
- if self._trigger:
- # Only if item has an eval_trigger
- if self._eval:
- # Only if item has an eval expression
- self._sh.trigger(name=self._path, obj=self.__run_eval, by='Init', value={'value': self._value, 'caller': 'Init'})
-
-
- def __run_eval(self, value=None, caller='Eval', source=None, dest=None):
- """
- evaluate the 'eval' entry of the actual item
- """
- if self._eval:
- # Test if a conditional trigger is defined
- if self._trigger_condition is not None:
-# logger.warning("Item {}: Evaluating trigger condition {}".format(self._path, self._trigger_condition))
- try:
- sh = self._sh
- cond = eval(self._trigger_condition)
- logger.warning("Item {}: Condition result '{}' evaluating trigger condition {}".format(self._path, cond, self._trigger_condition))
- except Exception as e:
- logger.warning("Item {}: problem evaluating trigger condition {}: {}".format(self._path, self._trigger_condition, e))
- return
- else:
- cond = True
-
- if cond == True:
- # if self._path == 'wohnung.flur.szenen_helper':
- # logger.info("__run_eval: item = {}, value = {}, self._eval = {}".format(self._path, value, self._eval))
- sh = self._sh # noqa
- shtime = self.shtime
-
- import math as mymath
- try:
- value = eval(self._eval)
- except Exception as e:
- logger.warning("Item {}: problem evaluating {}: {}".format(self._path, self._eval, e))
- else:
- if value is None:
- logger.debug("Item {}: evaluating {} returns None".format(self._path, self._eval))
- else:
- if self._path == 'wohnung.flur.szenen_helper':
- logger.info("__run_eval: item = {}, value = {}".format(self._path, value))
- self.__update(value, caller, source, dest)
-
-
- # New for on_update / on_change
- def _run_on_xxx(self, path, value, on_dest, on_eval, attr='?'):
- """
- common method for __run_on_update and __run_on_change
-
- :param path: path to this item
-
- :param attr: Descriptive text for origin of update of item
- :type: path: str
-
- :type attr: str
- """
- if self._path == 'wohnung.flur.szenen_helper':
- logger.info("_run_on_xxx: item = {}, value = {}".format(self._path, value))
- sh = self._sh
- logger.info("Item {}: '{}' evaluating {} = {}".format(self._path, attr, on_dest, on_eval))
- try:
- dest_value = eval(on_eval) # calculate to test if expression computes and see if it computes to None
- except Exception as e:
- logger.warning("Item {}: '{}' item-value='{}' problem evaluating {}: {}".format(self._path, attr, value, on_eval, e))
- else:
- if dest_value is not None:
- # expression computes and does not result in None
- if on_dest != '':
- dest_item = _items_instance.return_item(on_dest)
- if dest_item is not None:
- dest_item.__update(dest_value, caller=attr, source=self._path)
- logger.debug(" - : '{}' finally evaluating {} = {}, result={}".format(attr, on_dest, on_eval, dest_value))
- else:
- logger.error("Item {}: '{}' has not found dest_item '{}' = {}, result={}".format(self._path, attr, on_dest, on_eval, dest_value))
- else:
- dummy = eval(on_eval)
- logger.debug(" - : '{}' finally evaluating {}, result={}".format(attr, on_eval, dest_value))
- else:
- logger.debug(" - : '{}' {} not set (cause: eval=None)".format(attr, on_dest))
- pass
-
-
- def __run_on_update(self, value=None):
- """
- evaluate all 'on_update' entries of the actual item
- """
- if self._on_update:
- sh = self._sh # noqa
-# logger.info("Item {}: 'on_update' evaluating {} = {}".format(self._path, self._on_update_dest_var, self._on_update))
- for on_update_dest, on_update_eval in zip(self._on_update_dest_var, self._on_update):
- self._run_on_xxx(self._path, value, on_update_dest, on_update_eval, 'on_update')
-
-
- def __run_on_change(self, value=None):
- """
- evaluate all 'on_change' entries of the actual item
- """
- if self._on_change:
- sh = self._sh # noqa
-# logger.info("Item {}: 'on_change' evaluating lists {} = {}".format(self._path, self._on_change_dest_var, self._on_change))
- for on_change_dest, on_change_eval in zip(self._on_change_dest_var, self._on_change):
- self._run_on_xxx(self._path, value, on_change_dest, on_change_eval, 'on_change')
-
-
- def __trigger_logics(self, source_details=None):
- source={'item': self._path, 'details': source_details}
- for logic in self.__logics_to_trigger:
-# logic.trigger(by='Item', source=self._path, value=self._value)
- logic.trigger(by='Item', source=source, value=self._value)
-
- # logic.trigger(by='Logic', source=None, value=None, dest=None, dt=None):
-
- def __update(self, value, caller='Logic', source=None, dest=None):
- try:
- value = self.cast(value)
- except:
- try:
- logger.warning('Item {}: value "{}" does not match type {}. Via {} {}'.format(self._path, value, self._type, caller, source))
- except:
- pass
- return
- self._lock.acquire()
- _changed = False
- self.__prev_update = self.__last_update
- self.__last_update = self.shtime.now()
- self.__updated_by = "{0}:{1}".format(caller, source)
- trigger_source_details = self.__updated_by
- if value != self._value or self._enforce_change:
- _changed = True
- self.__prev_value = self.__last_value
- self.__last_value = self._value
- self._value = value
- self.__prev_change = self.__last_change
- self.__last_change = self.__last_update
- self.__changed_by = "{0}:{1}".format(caller, source)
- trigger_source_details = self.__changed_by
- if caller != "fader":
- self._fading = False
- self._lock.notify_all()
- self._change_logger("Item {} = {} via {} {} {}".format(self._path, value, caller, source, dest))
- if self._log_change_logger is not None:
- log_src = ''
- if source is not None:
- log_src += ' (' + source + ')'
- log_dst = ''
- if dest is not None:
- log_dst += ', dest: ' + dest
- self._log_change_logger.info("Item Change: {} = {} - caller: {}{}{}".format(self._path, value, caller, log_src, log_dst))
- self._lock.release()
- # ms: call run_on_update() from here
- self.__run_on_update(value)
- if _changed or self._enforce_updates or self._type == 'scene':
- # ms: call run_on_change() from here
- self.__run_on_change(value)
- for method in self.__methods_to_trigger:
- try:
- method(self, caller, source, dest)
- except Exception as e:
- logger.exception("Item {}: problem running {}: {}".format(self._path, method, e))
- if self._threshold and self.__logics_to_trigger:
- if self.__th_crossed and self._value <= self.__th_low: # cross lower bound
- self.__th_crossed = False
- self._threshold_data[2] = self.__th_crossed
- self.__trigger_logics(trigger_source_details)
- elif not self.__th_crossed and self._value >= self.__th_high: # cross upper bound
- self.__th_crossed = True
- self._threshold_data[2] = self.__th_crossed
- self.__trigger_logics(trigger_source_details)
- elif self.__logics_to_trigger:
- self.__trigger_logics(trigger_source_details)
- for item in self._items_to_trigger:
- args = {'value': value, 'source': self._path}
- self._sh.trigger(name=item.id(), obj=item.__run_eval, value=args, by=caller, source=source, dest=dest)
- if _changed and self._cache and not self._fading:
- try:
- _cache_write(self._cache, self._value)
- except Exception as e:
- logger.warning("Item: {}: could update cache {}".format(self._path, e))
- if self._autotimer and caller != 'Autotimer' and not self._fading:
-
- _time, _value = self._autotimer[0]
- compat = self._autotimer[1]
- if self._autotimer[2]:
- try:
- _time = eval('self._sh.'+self._autotimer[2]+'()')
- except:
- logger.warning("Item '{}': Attribute 'autotimer': Item '{}' does not exist".format(self._path, self._autotimer[2]))
- if self._autotimer[3]:
- try:
- _value = self._castvalue_to_itemtype(eval('self._sh.'+self._autotimer[3]+'()'), compat)
- except:
- logger.warning("Item '{}': Attribute 'autotimer': Item '{}' does not exist".format(self._path, self._autotimer[3]))
- self._autotimer[0] = (_time, _value) # for display of active/last timer configuration in backend
-
- next = self.shtime.now() + datetime.timedelta(seconds=_time)
- self._sh.scheduler.add(self._itemname_prefix+self.id() + '-Timer', self.__call__, value={'value': _value, 'caller': 'Autotimer'}, next=next)
-
-
- def add_logic_trigger(self, logic):
- """
- Add a logic trigger to the item
-
- :param logic:
- :type logic:
- :return:
- """
- self.__logics_to_trigger.append(logic)
-
- def remove_logic_trigger(self, logic):
- self.__logics_to_trigger.remove(logic)
-
- def get_logic_triggers(self):
- """
- Returns a list of logics to trigger, if the item gets changed
-
- :return: Logics to trigger
- :rtype: list
- """
- return self.__logics_to_trigger
-
- def add_method_trigger(self, method):
- self.__methods_to_trigger.append(method)
-
- def remove_method_trigger(self, method):
- self.__methods_to_trigger.remove(method)
-
- def get_method_triggers(self):
- """
- Returns a list of item methods to trigger, if this item gets changed
-
- :return: methods to trigger
- :rtype: list
- """
- return self.__methods_to_trigger
-
-
- def autotimer(self, time=None, value=None, compat=ATTRIB_COMPAT_V12):
- if time is not None and value is not None:
- self._autotimer = [(time, value), compat, None, None]
- else:
- self._autotimer = False
-
- def fade(self, dest, step=1, delta=1):
- dest = float(dest)
- self._sh.trigger(self._path, _fadejob, value={'item': self, 'dest': dest, 'step': step, 'delta': delta})
-
- def remove_timer(self):
- self._sh.scheduler.remove(self._itemname_prefix+self.id() + '-Timer')
-
- def return_children(self):
- for child in self.__children:
- yield child
-
- def return_parent(self):
- return self.__parent
-
- def set(self, value, caller='Logic', source=None, dest=None, prev_change=None, last_change=None):
- try:
- value = self.cast(value)
- except:
- try:
- logger.warning("Item {}: value {} does not match type {}. Via {} {}".format(self._path, value, self._type, caller, source))
- except:
- pass
- return
- self._lock.acquire()
- self._value = value
- if prev_change is None:
- self.__prev_change = self.__last_change
- else:
- self.__prev_change = prev_change
- if last_change is None:
- self.__last_change = self.shtime.now()
- else:
- self.__last_change = last_change
- self.__changed_by = "{0}:{1}".format(caller, None)
- self.__updated_by = "{0}:{1}".format(caller, None)
- self._lock.release()
- self._change_logger("Item {} = {} via {} {} {}".format(self._path, value, caller, source, dest))
-
- def timer(self, time, value, auto=False, compat=ATTRIB_COMPAT_DEFAULT):
- time = self._cast_duration(time)
- value = self._castvalue_to_itemtype(value, compat)
- if auto:
- caller = 'Autotimer'
- self._autotimer = [(time, value), compat, None, None]
- else:
- caller = 'Timer'
- next = self.shtime.now() + datetime.timedelta(seconds=time)
- self._sh.scheduler.add(self._itemname_prefix+self.id() + '-Timer', self.__call__, value={'value': value, 'caller': caller}, next=next)
-
- def get_children_path(self):
- return [item._path
- for item in self.__children]
-
- def jsonvars(self):
- """
- Translation method from object members to json
- :return: Key / Value pairs from object members
- """
- return { "id": self._path,
- "name": self._name,
- "value" : self._value,
- "type": self._type,
- "attributes": self.conf,
- "children": self.get_children_path() }
-
-# alternative method to get all class members
-# @staticmethod
-# def get_members(instance):
-# return {k: v
-# for k, v in vars(instance).items()
-# if str(k) in ["_value", "conf"] }
-# #if not str(k).startswith('_')}
-
- def to_json(self):
- return json.dumps(self.jsonvars(), sort_keys=True, indent=2)
-
-
-
-#####################################################################
-# Cast Methods
-#####################################################################
-
-def _cast_str(value):
- if isinstance(value, str):
- return value
- else:
- raise ValueError
-
-
-def _cast_list(value):
- if isinstance(value, str):
- try:
- value = literal_eval(value)
- except:
- pass
- if isinstance(value, list):
- return value
- else:
- raise ValueError
-
-
-def _cast_dict(value):
- if isinstance(value, str):
- try:
- value = literal_eval(value)
- except:
- pass
- if isinstance(value, dict):
- return value
- else:
- raise ValueError
-
-
-def _cast_foo(value):
- return value
-
-
-# TODO: Candidate for Utils.to_bool()
-# write testcase and replace
-# -> should castng be restricted like this or handled exactly like Utils.to_bool()?
-# Example: _cast_bool(2) is False, Utils.to_bool(2) is True
-
-def _cast_bool(value):
- if type(value) in [bool, int, float]:
- if value in [False, 0]:
- return False
- elif value in [True, 1]:
- return True
- else:
- raise ValueError
- elif type(value) in [str, str]:
- if value.lower() in ['0', 'false', 'no', 'off', '']:
- return False
- elif value.lower() in ['1', 'true', 'yes', 'on']:
- return True
- else:
- raise ValueError
- else:
- raise TypeError
-
-
-def _cast_scene(value):
- return int(value)
-
-
-def _cast_num(value):
- """
- cast a passed value to int or float
-
- :param value: numeric value to be casted, passed as str, float or int
- :return: numeric value, passed as int or float
- """
- if isinstance(value, str):
- value = value.strip()
- if value == '':
- return 0
- if isinstance(value, float):
- return value
- try:
- return int(value)
- except:
- pass
- try:
- return float(value)
- except:
- pass
- raise ValueError
-
-
-#####################################################################
-# Methods for handling of duration_value strings
-#####################################################################
-
-def _split_duration_value_string(value):
- """
- splits a duration value string into its three components
-
- components are:
- - time
- - value
- - compat
-
- :param value: raw attribute string containing duration, value (and compatibility)
- :return: three strings, representing time, value and compatibility attribute
- """
- time, __, value = value.partition('=')
- value, __, compat = value.partition('=')
- time = time.strip()
- value = value.strip()
- # remove quotes, if present
- if value != '' and ((value[0] == "'" and value[-1] == "'") or (value[0] == '"' and value[-1] == '"')):
- value = value[1:-1]
- compat = compat.strip().lower()
- if compat == '':
- compat = ATTRIB_COMPAT_DEFAULT
- return (time, value, compat)
-
-
-def _join_duration_value_string(time, value, compat=''):
- """
- joins a duration value string from its thre components
-
- components are:
- - time
- - value
- - compat
-
- :param time: time (duration) parrt for the duration_value_string
- :param value: value (duration) parrt for the duration_value_string
- """
- result = str(time)
- if value != '' or compat != '':
- result = result + ' ='
- if value != '':
- result = result + ' ' + value
- if compat != '':
- result = result + ' = ' + compat
- return result
-
-
-#####################################################################
-# Cache Methods
-#####################################################################
-
-def json_serialize(obj):
- """
- helper method to convert values to json serializable formats
- """
- import datetime
- if isinstance(obj, datetime.datetime):
- return obj.isoformat()
- if isinstance(obj, datetime.date):
- return obj.isoformat()
- raise TypeError("Type not serializable")
-
-def json_obj_hook(json_dict):
- """
- helper method for json deserialization
- """
- import dateutil
- for (key, value) in json_dict.items():
- try:
- json_dict[key] = dateutil.parser.parse(value)
- except Exception as e :
- pass
- return json_dict
-
-
-def _cache_read(filename, tz, cformat=CACHE_FORMAT):
- ts = os.path.getmtime(filename)
- dt = datetime.datetime.fromtimestamp(ts, tz)
- value = None
-
- if cformat == CACHE_PICKLE:
- with open(filename, 'rb') as f:
- value = pickle.load(f)
-
- elif cformat == CACHE_JSON:
- with open(filename, 'r', encoding='UTF-8') as f:
- value = json.load(f, object_hook=json_obj_hook)
-
- return (dt, value)
-
-def _cache_write(filename, value, cformat=CACHE_FORMAT):
- try:
- if cformat == CACHE_PICKLE:
- with open(filename, 'wb') as f:
- pickle.dump(value,f)
-
- elif cformat == CACHE_JSON:
- with open(filename, 'w', encoding='UTF-8') as f:
- json.dump(value,f, default=json_serialize)
- except IOError:
- logger.warning("Could not write to {}".format(filename))
-
-
-#####################################################################
-# Fade Method
-#####################################################################
-def _fadejob(item, dest, step, delta):
- if item._fading:
- return
- else:
- item._fading = True
- if item._value < dest:
- while (item._value + step) < dest and item._fading:
- item(item._value + step, 'fader')
- item._lock.acquire()
- item._lock.wait(delta)
- item._lock.release()
- else:
- while (item._value - step) > dest and item._fading:
- item(item._value - step, 'fader')
- item._lock.acquire()
- item._lock.wait(delta)
- item._lock.release()
- if item._fading:
- item._fading = False
- item(dest, 'Fader')
diff --git a/lib/log.py b/lib/log.py
index db1a364433..da4562f15c 100644
--- a/lib/log.py
+++ b/lib/log.py
@@ -1,9 +1,10 @@
#!/usr/bin/env python3
# vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab
#########################################################################
-# Copyright 2012-2013 Marcus Popp marcus@popp.mx
+# Copyright 2016-2021 Martin Sinn m.sinn@gmx.de
+# Parts Copyright 2013 Marcus Popp marcus@popp.mx
#########################################################################
-# This file is part of SmartHomeNG.
+# This file is part of SmartHomeNG.
#
# SmartHomeNG is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -19,39 +20,191 @@
# along with SmartHomeNG. If not, see .
#########################################################################
-import collections
import time
+import logging
+import logging.handlers
+import os
+import datetime
+
+import collections
+
+
+logs_instance = None
+
+
+class Logs():
+
+ _logs = {}
+ root_handler_name = ''
+
+
+ def __init__(self, sh):
+
+ self.logger = logging.getLogger(__name__)
+
+ global logs_instance
+ if logs_instance is None:
+ logs_instance = self
+ else:
+ self.logger.error(f"Another instance of Logs class already exists: {logs_instance}")
+
+ self._sh = sh
+
+ return
+
+
+ def configure_logging(self, config_dict):
+
+ if config_dict == None:
+ print()
+ print("ERROR: Invalid logging configuration in file 'logging.yaml'")
+ print()
+ exit(1)
+
+ # if logger 'lib.smarthome' is not defined or no level is defined for it,
+ # define logger with level 'NOTICE'
+ if config_dict['loggers'].get('lib.smarthome', None) is None:
+ config_dict['loggers']['lib.smarthome'] = {}
+ if config_dict['loggers']['lib.smarthome'].get('level', None) is None:
+ config_dict['loggers']['lib.smarthome']['level'] = 'NOTICE'
+
+ try:
+ root_handler_name = config_dict['root']['handlers'][0]
+ root_handler = config_dict['handlers'][ root_handler_name ]
+ root_handler_level = root_handler.get('level', None)
+ self.root_handler_name = root_handler_name
+ except:
+ root_handler_level = '?'
+
+ if root_handler_level.upper() in ['NOTICE', 'INFO', 'DEBUG']:
+ notice_level = 29
+ else:
+ notice_level = 31
+
+ self.add_logging_level('NOTICE', notice_level)
+ try:
+ logging.config.dictConfig(config_dict)
+ except Exception as e:
+ #self._logger_main.error(f"Invalid logging configuration in file 'logging.yaml' - Exception: {e}")
+ print()
+ print("ERROR: Invalid logging configuration in file 'logging.yaml'")
+ print(f" Exception: {e}")
+ print()
+ exit(1)
+
+ #self.logger.notice(f"Logs.configure_logging: Level NOTICE = {notice_level} / root_handler_level={root_handler_level}")
+
+ # Initialize MemLog Handler to output root log entries to smartVISU
+ self.initMemLog()
+
+ return
+
+
+ def add_logging_level(self, description, value):
+ """
+ Adds a new Logging level to the standard python logging
+
+ :param description: appearance within logs SYSINFO
+ :type description: string
+ :param value: numeric value for the logging level
+ :type value: int
+ :param tocall: function name to call for a log with the given level
+ :type tocall: String, optional, if not given description will be used with lower case
+
+ no error checking is performed here for typos, already existing levels or functions
+ """
+
+ def logForLevel(self, message, *args, **kwargs):
+ if self.isEnabledFor(value):
+ self._log(value, message, args, **kwargs)
+
+ def logToRoot(message, *args, **kwargs):
+ logging.log(value, message, *args, **kwargs)
+
+ logging.addLevelName(value, description)
+ setattr(logging, description, value)
+ setattr(logging.getLoggerClass(), description.lower(), logForLevel)
+ setattr(logging, description.lower(), logToRoot)
+ return
+
+
+ def initMemLog(self):
+ """
+ This function initializes all needed datastructures to use the 'env.core.log' mem-logger and
+ the (old) memlog plugin
+
+ It adds the handler log_mem (based on the custom lib.log.ShngMemLogHandler) to the root logger
+ It logs all WARNINGS from all (old) mem-loggers to the root Logger
+ """
+ log_mem = ShngMemLogHandler('env.core.log', maxlen=50, level=logging.WARNING)
+
+ # define formatter for 'env.core.log' log
+ _logdate = "%Y-%m-%d %H:%M:%S"
+ _logformat = "%(asctime)s %(levelname)-8s %(threadName)-12s %(message)s"
+ formatter = logging.Formatter(_logformat, _logdate)
+ log_mem.setFormatter(formatter)
+
+ # add handler to root logger
+ logging.getLogger('').addHandler(log_mem)
+ return
+
+
+ def add_log(self, name, log):
+ """
+ Adds a log (object) to the list of memory logs
+
+ :param name: Name of log
+ :param log: Log object
+ """
+ self._logs[name] = log
+
+
+ def return_logs(self):
+ """
+ Function to the list of memory logs
+
+ :return: List of logs
+ :rtype: list
+ """
+ return self._logs
+
+# -------------------------------------------------------------------------------
+
class Log(collections.deque):
- def __init__(self, smarthome, name, mapping, maxlen=50):
+ def __init__(self, smarthome, name, mapping, maxlen=40, handler=None):
"""
- Class to implement a log
- This is based on a double ended queue. New entries are appended left and old ones are popped right.
-
-
- As of version 1.7a develop this is used in core at bin/smarthome.py and
+ Class to implement a memory log
+
+ As of shng version 1.7a develop this is used in core at bin/smarthome.py and
in plugins memlog, operationlog and visu_websocket
- :param smarthome: the SmartHomeNG main object
- :param name: a descriptive name for the log
+ :param smarthome: Dummy, for backwart compatibility
+ :param name: name of the the log (used in cli plugin and smartVISU)
:param mapping: Kind of a headline for the entry which can be anything
- e.g. mappings can be [time, thread, level, message ] and log entry is a
+ e.g. mappings can be [time, thread, level, message ] and log entry is a
- :param maxlen: maximum length of the log, defaults to 50
+ :param maxlen: maximum length of the memory log, defaults to 40
+ :param handler: Python LoggingHandler that created this instance of a memory log
"""
collections.deque.__init__(self, maxlen=maxlen)
- self.mapping = mapping
- #self.update_hooks = [] # nowhere else found, maybe not needed any more
- self._sh = smarthome
+ if (mapping is None) or (mapping == []):
+ self.mapping = ['time', 'thread', 'level', 'message']
+ else:
+ self.mapping = mapping
+
+ self._sh = logs_instance._sh
self._name = name
- smarthome.add_log(name, self)
+ self.handler = handler
+ # Add this log to dict of defined memory logs
+ logs_instance.add_log(name, self)
def add(self, entry):
"""
- Just adds a log entry to the left side of the queue. If the queue already holds maxlen
- entries, the rightmost will be discarded automatically.
+ Adds a log entry to the memory log. If the log already has reached the maximum length, the oldest
+ entry is removed from the log automatically.
"""
self.appendleft(entry)
for listener in self._sh.return_event_listeners('log'):
@@ -59,19 +212,27 @@ def add(self, entry):
def last(self, number):
"""
- Returns the last ``number`` entries of the log
+ Returns the newest entries of the log
+
+ :param number: Number of entries to return
+
+ :return: List of log entries
"""
return(list(self)[-number:])
def export(self, number):
"""
- Returns up to ``number`` entries from the log and prepares them together with the mapping
+ Returns the newest entries of the log and prepares them with the mapping
+
+ :param number: Number of entries to return
+
+ :return: List of log entries
"""
return [dict(zip(self.mapping, x)) for x in list(self)[:number]]
def clean(self, dt):
"""
- Assuming dt to be a datetime: remove all entries that are smaller or equal
+ Assuming dt to be a datetime: remove all entries that are smaller or equal
to this given datetime from the right side of the queue
"""
while True:
@@ -82,4 +243,144 @@ def clean(self, dt):
if entry[0] > dt:
self.append(entry)
return
-
+
+
+# ================================================================================
+
+"""
+In the following part of the code, logging handlers are defined
+"""
+
+class ShngTimedRotatingFileHandler(logging.handlers.TimedRotatingFileHandler):
+ """
+ TimedRotatingFilehandler with a different naming scheme for rotated files
+ """
+
+ def getFilesToDelete(self):
+ """
+ Determine the files to delete when rolling over.
+
+ More specific than the earlier method, which just used glob.glob().
+ """
+ dirName, baseName = os.path.split(self.baseFilename)
+ # for changed naming scheme
+ fName, fExt = os.path.splitext(baseName)
+ fileNames = os.listdir(dirName)
+ result = []
+ # for changed naming scheme
+ #prefix = baseName + "."
+ prefix = fName + "."
+ plen = len(prefix)
+ # for changed naming scheme
+ # for fileName in fileNames:
+ # if fileName[:plen] == prefix:
+ # suffix = fileName[plen:]
+ # if self.extMatch.match(suffix):
+ # result.append(os.path.join(dirName, fileName))
+ elen = len(fExt)
+ for fileName in fileNames:
+ if fileName[:plen] == prefix and fileName[-elen:] == fExt:
+ if plen + elen < len(fileName):
+ # ...
+ suffix = fileName[plen:]
+ if self.extMatch.match(suffix):
+ result.append(os.path.join(dirName, fileName))
+ # ...
+ #result.append(os.path.join(dirName, fileName))
+ if len(result) < self.backupCount:
+ result = []
+ else:
+ result.sort()
+ result = result[:len(result) - self.backupCount]
+ return result
+
+ def doRollover(self):
+ """
+ do a rollover; in this case, a date/time stamp is appended to the filename
+ when the rollover happens. However, you want the file to be named for the
+ start of the interval, not the current time. If there is a backup count,
+ then we have to get a list of matching filenames, sort them and remove
+ the one with the oldest suffix.
+ """
+ if self.stream:
+ self.stream.close()
+ self.stream = None
+ # get the time that this sequence started at and make it a TimeTuple
+ currentTime = int(time.time())
+ dstNow = time.localtime(currentTime)[-1]
+ t = self.rolloverAt - self.interval
+ if self.utc:
+ timeTuple = time.gmtime(t)
+ else:
+ timeTuple = time.localtime(t)
+ dstThen = timeTuple[-1]
+ if dstNow != dstThen:
+ if dstNow:
+ addend = 3600
+ else:
+ addend = -3600
+ timeTuple = time.localtime(t + addend)
+
+ # from logging.handlers.TimedRotatingFileHandler
+ #dfn = self.rotation_filename(self.baseFilename + "." +
+ # time.strftime(self.suffix, timeTuple))
+
+ # for shng: splitext -> tuple ( path+fn , ext )
+ bfn = os.path.splitext(self.baseFilename)[0]
+ ext = os.path.splitext(self.baseFilename)[1]
+ dfn = self.rotation_filename(bfn + "." + time.strftime(self.suffix, timeTuple) + ext)
+
+ if os.path.exists(dfn):
+ os.remove(dfn)
+ self.rotate(self.baseFilename, dfn)
+ if self.backupCount > 0:
+ for s in self.getFilesToDelete():
+ os.remove(s)
+ if not self.delay:
+ self.stream = self._open()
+ newRolloverAt = self.computeRollover(currentTime)
+ while newRolloverAt <= currentTime:
+ newRolloverAt = newRolloverAt + self.interval
+ #If DST changes and midnight or weekly rollover, adjust for this.
+ if (self.when == 'MIDNIGHT' or self.when.startswith('W')) and not self.utc:
+ dstAtRollover = time.localtime(newRolloverAt)[-1]
+ if dstNow != dstAtRollover:
+ if not dstNow: # DST kicks in before next rollover, so we need to deduct an hour
+ addend = -3600
+ else: # DST bows out before next rollover, so we need to add an hour
+ addend = 3600
+ newRolloverAt += addend
+ self.rolloverAt = newRolloverAt
+
+
+class ShngMemLogHandler(logging.StreamHandler):
+ """
+ LogHandler used by MemLog
+ """
+ def __init__(self, logname='undefined', maxlen=35, level=logging.NOTSET):
+ super().__init__()
+ self.setLevel(level)
+
+ if self.get_name() is None:
+ # for 'env.core.log' memory logger
+ self.set_name('_shng_root_memory')
+
+ #logs_instance.logger.info(f"ShngMemLogHandler.__init__(): logname={logname}, self={self}, handlername={self.get_name()}, level={self.level}, levelname={logging.getLevelName(self.level)}, maxlen={maxlen}")
+
+ self._log = Log(self, logname, ['time', 'thread', 'level', 'message'], maxlen=maxlen, handler=self)
+
+ self._shtime = logs_instance._sh.shtime
+ # Dummy baseFileName for output in shngadmin (and priv_develop plugin)
+ self.baseFilename = "'" + self._log._name + "'"
+
+ def emit(self, record):
+ #logs_instance.logger.info(f"ShngMemLogHandler.emit() #1: logname={self._log._name}, handlername={self.get_name()}, level={self.level}, record.levelno={record.levelno}, record.levelname={record.levelname}, record={record}")
+ #logs_instance.logger.info(f"ShngMemLogHandler.emit() #2: self={self}, handlers={logging._handlers.data}")
+ try:
+ self.format(record)
+ timestamp = datetime.datetime.fromtimestamp(record.created, self._shtime.tzinfo())
+ self._log.add([timestamp, record.threadName, record.levelname, record.message])
+ except Exception:
+ self.handleError(record)
+
+
diff --git a/lib/logutils.py b/lib/logutils.py
index b2bf9439c1..87169f8378 100644
--- a/lib/logutils.py
+++ b/lib/logutils.py
@@ -106,6 +106,7 @@ def filter(self, record):
#invert is True: show record if all of the given parameters match
return True if hits < total and not self.invert else True if hits >= total and self.invert else False
+
class DuplicateFilter(object):
"""
This class builds a filter to be used in logging.yaml to configure logging
diff --git a/lib/metadata.py b/lib/metadata.py
index c952ae34e9..6c0daaf198 100644
--- a/lib/metadata.py
+++ b/lib/metadata.py
@@ -414,7 +414,6 @@ def test_shngcompatibility(self):
shng_version = l[0]+'.'+l[1]
if len(l) > 2:
shng_version += '.'+l[2]
-
l = str(self.get_string('sh_minversion')).split('.')
min_shngversion = l[0]
if len(l) > 1:
@@ -432,11 +431,15 @@ def test_shngcompatibility(self):
mod_version = self.get_string('version')
if min_shngversion != '':
- if min_shngversion > shng_version:
+ #r = self._compare_versions(min_shngversion, shng_version, '>', (min_shngversion > shng_version))
+ # if min_shngversion > shng_version:
+ if self._compare_versions(min_shngversion, shng_version, '>', (min_shngversion > shng_version)):
logger.error("{0} '{1}': The version {3} of SmartHomeNG is too old for this {0}. It requires at least version v{2}. The {0} was not loaded.".format(self._addon_type, self._addon_name, min_shngversion, shng_version))
return False
if max_shngversion != '':
- if max_shngversion < shng_version:
+ #self._compare_versions(max_shngversion, shng_version, '<', (max_shngversion < shng_version))
+ # if max_shngversion < shng_version:
+ if self._compare_versions(max_shngversion, shng_version, '<', (max_shngversion < shng_version)):
logger.error("{0} '{1}': The version {3} of SmartHomeNG is too new for this {0}. It requires a version up to v{2}. The {0} was not loaded.".format(self._addon_type, self._addon_name, max_shngversion, shng_version))
return False
return True
@@ -463,11 +466,15 @@ def test_pythoncompatibility(self):
mod_version = self.get_string('version')
if min_pyversion != '':
- if min_pyversion > py_version:
+ #self._compare_versions(min_pyversion, py_version, '>', (min_pyversion > py_version))
+ # if min_pyversion > py_version:
+ if self._compare_versions(min_pyversion, py_version, '>', (min_pyversion > py_version)):
logger.error("{0} '{1}': The Python version {3} is too old for this {0}. It requires at least version v{2}. The {0} was not loaded.".format(self._addon_type, self._addon_name, min_pyversion, py_version))
return False
if max_pyversion != '':
- if max_pyversion < py_version:
+ #self._compare_versions(max_pyversion, py_version, '<', (max_pyversion < py_version))
+ # if max_pyversion < py_version:
+ if self._compare_versions(max_pyversion, py_version, '<', (max_pyversion < py_version)):
logger.error("{0} '{1}': The Python version {3} is too new for this {0}. It requires a version up to v{2}. The {0} was not loaded.".format(self._addon_type, self._addon_name, max_pyversion, py_version))
return False
return True
@@ -1164,3 +1171,62 @@ def check_itemattribute(self, item, attribute, value, defined_in_file=None):
return value
+
+ def _compare_versions(self, vers1, vers2, operator, res_old=None):
+ """
+ Compare two version numbers and return if the condition is met
+
+ :param vers1:
+ :param vers2:
+ :param operator:
+ :type vers1: str
+ :type vers2: str
+ :type operator: str
+
+ :return: true if condition is met
+ :rtype: bool
+ """
+ v1 = self._version_to_list(vers1)
+ v2 = self._version_to_list(vers2)
+
+ result = False
+ if v1 == v2 and operator in ['>=', '==', '<=']:
+ result = True
+ if v1 < v2 and operator in ['<', '<=']:
+ result = True
+ if v1 > v2 and operator in ['>', '>=']:
+ result = True
+ #logger.warning(f"_compare_versions: {self._addon_name:12} v1={v1}, v2={v2}, operator='{operator}', result={result}, res_old={res_old}")
+
+ logger.debug("_compare_versions: - - - vers1 = {}, vers2 = {}, v1 = {}, v2 = {}, operator = '{}', result = {}".format(vers1, vers2, v1, v2, operator, result))
+ return result
+
+
+ def _version_to_list(self, vers):
+ """
+ Split version number to list and get rid of non-numeric parts
+
+ :param vers:
+
+ :return: version as list
+ :rtype: list
+ """
+ # create list with [major,minor,revision,build]
+ vsplit = vers.split('.')
+ while len(vsplit) < 4:
+ vsplit.append('0')
+
+ import re
+
+ # get rid of non numeric parts
+ vlist = []
+ for v in vsplit:
+ v = re.findall('\d+', v )[0]
+ vi = 0
+ try:
+ vi = int(v)
+ except:
+ pass
+ vlist.append(vi)
+
+ return vlist
diff --git a/lib/model/mqttplugin.py b/lib/model/mqttplugin.py
index 686e384c4e..a648e49115 100644
--- a/lib/model/mqttplugin.py
+++ b/lib/model/mqttplugin.py
@@ -22,7 +22,7 @@
import threading
from lib.module import Modules
-from lib.model.smartplugin import *
+from lib.model.smartplugin import SmartPlugin
from lib.shtime import Shtime
@@ -67,14 +67,11 @@ def start_subscriptions(self):
Should be called from the run method of a plugin
"""
if self.mod_mqtt:
- # lock
- self._subscribed_topics_lock.acquire()
- for topic in self._subscribed_topics:
- # start subscription to all items for this topic
- for item_path in self._subscribed_topics[topic]:
- self._start_subscription(topic, item_path)
- # unlock
- self._subscribed_topics_lock.release()
+ with self._subscribed_topics_lock:
+ for topic in self._subscribed_topics:
+ # start subscription to all items for this topic
+ for item_path in self._subscribed_topics[topic]:
+ self._start_subscription(topic, item_path)
self._subscriptions_started = True
return
@@ -86,16 +83,13 @@ def stop_subscriptions(self):
Should be called from the stop method of a plugin
"""
if self.mod_mqtt:
- # lock
- self._subscribed_topics_lock.acquire()
- for topic in self._subscribed_topics:
- # stop subscription to all items for this topic
- for item_path in self._subscribed_topics[topic]:
- current = str(self._subscribed_topics[topic][item_path]['current'])
- self.logger.info("stop(): Unsubscribing from topic {} for item {}".format(topic, item_path))
- self.mod_mqtt.unsubscribe_topic(self.get_shortname() + '-' + current, topic)
- # unlock
- self._subscribed_topics_lock.release()
+ with self._subscribed_topics_lock:
+ for topic in self._subscribed_topics:
+ # stop subscription to all items for this topic
+ for item_path in self._subscribed_topics[topic]:
+ current = str(self._subscribed_topics[topic][item_path]['current'])
+ self.logger.info("stop(): Unsubscribing from topic {} for item {}".format(topic, item_path))
+ self.mod_mqtt.unsubscribe_topic(self.get_shortname() + '-' + current, topic)
self._subscriptions_started = False
return
@@ -125,31 +119,27 @@ def add_subscription(self, topic, payload_type, bool_values=None, item=None, cal
:return:
"""
- # lock
- self._subscribed_topics_lock.acquire()
-
- # test if topic is new
- if not self._subscribed_topics.get(topic, None):
- self._subscribed_topics[topic] = {}
- # add this item to topic
- if item is None:
- item_path = '*no_item*'
- else:
- item_path = item.path()
- self._subscribed_topics[topic][item_path] = {}
- self._subscribe_current_number += 1
- self._subscribed_topics[topic][item_path]['current'] = self._subscribe_current_number
- self._subscribed_topics[topic][item_path]['item'] = item
- self._subscribed_topics[topic][item_path]['qos'] = None
- self._subscribed_topics[topic][item_path]['payload_type'] = payload_type
- if callback:
- self._subscribed_topics[topic][item_path]['callback'] = callback
- else:
- self._subscribed_topics[topic][item_path]['callback'] = self._on_mqtt_message
- self._subscribed_topics[topic][item_path]['bool_values'] = bool_values
-
- # unlock
- self._subscribed_topics_lock.release()
+ with self._subscribed_topics_lock:
+
+ # test if topic is new
+ if not self._subscribed_topics.get(topic, None):
+ self._subscribed_topics[topic] = {}
+ # add this item to topic
+ if item is None:
+ item_path = '*no_item*'
+ else:
+ item_path = item.path()
+ self._subscribed_topics[topic][item_path] = {}
+ self._subscribe_current_number += 1
+ self._subscribed_topics[topic][item_path]['current'] = self._subscribe_current_number
+ self._subscribed_topics[topic][item_path]['item'] = item
+ self._subscribed_topics[topic][item_path]['qos'] = None
+ self._subscribed_topics[topic][item_path]['payload_type'] = payload_type
+ if callback:
+ self._subscribed_topics[topic][item_path]['callback'] = callback
+ else:
+ self._subscribed_topics[topic][item_path]['callback'] = self._on_mqtt_message
+ self._subscribed_topics[topic][item_path]['bool_values'] = bool_values
if self._subscriptions_started:
# directly subscribe to added subscription, if subscribtions are started
diff --git a/lib/network.py b/lib/network.py
old mode 100755
new mode 100644
index 15d16d9a76..00458aaed1
--- a/lib/network.py
+++ b/lib/network.py
@@ -3,6 +3,7 @@
#########################################################################
# Parts Copyright 2016 C. Strassburg (lib.utils) c.strassburg@gmx.de
# Copyright 2017- Serge Wagener serge@wagener.family
+# Copyright 2020- Sebastian Helms morg @ knx-user-forum
#########################################################################
# This file is part of SmartHomeNG
#
@@ -21,209 +22,77 @@
#########################################################################
"""
-
-| *** ATTENTION: This is early work in progress. Interfaces are subject to change. ***
-| *** DO NOT USE IN PRODUCTION until you know what you are doing ***
-|
-
-This library contains the future network classes for SmartHomeNG.
+This library contains the network classes for SmartHomeNG.
New network functions and utilities are going to be implemented in this library.
-This classes, functions and methods are mainly meant to be used by plugin developers
+These classes, functions and methods are mainly meant to be used by plugin developers
+- class Network provides utility methods for network-related tasks
+- class Html provides methods for communication with resp. requests to a HTTP server
+- class Tcp_client provides a two-way TCP client implementation
+- class Tcp_server provides a TCP listener with connection / data callbacks
+- class Udp_server provides a UDP listener with data callbacks
"""
+from lib.utils import Utils
+import sys
+import traceback
+import re
import asyncio
-import ipaddress
import logging
-import queue
-import re
import requests
-from iowait import IOWait ### BMX
-import select ### should not be needed
+from iowait import IOWait # BMX
import socket
import struct
import subprocess
import threading
import time
+from . import aioudp
-# Turn off ssl warnings
-requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning)
-logging.getLogger("urllib3").setLevel(logging.WARNING)
-
-class Network(object):
- """ This Class has some usefull static methods that you can use in your projects """
-
- @staticmethod
- def is_mac(mac):
- """
- Validates a MAC address
-
- :param mac: MAC address
- :type string: str
-
- :return: True if value is a MAC
- :rtype: bool
- """
-
- mac = str(mac)
- if len(mac) == 12:
- for c in mac:
- try:
- if int(c, 16) > 15:
- return False
- except:
- return False
- return True
-
- octets = re.split('[\:\-\ ]', mac)
- if len(octets) != 6:
- return False
- for i in octets:
- try:
- if int(i, 16) > 255:
- return False
- except:
- return False
- return True
-
- @staticmethod
- def is_ip(string):
- """
- Checks if a string is a valid ip-address (v4 or v6)
-
- :param string: String to check
- :type string: str
-
- :return: True if an ip, false otherwise.
- :rtype: bool
- """
-
- return (Network.is_ipv4(string) or Network.is_ipv6(string))
-
- @staticmethod
- def is_ipv4(string):
- """
- Checks if a string is a valid ip-address (v4)
-
- :param string: String to check
- :type string: str
-
- :return: True if an ip, false otherwise.
- :rtype: bool
- """
-
- try:
- ipaddress.IPv4Address(string)
- return True
- except ipaddress.AddressValueError:
- return False
-
- @staticmethod
- def is_ipv6(string):
- """
- Checks if a string is a valid ip-address (v6)
-
- :param string: String to check
- :type string: str
-
- :return: True if an ipv6, false otherwise.
- :rtype: bool
- """
-
- try:
- ipaddress.IPv6Address(string)
- return True
- except ipaddress.AddressValueError:
- return False
-
- @staticmethod
- def is_hostname(string):
- """
- Checks if a string is a valid hostname
-
- The hostname has is checked to have a valid format
-
- :param string: String to check
- :type string: str
-
- :return: True if a hostname, false otherwise.
- :rtype: bool
- """
-
- try:
- return bool(re.match("^(([a-zA-Z]|[a-zA-Z][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z]|[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9])$", string))
- except TypeError:
- return False
- @staticmethod
- def get_local_ipv4_address():
- """
- Get's local ipv4 address of the interface with the default gateway.
- Return '127.0.0.1' if no suitable interface is found
+# Turn off ssl warnings from urllib
+requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning)
+logging.getLogger('urllib3').setLevel(logging.WARNING)
- :return: IPv4 address as a string
- :rtype: string
- """
- s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- try:
- s.connect(('8.8.8.8', 1))
- IP = s.getsockname()[0]
- except:
- IP = '127.0.0.1'
- finally:
- s.close()
- return IP
- @staticmethod
- def get_local_ipv6_address():
- """
- Get's local ipv6 address of the interface with the default gateway.
- Return '::1' if no suitable interface is found
+class Network(object):
+ """
+ Provide useful static methods that you can use in your projects.
- :return: IPv6 address as a string
- :rtype: string
- """
- s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
- try:
- s.connect(('2001:4860:4860::8888', 1))
- IP = s.getsockname()[0]
- except:
- IP = '::1'
- finally:
- s.close()
- return IP
+ NOTE: Some format check routines were duplicate with lib.utils. As these primarily check string formats and are used for metadata parsing, they were removed here to prevent duplicates.
+ """
@staticmethod
def ip_port_to_socket(ip, port):
"""
- Returns an ip address plus port to a socket string.
+ Return an ip address plus port to a socket string.
+
Format is 'ip:port' for IPv4 or '[ip]:port' for IPv6
- :return: Socket address / IPEndPoint as string
+ :return: Socket address / IP endpoint as string
:rtype: string
"""
- if Network.is_ipv6(ip):
- ip = '[{}]'.format(ip)
- return '{}:{}'.format(ip, port)
+ if Utils.is_ipv6(ip):
+ ip = f'[{ip}]'
+ return f'{ip}:{port}'
@staticmethod
- def ipver_to_string(ipver):
+ def family_to_string(family):
"""
- Converts a socket address family to an ip version string 'IPv4' or 'IPv6'
+ Convert a socket address family to an ip version string 'IPv4' or 'IPv6'.
- :param ipver: Socket family
- :type ipver: socket.AF_INET or socket.AF_INET6
+ :param family: Socket family
+ :type family: socket.AF_INET or socket.AF_INET6
:return: 'IPv4' or 'IPv6'
:rtype: string
"""
- return 'IPv6' if ipver == socket.AF_INET6 else 'IPv4'
+ return 'IPv6' if family == socket.AF_INET6 else 'IPv4'
@staticmethod
def ping(ip):
"""
- Tries to ICMP ping a host using external OS utilities. Currently IPv4 only.
+ Try to ICMP ping a host using external OS utilities. IPv4 only.
:param ip: IPv4 address as a string
:type ip: string
@@ -232,17 +101,17 @@ def ping(ip):
:rtype: bool
"""
logger = logging.getLogger(__name__)
- if subprocess.call("ping -c 1 %s" % ip, shell=True, stdout=open('/dev/null', 'w'), stderr=subprocess.STDOUT) == 0:
- logger.debug('Ping: {} is online'.format(ip))
+ if subprocess.call(f'ping -c 1 {ip}', shell=True, stdout=open('/dev/null', 'w'), stderr=subprocess.STDOUT) == 0:
+ logger.debug(f'Ping: {ip} is online')
return True
else:
- logger.debug('Ping: {} is offline'.format(ip))
+ logger.debug(f'Ping: {ip} is offline')
return False
@staticmethod
def ping_port(ip, port=80):
"""
- Tries to reach a given TCP port. Currently IPv4 only.
+ Try to reach a given TCP port. IPv4 only.
:param ip: IPv4 address
:param port: Port number
@@ -250,25 +119,25 @@ def ping_port(ip, port=80):
:type ip: string
:type port: int
- :return: True if a reachable, false otherwise.
+ :return: True if reachable, false otherwise.
:rtype: bool
"""
logger = logging.getLogger(__name__)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(2)
if sock.connect_ex((ip, int(port))) == 0:
- logger.debug('Ping: port {} on {} is reachable'.format(port, ip))
+ logger.debug(f'Ping: port {port} on {ip} is reachable')
sock.close()
return True
else:
- logger.debug('Ping: port {} on {} is offline or not reachable'.format(port, ip))
+ logger.debug(f'Ping: port {port} on {ip} is offline or not reachable')
sock.close()
return False
@staticmethod
def send_wol(mac, ip='255.255.255.255'):
"""
- Sends a wake on lan packet to the given mac address using ipv4 broadcast (or directed to specific ip)
+ Send a wake on lan packet to the given mac address using ipv4 broadcast (or directed to specific ip).
:param mac: Mac address to wake up (pure numbers or with any separator)
:type mac: string
@@ -291,30 +160,108 @@ def send_wol(mac, ip='255.255.255.255'):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
sock.sendto(send_data, (ip, 9))
- logger.debug('Sent WOL packet to {}'.format(mac))
+ logger.debug(f'Sent WOL packet to {mac}')
+
+ @staticmethod
+ def validate_inet_addr(addr, port):
+ """
+ Validate that addr:port resolve properly and return resolved IP address and port.
+
+ :param addr: hostname or ip address under test
+ :type addr: str
+ :param port: port number under test
+ :type port: num
+ :return: (ip_address, port, family) or (None, undef, undef) if error occurs
+ :rtype: tuple
+ """
+ logger = logging.getLogger(__name__)
+ # Test if host is empty
+ if addr == '':
+ return ('', port, socket.AF_INET)
+ else:
+ # try to resolve addr to get more info
+ logger.debug(f'trying to resolve addr {addr} with port {port}')
+ try:
+ family, sockettype, proto, canonname, socketaddr = socket.getaddrinfo(addr, None)[0]
+ # Check if resolved address is IPv4 or IPv6
+ if family == socket.AF_INET:
+ ip, _ = socketaddr
+ elif family == socket.AF_INET6:
+ ip, _, flow_info, scope_id = socketaddr
+ else:
+ # might be AF_UNIX or something esoteric?
+ logger.error(f'Unsupported address family {family}')
+ ip = None
+ if ip is not None:
+ logger.info(f'Resolved {addr} to {Network.family_to_string(family)} address {ip}')
+ except socket.gaierror as e:
+ # Unable to resolve hostname
+ logger.error(f'Cannot resolve {addr} to a valid ip address (v4 or v6): {e}')
+ ip = None
+
+ return (ip, port, family)
+
+ @staticmethod
+ def clean_uri(uri, mode='show'):
+ """
+ Check URIs for embedded http/https login data (http://user:pass@domain.tld...) and clean it.
+
+ Possible modes are:
+
+ - 'show': don't change URI (default) -> ``http://user:pass@domain.tld...``
+ - 'mask': replace login data with ``***`` -> ``http://***:***@domain.tld...``
+ - 'strip': remove login data part -> ``http://domain.tld...``
+
+ :param uri: full URI to check and process
+ :param mode: handling mode, one of 'show', 'strip', 'mask'
+ :return: resulting URI string
+
+ :type uri: str
+ :type mode: str
+ :rtype: str
+ """
+ # find login data
+ pattern = re.compile('http([s]?)://([^:]+:[^@]+@)')
+
+ # possible replacement modes
+ replacement = {
+ 'strip': 'http\\g<1>://',
+ 'mask': 'http\\g<1>://***:***@'
+ }
+
+ # if no change requested or no login data found, return unchanged
+ if mode not in replacement or not pattern.match(uri):
+ return uri
+
+ # return appropriately changed URI
+ return pattern.sub(replacement[mode], uri)
class Http(object):
"""
- Creates an instance of the Http class.
+ Provide methods to simplify HTTP connections, especially to talk to HTTP servers.
:param baseurl: base URL used everywhere in this instance (example: http://www.myserver.tld)
:param timeout: Set a maximum amount of seconds the class should try to establish a connection
+ :param hide_login: Hide or mask login data in logged http(s) requests (see ``Network.clean_uri()``)
:type baseurl: str
:type timeout: int
+ :type hide_login: str
"""
- def __init__(self, baseurl='', timeout=10):
+
+ def __init__(self, baseurl='', timeout=10, hide_login='show'):
self.logger = logging.getLogger(__name__)
self.baseurl = baseurl
self._response = None
self.timeout = timeout
self._session = requests.Session()
+ self._hide_login = hide_login
def HTTPDigestAuth(self, user=None, password=None):
"""
- Creates a HTTPDigestAuth instance and returns it to the caller.
+ Create a HTTPDigestAuth instance and returns it to the caller.
:param user: Username
:param password: Password
@@ -329,7 +276,7 @@ def HTTPDigestAuth(self, user=None, password=None):
def post_json(self, url=None, params=None, verify=True, auth=None, json=None, files={}):
"""
- Launches a POST request and returns JSON answer as a dict or None on error.
+ Launch a POST request and return JSON answer as a dict or None on error.
:param url: Optional URL to fetch from. If None (default) use baseurl given on init.
:param params: Optional dict of parameters to add to URL query string.
@@ -348,14 +295,14 @@ def post_json(self, url=None, params=None, verify=True, auth=None, json=None, fi
json = None
try:
json = self._response.json()
- except:
- self.logger.warning("Invalid JSON received from {} !".format(url if url else self.baseurl))
+ except Exception:
+ self.logger.warning(f'Invalid JSON received from {Network.clean_uri(url, self._hide_login) if url else self.baseurl}')
return json
return None
def get_json(self, url=None, params=None, verify=True, auth=None):
"""
- Launches a GET request and returns JSON answer as a dict or None on error.
+ Launch a GET request and return JSON answer as a dict or None on error.
:param url: Optional URL to fetch from. If None (default) use baseurl given on init.
:param params: Optional dict of parameters to add to URL query string.
@@ -374,14 +321,14 @@ def get_json(self, url=None, params=None, verify=True, auth=None):
json = None
try:
json = self._response.json()
- except:
- self.logger.warning("Invalid JSON received from {} !".format(url if url else self.baseurl))
+ except Exception:
+ self.logger.warning(f'Invalid JSON received from {Network.clean_uri(url if url else self.baseurl, self._hide_login) }')
return json
return None
def get_text(self, url=None, params=None, encoding=None, timeout=None):
"""
- Launches a GET request and returns answer as string or None on error.
+ Launch a GET request and return answer as string or None on error.
:param url: Optional URL to fetch from. Default is to use baseurl given to constructor.
:param params: Optional dict of parameters to add to URL query string.
@@ -400,13 +347,13 @@ def get_text(self, url=None, params=None, encoding=None, timeout=None):
if encoding:
self._response.encoding = encoding
_text = self._response.text
- except:
- self.logger.error("Successfull GET, but decoding response failed. This should never happen !")
+ except Exception as e:
+ self.logger.error(f'Successful GET, but decoding response failed. This should never happen...error was: {e}')
return _text
def download(self, url=None, local=None, params=None, verify=True, auth=None):
"""
- Downloads a binary file to a local path
+ Download a binary file to a local path.
:param url: Remote file to download. Attention: Must be full url. 'baseurl' is NOT prefixed here.
:param local: Local file to save
@@ -424,19 +371,20 @@ def download(self, url=None, local=None, params=None, verify=True, auth=None):
:rtype: bool
"""
if self.__get(url=url, params=params, verify=verify, auth=auth, stream=True):
- self.logger.debug("Download of {} successfully completed, saving to {}".format(url, local))
+ self.logger.debug(f'Download of {Network.clean_uri(url, self._hide_login)} successfully completed, saving to {local}')
with open(str(local), 'wb') as f:
for chunk in self._response:
f.write(chunk)
return True
else:
- self.logger.warning("Download error: {}".format(url))
+ self.logger.warning(f'Download error: {Network.clean_uri(url, self._hide_login)}')
return False
def get_binary(self, url=None, params=None):
"""
- Launches a GET request and returns answer as raw binary data or None on error.
- This is usefull for downloading binary objects / files.
+ Launch a GET request and return answer as raw binary data or None on error.
+
+ This is useful for downloading binary objects / files.
:param url: Optional URL to fetch from. Default is to use baseurl given to constructor.
:param params: Optional dict of parameters to add to URL query string.
@@ -452,22 +400,24 @@ def get_binary(self, url=None, params=None):
def response_status(self):
"""
- Returns the status code (200, 404, ...) of the last executed request.
- If GET request was not possible and thus no HTTP statuscode is available the returned status code = 0.
+ Return the status code (200, 404, ...) of the last executed request.
+
+ If GET request was not possible and thus no HTTP statuscode is available,
+ the returned status code is 0.
:return: Status code and text of last request
- :rtype: (int, str)
+ :rtype: tuple(int, str)
"""
try:
(code, reason) = (self._response.status_code, self._response.reason)
- except:
+ except Exception:
code = 0
reason = 'Unable to complete GET request'
return (code, reason)
def response_headers(self):
"""
- Returns a dictionary with the server return headers of the last executed request
+ Return a dictionary with the server return headers of the last executed request.
:return: Headers returned by server
:rtype: dict
@@ -476,7 +426,7 @@ def response_headers(self):
def response_cookies(self):
"""
- Returns a dictionary with the cookies the server may have sent on the last executed request
+ Return a dictionary with the cookies the server may have sent on the last executed request.
:return: Cookies returned by server
:rtype: dict
@@ -485,8 +435,7 @@ def response_cookies(self):
def response_object(self):
"""
- Returns the raw response object for advanced ussage. Use if you know what you are doing.
- Maybe this lib can be extented to your needs instead ?
+ Return the raw response object for advanced ussage.
:return: Reponse object as returned by underlying requests library
:rtype: `requests.Response `_
@@ -494,34 +443,65 @@ def response_object(self):
return self._response
def __post(self, url=None, params=None, timeout=None, verify=True, auth=None, json=None, data=None, files={}):
+ """
+ Send POST request. Non-documented arguments are passed on to requests.request().
+
+ :param url: URL to which to POST
+ :type url: str
+ :param data: data to submit to POST
+ :type data: dict or bytes or file
+
+ :return: True if POST was successful
+ :rtype: bool
+ """
url = self.baseurl + url if url else self.baseurl
timeout = timeout if timeout else self.timeout
data = json if json else data
- self.logger.info("Sending POST request {} to {}".format(json, url))
+ self.logger.info(f'Sending POST request {json} to {Network.clean_uri(url, self._hide_login)}')
try:
self._response = self._session.post(url, params=params, timeout=timeout, verify=verify, auth=auth, data=data, files=files)
- self.logger.debug("{} Posted to URL {}".format(self.response_status(), self._response.url))
+ self.logger.debug(f'{self.response_status()} Posted to URL {Network.clean_uri(self._response.url, self._hide_login)}')
except Exception as e:
- self.logger.warning("Error sending POST request to {}: {}".format(url, e))
+ self.logger.warning(f'Error sending POST request to {Network.clean_uri(url, self._hide_login)}: {e}')
return False
return True
def __get(self, url=None, params=None, timeout=None, verify=True, auth=None, stream=False):
+ """
+ Send POST request. Non-documented arguments are passed on to requests.request().
+
+ :param url: URL to which to GET
+ :type url: str
+
+ :return: True if GET was successful
+ :rtype: bool
+ """
url = self.baseurl + url if url else self.baseurl
timeout = timeout if timeout else self.timeout
- self.logger.info("Sending GET request to {}".format(url))
+ self.logger.info(f'Sending GET request to {Network.clean_uri(url, self._hide_login)}')
try:
self._response = self._session.get(url, params=params, timeout=timeout, verify=verify, auth=auth, stream=stream)
- self.logger.debug("{} Fetched URL {}".format(self.response_status(), self._response.url))
+ self.logger.debug(f'{self.response_status()} Fetched URL {Network.clean_uri(self._response.url, self._hide_login)}')
except Exception as e:
- self.logger.warning("Error sending GET request to {}: {}".format(url, e))
+ self.logger.warning(f'Error sending GET request to {Network.clean_uri(url, self._hide_login)}: {e}')
self._response = None
return False
return True
class Tcp_client(object):
- """ Creates a new instance of the Tcp_client class
+ """
+ Structured class to handle locally initiated TCP connections with two-way communication.
+
+ The callbacks need to be defined as follows:
+
+ def connected_callback(Tcp_client_instance)
+ def receiving_callback(Tcp_client_instance)
+ def disconnected_callback(Tcp_client_instance)
+ def data_received_callback(Tcp_client_instance, message)
+
+ (Class members need the additional first `self` parameter)
+
:param host: Remote host name or ip address (v4 or v6)
:param port: Remote host port to connect to
@@ -547,11 +527,11 @@ class Tcp_client(object):
def __init__(self, host, port, name=None, autoreconnect=True, connect_retries=5, connect_cycle=5, retry_cycle=30, binary=False, terminator=False):
self.logger = logging.getLogger(__name__)
- # Public properties
+ # public properties
self.name = name
self.terminator = terminator
- # "Private" properties
+ # protected properties
self._host = host
self._port = port
self._autoreconnect = autoreconnect
@@ -563,7 +543,7 @@ def __init__(self, host, port, name=None, autoreconnect=True, connect_retries=5,
self._timeout = 1
self._hostip = None
- self._ipver = socket.AF_INET
+ self._family = socket.AF_INET
self._socket = None
self._connect_counter = 0
self._binary = binary
@@ -573,49 +553,26 @@ def __init__(self, host, port, name=None, autoreconnect=True, connect_retries=5,
self._disconnected_callback = None
self._data_received_callback = None
- # "Secret" properties
+ # private properties
self.__connect_thread = None
self.__connect_threadlock = threading.Lock()
self.__receive_thread = None
self.__receive_threadlock = threading.Lock()
self.__running = True
- self.logger.setLevel(logging.DEBUG)
- self.logger.info("Initializing a connection to {} on TCP port {} {} autoreconnect".format(self._host, self._port, ('with' if self._autoreconnect else 'without')))
+ #self.logger.setLevel(logging.DEBUG) # Das sollte hier NICHT gesetzt werden, sondern in etc/logging.yaml im Logger lib.network konfiguriert werden!
- # Test if host is an ip address or a host name
- if Network.is_ip(self._host):
- # host is a valid ip address (v4 or v6)
- self.logger.debug("{} is a valid IP address".format(host))
- self._hostip = self._host
- if Network.is_ipv6(self._host):
- self._ipver = socket.AF_INET6
- else:
- self._ipver = socket.AF_INET
+ self._host = host
+ self._port = port
+ (self._hostip, self._port, self._family) = Network.validate_inet_addr(host, port)
+ if self._hostip is not None:
+ self.logger.info(f'Initializing a connection to {self._host} on TCP port {self._port} {"with" if self._autoreconnect else "without"} autoreconnect')
else:
- # host is a hostname, trying to resolve to an ip address (v4 or v6)
- self.logger.debug("{} is not a valid IP address, trying to resolve it as hostname".format(host))
- try:
- self._ipver, sockettype, proto, canonname, socketaddr = socket.getaddrinfo(host, None)[0]
- # Check if resolved address is IPv4 or IPv6
- if self._ipver == socket.AF_INET: # is IPv4
- self._hostip, port = socketaddr
- elif self._ipver == socket.AF_INET6: # is IPv6
- self._hostip, port, flow_info, scope_id = socketaddr
- else:
- # This should never happen
- self.logger.error("Unknown ip address family {}".format(self._ipver))
- self._hostip = None
- # Print ip address on successfull resolve
- if self._hostip is not None:
- self.logger.info("Resolved {} to {} address {}".format(self._host, 'IPv6' if self._ipver == socket.AF_INET6 else 'IPv4', self._hostip))
- except Exception as err:
- # Unable to resolve hostname
- self.logger.error("Cannot resolve {} to a valid ip address (v4 or v6). Error: {}".format(self._host, err))
- self._hostip = None
+ self.logger.error(f'Connection to {self._host} not possible, invalid address')
def set_callbacks(self, connected=None, receiving=None, data_received=None, disconnected=None):
- """ Set callbacks to caller for different socket events
+ """
+ Set callbacks to caller for different socket events.
:param connected: Called whenever a connection is established successfully
:param data_received: Called when data is received
@@ -631,17 +588,18 @@ def set_callbacks(self, connected=None, receiving=None, data_received=None, disc
self._data_received_callback = data_received
def connect(self):
- """ Connects the socket
+ """
+ Connect the socket.
:return: False if an error prevented us from launching a connection thread. True if a connection thread has been started.
:rtype: bool
"""
if self._hostip is None: # return False if no valid ip to connect to
- self.logger.error("No valid IP address to connect to {}".format(self._host))
+ self.logger.error(f'No valid IP address to connect to {self._host}')
self._is_connected = False
return False
if self._is_connected: # return false if already connected
- self.logger.error("Already connected to {}, ignoring new request".format(self._host))
+ self.logger.error(f'Already connected to {self._host}, ignoring new request')
return False
self.__connect_thread = threading.Thread(target=self._connect_thread_worker, name='TCP_Connect')
@@ -650,7 +608,8 @@ def connect(self):
return True
def connected(self):
- """ Returns the current connection state
+ """
+ Return the current connection state.
:return: True if an active connection exists,else False.
:rtype: bool
@@ -658,7 +617,8 @@ def connected(self):
return self._is_connected
def send(self, message):
- """ Sends a message to the server. Can be a string, bytes or a bytes array.
+ """
+ Send a message to the server. Can be a string, bytes or a bytes array.
:return: True if message has been successfully sent, else False.
:rtype: bool
@@ -666,27 +626,43 @@ def send(self, message):
if not isinstance(message, (bytes, bytearray)):
try:
message = message.encode('utf-8')
- except:
- self.logger.warning("Error encoding message for client {}".format(self.name))
+ except Exception:
+ self.logger.warning(f'Error encoding message for client {self.name}')
return False
try:
if self._is_connected:
- self._socket.send(message)
+ bytes_sent = self._socket.send(message)
+ if bytes_sent != len(message):
+ self.logger.warning(f'Error sending message {message} to host {self._host}: message truncated, sent {bytes_sent} of {len(message)} bytes')
else:
return False
- except Exception as e:
- self.logger.warning("No connection to {}, cannot send data {}. Error: {}".format(self._host, message, e))
+ except BrokenPipeError:
+ self.logger.warning(f'Detected disconnect from {self._host}, send failed.')
+ self._is_connected = False
+ if self._disconnected_callback:
+ self._disconnected_callback(self)
+ if self._autoreconnect:
+ self.logger.debug(f'Autoreconnect enabled for {self._host}')
+ self.connect()
return False
+
+ except Exception as e: # log errors we are not prepared to handle and raise exception for further debugging
+ self.logger.warning(f'Unhandleded error on sending to {self._host}, cannot send data {message}. Error: {e}')
+ raise
+
return True
def _connect_thread_worker(self):
+ """
+ Thread worker to handle connection.
+ """
if not self.__connect_threadlock.acquire(blocking=False):
- self.logger.warning("Connection attempt already in progress for {}, ignoring new request".format(self._host))
+ self.logger.warning(f'Connection attempt already in progress for {self._host}, ignoring new request')
return
if self._is_connected:
- self.logger.error("Already connected to {}, ignoring new request".format(self._host))
+ self.logger.error(f'Already connected to {self._host}, ignoring new request')
return
- self.logger.debug("Starting connection cycle for {}".format(self._host))
+ self.logger.debug(f'Starting connection cycle for {self._host}')
self._connect_counter = 0
while self.__running and not self._is_connected:
# Try a full connect cycle
@@ -695,124 +671,194 @@ def _connect_thread_worker(self):
if self._is_connected:
try:
self.__connect_threadlock.release()
- self._connected_callback and self._connected_callback(self)
- _name='TCP_Client'
+ if self._connected_callback:
+ self._connected_callback(self)
+ _name = 'TCP_Client'
if self.name is not None:
_name = self.name + '.' + _name
self.__receive_thread = threading.Thread(target=self.__receive_thread_worker, name=_name)
self.__receive_thread.daemon = True
self.__receive_thread.start()
- except:
+ except Exception:
raise
return True
- self._sleep(self._connect_cycle)
+ if self.__running:
+ self._sleep(self._connect_cycle)
- if self._autoreconnect:
+ if self._autoreconnect and self.__running:
self._sleep(self._retry_cycle)
self._connect_counter = 0
else:
break
try:
self.__connect_threadlock.release()
- except:
+ except Exception:
pass
def _connect(self):
- self.logger.debug("Connecting to {} using {} {} on TCP port {} {} autoreconnect".format(self._host, 'IPv6' if self._ipver == socket.AF_INET6 else 'IPv4', self._hostip, self._port, ('with' if self._autoreconnect else 'without')))
+ """
+ Initiate connection.
+ """
+ self.logger.debug(f'Connecting to {self._host} using {"IPv6" if self._family == socket.AF_INET6 else "IPv4"} {self._hostip} on TCP port {self._port} {"with" if self._autoreconnect else "without"} autoreconnect')
# Try to connect to remote host using ip (v4 or v6)
try:
- self._socket = socket.socket(self._ipver, socket.SOCK_STREAM)
+ self._socket = socket.socket(self._family, socket.SOCK_STREAM)
self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
self._socket.settimeout(5)
- self._socket.connect(('{}'.format(self._hostip), int(self._port)))
+ self._socket.connect((f'{self._hostip}', int(self._port)))
self._socket.settimeout(self._timeout)
self._is_connected = True
- self.logger.info("Connected to {} on TCP port {}".format(self._host, self._port))
+ self.logger.info(f'Connected to {self._host} on TCP port {self._port}')
# Connection error
except Exception as err:
self._is_connected = False
self._connect_counter += 1
- self.logger.warning("TCP connection to {}:{} failed with error {}. Counter: {}/{}".format(self._host, self._port, err, self._connect_counter, self._connect_retries))
+ self.logger.warning(f'TCP connection to {self._host}:{self._port} failed {self._connect_counter}/{self._connect_retries} times, last error was: {err}')
def __receive_thread_worker(self):
+ """
+ Thread worker to handle receiving.
+ """
waitobj = IOWait()
- waitobj.watch( self._socket, read=True)
- ### BMX poller = select.poll()
- ### BMX poller.register(self._socket, select.POLLIN | select.POLLPRI | select.POLLHUP | select.POLLERR)
+ waitobj.watch(self._socket, read=True)
__buffer = b''
self._is_receiving = True
- self._receiving_callback and self._receiving_callback(self)
- while self._is_connected and self.__running:
- ### BMX events = poller.poll(1000)
- events = waitobj.wait(1000) ### BMX
- ### BMX for fd, event in events:
- for fileno, read, write in events: ### BMX
- ### BMX if event & select.POLLHUP:
- ### BMX self.logger.warning("Client socket closed")
- # Check if POLLIN event triggered
- ### BMX if event & (select.POLLIN | select.POLLPRI):
- if read:
- msg = self._socket.recv(4096)
- # Check if incoming message is not empty
- if msg:
- # If we transfer in text mode decode message to string
- if not self._binary:
- msg = str.rstrip(str(msg, 'utf-8'))
- # If we work in line mode (with a terminator) slice buffer into single chunks based on terminator
- if self.terminator:
- __buffer += msg
- while True:
- # terminator = int means fixed size chunks
- if isinstance(self.terminator, int):
- i = self.terminator
- if i > len(__buffer):
- break
- # terminator is str or bytes means search for it
- else:
- i = __buffer.find(self.terminator)
- if i == -1:
- break
- i += len(self.terminator)
- line = __buffer[:i]
- __buffer = __buffer[i:]
+ if self._receiving_callback:
+ self._receiving_callback(self)
+ # try to find possible "hidden" errors
+ try:
+ while self._is_connected and self.__running:
+ events = waitobj.wait(1000) # BMX
+ for fileno, read, write in events: # BMX
+ if read:
+ msg = self._socket.recv(4096)
+ # Check if incoming message is not empty
+ if msg:
+ # TODO: doing this breaks line separation if multiple lines
+ # are read at a time, the next loop can't split it
+ # because line endings are missing
+ # find out reason for this operation...
+
+ # # If we transfer in text mode decode message to string
+ # # if not self._binary:
+ # # msg = str.rstrip(str(msg, 'utf-8')).encode('utf-8')
+
+ # If we work in line mode (with a terminator) slice buffer into single chunks based on terminator
+ if self.terminator:
+ __buffer += msg
+ while True:
+ # terminator = int means fixed size chunks
+ if isinstance(self.terminator, int):
+ i = self.terminator
+ if i > len(__buffer):
+ break
+ # terminator is str or bytes means search for it
+ else:
+ i = __buffer.find(self.terminator)
+ if i == -1:
+ break
+ i += len(self.terminator)
+ line = __buffer[:i]
+ __buffer = __buffer[i:]
+ if self._data_received_callback is not None:
+ try:
+ self._data_received_callback(self, line if self._binary else str(line, 'utf-8').strip())
+ except Exception as iex:
+ self._log_exception(iex, f'lib.network receive in terminator mode calling data_received_callback {self._data_received_callback} failed: {iex}')
+ # If not in terminator mode just forward what we received
+ else:
if self._data_received_callback is not None:
- self._data_received_callback(self, line)
- # If not in terminator mode just forward what we received
+ try:
+ self._data_received_callback(self, msg)
+ except Exception as iex:
+ self._log_exception(iex, f'lib.network calling data_received_callback {self._data_received_callback} failed: {iex}')
+ # If empty peer has closed the connection
else:
- if self._data_received_callback is not None:
- self._data_received_callback(self, msg)
- # If empty peer has closed the connection
- else:
- # Peer connection closed
- self.logger.warning("Connection closed by peer {}".format(self._host))
- self._is_connected = False
- ### BMX poller.unregister()
- waitobj.unwatch(self._socket)
- self._disconnected_callback and self._disconnected_callback(self)
- if self._autoreconnect:
- self.logger.debug("Autoreconnect enabled for {}".format(self._host))
- self.connect()
+ if self.__running:
+
+ # default state, peer closed connection
+ self.logger.warning(f'Connection closed by peer {self._host}')
+ self._is_connected = False
+ waitobj.unwatch(self._socket)
+ if self._disconnected_callback is not None:
+ try:
+ self._disconnected_callback(self)
+ except Exception as iex:
+ self._log_exception(iex, f'lib.network calling disconnected_callback {self._disconnected_callback} failed: {iex}')
+ if self._autoreconnect:
+ self.logger.debug(f'Autoreconnect enabled for {self._host}')
+ self.connect()
+ if self._is_connected:
+ self.logger.debug('set a read watch on socket again')
+ waitobj.watch(self._socket, read=True)
+ else:
+ # socket shut down by self.close, no error
+ self.logger.debug('Connection shut down by call to close method')
+ self._is_receiving = False
+ return
+ except Exception as ex:
+ if not self.__running:
+ self.logger.debug('lib.network receive thread shutting down')
+ self._is_receiving = False
+ return
+ else:
+ self._log_exception(ex, f'lib.network receive thread died with error: {ex}. Go tell...')
self._is_receiving = False
+
+ def _log_exception( self, ex, msg):
+ self.logger.error(msg + ' If stack trace is necessary, enable debug log')
+
+ if self.logger.isEnabledFor(logging.DEBUG):
+
+ # Get current system exception
+ ex_type, ex_value, ex_traceback = sys.exc_info()
+
+ # Extract unformatter stack traces as tuples
+ trace_back = traceback.extract_tb(ex_traceback)
+
+ # Format stacktrace
+ stack_trace = list()
+
+ for trace in trace_back:
+ stack_trace.append("File : %s , Line : %d, Func.Name : %s, Message : %s" % (trace[0], trace[1], trace[2], trace[3]))
+
+ self.logger.debug("Exception type : %s " % ex_type.__name__)
+ self.logger.debug("Exception message : %s" % ex_value)
+ self.logger.debug("Stack trace : %s" % stack_trace)
def _sleep(self, time_lapse):
+ """
+ Sleep (at least) seconds, but abort if self.__running changes to False.
+
+ :param time_lapse: wait time in seconds
+ :type time: int
+ """
time_start = time.time()
time_end = (time_start + time_lapse)
while self.__running and time_end > time.time():
- pass
+ # modified from 'pass' - this way intervals of 1 second are given up to other threads
+ # but the abort loop stays intact with a maximum of 1 second delay
+ time.sleep(1)
def close(self):
- """ Closes the current client socket """
- self.logger.info("Closing connection to {} on TCP port {}".format(self._host, self._port))
+ """
+ Close the current client socket.
+ """
+ self.logger.info(f'Closing connection to {self._host} on TCP port {self._port}')
self.__running = False
- if self.__connect_thread is not None and self.__connect_thread.isAlive():
+ self._socket.shutdown(socket.SHUT_RD)
+ if self.__connect_thread is not None and self.__connect_thread.is_alive():
self.__connect_thread.join()
- if self.__receive_thread is not None and self.__receive_thread.isAlive():
+ if self.__receive_thread is not None and self.__receive_thread.is_alive():
self.__receive_thread.join()
-class _Client(object):
- """ Client object that represents a connected client of tcp_server
+class ConnectionClient(object):
+ """
+ Client object that represents a connected client returned by a Tcp_server instance on incoming connection.
+
+ This class should normally **not be instantiated manually**, but is provided by the Tcp_server via the callbacks
:param server: The tcp_server passes a reference to itself to access parent methods
:param socket: socket.Socket class used by the Client object
@@ -822,12 +868,13 @@ class _Client(object):
:type socket: function
:type fd: int
"""
+
def __init__(self, server=None, socket=None, ip=None, port=None):
self.logger = logging.getLogger(__name__)
self.name = None
self.ip = ip
self.port = port
- self.ipver = None
+ self.family = None
self.writer = None
self.process_iac = True
@@ -838,10 +885,14 @@ def __init__(self, server=None, socket=None, ip=None, port=None):
@property
def socket(self):
+ """
+ Socket getter.
+ """
return self.__socket
def set_callbacks(self, data_received=None, will_close=None):
- """ Set callbacks for different socket events (client based)
+ """
+ Set callbacks for different socket events (client based).
:param data_received: Called when data is received
:type data_received: function
@@ -849,8 +900,18 @@ def set_callbacks(self, data_received=None, will_close=None):
self._data_received_callback = data_received
self._will_close_callback = will_close
+ async def __drain_writer(self):
+ """
+ Ensure drain() is called.
+ """
+ try:
+ await self.writer.drain()
+ except ConnectionResetError:
+ pass
+
def send(self, message):
- """ Send a string to connected client
+ """
+ Send a string to connected client.
:param msg: Message to send
:type msg: string | bytes | bytearray
@@ -861,43 +922,54 @@ def send(self, message):
if not isinstance(message, (bytes, bytearray)):
try:
message = message.encode('utf-8')
- except:
- self.logger.warning("Error encoding data for client {}".format(self.name))
+ except Exception:
+ self.logger.warning(f'Error encoding data for client {self.name}')
return False
try:
self.writer.write(message)
- self.writer.drain()
- except:
- self.logger.warning("Error sending data to client {}".format(self.name))
+ asyncio.ensure_future(self.__drain_writer())
+ except Exception as e:
+ self.logger.warning(f'Error sending data to client {self.name}: {e}')
return False
return True
def send_echo_off(self):
- """ Sends an IAC telnet command to ask client to turn it's echo off """
+ """
+ Send an IAC telnet command to ask client to turn its echo off.
+ """
command = bytearray([0xFF, 0xFB, 0x01])
string = self._iac_to_string(command)
- self.logger.debug("Sending IAC telnet command: '{}'".format(string))
+ self.logger.debug(f'Sending IAC telnet command: {string}')
self.send(command)
def send_echo_on(self):
- """ Sends an IAC telnet command to ask client to turn it's echo on again """
+ """
+ Send an IAC telnet command to ask client to turn its echo on again.
+ """
command = bytearray([0xFF, 0xFC, 0x01])
string = self._iac_to_string(command)
- self.logger.debug("Sending IAC telnet command: '{}'".format(string))
+ self.logger.debug(f'Sending IAC telnet command: {string}')
self.send(command)
def _process_IAC(self, msg):
- """ Processes incomming IAC messages. Does nothing for now except logging them in clear text """
+ """
+ Process incomming IAC messages.
+
+ NOTE: Does nothing for now except logging them in clear text
+ """
if len(msg) >= 3:
string = self._iac_to_string(msg[:3])
- self.logger.debug("Received IAC telnet command: '{}'".format(string))
+ self.logger.debug(f'Received IAC telnet command: {string}')
msg = msg[3:]
return msg
def close(self):
- """ Client socket closes itself """
- self._will_close_callback and self._will_close_callback(self)
+ """
+ Close client socket.
+ """
+ if self._will_close_callback:
+ self._will_close_callback(self)
self.set_callbacks(data_received=None, will_close=None)
self.writer.close()
return True
@@ -909,19 +981,25 @@ def _iac_to_string(self, msg):
if char in iac:
string += iac[char] + ' '
else:
- #string += ' '
string += chr(char)
return string.rstrip()
class Tcp_server(object):
- """ Creates a new instance of the Tcp_server class
+ """
+ Threaded TCP listener which dispatches connections (and possibly received data) via callbacks.
+
+ NOTE: The callbacks need to expect the following arguments:
- :param interface: Remote interface name or ip address (v4 or v6). Default is '::' which listens on all IPv4 and all IPv6 addresses available.
- :param port: Remote interface port to connect to
+ - ``incoming_connection(server, client)`` where ``server`` ist the ``Tcp_server`` instance and ``client`` is a ``ConnectionClient`` for the current connection
+ - ``data_received(server, client, data)`` where ``server`` ist the ``Tcp_server`` instance, ``client`` is a ``ConnectionClient`` for the current connection, and ``data`` is a string containing received data
+ - ``disconnected(server, client)`` where ``server`` ist the ``Tcp_server`` instance and ``client`` is a ``ConnectionClient`` for the closed connection
+
+ :param host: Local host name or ip address (v4 or v6). Default is '::' which listens on all IPv4 and all IPv6 addresses available.
+ :param port: Local port to connect to
:param name: Name of this connection (mainly for logging purposes)
- :type interface: str
+ :type host: str
:type port: int
:type name: str
"""
@@ -931,73 +1009,45 @@ class Tcp_server(object):
MODE_BINARY = 3
MODE_FIXED_LENGTH = 4
- def __init__(self, port, interface='', name=None, mode=MODE_BINARY, terminator=None):
+ def __init__(self, port, host='', name=None, mode=MODE_BINARY, terminator=None):
self.logger = logging.getLogger(__name__)
- # Public properties
+ # public properties
self.name = name
self.mode = mode
self.terminator = terminator
- # "Private" properties
- self._interface = interface
+ # private properties
+ self._host = host
self._port = port
self._is_listening = False
self._timeout = 1
- self._interfaceip = None
- self._ipver = socket.AF_INET
+ self._ipaddr = None
+ self._family = socket.AF_INET
self._socket = None
- self._listening_callback = None
self._incoming_connection_callback = None
self._data_received_callback = None
- # "Secret" properties
+ # protected properties
self.__loop = None
self.__coroutine = None
self.__server = None
self.__listening_thread = None
- self.__listening_threadlock = threading.Lock()
self.__running = True
# Test if host is an ip address or a host name
- if self._interface == '' or Network.is_ip(self._interface):
- # host is a valid ip address (v4 or v6)
- self._interfaceip = self._interface
- if self._interface == '':
- self._interface = 'All Ipv4/Ipv6'
- self.logger.debug("'{}' is a valid IP address".format(self._interface))
- if Network.is_ipv6(self._interfaceip):
- self._ipver = socket.AF_INET6
- else:
- self._ipver = socket.AF_INET
- else:
- # host is a hostname, trying to resolve to an ip address (v4 or v6)
- self.logger.debug("{} is not a valid IP address, trying to resolve it as hostname".format(self._interface))
- try:
- self._ipver, sockettype, proto, canonname, socketaddr = socket.getaddrinfo(self._interface, None)[0]
- # Check if resolved address is IPv4 or IPv6
- if self._ipver == socket.AF_INET:
- self._interfaceip, port = socketaddr
- elif self._ipver == socket.AF_INET6:
- self._interfaceip, port, flow_info, scope_id = socketaddr
- else:
- self.logger.error("Unknown ip address family {}".format(self._ipver))
- self._interfaceip = None
- if self._interfaceip is not None:
- self.logger.info("Resolved {} to {} address {}".format(self._interface, Network.ipver_to_string(self._ipver), self._interfaceip))
- except Exception as err:
- # Unable to resolve hostname
- self.logger.error("Cannot resolve {} to a valid ip address (v4 or v6). Error: {}".format(self._interface, err))
- self._interfaceip = None
+ (self._ipaddr, self._port, self._family) = Network.validate_inet_addr(host, port)
- self.__our_socket = Network.ip_port_to_socket(self._interfaceip, self._port)
- if not self.name:
- self.name = self.__our_socket
+ if self._ipaddr is not None:
+ self.__our_socket = Network.ip_port_to_socket(self._ipaddr, self._port)
+ if not self.name:
+ self.name = self.__our_socket
- def set_callbacks(self, listening=None, incoming_connection=None, disconnected=None, data_received=None):
- """ Set callbacks to caller for different socket events
+ def set_callbacks(self, incoming_connection=None, disconnected=None, data_received=None):
+ """
+ Set callbacks to caller for different socket events.
:param connected: Called whenever a connection is established successfully
:param data_received: Called when data is received
@@ -1007,13 +1057,13 @@ def set_callbacks(self, listening=None, incoming_connection=None, disconnected=N
:type data_received: function
:type disconnected: function
"""
- self._listening_callback = listening
self._incoming_connection_callback = incoming_connection
self._data_received_callback = data_received
self._disconnected_callback = disconnected
def start(self):
- """ Start the server socket
+ """
+ Start the server socket.
:return: False if an error prevented us from launching a connection thread. True if a connection thread has been started.
:rtype: bool
@@ -1021,54 +1071,63 @@ def start(self):
if self._is_listening:
return False
try:
- self.logger.info("Starting up TCP server socket {}".format(self.__our_socket))
+ self.logger.info(f'Starting up TCP server socket {self.__our_socket}')
self.__loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.__loop)
- self.__coroutine = asyncio.start_server(self.__handle_connection, self._interfaceip, self._port)
+ self.__coroutine = asyncio.start_server(self.__handle_connection, self._ipaddr, self._port)
self.__server = self.__loop.run_until_complete(self.__coroutine)
_name = 'TCP_Server'
if self.name is not None:
- _name = self.name + '.' + _name
+ _name = f'{self.name}.{_name}'
self.__listening_thread = threading.Thread(target=self.__listening_thread_worker, name=_name)
self.__listening_thread.daemon = True
self.__listening_thread.start()
- except:
+ except Exception as e:
+ self.logger.error(f'Error starting server: {e}')
return False
return True
def __listening_thread_worker(self):
- """ Runs the asyncio loop in a separate thread to not block the Tcp_server.start() method """
+ """
+ Run the asyncio loop in a separate thread to not block the Tcp_server.start() method.
+ """
asyncio.set_event_loop(self.__loop)
self._is_listening = True
try:
self.__loop.run_forever()
- except:
+ except Exception:
self.logger.debug('*** Error in loop.run_forever()')
finally:
-
- for task in asyncio.Task.all_tasks():
+ for task in asyncio.all_tasks(self.__loop):
task.cancel()
self.__server.close()
self.__loop.run_until_complete(self.__server.wait_closed())
- self.__loop.close()
+ try:
+ self.__loop.close()
+ except Exception:
+ pass
self._is_listening = False
- return True
async def __handle_connection(self, reader, writer):
- """ Handles incoming connection. One handler per client """
+ """
+ Handle incoming connection.
+
+ Each client gets its own handler.
+ """
peer = writer.get_extra_info('peername')
socket_object = writer.get_extra_info('socket')
peer_socket = Network.ip_port_to_socket(peer[0], peer[1])
- client = _Client(server=self, socket=socket_object, ip=peer[0], port=peer[1])
- client.ipver = socket.AF_INET6 if Network.is_ipv6(client.ip) else socket.AF_INET
+ client = ConnectionClient(server=self, socket=socket_object, ip=peer[0], port=peer[1])
+ client.family = socket.AF_INET6 if Utils.is_ipv6(client.ip) else socket.AF_INET
client.name = Network.ip_port_to_socket(client.ip, client.port)
client.writer = writer
- self.logger.info("Incoming connection from {} on socket {}".format(peer_socket, self.__our_socket))
- self._incoming_connection_callback and self._incoming_connection_callback(self, client)
+ self.logger.info(f'Incoming connection from {peer_socket} on socket {self.__our_socket}')
+ if self._incoming_connection_callback:
+ self._incoming_connection_callback(self, client)
while True:
try:
@@ -1077,7 +1136,7 @@ async def __handle_connection(self, reader, writer):
data = await reader.readline()
else:
data = await reader.read(4096)
- except:
+ except Exception:
data = None
if data and data[0] == 0xFF and client.process_iac:
@@ -1085,11 +1144,13 @@ async def __handle_connection(self, reader, writer):
if data:
try:
string = str.rstrip(str(data, 'utf-8'))
- self.logger.debug("Received '{}' from {}".format(string, client.name))
- self._data_received_callback and self._data_received_callback(self, client, string)
- client._data_received_callback and client._data_received_callback(self, client, string)
- except:
- self.logger.debug("Received undecodable bytes from {}".format(client.name))
+ self.logger.debug(f'Received "{string}" from {client.name}')
+ if self._data_received_callback:
+ self._data_received_callback(self, client, string)
+ if client._data_received_callback:
+ client._data_received_callback(self, client, string)
+ except Exception as e:
+ self.logger.debug(f'Received undecodable bytes from {client.name}: {data}, resulting in error: {e}')
else:
try:
self.__close_client(client)
@@ -1099,12 +1160,20 @@ async def __handle_connection(self, reader, writer):
return
def __close_client(self, client):
- self.logger.info("Lost connection to client {}".format(client.name))
- self._disconnected_callback and self._disconnected_callback(self, client)
+ """
+ Close client connection.
+
+ :param client: client object
+ :type client: lib.network.ConnectionClient
+ """
+ self.logger.info(f'Connection to client {client.name} closed')
+ if self._disconnected_callback:
+ self._disconnected_callback(self, client)
client.writer.close()
def listening(self):
- """ Returns the current listening state
+ """
+ Return the current listening state.
:return: True if the server socket is actually listening, else False.
:rtype: bool
@@ -1112,12 +1181,13 @@ def listening(self):
return self._is_listening
def send(self, client, msg):
- """ Send a string to connected client
+ """
+ Send a string to connected client.
:param client: Client Object to send message to
:param msg: Message to send
- :type client: network.Client
+ :type client: lib.network.ConnectionClient
:type msg: string | bytes | bytearray
:return: True if message has been queued successfully.
@@ -1127,28 +1197,225 @@ def send(self, client, msg):
return True
def disconnect(self, client):
- """ Disconnects a specific client
+ """
+ Disconnect a specific client.
:param client: Client Object to disconnect
- :type client: network.Client
+ :type client: lib.network.ConnectionClient
"""
client.close()
return True
def close(self):
- """ Closes running listening socket """
- self.logger.info("Shutting down listening socket on interface {} port {}".format(self._interface, self._port))
+ """
+ Close running listening socket.
+ """
+ self.logger.info(f'Shutting down listening socket on host {self._host} port {self._port}')
asyncio.set_event_loop(self.__loop)
try:
- active_connections = len([task for task in asyncio.Task.all_tasks() if not task.done()])
- except:
+ active_connections = len([task for task in asyncio.all_tasks(self.__loop) if not task.done()])
+ except Exception:
active_connections = 0
if active_connections > 0:
- self.logger.info('Tcp_server still has {} active connection(s), cleaning up'.format(active_connections))
+ self.logger.info(f'Tcp_server still has {active_connections} active connection(s), cleaning up')
self.__running = False
self.__loop.call_soon_threadsafe(self.__loop.stop)
while self.__loop.is_running():
pass
- if self.__listening_thread and self.__listening_thread.isAlive():
- self.__listening_thread.join()
+ try:
+ if self.__listening_thread and self.__listening_thread.is_alive():
+ self.__listening_thread.join()
+ except AttributeError: # thread can disappear between first and second condition test
+ pass
self.__loop.close()
+
+
+class Udp_server(object):
+ """
+ Threaded UDP listener which dispatches received data via callbacks.
+
+ NOTE: The callbacks need to expect the following arguments:
+
+ - ``data_received(addr, data)`` where ``addr`` is a tuple with ``('', remote_port)`` and ``data`` is the received data as string
+
+ :param host: Local hostname or ip address (v4 or v6). Default is '' which listens on all IPv4 addresses available.
+ :param port: Local port to connect to
+ :param name: Name of this connection (mainly for logging purposes)
+
+ :type host: str
+ :type port: int
+ :type name: str
+ """
+
+ def __init__(self, port, host='', name=None):
+ self.logger = logging.getLogger(__name__)
+
+ # Public properties
+ self.name = name
+
+ # protected properties
+ self._host = host
+ self._port = port
+ self._is_listening = False
+
+ self._ipaddr = None
+ self._family = socket.AF_INET
+ self._socket = None
+
+ self._data_received_callback = None
+
+ # provide a shutdown timeout for the server loop. emergency fallback only
+ self._close_timeout = 2
+
+ # private properties
+ self.__loop = None
+ self.__coroutine = None
+ self.__server = aioudp.aioUDPServer()
+ self.__listening_thread = None
+ self.__running = True
+
+ # create sensible ipaddr (resolve host, handle protocol family)
+ (self._ipaddr, self._port, self._family) = Network.validate_inet_addr(host, port)
+
+ if self._ipaddr is not None:
+ self.__our_socket = Network.ip_port_to_socket(self._ipaddr, self._port)
+ if not self.name:
+ self.name = self.__our_socket
+ else:
+ self.__running = False
+
+ def start(self):
+ """
+ Start the server socket.
+
+ :return: False if an error prevented us from launching a connection thread. True if a connection thread has been started.
+ :rtype: bool
+ """
+ if not self.__running:
+ self.logger.error('UDP server not initialized, can not start.')
+ return False
+ if self._is_listening:
+ self.logger.warning('UDP server already listening, not starting again')
+ return False
+ try:
+ self.logger.info(f'Starting up UDP server socket {self.__our_socket}')
+ self.__loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(self.__loop)
+ self.__coroutine = self.__start_server()
+ self.__loop.run_until_complete(self.__coroutine)
+
+ _name = 'UDP_Server'
+ if self.name is not None:
+ _name = self.name + '.' + _name
+ self.__listening_thread = threading.Thread(target=self.__listening_thread_worker, name=_name)
+ self.__listening_thread.daemon = True
+ self.__listening_thread.start()
+ except Exception as e:
+ self.logger.error(f'Error {e} setting up udp server for {self.__our_socket}')
+ return False
+ return True
+
+ def set_callbacks(self, data_received=None):
+ """
+ Set callbacks to caller for different socket events.
+
+ :param data_received: Called when data is received
+
+ :type data_received: function
+ """
+ self._data_received_callback = data_received
+
+ def listening(self):
+ """
+ Return the current listening state.
+
+ :return: True if the server socket is actually listening, else False.
+ :rtype: bool
+ """
+ return self._is_listening
+
+ def close(self):
+ """
+ Close running listening socket.
+ """
+ self.logger.info(f'Shutting down listening socket on host {self._host} port {self._port}')
+ asyncio.set_event_loop(self.__loop)
+ self.__running = False
+ self.__server.stop()
+
+ # cancel pending tasks
+ tasks = [t for t in asyncio.all_tasks(self.__loop) if t is not asyncio.current_task(self.__loop)]
+ [task.cancel() for task in tasks]
+
+ # close loop gracefully
+ self.__loop.call_soon_threadsafe(self.__loop.stop)
+
+ # this code shouldn't be needed, but include it with timeout just to be sure...
+ starttime = time.time()
+ while self.__loop.is_running() and time.time() < starttime + self._close_timeout:
+ pass
+ if self.__loop.is_running():
+ self.__loop.stop()
+ time.sleep(0.5)
+
+ try:
+ if self.__listening_thread and self.__listening_thread.is_alive():
+ self.__listening_thread.join()
+ except AttributeError: # thread can disappear between first and second condition test
+ pass
+ self.__loop.close()
+
+ async def __start_server(self):
+ """
+ Start the actual server class.
+ """
+ self.__server.run(self._ipaddr, self._port, self.__loop)
+ self.__server.subscribe(self.__handle_connection)
+
+ def __listening_thread_worker(self):
+ """
+ Run the asyncio loop in a separate thread to not block the Udp_server.start() method.
+ """
+ self._is_listening = True
+ self.logger.debug('listening thread set is_listening to True')
+ asyncio.set_event_loop(self.__loop)
+ try:
+ self.__loop.run_forever()
+ except Exception as e:
+ self.logger.debug(f'*** Error in loop.run_forever(): {e}')
+ finally:
+ self.__server.stop()
+ self.__loop.close()
+ self._is_listening = False
+ return True
+
+ async def __handle_connection(self, data, addr):
+ """
+ Handle incoming connection.
+
+ As UDP is stateless, each datagram creates a new handler.
+
+ :param data: data received from socket
+ :type data: bytes
+ :param addr: address info ('addr', port)
+ :type addr: tuple
+ """
+ if addr:
+ host, port = addr
+ else:
+ self.logger.debug(f'Address info {addr} not in format "(host, port)"')
+ host = '0.0.0.0'
+ port = 0
+
+ self.logger.info(f'Incoming datagram from {host}:{port} on socket {self.__our_socket}')
+
+ if data:
+ try:
+ string = str.rstrip(str(data, 'utf-8'))
+ self.logger.debug(f'Received "{string}" from {host}:{port}')
+ if self._data_received_callback:
+ self._data_received_callback(addr, string)
+ except UnicodeError:
+ self.logger.debug(f'Received undecodable bytes from {host}:{port}')
+ else:
+ self.logger.debug(f'Received empty datagram from {host}:{port}')
\ No newline at end of file
diff --git a/lib/orb.py b/lib/orb.py
index 0a5b9501c4..1a14fa7637 100644
--- a/lib/orb.py
+++ b/lib/orb.py
@@ -144,7 +144,7 @@ def rise(self, doff=0, moff=0, center=True, dt=None):
if not doff == 0:
doff = self._avoid_neverup(dt, date_utc, doff)
self._obs.horizon = str(doff)
- if doff != 0:
+ if not doff == 0:
next_rising = self._obs.next_rising(self._orb, use_center=center).datetime()
else:
next_rising = self._obs.next_rising(self._orb).datetime()
@@ -171,7 +171,7 @@ def set(self, doff=0, moff=0, center=True, dt=None):
if not doff == 0:
doff = self._avoid_neverup(dt, date_utc, doff)
self._obs.horizon = str(doff)
- if doff != 0:
+ if not doff == 0:
next_setting = self._obs.next_setting(self._orb, use_center=center).datetime()
else:
next_setting = self._obs.next_setting(self._orb).datetime()
@@ -181,7 +181,7 @@ def set(self, doff=0, moff=0, center=True, dt=None):
def pos(self, offset=None, degree=False, dt=None):
"""
Calculates the position of either sun or moon
- :param offset: given in minutesA, shifts the time of calculation by some minutes back or forth
+ :param offset: given in minutes, shifts the time of calculation by some minutes back or forth
:param degree: if True: return the position of either sun or moon from the observer as degrees, otherwise as radians
:param dt: time for which the position needs to be calculated
:return: a tuple with azimuth and elevation
diff --git a/lib/requirements.txt b/lib/requirements.txt
index 1ed6c9b3d4..d522e48d99 100644
--- a/lib/requirements.txt
+++ b/lib/requirements.txt
@@ -9,7 +9,7 @@ holidays>=0.9.11
psutil
portalocker
-# lib.connection / lib.network
+# lib.network
iowait
# lib.network, lib.shpypi:
diff --git a/lib/scene.py b/lib/scene.py
index 6fa4f2921b..6b4916b6ee 100644
--- a/lib/scene.py
+++ b/lib/scene.py
@@ -28,10 +28,13 @@
import os.path
import csv
+from lib.translation import translate
+
from lib.item import Items
from lib.logic import Logics
from lib.utils import Utils
+from lib.shtime import Shtime
import lib.shyaml as yaml
logger = logging.getLogger(__name__)
@@ -59,18 +62,28 @@ def __init__(self, smarthome):
import inspect
curframe = inspect.currentframe()
calframe = inspect.getouterframes(curframe, 2)
- logger.critical("A second 'scenes' object has been created. There should only be ONE instance of class 'Scenes'!!! Called from: {} ({})".format(calframe[1][1], calframe[1][3]))
+ logger.critical(translate("A second 'scenes' object has been created. There should only be ONE instance of class 'Scenes'!!! Called from: {frame1} ({frame2})", {'frame1': calframe[1][1], 'frame2': calframe[1][3]}))
_scenes_instance = self
self.items = Items.get_instance()
self.logics = Logics.get_instance()
+ self._load_scenes()
+ return
+
+
+ def _load_scenes(self):
+ """
+ Load defined scenes with learned values from ../scene directory
+
+ :return:
+ """
self._scenes = {}
self._learned_values = {}
- self._scenes_dir = smarthome._scenes_dir
+ self._scenes_dir = self._sh._scenes_dir
if not os.path.isdir(self._scenes_dir):
- logger.warning("Directory scenes not found. Ignoring scenes.".format(self._scenes_dir))
+ logger.warning(translate("Directory '{scenes_dir}' not found. Ignoring scenes.", {'scenes_dir': self._scenes_dir}))
return
self._learned_values = {}
@@ -94,9 +107,10 @@ def __init__(self, smarthome):
action.get('item', ''), str(action.get('value', '')),
action.get('learn', ''), scene_file_yaml[state].get('name', ''))
else:
- logger.warning("Scene {}, state {}: action '{}' is not a dict".format(item, state, action))
+ logger.warning(translate("Scene {scene}, state {state}: action '{action}' is not a dict", {'scene': item, 'state': state, 'action': action}))
else:
- logger.warning("Scene {}, state {}: actions are not a list".format(item, state))
+ logger.warning(translate("Scene {scene}, state {state}: actions are not a list", {'scene': item, 'state': state}))
+
self._load_learned_values(str(item.id()))
else:
# Trying to read conf file with scene definition
@@ -111,10 +125,12 @@ def __init__(self, smarthome):
continue
self._add_scene_entry(item, row[0], row[1], row[2])
except Exception as e:
- logger.warning("Problem reading scene file {0}: No .yaml or .conf file found with this name".format(self.scene_file))
+ logger.warning(translate("Problem reading scene file {file}: No .yaml or .conf file found with this name", {'file': self.scene_file}))
continue
item.add_method_trigger(self._trigger)
+ return
+
def _eval(self, value):
"""
@@ -126,11 +142,16 @@ def _eval(self, value):
:return: evaluated value or None
:rtype: type of evaluated expression or None
"""
- sh = self._sh # noqa
+ sh = self._sh
+ shtime = Shtime.get_instance()
+ items = Items.get_instance()
+ import math
+ import lib.userfunctions as uf
+
try:
rvalue = eval(value)
except Exception as e:
- logger.warning(" - Problem evaluating: {} - {}".format(value, e))
+ logger.warning(" - " + translate("Problem evaluating: {value} - {exception}", {'value': value, 'exception': e}))
return value
return rvalue
@@ -239,7 +260,7 @@ def _trigger(self, item, caller, source, dest):
if Utils.is_int(state):
state = int(state)
else:
- logger.error("Invalid state '{}' for scene {}".format(state, item.id()))
+ logger.error(translate("Invalid state '{state}' for scene {scene}", {'state': state, 'scene': item.id()}))
return
if (state >= 0) and (state < 64):
@@ -249,7 +270,7 @@ def _trigger(self, item, caller, source, dest):
# learn state
self._trigger_learnstate(item, state&127, caller, source, dest)
else:
- logger.error("Invalid state '{}' for scene {}".format(state, item.id()))
+ logger.error(translate("Invalid state '{state}' for scene {scene}", {'state': state, 'scene': item.id()}))
def _add_scene_entry(self, item, state, ditemname, value, learn=False, name=''):
@@ -271,13 +292,13 @@ def _add_scene_entry(self, item, state, ditemname, value, learn=False, name=''):
if learn:
rvalue = self._eval(value)
if str(rvalue) != value:
- logger.warning("_add_scene_entry - Learn set to 'False', because '{}' != '{}'".format(rvalue, value))
+ logger.warning(translate("_add_scene_entry - " + "Learn set to 'False', because '{rvalue}' != '{value}'", {'rvalue': rvalue, 'value': value}))
learn = False
if ditem is None:
ditem = self.logics.return_logic(ditemname)
if ditem is None:
- logger.warning("Could not find item or logic '{}' specified in {}".format(ditemname, self.scene_file))
+ logger.warning(translate("Could not find item or logic '{ditemname}' specified in {file}", {'ditemname': ditemname, 'file': self.scene_file}))
return
if item.id() in self._scenes:
@@ -359,7 +380,7 @@ def get_scene_action_name(self, scenename, action):
try:
return self._scenes[scenename][action][0][2]
except:
- logger.warning("get_scene_action_name: unable to get self._scenes['{}']['{}'][0][2] <- {}".format(scenename, action, self._scenes[scenename][action][0]))
+ logger.warning(translate("get_scene_action_name: " + "unable to get self._scenes['{scenename}']['{action}'][0][2] <- {res}", {'scenename': scenename, 'action': action, 'res': self._scenes[scenename][action][0]}))
return ''
def return_scene_value_actions(self, name, state):
@@ -379,3 +400,14 @@ def return_scene_value_actions(self, name, state):
action_list.append(return_action)
return action_list
+
+ def reload_scenes(self):
+ """
+ Reload defined scenes with learned values from ../scene directory
+
+ :return:
+ """
+
+ self._load_scenes()
+ logger.notice(translate("Reloaded all scenes"))
+ return True
diff --git a/lib/scheduler.py b/lib/scheduler.py
index 80f5d5b42e..03c4c86855 100644
--- a/lib/scheduler.py
+++ b/lib/scheduler.py
@@ -24,7 +24,6 @@
import logging
import time
import datetime
-import calendar
import sys
import traceback
import threading
@@ -45,6 +44,8 @@
import gc # noqa
import os
import math
+import lib.userfunctions as uf
+
import types
import subprocess
@@ -59,6 +60,8 @@
_scheduler_instance = None # Pointer to the initialized instance of the scheduler class (for use by static methods)
+from lib.triggertimes import TriggerTimes
+
class _PriorityQueue:
"""
@@ -152,6 +155,7 @@ def __init__(self, smarthome):
self.shtime = Shtime.get_instance()
self.items = Items.get_instance()
+ self.crontabs = TriggerTimes.get_instance()
self.mqtt = None
@@ -563,7 +567,9 @@ def _next_time(self, name, offset=None):
next_time = now + datetime.timedelta(seconds=offset)
if job['cron'] is not None:
for entry in job['cron']:
- ct = self._crontab(entry)
+ if entry == 'None':
+ continue
+ ct = self.crontabs.get_next(entry, now)
if next_time is not None:
if ct < next_time:
next_time = ct
@@ -667,235 +673,3 @@ def _task(self, name, obj, by, source, dest, value):
except Exception as e:
logger.exception("Method {0} exception: {1}".format(name, e))
threading.current_thread().name = 'idle'
-
- def _crontab(self, crontab):
- """
- inspects if a crontab entry contains a sunbound time instruction (e.g. "17:00 now_str, event_range)
- if not next_event:
- return False
- next_time = now
- day, hour, minute = next_event.split('-')
- return next_time.replace(day=int(day), hour=int(hour), minute=int(minute), second=0, microsecond=0)
-
- def _next(self, f, seq):
- for item in seq:
- if f(item):
- return item
- return False
-
- def _sun(self, crontab):
- """
- parses a given string with a time range to determine it's timely boundaries and
- returns a time
-
- :param: crontab contains a string with '[H:M<](sunrise|sunset)[+|-][offset][ next_time:
- next_time = dmin
- if smax is not None:
- h, sep, m = smax.partition(':')
- try:
- dmax = next_time.replace(hour=int(h), minute=int(m), second=0, tzinfo=self.shtime.tzinfo())
- except Exception:
- logger.error('Wrong syntax: {0}. Should be [H:M<](sunrise|sunset)[+|-][offset][ high: # entry above range
- item = high # truncate value to highest possible
- item_range.append(item)
- for entry in item_range:
- result.append('{:02d}'.format(entry))
-
- return result
-
- def _day_range(self, days):
- """
- inspect a given string with days given as integer numbers separated by ","
-
- :param days:
- :return: an array with strings containing the days of month
- """
- now = datetime.date.today()
- wdays = [MO, TU, WE, TH, FR, SA, SU]
- result = []
- for day in days.split(','):
- wday = wdays[int(day)]
- # add next weekday occurrence
- day = now + dateutil.relativedelta.relativedelta(weekday=wday)
- result.append(day.strftime("%d"))
- # safety add-on if weekday equals todays weekday
- day = now + dateutil.relativedelta.relativedelta(weekday=wday(+2))
- result.append(day.strftime("%d"))
- return result
diff --git a/lib/shpypi.py b/lib/shpypi.py
index a9bef7f4bc..c4b6105126 100644
--- a/lib/shpypi.py
+++ b/lib/shpypi.py
@@ -385,7 +385,12 @@ def install_requirements(self, req_type, logging=True, pip3_command=None):
pass
stdout, stderr = Utils.execute_subprocess(pip_command+' install -r requirements/'+req_type+'.txt --user --no-warn-script-location')
- ####
+ # ToDo
+ # create_directories is available in lib.smarthome.py but shpypi.py might be started prior to SH object creation
+ # thus it is needed to create the var/log directory here
+ os.makedirs(os.path.join(self._sh_dir, 'log'), exist_ok=True)
+ os.makedirs(os.path.join(self._sh_dir, 'var', 'log'), exist_ok=True)
+
pip_log_name = os.path.join(self._sh_dir, 'var', 'log', 'pip3_outout.log')
with open(pip_log_name, 'w', encoding='utf8') as outfile:
outfile.write(stdout)
diff --git a/lib/shtime.py b/lib/shtime.py
index 092aff9701..b9a15482f4 100644
--- a/lib/shtime.py
+++ b/lib/shtime.py
@@ -31,6 +31,7 @@
import pytz
from dateutil.tz import tzlocal
from dateutil import parser
+import dateutil.relativedelta
import json
import logging
import os
@@ -69,6 +70,7 @@ def __init__(self, smarthome):
_shtime_instance = self
self._starttime = datetime.datetime.now()
+ self.log_msg = "" # is overwritten in _initialize_holidays() if no error occurs
# set default timezone to UTC
# global TZ
@@ -462,7 +464,7 @@ def datetime_transform(self, key):
else:
raise TypeError(self.translate("Cannot convert type '{key}' to datetime").format(key=type(key)))
if isinstance(key, datetime.datetime) and key.tzinfo is None:
- key = self._timezone.localize(key)
+ key = self._timezone.localize(key)
return key
@@ -483,52 +485,57 @@ def date_transform(self, key):
return key
- def beginning_of_week(self, week=None, year=None):
+ def beginning_of_week(self, week=None, year=None, offset=0):
"""
Calculates the date of the beginning of a given week
If no week and no year are specified, the beginning of the current week is calculated
- :param week: week to use for calculation
+ :param week: calender week to use for calculation
:param year: year to use for calculation
+ :param offset: negative number for previous weeks, positive for future ones
:type week: int
:type year: int
+ :type offset: int
- :return: date the monday of given week
+ :return: date the monday of given calender week
:rtype: datetime.date
"""
+ month = self.current_month()
if week is None and year is None:
week = self.calendar_week(self.today())
year = self.current_year()
- month = self.current_month()
if month == 1 and week > 50:
year -= 1
- week -= 1
else:
- if year is None:
- year = self.current_year()
if week is None:
self.logger.error("beginning_of_week: "+self.translate("Week not specified"))
return self.today()
- #monday = datetime.datetime.strptime(f'{year}-{week}-1', "%Y-%W-%w") # geht erst ab Python 3.6
- monday = datetime.datetime.strptime('{year}-{week}-1'.format(year=year, week=week), "%Y-%W-%w")
-
- return monday.date()
+ if year is None:
+ year = self.current_year()
+ if month == 1 and week > 50:
+ year -= 1
+ self.logger.debug(self.translate("Calculating beginning of week based on year {year}, week {week} and offset {offset}").format(year=year, week=week, offset=offset))
+ week_beginning = datetime.datetime.strptime('{year}-{week}-1'.format(year=year, week=week), "%G-%V-%u") + dateutil.relativedelta.relativedelta(weeks=offset)
+ return week_beginning.date()
- def beginning_of_month(self, month=None, year=None):
+ def beginning_of_month(self, month=None, year=None, offset=0):
"""
Calculates the date of the beginning of a given month
This method is used to make code more readable
+
If no month is specified, the current month is used
If no year is specified, the current year is used
:param month: month to use for calculation
:param year: year to use for calculation
+ :param offset: negative number for previous months, positive for future ones
:type month: int
:type year: int
+ :type offset: int
:return: date the first day of given month
:rtype: datetime.date
@@ -537,10 +544,12 @@ def beginning_of_month(self, month=None, year=None):
month = self.current_month()
if year is None:
year = self.current_year()
- return datetime.date(year, month, 1)
+ month_beginning = datetime.date(year, month, 1) + dateutil.relativedelta.relativedelta(months=offset)
+ return month_beginning
- def beginning_of_year(self, year=None):
+
+ def beginning_of_year(self, year=None, offset=0):
"""
Calculates the date of the beginning of a given year
@@ -549,22 +558,28 @@ def beginning_of_year(self, year=None):
If no year is specified, the current year is used
:param year: year to use for calculation
+ :param offset: negative number for previous years, positive for future ones
:type year: int
+ :type offset: int
:return: date the first day of given year
:rtype: datetime.date
- """
- return self.beginning_of_month(1, year)
+ """
+ year_beginning = self.beginning_of_month(1, year) + dateutil.relativedelta.relativedelta(years=offset)
+ return year_beginning
- def today(self):
+ def today(self, offset=0):
"""
Return today's date
+ :param offset: negative number for previous days, positive for future ones
+ :type offset: int
+
:return: date of today
:rtype: datetime.date
"""
- return datetime.datetime.now().date()
+ return (datetime.datetime.now() + datetime.timedelta(days=offset)).date()
def tomorrow(self):
@@ -587,57 +602,74 @@ def yesterday(self):
return self.today() + datetime.timedelta(days=-1)
- def current_year(self):
+ def current_year(self, offset=0):
"""
Return the current year
+ :param offset: negative number for previous years, positive for future ones
+ :type offset: int
+
:return: year
:rtype: int
"""
- return self.today().year
+ return (self.today() + dateutil.relativedelta.relativedelta(years=offset)).year
- def current_month(self):
+ def current_month(self, offset=0):
"""
Return the current month
+ :param offset: negative number for previous months, positive for future ones
+ :type offset: int
+
:return: month
:rtype: int
"""
- return self.today().month
+ return (self.today() + dateutil.relativedelta.relativedelta(months=offset)).month
- def current_day(self):
+ def current_day(self, offset=0):
"""
Return the current day
+ :param offset: negative number for previous days, positive for future ones
+ :type offset: int
+
:return: day
:rtype: int
"""
- return self.today().day
+ return (self.today() + datetime.timedelta(days=offset)).day
- def length_of_year(self, year=None):
+ def length_of_year(self, year=None, offset=0):
"""
Returns the length of a given year
:param year: year to use for calculation
+ :param offset: negative number for previous months, positive for future ones
:type year: int
+ :type offset: int
:return: Length of year in days
:rtype: int
"""
- return self.length_of_month(1, year)
+ if year is None:
+ year = self.current_year()
+ year += offset
+ leap_year = True if year % 4 == 0 and (year % 100 != 0 or year % 400 == 0) else False
+ return 365 if leap_year is False else 366
- def length_of_month(self, month=None, year=None):
+ def length_of_month(self, month=None, year=None, offset=0):
"""
Returns the length of a given month for a given year
:param month: month to use for calculation
:param year: year to use for calculation
+ :param offset: negative number for previous months, positive for future ones
:type month: int
:type year: int
+ :type offset: int
:return: Length of month in days
:rtype: int
@@ -646,21 +678,28 @@ def length_of_month(self, month=None, year=None):
month = self.current_month()
if year is None:
year = self.current_year()
+ offset_dt = datetime.datetime(year, month, 1) + dateutil.relativedelta.relativedelta(months=offset)
+ month = offset_dt.month
+ year = offset_dt.year
next_month = month
next_year = year
if next_month == 12:
next_year += 1
next_month = 0
+ debug_month = "" if offset == 0 else " (offset {offset})".format(offset=offset)
+ self.logger.debug(self.translate("Calculating length of month based on year {year}, month {month}{debug_month}").format(year=year, month=month, debug_month=debug_month))
return (datetime.datetime(next_year, next_month+1, 1) - datetime.datetime(year, month, 1)).days
- def day_of_year(self, date=None):
+ def day_of_year(self, date=None, offset=0):
"""
Calculate which day of the year the given date is
:param date: date
+ :param offset: negative number for previous days, positive for future ones
:type date: str|datetime.datetime|datetime.date|int|float
+ :type offset: int
:return: day of year
:rtype: int
@@ -669,52 +708,60 @@ def day_of_year(self, date=None):
date = self.date_transform(date)
else:
date = self.today()
+ date = date + datetime.timedelta(days=offset)
return (date - datetime.date(date.year, 1, 1)).days + 1
- def weekday(self, date=None):
+ def weekday(self, date=None, offset=0):
"""
Returns the ISO weekday of a given date (or of today, if date is None)
Return the day of the week as an integer, where Monday is 1 and Sunday is 7. (ISO weekday)
:param date: date
+ :param offset: negative number for previous days, positive for future ones
:type date: str|datetime.datetime|datetime.date|int|float
+ :type offset: int
:return: weekday (1=Monday .... 7=Sunday)
:rtype: int
"""
if date:
- dt = self.date_transform(date)
- return dt.isoweekday()
+ weekday = self.date_transform(date)
+ weekday = weekday + datetime.timedelta(days=offset)
else:
- return datetime.datetime.now().isoweekday()
+ weekday = self.today() + datetime.timedelta(days=offset)
+ return weekday.isoweekday()
- def calendar_week(self, date=None):
+ def calendar_week(self, date=None, offset=0):
"""
Returns the calendar week (according to ISO)
:param date: date
+ :param offset: negative number for previous weeks, positive for future ones
:type date: str|datetime.datetime|datetime.date|int|float
+ :type offset: int
:return: week (ISO)
:rtype: int
"""
if date:
- dt = self.date_transform(date)
- return dt.isocalendar()[1]
+ cal_week = self.date_transform(date) + dateutil.relativedelta.relativedelta(weeks=offset)
else:
- return datetime.datetime.now().isocalendar()[1]
+ cal_week = self.today() + dateutil.relativedelta.relativedelta(weeks=offset)
+ return cal_week.isocalendar()[1]
- def weekday_name(self, date=None):
+ def weekday_name(self, date=None, offset=0):
"""
Returns the name of the weekday for a given date
:param date: date
+ :param offset: negative number for previous days, positive for future ones
:type date: str|datetime.datetime|datetime.date|int|float
+ :type offset: int
:return: weekday name
:rtype: str
@@ -723,6 +770,7 @@ def weekday_name(self, date=None):
dt = self.date_transform(date)
else:
dt = self.today()
+ dt = dt + datetime.timedelta(days=offset)
wday = self.weekday(dt)
if wday == 1:
@@ -730,7 +778,7 @@ def weekday_name(self, date=None):
elif wday == 2:
day = "Dienstag"
elif wday == 3:
- day = "Mittowch"
+ day = "Mittwoch"
elif wday == 4:
day = "Donnerstag"
elif wday == 5:
@@ -742,7 +790,7 @@ def weekday_name(self, date=None):
else:
day = "?"
- return translate(day)
+ return self.translate(day)
def _get_nth_dow_in_month(self, dow, dow_week, year, month):
@@ -872,6 +920,8 @@ def _add_custom_holidays(self):
return 0
custom = self.config.get('custom', [])
+ if custom is None:
+ custom = []
count = 0
if len(custom) > 0:
for entry in custom:
@@ -955,6 +1005,7 @@ def add_custom_holiday_range(self, from_date, to_date=None, holiday_name=''):
self.holidays.append(cust_dict)
return
+# {"dow": 5, "dow_week": "last", "month": 7, "name": "Sysadmin day"}
def _initialize_holidays(self):
"""
diff --git a/lib/smarthome.py b/lib/smarthome.py
index b08d4f491a..504bf4e931 100644
--- a/lib/smarthome.py
+++ b/lib/smarthome.py
@@ -26,7 +26,6 @@
#########################################################################
#
# TO DO:
-# - Isolate Logging (MemLog, etc.) to lib module
# - remove all remarks with old code (that has been moved to lib modules)
#
#########################################################################
@@ -96,8 +95,9 @@
from lib.shtime import Shtime
import lib.shyaml
from lib.shpypi import Shpypi
-
+from lib.triggertimes import TriggerTimes
from lib.constants import (YAML_FILE, CONF_FILE, DEFAULT_FILE)
+import lib.userfunctions as uf
#import bin.shngversion
#MODE = 'default'
@@ -108,23 +108,6 @@
# Classes
#####################################################################
-class _LogHandler(logging.StreamHandler):
- """
- LogHandler used by MemLog
- """
- def __init__(self, log, shtime):
- logging.StreamHandler.__init__(self)
- self._log = log
- self._shtime = shtime
-
- def emit(self, record):
- try:
- self.format(record)
- timestamp = datetime.datetime.fromtimestamp(record.created, self._shtime.tzinfo())
- self._log.add([timestamp, record.threadName, record.levelname, record.message])
- except Exception:
- self.handleError(record)
-
class SmartHome():
"""
SmartHome ist the main class of SmartHomeNG. All other objects can be addressed relative to
@@ -159,8 +142,6 @@ def initialize_vars(self):
self.plugin_start_complete = False
self._smarthome_conf_basename = None
- self._log_buffer = 50
- self.__logs = {}
self.__event_listeners = {}
self.__all_listeners = []
self.modules = []
@@ -212,7 +193,8 @@ def __init__(self, MODE, extern_conf_dir=''):
"""
self.shng_status = {'code': 0, 'text': 'Initalizing'}
self._logger = logging.getLogger(__name__)
- self._logger_main = logging.getLogger(__name__ + '.main')
+ self._logger_main = logging.getLogger(__name__)
+ self.logs = lib.log.Logs(self) # initialize object for memory logs
self.initialize_vars()
self.initialize_dir_vars()
@@ -243,6 +225,7 @@ def __init__(self, MODE, extern_conf_dir=''):
self._etc_dir = os.path.join(self._extern_conf_dir, 'etc')
self._items_dir = os.path.join(self._extern_conf_dir, 'items'+os.path.sep)
+ self._functions_dir = os.path.join(self._extern_conf_dir, 'functions'+os.path.sep)
self._logic_dir = os.path.join(self._extern_conf_dir, 'logics'+os.path.sep)
self._scenes_dir = os.path.join(self._extern_conf_dir, 'scenes'+os.path.sep)
self._smarthome_conf_basename = os.path.join(self._etc_dir,'smarthome')
@@ -316,9 +299,11 @@ def __init__(self, MODE, extern_conf_dir=''):
virtual_text = ''
if lib.utils.running_virtual():
virtual_text = ' in virtual environment'
- self._logger_main.warning("-------------------- Init SmartHomeNG {} --------------------".format(self.version))
- self._logger_main.warning(f"Running in Python interpreter 'v{self.PYTHON_VERSION}'{virtual_text}, from directory {self._base_dir}")
- self._logger_main.warning(f" - on {platform.platform()} (pid={pid})")
+ self._logger_main.notice("-------------------- Init SmartHomeNG {} --------------------".format(self.version))
+ self._logger_main.notice(f"Running in Python interpreter 'v{self.PYTHON_VERSION}'{virtual_text}, from directory {self._base_dir}")
+ self._logger_main.notice(f" - on {platform.platform()} (pid={pid})")
+ if logging.getLevelName('NOTICE') == 31:
+ self._logger_main.notice(f" - Loglevel NOTICE is set to value {logging.getLevelName('NOTICE')} because handler of root logger is set to level WARNING or higher - Set level of handler '{self.logs.root_handler_name}' to 'NOTICE'!")
default_encoding = locale.getpreferredencoding() # returns cp1252 on windows
if not (default_encoding in ['UTF8','UTF-8']):
@@ -355,7 +340,7 @@ def __init__(self, MODE, extern_conf_dir=''):
base_reqs = self.shpypi.test_base_requirements(self)
if base_reqs == 0:
self.restart('SmartHomeNG (Python package installation)')
- exit(5) # exit code 5 -> for systemctl to restart ShamrtHomeNG
+ exit(5) # exit code 5 -> for systemctl to restart SmartHomeNG
elif base_reqs == -1:
self._logger.critical("Python package requirements for modules are not met and unable to install base requirements")
self._logger.critical("Do you have multiple Python3 Versions installed? Maybe PIP3 looks into a wrong Python environment. Try to configure pip_command in etc/smarthome.yaml")
@@ -365,7 +350,7 @@ def __init__(self, MODE, extern_conf_dir=''):
plugin_reqs = self.shpypi.test_conf_plugins_requirements(self._plugin_conf_basename, self._plugins_dir)
if plugin_reqs == 0:
self.restart('SmartHomeNG (Python package installation)')
- exit(5) # exit code 5 -> for systemctl to restart ShamrtHomeNG
+ exit(5) # exit code 5 -> for systemctl to restart SmartHomeNG
elif plugin_reqs == -1:
self._logger.critical("Python package requirements for configured plugins are not met and unable to install those requirements")
self._logger.critical("Do you have multiple Python3 Versions installed? Maybe PIP3 looks into a wrong Python environment. Try to configure pip_command in etc/smarthome.yaml")
@@ -377,7 +362,7 @@ def __init__(self, MODE, extern_conf_dir=''):
self.shtime._initialize_holidays()
- self._logger_main.warning(" - " + self.shtime.log_msg)
+ self._logger_main.notice(" - " + self.shtime.log_msg)
# Add Signal Handling
# signal.signal(signal.SIGHUP, self.reload_logics)
@@ -394,20 +379,15 @@ def __init__(self, MODE, extern_conf_dir=''):
# Catching Exceptions
sys.excepthook = self._excepthook
- #############################################################
- # Setting debug level and adding memory handler
- self.initMemLog()
-
# test if a valid locale is set in the operating system
if os.name != 'nt':
- pass
- try:
- if not any(utf in os.environ['LANG'].lower() for utf in ['utf-8', 'utf8']):
- self._logger.error("Locale for the enviroment is not set to a valid value. Set the LANG environment variable to a value supporting UTF-8")
- except:
- self._logger.error("Locale for the enviroment is not set. Defaulting to en_US.UTF-8")
- os.environ["LANG"] = 'en_US.UTF-8'
- os.environ["LC_ALL"] = 'en_US.UTF-8'
+ try:
+ if not any(utf in os.environ['LANG'].lower() for utf in ['utf-8', 'utf8']):
+ self._logger.error("Locale for the enviroment is not set to a valid value. Set the LANG environment variable to a value supporting UTF-8")
+ except:
+ self._logger.error("Locale for the enviroment is not set. Defaulting to en_US.UTF-8")
+ os.environ["LANG"] = 'en_US.UTF-8'
+ os.environ["LC_ALL"] = 'en_US.UTF-8'
#############################################################
# Link Tools
@@ -528,15 +508,10 @@ def init_logging(self, conf_basename='', MODE='default'):
"""
if conf_basename == '':
conf_basename = self._log_conf_basename
- #fo = open(conf_basename + YAML_FILE, 'r')
- doc = lib.shyaml.yaml_load(conf_basename + YAML_FILE, True)
- if doc == None:
- print()
- print("ERROR: Invalid logging configuration in file 'logging.yaml'")
- exit(1)
- self.logging_config = doc
- logging.config.dictConfig(doc)
- #fo.close()
+ conf_dict = lib.shyaml.yaml_load(conf_basename + YAML_FILE, True)
+
+ self.logs.configure_logging(conf_dict)
+
if MODE == 'interactive': # remove default stream handler
logging.getLogger().disabled = True
elif MODE == 'verbose':
@@ -545,22 +520,7 @@ def init_logging(self, conf_basename='', MODE='default'):
logging.getLogger().setLevel(logging.DEBUG)
elif MODE == 'quiet':
logging.getLogger().setLevel(logging.WARNING)
-# log_file.doRollover()
-
-
- def initMemLog(self):
- """
- This function initializes all needed datastructures to use the (old) memlog plugin
- """
-
- self.log = lib.log.Log(self, 'env.core.log', ['time', 'thread', 'level', 'message'], maxlen=self._log_buffer)
- _logdate = "%Y-%m-%d %H:%M:%S"
- _logformat = "%(asctime)s %(levelname)-8s %(threadName)-12s %(message)s"
- formatter = logging.Formatter(_logformat, _logdate)
- log_mem = _LogHandler(self.log, self.shtime)
- log_mem.setLevel(logging.WARNING)
- log_mem.setFormatter(formatter)
- logging.getLogger('').addHandler(log_mem)
+ return
#################################################################
@@ -578,6 +538,11 @@ def start(self):
threading.currentThread().name = 'Main'
+ #############################################################
+ # Prepare TriggerTimes for Scheduler
+ #############################################################
+ self.triggertimes = TriggerTimes(self)
+
#############################################################
# Start Scheduler
#############################################################
@@ -601,6 +566,11 @@ def start(self):
self.modules = lib.module.Modules(self, configfile=self._module_conf_basename)
self.modules.start()
+ #############################################################
+ # Init and import user-functions
+ #############################################################
+ uf.init_lib(self.getBaseDir())
+
#############################################################
# Init Item-Wrapper
#############################################################
@@ -638,7 +608,7 @@ def start(self):
#############################################################
# Init Scenes
#############################################################
- lib.scene.Scenes(self)
+ self.scenes = lib.scene.Scenes(self)
#############################################################
# Start Connections
@@ -665,7 +635,7 @@ def start(self):
# Main Loop
#############################################################
self.shng_status = {'code': 20, 'text': 'Running'}
- self._logger_main.warning("-------------------- SmartHomeNG initialization finished --------------------")
+ self._logger_main.notice("-------------------- SmartHomeNG initialization finished --------------------")
while self.alive:
try:
@@ -714,14 +684,14 @@ def stop(self, signum=None, frame=None):
# if header_logged:
# self._logger.warning("SmartHomeNG stopped")
# else:
- self._logger_main.warning("-------------------- SmartHomeNG stopped --------------------")
+ self._logger_main.notice("-------------------- SmartHomeNG stopped --------------------")
self.shng_status = {'code': 33, 'text': 'Stopped'}
lib.daemon.remove_pidfile(PIDFILE)
logging.shutdown()
- exit(5) # exit code 5 -> for systemctl to restart ShamrtHomeNG
+ exit(5) # exit code 5 -> for systemctl to restart SmartHomeNG
def restart(self, source=''):
@@ -734,7 +704,7 @@ def restart(self, source=''):
self.shng_status = {'code': 30, 'text': 'Restarting'}
if source != '':
source = ', initiated by ' + source
- self._logger_main.warning("-------------------- SmartHomeNG restarting" + source + " --------------------")
+ self._logger_main.notice("-------------------- SmartHomeNG restarting" + source + " --------------------")
# python_bin could contain spaces (at least on windows)
python_bin = sys.executable
if ' ' in python_bin:
@@ -743,7 +713,7 @@ def restart(self, source=''):
self._logger.info("Restart command = '{}'".format(command))
try:
p = subprocess.Popen(command, shell=True)
- exit(5) # exit code 5 -> for systemctl to restart ShamrtHomeNG
+ exit(5) # exit code 5 -> for systemctl to restart SmartHomeNG
except subprocess.SubprocessError as e:
self._logger.error("Restart command '{}' failed with error {}".format(command,e))
@@ -774,33 +744,6 @@ def __iter__(self):
return self.items.get_toplevel_items()
- #################################################################
- # Log Methods
- #################################################################
- """
- SmartHomeNG internally keeps a list of logs which can be extended
- Currently these logs are created by several plugins
- (plugins memlog, operationlog and visu_websocket) and initMemLog function of SmartHomeNG
- """
- def add_log(self, name, log):
- """
- Adds a log to the list of logs
-
- :param name: Name of log
- :param log: Log object, essentially an object based of a double ended queue
- """
- self.__logs[name] = log
-
- def return_logs(self):
- """
- Function to the list of logs
-
- :return: List of logs
- :rtype: list
- """
- return self.__logs
-
-
#################################################################
# Event Methods
#################################################################
diff --git a/lib/tools.py b/lib/tools.py
index 458c1923b5..fa637b6e9b 100644
--- a/lib/tools.py
+++ b/lib/tools.py
@@ -61,7 +61,7 @@ def ping(self, host):
if ping_response.returncode == 0:
# need to inspect the returned output since it could be that
# **destination is unreachable** anyway which does not generate an error code
- # as the result is a bytearray which codepage might vary between cp850, cp1252 and utf8,
+ # as the result is a bytearray which codepage might vary between cp850, cp1252 and utf8,
# it is a quick hack to just look if ms is inside this string.
# if not, it is sure that destination could not be reached
if b'ms' in ping_response.stdout:
@@ -77,10 +77,12 @@ def dewpoint(self, t, rf):
return round((241.2 * log + 4222.03716 * t / (241.2 + t)) / (17.5043 - log - 17.5043 * t / (241.2 + t)), 2)
def dt2js(self, dt):
- return time.mktime(dt.timetuple()) * 1000 + int(dt.microsecond / 1000)
+ #return time.mktime(dt.timetuple()) * 1000 + int(dt.microsecond / 1000)
+ return dt.timestamp() * 1000 + int(dt.microsecond / 1000)
def dt2ts(self, dt):
- return time.mktime(dt.timetuple())
+ #return time.mktime(dt.timetuple())
+ return dt.timestamp()
def fetch_url(self, url, username=None, password=None, timeout=2, warn_no_connect=1, method = 'GET', body=None, errorItem = None):
connErrors = ['Host is down', 'timed out', '[Errno 113] No route to host']
@@ -125,16 +127,16 @@ def rel2abs(self, t, rf):
mix = 18.0160 / 28.9660 * rf * sat / (100000 - rf * sat)
rhov = 100000 / (287.0 * (1 - mix) + 462.0 * mix) / t
return mix * rhov * 1000
-
+
def abs2rel(self,t,ah):
"""
Return the relative humidity from the absolute humidity (g/cm3) and temperature (Celsius)
-
+
:param t: temperature in celsius
:type t: float
:param ah: absolute humidity (g/cm3)
:type t: float
-
+
:return: val = relative humidity (in percent)
:rtype: dict
"""
diff --git a/lib/triggertimes.py b/lib/triggertimes.py
new file mode 100644
index 0000000000..9faa04045c
--- /dev/null
+++ b/lib/triggertimes.py
@@ -0,0 +1,967 @@
+#!/usr/bin/env python3
+# vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab
+#########################################################################
+# Copyright 2016-2020 Martin Sinn m.sinn@gmx.de
+# Copyright 2016 Christian Straßburg c.strassburg@gmx.de
+# Copyright 2012-2013 Marcus Popp marcus@popp.mx
+# Copyright 2019-2021 Bernd Meiners Bernd.Meiners@mail.de
+#########################################################################
+# This file is part of SmartHomeNG.
+#
+# SmartHomeNG is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# SmartHomeNG is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with SmartHomeNG. If not, see .
+#########################################################################
+
+import logging
+import re
+import datetime
+import calendar
+import threading
+import time
+
+import dateutil.relativedelta
+from dateutil.relativedelta import MO, TU, WE, TH, FR, SA, SU
+from dateutil.tz import tzutc
+
+#print("lib.triggertimes is being imported")
+from lib.shtime import Shtime
+shtime = None
+
+logger = logging.getLogger(__name__)
+
+"""
+This library implements TriggerTimes in SmartHomeNG.
+
+The main class ``TriggerTimes`` implements the handling for
+Linux like crontab and sky event bound times
+
+This class has a static method to get a handle to the instance of the TriggerTimes class,
+that is created during initialization of SmartHomeNG.
+This method implements a way to access the API for handling TriggerTimes without having
+to juggle through the object hierarchy of the running SmartHomeNG.
+
+This API enables plugins and logics to access the details of the TriggerTimes initialized in SmartHomeNG.
+
+The methods of the class TriggerTimes implement the API for trigger times.
+They can be used the following way: To call eg. **get_toplevel_items()**, use the following syntax:
+
+.. code-block:: python
+
+ from lib.triggertimes import TriggerTimes
+ sh_triggertimes = TriggerTimes.get_instance()
+
+:Note: Do not use the functions or variables of the main smarthome object any more. They are deprecated.
+ Use the methods of the class **TriggerTimes** instead.
+
+:Note: This library is part of the core of SmartHomeNG. Regular plugins should not need to use this API.
+ It is mainly implemented for plugins near to the core like **scheduler** and the core itself!
+"""
+
+_triggertimes_instance = None # Pointer to the initialized instance of the TriggerTimes class (for use by static methods)
+
+def get_invalid_time():
+ return datetime.datetime.now(tzutc()) + dateutil.relativedelta.relativedelta(years=+10)
+
+class TriggerTimes():
+ """
+ TriggerTimes loader class. (TriggerTimes-methods from lib/scheduler.py are moved here.)
+
+ - An instance is created during initialization by bin/smarthome.py
+ - There should be only one instance of this class. So: Don't create another instance
+ """
+ # dict with all the items that are defined in the form:
+ # {"*/5 6-19/1 * * *": crontab object, "* * 6 * : crontab object, ..."}
+
+ def __init__(self, smarthome):
+ """
+ :param smarthome: Instance of the smarthome master-object
+ :type smarthome: object
+ """
+ self._sh = smarthome
+ Skytime.set_smarthome_reference(smarthome)
+ self.logger = logging.getLogger(__name__)
+
+ # a list with objects containing trigger times
+ self.__known_triggertimes = []
+
+ global _triggertimes_instance
+ if _triggertimes_instance is not None:
+ import inspect
+ curframe = inspect.currentframe()
+ calframe = inspect.getouterframes(curframe, 4)
+ self.logger.critical(f"A second 'TriggerTimes' object has been created. There should only be ONE instance of class 'TriggerTimes'!!! Called from: {calframe[1][1]} ({calframe[1][3]})")
+
+ _triggertimes_instance = self
+
+ def get_next(self, triggertime: str, starttime: datetime, location = None):
+ """
+ Find the next point in time starting from start for a given location
+ Location is important if there is sunrise/set or moonrise/set included
+ If location ist not given then the location of SmartHomeNG is used
+
+
+ :param triggertime: a user defined time description when to trigger
+ :type triggertime: str
+ :param start: starttime and date for the beginning of the search
+ :type start: datetime
+ :param location: Location information (not implemented yet), defaults to None
+ :type location: tupel with (lat,lon,elev), optional
+ :return: the time and date of next event
+ :rtype: datetime
+ """
+ triggertime = TriggerTimes.normalize(triggertime)
+ #self.logger.debug(f"get next triggertime for '{triggertime}' start search at '{starttime}'")
+ for tt in self.__known_triggertimes:
+ if tt.get_triggertime() == triggertime:
+ #self.logger.debug(f"Element found in list for {triggertime}")
+ break
+ else:
+ if any(substring in triggertime for substring in Skytime.get_skyevents() ):
+ self.logger.debug(f"create new Skytime('{triggertime}') object")
+ tt = Skytime(triggertime)
+ else:
+ self.logger.debug(f"create new Crontab('{triggertime}') object")
+ tt = Crontab(triggertime)
+ self.__known_triggertimes.append(tt)
+ self.logger.debug(tt)
+ return tt.get_next(starttime)
+
+ @staticmethod
+ def normalize(triggertime):
+ """
+ this removes unnecessary spaces from a triggertime definition
+
+ :param triggertime: definition of the triggertime
+ :type triggertime: str
+ :return: cleaned up triggertime
+ :rtype: str
+ """#
+ if not isinstance( triggertime, str):
+ triggertime = str(triggertime)
+ triggertime = triggertime.strip() # remove spaces in front and at end
+ triggertime = re.sub(' +', ' ',triggertime) # replace multiple spaces by a single one
+ return triggertime
+
+ # --------------------------------------------------------------------------------------------------------
+ # Following (static) method of the class TriggerTimes implement the API for trigger times in SmartHomeNG
+ # --------------------------------------------------------------------------------------------------------
+
+ @staticmethod
+ def get_instance():
+ """
+ Returns the instance of the TriggerTimes class, to be used to access the trigger times API
+
+ Use it the following way to access the API:
+
+ .. code-block:: python
+
+ from lib.triggertimes import TriggerTimes
+ sh_triggertimes = TriggerTimes.get_instance()
+
+ # to access a method (eg. get_next()):
+ sh_triggertimes.get_next(triggertime)
+
+
+ :return: Triggertimes instance
+ :rtype: object
+ """
+ return _triggertimes_instance
+
+
+"""
+Events for SmartHomeNG can be scheduled by a parameter to attribute ``crontab`` which essentially describe a triggertime.
+This attribute parameter may be one of:
+
+ * init optional with timeshift, the init keyword is handled by the scheduler itself and will never show up here
+ * crontab: init+1 --> 1 second after initialisation
+ * crontab: init+5m --> 5 minutes after initialisation
+
+ * sun- or moonbound time instruction
+ * crontab: 17:00 means sunset but a time at least equal or later than 17:00 and earlier or latest at 20:00
+ * crontab: 6:00 means sunrise but a time at least equal or later than 6:00 and earlier or latest at 10:00
+
+ * parameter which is similar but not equal to linux *crontab*
+ * crontab: */5 6-19/1 * * * ==> every 5 minutes between 6 and 19 at any day of any month or any weekday
+"""
+
+class TriggerTime():
+ """
+ This provides a base class for all trigger times like crontabs, or sun/moonbound trigger times
+ It is mainly to share the same basics and static methods
+ """
+ named_days = {
+ 'mon':'0', 'tue':'1','wed':'2','thu':'3','fri':'4','sat':'5','sun':'6',
+ 'mo':'0', 'di':'1','mi':'2','do':'3','fr':'4','sa':'5','so':'6'
+ }
+
+ named_months = {
+ 'jan':'1', 'feb':'2','mar':'3','apr':'4','may':'5','jun':'6','jul':'7',
+ 'aug': '8', 'sep': '9', 'oct': '10', 'nov': '11', 'dec': '12'
+ }
+
+ def __init__(self, triggertime):
+ self._lock = threading.Lock()
+ # save the original given triggertime
+ self._triggertime = triggertime
+ #if TriggerTime.shtime is None:
+ # TriggerTime.shtime = Shtime.get_instance()
+
+ def get_triggertime( self):
+ return self._triggertime
+
+ @staticmethod
+ def integer_range(entry, low, high):
+ """
+ Inspects a string containing
+ * intervals ('*/2' --> ['2','4','6', ... high]
+ * ranges ('9-11' --> ['9','10','11']),
+ * single values ('1,2,5,9' --> ['1','2','5','9'])
+ * or a combination of those and
+ returns a sorted and distinct list of found integers
+
+ :param entry: a string with single entries of intervals, numeric ranges or single values
+ :param low: lower limit as integer
+ :param high: higher limit as integer
+ :return: a list of found integers formatted as string
+ """
+ result = []
+ item_range = []
+
+ # Check for multiple items and process each item recursively
+ if ',' in entry:
+ for item in entry.split(','):
+ result.extend(Crontab.integer_range(item, low, high))
+
+ # Check for intervals, e.g. "*/2", "9-17/2"
+ elif '/' in entry:
+ spec_range, interval = entry.split('/')
+ logger.debug(f'Cron spec interval {entry} -> {spec_range},{interval}')
+ result = Crontab.integer_range(spec_range, low, high)[::int(interval)]
+
+ # Check for numeric ranges, e.g. "9-17"
+ elif '-' in entry:
+ spec_low, spec_high = entry.split('-')
+ result = Crontab.integer_range('*', int(spec_low), int(spec_high))
+
+ # Process single item
+ else:
+ if entry == '*':
+ item_range = list(range(low, high + 1))
+ else:
+ item = int(entry)
+ if item > high: # entry above range
+ item = high # truncate value to highest possible
+ item_range.append(item)
+ for entry in item_range:
+ result.append(entry)
+ result = sorted(list(set(result)))
+ # logger.debug('Crontab.integer_range {}[{},{}] results in {}'.format(entry, low, high, result))
+ return result
+
+ @staticmethod
+ def get_next_in_sorted_list( entry, items, minentry, maxentry):
+ newlist = sorted([i for i in items if i >= minentry and i <= maxentry])
+ if entry in newlist: return entry, True
+ result = [i for i in newlist if i > entry]
+ if len(result) == 0: return None, False
+ return min(result), False
+
+
+class Crontab(TriggerTime):
+ """One space or more spaces separate the time pieces from each other.
+ Currently sets of 4,5 or 6 parts are allowed.
+
+ Distinction with count of parameters
+ If 4 parts specified (normal case in SmartHomeNG up to version 1.8):
+
+ * * * *
+ │ │ │ │
+ │ │ │ └───────────── weekday (0 - 6)
+ │ │ └───────────── day of month (1 - 31)
+ │ └───────────── hour (0 - 23)
+ └───────────── minute (0 - 59)
+
+ If 5 parts specified:
+
+ * * * * *
+ │ │ │ │ │
+ │ │ │ │ └───────────── weekday (0 - 6)
+ │ │ │ └───────────── month (1 - 12) new 5th part
+ │ │ └───────────── day of month (1 - 31)
+ │ └───────────── hour (0 - 23)
+ └───────────── minute (0 - 59)
+
+ If 6 parts specified:
+
+ * * * * * *
+ │ │ │ │ │ │
+ │ │ │ │ │ └───────────── weekday (0 - 6)
+ │ │ │ │ └───────────── month (1 - 12)
+ │ │ │ └───────────── day of month (1 - 31) optional
+ │ │ └───────────── hour (0 - 23)
+ │ └───────────── minute (0 - 59)
+ └───────────── seconds (0 - 60) optional, only valid if month is present (which can be a range of 1-12 of course)
+
+ So the parameter count is the distinction which flavour is to be used for examination
+
+ There are predefined names to abbreviate certain recurrent time sets like **@midnight** which equals ``0 0 * * *``
+
+ * named presets like:
+ @yearly equals 0 0 1 1 *
+ @annually equals 0 0 1 1 *
+ @monthly equals 0 0 1 * *
+ @weekly equals 0 0 * * 0
+ @daily equals 0 0 * * *
+ @midnight equals 0 0 * * *
+ @hourly equals 0 * * * *
+
+ """
+ crontab_presets = {
+ "@yearly": "0 0 1 1 *",
+ "@annually": "0 0 1 1 *",
+ "@monthly": "0 0 1 * *",
+ "@weekly": "0 0 * * 0",
+ "@daily": "0 0 * * *",
+ "@midnight": "0 0 * * *",
+ "@hourly": "0 * * * *"
+ }
+
+ def __init__(self, triggertime):
+ super().__init__(triggertime)
+
+ self.next_event = None # store last result
+ self.max_calc_time = 0 # keep track of maximum calculation time
+
+ self.parse_triggertime()
+
+ def parse_triggertime(self):
+ """parse the crontab string for details and store them to the class variables for later use"""
+ logger.debug(f'Enter Crontab.parse_triggertime({self._triggertime})')
+ self._is_valid = False
+ with self._lock:
+ triggertime = self._triggertime
+ # replace @yearly etc. with correct preset
+ if triggertime in Crontab.crontab_presets:
+ triggertime = Crontab.crontab_presets[triggertime]
+
+ # find our how many parameters are given with this crontab and save them to the class variables
+ try:
+ parameter_set = triggertime.strip().split()
+ except:
+ logger.error(f"crontab entry '{triggertime}' can not be split up into 4 parts for minute, hour, day and weekday")
+ return False
+
+ self.parameter_count = len(parameter_set)
+ if self.parameter_count == 4:
+ logger.debug(f'old smarthome.py style parameter set {triggertime} given')
+ self.minute, self.hour, self.day, self.wday = parameter_set[0],parameter_set[1],parameter_set[2],parameter_set[3]
+ self.month='*'
+ self.second = '0'
+ elif self.parameter_count == 5:
+ logger.debug(f'new SmartHomeNG style parameter set {triggertime} given')
+ self.minute, self.hour, self.day, self.month, self.wday = parameter_set[0],parameter_set[1],parameter_set[2],parameter_set[3], parameter_set[4]
+ self.second = '0'
+ elif self.parameter_count == 6:
+ logger.debug(f'new SmartHomeNG style parameter set {triggertime} given')
+ self.second, self.minute, self.hour, self.day, self.month, self.wday = parameter_set[0],parameter_set[1],parameter_set[2],parameter_set[3], parameter_set[4], parameter_set[5]
+
+ if self.parameter_count > 4:
+ # replace abbreviated months like 'jan' with their number like '1'
+ self.month = self.month.lower()
+ for search in sorted(Crontab.named_months, key=len, reverse=True): # Through keys sorted by length
+ self.month = self.month.replace(search, Crontab.named_months[search])
+
+ # replace abbreviated days like 'sun' for sunday with their number like '6'
+ self.wday = self.wday.lower()
+ for search in sorted(Crontab.named_days, key=len, reverse=True): # Through keys sorted by length
+ self.wday = self.wday.replace(search, Crontab.named_days[search])
+
+ # evaluate the crontab parameter string to some lists of allowed points as integers
+ self.second_range = Crontab.integer_range(self.second, 0, 59)
+ self.minute_range = Crontab.integer_range(self.minute, 0, 59)
+ self.hour_range = Crontab.integer_range(self.hour, 0, 23)
+ self.day_range = Crontab.integer_range(self.day, 1, 31) # not zero based, limited to 1..31 days, needs to be clipped for actual month
+ self.month_range = Crontab.integer_range(self.month, 1, 12)
+ self.weekday_range = Crontab.integer_range(self.wday, 0, 6)
+ self._is_valid = True
+
+ logger.debug(f'Leave Crontab.parse_triggertime()')
+
+ def __str__(self):
+ r = f"""{self._triggertime} is {'' if self._is_valid else 'not'} valid, parameter count {self.parameter_count}:
+ Hours: {self.hour} -> {self.hour_range}
+ Minutes: {self.minute} -> {self.minute_range}
+ Seconds: {self.second} -> {self.second_range}
+ Days: {self.day} -> {self.day_range}
+ Weekday: {self.wday} -> {self.weekday_range}
+ Months: {self.month} -> {self.month_range}
+ """
+ return(r)
+
+
+ def get_next(self, starttime: datetime):
+ """
+ Calculates the next crontab triggertime
+
+ :param starttime: the datetime to start the search from
+ :type starttime: datetime
+ :return: found date and time of next occurence or a time way up in the future
+ :rtype: datetime
+ """
+ with self._lock:
+ tik = time.perf_counter()
+ if self.next_event is None:
+ self.next_event = datetime.datetime.min
+ self.next_event = self.next_event.replace(tzinfo=starttime.tzinfo)
+ if starttime < self.next_event:
+ logger.debug(f'looking for the next event after {starttime} was already calculated as {self.next_event}')
+ return self.next_event
+ days_max_count = 365*25
+ days = 0
+ searchtime = starttime
+ #logger.debug(f'looking for the next event after {starttime}')
+ searchtime = searchtime.replace(microsecond=0) + datetime.timedelta(seconds=1) # smallest amount higher than given time
+ while True:
+ #logger.warning(f"{searchtime}")
+ days = abs((starttime-searchtime).days)
+ if days > days_max_count:
+ logger.error(f'No matches after {days} examined days, giving up')
+ return get_invalid_time()
+ # preset current searcher
+ year = searchtime.year
+ month, em = Crontab.get_next_in_sorted_list(searchtime.month, self.month_range, 1, 12)
+ if month is not None:
+ if not em:
+ # if not an exact match for month then set starttime to earliest of next month
+ searchtime = searchtime.replace(month=month, day=1, hour=0, minute=0, second=0)
+ day, em = Crontab.get_next_in_sorted_list(searchtime.day, self.day_range, 1, calendar.monthrange(year, month)[1])
+ if day is not None:
+ if not em:
+ searchtime = searchtime.replace(day=day,hour=0, minute=0, second=0)
+ weekday = searchtime.weekday()
+ if weekday in self.weekday_range:
+ hour, em = Crontab.get_next_in_sorted_list(searchtime.hour, self.hour_range, 0, 23)
+ if hour is not None:
+ if not em:
+ searchtime = searchtime.replace(hour=hour, minute=0, second=0)
+ minute, em = Crontab.get_next_in_sorted_list(searchtime.minute, self.minute_range, 0, 59)
+ if minute is not None:
+ if not em:
+ searchtime = searchtime.replace(minute=minute, second=0)
+ second, em = Crontab.get_next_in_sorted_list(searchtime.second, self.second_range, 0, 59)
+ if second is not None:
+ if not em:
+ searchtime = searchtime.replace(second=second)
+ # we found the next event, so leave the while loop here
+ break
+ else:
+ searchtime = searchtime.replace(second=0) + datetime.timedelta(minutes=1)
+ continue
+ else:
+ searchtime = searchtime.replace(minute=0,second=0) + datetime.timedelta(minutes=60)
+ continue
+ else: # hour not found, goto next day at early morning
+ searchtime = searchtime.replace(hour=0, minute=0, second=0) + datetime.timedelta(days=1)
+ continue
+ else: # weekday not found, proceed at next day early morning
+ searchtime = searchtime.replace(hour=0, minute=0, second=0) + datetime.timedelta(days=1)
+ continue
+ else: # day not found, start at beginning of next month
+ advance_days = calendar.monthrange(year, searchtime.month)[1]
+ searchtime = searchtime.replace(day=1, hour=0, minute=0, second=0) + datetime.timedelta(days=advance_days)
+ continue
+ else:
+ # goto next month, set hour, minute and second to 0, set day to 1
+ searchtime = searchtime.replace(year=searchtime.year+1, month=1, day=1, hour=0, minute=0, second=0)
+ continue
+
+ self.next_event = searchtime
+ tok = time.perf_counter()-tik
+ self.max_calc_time = max(self.max_calc_time, tok)
+ logger.debug(f'next event is at {searchtime}, calc took {tok:0.4f} sec, max: {self.max_calc_time:0.4f} sec')
+
+ # find out how the new function compares to old implementation
+ if self.parameter_count == 4:
+ tik = time.perf_counter()
+ foo = self.get_next_old( starttime)
+ tok = time.perf_counter()-tik
+ logger.debug(f'OLD: next event is at {foo}, calc took {tok:0.4f} sec')
+ if searchtime != foo:
+ logger.error(f'NEW gives {searchtime} but OLD gives {foo}')
+
+ return searchtime
+
+
+ def get_next_old(self, starttime: datetime):
+ """
+ a crontab entry is expected the correct form as documented above
+ Part of the old implementation
+
+ :param crontab: a string containing an enhanced crontab entry
+ :return: a timezone aware datetime with the next event time or an error datetime object that lies 10 years in the future
+ """
+ try:
+ next_event = self._parse_month(starttime) # this month
+ if not next_event:
+ next_event = self._parse_month(starttime, next_month=True) # next month
+ #logger.debug(f'next event after {starttime} is {next_event}')
+ return next_event
+ except Exception as e:
+ logger.error(f'Error parsing crontab "{self._triggertime}": {e}')
+ return datetime.datetime.now(tzutc()) + dateutil.relativedelta.relativedelta(years=+10)
+
+ def _parse_month(self, starttime, next_month=False):
+ """
+ Inspects a given string with classic crontab information to calculate the next point in time that matches
+ Part of the old implementation
+
+ :param crontab: a string with crontab entries. It is expected to have the form of ``minute hour day weekday``
+ :param next_month: inspect the current month or the next following month
+ :return: false or datetime
+ """
+ # evaluate the crontab strings
+ minute_range = self._range(self.minute, 00, 59)
+ hour_range = self._range(self.hour, 00, 23)
+ if not next_month:
+ mdays = calendar.monthrange(starttime.year, starttime.month)[1]
+ elif starttime.month == 12:
+ mdays = calendar.monthrange(starttime.year + 1, 1)[1]
+ else:
+ mdays = calendar.monthrange(starttime.year, starttime.month + 1)[1]
+
+ if self.wday == '*' and self.day == '*':
+ day_range = self._day_range('0, 1, 2, 3, 4, 5, 6')
+ elif self.wday != '*' and self.day == '*':
+ day_range = self._range(self.wday,0,6)
+ day_range = self._day_range(','.join(day_range))
+ elif self.wday != '*' and self.day != '*':
+ day_range = self._range(self.wday,0,6)
+ day_range = self._day_range(','.join(day_range))
+ day_range = day_range + self._range(self.day, 0o1, mdays)
+ else:
+ day_range = self._range(self.day, 0o1, mdays)
+
+ # combine the different ranges
+ event_range = sorted([str(day) + '-' + str(hour) + '-' + str(minute) for minute in minute_range for hour in hour_range for day in day_range])
+ if next_month: # next month
+ next_event = event_range[0]
+ next_time = starttime + dateutil.relativedelta.relativedelta(months=+1)
+ else: # this month
+ now_str = starttime.strftime("%d-%H-%M")
+ next_event = self._next(lambda event: event > now_str, event_range)
+ if not next_event:
+ return False
+ next_time = starttime
+ day, hour, minute = next_event.split('-')
+ return next_time.replace(day=int(day), hour=int(hour), minute=int(minute), second=0, microsecond=0)
+
+ def _next(self, f, seq):
+ """Part of the old implementation"""
+ for item in seq:
+ if f(item):
+ return item
+ return False
+
+
+ def _range(self, entry, low, high):
+ """
+ inspects a single crontab entry for minutes our hours
+ Part of the old implementation
+
+ :param entry: a string with single entries of intervals, numeric ranges or single values
+ :param low: lower limit as integer
+ :param high: higher limit as integer
+ :return:
+ """
+ result = []
+ item_range = []
+
+ # Check for multiple comma separated values and process each of them recursively
+ if ',' in entry:
+ for item in entry.split(','):
+ result.extend(self._range(item, low, high))
+
+ # Check for intervals, e.g. "*/2", "9-17/2"
+ elif '/' in entry:
+ spec_range, interval = entry.split('/')
+ #logger.debug('Cron spec interval {} {}'.format(entry, interval))
+ result = self._range(spec_range, low, high)[::int(interval)]
+
+ # Check for numeric ranges, e.g. "9-17"
+ elif '-' in entry:
+ spec_low, spec_high = entry.split('-')
+ result = self._range('*', int(spec_low), int(spec_high))
+
+ # Process single value
+ else:
+ if entry == '*':
+ item_range = list(range(low, high + 1))
+ else:
+ item = int(entry)
+ if item > high: # entry above range
+ item = high # truncate value to highest possible
+ item_range.append(item)
+ for entry in item_range:
+ result.append('{:02d}'.format(entry))
+
+ return result
+
+ def _day_range(self, days):
+ """
+ inspect a given string with days given as integer numbers separated by ","
+ Part of the old implementation
+ :param days:
+ :return: an array with strings containing the days of month
+ """
+ now = datetime.date.today()
+ wdays = [MO, TU, WE, TH, FR, SA, SU]
+ result = []
+ for day in days.split(','):
+ wday = wdays[int(day)]
+ # add next weekday occurrence
+ day = now + dateutil.relativedelta.relativedelta(weekday=wday)
+ result.append(day.strftime("%d"))
+ # safety add-on if weekday equals todays weekday
+ day = now + dateutil.relativedelta.relativedelta(weekday=wday(+2))
+ result.append(day.strftime("%d"))
+ return result
+
+
+class Skytime(TriggerTime):
+ """
+ Implement sunrise/sunset/moonrise/moonset oriented triggertimes
+ """
+ sh = None
+ skyevents = ["sunrise","sunset","moonrise","moonset"]
+
+ def __init__(self, triggertime, location = None):
+ super().__init__(triggertime)
+
+ self.h_min = None # Either None or an int in range 0..23
+ self.m_min = None # Either None or an int in range 0..59
+ self.h_max = None # Either None or an int in range 0..23
+ self.m_max = None # Either None or an int in range 0..59
+ self.event = None # must be one of sunrise, sunset, moonrise, moonset
+ self.doff = None # Either None or a float in range -90.0 ... 90.0 (although the extreme values are nonsense)
+ self.moff = None # Either None or an int
+
+ # extended syntax that allows day, month and weekday as well
+ self.day = '*'
+ self.wday = '*'
+ self.month = '*'
+ self.day_range = Skytime.integer_range(self.day, 1, 31) # not zero based, limited to 1..31 days, needs to be clipped for actual month
+ self.month_range = Skytime.integer_range(self.month, 1, 12)
+ self.weekday_range = Skytime.integer_range(self.wday, 0, 6)
+
+ self.next_event = None
+ self.max_calc_time = 0
+
+ self._is_valid = False
+
+ self.parse_triggertime()
+
+ @staticmethod
+ def set_smarthome_reference(sh):
+ Skytime.sh = sh
+
+ @staticmethod
+ def get_skyevents():
+ return Skytime.skyevents
+
+ @staticmethod
+ def split_skyevents(triggertime: str):
+ """
+ Splits the triggertime into parts at '<'
+
+ :param triggertime: contains a trigger time like ``[H:M<](sunrise|sunset)[+|-][offset][ {value}={minvalue}")
+ if value > maxvalue:
+ value = maxvalue
+ logger.warning(f"{value}>{maxvalue} --> {value}={maxvalue}")
+ return value
+
+ @staticmethod
+ def split_times(timepoint: str):
+ if timepoint is None:
+ return None
+ timepoint = timepoint.strip()
+ if timepoint == "":
+ return None
+
+ h, sep, m = timepoint.partition(':')
+ try:
+ h = int(h)
+ h = Skytime.keep_in_range(h,0,23)
+ m = int(m)
+ m= Skytime.keep_in_range(m,0,59)
+ except ValueError:
+ pass
+ return(h, m)
+
+ def __str__(self):
+ r = f"""{self._triggertime} is {'' if self._is_valid else 'not '}valid and evaluates to:
+ min: {self.h_min}:{self.m_min}
+ event: {self.event}
+ degree offset: {self.doff}
+ time offset: {self.moff} min
+ max: {self.h_max}:{self.m_max}
+ Days: {self.day} -> {self.day_range}
+ Weekday: {self.wday} -> {self.weekday_range}
+ Months: {self.month} -> {self.month_range}
+ """
+ return(r)
+
+ def parse_triggertime(self):
+ """parses internal set triggertime into parts"""
+ logger.debug(f'Enter Skytime.parse_triggertime({self._triggertime})')
+ try:
+ with self._lock:
+ triggertime = self._triggertime
+ # find out how many parameters are given with this triggertime and save them to the class variables
+ try:
+ parameter_set = triggertime.strip().split()
+ except:
+ logger.error(f"skytime entry '{triggertime}' can not be split up into 1 or 4 parts")
+ return False
+
+ self.parameter_count = len(parameter_set)
+ if self.parameter_count == 1:
+ logger.debug(f'old smarthome.py style parameter set {triggertime} given')
+ self.timeset = parameter_set[0] # this contains something like 'mm:hh days_max_count:
+ logger.error(f'No matches after {days} examined days, giving up')
+ return get_invalid_time()
+ # preset current searcher
+ year = searchtime.year
+ month, em = Crontab.get_next_in_sorted_list(searchtime.month, self.month_range, 1, 12)
+ if month is not None:
+ if not em:
+ # if not an exact match for month then set starttime to earliest of next month
+ searchtime = searchtime.replace(month=month, day=1, hour=0, minute=0, second=0, microsecond=0)
+ day, em = Crontab.get_next_in_sorted_list(searchtime.day, self.day_range, 1, calendar.monthrange(year, month)[1])
+ if day is not None:
+ if not em:
+ searchtime = searchtime.replace(day=day,hour=0, minute=0, second=0)
+ weekday = searchtime.weekday()
+ if weekday in self.weekday_range:
+ # the day, month and weekday is correct with searchtime
+ # now get the skyevent time and see if it fits for this day.
+ if self.event in mappings:
+ eventtime = mappings[self.event](self.doff, self.moff, dt=searchtime)
+ # time in next_time will be in utctime. So we need to adjust it
+ if eventtime.tzinfo == tzutc():
+ eventtime = eventtime.astimezone(Skytime.sh.shtime.tzinfo())
+ logger.debug(f"starting with {starttime} the next {self.event}({self.doff},{self.moff}) is {eventtime}")
+ else:
+ logger.warning("searchtime.tzinfo was not given as utc!")
+ else:
+ logger.error(f'No function found to get next skyevent time for {self._triggertime}')
+ return get_invalid_time()
+
+ # eventtime will contain the next time e.g. a sunset will take place
+ # thus
+ # - searchtime must be smaller than eventtime and
+ # - eventtime might be one or more day(s) later
+
+ # if the dates differ then it must be certain that the new date adheres to the
+ # constraints of the day range.
+ if eventtime.date() > searchtime.date():
+ logger.debug(f"eventtime ({eventtime.date()}) is at least a day later than current searchtime ({searchtime}), skip to eventtime's early morning")
+ searchtime = eventtime.replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=Skytime.sh.shtime.tzinfo())
+ continue # need to start over for a matching date
+
+ # eventtime and searchtime have the same date
+ # now check time limits if given
+ if self.h_min is not None and self.m_min is not None:
+ try:
+ dmin = eventtime.replace(hour=self.h_min, minute=self.m_min, second=0, microsecond=0, tzinfo=Skytime.sh.shtime.tzinfo())
+ except Exception:
+ logger.error('Wrong syntax: {self._triggertime}. Should be [H:M<](skyevent)[+|-][offset][ eventtime:
+ eventtime = dmin
+
+ if self.h_max is not None and self.m_max is not None:
+ try:
+ dmax = eventtime.replace(hour=self.h_max, minute=self.m_max, second=0, microsecond=0, tzinfo=Skytime.sh.shtime.tzinfo())
+ logger.debug(f"searchtime={searchtime}, eventtime={eventtime}, dmax={dmax}")
+ except Exception:
+ logger.error('Wrong syntax: {self._triggertime}. Should be [H:M<](skyevent)[+|-][offset][ .
+#########################################################################
+
+"""
+This library imports modules with user-functions to be used in eval statements and in logics
+"""
+
+import os
+import logging
+
+from lib.translation import translate
+
+_logger = logging.getLogger(__name__)
+
+
+_uf_subdir = 'functions'
+
+_func_dir = None
+_user_modules = []
+
+
+def import_user_module(m):
+ """
+ Import a module with userfunctions
+
+ :param m: name of module to import from /functions
+
+ :return: True, if import was successful
+ """
+ modulename = _uf_subdir + '.' + m
+
+ import importlib
+ try:
+ exec(f"globals()['{m}']=importlib.import_module('{modulename}')")
+ except Exception as e:
+ _logger.error(translate("Error importing userfunctions from '{module}': {error}", {'module': m, 'error': e}))
+ return False
+ else:
+ global _uf_version
+ _uf_version = '?.?.?'
+ try:
+ exec( f"globals()['_uf_version'] = {m}._VERSION" )
+ except:
+ exec( f"{m}._VERSION = _uf_version" )
+
+ global _uf_description
+ _uf_description = '?'
+ try:
+ exec( f"globals()['_uf_description'] = {m}._DESCRIPTION" )
+ except:
+ exec( f"{m}._DESCRIPTION = _uf_description" )
+
+ _logger.notice(translate("Imported userfunctions from '{mmodule}' v{version} - {description}", {'module': m, 'version':_uf_version, 'description': _uf_description}))
+
+ return True
+
+
+def init_lib(shng_base_dir=None):
+ """
+ Initialize userfunctions module
+
+ :param shng_base_dir: Base dir of SmartHomeNG installation
+ """
+
+ global _func_dir
+ global _user_modules
+
+ if shng_base_dir is not None:
+ base_dir = shng_base_dir
+ else:
+ base_dir = os.getcwd()
+
+ _func_dir = os.path.join(base_dir, _uf_subdir)
+
+ user_modules = []
+ if os.path.isdir(_func_dir):
+ wrk = os.listdir(_func_dir)
+ for f in wrk:
+ if f.endswith('.py'):
+ user_modules.append(f.split(".")[0])
+
+ _user_modules = sorted(user_modules)
+
+ # Import all modules with userfunctions from /functions
+ for m in _user_modules:
+ import_user_module(m)
+ return
+
+
+def get_uf_dir():
+
+ return _func_dir
+
+
+def reload(userlib):
+
+ import importlib
+
+ if userlib in _user_modules:
+ try:
+ exec( f"importlib.reload({userlib})")
+ except Exception as e:
+ if str(e) == f"name '{userlib}' is not defined":
+ _logger.warning(translate("Error reloading userfunctions Modul '{module}': Module is not loaded, trying to newly import userfunctions '{module}' instead", {'module': userlib}))
+ if import_user_module(userlib):
+ return True
+ else:
+ return False
+ else:
+ _logger.error(translate("Error reloading userfunctions '{module}': {error} - old version of '{module}' is still active", {'module': userlib, 'error': e}))
+ return False
+
+ else:
+ _logger.notice(translate("Reloaded userfunctions '{module}'", {'module': userlib}))
+ return True
+ else:
+ if import_user_module(userlib):
+ #_logger.notice(translate("Reload: Loaded new userfunctions '{module}'", {'module': userlib}))
+ return True
+ else:
+ _logger.error(translate("Reload: Userfunctions '{module}' do not exist", {'module': userlib}))
+ return False
+
+
+def reload_all():
+
+ if _user_modules == []:
+ _logger.warning(translate('No userfunctions are loaded, nothing to reload'))
+ return False
+ else:
+ result = True
+ for lib in _user_modules:
+ if not reload(lib):
+ result = False
+ return result
+
+
+def list_userlib_files():
+
+ for lib in _user_modules:
+ _logger.warning(f'uf.{lib}')
+
diff --git a/lib/utils.py b/lib/utils.py
index 6eb2d5b376..197115abb9 100644
--- a/lib/utils.py
+++ b/lib/utils.py
@@ -34,6 +34,7 @@
import hashlib
import ipaddress
import socket
+import subprocess
logger = logging.getLogger(__name__)
@@ -55,23 +56,28 @@ def is_mac(mac):
"""
mac = str(mac)
+ # notation without separators
if len(mac) == 12:
for c in mac:
- try:
- if int(c, 16) > 15:
- return False
- except:
+ # each digit is hex
+ if c not in '0123456789abcdefABCDEF':
return False
return True
- octets = re.split('[\:\-\ ]', mac)
+ # notation with separators -> 12 digits + 5 separators
+ if len(mac) != 17:
+ return False
+ octets = re.split('[: -]', mac)
+ # 6 groups...
if len(octets) != 6:
return False
- for i in octets:
- try:
- if int(i, 16) > 255:
- return False
- except:
+ for o in octets:
+ # ... of 2 digits each
+ if len(o) != 2:
+ return False
+ # and each digit is hex
+ for c in ''.join(octets):
+ if c not in '0123456789abcdefABCDEF':
return False
return True
@@ -89,6 +95,7 @@ def is_ip(string):
"""
return Utils.is_ipv4(string)
+ # later: return (Utils.is_ipv4(string) or Utils.is_ipv6(string))
@staticmethod
def is_ipv4(string):
@@ -141,15 +148,18 @@ def is_hostname(string):
"""
try:
- return bool(re.match("^(([a-zA-Z]|[a-zA-Z][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z]|[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9])$", string))
+ return bool(re.match("^(([a-zA-Z]|[a-zA-Z][a-zA-Z0-9-]*[a-zA-Z0-9])\\.)*([A-Za-z]|[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9])$", string))
except TypeError:
return False
@staticmethod
def get_local_ipv4_address():
"""
- Get's local ipv4 address
- TODO: What if more than one interface present ?
+ Get local ipv4 address of the interface with the default gateway.
+ Return '127.0.0.1' if no suitable interface is found
+ NOTE: if more than one IP addresses are available and no external
+ gateway is configured, thie method returns one of the configured
+ addresses, but not deterministically.
:return: IPv4 address as a string
:rtype: string
@@ -168,15 +178,15 @@ def get_local_ipv4_address():
@staticmethod
def get_local_ipv6_address():
"""
- Get's local ipv6 address
- TODO: What if more than one interface present ?
+ Get local ipv6 address of the interface with the default gateway.
+ Return '::1' if no suitable interface is found
:return: IPv6 address as a string
:rtype: string
"""
try:
s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
- s.connect(('2001:4860:4860::8888', 1))
+ s.connect(('fda2:ffff:ffff:ffff:ffff:ffff:ffff:ffff', 1))
IP = s.getsockname()[0]
except:
IP = '::1'
@@ -536,10 +546,7 @@ def execute_subprocess(commandline, wait=True):
"""
Executes a subprocess via a shell and returns the output written to stdout by this process as a string
"""
- ## get subprocess module
- import subprocess
-
- ## call date command ##
+ # call date command
p = subprocess.Popen(commandline, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
# Talk with date command i.e. read data from stdout and stderr. Store this info in tuple ##
@@ -551,17 +558,13 @@ def execute_subprocess(commandline, wait=True):
# print("result="+str(result))
# print("err="+str(err))
if wait:
- ## Wait for date to terminate. Get return returncode ##
- p_status = p.wait()
+ # Wait for date to terminate. Get return returncode
+ p.wait()
return (str(result, encoding='utf-8', errors='strict'), str(err, encoding='utf-8', errors='strict'))
-
-
-
def get_python_version():
-
- PYTHON_VERSION = str(sys.version_info[0])+'.'+str(sys.version_info[1])+'.'+str(sys.version_info[2])+' '+str(sys.version_info[3])
+ PYTHON_VERSION = str(sys.version_info[0]) + '.' + str(sys.version_info[1]) + '.' + str(sys.version_info[2]) + ' ' + str(sys.version_info[3])
if sys.version_info[3] != 'final':
PYTHON_VERSION += ' '+str(sys.version_info[4])
return PYTHON_VERSION
@@ -571,15 +574,11 @@ def execute_subprocess(commandline, wait=True):
"""
Executes a subprocess via a shell and returns the output written to stdout by this process as a string
"""
- ## get subprocess module
- import subprocess
- ## call date command ##
p = subprocess.Popen(commandline, stdout=subprocess.PIPE, shell=True)
- # Talk with date command i.e. read data from stdout and stderr. Store this info in tuple ##
# Interact with process: Send data to stdin. Read data from stdout and stderr, until end-of-file is reached.
# Wait for process to terminate. The optional input argument should be a string to be sent to the child process, or None, if no data should be sent to the child.
(result, err) = p.communicate()
-# logger.warning("execute_subprocess: commandline='{}', result='{}', err='{}'".format(command, result, err))
+ # logger.warning("execute_subprocess: commandline='{}', result='{}', err='{}'".format(command, result, err))
print("err='{}'".format(err))
if wait:
## Wait for date to terminate. Get return returncode ##
@@ -592,4 +591,3 @@ def running_virtual():
# Check supports venv && virtualenv
return (getattr(sys, 'base_prefix', sys.prefix) != sys.prefix or
hasattr(sys, 'real_prefix'))
-
diff --git a/logics/check_items.py b/logics/check_items.py
new file mode 100644
index 0000000000..d009545f1f
--- /dev/null
+++ b/logics/check_items.py
@@ -0,0 +1,97 @@
+#!/usr/bin/env python3
+# check_items.py
+"""
+given following items within a yaml:
+
+
+MyItem:
+ MyChildItem:
+ type: num
+ initial_value: 12
+ MyGrandchildItem:
+ type: str
+ initial_value: "foo"
+
+Within a logic it is possible to set the value of MyChildItem to 42 with
+``sh.MyItem.MyChildItem(42)`` and retrieve the Items value with
+``value = sh.MyItem.MyChildItem()``
+
+Often beginners forget the parentheses and instead write
+``sh.MyItem.MyChildItem = 42`` when they really intend to assign the value ``42``
+to the item or write ``value = sh.MyItem.MyChildItem`` when they really want to
+retrieve the item's value.
+
+But using ``sh.MyItem.MyChildItem = 42`` destroys the structure here and makes
+it impossible to retrieve the value of the child
+``MyItem.MyChildItem.MyGrandchildItem``
+Alike, an instruction as ``value = sh.MyItem.MyChildItem`` will not assign the
+value of ``sh.MyItem.MyChildItem`` but assign a reference to the item object
+``sh.MyItem.MyChildItem``
+
+It is not possible with Python to intercept an assignment to a variable or an
+objects' attribute. The only thing one can do is search all items for a
+mismatching item type.
+
+This logic checks all items returned by SmartHomeNG, and if it encounters one
+which seems to be damaged like described before, it attempts to repair the
+broken assignment.
+
+"""
+from lib.item import Items
+
+
+def repair_item(sh, item):
+ path = item.id()
+ path_elems = path.split('.')
+ ref = sh
+
+ # traverse through object structure sh.path1.path2...
+ try:
+ for path_part in path_elems[:-1]:
+ ref = getattr(ref, path_part)
+
+ setattr(ref, path_elems[-1], item)
+ logger.info(f'Item reference repaired for {path}')
+ return True
+ except NameError:
+ logger.error(f'Error: item traversal for {path} failed at part {path_part}. Item list not sorted?')
+
+ return False
+
+
+def get_item_type(sh, path):
+ expr = f'type(sh.{path})'
+ return str(eval(expr))
+
+
+def check_item(sh, path):
+ global get_item_type
+
+ return get_item_type(sh, path) == ""
+
+
+# to get access to the object instance:
+items = Items.get_instance()
+
+# to access a method (eg. to get the list of Items):
+# allitems = items.return_items()
+problems_found = 0
+problems_fixed = 0
+
+for one in items.return_items(sorted=True):
+ # get the items full path
+ path = one.id()
+ try:
+ if not check_item(sh, path):
+ logger.error(f"Error: item {path} has type {get_item_type(sh, path)} but should be an Item Object")
+ problems_found += 1
+ if repair_item(sh, one):
+ if check_item(sh, path):
+ problems_fixed += 1
+ except ValueError as e:
+ logger.error(f'Error {e} while processing item {path}, parent defective? Items not sorted?')
+
+if problems_found:
+ logger.error(f"{problems_found} problematic item assignment{'' if problems_found == 1 else 's'} found, {problems_fixed} item assignment{'' if problems_fixed == 1 else 's'} fixed")
+else:
+ logger.notice("no problems found")
\ No newline at end of file
diff --git a/modules/admin/__init__.py b/modules/admin/__init__.py
index 56651b8f6a..e498f257ac 100644
--- a/modules/admin/__init__.py
+++ b/modules/admin/__init__.py
@@ -22,9 +22,9 @@
import os
import logging
-import json
import cherrypy
+from lib.utils import Utils
from lib.model.module import Module
from lib.module import Modules
@@ -42,6 +42,7 @@
from .api_config import *
from .api_files import *
from .api_items import *
+from .api_functions import *
from .api_loggers import *
from .api_logs import *
from .api_scenes import *
@@ -88,7 +89,7 @@ def __init__(self, sh, testparam=''):
self.mod_http = Modules.get_instance().get_module('http') # try/except to handle running in a core version that does not support modules
except:
self.mod_http = None
- if self.mod_http == None:
+ if self.mod_http is None:
self.logger.error(
"Module '{}': Not initializing - Module 'http' has to be loaded BEFORE this module".format(
self._shortname))
@@ -117,7 +118,7 @@ def __init__(self, sh, testparam=''):
mysuburl = ''
if suburl != '':
mysuburl = '/' + suburl
- ip = get_local_ipv4_address()
+ ip = Utils.get_local_ipv4_address()
self._port = self.mod_http._port
# self.logger.warning('port = {}'.format(self._port))
self.shng_url_root = 'http://' + ip + ':' + str(self._port) # for links mto plugin webinterfaces
@@ -223,19 +224,15 @@ def start(self):
return
-
def stop(self):
"""
- Stop the admin module
- Cleanup code of the admin module
"""
- self.logger.info("Shutting down".format(self._shortname))
+ self.logger.info(f"Shutting down {self._shortname}")
for stop_method in self._stop_methods:
stop_method()
- self.logger.info("Shutted down".format(self._shortname))
-
+ self.logger.info(f"{self._shortname} shut down ")
def add_stop_method(self, method, classname=''):
"""
@@ -263,19 +260,18 @@ def error_page(self, status, message, traceback, version):
:return: page to display (a redirect)
:rtype: str
"""
- ip = get_local_ipv4_address()
- mysuburl = ''
- if suburl != '':
- mysuburl = '/' + suburl
+ # ip = Utils.get_local_ipv4_address()
+ # mysuburl = ''
+ # if suburl != '':
+ # mysuburl = '/' + suburl
# page = ' '
# page = ' '
- page = '404: Page not found! '+message
+ page = '404: Page not found! ' + message
self.logger.warning(
"error_page: status = {}, message = {}".format(status, message))
return page
-
def _error_page(self, status, message, traceback, version):
"""
Generate html page for errors
@@ -293,7 +289,7 @@ def _error_page(self, status, message, traceback, version):
:rtype: str
"""
- show_traceback = True
+ # show_traceback = True
errno = status.split()[0]
result = ' '
result += ' '
@@ -304,7 +300,7 @@ def _error_page(self, status, message, traceback, version):
result += ' '
result += '' + message + ' '
- if (self._showtraceback == False) or (errno == '404'):
+ if not self._showtraceback or (errno == '404'):
traceback = ''
else:
traceback = traceback.replace('\n', ' ')
@@ -322,35 +318,12 @@ def _error_page(self, status, message, traceback, version):
return result
-def get_local_ipv4_address():
- """
- Get's local ipv4 address of the interface with the default gateway.
- Return '127.0.0.1' if no suitable interface is found
-
- :return: IPv4 address as a string
- :rtype: string
- """
- s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- try:
- s.connect(('8.8.8.8', 1))
- IP = s.getsockname()[0]
- except:
- IP = '127.0.0.1'
- finally:
- s.close()
- return IP
-
def translate(s):
+ # needed for Admin UI
return s
-import socket
-
-from lib.plugin import Plugins
-from lib.utils import Utils
-
-
class WebInterface(SystemData, ItemData, PluginData):
def __init__(self, webif_dir, module, shng_url_root, url_root):
@@ -399,6 +372,8 @@ def __init__(self, webif_dir, module, shng_url_root, url_root):
self.files = FilesController(self.module)
self.items = ItemsController(self.module)
self.items.list = ItemsListController(self.module)
+ self.functions = FunctionsController(self.module)
+ self.functions.reload = FunctionsReloadController(self.module)
self.logics = LogicsController(self.module)
self.loggers = LoggersController(self.module)
self.logs = LogsController(self.module)
@@ -410,6 +385,7 @@ def __init__(self, webif_dir, module, shng_url_root, url_root):
self.plugins.info = PluginsInfoController(self.module, self.shng_url_root)
self.plugins.logicparams = PluginsLogicParametersController(self.module)
self.scenes = ScenesController(self.module)
+ self.scenes.reload = ScenesReloadController(self.module)
self.schedulers = SchedulersController(self.module)
self.server = ServerController(self.module)
self.services = ServicesController(self.module)
@@ -417,9 +393,6 @@ def __init__(self, webif_dir, module, shng_url_root, url_root):
return
-
@cherrypy.expose(['home', ''])
def index(self):
return "Give SmartHomeNG a REST."
-
-
diff --git a/modules/admin/api_auth.py b/modules/admin/api_auth.py
index 9db9e8b88f..16ad273b63 100644
--- a/modules/admin/api_auth.py
+++ b/modules/admin/api_auth.py
@@ -129,7 +129,13 @@ def authenticate(self):
payload['ttl'] = self.module.login_expiration
payload['name'] = user.get('name', '?')
payload['admin'] = ('admin' in user.get('groups', []))
- response['token'] = jwt.encode(payload, self.jwt_secret, algorithm='HS256').decode('utf-8')
+ # try/except to support PyJWT 1.7.x and 2.x
+ try:
+ # For PyJWT <= 1.7.1 (and maybe higher?)
+ response['token'] = jwt.encode(payload, self.jwt_secret, algorithm='HS256').decode('utf-8')
+ except:
+ # For PyJWT >= 2.3.0 (and maybe lower?)
+ response['token'] = jwt.encode(payload, self.jwt_secret, algorithm='HS256')
self.logger.info("AuthController.authenticate(): payload = {}".format(payload))
self.logger.info("AuthController.authenticate(): response = {}".format(response))
self.logger.info("AuthController.authenticate(): cherrypy.url = {}".format(cherrypy.url()))
diff --git a/modules/admin/api_config.py b/modules/admin/api_config.py
index f68e3e315a..d63dbdc27d 100644
--- a/modules/admin/api_config.py
+++ b/modules/admin/api_config.py
@@ -130,6 +130,13 @@ def update_holidays(self, data):
except Exception as e:
self.logger.critical("update_holidays: Exception {}".format(e))
+ if self.holidays_confdata['custom'] == []:
+ #self.holidays_confdata['custom'] = None
+ del self.holidays_confdata['custom']
+
+ if self.holidays_confdata['location']['state'] is None:
+ del self.holidays_confdata['location']['state']
+
self.logger.info("update_holidays: self.holidays_confdata = '{}'".format(self.holidays_confdata))
shyaml.yaml_save_roundtrip(filename, self.holidays_confdata, create_backup=True)
return
diff --git a/modules/admin/api_files.py b/modules/admin/api_files.py
index 8647cf4d31..f30f4b05e4 100644
--- a/modules/admin/api_files.py
+++ b/modules/admin/api_files.py
@@ -53,6 +53,7 @@ def __init__(self, module):
self.etc_dir = self._sh._etc_dir
self.items_dir = self._sh._items_dir
+ self.functions_dir = self._sh._functions_dir
self.scenes_dir = self._sh._scenes_dir
self.logics_dir = self._sh._logic_dir
self.extern_conf_dir = self._sh._extern_conf_dir
@@ -317,6 +318,72 @@ def delete_scenes_config(self, filename):
return json.dumps(result)
+ # ======================================================================
+ # /api/files/functions
+ #
+ def get_functions_filelist(self):
+
+ list = os.listdir( self.functions_dir )
+ filelist = []
+ for filename in list:
+ if filename.endswith('.py'):
+ filelist.append(filename)
+
+ self.logger.info("filelist = {}".format(filelist))
+ self.logger.info("filelist.sort() = {}".format(filelist.sort()))
+ return json.dumps(sorted(filelist))
+
+ def get_functions_config(self, fn):
+
+ self.logger.info("FilesController.get_functions_config({})".format(fn))
+ if fn.endswith('.tpl'):
+ filename = os.path.join(self.functions_dir, fn)
+ else:
+ filename = os.path.join(self.functions_dir, fn + '.py')
+ read_data = None
+ with open(filename, encoding='UTF-8') as f:
+ read_data = f.read()
+ return cherrypy.lib.static.serve_file(filename, 'application/x-download',
+ 'attachment', fn + '.py')
+
+
+ def save_functions_config(self, filename):
+ """
+ Save function library
+
+ :return: status dict
+ """
+ params = None
+ params = self.get_body(text=True)
+ if params is None:
+ self.logger.warning("FilesController.save_functions_config(): Bad, request")
+ raise cherrypy.HTTPError(status=411)
+ self.logger.debug("FilesController.save_functions_config(): '{}'".format(params))
+
+
+ filename = os.path.join(self.functions_dir, filename + '.py')
+ read_data = None
+ with open(filename, 'w', encoding='UTF-8') as f:
+ f.write(params)
+
+ result = {"result": "ok"}
+ return json.dumps(result)
+
+
+ def delete_functions_config(self, filename):
+ """
+ Delete a scene configuration file
+
+ :return: status dict
+ """
+ self.logger.debug("FilesController.delete_functions_config(): '{}'".format(filename))
+
+ filename = os.path.join(self.functions_dir, filename + '.py')
+ os.remove(filename)
+
+ result = {"result": "ok"}
+ return json.dumps(result)
+
# ======================================================================
# /api/files/logics
#
@@ -520,6 +587,12 @@ def read(self, id='', filename=''):
cherrypy.response.headers['Cache-Control'] = 'no-cache, max-age=0, must-revalidate, no-store'
return self.get_scenes_config(filename)
+ elif (id == 'functions' and filename == ''):
+ return self.get_functions_filelist()
+ elif id == 'functions':
+ cherrypy.response.headers['Cache-Control'] = 'no-cache, max-age=0, must-revalidate, no-store'
+ return self.get_functions_config(filename)
+
elif (id == 'logics' and filename == ''):
return self.get_logics_filelist()
elif id == 'logics':
@@ -548,6 +621,8 @@ def update(self, id='', filename=''):
return self.save_items_config(filename)
elif (id == 'scenes' and filename != ''):
return self.save_scenes_config(filename)
+ elif (id == 'functions' and filename != ''):
+ return self.save_functions_config(filename)
elif (id == 'logics' and filename != ''):
return self.save_logics_config(filename)
elif (id == 'restore' and filename != ''):
@@ -584,6 +659,8 @@ def delete(self, id='', filename=''):
return self.delete_items_config(filename)
if (id == 'scenes' and filename != ''):
return self.delete_scenes_config(filename)
+ if (id == 'functions' and filename != ''):
+ return self.delete_functions_config(filename)
return None
diff --git a/modules/admin/api_functions.py b/modules/admin/api_functions.py
new file mode 100644
index 0000000000..7bb8ca961e
--- /dev/null
+++ b/modules/admin/api_functions.py
@@ -0,0 +1,189 @@
+#!/usr/bin/env python3
+# vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab
+#########################################################################
+# Copyright 2021- Martin Sinn m.sinn@gmx.de
+#########################################################################
+# This file is part of SmartHomeNG.
+#
+# SmartHomeNG is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# SmartHomeNG is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with SmartHomeNG. If not, see .
+#########################################################################
+
+
+import os
+import logging
+import json
+import cherrypy
+
+from lib.item import Items
+
+from .rest import RESTResource
+
+
+class FunctionsController(RESTResource):
+
+ def __init__(self, module):
+ self._sh = module._sh
+ self.module = module
+ self.base_dir = self._sh.get_basedir()
+ self.logger = logging.getLogger(__name__)
+
+ self.items = Items.get_instance()
+
+ return
+
+
+ # ======================================================================
+ # /api/scenes
+ #
+ # def root(self):
+ # if self.items == None:
+ # self.items = Items.get_instance()
+ #
+ # from lib.scene import Scenes
+ # get_param_func = getattr(Scenes, "get_instance", None)
+ # if callable(get_param_func):
+ # supported = True
+ # self.scenes = Scenes.get_instance()
+ # scene_list = []
+ # if self.scenes is not None:
+ # scene_list = self.scenes.get_loaded_scenes()
+ #
+ # disp_scene_list = []
+ # for scene in scene_list:
+ # scene_dict = {}
+ # scene_dict['path'] = scene
+ # # scene_dict['name'] = str(self._sh.return_item(scene))
+ # scene_dict['name'] = str(self.items.return_item(scene))
+ #
+ # action_list = self.scenes.get_scene_actions(scene)
+ # scene_dict['value_list'] = action_list
+ # # scene_dict[scene] = action_list
+ #
+ # disp_action_list = []
+ # for value in action_list:
+ # action_dict = {}
+ # action_dict['action'] = value
+ # action_dict['action_name'] = self.scenes.get_scene_action_name(scene, value)
+ # action_list = self.scenes.return_scene_value_actions(scene, value)
+ # for action in action_list:
+ # if not isinstance(action[0], str):
+ # action[0] = action[0].id()
+ # action_dict['action_list'] = action_list
+ #
+ # disp_action_list.append(action_dict)
+ # scene_dict['values'] = disp_action_list
+ # self.logger.debug("scenes_html: disp_action_list for scene {} = {}".format(scene, disp_action_list))
+ #
+ # disp_scene_list.append(scene_dict)
+ # else:
+ # supported = False
+ # return json.dumps(disp_scene_list)
+
+
+ # ======================================================================
+ # GET /api/functions
+ #
+ @cherrypy.expose
+ def read(self, id=None):
+ """
+ Handle GET requests for scenes API
+ """
+ if self.items == None:
+ self.items = Items.get_instance()
+
+ from lib.userfunctions import reload
+ from lib.userfunctions import reload_all
+
+ get_param_func = getattr(Scenes, "get_instance", None)
+ if callable(get_param_func):
+ supported = True
+ self.scenes = Scenes.get_instance()
+ scene_list = []
+ if self.scenes is not None:
+ scene_list = self.scenes.get_loaded_scenes()
+
+ disp_scene_list = []
+ for scene in scene_list:
+ scene_dict = {}
+ scene_dict['path'] = scene
+ # scene_dict['name'] = str(self._sh.return_item(scene))
+ scene_dict['name'] = str(self.items.return_item(scene))
+
+ action_list = self.scenes.get_scene_actions(scene)
+ scene_dict['value_list'] = action_list
+ # scene_dict[scene] = action_list
+
+ disp_action_list = []
+ for value in action_list:
+ action_dict = {}
+ action_dict['action'] = value
+ action_dict['action_name'] = self.scenes.get_scene_action_name(scene, value)
+ action_list = self.scenes.return_scene_value_actions(scene, value)
+ for action in action_list:
+ if not isinstance(action[0], str):
+ action[0] = action[0].id()
+ action_dict['action_list'] = action_list
+
+ disp_action_list.append(action_dict)
+ scene_dict['values'] = disp_action_list
+ self.logger.debug("scenes_html: disp_action_list for scene {} = {}".format(scene, disp_action_list))
+
+ disp_scene_list.append(scene_dict)
+ else:
+ supported = False
+ return json.dumps(disp_scene_list)
+
+ read.expose_resource = True
+ read.authentication_needed = True
+
+
+class FunctionsReloadController(RESTResource):
+
+ def __init__(self, module):
+
+ self._sh = module._sh
+ self.module = module
+ self.base_dir = self._sh.get_basedir()
+ self.logger = logging.getLogger(__name__)
+
+ self.items = Items.get_instance()
+
+ return
+
+
+ # ======================================================================
+ # /api/functions/reload
+ #
+
+ @cherrypy.expose
+ def update(self, id=None):
+ """
+ Handle PUT requests for scenes/reload API
+ """
+ from lib.scene import Scenes
+ self.scenes = Scenes.get_instance()
+
+ from lib.userfunctions import reload
+ from lib.userfunctions import reload_all
+
+ if id == 'all':
+ #result = self.scenes.reload_scenes()
+ result = reload_all()
+ return json.dumps(result)
+ else:
+ result = reload(id)
+ return json.dumps(result)
+
+ update.expose_resource = True
+ update.authentication_needed = True
diff --git a/modules/admin/api_items.py b/modules/admin/api_items.py
index 87563dfaa2..df771be5e7 100644
--- a/modules/admin/api_items.py
+++ b/modules/admin/api_items.py
@@ -59,7 +59,6 @@ def read(self, id=None):
# /api/items/structs
self.logger.info("ItemsController.root(): item_name = {}".format(id))
result = self.items.return_struct_definitions(all=False)
-
return json.dumps(result)
#raise cherrypy.NotFound
diff --git a/modules/admin/api_loggers.py b/modules/admin/api_loggers.py
index 539bbf61e2..e2dcde1231 100644
--- a/modules/admin/api_loggers.py
+++ b/modules/admin/api_loggers.py
@@ -48,6 +48,7 @@ def __init__(self, module):
self.logging_levels[50] = 'CRITICAL'
self.logging_levels[40] = 'ERROR'
self.logging_levels[30] = 'WARNING'
+ self.logging_levels[29] = 'NOTICE'
self.logging_levels[20] = 'INFO'
self.logging_levels[10] = 'DEBUG'
self.logging_levels[0] = 'NOTSET'
@@ -100,7 +101,8 @@ def get_active_loggers(self):
loggerlist = []
try:
- for l in logging.Logger.manager.loggerDict:
+ wrk_loggerDict = logging.Logger.manager.loggerDict
+ for l in wrk_loggerDict:
lg = logging.Logger.manager.loggerDict[l]
try:
@@ -154,7 +156,8 @@ def get_logger_active_configuration(self, loggername=None):
active = {}
active_logger = logging.getLogger(loggername)
active['disabled'] = active_logger.disabled
- active['level'] = self.logging_levels[active_logger.level]
+ #active['level'] = self.logging_levels[active_logger.level]
+ active['level'] = self.logging_levels.get(active_logger.level, 'UNKNOWN_'+str(active_logger.level))
active['filters'] = active_logger.filters
hl = []
diff --git a/modules/admin/api_plugins.py b/modules/admin/api_plugins.py
index 85cfd0ce11..8eafc46248 100644
--- a/modules/admin/api_plugins.py
+++ b/modules/admin/api_plugins.py
@@ -28,6 +28,7 @@
import requests
import time
import threading
+from random import randrange
import lib.shyaml as shyaml
import lib.config
@@ -324,7 +325,7 @@ def _test_for_blog_articles_task(self):
if self.plugins == None:
self.plugins = Plugins.get_instance()
if self.plugins != None and self._sh.shng_status.get('code', 0) == 20: # Running
- self._sh.scheduler._scheduler[self._blog_task_name]['cycle'] = {120 * 60 : None} # set scheduler cycle to test every 2 hours
+ self._sh.scheduler._scheduler[self._blog_task_name]['cycle'] = {120 * 60 + randrange(60) : None} # set scheduler cycle to test every 2 hours
start = time.time()
temp_blog_urls = {}
@@ -345,10 +346,14 @@ def _test_for_blog_articles_task(self):
if r.status_code == 404:
temp_blog_urls[plugin_name] = ''
elif r.status_code != 200:
- self.logger.error("Received status_code {} for get-request to {}".format(r.status_code, temp_blog_urls[plugin_name]))
+ if r.status_code in [500, 503]:
+ self.logger.info("www.smarthomeng.de sent status_code {} for get-request to {}".format(r.status_code, temp_blog_urls[plugin_name]))
+ else:
+ self.logger.notice("www.smarthomeng.de sent status_code {} for get-request to {}".format(r.status_code, temp_blog_urls[plugin_name]))
temp_blog_urls[plugin_name] = ''
else:
pass
+ time.sleep(1)
except OSError as e:
if str(e).find('[Errno 101]') > -1: # [Errno 101] Das Netzwerk ist nicht erreichbar
pass
@@ -391,7 +396,10 @@ def read(self, id=None):
plugin['stopped'] = False
# Update(s) triggered by < strong > {{p.instance._itemlist | length}} < / strong > items
- plugin['triggers'] = str(x._itemlist)
+ plugin['triggers'] = []
+ for it in x._itemlist:
+ plugin['triggers'].append(it._path)
+ #self.logger.warning("{} items={}, itemlist={}".format(x.get_shortname(), len(plugin['triggers']), plugin['triggers']))
if isinstance(x, SmartPlugin):
plugin['pluginname'] = x.get_shortname()
diff --git a/modules/admin/api_scenes.py b/modules/admin/api_scenes.py
index 402e1e33db..3831778373 100644
--- a/modules/admin/api_scenes.py
+++ b/modules/admin/api_scenes.py
@@ -145,3 +145,37 @@ def read(self, id=None):
read.expose_resource = True
read.authentication_needed = True
+
+class ScenesReloadController(RESTResource):
+
+ def __init__(self, module):
+ self._sh = module._sh
+ self.module = module
+ self.base_dir = self._sh.get_basedir()
+ self.logger = logging.getLogger(__name__)
+
+ self.items = Items.get_instance()
+
+ return
+
+
+ # ======================================================================
+ # /api/scenes/reload
+ #
+
+ @cherrypy.expose
+ def update(self, id=None):
+ """
+ Handle PUT requests for scenes/reload API
+ """
+ from lib.scene import Scenes
+ self.scenes = Scenes.get_instance()
+
+ if id == 'all':
+ result = self.scenes.reload_scenes()
+ return json.dumps(result)
+ else:
+ return json.dumps(False)
+
+ update.expose_resource = True
+ update.authentication_needed = True
diff --git a/modules/admin/api_services.py b/modules/admin/api_services.py
index a34ea6d3eb..219c002bb4 100755
--- a/modules/admin/api_services.py
+++ b/modules/admin/api_services.py
@@ -102,6 +102,8 @@ def eval_syntax_checker(self, eval_code, relative_to):
shtime = Shtime.get_instance()
items = Items.get_instance()
import math
+ import lib.userfunctions as uf
+
eval_code = eval_code.replace('\r', '').replace('\n', ' ').replace(' ', ' ').strip()
if relative_to == '':
diff --git a/modules/admin/itemdata.py b/modules/admin/itemdata.py
index 32d1cd7f96..54b5f86e5f 100644
--- a/modules/admin/itemdata.py
+++ b/modules/admin/itemdata.py
@@ -23,6 +23,7 @@
import collections
import html
import json
+import ast
import cherrypy
@@ -95,6 +96,20 @@ def _build_item_tree(self, parent_items_sorted):
# -----------------------------------------------------------------------------------
+ def escape_complex_value(self, value):
+
+ wrk = str(value)
+ if wrk == '':
+ return wrk
+ wrk = wrk.replace('&', '&')
+ wrk = wrk.replace('>', '>')
+ wrk = wrk.replace('<', '<')
+ try:
+ return str(ast.literal_eval(wrk))
+ except:
+ self.logger.error(f"escape_complex_value: cannot handle value = '{wrk}'")
+ return ''
+
@cherrypy.expose
def item_detail_json_html(self, item_path):
"""
@@ -225,6 +240,10 @@ def item_detail_json_html(self, item_path):
'on_update': html.escape(self.list_to_displaystring(on_update_list)),
'on_change': html.escape(self.list_to_displaystring(on_change_list)),
'log_change': self.disp_str(item._log_change),
+ 'log_level': self.disp_str(item._log_level_name),
+ 'log_text': self.disp_str(item._log_text),
+ 'log_mapping': self.disp_str(item._log_mapping),
+ 'log_rules': self.disp_str(item._log_rules),
'cycle': str(cycle),
'crontab': str(crontab),
'autotimer': self.disp_str(item._autotimer),
@@ -246,13 +265,19 @@ def item_detail_json_html(self, item_path):
data_dict['struct'] = item._struct
# cast raw data to a string
- if item.type() in ['foo', 'list', 'dict']:
+ if item.type() in ['foo']:
data_dict['value'] = str(item._value)
data_dict['last_value'] = str(last_value)
data_dict['previous_value'] = str(prev_value)
+ # cast list/dict data to a string
+ if item.type() in ['list', 'dict']:
+ data_dict['value'] = self.escape_complex_value(item._value)
+ data_dict['last_value'] = self.escape_complex_value(last_value)
+ data_dict['previous_value'] = self.escape_complex_value(prev_value)
+
+
item_data.append(data_dict)
- # self.logger.warning("details: item_data = {}".format(item_data))
return json.dumps(item_data)
else:
self.logger.error("Requested item '{}' is None, check if item really exists.".format(item_path))
diff --git a/modules/admin/webif/static/assets/i18n/de.json b/modules/admin/webif/static/assets/i18n/de.json
index d68e2e3deb..2065a7a54f 100644
--- a/modules/admin/webif/static/assets/i18n/de.json
+++ b/modules/admin/webif/static/assets/i18n/de.json
@@ -15,6 +15,7 @@
"SCENES": "Szenen",
"SCENE_LIST": "Szenen Liste",
"SCENE_CONFIGURATION": "Szenen Konfiguration",
+ "FUNCTION_CONFIGURATION": "User-Funktionen",
"THREADS": "Threads",
"LOGS": "Logs",
"LOGS_DISPLAY": "Logs Anzeigen",
@@ -30,6 +31,7 @@
"SHNG_VERSION": "SmartHomeNG Version",
"IN": "in",
"SHNG_PLG_VERSION": "SmartHomeNG Plugins Version",
+ "SHNG_ADMIN_GUI": "Administrations-Oberfläche",
"HOST": "Host",
"OS": "Betriebssystem",
"PID": "Prozess ID",
@@ -384,6 +386,18 @@
"DELETE_CONFIG": "Löschen der Szene bestätigen",
"DELETE_CONFIG_FILE": "Soll die Szenen-Konfigurationsdatei '{{config}}' wirklich gelöscht werden?"
},
+ "FUNCTION_CONFIG": {
+ "DEFINITION_FILES": "Funktions-Bibliotheken",
+ "CONFIG_FILE": "Funktion Bibliotheksdatei",
+ "FILE_NOT_FOUND": "Datei nicht gefunden!",
+ "FILETYPE_UNSUPPORTED": "Dateityp nicht unterstützt!",
+ "CONFIG_ERROR": "Konfigurations-Fehler - Konfiguration nicht gespeichert!",
+ "CONFIG_ERROR_TEXT": "Fehler in der Funktionsbibliothek gefunden.\nUngültiges Format der Datei.\n",
+ "NAME_CONFIGURATION": "Dateinamen für die neue Funktionsbibliothek wählen",
+ "UNIQUE_NAME": "Eindeutiger Dateiname (ohne Erweiterung)",
+ "DELETE_CONFIG": "Löschen der Funktions-Bibliothek bestätigen",
+ "DELETE_CONFIG_FILE": "Soll die Funktions-Bibliothek '{{config}}' wirklich gelöscht werden?"
+ },
"THREADS": {
"THREAD": "Thread",
"TOTAL": "gesamt",
@@ -448,6 +462,11 @@
"NEW_DEFINITION_FILE": "Neue Datei",
"NEW_LOGIC": "Neue Logik",
"NEW_SCENE": "Neue Szene",
+ "RELOAD_SCENES": "Szenen neu laden",
+ "RELOAD_SCENE": "Laden",
+ "NEW_FUNCTION": "Neue Bibliothek",
+ "RELOAD_FUNCTIONS": "Alle neu laden",
+ "RELOAD_FUNCTION": "Laden",
"LINE_WRAPPING": "Zeilenumbruch",
"TRIGGER": "Auslösen"
},
@@ -502,6 +521,7 @@
"CONFIGURATION": "Konfiguration",
"RESTART FOR CHANGES": "Änderungen werden erst nach einem Neustart von SmartHomeNG wirksam - Nach dem Neustart ein Reload der Admin GUI durchführen",
+ "RELOAD FOR CHANGES": "Änderungen werden erst nach dem erneuten Laden der Datei(en) oder einem Neustart von SmartHomeNG wirksam",
"RESTART LOGIC FOR CHANGES": "Änderungen werden erst nach einem Neustart der Logik wirksam",
"LINEBREAK": "Umbruch"
diff --git a/modules/admin/webif/static/assets/i18n/en.json b/modules/admin/webif/static/assets/i18n/en.json
index 52118ca7f8..0b138e19f6 100644
--- a/modules/admin/webif/static/assets/i18n/en.json
+++ b/modules/admin/webif/static/assets/i18n/en.json
@@ -15,6 +15,7 @@
"SCENES": "Scenes",
"SCENE_LIST": "Scene List",
"SCENE_CONFIGURATION": "Scene Configuration",
+ "FUNCTION_CONFIGURATION": "User functions",
"THREADS": "Threads",
"LOGS": "Logs",
"LOGS_DISPLAY": "Display Logs",
@@ -30,6 +31,7 @@
"SHNG_VERSION": "SmartHomeNG Version",
"IN": "in",
"SHNG_PLG_VERSION": "SmartHomeNG Plugins Version",
+ "SHNG_ADMIN_GUI": "Administration-GUI",
"HOST": "Host",
"OS": "Operating System",
"PID": "Process ID",
@@ -502,6 +504,7 @@
"CONFIGURATION": "Configuration",
"RESTART FOR CHANGES": "Restart SmartHomeNG for changes to take effect - Reload Admin GUI after restart",
+ "RELOAD FOR CHANGES": "Reload file(s) or restart SmartHomeNG for changes to take effect.",
"RESTART LOGIC FOR CHANGES": "Restart the logic for changes to take effect",
"LINEBREAK": "Line Break"
diff --git a/modules/admin/webif/static/assets/i18n/fr.json b/modules/admin/webif/static/assets/i18n/fr.json
index ec3e0d440f..3396dc5ef6 100644
--- a/modules/admin/webif/static/assets/i18n/fr.json
+++ b/modules/admin/webif/static/assets/i18n/fr.json
@@ -15,6 +15,7 @@
"SCENES": "Scènes",
"SCENE_LIST": "Liste des scènes",
"SCENE_CONFIGURATION": "Configuration des scènes",
+ "FUNCTION_CONFIGURATION": "Configuration des fonctions utilisateur",
"THREADS": "Tâches",
"LOGS": "Journaux",
"LOGS_DISPLAY": "Afficher les journaux",
@@ -30,6 +31,7 @@
"SHNG_VERSION": "Version de SmartHomeNG",
"IN": "dans",
"SHNG_PLG_VERSION": "Version des extensions",
+ "SHNG_ADMIN_GUI": "Interface d'administration",
"HOST": "Hôte",
"OS": "Système d'exploitation",
"ARCHITECTURE": "architecture",
@@ -500,6 +502,7 @@
"CONFIGURATION": "Configuration",
"RESTART FOR CHANGES": "Les changements deviennent seulement actifs après un redémarrage de SmartHomeNG",
+ "RELOAD FOR CHANGES": "Les modifications ne prennent effet qu'après rechargement du fichier ou redémarrage de SmartHomeNG",
"RESTART LOGIC FOR CHANGES": "Les changements deviennent seulement actifs après un redémarrage de la logique",
"LINEBREAK": "Saut de ligne"
diff --git a/modules/admin/webif/static/assets/testdata/api/files/functions/anhalter.txt b/modules/admin/webif/static/assets/testdata/api/files/functions/anhalter.txt
new file mode 100644
index 0000000000..6d339880f6
--- /dev/null
+++ b/modules/admin/webif/static/assets/testdata/api/files/functions/anhalter.txt
@@ -0,0 +1,24 @@
+
+import logging
+_logger = logging.getLogger(__name__)
+
+_VERSION = '0.1.0'
+_DESCRIPTION = 'Per Anhalter durch die Galaxis'
+
+def zweiundvierzig():
+
+ return 'Die Antwort auf die Frage aller Fragen'
+
+
+def itemtest(sh):
+
+ return sh.env.location.sun_position.elevation.degrees()
+
+def log_test():
+
+ _logger.warning('Log-Test aus einer Userfunction')
+
+ return
+
+
+#logger.warning('bewusster Fehler')
diff --git a/modules/admin/webif/static/assets/testdata/api/files/functions/beschattung.txt b/modules/admin/webif/static/assets/testdata/api/files/functions/beschattung.txt
new file mode 100644
index 0000000000..8c34f30780
--- /dev/null
+++ b/modules/admin/webif/static/assets/testdata/api/files/functions/beschattung.txt
@@ -0,0 +1,124 @@
+
+import logging
+_logger = logging.getLogger(__name__)
+
+_VERSION = '0.1.2'
+_DESCRIPTION = 'Hilfsfunktionen zur Beschattungssteuerung per Stateengine'
+
+# Ausrichtung der Südfront: 171,0°
+
+def beschatten_beginnen_ost(lux, azimut):
+ """
+ Bestimmen, ob die Stateengine die Beschattung beginnen soll
+
+ :param lux: Helligkeit Ost
+ :param azimut: Sonnen Position (Himmelsrichtung in Grad)
+
+ :return: Wenn True, soll Beschattung begonnen werden
+ """
+ # Beschattungs Daten (Hysterese) OST
+ #
+ # Azimut Elev Beginnen Beenden
+ # 0°-88° 0°-10° >4.000 Lux <2.600 Lux
+ # 0°-88° 10°- >5.000 Lux <2.800 Lux
+ # 88°-100° >7.000 Lux <2.800 Lux
+ # 100°-110° >15.000 Lux <12.000 Lux
+ # 110°-160° >35.000 Lux <20.000 Lux
+ # 160°-220° >27.000 Lux <10.000 Lux
+ # 220°- >27.000 Lux <7.500 Lux
+ #
+
+ _logger.notice(f"beschatten_beginnen_ost {lux} Lux, Azimut={azimut}°")
+
+ return (lux > 15000 and azimut < 88) or (lux > 15000 and int(azimut) in range(88, 100)) or (lux > 15000 and int(azimut) in range(100, 110)) or (lux > 27000 and azimut >= 110)
+
+
+def beschatten_beenden_ost(lux, azimut, elevation):
+
+ _logger.notice(f"beschatten_beenden_ost {lux} Lux, Azimut={azimut}°, Elevation={elevation}°")
+
+ return (lux <= 2600 and azimut < 100 and elevation <= 10) or (lux <= 2800 and azimut < 100 and elevation > 10) or (lux <= 12000 and int(azimut) in range(100, 110)) or (lux <= 20000 and int(azimut) in range(110, 160)) or (lux <= 10000 and int(azimut) in range(160, 220)) or (lux <= 7500 and azimut >= 220)
+
+
+def lamellen_oeffnung_ost(azimut, elevation):
+ """
+ Bestimmung der Stellung der Ost Lamellen im Wohnbereich
+
+ :param elevation: Sonnen Position (Höhe in Grad)
+ :return: Stellung der Lamellen in Prozent
+ """
+
+ # elevation = sh.env.location.sun_position.elevation.degrees()
+
+ default = 54
+
+ if azimut >= 171.0:
+ return default
+
+ elif elevation <= 6.6:
+ return 87
+ elif elevation <= 11.5:
+ return 84
+ elif elevation <= 14.8:
+ return 81
+ elif elevation <= 19.4:
+ return 78
+ elif elevation <= 16.1:
+ return 74
+ elif elevation <= 28:
+ return 70
+ elif elevation <= 30.9:
+ return 65
+ elif elevation <= 33.9:
+ return 60
+
+ return default
+
+ #return 87 if elevation <= 6.6 else 84 if elevation <= 11.5 else 81 if elevation <= 14.8 else 78 if elevation <= 19.4 else 74 if elevation <= 16.1 else 70 if elevation <= 28 else 65 if elevation <= 30.9 else 60 if elevation <= 33.9 else 54
+
+
+def lamellen_oeffnung_sued(azimut, elevation):
+ """
+ Bestimmung der Stellung der Süd Lamellen im Wohnbereich/Gästezimmer/Büro
+
+ :param azimut: Sonnen Position (Himmelsrichtung in Grad)
+ :param elevation: Sonnen Position (Höhe in Grad)
+ :return: Stellung der Lamellen in Prozent
+ """
+
+ # azimut = sh.env.location.sun_position.azimut.degrees()
+ # elevation = sh.env.location.sun_position.elevation.degrees()
+
+ default = 66
+
+ if azimut >= 230:
+ if elevation >= 6.0:
+ return 66
+ elif elevation >= 4.0:
+ return 63
+ elif elevation > 0.0:
+ return 60
+
+ # elif azimut >= 214:
+ # if elevation > 0.0:
+ # return 63
+
+ elif elevation >= 24.0:
+ return 70
+
+ elif elevation >= 19.5:
+ return 72
+
+ elif elevation >= 15.0:
+ return 74
+
+ elif elevation >= 11.8:
+ return 76
+
+ if elevation >= 5.5:
+ return 78
+
+ return default
+
+ # return 60 if (azimut>=230 and elevation>0.0 ) else 63 if (azimut>=214 and elevation>0.0 ) else 72 if elevation<=7.0 else 69 if elevation<=24.0 else 66
+
diff --git a/modules/admin/webif/static/assets/testdata/api/files/functions/default.json b/modules/admin/webif/static/assets/testdata/api/files/functions/default.json
new file mode 100644
index 0000000000..d65fac9dbd
--- /dev/null
+++ b/modules/admin/webif/static/assets/testdata/api/files/functions/default.json
@@ -0,0 +1,6 @@
+[
+ "anhalter.py",
+ "beschattung.py",
+ "functions.py",
+ "functions2.py"
+]
diff --git a/modules/admin/webif/static/assets/testdata/api/files/functions/functions.txt b/modules/admin/webif/static/assets/testdata/api/files/functions/functions.txt
new file mode 100644
index 0000000000..c1490bf165
--- /dev/null
+++ b/modules/admin/webif/static/assets/testdata/api/files/functions/functions.txt
@@ -0,0 +1,8 @@
+
+_VERSION = '0.1.3'
+_DESCRIPTION = 'My user-defined functions'
+
+
+def _version():
+
+ return (_VERSION, _DESCRIPTION)
diff --git a/modules/admin/webif/static/assets/testdata/api/files/functions/functions2.txt b/modules/admin/webif/static/assets/testdata/api/files/functions/functions2.txt
new file mode 100644
index 0000000000..61ded96986
--- /dev/null
+++ b/modules/admin/webif/static/assets/testdata/api/files/functions/functions2.txt
@@ -0,0 +1,7 @@
+
+_VERSION = '0.1.2'
+_DESCRIPTION = 'My user-defined functions 2'
+
+def _version():
+
+ return (_VERSION, _DESCRIPTION)
diff --git a/modules/admin/webif/static/assets/testdata/api/items/structs/default.json b/modules/admin/webif/static/assets/testdata/api/items/structs/default.json
index 7aff1cf376..2ea538d068 100644
--- a/modules/admin/webif/static/assets/testdata/api/items/structs/default.json
+++ b/modules/admin/webif/static/assets/testdata/api/items/structs/default.json
@@ -357,7 +357,7 @@
}
}
},
- "my_stateengine": {
+ "my.se.stateengine": {
"name": "Vorlage-Struktur für einen Zustandsautomaten",
"rules": {
"name": "Regeln für den Zustandsautomaten",
@@ -377,7 +377,7 @@
"cache": true
}
},
- "my_stateengine2": {
+ "my.se.stateengine2": {
"name": "Vorlage-Struktur 2 für einen Zustandsautomaten",
"rules": {
"name": "Regeln für den Zustandsautomaten",
diff --git a/modules/admin/webif/static/assets/testdata/api/loggers/default.json b/modules/admin/webif/static/assets/testdata/api/loggers/default.json
index 31e1524f0a..1108af3414 100644
--- a/modules/admin/webif/static/assets/testdata/api/loggers/default.json
+++ b/modules/admin/webif/static/assets/testdata/api/loggers/default.json
@@ -285,10 +285,10 @@
"handlers": [
"shng_details_file"
],
- "level": "WARNING",
+ "level": "NOTICE",
"active": {
"disabled": false,
- "level": "WARNING",
+ "level": "NOTICE",
"filters": [],
"handlers": [
"TimedRotatingFileHandler"
@@ -605,7 +605,7 @@
]
}
},
- "__main__": {
+ "lib.smarthome": {
"handlers": [
"shng_details_file",
"shng_develop_file",
@@ -616,10 +616,10 @@
"q21_phones_file",
"shng_stateengine_file"
],
- "level": "WARNING",
+ "level": "NOTICE",
"active": {
"disabled": false,
- "level": "WARNING",
+ "level": "UNKNOWN_31",
"filters": [],
"handlers": [
"TimedRotatingFileHandler",
diff --git a/modules/admin/webif/static/assets/testdata/api/scenes/reload/default.json b/modules/admin/webif/static/assets/testdata/api/scenes/reload/default.json
new file mode 100644
index 0000000000..0d4f101c7a
--- /dev/null
+++ b/modules/admin/webif/static/assets/testdata/api/scenes/reload/default.json
@@ -0,0 +1,2 @@
+[
+]
diff --git a/modules/admin/webif/static/assets/testdata/item_detail_json.html b/modules/admin/webif/static/assets/testdata/item_detail_json.html
index 20ab3c158f..060f62d662 100644
--- a/modules/admin/webif/static/assets/testdata/item_detail_json.html
+++ b/modules/admin/webif/static/assets/testdata/item_detail_json.html
@@ -1,4 +1,45 @@
[
+ {
+ "path": "test.string",
+ "name": "test.string",
+ "type": "str",
+ "value": "ein wirklich, wirklich, wirklich sehr laaaaanger String",
+ "change_age": 247402,
+ "update_age": 247402.7,
+ "last_update": "2018-08-08 00:42:52.837168+02:00",
+ "last_change": "2018-08-08 00:42:52.837127+02:00",
+ "changed_by": "Init",
+ "updated_by": "Init",
+ "last_value": "false",
+ "previous_value": "",
+ "previous_change_age": 597,
+ "previous_update_age": 597,
+ "previous_update": "2018-08-08 00:42:52.837344+02:00",
+ "previous_change": "2018-08-08 00:42:52.837303+02:00",
+ "previous_update_by": "me",
+ "previous_change_by": "you",
+ "enforce_updates": "on",
+ "enforce_change": "off",
+ "cache": "off",
+ "trigger": "-",
+ "trigger_condition": "-",
+ "trigger_condition_raw": "",
+ "on_update": "-",
+ "on_change": "-",
+ "log_change": "-",
+ "log_level": "-",
+ "log_text": "-",
+ "log_mapping": "-",
+ "log_rules": "-",
+ "cycle": "-",
+ "crontab": "-",
+ "autotimer": "-",
+ "threshold": "-",
+ "config": {"lin_mode": "ALL"},
+ "logics": ["test"],
+ "triggers": ["bound method WebSocket.update_item of plugins.visu_websocket.WebSocket"],
+ "filename": "q21_c26lintronic.yaml"
+ },
{
"path": "beoremote.beo4command",
"name": "beoremote.beo4command",
@@ -29,6 +70,10 @@
"on_update": "-",
"on_change": "-",
"log_change": "-",
+ "log_level": "-",
+ "log_text": "-",
+ "log_mapping": "-",
+ "log_rules": "-",
"cycle": "-",
"crontab": "-",
"autotimer": "-",
diff --git a/modules/admin/webif/static/assets/testdata/items.json b/modules/admin/webif/static/assets/testdata/items.json
index f7f65ebb80..6ac013f8e8 100644
--- a/modules/admin/webif/static/assets/testdata/items.json
+++ b/modules/admin/webif/static/assets/testdata/items.json
@@ -1,7 +1,33 @@
[
179,
[
- {
+ {
+ "label": "test",
+ "nodename": "test",
+ "name": "test",
+ "tags": [
+ 2
+ ],
+ "children": [
+ {
+ "label": "test.string",
+ "nodename": "string",
+ "name": "beschattung.config",
+ "tags": [
+ 3
+ ]
+ },
+ {
+ "label": "test.number",
+ "nodename": "number",
+ "name": "Test-Nummer",
+ "tags": [
+ 2
+ ]
+ }
+ ]
+ },
+ {
"label": "beoremote",
"nodename": "beoremote",
"name": "beoremote",
diff --git a/modules/admin/webif/static/index.html b/modules/admin/webif/static/index.html
index 48337754f8..6ffc51fbee 100644
--- a/modules/admin/webif/static/index.html
+++ b/modules/admin/webif/static/index.html
@@ -17,5 +17,5 @@
-
+