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.
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.
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.
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.
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.
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.
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.
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.