From 90c071da47697b13b0a3c5b38dfa7d034cd01d5e Mon Sep 17 00:00:00 2001 From: Morg42 <43153739+Morg42@users.noreply.github.com> Date: Tue, 1 Aug 2023 07:41:41 +0200 Subject: [PATCH] move suspend/resume function to smartplugin.py --- lib/model/sdp/connection.py | 21 +++---- lib/model/sdp/globals.py | 10 ++-- lib/model/smartdeviceplugin.py | 103 +++++++++++++++++---------------- lib/model/smartplugin.py | 32 ++++++++++ 4 files changed, 101 insertions(+), 65 deletions(-) diff --git a/lib/model/sdp/connection.py b/lib/model/sdp/connection.py index 5a704582b..6ee36edd2 100644 --- a/lib/model/sdp/connection.py +++ b/lib/model/sdp/connection.py @@ -41,7 +41,7 @@ PLUGIN_ATTR_CB_ON_CONNECT, PLUGIN_ATTR_CB_ON_DISCONNECT, PLUGIN_ATTR_CONNECTION, PLUGIN_ATTR_CONN_AUTO_CONN, PLUGIN_ATTR_CONN_AUTO_RECONN, PLUGIN_ATTR_CONN_BINARY, PLUGIN_ATTR_CONN_CYCLE, PLUGIN_ATTR_CONN_RETRIES, PLUGIN_ATTR_CONN_RETRY_CYCLE, - PLUGIN_ATTR_CONN_RETRY_STBY, PLUGIN_ATTR_CONN_TERMINATOR, PLUGIN_ATTR_CB_STANDBY, + PLUGIN_ATTR_CONN_RETRY_SUSPD, PLUGIN_ATTR_CONN_TERMINATOR, PLUGIN_ATTR_CB_SUSPEND, PLUGIN_ATTR_CONN_TIMEOUT, PLUGIN_ATTR_NET_HOST, PLUGIN_ATTR_NET_PORT, PLUGIN_ATTR_PROTOCOL, PLUGIN_ATTR_SERIAL_BAUD, PLUGIN_ATTR_SERIAL_BSIZE, PLUGIN_ATTR_SERIAL_PARITY, PLUGIN_ATTR_SERIAL_PORT, PLUGIN_ATTR_SERIAL_STOP, @@ -74,6 +74,7 @@ def __init__(self, data_received_callback, name=None, **kwargs): # set class properties self._is_connected = False self._data_received_callback = data_received_callback + self._suspend_callback = None self.dummy = None # dummy response for testing # we set defaults for all possible connection parameters, so we don't @@ -94,11 +95,11 @@ def __init__(self, data_received_callback, name=None, **kwargs): PLUGIN_ATTR_CONN_RETRIES: 3, PLUGIN_ATTR_CONN_CYCLE: 5, PLUGIN_ATTR_CONN_RETRY_CYCLE: 30, - PLUGIN_ATTR_CONN_RETRY_STBY: 0, + PLUGIN_ATTR_CONN_RETRY_SUSPD: 0, PLUGIN_ATTR_CONN_TERMINATOR: '', PLUGIN_ATTR_CB_ON_CONNECT: None, PLUGIN_ATTR_CB_ON_DISCONNECT: None, - PLUGIN_ATTR_CB_STANDBY: None} + PLUGIN_ATTR_CB_SUSPEND: None} # "import" options from plugin self._params.update(kwargs) @@ -457,7 +458,7 @@ def __init__(self, data_received_callback, name=None, **kwargs): if isinstance(self._params[PLUGIN_ATTR_CONN_TERMINATOR], str): self._params[PLUGIN_ATTR_CONN_TERMINATOR] = bytes(self._params[PLUGIN_ATTR_CONN_TERMINATOR], 'utf-8') - self._standby_callback = self._params[PLUGIN_ATTR_CB_STANDBY] + self._suspend_callback = self._params[PLUGIN_ATTR_CB_SUSPEND] # initialize connection self._tcp = Tcp_client(host=self._params[PLUGIN_ATTR_NET_HOST], @@ -468,8 +469,8 @@ def __init__(self, data_received_callback, name=None, **kwargs): connect_retries=self._params[PLUGIN_ATTR_CONN_RETRIES], connect_cycle=self._params[PLUGIN_ATTR_CONN_CYCLE], retry_cycle=self._params[PLUGIN_ATTR_CONN_RETRY_CYCLE], - retry_abort=self._params[PLUGIN_ATTR_CONN_RETRY_STBY], - abort_callback=self._abort_callback, + retry_abort=self._params[PLUGIN_ATTR_CONN_RETRY_SUSPD], + abort_callback=self._on_abort, terminator=self._params[PLUGIN_ATTR_CONN_TERMINATOR]) self._tcp.set_callbacks(data_received=self.on_data_received, disconnected=self.on_disconnect, @@ -498,11 +499,11 @@ def _send(self, data_dict): # we receive only via callback, so we return "no reply". return None - def _abort_callback(self): - if self._standby_callback: - self._standby_callback(True, by=self.__class__.__name__) + def _on_abort(self): + if self._suspend_callback: + self._suspend_callback(True, by=self.__class__.__name__) else: - self.logger.warning('standby callback wanted, but not set by plugin. Check plugin code...') + self.logger.warning('suspend callback wanted, but not set by plugin. Check plugin code...') class UDPServer(socket.socket): """ diff --git a/lib/model/sdp/globals.py b/lib/model/sdp/globals.py index f122e22bc..94fc03632 100644 --- a/lib/model/sdp/globals.py +++ b/lib/model/sdp/globals.py @@ -45,7 +45,7 @@ PLUGIN_ATTR_MODEL = 'model' # select model if applicable. Don't set if not necessary! PLUGIN_ATTR_CMD_CLASS = 'command_class' # name of class to use for commands PLUGIN_ATTR_RECURSIVE = 'recursive_custom' # indices of custom item attributes for which to enable recursive lookup (number or list of numbers) -PLUGIN_ATTR_STANDBY_ITEM = 'standby_item_path' # item to toggle standby mode +PLUGIN_ATTR_SUSPEND_ITEM = 'suspend_item_path' # item to toggle suspend/resume mode # general connection attributes PLUGIN_ATTR_CONNECTION = 'conn_type' # manually set connection class, classname or type (see below) @@ -57,7 +57,7 @@ PLUGIN_ATTR_CONN_AUTO_RECONN = 'autoreconnect' # (re)connect automatically on disconnect or failed connection cycle PLUGIN_ATTR_CONN_AUTO_CONN = 'autoconnect' # (re)connect automatically on send PLUGIN_ATTR_CONN_RETRY_CYCLE = 'retry_cycle' # if autoreconnect: how many seconds to wait between retry rounds -PLUGIN_ATTR_CONN_RETRY_STBY = 'retry_standby' # after this number of failed connect cycles, activate standby mode (if enabled) +PLUGIN_ATTR_CONN_RETRY_SUSPD = 'retry_suspend' # after this number of failed connect cycles, activate suspend mode (if enabled) # network attributes PLUGIN_ATTR_NET_HOST = 'host' # hostname / IP for network connection @@ -78,16 +78,16 @@ # callback functions, not in plugin.yaml PLUGIN_ATTR_CB_ON_CONNECT = 'connected_callback' # callback function, called if connection is established PLUGIN_ATTR_CB_ON_DISCONNECT = 'disconnected_callback' # callback function, called if connection is lost -PLUGIN_ATTR_CB_STANDBY = 'standby_callback' # callback function, called if connection attempts are aborted +PLUGIN_ATTR_CB_SUSPEND = 'suspend_callback' # callback function, called if connection attempts are aborted PLUGIN_ATTRS = (PLUGIN_ATTR_MODEL, PLUGIN_ATTR_CMD_CLASS, PLUGIN_ATTR_RECURSIVE, PLUGIN_ATTR_STANDBY_ITEM, PLUGIN_ATTR_CONNECTION, PLUGIN_ATTR_CONN_TIMEOUT, PLUGIN_ATTR_CONN_TERMINATOR, PLUGIN_ATTR_CONN_BINARY, PLUGIN_ATTR_CONN_RETRIES, PLUGIN_ATTR_CONN_CYCLE, PLUGIN_ATTR_CONN_AUTO_RECONN, PLUGIN_ATTR_CONN_AUTO_CONN, - PLUGIN_ATTR_CONN_RETRY_CYCLE, PLUGIN_ATTR_CONN_RETRY_STBY, PLUGIN_ATTR_NET_HOST, PLUGIN_ATTR_NET_PORT, + PLUGIN_ATTR_CONN_RETRY_CYCLE, PLUGIN_ATTR_CONN_RETRY_SUSPD, PLUGIN_ATTR_NET_HOST, PLUGIN_ATTR_NET_PORT, PLUGIN_ATTR_SERIAL_PORT, PLUGIN_ATTR_SERIAL_BAUD, PLUGIN_ATTR_SERIAL_BSIZE, PLUGIN_ATTR_SERIAL_PARITY, PLUGIN_ATTR_SERIAL_STOP, PLUGIN_ATTR_PROTOCOL, PLUGIN_ATTR_MSG_TIMEOUT, PLUGIN_ATTR_MSG_REPEAT, - PLUGIN_ATTR_CB_ON_CONNECT, PLUGIN_ATTR_CB_ON_DISCONNECT, PLUGIN_ATTR_CB_STANDBY) + PLUGIN_ATTR_CB_ON_CONNECT, PLUGIN_ATTR_CB_ON_DISCONNECT, PLUGIN_ATTR_CB_SUSPEND) # connection types for PLUGIN_ATTR_CONNECTION CONN_NULL = '' # use base connection class without real connection functionality, for testing diff --git a/lib/model/smartdeviceplugin.py b/lib/model/smartdeviceplugin.py index 9ebd257f2..307fad599 100644 --- a/lib/model/smartdeviceplugin.py +++ b/lib/model/smartdeviceplugin.py @@ -51,9 +51,11 @@ ITEM_ATTR_CYCLE, ITEM_ATTR_GROUP, ITEM_ATTR_LOOKUP, ITEM_ATTR_READ, ITEM_ATTR_READ_GRP, ITEM_ATTR_READ_INIT, ITEM_ATTR_WRITE, PLUGIN_ATTR_CB_ON_CONNECT, PLUGIN_ATTR_CB_ON_DISCONNECT, - PLUGIN_ATTR_CMD_CLASS, PLUGIN_ATTR_CONNECTION, PLUGIN_ATTR_STANDBY_ITEM, + PLUGIN_ATTR_CMD_CLASS, PLUGIN_ATTR_CONNECTION, PLUGIN_ATTR_SUSPEND_ITEM, PLUGIN_ATTR_CONN_AUTO_RECONN, PLUGIN_ATTR_CONN_AUTO_CONN, - PLUGIN_ATTR_PROTOCOL, PLUGIN_ATTR_RECURSIVE, PLUGIN_PATH) + PLUGIN_ATTR_PROTOCOL, PLUGIN_ATTR_RECURSIVE, PLUGIN_PATH, + PLUGIN_ATTR_CB_SUSPEND) + from lib.model.sdp.commands import SDPCommands from lib.model.sdp.command import SDPCommand from lib.model.sdp.connection import SDPConnection @@ -138,12 +140,12 @@ def __init__(self, sh, logger=None, **kwargs): # set to True to use on_connect and on_disconnect callbacks self._use_callbacks = False + # # set class properties + # - # "standby" mode properties - self.standby = False - self._standby_item = None - self._standby_item_path = self.get_parameter_value(PLUGIN_ATTR_STANDBY_ITEM) + # suspend mode properties + self._suspend_item_path = self.get_parameter_value(PLUGIN_ATTR_SUSPEND_ITEM) # connection instance self._connection = None @@ -196,12 +198,16 @@ def __init__(self, sh, logger=None, **kwargs): # call method for possible custom work (overwrite _post_init) self._post_init() + # self._webif might be set by smartplugin init if self._webif and self._sh: self.init_webinterface(self._webif) self.logger.debug(f'device initialized from {self.__class__.__name__}') def remove_item(self, item): + """ + remove item references from plugin + """ try: cmd = self._plg_item_dict[item]['mapping'] @@ -212,9 +218,8 @@ def remove_item(self, item): if not super().remove_item(item): return False - if item.path == self._standby_item_path: - self.standby(False) - self.logger.warning(f'removed standby item {item.path()}, disabling standby') + if item.path == self._suspend_item_path: + self.logger.warning(f'removed suspend item {item.path()}, ') return True """ remove item from custom plugin dicts/lists """ @@ -259,9 +264,8 @@ def update_plugin_config(self, **kwargs): self._parameters.update(kwargs) - # if standby mode is enabled, set callback for tcp client etc. - if self._standby_item_path: - self._parameters['standby_callback'] = self.set_standby + # set callback for tcp client etc. + self._parameters[PLUGIN_ATTR_CB_SUSPEND] = self.set_suspend # this is only viable for the base class. All derived plugin classes # will probably be created towards a specific command class @@ -289,35 +293,34 @@ def update_plugin_config(self, **kwargs): return True - def set_standby(self, stby_active=None, by=None): + def set_suspend(self, suspend_active=None, by=None): """ - enable / disable standby mode: open/close connections, schedulers + enable / disable suspend mode: open/close connections, schedulers """ - if stby_active is None: - if self._standby_item: - stby_active = bool(self._standby_item()) + if suspend_active is None: + if self._suspend_item: + suspend_active = bool(self._suspend_item()) else: - stby_active = False + suspend_active = False - by_msg = '' + if suspend_active: + msg = 'Suspend mode enabled' + else: + msg = 'Suspend mode disabled' if by: - by_msg = f' (set by {by})' + msg += f' (set by {by})' - self.logger.info(f'Standby mode set to {stby_active}{by_msg}') - self.standby = stby_active - if self._standby_item is not None: - self._standby_item(self.standby) + self.logger.info(msg) + if suspend_active: + self.suspend() + else: + self.resume() - if stby_active: + if suspend_active: if self.scheduler_get(self.get_shortname() + '_cyclic'): self.scheduler_remove(self.get_shortname() + '_cyclic') - self.logger.debug('closing connection on standby enabled') - self.disconnect() else: - self.logger.debug('opening connection after standby disabled') - self.connect() - if self._connection.connected() and not SDP_standalone: self._create_cyclic_scheduler() @@ -332,7 +335,7 @@ def run(self): # start the devices self.alive = True - self.set_standby(by='run') + self.set_suspend(by='run()') if self._connection.connected(): self._read_initial_values() @@ -399,9 +402,9 @@ def find_custom_attr(item, index=1): return find_custom_attr(parent, index) - # check for standby item - if item.path() == self._standby_item_path: - self._standby_item = item + # check for suspend item + if item.path() == self._suspend_item_path: + self._suspend_item = item self.add_item(item, updating=True) return self.update_item @@ -565,11 +568,11 @@ def update_item(self, item, caller=None, source=None, dest=None): self.logger.debug(f'Update_item was called with item "{item}" from caller {caller}, source {source} and dest {dest}') - # check for standby item - if item is self._standby_item: + # check for suspend item + if item is self._suspend_item: if caller != self.get_shortname(): - self.logger.debug(f'Standby item changed to {item()}') - self.set_standby(by=f'standby item {item.path()}') + self.logger.debug(f'Suspend item changed to {item()}') + self.set_suspend(by=f'suspend item {item.path()}') return if not (self.has_iattr(item.conf, self._item_attrs.get('ITEM_ATTR_COMMAND', 'foo')) or self.has_iattr(item.conf, self._item_attrs.get('ITEM_ATTR_READ_GRP', 'foo'))): @@ -622,8 +625,8 @@ def send_command(self, command, value=None, **kwargs): self.logger.warning(f'trying to send command {command} with value {value}, but plugin is not active.') return False - if self.standby: - self.logger.warning(f'trying to send command {command} with value {value}, but plugin is on standby.') + if self.suspended: + self.logger.warning(f'trying to send command {command} with value {value}, but plugin is suspended.') return False if not self._connection: @@ -711,15 +714,15 @@ def on_data_received(self, by, data, command=None): if self._discard_unknown_command: self.logger.debug(f'data "{data}" did not identify a known command, ignoring it') else: - if not self.standby: + if not self.suspended: self.logger.debug(f'data "{data}" did not identify a known command, forwarding it anyway for {self._unknown_command}') self._dispatch_callback(self._unknown_command, data, by) else: - self.logger.info(f'received data "{data}" not identifying a known command while in standby, aborting.') + self.logger.info(f'received data "{data}" not identifying a known command while suspended, aborting.') return - if self.standby: - self.logger.info(f'received data "{data}" from {by} for command {command} while on standby, ignoring.') + if self.suspended: + self.logger.info(f'received data "{data}" from {by} for command {command} while suspended, ignoring.') return assert(isinstance(commands, list)) @@ -756,7 +759,7 @@ def dispatch_data(self, command, value, by=None): :param by: str :type command: str """ - if self.alive and not self.standby: + if self.alive and not self.suspended: item = None @@ -768,8 +771,8 @@ def dispatch_data(self, command, value, by=None): self.logger.warning(f'Command {command} yielded value {value} by {by}, not assigned to any item, discarding data') return - if self.standby: - self.logger.error(f'Trying to update item {item.path()}, but on standby. This should not happen, please report to developer.') + if self.suspended: + self.logger.error(f'Trying to update item {item.path()}, but suspended. This should not happen, please report to developer.') return for item in items: @@ -1165,14 +1168,14 @@ def _set_item_attributes(self): """ plugins = Plugins.get_instance() - global_mod = sys.modules.get('lib.model.sdp.globals', '') + globals_mod = sys.modules.get('lib.model.sdp.globals', '') self._item_attrs = {} - if plugins and global_mod: + if plugins and globals_mod: keys = list(self.metadata.itemdefinitions.keys()) for attr in ATTR_NAMES: - attr_val = getattr(global_mod, attr) + attr_val = getattr(globals_mod, attr) for key in keys: if key.endswith(attr_val): self._item_attrs[attr] = key diff --git a/lib/model/smartplugin.py b/lib/model/smartplugin.py index 3510d3d52..88981a016 100644 --- a/lib/model/smartplugin.py +++ b/lib/model/smartplugin.py @@ -66,12 +66,39 @@ class SmartPlugin(SmartObject, Utils): 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 # 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 + def suspend(self, by=None): + """ + sets plugin into suspended mode, no network/serial activity and no item changed + """ + 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() + + def resume(self, by=None): + """ + disabled suspended mode, network/serial connections are resumed + """ + 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() + def deinit(self, items=[]): """ If the Plugin needs special code to be executed before it is unloaded, this method @@ -174,6 +201,11 @@ def remove_item(self, item): else: self.logger.debug(f'not stopping plugin for removal of item {item.path()}') + if item.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 = '' + # remove data from item_dict early in case of concurrent actions data = self._plg_item_dict[item.path()] del self._plg_item_dict[item.path()]