MudPi Extensions

MudPi has an extension system to allow easy expansion with future updates. Extensions are a powerful feature of MudPi because they enable custom behavior based on loaded configs. An extension is a simple python package that adds functionality to the MudPi system. This could be loading new components, listening to events or interacting with state. Most of the core systems for MudPi are built on this extension system.

Extension Basics

Extensions consist of interfaces and components to be loaded into MudPi. One powerful feature of extensions is that they are loaded at runtime based on configurations. This means that additional features can be added to MudPi without requiring them as a default. Extensions will typically do one of two things: either register components to the system and/or interact with events. This is done by either the extension or the extension interfaces.

Namespace

Extensions need a way to organize their data and logic. This is done under a namespace. A namespace is nothing more than a unique string to group the extension operations under. As a rule for extensions they should only interact with data and emit events under their own namespace. The namespace is significant because it is also the main key to loading extensions in the system. When you are updating your config files the extension namespace is used as the key to provide configs under. When MudPi detects a configuration that is a non-core component it will attempt to locate an extension with a namespace matching the configuration key. This namespace will also be the folder name for the extension. These are all important details on how MudPi is able to dynamically load extensions at runtime.

Update Interval

For extensions that provide components they will register each component to a worker. The worker is responsible for managing the components and requesting updates from them. The update_interval provided by the extension will tell the worker how long to wait in between each cycle of component updates. Slower components can typically have higher update intervals as they don't need as frequent of updates.

A Minimal Extension

Ok so lets take a look at an actual extension provided by MudPi. First lets look at the extension itself. Below is the actual example extension provided by MudPi. The example extension is an extension that provides interfaces with other extensions for core components.

from mudpi.extensions import BaseExtension


class Extension(BaseExtension):
    namespace = 'example'
    update_interval = 30

Yes, thats it! This is a minimal extension because it mainly provides interfaces to other extensions. The minimal requirements for an extension in MudPi is to extend from the BaseExtension class and set a namespace. Here in the example extension an update_interval was also supplied to override the default.

Creating a New Extension

You may be wondering what are all the benefits to extensions. Looking at a minimal extension boilerplate it may not be too clear what the great use is. Taking a look at another extension may help reveal the possibilities. Below is an example of a hello extension that displays a message set in configs. The extension would be a single __init__.py file containing the extension saved under a folder named after our namespace hello in the MudPi extensions folder. The only other file required is the extension.json file which contains information needed for importing the extension. The file structure would be along the lines of:

mudpi
└── extensions
   └── hello
      └── __init__.py
      └── extension.json

The init.py file always contains the Extension class that extends from the BaseExtension. The extension class must be named Extension as MudPi looks for a class by this name when loading the extension. Below would be the extension class for our hello example:

from mudpi.exceptions import ConfigError
from mudpi.extensions import BaseExtension


class Extension(BaseExtension):
    namespace = 'hello'

    def init(self, config):
        self.config = config[self.namespace]
        
        for entry in self.config:
            message = entry.get('message', 'Have a Great Day!')
            print(message)

        return True

    def validate(self, config):
        for entry in config[self.namespace]:
            if not entry.get('message'):
                raise ConfigError("No `message` set in configs!")
        return config

The extension.json file has details for importing the extension such as the namespace and any requirements that need to be installed via pip. Also if the extension relies on other extensions to load first, you can put the namespaces of those extensions in a dependencies list. Here is an example of one for our hello extension.

{
    "name": "Hello",
    "namespace": "hello",
    "details": {
        "description": "Display messages on load.",
        "documentation": "https://mudpi.app/docs"
    },
    "requirements": ["somefakepackage"]
}

You would load this extension by adding an entry in your configs using the namespace as the entry key. So below is an example of providing a config for the hello extension with two entries.

{
    "hello": [
        { "message": "Hello Mom" },
        { "message": "Hey Dad" }
    ]
}

Using the config above would result in our two messages to be displayed when the extension was initialized. Reviewing our hello extension we can see two new methods that were not in the example extension. First the init() method is provided for extensions to perform additional initialization. It is encouraged to use the init() method instead of overriding __init__() to avoid errors. The init() method should return True otherwise it is assumed an error occurred and the extension will be disabled.

Next you can see the validate() method. As the name suggests this is were configuration validation is done before the extension gets initialized. Both the init() and validate() method accept the configuration dict as their argument. This is the entire MudPi config dict loaded at start so that the extension may perform any needed setup. This is why you see the extension pulling only data based on its namespace to filter out its own data. The validate method should either return the validated config or raise a ConfigError for the system to catch.

Now you might be able to see how this system has its advantages. We can provide a configuration in our config file using the namespace as the config key. This key is used to find an extension with a folder name matching the namespace under the mudpi/extensions folder. If an extension is found the validate() method is called on that extension with the config dict. The validated config is then passed into the init() method for the extension to initialize. The opens the door for highly configurable and extendable system. We can use this design to dynamically load extensions based on configurations which enable custom functionality across MudPi.

Extension Interfaces

Now lets take a look at extension interfaces. An interface allows uniform behavior from different sources. In simple terms it allows situations like having two different sensors with two different ways of gathering data both be accessible the same way i.e. a single read() method. Interfaces are also a way for extensions to provide support for other extensions enabling even further customizations.

To help visualize this lets take a look at the example extension provided by MudPi again. In the extension folder you will see the __init__.py file and the extension.json file. You may have noticed some additional files as well such as sensor.py, control.py, toggle.py and char_display.py. These are all interfaces for other extensions provided by MudPi. The name of the file matches the namespace it is providing an interface for.

Below is what the sensor.py file contains:

import random
from mudpi.extensions import BaseInterface
from mudpi.extensions.sensor import Sensor


class Interface(BaseInterface):

    def load(self, config):
        sensor = ExampleSensor(self.mudpi, config)
        self.add_component(sensor)
        return True


class ExampleSensor(Sensor):
    """ Properties """
    @property
    def id(self):
        return self.config['key']

    @property
    def name(self):
        return self.config.get('name') or f"{self.id.replace('_', ' ').title()}"
    
    @property
    def state(self):
        return self._state

    @property
    def classifier(self):
        return self.config.get('classifier', "general")


    """ Methods """
    def update(self):
        self._state = random.randint(1, self.config.get('data', 10))
        return True

There are two main items that make up this interface file. The Interface class itself and a ExampleSensor which is a sensor component the interface is going to load.

MudPi looks in the interface file for the Interface class which has one main job to load and unload components. This class should always extend from the BaseInterface. The interface class has two main methods: load() and validate(). The validate() method is called first and is passed a single argument, a config dict that should be validated before loading components. This validate() method should either return the validated config dict or raise a ConfigError.

Once the config is validated then the load() method is called with this validated config as its only argument. The load() method should take the configs and load any components from it. In the case of the the example extension above an ExampleSensor which is a Sensor component is being loaded.

Loading Interfaces

When adding configs for an extension that supports interfaces the interface key is used to tell MudPi which extension to load the interface from. Continuing with the example extension, below is a configuration to load a sensor from it.

"sensor": [{
    "key": "example_sensor_1",
    "interface": "example"
}]

The current supported extension interfaces are: sensors, controls, cameras, triggers, character displays and toggles. Read the developer interface docs if you are interested in adding a new component interface.

If you would like to add another core component that supports interfaces contact the developer team.