Macro extensions

As macros are written in Python, you can use any Python module to extend your macros. Additionally Logic Manager has ability to extend macros with extensions.

Modules are more flexible and are a standard Python way to extend your software. However macro extensions are more simple, standardized and easy to configure. As the goal is to keep macro code as simple as possible, macro extensions are the best choice in many cases.

Loading macro extension

Macro extensions are stored in xc/extensions folder. To list available macro extension modules, use command:

eva lm ext mods

Next command returns extension info:

eva lm ext modinfo <ext_module>

To get information about extension configuration and functions provided, use commands:

eva lm ext modhelp <ext_module> cfg
eva lm ext modhelp <ext_module> functions

To load/unload macro extension, use command:

# load
eva lm ext load [-c CONFIG] [-y] <ext_id> <ext_module>
# unload
eva lm ext unload <ext_id>

where:

  • -c CONFIG extension configuration options, comma separated

  • -y save extension config after successful load

Extension functions

When extension is loaded, its functions become available in all macros automatically with names <ext_id>_<function>.

E.g. when extension audio is loaded with ID a1, its function play is available as a1_play. This allows you to load one extension multiple times and have different functionality according to specified configuration without need to configure module/class params in macros.

If you want to make a short alias for extension function, use alias (e.g. in xc/lm/common.py):

alias('play', 'a1_play')

Unlike play=a1_play alias doesn’t throw an exception and let macros work even if extension is failed to load or its functions are not available.

Included extensions

The following extensions are included in EVA ICS distribution by default:

Developing your own extension

Create new Python file in xc/extensions folder.

Required variables in a header

  • __author__ module author

  • __copyright__ copyright

  • __license__ module license

  • __version__ module version

  • __description__ module description (keep it short)

  • __api__ module API (integer number), current is 7

  • __mods_required__ required python modules (included neither in standard Python install nor in EVA ICS)

  • __config__help__ module configuration help (on load)

  • __functions__ exported functions

  • __help__ should contain the extended description and operation manual. May be in any variable format and use restructured text directives for formatting.

Configuration variable

Configuration variable (__config_help__) should be in list format, containing dictionaries with the following context:

  • name property name

  • help property description (help)

  • type property type

  • required True if property is required, False if it’s optional

  • default default value (for required only)

Property type may be:

  • bool boolean (True/False)

  • str string

  • url string containing url

  • int integer

  • uint unsigned integer (greater or equal to 0)

  • hex hexadecimal number

  • bin binary number

  • float float number

  • ufloat unsigned float (greater or equal to 0)

  • list:type list of variables with type specified

  • enum:type:a,b,c list of permitted specified type values

If a property accepts multiple types, they should be listed via or (|) symbol.

Exported functions

Exported functions (__functions__) variable is a dictionary in format:

{ 'function(params)': 'description' }
# e.g.
{
    'func1(param1, param2=0, param3=True)': 'This function does something',
    'func2(param1=0)': 'This function does something else'
}

All exported functions should be defined in a primary extension class.

Classes and modules

It’s allowed to import any Python system module or module installed by EVA ICS. If extension requires installing more modules, they should be listed in extension help and in __mods_required__ variable.

Warning

All non-standard modules (not included neither in Python install nor in EVA ICS) should be imported with try/catch with importlib, their unavailability shouldn’t block loading extension for informational purposes.

Importing EVA modules and functions from eva.lm.extapi:

  • get_version() get Extension API version

  • get_polldelay() get EVA poll delay

  • get_timeout() get default timeout

  • critical() send EVA critical call

  • log_traceback() log traceback debug info

is highly welcome.

The main class is defined as:

from eva.lm.extensions.generic import LMExt as GenericExt
from eva.lm.extapi import ext_constructor

class LMExt(GenericExt):
    #<your code>

Constructor

The constructor should set the above constants to class variables to let them be serialized by parent class if requested:

@ext_constructor
def __init__(self, **kwargs):
    # your code, e.g. parsing self.cfg

Decorator @ext_constructor automatically invokes parent constructor and handles special init requests.

If the constructor faces a problem (i.e. parsing a config or checking required modules) it may set self.ready=False to abort controller loading the extension.

Persistent data

Special dict variable self.data is used to keep extension persistent data. This data is automatically saved on controller stop or when save API / CLI command is called and automatically loaded on controller start.

Requirements:

  • self.data should always be a dict

  • dict keys, as well as keys of the sub-dicts must be strings

  • data is stored in JSON format, so should contain only numbers, strings and booleans, lists and dicts

  • it’s highly recommended to use self.data_lock before accessing self.data

  • to tell controller that data is modified and should be saved, set self.data_modified to True

Code example:

# read data
with self.data_lock:
   value = data.get('key')

# write data
with self.data_lock:
   data['key'] = 'value'
   self.data_modified = True

Warning

When extesion is unloaded, its data file is deleted

Exceptions

There’s no standard way to handle exceptions, however if any of exported functions raise them, this should be specified in extension help and readme file.

Config validation

Optional method validate_config can be implemented to automatically validate module configuration.

class eva.x :noindex:.GenericX
validate_config(config={}, config_type='config', ignore_private=False, **kwargs)

Validates module config

Does nothing by default. Can e.g. call self.validate_config_whi to validate config with module help info, validate config with JSON schema or do everything manually

Note: “driver_assign” always assigns the same parameters for “action” and “state” by default. Consider either ignoring config_type=’state’ validation or allow action parameters there.

Parameters
  • config – config to validate (may be modified on-the-flow to convert variable types for extension config)

  • config_type – validation config type (‘config’, ‘state’, ‘action’ etc., matches help variable)

  • ignore_private – allow any private (starting with “_”) parameters as they’re usually passed as-is to lower level extension (e.g. LPI -> PHI)

  • kwargs – reserved for the future

Returns

True if config is validated

Raises

eva.exceptions.InvalidParameter – if config contains invalid params

validate_config_whi(config=None, config_type='config', allow_extra=False, ignore_private=False, xparams=[])

Validate config with module help info

Help info: module help info variable (e.g. __config_help__ for config)

Parameters
  • config – config to validate

  • config_type – config type (help var to parse, default is ‘config’)

  • allow_extra – allow any extra params in config

  • xparams – list of allowed extra params

Returns

True if config is validated. Config dict variables are automatically parsed and converted to the required types (except extra params if not listed)

Raises

eva.exceptions.InvalidParameter – if configuration is invalid

Testing

Use bin/test-ext command-line tool to perform PHI module tests. The tool requires test Python file, which loads extension as _ and contains all its functions (e.g. __test for extension.test):

print('Testing extension')
__test(params)
__func2(params)
__func3(params)
print('Test completed')