diff --git a/dev/sample_module/module.yaml b/dev/sample_module/module.yaml index 7c89233cc..367c87fb7 100644 --- a/dev/sample_module/module.yaml +++ b/dev/sample_module/module.yaml @@ -3,10 +3,13 @@ module: # Global plugin attributes classname: SampleModule 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) + + # these min/max-versions MUST be given in quotes, or e.g. 3.10 will be interpreted as 3.1 (3.1 < 3.9 < 3.10) + sh_minversion: '1.10' # minimum shNG version to use this plugin +# sh_maxversion: '1.11' # maximum shNG version to use this plugin (omit if latest) +# py_minversion: '3.10' # minimum Python version to use for this plugin +# py_maxversion: '4.25' # maximum Python version to use for this plugin (omit if latest) + description: de: 'Modul implementiert ...' en: 'Module implements ...' diff --git a/dev/sample_mqttplugin/__init__.py b/dev/sample_mqttplugin/__init__.py index 83cd087e8..421580de5 100644 --- a/dev/sample_mqttplugin/__init__.py +++ b/dev/sample_mqttplugin/__init__.py @@ -59,6 +59,10 @@ def __init__(self, sh): # Call init code of parent class (MqttPlugin) super().__init__() + # if you want to use an item to toggle plugin execution, enable the + # definition in plugin.yaml and uncomment the following line + #self._pause_item_path = self.get_parameter_value('pause_item') + # Initialization code goes here # On initialization error use: @@ -78,6 +82,10 @@ def run(self): self.alive = True + # let the plugin change the state of pause_item + if self._pause_item: + self._pause_item(False, self.get_fullname()) + # start subscription to all topics self.start_subscriptions() @@ -88,6 +96,13 @@ def stop(self): self.logger.dbghigh(self.translate("Methode '{method}' aufgerufen", {'method': 'stop()'})) self.alive = False + # let the plugin change the state of pause_item + if self._pause_item: + self._pause_item(True, self.get_fullname()) + + # if you use schedulers, this stops all schedulers the plugin has started. + #self.scheduler_remove_all() + # stop subscription to all topics self.stop_subscriptions() @@ -104,6 +119,13 @@ def parse_item(self, item): with the item, caller, source and dest as arguments and in case of the knx plugin the value can be sent to the knx with a knx write function within the knx plugin. """ + # check for pause item + if item.property.path == self._pause_item_path: + self.logger.debug(f'pause item {item.property.path} registered') + self._pause_item = item + self.add_item(item, updating=True) + return self.update_item + if self.has_iattr(item.conf, 'foo_itemid'): self.logger.debug(f"parse item: {item.property.path}") @@ -148,6 +170,16 @@ 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 """ + # check for pause item + if item is self._pause_item: + if caller != self.get_shortname(): + self.logger.debug(f'pause item changed to {item()}') + if item() and self.alive: + self.stop() + elif not item() and not self.alive: + self.run() + return + 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: diff --git a/dev/sample_mqttplugin/plugin.yaml b/dev/sample_mqttplugin/plugin.yaml index c84015282..4a0666c58 100755 --- a/dev/sample_mqttplugin/plugin.yaml +++ b/dev/sample_mqttplugin/plugin.yaml @@ -13,26 +13,28 @@ plugin: # support: https://knx-user-forum.de/forum/supportforen/smarthome-py 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) + + # these min/max-versions MUST be given in quotes, or e.g. 3.10 will be interpreted as 3.1 (3.1 < 3.9 < 3.10) + sh_minversion: '1.10' # minimum shNG version to use this plugin +# sh_maxversion: '1.11' # maximum shNG version to use this plugin (omit if latest) +# py_minversion: '3.10' # minimum Python version to use for this plugin +# py_maxversion: '4.25' # maximum Python version to use for this plugin (omit if latest) + multi_instance: false # plugin supports multi instance - restartable: unknown - suspendable: false # plugin can be suspended, must be implemented in code + restartable: unknown # plugin supports stopping and starting again, must be implemented classname: SamplePlugin # class containing the plugin parameters: # Definition of parameters to be configured in etc/plugin.yaml (enter 'parameters: NONE', if section should be empty) - # item to toggle suspend mode - suspend_item: + # item to toggle plugin execution, remove if not wanted + # (needs plugin to be restartable) + pause_item: type: str default: '' description: - de: 'Item zum Schalten des Suspend-Modus' - en: 'item for toggling suspend mode' - + de: 'Item, um die Ausführung des Plugins zu steuern' + en: 'item for controlling plugin execution' item_attributes: # Definition of item attributes defined by this plugin (enter 'item_attributes: NONE', if section should be empty) diff --git a/dev/sample_plugin/__init__.py b/dev/sample_plugin/__init__.py index 0edd07a83..4f1cad81e 100644 --- a/dev/sample_plugin/__init__.py +++ b/dev/sample_plugin/__init__.py @@ -70,6 +70,10 @@ def __init__(self, sh): # (maybe you want to make it a plugin parameter?) # self._cycle = 60 + # if you want to use an item to toggle plugin execution, enable the + # definition in plugin.yaml and uncomment the following line + #self._pause_item_path = self.get_parameter_value('pause_item') + # Initialization code goes here # On initialization error use: @@ -88,10 +92,16 @@ def run(self): Run method for the plugin """ self.logger.dbghigh(self.translate("Methode '{method}' aufgerufen", {'method': 'run()'})) + + # connect to network / web / serial device + # (enable the following lines if you want to open a connection + # don't forget to implement a connect (and disconnect) method.. :) ) + #self.connect() + # setup scheduler for device poll loop # (enable the following line, if you need to poll the device. # Rember to un-comment the self._cycle statement in __init__ as well) - #self.scheduler_add('poll_device', self.poll_device, cycle=self._cycle) + #self.scheduler_add(self.get_fullname() + '_poll', self.poll_device, cycle=self._cycle) # Start the asyncio eventloop in it's own thread # and set self.alive to True when the eventloop is running @@ -100,8 +110,15 @@ def run(self): self.alive = True # if using asyncio, do not set self.alive here. Set it in the session coroutine + # let the plugin change the state of pause_item + if self._pause_item: + self._pause_item(False, self.get_fullname()) + # if you need to create child threads, do not make them daemon = True! # They will not shutdown properly. (It's a python bug) + # Also, don't create the thread in __init__() and start them here, but + # create and start them here. Threads can not be restarted after they + # have been stopped... def stop(self): """ @@ -110,13 +127,25 @@ def stop(self): self.logger.dbghigh(self.translate("Methode '{method}' aufgerufen", {'method': 'stop()'})) self.alive = False # if using asyncio, do not set self.alive here. Set it in the session coroutine - # if you use a scheduled poll loop, enable the following line - #self.scheduler_remove('poll_device') + # let the plugin change the state of pause_item + if self._pause_item: + self._pause_item(True, self.get_fullname()) + + # this stops all schedulers the plugin has started. + # you can disable/delete the line if you don't use schedulers + self.scheduler_remove_all() # stop the asyncio eventloop and it's thread # If you use asyncio, enable the following line #self.stop_asyncio() + # If you called connect() on run(), disconnect here + # (remember to write a disconnect() method!) + #self.disconnect() + + # also, clean up anything you set up in run(), so the plugin can be + # cleanly stopped and started again + def parse_item(self, item): """ Default plugin parse_item method. Is called when the plugin is initialized. @@ -130,6 +159,13 @@ def parse_item(self, item): with the item, caller, source and dest as arguments and in case of the knx plugin the value can be sent to the knx with a knx write function within the knx plugin. """ + # check for pause item + if item.property.path == self._pause_item_path: + self.logger.debug(f'pause item {item.property.path} registered') + self._pause_item = item + self.add_item(item, updating=True) + return self.update_item + if self.has_iattr(item.conf, 'foo_itemtag'): self.logger.debug(f"parse item: {item}") @@ -163,6 +199,16 @@ 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 """ + # check for pause item + if item is self._pause_item: + if caller != self.get_shortname(): + self.logger.debug(f'pause item changed to {item()}') + if item() and self.alive: + self.stop() + elif not item() and not self.alive: + self.run() + return + if self.alive and caller != self.get_fullname(): # code to execute if the plugin is not stopped # and only, if the item has not been changed by this plugin: diff --git a/dev/sample_plugin/plugin.yaml b/dev/sample_plugin/plugin.yaml index a67df3410..af0e085f5 100755 --- a/dev/sample_plugin/plugin.yaml +++ b/dev/sample_plugin/plugin.yaml @@ -13,25 +13,28 @@ plugin: # support: https://knx-user-forum.de/forum/supportforen/smarthome-py 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) + + # these min/max-versions MUST be given in quotes, or e.g. 3.10 will be interpreted as 3.1 (3.1 < 3.9 < 3.10) + sh_minversion: '1.10' # minimum shNG version to use this plugin +# sh_maxversion: '1.11' # maximum shNG version to use this plugin (omit if latest) +# py_minversion: '3.10' # minimum Python version to use for this plugin +# py_maxversion: '4.25' # maximum Python version to use for this plugin (omit if latest) + multi_instance: false # plugin supports multi instance - restartable: unknown - suspendable: false # plugin can be suspended, must be implemented in code + restartable: unknown # plugin supports stopping and starting again, must be implemented classname: SamplePlugin # class containing the plugin parameters: # Definition of parameters to be configured in etc/plugin.yaml (enter 'parameters: NONE', if section should be empty) - # item to toggle suspend mode - suspend_item: + # item to toggle plugin execution, remove if not wanted + # (needs plugin to be restartable) + pause_item: type: str default: '' description: - de: 'Item zum Schalten des Suspend-Modus' - en: 'item for toggling suspend mode' + de: 'Item, um die Ausführung des Plugins zu steuern' + en: 'item for controlling plugin execution' param1: type: str diff --git a/dev/sample_smartdevice_plugin/plugin.yaml b/dev/sample_smartdevice_plugin/plugin.yaml index 6837bc75d..9d2f104ba 100644 --- a/dev/sample_smartdevice_plugin/plugin.yaml +++ b/dev/sample_smartdevice_plugin/plugin.yaml @@ -16,8 +16,13 @@ plugin: # documentation: https://github.com/smarthomeNG/smarthome/wiki/ # support: https://knx-user-forum.de/forum/supportforen/smarthome-py/ version: 0.0.1 # !! adjust - sh_minversion: 1.9.5 - py_minversion: 3.6 + + # these min/max-versions MUST be given in quotes, or e.g. 3.10 will be interpreted as 3.1 (3.1 < 3.9 < 3.10) + sh_minversion: '1.10' # minimum shNG version to use this plugin +# sh_maxversion: '1.11' # maximum shNG version to use this plugin (omit if latest) +# py_minversion: '3.10' # minimum Python version to use for this plugin +# py_maxversion: '4.25' # maximum Python version to use for this plugin (omit if latest) + multi_instance: true restartable: true suspendable: true # plugin can be suspended, implemented in sdp diff --git a/dev/sample_smartdevice_standalone_plugin/plugin.yaml b/dev/sample_smartdevice_standalone_plugin/plugin.yaml index 6837bc75d..9d2f104ba 100644 --- a/dev/sample_smartdevice_standalone_plugin/plugin.yaml +++ b/dev/sample_smartdevice_standalone_plugin/plugin.yaml @@ -16,8 +16,13 @@ plugin: # documentation: https://github.com/smarthomeNG/smarthome/wiki/ # support: https://knx-user-forum.de/forum/supportforen/smarthome-py/ version: 0.0.1 # !! adjust - sh_minversion: 1.9.5 - py_minversion: 3.6 + + # these min/max-versions MUST be given in quotes, or e.g. 3.10 will be interpreted as 3.1 (3.1 < 3.9 < 3.10) + sh_minversion: '1.10' # minimum shNG version to use this plugin +# sh_maxversion: '1.11' # maximum shNG version to use this plugin (omit if latest) +# py_minversion: '3.10' # minimum Python version to use for this plugin +# py_maxversion: '4.25' # maximum Python version to use for this plugin (omit if latest) + multi_instance: true restartable: true suspendable: true # plugin can be suspended, implemented in sdp diff --git a/lib/model/mqttplugin.py b/lib/model/mqttplugin.py index 2d2707ceb..01099c85b 100644 --- a/lib/model/mqttplugin.py +++ b/lib/model/mqttplugin.py @@ -40,7 +40,7 @@ def __init__(self): """ Initialization Routine for the mqtt extension class to SmartPlugin """ - SmartPlugin.__init__(self) + super().__init__() # get instance of MQTT module try: @@ -60,8 +60,6 @@ def __init__(self): # get broker configuration (for display in web interface) self.broker_config = self.mod_mqtt.get_broker_config() - return True - def translate(self, txt, vars=None, block=None): """ diff --git a/lib/model/smartplugin.py b/lib/model/smartplugin.py index 11b647e0f..c471391ed 100644 --- a/lib/model/smartplugin.py +++ b/lib/model/smartplugin.py @@ -21,18 +21,18 @@ ######################################################################### from lib.model.smartobject import SmartObject - from lib.shtime import Shtime from lib.module import Modules import lib.shyaml as shyaml from lib.utils import Utils from lib.translation import translate as lib_translate + import logging import os import threading import asyncio import time -import inspect +from typing import Coroutine, Any class SmartPlugin(SmartObject, Utils): @@ -46,74 +46,179 @@ class SmartPlugin(SmartObject, Utils): PLUGIN_VERSION = '' ALLOW_MULTIINSTANCE = None + STOP_ON_ITEM_CHANGE = True # Plugin needs to be stopped on/before item changes + # needed by self.remove_item(), don't change unless you know how and why - __instance = '' #: Name of this instance of the plugin - _sh = None #: Variable containing a pointer to the main SmartHomeNG object; is initialized during loading of the plugin; :Warning: Don't change it - _configfilename = '' #: Configfilename of the plugin; is initialized during loading of the plugin; :Warning: Don't change it - _configname = '' #: Configname of the plugin; is initialized during loading of the plugin; :Warning: Don't change it - _shortname = '' #: Shortname of the plugin; is initialized during loading of the plugin; :Warning: Don't change it - _classname = '' #: Classname of the plugin; is initialized during loading of the plugin; :Warning: Don't change it - shtime = None #: Variable containing a pointer to the SmartHomeNG time handling object; is initialized during loading of the plugin; :Warning: Don't change it + # these variables are initialized by the plugin loader for each plugin - _stop_on_item_change = True # Plugin needs to be stopped on/before item changes - # needed by self.remove_item(), don't change unless you know how and why + __instance = '' #: Name of this instance of the plugin + _sh = None #: Variable containing a pointer to the main SmartHomeNG object; is initialized during loading of the plugin; :Warning: Don't change it + _configfilename = '' #: Configfilename of the plugin; is initialized during loading of the plugin; :Warning: Don't change it + _configname = '' #: Configname of the plugin; is initialized during loading of the plugin; :Warning: Don't change it + _shortname = '' #: Shortname of the plugin; is initialized during loading of the plugin; :Warning: Don't change it + _classname = '' #: Classname of the plugin; is initialized during loading of the plugin; :Warning: Don't change it + shtime = None #: Variable containing a pointer to the SmartHomeNG time handling object; is initialized during loading of the plugin; :Warning: Don't change it + + _parameters = {} # Dict for storing the configuration parameters read from /etc/plugin.yaml + _hide_parameters = {} # Dict for storing parameters to hide from AdminUI _pluginname_prefix = 'plugins.' - _plg_item_dict = {} # dict to hold the items assigned to the plugin and their plugin specific information - _item_lookup_dict = {} # dict for the reverse lookup from a mapping (device-command or matchstring) to an item, - # contains a list of items for each mapping - _add_translation = None + # these variables _should_ be instance members (not class members) ... + # BUT if a plugin fails to call the parent class' __init__(), these are + # not present and cause errors. + # So - until a proper QA for 3rd party plugins is in place - we define these + # as class members to prevent AttributeErrors later on... - _parameters = {} # Dict for storing the configuration parameters read from /etc/plugin.yaml - _hide_parameters = {} # Dict for storing parameters to hide from AdminUI - _schedulers = [] # List for all plugin schedulers logger = logging.getLogger(__name__) - alive = False - suspended = False # flag for setting suspended (inactive) state - _suspend_item = None # suspend item - _suspend_item_path = None # path of suspend item + _plg_item_dict = {} # dict to hold the items assigned to the plugin and their plugin specific information + _item_lookup_dict = {} # dict for the reverse lookup from a mapping (device-command or matchstring) to an item, + # contains a list of items for each mapping - # Initialization of SmartPlugin class called by super().__init__() from the plugin's __init__() method - def __init__(self, **kwargs): - self._plg_item_dict = {} # make sure, that the dict is local to the plugin - self._item_lookup_dict = {} # make sure, that the dict is local to the plugin - self._schedulers = [] # all created schedulers for this plugin + alive = False # flag if plugin is running + _schedulers = [] # all created schedulers for this plugin + + _add_translation = None + + _pause_item = None # pause item + _pause_item_path = '' # path of pause item + + _asyncio_loop = None # eventloop of the plugin + _asyncio_state = 'unused' # stored state of the asyncio use of the plugin + _used_plugin_coro = None # plugin coro used when calling start_asyncio (to be able to used by a generic 'restart asyncio' method + _run_queue = None # queue to send commends to the main-coro/plugin-coro - # set parameter value - # TODO: need to check for this item in parse_item and set self._suspend_item - # for suspend item functionality to work - self._suspend_item_path = self.get_parameter_value('suspend_item') +# +# the following methods need to be overwritten / implemented +# - def suspend(self, by=None): + def run(self): """ - sets plugin into suspended mode, no network/serial activity and no item changed + This method of the plugin is called to start the plugin + + :note: This method needs to be overwritten by the plugin implementation. Otherwise an error will be raised """ - if self.alive: - self.logger.info(f'plugin suspended by {by if by else "unknown"}, connections will be closed') - self.suspended = True - if self._suspend_item is not None: - self._suspend_item(True) - if hasattr(self, 'disconnect'): - self.disconnect() + raise NotImplementedError("'Plugin' subclasses should have a 'run()' method") - def resume(self, by=None): + def stop(self): """ - disabled suspended mode, network/serial connections are resumed + This method of the plugin is called to stop the plugin when SmartHomeNG shuts down + + :note: This method needs to be overwritten by the plugin implementation. Otherwise an error will be raised """ - if self.alive: - self.logger.info(f'plugin resumed by {by if by else "unknown"}, connections will be resumed') - self.suspended = False - if self._suspend_item is not None: - self._suspend_item(False) - if hasattr(self, 'connect'): - self.connect() + raise NotImplementedError("'Plugin' subclasses should have a 'stop()' method") + +# +# the following methods should be overwritten, but also called via super().() +# + + def __init__(self, **kwargs) -> None: + """ + Initialization of SmartPlugin instance + + Should be called by super().__init__() from the plugin's __init__() method + """ + # initialize instance members; for mutable types make sure these are "instance-specific" + + self._plg_item_dict = {} # dict to hold the items assigned to the plugin and their plugin specific information + self._item_lookup_dict = {} # dict for the reverse lookup from a mapping (device-command or matchstring) to an item, + # contains a list of items for each mapping + + self._schedulers = [] # all created schedulers for this plugin + + # initialize plugin variables + + self.alive = False # flag if plugin is running + + self._add_translation = None + + self._pause_item = None # pause item + self._pause_item_path = '' # pause item path + + self._asyncio_loop = None # eventloop of the plugin + self._asyncio_state = 'unused' # stored state of the asyncio use of the plugin + self._used_plugin_coro = None # plugin coro used when calling start_asyncio (to be able to used by a generic 'restart asyncio' method + self._run_queue = None # queue to send commends to the main-coro/plugin-coro + +# +# the following methods should be overwritten +# + + def update_item(self, item, caller=None, source=None, dest=None) -> None: + """ + Item has been updated + + This method is called, if the value of an item has been updated by + SmartHomeNG. It should write the changed value out to the device + (hardware/interface) that is managed by this plugin. + + Method must be overwritten for the plugin to be able to react to + item changes. + + :param item: item to be updated towards the plugin + :param caller: if given it represents the callers name + :param source: if given it represents the source + :param dest: if given it represents the dest + """ + # check for pause item + if item is self._pause_item: + if caller != self.get_shortname(): + self.logger.debug(f'pause item changed to {item()}') + if item() and self.alive: + self.stop() + elif not item() and not self.alive: + self.run() + return + + if not self.alive: + self.logger.warning(f'Received item update for item {item.property.path}, but plugin is not running. Ignoring...') + return + + def parse_item(self, item) -> Any: + """ + This method is used to parse the configuration of an item for this plugin. It is + called for each item before the plugins are started (calling all run methods). + Copy code to own function; calling via super() does not work without bending three arms... - def deinit(self, items=[]): + :note: This method should be overwritten by the plugin implementation. + """ + # check for pause item + if item.property.path == self._pause_item_path: + self.logger.debug(f'pause item {item.property.path} registered') + self._pause_item = item + self.add_item(item, updating=True) + return self.update_item + +# +# the following methods can be overwritten +# + + def poll_device(self) -> None: + """ + periodically poll device (or do other things periodically) + + :note: This method can be overwritten by plugin implementation. + """ + pass + + def parse_logic(self, logic) -> None: """ + This method is used to parse the configuration of a logic for this plugin. It is + called for all plugins before the plugins are started (calling all run methods). + + :note: This method should to be overwritten by the plugin implementation. + """ + pass + + def deinit(self, items=[]) -> None: + """ + This method "deinitializes" the plugin, i.e. prepares for unloading. + The plugin is stopped and all (or all provided) items are un-registered. + If the Plugin needs special code to be executed before it is unloaded, this method - has to be overwritten with the code needed for de-initialization + has to be overwritten with the code needed for de-initialization. Keep the + original code or call super().deinit()... If called without parameters, all registered items are unregistered. items is a list of items (or a single Item() object). @@ -129,7 +234,14 @@ def deinit(self, items=[]): for item in items: self.remove_item(item) - def add_item(self, item, config_data_dict={}, mapping=None, updating=False): +############################################################################### +# +# +# the following methods should NOT be overwritten +# +# + + def add_item(self, item, config_data_dict: dict = {}, mapping=None, updating: bool = False) -> bool: """ For items that are used/handled by a plugin, this method stores the configuration information that is individual for the plugin. The configuration information is/has to be stored in a dictionary @@ -189,7 +301,7 @@ def add_item(self, item, config_data_dict={}, mapping=None, updating=False): return True - def remove_item(self, item): + def remove_item(self, item) -> bool: """ Remove configuration data for an item (and remove the item from the mapping's list @@ -206,16 +318,16 @@ def remove_item(self, item): # check if plugin is running if self.alive: - if self._stop_on_item_change: + if self.STOP_ON_ITEM_CHANGE: self.logger.debug(f'stopping plugin for removal of item {item.property.path}') self.stop() else: self.logger.debug(f'not stopping plugin for removal of item {item.property.path}') - if item.property.path == self._suspend_item_path: - self.logger.warning(f'trying to remove suspend item {item}. Disabling suspend item function') - self._suspend_item = None - self._suspend_item_path = '' + if item.property.path == self._pause_item_path: + self.logger.warning(f'trying to remove pause item {item}. Disabling pause item function') + self._pause_item = None + self._pause_item_path = '' # remove data from item_dict early in case of concurrent actions data = self._plg_item_dict[item.property.path] @@ -233,32 +345,14 @@ def remove_item(self, item): return True - - def callerinfo(self, caller, source): + def callerinfo(self, caller: str, source: str) -> str: if source is None: return caller else: return caller + ':' + source - def update_item(self, item, caller=None, source=None, dest=None): - """ - Item has been updated - - This method is called, if the value of an item has been updated by - SmartHomeNG. It should write the changed value out to the device - (hardware/interface) that is managed by this plugin. - - Method must be overwritten to be functional. - - :param item: item to be updated towards the plugin - :param caller: if given it represents the callers name - :param source: if given it represents the source - :param dest: if given it represents the dest - """ - pass - - def register_updating(self, item): + def register_updating(self, item) -> None: """ Mark item in self._plg_item_dict as registered in shng for updating (usually done by returning self.update_item from self.parse_item) @@ -276,7 +370,7 @@ def register_updating(self, item): self.add_item(item) self._plg_item_dict[item.property.path]['is_updating'] = True - def get_item_config(self, item): + def get_item_config(self, item) -> dict: """ Returns the plugin-specific configuration information (config_data_dict) for the given item @@ -292,7 +386,7 @@ def get_item_config(self, item): item_path = item.property.path return self._plg_item_dict[item_path].get('config_data') - def get_item_mapping(self, item): + def get_item_mapping(self, item) -> str: """ Returns the plugin-specific mapping that was defined by add_item() @@ -310,7 +404,7 @@ def get_item_mapping(self, item): item_path = item.property.path return self._plg_item_dict[item_path].get('mapping') - def get_item_mapping_list(self): + def get_item_mapping_list(self) -> list: """ Returns the plugin-specific mapping that was defined by add_item() @@ -320,14 +414,14 @@ def get_item_mapping_list(self): Only available in SmartHomeNG versions **v1.10.0 and up**. :return: mapping string for that item - :rtype: str + :rtype: list """ result = [] for item_path in list(self._plg_item_dict.keys()): result.append([item_path, self._plg_item_dict[item_path].get('mapping')]) return result - def _string_compare(self, s1, s2: str, mode: str=None) -> str: + def _string_compare(self, s1: str, s2: str, mode: str='') -> bool: """ Compare strings of different length @@ -338,11 +432,10 @@ def _string_compare(self, s1, s2: str, mode: str=None) -> str: :param s1: First string to compare :param s2: Second string to compare :param mode: Compare mode ('start', 'end') for comparing strings of different length - :return: + :return: True if strings match, False otherwise + :rtype: bool """ - if mode is None: - return s1 == s2 - elif mode == 'end': + if mode == 'end': if len(s1) > len(s2): return s1.endswith(s2) else: @@ -352,8 +445,10 @@ def _string_compare(self, s1, s2: str, mode: str=None) -> str: return s1.startswith(s2) else: return s2.startswith(s1) + else: + return s1 == s2 - def get_item_path_list(self, filter_key: str=None, filter_value: str=None, mode: str=None) -> list: + def get_item_path_list(self, filter_key: str='', filter_value: str='', mode: str='') -> list: """ Return list of stored item paths used by this plugin @@ -366,14 +461,15 @@ def get_item_path_list(self, filter_key: str=None, filter_value: str=None, mode: :return: List of item pathes """ - if filter_key is None or filter_value is None: + if filter_key == '' or filter_value == '': return self._plg_item_dict.keys() - if mode is None: + if mode == '': return [item_path for item_path in list(self._plg_item_dict.keys()) if self._plg_item_dict[item_path]['config_data'].get(filter_key, None) == filter_value] + return [item_path for item_path in list(self._plg_item_dict.keys()) if self._string_compare(self._plg_item_dict[item_path]['config_data'].get(filter_key, None), filter_value, mode)] - def get_item_list(self, filter_key: str=None, filter_value: str=None, mode: str=None) -> list: + def get_item_list(self, filter_key: str='', filter_value: str='', mode: str='') -> list: """ Return list of stored items used by this plugin @@ -386,14 +482,15 @@ def get_item_list(self, filter_key: str=None, filter_value: str=None, mode: str= :return: List of item objects """ - if filter_key is None or filter_value is None: + if filter_key == '' or filter_value == '': return [self._plg_item_dict[item_path]['item'] for item_path in list(self._plg_item_dict.keys())] - if mode is None: + if mode == '': return [self._plg_item_dict[item_path]['item'] for item_path in list(self._plg_item_dict.keys()) if self._plg_item_dict[item_path]['config_data'].get(filter_key, None) == filter_value] + return [self._plg_item_dict[item_path]['item'] for item_path in list(self._plg_item_dict.keys()) if self._string_compare(self._plg_item_dict[item_path]['config_data'].get(filter_key, None), filter_value, mode)] - def get_trigger_items(self): + def get_trigger_items(self) -> list: """ Return list of stored items which were marked as updating @@ -401,7 +498,7 @@ def get_trigger_items(self): """ return [self._plg_item_dict[item_path]['item'] for item_path in self._plg_item_dict if self._plg_item_dict[item_path]['is_updating']] - def get_items_for_mapping(self, mapping): + def get_items_for_mapping(self, mapping: str) -> list: """ Returns a list of items that should receive data for the given mapping @@ -415,7 +512,7 @@ def get_items_for_mapping(self, mapping): """ return self._item_lookup_dict.get(mapping, []) - def get_mappings(self): + def get_mappings(self) -> list: """ Returns a list containing all mappings, which have items associated with it @@ -426,7 +523,7 @@ def get_mappings(self): """ return list(self._item_lookup_dict.keys()) - def unparse_item(self, item): + def unparse_item(self, item) -> bool: """ Ensure that changes to are no longer propagated to this plugin @@ -439,7 +536,7 @@ def unparse_item(self, item): except Exception: return False - def get_configname(self): + def get_configname(self) -> str: """ return the name of the plugin instance as defined in plugin.yaml (section name) @@ -449,7 +546,7 @@ def get_configname(self): return self._configname - def _set_configname(self, configname): + def _set_configname(self, configname: str) -> None: """ set the name of the plugin instance as defined in plugin.yaml (section name) @@ -461,7 +558,7 @@ def _set_configname(self, configname): self._configname = configname - def get_shortname(self): + def get_shortname(self) -> str: """ return the shortname of the plugin (name of it's directory) @@ -471,7 +568,7 @@ def get_shortname(self): return self._shortname - def _set_shortname(self, shortname): + def _set_shortname(self, shortname: str) -> None: """ ... @@ -483,7 +580,7 @@ def _set_shortname(self, shortname): self._shortname = shortname - def get_instance_name(self): + def get_instance_name(self) -> str: """ Returns the name of this instance of the plugin @@ -493,7 +590,7 @@ def get_instance_name(self): return self.__instance - def _set_instance_name(self, instance): + def _set_instance_name(self, instance: str) -> None: """ set instance name of the plugin @@ -508,7 +605,7 @@ def _set_instance_name(self, instance): self.logger.warning(f"Plugin '{self.get_shortname()}': Only multi-instance capable plugins allow setting a name for an instance") - def get_fullname(self): + def get_fullname(self) -> str: """ return the full name of the plugin (shortname & instancename) @@ -521,7 +618,7 @@ def get_fullname(self): return self.get_shortname() + '_' + self.get_instance_name() - def get_classname(self): + def get_classname(self) -> str: """ return the classname of the plugin @@ -531,7 +628,7 @@ def get_classname(self): return self._classname - def _set_classname(self, classname): + def _set_classname(self, classname: str) -> None: """ ... @@ -543,7 +640,7 @@ def _set_classname(self, classname): self._classname = classname - def get_version(self, extended=False): + def get_version(self, extended: bool=False) -> str: """ Return plugin version @@ -559,7 +656,7 @@ def get_version(self, extended=False): return self.PLUGIN_VERSION - def _set_multi_instance_capable(self, mi): + def _set_multi_instance_capable(self, mi: bool) -> bool: """ Sets information if plugin is capable of multi instance handling (derived from metadate), but only, if ALLOW_MULTIINSTANCE is not set in source code @@ -576,20 +673,19 @@ def _set_multi_instance_capable(self, mi): return True - def is_multi_instance_capable(self): + def is_multi_instance_capable(self) -> bool: """ Returns information if plugin is capable of multi instance handling :return: True: If multiinstance capable :rtype: bool """ - if self.ALLOW_MULTIINSTANCE: - return True - else: + if not hasattr(self, 'ALLOW_MULTIINSTANCE') or self.ALLOW_MULTIINSTANCE is None: return False + return self.ALLOW_MULTIINSTANCE - def get_plugin_dir(self): + def get_plugin_dir(self) -> str: """ return the directory where the pluing files are stored in @@ -599,7 +695,7 @@ def get_plugin_dir(self): return self._plugin_dir - def _set_plugin_dir(self, dir): + def _set_plugin_dir(self, dir: str) -> None: """ Set the object's local variable `_plugin_dir` to root directory of the plugins. You can reference the main object of SmartHmeNG by using self._plugin_dir. @@ -612,7 +708,7 @@ def _set_plugin_dir(self, dir): self._plugin_dir = dir - def get_info(self): + def get_info(self) -> str: """ Returns a small plugin info like: class, version and instance name @@ -622,7 +718,7 @@ def get_info(self): return f"Plugin: '{self.get_shortname()}.{self.__class__.__name__}', Version: '{self.get_version()}', Instance: '{self.get_instance_name()}'" - def get_parameter_value(self, parameter_name): + def get_parameter_value(self, parameter_name: str) -> Any: """ Returns the configured value for the given parameter name @@ -637,7 +733,7 @@ def get_parameter_value(self, parameter_name): return self._parameters.get(parameter_name, None) - def get_parameter_value_for_display(self, parameter_name): + def get_parameter_value_for_display(self, parameter_name: str) -> Any: """ Returns the configured value for the given parameter name @@ -658,21 +754,7 @@ def get_parameter_value_for_display(self, parameter_name): else: return param - -# def has_parameter_value(self, key): -# """ -# Returns True, if a value is configured for the given parameter name -# -# :param parameter_name: Name of the parameter for which the value should be retrieved -# :type parameter_name: str -# -# :return: True, if a value is configured for the given parameter name -# :rtype: bool -# """ -# return (self.get_parameter_value(key) is not None) - - - def update_config_section(self, param_dict): + def update_config_section(self, param_dict: dict) -> None: """ Update the config section of ../etc/plugin.yaml @@ -727,9 +809,8 @@ def update_config_section(self, param_dict): if parameters_changed: shyaml.yaml_save_roundtrip(self._configfilename, plugin_conf, True) self.logger.debug(f"update_config_section: Finished updating section '{self._configname}' of ../etc/plugin.yaml") - return - def get_loginstance(self): + def get_loginstance(self) -> str: """ Returns a prefix for logmessages of multi instance capable plugins. @@ -751,7 +832,7 @@ def get_loginstance(self): return self.__instance + '@: ' - def __get_iattr(self, attr): + def __get_iattr(self, attr: str) -> str: """ Returns the given item attribute name for this plugin instance (by adding the instance to the attribute name) @@ -768,7 +849,7 @@ def __get_iattr(self, attr): return f"{attr}@{self.__instance}" - def __get_iattr_conf(self, conf, attr): + def __get_iattr_conf(self, conf: str, attr: str) -> Any: """ returns item attribute name including instance if required and found in item configuration @@ -818,7 +899,7 @@ def get_iattr_value(self, conf: str, attr: str, default=None) -> str: return default if __attr is None else conf[__attr] - def set_attr_value(self, conf, attr, value): + def set_attr_value(self, conf: str, attr: str, value: str) -> None: """ Set value for an attribute in item configuration @@ -834,7 +915,7 @@ def set_attr_value(self, conf, attr, value): conf[self.__get_iattr(attr)] = value - def __new__(cls, *args, **kargs): + def __new__(cls, *args, **kargs) -> Any: """ This method ic called during the creation of an object of the class SmartPlugin. @@ -845,7 +926,7 @@ def __new__(cls, *args, **kargs): return SmartObject.__new__(cls, *args, **kargs) - def get_sh(self): + def get_sh(self) -> object: """ Return the main object of smarthomeNG (usually refered to as **smarthome** or **sh**) You can reference the main object of SmartHomeNG by using self.get_sh() in your plugin @@ -856,7 +937,7 @@ def get_sh(self): return self._sh - def _set_sh(self, smarthome): + def _set_sh(self, smarthome: object) -> None: """ Set the object's local variable `_sh` to the main smarthomeNG object. You can reference the main object of SmartHomeNG by using self._sh. @@ -872,7 +953,7 @@ def _set_sh(self, smarthome): self.shtime = Shtime.get_instance() - def get_module(self, modulename): + def get_module(self, modulename: str) -> object: """ Test if module http is loaded and if loaded, return a handle to the module """ @@ -893,47 +974,20 @@ def path_join(self, path, dir): """ return os.path.join(path, dir) - - def parse_logic(self, logic): - """ - This method is used to parse the configuration of a logic for this plugin. It is - called for all plugins before the plugins are started (calling all run methods). - - :note: This method should to be overwritten by the plugin implementation. - """ - pass - - - def parse_item(self, item): - """ - This method is used to parse the configuration of an item for this plugin. It is - called for all plugins before the plugins are started (calling all run methods). - - :note: This method should be overwritten by the plugin implementation. - """ - # check for suspend item - if item.property.path == self._suspend_item_path: - self.logger.debug(f'suspend item {item.property.path} registered') - self._suspend_item = item - self.add_item(item, updating=True) - return self.update_item - - - def now(self): """ Returns SmartHomeNGs current time (timezone aware) """ return self.shtime.now() - def scheduler_return_next(self, name): + def scheduler_return_next(self, name: str) -> Any: if name != '': name = '.' + name name = self._pluginname_prefix + self.get_fullname() + name self.logger.debug(f"scheduler_return_next: name = {name}") return self._sh.scheduler.return_next(name, from_smartplugin=True) - def scheduler_trigger(self, name, obj=None, by=None, source=None, value=None, dest=None, prio=3, dt=None): + def scheduler_trigger(self, name, obj=None, by=None, source=None, value=None, dest=None, prio=3, dt=None) -> None: """ This methods triggers the scheduler entry for a plugin-scheduler @@ -951,7 +1005,7 @@ def scheduler_trigger(self, name, obj=None, by=None, source=None, value=None, de self.logger.debug(f"scheduler_trigger: name = {name}, parameters: {parameters}") self._sh.scheduler.trigger(name, obj, by, source, value, dest, prio, dt, from_smartplugin=True) - def scheduler_add(self, name, obj, prio=3, cron=None, cycle=None, value=None, offset=None, next=None): + def scheduler_add(self, name: str, obj: object, prio: int=3, cron=None, cycle=None, value=None, offset=None, next=None) -> None: """ This methods adds a scheduler entry for a plugin-scheduler @@ -968,7 +1022,7 @@ def scheduler_add(self, name, obj, prio=3, cron=None, cycle=None, value=None, of self.logger.debug(f"scheduler_add: name = {name}, parameters: {parameters}") self._sh.scheduler.add(name, obj, prio, cron, cycle, value, offset, next, from_smartplugin=True) - def scheduler_change(self, name, **kwargs): + def scheduler_change(self, name: str, **kwargs) -> None: """ This methods changes a scheduler entry of a plugin-scheduler @@ -984,7 +1038,7 @@ def scheduler_change(self, name, **kwargs): self.logger.debug(f"scheduler_change: name = {name}, parameters: {parameters}") self._sh.scheduler.change(name, **kwargs) - def scheduler_remove(self, name): + def scheduler_remove(self, name: str) -> None: """ This methods removes a scheduler entry of a plugin-scheduler @@ -995,14 +1049,14 @@ def scheduler_remove(self, name): try: self._schedulers.remove(name) except ValueError: - pass # TODO: maybe give a warning? + pass if name != '': name = '.' + name name = self._pluginname_prefix + self.get_fullname() + name self.logger.debug(f"scheduler_remove: name = {name}") self._sh.scheduler.remove(name, from_smartplugin=True) - def scheduler_get(self, name): + def scheduler_get(self, name: str) -> dict: """ This methods gets a scheduler entry of a plugin-scheduler @@ -1032,13 +1086,6 @@ def scheduler_remove_all(self): # Ascyncio handling # ---------------------------------------------------------------------------------- - from typing import Coroutine, Any - - _asyncio_loop = None # eventloop of the plugin - _asyncio_state = 'unused' # stored state of the asyncio use of the plugin - _used_plugin_coro = None # plugin coro used when calling start_asyncio (to be able to used by a generic 'restart asyncio' method - _run_queue = None # queue to send commends to the main-coro/plugin-coro - def asyncio_state(self) -> str: """ Returns the state of asyncio for the plugin @@ -1082,7 +1129,6 @@ def _start_known_asyncio_coro(self) -> None: self.pluginThread.start() except Exception as e: self.logger.error(f"Cannot start thread '{threadname}' - Error: {e}") - return def stop_asyncio(self) -> None: """ @@ -1102,9 +1148,8 @@ def stop_asyncio(self) -> None: self.logger.notice(f"Error stopping _asyncio_loop_thread: {err}") pass self._asyncio_state = 'stopped' - return - def _asyncio_loop_thread(self, plugin_coro: Coroutine): + def _asyncio_loop_thread(self, plugin_coro: Coroutine) -> None: """ Thread to start and execute the asyncio event loop @@ -1116,8 +1161,6 @@ def _asyncio_loop_thread(self, plugin_coro: Coroutine): asyncio.run(self._asyncio_main(plugin_coro)) - return - async def _asyncio_main(self, plugin_coro: Coroutine) -> None: """ main coroutine to set up the environment for the coroutine of the specific plugin @@ -1187,8 +1230,6 @@ async def wait_for_asyncio_termination(self) -> None: # put command back to queue? await asyncio.sleep(0.1) - return - def put_command_to_run_queue(self, command: str) -> None: """ Put an entry to the run-queue (if implemented in the plugin_coro) @@ -1205,7 +1246,6 @@ def put_command_to_run_queue(self, command: str) -> None: time.sleep(3) else: self.logger.warning(f"put_command_to_run_queue: Cannot write command '{command}' to run-queue, no active event-loop") - return async def get_command_from_run_queue(self) -> str: """ @@ -1233,27 +1273,6 @@ async def list_asyncio_tasks(self) -> None: task.set_name('ListTasks') self.logger.notice(f" - {task}") - # ---------------------------------------------------------------------------------- - - - def run(self): - """ - This method of the plugin is called to start the plugin - - :note: This method needs to be overwritten by the plugin implementation. Otherwise an error will be raised - """ - raise NotImplementedError("'Plugin' subclasses should have a 'run()' method") - - - def stop(self): - """ - This method of the plugin is called to stop the plugin when SmartHomeNG shuts down - - :note: This method needs to be overwritten by the plugin implementation. Otherwise an error will be raised - """ - raise NotImplementedError("'Plugin' subclasses should have a 'stop()' method") - - def translate(self, txt, vars=None, block=None): """ Returns translated text for class SmartPlugin @@ -1273,7 +1292,7 @@ def translate(self, txt, vars=None, block=None): return lib_translate(txt, vars) - def init_webinterface(self, WebInterface=None): + def init_webinterface(self, WebInterface=None) -> bool: """" Initialize the web interface for this plugin