Skip to content

Commit

Permalink
move suspend/resume function to smartplugin.py
Browse files Browse the repository at this point in the history
  • Loading branch information
Morg42 committed Aug 1, 2023
1 parent 569576f commit 90c071d
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 65 deletions.
21 changes: 11 additions & 10 deletions lib/model/sdp/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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],
Expand All @@ -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,
Expand Down Expand Up @@ -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):
"""
Expand Down
10 changes: 5 additions & 5 deletions lib/model/sdp/globals.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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
Expand Down
103 changes: 53 additions & 50 deletions lib/model/smartdeviceplugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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']
Expand All @@ -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 """
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()

Expand All @@ -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()
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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'))):
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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

Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down
32 changes: 32 additions & 0 deletions lib/model/smartplugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()]
Expand Down

0 comments on commit 90c071d

Please sign in to comment.